Goでのベンチマークツールを作ろうとした話

この記事は [Applibot Advent Calendar 2021](https://qiita.com/advent-calendar/2021/applibot) 13日目の記事です。
前日は@dears31さんの「[Zoomコメントから自由回答のクイズを集計してみた](https://qiita.com/dears31/items/62685ae0369276aa38fe)」という記事でした。


##  はじめに

はじめまして。アプリボットでサーバサイドエンジニアをしているものです。
最近普段の業務で、サービスの性能を計測して、結果まとめ技術選定の材料にするといったことが少しずつですが、増えてきました。
最近で言うと、Elasticache Redisの性能を計測することがありました。

最近あったAurora3の登場や、TiDBやSpannerなどの分散DBの採用事例の増加、KeyDBがマネージドサービスとして提供が開始されるようになるなど、今後も様々なミドルウェアの技術選定、メジャーバージョンの更新などで既存の技術を大きく入れ替える機会が増えるのではないかと思っています。
新しい技術を導入する際は、費用面、運用面、性能面など様々な面から導入検討をしたいと常々思っています。

性能面を見るためには、ドキュメントを読むだけではなく、ベンチマークツールを使用し実際に計測することが多いかと思います。
ベンチマークツールは様々あり、Redisだと、公式ツールの[redis-benchmark](https://github.com/redis/redis-doc/blob/master/topics/benchmarks.md)や、memcachedにも対応している[memtier_benchmark](https://github.com/RedisLabs/memtier_benchmark)があったり、HTTP系ベンチマークツールだと、[apache benchmark](https://httpd.apache.org/docs/2.4/programs/ab.html)が有名かと思います。
ツールによってインターフェースが違って、結果をまとめる際に大変になったりや、そもそも適切なツールが存在しないなんてこともあるかなと思ってます。
また、ドキュメントだけだと掴めきれない細かい挙動を確認することも、実装されてる言語によって解読が難しかったり、シナリオをカスタマイズしたいときもあったり、、、

これらの要望を叶えることができうように、ベンチマークツールを普段から慣れ親しんでいるGoで開発してみて、その時にあった話を記事にまとめたいと思います。

※これらの要望を全て叶えたツールを開発したわけではなく、開発し始めただけになります。

## 仕様

- 性能を見るために必要なクライアント側の指標
    - Latency(avg, min, max, %tile)
    - RPS
- シナリオのカスタマイズ性
- 最終レポートを出力出来る機能

今回は、上記を実装してみます。
また、Redisに対してベンチマークを実行できるものを実装します。

実際に検証する際は、同時にサービス側のCPU使用率やレイテンシなども監視したり、などもありますが、それを実装しようとするとかなり沼りそうなため、本ツールでは実装しないものとします。

## 実装

成果物は[github](https://github.com/naoto67/instabench)にあげています。
ソースコードは整理されてなく、時間がある際に整理しようかとおもっているので、今回はご容赦ください。

開発したものは、下記のインターフェースが重要になっています。

```go

type Executor interface {
	Setup(context.Context) (context.Context, error)
	ExecContext(context.Context) (context.Context, error)
}

type Preparer interface {
	PrepareContext(context.Context) (context.Context, error)
}

```

`Executor` は一つのシナリオみたいなものを想定しています。

`Setup` メソッドは前処理になります。
`Setup` はベンチマークを開始する前に、1度実行され、RedisへのGETコマンドのベンチマークの場合は、ここでデータを挿入し、キャッシュヒット率をあげたりすることができるようになっています。
ここで、`Setup` がcontext.Contextを返却しているのは、前処理段階でした情報をcontext上に持たせたいケースを想定して、返却するようにしています。
たとえば、「RedisのGETメソッドのキャッシュヒット率を100%にしたい、ただキーはランダムで生成したい」などといったユースケースがあると、Setupで事前に発行したランダムなキーで、RedisへSETを行い、そのSETしたキーをcontextに詰め、実際にGETする際にcontextからキーを取得し、GETを行う
といった処理を書くと、実現できるようになります。

`ExecContext` メソッドは、実際に計測する処理を実装する部分になります。
Redisで考えると、ここは実際にGETメソッドを発行することになります。
このメソッドがcontextを返す理由は現状ほぼないです。
今後、一連の処理を1つずつ計測すると言った際に、ExecContextの結果を持ったまま、次の処理にいくなどと言った際にひつようになるため、追加しています。
※[YAGNI](https://ja.wikipedia.org/wiki/YAGNI)です。


続いて、`Preparer` ですが、こちらは `ExecContext` で必要な情報を準備する処理を行うinterfaceになります。
準備処理が結果のレイテンシに影響しないように、インターフェースを分けています。
Preparerは実装されてる場合のみ、呼ばれるようになります。

基本的にこのインターフェースを追加していくことで、Redis以外のものだったりが実装していけるようなります。

その他は、特に解説しないので、詳しくはソースコードを参照してください。

## 検証

では、実際に検証していきます。

今回はRedisのベンチマークツールを実装したので、比較として[redis-benchmark](https://github.com/redis/redis-doc/blob/master/topics/benchmarks.md)でも検証していきます。

#### 環境詳細

Redisは、Elasticacheを使用し、Redis6.x系を使用し、Redisクラスタモードは無効にしています。Redisのインスタンスタイプは、r6g.largeを使用しています。
ベンチマークを実行するマシンは、EC2インスタンスを使用し、インスタンスタイプはc6g.4xlargeを使用しています。
どちらも1台ずつ用意しています。

今回のベンチマークでは、az間を跨いだレイテンシを見たいと言うよりも、ElasticacheRedis自体のレイテンシが見たい、また検証ツール自体が有効なものかを検証したいため、azはap-northeast-1aに寄せています。

Redisへのベンチマーク内容

- GETコマンド
- キャッシュヒット率0%
- 同時接続クライアント500

今回考慮してない内容

- キーの長さ
- 実行時間

#### 実測

1. redis-benchmarkでの結果

コマンド

```
$ redis-benchmark -h $HOST -t get -c 100 --threads 5 -n 40000000
```

結果

```

 ...

  throughput summary: 253900.53 requests per second
  latency summary (msec):
          avg       min       p50       p95       p99       max
        0.380     0.088     0.375     0.519     0.591    59.455

```

RPS 25万、ElasticacheRedisすごいですね。。。
Latencyも、平均、p95, p99含めて性能かなりたかい。。。

2.今回のツールでの結果

コマンド

```
cmd bench --threads=500 --clients=500 --host=$HOST --duration=180 --output-json-path=result.json
```

結果

```
...
    {
      "avg(ms)": 1.953908553753247,
      "max(ms)": 9.575562,
      "min(ms)": 0.123043,
      "p50(ms)": 1.8975165033781725,
      "p90(ms)": 2.048262475701852,
      "p95(ms)": 2.1775845942189624,
      "p99(ms)": 3.569607007901278,
      "rps": 222619,
      "start_time": "2021-12-12T15:22:48.817915446Z"
    },
...
```

あれ、、、redis-benchmarkほど性能でない (^_^;)
RPSも3万と大きな差があるのと、Latencyも4~6倍ぐらい差がありますね。。。

※今回実装したツールは、すべての間隔で集計しているわけではなく、1秒ごとにのみ集計しているので、今回記した結果は単一ポイントでの結果になります。

## まとめ

今回は、汎用的に実装できるベンチマークツールを開発してみました。
>あれ、、、redis-benchmarkほど性能でない (^_^;)

この状態で本記事終わること、申し訳ないです。
原因追求まで行う時間と気力がありませんでした。

redis-benchmark実行時のEC2のCPU使用率は、300%前後にたいして、今回作成したツールは、600%前後までに達していました。
redis-benchmarkより、負荷的にも問題があり、また結果も期待通りのものが得られませんでした。
go側のコードでは、goredisを使用していますが、直接TCPクライアントを使用した実装に変更してみたのですが、特に影響なく、どこにボトルネックがあるのやら。。。
わかる方がいたらコメントいただけると助かります！

## 今後

今回開発したツールは、まだかなり精度が低いかつ、開発がしにくいなと感じています。
また、結果をまとめる際に必要な情報もまだ出し切れてなかったりなど、完成度はまだあげられそうです。
今後はリファクタしつつ、必要なときにさっと追加で開発できて、使用できるようにできたらなと思ってます:vulcan_tone4: