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

## はじめに

[前回](https://qiita.com/raiich/items/baa680c7ca535a786cca)はシンプルな構成でサーキットブレーカーの効用を確認しましたが、その構成では下記のわかりにくさがありました。

- 「障害の連鎖を防げる」とはどういうことかわからない
- よくある「スロットリング（フロント側で `503` などのステータスコードを返してサービス全体を防御）」との違いがわからない

そこで今回は少し複雑な構成でサーキットブレーカーの効用を検証してみました。

## 構成

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

- 下記のサービスを用意
  - サービス1: Busy Loopするバックエンドを利用するサービス
    - Busy LoopサービスにEnvoy経由でアクセスするものと、直接アクセスするものの2パターン用意
  - サービス2: シンプルなレスポンスを返すバックエンド（Light Backend）を利用するサービス
- 負荷をかけるGatlingアプリがフロントサービス役のOpenRestyコンテナにアクセス、サービス1とサービス2を呼び出す

## テストシナリオ

下記「負荷A」と「負荷B」を並列に実行しました。

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

- 負荷B: 
  1. サービス2に一定の負荷でアクセス（400 QPS）

## 結果

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

サービス1:
<img width="906" alt="no-envoy-knockouted-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/148979/ef815e81-0444-8be0-0d29-f101a70b9810.png">

サービス2:
<img width="905" alt="no-envoy-knockouted-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/148979/908aff5f-dae6-f31d-fc3c-5a8fb20440ea.png">

- サービス1へのアクセスで負荷がかかりすぎたタイミングで、サービス2の呼び出しもノックアウトされてしまっている
    - → 障害が連鎖してしまった（フロント側に波及してしまった）
- 負荷が下がっても、もとのようにレスポンスを返せないでいる（前回と同様）


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

サービス1:
<img width="904" alt="envoy-1.png" src="https://qiita-image-store.s3.amazonaws.com/0/148979/b639bca2-45cb-641c-c8c9-bc4f0f7f7024.png">

サービス2:
<img width="904" alt="envoy-2.png" src="https://qiita-image-store.s3.amazonaws.com/0/148979/7bf4586c-0c92-a320-e10c-a988d51975f6.png">

サービス1へのアクセスで負荷がかかりすぎても、サーキットブレーカーオープンにより障害を限定できている：

- サービス1へのアクセスに対しては処理可能な範囲でレスポンスを返せている
- サービス2は変わらないQPSでレスポンスを返せている

サーキットブレーカーによって、無理なアクセスの削減＝負荷が高いサービスへのアクセスで開放できないリソースの早期削減をすることでサービス全体が落ちるのを防げています。

単純なスロットリングではサービス全体でのリクエスト量を制限することでサービス2へのアクセスも減ると思いますが、今回の構成ではバックエンドの呼び出し側箇所のサーキットブレーカーがサービス1のみ流量を抑えてサービス2に影響しないようにできました。

## 詳細・補足

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

#### サービス1

| Error | Count | Percentage |
| ----- | ----- | ---------- |
| j.u.c.TimeoutException: Request timeout to not-connected after 1000 ms | 6727 | 66.328 % |
| j.u.c.TimeoutException: Request timeout to localhost/127.0.0.1:8080 after 1000 ms | 2796 | 27.569 % |
| j.i.IOException: Connection reset by peer | 570 | 5.62 % |
| o.a.e.RemotelyClosedException: Remotely closed | 49 | 0.483 % |

#### サービス2

| Error | Count | Percentage |
| ----- | ----- | ---------- |
| j.u.c.TimeoutException: Request timeout to not-connected after 1000 ms | 6402 | 75.97 % |
| j.u.c.TimeoutException: Request timeout to localhost/127.0.0.1:8080 after 1000 ms | 1470 | 17.444 % |
| j.i.IOException: Connection reset by peer | 535 | 6.349 % |
| o.a.e.RemotelyClosedException: Remotely closed | 19 | 0.225 % |
| j.n.ConnectException: Connection reset by peer: localhost/127.0.0.1:8080 | 1 | 0.012 % |

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

#### サービス1

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

#### サービス2

エラーなし

### Example Code

- https://github.com/raiich/envoy-circuit-breaking-example/tree/compound
    - compoundブランチ
- 測定環境
  - MacBook Pro (15-inch, 2017), 2.8 GHz Intel Core i7, 16 GB 2133 MHz LPDDR3