Kubernetesは、コンテナアプリケーションをデプロイするためのオーケストレーションツールです。Kuberenetesは分散環境におけるスケーラブルなコンテナ実行環境をつくるための、さまざまな機能が提供されています。
もともとはGoogleが開発したBorgをもとにOSS化したものですが、今日ではマイクロソフトや(ry
Kubernetesをつかうとステートレスでマイクロサービス的なアプリケーションを1日に何度もデプロイでき、スパイクアクセスがきても水平スケールが容易なので、大規模Webシステムでスケーラブルな基盤を作れる、というのは広く知られています。
一方、Kubernetesには「Resource Requests」という機能があり、これはPodをデプロイする時に必要とするリソース(CPU/メモリ)を指定できるものです。これにより、Kubernetesクラスタのリソースの使用率を高め、効率よくアプリを稼働させることができるのも、うれしさの一つです。
Kubernetesではコンテナが使用するコンピューティングリソースを明示的に要求したり制限をかけたりすることができます。
リソース要求(Resource Request)
- コンテナに対して指定したリソースが使えるように保証するしくみ
- アプリを動かすのに最低限必要なリソースを指定
リソース制限(Resurce Limits)
- コンテナのリソース使用量の上限を決めるしくみ
- アプリケーションが使用する可能性のある最大リソースを指定
ということで、、、まずはメモリのリソース要求(Resource Request)ついて、
- コンテナアプリが使うメモリを明示的に要求するとクラスタ上でどういう動きをするのか?
- メモリをたくさん使う別アプリが邪魔してきたとき、きちんとシステムは動作するのか?
が気になり、簡単な検証をしてみました。
前提読者
- Dockerの使い方が分かる人
- クラウドマネージド(AKSまたはGKE)でKubernetesクラスタが構築できる人
- Kubernetesの基本コマンドを知っている人
実験環境
AzureのKubernetesのフルマネージドサービスであるAKSを使います。公式サイトをもとにKubernetesクラスタを構築しました。
クイック スタート:Azure Kubernetes Service クラスターをデプロイする | Microsoft Docs
ここでは、次の構成のクラスタで実験します。
MasterもNodeもkubernetes1.9.6で検証しています。
コンテナアプリが使うメモリを明示的に要求するとクラスタ上でどういう動きをするのか?
Kubernetesがシステムで使用するメモリ量を確認
今回のクラスタはAzure VMの「Standard_D1_v2」をNodeにつかっています。Standard_D1_v2は1台当たり約3.3GBのメモリが使えます。
念のため、次のコマンドで確認します。
$ kubectl describe node Name: aks-nodepool1-12354740-1 Roles: agent Labels: agentpool=nodepool1 ~中略~ Capacity: memory: 3501592Ki Allocatable: memory: 3399192Ki ~中略~ Name: aks-nodepool1-12354740-2 Roles: agent Labels: agentpool=nodepool1 Capacity: memory: 3501592Ki Allocatable: memory: 3399192Ki
Node上ではkube-proxy等も動作しているため、Podを1つも動作させていない状態でも、223M/499Mのメモリを使用しています。
$ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% aks-nodepool1-12354740-1 28m 2% 223Mi 6% aks-nodepool1-12354740-2 128m 12% 499Mi 15%
アプリを動かすのに必要なメモリを要求してPodを動かす
まずは、Resource Requestの動きを確認します。コンテナに制限をかけるには[resources:]-[requests:]で値を指定します。 次の例(resource-pod.yaml)は、「nginx」の公式イメージをもとに動かしたコンテナが1つ含まれるPodをテンプレートとして定義し、それを「replicas: 2」つまり2つのPodがKubernetesクラスタ上にスケジューリングされる定義になります。
その際、メモリのリソース要求として[memory: 1.5Gi]を指定します。つまりこれはKubernetes に対して「このPodにあるNginxコンテナは1.5Giのメモリ必要だからよろしく!」を宣言しています。
apiVersion: apps/v1 kind: ReplicaSet metadata: name: reqest-exp spec: replicas: 2 selector: matchLabels: app: replicaset-temp template: metadata: labels: app: replicaset-temp spec: containers: - name: pod-sample image: nginx resources: requests: memory: 1.5Gi
これを次のコマンドを実行してクラスタ上でリソースを作ります。
$ kubectl create -f resource-pod.yaml replicaset.apps "reqest-exp" created
PodがそれぞれNode1とNode2で1つずつ動いています。
$ kubectl get po -o wide NAME READY STATUS RESTARTS AGE IP NODE reqest-exp-9kztw 1/1 Running 0 55s 10.244.2.153 aks-nodepool1-12354740-2 reqest-exp-b4t8h 1/1 Running 0 55s 10.244.3.184 aks-nodepool1-12354740-1
絵で描くとこんな感じ。
ただ、起動しているNginxは何も仕事をしていないので、実際のメモリ使用量はほとんど増えていません。
$ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% aks-nodepool1-12354740-1 33m 3% 251Mi 7% aks-nodepool1-12354740-2 133m 13% 505Mi 15%
要求されたメモリが実際のメモリを超えるとどうなるか
次のコマンドを実行して、Podのレプリカ数を2から3に増やします。どうなるかをみてみます。
$ kubectl scale --replicas=3 -f resource-pod.yaml replicaset.apps "reqest-exp" scaled
Node1に2つのPod、Node2に1つのPodが動いています。まだ定義ファイルで要求した総メモリ量は、実際のクラスタのメモリ量を超えていません。
$ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE reqest-exp-9kztw 1/1 Running 0 8m 10.244.2.153 aks-nodepool1-12354740-2 reqest-exp-b4t8h 1/1 Running 0 8m 10.244.3.184 aks-nodepool1-12354740-1 reqest-exp-wzg7t 1/1 Running 0 43s 10.244.3.185 aks-nodepool1-12354740-1
絵で描くとこんな感じです。
次はレプリカ数を3から4に変更します。
$ kubectl scale --replicas=4 -f resource-pod.yaml replicaset.apps "reqest-exp" scaled
4つ目のPodである「reqest-exp-fgrpn」はどこのNodeでも起動せず「Pending」になっているのが分かります。
$ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE reqest-exp-9kztw 1/1 Running 0 13m 10.244.2.153 aks-nodepool1-12354740-2 reqest-exp-b4t8h 1/1 Running 0 13m 10.244.3.184 aks-nodepool1-12354740-1 reqest-exp-fgrpn 0/1 Pending 0 12s <none> <none> reqest-exp-wzg7t 1/1 Running 0 5m 10.244.3.185 aks-nodepool1-12354740-1
またまた絵で描くとこんな感じ。
しつこいですが、実際のメモリ使用量はほとんど増えていません。つまり実際はアイドル状態なのに4つ目のPodはスケジューリングされていないということをいっています。これは、4つ目のPodを配置しようとしても、メモリの空きがマニュフェストで定義した「1.5GB」を確実に確保できるスペースがNode上にないからこのような状態になっています。
$ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% aks-nodepool1-12354740-1 30m 3% 259Mi 7% aks-nodepool1-12354740-2 133m 13% 507Mi 15%
なぜ、4つ目のPodはスケジューリングされなかったのか
KubernetesのResource Requestsは、Podに対して指定したリソースが使えるように保証する仕組みです。
この値はPodをNodeにスケジューリングするときに使われます。Resource Requestsを指定したコンテナを含むPodを作成すると、KubernetesのスケジューラーはNodeを選択して実行します。その際、各Nodeには、各リソースタイプの最大容量をもっていてスケジューラは、各リソースタイプに対して、スケジュールされたコンテナのリソース要求の合計がノードの容量よりも少ないことをチェックします。たとえ実際のメモリ使用率は非常に低くても、スケジューラはチェックが失敗した場合はNodeにPodを配置しません。
Managing Compute Resources for Containers - Kubernetes
なおこのResource Requestsの設定はPodではなく、コンテナに対して行われます。今回指定したメモリはバイト単位で設定でき、E/P/T/G/M/K(Ei/Pi/Ti/Gi/Mi/Ki)のいずれかを使用して、メモリを整数または固定小数点整数として指定します。
今回のケースでは、実際のメモリがたくさん空いているにもかかわらずPodは配置されませんでした。Nodeのメモリの最大容量は分かっているので、あらかじめ基盤の方式設計段階でNodeにぎゅうぎゅうにPodがスケジューリングされることを避けることができます。
ただ、、、今回は同じサイズのNodeで検証しているのできちんと2台でスケジューリングされそうな気もしますが、なにか見落としているのかもわかりません。というわけで、ソースコードをきちんと読みたいと思います。 (理由をご存知の人がいればおしえてください)
追記:まかべ@マイクロソフトさんから教えてもらいました!ご指摘通り、Node1だけheapsterが動いておりました。すっきり理解しました。 (以下原文ママ)前半のMemory Requestsについてですが、片方のNodeだけheapsterが動いていて非対称なので、その分メモリが296Mi足りなくて4つめのPodが行儀よく待っちゃってるかも。heapster分を考慮してPodごとのRequestを1.3Giにしたら、4つ動きました。
メモリをたくさん使う別アプリが邪魔してきたとき、きちんとシステムが動作するのか?
Kubernetesでは「Resource Requests」で宣言した分のメモリをきっちり確保してPodをスケジューリングしているのが分かりました。このResource Requestsは、実際のメモリの使用量ではなく、クラスタに割り当てられているリソースに基づいていることもわかりました。それでは、メモリリソースが余っているクラスタ上に、「Resource Requests」の宣言なしにメモリをたくさん使う野良Podががんがんデプロイされたらどうなるでしょうか?
まず実験のため、次のようなメモリを1.0GBつかうDockerイメージを作ります。これをビルドしてmem-stressという名前のDockerイメージを作ります。
FROM ubuntu:latest RUN apt-get -y update && apt-get -y upgrade RUN apt-get -y install stress # 1つのstressプロセスが起動し、プロセスあたり1GBのメモリの負荷をかける CMD ["stress", "-m","1", "--vm-bytes" ,"1G","--vm-hang","0","-q"]
これをDocker HubやAzure Container Registryなどのレジストリで公開します。
次にこのDockerイメージを動かすPodを起動するReplicaSetのマニュフェスト(mem-stress.yaml)を作ります。この例では上記のDockerfileをビルドした「image: asashiho/mem-stress:latest」から生成するコンテナが1つ含まれたPodをテンプレートで定義し、そのPodを2つ動かすため「replicas: 5」と定義しています。クラスタには理論上各Nodeに3.5GBずつメモリがありますので、ざっくり5つくらいの野良Podを起動してみます。
このマニュフェストのポイントは、上のNginxのPodと異なりメモリに対するリソース要求(Resource Request)を指定していないことです。 このコンテナを作った張本人である私はアプリがメモリを1.0G使うことを知っていますが、Kubernetes はどれだけリソースを使うコンテナが入ったPodなのか一切知らないのです。
apiVersion: apps/v1 kind: ReplicaSet metadata: name: mem-exp spec: replicas: 5 selector: matchLabels: app: mem-stress-temp template: metadata: labels: app: mem-stress-temp spec: containers: - name: pod-sample image: asashiho/mem-stress:latest
次のコマンドを実行すると、新しく1Gのメモリを使うPodが5つ動く、、、、、はずです
$ kubectl create -f mem-stress.yaml replicaset.apps "mem-exp" created
なるほど。
「mem-exp-xxx」のPodは4つしか起動せず、うちいずれかの1つはCrashLoopBackOff―Errorで再起動を繰り返しています。
$ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE mem-exp-6dcgh 0/1 CrashLoopBackOff 2 1m 10.244.3.188 aks-nodepool1-12354740-1 mem-exp-dnwjk 1/1 Running 1 1m 10.244.3.186 aks-nodepool1-12354740-1 mem-exp-grxvz 1/1 Running 0 1m 10.244.2.155 aks-nodepool1-12354740-2 mem-exp-v5j29 1/1 Running 0 1m 10.244.2.154 aks-nodepool1-12354740-2 mem-exp-x9gxl 1/1 Running 2 1m 10.244.3.187 aks-nodepool1-12354740-1 reqest-exp-9kztw 1/1 Running 0 26m 10.244.2.153 aks-nodepool1-12354740-2 reqest-exp-b4t8h 1/1 Running 0 26m 10.244.3.184 aks-nodepool1-12354740-1 reqest-exp-fgrpn 0/1 Pending 0 13m <none> <none> reqest-exp-wzg7t 1/1 Running 0 18m 10.244.3.185 aks-nodepool1-12354740-1
絵にかくと次の感じです。
Podのログを確認するとエラーが出ているのが分かります。
$ kubectl logs mem-exp-6dcgh stress: FAIL: [1] (415) <-- worker 5 got signal 9 stress: FAIL: [1] (421) kill error: No such process stress: FAIL: [1] (451) failed run completed in 39s
システム全体のメモリ使用量はきちんと上がっています。
$ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% aks-nodepool1-12354740-1 33m 3% 2275Mi 68% aks-nodepool1-12354740-2 127m 12% 2573Mi 77%
これを、40分ほど放置します。その間に夕飯のしたくを。
$ kubectl get po -o wide NAME READY STATUS RESTARTS AGE IP NODE mem-exp-6dcgh 0/1 CrashLoopBackOff 11 44m 10.244.3.188 aks-nodepool1-12354740-1 mem-exp-dnwjk 1/1 Running 6 44m 10.244.3.186 aks-nodepool1-12354740-1 mem-exp-grxvz 1/1 Running 0 44m 10.244.2.155 aks-nodepool1-12354740-2 mem-exp-v5j29 1/1 Running 0 44m 10.244.2.154 aks-nodepool1-12354740-2 mem-exp-x9gxl 1/1 Running 6 44m 10.244.3.187 aks-nodepool1-12354740-1 reqest-exp-9kztw 1/1 Running 0 1h 10.244.2.153 aks-nodepool1-12354740-2 reqest-exp-b4t8h 1/1 Running 0 1h 10.244.3.184 aks-nodepool1-12354740-1 reqest-exp-fgrpn 0/1 Pending 0 56m <none> <none> reqest-exp-wzg7t 1/1 Running 0 1h 10.244.3.185 aks-nodepool1-12354740-1
何が起こっているか、よーく見てみます。
1.Node1にスケジューリングされた野良Pod3つ
「mem-exp-6dcgh」と「mem-exp-dnwjk」と「mem-exp-x9gxl」はPodを稼働させたくても実際のメモリが足りないため、うち1つのいずれかが再起動を繰り返し「CrashLoopBackOff」のステータスになっています。
2.Node2にスケジューリングされた野良Pod2つ
「mem-exp-grxvz」と「mem-exp-v5j29」は再起動されることもなく順調に稼働しています。
3.マニュフェストでメモリの使用量をあらかじめ宣言したPod4つ
「reqest-exp-9kztw」は再起動されることなく3つが稼働中、そして空き容量待ち「reqest-exp-fgrpn」はPenddingでお行儀よく待っていること分かります。
なるほど!
どのようなしくみでPodがスケジューリングされたのか
KubernetesのResource Requestsでメモリを記述しないと、「Podがいつメモリ不足で終了されても構わない」という意味になります。なにがそうさせているかというと、、、、、
Pod QoSについて
KubernetesはPodに対して3つのQuality of Service (QoS)クラスを提供しています。このQoSは、今回検証したResource Requestsとリソースの上限を決めるResource Limitsの2つから次の条件でclassが決まります。
BestEffort
pod内のどのコンテナにもResource RequestsとResource Limitsが設定されていない時に設定される
Burstable
BestEffortもGuaranteedも設定されていないときに設定される
Guaranteed
CPUとメモリの両方に、Resouce RequestsとResource Limitsがセットされていること/Pod内のそれぞれのコンテナにセットされていること/Resouce RequestsとResource Limitsの値がそれぞれ同じであること、で設定される
このQoSの確認は、kubectl describeでできます。例えば「mem-exp-96vdn」のQoSを確認するときは次のコマンドになります。
$ kubectl describe pod mem-exp-96vdn |grep QoS
QoS Class: BestEffort
ちなみにpod内に複数のコンテナが存在する場合はまず各コンテナに上記のルールに従ってQoSを割り当てます。全てのコンテナがBestEffortならPodのQoSはBestEffortとなり、全てのコンテナがGuaranteedならPodのQoSはGuaranteedになるとのことです。そして、いずれの条件にも当てはまらない場合BestEffortになるとのことです。
今回の検証では、1.5GのResouce Requestsを指定したPodのQoSは「Burstable」、なにも指定しない野良Podは「BestEffort」が設定されていました。
Nodeのメモリ上限に達した場合、どういった判断でkillされるか
Kubernetesクラスタは魔法の箱ではないので、実際のメモリを超えるアプリは実行できません。今回の検証のNode1でなにがおきていたかというと。。。
Kubernetesでは、QoSに従ってどのPodのコンテナがkillされるか決まります。最も優先度が低く最初にkillされるのはBestEffort、つぎにはBurstable、最後にGuaranteedがkillされます。このGuaranteedはsystem processがメモリを必要とした場合のみkillされます。
もし同じQoSクラスだった場合、OutOfMemory (OOM) scoreのよってどのプロセスをkillするかを比較して決めているようです。決して、適当にkillしているわけではなく、Kubernetesはちゃんと忖度しながらkillしています。
今回の検証では、メモリが足りていないNode0上で、優先度の低い野良Podがkillされ、再起動されていました。この再起動のルールも、実はとても興味深い処理方式で動いています。こちらは後日別ブログで。
まとめ
Kubernetesでは、アプリケーション(コンテナ)が使うメモリに制限をかけると、クラスタ上の理論的なリソースが配分されてPodがスケジューリングされることがわかりました。また、Podの優先度が高くなり、クラスタの実際の使用メモリが多くなったときも、killされる可能性が低くなることが分かりました。
検証していませんが、ServiveやIngressでサービスディスカバリーを定義しておけば、今回のケースではいずれかのPodは動いているはずですので、クラスタ外部からのアクセスをみてみると、サービスが正しく動作している状態を維持できているはずです。
すごい。
なお、今回は設定しませんでしたがメモリにRequest Limitsを与えたときの動きや、CPUに関しても後日ひまを見つけて検証してまとめてみたいと思います。
というわけで、クラウドのKubernetesマネージドサービスを使うと、クラスタそのものの構築やMasterのメンテはお任せできますので、分散基盤のめんどうがみられるレベルのインフラエンジニアがいなくても、快適なデプロイ環境を作れますが、一方、、、知識としてのインフラ技術は、ある程度勉強しておかないと、障害時に中でなにがおこっているのか、だけでなく適切な基盤方式設計ができない、ということにもなります。難しいですが勉強だいじ。
おわり