Node.jsを高速に保つ:高性能のNode.jsサーバーを作成するためのツール、テクニック、ヒント

公開: 2022-03-10
簡単な要約↬ノードは非常に用途の広いプラットフォームですが、主要なアプリケーションの1つはネットワーク化されたプロセスの作成です。 この記事では、これらの中で最も一般的なHTTPWebサーバーのプロファイリングに焦点を当てます。

Node.jsを使用して十分長い間何かを構築している場合は、予期しない速度の問題の痛みを経験したことは間違いありません。 JavaScriptは、イベント型の非同期言語です。 明らかになるように、それはパフォーマンスについての推論をトリッキーにする可能性があります。 Node.jsの人気が急上昇しているため、サーバー側のJavaScriptの制約に適したツール、テクニック、思考の必要性が明らかになっています。

パフォーマンスに関しては、ブラウザーで機能するものがNode.jsに必ずしも適しているとは限りません。 では、Node.jsの実装が高速で、目的に適合していることをどのように確認するのでしょうか。 実践的な例を見ていきましょう。

ツール

ノードは非常に用途の広いプラットフォームですが、主要なアプリケーションの1つは、ネットワーク化されたプロセスの作成です。 これらの中で最も一般的なHTTPWebサーバーのプロファイリングに焦点を当てます。

パフォーマンスを測定しながら、大量のリクエストでサーバーを爆破できるツールが必要になります。 たとえば、AutoCannonを使用できます。

 npm install -g autocannon

他の優れたHTTPベンチマークツールにはApacheBench(ab)とwrk2がありますが、AutoCannonはNodeで記述されており、同様の(または場合によってはそれ以上の)負荷圧力を提供し、Windows、Linux、およびMac OSXに非常に簡単にインストールできます。

ジャンプした後もっと! 以下を読み続けてください↓

ベースラインパフォーマンス測定を確立した後、プロセスがより高速である可能性があると判断した場合、プロセスの問題を診断するための何らかの方法が必要になります。 さまざまなパフォーマンスの問題を診断するための優れたツールはノードクリニックです。これはnpmと一緒にインストールすることもできます。

 npm install -g clinic

これにより、実際に一連のツールがインストールされます。 ClinicDoctorとClinicFlame(0xのラッパー)を使用します。

この実践的な例では、ノード8.11.2以降が必要です。

コード

この例のケースは、単一のリソースを持つ単純なRESTサーバーです。つまり、 /seed/v1でGETルートとして公開される大きなJSONペイロードです。 サーバーは、 package.jsonファイル( restify 7.1.0に依存)、 index.jsファイル、およびutil.jsファイルで構成されるappフォルダーです。

サーバーのindex.jsファイルは次のようになります。

 'use strict' const restify = require('restify') const { etagger, timestamp, fetchContent } = require('./util')() const server = restify.createServer() server.use(etagger().bind(server)) server.get('/seed/v1', function (req, res, next) { fetchContent(req.url, (err, content) => { if (err) return next(err) res.send({data: content, url: req.url, ts: timestamp()}) next() }) }) server.listen(3000)

このサーバーは、クライアントにキャッシュされた動的コンテンツを提供する一般的なケースを表しています。 これは、コンテンツの最新の状態のETagヘッダーを計算するetaggerミドルウェアを使用して実現されます。

util.jsファイルは、このようなシナリオで一般的に使用される実装部分、バックエンドから関連コンテンツをフェッチする関数、etagミドルウェア、および分単位でタイムスタンプを提供するタイムスタンプ関数を提供します。

 'use strict' require('events').defaultMaxListeners = Infinity const crypto = require('crypto') module.exports = () => { const content = crypto.rng(5000).toString('hex') const ONE_MINUTE = 60000 var last = Date.now() function timestamp () { var now = Date.now() if (now — last >= ONE_MINUTE) last = now return last } function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag }) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } } function fetchContent (url, cb) { setImmediate(() => { if (url !== '/seed/v1') cb(Object.assign(Error('Not Found'), {statusCode: 404})) else cb(null, content) }) } return { timestamp, etagger, fetchContent } }

