社内ISUCON(N-ISUCON)におけるベンチマーカにおいて考慮・実装で工夫したこと

この記事は [NTTコミュニケーションズ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nttcom)の2日目の記事です。本記事は、昨日の @naosuke2dx の[記事](https://qiita.com/naosuke2dx/items/d395cd2a52e1dd5fde62)の続編になります。

## ベンチマーカ実装にあたり考慮・工夫したこと

大前提として考えていたのは、いかに実装コストを下げるか、という点です。社内ISUCONは別業務と並行して実装することになるため、十分に時間がとれない可能性が高いです[^1]。

ベンチマーカの準備には、方針がいくつかあります。

1. ベンチそのもの、シナリオをフルスクラッチで実装する
    - HTTPリクエストを大量に送り、レスポンスを確認する、という本質的に負荷試験ツールに動作が必要になる
    - 実装コスト: 負荷試験は専門領域であり、実装コストは非常に高い
2. https://github.com/isucon に修正を加えて実装をする
    - Webアプリは自前で用意することになるので、どのようなAPIを叩くかといったシナリオは自分で用意する必要がある
    - 実装コスト: シナリオのみなので、[1]に比べ実装コストは大幅に下る
        - isuconのベンチマーカはGolangで実装されているので、Golangが手に馴染んでいる人におすすめ
3. isucon用ではない既存のコマンド・ツールを組み合わせて実装する
    - 既存のコマンド、負荷試験ツールなどを組み合わる
    - 実装コスト: 既存のツールに組み合わせであるため、[1]に比べると実装コストは大幅に低い

今回は[3]の"isucon用ではない既存のコマンド・ツールを組み合わせて実装する"の方針で進めました。理由は、A. Webアプリ実装の効率化のため、B. Aをベンチマーカで使い回すため、C. 別プロジェクトで使える負荷試験ツールを利用してみたかったため、です。

いかで、[A][B][C]について説明します。

### [A] Webアプリ実装の効率化

今回のN-ISUCONのWebアプリでは、社内に利用者の多いPythonの実装を最初に進め、Ruby/Golang/Node.js を後から進めました。

複数言語の実装において課題となるのは、すべての言語実装の動作が同じことをどう担保するか、という点です。GolangやNode.jsではそもそも言語モデルがかなり異なるので、単純に内部の動作を追うだけでは、外部条件として同じ動作しているのか、把握するのが困難です。

そこで、今回は [Postman](https://www.getpostman.com/) を用いています。Postmanは、シナリオをJavaScriptで記述し、指定したエンドポイントに対して試験できます。まず、PostmanでPythonのWebアプリの動作確認をし、続いて他の言語でも同様にPostmanを通すことで各言語のWebアプリの動作の整合をとっていました。

### [B] Aをベンチマーカで使い回すため

[A]で述べたPostmanは実はベンチマーカの処理の一部に利用しています。Postmanには[newman](https://github.com/postmanlabs/newman)というCLI実装が用意されています。このCLI実装が本番で動くベンチマーカの動作に非常に便利なのです。ここで、ちょっとだけ脱線してベンチマーカの動作概略を述べます。

ISUCONベンチマーカは、いくつかのSTEPの組み合わせで動作します。 @catatsuy 氏の[資料:ISUCONのベンチマーカーとGo
](https://gist.github.com/catatsuy/74cd66e9ff69d7da0ff3311e9dcd81fa)を参考にさせていただきつつ、一部改変して概略を述べると以下の4STEPとなります。

1. Initialize: DBのデータを初期化
2. Verify: Webアプリが正しく動作するか検証
3. Stress: Webアプリの動作確認、Webアプリへの負荷印加
4. Post process: スコア算出・保存

[1-4] の組み合わせでベンチマーカは動作しています。

本[B]の理由が当てはまるのが、STEP2です。STEP2では、HTTPレスポンスが期待している内容になっているか（たとえば、HTTP POSTでデータを追加した後のHTTP GETで追加したデータが取得できているか など)を検証します。これは、まさにWebアプリの外部条件のテストと同じなのです。そこで、STEP2ではnewmanシナリオをそのまま動作させています。

ただし、エンドポイントは参加者によって異なります。newmanは実行時のコマンド引数で外部からデータを注入できるため、以下のように参加者ごとのエンドポイントを注入していました。

`$ newman run scenario.json --global-var "url=http://${target_host}"`

### [C] 別プロジェクトで使える負荷試験ツールを利用してみたかったため

ベンチマーカの肝となるのは、前述の4STEPのうち `3. Stress: Webアプリの動作確認、Webアプリへの負荷印加` であると個人的に考えています。このSTEP3は負荷試験動作であり、大量のリクエストを送りつつも、Webアプリが低速・高速のいずれかであっても期待したスコアが出ることを保証する必要があるためです。このあたりの難しさは、様々な記事で言及されております。[^1]

[^1]: [ISUCON8 本選出題記 あるいはISUCONベンチマーカー負荷調整の歴史](https://sfujiwara.hatenablog.com/entry/2018/10/25/084543)、[#isucon を支えた技術: ベンチマークmaster/agentの構造とnode.jsの話](https://tagomoris.hatenablog.com/entry/20110906/1315279065)、[ISUCONのベンチマーカーとGo](https://gist.github.com/catatsuy/74cd66e9ff69d7da0ff3311e9dcd81fa) など

負荷試験は専門領域であり、すでに様々なツール・プロダクトが世の中で生み出されています。いくつか代表的なものをあげると

1. [Gatling](https://gatling.io/)
- [Apache JMeter](http://jmeter.apache.org/)
- [locust](https://locust.io/)
- [k6](https://github.com/loadimpact/k6)

などがあります。これらのツールは実際にプロダクトでも活用できます。そこで、今回の実装を機に手になじませておけるチャンスと考えた、というのが[C]の理由になります。

さて、これらの代表的な負荷試験ツールのうち、今回は[4]のk6を選定しました。理由はベンチマーカに必要な要件を満たしていたため、です。ベンチマーカに必要な要件は次のとおりです。

- 時間が来たら強制的に停止できること
    - スコア算出は指定した時間(たとえば、60秒)のリクエスト・レスポンスで算出している。時間が変化すると、参加者ごとで公平な計測ができなくなる
- レスポンスが正常かどうかテストできること
- 結果を外部出力できること
- Cookieを処理できること
- Multipartなどのファイルアップロードに対応していること

k6では、 `--duration` によって実行時間を制御できます。また、テストシナリオをJavaScriptで記述できます。たとえば、単にステータスコードが200かどうかチェックするだけであれば、以下のようなシンプルなコードで記述可能です。（もちろん、レスポンスのBodyの内容を確認するなど、複雑な処理も可能です）

```javascript
export default function() {
  const res = http.get("https://example.com/");
  check(res, {
    "status was 200": (r) => r.status == 200
  });
}
```

さらに、Cookieやファイルアップロードの対応もしています。

最後に、`--out` オプションで、外部のファイルとして実行中の動作や、シナリオテスト結果などを出力できます。（かなり多くのデータが出力できるので、ご興味あれば[公式ドキュメント](https://docs.k6.io/docs/results-output)を参照ください）

以上のことから、k6で問題ないと判断しました。参考までに実際のN−ISUCONでは、以下のようなコマンドで負荷をかけていました。[^2]

`$ k6 run --out json=/var/tmp/${filename}.json --duration ${BENCH_DURATION_SEC}s --vus ${VUS} k6_scenario.js`

[^2]: vusは仮想的なユーザの数。

なお、リクエスト・レスポンス結果に応じて負荷を増やす、という動作は実装していません。実装時間が足りなかったこと、および軽くWebアプリのチューニングテストした段階で、低速 -> 高速で適切にスコアが伸びたのを確認できたためです。

## まとめ

本記事では、社内ISUCON(N-ISUCON)のベンチマーカ実装について、考慮したこと・実装で工夫したことを述べました。皆様の参考になる点があれば幸いです。

明日は @diesekiefer さんの記事です！お楽しみに！