前回、Podの割り当てリソース量が小さいほど、急増する負荷や、Nodeのスケールアウト時に弱いのでは疑惑が上昇しました。
その対策として、メモリのLimitを外すことはアリなのかどうか。メモリの NoLimit はそもそもどういう挙動なのか、を知っておくために行った攻めの検証となります。
概要というか検証前の頭ン中
PodのメモリもCPUみたいに NoLimit で動かすなんて、無理っていうか非人道的っていうか、絶対死しか待っていないでしょって思い込むのは良くない!と思い直すことにした。Pod で動かすWEB・APP系のデーモンの機能次第ではあるんだけど、もしメモリを allocatable いっぱいまで使ったり、落ち着いたら複数のPodでの使用メモリ量が平均化したり、をできるならば、小さいPod君でも一時的にイキれることで弱点を克服できるかもしれない、と思ったのだ。
多分、ていうかほぼ絶対ムリなんだけど、無理なら無理で無理な理由を突き止めておきたい、あわよくば黒魔術・銀の弾丸を見つけ出したい。そんな下心で検証開始☆
検証準備
ホスト情報
ということで、落ちづらくて小さめな c3.large をスポットで起動し、Allocatable 容量の確認から。2vCPU, 3.75GiB でございます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$ kubectl describe node ip-10-123-4-56.ap-northeast-1.compute.internal ... Capacity: attachable-volumes-aws-ebs: 39 cpu: 2 ephemeral-storage: 20959212Ki hugepages-2Mi: 0 memory: 3845876Ki pods: 29 Allocatable: attachable-volumes-aws-ebs: 39 cpu: 2 ephemeral-storage: 19316009748 hugepages-2Mi: 0 memory: 3743476Ki pods: 29 |
パッケージ追加
メモリ消費用のスクリプトで使うので、追加。と、備え付けの ps コマンドだと RSS 見れないので procps も。
1 |
apk add procps py3-psutil |
メモリ消費スクリプト
適当に大容量の変数を作って、RSS値を出力してガッツリ sleep するだけ。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import os import time import psutil tmp = list(range(60**4)) pid = os.getpid() process = psutil.Process(pid) print("pid = %s" % pid) print("mem = %d" % process.memory_info().rss) time.sleep(86400) |
適当に数字変えてみたけど、めんどくなったので、だいたいこれで 500MiB強 の消費。これを Pod で & つけてバックグラウンドで複数常駐させてオラオラしていく。
1 2 3 |
# python3 memory.py pid = 190 mem = 533991424 |
1Podで Memory Limit 有りでメモリオーバーした場合
こんな制限のPod。1GiB 超えたらどうなるかチェキ!
1 2 3 4 5 6 7 |
$ kubectl describe pod production-test-6f858ff665-r2btd ... Limits: memory: 1Gi Requests: cpu: 500m memory: 1Gi |
ちな、Podで直にメモリ量を見たら、vCPU と一緒で、Nodeと同じ容量がみえる。
1 2 3 4 5 6 |
$ kubectl exec -it production-test-6f858ff665-r2btd /bin/sh # free total used free shared buff/cache available Mem: 3845876 542020 1975680 612 1328176 3262364 Swap: 0 0 0 |
実際にオーバーさせた時の状況
シェルを2つ出して、それぞれでスクリプトと、ps を実行。
1 2 3 4 5 6 7 8 9 10 11 12 |
1# python3 memory.py & 2# ps axwwwu | grep python root 190 3.6 13.5 534816 521476 pts/0 S+ 02:20 0:00 python3 memory.py 1# python3 memory.py & 2# ps axwwwu | grep python /bin/sh: can't fork: Out of memory 1# [1]- Killed python3 memory.py |
このときに起こったことは、
- メモリオーバーが発生した時点で、両方のシェルがフリーズした
- 先に起動したプロセスが kill され、後発のスクリプトは正常に処理が完了した
- そのあと、シェルはどちらも操作できるようになった
dmesg
ちょっと長いけど、メモなので該当部を記載しておく。cgroup 先生がしゃしゃり出てきて殺られたのがわかる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
[ 8437.509704] python3 invoked oom-killer: gfp_mask=0x14200ca(GFP_HIGHUSER_MOVABLE), nodemask=(null), order=0, oom_score_adj=728 [ 8437.519951] python3 cpuset=d108940861f6d26aa47d6b43e388ec06a6115e59eb900522f469b79d800fcd3a mems_allowed=0 [ 8437.528246] CPU: 0 PID: 4155 Comm: python3 Not tainted 4.14.123-111.109.amzn2.x86_64 #1 [ 8437.535509] Hardware name: Xen HVM domU, BIOS 4.2.amazon 08/24/2006 [ 8437.540813] Call Trace: [ 8437.543563] dump_stack+0x5c/0x82 [ 8437.546924] dump_header+0x94/0x229 [ 8437.550385] oom_kill_process+0x223/0x420 [ 8437.553999] out_of_memory+0x2af/0x4d0 [ 8437.557514] mem_cgroup_out_of_memory+0x49/0x80 [ 8437.561812] mem_cgroup_oom_synchronize+0x2ed/0x330 [ 8437.566115] ? mem_cgroup_css_online+0x30/0x30 [ 8437.570625] pagefault_out_of_memory+0x32/0x77 [ 8437.574721] __do_page_fault+0x4b4/0x4c0 [ 8437.578325] ? page_fault+0x2f/0x50 [ 8437.581611] page_fault+0x45/0x50 [ 8437.584846] RIP: de7114c9: (null) [ 8437.588655] RSP: ac15b9:0000000000000001 EFLAGS: 7f9cc14c8450 [ 8437.588692] Task in /kubepods/burstable/pod3d68b233-0fec-11ea-937b-0a1e24cfb94a/d108940861f6d26aa47d6b43e388ec06a6115e59eb900522f469b79d800fcd3a killed as a result of limit of /kubepods/burstable/pod3d68b233-0fec-11ea-937b-0a1e24cfb94 a [ 8437.612092] memory: usage 1048576kB, limit 1048576kB, failcnt 242172 [ 8437.617846] memory+swap: usage 1048576kB, limit 9007199254740988kB, failcnt 0 [ 8437.623579] kmem: usage 10372kB, limit 9007199254740988kB, failcnt 0 [ 8437.628855] Memory cgroup stats for /kubepods/burstable/pod3d68b233-0fec-11ea-937b-0a1e24cfb94a: cache:0KB rss:0KB rss_huge:0KB shmem:0KB mapped_file:0KB dirty:0KB writeback:0KB swap:0KB inactive_anon:0KB active_anon:0KB inactive_file :0KB active_file:0KB unevictable:0KB [ 8437.648341] Memory cgroup stats for /kubepods/burstable/pod3d68b233-0fec-11ea-937b-0a1e24cfb94a/4ae17a22ed29d4b164ac2d797bf8737a84e631bbf8ca778a7f0306078c928da6: cache:0KB rss:48KB rss_huge:0KB shmem:0KB mapped_file:0KB dirty:0KB writ eback:0KB swap:0KB inactive_anon:0KB active_anon:48KB inactive_file:0KB active_file:0KB unevictable:0KB [ 8437.671545] Memory cgroup stats for /kubepods/burstable/pod3d68b233-0fec-11ea-937b-0a1e24cfb94a/d108940861f6d26aa47d6b43e388ec06a6115e59eb900522f469b79d800fcd3a: cache:48KB rss:1038108KB rss_huge:0KB shmem:8KB mapped_file:16KB dirty:4 KB writeback:0KB swap:0KB inactive_anon:8KB active_anon:1038108KB inactive_file:32KB active_file:8KB unevictable:0KB [ 8437.695370] [ pid ] uid tgid total_vm rss nr_ptes nr_pmds swapents oom_score_adj name [ 8437.701808] [15647] 0 15647 256 1 4 2 0 -998 pause [ 8437.708878] [15747] 0 15747 406 238 5 3 0 728 busybox [ 8437.715832] [15800] 0 15800 1458 223 7 3 0 728 nginx [ 8437.722476] [15801] 1000 15801 1572 446 7 3 0 728 nginx [ 8437.728901] [15802] 1000 15802 1572 446 7 3 0 728 nginx [ 8437.735395] [15803] 0 15803 7385 4659 16 3 0 728 bundle [ 8437.742052] [15850] 0 15850 22005 15263 45 3 0 728 bundle [ 8437.748444] [29895] 0 29895 408 296 5 3 0 728 sh [ 8437.755045] [29944] 0 29944 408 278 5 3 0 728 sh [ 8437.761490] [ 4026] 0 4026 133704 129712 259 3 0 728 python3 [ 8437.768087] [ 4155] 0 4155 120392 113152 227 3 0 728 python3 [ 8437.774596] Memory cgroup out of memory: Kill process 4026 (python3) score 1223 or sacrifice child [ 8437.781288] Killed process 4026 (python3) total-vm:534816kB, anon-rss:518548kB, file-rss:300kB, shmem-rss:0kB [ 8437.825857] oom_reaper: reaped process 4026 (python3), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB |
わかること
まずは、Limits memory は正常に機能している、ということを確認できた。一番大事なことだ。1Pod でメモリ NoLimit でオーバーした場合
allocatable 約3655MiB に対し、約500MiB強 のプロセスを常駐させていくと、7~8個目でどうなるか、という実験。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/app # python3 memory.py & /app # pid = 44 mem = 534065152 /app # python3 memory.py & /app # pid = 45 mem = 534065152 /app # python3 memory.py & /app # pid = 46 mem = 534065152 /app # python3 memory.py & /app # pid = 47 mem = 534065152 /app # python3 memory.py & /app # pid = 48 mem = 534052864 /app # python3 memory.py & /app # pid = 49 mem = 534048768 /app # python3 memory.py & /app # pid = 50 mem = 531238912 [1] Killed python3 memory.py |
無事、7プロセス目で死亡を確認。
そして予想外の dmesg である。長過ぎるけど、大事なのでファイルで添付しておく。
cgroup の記述はどこにもなく、Nodeで素の OOM_KILLER 先生が走ったようなログが観測できた。kill られたプロセスだけ抜粋すると、なんと aws-k8s-agent までぶった切られているのがわかる。
1 2 3 4 5 6 7 |
[19382.500626] Out of memory: Kill process 1682 (aws-k8s-agent) score 1000 or sacrifice child [19384.168785] Out of memory: Kill process 1658 (spot-monitor.sh) score 998 or sacrifice child [19385.779516] Out of memory: Kill process 1658 (spot-monitor.sh) score 998 or sacrifice child [19386.879887] Out of memory: Kill process 1540 (bash) score 998 or sacrifice child [19388.360494] Out of memory: Kill process 1602 (busybox) score 998 or sacrifice child [19389.160611] Out of memory: Kill process 1602 (busybox) score 998 or sacrifice child [19390.690125] Out of memory: Kill process 1362 (python3) score 862 or sacrifice child |
わかったこと
ここでわかったフリしても何もないので、次へ行く。落ち着くためメモリ変動の確認
予想外のログをみたことで動揺したので、メモリ容量は実体と監視であっているのか、の確認をしておいた。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# # 1pod 6process が限界 # $ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-123-2-38.ap-northeast-1.compute.internal 36m 1% 3313Mi 90% ip-10-123-9-108.ap-northeast-1.compute.internal 52m 2% 456Mi 5% $ kubectl top pod NAME CPU(cores) MEMORY(bytes) node-manager-d7kpl 2m 3Mi node-manager-n47d7 2m 10Mi production-test-f785cc9f4-k6252 1m 3117Mi # # 1pod 5process に減らしたら、ちゃんと減った # $ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-123-2-38.ap-northeast-1.compute.internal 41m 2% 2807Mi 76% ip-10-123-9-108.ap-northeast-1.compute.internal 57m 2% 458Mi 5% $ kubectl top pod NAME CPU(cores) MEMORY(bytes) node-manager-d7kpl 2m 4Mi node-manager-n47d7 2m 10Mi production-test-f785cc9f4-k6252 1m 2610Mi |
気を取り直して、次へ行く。
複数Pod でメモリ NoLimit でオーバーした場合
1Pod目が、あと500MiB 追加で死ぬって状況で、2Pod目を起動してみる。そもそも起動するのか。
1 2 3 4 5 6 7 8 9 |
1# free total used free shared buff/cache available Mem: 3845876 3514732 83404 620 247740 152864 Swap: 0 0 0 2# free total used free shared buff/cache available Mem: 3845876 3595148 83404 620 167324 152864 Swap: 0 0 0 |
した。Requests memory 的には、実体と関係ないただの要求容量の合計値なので、これは想定通り。そして、やはりメモリ状況をみると、Node のそれが見えるので共通した内容になる。
次に、Pod 1 がパンパンの状態で、Pod 2 で同様のプロセスを1つ起動してみる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
1# ps axuw | grep python root 41 0.0 13.5 534800 520280 pts/0 S 07:27 0:00 python3 memory.py root 42 0.0 13.5 534800 520280 pts/0 S 07:27 0:00 python3 memory.py root 43 0.0 13.5 534800 520280 pts/0 S 07:27 0:00 python3 memory.py root 44 0.0 13.5 534800 520284 pts/0 S 07:27 0:00 python3 memory.py root 45 0.0 13.5 534800 520288 pts/0 S 07:27 0:00 python3 memory.py root 46 0.0 13.5 534800 520284 pts/0 S 07:27 0:00 python3 memory.py 2# python3 memory.py aaapid = 45 mem = 531087360 # ここで両Podがフリーズ 1# で1つのpython3 がkillのエラー出力 2# でpython3 が正常に起動 |
1Pod での検証と同じような結果になった。オーバー時にフリーズするが、kill られたあとは処理が正常に完了し、シェルも操作できるようになる。
dmesg
どちらの Pod にも、時間を含めた完全に同じ内容が記録されていた。
1 2 3 4 5 6 7 8 9 |
# dmesg | grep "Out of memory" | less [26736.333997] Out of memory: Kill process 2391 (aws-k8s-agent) score 1000 or sacrifice child [26737.327358] Out of memory: Kill process 2278 (spot-monitor.sh) score 998 or sacrifice child [26738.240583] Out of memory: Kill process 2278 (spot-monitor.sh) score 998 or sacrifice child [26739.312387] Out of memory: Kill process 2347 (bash) score 998 or sacrifice child [26740.208250] Out of memory: Kill process 2283 (tail) score 998 or sacrifice child [26743.272745] Out of memory: Kill process 2243 (busybox) score 998 or sacrifice child [26744.655477] Out of memory: Kill process 18372 (python3) score 862 or sacrifice child |
NoLimit において Node上ではどのようなことが起きているか
まず、メモリオーバーが起きる前の、Node上でのプロセスの下の方を記録。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
root 23178 0.0 0.1 7512 3976 ? Sl 07:54 0:00 docker-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/d root 23194 0.0 0.0 8920 3568 ? Sl 07:54 0:00 docker-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/d root 23204 0.0 0.0 11620 2688 ? Ss 07:54 0:00 bash /app/install-aws.sh root 23244 0.0 0.0 1620 4 ? Ss 07:54 0:00 ash /srv/entrypoint.sh root 23306 0.0 0.0 2224 1656 ? S 07:54 0:00 /bin/bash /srv/spot-monitor.sh root 23313 0.0 0.0 1552 4 ? S 07:54 0:00 tail -f /dev/null root 23321 0.1 0.8 143032 34596 ? Sl 07:54 0:02 /app/aws-k8s-agent root 24241 0.0 0.0 0 0 ? I 08:00 0:00 [kworker/u30:2] root 24356 0.0 0.0 0 0 ? I 08:01 0:00 [kworker/0:0] root 25131 0.0 0.0 0 0 ? I 08:05 0:00 [kworker/1:2] root 25546 0.0 0.0 0 0 ? I 08:08 0:00 [kworker/u30:1] root 25554 0.0 0.2 148524 8580 ? Ss 08:08 0:00 sshd: ec2-user [priv] ec2-user 25564 0.0 0.1 148524 4780 ? S 08:08 0:00 sshd: ec2-user@pts/0 ec2-user 25566 0.0 0.0 122180 3544 pts/0 Ss 08:08 0:00 -bash root 26042 0.0 0.0 0 0 ? I 08:10 0:00 [kworker/1:1] root 26930 0.0 0.0 0 0 ? I 08:16 0:00 [kworker/1:0] root 27176 0.0 0.0 1552 4 ? S 08:17 0:00 sleep 5 |
まさに死んだ瞬間を記録。ただし、Pod同様、一時的にフリーズするので、フリーズ解除直後の記録。
めちゃ systemd が復活の儀をしているのがわかる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
root 26042 0.0 0.0 0 0 ? I 08:10 0:00 [kworker/1:1] root 26930 0.0 0.0 0 0 ? I 08:16 0:00 [kworker/1:0] root 27343 2.9 13.4 534800 518356 pts/0 S 08:18 0:00 python3 memory.py root 27358 34.6 13.4 534800 518772 pts/0 S 08:18 0:03 python3 memory.py root 27361 0.0 0.0 0 0 ? I 08:18 0:00 [kworker/0:1] root 27362 0.0 0.0 0 0 ? I 08:18 0:00 [kworker/1:3] root 27459 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/u30:0] root 27461 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/u30:3] root 27512 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/1:4] root 27513 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/1:5] root 27514 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/1:6] root 27515 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/1:7] root 27516 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/1:8] root 27517 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/1:9] root 27518 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/1:10] root 27519 0.0 0.0 42612 920 ? S 08:19 0:00 /usr/lib/systemd/systemd-udevd root 27520 0.0 0.0 42612 2164 ? S 08:19 0:00 /usr/lib/systemd/systemd-udevd root 27521 0.0 0.0 42612 2164 ? S 08:19 0:00 /usr/lib/systemd/systemd-udevd root 27522 0.0 0.0 42612 2164 ? S 08:19 0:00 /usr/lib/systemd/systemd-udevd root 27523 0.0 0.0 42612 2164 ? S 08:19 0:00 /usr/lib/systemd/systemd-udevd root 27524 0.0 0.0 42612 2100 ? S 08:19 0:00 /usr/lib/systemd/systemd-udevd root 27525 0.0 0.0 42612 2096 ? S 08:19 0:00 /usr/lib/systemd/systemd-udevd root 27526 0.0 0.0 42612 1972 ? S 08:19 0:00 /usr/lib/systemd/systemd-udevd root 27527 0.0 0.0 42612 1908 ? S 08:19 0:00 /usr/lib/systemd/systemd-udevd |
さらに数秒後、何事もなかったようにしれっと色んなプロセスが復活した。
1 2 3 4 5 6 7 8 9 10 |
root 27517 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/1:9] root 27518 0.0 0.0 0 0 ? I 08:19 0:00 [kworker/1:10] root 27530 0.0 0.0 7384 3188 ? Sl 08:19 0:00 docker-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/d root 27540 0.0 0.0 8792 3296 ? Sl 08:19 0:00 docker-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/d root 27580 0.0 0.0 11620 2560 ? Ss 08:19 0:00 bash /app/install-aws.sh root 27610 0.0 0.0 1620 4 ? Ss 08:19 0:00 ash /srv/entrypoint.sh root 27651 0.0 0.0 2220 1696 ? S 08:19 0:00 /bin/bash /srv/spot-monitor.sh root 27652 0.0 0.0 1552 4 ? S 08:19 0:00 tail -f /dev/null root 27676 1.5 0.8 143032 31604 ? Sl 08:19 0:00 /app/aws-k8s-agent root 27740 0.0 0.0 1552 4 ? S 08:19 0:00 sleep 5 |
この時の dmesg は、Node も各Pod も、完全に同じ内容である。
Memory NoLimit は採用する価値があるかどうか
検証結果
大雑把だけどこんな感じ。急増する負荷に対して
例えば、新Nodeで起動した1Pod が、想定とする Requests 以上のリソースを必要とした時、NoLimit によって次のPodが起動するまでに余っているリソースを扱えることは、一見強度が上がるように見える。トラフィックの上がり方が中規模ならば、その Pod は Node の 1/2 程度のリソースを食う程度で収まるかもしれないが、7/8 くらい食った場合はどうなるだろうか。次に起動してくる Pod によって、おそらく Node での OOM_KILLER が起こることになる。
どの程度 Turbo Boost するかによって、無事で済むか、Node破壊が起こるか、のような不安定な運用は採用できるはずがない。
アプリケーションのメモリ変動
そもそも、WEBサーバーやアプリケーション・サーバーといったデーモンが、どのようにメモリを扱うのか、が先にくる話だったりする。常駐プロセス(やスレッド)数が一定なのか、動的なのか。動的ならば最大値はいくらなのか、不要になった分はどれくらいの期間で削減されていくのか。
仮に、動的 かつ NoLimit で運用したとして、ブースト耐久できるし、複数Podのメモリ量合計値が Node 容量を上回らないように増減する・・・そんなものは存在しないし、工夫で確実になんとかなるものでもないだろう。
ただでさえ、アプリケーション・プロセスのメモリ容量は、想定以上に膨れることが多いのに、さらに不安定要素を組み込んでいくのは、あまりに現実的ではない。
Node の死だけは避けたい
Node の死 = Podの死 に等しいとすると、Pod, Node とスケールアウトした時、Node が死にかけるとそこのリソースが一時的にいなくなり、スケールアウトとして機能しないことになる。
しかし、Node さえ生きていれば、仮に Pod がいっぱいいっぱい起動してリソース的にも限界だとしても、スケールアウトしてリソースが増加しているという事実は変わらない。
Node の生き死にが不安定ということは、スケールアウトの機能も不安定になるということなので、たとえ全体のキャパシティオーバーが地獄だとしても、少しずつスケールアウトして解決に向かっていくという安定さだけは確保したいところだ。
結論。
Limits Memory はつける。
Pod 内での最大使用メモリ容量の試算を頑張る。できれば指定リソースとアプリケーション・サーバー設定による、1Pod あたりの性能── 例えば同時接続数や、処理可能な平均 req/s などの試算精度を向上していく。
Autoscaling のリスクについては、また別途、試算と調整をする。
風邪気味だったので文章が適当だけど、穴埋めが1つ進んだのでよしとする。
にんげんだもの。