例としてfasthttpを使用して、高性能のhttpクライアントを作成しています。 アレクサンダー・ヴァリャルキン(VertaMedia)

Fasthttpライブラリは、標準のGolangパッケージのnet / httpの高速化された代替手段です。

どのように配置されていますか? なぜ彼女はそんなに速いのですか?







Alexander Valyalkin Fasthttpクライアント内部のレポートのトランスクリプトを注意してください。

Fasthttpパターンを使用して、アプリケーションやコードを高速化できます。









誰も気にしない、猫へようこそ。







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













Fasthttpは、httpサーバーおよびクライアントの高速実装です。 github.comのfasthttpにあります













fasthttpサーバーについて多くの人が聞いたことがあると思います。それは非常に高速です。 しかし、fasthttpクライアントについて聞いた人はほとんどいません。 Fasthttpサーバーは、 techempowerベンチマーク(httpサーバーの有名な狭いベンチマーク)に参加しています。 Fasthttpサーバーは、ラウンド12とラウンド13に参加します。 ラウンド13はまだ出ていません(2016年-概算)。













第12ラウンドのテストの1つの結果。fasthttpはほぼ最上部にあります。 数字は、このテストで彼が1秒間に行うクエリの数を示しています。 このテストでは、hello worldを返すページの要求が行われます。 Hello Worldでは、fasthttpは非常に高速です。













まだリリースされていない次のラウンドの予備結果(2016年-概算)。 4つのfasthttp実装がベンチマークの最初の位置を占めます。これは、hello worldが提供するだけでなく、データベースにクロールし、テンプレートに基づいてhtmlページを形成します。













fasthttpクライアントについて知っている人はほとんどいません。 しかし、実際には彼もクールです。 このレポートでは、内部デバイスのfasthttpクライアントと、それが開発された理由について説明します。













実際、fasthttpにはいくつかのクライアントがあります:Client、HostClient、PipelineClient。 さらに、それぞれについて詳しく説明します。













Fasthttp.Clientは、通常の汎用HTTPクライアントです。 これを使用して、任意のインターネットサイトにリクエストを送信し、回答を受け取ることができます。 その機能:迅速に動作し、net / httpパッケージとは異なり、ホストごとに開いている接続の数を制限できます。 ドキュメントはhttps://godoc.org/github.com/valyala/fasthttp#Clientにあります













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は、サーバーまたは限られた数のサーバーへのパイプラインリクエストを管理できる特殊なクライアントです。 HTTPプロトコルを介してAPIにアクセスするために使用できます。この場合、できるだけ多くの要求を迅速に実行する必要があります。 Fasthttp.PipelineClientの制限は、Head of Lineブロックの影響を受ける可能性があることです。 これは、サーバーに多くのリクエストを送信し、各リクエストへの回答を待たない場合です。 サーバーはこれらの要求の1つでブロックされています。 このため、彼に続く他のすべてのリクエストは、このサーバーが遅いリクエストを処理するまで待機します。 Fasthttp.PipelineClientは、サーバーが要求に即座に応答することが確実な場合にのみ使用してください。 ドキュメンテーション













次に、これらの各クライアントの内部実装について説明します。 Fasthttp.HostClientから始めます。これは、他のほとんどすべてのクライアントがそれに基づいて構築されているためです。













これは、Goの擬似コードでのHTTPクライアントの最も単純な実装です。 接続され、このURLでhttp応答を取得します。 このホストに接続しています。 接続を取得します。 このコードでは、ボリュームよりも小さくなるように、すべてのエラーチェックが欠落しています。 実際、これはそうではありません。 エラーを常に確認する必要があります。 接続を作成します。 接続を延期して閉じます。 この接続のリクエストをURLで送信します。 答えを受け取り、この答えを返します。 このHTTPクライアントの実装の何が問題になっていますか?













最初の問題は、この実装では、リクエストごとに接続が確立されることです。 この実装は、HTTPキープアライブをサポートしていません。 この問題を解決するには? サーバーごとに接続プールを使用できます。 次のリクエストではどのサーバーに送信するかが明確ではないため、すべてのサーバーに接続プールを使用することはできません。 各サーバーには独自の接続プールが必要です。 そして、HTTP KeepAliveを使用します。 これは、ヘッダーでConnection Closeを指定する必要がないことを意味します。 HTTP / 1.1では、デフォルトでHTTP KeepAliveがサポートされており、ヘッダーからConnection Closeを削除する必要があります。 以下は、接続プールをサポートするクライアント擬似コードでの実装です。 各ホストには、いくつかの接続プールのセットがあります。 最初の関数connPoolForHostは、指定されたURLから指定されたホストの接続プールを返します。 次に、この接続プールから接続を取得し、Deferを使用してこの接続をプールに送り返し、この接続のKeepAliveリクエストを送信し、応答を返します。 応答後、延期が実行され、接続がプールに戻ります。 したがって、HTTP KeepAliveサポートを有効にし、すべてがより速く動作し始めます。 リクエストごとに接続を作成する時間を無駄にしないためです。







