고가용성 시험이라며 노드를 하나 내리면 이런 상태가 된다. 노드는 NotReady 인데 그 노드의 파드는 여전히 Running 1/1 로 보이고 다른 노드로 넘어가지 않는다. 정상이다.
Kubernetes 는 파드를 옮기지 않는다. 기존 파드를 지우고 컨트롤러가 새 파드를 만든다. 그리고 그 삭제가 곧바로 일어나지 않는다.
노드와 통신이 끊기면 API 서버는 kubelet 으로부터 갱신을 받지 못하므로 마지막으로 보고된 상태가 그대로 남는다. 그래서 실제로는 컨테이너가 죽었어도 한동안 Running 으로 보인다.
kubelet 하트비트 중단
↓ (node-monitor-grace-period, 수십 초)
노드 Ready=Unknown, NotReady 이벤트
↓ 노드에 taint 부착
node.kubernetes.io/unreachable:NoExecute
node.kubernetes.io/not-ready:NoExecute
↓ (파드의 tolerationSeconds, 기본 300초)
파드 삭제(eviction)
↓
컨트롤러가 다른 노드에 새 파드 생성 → Ready → 트래픽 수용
파드가 언제 내려가는지는 그 파드가 NoExecute taint 를 얼마나 견디도록 설정됐는지가 정한다. 어드미션 컨트롤러가 모든 파드에 기본 300초짜리 toleration 을 붙이므로, 별도 설정이 없으면 약 5분 뒤에 움직인다. 예전 컨트롤러 매니저 플래그(--pod-eviction-timeout)로 조절하던 방식은 최신 버전에서 없어졌으므로, 지금은 파드 쪽 toleration 을 본다.
kubectl describe node <노드> | grep -A5 Taints
kubectl get pod -n <네임스페이스> <파드> -o jsonpath='{.spec.tolerations}' | tr ',' '\n'
tolerationSeconds 가 길게 잡힌 파드는 오래 버틴다. 제품이 기본값으로 큰 값을 넣어 두는 경우가 있으니 안 넘어간다고 느끼면 여기를 먼저 본다.
바로 넘기지 않는 이유는 둘이다.
일시적인 끊김이 흔하다. 네트워크 순단, kubelet 의 일시 정지, 디스크 지연으로 하트비트가 수십 초 끊기는 일은 자주 있다. 이때마다 재배치하면 클러스터가 요동친다.
중복 실행이 더 위험하다. 통신만 끊겼고 노드는 살아 있는 경우, 새 노드에 같은 파드를 띄우면 같은 서비스가 두 벌 돈다. ReadWriteOnce 볼륨을 양쪽이 잡거나, 리더가 둘이 되는 상태는 데이터 손상으로 이어진다. 유예 시간은 그 확률을 줄이는 장치다.
그래서 노드 하드 다운 방식의 장애 시험에서는 중단 시간이 0 이 될 수 없다. 감지에 수십 초, eviction 대기에 수 분, 새 파드 기동과 준비에 다시 시간이 든다. 무중단에 가깝게 하려면 레플리카를 여러 노드에 미리 분산해 두고(topologySpreadConstraints), readiness 를 정확히 잡고, PodDisruptionBudget 으로 동시 중단 수를 제한한다.
| 종류 | 동작 |
|---|---|
| Deployment · ReplicaSet · StatefulSet | eviction 뒤 다른 노드에 새 파드 생성 |
| DaemonSet | 노드마다 하나이므로 옮겨가지 않는다 |
| 정적 파드 | kubelet 이 관리한다. 그 노드가 살아나야 돌아온다 |
| 컨트롤러 없이 만든 파드 | 다시 만들어지지 않는다 |
StatefulSet 은 같은 이름의 파드가 둘이 되면 안 되므로 더 보수적이다. 노드가 unreachable 인 동안에는 새 파드를 만들지 않고, 노드가 삭제되거나 파드가 확실히 지워져야 진행한다. 강제로 넘기려면 노드를 클러스터에서 지운다. 이 조작은 원래 노드에서 프로세스가 아직 돌고 있을 때 중복 실행을 부르므로, 노드가 확실히 죽었을 때만 한다.
RWO 볼륨을 쓰는 파드는 볼륨이 이전 노드에서 분리된 뒤에야 새 노드에 붙는다. 스토리지에 따라 이 과정이 몇 분 걸린다. RWX 볼륨은 이 대기가 없다.
| eviction | OOMKilled | |
|---|---|---|
| 주체 | kubelet 이 파드를 삭제한다 | 커널이 프로세스를 죽인다 |
| 단위 | 파드 전체 | 한 컨테이너 |
| 계기 | 노드의 메모리·디스크·PID 압박 | 컨테이너가 메모리 상한 초과 |
eviction 은 항상 파드 단위다. 컨테이너 하나만 골라 내보내지 않는다. 순서는 QoS 등급을 따른다.
| 등급 | 조건 | 순서 |
|---|---|---|
| BestEffort | 모든 컨테이너에 requests · limits 없음 | 가장 먼저 |
| Burstable | 일부만 설정 | 다음 |
| Guaranteed | 모든 컨테이너가 requests = limits (CPU·메모리 모두) | 가장 마지막 |
QoS 는 파드 전체 기준이다. 컨테이너 하나에만 requests 를 넣었다면 그 파드는 Guaranteed 가 아니라 Burstable 이다. 그리고 Guaranteed 는 면제가 아니라 마지막 차례일 뿐이다. 압박이 계속되면 결국 내보내지고, drain 이나 노드 종료에는 등급이 무관하다.
컨테이너가 자기 limit 을 넘으면 그 컨테이너만 OOMKilled 되고 노드는 멀쩡하다. 하지만 노드 위 모든 프로세스의 합이 물리 메모리를 넘으면 커널 OOM Killer 가 노드 전체에서 대상을 고른다. 이때 kubelet 이나 containerd 가 희생되면 노드가 통째로 NotReady 가 된다.
막는 방법은 정해져 있다. 파드마다 requests 를 정해 스케줄러가 과배치하지 않게 하고, kubelet 에 --system-reserved 와 --kube-reserved 로 OS·시스템 몫을 떼어 둔다. requests 없이 도는 파드가 많은 클러스터가 이 사고를 가장 자주 낸다.