EKS上のLocustから負荷をかける際のリソースの割り当てやインスタンスタイプの調整

awskubernetes

EKS上にLocustをインストールしたのだが、ユーザーを増やしてもRPSが大して伸びない。リソースを調整してなるべく効率的に負荷をかけられるようにする。

CDKでEKSクラスタの作成からHelm ChartでのLocustのインストールまでを一気に行う - sambaiz-net

前提

実行するシナリオは次のGETリクエストを送るだけのもの。Chartの都合で0.x系のlocustfileになっている。

from locust import HttpLocust, TaskSet, task

class MyTaskSet(TaskSet):
  @task
  def index(self):
    self.client.get("/")

class MyUser(HttpLocust):
  task_set = MyTaskSet
  min_wait = 5
  max_wait = 15

ちなみに負荷をかける対象はECS+Fargateに立ち上げたAPIサーバーで、こちらが問題にならないよう余裕を持って動かしている。 スケールするからといってAPI Gatewayなどに向けるとリクエスト数による多額の課金が発生し得るので注意だ。

ECSでアプリケーションを動かすBoilerplateを作った - sambaiz-net

なお、ファイルディスクリプタの数は元から十分大きかったため特に変更していない。

ファイルディスクリプタの上限を増やす - sambaiz-net

$ kubectl exec tryeksstackclusterchartlocustchart1abdd876-worker-d5c7b85cbq6sh -- /bin/sh -c "ulimit -n"
1048576

ワーカー数とリソースの割り当て

m5.large (2vCPU, メモリ10GiB)の2ノードに5ワーカーを立ち上げ負荷をかけたところ230RPSあたりで頭打ちになってしまった。

初期のRPS

Container Insightsのメトリクスを見るとワーカーPodのCPUの使用率が100%に張り付いていることが分かる。

CloudWatch Container InsightsでEKSのメトリクスを取得する - sambaiz-net

PodのCPU/メモリの使用率

ノードのCPUは40%ほどしかリクエストされておらず同量のlimitsがかかっているので、ワーカーのリクエストCPUを増やすか数を増やせば簡単にRPSを増やせそうだ。

ノードのCPU/メモリのリクエスト割合

まずはリクエストCPUを100mから500mに増やした。2.5倍ではないのは40%の中にはkube-systemやContainer InsightsのDaemonSetが含まれているため。リクエスト率は90%ほどになった。

$ kubectl describe nodes
...
Non-terminated Pods:         (9 in total)
  Namespace                  Name                                                               CPU Requests  CPU Limits  Memory Requests  Memory Limits  AGE
  ---------                  ----                                                               ------------  ----------  ---------------  -------------  ---
  amazon-cloudwatch          cloudwatch-agent-sfz5l                                             200m (10%)    200m (10%)  200Mi (6%)       200Mi (6%)     5m49s
  amazon-cloudwatch          fluentd-cloudwatch-5vnhg                                           100m (5%)     0 (0%)      200Mi (6%)       400Mi (13%)    5m49s
  default                    tryeksstackclusterchartlocustchart1abdd876-master-595c44cczwmcm    100m (5%)     100m (5%)   128Mi (4%)       128Mi (4%)     4m54s
  default                    tryeksstackclusterchartlocustchart1abdd876-worker-fddd54db4d7xr    500m (25%)    500m (25%)  128Mi (4%)       128Mi (4%)     4m54s
  default                    tryeksstackclusterchartlocustchart1abdd876-worker-fddd54dbz2kpj    500m (25%)    500m (25%)  128Mi (4%)       128Mi (4%)     4m54s
  kube-system                aws-node-875rb                                                     10m (0%)      0 (0%)      0 (0%)           0 (0%)         6m39s
  kube-system                coredns-75b44cb5b4-7qpfl                                           100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     4m54s
  kube-system                coredns-75b44cb5b4-gc6mv                                           100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     4m54s
  kube-system                kube-proxy-wj8fk                                                   100m (5%)     0 (0%)      0 (0%)           0 (0%)         6m39s
worker: {
  replicaCount: 5,
  config: {
    configmapName: configmap.metadata.name,
  },
  resources: {
    limits: {
      cpu: '500m',
      memory: '128Mi'
    },
    requests: {
      cpu: '500m',
      memory: '128Mi'
    } 
  }
}

これで負荷をかけたところ1150RPSまで伸び、リソースを増やした分だけ増えたことになる。

