GO日記-fasthttpクライアントが速い理由

翻译自 https://weekly-geekly.github.io/articles/443378/index.html

fasthttpの例を使用して、高性能のhttpクライアントを作成します。Alexander Valyalkin(VertaMedia)
Fasthttpライブラリは、標準のGolangパッケージのnet / httpの高速化された代替手段です。
手配方法は?なんでこんなに速いの?

アレクサンダー・ヴァリヤルキンです。私はVertaMediaで働いています。私は私たちのニーズに合わせてfasthttpを設計しました。これには、httpクライアントの実装とhttpサーバーの実装が含まれます。Fasthttpは、標準のGoパッケージのnet / httpよりもはるかに高速に動作します。

fasthttpとは何ですか?

ここに写真の説明を挿入
Fasthttpは、httpサーバーとクライアントの高速実装です。アドレス:https//github.com/valyala/fasthttp

性能ストレステスト

ここに写真の説明を挿入
多くの人がfasthttpサーバーについて聞いたことがあると思います。それは非常に高速です。しかし、fasthttpクライアントについて聞いたことがある人はほとんどいません。Fasthttpサーバーは、httpサーバーの有名なベンチマークであるtechempowerベンチマークテストに参加しました。Fasthttpサーバーは第12ラウンドと第13ラウンドに参加します。第13ラウンドはまだ登場していません(2016年版)。
ここに写真の説明を挿入
fasthttpがほぼトップにある第12ラウンドのテストの結果の1つ。これらの数値は、特定のテストで彼が1秒あたりに行った要求の数を示しています。このテストでは、helloworldに提供されるページが要求されます。fasthttpは、helloworldページを非常にすばやく返します。
ここに写真の説明を挿入
次のラウンドの予備的な結果はまだ発表されていません(2016年-編集)。4つのfasthttp実装は、ベンチマークテストの最初の位置を占めます。これは、hello worldリターンを提供するだけでなく、テンプレートに基づいてhtmlページを形成します。

Fasthttpサーバーは非常に強力ですが、Fasthttpクライアントはどうですか?

fasthttpクライアントについて知っている人はほとんどいません。しかし実際には、彼もとてもかっこいいです。このレポートでは、fasthttpクライアントの内部構造とその開発の理由を紹介します。
ここに写真の説明を挿入
実際、fasthttpには、Client、HostClient、PipelineClientといういくつかのクライアントがあります。次に、それらについて詳しく説明します。

fasthttp.Client

ここに写真の説明を挿入
Fasthttp.Clientは、一般的な一般的なhttpクライアントです。これを使用すると、任意のインターネットサイトにリクエストを送信して、応答を得ることができます。その機能:非常に高速に動作し、ホストごとに開いている接続の数を制限できます。これは、net / httpパッケージとは異なります。このドキュメントはhttps://godoc.org/github.com/valyala/fasthttp#Clientにあります。

fasthttp.HostClient

ここに写真の説明を挿入
Fasthttp.HostClientは、1つのサーバーとのみ通信する専用クライアントです。通常、HTTP API(REST API、JSON API)にアクセスするために使用されます。また、インターネットから複数のサーバー上の内部DataCenterにトラフィックをプロキシするために使用することもできます。ドキュメントはここにあります:https//godoc.org/github.com/valyala/fasthttp#HostClient

Fasthttp.Clientと同様に、Fasthttp.HostClientを使用すると、各バックエンドサーバーで開いている接続の数を制限できます。この機能はnet / httpには存在せず、無料のnginxにも存在しません。私の知る限り、この機能は有料のnginxでのみ使用できます。

fasthttp.PipelineClient

ここに写真の説明を挿入
Fasthttp.PipelineClientは、サーバーまたは限られた数のサーバーからのパイプライン要求を管理できるようにする専用クライアントです。HTTPプロトコルを介してAPIにアクセスするために使用でき、多数の要求をできるだけ早く実行する必要があります。Fasthttp.PipelineClientの制限は、Head ofLineブロッキングの影響を受ける可能性があることです。これは、各リクエストへの応答を待たずに、サーバーに多くのリクエストを送信する場合です。一部の要求されたサーバーはブロックされます。したがって、彼の後の他のすべての要求は、このサーバーが遅い要求を処理するまで待機します。Fasthttp.PipelineClientは、サーバーが要求にすぐに応答することが確実な場合にのみ使用してください。ドキュメントはここにあります:https//godoc.org/github.com/valyala/fasthttp#PipelineClient

