기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만 있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문 종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다. SSOT 결함 둘을 고쳤다. - kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이 「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽 `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다. - virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데 원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다. 기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은 둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더 든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다). 계약을 셋 고쳤다. - kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/** 28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는 것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온 것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 커밋도 28개가 어긋났다). - virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고 재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다. - kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고 넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다. style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져 (실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고, engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고 있었다(Question 기록에서 11.94 → 3.86). verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9.7 KiB
kind, slug, title, topic, topicName, project, status, basisVersion, sourceRevision, source, assets, evidence
| kind | slug | title | topic | topicName | project | status | basisVersion | sourceRevision | source | assets | evidence | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CONCEPT | readiness-hides-the-broken-node | readiness 가 깨진 노드를 시야에서 먼저 치운다 | losing-a-node-or-the-store | PostgreSQL 을 내리고 노드 전원을 뽑았을 때 | keycloak-session-store | 게시 전 | k3s · Kubernetes readiness probe · Keycloak 26.7.0 /health | cdac9b8178391311d8eca1ebc6cac15bb62d79af |
|
|
|
readiness 가 깨진 노드를 시야에서 먼저 치운다
readiness 프로브가 실패한 파드는 Service 엔드포인트에서 빠져, 밖에서 재면 장애가 200 으로 보인다. A-1 에서 실제로 그랬다. A-4 는 방향이 반대로, kubelet 이 멈춰 죽은 파드가 READY 로 남았다.
관계
- 노드를 잃는 두 가지 — 저장소가 같이 죽는 것과 들어갈 길이 없는 것 거기서 죽은 노드의 파드가 READY 로 남고 살아 있는 노드의 파드가 READY 에서 빠졌다. 그 역전은 kubelet 이 상태를 더 보고하지 못한 데서 나왔다.
- 200 밀리초를 넣었더니 응답이 22.2 초가 됐다 그 실험에서도 readiness 프로브가 타임아웃으로 실패했는데, 엔드포인트 목록까지는 읽지 않았다.
- up 지표는 살아 있지만 쓸모없는 상태를 보지 못한다
readiness 와
up은 같은 파드를 보면서 서로 다른 조건으로 1 과 0 을 낸다.
본문
프로브 결과가 엔드포인트 목록까지 가는 경로
파드 하나가 트래픽을 받을지는 네 곳을 거쳐 정해진다. kubelet 이 그 노드에 있는 파드의 readiness 프로브를 주기적으로 호출하고, 결과를 파드 상태의 READY(ready, 트래픽을 받을 준비가 됐는지) 값으로 API 서버에 보고한다. 엔드포인트 컨트롤러가 그 값을 읽어 Service 의 주소 목록을 ready 와 notReady 로 나누게 되고, 프록시는 ready 쪽 주소로만 요청을 넘긴다. 이 실험대에서는 그 앞에 traefik 과 호스트 nginx 가 더 있어서, 외부에서 들어온 로그인 요청은 ready 로 남은 파드에만 도착한다.
Keycloak 26.7.0 은 관리 포트 9000 에 헬스와 메트릭을 연다. 프로브가 부르는 주소는 파드 이벤트에 그대로 찍히는데, A-4 의 이벤트 목록에는 http://10.42.1.67:9000/health/ready 로 남아 있다. A-1 에서 NetworkPolicy 로 JGroups 전송 포트 7800 을 막을 때 8080 과 9000 을 허용 목록에 남긴 이유가 이 경로에 있다. 9000 을 빠뜨리면 프로브가 응답을 못 받아 kubelet 이 파드를 죽이므로, 분단을 재려던 실험이 죽은 Keycloak 을 재는 실험으로 바뀐다.
A-1 에서 그 경로가 한 번에 보인다
A-1 은 NetworkPolicy 의 허용 목록에서 7800 을 빼서 두 노드를 갈라놓은 실험이다. NetworkPolicy 는 허용목록이라 「deny 7800」 같은 규칙을 쓸 수 없어서, 8080 과 9000 만 열고 7800 을 목록에서 빼는 방식으로 막았다. 갈라진 뒤 파드 상태와 Service 엔드포인트와 외부 로그인을 같은 시점에 읽었다.
| 어디를 읽었나 | 무엇이 찍혀 있었나 |
|---|---|
| 파드 READY | keycloak-0 false · keycloak-1 true |
| Service 엔드포인트 | ready 주소 10.42.0.35 · notReady 주소 10.42.1.67 |
| 외부 진입점 | /realms/master 200 · 토큰 발급 200 |
분단된 keycloak-0 이 프로브에 실패해 READY 에서 빠졌고, 엔드포인트 목록에서도 notReady 쪽으로 옮겨졌다. 남은 ready 주소가 하나뿐이어도 Service 는 그쪽으로 요청을 넘기므로 외부 로그인은 200 이 된다. 밖에서만 보면 이 실험은 「아무 일도 없음」으로 끝난다.
Keycloak 은 클러스터 분단을 readiness 로 신고한다. 그때 /health/ready 가 돌려준 JSON 은 데이터베이스 연결 검사를 UP 으로 두고 클러스터 검사만 DOWN 으로 적었다. liveness 로 신고했다면 kubelet 이 파드를 재시작했을 텐데 분단은 재시작해도 안 나아지므로, 격리 쪽인 readiness 가 맞는 신호다.
이 결과는 노드 둘 가운데 하나만 분단된 조건에서 나왔다. 양쪽이 함께 프로브에 실패하면 어떻게 되는지는 A-1 이 남기지 못했는데, 그 값을 찍으려던 증거 파일의 명령이 JSON 파싱 오류로 끝나 출력 자리가 비어 있다.
관측 지점을 셋으로 늘린 이유
처음에는 밖에서만 쟀다. curl 로 외부 진입점을 찍고 상태 코드를 세는 방식이었는데, A-1 에서 그 방식이 무너졌다. 그래서 관측 지점을 셋으로 늘렸다.
| 어디서 재나 | 무엇을 보는가 |
|---|---|
외부 curl |
사용자가 겪는 것 |
| Prometheus 지표 | vendor_cluster_size · vendor_jgroups_* · agroal_* |
| PostgreSQL 직접 조회 | 실제로 무엇이 저장됐는가 |
세 지점이 서로 다른 층을 보므로, 하나만 두면 그 지점이 보지 못하는 상태를 아무도 읽지 못한다.
Prometheus 의 up 도 readiness 와 다른 조건으로 값을 낸다. A-2 에서 데이터베이스를 멈춰 서비스가 503 을 내는 동안 up{pod=keycloak-0} 과 up{pod=keycloak-1} 은 둘 다 1 이었다. 프로세스가 살아 있고 /metrics 가 응답하기만 하면 1 이 되므로, 「살아 있지만 쓸모없는」 상태는 이 지표에 나타나지 않는다.
갱신이 멈추면 같은 값이 반대로 읽힌다
A-4 는 워커 노드 kc-lab-2 의 전원을 끊은 실험이고, 여기서는 READY 값이 A-1 과 반대 방향으로 어긋났다.
| 어느 파드를 읽었나 | 그때 무엇이 찍혀 있었나 |
|---|---|
| 전원이 끊긴 kc-lab-2 의 keycloak-0 | Running · READY true · up 0 |
| 살아 있는 kc-lab-1 의 keycloak-1 | Running · READY false · up 1 |
파드의 READY 값은 그 노드의 kubelet 이 보고하는데, 노드가 사라지면 보고할 주체가 없어져 API 서버가 마지막으로 받은 값을 계속 돌려준다. 그래서 죽은 파드가 산 파드보다 건강해 보인다. Prometheus 는 그 파드를 직접 스크레이프하므로 같은 시점에 up 을 0 으로 적었고, 두 신호가 서로 반대를 가리켰다.
살아 있는 쪽의 keycloak-1 이 READY 에서 빠진 이유는 이벤트에 남아 있다. /health/ready 가 503 을 돌려줬고, kc-lab-2 에 함께 있던 PostgreSQL 이 노드와 같이 사라져 세션을 읽을 곳이 없었기 때문이다.
A-1 과 A-4 를 같은 문장으로 묶지 않는다. A-1 에서는 프로브가 제때 실패해서 그 파드가 엔드포인트 목록에서 빠졌고, 그 목록을 직접 읽어 확인했다. A-4 에서는 프로브 결과가 더 갱신되지 않았고, 그때 엔드포인트가 어떻게 됐는지는 읽지 않았다 — A-4 의 관찰은 파드 상태와 이벤트와 up 셋뿐이다. 앞쪽은 readiness 가 동작해서 생긴 착시이고, 뒤쪽은 readiness 가 멈춰서 생긴 착시인데, 뒤쪽에서 엔드포인트까지 그대로였는지는 이 실험이 답하지 않는다.
A-6 은 프로브 실패까지만 남겼다
A-6 은 200 밀리초 네트워크 지연을 주입한 실험이다. 동시 부하 20 건을 keycloak-1 에 넣은 직후 이벤트 목록에 readiness 프로브가 context deadline exceeded 로 실패한 줄이 있고, 프로브 주소는 http://10.42.0.42:9000/health/ready 다.
거기서 멈춘다. 그 시점에 Service 엔드포인트를 다시 읽어 keycloak-1 이 notReady 로 옮겨졌는지 확인한 값이 없고, 재시작 횟수는 지연 구간 앞뒤로 같다. 부하 직후 파일과 지연 해제 뒤 파일 모두 두 파드를 1/1 Running 으로 적고 keycloak-1 의 RESTARTS 를 1 로 적는데, 괄호 안의 경과 시간만 51 분 전에서 52 분 전으로 넘어간다.
그래서 A-6 에서 읽은 것은 프로브가 타임아웃으로 실패했다는 사실 하나다. 느린 노드가 실제로 엔드포인트에서 빠졌는지, 그다음에 파드가 새로 죽었는지는 아직 읽지 않은 값이다. 확인하려면 지연을 주입하고 있는 동안 Service 엔드포인트 목록과 파드 재시작 횟수를 같은 시점에 읽어야 한다.
이 동작이 막지 않는 것
readiness 는 트래픽을 받을 파드를 고르는 장치이고, 장애를 알리는 장치가 아니다. 프로브가 정확히 실패할수록 외부 상태 코드는 깨끗해지므로, 밖에서 코드만 세는 관측은 정상과 「한쪽이 빠진 채로 버티는 중」을 구분하지 못한다.
엔드포인트를 ready 와 notReady 로 나누는 것은 쿠버네티스의 동작이고, 이 실험대가 읽은 것은 그 동작이 실험마다 어디까지 진행됐는지다. 그 진행이 세 실험에서 서로 달랐다.
| 어느 실험인가 | 어디까지 읽었나 |
|---|---|
| A-1 · JGroups 전송 차단 | 파드 READY · 엔드포인트 목록 · 외부 응답 |
| A-4 · 워커 노드 상실 | 파드 READY · up · 프로브 실패 이벤트 |
| A-6 · 지연 주입 | 프로브 실패 이벤트 |