TT Lab
블로그 시작하기
배우기 러닝패스 코스

쿠버네티스 전문가 — 장애를 진단하고 클러스터를 지킨다

NotReady 는 진단이 아니라 증상이다

TT Lab 에서 이어서 보기

한 줄 요약

NotReady 는 「컨트롤 플레인이 그 노드의 kubelet 에게서 최근 소식을 못 들었다」 거나 「kubelet 이 스스로 준비되지 않았다고 보고했다」 는 뜻이고, 원인은 언제나 그 노드 안에 있습니다. 진단은 노드 객체가 아니라 그 노드의 kubelet·런타임·커널에서 시작합니다.

왜 이게 필요했나

노드 하나가 NotReady 가 되면 화면에는 그 한 단어만 남습니다. 그런데 그 뒤에서 일어나는 일은 원인마다 전혀 다릅니다. kubelet 프로세스가 아예 뜨지 못하는 경우(스왑, 설정 오류), kubelet 은 떠 있는데 API 서버가 받아 주지 않는 경우(인증서·인가), kubelet 은 보고하는데 「네트워크가 준비되지 않았다」 고 스스로 말하는 경우(CNI), 런타임 데몬은 살아 있는데 그 안의 CRI 플러그인만 죽은 경우(커널 자원). 같은 증상에 처방이 다섯 가지이고, 틀린 처방(재부팅, 노드 삭제 후 재조인)은 원인을 지운 채 다음 고장을 기다리게 만듭니다.

어떻게 동작하나

노드 상태는 두 경로로 갱신됩니다. kubelet 은 10초마다 노드 리스(kube-node-lease 네임스페이스의 Lease)를 갱신하고, 상태가 바뀌거나 일정 시간이 지나면 노드의 status.conditions 를 씁니다. 컨트롤 플레인의 노드 수명 주기 컨트롤러는 리스 갱신이 끊긴 지 grace period(기본 40초)가 지나면 조건을 Unknown(사유 NodeStatusUnknown)으로 바꾸고 node.kubernetes.io/unreachable 테인트를 NoSchedule·NoExecute 로 붙입니다. 이 실습 클러스터에서 kubelet 을 멈춘 뒤 Ready 가 바뀌기까지 47초가 걸렸습니다(실측). 다시 띄우면 1초 만에 True 로 돌아왔습니다. 반대로 kubelet 이 살아서 「준비 안 됨」 을 보고하면(CNI 사건) 조건은 False 가 되고 메시지에 이유가 그대로 적힙니다 — 실측에서 10초 만이었습니다. Unknown 이면 kubelet 쪽을, False 면 메시지를 먼저 봅니다.

NoExecute 테인트에는 시계가 붙어 있습니다. 파드는 기본으로 unreachable·not-ready 테인트를 300초 동안 견디도록(tolerationSeconds) 만들어집니다. 그러니 노드가 5분 넘게 돌아오지 않으면 그 노드의 파드는 축출되어 다른 곳에서 다시 만들어집니다. 그 5분 동안 컨테이너는 대개 계속 돌고 있습니다 — 런타임 데몬이 죽어도 컨테이너를 붙잡고 있는 shim 프로세스는 남기 때문입니다(실측: containerd 가 설정 오류로 못 뜨는 동안에도 containerd-shim 이 남아 있었습니다).

고장 다섯이 남기는 흔적(모두 이 클러스터에서 잰 것).

원인 kubelet 노드 조건 결정적 로그
스왑 켜짐 기동 실패 반복 Unknown(45초) running with swap on is not supported
다른 노드의 클라이언트 인증서 active Unknown(53초) User "system:node:node02" cannot get resource "nodes"
CNI 설정 파일 없음 active False(10초) NetworkPluginNotReady ... cni plugin not initialized
inotify 인스턴스 상한 16 기동 실패 반복 Unknown(49초) containerd: failed to create cni conf monitor ... too many open files
containerd 설정에 같은 TOML 표 두 번 기동 실패 반복 Unknown failed to unmarshal TOML: toml: table options already exists

넷째 줄이 가장 고약합니다. containerd 는 active 인데 CRI 플러그인만 올라오지 못했고, kubelet 의 오류는 rpc error: code = Unimplemented desc = unknown service runtime.v1.RuntimeService 라 마치 containerd 판이 맞지 않는 것처럼 보입니다. 원인은 containerd 의 로그에서 failed to load plugin 줄을 찾아야 보입니다. 그리고 낮춘 값을 적은 sysctl 파일을 지우고 sysctl --system 을 해도 지금 커널 값은 16 그대로 였습니다(실측) — 그 값을 되돌려 줄 파일이 없기 때문입니다. 지금 값은 sysctl -w 로, 부팅 값은 /etc/sysctl.d/ 로 따로 맞춰야 합니다.

현장에서 만나는 모습

containerd 드롭인 사고. 이 사이트를 돌리는 홈랩 클러스터에서 런타임 설정 한 줄을 넣으려고 >> 로 덧붙이는 명령이 두 번 돌았고, 같은 TOML 표가 두 번 들어가 노드가 NotReady 가 됐습니다. 그 뒤로 설정은 후보 파일을 containerd --config <후보> config dump 로 먼저 읽혀 본 뒤에만 넣습니다. inotify 상한이 말라 노드가 조용히 NotReady 가 된 일도 같은 클러스터에서 있었고, 증상이 설정 오류와 구별되지 않았습니다.

인증서는 이름이 아니라 내용으로 고릅니다. kubelet 의 클라이언트 인증서는 /var/lib/kubelet/pki/ 에 날짜가 붙은 파일로 쌓이고 kubelet-client-current.pem 이 그중 하나를 가리킵니다. 공식 문서에 따르면 kubelet 은 수명이 30%에서 10% 남았을 때 새 인증서를 요청합니다. 날짜가 가장 늦은 파일이 맞는 파일이라는 보장은 없습니다 — openssl verify 로 클러스터 CA 가 서명했는지, -subject 로 이 노드의 이름인지 확인합니다. 실측에서 다른 노드의 인증서를 물린 kubelet 은 이 노드 이름으로 새 인증서를 요청하려 했지만, 노드 인가가 「자기 이름으로만 요청할 수 있다」 며 거절했습니다. 스스로 낫지 않습니다.

다음 실습에서 할 것

kubelet 을 멈춰 노드 상태가 바뀌는 시간을 직접 재고, 사건 다섯을 하나씩 내서 원인 코드와 증거 한 줄을 남기며 고칩니다. 마지막에 node01 을 재부팅해 다섯 가지 고침이 모두 설정 파일에 남았는지 확인합니다.