Fasthttp.HostClientの内部実装

次に、各クライアントの内部実装について説明します。他のほとんどすべてのクライアントがFasthttp.HostClientに基づいているため、Fasthttp.HostClientから始めます。
ここに写真の説明を挿入

これは、Goの疑似コードでの最も単純なHTTPクライアントの実装です。このURLのhttp応答を取得するために接続します。このホストに接続しています。連絡がありました。このコードでは、ボリュームよりも小さいため、すべてのエラーチェックが失われます。実際、これは不可能です。常にエラーをチェックする必要があります。接続を作成します。遅延を使用して接続を閉じます。URLを介してこの接続にリクエストを送信します。返信をいただき、この返信を差し上げました。このHTTPクライアントの実装の何が問題になっていますか?
ここに写真の説明を挿入
最初の問題は、この実装では、接続がリクエストごとに設定されることです。この実装はHTTPKeepAliveをサポートしていません。この問題を解決する方法は?サーバーごとに接続プールを使用できます。次の要求がどのサーバーに対して明確でないため、すべてのサーバーに接続プーリングを使用することはできません。各サーバーには、独自の接続プールが必要です。そして、HTTPKeepAliveを使用します。これは、ヘッダーで接続が閉じられていることを指定する必要がないことを意味します。HTTP / 1.1では、HTTP KeepAliveがデフォルトでサポートされており、接続を閉じることをヘッダーから削除する必要があります。以下は、クライアントの疑似コードでの実装であり、接続プーリングをサポートします。各ホストには、一連の接続プールがあります。最初の関数connPoolForHostは、指定されたURLから指定されたホストの接続プールを返します。次に、この接続プールから接続を取り出し、Deferを使用してこの接続をプールに送り返し、KeepAlive要求をこの接続に送信して、応答を返すことを計画します。応答後、Deferを実行し、接続プールに戻ります。したがって、HTTP KeepAliveサポートを有効にすると、すべてが高速になり始めました。リクエストごとに接続を作成する時間を無駄にしないためです。

しかし、解決策にも問題があります。関数の署名を見ると、要求ごとに応答オブジェクトが返されていることがわかります。これは、このオブジェクトにメモリを割り当てる必要があるたびに、初期化して返すことを意味します。これはパフォーマンスに悪影響を及ぼします。Get関数に対してこのような呼び出しを何度も行うと、問題が発生する可能性があります。
ここに写真の説明を挿入

したがって、この問題は、Fasthttpで解決できるため、この関数のパラメーターの応答オブジェクトにポインターオブジェクトを配置することで解決できます。したがって、呼び出し元のコードは応答オブジェクトを複数回再利用できます。スライドで、アイデアを実行します。Get関数では、応答オブジェクトへの参照を渡し、関数はこの応答を埋めます。最後の行がこのオブジェクトを埋めます。
ここに写真の説明を挿入

以下は、コードでどのように見えるかです。ポーリングするURLのリストを送信する受信チャネルの機能。このチャンネルでリングを整理しましょう。応答オブジェクトを作成し、ループで再利用します。Getを発生させ、オブジェクトへのポインタを渡し、応答を処理します。処理後、元の状態にリセットしてください。したがって、メモリの割り当てを回避し、コードを高速化します。
ここに写真の説明を挿入

3番目の問題は、接続が閉じていることです。接続クローズは、要求と応答の両方に表示される可能性のあるHTTPヘッダーです。このようなヘッダーを取得した場合は、この接続を閉じる必要があります。したがって、クライアントの実装では、接続を閉じるために提供する必要があります。ヘッダー接続を使用して要求を送信した場合は、応答を受信した後で接続を閉じる必要があります。接続せずにリクエストを送信し、接続を閉じるために応答を返す場合は、応答を受信した後に接続を閉じる必要があることも意味します。
ここに写真の説明を挿入

