【Docker】 CPU 使用率 100% のコンテナを軽減・回避する

> `foreach` ループを使い [Server-Sent Events](https://developer.mozilla.org/ja/docs/Web/API/Server-sent_events/Using_server-sent_events) を監視している俺様アプリの入った Docker コンテナがあります。
> コンテナを起動して、しばらくするとマシンが「フォーーっ」と唸り始めたので、 `docker stats` コマンドで確認すると CPU 使用率が 100% を超えていました。
>
> どうしよう

Qiita 記事に絞って[「Docker CPU 使用率 100% コンテナ 軽減」でググって](https://www.google.com/search?q=site:qiita.com+Docker+CPU+%E4%BD%BF%E7%94%A8%E7%8E%87+100%25+%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A+%E8%BB%BD%E6%B8%9B)もヒットしなかったので、自分のググラビリティとして。

## TL; DR <sub><sup>（今北産業）</sup></sub>

1. コンテナ内のプログラムが、<font color=darkorange>**条件により空ループ（何も処理をしないでループ）する箇所が発生していないか確認**</font>する。<br>（`for`/`foreach`/`while` ループなど）
2. 該当ループ処理の最後に<font color=darkorange>**支障をきたさない範囲で `sleep` を入れる**</font>。<br>（1 〜 0.5 秒入れるだけでも劇的に変わる）
3. 上記を確認した上で Docker もしくは docker-compose の設定で<font color="darkorange">最大使用率に制限をかける</font>のがベター

## TS; DR <sub><sup>（kwsk）</sup></sub>

### 強制的に制限する（`docker` 編）

もともと Docker にはコンテナごとに CPU の最大使用率を制限する `--cpus` オプションがあります。つまり、コンテナに対して使用する CPU の個数を指定してリソースを分配することができるため、[対処治療（対症療法）](https://ja.wikipedia.org/wiki/%E5%AF%BE%E7%97%87%E7%99%82%E6%B3%95)に使うことができます。

例えば 4 コア CPU の場合、CPU を 2 つ割り当てると使用率を最大 50% に制限できます。

```bash:2つぶんのCPUを割り当てる（2/4=0.5=50%）
docker run -it --cpus="2" ubuntu /bin/bash
```

同じく 4 コアで最大 80% に制限したい場合は以下。

```bash:3.2個ぶんのCPUを割り当てる（3.2/4=0.8=80%）
docker run -it --cpus="3.2" ubuntu /bin/bash
```

- 参考文献
    - [Configure the default CFS scheduler](https://docs.docker.com/config/containers/resource_constraints/#cpu) | CPU @ Docker 公式ドキュメント
    - [Docker コンテナの CPU 使用率を制限する Tips](https://qiita.com/hoto17296/items/43d8c2d695c8724b99aa) @ Qiita


### 強制的に制限する（`docker-compose` 編）

`docker compose`（旧 `docker-compose`）で制限したい場合は、<code>[deploy](https://docs.docker.com/compose/compose-file/#deploy)</code> ディレクティブで `cpus` を指定します。

```yaml:docker-compose.yml（4コアのCPUを80%に制限する例）
version: '3.8'

services:
  my-service:
    container_name: my-cont1
    build: .
    deploy:
      resources:
        limits:
          cpus: "3.2"
```

:::note warn
<font color=darkorange><strong>注意点</strong></font>として、<strong><code>docker compose up</code> 時に <code>--compatibility</code> を付る</strong>必要があります。

これは <code>docker-compose.yaml</code> における <code>deploy</code> の項目は、基本的に <a href="https://docs.docker.jp/swarm/overview.html">Docker Swarm</a> 用の設定だからです。つまり、<code>compose</code> コマンドではデフォルトで無視されてしまいます。<code>compose</code> でも使えるようにするには <code>--compatibility</code> フラグ・オプションを付けて**互換モードで動作させる**必要があります。
:::

```diff:compatibilityオプションを付けないとdeployの内容は有効にならない
- docker compose up myservice
+ docker compose --compatibility up myservice
```

- 参考文献：
    - `docker-compose --help`
    - [How to specify Memory & CPU limit in docker compose version 3](https://stackoverflow.com/a/57135933/12102603) @ StackOverflow

この設定は、コンテナが暴走した場合に備えて被害の範囲をあらかじめ抑えておくパターンです。つまり、**これらは制限をかけているだけ**ということです。

逆に言えば正常稼働していても、それ以上の CPU リソースを使うことができません。

そもそも、抜本的に CPU の使用率を下げない限り、リソースは制限値まで無駄に消費されることになります。おそらく大抵の場合は Docker の問題ではなく、プログラムの組み方に問題があるのは容易に想像できます。

### プログラムを見直す

今回、筆者の問題となっているプログラム自体はシンプルなものです。SSL 接続のソケットからデータを読み込み、[Server-Sent Events](https://developer.mozilla.org/ja/docs/Web/API/Server-sent_events/Using_server-sent_events) のメッセージを受信して文字列を置き換えてログを取るだけです。ファイルのアクセスすらありません。

そうなると、メッセージ読み取りと文字列処理のループ箇所が臭います。

そこで、VS Code のデバッグ機能を使い[ステップイン](https://www.atmarkit.co.jp/ait/articles/1707/21/news030_3.html)で１行ずつ実行を確認してみます。

ポチ、ポチ... ポチ... ... あ...

どうやら、データが流れてこない間は処理を行わない「空ループ」が発生している箇所がありました。つまり、**何も処理しない最速のループが実行されている**わけで、「そりゃ CPU 使用率 100% になるわ」と納得しました。

```php
$read = false;
while ($read === false) {
    $read = readLine($socket); // データが届いてない間は false で高速ループ
}
```

そこで、PHP の「[ループ処理を行った際の CPU の負荷](https://qiita.com/horikeso/items/4a0c94c6aed6f622fbef)」や .NET の「[CPU使用率100%を回避する方法]()」を参考に、試しに１秒間の `sleep` をループの最後に入れてみました。

```php
$read = false;
while ($read === false) {
    $read = readLine($socket);
    sleep(1);
}
```

これが、効果てきめん！ 100〜110% であった使用率が 0.00〜0.01% をウロチョロする程度にまで押さえ込めました。`sleep` が 0.5 秒でも 0.01〜0.03% 程度の負荷でした。

以上から、PHP に限らず、まずはステップ実行などでループ箇所を見直し、<font color="darkorange">**ループの支障をきたさない範囲で `sleep` を入れるのは効果的**</font>だと思いました。その上で、保険として `docker` や `docker-compose` の `cpus` で最大値を制限するのがベターだと思われます。

## 参考文献

- [Dockerのメモリ使用量を確認したい](https://qiita.com/takkii1010/items/98d47a13a6ae0ce7446f) @ Qiita
- [Dockerリソース使用状況の確認方法](https://qiita.com/ksugawara61/items/2a015f205e9c9b93026d) @ Qiita
- [ループ処理を行った際のCPUの負荷](https://qiita.com/horikeso/items/4a0c94c6aed6f622fbef) @ Qiita
- [CPU使用率100%を回避する方法](https://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=35231&forum=7) | 旧＠IT会議室 @ ＠IT