AWS: ECSのCPUユニット(CPU Units)について

# はじめに
自社サービスの実行基盤としてAWS ECS(on EC2)を利用しています。

これまでは4台固定のECSクラスタでトラフィックを捌いていましたが、ピークタイムの負荷状況が怪しくなってきたため、ECSクラスタへAutoScalingを導入する事にしました。

ひとまずはクラスタのCPU負荷をスケールアウトの基準とする事としたのですが、CloudWatchに表示されるCPU使用率(CPUUtilication)が示す値の意味が良く分からない。

[こちらのドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/cloudwatch-metrics.html#cluster_utilization)によれば、クラスタの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タスク定義パラメータです。

```json:ECSタスク定義の例
{
    "name": "sample-task",
    "image": "alpine",
    "cpu": 1024,
    "memory": 256
}
```

設定した値は`docker run`の`--cpu-shares`パラメータとして使用されます。

```shell
$ docker inspect 9a7c6f9a5ea0 | grep -i cpushares
            "CpuShares": 1024
```

ECSインスタンスのCPUユニットについて、[ドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size)には以下のような記載があります。

> 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`環境変数で指定されたコア数分の負荷を発生させます。

```shell
#!/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)をそれぞれ起動します。

```shell
$ 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負荷を確認すると、予想通りの比率となっている事が分かります。

```shell
$ docker stats --no-stream --format '{{.Name}} {{.CPUPerc}}' | sort
TASK1 41.20%
TASK2 159.76%
```

ここでタスク2を停止します。

```shell
$ docker kill TASK2
```

CPU負荷を確認すると、タスク1のCPU使用率が100%(=1コアを専有)に上昇しました。

```shell
$ 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となります。
<img width="1051" alt="ECSクラスタ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244433/73405227-e3a5-b119-894d-e875119f676c.png">

先ほどのシェルスクリプトをDockerイメージ化した上で、5個のECSサービスを作成します。
<img width="625" alt="ECSサービス一覧" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244433/8ce4ae45-479f-dc09-b383-e3efa90d95c1.png">

個々の内訳は以下の様になっています。

| サービス名 | 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で確認します。
<img width="922" alt="512CPU-1PROC = 1タスク" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244433/4710480b-2556-6458-6a9a-d43c47efa2ef.png">

サービスのCPU使用率が200%となっていますが、これはCPUユニット=512のタスクが実際はその2倍となる1024ユニットを消費しているためです。

また、1024ユニットを消費しているという事は1コアを専有しているのと同じであるため、クラスタCPU使用率は25％となります。

## 1024CPU-1PROC = 1タスク
続いて、`1024CPU-1PROCを1タスク`起動します。
<img width="916" alt="1024CPU-1PROC = 1タスク" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244433/5c154fdb-7484-094a-c871-ffe7b6a76ae9.png">

CPUユニット=1024のタスクが1024ユニットを消費しているため、サービスのCPU使用率はきっかり100%となります。

クラスタのCPU使用率は変わらず25%(1コア)です。

ちなみに、タスク数を2にしたとしてもサービスのCPU使用率は100%のままに見えますが、メトリクスのタイプを`合計`に変えるとちゃんと200%になります。（クラスタのCPU使用率は50%になります）
<img width="918" alt="1024CPU-1PROC = 2タスク" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244433/260d3f48-68b0-8d8f-1036-e19ffc67c787.png">

## 512CPU-2PROC = 1タスク
`512CPU-2PROCを1タスク`起動します。
<img width="915" alt="512CPU-2PROC = 1タスク" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244433/acf54e00-8836-7890-1778-7dd2f9099c68.png">

CPUユニット=512のタスク内で2個のプロセスがフル稼働しているため、実際は4倍の2048ユニットを消費している事になり、サービスのCPU使用率は400%となります。

また、2コアを専有している事でもあるのでクラスタのCPU使用率は50%となります。

## 1024CPU-2PROC = 1タスク & 3072CPU-3PROC = 1タスク
最後に`1024CPU-2PROCを1タスク`と`3072CPU-3PROCを1タスク`、同時に稼働させるとどうなるか検証してみます。

まずは`1024CPU-2PROC`のタスクを起動します。
<img width="915" alt="1024CPU-2PROC = 1タスク" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244433/2d8f8326-2dcd-d75d-25f9-a979cd83956c.png">

2個のプロセスが稼働しているため、サービスのCPU使用率は200％となります。

また、インスタンス上でプロセス状況を確認すると、各プロセスはCPUを100%使用出来ている事が分かります。
<img width="845" alt="1024CPU-2PROC = 1タスク & 3072CPU-3PROC - topコマンド01" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244433/a632d96d-8883-397f-936e-c60fa112e34f.png">

次に、`3072CPU-3PROC`のタスクを起動します。
<img width="913" alt="1024CPU-2PROC = 1タスク & 3072CPU-3PROC = 1タスク" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244433/f004e85d-d24f-e871-c0b1-85b7a16008c0.png">

`4096ユニットのECSクラスタ`上で`1024ユニットのタスク`と`3072ユニットのタスク`が全力で稼働しているため、それぞれのCPU使用率はきっちり100％となります。

また、先ほどはCPUを100%使用出来ていた`1024CPU-2PROC`タスクのプロセスですが、現在は50%しか使用出来ていません。
<img width="845" alt="1024CPU-2PROC = 1タスク & 3072CPU-3PROC - topコマンド02" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244433/7d9f53eb-7f53-b5d1-b391-91f3fee5a1a7.png">

--cpu-sharesと同様に、`CPUユニットに基づいたタスクの相対的なCPU能力の設定`が上手く機能している事が良く分かります。

# まとめ
- ECSインスタンスは1コアあたり1024のCPUユニットを持つ
- CPUユニットは`そのタスクが最低限必要とするCPU能力を他タスクとの相対値`で指定するためのパラメータである
- クラスタの負荷状況に余裕がある時は、タスクは与えられたCPUユニット以上にCPUを利用できる
    - その場合、CloudWatchのサービスCPU使用率は100%を超える
- クラスタの負荷が高まって来た場合、タスクは処理能力を押さえつけられる場合がある
    - 当然、CPUユニットで与えた分は保証される
    - とあるタスクのCPU使用率が100%で張り付いている場合、そのタスクのCPUユニットの追加を検討するべきである