これは、この実装の疑似コードです。応答を受信した後、ヘッダーでConnectedがcloseに設定されているかどうかを確認します。設定されている場合は、接続を閉じるだけです。設定されていない場合は、接続をプールに戻します。そうしないと、応答を返した後にサーバーが接続を閉じると、接続プールにはサーバーが閉じた切断された接続が含まれ、書き込みを試みるとエラーが発生します。
ここに写真の説明を挿入

HTTPクライアントに影響を与える4番目の問題は、サーバーの速度が遅いか、ネットワークが動作していないことです。サーバーは、さまざまな理由で要求への応答を停止する場合があります。たとえば、サーバーが破損しているか、クライアントとサーバー間のネットワークが機能しなくなっています。したがって、上記のGet関数を呼び出すすべてのゴルチンはブロックされ、サーバーからの応答を無期限に待機します。たとえば、着信接続を受け入れるhttpプロキシを実装し、接続ごとにGet関数を呼び出すと、多数のゴルチンが作成され、メモリが使い果たされるまでサーバーがクラッシュするまで、それらはすべてサーバー上でハングします。
[外部リンク画像の転送に失敗しました(img-Ws7lKFjh-1565491354888)(leanote:// file / getImage?fileId = 5d4bbedbae67871dc8000011)]

この問題を解決する方法は?そのような素朴な解決策があります、これは頭に浮かぶのは初めてです-それを独立した山に変えるだけです。次に、空のチャネルをgorutineに渡します。これは、Get後に閉じられます。ゴルチンが解放された後、このチャネルで一定時間(タイムアウト)待ちます。この場合、時間がかかり、このGetが失敗すると、この関数はタイムアウトしたときに終了します。このGetが実行されると、チャネルは閉じられて終了します。しかし、この決定は、問題を病気の頭から健康な頭に移したため、間違っていました。それでも、どのタイムアウトが使用されても、ゴルチンは作成されて一時停止されます。Getタイムアウトを引き起こすゴルチンの数は制限されますが、タイムアウト期間内に無制限の数のゴルチンが作成されます。
[外部リンク画像の転送に失敗しました(img-fMAfvgbS-1565491354888)(leanote:// file / getImage?fileId = 5d4bbeecae67871dc8000012)]

この問題を解決する方法は?最初の解決策があります-これは、関数Getでブロックされるゴルチンの数を制限することです。これは、制限された長さのバッファーチャネルを使用するなど、よく知られたパターンを使用して実行できます。これにより、Get関数を実行するゴルチンの数がカウントされます。このゴルチンの量が特定の制限(このチャネルの容量)を超えると、デフォルトのブランチに戻ります。これは、Getが実行するすべてのGortoninがビジー状態であることを意味し、デフォルトでは、ブランチは空きリソースがないというエラーを返すだけで済みます。山を作成する前に、このチャネルにいくつかの空の構造を書き込もうとします。これが機能しない場合は、ゴルチンの数を超えています。有効な場合は、このゴルチンを作成し、Getが完了した後、このチャネルから値を読み取ります。
[外部リンク画像の転送に失敗しました(img-R3D0TYKr-1565491354891)(leanote:// file / getImage?fileId = 5d4bbf1eae67871dc8000013)]

最初の解決策を補足する2番目の解決策は、サーバーとの接続のタイムアウトを設定することです。サーバーが長時間応答しない場合、またはネットワークが中断された場合、これによりget機能のロックが解除されます。

ネットワークがソリューション#1で機能しない場合、すべてがハングします。cuncurrencyを使用して限られた数のゴルチンをスコアリングした後、gettimeout関数は常にエラーを返します。これを機能させるには、接続からの読み取りと書き込みのタイムアウトを設定する2番目のソリューション(ソリューション#2)が必要です。ネットワークまたはサーバーが機能しなくなった場合、これはブロックされたゴルチンのロックを解除するのに役立ちます。
[外部リンク画像の転送に失敗しました(img-Wkg7MoZa-1565491354892)(leanote:// file / getImage?fileId = 5d4bbf28ae67871dc8000014)]

ソリューション#1にはデータの競合があります。Getブロッキングがある場合、ポインターを渡す応答オブジェクトはビジーになります。ただし、この機能はタイムアウトする可能性があります。この場合、この機能を終了すると、応答がハングし、時間の経過とともに上書きされます。したがって、データが競合していることがわかります。関数を終了した後に応答があるので、それはまだゴルチンのどこかで使用されています。

応答のコピーを作成して山に渡すことにより、問題を解決します。Getを実行した後、この応答のコピーを元の応答のコピーにコピーします。これはここで送信されます。したがって、データの競合は解決されます。この応答のコピーが短時間存在し、プールに戻ります。応答を再利用します。タイムアウトした場合にのみ、応答のコピーがプールに収まらない場合があります。タイムアウト後、プール内の応答は失われます。
ここに写真の説明を挿入

タイムアウト期間中にサーバーが応答を返さなかった後、接続を閉じる必要がありますか?答えはノーです。それどころか、あなたがサーバーになりたいのなら、そうです。サーバーにリクエストを送信するときは、誰かをしばらく待つと、サーバーはこの期間中は応答しません。サーバーはリクエストを処理できません。たとえば、この接続を閉じますが、これはサーバーがこの要求の実行をすぐに停止することを意味するものではありません。サーバーは実行を継続します。あなたに応答を返そうとした後、サーバーはこの要求を実行する必要がないことを発見します。接続を閉じ、新しいリクエストを再度作成しようとし、再度タイムアウトし、再度閉じて、新しいリクエストを作成しました。サーバーの負荷が増加します。したがって、サービスは要求されたDoSに適用されます。これは、httpリクエストレベルのDoSです。サーバーが遅く、それらを無効にしたくない場合は、タイムアウト後に接続を閉じる必要はありません。このサーバーから補償を得るには、しばらく待って接続を維持する必要があります。彼にあなたに返事をさせてください。この期間中、他のアイドル接続が使用されます。以前に説明したすべてのコンテンツは、Fasthttp.Client実装のすべての段階と、Fasthttp.Clientの実装中に発生した問題です。これらの問題はFasthttp.HostClientで解決されています。他のアイドル接続を使用します。以前に説明したすべてのコンテンツは、Fasthttp.Client実装のすべての段階と、Fasthttp.Clientの実装中に発生した問題です。これらの問題はFasthttp.HostClientで解決されています。他のアイドル接続を使用します。以前に説明したすべてのコンテンツは、Fasthttp.Client実装のすべての段階と、Fasthttp.Clientの実装中に発生した問題です。これらの問題はFasthttp.HostClientで解決されています。

今、速い顧客がいますか?あんまり。接続プールがどのように実装されているかを理解する必要があります。

ここに写真の説明を挿入

接続プールの単純な実装は次のようになります。接続のサーバーアドレスを確立する必要があります。無料の接続のリストと、このリストへのアクセスを同期するためのロックがあります。
ここに写真の説明を挿入

以下は、接続プールから接続を取得するための関数です。コレクションリストを見ました。そこに何かがある場合は、アイドル状態の接続を取得して返します。そうでない場合は、このサーバーへの新しい接続を作成して返します。これの何が問題になっていますか?
ここに写真の説明を挿入

関数connPool.Putは、アイドル状態の接続を返します。

タイムアウトのため。Fasthttp.Clientでは、開いている未使用の接続の最長ライフサイクルを指定できます。この時間の後、未使用の接続は自動的に閉じられ、このプールからスローされます。

古い接続は時間の経過とともに使用されなくなり、自動的に閉じられてプールから削除されます。

プールから接続を取得し、サーバーがダウンしていることが判明し、そこに何かを書き込もうとすると、2回目の試行が行われます。新しい接続を取得し、その接続の要求を再度送信しようとします。ただし、これは、要求が同一である場合、つまり、副作用なしに複数回実行できる要求がGETまたはHEAD要求である場合のみです。たとえば、標準のnet / httpでは、閉じた接続のチェックを追加しただけです。彼らはより賢いチェックをしました。プールから接続に新しい要求を送信しようとすると、少なくとも1バイトが接続に入ると、チェックされます。行った場合は、エラーを返します。そうでない場合は、プールから新しい接続を取得します。
ここに写真の説明を挿入

プールの何が問題になっていますか?サイズに制限はありません。net / httpでの実装と同じです。数百万のゴルチンから低速のサーバーに移行するクライアントを作成すると、クライアントはそのサーバーへの数百万の接続を作成しようとします。標準のnet / httpパッケージでは、接続の最大数は無制限です。HTTP経由でAPIにアクセスするクライアントの場合、この接続プールのサイズを制限することをお勧めします。そうしないと、ストリーム、オブジェクト、接続、ゴルチン、メモリなどのすべてのリソースが使用されるため、クライアントがダウンする可能性があります。また、サーバーには多くの接続があり、使用されていないか非効率的であるため、サーバーのDoSが発生する可能性があります。これは、サーバーがそれほど多くの接続に対応できないためです。
[外部リンク画像の転送に失敗しました(img-yUdqWTe3-1565491354897)(leanote:// file / getImage?fileId = 5d4bbf80ae67871dc800001a)]

接続プールを制限します。スライドに収まらないほど大きいため、ここにはコードはありません。興味のある方は、github.comでこの機能の実装をご覧ください。アドレス:https//github.com/valyala/fasthttp/blob/master/workerpool.go
ここに写真の説明を挿入

2番目の質問。多くのリクエストは、特定の時点でクライアントに送信されます。その後、ドロップして前のリクエスト番号に戻ります。たとえば、10,000のリクエストを同時に送信してから、単位時間あたり1,000のリクエスト数を返します。この接続プールは10,000接続に増加します。これらの接続は際限なくそこにぶら下がっています。この問題は、バージョン1.7の標準のnet / httpクライアントで発生しました。したがって、この問題を解決する必要があります。
ここに写真の説明を挿入

未使用の接続の寿命を制限することにより、この問題を解決します。単一のリクエストが接続を介して一定期間送信されない場合、リクエストは閉じられ、プールからスローされます。

高速でクールなクライアントはありますか?ある角度で不確か。接続を作成する関数がまだあります-dialHost。

ここに写真の説明を挿入

その実装を見てみましょう。素朴な実現はこのように見えます。接続する必要があるアドレスだけです。これを標準のnet.Dial関数と呼びます。接続を返します。この実装の何が問題になっていますか?
ここに写真の説明を挿入

デフォルトでは、net.Dialは呼び出しごとにdns要求を発行します。これにより、DNSサブシステムのリソース使用率が増加する可能性があります。APIクライアントがKeepAlive接続をサポートしていないサーバーに接続すると、接続が閉じられます。KeepAliveはあなたをサポートしますが、サーバーはサポートしません。この応答の後、サーバーは接続を閉じます。net.Dialはリクエストごとに呼び出されることがわかりました。このような要求は1秒あたり約10,000です。dnsで毎秒100,000の解像度があります。これにより、DNSサブシステムがロードされます。
ここに写真の説明を挿入

この問題を解決する方法は?Goコード、つまりホストのマップにキャッシュを作成し、dnsを各net.Dialに解決しないでください。レディIPアドレスに接続します。
ここに写真の説明を挿入

2番目の問題は、ドメイン名の背後に複数のサーバーが隠れている場合、サーバーの負荷が不均一になることです。たとえば、ラウンドロビンDNSのように。一定期間DNSにIPアドレスをキャッシュすると、この期間中のすべての要求は1つのサーバーに送信されます。あなたはいくつか持っているかもしれませんが。この問題を解決する必要があります。これは、特定のドメイン名の背後に隠されている使用可能なすべてのIPを列挙することで解決されます。これはFasthttp.Clientでも行われます。
ここに写真の説明を挿入

3番目の問題は、ネットワークの問題または接続しようとしているサーバーが原因で、net.Dialが無期限にハングする可能性があることです。この場合、ゴルチンはGetメソッドでハングします。これにより、リソースの使用率も向上します。
ここに写真の説明を挿入

解決策は、タイムアウトを追加することです。または、標準のパケットネットワークでタイムアウトダイヤルを使用します。しかし、私が知る限り、それは正しく実装されていません。たぶん彼らは今それを修正しました、しかし私があなたに言ったようにそれを以前に実装しました。
ここに写真の説明を挿入

これが実装方法です。Getの代わりに、Dial機能があります。それは一種のゴルチンで行われます。Dialが電話を切ると、ゴルチンが蓄積していることがわかります。そのようなゴルチンをぶら下げる数は無期限に増える可能性があります。これは、DialTimeoutの標準実装です。多分彼らはそれを修正しました。
ここに写真の説明を挿入

また、HostClientには以下の機能があります。

HostClientは、指定したサーバーリストに負荷を分散できます。したがって、元のLoadBalanceが実現されます。

HostClientは、機能していないサーバーをスキップすることもできます。一部のサーバーが特定の時点で動作を停止した場合、HostClientはこのサーバーにアクセスしようとしたときにこれを検出します。次の接続では、彼はこのサーバーにアクセスしません。これにより、負荷分散を実現します。最小数のリクエストを失いました。

間違ったホストには2つの理由があります。

最初の理由は、サーバーとの接続を確立できないことです。ダイヤルに掛けます。この場合、このダイヤルでスタックしていることがわかります。取得、凍結、しばらく待ちます。彼が待っている間、他のすべての要求は他のサーバーに送られます。したがって、このホスト以外のホストを経由するリクエストが多くなります。

2番目のオプションは、サーバーがゆっくりと応答を開始することです。彼は他のサーバーよりもGetで多くの時間を取得します。この場合、このサーバーに送信される要求の数は他のサーバーよりも少なくなります。

エラーが返されたばかりの場合は、ループ内の次のサーバーに接続してみてください。

Golangの実装は非常に優れているため、SSLサポートは非​​常に簡単です。意思決定での使用と接続は簡単です。

Fasthttp.Clientの内部実装

fasthttp.Clientに移動します。実際、fasthttp.ClientはHostClientに基づいて実装されているため、HostClientと比較してすべてがはるかに簡単です。
ここに写真の説明を挿入

これは、クライアントがGet関数を実装するための元の疑似コードです。既知のホストごとにHostClientリストがあります。この関数は、特定のホストに必要なHostClientを特定の角度から返します。次に、このHostClientで関数Getを呼び出します。以下は、HostClientに基づくクライアントの実装全体です。
ここに写真の説明を挿入

この関数は、URLに表示される新しいテールに対して新しいHostClientを作成できます。Webクローラーを使用すると、クライアントは何百万ものサイトにアクセスできます。その結果、各サイトは100万のHostClientを取得し、すべてのメモリが終了します。これが標準のnet / httpでの方法であり、おそらくこの問題は解決されています。これを防ぐには、HostClientを定期的にクリーンアップする必要があります。長い間解決されていません。同じことがfasthttpにも当てはまります。

fasthttp.PipelineClientの内部実装

ClientやHostClientとは異なり、PipelineClientの実装は少し異なります。PipelineClientに接続プールはありません。PipelineClientは、ホスト上で確立する必要のある接続の数を選択できます。PipelineClientは、この数の接続を介してすべての要求をプッシュしようとします。したがって、接続プールはありません。PipelineClientはすぐに接続を確立し、着信要求を使用可能な接続に配布します。
ここに写真の説明を挿入

PipelineClientの場合、接続ごとに2つのゴルチンが開始されます。PipelineConnClient.writer-応答を待たずに接続に要求を書き込みます。PipelineConnClient.reader-この接続の応答を読み取り、PipelineConnClient.writerを介して送信された要求と照合します。PipelineConnClient.readerは、このGet関数を呼び出したコードへの応答を返します。
ここに写真の説明を挿入

スライドでは、PipelineClientのPipelineClient.Get関数の実装例を示しています。パイプライン作業構造には、引用する必要のあるURL、応答へのポインターがあり、チャネルの完了は、応答の準備ができていることを示します。

以下はGetの実装です。構造を作成して塗りつぶします。それをチャネルに送信し、PipelineConnClient.writerによって読み取られ、すべての要求が接続に書き込まれます。このリクエストへの応答が送信されたとき、PipelineConnClient.readerによって閉じられたチャネルw.doneを待っていました。

パフォーマンスの比較

次の2つのスライドで、net / httpクライアントのパフォーマンスとfasthttp.Clientを比較します。
ここに写真の説明を挿入

これらのスライドに示されているベンチマークは、fasthttpに示されています。自分で実行、テスト、テストできます。fasthttp。、Fasthttp 、。配布。
ここに写真の説明を挿入

出典:https//habr.com/ru/post/443378/

おすすめ

転載: blog.csdn.net/qq_32198277/article/details/99172183