Files
keycloak-pattern/docs/experiment-a4-node-loss.md
T
DongHyeonkaandClaude Opus 5 d0666c5ba0 docs: A-4 — a dead pod reports healthier than a live one
Kubernetes keeps calling the node Ready for forty seconds while users already see failures, and the pod on the powered-off machine stays ready=true because its kubelet can no longer contradict itself. Eviction waits another five minutes, then the StatefulSet refuses to recreate its pod and the replacement Deployment pod cannot schedule because the local-path volume is pinned to the dead node.

Killing the server node instead shows the opposite shape: containerd keeps the workload running while the API server, Traefik and the observability stack disappear, so the outage is the missing path rather than the missing application. Traefik at one replica is the ingress single point of failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:25:30 +09:00

13 KiB
Raw Blame History

A-4 — 노드가 통째로 죽으면 어떻게 되는가

브랜치 feature/keycloak-a4-node-loss · 증거 docs/evidence/a4-node-loss/ · 2026-09-04 12:0712:24 KST

선행: A-2 · A-3

두 판본으로 나눠 실행했다.

죽인 노드 성격
4a kc-lab-2 (워커) Keycloak + DB 동시 상실
4b kc-lab-1 (k3s 서버) 컨트롤 플레인 + 진입점 상실

virsh destroy 는 종료 신호를 보내지 않는다. 전원을 뽑는 것과 같다.


0. 결론부터

4a 워커 상실 4b 컨트롤 플레인 상실
외부 응답 503 000 (연결 자체가 안 됨)
kubectl 정상 불통
살아 있는 워크로드 keycloak-1 (하지만 DB 없음) keycloak-0 은 계속 돌고 있다
복구 시간 60초 60초

둘 다 전면 장애지만 이유가 다르다. 4a 는 DB 가 같이 죽어서, 4b 는 들어갈 길이 없어서다.

그리고 예상하지 못한 것 셋 — 죽은 파드가 산 파드보다 건강해 보이고, StatefulSet 은 대체 파드를 만들지 않으며, PVC 때문에 재배치가 불가능하다.


4a. 워커 노드 상실 (kc-lab-2)

기준선

kc-lab-1   Ready   control-plane=true
kc-lab-2   Ready   <none>

keycloak-0   ready=true   kc-lab-2
keycloak-1   ready=true   kc-lab-1
postgres     ready=true   kc-lab-2

PVC postgres-data → kc-lab-2        ← 재배치 가능성을 여기서 이미 알 수 있다
외부 HTTP 200

주입

virsh destroy kc-lab-2

관찰

  +15초  node=Ready     | keycloak-0=Running postgres=Running | 외부 HTTP 000
  +30초  node=Ready     | keycloak-0=Running postgres=Running | 외부 HTTP 000
  +45초  node=NotReady  | keycloak-0=Running postgres=Running | 외부 HTTP 503
  ...
  +180초 node=NotReady  | keycloak-0=Running postgres=Running | 외부 HTTP 503

발견 ① — 쿠버네티스가 알아채는 데 40초 걸린다

+15초, +30초 에도 노드는 여전히 Ready 다. 기계는 이미 없는데.

kube-controller-manager 의 node-monitor-grace-period (기본 40초) 동안 kubelet 의 하트비트가 없어야 NotReady 로 바꾼다.

그 40초 동안 쿠버네티스는 거짓말을 한다. 사용자는 이미 장애를 겪고 있다 (000). 노드 상태를 알림 근거로 삼으면 항상 늦는다.

발견 ② — 죽은 파드가 산 파드보다 건강해 보인다

POD          PHASE     READY   NODE
keycloak-0   Running   true    kc-lab-2      ← 기계가 꺼져 있다
keycloak-1   Running   false   kc-lab-1      ← 살아 있다

keycloak-0ready=true, keycloak-1ready=false.

keycloak-0 kubelet 이 없어 상태를 갱신할 수 없다. 마지막으로 보고한 값이 그대로 얼어 있다
keycloak-1 살아서 정직하게 보고한다 — DB 가 없으니 readiness 실패

파드 상태는 "지금 어떤가"가 아니라 "마지막으로 그렇게 들었다"이다. 노드가 죽으면 그 노드 파드의 상태는 화석이 된다.

Prometheus 는 정확했다.

  up{job=keycloak      pod=keycloak-1} = 1
  up{job=keycloak      pod=keycloak-0} = 0     ← 긁기에 실패했다 = 사실
  up{job=kubelet                     } = 0
  up{job=node-exporter node=kc-lab-2 } = 0

A-2 와 정반대다. A-2 에서는 up=1 인데 서비스가 죽었고, 여기서는 up=0 이 정확했다.

up 이 잡는가
대상이 사라짐 (노드 상실) 잡는다
대상이 살아서 못 씀 (DB 상실) 못 잡는다

up 이 노드별로 0 으로 떨어졌다

발견 ③ — 축출은 5분 뒤에 시작된다

쿠버네티스가 노드에 taint 를 붙인다.

  node.kubernetes.io/unreachable=:NoSchedule
  node.kubernetes.io/unreachable=:NoExecute

