kubelet 이 원하는 커널 파라미터
Kubelet 은 실행하는 머신의 커널 파라미터가 특정 값이 제대로 설정되어있지 않다면 동작하지 않고 오류가 발생하게 됩니다. 이 때 kubelet에 --protect-kernel-defaults=false 라는 실행 인자를 념겨주면 정상 동작을 하게 됩니다. 이런 단순한 오류 해결을 넘어서 실제로 kubelet 은 어떤 커널 파라미터가 세팅되어있기를 원하는지, 그리고 어떤 코드에서 그런 내용을 볼 수 있는지 등에 대해서 다루는 글입니다.

최근 packer
를 이용하여 golden image
를 생성해서 Kubernetes 클러스터를 생성하는 작업을, 별다른 프로비저닝 툴의 도움 없이 (kubeadm 조차도 쓰지 않고)진행하고 있습니다.

이 작업을 하는 이유는 Virtual Machine
들로 이루어진 쿠버네티스 클러스터를 프로비저닝/운영 하기 위함이고. 전반적인 모습은 아마도 EKS
와 매우 비슷한 모습일겁니다.
EKS
는 각 쿠버네티스 컴포넌트(바이너리)가 사전에 설치된 쿠버네티스용 ami
기반으로 가상머신(ec2)를 생성하고, 그것들을 클러스터에 join 시키며, 컨트롤 플레인은 AWS가 managed 로 처리해주는 컨셉입니다.
여기서 Virtual Machine
기반의 쿠버네티스 클러스터를 만들어보고 테스트하는 과정에서, 가상머신을 한번 껐다가 켰더니 kubelet
이 아래와 같은 에러 로그를 발생시키고 있었습니다.
systemd
로 보통 실행되기때문에, 로그를 보기 위해서는 journalctl -xeu kubelet -f
와 같은 명령어를 수행하면 됩니다. 
May 02 03:05:49 control1 kubelet[1295]: E0502 03:05:49.409568 1295
kubelet.go:1542] "Failed to start ContainerManager" err="[invalid kernel flag: kernel/panic, expected value: 10, actual value: 0, invalid kernel flag: vm/overcommit_memory, expected value: 1, actual value: 0]"
위 에러 로그를 해석해보자면 kubelet
이 컨테이너 매니저를 실행시키는데 실패했고, 그 이유는 kernal/panic
커널 파라미터와 vm/overcommit_memory
커널 파라미터가 kubelet
이 “예상” 한 값과 다르기 때문에 kubelet
의 실행이 실패한것입니다.
우선 잘 알려진 해결 방법은 kubelet
을 실행 할 때 --protect-kernel-defaults=false
실행 인자를 추가해주는 방법이 소개되고 있습니다. 사실 --protect-kernel-defaults=true
가 기본값이기 때문에 위 에러 로그가 발생한것이라고 다시 해석이 가능한데요.
이 실행 인자는 kubelet
이 권장/원하는 커널 파라미터와 실제 커널 파라미터 설정값이 차이가 있을 경우 kubelet
이 오버라이드 할것이냐 말것이냐를 지정하는 옵션으로. 말 그대로 커널 파라미터 설정값을 보호 하겠냐, 보호하지 않겠냐의 의미로 해석하시면 됩니다.
따라서 --protect-kernel-defaults=false
인자를 kubelet
에 실행 인자로 주는 경우 커널 파라미터가 변경되면서 잘 동작하게 됩니다.
kubelet 의 실행 인자 추가/변경하는 방법
systemd drop in
파일에서 해당 인자를 넣어줄수도 있습니다. 각자의 쿠버네티스 환경에 맞게 적용해야 합니다. Kubelet systemd 서비스 파일을 직접 만들어서 클러스터 프로비저닝 한 경우
우선 kubelet
에 대한 systemd
drop-in
파일이 없는 구조에서, 환경변수를 사용하지 않고 kubelet
실행인자를 직접 service
파일에 지정한 경우에는 아래와 같습니다.
[Install]
WantedBy=multi-user.target
[Unit]
Description=Kubernetes Kubelet Server
Documentation=https://github.com/GoogleCloudPlatform/kubernetes
After=containerd.service
Wants=containerd.service
[Service]
ExecStart=/usr/local/bin/kubelet \
--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubeconfig \
--config=/etc/kubernetes/kubelet-config.yaml \
--kubeconfig=/etc/kubernetes/kubelet.kubeconfig \
--register-node=true \
--v=2 \
--protect-kernel-defaults=false
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target
kubelet.service
위와 같이 실행 인자에 직접 protect-kernel-defaults=false
를 넣어주면 됩니다.
Kubeadm 을 통해 프로비저닝한 클러스터