このコードをベストプラクティスの例として取り上げることは決してありません。 このファイルには複数のコードの臭いがありますが、アプリケーションを測定してプロファイリングするときにそれらを見つけます。

出発点の完全なソースを入手するには、ここで低速サーバーを見つけることができます。

プロファイリング

プロファイルを作成するには、2つの端末が必要です。1つはアプリケーションの起動用で、もう1つはアプリケーションの負荷テスト用です。

1つのターミナルで、 app内のフォルダーを実行できます。

 node index.js

別の端末では、次のようにプロファイルできます。

 autocannon -c100 localhost:3000/seed/v1

これにより、100の同時接続が開かれ、サーバーに10秒間リクエストが送信されます。

結果は次のようになります(10s test @ https://localhost:3000/seed/v1 — 100接続を実行):

統計平均Stdev マックス
レイテンシー(ミリ秒) 3086.81 1725.2 5554
要求/秒23.1 19.18 65
バイト/秒237.98 kB 197.7 kB 688.13 kB
10秒で231リクエスト、2.4MB読み取り

結果はマシンによって異なります。 ただし、「HelloWorld」Node.jsサーバーは、これらの結果を生成したマシンで1秒あたり3万件のリクエストを簡単に処理できることを考えると、平均レイテンシが3秒を超える1秒あたり23件のリクエストは悲惨です。

診断

問題領域の発見

Clinic Doctorの-on-portコマンドのおかげで、1つのコマンドでアプリケーションを診断できます。 appフォルダ内で実行します:

 clinic doctor --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

これにより、プロファイリングが完了するとブラウザで自動的に開くHTMLファイルが作成されます。

結果は次のようになります。

ClinicDoctorがイベントループの問題を検出しました
クリニックドクターの結果

医師は、おそらくイベントループの問題が発生したと言っています。

UIの上部にあるメッセージに加えて、イベントループチャートが赤であり、継続的に増加する遅延を示していることもわかります。 これが何を意味するのかを深く掘り下げる前に、まず、診断された問題が他のメトリックに与える影響を理解しましょう。

プロセスがキューに入れられた要求を処理するために一生懸命働いているので、CPUが常に100%以上であることがわかります。 この場合、マシンはマルチコアであり、V8は2つのスレッドを使用するため、ノードのJavaScriptエンジン(V8)は実際には2つのCPUコアを使用します。 1つはイベントループ用で、もう1つはガベージコレクション用です。 場合によってはCPUが120%まで急上昇しているのを見ると、プロセスは処理されたリクエストに関連するオブジェクトを収集しています。

これは、メモリグラフで相関していることがわかります。 メモリチャートの実線は、ヒープ使用量のメトリックです。 CPUにスパイクがあるときはいつでも、ヒープ使用量の行が減少し、メモリの割り当てが解除されていることを示しています。

アクティブなハンドルは、イベントループの遅延の影響を受けません。 アクティブハンドルは、I / O(ソケットやファイルハンドルなど)またはタイマー( setIntervalなど)のいずれかを表すオブジェクトです。 AutoCannonに100個の接続( -c100 )を開くように指示しました。 アクティブなハンドルは一貫して103のカウントを維持します。他の3つは、STDOUT、STDERRのハンドル、およびサーバー自体のハンドルです。

画面の下部にある[推奨事項]パネルをクリックすると、次のように表示されます。

クリニックドクター推奨パネルが開かれました
問題固有の推奨事項の表示

短期的な緩和

重大なパフォーマンス問題の根本原因分析には時間がかかる場合があります。 ライブデプロイされたプロジェクトの場合、サーバーまたはサービスに過負荷保護を追加する価値があります。 過負荷保護の考え方は、(とりわけ)イベントループの遅延を監視し、しきい値を超えた場合は「503ServiceUnavailable」で応答することです。 これにより、ロードバランサーを他のインスタンスにフェイルオーバーできます。最悪の場合、ユーザーは更新する必要があります。 過負荷保護モジュールは、Express、Koa、およびRestifyのオーバーヘッドを最小限に抑えてこれを提供できます。 Hapiフレームワークには、同じ保護を提供する負荷構成設定があります。

問題領域を理解する

Clinic Doctorの簡単な説明で説明されているように、イベントループが観察しているレベルまで遅延している場合、1つ以上の関数がイベントループを「ブロック」している可能性が非常に高くなります。

Node.jsでは、この主要なJavaScriptの特性を認識することが特に重要です。非同期イベントは、現在実行中のコードが完了するまで発生しません。

これが、 setTimeoutを正確にできない理由です。

たとえば、ブラウザのDevToolsまたはノードREPLで次のコマンドを実行してみてください。

 console.time('timeout') setTimeout(console.timeEnd, 100, 'timeout') let n = 1e7 while (n--) Math.random()

結果として得られる時間測定値が100msになることはありません。 150msから250msの範囲になる可能性があります。 setTimeoutは非同期操作( console.timeEnd )をスケジュールしましたが、現在実行中のコードはまだ完了していません。 さらに2行あります。 現在実行中のコードは、現在の「ティック」と呼ばれます。 ティックを完了するには、 Math.randomを1,000万回呼び出す必要があります。 これに100ミリ秒かかる場合、タイムアウトが解決するまでの合計時間は200ミリ秒になります(さらに、 setTimeout関数が実際にタイムアウトを事前にキューに入れるのにかかる時間(通常は数ミリ秒))。

サーバー側のコンテキストでは、現在のティックでの操作が要求を完了するのに長い時間がかかる場合、要求を処理できず、現在のティックが完了するまで非同期コードが実行されないため、データフェッチを実行できません。 これは、計算コストの高いコードがサーバーとのすべての対話を遅くすることを意味します。 したがって、リソースを大量に消費する作業を個別のプロセスに分割し、メインサーバーから呼び出すことをお勧めします。これにより、めったに使用されないが高価なルートで、頻繁に使用されるが安価な他のルートのパフォーマンスが低下する場合を回避できます。

サンプルサーバーにはイベントループをブロックしているコードがあるため、次のステップはそのコードを見つけることです。

分析

パフォーマンスの低いコードをすばやく特定する1つの方法は、フレームグラフを作成して分析することです。 フレームグラフは、関数呼び出しを、時間の経過ではなく全体として、互いの上にあるブロックとして表します。 「フレームグラフ」と呼ばれる理由は、通常、オレンジから赤の配色を使用しているためです。ブロックが赤くなるほど、関数は「熱く」なります。つまり、イベントループをブロックする可能性が高くなります。 フレームグラフのデータのキャプチャは、CPUをサンプリングすることで実行されます。つまり、現在実行されている関数とそのスタックのスナップショットが取得されます。 熱は、特定の関数が各サンプルのスタックの最上位にある(たとえば、現在実行されている関数)プロファイリング中の時間のパーセンテージによって決定されます。 そのスタック内で呼び出される最後の関数ではない場合は、イベントループをブロックしている可能性があります。

clinic flameを使用して、サンプルアプリケーションの炎グラフを生成してみましょう。

 clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

結果は、次のようなものでブラウザに表示されます。

クリニックの炎のグラフは、server.onがボトルネックであることを示しています
クリニックのフレームグラフの視覚化

ブロックの幅は、CPU全体に費やした時間を表します。 3つのメインスタックが最も時間を費やしていることがわかります。これらはすべて、最もホットな機能としてserver.onを強調しています。 実際、3つのスタックはすべて同じです。 プロファイリング中に最適化された関数と最適化されていない関数が別々の呼び出しフレームとして扱われるため、これらは分岐します。 *で始まる関数はJavaScriptエンジンによって最適化され、 ~で始まる関数は最適化されません。 最適化された状態が重要でない場合は、[マージ]ボタンを押すことでグラフをさらに簡略化できます。 これにより、次のようなビューが表示されます。

マージされたフレームグラフ
炎のグラフをマージする

最初から、問題のあるコードはアプリケーションコードのutil.jsファイルにあると推測できます。

slow関数はイベントハンドラーでもあります。関数に至るまでの関数はコアeventsモジュールの一部であり、 server.onはイベント処理関数として提供される無名関数のフォールバック名です。 また、このコードは、実際にリクエストを処理するコードと同じ目盛りではないことがわかります。 もしそうなら、コアhttpnetstreamモジュールの関数がスタックに含まれます。

このようなコア機能は、フレームグラフの他のはるかに小さい部分を展開することで見つけることができます。 たとえば、UIの右上にある検索入力を使用して、 sendrestifyhttpの両方の内部メソッドの名前)を検索してみてください。 グラフの右側にあるはずです(関数はアルファベット順にソートされています):

