foreach
ループを使い Server-Sent Events を監視している俺様アプリの入った Docker コンテナがあります。
コンテナを起動して、しばらくするとマシンが「フォーーっ」と唸り始めたので、docker stats
コマンドで確認すると CPU 使用率が 100% を超えていました。どうしよう
Qiita 記事に絞って「Docker CPU 使用率 100% コンテナ 軽減」でググってもヒットしなかったので、自分のググラビリティとして。
TL; DR (今北産業)
- コンテナ内のプログラムが、条件により空ループ(何も処理をしないでループ)する箇所が発生していないか確認する。
(for
/foreach
/while
ループなど) - 該当ループ処理の最後に支障をきたさない範囲で
sleep
を入れる。
(1 〜 0.5 秒入れるだけでも劇的に変わる) - 上記を確認した上で Docker もしくは docker-compose の設定で最大使用率に制限をかけるのがベター
TS; DR (kwsk)
強制的に制限する(docker
編)
もともと Docker にはコンテナごとに CPU の最大使用率を制限する --cpus
オプションがあります。つまり、コンテナに対して使用する CPU の個数を指定してリソースを分配することができるため、対処治療(対症療法)に使うことができます。
例えば 4 コア CPU の場合、CPU を 2 つ割り当てると使用率を最大 50% に制限できます。
docker run -it --cpus="2" ubuntu /bin/bash
同じく 4 コアで最大 80% に制限したい場合は以下。
docker run -it --cpus="3.2" ubuntu /bin/bash
- 参考文献
- Configure the default CFS scheduler | CPU @ Docker 公式ドキュメント
- Docker コンテナの CPU 使用率を制限する Tips @ Qiita
強制的に制限する(docker-compose
編)
docker compose
(旧 docker-compose
)で制限したい場合は、deploy
ディレクティブで cpus
を指定します。
version: '3.8'
services:
my-service:
container_name: my-cont1
build: .
deploy:
resources:
limits:
cpus: "3.2"
注意点として、docker compose up
時に --compatibility
を付る必要があります。
これは docker-compose.yaml
における deploy
の項目は、基本的に Docker Swarm 用の設定だからです。つまり、compose
コマンドではデフォルトで無視されてしまいます。compose
でも使えるようにするには --compatibility
フラグ・オプションを付けて互換モードで動作させる必要があります。
- docker compose up myservice
+ docker compose --compatibility up myservice
- 参考文献:
docker-compose --help
- How to specify Memory & CPU limit in docker compose version 3 @ StackOverflow
この設定は、コンテナが暴走した場合に備えて被害の範囲をあらかじめ抑えておくパターンです。つまり、これらは制限をかけているだけということです。
逆に言えば正常稼働していても、それ以上の CPU リソースを使うことができません。
そもそも、抜本的に CPU の使用率を下げない限り、リソースは制限値まで無駄に消費されることになります。おそらく大抵の場合は Docker の問題ではなく、プログラムの組み方に問題があるのは容易に想像できます。
プログラムを見直す
今回のプログラム自体は、SSL 接続のソケットからデータを読み込み Server-Sent Events のメッセージを受信して文字列を置き換える処理を行うだけのシンプルなものです。ファイルのアクセスすらありません。
そうなると、メッセージ読み取りと文字列処理のループ箇所が臭います。
そこで、VS Code のデバッグ機能を使いステップインで1行ずつ実行を確認してみます。
ポチ、ポチ、、、ポチ、、、あ...
どうやら、データが流れてこない間は処理を行わない「空ループ」が発生している箇所がありました。つまり、何も処理しない最速のループが実行されているわけで、「そりゃ CPU 使用率 100% になるわ」と納得しました。
$read = false;
while ($read === false) {
$read = readLine($socket); // データが届いてない間は false で高速ループ
}
そこで、PHP の「ループ処理を行った際の CPU の負荷」や .NET の「CPU使用率100%を回避する方法」を参考に、試しに1秒間の sleep
をループの最後に入れてみました。
$read = false;
while ($read === false) {
$read = readLine($socket);
sleep(1);
}
これが、効果てきめん! 100〜110% であった使用率が 0.00〜0.01% をウロチョロする程度にまで押さえ込めました。sleep
が 0.5 秒でも 0.01〜0.03% 程度の負荷でした。
以上から、PHP に限らず、まずはステップ実行などでループ箇所を見直し、ループの支障をきたさない範囲で sleep
を入れるのは効果的だと思いました。その上で、保険として docker
や docker-compose
の cpus
で最大値を制限するのがベターだと思われます。
参考文献
- Dockerのメモリ使用量を確認したい @ Qiita
- Dockerリソース使用状況の確認方法 @ Qiita
- ループ処理を行った際のCPUの負荷 @ Qiita
- CPU使用率100%を回避する方法 | 旧@IT会議室 @ @IT