--- id: d845adc8-be2c-4471-aa1d-7e4db864c471 kind: SETUP slug: reproduce-a4-node-loss title: 기계 전원을 뽑고 쿠버네티스가 알아채는 시각을 잰다 topic: losing-a-node-or-the-store topicName: PostgreSQL 을 내리고 노드 전원을 뽑았을 때 project: keycloak-session-store status: 게시 전 studio: "https://hyeonworks.com/studio/documents/d845adc8-be2c-4471-aa1d-7e4db864c471/edit" pinnedVersions: - name: Keycloak version: 26.7.0 source: - final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-4 sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af --- # 기계 전원을 뽑고 쿠버네티스가 알아채는 시각을 잰다 워커 노드와 k3s 서버를 차례로 `virsh destroy` 로 끄고 노드 이탈 인지와 축출 시작과 재배치 실패 시각을 재는 절차다. 주입이 하이퍼바이저 명령이라 되돌리기도 물리적이고 터미널을 세 개 쓴다. 전 구간 약 40분이고 실험대에서만 친다. ## 관계 - **노드를 잃는 두 가지 — 저장소가 같이 죽는 것과 들어갈 길이 없는 것** 이 절차가 재현하는 사건이고, 4a 와 4b 가 왜 다른 고장인지는 그 기록이 결론으로 갖는다. - **장애 시간의 대부분은 알아채는 데 걸린다** 40초와 5분 40초를 차단 시각에서 빼서 재는 순서가 그 기준이 선 근거다. - **up 지표는 살아 있지만 쓸모없는 상태를 보지 못한다** 노드가 통째로 없어지는 이 절차에서는 `up` 이 정확히 0 을 낸다. DB 만 잃었을 때와 왜 다른지를 그 기록이 설명한다. - **readiness 가 깨진 노드를 시야에서 먼저 치운다** 꺼진 기계의 파드가 `ready=true` 로 얼어 있고 살아 있는 파드가 `ready=false` 인 상태를 이 절차가 만든다. ## 본문 ## 읽기 전에 — 어디서 치는가 명령을 치는 곳이 세 군데이고, 그 구별이 이 절차의 내용이다. 코드블록마다 어느 터미널인지 붙여 두었다. | 터미널 | 어디서 치나 | 무엇을 치나 | |---|---|---| | A | `test-server` (VM 호스트) | `virsh` — 전원을 뽑고 다시 넣는다 | | B | `test-server` | `kubectl` — 관찰. 4b 에서는 이 터미널이 살아 있고 `kubectl` 만 막힌다 | | C | `test-server` | 밖에서 `curl`. 사용자 시점 | 4b 에서는 `kc-lab-2` 에도 붙는다. 터미널 A 와 같은 기계에서 `ssh kc-lab-2` 로 붙고, 이름이 안 풀리면 `ssh 192.168.122.12` 다. | 무엇 | 값 | |---|---| | 네임스페이스 | `keycloak-lab` · 관측 스택은 `observability` | | 대상 | 게스트 VM 둘 — `kc-lab-2`(워커) 와 `kc-lab-1`(k3s 서버) | | 주입 | `virsh destroy`. `virsh shutdown` 을 쓰면 이 절차가 아니다 | | 외부 확인 | 모든 `curl` 에 `--max-time 8` | | 걸리는 시간 | 전 구간 약 40분. 4a 의 축출을 보는 데만 7분 | | 되돌리기 | `virsh start`. `--grace-period=0 --force` 로 파드를 지우지 않는다 | **공개 이름은 랩 안에서 안 풀린다.** `auth.hyeonworks.com` 같은 공개 이름이 랩 호스트에서도 게스트에서도 호스트 자신의 tailnet 주소 `100.83.212.4` 로 풀리는데 그 주소에는 443 을 듣는 것이 없다. 이름만 치면 `curl` 이 `000` 을 낸다(2026-09-17, 랩 호스트와 `kc-lab-1` 양쪽에서 쳐서 확인했다, observed). 그래서 랩 안에서 치는 `curl` 에는 `--resolve <이름>:443:192.168.122.10` 을 붙여 엣지 게스트를 짚었고, 그 형태로는 정문이 `200` 이다(observed). tailnet 에 붙은 다른 기계에서 치면 이름 그대로 닿으므로 `--resolve` 가 필요 없다. ## 이 실험이 가르는 것 A-1 과 A-5 는 네트워크만 끊는다. 파드는 살아 있고 쿠버네티스는 계속 정확한 상태를 안다. 여기서는 기계 자체를 없애므로 상태를 보고할 주체가 사라진다. A층은 예측을 먼저 적어 두고 주입했다. A-0 의 예측표는 이 실험을 옛 로드맵 번호인 `A-3 노드 상실 (kc-lab-2)` 로 적고 「세션은 살아남는다. 죽은 노드의 캐시만 사라진다」를 룩어사이드 캐시에서 끌어냈다. 그 한 줄에 postgres 는 나오지 않는다. 판본은 둘이고, 어느 노드를 끄느냐가 결과를 전부 바꾼다. | 판본 | 끄는 노드 | 그 노드에 있는 것 | 무엇을 묻나 | |---|---|---|---| | 4a | `kc-lab-2` (워커) | keycloak-0 · postgres · postgres PVC | Keycloak 과 DB 를 동시에 잃으면 | | 4b | `kc-lab-1` (k3s 서버) | keycloak-1 · Traefik · 컨트롤 플레인 · 관측 스택 | 들어갈 문을 잃으면 | 끝나면 셋을 말할 수 있다. ```text 쿠버네티스는 언제 알아채는가 → 40초 (그동안 거짓말을 한다) 무엇을 스스로 고치는가 → 축출. 단 5분 뒤 무엇을 못 고치는가 → PVC 가 묶인 재배치, StatefulSet 이름 ``` ## 전제와 되돌리기 - `virsh` 가 시스템 하이퍼바이저를 보고 있어야 한다. `qemu:///system` 이 아니면 VM 이 안 보인다. - 4b 에서는 `kc-lab-2` 에도 붙는다. 터미널 A 와 같은 기계에서 `ssh kc-lab-2` 로 붙고, 이름이 안 풀리면 `ssh 192.168.122.12` 다. - 4a 의 확인표를 통과하기 전에 4b 로 넘어가지 않는다. 두 고장이 겹치면 무엇이 원인인지 못 가린다. ### 1. virsh 가 시스템 하이퍼바이저를 보게 맞춘다 **목적** — 터미널 A 의 `virsh` 가 `qemu:///system` 을 보게 한다. ```bash label="[터미널 A] ① 연결 URI 를 이 셸에 건다" export LIBVIRT_DEFAULT_URI=qemu:///system ``` ```bash label="[터미널 A] ② 무엇을 보고 있는지 확인한다" virsh uri ``` **예상 결과** — `qemu:///system` 한 줄. **왜 필요한가** — `qemu:///session` 과 `qemu:///system` 은 서로 다른 libvirt 연결이다. VM 은 system 쪽에 있으므로 session 을 보고 있으면 목록 자체가 비어 나오고, 그것을 「VM 이 죽었다」로 읽게 된다. **문제가 생기면** — 다음 단계에서 `virsh list --all` 이 빈 목록을 내면 여기부터 다시 본다. :::warning `virsh destroy` 는 종료 신호를 보내지 않는다. 전원 코드를 뽑는 것과 같아서 게스트 파일시스템이 더러운 채로 멈춘다. 실험대에서만 한다. ::: 어느 단계에서든 그만두려면 터미널 A 에서 한 줄이면 된다. ```bash label="[터미널 A] 중단 — 두 VM 을 다시 켠다" virsh start kc-lab-2 ; virsh start kc-lab-1 ``` ## 주입 전에 같은 명령으로 먼저 본다 주입 뒤에 치는 명령과 같은 명령을 먼저 친다. 순서는 이렇다. ```text VM → 노드 → 파드 배치 → 볼륨이 어디 묶여 있나 → 외부 응답 → 관측자가 어디 있나 ``` ### 1. VM 전원과 노드와 파드 배치 **무엇을 확인하는가** — 두 게스트가 켜져 있고 어느 파드가 어느 노드에 있는지. ```bash label="[터미널 A] ① VM 전원" virsh list --all ``` ```bash label="[터미널 B] ② 노드와 파드 배치" kubectl get nodes ``` ```bash label="[터미널 B] ③ 파드가 어느 노드에 있나" kubectl -n keycloak-lab get pods -o wide ``` **출력에서 답이 되는 것** — `virsh list --all` 의 상태 열과 `get pods -o wide` 의 `NODE` 열이다. 실측은 이렇다. ```text -------------------------- 1 kc-lab-1 running 2 kc-lab-2 running ``` ```text kc-lab-1 Ready true kc-lab-2 Ready a2-probe true kc-lab-2 keycloak-0 true kc-lab-2 keycloak-1 true kc-lab-1 postgres-7b474b88c8-2gf27 true kc-lab-2 ``` **이 결과가 뜻하는 것** — `kc-lab-2` 에 keycloak-0 과 postgres 가 함께 있으므로 4a 는 Keycloak 한 대를 잃는 절차가 아니라 Keycloak 한 대와 DB 를 동시에 잃는 절차다. `virsh list --all` 앞의 숫자는 도메인 ID 이고 VM 을 껐다 켜면 바뀌므로 이름으로 다룬다. `a2-probe` 는 A-2 에서 띄우고 안 지운 상주 파드라 없어도 지장이 없다. ### 2. 볼륨이 어느 노드에 못박혀 있나 **무엇을 확인하는가** — postgres PVC(PersistentVolumeClaim, 영구 볼륨 요청) 가 요구하는 노드. 이 값이 뒤의 재배치 결과를 미리 정한다. ```bash label="[터미널 B] ① PVC 목록" kubectl -n keycloak-lab get pvc ``` ```bash label="[터미널 B] ② PV 이름" kubectl -n keycloak-lab get pvc postgres-data -o jsonpath='{.spec.volumeName}' ; echo ``` ```bash label="[터미널 B] ③ 그 PV 가 요구하는 노드 — 읽는 형태" kubectl describe pv $(kubectl -n keycloak-lab get pvc postgres-data \ -o jsonpath='{.spec.volumeName}') | grep -A6 'Node Affinity' ``` **출력에서 답이 되는 것** — `Required Terms` 아래 호스트 이름이다. 값은 환경마다 다르다. ```text Node Affinity: Required Terms: Term 0: kubernetes.io/hostname in [kc-lab-2] ``` 값만 여러 번 비교할 때는 뽑는 형태로 줄인다. ```bash label="[터미널 B] ④ 같은 값을 한 줄로 — 뽑는 형태" kubectl get pv $(kubectl -n keycloak-lab get pvc postgres-data \ -o jsonpath='{.spec.volumeName}') \ -o jsonpath='{.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]}' ; echo ``` **이 결과가 뜻하는 것** — 이 실험대에서 `postgres-data` 는 `kc-lab-2` 를 요구했다. `local-path` PVC 는 그 노드의 로컬 디렉터리(`/var/lib/rancher/k3s/storage/...`)이므로 노드가 죽으면 볼륨도 같이 죽고, 스케줄러는 `nodeAffinity` 로 그것을 알고 있어 다른 노드에 파드를 만들지 않는다. 결함이 아니라 이 실험대의 조건이다. ### 3. 밖에서 보이는 상태와 관측자의 위치 **무엇을 확인하는가** — 정문이 지금 무엇을 답하는지, 그리고 Prometheus 와 Grafana 가 어느 노드에 있는지. ```bash label="[터미널 C] ① 응답을 통째로 읽는 형태" curl -I --max-time 8 --resolve auth.hyeonworks.com:443:192.168.122.10 \ https://auth.hyeonworks.com/realms/master ``` ```bash label="[터미널 C] ② 여러 번 비교할 것이므로 코드만 뽑는 형태" curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 --resolve auth.hyeonworks.com:443:192.168.122.10 \ https://auth.hyeonworks.com/realms/master ``` ```bash label="[터미널 B] ③ 관측 스택이 어느 노드에 있나" kubectl -n observability get pods -o wide ``` **출력에서 답이 되는 것** — ② 는 `200`, ③ 은 `grafana` 와 `prometheus` 의 `NODE` 열이다. 이 실험대에서는 둘 다 `kc-lab-1` 에 있었다. **이 결과가 뜻하는 것** — 4a 는 `kc-lab-2` 를 끄므로 Prometheus 가 살아남아 관측이 정확하고, 4b 는 관측자가 같이 죽는다. 미리 알아 두지 않으면 나중에 그래프의 빈 구간을 값 0 으로 읽는다. `--max-time` 은 모든 외부 확인에 준다. 4b 에서 그 값이 없으면 `curl` 이 몇 분씩 매달리고, 타임아웃이 곧 결과다. ## 주입 **주입은 둘이고 한 번에 하나씩만 건다.** 4a 를 끝까지 밟고 복구 확인표를 통과한 뒤에 4b 를 건다. 두 노드가 동시에 꺼져 있으면 무엇이 무엇의 결과인지 가릴 수 없고, 4b 는 API 서버를 끄기 때문에 4a 를 관찰할 `kubectl` 자체가 없어진다. 치는 순서는 이렇다. ```text 4a 주입 ─▶ 주입 검증 §1·§2 ─▶ 관찰 §1~§7 ─▶ 복구 §1 (확인표 통과) │ ┌─────────────────────────────────┘ ▼ 4b 주입 ─▶ 주입 검증 §3 ─▶ 관찰 §8·§9 ─▶ 복구 §2 ``` ### 1. 워커 노드의 전원을 뽑는다 (4a) **목적** — `kc-lab-2` 를 신호 없이 정지시켜 keycloak-0 과 postgres 를 동시에 잃는다. ```bash label="[터미널 A] ① 차단 시각을 먼저 찍는다" date '+%H:%M:%S 차단' ``` ```bash label="[터미널 A] ② 전원을 뽑는다" virsh destroy kc-lab-2 ``` **예상 결과** ```text 차단 시각: 12:07:43 Domain 'kc-lab-2' destroyed ``` **왜 필요한가** — `virsh shutdown` 은 ACPI(Advanced Configuration and Power Interface, 전원 관리 규격) 종료 신호를 보내 kubelet 이 정상 종료하고 파드가 정리되므로, 쿠버네티스가 정상적인 노드 이탈로 처리해 이 절차의 발견 둘이 통째로 안 나온다. `virsh destroy` 는 신호가 없어 마지막 상태가 그대로 얼어붙는다. 시각을 찍는 것도 같은 이유다 — 40초와 5분은 이 시각에서 뺀 값이고, 기준점이 없으면 뒤의 관찰은 나열로 끝난다. **문제가 생기면** — `virsh` 가 도메인을 못 찾으면 `qemu:///session` 을 보고 있는 것이므로 전제의 1번으로 돌아가 `virsh uri` 를 본다. ### 2. 컨트롤 플레인 노드의 전원을 뽑는다 (4b) — 4a 를 끝낸 뒤에 친다 **언제 치는가** — 복구 §1 의 원상복구 확인표를 전부 통과한 뒤다. 4a 의 주입 검증·관찰·복구를 먼저 밟고 여기로 온다. **목적** — `kc-lab-1` 을 정지시켜 API 서버와 Traefik 과 관측 스택을 한꺼번에 잃는다. ```bash label="[터미널 B] ① 이 노드에 무엇이 있는지 — 이것이 곧 영향 범위다" kubectl get pods -A -o wide --field-selector spec.nodeName=kc-lab-1 ``` ```bash label="[터미널 B] ② 진입점의 복제본 수" kubectl -n kube-system get deploy traefik ``` ```bash label="[터미널 A] ③ 차단 시각" date '+%H:%M:%S 차단' ``` ```bash label="[터미널 A] ④ 전원을 뽑는다" virsh destroy kc-lab-1 ``` **예상 결과** — ① 의 실측은 이렇다. ```text keycloak-lab keycloak-1 kube-system coredns-54996dc9b4-8k8fj kube-system helm-install-traefik-crd-q29b5 kube-system local-path-provisioner-77b9867795-g27z8 kube-system metrics-server-6dc596dfb8-7xxq4 kube-system svclb-traefik-5eb6a9a1-qwwk5 kube-system traefik-5d6fcf895-wpfhr observability grafana-845b5678cf-b6gvc observability node-exporter-9qk9w observability prometheus-6774f94f7c-pzr2t ``` ④ 뒤에는 `차단 시각: 12:18:08` 과 `Domain 'kc-lab-1' destroyed` 가 나오고, 원래 실행에서는 터미널 B 의 SSH 세션이 그대로 끊긴다 — 그 실행은 터미널 B 를 `kc-lab-1` 안에 두었기 때문이다. **터미널 B 를 `test-server` 에 두면 세션은 안 끊긴다.** 대신 `kubectl` 이 API 서버를 못 찾는다 — k3s server 가 방금 내린 그 노드에 있고 kubeconfig 의 `server:` 가 `https://192.168.122.11:6443` 을 가리키기 때문이다. 화면에 남는 것이 「셸이 죽었다」에서 「`kubectl` 이 안 닿는다」로 바뀔 뿐, **관찰이 멈춘다는 사실은 같다.** 이 실험이 보여 주려는 것도 그것이다 — 죽은 노드로 관찰하고 있었으면 죽는 순간을 못 본다. 터미널 C 의 `curl` 이 그래서 따로 있다. **왜 필요한가** — `traefik` 이 이 노드에 있고 `replicas` 가 `1` 이라, 이 노드를 뽑으면 클러스터로 들어갈 문이 사라진다. 인벤토리를 먼저 뽑아 두지 않으면 4b 에서 무엇이 없어졌는지 사후에 못 센다. **문제가 생기면** — 4a 의 확인표를 통과하기 전에는 여기로 오지 않는다. 두 고장이 겹치면 무엇이 원인인지 못 가린다. **다음** — 아래 「주입 검증」 §3 으로 간다. §1 과 §2 는 4a 의 것이라 이미 쳤다. ## 주입 검증 A-5 와 A-6 에서는 규칙을 넣었는데 카운터가 0 인 것이 실패였다. 여기서 검증하는 대상은 다르다. 믿을 수 있는 것은 하이퍼바이저이고, 쿠버네티스가 뭐라고 하든 그것은 결과다. ### 1. 기계가 실제로 꺼졌나 (4a) **무엇을 확인하는가** — 게스트의 전원 상태. ```bash label="[터미널 A] ① 하이퍼바이저에게 묻는다" virsh list --all ``` ```bash label="[터미널 A] ② 미검증 — 원 실행에는 이 확인이 없다" ping -c 2 -W 2 192.168.122.12 ``` **출력에서 답이 되는 것** — ① 에서 `shut off` 이면 꺼진 것이고, ID 가 `-` 로 바뀐 것도 같은 말이다. ② 에서 `0 received` 가 나오면 꺼져 있다. **이 결과가 뜻하는 것** — 여기까지가 주입 검증이다. 다음 단계의 `kubectl` 출력은 검증이 아니라 관측 대상이다. ### 2. 쿠버네티스는 아직 Ready 라고 말한다 (4a) **무엇을 확인하는가** — 노드 상태와 사용자가 겪는 응답을 같은 시각에 나란히 본다. ```bash label="[터미널 B] ① 15초 간격으로 몇 번 친다" kubectl get node kc-lab-2 ``` ```bash label="[터미널 C] ② 같은 간격으로 밖에서" curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 --resolve auth.hyeonworks.com:443:192.168.122.10 \ https://auth.hyeonworks.com/realms/master ``` 손이 아프면 한 줄로 묶는 형태가 있고, 그 루프는 미검증이다. `Ctrl-C` 로 멈춘다. ```bash label="[터미널 B] ③ 미검증 — 두 줄을 하나로 묶는다" while true; do printf '%s node=%s 외부=%s\n' "$(date +%H:%M:%S)" \ "$(kubectl get node kc-lab-2 --no-headers | awk '{print $2}')" \ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 \ --resolve auth.hyeonworks.com:443:192.168.122.10 https://auth.hyeonworks.com/realms/master)" sleep 15 done ``` **출력에서 답이 되는 것** — 노드 상태가 넘어가는 줄과, 그 앞뒤의 외부 코드다. 실측은 이렇다. ```text +15초 node=Ready | keycloak-0=Running postgres-7b474b88c8-2gf27=Running | 외부 HTTP 000 +30초 node=Ready | keycloak-0=Running postgres-7b474b88c8-2gf27=Running | 외부 HTTP 000 +45초 node=NotReady | keycloak-0=Running postgres-7b474b88c8-2gf27=Running | 외부 HTTP 503 +60초 node=NotReady | keycloak-0=Running postgres-7b474b88c8-2gf27=Running | 외부 HTTP 503 ``` 2026-09-17 에 새 실험대에서 다시 재니 같은 모양이었다(observed). 차단은 `14:39:28` 이다. ```text 14:39:56 (+28초) kc-lab-2=Ready | keycloak-0=Running postgres=Running | 정문 000 14:40:34 (+66초) kc-lab-2=NotReady | keycloak-0=Running postgres=Running | 정문 503 14:42:41 (+3분) kc-lab-2=NotReady | keycloak-0=Running postgres=Running | 정문 503 ``` **넘어가는 대목은 같고 초는 실행마다 다르다.** 30초 간격으로 쟀으므로 이번 판에서는 `+28초` 와 `+66초` 사이 어디쯤이다. `node-monitor-grace-period` 가 40초라 그 범위에 들어간다. 그 뒤로 3분이 지나도 두 파드는 계속 `Running` 이었다. **이 결과가 뜻하는 것** — `+30초` 줄과 `+45초` 줄 사이에서 노드 상태가 넘어간다. kube-controller-manager 는 kubelet 의 하트비트가 `node-monitor-grace-period`(이 실험대에서 40초) 동안 없어야 `NotReady` 로 바꾸고, 그 40초 동안 쿠버네티스는 거짓말을 한다. 사용자는 그 40초에도 이미 장애를 겪고 있고 `000` 이 그것을 말한다. 여기서 주입이 안 걸렸다고 결론 내리면 틀린다 — 기계는 꺼져 있고 쿠버네티스가 아직 모를 뿐이다. 처음 40초가 `503` 이 아니라 `000` 인 까닭은 층이 다르기 때문이다. `000` 은 `curl` 이 응답 자체를 못 받은 것(타임아웃 또는 연결 실패)이고, `503` 은 nginx 와 Traefik 은 살아 있고 뒤로 보낼 파드가 없는 것이다. 엣지 nginx(`kc-lab-edge`)의 upstream 에는 두 노드가 다 들어 있다. ```text upstream k3s_traefik { server 192.168.122.11:80; server 192.168.122.12:80; } ``` 죽은 쪽으로 배분된 요청은 응답도 거절도 못 받고 `--max-time 8` 에 걸린다. 이 까닭을 nginx 로그로 확인하려던 절이 증거 파일에 제목만 있고 아래가 비어 있다. 직접 볼 수 있는 줄을 가이드가 미검증으로 표시했다. `upstream timed out` 이 `192.168.122.12` 에 대해 찍히면 그것이 답이고, nginx 에러 로그는 2048바이트에서 잘리므로 잘려 보이면 access 로그를 본다. ```bash label="[터미널 C] 미검증 — 엣지 게스트의 에러 로그" sudo tail -f /var/log/nginx/error.log ``` ### 3. 4b 의 검증은 kubectl 이 죽은 것 자체다 **무엇을 확인하는가** — API 서버에 닿는지. ```bash label="[터미널 B] ① 붙어 있던 그 터미널에서" kubectl get nodes ``` **출력에서 답이 되는 것** — `kubectl: Unable to connect to the server: dial tcp`. **이 결과가 뜻하는 것** — API 서버가 `kc-lab-1:6443` 에 있었으므로 당연한 결과다. 4a 에서는 쿠버네티스가 뭐라고 하는가를 물을 수 있었고 여기서는 물어볼 상대가 없어, 이 절차의 관찰 도구가 통째로 바뀐다. ## 관찰 ### 1. 죽은 파드가 산 파드보다 건강해 보인다 ```bash label="[터미널 B] ① 파드마다 phase 와 ready 와 노드를 함께" kubectl -n keycloak-lab get pods -o "custom-columns=NAME:.metadata.name,PHASE:.status.phase,READY:.status.containerStatuses[0].ready,NODE:.spec.nodeName" ``` ```text a2-probe Running true kc-lab-2 keycloak-0 Running true kc-lab-2 keycloak-1 Running false kc-lab-1 postgres-7b474b88c8-2gf27 Running true kc-lab-2 ``` `keycloak-0` 은 꺼진 기계 위에서 `ready=true` 이고 `keycloak-1` 은 살아 있는데 `ready=false` 다. `keycloak-0` 은 kubelet 이 없어 상태를 갱신할 수 없으므로 마지막으로 보고한 값이 얼어 있고, `keycloak-1` 은 살아서 정직하게 보고한다 — DB 가 없으니 readiness 실패다. 파드 상태는 지금 어떤가가 아니라 마지막으로 그렇게 들었다이고, 노드가 죽으면 그 노드 파드의 상태는 갱신을 멈춘 값이 된다. ### 2. 이벤트는 Age 로 먼저 거른다 ```bash label="[터미널 B] ① 최근 이벤트 스무 줄" kubectl -n keycloak-lab get events --sort-by=.lastTimestamp | tail -20 ``` ```text 10m Warning Unhealthy pod/keycloak-0 Readiness probe failed: Get "http://10.42.1.67:9000/health/ready": context deadline exceeded (Client.Timeout exceeded while awaiting headers) 3m15s Warning NodeNotReady pod/postgres-7b474b88c8-2gf27 Node is not ready 3m15s Warning NodeNotReady pod/keycloak-0 Node is not ready 3m15s Warning NodeNotReady pod/a2-probe Node is not ready 2m27s Warning Unhealthy pod/keycloak-1 Readiness probe failed: Get "http://10.42.0.35:9000/health/ready": context deadline exceeded (Client.Timeout exceeded while awaiting headers) 2s Warning Unhealthy pod/keycloak-1 Readiness probe failed: HTTP probe failed with statuscode: 503 ``` 노드를 뽑은 것은 `3m15s` 전인데 맨 위 줄은 `10m` 짜리라 주입보다 앞선 사건이고 앞 실험이 남겼다. 이벤트 목록은 시간대가 섞여 있으므로 `Age` 로 먼저 걸러야 내가 만든 일을 고를 수 있다. 주입 이후 `keycloak-0` 에 붙은 이벤트는 `NodeNotReady` 하나뿐인데 그것은 컨트롤러가 쓴 것이고, kubelet 이 없으니 그 파드에 대해 말해 줄 주체가 없다. `keycloak-1` 의 실패는 두 종류다 — 처음에는 프로브 자체가 타임아웃되고(`context deadline exceeded`), 나중에는 `503` 을 받는다. Keycloak 이 DB 없음을 스스로 판단해 답할 수 있게 된 것이라 같은 `Unhealthy` 라도 층이 다르다. ### 3. Prometheus 는 정확했다 ```bash label="[터미널 B] ① 한 줄짜리 JSON 을 처음 한 번은 그대로 본다" kubectl -n observability exec deploy/prometheus -- \ wget -qO- 'localhost:9090/api/v1/query?query=up' ``` ```bash label="[터미널 B] ② 미검증 — 라벨만 남겨 자르는 형태" kubectl -n observability exec deploy/prometheus -- \ wget -qO- 'localhost:9090/api/v1/query?query=up' \ | tr ',' '\n' | grep -E '"job":|"pod":|"node":|^"[0-9]' ``` ```text up{job=keycloak pod=keycloak-1 } = 1 up{job=keycloak pod=keycloak-0 } = 0 up{job=kubelet pod=- } = 1 up{job=kubelet pod=- } = 0 up{job=node-exporter pod=kc-lab-1 } = 1 up{job=node-exporter pod=kc-lab-2 } = 0 up{job=prometheus pod=- } = 1 ``` `kc-lab-2` 쪽이 전부 `0` 이고, `kubelet` job 이 두 줄인 것은 노드마다 하나씩이기 때문이다. A-2 와 정반대로, 대상이 사라진 노드 상실은 `up` 이 잡고 대상이 살아서 못 쓰는 DB 상실은 못 잡는다. ### 4. taint 는 붙었는데 5분 동안 아무 일도 안 일어난다 ```bash label="[터미널 B] ① 죽은 노드에 붙은 taint" kubectl describe node kc-lab-2 | grep -A3 Taints ``` ```bash label="[터미널 B] ② 같은 값을 한 줄로" kubectl get node kc-lab-2 -o jsonpath='{.spec.taints}' ; echo ``` ```bash label="[터미널 B] ③ 살아 있는 파드가 그것을 얼마나 참나" kubectl -n keycloak-lab describe pod keycloak-1 | grep -A4 Tolerations ``` ①② 는 `node.kubernetes.io/unreachable=:NoSchedule` 과 `node.kubernetes.io/unreachable=:NoExecute` 를 낸다. `NoSchedule` 은 새 파드를 여기 보내지 말라는 뜻이고 `NoExecute` 는 이미 있는 파드도 쫓아내라는 뜻인데, 그런데도 아무 일이 안 일어나는 까닭이 ③ 에 있다. ```text === NoExecute taint 의 tolerationSeconds — 언제 축출되는가 === node.kubernetes.io/not-ready NoExecute tolerationSeconds=300 node.kubernetes.io/unreachable NoExecute tolerationSeconds=300 ``` `tolerationSeconds=300` 은 쿠버네티스가 모든 파드에 자동으로 붙인 값이다. ```text 기계 정지 │ │ 40초 node-monitor-grace-period → 노드 NotReady │ │ +300초 tolerationSeconds (NoExecute) → 파드 축출 시작 ▼ 총 약 5분 40초 동안 쿠버네티스는 아무것도 하지 않는다 ``` ### 5. 5분을 실제로 기다린다 ```bash label="[터미널 B] ① 30초 간격으로 본다" watch -n 30 'kubectl -n keycloak-lab get pods -o wide' ``` ```text +240초 a2-probe:Running keycloak-0:Running keycloak-1:Running postgres-7b474b88c8-2gf27:Running +270초 a2-probe:Terminating keycloak-0:Terminating keycloak-1:Running postgres-7b474b88c8-2gf27:Terminating postgres-7b474b88c8-9cmsv:Pending +300초 a2-probe:Terminating keycloak-0:Terminating keycloak-1:Running postgres-7b474b88c8-2gf27:Terminating postgres-7b474b88c8-9cmsv:Pending ``` `+240초` 와 `+270초` 사이에 두 가지가 동시에 일어난다. `kc-lab-2` 의 파드들이 `Terminating` 으로 바뀌고, 새 이름의 postgres 파드가 생기며 `Pending` 이다. 축출이 시작됐는데 `Terminating` 이 안 끝나고 새 파드는 뜨지 못하며, 두 문제는 원인이 다르다. 2026-09-17 에 새 실험대에서 다시 재니 같은 모양이었다(observed). 차단 `05:39:28` 에 대해 두 파드의 `deletionTimestamp` 가 `05:44:17` 로 찍혔다 — **4분 49초**다. ```text keycloak-0 Terminating kc-lab-2 deletionTimestamp=2026-09-17T05:44:17Z postgres-7b474b88c8-prlxx Terminating kc-lab-2 deletionTimestamp=2026-09-17T05:44:17Z postgres-7b474b88c8-5zp6v Pending deletionTimestamp= keycloak-1 Running kc-lab-1 deletionTimestamp= ``` **StatefulSet 과 Deployment 의 처지가 그대로 갈린다.** 새 이름으로 생긴 것은 postgres 쪽뿐이고 `keycloak-0` 은 대체 파드 없이 `Terminating` 으로만 남는다. StatefulSet 은 이름이 고정이라 같은 이름의 파드를 둘로 만들 수 없고, 죽은 노드의 kubelet 이 「지웠다」고 답해 주지 못하므로 그 이름이 영원히 안 비워진다. ### 6. 새 파드가 갈 곳이 없다 ```bash label="[터미널 B] ① Pending 인 파드 이름을 먼저 확인한다" kubectl -n keycloak-lab get pods --field-selector=status.phase=Pending ``` ```bash label="[터미널 B] ② 이름은 매번 다르므로 ① 에서 본 것을 옮겨 적는다" kubectl -n keycloak-lab describe pod postgres-7b474b88c8-9cmsv | grep -A6 Events ``` ```text Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 4m45s default-scheduler 0/2 nodes are available: 1 node(s) didn't match PersistentVolume's node affinity, 1 node(s) had untolerated taint(s). no new claims to deallocate, preemption: 0/2 nodes are available: 2 Preemption is not helpful for scheduling. ``` `0/2 nodes are available` 뒤에 이유가 노드 수만큼 나열되므로 한 줄에 두 노드의 사연이 다 들어 있다. ```text kc-lab-2 → had untolerated taint(s) (죽은 노드) kc-lab-1 → didn't match PersistentVolume's node affinity ``` 주입 전에 이미 알고 있던 것이 그대로 벌어졌다. 볼륨이 `kc-lab-2` 에 못박혀 있어 살아 있는 노드로 못 가고, 죽은 노드에는 taint 때문에 못 간다. 갈 곳이 없다. 이 실험대의 조건이고, 운영이라면 네트워크 스토리지나 DB 복제가 그 몫을 맡아야 한다. 노드가 영영 안 돌아오면 백업 복원(D-1)으로 간다. ### 7. StatefulSet 은 대체 파드를 안 만든다 ```bash label="[터미널 B] ① 원하는 수와 현재 수" kubectl -n keycloak-lab get statefulset keycloak ``` ```bash label="[터미널 B] ② keycloak 파드만" kubectl -n keycloak-lab get pods | grep keycloak ``` ```text keycloak 2 1 keycloak-0 1/1 Terminating 0 30m keycloak-1 0/1 Running 0 143m ``` `DESIRED=2` 인데 `CURRENT=1` 이고 `keycloak-0` 이 30분째 `Terminating` 이다. StatefulSet 의 계약은 같은 이름의 파드가 클러스터에 하나뿐이어야 한다는 것이고, 컨트롤 플레인은 노드가 안 보이니 파드가 죽었는지 확신할 수 없어 옛 파드를 확실히 지우기 전엔 새 `keycloak-0` 을 못 만든다. ```text 파드 삭제 요청 └─ kubelet 이 컨테이너를 멈추고 "지웠다"고 보고해야 끝난다 └─ kubelet 이 없다 → 보고가 없다 → 영원히 Terminating ``` Deployment 였다면 즉시 새 파드를 만든다. 이름이 아무래도 되기 때문이고, postgres 가 실제로 그랬다. 강제로 진행시키는 명령이 있지만 이 절차에서는 치지 않는다. ```text kubectl -n keycloak-lab delete pod keycloak-0 --grace-period=0 --force ``` 그것은 컨테이너가 실제로 죽었는지 모른 채 API 에서 지우는 것이라, 노드가 사실은 살아 있고 네트워크만 끊긴 것이라면 같은 이름의 파드 둘이 동시에 존재하게 된다. 이 실험대에서는 `virsh start` 가 훨씬 안전하고 빠르다. ### 8. 4b — kubectl 이 없으면 컨테이너 런타임에 직접 묻는다 **이 실험대는 한 줄로 쳤다.** ```bash label="[터미널 A] 실제로 친 형태" ssh kc-lab-2 'sudo crictl ps --name keycloak' ``` **따라 하는 사람은** 붙고 나서 원격 셸에서 친다. 한 줄에 SSH 접속과 원격 셸의 인용을 겹쳐 놓지 않는다. 이 두 단계 형태는 이 실험대에서 치지 않았다. ```bash label="[터미널 A] ① 살아 있는 노드에 붙는다" ssh kc-lab-2 ``` ```bash label="[kc-lab-2] ② 컨테이너 런타임에 직접 묻는다" sudo crictl ps --name keycloak ``` ```text CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD NAMESPACE e5f777900b762 60e153026e8f5 4 minutes ago Running keycloak 0 640d4dafaefb3 keycloak-0 keycloak-lab ``` `STATE` 가 `Running`, `ATTEMPT` 가 `0` 이다. API 서버가 없는데도 컨테이너는 돌고 있다. ```text 죽은 것: API 서버 · 스케줄러 · coredns · Traefik · Prometheus · Grafana 산 것: keycloak-0 · postgres · containerd 문제: 들어갈 문(Traefik)이 없다 ``` 컨트롤 플레인 상실은 워크로드 상실과 다르다. 이미 떠 있는 것은 계속 돌고, 새로 뜨거나 옮기거나 고치는 것이 안 된다. 전체 목록은 `sudo crictl ps` 로 본다. **③ 원격 셸에서 나온다.** 뒤의 단계는 다시 터미널 A 에서 치므로, 나오지 않으면 `kc-lab-2` 안에서 `kc-lab-2` 로 또 붙게 된다. ```bash label="[kc-lab-2] ③ 원격 셸에서 나온다" exit ``` `crictl` 이 소켓을 못 찾으면 k3s 의 것을 직접 주는데, 그 줄은 미검증이다. **아래는 원격 셸이 아니라 터미널 A 에서 치는 형태다.** ```bash label="[터미널 A] 미검증 — 소켓을 직접 준다" ssh kc-lab-2 'sudo crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock ps' ``` ### 9. 4b — 밖에서는 두 주소를 함께 본다 ```bash label="[터미널 C] ① 20초 간격으로, 인증 서버" curl -s -o /dev/null -w 'auth=%{http_code}\n' --max-time 8 --resolve auth.hyeonworks.com:443:192.168.122.10 \ https://auth.hyeonworks.com/realms/master ``` ```bash label="[터미널 C] ② 같은 간격으로, Grafana" curl -s -o /dev/null -w 'grafana=%{http_code}\n' --max-time 8 https://grafana.hyeonworks.com/ ``` **★ ② 는 이 배치에서 `000` 밖에 못 낸다 — 노드가 살아 있어도 그렇다**(2026-09-17, observed). 원래 실행은 여기서 `502` 와 `000` 이 갈렸는데 지금은 처음부터 `000` 이라 판정에 못 쓴다. 이유가 이름 해석이 아니라 **인증서**다. ```bash label="[lab host] 왜 000 인지 세 단계로 가른다" curl -s -o /dev/null --max-time 8 --resolve grafana.hyeonworks.com:443:192.168.122.10 https://grafana.hyeonworks.com/ ; echo "curl exit=$?" curl -sk -o /dev/null -w 'grafana=%{http_code}\n' --max-time 8 --resolve grafana.hyeonworks.com:443:192.168.122.10 https://grafana.hyeonworks.com/ echo | openssl s_client -connect 192.168.122.10:443 -servername auth.hyeonworks.com 2>/dev/null | openssl x509 -noout -ext subjectAltName ``` ```text label="이 실험대의 값" curl exit=60 grafana=404 X509v3 Subject Alternative Name: DNS:app1.hyeonworks.com, DNS:app2.hyeonworks.com, DNS:auth.hyeonworks.com ``` `curl exit=60` 은 인증서 검증 실패다. 엣지의 인증서가 덮는 이름은 셋뿐이라 `grafana.hyeonworks.com` 은 TLS 단계에서 끝나고 HTTP 는 시작도 안 한다. `-k` 로 검증을 끄면 그제서야 nginx 가 답하는데 `404` 다 — 그 이름으로 갈 곳이 없다. 가이드 06 도 **Grafana 는 밖에 열지 않고 `port-forward svc/grafana 3000:3000` 으로 본다**고 적어 둔다. **그래서 ② 는 이 절의 신호가 못 된다.** 판정은 `auth` 쪽 ① 로 한다. 관측 스택이 살아 있는지를 밖에서 보려면 `port-forward` 를 띄운 터미널이 끊기는지로 보거나, 04 의 인증서에 이름을 더하고 엣지에 서버 블록을 올려야 한다(unknown — 이 실험대는 안 했다). ```text +20초 외부 auth=000 grafana=000 | kubectl: Unable to connect to the server: dial tcp +60초 외부 auth=000 grafana=000 | kubectl: Unable to connect to the server: dial tcp +120초 외부 auth=000 grafana=502 | kubectl: Unable to connect to the server: dial tcp +160초 외부 auth=000 grafana=000 | kubectl: Unable to connect to the server: dial tcp ``` `+120초` 의 `grafana=502` 한 줄만 다르다. `503`(4a)은 nginx 와 Traefik 이 살아 있고 뒤에 보낼 파드가 없는 것, `502`(4b, 한 번)는 nginx 가 연결 실패를 제때 판정해 자기 힘으로 만든 것, `000`(4b, 대부분)은 nginx 가 죽은 주소를 기다리다 `--max-time 8` 에 먼저 걸렸다. `502` 가 한 번이라도 찍혔으므로 nginx 는 살아 있었고, 같은 고장인데 코드가 흔들리는 까닭은 타임아웃 경주다. 4b 구간의 지표는 지금 확인할 수 없다. Prometheus 도 Grafana 도 `kc-lab-1` 에 있었다. ```text 대상이 죽음 → up = 0 → "언제 죽었는지" 알 수 있다 관측자가 죽음 → 데이터 없음 → "그때 무슨 일이 있었는지" 모른다 ``` ## 복구와 원상복구 확인표 ### 1. 워커 노드를 다시 켠다 (4a) **목적** — `kc-lab-2` 를 되살려 파드와 볼륨과 정문을 제자리로 돌린다. ```bash label="[터미널 A] ① 재기동 시각" date '+%H:%M:%S 재기동' ``` ```bash label="[터미널 A] ② 전원을 넣는다" virsh start kc-lab-2 ``` ```bash label="[터미널 B] ③ 30초 간격으로 노드와 파드" kubectl get nodes ``` ```bash label="[터미널 B] ④ 파드 상태" kubectl -n keycloak-lab get pods ``` ```bash label="[터미널 C] ⑤ 같은 간격으로 밖에서" curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 --resolve auth.hyeonworks.com:443:192.168.122.10 \ https://auth.hyeonworks.com/realms/master ``` **예상 결과** ```text +30초 node=Ready | Running 파드 3 개 | 외부 HTTP 503 +60초 node=Ready | Running 파드 3 개 | 외부 HTTP 200 → 서비스 복귀 ``` 60초 만에 사람 개입 없이 전부 제자리로 돌아왔다. `Terminating` 이던 파드도 `Pending` 이던 파드도 kubelet 이 돌아오자 정리됐다. **왜 필요한가** — 이 60초는 MTTR(Mean Time To Recovery, 평균 복구 시간)이 아니다. `virsh start` 를 친 뒤의 시간이고, 실제 장애 구간은 `12:07:43`(차단)에서 `12:17:31`(서비스 복귀)까지 약 10분이며 그 대부분은 사람이 관찰하고 결정하는 데 썼다. **문제가 생기면** — `postgres` 파드 이름이 바뀌어 있으면 정상이다. 축출 때 생겼다가 `Pending` 이던 파드가 노드가 살아나자 그대로 뜬 것이고, `keycloak-0` 은 StatefulSet 이라 이름이 그대로다. ```text === 복구 확인 === keycloak-0 1/1 Running 0 68s keycloak-1 1/1 Running 0 144m postgres-7b474b88c8-9cmsv 1/1 Running 0 4m20s ``` | 항목 | 명령 | 돌아왔을 때 | |---|---|---| | VM | `virsh list --all` | 둘 다 `running` | | 노드 | `kubectl get nodes` | 둘 다 `Ready` | | 파드 | `kubectl -n keycloak-lab get pods` | 전부 `1/1 Running`, `Pending` 없음 | | 밖 | `curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master` | `200` | 이 표를 통과하기 전에는 4b 로 넘어가지 않는다. ### 2. 컨트롤 플레인 노드를 다시 켠다 (4b) **목적** — `kc-lab-1` 을 되살려 API 서버와 진입점과 관측 스택을 돌린다. ```bash label="[터미널 A] ① 재기동 시각" date '+%H:%M:%S 재기동' ``` ```bash label="[터미널 A] ② 전원을 넣는다" virsh start kc-lab-1 ``` ```bash label="[터미널 B] ③ 파드 상태" kubectl -n keycloak-lab get pods ``` **예상 결과** ```text 재기동: 12:23:39 Domain 'kc-lab-1' started +30초 외부=502 | kc-lab-1=Ready kc-lab-2=Ready +60초 외부=200 | kc-lab-1=Ready kc-lab-2=Ready → 서비스 복귀 (총 60초) ``` `+30초` 의 `502` 는 nginx 가 먼저 살아나고 Traefik 이 아직 안 뜬 중간 상태이고, 4b 내내 보던 `000` 과 층이 다르다. ```text keycloak-0 1/1 Running 0 7m57s keycloak-1 1/1 Running 1 ( ago) 151m postgres-7b474b88c8-9cmsv 1/1 Running 0 11m ``` **왜 필요한가** — 세 가지를 한 줄에서 읽는다. `keycloak-1` 의 `RESTARTS` 가 1 인 것은 `kc-lab-1` 위에 있었으니 당연하고, `AGE` 가 `151m` 인데 재시작은 방금인 것은 `AGE` 가 파드가 만들어진 시각이지 컨테이너가 시작한 시각이 아니기 때문이며, `( ago)` 는 재시작 시각이 API 서버 시계보다 미래로 보일 때 나온다. 그 원인은 이 절차가 확정하지 못했고, 잠시 뒤 다시 치면 정상 값으로 바뀐다. **문제가 생기면** — 복구 뒤에 Grafana 에서 `up{job="keycloak"}` 그래프를 `12:15–12:30` 으로 열어 `12:18–12:23` 구간이 0 이 아니라 빈칸인 것을 확인한다. 그 구간은 선이 0 으로 내려간 것이 아니라 아예 끊겨 있다. Grafana 로그인도 풀려 있는데 데이터가 `emptyDir` 이라 파드 재시작에 사라지기 때문이고, Prometheus 는 PVC 라 지표가 남았지만 관측자가 죽어 있던 구간의 데이터는 애초에 수집되지 않았다. Prometheus 를 `port-forward` 로 보고 있었다면 다시 연결한다. | 항목 | 명령 | 돌아왔을 때 | |---|---|---| | VM | `virsh list --all` | 둘 다 `running` | | 노드 | `kubectl get nodes` | 둘 다 `Ready` | | 파드 | `kubectl -n keycloak-lab get pods -o wide` | 전부 `1/1 Running` | | 진입점 | `kubectl -n kube-system get deploy traefik` | `1/1` | | Service | `kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak` | ready 주소 둘 | | 클러스터 뷰 | `kubectl -n keycloak-lab logs keycloak-0 \| grep ISPN000094 \| tail -1` | 멤버 `(2)` | | 관측 | Prometheus `up` | 전부 1 | | 밖 | `curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master` | `200` | ## 막히면 | 증상 | 원인 | 확인 | |---|---|---| | `virsh` 가 도메인을 못 찾는다 | `qemu:///session` 을 보고 있다 | `virsh uri` — `system` 이어야 한다 | | VM 을 껐는데 노드가 `Ready` | 정상이다. `node-monitor-grace-period` 40초 | `virsh list --all` 로 전원을 먼저 본다 | | 5분이 지나도 축출이 안 온다 | 40초 + `tolerationSeconds=300` = 5분 40초 | `describe pod \| grep -A4 Tolerations` | | 새 파드가 계속 `Pending` | PVC 가 죽은 노드에 못박혀 있다 | `describe pod` 의 `FailedScheduling` | | `keycloak-0` 이 30분째 `Terminating` | StatefulSet + kubelet 없음. 정상이다 | `get statefulset` 의 `CURRENT` | | `--force` 로 지우고 싶다 | 노드가 살아 있으면 중복 실행이 된다 | 치지 말고 `virsh start` | | `kubectl` 이 전혀 안 된다 (4b) | API 서버가 죽은 노드에 있었다. 정상 | `ssh kc-lab-2 'sudo crictl ps'` | | `crictl` 이 소켓을 못 찾는다 | k3s 는 자기 containerd 소켓을 쓴다 | `--runtime-endpoint unix:///run/k3s/containerd/containerd.sock` | | `503` 을 기대했는데 `000` | 층이 다르다. nginx 가 죽은 주소를 기다린다 | `--max-time` 을 늘려 보면 `502` 가 나온다 | | `curl` 이 몇 분씩 안 끝난다 | `--max-time` 을 안 줬다 | 모든 외부 확인에 `--max-time 8` | | 그래프의 그 구간이 0 으로 보인다 | 0 이 아니라 데이터 없음이다 | 점 사이가 이어져 있는지 본다 | | Grafana 로그인이 풀렸다 | 데이터가 `emptyDir` | 재시작마다 그렇다. PVC 로 바꾸면 남는다 | | Prometheus 가 갑자기 안 보인다 | `port-forward` 가 끊겼다 | 다시 연다 | | `RESTARTS` 가 `1 ( ago)` | 재시작 직후에 나온다. 원인 미확정 | 잠시 뒤 다시 친다 | | 4b 결과가 4a 와 섞인다 | 4a 복구를 확인하지 않고 넘어갔다 | 4a 확인표를 통과한 뒤 시작 | ## 무엇이 관측이고 무엇이 아닌가 - (observed) 차단 `12:07:43` · 재기동 `12:16:31` · 2차 차단 `12:18:08` · 2차 재기동 `12:23:39`, `+30초` 까지 `Ready` 이고 `+45초` 에 `NotReady`, 그 동안 외부가 `000` 에서 `503` 으로, 죽은 노드 파드의 `ready=true`, `up` 일곱 줄, taint 두 종류, `tolerationSeconds=300`, `+270초` 의 축출과 `Pending`, `0/2 nodes are available` 줄, `DESIRED=2 / CURRENT=1` 과 30분째 `Terminating`, `crictl` 출력 한 줄, `grafana=502` 한 번, 양쪽 복구 60초, postgres 이름이 `-2gf27` 에서 `-9cmsv` 로 바뀐 것. - 인용한 값이고 잰 값이 아닌 것 — 본문의 40초와 5분은 쿠버네티스 기본값을 인용했다. 관측된 전이 시점(`+45초`, `+270초`)이 그 값과 모순되지 않는다는 것까지가 이 절차가 말할 수 있는 범위이고, 값 자체를 측정하지는 않았다. - (unknown) `ping -c 2 -W 2 192.168.122.12`(원 실행에 없다), 노드 상태와 외부 코드를 한 줄로 묶는 `while` 루프, `tr ',' '\n' | grep -E` 로 자른 `up` 출력, `sudo tail -f /var/log/nginx/error.log`, `crictl` 에 소켓을 직접 주는 줄, `ssh kc-lab-2` 로 들어가서 `crictl ps` 를 따로 치는 두 단계 형태. - 이 절차가 답을 못 남긴 곳 — 진입점이 처음 40초간 `000` 이었던 까닭을 nginx 로그로 확인하려던 절이 증거 파일에서 제목만 있고 아래가 비어 있다. 명령이 아무것도 찍지 못했다. - 원인을 확정하지 않은 것 — `RESTARTS` 의 `( ago)`. - 재지 않은 것 — 노드가 영영 안 돌아오는 경우. `local-path` PVC 가 그 노드와 함께 없어진 상태에서의 복구는 D-1(백업·복원)의 주제다. - 이 절차가 남기는 구성 숙제 셋 — Traefik `replicas=1` 이라 진입점이 단일 장애점인 것(`replicas=2` 로 늘리거나 DaemonSet 으로), Grafana 가 `emptyDir` 이라 재시작마다 세션이 사라지는 것(PVC 를 붙인다), 관측 스택이 실험 대상 노드에 함께 있는 것(노드가 둘뿐이라 완전히는 못 피한다).