12. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
ベンチマークの結果(改善前)
12
13. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
ベンチマークの結果
想像以上にパフォーマンスが良くない
- 秒間リクエスト数は23req/s程度で頭打ち
- レスポンスタイムも並列数に比例してあがっていく
多数のインスタンスを並べれば要件は満たせるが…
何らかのボトルネックがありそうな気がする。
13
14. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
さてどうしよう
15. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
先入観
「SSR時のrenderToNodeStreamが重いのかなと思っています」
「SSR自体重いと聞きますし、確かに怪しそうですね」
喜楽
後藤
15
16. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
まずはボトルネックを特定したい
- 地道にconsole.time, console.timeEndを仕込む?
- sjspのようなプロファイリングコードを挿入してくれるツールを使う?
- functionの実行時間と実行回数が出力できる
- nodeの—profオプションを使う?
- ちょっと調べた感じ難しそう…
16
17. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
Nodeサポートチームに相談
「ボトルネック特定するベストプラクティスを教えてほしいです」
「まずは profile を取ることからだと思います」
後藤
Nodeサポート
17
18. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
プロファイルをとる?
Node.js (V8)に備わっているプロファイリング機能がある
--profオプションを指定してアプリケーションサーバを起動すると
isolate-0xnnnnnnnnnnnn-v8.logというファイルにログが出力される
このファイルにnode --process-profを適用することで読める形に変換される
Easy profiling for Node.js Applications | Node.js
https://nodejs.org/en/docs/guides/simple-profiling/
$ node —prof index.js
$ node —process-prof isolate-0xnnnnnnnnnnnn-v8.log > result.txt
18
19. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
プロファイル結果の見方
プロファイル結果はいくつかのセクションに分かれているが、
まずはSummaryセクションを確認する。
今回の場合、C++ のCPU Timeが多いことがわかる
[Summary]:
ticks total nonlib name
186 9.2% 9.3% JavaScript
1788 88.5% 89.8% C++
39 1.9% 2.0% GC
29 1.4% Shared libraries
18 0.9% Unaccounted
19
20. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
プロファイル結果の見方
[C++]:
ticks total nonlib name
453 22.4% 22.7% t node::fs::Open(v8::FunctionCallbackInfo<v8::Value> const&)
379 18.8% 19.0% T node::contextify::ContextifyScript::New(v8::FunctionCallbackInfo<v8::Value> const&)
90 4.5% 4.5% t node::fs::InternalModuleReadJSON(v8::FunctionCallbackInfo<v8::Value> const&)
85 4.2% 4.3% t node::fs::Read(v8::FunctionCallbackInfo<v8::Value> const&)
C++の処理の内訳を確認すると、File関連の処理が重そう
20
21. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
プロファイル結果の見方
[Bottom up (heavy) profile]:
ticks parent name
453 22.4% t node::fs::Open(v8::FunctionCallbackInfo<v8::Value> const&)
453 100.0% T v8::internal::Builtin_HandleApiCall(int, v8::internal::Object**, v8::internal::Isolate*)
453 100.0% LazyCompile: ~openSync fs.js:429:18
453 100.0% LazyCompile: ~readFileSync fs.js:341:22
Bottom upの項目を確認すると、readFileSyncが重そう
21
22. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
ボトルネックの可視化
重い箇所のあたりはついてきた。
ボトルネックをコードレベルで特定するのに以下のツールを使う。
- flamebearer
- node --inspect
22
23. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
flamebearerによる可視化
プロファイルログをフレームグラフで表示してくれるツール
$ node --prof-process --preprocess -j isolate*.log | flamebearer
23
24. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
flamebearerによる可視化
パスを一部を指定するとハイライト表示してくれる(緑色の部分)
24
25. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
node —inspect
--inspectを付けてnodeを立ち上げることで、Chromeと連携してデバッグが可能
起動後、chrome://inspect/ を開くと
実行中のプロセスにアタッチできる
$ node —inspect dist/index.js
Debugger listening on ws://127.0.0.1:9229/0dbdaf5f-7012-4ad0-8693-4af2906a9fb7
For help, see: https://nodejs.org/en/docs/inspector
25
26. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
node —inspect
Chart表示が視覚的に時間がかかっている箇所を特定しやすくおすすめ
26
27. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
わかったこと
JSON.parseの頻繁な呼び出しがボトルネックになっていた
- axios-cache-adapterのキャッシュ結果が文字列で保持されており、
キャッシュ取得時はJSON.parseしていた
- JSON.parseは同期処理なためボトルネックに
Intl.DateTimeFormatがボトルネックになっていた
- 社内製ライブラリで使われていた
DIコンテナからインスタンスを取り出すとき毎回constructorが呼ばれていた
- constructor内でreadFileSyncがあった
- Spring Bootの経験からデフォルトがシングルトンだと思い込んでいた
27
28. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
それぞれ対応
29. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
改善1:axiosのキャッシュパフォーマンス改善
課題
axios-cache-adapterのキャッシュ結果が文字列で保持されており、
キャッシュ取得時は毎回JSON.parseしていた。
NewsWebではバックエンドAPIの数が多く、また、
JSON.parseは同期処理なためボトルネックになっていた。
対応
axiosのキャッシュアダプタを自作。
JSON(文字列)ではなくオブジェクトをキャッシュして返すように変更した。
※ボトルネック解消を目的としているため、これが設計として良いかどうかは別の問題です
29
30. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
改善2:社内製のライブラリの実装改善
課題
Intl.DateTimeFormatを使ったある社内ライブラリ(ExpressのMiddleware)
の処理が重く、ボトルネックになっていた。
対応
ライブラリの開発者にDate.toUTCStringを使った実装へ変更してもらい、
そちらを利用するようにした。
30
31. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
参考: Intl.DateTimeFormatのパフォーマンス
31
const date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));
console.time('Intl.DateTimeFormat');
for(let i = 0; i < 10000; i++) {
new Intl.DateTimeFormat('en-US').format(date);
}
console.timeEnd('Intl.DateTimeFormat');
console.time('Date.toUTCString');
for(let i = 0; i < 10000; i++) {
date.toUTCString();
}
console.timeEnd('Date.toUTCString');
$ node index.js
Intl.DateTimeFormat: 5991.971ms
Date.toUTCString: 5.954ms
検証コード 実行結果
1万回のループで検証。1000倍近いパフォーマンスの差がありそう。
32. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
改善3:DIコンテナ内のインスタンスをシングルトンに
課題
DIコンテナのインスタンスを参照する度にコンストラクタが実行されている。
コンストラクタ内でReadFileSyncするクラスがありボトルネックになっていた。
対応
コンテナにSingletonスコープでオブジェクトを登録をした。
https://github.com/inversify/InversifyJS/blob/master/wiki/scope.md
// 修正前
container.bind(DISymbols.TopicsService).to(TopicsService);
// 修正後
container.bind(DISymbols.TopicsService).to(TopicsService).inSingletonScope();
32
33. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
改善4:App起動時にDIコンテナのwarmup
課題
InversifyJSのコンテナからオブジェクトを取り出すとき、
コンストラクタが実行されるため、初回の呼び出しが遅い。
対応
App起動時に、コンテナに登録された全オブジェクトをgetし、
コンストラクタを実行しておく。
// 予めDI Containerのすべてを呼び出して、singletonインスタンスを初期化する
// typescriptのチェックを回避するために型定義をして再代入しています
const symbols: { [key: string]: symbol } = {
...DISymbols
};
for (const key in symbols) {
if (key) {
container.get(symbols[key]);
}
}
33
34. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
それ以外にも気づきベースで対応したこと
ボトルネック調査の結果とは別に、以下の改善の実施
- Clusterモジュールの利用
- バックエンドAPIへのKeep Alive対応
34
35. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
改善5:Clusterモジュールの導入
課題
Private PaaS は1インスタンスあたり8コア割り当てられていた。
Node.jsはデフォルトでは1コアしか使わないため、有効活用できていなかった。
対応
Node.jsの標準モジュールのClusterモジュールを導入した。
https://nodejs.org/api/cluster.html 35
36. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
Clusterモジュールの実装
36
import cluster from 'cluster';
import express from 'express';
import os from 'os';
const port = process.env.PORT || '3000';
if (cluster.isMaster) {
for (let i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
cluster.on('exit', worker => {
cluster.fork();
});
} else {
const app = express();
app.listen(port);
}
import express from 'express';
const port = process.env.PORT || '3000';
const app = express();
app.listen(port);
導入前 導入後
得られる恩恵の割に非常に導入が容易です。
37. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
補足:Clusterモジュールとnode --inspect
Clusterモジュールを導入した後、node --inspectを使うと
各プロセスでデバッグ機能が有効になってしまい使い勝手が悪い
例えば、以下のように--inspectパラメータが指定されているかを判定し、
Clusterモジュールを使わずに起動できるようにしておくと捗ります。
37
const debug = /--inspect/.test(process.execArgv.join(' '));
if (debug) {
// expressを起動
} else {
// clusterモジュールでforkして、アプリケーションを起動
if (cluster.isMaster) {
// fork...
} else {
// expressを起動...
}
}
38. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
改善6:バックエンドAPIへのKeep Alive対応
課題
バックエンドAPIへのリクエスト時にKeep Aliveが有効になっていなかった。
対応
axiosに以下のように設定をしてKeep Aliveを有効化
特に、ユーザ情報を含むようなRedisにキャッシュできないAPIに対して効果
接続時のレイテンシ低減、APIサーバの負荷軽減に繋がる。
const axiosClient = axios.create({
timeout,
adapter: createCacheAdapter(axios, {
maxAge
}),
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true })
});
38
39. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
再ベンチマーク
40. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
対応実施後の性能を要件毎にみていきます
以下の3パターンに分けて、再度性能評価を行いました。
- 対応前
- Clusterモジュール導入を除く改善実施後
- Clusterモジュール導入を含んだ改善実施後
40
41. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
要件のおさらい
秒間リクエスト数
- 4000req/s以上
レスポンスタイム
- 99パーセンタイル値が1秒以下
- 80パーセンタイル値が0.5秒以下
現行のシステムの性能をベースにした要件で、概ね上記を満たしたい。
41
42. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
要件のおさらい
秒間リクエスト数
- 4000req/s以上
レスポンスタイム
- 99パーセンタイル値が1秒以下
- 80パーセンタイル値が0.5秒以下
現行のシステムの性能をベースにした要件で、概ね上記を満たしたい。
👈
42
43. Copyright (C) 2019 Yahoo Japan Corporation. All Rights Reserved.
秒間リクエスト数の比較
Clusterモジュールを除いた改善で1インスタンスあたり57req/sec、
Clusterモジュール導入で330req/sec程度の性能がでることがわかりました。
0
50
100
150
200
250
300
350
400
並列数ごとの秒間リクエスト数
対応前 Cluster以外の対応後 Cluster含む対応後
43