Dockerコンテナのリソース制限機能を使った実践的な体験談 その1

このチュートリアルでは、**Alibaba Cloud** ECS上で**Dockerコンテナ**のリソース制限機能を利用した実践的な体験ができます。

*本ブログは英語版からの翻訳です。オリジナルは[こちら](https://www.alibabacloud.com/blog/docker-container-resource-management-cpu-ram-and-io-part-1_594573)からご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。*

#前提条件
最新バージョンの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/ ](https://docs.docker.com/config/containers/resource_constraints/?spm=a2c65.11461447.0.0.6c85566d7OkBYj)より

>メモリ不足や競合を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/ ](https://docs.docker.com/config/containers/resource_constraints/?spm=a2c65.11461447.0.0.6c85566d0bPtzG)より

- -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](https://docs.docker.com/config/containers/resource_constraints/?spm=a2c65.11461447.0.0.6c85566d10RQyD#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 
```