Istio enabled Kubernetesで高負荷テスト

# Istio enabled Kubernetesで高負荷テスト

Kubernetesクラスターで[Istio](https://istio.io/)をインストールしてトラフィックをコントロールする用例が増えていると思います。Istioを使いこなせば、トラフィックシフティングによるカナリアリリースや、ミラーリングによるシャドウABテスト、サーキットブレーカーによる障害回避等々、便利なことが簡単に実現できます。
IstioはKubernetesのpodにistio sidecarというenvoyをインジェクトし、サイドカーコンテナとして稼働させることでpodへの通信を制御します。istio sidecarは追加可否をpodごとに設定できますが、istio sidecarを追加した場合は基本的にプロキシとして全ての通信を仲介します。
これは言い方を変えると、istio sidecarがパフォーマンスを出せない場合、通信上のボトルネックになりうるということです。
今回はIstioの有効化されたKubernetesクラスターで、istio sidecar有り無しによるネットワークパフォーマンスの差異と、istio sidecarがボトルネックになった場合の対処法を説明します。

Istio: https://istio.io/
vegeta attack: https://github.com/tsenart/vegeta

### なぜ負荷テスト？

ちょっと仕事で（というか9割方以上趣味で）Istioの高アクセス負荷テストをやることになって、そのボトルネック調査をしたのがきっかけです。
負荷テストは楽しい。

今回は使ったコードはこちらです。
https://github.com/shibuiwilliam/istio-load-test

### 負荷テストの条件

負荷テストはGolangで書いた簡単なHTTPサーバ（goapi）へのGETリクエストで実施します。
負荷テストクライアントには[vegeta attack](https://github.com/tsenart/vegeta)というREST用負荷テストツールを使います。vegeta attackでは某サイヤ人の王子が得意とする連続エネルギー波のように、RESTリクエストを連続して送信します。
そのレイテンシーやタイムアウト、失敗を計測し、goapiの負荷耐性を検証します。

goapiは以下のとおり乱数を足し算して返すだけのとてもシンプルな内容です。Kubernetesクラスターで充分なリソースを設定していれば、10,000リクエスト／秒の高負荷環境でも、1リクエスト10ms以内で返せるはずの実装です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55384/716bb18a-67a8-851b-6eb3-f7911508c336.png)


```golang
package main

import (
	"flag"
	"fmt"
	"math/rand"
	"net/http"
	"strconv"
)

var (
	num int
)

func add() string {
	m := rand.Intn(num)
	n := rand.Intn(num)
	s := strconv.Itoa(m + n)
	fmt.Printf("%v\n", s)
	return s
}

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, add())
}

func main() {
	flag.IntVar(&num, "num", 1000000, "num")
	flag.Parse()
	http.HandleFunc("/add", handler)
	http.ListenAndServe(":8080", nil)
}
```

負荷テストでは5分間、一律で10,000リクエスト／秒の負荷を与えます。タイムアウトは10msとします。
goapiはdeploymentで配備し、5podとします。リソースは一律、CPUリクエストは1000m、メモリリクエストは300Miとします。
vegeta attackは単一podで配備し、リソースはCPUリクエスト500m、メモリリクエスト300Miとします。

ただし、負荷テストの条件によってはそれぞれのリソースリクエストを調整します。


### 負荷テストクライアント、サーバ両方ともistio sidecarなし

比較対象としてistio sidecarなしで負荷テストです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55384/8c269343-6b7e-d74d-8c6f-a2eeca6de5eb.png)


```sh
root@vegeta:/opt# vegeta attack -duration=300s -rate=10000 -timeout=10ms -targets=target | vegeta report -type='hist[0,2ms,4ms,6ms]'
Bucket         #        %       Histogram
[0s,    2ms]   2988892  99.63%  ##########################################################################
[2ms,   4ms]   9126     0.30%
[4ms,   6ms]   1170     0.04%
[6ms,   +Inf]  812      0.03%
```

良い感じに99.99%以上がタイムアウトせず、10ms以内にレスポンスされています。


### 負荷テストクライアント、サーバ両方ともistio sidecarあり、サーバのみvirtualservice、destinationruleあり

再度比較対象として、負荷テストクライアント、サーバ両方にistio sidecarを追加しています。
サーバにはvirtualserviceとdestinationruleを入れています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55384/31778b15-b466-ca14-ede5-3f50d8846220.png)


