YasuBlog

中年インフラエンジニアの備忘録です。

Kubernetes の kube-reserved と system-reserved

Kubernetes のシステム用に割り当てられるリソースについて整理し、実際の挙動を確認しました。

1. Pod に割り当て可能なリソース

Node 上では OS, Kubernetes が動いており、その上で Pod が動いているため Pod が Node のリソースを全て使用すると問題(OS 全体に影響が出る、Kubernetes 自体が動作しなくなる等)が発生します。 それを回避するため、OS, Kubernetes 用リソース kube-reserved, system-reserved と、Node の最低空きリソース eviction-threshold があります。

Pod に割り当てられるリソースは Node のリソース量から、kube-reserved,system-reserved,eviction-threshold を引いた分になります。

f:id:dunkshoot:20220117203437p:plain

本記事では kube-reserved,system-reserved についてまとめています。eviction-threshold については別の記事でまとめます。

2. cgroup

kube-reserved,system-reserved の説明に cgroup が出てくるため、まずは cgroup について簡単に整理します。

cgroup とは Control Group の略で、プロセスをグループ化し CPU やメモリ等のリソースをグループ毎に制限する Linux カーネルの機能です。

cgroupfs という仮想ファイルシステム上で cgroup を操作します。cgroupfs は /sys/fs/cgroup にマウントされています。

eksctl コマンドで EKS Cluster を作成する - 俺のメモ帳 の記事で作成した EKS Node で確認すると cgroupfs が /sys/fs/cgroup にマウントされている事が確認できます。

$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
devtmpfs         1.9G     0  1.9G    0% /dev
tmpfs            1.9G     0  1.9G    0% /dev/shm
tmpfs            1.9G  760K  1.9G    1% /run
tmpfs            1.9G     0  1.9G    0% /sys/fs/cgroup
/dev/nvme0n1p1    30G  2.7G   28G    9% /
tmpfs            388M     0  388M    0% /run/user/1000

~省略~

$ mount -l | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)

/sys/fs/cgroup 配下のファイルで cgroup を管理しています。

$ ls /sys/fs/cgroup/
blkio  cpu  cpu,cpuacct  cpuacct  cpuset  devices  freezer  hugetlb  memory  net_cls  net_cls,net_prio  net_prio  perf_event  pids  systemd

例えば、/sys/fs/cgroup/cpu/tasks に記載されたプロセス(PID)の CPU リソース は /sys/fs/cgroup/cpu/cpu.shares に記載された 1024 に制限されます。※ CPU share は CPU の相対値

$ cat /sys/fs/cgroup/cpu/tasks
1
2
3
4
6
8

~省略~

$ cat /sys/fs/cgroup/cpu/cpu.shares
1024

cgroup を追加する際はディレクトリを作成します。test という cgroup を作成してみます。

$ mkdir /sys/fs/cgroup/cpu/test

ディレクトリを作成するとその配下に自動でファイルが作成されます。

$ ls /sys/fs/cgroup/cpu/test
cgroup.clone_children  cgroup.procs  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.rt_period_us  cpu.rt_runtime_us  cpu.shares  cpu.stat  cpuacct.stat  cpuacct.usage  cpuacct.usage_all  cpuacct.usage_percpu  cpuacct.usage_percpu_sys  cpuacct.usage_percpu_user  cpuacct.usage_sys  cpuacct.usage_user  notify_on_release  tasks

例えば tasks に 2000、cpu.share に 512 を記載すると PID 2000 のプロセスの CPU share を 512 に制限する事になります。

$ cat /sys/fs/cgroup/cpu/test/tasks
2000
$ cat /sys/fs/cgroup/cpu/test/cpu.shares
512

コンテナは OS から見たらただのプロセスなのでこのようにリソースを制限出来ます。

なお、systemd-cgls コマンドで cgroup 階層全体が表示されます。

$ systemd-cgls
├─   1 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
├─2913 bpfilter_umh
├─kubepods
│ └─burstable
│   ├─pod2a7eefa0-6719-4cac-82f4-6b7d58d54635
│   │ ├─510f73e18aa7dac452276e74f7acfd7087f44562eae619e05654bae567f5b460
│   │ │ ├─4276 bash /app/entrypoint.sh
│   │ │ ├─4327 ./aws-k8s-agent
│   │ │ └─4328 tee -i aws-k8s-agent.log
│   │ └─e7101431d6f2cad771a0d30c81101d06475c2b4dad6b033eaf58093e636679a8
│   │   └─3585 /pause
│   ├─pode895364f-3e61-48db-93cc-b0f8daefaa55
│   │ ├─c63c43b9ab660f4e2cab66f576e1a3208b6b97d659c0666c7c2c342daeecef8e
│   │ │ └─4891 /coredns -conf /etc/coredns/Corefile
│   │ └─aa5779e764f7fbfe68948de0ce425e7e03b2c5a2082586f6eb94e9a29b4670a3
│   │   └─4587 /pause
│   ├─poddd65f1eb-593a-4882-bb86-9f453fc1a1d2
│   │ ├─0d51784c7ac2cdc9a013d19d79a5617c27c6d4f86c3dfc28c7517f96eb9c8743
│   │ │ └─3637 /pause
│   │ └─cb2b42771c6e91de40b0cddde1e57677f5c112fa013fd9c601fa960e2e9d785d
│   │   └─3876 kube-proxy --v=2 --config=/var/lib/kube-proxy-config/config
│   └─podab6c28cc-525a-4fd2-9a69-07720eeed5ee
│     ├─73c143c3c6b7706e3ef2e45b57ca0d6484b9679ba1920c095ba3c1b9025892b2
│     │ └─4626 /pause
│     └─8dd43dd500d50180b3d287d96be60c1aa341dff864c9ce1903825dfeae6a7c04
│       └─4925 /coredns -conf /etc/coredns/Corefile
├─user.slice
│ └─user-1000.slice
│   ├─session-4.scope
│   │ ├─28624 sshd: ec2-user [priv]
│   │ ├─28655 sshd: ec2-user@pts/1
│   │ ├─28656 -bash
│   │ ├─28700 sudo -s
│   │ ├─28701 /bin/bash
│   │ └─29383 systemd-cgls
│   └─session-1.scope
│     ├─5724 sshd: ec2-user [priv]
│     ├─5779 sshd: ec2-user@pts/0
│     ├─5794 -bash
│     ├─7556 sudo -s
│     ├─7557 /bin/bash
│     ├─8704 systemd-cgls
│     └─8705 less
└─system.slice
  ├─rngd.service
  │ └─1875 /sbin/rngd -f --fill-watermark=0 --exclude=jitter
  ├─irqbalance.service
  │ └─1853 /usr/sbin/irqbalance --foreground
  ├─amazon-ssm-agent.service
  │ ├─2361 /usr/bin/amazon-ssm-agent
  │ └─2518 /usr/bin/ssm-agent-worker
  ├─containerd.service
  │ ├─2781 /usr/bin/containerd
  │ ├─3545 /usr/bin/containerd-shim-runc-v2 -namespace moby -id e7101431d6f2c...
  │ ├─3546 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 0d51784c7ac2c...
  │ ├─3856 /usr/bin/containerd-shim-runc-v2 -namespace moby -id cb2b42771c6e9...
  │ ├─4257 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 510f73e18aa7d...
  │ ├─4528 /usr/bin/containerd-shim-runc-v2 -namespace moby -id aa5779e764f7f...
  │ ├─4530 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 73c143c3c6b77...
  │ ├─4826 /usr/bin/containerd-shim-runc-v2 -namespace moby -id c63c43b9ab660...
  │ └─4844 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 8dd43dd500d50...
  ├─systemd-udevd.service
  │ └─1305 /usr/lib/systemd/systemd-udevd
  ├─system-serial\x2dgetty.slice
  │ └─serial-getty@ttyS0.service
  │   └─2377 /sbin/agetty --keep-baud 115200,38400,9600 ttyS0 vt220
  ├─docker.service
  │ └─2870 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd....
  ├─chronyd.service
  │ └─1861 /usr/sbin/chronyd
  ├─auditd.service
  │ └─1826 /sbin/auditd
  ├─kubelet.service
  │ └─3054 /usr/bin/kubelet --cloud-provider aws --config /etc/kubernetes/kub...
  ├─systemd-journald.service
  │ └─1284 /usr/lib/systemd/systemd-journald
  ├─sshd.service
  │ └─2423 /usr/sbin/sshd -D
  ├─crond.service
  │ └─2376 /usr/sbin/crond -n
  ├─gssproxy.service
  │ └─1887 /usr/sbin/gssproxy -D
  ├─rsyslog.service
  │ └─2367 /usr/sbin/rsyslogd -n
  ├─rpcbind.service
  │ └─1851 /sbin/rpcbind -w
  ├─network.service
  │ ├─2087 /sbin/dhclient -q -lf /var/lib/dhclient/dhclient--eth0.lease -pf /...
  │ └─2122 /sbin/dhclient -6 -nw -lf /var/lib/dhclient/dhclient6--eth0.lease ...
  ├─lvm2-lvmetad.service
  │ └─1302 /usr/sbin/lvmetad -f
  ├─postfix.service
  │ ├─ 2268 /usr/libexec/postfix/master -w
  │ ├─ 2270 qmgr -l -t unix -u
  │ └─13892 pickup -l -t unix -u
  ├─dbus.service
  │ └─1857 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidf...
  ├─system-getty.slice
  │ └─getty@tty1.service
  │   └─2373 /sbin/agetty --noclear tty1 linux
  └─systemd-logind.service
    └─1864 /usr/lib/systemd/systemd-logind

2. kube-reserved/system-reserved

公式ドキュメント Reserve Compute Resources for System Daemons | Kubernetes には以下のように記載されています。(意訳)

項目 概要
kube-reserved ・kubelet や container runtime, node problem detector などの Kubernetes system daemon 用に確保されるリソース
・Pod として動く system deamon は対象外
・CPU/メモリ/エフェメラルストレージ/PID 数 を指定可能
・kubelet フラグ例
 --kube-reserved=cpu=100m,memory=100Mi,ephemeral-storage=1Gi,pid=1000
Kubernetes system daemon--kube-reserved を強制するには --enforce-node-allocatable=kube-reserved--kube-reserved-cgroup=<cgroup> が必要
system-reserved sshd や udev などの OS system daemon 用に確保されるリソース
カーネルメモリは Pod に割り当てられないためカーネル用のメモリも確保した方がよい
・ユーザのログインセッション用リソースを確保する事も推奨(systemd の世界では user.slice)
・CPU/メモリ/エフェメラルストレージ/PID 数 を指定可能
・kubelet フラグ例
 --system-reserved=cpu=100m,memory=100Mi,ephemeral-storage=1Gi,pid=1000
・OS system daemon--system-reserved を強制するには --enforce-node-allocatable=system-reserved--system-reserved-cgroup=<cgroup> が必要

Kubernetes/OS system daemon--kube-reserved/--system-reserved を強制するには --enforce-node-allocatable=kube-reserved/system-reserved--kube-reserved-cgroup/--systemreserved-cgroup=<cgroup> が必要」がいまいちピンと来なかったです。

--kube-reserved/--system-reserved だけでは Kubernetes/OS system daemon 用にリソースが確保されて強制はされない?

他にも、Kubernetes/OS が設定値以上使おうとするとどうなるのか?推奨値はあるのか?

などなど気になる点が多いため検証してみました。

3. 検証環境構築

eksctl コマンドで EKS Cluster を作成する - 俺のメモ帳 で作成した EKS Cluster を使用します。

4 検証

4.1. EKS の kube-reserved/system-reserved 設定値

まずは kube-reserved/system-reserved の確認方法です。Node の情報なので KubernetesAPI サーバから取得できます。