그런데 모든 파드에는 기본 관용이 붙어 있다.

  node.kubernetes.io/not-ready    NoExecute  tolerationSeconds=300
  node.kubernetes.io/unreachable  NoExecute  tolerationSeconds=300
  +240초  전부 Running
  +270초  a2-probe:Terminating keycloak-0:Terminating postgres:Terminating
          postgres-...-9cmsv:Pending          ← 새 파드가 생겼다

노드가 죽고 약 5분이 지나야 파드가 축출된다. 잠깐 끊긴 노드가 돌아올 수도 있으니 성급하게 옮기지 않겠다는 설계다. 대신 그동안은 아무 일도 일어나지 않는다.

발견 ④ — 축출돼도 갈 곳이 없다

Warning  FailedScheduling  default-scheduler
  0/2 nodes are available:
    1 node(s) didn't match PersistentVolume's node affinity,
    1 node(s) had untolerated taint(s).
   kc-lab-2  →  taint 때문에 못 간다 (죽은 노드)
   kc-lab-1  →  PVC 가 kc-lab-2 에 묶여 있어 못 간다

local-path PVC 는 그 노드의 로컬 디스크 디렉터리다. 노드가 죽으면 볼륨도 죽는다. 새 파드는 영원히 Pending 이다.

결함이 아니라 조건이다. 이 실험대는 그걸 알고 local-path 를 골랐다. 운영이라면 네트워크 스토리지나 DB 복제가 이 자리를 메워야 한다.

발견 ⑤ — StatefulSet 은 대체 파드를 만들지 않는다

NAME       DESIRED   READY     CURRENT
keycloak   2         <none>    1

keycloak-0   1/1   Terminating   30m       ← 30분째
keycloak-1   0/1   Running

keycloak-0Terminating 에서 영원히 멈춰 있고, 대체 파드가 안 생긴다.

StatefulSet 의 계약 같은 이름의 파드는 클러스터에 하나뿐이어야 한다
컨트롤 플레인이 아는 것 노드가 안 보인다 = 파드가 죽었는지 확신할 수 없다
그래서 옛 파드를 확실히 지우기 전엔 새 keycloak-0 을 못 만든다

Deployment 였다면 즉시 새 파드를 만든다. 이름이 아무래도 되기 때문이다. StatefulSet 의 "안정된 이름"이라는 이득의 반대편 비용이 여기다.

강제로 진행시키려면 사람이 개입해야 한다.

kubectl -n keycloak-lab delete pod keycloak-0 --grace-period=0 --force

위험한 명령이다. 노드가 사실은 살아 있고 네트워크만 끊긴 것이라면 같은 이름의 파드 둘이 동시에 존재하게 된다 (split brain).

복구

virsh start kc-lab-2
  +30초  node=Ready | Running 파드 3 개 | 외부 HTTP 503
  +60초  node=Ready | Running 파드 3 개 | 외부 HTTP 200
  → 서비스 복귀

60초. 사람 개입 없이 전부 제자리로 돌아왔다.


4b. 컨트롤 플레인 노드 상실 (kc-lab-1)

이 노드에 무엇이 있었나 — 그게 곧 영향 범위다

keycloak-lab    keycloak-1
kube-system     coredns
kube-system     local-path-provisioner
kube-system     metrics-server
kube-system     svclb-traefik
kube-system     traefik                    ← replicas=1
observability   grafana
observability   prometheus
observability   node-exporter
NAME      REPLICAS   READY
traefik   1          1        ← 진입점이 한 개다

주입과 관찰

virsh destroy kc-lab-1
  +20초  외부 auth=000 grafana=000 | kubectl: Unable to connect to the server
  +60초  외부 auth=000 grafana=000 | kubectl: Unable to connect to the server
  +120초 외부 auth=000 grafana=502 | kubectl: Unable to connect to the server
  +160초 외부 auth=000 grafana=000 | kubectl: Unable to connect to the server

발견 ⑥ — 워크로드는 살아 있는데 길이 없다

살아남은 노드에서 직접 확인했다.

ssh kc-lab-2 'sudo crictl ps --name keycloak'
CONTAINER      STATE     NAME       POD          NAMESPACE
e5f777900b76   Running   keycloak   keycloak-0   keycloak-lab

keycloak-0 은 멀쩡히 돌고 있다. 컨테이너 런타임(containerd)은 API 서버 없이도 이미 떠 있는 파드를 계속 굴린다.

   죽은 것:  API 서버 · 스케줄러 · coredns · Traefik · Prometheus · Grafana
   산 것:    keycloak-0 · postgres · containerd
   문제:     들어갈 문(Traefik)이 없다

컨트롤 플레인 상실은 워크로드 상실이 아니다. 이미 떠 있는 것은 계속 돈다. 새로 뜨거나 옮기거나 고치는 것이 안 될 뿐.

발견 ⑦ — 000503 의 차이

코드
503 (4a) nginx·Traefik 은 살아 있고 뒤에 보낼 파드가 없다
000 (4b) curl 이 응답 자체를 못 받았다 — Traefik 이 없어 nginx 가 죽은 주소를 기다리다 타임아웃