フレームグラフには、HTTP処理機能を表す2つの小さなブロックが強調表示されています
フレームグラフでHTTP処理関数を検索する

すべての実際のHTTP処理ブロックが比較的小さいことに注意してください。

シアンで強調表示されているブロックの1つをクリックすると、展開してwriteHeadなどの関数が表示され、 http_outgoing.jsファイル(ノードコアhttpライブラリの一部)にwriteます。

フレームグラフは、HTTP関連のスタックを示す別のビューにズームインしました
フレームグラフをHTTP関連のスタックに拡張する

すべてのスタックをクリックして、メインビューに戻ることができます。

ここで重要な点は、 server.on関数が実際のリクエスト処理コードと同じ目盛りになっていない場合でも、パフォーマンスの高いコードの実行を遅らせることで、サーバー全体のパフォーマンスに影響を与えていることです。

デバッグ

フレームグラフから、問題のある関数はutil.jsファイルでserver.onに渡されるイベントハンドラーであることがわかります。

見てみましょう:

 server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag })

シリアル化( JSON.stringify )と同様に、暗号化は高価になる傾向があることはよく知られていますが、フレームグラフに表示されないのはなぜですか? これらの操作はキャプチャされたサンプルに含まれていますが、 cppフィルターの背後に隠されています。 cppボタンを押すと、次のように表示されます。