リクエストCPU500mでのRPS

次にリクエストCPUは戻してワーカー数を同じく5倍の25にしてみたところ1070RPSほどに留まった。 ワーカー数を27まで増やしたところ先ほどと同程度のRPSが出た。

worker: {
  replicaCount: 25,
  config: {
    configmapName: configmap.metadata.name,
  },
  resources: {
    limits: {
      cpu: '100m',
      memory: '128Mi'
    },
    requests: {
      cpu: '100m',
      memory: '128Mi'
    } 
  }
}

続いてワーカー数を2に減らしリクエストCPUを1250mにしようとしたが、1つがリソース不足で立ち上がらなかったため1100mにした。

worker: {
    replicaCount: 2,
    config: {
      configmapName: configmap.metadata.name,
    },
    resources: {
      limits: {
        cpu: '1100m',
        memory: '128Mi'
      },
      requests: {
        cpu: '1100m',
        memory: '128Mi'
      } 
    }
  }
}

これで1450RPSまで出るようになった。 CPUの使用率が90%までしか上がっていないのはLocustのワーカーが1コアしか使わないため。

CPUを1100mにしたときのPodのCPU/メモリの使用率

以上の結果から1コア(vCPU)を刻んでワーカー数を増やすよりも集中させた方が、多少リソースを埋めきれなくてもパフォーマンスが良いことが分かったので、 デフォルトの100mから1000mに変更しワーカー数もそれに合わせることにする。シナリオによっては併せてメモリを増やす。

インスタンスタイプ

ノードのCPUは十分使われるようになったがメモリはかなり余っている。 メモリが足りなくなるとOOMになってしまうので切り詰めすぎるのも良くないが、少なくとも今回の場合は汎用の m5.large (2 vCPU, 最大 3.1 GHz, 0.096USD/時間) * 2 より、コンピューティング特化のc系インスタンスの方が無駄なくリソースを使えそうだ。なお、その点ではインスタンスではなく使ったリソース量で課金されるFargateという選択肢もあって、 コストはvCPUあたり$0.04/時と、ほとんどメモリを使わないなら良さそうだが、プロセッサがランダムで大抵c5のものより性能が低いので考慮しない。

ということで同サイズのc5.large (2 vCPU, 最大 3.6 GHz, 0.085USD/時間) * 2にしたところ1570RPSと、若干コストが下がりパフォーマンスが上がった。 メモリは割り当てを上げていないがまだ余裕がある。

c5.largeにしたときのPodのCPU/メモリの使用率

次にワーカー数を倍にして、c5.largeで倍の4ノードに増やした場合と、ノード数はそのままサイズをc5.xlargeに上げた場合を比較する。 サイズを上げるとリソースが倍になって料金も倍になるので総vCPUと料金は変わらない。 結果、前者は3130RPS、後者は2990RPSとなった。これだけだとノードを増やした方が良さそうだが、 各ノードで走るDaemonSetの総リソースが少なくなるのと、1vCPUの確保しやすさから同量のリソースならノード数が少ない方が多くのワーカーを配置できる。 そこで後者のノードに目一杯6ワーカー配置すると3430RPSとなり逆転した。

さらに増やすとc5.large16ノードで16ワーカー動かした場合は12300RPSc5.4xlarge2ノードで30ワーカー動かした場合は15200RPSと、その差が大きくなる。 ノード数が少ないとサブネットのIPアドレスやクォータの消費も抑えられるが、ネットワーク帯域幅はサイズを上げても倍になるわけではないので、そこがボトルネックになるとしたら分けた方が良い。

マスターのリソース

ここまでマスターのPodのリソースは変更していないが、ワーカーの数がそれほど多くないこともあってまだ余裕がある。 CPUをデフォルトの100mに戻し、インスタンスタイプをm5.4xlargeに変更して増やしていったところ、900ワーカーで90%近くになった。メモリも限界だ。 デフォルトで100mしか割り当てられていないがしばらく大丈夫そうだ。

900ワーカーでのマスターのCPU/メモリの使用率

まとめ

  • ワーカーのcpuをデフォルトの100mから1000mに上げる
  • インスタンスタイプは帯域幅が問題にならない限り大きくした方が有利
  • マスターはデフォルトのリソースでもしばらく大丈夫

参考

EKS/ECS Fargate のCPUモデルチェック | 外道父の匠