kubectl proxy コマンドでプロキシをローカルに起動すると API サーバにアクセスできます。

$ kubectl proxy
Starting to serve on 127.0.0.1:8001

デフォルトでローカルの 8001 ポートでプロキシが起動するので、別ターミナルで http://localhost:8001/api/v1/nodes/<Node 名>/proxy/configz にアクセスします。json なので jq で整形すると見やすいです。

$ kubectl get node
NAME                                              STATUS   ROLES    AGE    VERSION
ip-10-0-101-249.ap-northeast-1.compute.internal   Ready    <none>   2m36s   v1.21.5-eks-bc4871b
ip-10-0-102-241.ap-northeast-1.compute.internal   Ready    <none>   2m27s   v1.21.5-eks-bc4871b
ip-10-0-103-143.ap-northeast-1.compute.internal   Ready    <none>   2m12s   v1.21.5-eks-bc4871b
$ curl -sSL "http://localhost:8001/api/v1/nodes/ip-10-0-101-249.ap-northeast-1.compute.internal/proxy/configz" | jq .
{
  "kubeletconfig": {
    "enableServer": true,
    "syncFrequency": "1m0s",
    "fileCheckFrequency": "20s",
    "httpCheckFrequency": "20s",
    "address": "0.0.0.0",
    "port": 10250,
    "tlsCipherSuites": [
      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
      "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_256_GCM_SHA384",
      "TLS_RSA_WITH_AES_128_GCM_SHA256"
    ],
    "serverTLSBootstrap": true,
    "authentication": {
      "x509": {
        "clientCAFile": "/etc/kubernetes/pki/ca.crt"
      },
      "webhook": {
        "enabled": true,
        "cacheTTL": "2m0s"
      },
      "anonymous": {
        "enabled": false
      }
    },
    "authorization": {
      "mode": "Webhook",
      "webhook": {
        "cacheAuthorizedTTL": "5m0s",
        "cacheUnauthorizedTTL": "30s"
      }
    },
    "registryPullQPS": 5,
    "registryBurst": 10,
    "eventRecordQPS": 5,
    "eventBurst": 10,
    "enableDebuggingHandlers": true,
    "healthzPort": 10248,
    "healthzBindAddress": "127.0.0.1",
    "oomScoreAdj": -999,
    "clusterDomain": "cluster.local",
    "clusterDNS": [
      "172.20.0.10"
    ],
    "streamingConnectionIdleTimeout": "4h0m0s",
    "nodeStatusUpdateFrequency": "10s",
    "nodeStatusReportFrequency": "5m0s",
    "nodeLeaseDurationSeconds": 40,
    "imageMinimumGCAge": "2m0s",
    "imageGCHighThresholdPercent": 85,
    "imageGCLowThresholdPercent": 80,
    "volumeStatsAggPeriod": "1m0s",
    "cgroupRoot": "/",
    "cgroupsPerQOS": true,
    "cgroupDriver": "cgroupfs",
    "cpuManagerPolicy": "none",
    "cpuManagerReconcilePeriod": "10s",
    "memoryManagerPolicy": "None",
    "topologyManagerPolicy": "none",
    "topologyManagerScope": "container",
    "runtimeRequestTimeout": "2m0s",
    "hairpinMode": "hairpin-veth",
    "maxPods": 17,
    "podPidsLimit": -1,
    "resolvConf": "/etc/resolv.conf",
    "cpuCFSQuota": true,
    "cpuCFSQuotaPeriod": "100ms",
    "nodeStatusMaxImages": 50,
    "maxOpenFiles": 1000000,
    "contentType": "application/vnd.kubernetes.protobuf",
    "kubeAPIQPS": 5,
    "kubeAPIBurst": 10,
    "serializeImagePulls": false,
    "evictionHard": {
      "memory.available": "100Mi",
      "nodefs.available": "10%",
      "nodefs.inodesFree": "5%"
    },
    "evictionPressureTransitionPeriod": "5m0s",
    "enableControllerAttachDetach": true,
    "protectKernelDefaults": true,
    "makeIPTablesUtilChains": true,
    "iptablesMasqueradeBit": 14,
    "iptablesDropBit": 15,
    "featureGates": {
      "RotateKubeletServerCertificate": true
    },
    "failSwapOn": true,
    "containerLogMaxSize": "10Mi",
    "containerLogMaxFiles": 5,
    "configMapAndSecretChangeDetectionStrategy": "Watch",
    "kubeReserved": {
      "cpu": "70m",
      "ephemeral-storage": "1Gi",
      "memory": "442Mi"
    },
    "enforceNodeAllocatable": [
      "pods"
    ],
    "volumePluginDir": "/usr/libexec/kubernetes/kubelet-plugins/volume/exec/",
    "logging": {
      "format": "text"
    },
    "enableSystemLogHandler": true,
    "shutdownGracePeriod": "0s",
    "shutdownGracePeriodCriticalPods": "0s",
    "enableProfilingHandler": true,
    "enableDebugFlagsHandler": true
  }
}

104 行目から 108 行目に kube-reserved が表示されています。 kubeReservedCgroup/systemReservedCgroup が無いため、--kube-reserved-cgroup/--system-reserved-cgroup が設定されていない事がわかります。 また、enforceNodeAllocatablepods のみ設定されています。(kube-reserved/system-reserved は設定されていない)

なお、API サーバではなく Node にログインして設定ファイルから値を確認する事も可能です。

