このチュートリアルでは、Alibaba Cloud ECS上でDockerコンテナのリソース制限機能を利用した実践的な体験ができます。
本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。
#前提条件
最新バージョンのDockerがすでにインストールされているECSサーバーにアクセスする必要があります。もしまだ持っていない場合は、このチュートリアルの手順に従ってください。
これらのリソース制限テストでは20~30MBのRAMを使用するので、合計512MBのRAMしかないサーバでも問題ありません。
CPUテストは2コアのみのサーバで行われます。サーバーのコア数が 4 つ以上の場合、テストの一つとして、より興味深い結果が得られるでしょう。
いくつかの CPU テストでは、15 秒間すべての CPU を占有します。このチュートリアルを共有開発サーバではなく、あなたのコンピュータで直接行った方が、あなたのチームメイトのためにも良いでしょう。
私はCentOSを使ってこのチュートリアルを書いています。Debian / Ubuntuを使用することができます。このチュートリアルは主にDockerコマンドを使用しているので、99%はどのLinuxディストロでも動作します。
Docker、イメージ、コンテナ、そしてdocker runとdocker ps -aの使用については、非常に基本的な理解が必要です。
#クリーンアップの準備
実行中のコンテナーが少ない(できればない)場合に非常に役立ちます。このようにすると、Docker ps -a出力リストでチュートリアルコンテナーを簡単に見つけることができます。
そこで、実行している必要のないすべてのコンテナを停止してプルーニングしてください。
(開発環境で)次のようにしてすばやく実行できます。
docker stop $(docker ps -a -q) #stop ALL containers
すべてのコンテナを削除するには、次のように実行します。
docker rm -f $(docker ps -a -q) # remove ALL containers
#––memory-reservation
https://docs.docker.com/config/containers/resource_constraints/ より
メモリ不足や競合をDockerが検知したときに起動する--memoryよりも小さいソフトリミットを指定することができます。もし --memory-reservation を使用する場合は --memory よりも小さい値を設定しなければ優先されません。ソフトリミットなので、コンテナがリミットを超えないことを保証するものではありません。
私はこれを1GB RAMのサーバーで実行しています。
5つのコンテナをそれぞれ250MBのRAMを確保して実行してみましょう。
docker container run -d --memory-reservation=250m --name mymem1 alpine:3.8 sleep 3600
docker container run -d --memory-reservation=250m --name mymem2 alpine:3.8 sleep 3602
docker container run -d --memory-reservation=250m --name mymem3 alpine:3.8 sleep 3603
docker container run -d --memory-reservation=250m --name mymem4 alpine:3.8 sleep 3604
docker container run -d --memory-reservation=250m --name mymem5 alpine:3.8 sleep 3605
私は250 MBのRAMを過剰予約したにもかかわらず、すべてのコンテナが実行されています。つまり、これは絶望的です: そして、過剰予約を防ぐことはできません。
topを実行すると、仮想RAMが割り当てられていないことがわかります。この設定はDockerの内部です。
PID USER VIRT RES SHR S %MEM TIME+ COMMAND
933 root 967.4m 86.0m 24.3m S 8.7 0:55.87 dockerd
940 root 582.0m 36.3m 12.3m S 3.7 0:46.50 docker-containe
13422 root 8.7m 3.3m 2.5m S 0.3 0:00.02 docker-containe
13309 root 7.3m 3.0m 2.3m S 0.3 0:00.02 docker-containe
13676 root 7.3m 2.9m 2.2m S 0.3 0:00.01 docker-containe
13540 root 7.3m 2.8m 2.1m S 0.3 0:00.01 docker-containe
13793 root 8.7m 2.7m 2.1m S 0.3 docker-containe
docker stats は RAM の予約状況を表示しません。
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
a1a4bd1c226b mymem5 0.00% 1.086MiB / 985.2MiB 0.11% 578B / 0B 1.19MB / 0B 0
9ced89c63a7e mymem4 0.00% 1.105MiB / 985.2MiB 0.11% 648B / 0B 1.19MB / 0B 0
696f1cef7d57 mymem3 0.00% 1.113MiB / 985.2MiB 0.11% 648B / 0B 1.19MB / 0B 0
77d61012b5fd mymem2 0.00% 1.086MiB / 985.2MiB 0.11% 648B / 0B 1.19MB / 0B 0
fab3faa6d23d mymem1 0.00% 1.043MiB / 985.2MiB 0.11% 648B / 0B 1.19MB / 0B 0
docker ps -a
5つのコンテナが正常に動作していることを表示します。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a1a4bd1c226b alpine:3.8 "sleep 3605" 2 minutes ago Up 2 minutes mymem5
9ced89c63a7e alpine:3.8 "sleep 3604" 4 minutes ago Up 4 minutes mymem4
696f1cef7d57 alpine:3.8 "sleep 3603" 5 minutes ago Up 5 minutes mymem3
77d61012b5fd alpine:3.8 "sleep 3602" 6 minutes ago Up 6 minutes mymem2
fab3faa6d23d alpine:3.8 "sleep 3600" 8 minutes ago Up 8 minutes mymem1
これらのコンテナは終了しました。止めてからプルーニングすればいいのです。
docker container stop mymem1 -t 0
docker container stop mymem2 -t 0
docker container stop mymem3 -t 0
docker container stop mymem4 -t 0
docker container stop mymem5 -t 0
docker container prune -f
--memory and --memory-swap (スワップ不可)
https://docs.docker.com/config/containers/resource_constraints/ より
- -m または --memory= コンテナが使用できる最大メモリ量。このオプションを設定した場合、最小許容値は 4m (4 メガバイト) です。
- --memory-swap このコンテナがディスクにスワップすることを許可するメモリの量です。
- memory-swap が --memory と同じ値に設定され、 --memory が正の整数に設定されている場合、コンテナはスワップにアクセスできません。
現在、スワップを許可しないことをテストしています。
慎重にMB単位でRAMを割り当てるためのツールが必要です - そうすれば、定義されたRAMの限界を慎重に超えてしまうだけです。私はPythonに決めました。( ここで使用されている4行のコードを理解するためにPythonを知る必要はありません。)
このチュートリアルの後編では、実際のベンチマークツールを使用します。
PythonのDockerイメージをまだお持ちでない方はダウンロードしてください。
docker pull python:3-alpine
コンテナを実行し、RAMを制限: --memory=20m --memory-swap=20m
docker container run -d --memory=20m --memory-swap=20m --name myPython python:3-alpine sleep 3600
docker exec -it myPython /bin/sh
シェルプロンプトでpython3と入力して、対話型のPythonエディタに入ります。以下のコードをカットアンドペーストしてください。Pythonではスペースには構文上の意味があるので、スペースやタブを追加しないように注意してください。
longstring = []
for x in range(17):
len(longstring)
longstring.append('1' * 10**6)
Enterキーを押してステートメントブロックを終了します。これでコードが実行されます。
期待される出力 .
>>> for x in range(17):
... len(longstring)
... longstring.append('1' * 10**6)
...
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Killed
このコンテナに20MBのRAMを割り当てました。Python は 5 MB を使用しています。forループは16MBの'1'文字をlongstring変数に追加しようとするとkillされてしまいます。
3つの注意点があります。
- 20 MB の制限内で RAM を割り当てた場合は動作しました。
- 制限を超えたRAMの割り当ては強制終了しました。
- スワップを使用しない:スワップを使用することで、割り当てが静かに動作を継続しませんでした。
要約: --memory と --memory-swap (スワップは許可されていません) は両方とも同じ値に設定されている場合に動作します。コンテナ内で実行されているアプリケーションの知識に基づいて、これらの値を適切に設定する必要があります。
これでこのコンテナは終了です。これでこのコンテナは終了です。
docker container stop myPython
docker container prune -f
#––memory and --memory-swap (スワップ可能)
memory=20m と --memory-swap=30m を指定すると、10MBのスワップが可能になります。
どのように動作するか見てみましょう。
docker container run -d --memory=20m --memory-swap=30m --name myPython python:3-alpine sleep 3600
docker exec -it myPython /bin/sh
シェルプロンプトでpython3と入力して、インタラクティブなPythonエディタを起動します。以下のコードをカットアンドペーストしてください。Pythonではスペースには構文上の意味があるので、スペースやタブを追加しないように注意してください。
longstring = []
for x in range(24):
len(longstring)
longstring.append('1' * 10**6)
ENTERを押してfor文ブロックを終了します。これでコードが実行されます。
期待される出力 .
0 to 24 shown ... no killing
Pythonが使用する5MBのRAM。上記のように25MBのRAMを確保していますが、エラーはありません。
メモリ=20m --メモリスワップ=30mを指定しています。
30MBを使っただけなので、10MBがスワップされていることになります。topを別のシェルで実行して確認してみましょう。
top - 13:20:38 up 4:41, 2 users, load average: 0.11, 0.05, 0.06
Tasks: 119 total, 1 running, 118 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.2 us, 0.3 sy, 0.0 ni, 99.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 985.219 total, 466.879 free, 190.812 used, 327.527 buff/cache
MiB Swap: 1499.996 total, 1490.078 free, 9.918 used. 618.730 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND SWAP
933 root 20 0 967.4m 91.5m 24.3m S 9.3 0:45.46 dockerd
940 root 20 0 579.9m 33.1m 12.3m S 0.3 3.4 0:36.73 docker-containe
11900 root 20 0 253.5m 19.1m 10.5m S 1.9 0:00.25 docker
11941 root 20 0 39.1m 17.4m S 1.8 0:00.39 python3 9.5m
予想通り、10MBのスワップが使用されています。(SWAPフィールドをトップに表示する必要があります。)
慎重に2MB以上のRAMを使うようにしましょう - コンテナはRAMを使い果たしているはずです。
これを切り取り、Pythonエディタに貼り付けます。ENTERキーを押して実行します。
longstring = []
for x in range(26):
len(longstring)
longstring.append('1' * 10**6)
期待される出力 .
it gets killed
このコンテナが完成しました。停止してプルーニングすることができます。
docker container stop myPython
docker container prune -f
概要: --memoryと--memory-swap (スワップ可能)は--memory-swapが--memoryよりも大きい場合に動作します。
制限は完全に適用されます。
本番環境ではコンテナに適切な制限を指定する必要があります。
現在のprodシステムのRAM使用量を調査してください。それに応じて制限値を定義することで、エラーの可能性を大きく広げながらも、暴走したコンテナがprodサーバをクラッシュさせるのを防ぎます。
#--ooom-kill-disable
これまでのところ、自動的に有効になったアウトオブメモリ機能は、私たちの暴走するPythonプログラムをkillしてしまいました。
これを無効にするとどうなるか見てみましょう。
以下の --oom-kill-disable に注意してください。
docker container run -d --oom-kill-disable --memory=20m --memory-swap=30m --name myPython python:3-alpine sleep 3600
無防備なコンテナに入力してください。
docker exec -it myPython /bin/sh
python3エディタを入力し、そのコードを貼り付け、ENTERを押して実行します。
python3
a = []
for x in range(26):
len(a)
a.append('1' * 10**6)
コンテナがハングします。
別のシェルコンソールで top を実行します。
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND SWAP
12317 root 20 0 41.0m 17.6m 0.0m D 1.8 0:00.32 python3 10.7m
コンテナの状態は D : uninterruptible sleep
別のシェルでは
docker exec -it myPython /bin/sh
これもハングします。
別のシェルを使用して、ぶら下がっているコンテナのPIDを取得して、強制終了できるようにします。
docker inspect myPython
PIDを取得します。
topかkill -9 your-PIDを使用して、それを強制終了します。。
結論:
ooom-kill-disableは使用しないでください。
あなたのハングアップしたシェルは、Linuxプロンプトに戻っています。これらを終了することができます。
#––cpu-shares
https://docs.docker.com/config/containers/resource_constraints/#cpu より
--cpu-share. このフラグをデフォルトの1024よりも大きいか小さい値に設定することで、コンテナの重量を増減させ、ホストマシンのCPUサイクルの大きいか小さいかの割合でアクセスできるようにします。
これは、CPU サイクルが制限されている場合にのみ適用されます。多くのCPUサイクルが利用可能な場合、すべてのコンテナは必要なだけのCPUを使用します。これはソフトリミットです。--cpu-sharesは、スウォームモードでコンテナがスケジューリングされるのを防ぐものではありません。
利用可能なCPUサイクルに対してコンテナのCPUリソースを優先します。特定のCPUアクセスを保証したり予約したりするものではありません。
計画:100、500、1000のCPUシェアを提供する3つのコンテナを実行します。
以下はひどいテストです。上記の説明をもう一度注意深く読んでから、次の3つのコマンドを読んで、なぜ正しく割り当てられたCPUの割合が明確に表示されないのかを判断できるかどうかを確認してください。
これらの CPU テストは、あなたが自分のコンピュータでこれを実行していて、共有開発サーバではないと仮定していることに注意してください。3つのテストでは、20秒間100%のCPUを占有します。
このチュートリアルシリーズの後半では、実際の Linux ベンチマークツールを使用して、独自のベンチコンテナを使用してこれらのテストを行います。特に、これらの CPU ホッグを非常に短いランタイムで実行して、正確な結果を得ることに焦点を当てます。
しかし、これらの CPU テストを読んで、このクイックハックテストがいかに間違っていて遅いかを知ってください。
dd, urandom, md5sum はベンチツールではないことに注意してください。
問題はddやそのタイミングではありません。
CPUストレスアプリケーション: time dd if=/dev/urandom bs=1M count=2 | md5sum
ベンチマークの説明:
- time ... 経過時間を測定します。
- dd if=/dev/urandom bs=1M count=2 ... copies bs=blocksize 1MBのランダム性を2回繰り返す
- md5sum ... md5セキュリティハッシュを計算 (CPUに負荷を与える)
実行して結果を調べてみましょう。
docker container run -d --cpu-shares=1024 --name mycpu1024 alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'
docker container run -d --cpu-shares=500 --name mycpu500 alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'
docker container run -d --cpu-shares=100 --name mycpu100 alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'
ランタイムを判断するためにログを調査してみましょう。
docker logs mycpu1024
docker logs mycpu500
docker logs mycpu100
期待される出力:
docker logs mycpu1024
real 0m 15.29s
user 0m 0.00s
sys 0m 14.51s
docker logs mycpu500
real 0m 18.65s
user 0m 0.00s
sys 0m 15.28s
docker logs mycpu100
real 0m 23.28s
user 0m 0.00s
sys 0m 13.09s
すべてのコンテナがほぼ同じ sys cpu 時間を使用していることに注意してください。
--cpu-shares=100 は明らかに時間がかかりますが、 --cpu-shares=500 は --cpu-shares=1024 よりもわずかに遅くなります。
問題は --cpu-shares=1024 が非常に高速に動作した後に終了してしまうことです。
すると、--cpu-shares=500と--cpu-shares=100がCPUをフルに利用できるようになります。
すると--cpu-shares=500はほとんどのCPUを占有しているので、すぐに終了します。
そして、--cpu-shares=100は、ほとんどのCPUのシェアを持っているので、すぐに終了します - 他には何も動いていません。
この問題とその解決方法を考えてみてください。
これ以上読む前に解決策を考えてみてください。
そしてあなたの解決策を試してみてください。
解決策:
これら3つのコンテナはすべて常時並列に動作しなければなりません。CPUシェアはCPUがストレスを受けているときにのみ動作します。
mycpu1024 - カウントは mycpu100 の 10 倍に設定する必要があります。
mycpu500 - カウントはmycpu100の5倍に設定する必要があります。
このようにして、3つのコンテナの実行時間はほぼ同じになります - CPUシェアに基づいて、CPUシェアに応じた似たような作業量になります。
次に、mycpu1024の実行時間を10で割ってみると、mycpu100の10倍のワークロードが得られます。
そして、mycpu500のランタイムを5で割ってみると、 mycpu100の10倍のワークロードが得られました。
DockerがCPUシェアを適当に分けたのは、ごくごく当たり前のことのはずです。
忙しいDocker管理者のショートカット/クイックメソッド:
上記のコンテナを全て提出して再度実行する。
以下も実行できるように準備しておきましょう。
--cpu-shares=250 and --cpu-shares=200 containers
次に別のシェルで docker stats を実行して ctrl c を押して表示をフリーズさせます。
CPUシェアが正しく割り当てられていることは明らかです。
コンテナをクリーンアップします:
docker container prune -f
#同一に割り当て --cpu-share
--cpu-share. このフラグを設定すると、コンテナの重量を増やしたり減らしたりして、ホストマシンのCPUサイクルの割合を増やしたり減らしたりします。
これは、CPU-sharesが同じに設定されていると、CPUの占有率が同じになることを意味します。
3つのコンテナを実行していて、すべてのコンテナのCPUシェアが1024であるとします。
docker container run -d --cpu-shares=1024 --name mycpu1024a alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'
docker container run -d --cpu-shares=1024 --name mycpu1024b alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'
docker container run -d --cpu-shares=1024 --name mycpu1024c alpine:3.8 /bin/sh -c 'time dd if=/dev/urandom bs=1M count=100 | md5sum'
実行:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
c4625617f339 mycpu1024c 63.79% 1.262MiB / 985.2MiB 0.13% 648B / 0B 1.33MB / 0B 0
44362316e33a mycpu1024b 68.44% 1.254MiB / 985.2MiB 0.13% 648B / 0B 1.33MB / 0B 0
a704aca5c0d7 mycpu1024a 66.27% 1.254MiB / 985.2MiB 0.13% 648B / 0B 1.35MB / 0B 0
予想通り、3つのコンテナはすべて同じ割合のCPU時間を取得します。
docker logs mycpu1024a
docker logs mycpu1024b
docker logs mycpu1024c
すべてが同じ経過時間を実行したことを確認するだけです。
docker logs mycpu1024a
real 0m 21.25s
user 0m 0.00s
sys 0m 14.72s
docker logs mycpu1024b
real 0m 22.53s
user 0m 0.00s
sys 0m 15.21s
docker logs mycpu1024c
real 0m 21.45s
user 0m 0.00s
sys 0m 15.09s
プルーンコンテナ、終了です。
docker container prune -f