高スループットの Web アプリケーションを構築する場合、NginX と Node.js は自然にマッチします。これらはすべてイベント駆動型モデルに基づいて設計されており、Apache などの従来の Web サーバーの C10K ボトルネックを簡単に突破できます。デフォルトの構成でもすでに高い同時実行性を実現できますが、安価なハードウェアで 1 秒あたり数千件を超えるリクエストを実現したい場合は、まだやるべきことがいくつかあります。
この記事は、読者が NginX の HttpProxyModule を使用して、上流の Node.js サーバーのリバース プロキシとして機能することを前提としています。 Ubuntu 10.04 以降のシステムでの sysctl のチューニングと、node.js アプリケーションと NginX のチューニングを紹介します。もちろん、Debian システムを使用している場合も同じ目標を達成できますが、チューニング方法が異なります。
ネットワークチューニング
Nginx と Node.js の根本的な送信メカニズムを理解し、目的を絞った最適化を実行しないと、両者の最適化をどれだけ詳細に行っても無駄になる可能性があります。通常、Nginx は TCP ソケットを介してクライアントとアップストリーム アプリケーションを接続します。
私たちのシステムには TCP に対する多くのしきい値と制限があり、これらはカーネル パラメーターを通じて設定されます。これらのパラメータのデフォルト値は一般的な目的のために設定されていることが多く、Web サーバーの高トラフィックと短い寿命の要件を満たすことができません。
TCP チューニングの候補となるパラメーターをいくつか示します。これらを有効にするには、それらを /etc/sysctl.conf ファイルに入れるか、/etc/sysctl.d/99-tuning.conf などの新しい構成ファイルに入れてから sysctl -p を実行します。カーネルにそれらをロードさせます。この物理的な作業には sysctl-cookbook を使用します。
ここにリストされている値は安全に使用できますが、負荷、ハードウェア、使用法に基づいてより適切な値を選択するために、各パラメーターの意味を検討することをお勧めします。
重要なものをいくつか強調表示します。
上流アプリケーションのために下流クライアントにサービスを提供するには、NginX は 2 つの TCP 接続 (1 つはクライアントへ、もう 1 つはアプリケーションへ) を開く必要があります。サーバーが多数の接続を受信すると、システムの使用可能なポートがすぐに枯渇してしまいます。 net.ipv4.ip_local_port_range パラメータを変更すると、使用可能なポートの範囲を増やすことができます。 /var/log/syslog で「ポート 80 で SYN フラッディングの可能性があります。Cookie を送信しています」というエラーが見つかった場合は、システムが使用可能なポートを見つけられないことを意味します。 net.ipv4.ip_local_port_range パラメータを増やすと、このエラーを減らすことができます。
サーバーが多数の TCP 接続を切り替える必要がある場合、TIME_WAIT 状態の接続が多数生成されます。 TIME_WAIT は、接続自体は閉じられていますが、リソースは解放されていないことを意味します。 net_ipv4_tcp_tw_reuse を 1 に設定すると、カーネルは安全なときに接続をリサイクルしようとします。これは、新しい接続を再確立するよりもはるかに安価です。
これは、TIME_WAIT 状態の接続がリサイクルするまで待機する必要がある最小時間です。小さくすることでリサイクルを早めることができます。
接続状況の確認方法
netstat を使用します:
または ss を使用します:
NginX
Web サーバーの負荷が徐々に増加すると、NginX の奇妙な制限に遭遇し始めます。接続が切断され、カーネルは SYN フラッドを報告し続けます。現時点では、負荷平均と CPU 使用率は非常に小さく、サーバーは明らかにより多くの接続を処理できるため、非常にイライラします。
調査の結果、TIME_WAIT 状態の接続が多数あることが判明しました。これはサーバーの 1 つからの出力です:
合計 IP IPv6 のトランスポート
* 541 - - -
生 0 0 0 0
UDP 13 10 3
TCP 326 325 1
INET 339 335 4
フラグ 0 0 0 0
TIME_WAIT 接続が 47135 件あります。また、ss を見ると、それらはすべて閉じた接続であることがわかります。これは、サーバーが使用可能なポートのほとんどを消費したことを示し、サーバーが接続ごとに新しいポートを割り当てていることも意味します。ネットワークを調整することで問題は少し解決しましたが、それでも十分なポートがありませんでした。
さらに調査した結果、アップリンク キープアライブ コマンドに関する次のドキュメントを見つけました。
興味深いですね。理論的には、この設定では、キャッシュされた接続を介してリクエストを渡すことにより、無駄な接続が最小限に抑えられます。ドキュメントには、proxy_http_version を「1.1」に設定し、「Connection」ヘッダーをクリアする必要があるとも記載されています。さらに調査した結果、HTTP/1.1 は HTTP1.0 に比べて TCP 接続の使用を大幅に最適化しており、Nginx はデフォルトで HTTP/1.0 を使用するため、これは良いアイデアであることがわかりました。
ドキュメントで提案されているように変更すると、アップリンク構成ファイルは次のようになります:
提案どおり、サーバーセクションのプロキシ設定も変更しました。同時に、失敗したサーバーをスキップするために proxy_next_upstream が追加され、クライアントの keepalive_timeout が調整され、アクセス ログがオフになりました。構成は次のようになります:
client_max_body_size 16M;
keepalive_timeout 10;
場所 / {
proxy_next_upstream エラー タイムアウト http_500 http_502 http_503 http_504;
proxy_set_header 接続 "";
proxy_http_version 1.1;
proxy_pass http://backend_nodejs;
}
access_log off;
error_log /dev/null crit;
}
新しい構成を採用した後、サーバーが占有するソケットが 90% 削減されたことがわかりました。はるかに少ない接続を使用してリクエストを送信できるようになりました。新しい出力は次のとおりです:
合計: 558 (カーネル 604)
TCP: 4675 (estab 485、closed 4183、孤立した 0、synrecv 0、timewait 4183/0)、ポート 2768
合計 IP IPv6 のトランスポート
* 604 - - -
生 0 0 0 0
UDP 13 10 3
TCP 492 491 1
INET 505 501 4
Node.js
I/O を非同期に処理するイベント駆動型の設計のおかげで、Node.js はすぐに大量の接続とリクエストを処理できます。他にもチューニング方法はありますが、この記事では主にnode.jsのプロセス面に焦点を当てます。
ノードはシングルスレッドであり、複数のコアを自動的に使用しません。つまり、アプリケーションはサーバーのすべての機能を自動的に取得できません。
ノードプロセスのクラスタリングを実現します
アプリケーションを変更して、複数のスレッドをフォークし、同じポートでデータを受信できるようにすることで、負荷が複数のコアにまたがるようにすることができます。ノードには、この目標を達成するために必要なすべてのツールを提供するクラスター モジュールがありますが、それらをアプリケーションに追加するには多くの手作業が必要です。 express を使用している場合、eBay には使用できる cluster2 というモジュールがあります。
コンテキストの切り替えを防止します
複数のプロセスを実行する場合、各 CPU コアが一度に 1 つのプロセスのみでビジーになるようにする必要があります。一般に、CPU に N 個のコアがある場合、N-1 個のアプリケーション プロセスを生成する必要があります。これにより、各プロセスが適切なタイム スライスを取得し、カーネル スケジューラが他のタスクを実行できるように 1 つのコアが空きます。また、CPU 競合を防ぐために、基本的に Node.js 以外の他のタスクがサーバー上で実行されないようにする必要もあります。
私たちはかつて間違いを犯し、サーバー上に 2 つの Node.js アプリケーションをデプロイしてしまい、各アプリケーションが N-1 個のプロセスを開いてしまいました。その結果、CPU を奪い合うことになり、システム負荷が急激に上昇します。当社のサーバーはすべて 8 コア マシンですが、コンテキストの切り替えによって生じるパフォーマンスのオーバーヘッドは依然としてはっきりと感じられます。コンテキストスイッチングとは、CPU が他のタスクを実行するために現在のタスクを一時停止する現象を指します。切り替えるとき、カーネルは現在のプロセスのすべての状態を一時停止してから、別のプロセスをロードして実行する必要があります。この問題を解決するために、各アプリケーションが起動するプロセスの数を減らし、CPU を公平に共有できるようにしました。その結果、システムの負荷が軽減されました。
上の図に注目して、システム負荷 (青線) が CPU コア数 (赤線) を下回る様子を確認してください。他のサーバーでも同じことが確認されました。合計のワークロードは同じままであるため、上記のグラフのパフォーマンスの向上はコンテキスト スイッチの減少によるものとしか考えられません。