@node$ cat /etc/kubernetes/kubelet/kubelet-config.json
{
  "kind": "KubeletConfiguration",
  "apiVersion": "kubelet.config.k8s.io/v1beta1",
  "address": "0.0.0.0",
  "authentication": {
    "anonymous": {
      "enabled": false
    },
    "webhook": {
      "cacheTTL": "2m0s",
      "enabled": true
    },
    "x509": {
      "clientCAFile": "/etc/kubernetes/pki/ca.crt"
    }
  },
  "authorization": {
    "mode": "Webhook",
    "webhook": {
      "cacheAuthorizedTTL": "5m0s",
      "cacheUnauthorizedTTL": "30s"
    }
  },
  "clusterDomain": "cluster.local",
  "hairpinMode": "hairpin-veth",
  "readOnlyPort": 0,
  "cgroupDriver": "cgroupfs",
  "cgroupRoot": "/",
  "featureGates": {
    "RotateKubeletServerCertificate": true
  },
  "protectKernelDefaults": true,
  "serializeImagePulls": false,
  "serverTLSBootstrap": true,
  "tlsCipherSuites": [
    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
    "TLS_RSA_WITH_AES_256_GCM_SHA384",
    "TLS_RSA_WITH_AES_128_GCM_SHA256"
  ],
  "maxPods": 20,
  "clusterDNS": [
    "172.20.0.10"
  ],
  "evictionHard": {
    "memory.available": "100Mi",
    "nodefs.available": "10%",
    "nodefs.inodesFree": "5%"
  },
  "kubeReserved": {
    "cpu": "70m",
    "ephemeral-storage": "1Gi",
    "memory": "442Mi"
  }

上記の方法でインスタンスタイプ毎の設定値を確認してみました。Node のリソース量から自動で計算されているようです。

インスタンスタイプ kube-reserved system-reserved kube-reserved-cgroup system-reserved-cgroup
t3.small cpu:70m
memory:376Mi
ephemeral-storage:1Gi
なし なし なし
t3.medium cpu:70m
memory:442Mi
ephemeral-storage:1Gi
なし なし なし
m5.large cpu:80m
memory:574Mi
ephemeral-storage:1Gi
なし なし なし
m5.xlarge cpu:80m
memory:893Mi
ephemeral-storage:1Gi
なし なし なし

4.2. 設定値変更

AWS マネージドではないセルフマネージド NodeGroup の場合は以下の通り設定できそうです。

Customizing kubelet configuration - eksctl

AWS マネージドの場合は yaml での設定方法は見つかりませんでした。 Node にログインして無理やり設定(/etc/kubernetes/kubelet/kubelet-config.json や /etc/systemd/system/kubelet.service を修正)する事は可能ですが、それだと Node が再作成された際に新しい Node には設定されないので、カスタム AMI を使用する事で常に同じ設定の Node を起動する事が可能です。

起動テンプレートのサポート - Amazon EKS

個人的には AWS マネージドという事は AWS が良かれと思って設計した値なのでユーザ側がカスタマイズする必要は無いと思っています。

4.3. --enforce-node-allocatable と --kube-reserved-cgroup/--system-reserved-cgroup 無しの場合の制限

EKS のデフォルト状態(--enforce-node-allocatable--kube-reserved-cgroup/--system-reserved-cgroup が設定されていない状態)での制限を確認します。

検証用 Node(t3.medium)の設定値は以下です。

cpu memory ephemeral-storage
kube-reserved 70m 442Mi 1Gi
system-reserved なし なし なし

Kubernetes system daemon 用に cpu 70M, memory 442Mi, ephemeral-storage 1Gi が確保されています。最初に記載した通り、Pod に割り当てられるリソース = Node - (kube-reserved + system-reserved + eviction-threshold) のため、「確保されている」と言えると思います。

ただし、--enforce-node-allocatable--kube-reserved-cgroup/--system-reserved-cgroup が設定されていないため強制はされていないはずです。 cpu.shares に 70m 、memory.limit_in_bytes に 442Mi が設定されている cgroup の有無を確認しましたが存在しなかったので確かに「強制されていない(リソースを制限していない)」という事がわかります。同様に --system-reserved を設定して検証しても同じ結果でした。

@node$ find /sys/fs/cgroup/ -type f -name 'cpu.shares' -exec cat {} \; | sort | uniq
102
1024
128
1976
2
25
@node$ find /sys/fs/cgroup/ -type f -name 'memory.limit_in_bytes' -exec cat {} \; | sort | uniq
3135741952
9223372036854771712

つまり、--kube-reserved/--system-reservedKubernetes 用とか OS 用とか関係なく、単純に Pod に割り当てられない領域として確保しているだけという事になります。これは --kube-reserved--system-reserved が同じ意味を表しているという事になるかと思います。※--enforce-node-allocatable--kube-reserved-cgroup/--system-reserved-cgroup が無い場合

マネージドサービスの EKS が --system-reserved を設定していない理由はこれかと思います。--kube-reserved 一つ設定しておけば Kubernetes/OS 用のリソースとして確保できるからです。(同じ意味なので --system-reserved を設定する必要がない)

当然ですが、リソースを制限しているわけではないので、Kubernetes/OS が --kube-reserved/--system-reserved に設定されている値より多くのリソースを使用する事が可能です。

4.4. --enforce-node-allocatable と --kube-reserved-cgroup/--system-reserved-cgroup ありの場合の制限

まずは --kube-reserved-cgroup に設定する cgroup を作成します。プロセスは dockerd を設定します。

@node$ # find /sys/fs/cgroup/ -maxdepth 1 ! -type l -exec mkdir {}/K8s \;
mkdir: ディレクトリ `/sys/fs/cgroup//K8s` を作成できません: Read-only file system
@node$ pgrep -fl dockerd
2877 dockerd
@node$ find /sys/fs/cgroup/ -type d -name K8s -exec sh -c 'echo 2877 > {}/tasks' \;
sh: 0 行: echo: 書き込みエラー: No space left on device

systemd-cgls で cgroup を確認します。

@node$ systemd-cgls
├─   1 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
├─2986 bpfilter_umh
├─kubepods
│ └─burstable
│   ├─pode887cc03-8c8a-47fe-94ce-77f75b71b7e2
│   │ ├─5724c33afacf82531d99d78fcea7b5b07b54105d2729b20953eb3e6a3da9dcfb
│   │ │ └─3674 /pause
│   │ └─b263bef8f5b3aff3df5430cc05551a42d72ffbd93a8e56ed10b3b2e854f2f160
│   │   ├─4309 bash /app/entrypoint.sh
│   │   ├─4357 ./aws-k8s-agent
│   │   └─4358 tee -i aws-k8s-agent.log
│   └─pod637abaec-5fc8-4855-9952-ac08730384df
│     ├─1ed3616be7204d70dda58c0ac3a6060fd26b6d595b0a832604cb9b5b777bb91e
│     │ └─3629 /pause
│     └─f703824f768510f7b8dfd989c8f64d8784cb201d7fe81949a000baed59c869c3
│       └─3911 kube-proxy --v=2 --config=/var/lib/kube-proxy-config/config
├─user.slice
│ └─user-1000.slice
│   └─session-1.scope
│     ├─4928 sshd: ec2-user [priv]
│     ├─5012 sshd: ec2-user@pts/0
│     ├─5036 -bash
│     ├─5058 sudo -s
│     ├─5060 /bin/bash
│     ├─5640 systemd-cgls
│     └─5641 less
├─system.slice
│ ├─rngd.service
│ │ └─1866 /sbin/rngd -f --fill-watermark=0 --exclude=jitter
│ ├─irqbalance.service
│ │ └─1853 /usr/sbin/irqbalance --foreground
│ ├─amazon-ssm-agent.service
│ │ ├─2363 /usr/bin/amazon-ssm-agent
│ │ └─2519 /usr/bin/ssm-agent-worker
│ ├─containerd.service
│ │ ├─2800 /usr/bin/containerd
│ │ ├─3577 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 1ed3616be7204d70dda58c0ac3a6060fd26b6d595b0a832604cb9b5b777bb91e -address /run/containerd/containerd.sock
│ │ ├─3578 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 5724c33afacf82531d99d78fcea7b5b07b54105d2729b20953eb3e6a3da9dcfb -address /run/containerd/containerd.sock
│ │ ├─3887 /usr/bin/containerd-shim-runc-v2 -namespace moby -id f703824f768510f7b8dfd989c8f64d8784cb201d7fe81949a000baed59c869c3 -address /run/containerd/containerd.sock
│ │ └─4279 /usr/bin/containerd-shim-runc-v2 -namespace moby -id b263bef8f5b3aff3df5430cc05551a42d72ffbd93a8e56ed10b3b2e854f2f160 -address /run/containerd/containerd.sock
│ ├─systemd-udevd.service
│ │ └─1686 /usr/lib/systemd/systemd-udevd
│ ├─system-serial\x2dgetty.slice
│ │ └─serial-getty@ttyS0.service
│ │   └─2375 /sbin/agetty --keep-baud 115200,38400,9600 ttyS0 vt220
│ ├─docker.service
│ │ └─2877 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
│ ├─chronyd.service
│ │ └─1884 /usr/sbin/chronyd
│ ├─auditd.service
│ │ └─1827 /sbin/auditd
│ ├─kubelet.service
│ │ └─3146 /usr/bin/kubelet --cloud-provider aws --config /etc/kubernetes/kubelet/kubelet-config.json --kubeconfig /var/lib/kubelet/kubeconfig --container-runtime docker --network-plugin cni --node-ip=10.0.102.145 --pod-infra-container-image=602401143452.dkr.ecr.ap-northe
│ ├─systemd-journald.service
│ │ └─1285 /usr/lib/systemd/systemd-journald
│ ├─sshd.service
│ │ └─2416 /usr/sbin/sshd -D
│ ├─crond.service
│ │ └─2373 /usr/sbin/crond -n
│ ├─gssproxy.service
│ │ └─1888 /usr/sbin/gssproxy -D
│ ├─rsyslog.service
│ │ └─2365 /usr/sbin/rsyslogd -n
│ ├─rpcbind.service
│ │ └─1867 /sbin/rpcbind -w
│ ├─network.service
│ │ ├─2087 /sbin/dhclient -q -lf /var/lib/dhclient/dhclient--eth0.lease -pf /var/run/dhclient-eth0.pid eth0
│ │ └─2123 /sbin/dhclient -6 -nw -lf /var/lib/dhclient/dhclient6--eth0.lease -pf /var/run/dhclient6-eth0.pid eth0
│ ├─lvm2-lvmetad.service
│ │ └─1301 /usr/sbin/lvmetad -f
│ ├─postfix.service
│ │ ├─2285 /usr/libexec/postfix/master -w
│ │ ├─2286 pickup -l -t unix -u
│ │ └─2287 qmgr -l -t unix -u
│ ├─dbus.service
│ │ └─1860 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
│ ├─system-getty.slice
│ │ └─getty@tty1.service
│ │   └─2374 /sbin/agetty --noclear tty1 linux
│ └─systemd-logind.service
│   └─1864 /usr/lib/systemd/systemd-logind
└─K8s
  └─2877 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

dockerd servcie 用の cgroup K8s が作成されている事がわかります。cgroup K8s のリソースが --kube-reserved に設定した値で制限されるように etc/systemd/system/kubelet.service を修正して kubelet を再起動します。

@node$ cat /etc/systemd/system/kubelet.service 
[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/kubernetes/kubernetes
After=docker.service iptables-restore.service
Requires=docker.service

[Service]
ExecStartPre=/sbin/iptables -P FORWARD ACCEPT -w 5
ExecStart=/usr/bin/kubelet --cloud-provider aws \
    --config /etc/kubernetes/kubelet/kubelet-config.json \
    --kubeconfig /var/lib/kubelet/kubeconfig \
    --container-runtime docker \
    --network-plugin cni $KUBELET_ARGS $KUBELET_EXTRA_ARGS \
    --enforce-node-allocatable=pods,kube-reserved \
    --kube-reserved-cgroup=/K8s

Restart=always
RestartSec=5
KillMode=process

[Install]
WantedBy=multi-user.target
@node$ systemctl daemon-reload
@node$ systemctl restart kubelet

kubelet 再起動後に cgroup K8s を確認すると cpu が --kube-reserved に設定した 70m で制限されていました。

@node$ cat /sys/fs/cgroup/cpu/K8s/cpu.shares
71

--enforce-node-allocatable=kube-reserved,system-reserved--kube-reserved-cgroup/--system-reserved-cgroup を設定する事で、--kube-reserved/--system-reserved に設定した値で Kubernetes/OS のリソースを制限できる事がわかりました。

マネージドではない手組みの Kubernetes の場合は、Kubernetes/OS 用に cgroup を作成してそれぞれのリソースを制限するという使い方が可能という事かと思います。

なお、cpu.shares は相対値のためリソースに空きがあれば設定された数値より多くのリソースを使用する事が可能です。

5. まとめ

上記の検証結果によるまとめです。

  • --enforce-node-allocatable=kube-reserved,system-reserved--kube-reserved-cgroup/--system-reserved-cgroup を設定しない場合
    • 特定のプロセス用にリソースを確保しているわけではなく、単純に Pod が使えないリソースというだけ
    • --kube-reserved--system-reserved は同じ意味(そのため EKS では --kube-reserved のみ設定)
  • --enforce-node-allocatable=kube-reserved,system-reserved--kube-reserved-cgroup/--system-reserved-cgroup を設定する場合
    • 指定した cgroup に対してリソースを制限する事が可能
    • どのプロセスをどのようにグループ(cgroup)化し、どれぐらいリソースを割り当てるかという設計が必要
  • (個人的には)EKS 等のマネージドサービスの場合はこれらの値を気にしなくて良い(クラウドベンダの設計にまかせて何か問題が起きた際に考える)

6. 参考

Reserve Compute Resources for System Daemons | Kubernetes

design-proposals-archive/node-allocatable.md at main · kubernetes/design-proposals-archive · GitHub

Kubernetes のリソース制限とその挙動確認

Kubernetes でコンテナのリソースを制限について整理し、実際の挙動を確認しました。

1. Kubernetes のリソース制限

Kubernetes ではコンテナ単位でリソース制限が可能です。

1.1. 制限をかけられるリソース

リソース 単位
CPU 1 = 1000m = 1vCPU
例:500m(0.5 vCPU)
メモリ バイト(E,P,T,G,M,K,Ei,Pi,Ti,Gi,Mi,Ki を使用可能)
例:100M
※128974848, 129e6, 129M, 123Mi はほぼ同じ値
エフェメラルストレージ バイト
※メモリと同様

※ デバイスプラグインを使用する事で GPU 等のリソースも制限可能(GPUのスケジューリング | Kubernetes

1.2. 設定方法

以下のように manifest 内で spec.containers.resources.requests と spec.containers.resources.limits に値を指定します。

apiVersion: v1
kind: Pod
metadata:
  name: sample-pod
spec:
  containers:
  - name: nginx-container
    image: nginx
    resources:
      requests:
        memory: "64M"
        cpu: "250m"
      limits:
        memory: "128M"
        cpu: "500m"

1.3. requests,limits

パラメータ 説明
requests ・コンテナを起動するために必要な空きリソース
 ・空きリソース = 割り当て可能なリソース - 割り当て済みリソース
・指定した値の空きリソースが Node に無い場合はコンテナを起動できない
 ・実際の Node のリソース使用量は見ていない
・コンテナは requests の値を超えてリソースを使用する事ができる
limists ・コンテナが使用するリソースの上限
・指定した値の空きリソースが Node に無くてもコンテナを起動できる
・コンテナは limits の値を超えてリソースを使用する事はできない

・値を指定しない場合、Node のリソースを消費し続ける
・limits のみ指定した場合、requests は limits と同じ値が設定される
・limits を超えようとした場合の挙動
 ・メモリ:OOM エラーでコンテナが強制終了する(SIGTERM は送られずに kill される)
 ・CPU:コンテナは終了されない(ただ CPU を使えないだけ)

1.4. オーバーコミット

requests と limits に差があるとオーバコミットをさせる事ができます。オーバーコミットにはメリット、デメリットがあるのでメモリを例として説明します。

※ オーバーコミットとは、コンテナの limits の合計が Node のリソースより大きい事を許容する方式

割り当て可能リソース(Allocatable) が 2GB の Node には、requests 0.5GB,limits 1GB の Pod を 4 個起動できます。limits の合計が 4GB となり Node のリソースより大きいためオーバーコミットの状態です。Pod が常に多くのリソースを使うわけでは無い場合にこの構成が可能です。

f:id:dunkshoot:20211107142831p:plain

しかし、この状態で Pod の負荷が高まると Eviction Manager により最も requests を超過してリソースを使用している Pod が Evict されます。(PodPriority を設定していない場合)

※ Eviction Manager については別の記事で整理

f:id:dunkshoot:20211107143230p:plain

つまり、オーバコミットさせると Pod の集約率は上がりますが、Pod の稼働が不安定になる可能性があります。

逆に requests と limits を同じ値に設定すると Pod の集約率は下がりますが、Pod の稼働は安定します。

1.5. 実際にどのようにリソースを制限しているか

コンテナランタイムに Docker を使用している場合、manifest で指定した requests,limits のうち requests.memory 以外は docker のオプションに渡されてコンテナのリソースを制限します。

パラメータ 説明
requests.cpu ・コア値に変換され 1024倍された値と 2 のいずれか大きい方が docker の --cpu-shares オプションとして使用される
 ※--cpu-shares はコンテナに割り当てる CPU の相対値
・例えば 500m を指定した場合、--cpu-shares の値は(500 / 1000 * 1024 =) 512 となる
limits.cpu ・ミリコア値に変換され 100倍された後に docker の --cpu-quota オプションとして使用される
 ※--cpu-quota--cpu-period ごとに使用できる CPU 時間の合計(マイクロ秒)
 ※--cpu-periodKubernetes では 100 ミリ秒で固定
・例えば 1000m を指定した場合、--cpu-quota の値は(1000 * 100 =)100000 になる(つまり 100 ミリ秒間に使用できる CPU 時間は 100000 マイクロ秒 = 100 ミリ秒となり、1 vCPU が上限となる)
requests.memory ・docker には渡されていない
・単純にコンテナ起動時に割り当て可能なリソースがあるかを見てるだけ
limits.memory ・整数に変換された後に docker の --memory オプションとして使用される
 ※--memory はコンテナに割り当てるメモリ最大使用量(バイト)
・例えば 512M を指定した場合、--memory の値は(512 * 1024 * 1024 = )536870912 となる

1.6. LimitRange

Pod やコンテナに対して、リソース制限のデフォルト値、最小、最大値等を制限できる LimitRange というリソースもあります。Namespace レベルで制限可能です。

設定できる項目と制限をかけられるリソースは以下です。

設定項目 制限をかけられるリソース
デフォルトの requests コンテナ
デフォルトの limits コンテナ
リソース使用量の最大値(最大の limits) Pod,コンテナ,PersistentVolumeClaim
リソース使用量の最小値(最小の requests) Pod,コンテナ,PersistentVolumeClaim
requests と limits の割合 Pod,コンテナ

LimitRange のユースケースは以下です。

  • requests を指定しない場合は無限に Pod を起動しようとするので、デフォルトの requests を設定しておく事でその状況を防ぐ
  • リソース使用量の最大値を設定することにより、過度なリソース使用を防ぐ
  • requests と limits の割合を設定する事により、過度なオーバーコミットを防ぐ

1.7. QoS Class

Kubernetes は requests,limits に応じて Pod に QoS(Quality of Service)Class を割り当てます。 QoS Class には 3 種類あり、Kubernetes がコンテナの oom score を設定する際に使用されます。

Class 割り当てられる条件 OOMKill される優先順位
Guaranteed Pod 内の全てのコンテナに CPU,メモリの requests,limits が設定されていて、requests と limits が同じ値 3
Burstable Guaranteed の条件を満たさない、かつ、Pod 内の一つ以上のコンテナに CPU,メモリの requests が設定されている 2
BestEffort requests,limits 共に設定されていない 1

Node のメモリ使用量が上限に達した場合には、まず BestEffort、次に Burstable、最後に Guaranteed のコンテナが kill されます。

1.8. ResourceQuota

Pod や Configmap,Service などの Kubernetes リソースの個数や使用量を制限できる ResourceQuota というリソースもあります。Namespace レベルで制限可能です。

CPU,メモリに関しては以下が設定可能です。

パラメータ 説明
requests.cpu 全 Pod の requests.cpu の合計
limits.cpu 全 Pod の limits.cpu の合計
requests.memory 全 Pod の requests.memory の合計
limits.memory 全 Pod の limits.memory の合計

リソースの個数も制限可能です。以下はその一部です。

パラメータ 説明
count/pods pod の数
count/deployments deployment の数
count/services service の数
count/configmaps configmap の数

2. 検証環境構築

2.1. EKS Cluster 構築

requests,limits の挙動を確認するため EKS 環境(Kubernetes のバージョンは 1.21)を構築します。

eksctl コマンドで EKS Cluster を作成する - 俺のメモ帳 の記事で作成した yaml を以下のように一部変更して Cluster を作成します。

---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: ekstest
  region: ap-northeast-1
  version: "1.21"

vpc:
  id: "vpc-063b58ff16344acfd"
  subnets:
    public:
      ap-northeast-1a:
          id: "subnet-06324dcadf5706acb"
      ap-northeast-1c:
          id: "subnet-048cad38a6c49de67"
      ap-northeast-1d:
          id: "subnet-0bb85370bb4b4528d"

managedNodeGroups:
  - name: managed-ng
    instanceType: t3.medium
    desiredCapacity: 1
    volumeSize: 30
    availabilityZones: ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
    maxPodsPerNode: 20
    ssh:
      allow: true
      publicKeyName: ekstest

以下構成の Cluster が作成されます。

f:id:dunkshoot:20211105011253p:plain

2.2. Node のリソース量確認

初期構築後(デフォルトで起動する Pod 以外の Pod は起動していない状態)の Node のリソースの状態を確認します。 Node のインスタンスタイプは t3.micro のため、2vCPU、4GiB メモリのキャパシティがあります。

インスタンスタイプ - Amazon EC2 | AWS

初期構築後の node describe コマンドの結果は以下です。

$ kubectl describe node ip-10-0-102-221.ap-northeast-1.compute.internal
Name:               ip-10-0-102-221.ap-northeast-1.compute.internal
Roles:              <none>
Labels:             alpha.eksctl.io/cluster-name=ekstest
                    alpha.eksctl.io/nodegroup-name=managed-ng
                    beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/instance-type=t3.medium
                    beta.kubernetes.io/os=linux
                    eks.amazonaws.com/capacityType=ON_DEMAND
                    eks.amazonaws.com/nodegroup=managed-ng
                    eks.amazonaws.com/nodegroup-image=ami-0c78e48568f9ea043
                    eks.amazonaws.com/sourceLaunchTemplateId=lt-0296f77bcc1eccc99
                    eks.amazonaws.com/sourceLaunchTemplateVersion=1
                    failure-domain.beta.kubernetes.io/region=ap-northeast-1
                    failure-domain.beta.kubernetes.io/zone=ap-northeast-1c
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=ip-10-0-102-221.ap-northeast-1.compute.internal
                    kubernetes.io/os=linux
                    node.kubernetes.io/instance-type=t3.medium
                    topology.kubernetes.io/region=ap-northeast-1
                    topology.kubernetes.io/zone=ap-northeast-1c
Annotations:        node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Sat, 06 Nov 2021 00:48:46 +0900
Taints:             <none>
Unschedulable:      false
Lease:
  HolderIdentity:  ip-10-0-102-221.ap-northeast-1.compute.internal
  AcquireTime:     <unset>
  RenewTime:       Sat, 06 Nov 2021 01:38:30 +0900
Conditions:
  Type             Status  LastHeartbeatTime                 LastTransitionTime                Reason                       Message
  ----             ------  -----------------                 ------------------                ------                       -------
  MemoryPressure   False   Sat, 06 Nov 2021 01:34:30 +0900   Sat, 06 Nov 2021 00:48:46 +0900   KubeletHasSufficientMemory   kubelet has sufficient memory available
  DiskPressure     False   Sat, 06 Nov 2021 01:34:30 +0900   Sat, 06 Nov 2021 00:48:46 +0900   KubeletHasNoDiskPressure     kubelet has no disk pressure
  PIDPressure      False   Sat, 06 Nov 2021 01:34:30 +0900   Sat, 06 Nov 2021 00:48:46 +0900   KubeletHasSufficientPID      kubelet has sufficient PID available
  Ready            True    Sat, 06 Nov 2021 01:34:30 +0900   Sat, 06 Nov 2021 00:49:06 +0900   KubeletReady                 kubelet is posting ready status
Addresses:
  InternalIP:   10.0.102.221
  ExternalIP:   54.238.138.51
  Hostname:     ip-10-0-102-221.ap-northeast-1.compute.internal
  InternalDNS:  ip-10-0-102-221.ap-northeast-1.compute.internal
Capacity:
  attachable-volumes-aws-ebs:  25
  cpu:                         2
  ephemeral-storage:           31444972Ki
  hugepages-1Gi:               0
  hugepages-2Mi:               0
  memory:                      3967480Ki
  pods:                        20
Allocatable:
  attachable-volumes-aws-ebs:  25
  cpu:                         1930m
  ephemeral-storage:           27905944324
  hugepages-1Gi:               0
  hugepages-2Mi:               0
  memory:                      3412472Ki
  pods:                        20
System Info:
  Machine ID:                 ec2589410cfceb731bf7e7a6679c9683
  System UUID:                ec258941-0cfc-eb73-1bf7-e7a6679c9683
  Boot ID:                    a72aa7b5-a80b-4b01-b5c4-fc006aafb13d
  Kernel Version:             5.4.149-73.259.amzn2.x86_64
  OS Image:                   Amazon Linux 2
  Operating System:           linux
  Architecture:               amd64
  Container Runtime Version:  docker://20.10.7
  Kubelet Version:            v1.21.4-eks-033ce7e
  Kube-Proxy Version:         v1.21.4-eks-033ce7e
ProviderID:                   aws:///ap-northeast-1c/i-0cf593c64ada537b0
Non-terminated Pods:          (4 in total)
  Namespace                   Name                        CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                        ------------  ----------  ---------------  -------------  ---
  kube-system                 aws-node-vqnj6              10m (0%)      0 (0%)      0 (0%)           0 (0%)         49m
  kube-system                 coredns-76f4967988-9wgqt    100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     57m
  kube-system                 coredns-76f4967988-zcqwc    100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     57m
  kube-system                 kube-proxy-gbjgl            100m (5%)     0 (0%)      0 (0%)           0 (0%)         49m
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource                    Requests    Limits
  --------                    --------    ------
  cpu                         310m (16%)  0 (0%)
  memory                      140Mi (4%)  340Mi (10%)
  ephemeral-storage           0 (0%)      0 (0%)
  hugepages-1Gi               0 (0%)      0 (0%)
  hugepages-2Mi               0 (0%)      0 (0%)
  attachable-volumes-aws-ebs  0           0
Events:
  Type    Reason                   Age                From        Message
  ----    ------                   ----               ----        -------
  Normal  Starting                 49m                kubelet     Starting kubelet.
  Normal  NodeHasSufficientMemory  49m (x2 over 49m)  kubelet     Node ip-10-0-102-221.ap-northeast-1.compute.internal status is now: NodeHasSufficientMemory
  Normal  NodeHasNoDiskPressure    49m (x2 over 49m)  kubelet     Node ip-10-0-102-221.ap-northeast-1.compute.internal status is now: NodeHasNoDiskPressure
  Normal  NodeHasSufficientPID     49m (x2 over 49m)  kubelet     Node ip-10-0-102-221.ap-northeast-1.compute.internal status is now: NodeHasSufficientPID
  Normal  NodeAllocatableEnforced  49m                kubelet     Updated Node Allocatable limit across pods
  Normal  Starting                 49m                kube-proxy  Starting kube-proxy.
  Normal  NodeReady                49m                kubelet     Node ip-10-0-102-221.ap-northeast-1.compute.internal status is now: NodeReady

43 行目から 50 行目が Node としての Capacity(リソース総量)を表示しています。

CPU が 2、メモリが 3967480 KiB となっています。t3.medium 的にはメモリ量は 4GiB なので微妙に差がありますが、Node に SSH ログインして free でメモリ量を確認すると describe node 結果と同じ値になっています。

$ free
              total        used        free      shared  buff/cache   available
Mem:        3967480      409292     2092724         876     1465464     3353596
Swap:             0           0           0

ちなみに EC2 のスペックと OS から見えるメモリ量に差がある理由は以下に記載されています。

よくある質問 - Amazon EC2 | AWS

Q: オペレーティングシステムが報告するメモリ総量とインスタンスタイプの提供メモリが正確に一致しないのはなぜですか?

EC2 インスタンスメモリの一部は、ビデオ RAM、DMI、および ACPI のため、仮想 BIOS によって予約され使用されます。さらに、AWS Nitro Hypervisor を搭載したインスタンスでは、仮想化を管理するために、インスタンスメモリのごく一部が Amazon EC2 Nitro Hypervisor によって予約されます。

2.3. Pod に割り当て可能なリソース量確認

f:id:dunkshoot:20211106014214p:plain

51 行目から 58 行目が Allocatable(Pod に割り当て可能なリソース量)を表示しています。

CPU が 1930m、メモリが 3412472 KiB となっています。 これは Node のキャパシティから (system-reserved + kube-reserved + eviction-threshold) を引いた値です。

ここらへんは別の記事で整理しますが、t3.medium の場合は以下のようになっています。

system-reserved kube-reserved eviction-threshold
CPU 0 70m 0
メモリ 0 442Mi 100Mi

そのため Allocatable は以下になり、describe node の結果と同じになります。

CPU = 2000 - 70 = 1930m

メモリ = 3967480 - (442 + 100) * 1024 = 3412472 KiB

なお、Allocatable はあくまでも Pod に割り当て可能なリソースの総量を表示しているため、Pod を新たに起動しても Allocatable は減りません。

2.4. Pod に割り当て済みのリソース量確認

f:id:dunkshoot:20211106014423p:plain

71 行目から 77 行目が Pod に割り当てたリソース、78 行目から 87 行目が割り当てたリソースのサマリを表示しています。

2.5. metrics-server インストール

Node や Pod の負荷を確認するのに便利な metrics-server というものをインストールします。手順は Kubernetes メトリクスサーバーのインストール - Amazon EKS に記載されている通りです。

$ kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
serviceaccount/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
service/metrics-server created
deployment.apps/metrics-server created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created
$ kubectl get deployment metrics-server -n kube-system
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
metrics-server   1/1     1            1           3m48s

metrics-server を起動すると Node や Pod の負荷を kubectl top コマンドで確認できるようになります。

$ kubectl top node
NAME                                             CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ip-10-0-101-50.ap-northeast-1.compute.internal   57m          2%     580Mi           17%
$ kubectl top pod
NAME                              CPU(cores)   MEMORY(bytes)
test-deployment-d5d8f4656-b4gfp   0m           1Mi

3. 検証

3.1 requests,limits に指定した値が docker のオプションに使用されている事の確認

以下設定の deployment を起動して確認します。

Kubernetes パラメータ
requests.cpu 100m
limits.cpu 200m
requests.memory 64MB
limits.memory 128MB

manifest は以下です。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-app
  template:
    metadata:
      labels:
        app: test-app
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "sleep 3600"
        resources:
          requests:
            cpu: "100m"
            memory: "64M"
          limits:
            cpu: "200m"
            memory: "128M"

deployment 起動後に Node へ SSH ログインしてコンテナの情報を取得します。 dockder ps でコンテナ ID を確認し、docker inspect <コンテナ ID> で情報を取得できます。

$docker inspect c092f85463d0 | grep -i 'cpu\|memory'
            "CpuShares": 102,
            "Memory": 128000000,
            "NanoCpus": 0,
            "CpuPeriod": 100000,
            "CpuQuota": 20000,
            "CpuRealtimePeriod": 0,
            "CpuRealtimeRuntime": 0,
            "CpusetCpus": "",
            "CpusetMems": "",
            "KernelMemory": 0,
            "KernelMemoryTCP": 0,
            "MemoryReservation": 0,
            "MemorySwap": 128000000,
            "MemorySwappiness": null,
            "CpuCount": 0,
            "CpuPercent": 0,

requests,limits に設定した値と docker オプションに設定されている値を整理すると以下になります。

Kubernetes パラメータ 設定値 docker へ渡す際の計算式 docker オプション 実際に設定されていた値
requests.cpu 100m コア値に変換され 1024倍された値と 2 の大きい方が使われる
100 / 1000 * 1024 = 102.4
--cpu-shares 102
limits.cpu 200m ミリコア値に変換され 100倍される
200 * 100 = 20000
--cpu-quota 20000
requests.memory 64M (docker オプションに渡さない) - -
limits.memory 128M 整数に変換される
128 * 1000 * 1000 = 128000000(MB を B に変換)
--memory 128000000

Kubernetes 側に設定したパラメータが実際に docker オプションに渡されている事が確認できました。

requests.cpu の「コア値に変換され 1024倍された値と 2 の大きい方が使われる」の確認のため、数パターンの requests,limits で試してみました。

requests.cpu --cpu-shares
指定なし 2
0 2
1m 2
2m 2
3m 3
4m 4
100m 102
1000m 1024
limits.cpu --cpu-quota
指定なし 0
0 0
1m 1000
2m 1000
3m 1000
4m 1000
10m 1000
11m 1100
12m 1200
100m 10000
1000m 100000
limits.memory --memory
指定なし 0
0 0
8M 8000000
16M 16000000
1G 1000000000

requests.cpu と limits.memory は想定通りでしたが、limits.cpu に関して、1m - 10m を指定した場合は --cpu-quota が 1000 になる事がわかりました。 (100 ミリ秒間に使用できる CPU 時間は 1000 マイクロ秒 = 1 ミリ秒となり、0.01 vCPU が上限となる)

3.2. requests

続いて、requests まわりの検証を実施します。作業前の describe node の結果は以下です。

$ kubectl describe node
~省略~

Capacity:
  attachable-volumes-aws-ebs:  25
  cpu:                         2
  ephemeral-storage:           31444972Ki
  hugepages-1Gi:               0
  hugepages-2Mi:               0
  memory:                      3967480Ki
  pods:                        20
Allocatable:
  attachable-volumes-aws-ebs:  25
  cpu:                         1930m
  ephemeral-storage:           27905944324
  hugepages-1Gi:               0
  hugepages-2Mi:               0
  memory:                      3412472Ki
  pods:                        20

~省略~

Non-terminated Pods:          (5 in total)
  Namespace                   Name                               CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                               ------------  ----------  ---------------  -------------  ---
  kube-system                 aws-node-drqbq                     10m (0%)      0 (0%)      0 (0%)           0 (0%)         10h
  kube-system                 coredns-76f4967988-kdvr5           100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     10h
  kube-system                 coredns-76f4967988-rf7fv           100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     10h
  kube-system                 kube-proxy-lzz5r                   100m (5%)     0 (0%)      0 (0%)           0 (0%)         10h
  kube-system                 metrics-server-7b9c4d7fd9-r6x5l    100m (5%)     0 (0%)      200Mi (6%)       0 (0%)         6m55s
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource                    Requests     Limits
  --------                    --------     ------
  cpu                         410m (21%)   0 (0%)
  memory                      340Mi (10%)  340Mi (10%)
  ephemeral-storage           0 (0%)       0 (0%)
  hugepages-1Gi               0 (0%)       0 (0%)
  hugepages-2Mi               0 (0%)       0 (0%)
  attachable-volumes-aws-ebs  0            0

requests を設定して Pod を起動した際の Node の状態

以下設定の deployment で確認します。

resources:
requests:
  cpu: "100m"
  memory: "64M"

deployment を起動します。

$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created

deployment 起動後の describe node の結果です。

$ kubectl describe node
~省略~

Non-terminated Pods:          (6 in total)
  Namespace                   Name                               CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                               ------------  ----------  ---------------  -------------  ---
  default                     test-deployment-d5d8f4656-585d7    100m (5%)     0 (0%)      64M (1%)         0 (0%)         11s
  kube-system                 aws-node-drqbq                     10m (0%)      0 (0%)      0 (0%)           0 (0%)         10h
  kube-system                 coredns-76f4967988-kdvr5           100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     10h
  kube-system                 coredns-76f4967988-rf7fv           100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     10h
  kube-system                 kube-proxy-lzz5r                   100m (5%)     0 (0%)      0 (0%)           0 (0%)         10h
  kube-system                 metrics-server-7b9c4d7fd9-r6x5l    100m (5%)     0 (0%)      200Mi (6%)       0 (0%)         7m27s
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource                    Requests         Limits
  --------                    --------         ------
  cpu                         510m (26%)       0 (0%)
  memory                      420515840 (12%)  340Mi (10%)
  ephemeral-storage           0 (0%)           0 (0%)
  hugepages-1Gi               0 (0%)           0 (0%)
  hugepages-2Mi               0 (0%)           0 (0%)
  attachable-volumes-aws-ebs  0                0

7 行目に新しく起動した Pod の情報が表示されています。 17,18 行目の Allocated(割り当て済みリソース)のサマリにも起動した Pod 分が加算されています。

requests を超えてリソースを使用できる

上記で作成した Pod に負荷をかけてみます。 まずは Pod にログインします。

$ kubectl get pod
NAME                              READY   STATUS    RESTARTS   AGE
test-deployment-d5d8f4656-585d7   1/1     Running   0          41s
$ kubectl exec -it test-deployment-d5d8f4656-585d7 -- /bin/bash

CPU 負荷をかけるため、定番の yes コマンドを Pod 内で実行します。

@pod$ yes > /dev/null

kubectl top コマンドで CPU 負荷を確認します。

## yes 実行前
$ kubectl top node
NAME                                             CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ip-10-0-101-50.ap-northeast-1.compute.internal   59m          3%     581Mi           17%
$ kubectl top pod
NAME                              CPU(cores)   MEMORY(bytes)
test-deployment-d5d8f4656-585d7   0m           1Mi

## yes 実行中
$ kubectl top node
NAME                                             CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ip-10-0-101-50.ap-northeast-1.compute.internal   1049m        54%    582Mi           17%
$ kubectl top pod
NAME                              CPU(cores)   MEMORY(bytes)
test-deployment-d5d8f4656-585d7   1001m        1Mi

15 行目の通り、Pod が requests の 100m を超えてリソースを使用できている事がわかります。(メモリも同様に requests を超過する事が可能です)

requests 分の空きが無い場合は Pod を起動できない

3 vCPU を Pod の requests.cpu に設定して確認します。(Node のキャパシティは 2 vCPU)

resources:
requests:
  cpu: "3000m"
  memory: "64M"

deployment を起動します。

$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created

Pod の状態が Pending のまま変わりません。

$ kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
test-deployment-54657959ff-zzxlm   0/1     Pending   0          2m28s

describe pod の結果です。CPU が足りないためスケジューリングできていない事がわかります

$ kubectl describe pod test-deployment-54657959ff-zzxlm
~省略~

Events:
  Type     Reason            Age                  From               Message
  ----     ------            ----                 ----               -------
  Warning  FailedScheduling  60s (x4 over 3m15s)  default-scheduler  0/1 nodes are available: 1 Insufficient cpu.

Node のメモリ上限に達すると Evict される

以下設定の deployment で確認します。

resources:
requests:
  cpu: "100m"
  memory: "64M"

Pod が起動している事を確認します。

$ kubectl get pod
NAME                              READY   STATUS    RESTARTS   AGE
test-deployment-d5d8f4656-6tdj7   1/1     Running   0          2m55s

kubectl top コマンドで Node と Pod のメモリ負荷を確認します。

$ kubectl top node
NAME                                             CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ip-10-0-102-56.ap-northeast-1.compute.internal   57m          2%     565Mi           16%
$ kubectl top pod
NAME                              CPU(cores)   MEMORY(bytes)
test-deployment-d5d8f4656-6tdj7   0m           0Mi

kubectl top だと実際のリソース使用量が反映されるまで少しタイムラグがあるため、今回は Node にログインして docker stats コマンドで Pod の負荷状況を確認します。

@node$ docker ps | grep amazonlinux
17709da044f5   public.ecr.aws/amazonlinux/amazonlinux                                       "bin/bash -c 'sleep …"   11 minutes ago   Up 11 minutes             k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0
@node$ while true; do docker stats 17709da044f5 --no-stream | tail -n1; sleep 2; done;

Pod にメモリ負荷をかけるため、Pod にログインして yes コマンドを実行します。一つでは足りないので複数実行します。

$ kubectl exec -it test-deployment-d5d8f4656-6tdj7 -- /bin/bash
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &

docker stats の実行結果は以下のようになりました。

CONTAINER ID   NAME                                                                                             CPU %     MEM USAGE / LIMIT   MEM %     NET I/O   BLOCK I/O   PIDS
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   0.00%     844KiB / 3.784GiB   0.02%     0B / 0B   0B / 0B     1
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   0.00%     844KiB / 3.784GiB   0.02%     0B / 0B   0B / 0B     1
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   172.94%   45.49MiB / 3.784GiB   1.17%     0B / 0B   0B / 0B     8
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   196.51%   242.9MiB / 3.784GiB   6.27%     0B / 0B   0B / 0B     26
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   195.59%   438.9MiB / 3.784GiB   11.33%    0B / 0B   0B / 0B     26
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   190.47%   636.3MiB / 3.784GiB   16.42%    0B / 0B   0B / 0B     30
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   191.89%   829.1MiB / 3.784GiB   21.40%    0B / 0B   0B / 0B     31
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   196.63%   1.007GiB / 3.784GiB   26.61%    0B / 0B   0B / 0B     41
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   189.07%   1.2GiB / 3.784GiB   31.71%    0B / 0B   0B / 0B     51
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   194.21%   1.393GiB / 3.784GiB   36.81%    0B / 0B   0B / 0B     53
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   186.31%   1.586GiB / 3.784GiB   41.92%    0B / 0B   0B / 0B     53
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   189.37%   1.775GiB / 3.784GiB   46.92%    0B / 0B   0B / 0B     53
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   193.21%   1.959GiB / 3.784GiB   51.78%    0B / 0B   0B / 0B     53
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   184.15%   2.133GiB / 3.784GiB   56.36%    0B / 0B   0B / 0B     53
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   194.42%   2.323GiB / 3.784GiB   61.40%    0B / 0B   0B / 0B     53
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   196.99%   2.517GiB / 3.784GiB   66.51%    0B / 0B   0B / 0B     53
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   186.78%   2.704GiB / 3.784GiB   71.47%    0B / 0B   0B / 0B     53
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   191.34%   2.894GiB / 3.784GiB   76.50%    0B / 0B   0B / 0B     53
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   193.25%   3.088GiB / 3.784GiB   81.61%    0B / 0B   0B / 0B     53
17709da044f5   k8s_amazonlinux_test-deployment-d5d8f4656-6tdj7_default_20f56f0e-f3d9-4e8d-87c5-3b2461bf742a_0   7.89%     3.099GiB / 3.784GiB   81.91%    0B / 0B   467MB / 0B   52
EOF
Error response from daemon: No such container: 17709da044f5
Error response from daemon: No such container: 17709da044f5

メモリ使用量が 3GiB を超えたあたりでコンテナが消えました。 Pod の状況を確認します。

$ kubectl get pod
NAME                              READY   STATUS    RESTARTS   AGE
test-deployment-d5d8f4656-6tdj7   0/1     Evicted   0          22m
test-deployment-d5d8f4656-rpkwz   0/1     Pending   0          3m13s
$ kubectl describe pod test-deployment-d5d8f4656-6tdj7
Name:           test-deployment-d5d8f4656-6tdj7
Namespace:      default
Priority:       0
Node:           ip-10-0-102-56.ap-northeast-1.compute.internal/
Start Time:     Wed, 17 Nov 2021 23:07:18 +0900
Labels:         app=test-app
                pod-template-hash=d5d8f4656
Annotations:    kubernetes.io/psp: eks.privileged
Status:         Failed
Reason:         Evicted
Message:        The node was low on resource: memory. Container amazonlinux was using 3274620Ki, which exceeds its request of 64M.

~省略~

Events:
  Type     Reason        Age    From               Message
  ----     ------        ----   ----               -------
  Normal   Scheduled     22m    default-scheduler  Successfully assigned default/test-deployment-d5d8f4656-6tdj7 to ip-10-0-102-56.ap-northeast-1.compute.internal
  Normal   Pulling       22m    kubelet            Pulling image "public.ecr.aws/amazonlinux/amazonlinux:latest"
  Normal   Pulled        22m    kubelet            Successfully pulled image "public.ecr.aws/amazonlinux/amazonlinux:latest" in 10.601201753s
  Normal   Created       22m    kubelet            Created container amazonlinux
  Normal   Started       22m    kubelet            Started container amazonlinux
  Warning  NodeNotReady  3m43s  node-controller    Node is not ready
  Warning  Evicted       3m35s  kubelet            The node was low on resource: memory. Container amazonlinux was using 3274620Ki, which exceeds its request of 64M.
  Normal   Killing       3m32s  kubelet            Stopping container amazonlinux

15,16 行目の通り、Node のリソースが不足したため Pod が Evict された事がわかります。

CPU リソースの奪い合いになった際の挙動

※メモリは Eviction Manager による Evict か OOMKiller により Kill されるため CPU のみ確認

requests.cpu が 200m の Pod(deployment)を二つ起動します。 最初に記載した通り requests.cpu は --cpu-shares に渡されます。--cpu-shares は CPU 割り当ての相対値のため今回のケースでは Pod A と B が使用できる CPU の比率は 1 対 1 となります。(A,B ともに 200m のため)

f:id:dunkshoot:20211119010606p:plain

まずは Pod A に yes コマンドで限界まで CPU 負荷をかけてみます。

$ kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
deployment-a-6896bf676b-2wm6r   1/1     Running   0          13s
deployment-b-567c479789-mk4wm   1/1     Running   0          9s
$ kubectl exec -it deployment-a-6896bf676b-2wm6r -- /bin/bash
@pod A$ yes > /dev/null &
@pod A$ yes > /dev/null &

kubectl top で確認すると Pod A が 2vCPU を全て使っており、Pod B は全く使っていないことがわかります。

$ kubectl top node
NAME                                             CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ip-10-0-101-93.ap-northeast-1.compute.internal   2004m        103%   581Mi           17%
$ kubectl top pod
NAME                            CPU(cores)   MEMORY(bytes)
deployment-a-6896bf676b-2wm6r   1916m        1Mi
deployment-b-567c479789-mk4wm   0m           0Mi

図に表すと以下のようになります。

f:id:dunkshoot:20211119212742p:plain

この時に Pod B に同様に限界まで CPU を負荷をかけてみます。CPU 割り当て比率は 1 対 1 のため、Pod A の CPU 使用量が半減して Pod B が CPU を使えるはずです。

$ kubectl exec -it deployment-b-567c479789-mk4wm -- /bin/bash
@pod B$ yes > /dev/null &
@pod B$ yes > /dev/null &

kubectl top で確認すると Pod A と B の CPU 使用量が同じぐらいになりました。requests.cpu が相対値となっている事がわかります。

$ kubectl top node
NAME                                             CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ip-10-0-101-93.ap-northeast-1.compute.internal   2007m        103%   578Mi           17%
$ kubectl top pod
NAME                            CPU(cores)   MEMORY(bytes)
deployment-a-6896bf676b-2wm6r   1011m        1Mi
deployment-b-567c479789-mk4wm   974m         1Mi

図に表すと以下のようになります。

f:id:dunkshoot:20211119213004p:plain

3.3. limits

続いて、limits まわりの検証を実施します。

limits を設定して Pod を起動した際の Node の状態

以下設定の deployment で確認します。

resources:
requests:
  cpu: "100m"
  memory: "64M"
limits:
  cpu: "200m"
  memory: "128M"

deployment を起動します。

$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created

deployment 起動後の describe node の結果です。

$ kubectl describe node
~省略~

Non-terminated Pods:          (6 in total)
  Namespace                   Name                                CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                                ------------  ----------  ---------------  -------------  ---
  default                     test-deployment-75cff5888b-tbh5p    100m (5%)     200m (10%)  64M (1%)         128M (3%)      93s
  kube-system                 aws-node-j44fg                      10m (0%)      0 (0%)      0 (0%)           0 (0%)         35m
  kube-system                 coredns-76f4967988-tczcz            100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     50m
  kube-system                 coredns-76f4967988-ttpdm            100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     50m
  kube-system                 kube-proxy-nkp2v                    100m (5%)     0 (0%)      0 (0%)           0 (0%)         35m
  kube-system                 metrics-server-dbf765b9b-m2msv      100m (5%)     0 (0%)      200Mi (6%)       0 (0%)         23m
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource                    Requests         Limits
  --------                    --------         ------
  cpu                         510m (26%)       200m (10%)
  memory                      420515840 (12%)  484515840 (13%)
  ephemeral-storage           0 (0%)           0 (0%)
  hugepages-1Gi               0 (0%)           0 (0%)
  hugepages-2Mi               0 (0%)           0 (0%)
  attachable-volumes-aws-ebs  0                0

7 行目に新しく起動した Pod の情報が表示されています。requests だけでなく limits も表示されています。 17,18 行目の Allocated(割り当て済みリソース)のサマリにも起動した Pod 分が加算されています。

limits を超えてリソースを使用できない(CPU)

上記で作成した Pod に CPU 負荷をかけてみます。

リソース設定は以下の通り、200m が上限です。

resources:
requests:
  cpu: "100m"
  memory: "64M"
limits:
  cpu: "200m"
  memory: "128M"

Pod にログインして yes コマンドを実行します。

$ kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
test-deployment-75cff5888b-tbh5p   1/1     Running   0          109s
$ kubectl exec -it test-deployment-75cff5888b-tbh5p -- /bin/bash
@pod$ yes > /dev/null

Node にログインして docker stats で負荷を確認します。

@node$ while true; do docker stats fd0ba9708571 --no-stream | tail -n1; sleep 2; done;
CONTAINER ID   NAME                                                                                              CPU %     MEM USAGE / LIMIT    MEM %     NET I/O   BLOCK I/O     PIDS
fd0ba9708571   k8s_amazonlinux_test-deployment-75cff5888b-tbh5p_default_effa950c-0594-42e2-92b8-aa957b064339_0   0.00%     1.312MiB / 122.1MiB   1.08%     0B / 0B   65.5kB / 0B   2
fd0ba9708571   k8s_amazonlinux_test-deployment-75cff5888b-tbh5p_default_effa950c-0594-42e2-92b8-aa957b064339_0   0.00%     1.312MiB / 122.1MiB   1.08%     0B / 0B   65.5kB / 0B   2
fd0ba9708571   k8s_amazonlinux_test-deployment-75cff5888b-tbh5p_default_effa950c-0594-42e2-92b8-aa957b064339_0   0.00%     1.312MiB / 122.1MiB   1.08%     0B / 0B   65.5kB / 0B   2
fd0ba9708571   k8s_amazonlinux_test-deployment-75cff5888b-tbh5p_default_effa950c-0594-42e2-92b8-aa957b064339_0   21.56%    1.562MiB / 122.1MiB   1.28%     0B / 0B   65.5kB / 0B   3
fd0ba9708571   k8s_amazonlinux_test-deployment-75cff5888b-tbh5p_default_effa950c-0594-42e2-92b8-aa957b064339_0   20.99%    1.562MiB / 122.1MiB   1.28%     0B / 0B   65.5kB / 0B   3
fd0ba9708571   k8s_amazonlinux_test-deployment-75cff5888b-tbh5p_default_effa950c-0594-42e2-92b8-aa957b064339_0   19.69%    1.562MiB / 122.1MiB   1.28%     0B / 0B   65.5kB / 0B   3
fd0ba9708571   k8s_amazonlinux_test-deployment-75cff5888b-tbh5p_default_effa950c-0594-42e2-92b8-aa957b064339_0   22.36%    1.562MiB / 122.1MiB   1.28%     0B / 0B   65.5kB / 0B   3
fd0ba9708571   k8s_amazonlinux_test-deployment-75cff5888b-tbh5p_default_effa950c-0594-42e2-92b8-aa957b064339_0   21.33%    1.562MiB / 122.1MiB   1.28%     0B / 0B   65.5kB / 0B   3
fd0ba9708571   k8s_amazonlinux_test-deployment-75cff5888b-tbh5p_default_effa950c-0594-42e2-92b8-aa957b064339_0   20.24%    1.562MiB / 122.1MiB   1.28%     0B / 0B   65.5kB / 0B   3
fd0ba9708571   k8s_amazonlinux_test-deployment-75cff5888b-tbh5p_default_effa950c-0594-42e2-92b8-aa957b064339_0   19.77%    1.562MiB / 122.1MiB   1.28%     0B / 0B   65.5kB / 0B   3

Pod の CPU 使用率の上限が 20%(200m コア)あたりになっており、limits を超えていない事がわかります。

limits を越えようとしても Pod は削除されずに起動し続けました。(メモリの場合とは挙動が異なります)

※docker stats の CPU % は 1 コアで 100% のため、20% の場合は 0.2 コア = 200m コアになる

limits を超えてリソースを使用できない(メモリ)

続いてメモリに負荷をかけます。

リソース設定は以下の通り、128MB が上限です。

resources:
requests:
  cpu: "100m"
  memory: "64M"
limits:
  cpu: "200m"
  memory: "128M"

Pod にログインして yes コマンドを実行します。

$ kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
test-deployment-75cff5888b-hd8xz   1/1     Running   0          5m44s
$ kubectl exec -it test-deployment-75cff5888b-hd8xz -- /bin/bash
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &
@pod$ /dev/null < $(yes) &
@pod$ [1]   Killed                  /dev/null < $(yes)
@pod$ [2]   Killed                  /dev/null < $(yes)
@pod$ [3]   Killed                  /dev/null < $(yes)
@pod$ [4]-  Killed                  /dev/null < $(yes)
@pod$ [5]+  Killed                  /dev/null < $(yes)

yes コマンドが kill されました。Pod ごと Kill される想定でしたが違った結果になりました。

docker stats の結果です。

@node$ while true; do docker stats 505ae5eb47a6 --no-stream | tail -n1; sleep 2; done;
CONTAINER ID   NAME 
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   0.00%     720KiB / 122.1MiB   0.58%     0B / 0B   0B / 0B     1
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   0.00%     720KiB / 122.1MiB   0.58%     0B / 0B   0B / 0B     1
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   0.00%     720KiB / 122.1MiB   0.58%     0B / 0B   0B / 0B     1
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   0.00%     720KiB / 122.1MiB   0.58%     0B / 0B   0B / 0B     1
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   0.00%     720KiB / 122.1MiB   0.58%     0B / 0B   0B / 0B     1
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   0.00%     720KiB / 122.1MiB   0.58%     0B / 0B   0B / 0B     1
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   0.00%     1.281MiB / 122.1MiB   1.05%     0B / 0B   0B / 0B     2
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   19.42%    20.14MiB / 122.1MiB   16.50%    0B / 0B   0B / 0B     12
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   19.87%    41.03MiB / 122.1MiB   33.61%    0B / 0B   0B / 0B     12
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   20.21%    62.81MiB / 122.1MiB   51.46%    0B / 0B   0B / 0B     12
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   20.91%    83.11MiB / 122.1MiB   68.09%    0B / 0B   0B / 0B     12
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   19.87%    104.1MiB / 122.1MiB   85.30%    0B / 0B   0B / 0B     12
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   77.51%    92.06MiB / 122.1MiB   75.41%    0B / 0B   0B / 90.6kB   12
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   37.26%    65.62MiB / 122.1MiB   53.75%    0B / 0B   0B / 90.6kB   11
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   14.88%    23.12MiB / 122.1MiB   18.94%    0B / 0B   0B / 90.6kB   9
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   0.00%     1.547MiB / 122.1MiB   1.27%     0B / 0B   0B / 90.6kB   7
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   0.00%     1.547MiB / 122.1MiB   1.27%     0B / 0B   0B / 90.6kB   7
505ae5eb47a6   k8s_amazonlinux_test-deployment-75cff5888b-hd8xz_default_cf5d282d-f128-44f9-9586-0eace273c69a_0   0.00%     1.547MiB / 122.1MiB   1.27%     0B / 0B   0B / 90.6kB   7

100 MB を超えたあたりでメモリ使用量が下がってます。このタイミングで yes が kill されています。

Pod の状態を確認します。

$ kubectl describe pod test-deployment-75cff5888b-hd8xz
~省略~

Containers:
  amazonlinux:
    Container ID:  docker://505ae5eb47a638015f612b6b364b3984e73bfde35c187b252aa18719d706c6e7
    Image:         public.ecr.aws/amazonlinux/amazonlinux:latest
    Image ID:      docker-pullable://public.ecr.aws/amazonlinux/amazonlinux@sha256:916dbbb288948b54c94b5b9f0769085aa601d4468d099e90d8a7da5cfa551b50
    Port:          <none>
    Host Port:     <none>
    Command:
      bin/bash
      -c
      sleep 3600
    State:          Running

State が Running になっており kill されていません。OOM Killer によってコンテナのプロセスではなく負荷コマンド yes のプロセスが kill されたためです。

想定挙動とは異なる結果となったため、今度はコンテナ起動時に負荷コマンドを実行してみます。(コンテナのプロセスそのものが負荷をかけている状態にする)

負荷テスト用の polinux/stress イメージを使って 128MB 上限の Pod に 256MB の負荷をかけます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: test-app
  template:
    metadata:
      labels:
        app: test-app
    spec:
      containers:
      - name: memory-test
        image: polinux/stress
        resources:
          requests:
            memory: "64M"
          limits:
            memory: "128M"
        command: ["stress"]
        args: ["--vm", "1", "--vm-bytes", "256M", "--vm-hang", "1"]
$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created
$ kubectl get pod -w
NAME                              READY   STATUS              RESTARTS   AGE
test-deployment-f5784d6f4-rbrfb   0/1     ContainerCreating   0          2s
test-deployment-f5784d6f4-rbrfb   1/1     Running             0          3s
test-deployment-f5784d6f4-rbrfb   0/1     OOMKilled           0          4s
test-deployment-f5784d6f4-rbrfb   1/1     Running             1          6s
test-deployment-f5784d6f4-rbrfb   0/1     OOMKilled           1          7s
test-deployment-f5784d6f4-rbrfb   0/1     CrashLoopBackOff    1          8s
test-deployment-f5784d6f4-rbrfb   0/1     OOMKilled           2          22s
test-deployment-f5784d6f4-rbrfb   0/1     CrashLoopBackOff    2          34s
test-deployment-f5784d6f4-rbrfb   0/1     OOMKilled           3          49s

Running になった直後に OOMKilled 状態になりました。 describe の結果からも OOMKiller によって kill された事がわかります。

$ kubeclt describe pod test-deployment-f5784d6f4-rbrfb
~省略~

Containers:
  memory-test:
    Container ID:  docker://05c9e3436ea39469706baab7996ce056b152a3be3fe8aebc31ca9c4d07d15072
    Image:         polinux/stress
    Image ID:      docker-pullable://polinux/stress@sha256:b6144f84f9c15dac80deb48d3a646b55c7043ab1d83ea0a697c09097aaad21aa
    Port:          <none>
    Host Port:     <none>
    Command:
      stress
    Args:
      --vm
      1
      --vm-bytes
      256M
      --vm-hang
      1
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    1
      Started:      Thu, 18 Nov 2021 00:18:02 +0900
      Finished:     Thu, 18 Nov 2021 00:18:03 +0900
    Ready:          False
    Restart Count:  3

※ OOM score まわりについては別記事で整理します

limits 分の空きが無い場合でも Pod を起動できる

3 vCPU を Pod の limits.cpu に設定して確認します。(Node のキャパシティは 2 vCPU)

resources:
requests:
  cpu: "100m"
  memory: "64M"
limits:
  cpu: "3000m"
  memory: "64M"

エラーなく Pod を起動できます。

$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created
$ kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
test-deployment-9f4c9f997-zgngg   1/1     Running   0          7s

describe node の結果は以下です。17 行目の通り limits に 100% を超えた値が設定されています。

$ kubectl describe node
~省略~

Non-terminated Pods:          (6 in total)
  Namespace                   Name                               CPU Requests  CPU Limits  Memory Requests  Memory Limits  Age
  ---------                   ----                               ------------  ----------  ---------------  -------------  ---
  default                     test-deployment-9f4c9f997-zgngg    100m (5%)     3 (155%)    64M (1%)         128M (3%)      48s
  kube-system                 aws-node-j44fg                     10m (0%)      0 (0%)      0 (0%)           0 (0%)         151m
  kube-system                 coredns-76f4967988-tczcz           100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     166m
  kube-system                 coredns-76f4967988-ttpdm           100m (5%)     0 (0%)      70Mi (2%)        170Mi (5%)     166m
  kube-system                 kube-proxy-nkp2v                   100m (5%)     0 (0%)      0 (0%)           0 (0%)         151m
  kube-system                 metrics-server-dbf765b9b-m2msv     100m (5%)     0 (0%)      200Mi (6%)       0 (0%)         139m
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource                    Requests         Limits
  --------                    --------         ------
  cpu                         510m (26%)       3 (155%)
  memory                      420515840 (12%)  484515840 (13%)
  ephemeral-storage           0 (0%)           0 (0%)
  hugepages-1Gi               0 (0%)           0 (0%)
  hugepages-2Mi               0 (0%)           0 (0%)
  attachable-volumes-aws-ebs  0                0

requests に何も設定しない場合は limits と同じ値が requests に設定される

以下設定の deployment で確認します。

resources:
limits:
  cpu: "200m"
  memory: "128M"

Pod を起動します。

$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment configured
$ kubectl get pod
NAME                               READY   STATUS        RESTARTS   AGE
test-deployment-6f5f85d8d4-hn5f9   1/1     Running       0          33s

describe pod の結果です。

$ kubectl describe pod test-deployment-6f5f85d8d4-hn5f9
~省略~

    State:          Running
      Started:      Thu, 18 Nov 2021 01:39:51 +0900
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     200m
      memory:  128M
    Requests:
      cpu:        200m
      memory:     128M

11-13 行目の通り、requests に limits と同じ値が設定されています。

3.4. LimitRange

デフォルトの requests,limits を設定

コンテナに対してデフォルトの requests.cpu を 250m、limits.cpu を 500m に設定した LimitRange(limitrange.yaml)を作成します。

apiVersion: v1
kind: LimitRange
metadata:
  name: cpu-limit-range
spec:
  limits:
  - default:
      cpu: 500m
    defaultRequest:
      cpu: 250m
    type: Container

apply します。

$ kubectl apply -f limitrange.yaml
limitrange/cpu-limit-range created
$ kubectl get limitrange
NAME              CREATED AT
cpu-limit-range   2021-11-19T13:26:17Z
$ kubectl describe limitrange
Name:       cpu-limit-range
Namespace:  default
Type        Resource  Min  Max  Default Request  Default Limit  Max Limit/Request Ratio
----        --------  ---  ---  ---------------  -------------  -----------------------
Container   cpu       -    -    250m             500m           -

requests.cpu,limits.cpu を指定していない Pod を起動します。

$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created

describe pod を確認すると、LimitsRange で指定した requests.cpu と limits.cpu が Pod に設定されている事がわかります。

$ kubectl describe pod
~省略~

    State:          Running
      Started:      Fri, 19 Nov 2021 22:27:23 +0900
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:  500m
    Requests:
      cpu:        250m

ちなみに limitrange を削除した後に Pod を再起動(delete pod 実行後に deployment により新しい Pod が起動)したところ requests,limits は設定されていませんでした。

リソース使用量の最小値、最大値を設定

メモリの最小使用量を 64MB、最大使用量を 128MB に設定した LimitRange を作成します。

apiVersion: v1
kind: LimitRange
metadata:
  name: memory-max
spec:
  limits:
  - min:
      memory: 64M
    max:
      memory: 128M
    type: Container

apply します。

$ kubectl apply -f limitrange.yaml
limitrange/memory-max created
$ kubectl get limitrange
NAME         CREATED AT
memory-max   2021-11-19T13:33:06Z
$ kubectl describe limitrange
Name:       memory-max
Namespace:  default
Type        Resource  Min  Max   Default Request  Default Limit  Max Limit/Request Ratio
----        --------  ---  ---   ---------------  -------------  -----------------------
Container   memory    64M  128M  128M             128M           -

最小値、最大値だけでなくデフォルトの requests,limits も設定されました。 これは、デフォルトの requests,limits を指定しない場合は最大値に指定した値が設定されるためです。

256MB のメモリを使用する Pod を起動します。

$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created
$ kubectl describe pod
~省略~

    Limits:
      memory:  128M
    Requests:
      memory:     128M

LimitRange の最大値に指定した値(128M)が Pod の limits に設定されるため、256M を使用する Pod は OOMKilled になり再起動を繰り返す状態になりました。

$ kubectl get pod -w
NAME                              READY   STATUS    RESTARTS   AGE
test-deployment-889b488cd-gf7mx   0/1     Pending   0          0s
test-deployment-889b488cd-gf7mx   0/1     ContainerCreating   0          0s
test-deployment-889b488cd-gf7mx   0/1     OOMKilled           0          3s
test-deployment-889b488cd-gf7mx   1/1     Running             1          5s
test-deployment-889b488cd-gf7mx   0/1     OOMKilled           1          6s
test-deployment-889b488cd-gf7mx   0/1     CrashLoopBackOff    1          7s
test-deployment-889b488cd-gf7mx   1/1     Running             2          24s
test-deployment-889b488cd-gf7mx   0/1     OOMKilled           2          25s

3.5. QoS Class

requests,limits が CPU,メモリともに設定されていて requests と limits が同じ値となる Pod を起動します。(Guaranteed の条件)

$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created
$ kubectl describe pod
~省略~

QoS Class:                   Guaranteed

QoS Class が Guaranteed になっています。

requests,limits が異なる Pod を起動します。(Burstable の条件)

$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created
$ kubectl describe pod
~省略~

QoS Class:                   Burstable

QoS Class が Burstable になっています。

requests,limits を指定しない Pod を起動します。(BestEffort の条件)

$ kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created
$ kubectl describe pod
~省略~

QoS Class:                   BestEffort

QoS Class が BestEffort になっています。

3.6. ResourceQuota

全 Pod の requests.cpu の合計を 1 に設定する ResourceQuota(resourcequota.yaml)を作成します。

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-resources
spec:
  hard:
    requests.cpu: "1"

apply します。

$ kubectl apply -f resoucequota.yaml
resourcequota/compute-resources created
$ kubectl get resourcequota
NAME                AGE   REQUEST             LIMIT
compute-resources   31s   requests.cpu: 0/1
$ kubectl describe resourcequota
Name:         compute-resources
Namespace:    default
Resource      Used  Hard
--------      ----  ----
requests.cpu  0     1

requests.cpu が 200m の Pod を 10 個起動する deployment を起動してみます。

$ kubectl apply -f test-deployment5.yaml
deployment.apps/test-deployment created
$kubectl get pod
NAME                              READY   STATUS    RESTARTS   AGE
test-deployment-8d58b6999-7j9cl   1/1     Running   0          10s
test-deployment-8d58b6999-7rx82   1/1     Running   0          10s
test-deployment-8d58b6999-9cpvk   1/1     Running   0          10s
test-deployment-8d58b6999-swk4v   1/1     Running   0          10s
test-deployment-8d58b6999-z2xww   1/1     Running   0          10s
$ kubectl get deploy
NAME              READY   UP-TO-DATE   AVAILABLE   AGE
test-deployment   5/10    5            5           52s

Pod が ResourceQuota で設定した 5 個までしか起動できていない事がわかります。

4. 所管

かなり長くなってしまった。Kubernetes は考えなきゃいけない事が多いですね。

5. 参考

コンテナのリソース管理 | Kubernetes

ノード | Kubernetes

Reserve Compute Resources for System Daemons | Kubernetes

Limit Range | Kubernetes

PodにQuality of Serviceを設定する | Kubernetes

community/resource-qos.md at master · kubernetes/community · GitHub

リソースクォータ | Kubernetes

Node-pressure Eviction | Kubernetes

Pod Priority and Preemption | Kubernetes

Configure Default CPU Requests and Limits for a Namespace | Kubernetes

リソースクォータ | Kubernetes

eksctl コマンドで EKS Cluster を作成する

1. 構成

以下の構成で EKS Cluster を作成します。 f:id:dunkshoot:20211031131158p:plain

2. 手順

2.1. VPC 作成 

Terraform(1.0.8)を使用します。イチからコードを書くのは面倒なので今回は terraform registry の VPC Module(Simple VPC)を使用します。

https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest

必要なファイルは 2 個です。 まずは provider.tf を作成します。

provider "aws" {
  region  = "ap-northeast-1"
  profile = "default"
}


二つ目は上記 Module のドキュメントに記載の通り main.tf を作成します。 なお、今回は検証環境のため private subnet は作成しません。(NAT Gateway にお金がかかるため)

module "ekstest_vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "ekstest-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway = false
  enable_vpn_gateway = false

  tags = {
    Terraform = "true"
    Environment = "dev"
  }
}


あとは terraform init -> plan -> apply するだけです。

%terraform init
%terraform plan
%terraform apply


以下を参考に、Node に設定する SSH キーペア(名前:ekstest)も作成しておきます。(手順は割愛)

Amazon EC2 のキーペアと Linux インスタンス - Amazon Elastic Compute Cloud

2.2. EKS Cluster 作成

まずは以下を参考にして eksctl をインストールします。

eksctl コマンドラインユーティリティ - Amazon EKS

mac だとこんな感じです。

%/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
%brew tap weaveworks/tap
%brew install weaveworks/tap/eksctl
%eksctl version
0.71.0


次に eks cluster の yaml を作成します。

ドキュメントとサンプルコードは以下です。

https://eksctl.io/usage/creating-and-managing-clusters/

https://github.com/weaveworks/eksctl/tree/main/examples

今回は以下の要件で yaml を作成しました。

  • 手順 2.1. で作成した VPC,キーペアを使用
  • Node は Self ではなく Managed
  • Node のインスタントタイプは t3.micro
  • EBS は 30GB(指定しない場合のデフォルトは 80GB)
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: ekstest
  region: ap-northeast-1
  version: "1.21"

vpc:
  id: "vpc-063b58ff16344acfd"
  subnets:
    public:
      ap-northeast-1a:
          id: "subnet-06324dcadf5706acb"
      ap-northeast-1c:
          id: "subnet-048cad38a6c49de67"
      ap-northeast-1d:
          id: "subnet-0bb85370bb4b4528d"

managedNodeGroups:
  - name: managed-ng
    instanceType: t3.micro
    desiredCapacity: 3
    volumeSize: 30
    availabilityZones: ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
    ssh:
      allow: true
      publicKeyName: ekstest


作成した yaml を eksctl コマンドのオプションに指定して Cluster を作成します。 裏で CloudFormation が動き、作成完了までに 20 分ぐらいかかりました。

%eksctl create cluster -f ekstest.yaml

2.3. 動作確認

まずは kubectl をインストールします。

kubectl のインストール - Amazon EKS

%curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.21.2/2021-07-05/bin/darwin/amd64/kubectl
%chmod +x ./kubectl
%mkdir -p $HOME/bin && cp ./kubectl $HOME/bin/kubectl && export PATH=$HOME/bin:$PATH
%kubectl version --short --client
Client Version: v1.21.2-13+d2965f0db10712


次に aws CLI で、Cluster 用 kubeconfig ファイルをローカルに作成します。この作業により Cluster にアクセスできるようになります。

Amazon EKS の kubeconfig を作成する - Amazon EKS

%aws eks update-kubeconfig --name ekstest


Cluster の情報を取得できる事を確認します。

%kubectl get node
NAME                                              STATUS   ROLES    AGE     VERSION
ip-10-0-101-250.ap-northeast-1.compute.internal   Ready    <none>   5m22s   v1.21.4-eks-033ce7e
ip-10-0-102-235.ap-northeast-1.compute.internal   Ready    <none>   5m16s   v1.21.4-eks-033ce7e
ip-10-0-103-105.ap-northeast-1.compute.internal   Ready    <none>   5m19s   v1.21.4-eks-033ce7e

%kubectl get ns
NAME              STATUS   AGE
default           Active   13m
kube-node-lease   Active   13m
kube-public       Active   13m
kube-system       Active   13m


適当な deployment を作成して Pod が起動できる事を確認します。

Amazon Linux 2 のコンテナを 1 時間起動する deployment のマニフェスト(test-deployment.yaml)を作成します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: test-app
  template:
    metadata:
      labels:
        app: test-app
    spec:
      containers:
      - name: amazonlinux
        image: public.ecr.aws/amazonlinux/amazonlinux:latest
        command:
          - "bin/bash"
          - "-c"
          - "sleep 3600"

マニフェストをデプロイします。

%kubectl apply -f test-deployment.yaml
deployment.apps/test-deployment created

Amazon Linux 2 のコンテナ が起動している事を確認します。

% get pod
NAME                               READY   STATUS    RESTARTS   AGE
test-deployment-568d74bfdb-8hmjt   1/1     Running   0          5m3s
test-deployment-568d74bfdb-8trxr   1/1     Running   0          3m55s
test-deployment-568d74bfdb-k8mcj   1/1     Running   0          4m28s

%kubectl exec -it test-deployment-568d74bfdb-8hmjt -- /bin/bash
bash-4.2# uname -r
5.4.149-73.259.amzn2.x86_64

2.4. EKS Cluster 削除

delete cluster を実行します。

%eksctl delete cluster -f ekstest.yaml

2.5. VPC 削除

terraform destroy を実行します。

%terraform destroy

3. 料金

今回の構成にかかる費用は以下です。(NW 通信料とか細かいものは除外)

リソース 料金(USD/時) 料金(USD/月(30 日)) メモ
EKS 1 Cluster 0.1 72 料金 - Amazon EKS | AWS
EC2 t3.micro x 3 0.0408 29.376 オンデマンドインスタンスの料金 - Amazon EC2 (仮想サーバー) | AWS
EBS gp3 30GB x 3 0.012 8.64 ハイパフォーマンスブロックストレージの料金 – Amazon EBS の料金 – Amazon Web Services
合計 - 0.1528 110.016

今回は Ondemand Instance 使いましたが Spot Instance を使えばもっと安くなります。

Amazon EKS が、マネージド型ノードグループでの EC2 スポットインスタンスのプロビジョニングと管理をサポート | Amazon Web Services ブログ

4. 所管

terraform module を使えばサクッと VPC を構築できますし、EKS Cluster の yaml もシンプルなので簡単に EKS 検証環境を構築できました。

なお、Cluster 作成の eksctl コマンドを Ctrl-C で kill しても CloudFormation Stack は削除されないため手動で Stack を削除するか eks delete cluster を実行する必要があるので注意が必要です。(CloudFormation と連携してるサービスあるあるかもですが)