Kubernetesの負荷試験で絶対に担保したい13のチェックリスト

## 概要
※この記事は弊ブログ記事(はてな)、[Kubernetesの負荷試験で絶対に担保したい13のチェックリスト](https://taisho6339.hatenablog.com/entry/2020/09/12/225840)と同内容です

ここ最近、Kubernetesクラスタを本番運用するにあたって負荷試験を行ってきました。

Kubernetesクラスタに乗せるアプリケーションの負荷試験は、通常の負荷試験でよく用いられる観点に加えて、クラスタ特有の観点も確認していく必要があります。

適切にクラスタやPodが設定されていない場合、意図しないダウンタイムが発生したり、想定する性能を出すことができません。

そこで私が設計した観点を、汎用的に様々なPJでも応用できるよう整理しました。
一定の負荷、スパイク的な負荷をかけつつ、主に下記の観点を重点的に記載します。

- Podの性能
- Podのスケーラビリティ
- クラスタのスケーラビリティ
- システムとしての可用性

本記事ではこれらの観点のチェックリスト的に使えるものとしてまとめてみます。

![kubernetes](https://www.linuxfoundation.jp/wp-content/uploads/2019/03/trademark-kubernetes-icon-correct.png)






## 確認観点
- 攻撃ツール
  - 1: ボトルネックになりえないこと

- Podレベル
  - 2: 想定レイテンシでレスポンスを返せること
  - 3: 想定スループットを満たせること
  - 4: 突然のスパイクに対応できること
  - 5: ノードレベルの障害、ダウンを想定した設定になっていること
  - 6: 配置が想定どおりに行われていること
  - 7: 新バージョンリリースがダウンタイム無しで可能なこと
  - 8: 長時間運転で問題が起こり得ないこと

- クラスタレベル
  - 9:  Podの集約度が適切であること
  - 10: 配置するPodの特性に合わせたノードになっていること
  - 11: 突然のスパイクに対応できること
  - 12: クラスタの自動アップグレードの設定が適切であること
  - 13: Preemptibleノードの運用が可能であるか

## 攻撃ツールの観点

### 1: 攻撃ツールがボトルネックになりえないこと

**攻撃ツール(locust, JMeter, Gatlingなど)を使って、担保したいRPSの負荷を攻撃対象に対してかけることができるか**

#### 解説

意外と失念しがちですが、攻撃ツール自体が想定する負荷をかけられるとは限りません。

たとえば一つのマシンで大きな負荷をかけようとすればファイルディスクリプタやポート、CPUのコアなどが容易に枯渇します。

大きなRPSを扱う場合は、JMeterやlocustなど柔軟にスケールして、分散して負荷をかけることのできる環境を用意しましょう。

#### 検証方法
私のPJではlocustのk8sクラスタを立て、同じクラスタ内にNginxのPodを立て、静的ページに対してリクエストさせてどのくらいのスループットが出るかを検証しました。

要件的には6000RPSほど担保すれば良いシステムだったので、workerの数やユーザの数を調整して余裕をもって8000RPSくらいまでは出せることを確認しました。

## Podの観点

### 2: 想定レイテンシでレスポンスを返せること

**Pod単体で想定するレイテンシでレスポンスを返すことができるか**

#### 解説

HPAを無効にしてPod単体の性能が要件を満たすかをまず確認します。

想定レイテンシを超過してしまう場合、Podのresource requestとlimitを積んでいきましょう。

また、もしアプリケーションに問題があってレイテンシが超過する場合はアプリケーションのチューニングが必要です。


### 3: 想定スループットを満たせること

**Podレベルで見たときに想定するスループットを出すことができるか**

#### 解説

HPAを無効にしてPodを手動でスケールアウトしていき、どこまでスループットを伸ばせるかを確認します。

スケールアウトしてもスループットが伸びない場合、どこかにボトルネックが出ている可能性が高いです。

その場合まず私は下記を確認します。

- 各PodのCPU使用率
  - 特定のPodだけCPU使用率が偏ってる場合はルーティングポリシーの再確認

- 各Podのレイテンシ
  - 一つ前の「想定レイテンシでレスポンスを返せること」に戻って確認

- 攻撃側のCPU使用率
  - 攻撃ツールのスケールアップ、スケールアウト

### 4: 突然のスパイクに対応できること

**突然急激に負荷が高まったときに対応することができるか**

#### 解説

HPAを設定しておけばオートスケーリングしてくれますが、ポリシーを適切に設定する必要があります。

HPAは一定期間でPodの数をアルゴリズムに応じて算出し、定期的に調整することで実現されています。

![HPA Image](https://d33wubrfki0l68.cloudfront.net/4fe1ef7265a93f5f564bd3fbb0269ebd10b73b4e/1775d/images/docs/horizontal-pod-autoscaler.svg)


[HPAのスケールアルゴリズム](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#algorithm-details)

よって、スケールする条件がギリギリに設定されていたりすると突然負荷が高まっても急にスケールできずに最悪ダウンタイムを挟んでしまったりします。

また、k8sの1.18からはスパイクで急激にPodが増えたり減ったりしすぎないように、behaviorという設定項目も追加されています。

私の場合はlocustで急激な負荷を再現し、下記の観点をチェックしました。

- 監視するメトリクスは適切か？
- 監視するメトリクスのしきい値は適切か？
- Podの増減制御が必要そうか？

### 5: ノードレベルの障害、ダウンを想定した設定になっていること

**ノードが柔軟にダウンしてもServiceに紐づくPodレベルで正常にレスポンスを返し続けることができるか**

#### 解説

k8sは、クラスタのオートアップグレードなどでノードが柔軟にダウンしたり、
クラスタのオートスケールでノードがスケールインするので、かなりの頻度でPodが削除されて再作成されることを考慮する必要があります。

そこで注意することとして２つの観点があります。

１つ目はPodのライフサイクルです。

Podが削除されるとき、まずServiceからルーティングされないようにすることと、コンテナへのSIGTERMが同時に発生するため、ルーティングが止まる前にコンテナが終了しないようにする必要があります。

具体的にはlifecycle hookを使い、preStopでsleepしてルーティングが止まるまで待ちましょう。
[Kubernetes: 詳解 Pods の終了](https://qiita.com/superbrothers/items/3ac78daba3560ea406b2)

```
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 30"]
```


また、NEGなどを活用してContainer Nativeなロードバランシングを行っている場合、下記のような配慮も必要です。

[【Kubernetes】GKEのContainer Native LoadbalancingのPodのTerminationの注意点](https://taisho6339.hatenablog.com/entry/2020/09/10/185238)

2つ目はPod Distruption Budgetです。

これを適切に設定しておくことで、ノードのアップデートなどでPodが排出される際に一気に排出されないよう制御することができます。
後述のノードのSurge Updateと合わせて確認するといいでしょう。

```
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: sample-service
spec:
  maxUnavailable: "25%"
  selector:
    matchLabels:
      app: sample-service
      namespace: default
      project: default

```

[クラスタのサージアップグレード](https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-upgrades?hl=ja#surge)

### 6: Podの配置が想定どおりに行われていること

**Podが想定するノードに配置されているか、想定どおりに分散されているか**

#### 解説

Podの配置は何も考えないと空いているリソースからkube-schedulerが任意に選択してしまうため、下記のような観点の考慮が不可欠です。

- 特定のゾーン、ノードに集中して置かれてしまっている場合、いくらPodを冗長化したとしても、ノードのダウンや、ゾーン障害で一斉にサービスが止まる

- Preemptibleノードなどを使っている場合は一斉に複数ノードが落ちることがある
  - (通常ノードとPreemptibleノードは併用するのが定石になっています)

- Container Native Loadbalancingを使用する場合、LBはPodに平等にルーティングするわけではなく、NEGに対して均等にルーティングしようとするため、NEG(つまりはゾーン)でPod数の偏りがあると安定性やスケーラビリティ、スループットに悪影響を与える

- ディスクIO処理が多いPodなどはディスクタイプがssdのノードの配置するなど、Podの特性に応じたノード選択が必要かどうか

Podは、下記を活用することで配置制御を行うことができるので、これらを駆使して配置制御を行いましょう。

- TaintとToleration
- Pod Affinity
- Node Selector
- Node Affinity
- Topology Spread Constraints

![Affinity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/45875/acaa6d1e-2e64-2da3-fb51-e1a980f0601b.png)


### 7: 新バージョンリリースがダウンタイム無しで可能なこと

**PodのDeploy戦略が正しく機能しているか**

#### 解説

これはチームのデプロイ運用方針でも変わりますが、一定の負荷をかけつつ、ダウンタイム無しでPodのバージョンを切り替えられるか検証しておくと良いと思います。

### 8: 長時間運転で問題が起こり得ないこと

**長時間運用することによって顕在化する問題を含んでいないか**

#### 解説

アプリケーションの実装がイケてない場合、メモリリークや、ファイルディスクリプタの枯渇などがよく発生します。

1日以上負荷をかけ続けたときに消費が増加し続けるようなリソースがないか、ノードのスケールイン、スケールアウト、GKEならメンテナンスウィンドウの時間でも問題なく稼働し続けられているかは検証しておくと良いでしょう。

## クラスタ観点

### 9: Podの集約度が適切であること

**Podが効率的かつ安全にノードのリソースを活用できているか**

#### 解説

Podは、PodのRequestされたリソースと、ノード内の割当可能なリソースを加味してスケジューリングされます。

つまりRequestが適切に設定されていないとリソースが全然余っているのにどんどんノードが増えてしまったり、逆にスケールしてほしいのに全然スケールしてくれない、といったことが起こりえます。

GCPなどのダッシュボードや、kubectl topコマンドを用いてノードのリソースを有効に活用できているかをチェックしておきましょう。

### 10: 配置するPodの特性に合わせたノードになっていること

**IOアクセスが頻繁なPodなど、特性に応じたノードが選択されているか**

#### 解説

特定のPodはSSDを搭載したNodeに配置されてほしいなど、Podに応じた要件がある場合、
Node Selector、Node Affinityを利用して適切に配置されているかを確認しましょう。

### 11: クラスタの自動アップグレードの設定が適切であること

**クラスタの自動アップグレード設定が意図した通りになっていること**

#### 解説

例えばGKEを利用している場合、メンテナンスウィンドウとしてメンテナンス可能な時間を設定してあげることで、アクセスが少ない時間にアップグレードを行うなどの制御が可能です。
また、一気にノードが再起動しないようサージアップグレードを積極的に活用していきましょう。

[クラスタのサージアップグレード](https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-upgrades?hl=ja#surge)

### 12: 突然のスパイクに対応できること

**クラスタがPodのスケールに追従し、スパイクに対応することができるか**

#### 解説

これは前述の集約度の話と共通していますが、PodのRequestによってどのようにしてノードがスケールするか、という点が決まります。

基本的に、GKEではスケジュールするためのノードが足りなくなって初めてスケールアウトします。
つまり、突然スパイクしてPodがスケールアウトしようとしたものの、配置できるノードが足りないため、まずノードがスケールアウトしてからPodのスケジューリングがされるケースが発生します。
このような場合にも突然のスパイクに耐えうるか、というのは検証しておく必要があります。

運用したいシステムの要件次第ですが、柔軟にスケールしたい場合はPodのHPAの設定をゆるくしたりなど工夫が必要になります。

[クラスタ オートスケーラー](https://cloud.google.com/kubernetes-engine/docs/concepts/cluster-autoscaler?hl=ja)

### 13: Preemptibleノードの運用が適切であるか

**Preemptibleノードへの過度なPodの集中など、意図しないリソースの使われ方をしていないか**

#### 解説

基本的に本番クラスタでPreemptibleノードOnlyで運用するのは危険です。
Preemptibleノードを運用する場合は、通常のノードと一緒に運用し、
かつTaintとTolerationを適切に設定して、Preemptibleノードによりすぎないようにしましょう。

## まとめ

今回は私が負荷試験によって担保した、

- スケーラビリティ
- 可用性、安定性
- レイテンシとスループット
- リソース利用効率

の観点を整理しました。
もしご意見、感想あればぜひコメントなどいただけると嬉しいです！