しかし、解決策にも問題があります。 関数のシグネチャを見ると、各リクエストに対して応答オブジェクトを返すことがわかります。 これは、このオブジェクトに対して毎回メモリを割り当て、初期化して返す必要があることを意味します。 これはパフォーマンスに悪いです。 このようなGet関数呼び出しが多数ある場合、問題が発生する可能性があります。













したがって、この問題はFasthttpで解決されるため、この関数のパラメーターの応答オブジェクトにポインターオブジェクトを配置することで解決できます。 そうすれば、その呼び出しコードはこの応答オブジェクトを何度も再利用できます。 スライドには、このアイデアの実装があります。 Get関数では、応答オブジェクトへの参照を渡します-関数はこの応答を満たします。 最後の行はこのオブジェクトを埋めます。













コードでどのように見えるかを以下に示します。 ポーリングされるURLのリストが渡されるチャネルを受け入れる関数。 このチャンネルでサイクルを整理します。 応答オブジェクトを一度作成し、ループで再利用します。 Getを呼び出して、オブジェクトへのポインターを渡し、この応答を処理します。 処理した後、元の状態にリセットします。 このようにして、メモリ割り当てを回避し、コードを高速化します。













3番目の問題は、接続のクローズです。 接続のクローズ-HTTPヘッダー。要求と応答の両方で見つけることができます。 このようなヘッダーを取得した場合、このConnectionは閉じられます。 したがって、クライアントの実装では、Connection closeを提供することが不可欠です。 ヘッダーConnection closeを使用してリクエストを送信した場合、レスポンスを受信した後、この接続を閉じる必要があります。 接続を閉じずにリクエストを送信し、接続を閉じて応答を返した場合、応答を受信した後にこの接続を閉じる必要もあります。













この実装の擬似コードは次のとおりです。 応答を受け取った後、Connection closeヘッダーがそこにインストールされているかどうかを確認します。 インストールされている場合は、接続を閉じます。 インストールされていない場合は、接続をプールに戻します。 これが行われない場合、回答を返した後にサーバーが接続を閉じると、接続プールにはサーバーが閉じた壊れた接続が含まれ、それらに何かを書き込もうとするとエラーが発生します。













HTTPクライアントがさらされる4番目の問題は、サーバーの速度が遅いこと、またはアイドル状態のネットワークが遅いことです。 サーバーは、さまざまな理由でリクエストへの応答を停止する場合があります。 たとえば、サーバーが壊れているか、クライアントとサーバー間のネットワークが機能しなくなっています。 このため、前述のGet関数を呼び出すゴルーチンはすべてブロックされ、サーバーからの応答を無期限に待機します。 たとえば、着信接続を受け入れ、各接続でGet関数を呼び出すhttpプロキシを実装すると、多数のゴルーチンが作成され、サーバーがクラッシュするまで、メモリがなくなるまで、すべてのゴルーチンがサーバーでハングします。













この問題を解決するには? 最初に思い浮かぶのは、このような単純な決定です。このGetを別のゴルーチンでラップするだけです。 次に、ゴルーチンで、空のチャネルを渡します。このチャネルは、Getの実行後に閉じられます。 このゴルーチンを開始した後、このチャネルでしばらく待ちます(タイムアウト)。 この場合、しばらく時間が経過してこのGetが実行されない場合、この関数からのタイムアウトはタイムアウトによって発生します。 このGetが実行されると、チャネルが閉じて終了します。 しかし、病気の頭から健康な頭に問題を移すため、この決定は間違っています。 とにかく、ゴルーチンは作成され、使用するタイムアウトに関係なくハングします。 Getタイムアウトの原因となるゴルーチンの数は制限されますが、Get内でタイムアウト付きで作成されるゴルーチンの数に制限はありません。