C ++に関連する追加のブロックがフレームグラフ(メインビュー)で明らかになりました
シリアル化と暗号化のC ++フレームを明らかにする

シリアル化と暗号化の両方に関連する内部V8命令は、最もホットなスタックとして表示され、ほとんどの時間を占めるようになりました。 JSON.stringifyメソッドはC ++コードを直接呼び出します。 これが、JavaScript関数が表示されない理由です。 暗号化の場合、 createHashupdateなどの関数はデータに含まれていますが、インライン化されている(つまり、マージされたビューで表示されない)か、小さすぎてレンダリングできません。

etagger関数のコードについて推論し始めると、設計が不十分であることがすぐに明らかになります。 関数コンテキストからserverインスタンスを取得するのはなぜですか? たくさんのハッシュが行われていますが、それはすべて必要ですか? また、実装にはIf-None-Matchヘッダーのサポートがなく、クライアントは鮮度を判断するためにヘッドリクエストを行うだけなので、実際のシナリオでの負荷の一部を軽減します。

今のところこれらの点をすべて無視して、 server.onで実行されている実際の作業が実際にボトルネックであるという発見を検証しましょう。 これは、 server.onコードを空の関数に設定し、新しいフレームグラフを生成することで実現できます。

etagger関数を次のように変更します。

 function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => {}) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } }

server.onに渡されるイベントリスナー関数はno-opになりました。

clinic flameをもう一度実行してみましょう:

 clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

これにより、次のようなフレームグラフが生成されます。