grafana=502 가 한 번 찍힌 것이 증거다 — nginx 는 살아서 502 를 만들 수 있었지만, 대부분은 타임아웃(--max-time 8)에 걸렸다.

두 코드가 다른 층의 고장을 가리킨다.

발견 ⑧ — 관측 시스템이 같이 죽으면 0 이 아니라 구멍이 남는다

Prometheus 가 kc-lab-1 에 있었다. 그 노드가 죽은 12:18–12:23 구간은 그래프에 0 이 아니라 데이터 없음으로 남는다.

   대상이 죽음        →  up = 0        →  "언제 죽었는지" 알 수 있다
   관측자가 죽음      →  데이터 없음    →  "그때 무슨 일이 있었는지" 모른다

계획 단계에서 "관측 스택은 죽이지 않는 노드에 둔다"고 정했던 이유를 실제로 확인한 셈이다. 다만 노드가 둘뿐이라 완전히 피할 수는 없다.

복구

virsh start kc-lab-1
  +30초  외부=502 | kc-lab-1=Ready kc-lab-2=Ready
  +60초  외부=200 | kc-lab-1=Ready kc-lab-2=Ready
  → 서비스 복귀 (총 60초)

Grafana 는 로그인 세션을 잃었다 — 데이터가 emptyDir 이라 파드 재시작에 사라진다. Prometheus 는 PVC 라 지표가 남았다. 의도한 설계대로 동작했다.


5. 개념

node-monitor-grace-periodtolerationSeconds

   기계 정지
      │
      │  40초   node-monitor-grace-period      → 노드 NotReady
      │
      │  +300초 tolerationSeconds (NoExecute)  → 파드 축출 시작
      │
      ▼
   총 약 5분 40초 동안 쿠버네티스는 아무것도 하지 않는다

빠른 장애 대응이 필요하면 이 값을 줄여야 하지만, 짧게 하면 일시적 네트워크 끊김에도 파드를 옮기게 되어 불안정해진다. 맞바꿈이다.

Terminating 이 안 끝나는 이유

   파드 삭제 요청
     └─ kubelet 이 컨테이너를 멈추고 "지웠다"고 보고해야 끝난다
          └─ kubelet 이 없다 → 보고가 없다 → 영원히 Terminating

--grace-period=0 --force보고를 기다리지 않고 API 에서 지우는 것이다. 컨테이너가 실제로 죽었는지는 모른 채 진행한다.

StatefulSet vs Deployment — 노드 상실에서

Deployment StatefulSet
대체 파드 즉시 만든다 (이름 무관) 안 만든다 (이름이 유일해야)
노드 상실 시 자동 회복 사람이 강제 삭제해야
위험 없음 강제 삭제 시 중복 실행 가능

A-0 에서 "로그 대조가 쉬워서" StatefulSet 을 골랐는데, 그 대가가 여기 있다.

local-path PVC 와 재배치

   PVC → PV → nodeAffinity: kubernetes.io/hostname = kc-lab-2
                              └─ 그 노드의 /var/lib/rancher/k3s/storage/... 디렉터리

볼륨이 노드에 못박히면 파드도 못박힌다. 노드가 죽으면 데이터도 함께 접근 불가가 된다.


6. 재현 절차 (명령어)

export LIBVIRT_DEFAULT_URI=qemu:///system

# 사전 — PVC 가 어디 묶여 있는지 먼저 본다 (재배치 가능성이 여기서 정해진다)
kubectl -n keycloak-lab get pvc -o name | while read p; do
  V=$(kubectl -n keycloak-lab get $p -o jsonpath='{.spec.volumeName}')
  kubectl get pv $V -o jsonpath='{.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]}'
done

# 4a 워커 상실
virsh destroy kc-lab-2
kubectl get node kc-lab-2                        # 40초 뒤 NotReady
kubectl -n keycloak-lab get pods -o wide         # Running 인 채로 얼어 있다
kubectl get node kc-lab-2 -o jsonpath='{.spec.taints}'
#   5분 뒤 Terminating + 새 파드 Pending
kubectl -n keycloak-lab describe pod <new-pod> | grep -A4 Events

# 4b 컨트롤 플레인 상실 — kubectl 이 죽으므로 노드에서 직접 본다
virsh destroy kc-lab-1
ssh kc-lab-2 'sudo crictl ps'                    # 워크로드는 살아 있다

# 복구
virsh start kc-lab-2 && virsh start kc-lab-1

7. 다음 실험에 남기는 것

실험 이 실험이 준 것
A-5 비대칭 파티션 여기서는 노드가 완전히 사라졌다. 부분 단절은 더 고약하다
D-1 백업·복구 PVC 가 노드에 묶여 있다 — 노드가 영영 안 돌아오면 백업이 유일한 길
구성 개선 Traefik replicas=1 은 진입점 단일 장애점 — 2로 늘리거나 DaemonSet 으로
구성 개선 Grafana 가 emptyDir — 재시작마다 로그인 세션이 사라진다