この問題を解決するには? 最初の解決策は、Get関数でブロックされるゴルーチンの数を制限することです。 これは、Get関数を実行するゴルーチンの数をカウントする、制限された長さのバッファー付きチャネルの使用など、よく知られているパターンを使用して実行できます。 このゴルーチンの量が特定の制限(このチャネルの容量)を超える場合、デフォルトのブランチに戻ります。 これは、ビジー状態になるすべてのゴルーチンがあり、デフォルトのブランチではエラーを返すだけで、空きリソースがないことを意味します。 ゴルーチンを作成する前に、このチャネルに空の構造を書き込もうとします。 これがうまくいかない場合は、ゴルーチンの量を超えています。 判明した場合は、このgorutinを作成し、Getを実行した後、このチャネルから1つの値を読み取ります。 したがって、Getでブロックできるゴルーチンの量を制限します。













1つ目を補完する2つ目のソリューションは、サーバーへの接続にタイムアウトを設定することです。 これにより、サーバーが長時間応答しない場合、またはネットワークがダウンした場合にget機能がロック解除されます。







ソリューション1でネットワークが機能しない場合、すべてがハングします。 ここでハングした限られた数のゴルーチンをcuncurrencyと入力すると、getimeout関数は常にエラーを返します。 正常に動作を開始するには、接続からの読み取りと書き込みのタイムアウトを設定する2番目のソリューション(解決策2)が必要です。 これにより、ネットワークまたはサーバーが機能しなくなった場合に、ブロックされたゴルーチンのロックを解除できます。













ソリューション#1にはデータの競合があります。 Getがブロックされると、ポインタが渡された応答オブジェクトが占有されます。 ただし、このGetタイムアウト関数はタイムアウトする場合があります。 この場合、この関数を終了し、応答がハングし、しばらくしてから書き換えられます。 したがって、データの競合が発生します。 関数を終了した後の応答は、ゴルーチンのどこかでまだ使用されているためです。







この問題は、応答コピーを作成し、応答コピーをゴルーチンに渡すことで解決されます。 Getが完了したら、この応答からの応答を元の応答にコピーします。この応答はここに渡されます。 したがって、データの競合が解決されます。 この応答のコピーは短期間存続し、プールに戻ります。 応答を再利用します。 応答コピーは、タイムアウトによってのみプールに収まらない場合があります。 タイムアウトにより、プールからの応答が失われます。













サーバーがタイムアウト内に応答を返さなかった後、接続を閉じる必要がありますか? 答えはノーです。 むしろ、はい、サーバーをバックアップしたい場合。 サーバーにリクエストを送信するとき、しばらく待つと、サーバーはこの時間中に応答しないため、リクエストに対応しません。 たとえば、この接続を閉じますが、これはサーバーがこの要求の実行をすぐに停止するという意味ではありません。 サーバーはそれを実行し続けます。 サーバーは、応答を返そうとした後、この要求を実行する必要がないことを検出します。 接続を閉じ、新しいリクエストを作成しようとしましたが、タイムアウトが再び経過し、再び閉じて、新しいリクエストを作成しました。 サーバーの負荷が増加します。 その結果、サービスはリクエストに依存します。 これらは、http要求のレベルでのDoSです。 実行速度の遅いサーバーがあり、それらをバックアップしたくない場合は、タイムアウト後に接続を閉じる必要はありません。 しばらく待つ必要がありますが、このサーバーのために接続を維持してください。 彼にあなたに答えを与えてもらいましょう。 それまでの間、他の無料の接続を使用してください。 これまでに言われたことは、Fasthttp.Clientの実装のすべての段階と、Fasthttp.Clientの実装中に発生した問題だけです。 これらの問題はFasthttp.HostClientで解決されています。







現在、高速クライアントがありますか? そうでもない。 接続プールの実装方法を確認する必要があります。













単純な接続プールの実装は次のようになります。 接続をインストールする必要があるサーバーアドレスがあります。 空き接続のリストと、このリストへのアクセスを同期するためのロックがあります。













接続プールから接続を取得する関数は次のとおりです。 コレクションのリストを見ています。 そこに何かがある場合は、無料の接続を取得して返します。 何もない場合は、このサーバーへの新しい接続を作成して返します。 ここで何が間違っていますか?







connPool.Put関数は、空き接続を返します。







タイムアウトアカウントで。 Fasthttp.Clientでは、開いている未使用の接続の最大有効期間を指定できます。 この時間が経過すると、未使用の接続は自動的に閉じられ、このプールからスローされます。







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







