Circuit Breakerの効用をEnvoy + Gatlingで検証してみた

マイクロサービス時代に可用性を高めるにはサービス間に Circuit Breaker をはさむと良いらしいと聞きます。これにより障害の連鎖を防ぐことが重要らしいのですが、具体的にどのように効果があるのかイメージがわかなかったのでEnvoyをつかって検証してみました。

- 検証コードなど：
    - https://github.com/raiich/envoy-circuit-breaking-example

## 構成

![Layout](https://raw.githubusercontent.com/raiich/envoy-circuit-breaking-example/images/layout.png "layout")


MacBook Proのローカル環境でDocker for Macを動かし、その上で下記のコンテナを動作させました。本来はちゃんとしたアプリを作ったほうがいい＆複数のバックエンドも用意したほうが本物っぽいですが、今回は極力簡略化しています。

- フロントサービス役： OpenResty （← nginx + lua）
- Envoy Proxy （サイドカーコンテナ）
- バックエンドサービス： Busy Loopで高負荷な状態をシミュレートしたOpenResty
- ベンチマークアプリ（Gatling/同一のMacBook上で実行）

## テストシナリオ

負荷テストはGatlingアプリで実施しました。負荷のかけ方は下記のような流れです：

1. さばける最大QPS（400 QPS）まで徐々に上げていき、しばらくキープ (0-30秒目)
2. 600 QPS程度まで、さらにQPSを上げていく (30-45秒目)
3. 400QPSに下げる (45-60秒目)

## 結果

### サーキットブレーカーがないときー...... : 

<img width="905" alt="gatling-knockouted-wo-circuit-breaker.png" src="https://qiita-image-store.s3.amazonaws.com/0/148979/448894c3-9496-2d3b-330e-eb92751f1223.png">

- 上記シナリオ2（負荷を上げすぎたタイミング）ですべてのリクエストに対してノックアウト
- 負荷を400QPSに戻しても、ノックアウト状態のまま

例えばECやインターネット広告などのレイテンシーが重要なシステムにおいては、この「一度負荷があがったらその後のすべてのレスポンスでレイテンシーが落ちる」といった状態は致命的ではないでしょうか。

### サーキットブレーカーがあるときー！ :

<img width="906" alt="gatling-with-circut-breaker.png" src="https://qiita-image-store.s3.amazonaws.com/0/148979/9b047ee4-ba7b-af4c-416f-4ae1c2841576.png">

- 一時的な負荷増大に対して、サーキットブレーカーが落ちることでバックエンドに過負荷がかからない
- 負荷が400QPSに戻ったときに、負荷がかかる前と同じようにレスポンスを返せている

## まとめ

### サーキットブレーカーの効果

- サーキットブレーカーがないと...
    - 見積り以上の負荷がいったんかかると、負荷が下がっても正常にレスポンスを返せるようにならない
    - → システムのキャパシティに余裕を持たせなければならず、余計なシステムコストかかる

- サーキットブレーカーがあれば...
    - 突発的に負荷がかかっても、さばける範囲内で処理できる
    - キャパシティの余裕を減らせてシステムコストを減らせる

- Envoyはバックエンドへの接続数/リクエスト数・ペンディング数/リトライ回数にもとづきサーキットオープンするので
    - バックエンドの負荷増減に対して反応がいい
        - → 障害の連鎖も起きにくい、ちゃんと縮退運行になりやすい
        - → 復旧もクイック
    - よくある Circuit Breaker の「タイムアウトが頻発したらサーキットオープン」ではない

### サーキットブレーカーの難しいところ

- パラメータ調整が手間
    - EnvoyのCircuit Breakerではバックエンドへの `最大接続数`, `最大リクエストペンディング数`, `最大リクエスト数 (HTTP/2)`, `リトライ回数` の設定が可能ですが、バックエンドの処理可能なワークロードに応じて調整する必要があり、手間
        - 例： 値を低く設定すると、バックエンドの遊び・無駄があるのにレスポンスを返せない
        - 例： 値を高く設定すると、処理できない量のリクエストがバックエンドに流れて徐々にレスポンス悪化 or ノックアウトにつながる

## 詳細・補足

#### サーキットブレーカーがないとき

| Total | OK | KO | % KO |
|-------|----|----|------|
| 23249 | 14634 | 8615 | 37% |

| Error | Count | Percentage | 
|-------|-------|------------|
| status.find.is(200), but actually found 502 | 5595 | 64.945 % |
| j.u.c.TimeoutException: Request timeout to localhost/127.0.0.1:8080 after 1000 ms | 2909 | 33.767 % |
| j.i.IOException: Connection reset by peer | 100 | 1.161 % |
| o.a.e.RemotelyClosedException: Remotely closed | 11 | 0.128 % |


#### サーキットブレーカーがあるとき

| Total | OK | KO | % KO |
|-------|----|----|------|
| 23249 | 22750 | 499 | 2% |

| Error | Count | Percentage |
|:--|:--|:--|
| status.find.is(200), but actually found 503 | 499 | 100 % |

#### その他

- [検証Exampleコード](https://github.com/raiich/envoy-circuit-breaking-example)
- 測定環境
    - MacBook Pro (15-inch, 2017), 2.8 GHz Intel Core i7, 16 GB 2133 MHz LPDDR3
- Throttlingとの違い: https://ackintosh.github.io/blog/2017/02/18/2017-02-18/
    
> (※ サーキットブレーカーは “リクエストする側” の対策。スロットリングは “リクエストされる側” の対策。という理解 ※)