```sh
root@vegeta:/opt# vegeta attack -duration=300s -rate=10000 -timeout=10ms -targets=target | vegeta report -type='hist[0,2ms,4ms,6ms]'
Bucket         #        %        Histogram
[0s,    2ms]   0        0.00%
[2ms,   4ms]   10       0.00%
[4ms,   6ms]   28       0.00%
[6ms,   +Inf]  1344391  100.00%  ##########################################################################
```

見事にほとんどがタイムアウトしています。
どこにボトルネックがあるのでしょう・・・？


### 負荷テストクライアント、サーバ両方ともistio sidecarあり、virtualservice、destinationruleなし

ボトルネックを見つけるため、一つずつ機能を排除していきます。
まずはvirtualservice、destinationruleを排除します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55384/3dbb6daf-aff7-6dbb-1d84-03283db16f3c.png)

```sh
root@vegeta:/opt# vegeta attack -duration=300s -rate=10000 -timeout=10ms -targets=target | vegeta report -type='hist[0,2ms,4ms,6ms]'
Bucket         #        %       Histogram
[0s,    2ms]   4        0.00%
[2ms,   4ms]   67       0.00%
[4ms,   6ms]   130      0.01%
[6ms,   +Inf]  1353854  99.99%  ##########################################################################
```

またほとんどがタイムアウトです。
virtualservice、destinationruleが原因ではなさそうですね。

### 負荷テストクライアントのistio sidecarあり、サーバのistio sidecarなし

負荷テストではボトルネックはサーバだけでなく、負荷テストクライアント側に存在することもあります。
ボトルネックを突き止めるためには両者に機能を付け足しながら調査していくことになります。
今回は負荷テストクライアントのみistio sidecarを追加し、サーバからはistio sidecarを削除しました。
これで、最初のistio sidecarを使わない比較対象と比べて、負荷テストクライアントのみistio sidecarがくっついている状態になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55384/8aa23f50-e97d-407d-8074-446d203d68aa.png)


```sh
root@vegeta:/opt# vegeta attack -duration=300s -rate=10000 -timeout=10ms -targets=target | vegeta report -type='hist[0,2ms,4ms,6ms]'
Bucket         #        %       Histogram
[0s,    2ms]   7        0.00%
[2ms,   4ms]   31       0.00%
[4ms,   6ms]   39       0.00%
[6ms,   +Inf]  1359632  99.99%  ##########################################################################
```

結果、またほとんどがタイムアウトでした。
これで負荷テストクライアントのistio sidecarが遅い要因と言えるかと思います。
負荷テストクライアントのistio sidecarが確実にボトルネックと言えることを確認するため、負荷テストクライアントからistio sidecarを外し、逆にサーバにistio sidecarを追加します。

### 負荷テストクライアントのistio sidecarなし、サーバのistio sidecarあり、virtualservice、destinationruleなし

サーバのみistio sidecarが追加された構成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55384/505445eb-e5e8-0602-f7d6-71d47387f4b8.png)


```sh
root@vegeta:/opt# vegeta attack -duration=300s -rate=10000 -timeout=10ms -targets=target | vegeta report -type='hist[0,2ms,4ms,6ms]'

Bucket         #        %       Histogram
[0s,    2ms]   2964541  98.82%  ##########################################################################
[2ms,   4ms]   31232    1.04%
[4ms,   6ms]   2266     0.08%
[6ms,   +Inf]  1961     0.07%
```

99.9%以上が成功し、ほとんどタイムアウトしていません。
やはり負荷テストクライアントのistio sidecarがボトルネックになっているようです。

---

ボトルネックは排除して、Istioを使わないと判断すれば負荷テストは終了ですが、そうすると便利なトラフィックマネジメントが使えなくなります。
どうにかしてistio sidecarのパフォーマンスを上げて、Istioライフを満喫したいです。
そこで、istio sidecarのリソースをチューニングします。

### 負荷テストクライアントのistio sidecarあり、サーバのistio sidecarあり。コンテナ、サイドカーのCPUを増強

Istio sidecarはKubernetes podのコンテナなので、CPU、メモリのリクエストを設定することができます。
Istio sidecarのリソースリクエストはpodのアノテーションで設定します。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: vegeta
  namespace: go
  annotations:
    sidecar.istio.io/inject: "true"
    sidecar.istio.io/proxyCPU: "5000m"
    sidecar.istio.io/proxyMemory: "1Gi"