接続がプールから取得され、そのサーバーが閉じられていることが判明し、そこに何かを書き込もうとすると、2回目の試行が行われます。新しい接続が取得され、この接続の要求を再度送信しようとします。 しかし、これは、このリクエストがべき等である場合(つまり、サーバーに副作用を与えることなく何度も実行できるリクエスト)がGETまたはHEADリクエストである場合のみです。 たとえば、標準のnet / httpに、閉じた接続のチェックを追加しました。 そこで、彼らはより巧妙なチェックを行いました。 プールから接続に新しい要求を送信しようとすると、少なくとも1バイトがこの接続に送信されるかどうかをチェックします。 オフに設定すると、エラーが返されます。 離れなかった場合、プールから新しい接続を取得します。













プールの何が問題になっていますか? そのサイズに制限はありません。 net / httpと同じ実装。 数百万のゴルーチンから低速のサーバーに分割するクライアントを作成すると、クライアントはこのサーバーへの100万の接続を作成しようとします。 標準のnet / httpパッケージの最大接続数に制限はありません。 HTTP経由でAPIにアクセスするために使用されるクライアントの場合、この接続プールのサイズを制限することをお勧めします。 そうしないと、スレッド、オブジェクト、接続、ゴルーチン、メモリなどのすべてのリソースを使用するため、クライアントがダウンする可能性があります。 また、サーバーが大量の接続を保持できないため、使用されないか、非効率的に使用される多くの接続がサーバーに確立されるため、サーバーのDoSにつながる可能性があります。













接続プールを制限します。 コードは大きすぎて1つのスライドに収まらないため、ここにはありません。 関心のある方は、github.comでこの関数の実装を確認できます。













2番目の問題。 ある時点で多くのリクエストがクライアントに届きます。 その後、減少し、以前のリクエスト数に戻ります。 たとえば、10,000件のリクエストが同時に到着すると、リクエストの数は単位時間あたり1000件に戻ります。 その後、接続プールは10000接続に拡大します。 これらの接続は無限にハングします。 この問題は、バージョン1.7より前の標準のnet / httpクライアントにありました。 したがって、この問題を解決する必要があります。













この問題は、未使用の接続の寿命を制限することで解決されます。 しばらくの間、単一の要求が接続経由で送信されなかった場合、単純に閉じられ、プールからスローされます。 実装が大きすぎるため、実装はありません。













高速でクールな仕事をするクライアントがいますか? あまり好きではありません。 接続を作成する機能-dialHostがまだあります。













その実装を見てみましょう。 素朴な実装は次のようになります。 接続したいアドレスは単に送信されます。 標準関数net.Dialを呼び出します。 彼女は接続を返します。 この実装の何が問題になっていますか?













デフォルトでは、net.Dialは各呼び出しに対してDNS要求を作成します。 これにより、DNSサブシステムのリソースの使用が増加する可能性があります。 APIクライアントがKeepAlive接続をサポートしないサーバーに接続すると、接続が閉じられます。 KeepAliveによってサポートされていますが、サーバーはサポートされていません。 このような応答の後、サーバーは接続を閉じます。 net.Dialはすべてのリクエストで呼び出されます。 1秒あたり約1万件のこのような要求があります。 DNSで解決されるのは1秒あたり1万回です。 これにより、DNSサブシステムがロードされます。













この問題を解決するには? Goコードで短時間IPに直接マップするキャッシュを作成し、各net.DialでDNS解決を呼び出さないでください。 既製のIPアドレスに接続します。













2番目の問題は、ドメイン名の背後に複数のサーバーが隠れている場合、サーバーの負荷が不均等になることです。 たとえば、ラウンドロビンDNSなど。 DNSに単一のIPアドレスをしばらくキャッシュすると、この時間中にすべてのリクエストが1つのサーバーに送信されます。 そこにいくつかあるかもしれませんが。 この問題を解決する必要があります。 特定のドメイン名の背後に隠されている使用可能なすべてのIPを列挙することで解決します。 これはFasthttp.Clientでも実行されます。













3番目の問題は、接続しようとしているネットワークまたはサーバーの問題により、net.Dialが無期限にハングする可能性があることです。 この場合、ゴルーチンはGet関数でハングします。 これにより、リソースの使用量が増加する可能性もあります。







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













これが実装された方法です。 Getの代わりに、Dial機能がありました。 それはある種のゴルーチンで行われました。 Dialがハングした場合、ゴルーチンが蓄積されていることがわかりました。 ハングしたそのようなゴルーチンの数は無期限に増加する可能性があります。 これは、DialTimeoutの標準実装です。 多分彼らはすでにそれを修正しているのでしょう。













