こんにちは、Red Hatでソリューションアーキテクトをしている石川です。
OpenShiftを含むKubernetesのクラスタを設計する際に悩まれるポイントの一つとして、ノードのサイジングがあると思います。
実際に必要となるノードのサイズは、その上で動かすコンテナアプリに依存するため、その要件を確認した上で決める必要がありますが、一つ抑えておきたいポイントとしてノードの保有するCPU、メモリを全てコンテナアプリで使用することができるわけではないということです。
この時関係してくるのがkubeletのsystem-reservedという設定であり、今回はOpenShiftにおいてこのsystem-reservedがどのように設定されるのかを紹介したいと思います。
Kubernetesのノードでコンテナに割り当て可能なリソース
Kubernetesの公式ドキュメントを見ると、ノードのリソースのうち、実際にPodに割り当て可能な領域は以下のallocatable
で表される領域であると書かれています。
式として表すと、
割り当て可能なリソース(allocatable) = ノードのリソース - kube-reserved
- system-reserved
- eviction-threshold
ですね。
ここで登場する、kube-reserved
、system-reserved
、eviction-threshold
はノードで動かしているkubeletの設定オプションになります。(正確にはeviction-threshold
は--eviction-hard
というオプションで指定します。)
Kubernetesクラスタの一部としてノードを動かす場合、kube-api-serverと通信するためのkubeletと、実際にコンテナの操作を担うコンテナランタイム(OpenShiftの場合cri-o)が必要となります。
仮にノードのリソースを全てコンテナ向けに割り当てると考えると、kubeletやcri-oを動かすためのリソースが圧迫されてしまい、結果としてそのノード上で動作するコンテナ全体に影響を及ぼしてしまう(最悪、全てのコンテナが停止する)ことが考えられます。
そのためkubeletやcri-oなどのプロセスが安定的に稼働できるよう、ノード上であらかじめ必要なリソースを確保する仕組みとしてkube-reserved
、system-reserved
、eviction-threshold
が存在しています。
kube-reserved、system-reserved、eviction-thresholdについて
再び公式ドキュメントを見るとそれぞれの設定について説明があります。
kube-reservedは、kubeletやコンテナランタイムなど、
system-reservedはsshdやudevなどのリソース確保を目的とした設定項目となっています。
それぞれでCPU、Memory、ephemeral-storageの値を入力可能です。
kube-reservedとsystem-reservedはそれぞれkube-reserved-cgroup
とsystem-reserved-cgroup
というオプションと一緒に使用することができ、ここでcgroupを指定するとそのための占有リソースが確保されるようになります。
もしかするとここで、おや、と思われる方もいるかも知れません。というのもkube-reservedもsystem-reservedも、結局はkube-reserved-cgroup
とsystem-reserved-cgroup
で指定したcgroupを見て、それらのためのリソースを確保するため、設定項目の名前は違うもののできることは同じになるからです。
またこのkube-reserved-cgroup
とsystem-reserved-cgroup
は設定しないことも可能です。
その場合特定のcgroup向けのリソースを確保するのではなく、単純にPodに割り当て可能なリソースにkube-reservedとsystem-reservedで設定した値が含まれなくなるという形になります。
eviction-thresholdはこれらとはまた違う役割を持っています。
あるノードのMemoryが、その上で動くPodもしくはkubelet等のプロセスにより全て使われてしまうとSystem OOMsの状態となり、ノード上で動いている全てのPodの動作に影響を与えることが考えられます。
そのためkubeletのeviction-hard
という設定では、現在利用可能なリソースの値がeviction-hard
を下回ったら、そのノード上で動いているPodを停止させることで、リソース枯渇を防ぐという動作を取るようになります。この時どのPodが停止するかはPodのPriority等により決定されます。気をつけたい点として、この時のPodの停止は通常のGraceful Shutdownではないため注意が必要です。
kubeletではデフォルトでこのeviction-hard
が設定されています。
kubernetes.io
OpenShiftでの設定
ここまでご紹介した各設定項目をOpenShiftでどのように設定しているかについては、以下のドキュメントにまとまっています。 docs.openshift.com
OpenShiftではkube-reservedについては設定をしておらず、system-reservedのみを設定しています。これは先ほど書いたように、実際にはこの二つの項目でできることは同じであるため、system-reservedのみを使用しているのだと考えられます。
system-reservedで具体的にどれだけのリソースが確保されるかは、利用しているOpenShiftの環境により異なってきます。
OpenShiftではバージョン4.8より、system-reservedの値を使用しているノードのリソースから自動計算するという機能が追加されています。
docs.openshift.com
こちらはSelf ManagedなOpenShiftではデフォルトでオフとなっているのですが、例えばManged OpenShiftのROSAなどではデフォルトでこの機能が有効化されており、自動で計算された値がsystem-reservedに設定されています。
この時の計算方法は以下のナレッジベースに記載があるため、気になる方はご確認下さい。
access.redhat.com
ここからは実際の設定値を確認していきましょう。今回はsystem-reservedの自動計算がオンになっている環境を使っています。
まずoc proxy
コマンドを実行しておきます。
oc proxy
そうしたら別のターミナルを立ち上げ、oc get node
でノードの情報を取得します。
oc get node --- NAME STATUS ROLES AGE VERSION ip-10-0-155-114.us-east-2.compute.internal Ready worker 16h v1.22.8+9e95cb9 ip-10-0-158-172.us-east-2.compute.internal Ready master 16h v1.22.8+9e95cb9 ip-10-0-162-108.us-east-2.compute.internal Ready master 16h v1.22.8+9e95cb9 ip-10-0-168-39.us-east-2.compute.internal Ready worker 16h v1.22.8+9e95cb9 ip-10-0-207-222.us-east-2.compute.internal Ready master 16h v1.22.8+9e95cb9
あとは以下のcurlコマンドを実行することでノード上で動いているkubeletの設定を確認することができます。
curl -sSL "http://localhost:8001/api/v1/nodes/${NODE_NAME}/proxy/configz" | jq .
実行結果 (クリックで展開)
{
"kubeletconfig": {
"enableServer": true,
"staticPodPath": "/etc/kubernetes/manifests",
"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_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
],
"tlsMinVersion": "VersionTLS12",
"rotateCertificates": true,
"serverTLSBootstrap": true,
"authentication": {
"x509": {
"clientCAFile": "/etc/kubernetes/kubelet-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.30.0.10"
],
"streamingConnectionIdleTimeout": "4h0m0s",
"nodeStatusUpdateFrequency": "10s",
"nodeStatusReportFrequency": "5m0s",
"nodeLeaseDurationSeconds": 40,
"imageMinimumGCAge": "2m0s",
"imageGCHighThresholdPercent": 85,
"imageGCLowThresholdPercent": 80,
"volumeStatsAggPeriod": "1m0s",
"systemCgroups": "/system.slice",
"cgroupRoot": "/",
"cgroupsPerQOS": true,
"cgroupDriver": "systemd",
"cpuManagerPolicy": "none",
"cpuManagerReconcilePeriod": "10s",
"memoryManagerPolicy": "None",
"topologyManagerPolicy": "none",
"topologyManagerScope": "container",
"runtimeRequestTimeout": "2m0s",
"hairpinMode": "promiscuous-bridge",
"maxPods": 250,
"podPidsLimit": -1,
"resolvConf": "/etc/resolv.conf",
"cpuCFSQuota": true,
"cpuCFSQuotaPeriod": "100ms",
"nodeStatusMaxImages": 50,
"maxOpenFiles": 1000000,
"contentType": "application/vnd.kubernetes.protobuf",
"kubeAPIQPS": 50,
"kubeAPIBurst": 100,
"serializeImagePulls": false,
"evictionHard": {
"imagefs.available": "15%",
"memory.available": "100Mi",
"nodefs.available": "10%",
"nodefs.inodesFree": "5%"
},
"evictionPressureTransitionPeriod": "5m0s",
"enableControllerAttachDetach": true,
"makeIPTablesUtilChains": true,
"iptablesMasqueradeBit": 14,
"iptablesDropBit": 15,
"featureGates": {
"APIPriorityAndFairness": true,
"DownwardAPIHugePages": true,
"LegacyNodeRoleBehavior": false,
"NodeDisruptionExclusion": true,
"RotateKubeletServerCertificate": true,
"ServiceNodeExclusion": true,
"SupportPodPidsLimit": true
},
"failSwapOn": true,
"memorySwap": {},
"containerLogMaxSize": "50Mi",
"containerLogMaxFiles": 5,
"configMapAndSecretChangeDetectionStrategy": "Watch",
"systemReserved": {
"cpu": "0.11",
"memory": "5.3Gi"
},
"enforceNodeAllocatable": [
"pods"
],
"volumePluginDir": "/etc/kubernetes/kubelet-plugins/volume/exec",
"logging": {
"format": "text"
},
"enableSystemLogHandler": true,
"shutdownGracePeriod": "0s",
"shutdownGracePeriodCriticalPods": "0s",
"enableProfilingHandler": true,
"enableDebugFlagsHandler": true,
"seccompDefault": false,
"memoryThrottlingFactor": 0.8
}
}
この設定の中で"evictionHard"や"systemReserved"として値が設定されているのを確認できます。
"evictionHard"を見るとkubeletのデフォルトの設定値がOpenShiftでも適用されているのがわかります。
"evictionHard": { "imagefs.available": "15%", "memory.available": "100Mi", "nodefs.available": "10%", "nodefs.inodesFree": "5%" },
"systemReserved"には自動計算された値が設定されています。
今回はノードのメモリが約62GiBなので比較的大きな値がここに設定されているのがわかります。
"systemReserved": { "cpu": "0.11", "memory": "5.3Gi" },
また設定値を確認すると"systemReservedCgroup"が設定されていません。
つまり、OpenShiftではkubeletに--system-reserved-cgroup
を設定しておらず、特定のcgroupにリソースを割り振る動きをしていないということがわかります。
自分で割り当て可能なリソースを計算してみる
ここまでで実際の設定値も含めてsystem-reserved等について確認することができました。
折角なので、メモリを対象としてPodに割り当て可能なリソースを自分で計算したいと思います。
まずノード全体のメモリの値を以下のコマンドで取得します。
oc get node ${NODE_NAME} -o jsonpath="{.status.capacity.memory}" --- 64790784Ki
この値から先ほど確認したsystem-reservedのメモリの値とevicition-hardの値を引けば割り当て可能リソースになるはずです。
単位をKiBに合わせると、
となります。
答え合わせのため、以下のコマンドを実行しましょう。
割り当て可能なリソースはノードの.status.allocatable
から確認できます。
oc get node ${NODE_NAME} -o jsonpath="{.status.allocatable.memory}" --- 60550073548800m
単位をKiBに合わせると
ということで無事正しい値を計算できたことが確認できました。
まとめ
今回はOpenShiftのsystem-reservedの設定についてご紹介しました。
system-reserved等の設定はノードで実際にどれだけのリソースを使うことができるのかということを理解する上で大事なポイントになります。
またOpenShiftにはノードを安定して運用するためのsystem-reservedの自動計算機能もありますので、本記事を見てご興味持たれたらぜひこちらも試してみて下さい。