spec:
  containers:
    - name: vegeta
      image: shibui/istio-load-test:vegeta_0.0.0
      imagePullPolicy: Always
      command:
        - tail
        - -f
        - /dev/null
      resources:
        requests:
          cpu: 5000m
          memory: "1Gi"
```

アノテーションで設定可能なパラメータは他にも多々あります。以下をご参照ください。
https://istio.io/latest/docs/reference/config/annotations/

今回はひとまずCPU 5000m、メモリ 1Giを設定しました。充分であろうリソースを設定して、CPU、メモリがボトルネックの原因なのか判定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55384/75e52e8e-3ee0-449c-46e1-84cca6fed22e.png)


```sh
root@vegeta:/opt# vegeta attack -duration=300s -rate=10000 -timeout=10ms -targets=target | vegeta report -type='hist[0,2ms,4ms,6ms]'
Bucket         #        %        Histogram
[0s,    2ms]   0        0.00%
[2ms,   4ms]   12       0.00%
[4ms,   6ms]   28       0.00%
[6ms,   +Inf]  1298137  100.00%  ##########################################################################
```

おっと、まだタイムアウトします。どうもリソースリクエストがボトルネックではないようです。
以下は負荷テスト中のクライアントのCPU、メモリの使用率です。どうもvegetaのCPU使用はオーバーコミット気味なのに、istio sidecarのCPUを有効に使い切っていないようです。

```sh
$ kubectl -n go top pod vegeta --containers
POD      NAME          CPU(cores)   MEMORY(bytes)
vegeta   vegeta        15407m       1078Mi
vegeta   istio-proxy   10m          90Mi
```

続いてサーバのCPU、メモリ使用率です。

```sh
POD                             NAME           CPU(cores)   MEMORY(bytes)
goapi-medium-766b4c66c6-8pzxc   goapi-medium   0m           5Mi
goapi-medium-766b4c66c6-8pzxc   istio-proxy    2m           51Mi
```

こちらも対して負荷がかかっているように見えません。リクエストが飛んできていないのでしょうか？
リソースをもっと有効活用できれば、もしかしたらもっとパフォーマンスが出るかもしれません。

### 負荷テストクライアントのistio sidecarあり、サーバのistio sidecarあり。コンテナ、サイドカーのCPUを増強、サイドカーのconcurrencyを増やす

Istio sidecarにはconcurrencyというパラメータがあります。これは文字通り並列処理数で、ワーカースレッド数になるようです。
https://istio.io/latest/docs/reference/config/istio.mesh.v1alpha1/#ProxyConfig

この値を調整してみましょう。podではconcurrencyを`proxy.istio.io/config: "{'concurrency':'16'}"` というアノテーションで設定することができます。

```yaml
apiVersion: v1
kind: Pod
metadata:
  name: vegeta
  namespace: go
  annotations:
    sidecar.istio.io/inject: "true"
    sidecar.istio.io/proxyCPU: "5000m"
    sidecar.istio.io/proxyMemory: "1Gi"
    proxy.istio.io/config: "{'concurrency':'16'}"
spec:
  containers:
    - name: vegeta
      image: shibui/istio-load-test:vegeta_0.0.0
      imagePullPolicy: Always
      command:
        - tail
        - -f
        - /dev/null
      resources:
        requests:
          cpu: 5000m
          memory: "1Gi"
```

これで負荷テスト！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/55384/5c5a8325-1a1c-22ae-f008-18f2fc501f43.png)


```sh
root@vegeta:/opt# vegeta attack -duration=300s -rate=10000 -timeout=10ms -targets=target | vegeta report -type='hist[0,2ms,4ms,6ms]'
Bucket         #        %       Histogram
[0s,    2ms]   2912284  97.08%  ########################################################################
[2ms,   4ms]   85348    2.84%   ##
[4ms,   6ms]   1501     0.05%
[6ms,   +Inf]  867      0.03%
```

やっとパフォーマンスが出ました。総じて99.96%程度のリクエストをタイムアウトせずにレスポンスできています。
めでたしめでたし。

## まとめ

というわけで、Istioを使ったKubernetesクラスターで負荷テストをやった結果でした。
今回はサーバではなく負荷テストクライアント側にボトルネックがあって、その調査をしました。
楽しかったです。