フレームグラフは、Node.jsイベントシステムスタックが依然としてボトルネックであることを示しています
server.onが空関数の場合のサーバーのフレームグラフ

これは見栄えが良く、1秒あたりのリクエスト数の増加に気付くはずです。 しかし、なぜイベント発行コードはそれほどホットなのですか? この時点で、HTTP処理コードがCPU時間の大部分を占めると予想され、 server.onイベントでは何も実行されません。

このタイプのボトルネックは、関数が本来よりも多く実行されていることが原因です。

util.jsの上部にある次の疑わしいコードが手がかりになる可能性があります。

 require('events').defaultMaxListeners = Infinity

この行を削除して、 --trace-warningsフラグを使用してプロセスを開始しましょう。

 node --trace-warnings index.js

次のように、別の端末でAutoCannonを使用してプロファイルを作成する場合:

 autocannon -c100 localhost:3000/seed/v1

このプロセスでは、次のようなものが出力されます。

 (node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit at _addListener (events.js:280:19) at Server.addListener (events.js:297:10) at attachAfterEvent (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14) at Server. (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7) at call (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9) at next (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9) at Chain.run (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5) at Server._runUse (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19) at Server._runRoute (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10) at Server._afterPre (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10) (node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit at _addListener (events.js:280:19) at Server.addListener (events.js:297:10) at attachAfterEvent (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14) at Server. (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7) at call (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9) at next (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9) at Chain.run (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5) at Server._runUse (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19) at Server._runRoute (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10) at Server._afterPre (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10)

ノードは、多くのイベントがサーバーオブジェクトにアタッチされていることを通知しています。 これは奇妙なことです。イベントがアタッチされているかどうかをチェックし、最初のイベントがアタッチされた後、本質的にattachAfterEventをno-opにするブール値があります。

attachAfterEvent関数を見てみましょう。

 var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => {}) }

条件チェックが間違っています! afterEventAttachedではなくattachAfterEventがtrueであるかどうかをチェックします。 これは、すべてのリクエストで新しいイベントがserverインスタンスにアタッチされ、その後、各リクエストの後に以前にアタッチされたすべてのイベントが発生することを意味します。 おっと!

最適化

問題のある領域を発見したので、サーバーを高速化できるかどうかを見てみましょう。

容易に解決できる問題

server.onリスナーコードを(空の関数の代わりに)戻し、条件付きチェックで正しいブール名を使用してみましょう。 etagger関数は次のようになります。

 function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (afterEventAttached === true) return afterEventAttached = true server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag }) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } }

次に、再度プロファイリングして修正を確認します。 1つの端末でサーバーを起動します。

 node index.js

次に、AutoCannonでプロファイルします。

 autocannon -c100 localhost:3000/seed/v1

