はじめに
自社サービスの実行基盤としてAWS ECS(on EC2)を利用しています。
これまでは4台固定のECSクラスタでトラフィックを捌いていましたが、ピークタイムの負荷状況が怪しくなってきたため、ECSクラスタへAutoScalingを導入する事にしました。
ひとまずはクラスタのCPU負荷をスケールアウトの基準とする事としたのですが、CloudWatchに表示されるCPU使用率(CPUUtilication)が示す値の意味が良く分からない。
こちらのドキュメントによれば、クラスタのCPU使用率は下記の計算式で算出されているとの事ですが...。
(Total CPU units used by tasks in cluster) x 100
Cluster CPU utilization = --------------------------------------------------------------
(Total CPU units registered by container instances in cluster)
重要なのはタスクのCPUユニット(CPU Units)
の様ですが、これまではその意味を深く考えた事はありませんでした。
(数十種類のタスクを本番稼働させていますが、全てCPUユニット=10で固定しちゃってます)
という事で、CPUユニットの意味とその動作についての検証を行いました。
「CPUユニットっていくつにすれば良いの?」とお悩みの方に参考になれば幸いです。
CPUユニット(CPU Units)とは
CPUユニットはECSタスク(=コンテナ)が使用できるCPU能力を制御するための、ECSタスク定義パラメータです。
{
"name": "sample-task",
"image": "alpine",
"cpu": 1024,
"memory": 256
}
設定した値はdocker run
の--cpu-shares
パラメータとして使用されます。
$ docker inspect 9a7c6f9a5ea0 | grep -i cpushares
"CpuShares": 1024
ECSインスタンスのCPUユニットについて、ドキュメントには以下のような記載があります。
Amazon EC2 Instances 詳細ページのインスタンスタイプに一覧表示されている vCPU 数に 1,024 を乗算して、Amazon EC2 インスタンスタイプごとに使用可能な CPU ユニットの数を判断できます。
これは4コアのインスタンスは4096のCPUユニットを持つ
という事を意味します。
言い換えれば1024のCPUユニットを持つタスクは、1個のCPUコアを専有出来る
という事でもあります。
ローカルマシン上での検証
まずは--cpu-shares
の動作を2コアのローカルマシン上で検証します。
下記の2コンテナを起動するとします。
| タスク | --cpu-shares |
|---|---|---|
|タスク1|512|
|タスク2|2048|
この場合、、2コアのCPU能力を1:4の割合で分け合う
事になるため、各コンテナのCPU使用率は以下の様になるはずです。
タスク | --cpu-shares | CPU使用率 |
---|---|---|
タスク1 | 512 | 40% |
タスク2 | 2048 | 160% |
では本当にそうなるのか、実際に試してみます。
まず、無限ループで100%のCPU負荷を発生させるシェルスクリプトを用意します。
このスクリプトはPROC
環境変数で指定されたコア数分の負荷を発生させます。
#!/bin/sh
PROC="${PROC:-1}"
for i in $(seq 1 $PROC); do
/bin/sh -c 'while true; do :; done' &
done
wait
そうしたら、タスク1(CPU=512, PROC=1)とタスク2(CPU=2048, PROC=2)をそれぞれ起動します。
$ docker run --rm -d --name TASK1 --cpu-shares 512 -e PROC=1 -v $PWD/app.sh:/app.sh alpine /bin/sh /app.sh
$ docker run --rm -d --name TASK2 --cpu-shares 2048 -e PROC=2 -v $PWD/app.sh:/app.sh alpine /bin/sh /app.sh
CPU負荷を確認すると、予想通りの比率となっている事が分かります。
$ docker stats --no-stream --format '{{.Name}} {{.CPUPerc}}' | sort
TASK1 41.20%
TASK2 159.76%
ここでタスク2を停止します。
$ docker kill TASK2
CPU負荷を確認すると、タスク1のCPU使用率が100%(=1コアを専有)に上昇しました。
$ docker stats --no-stream --format '{{.Name}} {{.CPUPerc}}' | sort
TASK1 100.29%
以上の結果から分かるのは、--cpu-shares(=CPUユニット)はそのコンテナが必要とするCPU能力を、他コンテナとの相対的な割合で指定する
パラメータであるという事です。
また、CPU能力の上限を決められる物では無い
という事も分かります。
ECS上での検証
では、ECSのCPUユニットでも同様の事が言えるのか、c5.xlargeのインスタンス1台で構成されたECSクラスタで検証します。
c5.xlargeは4コアのインスタンスなので、クラスタ全体で利用可能なCPUユニットは4096となります。
先ほどのシェルスクリプトをDockerイメージ化した上で、5個のECSサービスを作成します。
個々の内訳は以下の様になっています。
サービス名 | CPUユニット | PROC変数(プロセス数) |
---|---|---|
512CPU-1PROC | 512 | 1 |
512CPU-2PROC | 512 | 2 |
1024CPU-1PROC | 1024 | 1 |
1024CPU-2PROC | 1024 | 2 |
3072CPU-3PROC | 3072 | 3 |
512CPU-1PROC = 1タスク
512CPU-1PROCを1タスク起動
してから、クラスタとサービスのCPU使用率をCloudWatchで確認します。
サービスのCPU使用率が200%となっていますが、これはCPUユニット=512のタスクが実際はその2倍となる1024ユニットを消費しているためです。
また、1024ユニットを消費しているという事は1コアを専有しているのと同じであるため、クラスタCPU使用率は25%となります。
1024CPU-1PROC = 1タスク
CPUユニット=1024のタスクが1024ユニットを消費しているため、サービスのCPU使用率はきっかり100%となります。
クラスタのCPU使用率は変わらず25%(1コア)です。
ちなみに、タスク数を2にしたとしてもサービスのCPU使用率は100%のままに見えますが、メトリクスのタイプを合計
に変えるとちゃんと200%になります。(クラスタのCPU使用率は50%になります)
512CPU-2PROC = 1タスク
CPUユニット=512のタスク内で2個のプロセスがフル稼働しているため、実際は4倍の2048ユニットを消費している事になり、サービスのCPU使用率は400%となります。
また、2コアを専有している事でもあるのでクラスタのCPU使用率は50%となります。
1024CPU-2PROC = 1タスク & 3072CPU-3PROC = 1タスク
最後に1024CPU-2PROCを1タスク
と3072CPU-3PROCを1タスク
、同時に稼働させるとどうなるか検証してみます。
2個のプロセスが稼働しているため、サービスのCPU使用率は200%となります。
また、インスタンス上でプロセス状況を確認すると、各プロセスはCPUを100%使用出来ている事が分かります。
4096ユニットのECSクラスタ
上で1024ユニットのタスク
と3072ユニットのタスク
が全力で稼働しているため、それぞれのCPU使用率はきっちり100%となります。
また、先ほどはCPUを100%使用出来ていた1024CPU-2PROC
タスクのプロセスですが、現在は50%しか使用出来ていません。
--cpu-sharesと同様に、CPUユニットに基づいたタスクの相対的なCPU能力の設定
が上手く機能している事が良く分かります。
まとめ
- ECSインスタンスは1コアあたり1024のCPUユニットを持つ
- CPUユニットは
そのタスクが最低限必要とするCPU能力を他タスクとの相対値
で指定するためのパラメータである - クラスタの負荷状況に余裕がある時は、タスクは与えられたCPUユニット以上にCPUを利用できる
- その場合、CloudWatchのサービスCPU使用率は100%を超える
- クラスタの負荷が高まって来た場合、タスクは処理能力を押さえつけられる場合がある
- 当然、CPUユニットで与えた分は保証される
- とあるタスクのCPU使用率が100%で張り付いている場合、そのタスクのCPUユニットの追加を検討するべきである