さらに、HostClientには次の機能があります。







HostClientは、指定したサーバーのリストに負荷を分散できます。 したがって、プリミティブなLoadBalanceが実装されます。







HostClientは、アイドル状態のサーバーをスキップすることもできます。 ある時点で一部のサーバーが動作を停止すると、HostClientはこのサーバーにアクセスしようとしたときにこれを検出します。 次の接続では、このサーバーにアクセスしません。 したがって、負荷分散が実装されます。 リクエストの最小数を失います。







Faulyホストには2つの理由があります。







最初の理由は、サーバーへの接続を確立できないことです。 ダイヤルオンハング。 この場合、このダイヤルに引っかかっていることがわかります。 ハングするGetはしばらく待機します。 彼が待機している間、この時点で他のすべての要求は他のサーバーに送られます。 したがって、より多くの要求がこのホストを経由するよりも他のホストを経由します。







2番目のオプションは、サーバーの応答が遅い場合です。 Getの彼は、サーバーの他の部分よりも多くの時間を費やしています。 この場合、このサーバーに送信されるリクエストの数は他のサーバーよりも少なくなります。







エラーのみが返された場合、ラウンドロビンで次のサーバーに接続しようとしています。







Golangには非常に優れた実装があるため、SSLサポートは非​​常に簡単です。 ソリューションで使用して接続すると便利です。













fasthttp.Clientに移動します。 実際、fasthttp.ClientはHostClientに基づいて実装されているため、ここではHostClientと比較してすべてがはるかに単純です。













Get関数クライアントを実装するためのプリミティブな擬似コードを次に示します。 既知の各ホストのHostClientのリストがあります。 この関数は、特定の角度から特定のホストに必要なHostClientを返します。 次に、このHostClientで関数Getを呼び出します。 HostClientに基づいたクライアント実装全体を以下に示します。













この関数は、URLに表示される新しいテールの新しいHostClientを作成できます。 Webクロール(インターネットでのクライミング)に使用する場合、クライアントは何百万ものサイトにアクセスできます。 その結果、これらのHostClientの数百万を各サイトに取得し、すべてのメモリが不足します。 これはまさに標準のネット/ httpで起こったことであり、おそらく彼らはすでに問題を解決しているでしょう。 これを回避するには、長期間アクセスされていないHostClientを定期的にクリーニングする必要があります。 これがfasthttpの機能です。













ClientおよびHostClientとは異なり、PipelineClientの実装はわずかに異なります。 PipelineClientには接続プールがありません。 PipelineClientには、ホストにインストールする必要がある接続数のオプションがあります。 PipelineClientは、この接続量を介してすべての要求をプッシュしようとします。 したがって、接続プールはありません。 PipelineClientはすぐに接続を確立し、着信要求を利用可能な接続に分散します。













PipelineClientは、接続ごとに2つのゴルーチンを起動します。 PipelineConnClient.writer-応答を待たずに接続に要求を書き込みます。 PipelineConnClient.reader-この接続からの応答を読み取り、PipelineConnClient.writerを介して送信された要求と一致させます。 PipelineConnClient.readerは、このGet関数を呼び出したコードへの応答を返します。













このスライドは、PipelineClientのPipelineClient.Get関数の実装例を示しています。 pipelineWork構造には、アドレス指定するURL、応答へのポインタ、完了チャネルがあり、応答の準備ができていることを通知します。







以下のスライドは、Getの実装です。 構造を作成して塗りつぶします。 PipelineConnClient.writerによって読み取られるチャネルに送信し、すべての要求が接続に書き込まれます。 この要求に対する応答が到着すると、PipelineConnClient.readerによって閉じられるチャネルw.doneで待機しています。













次の2つのスライドのfasthttp.Clientとのネット/ httpクライアントパフォーマンスの比較。













これらのスライドに示されているベンチマークはfasthttpにあります。 それらを実行し、確認し、テストすることができます。 fasthttp. , fasthttp, . allocation . .













net/http. , allocation net/nttp. .













: PipelineClient connection?







: — pending , . . request, pending , Error.







: API , fasthttp, net/http?







: . net/http . . string -, string . , net/http, . - , . fasthttp , . . net/http fasthttp , net/http POST-, response, () . fasthttp , request response . 10 request 10 response . , . fasthttp 10 request 10 response? . — . , net/http. . , net/http — .







PS .







.







— .








All Articles