エンジニアリンググループの山口 (@no_clock) です。
性能試験、していますか。
非常にニッチですが、バックエンドをモック化してリバースプロキシの性能試験をお手軽にやったので、その知見を記録しておきます。
- なぜやったのか
- なぜ「モック」を選んだのか
- モックサーバ WireMock 〜 Infrastructure as Code 風、 curl サイドカーを添えて〜
- 負荷テストツール Locust
- いざ性能試験
- そして本番環境へ、さらに 1 年経ちました
- まとめ
- 参考
この記事は、 エムスリー Advent Calendar 2021 3 日目の記事です。
なぜやったのか
クラウド電子カルテ「エムスリーデジカル」は、データベースではなくシステム全体を水平分割する方針でスケーラビリティを確保しました(参考: Infrastructure as "型付き" Code)。
ただし、利用者は水平分割を意識する必要はありません。リバースプロキシを実装し、クライアント証明書に基づいた水平分割先へのルーティングを行っています。
Go で実装していて、実装およそ 500 行、テストコードおよそ 1,000 行の小さいアプリケーションです。
$ cloc --not-match-f '_test\.go$' --quiet . github.com/AlDanial/cloc v 1.88 T=0.03 s (311.5 files/s, 25716.6 lines/s) --------------------------------------------------------------------- Language files blank comment code --------------------------------------------------------------------- Go 7 106 19 544 YAML 1 3 2 57 Dockerfile 1 2 1 9 --------------------------------------------------------------------- SUM: 9 111 22 610 --------------------------------------------------------------------- $ cloc --match-f '_test\.go$' --quiet . github.com/AlDanial/cloc v 1.88 T=0.03 s (199.5 files/s, 46167.6 lines/s) --------------------------------------------------------------------- Language files blank comment code --------------------------------------------------------------------- Go 5 150 48 959 --------------------------------------------------------------------- SUM: 5 150 48 959 ---------------------------------------------------------------------
このリバースプロキシに、サービスのほぼ全トラフィックが入ってきます。
当然、性能が気になります。「どのくらいのトラフィックに耐えられるのか」「どこがボトルネックになるか(どのメトリックを意識するとよいか)」の情報は、本番環境への導入時はもちろん、今後のキャパシティプランニングや監視に有用です。そのため、性能試験をすることとしました。
なぜ「モック」を選んだのか
では、どう性能試験すればよいでしょうか。ざっと考えつくのは次の方法です。
- 本番そっくりの構成を用意し、トラフィックパターンを再現する
- シンプルに GET リクエストを投げ続ける
- モックサーバを用意し、レスポンスタイムなど一部項目のみを再現する
そして… 独断と偏見で、「手軽さ」「信憑性」を評価したのが次の表です。
方法 | 手軽さ | 信憑性 |
---|---|---|
本番そっくり | 低い | 中くらい |
シンプル GET | 高い | 低い |
モックサーバ | 中くらい | 中くらい |
それぞれ具体的に考えてみましょう。
本番そっくり
構成要素が多すぎます。出てきた数値がリバースプロキシの性能なのか、それ以外のボトルネックか、正しく切り分けられるでしょうか。気がつけば、延々と別コンポーネントの性能試験をしていた… なんてことになりかねません。
「リバースプロキシそのもの」の性能を測定するには不向きと判断しました。
シンプル GET
シンプルな GET リクエストなら、「リバースプロキシそのもの」の性能が明確になるでしょうか。
本番環境では、メッセージボディを含んだり、レスポンスに時間のかかるリクエストも当然存在します。メッセージボディはいくらかメモリを消費しますし、レスポンスを待つ間のコネクションはメモリだけでなくファイルディスクリプタなんかも消費します。シンプルな GET リクエストだと、その乖離が大きく出てしまいそうです。よって信憑性が低いと判断しました。
モックサーバ
前述の 2 案の欠点を緩和するのがモックサーバを用意する案です。
- スケールが容易なモックサーバを用意し、リバースプロキシ以外のボトルネックを減らす
- レスポンスタイムやデータサイズを本番環境に近づけて、信憑性を高める
極端に言えばリバースプロキシはリクエストとレスポンスにしか興味がありませんから、そこだけ再現度を高める作戦です。
検討の末、このモックサーバを用意する案を採用しました。
モックサーバ WireMock 〜 Infrastructure as Code 風、 curl サイドカーを添えて〜
モックサーバには WireMock を用います*1。 WireMock には Admin API があり、 HTTP リクエストを投げてスタブを定義できます。
つまり、 curl サイドカーコンテナを添えてちょちょいと HTTP リクエストを投げれば、モックサーバの起動とセットアップが完結します。例えば Amazon ECS なら、タスク定義だけで可能です。
- wiremock/wiremock のコンテナを起動
- サイドカーとして yauritux/busybox-curl のコンテナを起動
- サイドカーは WireMock の起動を待って、 curl コマンドで WireMock の Admin API を叩いてスタブ定義した後、終了する
タスク定義だけで可能ということは、 Infrastructure as Code も容易です。
AWS CDK の ContainerDefinitionProperty[] 型で表現すると以下のコードになります。 CDK に馴染みがない方も「 preparing コンテナが curl コマンド実行してるっぽいな」とざっくりイメージいただければと思います。
[ { name: "wiremock", image: "wiremock/wiremock:latest", essential: true, healthCheck: { startPeriod: 3, retries: 1, interval: 10, timeout: 3, command: ["CMD-SHELL", `curl -f http://127.0.0.1:${containerPort}/__admin/mappings || exit 1`], }, portMappings: [ { containerPort, }, ], linuxParameters: { initProcessEnabled: true, }, ulimits: [ { name: "nofile", softLimit: 32768, hardLimit: 32768 }, { name: "nproc", softLimit: 4096, hardLimit: 4096 }, ], }, { name: "preparing", image: "yauritux/busybox-curl:latest", essential: false, dependsOn: [ { containerName: "wiremock", condition: "HEALTHY", }, ], entryPoint: ["sh", "-c"], command: [ `curl -X POST --data '${JSON.stringify({ request: { url: "/mock/example", method: "ANY", }, response: { status: 200, body: "Response body", }, })}' http://127.0.0.1:${containerPort}/__admin/mappings && ` + `curl -X POST --data '${JSON.stringify({ request: { url: "/mock/delayed/short", method: "ANY", }, response: { status: 200, body: "Short response", // 対数正規分布による遅延を加える // ref: http://wiremock.org/docs/simulating-faults/ delayDistribution: { type: "lognormal", median: 100, sigma: 1.1, }, }, })}' http://127.0.0.1:${containerPort}/__admin/mappings`, ], }, ],
これをロードバランサにぶら下げるだけで、スケーラブルなモックサーバの完成です。
〜ランダム遅延も添えて〜
WireMock は、対数正規分布や一様分布で遅延を挿入できます (Simulating Faults - WireMock) 。
前述のコード例だと /mock/delayed/short
が遅延してレスポンスを返すようになっています。これを なるべく本番環境に近づくよう 設定することでレスポンスタイムを再現できます。
負荷テストツール Locust
負荷テストツールには Locust を使います*2。 Locust には分散実行の仕組みが備わっており (Distributed load generation) 、スケーラブルな負荷生成が容易です(結果も集約してくれます)。
特にひねりはありませんが、強いて言えば以下のことをしています。
- 再現性を確保するため locustfile.py (テストコード)もバージョン管理する
- task decorator で GET/POST/PUT/DELETE メソッドの比率を本番環境に近くする
いざ性能試験
ここまでくれば、やるだけです*3。
実際の試験結果の一部をご紹介します。 WireMock 50 タスク、 Locust 25 タスク、試験対象のリバースプロキシは 1 タスク (2 vCPU) で実施しました。
10 時 09 分頃に注目すると、 3,000 req/s 付近でレスポンスエラー (赤点) やレスポンスタイムの悪化が確認できます。また、 Ingress (リバースプロキシを含む ECS サービス) の CPUUtilization が 97 %を超えており、 CPU がボトルネックになっていることが分かります。
一方、 MockApi (WireMock) や Locust の CPUUtilization は 25 %未満で余裕があります。
リバースプロキシそのものを試験でき、キャパシティプランニングや監視に有用な情報が得られたと言えそうです。
そして本番環境へ、さらに 1 年経ちました
そして… 本番環境に投入して、無事に 1 年が経ちました。幸い、リバースプロキシは不具合や性能問題なく動き続けています。
また、最近はリバースプロキシに分散トレーシングを導入すべく、性能影響を見るために WireMock, Locust を 1 年前と同じ条件で走らせる、といったこともチームで行っています。性能試験を再現させやすい形でやっていてよかった… と思った瞬間です。
うまくいった、と言ってよいのではないでしょうか。めでたしめでたし。
まとめ
- 「リバースプロキシそのもの」の性能を測定すべく、バックエンドのモック化を実施しました
- WireMock を curl サイドカーコンテナと組み合わせ、タスク定義だけでモックサーバをセットアップしました
- 再現性を高めておくと、将来役に立ちます(役に立ちました)
リバースプロキシを作る際にぜひご活用ください。
リバースプロキシは作らないけれど、ルーティングされるリクエストの気持ちになりながらクラウド電子カルテを一緒に作ってみたい、という方は、ぜひこちらからカジュアルにお話ししましょう。
参考
- WireMock - WireMock
- yauritux/busybox-curl - Docker Image | Docker Hub
- Amazon ECS: curl コンテナを使ってタスク定義だけでモックサーバを設定する - にゃみかんてっくろぐ (私の個人ブログです)
- Locust - A modern load testing framework
*1:MockServer と比較し、後述するランダム遅延機能があることを理由に選びました
*2:分散実行が容易である、過去チームで利用実績がある、などの理由で選定しました
*3:ただし、 AWS は一定以上の負荷テストは事前の申請が必要です(テストポリシー - Amazon EC2 | AWS)