만약 systemctl status kubelet
을 했을 때 drop-in
파일이 있는경우 해당 파일을 확인합니다.
위 케이스의 경우 kubeadm
을 통해 프로비저닝한 클러스터이고. /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf
파일에 지정된 systemd
파일의 선언 블록들이 오버라이드 됩니다. 따라서 해당 파일을 확인해봅니다.
# Note: This dropin only works with kubeadm and kubelet v1.11+
[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
# This is a file that "kubeadm init" and "kubeadm join" generates at runtime, populating the KUBELET_KUBEADM_ARGS variable dynamically
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
# This is a file that the user can use for overrides of the kubelet args as a last resort. Preferably, the user should use
# the .NodeRegistration.KubeletExtraArgs object in the configuration files instead. KUBELET_EXTRA_ARGS should be sourced from this file.
EnvironmentFile=-/etc/sysconfig/kubelet
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS
10-kubeadm.conf
위 항목에 여러 환경변수를 지정하는데. $KUBELET_EXTRA_ARGS
의 경우/etc/sysconfig/kubelet
파일을 통해 로드하고. 해당 파일을 열어보면
KUBELET_EXTRA_ARGS=
/etc/sysconfig/kubelet
위와 같이 KUBELET_EXTRA_ARGS
를 볼수 있습니다. 여기서
KUBELET_EXTRA_ARGS="--protect-kernel-defaults=false"
/etc/sysconfig/kubelet
를 넣어주면 됩니다.
Kubespray 를 통해 프로비저닝한 클러스터

kubespray 를 통해 프로비저닝한 클러스터는 단일 systemd service 파일로 구성되어있습니다.
[Unit]
Description=Kubernetes Kubelet Server
Documentation=https://github.com/GoogleCloudPlatform/kubernetes
After=containerd.service
Wants=containerd.service
[Service]
EnvironmentFile=-/etc/kubernetes/kubelet.env
ExecStart=/usr/local/bin/kubelet \
$KUBE_LOGTOSTDERR \
$KUBE_LOG_LEVEL \
$KUBELET_API_SERVER \
$KUBELET_ADDRESS \
$KUBELET_PORT \
$KUBELET_HOSTNAME \
$KUBELET_ARGS \
$DOCKER_SOCKET \
$KUBELET_NETWORK_PLUGIN \
$KUBELET_VOLUME_PLUGIN \
$KUBELET_CLOUDPROVIDER
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target
kubelet.service
환경변수를 /etd/kubernetes/kubelet.env
를 통해 로드합니다.
해당 파일을 열어보면
KUBE_LOG_LEVEL="--v=2"
KUBELET_ADDRESS="--node-ip=192.168.207.3"
KUBELET_HOSTNAME="--hostname-override=node1"
KUBELET_ARGS="--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
--config=/etc/kubernetes/kubelet-config.yaml \
--kubeconfig=/etc/kubernetes/kubelet.conf \
--container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \
--runtime-cgroups=/system.slice/containerd.service \
"
KUBELET_CLOUDPROVIDER=""
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/etd/kubernetes/kubelet.env
위와 같이 되어있기 때문에, 아래와 같이 수정해서 저장하면 된다.
KUBE_LOG_LEVEL="--v=2"
KUBELET_ADDRESS="--node-ip=192.168.207.3"
KUBELET_HOSTNAME="--hostname-override=node1"
KUBELET_ARGS="--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
--config=/etc/kubernetes/kubelet-config.yaml \
--kubeconfig=/etc/kubernetes/kubelet.conf \
--container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \
--runtime-cgroups=/system.slice/containerd.service \
--protect-kernel-defaults=false \
"
KUBELET_CLOUDPROVIDER=""
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/etd/kubernetes/kubelet.env KUBELET_ARGS 부분의 맨 아래쪽에 --protect-kernel-defaults=false
추가됨
kubelet 이 어떤 커널 파라미터들이 어떻게 설정되있기를 예상하나요?
위 트러블 슈팅을 하면서 궁금했던건, kubelet
은 그럼 어떤 커널 파라미터가 어떻게 설정되있기를 원하느냐가 궁금했습니다.
단순히 kubelet
이 커널 파라미터를 오버라이드 하도록 두는것보다, 애초에 VM 의 커널 설정을 그렇게 하는것이 좋겠다고 생각했습니다.
거기다가 kubelet 이 해당 커널 파라미터를 원하는데에는 kubernetes 클러스터를 안정적으로 운영하기 위함이라는 추측이 있었고, 커널 파라미터가 어떻게 설정되있기를 바라는지 아는것 또한 충분히 의미가 있을 것이라고 생각했습니다.
// setupKernelTunables validates kernel tunable flags are set as expected
// depending upon the specified option, it will either warn, error, or modify the kernel tunable flags
func setupKernelTunables(option KernelTunableBehavior) error {
desiredState := map[string]int{
utilsysctl.VMOvercommitMemory: utilsysctl.VMOvercommitMemoryAlways,
utilsysctl.VMPanicOnOOM: utilsysctl.VMPanicOnOOMInvokeOOMKiller,
utilsysctl.KernelPanic: utilsysctl.KernelPanicRebootTimeout,
utilsysctl.KernelPanicOnOops: utilsysctl.KernelPanicOnOopsAlways,
utilsysctl.RootMaxKeys: utilsysctl.RootMaxKeysSetting,
utilsysctl.RootMaxBytes: utilsysctl.RootMaxBytesSetting,
}
sysctl := utilsysctl.New()
errList := []error{}
for flag, expectedValue := range desiredState {
val, err := sysctl.GetSysctl(flag)
if err != nil {
errList = append(errList, err)
continue
}
if val == expectedValue {
continue
}
switch option {
case KernelTunableError:
errList = append(errList, fmt.Errorf("invalid kernel flag: %v, expected value: %v, actual value: %v", flag, expectedValue, val))
case KernelTunableWarn:
klog.V(2).InfoS("Invalid kernel flag", "flag", flag, "expectedValue", expectedValue, "actualValue", val)
case KernelTunableModify:
klog.V(2).InfoS("Updating kernel flag", "flag", flag, "expectedValue", expectedValue, "actualValue", val)
err = sysctl.SetSysctl(flag, expectedValue)
if err != nil {
if inuserns.RunningInUserNS() {
if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.KubeletInUserNamespace) {
klog.V(2).InfoS("Updating kernel flag failed (running in UserNS, ignoring)", "flag", flag, "err", err)
continue
}
klog.ErrorS(err, "Updating kernel flag failed (Hint: enable KubeletInUserNamespace feature flag to ignore the error)", "flag", flag)
}
errList = append(errList, err)
}
}
}
return utilerrors.NewAggregate(errList)
}
kubelet
의 linux 컨테이너 매니저 코드인 pkg/kubelet/cm/container_manager_linux.go
코드를 확인해보면
VMOvercommitMemory
, VMPanicOnOOM
과 같은 값을 볼 수 있는데 이것은 utilsysctl
쪽에 아래와 같이 정의되어있습니다.
const (
sysctlBase = "/proc/sys"
// VMOvercommitMemory refers to the sysctl variable responsible for defining
// the memory over-commit policy used by kernel.
VMOvercommitMemory = "vm/overcommit_memory"
// VMPanicOnOOM refers to the sysctl variable responsible for defining
// the OOM behavior used by kernel.
VMPanicOnOOM = "vm/panic_on_oom"
// KernelPanic refers to the sysctl variable responsible for defining
// the timeout after a panic for the kernel to reboot.
KernelPanic = "kernel/panic"
// KernelPanicOnOops refers to the sysctl variable responsible for defining
// the kernel behavior when an oops or BUG is encountered.
KernelPanicOnOops = "kernel/panic_on_oops"
// RootMaxKeys refers to the sysctl variable responsible for defining
// the maximum number of keys that the root user (UID 0 in the root user namespace) may own.
RootMaxKeys = "kernel/keys/root_maxkeys"
// RootMaxBytes refers to the sysctl variable responsible for defining
// the maximum number of bytes of data that the root user (UID 0 in the root user namespace)
// can hold in the payloads of the keys owned by root.
RootMaxBytes = "kernel/keys/root_maxbytes"
// VMOvercommitMemoryAlways represents that kernel performs no memory over-commit handling.
VMOvercommitMemoryAlways = 1
// VMPanicOnOOMInvokeOOMKiller represents that kernel calls the oom_killer function when OOM occurs.
VMPanicOnOOMInvokeOOMKiller = 0
// KernelPanicOnOopsAlways represents that kernel panics on kernel oops.
KernelPanicOnOopsAlways = 1
// KernelPanicRebootTimeout is the timeout seconds after a panic for the kernel to reboot.
KernelPanicRebootTimeout = 10
// RootMaxKeysSetting is the maximum number of keys that the root user (UID 0 in the root user namespace) may own.
// Needed since docker creates a new key per container.
RootMaxKeysSetting = 1000000
// RootMaxBytesSetting is the maximum number of bytes of data that the root user (UID 0 in the root user namespace)
// can hold in the payloads of the keys owned by root.
// Allocate 25 bytes per key * number of MaxKeys.
RootMaxBytesSetting = RootMaxKeysSetting * 25
)
위 코드의 내용들을 정리해보면 kubelet
이 검증( --protect-kernel-defaults=false
인자가 들어온다면 수정)하는 커널 파라미터는 아래와 같았습니다.
vm.overcommit_memory = 1
vm.panic_on_oom = 0
kernel.panic = 10
kernel.panic_on_oops = 1
kernel.keys.root_maxkeys = 1000000
kernel.keys.root_maxbytes = 25000000 # root_maxkeys * 25
제가 작업하는 환경에서는 packer
를 통해 쿠버네티스 노드(컨트롤 플레인 노드, 워커노드 모두)의 이미지를 만들기 때문에, 이미지를 빌드할 때에 아래와 같은 ansible
task 를 추가하여서 반영했습니다.
- name: kubelet이 원하는 커널 파라미터 변경
ansible.posix.sysctl:
sysctl_file: /etc/sysctl.d/k8s.conf
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
reload: yes
with_items:
- { name: kernel.keys.root_maxbytes, value: 25000000 }
- { name: kernel.keys.root_maxkeys, value: 1000000 }
- { name: kernel.panic, value: 10 }
- { name: kernel.panic_on_oops, value: 1 }
- { name: vm.overcommit_memory, value: 1 }
- { name: vm.panic_on_oom, value: 0 }
ansible task
실제로 kubespray
또한 비슷한 작업을 하는데요.
위 코드를 통해 볼 수 있습니다.
다만, task 이름이 명확하지 않아서 관련한 이슈/PR 까지도 남겼습니다.
Wrapping up
이렇게 해서 가상머신이 부팅 된 이후에 발생하는 kubelet 에러의 원인이 무엇인지 확인했고, 더 나아가서 kubelet 코드 내부에서 이것들을 어떻게 처리하고있고, 어떤 커널 파라미터들을 expect 하고 있는지 확인 할 수 있었습니다.
--protect-kernel-defaults=false
옵션을 kubelet 실행인자에 주면 위 에러는 쉽게 해결됩니다. 해당 커널 파라미터들을 kubelet 이 덮어씌우기 때문이죠.
위와 같은 방법으로 쉽게 해결되는 문제이지만, 근본적으로 kubelet 이 예상하는(expect)커널 파라미터에 무엇이 있으며, 어떤 값으로 설정되어야하는지. 더 나아가서는 그 커널 파라미터, 값들이 어떤 의미를 갖는지까지 알아보는것이 좋다고 생각합니다.