docs(guides): reproduction guides for all 26 experiments
Written by subagents running under the writing-practitioner-guides skill,
one guide per experiment, 22,566 lines. Each walks a reader from baseline
capture through injection, injection verification, observation and recovery.
Section 3 carries the weight in most of them. Injection failed silently nine
times in this lab, and a failed injection looks exactly like no effect — so
the guides verify the target is actually in the intended state before
reading any result. A-4 makes virsh list the only proof because the node
reads Ready for 40 seconds after the machine is off; A-5 makes the packet
counter the sole go/no-go because a rule on the wrong node produces an empty
result that reads like a finding; A-6 quotes the run where 적용완료 was
printed between four Cannot find device "eth0" lines.
The traps the guides are built around are ones that invert a conclusion
rather than merely annoy:
A-0 emptying the session table without a restart leaves cache entries
that get counted as replication arriving
A-2 dropping -o /dev/null fuses body and status into one string
A-3 presence of "ready to accept connections" instead of its timestamp
B-2 row count alone reads an UPDATE as nothing having happened
B-4 tr ',' '\n' splits ["admin","editor"] so only admin is seen
B-7 no login screen means the cookie died and SSO re-authenticated
C-1 counting sessions without joining realm counts your own kcadm one
D-1 kubectl exec without -i restores nothing and still exits 0
D-4a "ran with error output" is what success looks like
Every quoted block is copied from docs/evidence/ and marked 실측; reshaped
commands are marked 미검증 rather than passed off as measured. Where a source
document carries a ★ correction the guides follow the corrected claim — A-7's
REVOKED_TOKEN hypothesis, C-1's session count, B-2's schema attribution.
Two hazards are stated rather than smoothed over: B-6 deletes a key that
cannot be recreated, and D-1/D-4 need host sudo, which asks for a password,
so those steps say a person must type them.
Audit over all 26: 672 interpretation pairs, 486 evidence citations, 117
undo sections, and zero occurrences of the patterns the skill forbids —
no python data processing, no deprecated kubectl get endpoints, no
placeholders, no bare kcadm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8062cc9a19
commit
6f6ab86345
@@ -0,0 +1,978 @@
|
||||
# A-4 재현 가이드 — 기계 전원을 뽑고 쿠버네티스가 언제 알아채는지 직접 본다
|
||||
|
||||
해설 문서: [`docs/experiment-a4-node-loss.md`](../../experiment-a4-node-loss.md) ·
|
||||
증거 원문: [`docs/evidence/a4-node-loss/`](../../evidence/a4-node-loss/)
|
||||
|
||||
## 이 가이드가 끝나면
|
||||
|
||||
당신 터미널에서 이것들을 **직접 본다.**
|
||||
|
||||
| 보게 되는 것 | 어디서 |
|
||||
|---|---|
|
||||
| 기계는 없는데 쿠버네티스가 40초 동안 `Ready` 라고 말하는 것 | `kubectl get node` 와 외부 `curl` 을 나란히 |
|
||||
| **죽은 파드가 `ready=true`, 산 파드가 `ready=false`** 인 것 | `get pods -o custom-columns` |
|
||||
| 그 와중에 `up` 은 정확히 0 인 것 | Prometheus |
|
||||
| 축출이 5분 뒤에야 시작되는 것 | `tolerationSeconds` 와 파드 상태 |
|
||||
| 새 파드가 **영원히 `Pending`** 인 것 | `describe pod` 의 Events |
|
||||
| StatefulSet 이 대체 파드를 **안 만드는** 것 | `get statefulset` 의 `CURRENT` |
|
||||
| `kubectl` 이 죽어도 컨테이너는 도는 것 | `kc-lab-2` 에서 `crictl ps` |
|
||||
| 관측자가 같이 죽으면 **0 이 아니라 구멍**이 남는 것 | Grafana |
|
||||
|
||||
## 전제
|
||||
|
||||
- [`05-keycloak`](../05-keycloak/) · [`06-observability`](../06-observability/) 가 끝나 있다.
|
||||
- **명령을 치는 곳이 세 군데다.** 이 실험은 그 구별이 곧 내용이다.
|
||||
|
||||
| 터미널 | 어디 | 무엇을 |
|
||||
|---|---|---|
|
||||
| **A** | `test-server` (VM 호스트) | `virsh` — 전원을 뽑고 다시 넣는다 |
|
||||
| **B** | `kc-lab-1` | `sudo kubectl` — 관찰. **4b 에서는 이 터미널이 죽는다** |
|
||||
| **C** | `test-server` | 밖에서 `curl`. 사용자 시점 |
|
||||
|
||||
- 터미널 A 에서 `virsh` 가 시스템 하이퍼바이저를 보고 있어야 한다.
|
||||
```bash
|
||||
export LIBVIRT_DEFAULT_URI=qemu:///system
|
||||
virsh uri
|
||||
```
|
||||
`qemu:///system` 이 아니면 **VM 이 안 보인다.** [`00-lab-host`](../00-lab-host/) 5절.
|
||||
- 4b 에서는 `kc-lab-2` 에도 붙는다. 터미널 A 와 같은 기계에서 `ssh kc-lab-2`.
|
||||
이름이 안 풀리면 `ssh 192.168.122.12`.
|
||||
|
||||
## 주의 — 이건 기계를 끄는 실험이다
|
||||
|
||||
`virsh destroy` 는 **종료 신호를 보내지 않는다. 전원 코드를 뽑는 것과 같다.**
|
||||
게스트 파일시스템이 더러운 채로 멈춘다. **실험대에서만 한다.**
|
||||
|
||||
전 구간 약 **40분**이다. 4a 에서 축출을 보려면 그것만 7분을 기다려야 한다.
|
||||
어느 시점에서든 그만두려면 터미널 A 에서 한 줄이면 된다.
|
||||
|
||||
```bash
|
||||
virsh start kc-lab-2 ; virsh start kc-lab-1
|
||||
```
|
||||
|
||||
## 표시 규약
|
||||
|
||||
| 표시 | 뜻 |
|
||||
|---|---|
|
||||
| **실측** | 2026-09-04 12:05–12:23 KST 실행 기록의 **출력 원문**. 증거 파일에 그대로 있다 |
|
||||
| **형태** | 값이 매번 달라지는 출력. 모양만 보이고 숫자는 당신 것과 다르다 |
|
||||
| **미검증** | 손으로 치기 좋게 이 가이드에서 고친 형태. 원래 실행은 스크립트로 했다 |
|
||||
|
||||
파드 이름·IP·시각은 **당신 환경에서 다르다.** 자리표시자(`<...>`)를 쓰지 않는
|
||||
대신 그 값을 뽑는 명령을 먼저 적는다. 예시로 실린 값은 전부 위 실행 기록의
|
||||
실제 값이다.
|
||||
|
||||
---
|
||||
|
||||
# 0. 왜 이 실험을 하는가
|
||||
|
||||
A-1 과 A-5 는 **네트워크만** 끊었다. 파드는 살아 있었고, 쿠버네티스는 계속
|
||||
정확한 상태를 알고 있었다. 여기서는 **기계 자체를 없앤다.** 그러면 상태를
|
||||
보고할 주체가 사라진다.
|
||||
|
||||
두 판본으로 나눈다. **어느 노드를 죽이느냐가 전부**이기 때문이다.
|
||||
|
||||
| | 죽이는 노드 | 그 노드에 있는 것 | 묻는 것 |
|
||||
|---|---|---|---|
|
||||
| **4a** | `kc-lab-2` (워커) | keycloak-0 · **postgres** · postgres PVC | Keycloak 과 DB 를 **동시에** 잃으면 |
|
||||
| **4b** | `kc-lab-1` (k3s 서버) | keycloak-1 · **Traefik** · 컨트롤 플레인 · 관측 스택 | **들어갈 문**을 잃으면 |
|
||||
|
||||
세 가지를 확인한다.
|
||||
|
||||
```
|
||||
쿠버네티스는 언제 알아채는가 → 40초 (그동안 거짓말을 한다)
|
||||
무엇을 스스로 고치는가 → 축출. 단 5분 뒤
|
||||
무엇을 못 고치는가 → PVC 가 묶인 재배치, StatefulSet 이름
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 1. 기준선 — 전원을 뽑기 전에
|
||||
|
||||
**시험군만 재는 측정은 측정이 아니다.** 뽑은 뒤에 볼 것을 뽑기 전에 **똑같은
|
||||
명령으로** 먼저 봐 둔다.
|
||||
|
||||
넓은 것부터 좁혀 간다.
|
||||
|
||||
```
|
||||
VM → 노드 → 파드 배치 → 볼륨이 어디 묶여 있나 → 외부 응답 → 관측자가 어디 있나
|
||||
```
|
||||
|
||||
## 1-1. VM 이 둘 다 살아 있나
|
||||
|
||||
**확인** — 터미널 A
|
||||
```bash
|
||||
virsh list --all
|
||||
```
|
||||
**실측** — [`01-baseline.txt`](../../evidence/a4-node-loss/01-baseline.txt)
|
||||
```
|
||||
--------------------------
|
||||
1 kc-lab-1 running
|
||||
2 kc-lab-2 running
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — 둘 다 `running`. 앞의 숫자는 **도메인 ID** 이며
|
||||
VM 을 껐다 켜면 바뀐다. 이름으로 다룬다.
|
||||
|
||||
## 1-2. 노드와 파드 배치
|
||||
|
||||
**확인** — 터미널 B
|
||||
```bash
|
||||
sudo kubectl get nodes
|
||||
sudo kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
**실측** — [`01-baseline.txt`](../../evidence/a4-node-loss/01-baseline.txt)
|
||||
```
|
||||
kc-lab-1 Ready true
|
||||
kc-lab-2 Ready <none>
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`NODE` 열.** 이 실험은 배치가 전부다.
|
||||
|
||||
**이 결과가 의미하는 것** — `kc-lab-2` 에 **keycloak-0 과 postgres 가 함께**
|
||||
있다. 그래서 4a 는 「Keycloak 한 대를 잃는 실험」이 아니라 **「Keycloak 한 대와
|
||||
DB 를 동시에 잃는 실험」**이다. 배치가 다르면 결과도 다르다 — 먼저 확인한다.
|
||||
|
||||
> `a2-probe` 는 A-2 에서 띄워 두고 안 지운 상주 파드다. 당신 환경에는 없을 수
|
||||
> 있다. 없어도 이 실험에는 지장이 없다.
|
||||
|
||||
## 1-3. ★ 볼륨이 어느 노드에 못박혀 있나
|
||||
|
||||
**이 한 줄이 뒤의 결과를 이미 결정한다.** 4a 에서 「새 파드가 왜 영원히
|
||||
Pending 인가」의 답이 여기 있다.
|
||||
|
||||
**확인** — 어떤 PVC 가 있나
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pvc
|
||||
```
|
||||
|
||||
**확인** — 그 PVC 뒤의 PV 가 어느 노드를 요구하나. 먼저 **읽는 형태**로 한 번 본다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pvc postgres-data -o jsonpath='{.spec.volumeName}' ; echo
|
||||
sudo kubectl describe pv $(sudo kubectl -n keycloak-lab get pvc postgres-data \
|
||||
-o jsonpath='{.spec.volumeName}') | grep -A6 'Node Affinity'
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
Node Affinity:
|
||||
Required Terms:
|
||||
Term 0: kubernetes.io/hostname in [kc-lab-2]
|
||||
```
|
||||
|
||||
값만 필요하면 **뽑는 형태**로 줄인다.
|
||||
```bash
|
||||
sudo kubectl get pv $(sudo kubectl -n keycloak-lab get pvc postgres-data \
|
||||
-o jsonpath='{.spec.volumeName}') \
|
||||
-o jsonpath='{.spec.nodeAffinity.required.nodeSelectorTerms[0].matchExpressions[0].values[0]}' ; echo
|
||||
```
|
||||
**실측** — [`01-baseline.txt`](../../evidence/a4-node-loss/01-baseline.txt)
|
||||
```
|
||||
=== PVC 가 어느 노드에 묶여 있는가 (재배치 가능성) ===
|
||||
persistentvolumeclaim/postgres-data → kc-lab-2
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — 오른쪽의 노드 이름. **그것이 `kc-lab-2` 라면 4a 에서
|
||||
postgres 는 갈 곳이 없다.**
|
||||
|
||||
**이 결과가 의미하는 것** — `local-path` PVC 는 **그 노드의 로컬 디렉터리**다
|
||||
(`/var/lib/rancher/k3s/storage/...`). 노드가 죽으면 볼륨도 같이 죽는다.
|
||||
스케줄러는 그 사실을 `nodeAffinity` 로 알고 있어서, 다른 노드에 파드를
|
||||
**만들지 않는다.** 결함이 아니라 이 실험대의 **조건**이다.
|
||||
|
||||
## 1-4. 밖에서 보이는 상태
|
||||
|
||||
**확인** — 터미널 C. 눈으로 한 번 볼 때는 `-I` 로 충분하다
|
||||
```bash
|
||||
curl -I --max-time 8 https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
HTTP/2 200
|
||||
content-type: application/json
|
||||
```
|
||||
|
||||
여러 번 재서 비교할 것이므로, 이제부터는 **코드만** 뽑는다.
|
||||
```bash
|
||||
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
**실측** — [`01-baseline.txt`](../../evidence/a4-node-loss/01-baseline.txt)
|
||||
```
|
||||
=== 서비스 정상 확인 ===
|
||||
https://auth.hyeonworks.com/realms/master HTTP 200
|
||||
```
|
||||
|
||||
**`--max-time` 을 반드시 준다.** 4b 에서 이 값이 없으면 curl 이 몇 분씩
|
||||
매달린다. 그리고 **타임아웃이 곧 결과**다 — 뒤에서 `000` 이 나오는 이유가
|
||||
그것이다.
|
||||
|
||||
## 1-5. 관측자가 어디 있나 — 미리 알아 둔다
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n observability get pods -o wide
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
NAME READY STATUS NODE
|
||||
grafana-845b5678cf-b6gvc 1/1 Running kc-lab-1
|
||||
prometheus-6774f94f7c-pzr2t 1/1 Running kc-lab-1
|
||||
```
|
||||
|
||||
**이 결과가 의미하는 것** — 관측 스택이 `kc-lab-1` 에 있다. **4a(`kc-lab-2`
|
||||
살해)에서는 Prometheus 가 살아남아 관측이 정확하고, 4b 에서는 관측자가 같이
|
||||
죽는다.** 그 차이를 발견 ⑧ 에서 본다. 지금 알아 두지 않으면 나중에 그래프의
|
||||
빈 구간을 「값이 0」으로 잘못 읽는다.
|
||||
|
||||
---
|
||||
|
||||
# 2. 주입 4a — 워커 노드의 전원을 뽑는다
|
||||
|
||||
여기부터 상태가 바뀐다. **되돌리는 명령을 먼저 읽어 둔다.**
|
||||
|
||||
**되돌리기** — 터미널 A
|
||||
```bash
|
||||
virsh start kc-lab-2
|
||||
```
|
||||
|
||||
## 2-1. `destroy` 와 `shutdown` 의 차이
|
||||
|
||||
| 명령 | 게스트에 무슨 일이 | 이 실험에 |
|
||||
|---|---|---|
|
||||
| `virsh shutdown` | ACPI 종료 신호 → kubelet 이 정상 종료 → 파드가 정리된다 | **쓰면 안 된다** |
|
||||
| **`virsh destroy`** | **전원 차단.** 신호 없음. 마지막 상태가 그대로 얼어붙는다 | 이것이 「노드 상실」이다 |
|
||||
|
||||
`shutdown` 을 쓰면 쿠버네티스가 **정상적인 노드 이탈**로 처리해서
|
||||
이 실험의 발견 ①·② 가 통째로 안 나온다.
|
||||
|
||||
## 2-2. 뽑는다
|
||||
|
||||
**하기** — 터미널 A
|
||||
```bash
|
||||
date '+%H:%M:%S 차단'
|
||||
virsh destroy kc-lab-2
|
||||
```
|
||||
**실측** — [`02-worker-node-killed.txt`](../../evidence/a4-node-loss/02-worker-node-killed.txt)
|
||||
```
|
||||
차단 시각: 12:07:43
|
||||
Domain 'kc-lab-2' destroyed
|
||||
```
|
||||
|
||||
**시각을 반드시 적어 둔다.** 40초·5분 같은 숫자는 **이 시각에서 뺀 값**이다.
|
||||
기준점이 없으면 뒤의 관찰은 그냥 나열이다.
|
||||
|
||||
---
|
||||
|
||||
# 3. 주입이 실제로 걸렸는지 확인한다
|
||||
|
||||
**결과를 해석하기 전에, 주입이 의도한 것만 건드렸는지 먼저 본다.**
|
||||
|
||||
A-5·A-6 에서는 「규칙을 넣었는데 카운터가 0」이 실패였다. **이 실험의 검증
|
||||
대상은 다르다.** 여기서 믿을 수 있는 것은 **하이퍼바이저**뿐이고, 쿠버네티스가
|
||||
뭐라고 하든 그것은 결과이지 검증이 아니다.
|
||||
|
||||
## 3-1. VM 이 실제로 꺼졌나 — 이것이 유일한 주입 검증이다
|
||||
|
||||
**확인** — 터미널 A
|
||||
```bash
|
||||
virsh list --all
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
1 kc-lab-1 running
|
||||
- kc-lab-2 shut off
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `shut off`. ID 가 `-` 로 바뀐 것도 같은 말이다.
|
||||
|
||||
**확인** — 정말 응답이 없나
|
||||
```bash
|
||||
ping -c 2 -W 2 192.168.122.12
|
||||
```
|
||||
**미검증** — 원 실행에는 이 확인이 없다. `0 received` 가 나오면 꺼진 것이다.
|
||||
|
||||
## 3-2. ★ 그런데 쿠버네티스는 아직 `Ready` 라고 말한다
|
||||
|
||||
**확인** — 터미널 B
|
||||
```bash
|
||||
sudo kubectl get node kc-lab-2
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
NAME STATUS ROLES AGE VERSION
|
||||
kc-lab-2 Ready <none> 12d v1.33.x+k3s1
|
||||
```
|
||||
|
||||
**여기서 「주입이 안 걸렸다」고 결론 내리면 틀린다.** 기계는 3-1 에서 확인한
|
||||
대로 꺼져 있다. 쿠버네티스가 아직 모를 뿐이다.
|
||||
|
||||
노드 상태와 사용자 경험을 **나란히** 봐야 이게 보인다. 터미널 B 에서:
|
||||
|
||||
```bash
|
||||
sudo kubectl get node kc-lab-2
|
||||
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
두 줄을 15초 간격으로 몇 번 친다. 손이 아프면 한 줄로 묶는다. **미검증**
|
||||
```bash
|
||||
while true; do
|
||||
printf '%s node=%s 외부=%s\n' "$(date +%H:%M:%S)" \
|
||||
"$(sudo kubectl get node kc-lab-2 --no-headers | awk '{print $2}')" \
|
||||
"$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 \
|
||||
https://auth.hyeonworks.com/realms/master)"
|
||||
sleep 15
|
||||
done
|
||||
```
|
||||
`Ctrl-C` 로 멈춘다.
|
||||
|
||||
**실측** — [`02-worker-node-killed.txt`](../../evidence/a4-node-loss/02-worker-node-killed.txt)
|
||||
```
|
||||
+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
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `+30초` 줄과 `+45초` 줄 사이. **노드 상태가 그때
|
||||
넘어간다.**
|
||||
|
||||
**이 결과가 의미하는 것** — kube-controller-manager 는 kubelet 의 하트비트가
|
||||
`node-monitor-grace-period`(이 실험대에서 **40초**) 동안 없어야 `NotReady` 로
|
||||
바꾼다. 그 40초 동안 **쿠버네티스는 거짓말을 한다.** 그리고 사용자는 그
|
||||
40초에도 이미 장애를 겪고 있다 — `000` 이 그 증거다.
|
||||
|
||||
> **노드 상태를 알림 근거로 삼으면 항상 늦는다.** 사용자가 먼저 안다.
|
||||
|
||||
## 3-3. 왜 처음 40초는 `503` 이 아니라 `000` 인가
|
||||
|
||||
```
|
||||
000 curl 이 응답 자체를 못 받았다 = 타임아웃 또는 연결 실패
|
||||
503 nginx·Traefik 은 살아 있고 뒤로 보낼 파드가 없다
|
||||
```
|
||||
|
||||
호스트 nginx 의 upstream 에는 **두 노드가 다 들어 있다**
|
||||
([`03-nginx`](../03-nginx/) 1절).
|
||||
|
||||
```
|
||||
upstream k3s_traefik {
|
||||
server 192.168.122.11:80;
|
||||
server 192.168.122.12:80;
|
||||
}
|
||||
```
|
||||
|
||||
죽은 쪽으로 배분된 요청은 **응답도 거절도 못 받고** `--max-time 8` 에 걸린다.
|
||||
|
||||
> **★ 여기는 이 실험이 답을 못 남긴 자리다.** 증거 파일
|
||||
> [`03-state-during-loss.txt`](../../evidence/a4-node-loss/03-state-during-loss.txt)
|
||||
> 의 마지막 절 제목이 「진입점이 처음 40초간 000 이었던 이유 — nginx upstream」
|
||||
> 인데 **그 아래가 비어 있다.** 명령이 아무것도 찍지 못했다.
|
||||
> **당신은 지금 직접 볼 수 있다** — 터미널 C 에서. **미검증**
|
||||
> ```bash
|
||||
> sudo tail -f /var/log/nginx/error.log
|
||||
> ```
|
||||
> `upstream timed out` 이 `192.168.122.12` 에 대해 찍히면 그것이 답이다.
|
||||
> nginx 에러 로그는 2048바이트에서 잘리므로, 잘려 보이면 access 로그를 본다.
|
||||
|
||||
---
|
||||
|
||||
# 4. 효과를 관찰한다 (4a)
|
||||
|
||||
## 4-1. ★ 죽은 파드가 산 파드보다 건강해 보인다
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pods -o custom-columns=\
|
||||
NAME:.metadata.name,PHASE:.status.phase,READY:.status.containerStatuses[0].ready,NODE:.spec.nodeName
|
||||
```
|
||||
**실측** — [`03-state-during-loss.txt`](../../evidence/a4-node-loss/03-state-during-loss.txt)
|
||||
```
|
||||
a2-probe Running true kc-lab-2 <none>
|
||||
keycloak-0 Running true kc-lab-2 <none>
|
||||
keycloak-1 Running false kc-lab-1 <none>
|
||||
postgres-7b474b88c8-2gf27 Running true kc-lab-2 <none>
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `keycloak-0` 은 **꺼진 기계 위에서 `ready=true`**,
|
||||
`keycloak-1` 은 **살아 있는데 `ready=false`.**
|
||||
|
||||
**이 결과가 의미하는 것**
|
||||
|
||||
| 파드 | 왜 |
|
||||
|---|---|
|
||||
| `keycloak-0` | kubelet 이 없어 **상태를 갱신할 수 없다.** 마지막으로 보고한 값이 얼어 있다 |
|
||||
| `keycloak-1` | 살아서 **정직하게 보고한다** — DB 가 없으니 readiness 실패 |
|
||||
|
||||
> **파드 상태는 「지금 어떤가」가 아니라 「마지막으로 그렇게 들었다」이다.**
|
||||
> 노드가 죽으면 그 노드 파드의 상태는 **화석**이 된다.
|
||||
|
||||
이유를 이벤트로 확인한다.
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get events --sort-by=.lastTimestamp | tail -20
|
||||
```
|
||||
**실측** — 같은 파일
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
**`keycloak-1` 의 실패가 두 종류다.** 처음에는 프로브 자체가 타임아웃되고
|
||||
(`context deadline exceeded`), 나중에는 `503` 을 받는다. Keycloak 이 DB 없음을
|
||||
스스로 판단해 답할 수 있게 된 것이다. **같은 「Unhealthy」라도 층이 다르다.**
|
||||
|
||||
**`Age` 를 반드시 같이 본다.** 노드를 뽑은 것은 `3m15s` 전인데 맨 위 줄은
|
||||
`10m` 짜리다 — **주입보다 앞선 사건**이고, 앞 실험의 잔재다. 이벤트 목록은
|
||||
시간대가 섞여 있으므로 **`Age` 로 먼저 걸러야** 내가 만든 일을 고를 수 있다.
|
||||
|
||||
그리고 주입 이후 `keycloak-0` 에 붙은 이벤트는 `NodeNotReady` **하나뿐**이다.
|
||||
그것은 컨트롤러가 쓴 것이지 kubelet 이 쓴 것이 아니다. **kubelet 이 없으니
|
||||
그 파드에 대해 말해 줄 주체가 없다** — 4-1 의 `ready=true` 가 화석인 이유다.
|
||||
|
||||
## 4-2. Prometheus 는 정확했다
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=up'
|
||||
```
|
||||
|
||||
한 줄짜리 JSON 이 통째로 나온다. **처음 한 번은 그대로 본다.** 어떤 job 과
|
||||
라벨이 있는지 알아야 다음부터 무엇으로 걸러야 할지 안다. 읽기 좋게 자르려면
|
||||
(`jq` 는 이 실험대에 없다) **미검증**
|
||||
```bash
|
||||
sudo kubectl -n observability exec deploy/prometheus -- \
|
||||
wget -qO- 'localhost:9090/api/v1/query?query=up' \
|
||||
| tr ',' '\n' | grep -E '"job":|"pod":|"node":|^"[0-9]'
|
||||
```
|
||||
|
||||
**실측** — [`03-state-during-loss.txt`](../../evidence/a4-node-loss/03-state-during-loss.txt)
|
||||
```
|
||||
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 이 두 줄인
|
||||
것**도 본다 — 노드마다 하나씩이라 하나는 1, 하나는 0 이다.
|
||||
|
||||
**이 결과가 의미하는 것** — `up` 은 **Prometheus 가 그 대상을 긁는 데
|
||||
성공했는가**다. 대상이 사라졌으니 실패했고, 그 0 은 **사실**이다.
|
||||
|
||||
**A-2 와 정반대다.** A-2(DB 상실)에서는 `up=1` 인데 서비스가 죽어 있었다.
|
||||
|
||||
| | `up` 이 잡는가 |
|
||||
|---|---|
|
||||
| **대상이 사라짐** (노드 상실) | **잡는다** |
|
||||
| **대상이 살아서 못 씀** (DB 상실) | **못 잡는다** |
|
||||
|
||||
Grafana 에서 같은 것을 그림으로 본다 —
|
||||
[`a4-up-dropped-per-node.png`](../../evidence/a4-node-loss/a4-up-dropped-per-node.png).
|
||||
|
||||
## 4-3. 쿠버네티스가 노드에 무엇을 붙였나
|
||||
|
||||
**확인** — 읽는 형태
|
||||
```bash
|
||||
sudo kubectl describe node kc-lab-2 | grep -A3 Taints
|
||||
```
|
||||
값만 필요하면
|
||||
```bash
|
||||
sudo kubectl get node kc-lab-2 -o jsonpath='{.spec.taints}' ; echo
|
||||
```
|
||||
**실측** — [`03-state-during-loss.txt`](../../evidence/a4-node-loss/03-state-during-loss.txt)
|
||||
```
|
||||
=== 노드 taint — 쿠버네티스가 붙인 것 ===
|
||||
node.kubernetes.io/unreachable=:NoSchedule
|
||||
node.kubernetes.io/unreachable=:NoExecute
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — 두 종류가 붙어 있다.
|
||||
|
||||
| taint | 뜻 |
|
||||
|---|---|
|
||||
| `NoSchedule` | **새 파드를 여기 보내지 마라** |
|
||||
| `NoExecute` | **이미 있는 파드도 쫓아내라** |
|
||||
|
||||
`NoExecute` 가 붙었는데 왜 아무 일도 안 일어나는가 — 다음 절.
|
||||
|
||||
## 4-4. 축출은 왜 5분 뒤인가
|
||||
|
||||
**확인** — 파드에 붙어 있는 관용을 본다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab describe pod keycloak-1 | grep -A4 Tolerations
|
||||
```
|
||||
**실측** — [`04-eviction-timing.txt`](../../evidence/a4-node-loss/04-eviction-timing.txt)
|
||||
```
|
||||
=== NoExecute taint 의 tolerationSeconds — 언제 축출되는가 ===
|
||||
node.kubernetes.io/not-ready NoExecute tolerationSeconds=300
|
||||
node.kubernetes.io/unreachable NoExecute tolerationSeconds=300
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `tolerationSeconds=300`. **당신이 쓴 적 없는 값**이다.
|
||||
쿠버네티스가 모든 파드에 자동으로 붙인다.
|
||||
|
||||
```
|
||||
기계 정지
|
||||
│
|
||||
│ 40초 node-monitor-grace-period → 노드 NotReady
|
||||
│
|
||||
│ +300초 tolerationSeconds (NoExecute) → 파드 축출 시작
|
||||
▼
|
||||
총 약 5분 40초 동안 쿠버네티스는 아무것도 하지 않는다
|
||||
```
|
||||
|
||||
## 4-5. 그 5분을 실제로 기다린다
|
||||
|
||||
**확인** — 30초 간격으로 본다. 손으로 치기 싫으면 `watch`
|
||||
```bash
|
||||
watch -n 30 'sudo kubectl -n keycloak-lab get pods -o wide'
|
||||
```
|
||||
**실측** — [`04-eviction-timing.txt`](../../evidence/a4-node-loss/04-eviction-timing.txt)
|
||||
```
|
||||
+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 파드**(`...-9cmsv`)가 생기고 **`Pending`** 이다
|
||||
|
||||
**이 결과가 의미하는 것** — 축출이 시작됐다. 그런데 `Terminating` 이 안 끝나고,
|
||||
새 파드는 뜨지 못한다. 두 문제는 원인이 다르다 — 4-6 과 4-7.
|
||||
|
||||
## 4-6. 새 파드는 왜 영원히 `Pending` 인가
|
||||
|
||||
**확인** — 파드에게 직접 물어본다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pods --field-selector=status.phase=Pending
|
||||
sudo kubectl -n keycloak-lab describe pod postgres-7b474b88c8-9cmsv | grep -A6 Events
|
||||
```
|
||||
이름은 매번 다르므로 위 `get` 으로 먼저 확인하고 옮겨 적는다.
|
||||
|
||||
**실측** — [`05-recovery.txt`](../../evidence/a4-node-loss/05-recovery.txt)
|
||||
```
|
||||
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` 뒤에 이유가 노드 수만큼
|
||||
나열된다.** 이 줄 하나에 두 노드의 사연이 다 들어 있다.
|
||||
|
||||
```
|
||||
kc-lab-2 → had untolerated taint(s) (죽은 노드)
|
||||
kc-lab-1 → didn't match PersistentVolume's node affinity
|
||||
```
|
||||
|
||||
**이 결과가 의미하는 것** — **1-3 에서 이미 알고 있던 것이 그대로 벌어졌다.**
|
||||
볼륨이 `kc-lab-2` 에 못박혀 있어서 살아 있는 노드로 못 간다. 죽은 노드에는
|
||||
taint 때문에 못 간다. **갈 곳이 없다.**
|
||||
|
||||
> 이건 결함이 아니라 **조건**이다. 이 실험대는 그걸 알고 `local-path` 를
|
||||
> 골랐다. 운영이라면 네트워크 스토리지나 DB 복제가 이 자리를 메워야 한다.
|
||||
> 노드가 영영 안 돌아오면 남는 길은 **백업 복원(D-1)** 뿐이다.
|
||||
|
||||
## 4-7. StatefulSet 은 대체 파드를 만들지 않는다
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get statefulset keycloak
|
||||
sudo kubectl -n keycloak-lab get pods | grep keycloak
|
||||
```
|
||||
**실측** — [`05-recovery.txt`](../../evidence/a4-node-loss/05-recovery.txt)
|
||||
```
|
||||
keycloak 2 <none> 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` 을 못 만든다 |
|
||||
|
||||
`Terminating` 이 안 끝나는 사슬은 이렇다.
|
||||
|
||||
```
|
||||
파드 삭제 요청
|
||||
└─ kubelet 이 컨테이너를 멈추고 "지웠다"고 보고해야 끝난다
|
||||
└─ kubelet 이 없다 → 보고가 없다 → 영원히 Terminating
|
||||
```
|
||||
|
||||
**Deployment 였다면 즉시 새 파드를 만든다.** 이름이 아무래도 되기 때문이다
|
||||
(postgres 가 실제로 그랬다 — 4-5 에서 새 이름의 파드가 생겼다. 다만 갈 곳이
|
||||
없었을 뿐이다). **StatefulSet 의 「안정된 이름」이라는 이득의 반대편 비용**이
|
||||
여기다.
|
||||
|
||||
> **강제로 진행시키는 명령이 있지만, 이 가이드에서는 치지 않는다.**
|
||||
> ```
|
||||
> kubectl -n keycloak-lab delete pod keycloak-0 --grace-period=0 --force
|
||||
> ```
|
||||
> 이것은 **컨테이너가 실제로 죽었는지 모른 채 API 에서 지우는 것**이다.
|
||||
> 노드가 사실은 살아 있고 네트워크만 끊긴 것이라면 **같은 이름의 파드 둘이
|
||||
> 동시에 존재**하게 된다 — 그게 split brain 이고, 이 실험대에서는 5절의
|
||||
> `virsh start` 가 훨씬 안전하고 빠르다.
|
||||
|
||||
---
|
||||
|
||||
# 5. 복구 (4a)
|
||||
|
||||
## 5-1. 전원을 다시 넣는다
|
||||
|
||||
**하기** — 터미널 A
|
||||
```bash
|
||||
date '+%H:%M:%S 재기동'
|
||||
virsh start kc-lab-2
|
||||
```
|
||||
**실측** — [`05-recovery.txt`](../../evidence/a4-node-loss/05-recovery.txt)
|
||||
```
|
||||
재기동 시각: 12:16:31
|
||||
Domain 'kc-lab-2' started
|
||||
```
|
||||
|
||||
## 5-2. 얼마나 걸리나
|
||||
|
||||
**확인** — 30초 간격
|
||||
```bash
|
||||
sudo kubectl get nodes
|
||||
sudo kubectl -n keycloak-lab get pods
|
||||
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
**실측** — 같은 파일
|
||||
```
|
||||
+30초 node=Ready | Running 파드 3 개 | 외부 HTTP 503
|
||||
+60초 node=Ready | Running 파드 3 개 | 외부 HTTP 200
|
||||
→ 서비스 복귀
|
||||
```
|
||||
|
||||
**60초. 사람 개입 없이 전부 제자리로 돌아왔다.** `Terminating` 이던 파드도,
|
||||
`Pending` 이던 파드도 kubelet 이 돌아오자 정리됐다.
|
||||
|
||||
> **이 60초는 MTTR 이 아니다.** `virsh start` 를 친 **뒤**의 시간이다.
|
||||
> 실제 장애 구간은 **12:07:43(차단) → 12:17:31(서비스 복귀) ≈ 10분**이고,
|
||||
> 그 대부분은 사람이 관찰하고 결정하는 데 쓴 시간이다. **현실의 MTTR 도
|
||||
> 대개 그렇다.**
|
||||
>
|
||||
> 그리고 본문의 `40초`와 `5분`은 **쿠버네티스 기본값을 인용한 것**이며,
|
||||
> 관측된 전이 시점(+45초, +270초)이 그 값과 모순되지 않는다는 것까지가
|
||||
> 이 실험이 말할 수 있는 범위다. 값 자체를 측정한 것은 아니다.
|
||||
|
||||
## 5-3. 4b 로 넘어가기 전 확인표
|
||||
|
||||
| 항목 | 명령 | 돌아왔을 때 |
|
||||
|---|---|---|
|
||||
| VM | `virsh list --all` | 둘 다 `running` |
|
||||
| 노드 | `sudo kubectl get nodes` | 둘 다 `Ready` |
|
||||
| 파드 | `sudo 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` |
|
||||
|
||||
**실측** — [`06-control-plane-inventory.txt`](../../evidence/a4-node-loss/06-control-plane-inventory.txt)
|
||||
```
|
||||
=== 복구 확인 ===
|
||||
keycloak-0 1/1 Running 0 68s
|
||||
keycloak-1 1/1 Running 0 144m
|
||||
postgres-7b474b88c8-9cmsv 1/1 Running 0 4m20s
|
||||
```
|
||||
|
||||
**`postgres` 의 이름이 바뀌어 있다** (`-2gf27` → `-9cmsv`). 4-5 에서 생겼다가
|
||||
`Pending` 이던 그 파드가 노드가 살아나자 그대로 뜬 것이다. **`keycloak-0` 은
|
||||
이름이 그대로** — StatefulSet 이라 그렇다. 두 컨트롤러의 차이가 이름에 남는다.
|
||||
|
||||
여기까지 안 돌아왔으면 **4b 로 넘어가지 않는다.** 두 고장이 겹치면 무엇이
|
||||
원인인지 못 가린다.
|
||||
|
||||
---
|
||||
|
||||
# 6. 4b — 이번엔 컨트롤 플레인을 뽑는다
|
||||
|
||||
## 6-1. 먼저 인벤토리 — 그게 곧 영향 범위다
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl get pods -A -o wide --field-selector spec.nodeName=kc-lab-1
|
||||
```
|
||||
**실측** — [`06-control-plane-inventory.txt`](../../evidence/a4-node-loss/06-control-plane-inventory.txt)
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `traefik`. **진입점이 여기 있다.**
|
||||
|
||||
**확인** — 진입점이 몇 개인가
|
||||
```bash
|
||||
sudo kubectl -n kube-system get deploy traefik
|
||||
```
|
||||
**실측** — 같은 파일
|
||||
```
|
||||
traefik 1 1
|
||||
```
|
||||
|
||||
**이 결과가 의미하는 것** — **`replicas=1`. 진입점이 단일 장애점이다.**
|
||||
이 노드를 뽑으면 클러스터로 들어갈 문이 사라진다. 4a 와 결과가 다른 이유가
|
||||
여기서 이미 예측된다.
|
||||
|
||||
## 6-2. 뽑는다
|
||||
|
||||
**되돌리기** — 터미널 A
|
||||
```bash
|
||||
virsh start kc-lab-1
|
||||
```
|
||||
|
||||
**하기** — 터미널 A
|
||||
```bash
|
||||
date '+%H:%M:%S 차단'
|
||||
virsh destroy kc-lab-1
|
||||
```
|
||||
**실측** — [`07-control-plane-loss.txt`](../../evidence/a4-node-loss/07-control-plane-loss.txt)
|
||||
```
|
||||
차단 시각: 12:18:08
|
||||
Domain 'kc-lab-1' destroyed
|
||||
```
|
||||
|
||||
**터미널 B 가 여기서 죽는다.** SSH 세션이 그대로 끊긴다. 놀랄 일이 아니다.
|
||||
|
||||
## 6-3. 주입 검증 — `kubectl` 이 죽은 것이 곧 증거다
|
||||
|
||||
**확인** — 터미널 A 나 C 에서
|
||||
```bash
|
||||
sudo kubectl get nodes
|
||||
```
|
||||
**실측** — 같은 파일
|
||||
```
|
||||
kubectl: Unable to connect to the server: dial tcp
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `Unable to connect to the server`. API 서버가
|
||||
`kc-lab-1:6443` 에 있었으므로 **당연한 결과**다.
|
||||
|
||||
**이 결과가 의미하는 것** — 4a 에서는 「쿠버네티스가 뭐라고 하는가」를 물을 수
|
||||
있었다. **여기서는 물어볼 상대 자체가 없다.** 이 실험의 관찰 도구가 통째로
|
||||
바뀐다.
|
||||
|
||||
## 6-4. 밖에서는 어떻게 보이나
|
||||
|
||||
**확인** — 터미널 C. 20초 간격으로 두 주소를 본다
|
||||
```bash
|
||||
curl -s -o /dev/null -w 'auth=%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
|
||||
curl -s -o /dev/null -w 'grafana=%{http_code}\n' --max-time 8 https://grafana.hyeonworks.com/
|
||||
```
|
||||
**실측** — [`07-control-plane-loss.txt`](../../evidence/a4-node-loss/07-control-plane-loss.txt)
|
||||
```
|
||||
+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` 한 줄.** 나머지는 전부
|
||||
`000` 인데 여기만 다르다.
|
||||
|
||||
**이 결과가 의미하는 것** — **`000` 과 `503`/`502` 는 서로 다른 층의 고장을
|
||||
가리킨다.**
|
||||
|
||||
| 코드 | 어디까지 살아 있는가 |
|
||||
|---|---|
|
||||
| **`503`** (4a) | nginx·Traefik 은 살아 있고 **뒤에 보낼 파드가 없다** |
|
||||
| **`502`** (4b, 한 번) | nginx 가 **연결 실패를 제때 판정해** 자기 힘으로 502 를 만들었다 |
|
||||
| **`000`** (4b, 대부분) | nginx 가 죽은 주소를 기다리다 **우리 `--max-time 8` 이 먼저 끝났다** |
|
||||
|
||||
`502` 가 한 번이라도 찍혔다는 것이 **nginx 는 살아 있었다**는 증거다.
|
||||
같은 고장인데 코드가 흔들리는 이유는 **타임아웃 경주**다.
|
||||
|
||||
## 6-5. ★ 그런데 워크로드는 살아 있다
|
||||
|
||||
`kubectl` 이 없으니 **노드의 컨테이너 런타임에 직접 묻는다.**
|
||||
|
||||
**확인** — 터미널 A 에서 살아남은 노드로
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo crictl ps --name keycloak'
|
||||
```
|
||||
**실측** — [`07-control-plane-loss.txt`](../../evidence/a4-node-loss/07-control-plane-loss.txt)
|
||||
```
|
||||
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 서버가 없는데도 컨테이너는 돌고 있다.**
|
||||
|
||||
**이 결과가 의미하는 것**
|
||||
|
||||
```
|
||||
죽은 것: API 서버 · 스케줄러 · coredns · Traefik · Prometheus · Grafana
|
||||
산 것: keycloak-0 · postgres · containerd
|
||||
문제: 들어갈 문(Traefik)이 없다
|
||||
```
|
||||
|
||||
> **컨트롤 플레인 상실 ≠ 워크로드 상실.**
|
||||
> 이미 떠 있는 것은 계속 돈다. **새로 뜨거나 옮기거나 고치는 것이 안 될 뿐.**
|
||||
|
||||
전체 목록도 본다.
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo crictl ps'
|
||||
```
|
||||
`crictl` 이 소켓을 못 찾으면 k3s 의 것을 직접 준다. **미검증**
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock ps'
|
||||
```
|
||||
|
||||
## 6-6. 관측자가 같이 죽으면 0 이 아니라 구멍이 남는다
|
||||
|
||||
**지금은 확인할 수 없다.** Prometheus 도 Grafana 도 `kc-lab-1` 에 있었다.
|
||||
**그것이 이 발견이다.**
|
||||
|
||||
```
|
||||
대상이 죽음 → up = 0 → "언제 죽었는지" 알 수 있다
|
||||
관측자가 죽음 → 데이터 없음 → "그때 무슨 일이 있었는지" 모른다
|
||||
```
|
||||
|
||||
복구 뒤에 Grafana 에서 `up` 그래프를 다시 열어 **12:18–12:23 구간이 0 이
|
||||
아니라 빈칸**인 것을 확인한다. 6-8 에서 한다.
|
||||
|
||||
## 6-7. 복구
|
||||
|
||||
**하기** — 터미널 A
|
||||
```bash
|
||||
date '+%H:%M:%S 재기동'
|
||||
virsh start kc-lab-1
|
||||
```
|
||||
**실측** — [`08-control-plane-recovery.txt`](../../evidence/a4-node-loss/08-control-plane-recovery.txt)
|
||||
```
|
||||
재기동: 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초)
|
||||
```
|
||||
|
||||
**여기서도 60초.** `+30초` 의 `502` 는 **nginx 가 먼저 살아나고 Traefik 이
|
||||
아직 안 뜬** 중간 상태다. 4b 내내 보던 `000` 과 층이 다르다.
|
||||
|
||||
## 6-8. 복구 후에 확인할 것
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pods
|
||||
```
|
||||
**실측** — 같은 파일
|
||||
```
|
||||
keycloak-0 1/1 Running 0 7m57s
|
||||
keycloak-1 1/1 Running 1 (<invalid> ago) 151m
|
||||
postgres-7b474b88c8-9cmsv 1/1 Running 0 11m
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — 세 가지가 한 줄에 있다.
|
||||
|
||||
- `keycloak-1` 의 `RESTARTS` 가 **1** — `kc-lab-1` 위에 있었으니 당연하다
|
||||
- `AGE` 가 `151m` 인데 재시작은 방금 — **AGE 는 파드가 만들어진 시각**이지
|
||||
컨테이너가 시작한 시각이 아니다
|
||||
- **`(<invalid> ago)`** — 재시작 시각이 API 서버 시계보다 미래로 보일 때 나온다.
|
||||
**원인은 이 실험이 확정하지 않았다.** 잠시 뒤 다시 치면 정상 값으로 바뀐다
|
||||
|
||||
**확인** — Grafana. 6-6 에서 예고한 구멍
|
||||
```
|
||||
브라우저로 Grafana 를 열어 up{job="keycloak"} 그래프를 12:15–12:30 으로 본다
|
||||
```
|
||||
**실측** — [`a4-up-dropped-per-node.png`](../../evidence/a4-node-loss/a4-up-dropped-per-node.png)
|
||||
그림에서 12:18–12:23 은 **선이 0 으로 내려간 것이 아니라 아예 끊겨 있다.**
|
||||
|
||||
**Grafana 로그인이 풀려 있다.** Grafana 데이터가 `emptyDir` 이라 파드
|
||||
재시작에 사라진다. Prometheus 는 PVC 라 지표가 남았다 — 다만 관측자가 죽어
|
||||
있던 구간의 데이터는 애초에 수집되지 않았다. **의도한 설계대로 동작했고,
|
||||
그 설계의 한계도 함께 드러났다.**
|
||||
|
||||
Prometheus 를 `port-forward` 로 보고 있었다면 **다시 연결해야 한다** (실측
|
||||
기록의 마지막 줄이 그것이다).
|
||||
|
||||
## 6-9. 원상복구 확인표
|
||||
|
||||
| 항목 | 명령 | 돌아왔을 때 |
|
||||
|---|---|---|
|
||||
| VM | `virsh list --all` | 둘 다 `running` |
|
||||
| 노드 | `sudo kubectl get nodes` | 둘 다 `Ready` |
|
||||
| 파드 | `sudo kubectl -n keycloak-lab get pods -o wide` | 전부 `1/1 Running` |
|
||||
| 진입점 | `sudo kubectl -n kube-system get deploy traefik` | `1/1` |
|
||||
| Service | `sudo kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak` | ready 주소 **둘** |
|
||||
| 클러스터 뷰 | `sudo 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` |
|
||||
|
||||
> **이 실험이 재지 않은 것** — 노드가 **영영 안 돌아오는** 경우는 재지 않았다.
|
||||
> `local-path` PVC 가 그 노드와 함께 사라진 상태에서의 복구는 **D-1(백업·복원)**
|
||||
> 의 주제다.
|
||||
|
||||
---
|
||||
|
||||
# 막히면
|
||||
|
||||
전부 이 실험대가 **실제로 겪은** 증상이다. 지어낸 것은 없다.
|
||||
|
||||
| 증상 | 원인 | 확인 |
|
||||
|---|---|---|
|
||||
| `virsh` 가 도메인을 못 찾는다 | `qemu:///session` 을 보고 있다 | `virsh uri` — `system` 이어야 한다 |
|
||||
| VM 을 껐는데 노드가 `Ready` | **정상.** `node-monitor-grace-period` 40초 | `virsh list --all` 로 전원을 먼저 본다 — 3-1 |
|
||||
| 5분이 지나도 축출이 안 온다 | 40초 + `tolerationSeconds=300` = **5분 40초** | `describe pod \| grep -A4 Tolerations` |
|
||||
| 새 파드가 계속 `Pending` | **PVC 가 죽은 노드에 못박혀 있다** | `describe pod` 의 `FailedScheduling` — 4-6 |
|
||||
| `keycloak-0` 이 30분째 `Terminating` | StatefulSet + kubelet 없음. **정상이다** | `get statefulset` 의 `CURRENT` — 4-7 |
|
||||
| `--force` 로 지우고 싶다 | 노드가 살아 있으면 **중복 실행**이 된다 | 치지 말고 `virsh start` — 4-7 |
|
||||
| `kubectl` 이 전혀 안 된다 (4b) | **API 서버가 죽은 노드에 있었다.** 정상 | `ssh kc-lab-2 'sudo crictl ps'` — 6-5 |
|
||||
| `crictl` 이 소켓을 못 찾는다 | k3s 는 자기 containerd 소켓을 쓴다 | `--runtime-endpoint unix:///run/k3s/containerd/containerd.sock` |
|
||||
| `503` 을 기대했는데 `000` | 층이 다르다. nginx 가 죽은 주소를 기다린다 | `--max-time` 을 늘려 보면 `502` 가 나온다 — 6-4 |
|
||||
| `curl` 이 몇 분씩 안 끝난다 | `--max-time` 을 안 줬다 | 모든 외부 확인에 `--max-time 8` |
|
||||
| 그래프의 그 구간이 0 으로 보인다 | **0 이 아니라 데이터 없음이다** | 점 사이가 이어져 있는지 본다 — 6-6 |
|
||||
| Grafana 로그인이 풀렸다 | 데이터가 `emptyDir` | 재시작마다 그렇다. PVC 로 바꾸면 남는다 |
|
||||
| Prometheus 가 갑자기 안 보인다 | `port-forward` 가 끊겼다 | 다시 연다 |
|
||||
| `RESTARTS` 가 `1 (<invalid> ago)` | 재시작 직후에 나온다. **원인 미확정** | 잠시 뒤 다시 친다 — 6-8 |
|
||||
| 4b 결과가 4a 와 섞인다 | 4a 복구를 확인하지 않고 넘어갔다 | 5-3 확인표를 통과한 뒤 시작 |
|
||||
|
||||
---
|
||||
|
||||
# 이 실험이 남기는 구성 숙제
|
||||
|
||||
관찰만 하고 끝내면 아깝다. **두 가지는 지금 고칠 수 있다.**
|
||||
|
||||
| 발견 | 고치는 방향 |
|
||||
|---|---|
|
||||
| Traefik `replicas=1` 이라 진입점이 단일 장애점 | `replicas=2` 로 늘리거나 DaemonSet 으로 |
|
||||
| Grafana 가 `emptyDir` 이라 재시작마다 세션이 사라짐 | PVC 를 붙인다 |
|
||||
| 관측 스택이 실험 대상 노드에 함께 있음 | 노드가 둘뿐이라 완전히는 못 피한다. **아는 것이 먼저** |
|
||||
|
||||
---
|
||||
|
||||
# 다음
|
||||
|
||||
| 실험 | A-4 가 남긴 질문 |
|
||||
|---|---|
|
||||
| [A-5](../../experiment-a5-asymmetric-partition.md) 비대칭 파티션 | 여기서는 노드가 **완전히** 사라졌다. **부분 단절은 더 고약하다** |
|
||||
| [D-1](../../experiment-d1-backup-restore.md) 백업·복구 | **PVC 가 노드에 묶여 있다.** 노드가 영영 안 돌아오면 백업이 유일한 길 |
|
||||
| [A-6](../../experiment-a6-latency-injection.md) 지연 주입 | 여기서는 `up=0` 이 정확했다. **느려짐은 `up` 이 못 잡는다** |
|
||||
| 전부 | **주입 검증의 기준을 먼저 정한다.** 여기서는 쿠버네티스가 아니라 하이퍼바이저가 기준이었다 |
|
||||
Reference in New Issue
Block a user