200倍の改善の範囲のどこかで結果が表示されるはずです(10秒テスト@ https://localhost:3000/seed/v1 — 100接続を実行):

統計平均Stdev マックス
レイテンシー(ミリ秒) 19.47 4.29 103
要求/秒5011.11 506.2 5487
バイト/秒51.8 MB 5.45 MB 58.72 MB
10秒で5万件のリクエスト、519.64MBの読み取り

サーバーの潜在的なコスト削減と開発コストのバランスを取ることが重要です。 私たちは、私たち自身の状況の文脈で、プロジェクトを最適化するためにどこまで進む必要があるかを定義する必要があります。 そうしないと、80%の労力を20%の速度向上に費やすのは簡単すぎる可能性があります。 プロジェクトの制約はこれを正当化しますか?

いくつかのシナリオでは、ぶら下がっている果物で200倍の改善を達成し、それを1日と呼ぶことが適切な場合があります。 また、実装をできるだけ速くしたい場合もあります。 それは本当にプロジェクトの優先順位に依存します。

リソースの消費を制御する1つの方法は、目標を設定することです。 たとえば、10倍の改善、つまり1秒あたり4000リクエスト。 これをビジネスニーズに基づいて行うのが最も理にかなっています。 たとえば、サーバーのコストが予算を100%超えている場合、2倍の改善という目標を設定できます。

さらに進んで

サーバーの新しいフレームグラフを作成すると、次のようなものが表示されます。

フレームグラフでは、server.onがボトルネックとして表示されますが、ボトルネックは小さくなります。
パフォーマンスのバグ修正が行われた後のフレームグラフ

イベントリスナーは依然としてボトルネックであり、プロファイリング中にCPU時間の3分の1を占めています(幅はグラフ全体の約3分の1です)。

どのような追加の利益を得ることができますか、そして変更は(関連する混乱とともに)行う価値がありますか?

最適化された実装では、それでもわずかに制約がありますが、次のパフォーマンス特性を実現できます(10sテスト@ https://localhost:3000/seed/v1 — 10接続の実行)。

統計平均Stdev マックス
レイテンシー(ミリ秒) 0.64 0.86 17
要求/秒8330.91 757.63 8991
バイト/秒84.17 MB 7.64 MB 92.27 MB
11秒で92kリクエスト、937.22MB読み取り

1.6倍の改善は重要ですが、この改善を作成するために必要な労力、変更、およびコードの中断が正当化されるかどうかは、状況によって異なります。 特に、単一のバグ修正による元の実装の200倍の改善と比較した場合。

この改善を達成するために、プロファイル、フレームグラフの生成、分析、デバッグ、および最適化の同じ反復手法を使用して、最終的に最適化されたサーバーに到達しました。そのコードはここにあります。

8000 req / sに到達するための最終的な変更は次のとおりです。

  • オブジェクトを作成してからシリアル化せず、JSONの文字列を直接作成します。
  • ハッシュを作成するのではなく、コンテンツに固有の何かを使用してEtagを定義します。
  • URLをハッシュせず、キーとして直接使用します。

これらの変更は少し複雑で、コードベースを少し混乱させ、 Etag値を提供するためのルートに負担をかけるため、 etaggerミドルウェアの柔軟性が少し低下します。 ただし、プロファイリングマシンで1秒あたり3000リクエストが追加されます。

これらの最終的な改善のための炎のグラフを見てみましょう:

フレームグラフは、ネットモジュールに関連する内部コードがボトルネックになっていることを示しています
すべてのパフォーマンスが改善された後の健全な炎のグラフ

フレームグラフの最もホットな部分は、 netモジュールのノードコアの一部です。 これは理想的です。

パフォーマンスの問題の防止

最後に、展開前にパフォーマンスの問題を防ぐ方法に関するいくつかの提案を示します。

開発中に非公式のチェックポイントとしてパフォーマンスツールを使用すると、パフォーマンスのバグを本番環境に移行する前に除外できます。 AutoCannonとClinic(または同等のもの)を日常の開発ツールの一部にすることをお勧めします。

フレームワークを購入するときは、パフォーマンスに関するポリシーを確認してください。 フレームワークがパフォーマンスを優先しない場合は、それがインフラストラクチャの慣行やビジネス目標と一致しているかどうかを確認することが重要です。 たとえば、Restifyは明らかに(バージョン7のリリース以降)ライブラリのパフォーマンスの向上に投資してきました。 ただし、低コストと高速が絶対的な優先事項である場合は、Restifyの寄稿者によって17%高速であると測定されたFastifyを検討してください。

他の広く影響を与えるライブラリの選択に注意してください—特にロギングを検討してください。 開発者が問題を修正すると、将来、関連する問題のデバッグに役立つログ出力を追加することを決定する場合があります。 パフォーマンスの悪いロガーを使用すると、茹でガエルの寓話が流行した後、時間の経過とともにパフォーマンスが低下する可能性があります。 ピノロガーは、Node.jsで利用可能な最速の改行区切りのJSONロガーです。

最後に、イベントループは共有リソースであることを常に忘れないでください。 Node.jsサーバーは、最終的に最もホットなパスで最も遅いロジックによって制約されます。