コンテナは便利イコール運用も簡単。とはならず、むしろ仕組みの理解や設定調整は、旧来のシステム構成よりもしっかり取り組まなければいけないイメージがよろしいかと思います。
適当に運用がうまくいくのは小規模までで、それ以降はおそらく偶然上手くいってるだけか、突然の死を向かえるかの二択になる気がしています。特に今回のリソース周りは旧来のシステムとはけっこう感覚が異なるので、脳みそのアップデートが必要なくらい大事な部分だと感じています。
Resource の requests / limits と CPU / Memory
リンク集
この辺はちゃんと読んでおいたほうがよいです。- Managing Compute Resources for Containers – Kubernetes
- KubernetesのResource RequestsとResource Limitsについて – Qiita
- Kubernetesで実際のメモリを超えるコンテナアプリを動かすと、どうなるか? – あさのひとりごと
本記事では、細かいところはリンク先に任せて、実際の運用ではどう動くの?どういう設定にしたらいいの?ってのを考察していきます。
Requests CPU
実際の使用量とは関係なく、仮想的にCPUリソースを確保するための指定値です。例えば、8vCPU = 8000m の Node にて、
resources > requests > cpu = 1000m
で指定したPodを複数起動しようとすると、
7pods (1000m * 7 = 7000m)
まで起動できることになります。
なぜ 8pods じゃないかというと、namespace = kube-system に aws-node や kube-proxy など基盤用Pod がいて、数m ~ 数百m を他で確保しているからです。
この Requests CPU は、指定が必須となります。理由は、リソース使用量を管理する metrics-server が値を取得したり、HPAのCPU条件判断に必要だからです。シンプルにみえてなにかと面倒くさい調整項目なので可愛がってあげましょう。
Requests Memory
考え方は Requests CPU と同じで、そのメモリ版となります。例えば、7.5GiB メモリを搭載した Node にて、
resources > requests > memory = 2GiB
で指定したPodを複数起動しようとすると、
3pods (2GiB * 3 = 6GiB)
まで起動できることになります。
もちろん、基盤用Pod で同じように Requests Memory が指定されていれば、その分は差っ引かれるので、ギリギリちょうどでの起動はできません。aws-node や kube-proxy では指定されていないので、それだけならギリいっぱいでの起動ができそうですが、自前の DaemonSet があるなら微量を確保するでしょうし、そもそもメモリをギリってのは避ける考えがベターです。
また、Node の全メモリ容量に対して、Pod が使えるメモリ容量ってのは決まっていて、クラスタでちゃんと大事な分は確保しているのが、下記ページあたりでわかります。
- Reserve Compute Resources for System Daemons – Kubernetes
- Does your application kill AWS EKS worker node? — Set Node Allocatables
- kubelet – Kubernetes
EKS でも調整できるようですが、この辺にまで手を出すのは、よっぽど検証や実戦が進んでからの話になるでしょう。
Limits CPU
こちらは実際に使用されるCPU使用量の最大値です。Requests CPU 以上の値にするか、指定なし=上限なし にします。この項目は、基本的に指定しないで運用します。という前提ありきではありますが、念の為にこの挙動についても調べてみました。
指定しない場合
まず、指定しないとどうなるかというと、例えば Node 8000m において、Pod Requests 1000m で起動した場合、この Pod は 8000m 弱の 7900m台 まで活性化することができます。もし 2Pod 同時に最大まで負荷をかければ、どちらも 4000m弱 を推移します。複数Pod の場合、だいたいPod数で割ったCPUリソース値で推移しますが、Pod数が多い場合は平均化せず倍以上の差がついて稼働することもままあります。詳しく知りたい場合はこの辺を読むとよいですが、スケールアウトを前提とした同内容のPod群の場合は、先にスケールアウトの性質や挙動について突き詰めるほうが大事ではあります。
指定する場合
そして指定した場合ですが、期待したとおりに制限がかかります。Limits CPU を 2000m にすれば 2000m まで、200m にすれば 200m までしか Pod はリソースを使えなくなります。この項目を扱うのは、異なる内容のPodが同Node内に混在し、それぞれが一定以上のCPUリソースを確保したい場合に必要となるでしょう。しかし、WEBサーバーのような同内容Pod群の場合は、次のPodが起動してくるまでは、空いているリソースの分は消費してしまった方が、サービス全体のレスポンス品質としては良くなるので、制限するメリットはほぼありません。
ちなみに、指定値を 1000m 未満にする場合、その性質について理解しておいたほうがよいです。
1000m は vCPU = 1 を丸々利用できるに等しい意味ですが、たいていの処理は1処理につき1スレッドを、可能な限り最大までフル稼働させて処理を終わらせようとします。例えば 1000m あれば、1秒かかる処理は1秒で完了できますが、同じ処理を 500m で稼働させると、2秒かかってしまいます。200m にしたら、5秒です。
単発の処理だけで考えれば、1000m 以上にした場合は通常通りの速度で完了しますが、未満にした場合は N/1000 の速度しかでない = 1000/N 倍の時間がかかる、ということになってしまいます。管理系などの、どれだけ軽い処理をするとしても、Limits を設けてその処理時間自体が伸びることは、ほとんどの場合は望まないことでしょうから、Limits をかける場合はその性質を理解しておくべきです。
ちな、こんな感じで遊びました。
1 2 3 4 5 6 7 8 |
COUNT=0 while true; do COUNT=`expr $COUNT + 1` if [ `expr $COUNT % 10000` -eq 0 ]; then echo `date` $COUNT fi done |
このスクリプトを、Pod の Limits CPU 指定無し, 500m, 200m で回しつつ、Pod のCPUを表示したりすると……こうじゃ。
指定無し
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 |
# だいたい9~10秒毎に10000 Tue Oct 29 05:54:22 UTC 2019 10000 Tue Oct 29 05:54:32 UTC 2019 20000 Tue Oct 29 05:54:41 UTC 2019 30000 Tue Oct 29 05:54:50 UTC 2019 40000 Tue Oct 29 05:54:59 UTC 2019 50000 # Pod での外見的な thread 構成は Node と一緒、で負荷は各所へバラバラ $ top -d1 Mem: 1943332K used, 14007220K free, 548K shrd, 2704K buff, 1322932K cached CPU0: 13% usr 23% sys 0% nic 64% idle 0% io 0% irq 0% sirq CPU1: 6% usr 12% sys 0% nic 80% idle 0% io 0% irq 0% sirq CPU2: 8% usr 18% sys 1% nic 72% idle 0% io 0% irq 0% sirq CPU3: 8% usr 17% sys 0% nic 75% idle 0% io 0% irq 0% sirq $ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-131-11-148.ap-northeast-1.compute.internal 56m 2% 432Mi 5% ip-10-131-18-76.ap-northeast-1.compute.internal 1114m 27% 485Mi 3% $ kubectl top pod NAME CPU(cores) MEMORY(bytes) node-manager-c9nxb 1m 3Mi node-manager-jmhrr 2m 4Mi production-test-57954cf55f-ls8cq 1066m 82Mi |
500m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# だいたい倍の20秒毎に10000に伸びた Tue Oct 29 05:58:32 UTC 2019 10000 Tue Oct 29 05:58:52 UTC 2019 20000 Tue Oct 29 05:59:12 UTC 2019 30000 $ top -d1 Mem: 1940428K used, 14010124K free, 548K shrd, 2704K buff, 1322992K cached CPU0: 7% usr 9% sys 0% nic 82% idle 0% io 0% irq 0% sirq CPU1: 3% usr 4% sys 0% nic 92% idle 0% io 0% irq 0% sirq CPU2: 3% usr 8% sys 0% nic 87% idle 0% io 0% irq 0% sirq CPU3: 4% usr 8% sys 0% nic 87% idle 0% io 0% irq 0% sirq $ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-131-11-148.ap-northeast-1.compute.internal 61m 3% 433Mi 5% ip-10-131-18-76.ap-northeast-1.compute.internal 532m 13% 485Mi 3% $ kubectl top pod NAME CPU(cores) MEMORY(bytes) node-manager-c9nxb 2m 4Mi node-manager-jmhrr 2m 4Mi production-test-5cdfc5649b-9845x 490m 81Mi |
200m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# だいたい 52秒毎に 10000 であくびがでるぜ Tue Oct 29 06:01:33 UTC 2019 10000 Tue Oct 29 06:02:25 UTC 2019 20000 Tue Oct 29 06:03:17 UTC 2019 30000 $ top -d1 Mem: 1942188K used, 14008364K free, 548K shrd, 2704K buff, 1323116K cached CPU0: 2% usr 3% sys 0% nic 94% idle 0% io 0% irq 0% sirq CPU1: 1% usr 4% sys 0% nic 95% idle 0% io 0% irq 0% sirq CPU2: 2% usr 2% sys 0% nic 95% idle 0% io 0% irq 0% sirq CPU3: 1% usr 3% sys 0% nic 95% idle 0% io 0% irq 0% sirq $ kubectl top node NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% ip-10-131-11-148.ap-northeast-1.compute.internal 56m 2% 433Mi 5% ip-10-131-18-76.ap-northeast-1.compute.internal 227m 5% 486Mi 3% $ kubectl top pod NAME CPU(cores) MEMORY(bytes) node-manager-c9nxb 2m 4Mi node-manager-jmhrr 2m 4Mi production-test-5689699cc8-p46bd 190m 79Mi |
わりと綺麗に、指定値に対する性能比がみてとれます。
小さな管理系のPodだからって、なんとなく善意的な気持ちで小さな Limits つけちゃうと、知らんうちに無駄な遅さを発揮する恐れがあるので、基本はつけないで、わかってる人だけ使うかもな機能という位置づけで良いと思います。
Limits Memory
Limits CPU同様、実際に使用できるメモリの上限ですが…… 一応、流れ的に機能としては存在するのでしょうけど、ぶっちゃけメモリは使える容量が増減していい項目じゃないので、確定で Requests Memory と同値にしてしまってよいと思います。Pod のメモリ上限を確定させ、中で動くプロセスに必要なメモリ容量もきっちり把握して運用する、というところが安定運用に必要な一歩になるでしょう。
Horizontal Pod Autoscaling (HPA) でのCPU使用率
話は少し飛んで、HPA でのCPU使用率とRequests CPU の関係に移ります。まず、HPA には targetCPUUtilizationPercentage という設定項目があり、これは Pod のCPU使用率が何%を超えたらスケールアウトを発動してPod数を増やすかというものになっています。雰囲気は AutoscalingGroup の ScalingPolicy でやっていたことに近いのですが、感覚的にはちょっと違います。
HPA の詳細を見るとこんな表示を見れるのですが、
1 2 3 4 5 6 7 8 9 10 11 |
$ kubectl describe hpa Name: production-test Namespace: default ... Metrics: ( current / target ) resource cpu on pods (as a percentage of request): 96% (968m) / 50% Min replicas: 1 Max replicas: 2 Deployment pods: 2 current / 2 desired Conditions: ... |
この部分に注目してみましょう。
1 2 |
Metrics: ( current / target ) resource cpu on pods (as a percentage of request): 96% (968m) / 50% |
右の target と書かれている 50% は、まさに自分で指定した targetCPUUtilizationPercentage の値です。
では左の current はというと、(968m) の部分が対象Podsの、CPU使用量の平均値です。そして 96% の部分は、Requests CPU に対する使用比率を表しています。整理すると……
targetCPUUtilizationPercentage | 50 |
Requests CPU | 1000m |
CPU Average of Pods | 968m |
resource cpu on pods | 968m / 1000m * 100% ≒ 96% |
で、HPAとしては下記ページのこの計算によってPod数を変化させます。
1 |
desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )] |
上記式での desiredMetricValue とは、targetCPUUtilizationPercentage であり、
currentMetricValue は resource cpu on pods ということになります。
で、それを現在のPod数にかけて増減判断をする……と。
整理してみると普通な仕組みなんですが、初見は旧式と混同して脳が少々混乱してしまいました。
旧式との違い
EC2インスタンス群の AutoscalingGroup に対して、CPU 50% で発動と指定したら、その時に存在するEC2群・総リソースの 50% を超えたら増加するという意味であるのに対し、Pod群である Deployment に CPU 50% と指定しても、Node群のCPUに関係なく、あくまでPod群に仮割り当てした Requests CPU の 50% を超えたら増加する、という内容になります。しかも、Limits CPUなしの Pod は Node内の状況次第では、Requests CPU を遥かに超えたリソース消費もできるわけで、総リソース実体の状況と直結していない点が、最初は気持ち悪く感じるかもしれません。
ぶっちゃけ、HPA と cluster-autoscaler はよくできているので、なんとなく増減を任せていてもそれなりに動いてくれます。しかし、従来と異なる仕組みに適応して、エンジニアに重要な運用感覚ってやつを正すためにも、この辺の Autoscaling 的な話は、また別記事で掘り下げたいと思います。
Deployment での Resource 指定
Resource に関わる仕組みについては、いったんここまででまとまったとして、では具体的に Resource には何をどのように設定したらよいのか、という話になってきます。Deployment でいえば、この部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
spec: ... template: ... spec: ... containers: - env: ... resources: limits: memory: 8000Mi requests: cpu: 2000m memory: 8000Mi |
この設定をするにあたって、確定しているのはこれらです。
また、requests: cpu と memory の値の関係によって Node あたりの Pod数が確定します。
ResourceとPod数
例として、端数にすると分かりづらいので Node の CPU = 8000m, Memory 32GiB があるとします。Pod の Requests において、
CPU = 8000m, Memory = 32GiB にすると当然 1Pod だけが起動し、
CPU = 2000m, Memory = 8GiB にすると、ちょうど 4Pod まで起動し、
CPU = 1000m, Memory = 2GiB にするとCPU限界により 8Pod まで起動し、
CPU = 200m, Memory = 1GiB にするとメモリ限界により 32Pod まで起動できる
CPU は Limits をつけないので、最大でNode CPU に近いCPUリソースを消費できるポテンシャルはあるものの、基本的には Requests CPU まで発揮する、という考えで計算と運用をしていくことになります。
なので、Node のリソース量に対し、Pod の指定値は、CPU と Memory の両方が少し余裕を持ってちょうどよく使い切る、くらいの設定にすることが無駄の少ない設定になるといえるでしょう。そういう意味では、インスタンスタイプを c, m, r系 と混合させて扱うと、同vCPU帯においてメモリ容量比率が c : m : r = 2 : 4 : 8 となって無駄が生じることになるため、なにかしら黒魔法を考えるか、別カテゴリ混合を諦めるか、マネーisパワーかという話になりそうです。
で、ここまでまとめると、当然1つの疑問が湧いてきます。
『NodeとPodのリソース量と、
NodeあたりのPod数はどのくらいで運用するのが正着なの?』
です。
深いところまでくると、こういう疑問に詰まるであろうことは、コンテナに取り組む前からなんとなくわかるわけで、言ってしまえば環境によって正解は異なるわけでして。でも考えていかなければ話にならないので、長くなりすぎるので次回に続くのです。
ブログで Kubernetes を突っつくっては、こうなるって覚悟してましたよ、自分:-)