Carpe Diem

備忘録

Kubernetesのresource requests, limits

概要

Kubernetesには以下のフィールドでCPUやメモリを制限することが可能です。

  • spec.containers[].resources.limits.cpu
  • spec.containers[].resources.limits.memory
  • spec.containers[].resources.requests.cpu
  • spec.containers[].resources.requests.memory

ref: Managing Compute Resources for Containers - Kubernetes

cpu: 1cpu:2といった整数値はイメージが湧きやすいですが、

  • cpu: 100mといった時にどういう動きをするのか?
  • マルチコアのノード環境で↑の時、アプリケーションコードでマルチコア前提のものはきちんと動くのだろうか?

という疑問があったので調べてみました。

環境

サンプル

今回検証に使用したサンプルコードは以下です。

github.com

検証

まずは推測よりも計測です。

アプリケーションコード

以下のような

  • 最初にCPU数を出力
  • 多数のgoroutineを走らせる

プログラムを用意しました。

package main

import (
        "fmt"
        "net/http"
        "runtime"
)

func main() {
        cpus := runtime.NumCPU()
        fmt.Println("CPUs:", cpus)

        for i := 0; i < 100; i++ {
                goroutine()
        }

        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintf(w, "hello world")
        })

        http.ListenAndServe(":8000", nil)
}

func goroutine() {
        go func() {
                var counter int64
                for {
                        counter++
                }
        }()
}

goroutineを用意しているのは

christina04.hatenablog.com

で検証した時のように、スケジューラの挙動を確認するためです。

deployment

Kubernetesのdeployment.yamlは以下。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-resource
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: golang
          image: jun06t/show-cpus
#          env:
#          - name: GODEBUG
#            value: "scheddetail=1,schedtrace=1000"
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "200m"

実行

f:id:quoll00:20190622182203p:plain

  • CPUコア数:6
  • メモリ:4G

で検証してみます。

CPU数チェック

$ kubectl apply -f resource.yaml
deployment.apps "sample-resource" created

ログを見てみます。

$ stern sample
+ sample-resource-5bd456968-44kxv › golang
sample-resource-5bd456968-44kxv golang CPUs: 6

ちゃんと6つ検知していますね。

試しにreplicas: 2にしてpodを増やしてみます。

$ stern sample
+ sample-resource-5bd456968-44kxv › golang
+ sample-resource-5bd456968-prqnc › golang
sample-resource-5bd456968-prqnc golang CPUs: 6
sample-resource-5bd456968-44kxv golang CPUs: 6

どちらも6です。CPU数は全て検知できているようです。

GODEBUGでgoroutineの動きをチェック

先程のdeploymentの環境変数をアンコメントして実行します。

SCHED 11095ms: gomaxprocs=6 idleprocs=0 threads=9 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
  P0: status=1 schedtick=1 syscalltick=25 m=0 runqsize=12 gfreecnt=0
  P1: status=1 schedtick=2 syscalltick=0 m=3 runqsize=24 gfreecnt=0
  P2: status=1 schedtick=1 syscalltick=0 m=5 runqsize=49 gfreecnt=0
  P3: status=1 schedtick=1 syscalltick=0 m=4 runqsize=3 gfreecnt=0
  P4: status=1 schedtick=1 syscalltick=0 m=7 runqsize=5 gfreecnt=0
  P5: status=1 schedtick=1 syscalltick=0 m=8 runqsize=2 gfreecnt=0
  M8: p=5 curg=88 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M7: p=4 curg=85 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M6: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=true lockedg=-1
  M5: p=2 curg=54 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M4: p=3 curg=92 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M3: p=1 curg=79 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 spinning=false blocked=false lockedg=-1
  M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=17
  M0: p=0 curg=105 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 spinning=false blocked=false lockedg=-1
  G1: status=4(IO wait) m=-1 lockedm=-1
  G17: status=6() m=1 lockedm=1
  G2: status=4(force gc (idle)) m=-1 lockedm=-1

するとこの様になりました。Pが6つ、Mが9つあることからちゃんと全CPUを使っているようですね。

解説

requests, limitsはどうリソース制限しているのか。

Kubernetesのこのリソース制限はDockerの機能を利用しています。

Kubernets Docker
requests.cpu --cpu-sharesとして
limits.cpu --cpu-period: 100ms固定で
--cpu-quotaとして

cpu-sharesとは

cpu-sharesはCPUリソースを配分する際の重みです。
具体的に表すと以下です。

CPUコア数 コンテナ数 cpu-shares リソース配分
1 1 1024 1コアの100%
1 2 コンテナA: 1024
コンテナB: 1024
コンテナA: 1コアの50%
コンテナB: 1コアの50%
1 2 コンテナA: 2048
コンテナB: 2048
コンテナA: 1コアの50%
コンテナB: 1コアの50%
1 2 コンテナA: 1024
コンテナB: 3072
コンテナA: 1コアの25%
コンテナB: 1コアの75%
2 2 コンテナA: 1024
コンテナB: 1024
コンテナA: 2コアの50%
(=1コアの100%分)
コンテナB: 2コアの50%

まとめると

  • リソース配分はコンテナ数に影響される
  • 値は絶対値ではなく他の重みとの相対値
  • 全CPUが使用される

なので、リソースの下限値として説明されることが多いです。

cpu-period, cpu-quota

これはカーネルのCFS(Completely Fair Scheduler)を利用しています。

--cpu-periodで指定した時間の内--cpu-quotaで指定した時間までコンテナにCPUを割り当てる事が可能です。

k8sでは--cpu-period: 100msでセットされており、limitsの数値のx100--cpu-quotaにセットされます。
以下具体的な設定とリソース配分です。

CPUコア数 limits cpu-quata(ms) リソース配分
1 1 100 100ms中100ms使う
=1コアの100%
1 100m(=0.1) 10 100ms中10ms使う
=1コアの10%
2 1 100 100ms中100ms使う
=1コアの100%
=2コアの50%
2 2 200 200ms中200ms使う
=2コアの100%
=1コアの200%
4 2 200 400ms中200ms使う
=4コアの50%
=1コアの200%

まとめると

  • 値はCPU数に限らず絶対値
  • 全CPUが使用される

となります。 CFSその設定を超えた秒数CPUを使用した場合、次のperiodまで利用できなくなるので、CPUヘビーなアプリケーションであれば不可解な一時停止が発生する可能性があります。

まとめ

結論としてマルチコアであればrequests, limitsをどう設定してもマルチコアを使用するということが分かりました。
なのでアプリケーション側でそこを意識して何かする必要はなさそうです。

ソース