기록 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>
45 KiB
id, kind, slug, title, topic, topicName, project, status, studio, pinnedVersions, source, sourceRevision
| id | kind | slug | title | topic | topicName | project | status | studio | pinnedVersions | source | sourceRevision | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 95e29500-b535-4246-86db-d569ed814904 | SETUP | reproduce-a1-jgroups-transport-block | 7800 을 막고 디스커버리와 트랜스포트를 갈라 끊는다 | session-custody-across-nodes | Keycloak 두 노드가 같은 세션을 읽는 경로 | keycloak-session-store | 게시 전 | https://hyeonworks.com/studio/documents/95e29500-b535-4246-86db-d569ed814904/edit |
|
|
cdac9b8178391311d8eca1ebc6cac15bb62d79af |
7800 을 막고 디스커버리와 트랜스포트를 갈라 끊는다
NetworkPolicy 로 8080 과 9000 만 열어 JGroups 트랜스포트인 TCP 7800 만 끊는 절차다. 그러고도 25분 동안 클러스터가 안 깨지는 것을 보고 conntrack 표에서 까닭을 찾은 뒤, 정책이 걸린 채로 파드를 지워 분단을 만든다. 전 구간 약 30분.
관계
- 클러스터는 형성됐는데 세션을 나르는 것은 데이터베이스였다 이 절차가 낸 결론을 담은 기록이다. 여기에는 치는 순서만 있다.
- 주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다
정책을 걸었는데
vendor_cluster_size가 25분 내내 2 였던 것이 그 아홉 건 중 하나다. - readiness 가 깨진 노드를 시야에서 먼저 치운다
분단된 노드가 스스로 Service 에서 빠져 정문이
200을 유지한 까닭을 다룬다. - 산문으로 적힌 측정 장치를 실행 가능하게 고쳤더니 한 건이 깨졌다 이 편의 원래 실행이 빈 문자열을 「변화」로 읽고 빠져나온 판정 조건을 담고 있다.
- 세션을 공유하는 것이 Infinispan 인지 PostgreSQL 인지 손으로 가른다
이 편의 대조군 값(
200·400)이 거기서 나온다. 먼저 해 두지 않으면 차단 후의200이 무엇과 다른지 알 수 없다. - 한 방향만 끊어 보고 raw PREROUTING 까지 내려간다
같은 7800 을
iptables로 한 방향만 끊는 편이고, conntrack 으로 연결 방향을 먼저 보는 순서를 그쪽에서 다시 쓴다.
본문
읽기 전에 — 어디서 치는가
kubectl 은 [kc-lab-1] 에서 친다. conntrack 은 노드 자체를 건드리는 명령이라 게스트 셸이 필요하고 두 노드 모두에서 봐야 한다 — 이쪽 노드는 그대로 치고 반대 노드는 ssh kc-lab-2 로 붙어서 친다. kubectl 에는 sudo 를 붙이지 않는다. root 홈에 ~/.kube/config 가 없어서 localhost:8080 으로 붙으려다 connection refused 로 끝난다. 코드블록마다 어느 셸인지 붙여 두었다.
주입에 쓰는 매니페스트 경로 deploy/lab/k8s/a1-block-jgroups-transport.yaml 는 저장소 상대경로다. 체크아웃을 kc-lab-1 의 어디에 뒀는지는 원 가이드에 없고 거기로 옮기는 명령도 없으므로(unknown), 이 상대경로가 그대로 통하는 디렉터리에서 시작한다. 다른 디렉터리에서 치면 cat 도 kubectl apply 도 파일을 못 찾고 끝난다.
터미널은 둘을 연다. 하나는 임시 curl 파드용, 하나는 관찰용이다. 그래서 [kc-lab-1] 라벨이 붙은 블록이 [탐침 파드] 블록 사이에 끼어 있으면 관찰용 터미널에서 친다 — 파드 셸을 나가라는 뜻이 아니다. 나가라고 할 때는 exit 를 블록으로 따로 적는다.
| 무엇 | 값 |
|---|---|
| 네임스페이스 | keycloak-lab · 관측 스택은 observability |
| 막는 포트 | 7800(트랜스포트). 57800(FD_SOCK2 = bind_port + 50000)도 함께 막힌다 |
| 주입 수단 | NetworkPolicy a1-block-jgroups-transport — 허용 목록이라 8080·9000 만 연다 |
| 탐침 파드 | kc-probe — curlimages/curl:8.11.1, --rm -it, --restart=Never |
| 분단 판정 | jgroups_ping 의 coord = t 가 두 줄 |
| 걸리는 시간 | 전 구간 약 30분 |
| 도구 | jq 가 이 실험대에 없다. Prometheus 출력은 tr 과 grep 으로 자른다 |
이 실험이 가르는 것
A-0 은 세션이 Infinispan 복제가 아니라 PostgreSQL 로 공유된다고 측정했다. Keycloak 24 이전 자료는 세션이 7800 으로 복제된다고 말한다. 통념은 7800 을 막으면 세션 공유가 깨진다고 예측하고 A-0 모델은 안 깨진다고 예측하므로, 7800 만 끊어 보면 둘 중 어느 쪽이 틀렸는지 판정된다.
끊을 때 두 가지를 갈라야 한다.
디스커버리 노드가 서로를 어떻게 찾는가 → PostgreSQL 의 JGROUPS_PING 테이블
트랜스포트 실제로 어떻게 말하는가 → TCP 7800
트랜스포트만 막으면 DB 에는 둘 다 등록되어 있는데 메시지는 안 가는 상태가 된다. 단일 노드에서는 만들 수 없는 고장이고, 이 실험대가 VM 두 대인 까닭이 여기 있다.
절차를 끝까지 밟으면 NetworkPolicy 를 걸었는데도 클러스터가 안 깨지는 상태, coord = t 가 두 줄인 split brain, 분단인데도 교차 노드 refresh 가 200 인 것, 로그아웃했는데 반대편이 200 을 주는 것, 분단된 노드가 스스로 Service 에서 빠지는 것, 90초 만에 자동으로 다시 붙는 것을 자기 화면에서 보게 된다.
전제와 되돌리기
05-keycloak·06-observability가 끝나 있다.- 두 Keycloak 파드가 서로 다른 노드에 있어야 한다. 단일 노드에서는 이 고장을 만들 수 없다.
kc-lab-2에ssh로 붙을 수 있어야 한다. conntrack 은 두 노드 모두에서 봐야 한다.
이건 상태를 부수는 실험이다. Keycloak 클러스터를 실제로 분단시키고 파드를 재시작한다. 실험대에서만 한다. 중간에 그만두려면 아래 한 줄이면 된다.
kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
주입 전에 같은 명령으로 먼저 본다
차단 후에 볼 것을 차단 전에 똑같은 명령으로 먼저 봐 둔다. 시험군만 재는 측정은 측정이 아니다.
노드 → 파드 → 정책 → 클러스터 뷰(로그) → 디스커버리(DB) → 지표(Prometheus) → 대조군 시험
1. 두 파드가 서로 다른 노드에 있는가
무엇을 보는가 — 파드 둘의 상태와 배치, 그리고 뒤에서 쓸 파드 주소.
kubectl get nodes
kubectl -n keycloak-lab get pods -o wide
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
echo "$K0 $K1"
어디를 보나 — READY 가 둘 다 1/1, RESTARTS 가 0, NODE 열이 서로 다른지. 실측은 10.42.1.43 10.42.0.35 였다(observed).
이 값이 뜻하는 것 — RESTARTS 는 뒤에서 다시 센다. 이 값이 오르면 주입이 엉뚱한 곳을 건드렸다.
2. 기존 정책이 없는가
무엇을 보는가 — 네임스페이스에 이미 걸린 NetworkPolicy.
kubectl -n keycloak-lab get networkpolicy
어디를 보나 — 실측은 이렇다(observed, 01-baseline-cluster.txt).
No resources found in keycloak-lab namespace.
이 값이 뜻하는 것 — NetworkPolicy 는 합집합으로 허용되므로 두 개가 겹치면 무엇이 열려 있는지 한눈에 안 보인다.
3. 양쪽 클러스터 뷰가 같은 줄인가
무엇을 보는가 — 두 노드가 같은 멤버 목록을 찍고 있는지.
kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
어디를 보나 — 두 줄이 완전히 같은지. 실측은 이렇다(observed).
keycloak-0: [keycloak-1-48749(v=16.0.12)|5] (2) [keycloak-1-48749(v=16.0.12), keycloak-0-30843(v=16.0.12)]
keycloak-1: [keycloak-1-48749(v=16.0.12)|5] (2) [keycloak-1-48749(v=16.0.12), keycloak-0-30843(v=16.0.12)]
이 값이 뜻하는 것 — keycloak-0-30843 의 뒤 숫자는 JGroups 가 붙인 것이고 파드가 재시작되면 바뀐다. 나중에 keycloak-0-26403 이 나오면 같은 파드의 새 인스턴스다.
4. 디스커버리 테이블에 둘 다 등록돼 있는가
무엇을 보는가 — JGROUPS_PING 의 세 열.
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select name, ip, coord from jgroups_ping order by name"
어디를 보나 — coord 열의 t 개수. 실측은 이렇다(observed).
name | ip | coord
------------------+-----------------+-------
keycloak-0-30843 | 10.42.1.43:7800 | f
keycloak-1-48749 | 10.42.0.35:7800 | t
(2 rows)
이 값이 뜻하는 것 — 여기서 셋이 서로 다른 것을 말한다. 로그는 그때 그렇게 보였다는 기록이고, 테이블은 지금 등록되어 있다는 것이며, 지표는 지금 그 노드가 그렇게 안다는 것이다. A-1 에서 이 셋이 갈린다.
5. 두 노드가 아는 멤버 수를 지표로 본다
무엇을 보는가 — 각 노드가 스스로 세는 클러스터 크기.
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
라벨을 보고 나면 읽기 좋게 자른다. 아래 두 줄은 가이드가 미검증으로 표시했고(unknown), 둘째 줄은 jq 가 깔려 있는 환경용이라 이 실험대에서는 쓸 수 없다.
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
| jq -r '.data.result[] | "\(.metric.pod) \(.metric.node) \(.value[1])"'
어디를 보나 — 결과가 두 줄이고 값이 둘 다 2 인지. 한 노드만 보면 분단을 놓친다. 분단되면 한쪽만 1 이 되는 경로가 있다.
JGroups 카운터도 지금 0 인 것을 봐 둔다.
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_merge3_get_num_merge_events'
실측은 이렇다(observed, 02-control-before-block.txt).
vendor_jgroups_merge3_get_num_merge_events 0.0 (양쪽 노드)
vendor_jgroups_fd_sock2_get_num_suspected_members 0.0 (양쪽 노드)
6. 대조군 시험을 차단 전에 한 번 돌린다
무엇을 보는가 — 정상 클러스터에서 교차 노드 refresh 와 로그아웃 전파가 어떤 코드를 주는지.
임시 파드를 띄우고 그 안에서 A-0 과 같은 순서로 로그인·refresh·로그아웃을 친다. 임시 파드인 까닭은 셋이다. Keycloak 이미지에 curl 이 없어 Keycloak 파드 안에서는 못 치고, 토큰을 단계 사이로 넘겨야 하니 한 셸 안에서 다 끝내야 하며, Service 로 보내면 어느 노드가 처리했는지 알 수 없다. 이 실험의 질문 자체가 「어느 노드인가」라서 파드 주소로 직접 친다.
kubectl -n keycloak-lab run kc-probe --rm -it --restart=Never \
--image=curlimages/curl:8.11.1 \
--env="K0=$K0" --env="K1=$K1" \
--env="PW=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)" \
--command -- sh
비밀번호는 명령 치환으로 넘기므로 값이 터미널에도 셸 히스토리에도 남지 않는다. 파드 안에서는 echo ${#PW} 로 길이만 본다.
파드 안 셸에서 먼저 keycloak-0 에 로그인한다. 뒤의 refresh 가 쓰는 $TOK 와 $RT 가 여기서 생기므로 이 블록을 건너뛰면 다음 명령이 빈 문자열을 보낸다.
TOK=/realms/master/protocol/openid-connect/token
R=$(curl -s -X POST "http://$K0:8080$TOK" \
-d grant_type=password -d client_id=admin-cli \
-d username=admin -d "password=$PW")
RT=$(echo "$R" | sed -n 's/.*"refresh_token":"\([^"]*\)".*/\1/p')
AT=$(echo "$R" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
echo "refresh=${#RT}자 access=${#AT}자"
refresh=1187자 access=2043자 같은 모양이 나온다. 길이가 0자 면 로그인이 실패한 것이고 echo "$R" 로 에러 본문을 본다.
같은 응답에 access token 도 들어 있지만 탐침으로 쓰지 않는다. access token 은 60초짜리고 그동안은 서버에 안 물어보므로, 노드가 세션 저장소를 실제로 뒤져야 답할 수 있는 refresh 를 쓴다.
R=$(curl -s -w '\n%{http_code}' -X POST "http://$K1:8080$TOK" \
-d grant_type=refresh_token -d client_id=admin-cli -d "refresh_token=$RT")
echo "$R" | tail -1
RT=$(echo "$R" | sed -n 's/.*"refresh_token":"\([^"]*\)".*/\1/p')
마지막 줄이 RT 를 다시 담는 까닭은 refresh token 이 회전하기 때문이다. 갱신할 때마다 새 것이 나오므로, 옛 것을 계속 쓰면 뒤에 나오는 400 이 무효화 때문인지 재사용 때문인지 갈리지 않는다.
실측은 이렇다(observed, 02-control-before-block.txt).
sid tAWs2gCPr6SOcD4jDR9-_CzB
keycloak-1 에서 refresh: 200
이 값이 뜻하는 것 — 이 200 이 대조군이다. 차단 후에도 200 이면 원래 되던 것이 그대로 된 것이고, 차단 후 400 이면 내가 깨뜨렸다는 뜻이다.
로그아웃 전파도 대조군을 잡는다.
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
"http://$K1:8080/realms/master/protocol/openid-connect/logout" \
-d client_id=admin-cli -d "refresh_token=$RT"
curl -s -w '\n%{http_code}\n' -X POST "http://$K0:8080$TOK" \
-d grant_type=refresh_token -d client_id=admin-cli -d "refresh_token=$RT"
정상 클러스터에서는 204 다음에 이 두 줄이 나온다.
{"error":"invalid_grant","error_description":"Session not active"}
400
이 400 은 A-0 에서 측정한 값이고, A-1 의 대조군 기록에는 refresh 200 만 있고 로그아웃 단계가 없다. 그래서 차단 전에 직접 재 두는 편이 낫다.
대조군을 다 잡았으면 파드 셸에서 나온다. 나가지 않으면 kc-probe 가 그대로 살아 있어서, 뒤에서 같은 이름으로 다시 띄울 때 AlreadyExists 로 거절된다.
exit
주입
1. NetworkPolicy 로 7800 만 뺀다
목적 — 8080 과 9000 은 열어 둔 채 7800 으로 오는 새 연결만 막는다.
cat deploy/lab/k8s/a1-block-jgroups-transport.yaml
파일은 앞 31행이 영어 주석이고 그 아래가 매니페스트다. 주석 31행을 뺀 본문 전문은 이렇다. 매니페스트 안에 남은 # 주석도 파일에 적힌 영어 그대로다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: a1-block-jgroups-transport
namespace: keycloak-lab
spec:
podSelector:
matchLabels:
app: keycloak
policyTypes: [Ingress]
ingress:
- ports:
- { port: 8080, protocol: TCP } # HTTP — must stay open
- { port: 9000, protocol: TCP } # health + metrics — must stay open
# 7800 is absent on purpose. That is the whole experiment.
① 에서 이 매니페스트를 못 찾으면 파일을 직접 만든다. 같은 경로를 에디터로 열어 위 열다섯 줄을 그대로 넣고 저장한다. printf 나 cat <<EOF 로 찍어 내지 않는다. 주입 검증에서 describe 로 대조할 것이 이 줄들이고, 덧붙이는 형태로 쓰면 같은 절차를 두 번 밟았을 때 파일에 같은 내용이 두 벌 쌓인다. 파일을 여는 명령은 원 가이드에 없다(unknown).
매니페스트 안에 metadata.namespace 가 keycloak-lab 으로 적혀 있다. ② 의 apply 에는 -n 이 없으니 네임스페이스는 이 줄이 정한다. 손으로 옮겨 적다 이 줄을 빠뜨리면 정책이 default 네임스페이스에 걸린다. 거기에는 app=keycloak 인 파드가 없으니 정책은 만들어지는데 아무 파드도 안 잡힌다.
kubectl apply -f deploy/lab/k8s/a1-block-jgroups-transport.yaml
date '+%H:%M:%S 적용'
예상 결과 — 실측은 이렇다(observed, 03-block-applied.txt).
networkpolicy.networking.k8s.io/a1-block-jgroups-transport created
적용 시각: 11:38:08
왜 필요한가 — NetworkPolicy 는 방화벽이 아니라 허용 목록이다. 「7800 을 거부」라고 쓸 방법이 없고, 파드가 policyTypes: [Ingress] 를 가진 정책에 선택되는 순간 모든 인바운드가 거부되며 규칙에 적힌 것만 통과한다. 7800 은 빠뜨림으로써 막힌다. 그 구조 때문에 두 허용 규칙이 결정적이다 — 8080 을 빼면 Traefik 과 상대 노드의 REST 호출이 전부 끊기고, 9000 을 빼면 readiness 프로브가 실패해 kubelet 이 파드를 죽여 엉뚱한 이유로 클러스터가 깨진다. 덤으로 57800 도 막힌다. FD_SOCK2(장애 감지 채널)는 bind_port + 50000 을 쓰는데, 손으로 「7800 거부」 규칙을 쓰면 이 포트를 빠뜨리기 쉽고 허용 목록 방식은 8080·9000 외 전부 거부이므로 자동으로 같이 막힌다.
문제가 생기면 — 적용 시각을 반드시 적어 둔다. 이 실험은 시각이 겹친 것을 인과로 잘못 읽었다가 나중에 정정했다.
주입 검증
결과를 해석하기 전에 주입이 의도한 것을 정확히 했는지 본다. 「아무 일도 없었다」는 「영향이 없다」와 구별되지 않아서, 걸렸는지를 결과와 따로 확인하지 않으면 둘이 같은 화면으로 보인다. 원 가이드는 이 순서를 이 실험이 남긴 가장 큰 교훈으로 적고, 뒤의 실험 전부에 같은 확인을 요구한다. 이 편에서는 그 확인이 「안 걸렸다」를 먼저 알려 준다.
1. 정책이 어떤 파드를 잡았는가
kubectl -n keycloak-lab get networkpolicy
kubectl -n keycloak-lab describe networkpolicy a1-block-jgroups-transport
To Port 목록에 7800 이 없는 것, 그리고 PodSelector 가 app=keycloak 인 것을 본다. 오타로 아무 파드도 안 잡히면 정책은 걸렸는데 아무 일도 일어나지 않는다.
kubectl -n keycloak-lab get pods -o wide | grep keycloak
실측은 keycloak-0 ready=true restarts=0, keycloak-1 ready=true restarts=0 이다(observed). RESTARTS 가 여전히 0 이면 9000 을 제대로 열어 뒀다는 뜻이다.
2. 열어 둔 포트는 살아 있고 막은 포트는 죽었는가
kubectl -n keycloak-lab run kc-probe --rm -it --restart=Never \
--image=curlimages/curl:8.11.1 --env="K0=$K0" --env="K1=$K1" --command -- sh
curl -s -o /dev/null -w '9000 %{http_code}\n' --max-time 5 "http://$K0:9000/health/ready"
curl -s -o /dev/null -w '8080 %{http_code}\n' --max-time 5 "http://$K0:8080/realms/master"
curl -s -o /dev/null -w '7800 %{http_code}\n' --max-time 5 "http://$K0:7800/" ; echo "exit=$?"
실측은 이렇다(observed).
9000 도달: 10.42.1.43:9000 health=200 / 10.42.0.35:9000 health=200
8080 도달: 10.42.1.43:8080 root=200 / 10.42.0.35:8080 root=200
7800: curl exit=7 (연결 실패)
curl 종료코드로 읽는다 — 7 은 연결 자체가 안 됨, 28 은 --max-time 초과로 SYN 이 조용히 버려지고 있음, 0 은 닿았다는 뜻이라 정책이 안 걸렸다.
포트 셋을 다 봤으면 또 나온다. 이 파드도 kc-probe 라 살려 두면 본 시험에서 같은 이름이 겹친다.
exit
3. 그런데 클러스터가 안 깨졌다
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
실측은 이렇다(observed, 07-cluster-size.txt).
=== vendor_cluster_size — 지난 25분 (차단 11:38:08) ===
keycloak-0: 11:38=2 11:39=2 11:40=2 11:41=2 11:42=2 11:43=2 11:44=2 11:45=2
keycloak-1: 11:38=2 11:39=2 11:40=2 11:41=2 11:42=2 11:43=2 11:44=2 11:45=2
여기서 실험 실패라고 결론 내리면 틀린다. 파드 안 소켓을 본다. Keycloak 이미지에는 ss 도 없으므로 /proc 을 직접 읽는다.
kubectl -n keycloak-lab exec keycloak-0 -- cat /proc/net/tcp6 | grep 1E78
실측은 이렇다(observed).
=== /proc/net/tcp6 · 7800 = 0x1E78 ===
keycloak-0: ...2B012A0A:1E78 ...23002A0A:9C57 01 ← 01 = ESTABLISHED
keycloak-1: ...23002A0A:9C57 ...2B012A0A:1E78 01
(10.42.0.35:40023 → 10.42.1.43:7800)
포트는 16진수다. 7800 = 0x1E78 이고 세 번째 열 01 이 TCP 상태이며 01 이 ESTABLISHED 다. 기존 연결이 멀쩡히 살아 있다.
까닭은 conntrack 이다.
패킷 도착
│
├─▶ [ conntrack: ESTABLISHED/RELATED 이면 ACCEPT ] ← 여기서 통과해버린다
│
└─▶ [ NetworkPolicy 규칙 평가 ] ← 여기까지 오지 않는다
4. conntrack 을 두 노드 모두에서 본다
sudo conntrack -L 2>/dev/null | grep 7800
반대편 노드는 붙어서 친다. 아래 세 줄 형태는 이 실험대에서 치지 않았다(unknown) — 원래 실행은 ssh kc-lab-2 'sudo conntrack -L 2>/dev/null | grep 7800' 한 줄로 쳤고, 한 줄에 원격 접속과 원격 셸의 인용을 겹쳐 놓는 대신 행동 하나를 명령 하나로 나눴다.
ssh kc-lab-2
sudo conntrack -L 2>/dev/null | grep 7800
exit
폴더 README 는 게스트 셸이 필요한 것을 nft·tc·systemctl 처럼 노드 자체를 건드리는 명령뿐이라고 적는데, conntrack 이 그 경우다.
실측은 이렇다(observed, 05-conntrack-problem.txt).
--- kc-lab-1 ---
tcp 6 86398 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=40023 dport=7800 src=10.42.1.43 dst=10.42.0.35 sport=7800 dport=40023 [ASSURED] mark=0 use=1
tcp 6 79982 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=50477 dport=57800 src=10.42.1.43 dst=10.42.0.35 sport=57800 dport=50477 [ASSURED] mark=0 use=1
--- kc-lab-2 ---
tcp 6 86398 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=40023 dport=7800 ...
tcp 6 33 SYN_SENT src=10.42.1.58 dst=10.42.0.35 sport=34824 dport=7800 [UNREPLIED] ...
2>/dev/null 은 conntrack 이 stderr 로 찍는 「N flow entries have been shown」 요약을 지우려는 것이고, 처음에는 빼고 쳐서 그 줄도 한번 본다. 상태 열을 읽는다 — ESTABLISHED 는 양방향 통신이 성립해 규칙 평가를 건너뛰고, [ASSURED] 는 표가 꽉 차도 안 지워지는 오래된 연결이며, SYN_SENT [UNREPLIED] 가 정책이 동작하고 있다는 것을 보여 준다. dport=57800 도 ESTABLISHED 로 살아 있다. NetworkPolicy 는 이미 붙어 있는 연결을 떼어내지 못하므로, 보안 사고 대응으로 「지금 당장 이 통신을 끊어라」에 NetworkPolicy 를 걸면 새 연결만 막히고 진행 중인 연결은 계속된다. 같은 7800 을 한 방향만 끊는 A-5 가 NetworkPolicy 대신 iptables 의 raw PREROUTING 으로 간 까닭도 여기서 나왔다 — 그 체인은 conntrack 조회보다 먼저 평가된다.
5. conntrack 항목을 튜플 그대로 지운다
목적 — 규칙 평가를 건너뛰게 만들던 기존 연결 기록을 표에서 없앤다.
네 값은 바로 위 4단계의 conntrack -L 출력에서 읽는다. 한 줄에 src= dst= sport= dport= 가 두 벌 나오는데 앞의 한 벌이 원 방향, 뒤의 한 벌이 응답 방향이고 두 벌을 각각 한 번씩 지운다. 포트는 ephemeral 이라 재시작할 때마다 바뀐다.
sudo conntrack -D -p tcp -s <src> -d <dst> --sport <sp> --dport <dp>
이 실험대에서는 7800 두 방향과 57800 한 방향, 이렇게 세 번 쳤다(observed). 그대로 옮겨 치면 자기 실험대에는 없는 튜플이라 0건이 나온다.
sudo conntrack -D -p tcp -s 10.42.0.35 -d 10.42.1.43 --sport 40023 --dport 7800
sudo conntrack -D -p tcp -s 10.42.1.43 -d 10.42.0.35 --sport 7800 --dport 40023
sudo conntrack -D -p tcp -s 10.42.0.35 -d 10.42.1.43 --sport 50477 --dport 57800
kc-lab-2 에서도 같은 일을 한다. 서버 쪽 노드에는 튜플이 뒤집혀 기록되어 있으므로, 그쪽에 붙어 그쪽 -L 출력을 보고 옮긴다.
ssh kc-lab-2
sudo conntrack -L 2>/dev/null | grep 7800
sudo conntrack -D -p tcp -s <src> -d <dst> --sport <sp> --dport <dp>
exit
예상 결과 — 삭제 건수가 나온다.
왜 필요한가 — 위 소켓 출력이 보여 준 대로 ESTABLISHED 인 연결은 규칙 평가 앞에서 통과한다. 그 기록을 지워야 다음 패킷이 정책을 만난다.
문제가 생기면 — 0 flow entries have been deleted 면 튜플이 틀린 것이고, --dport 7800 만 주면 0건이 나온다. 원래 실행에서 실제로 그렇게 나왔다.
여기에 정직하게 적어 둘 것이 있다(observed). 원래 실행에서 conntrack 을 지운 뒤에도 vendor_cluster_size 는 계속 2 였다. 해설 문서는 처음에 「conntrack 삭제 → 3분 뒤 분단」이라고 썼다가 증거를 다시 보고 정정했고, 실제 하락은 파드가 재시작된 4초 뒤에 일어났다. 이 단계만으로 분단이 만들어지는지는 이 실험이 판정하지 못했다.
6. 정책이 걸린 채로 파드를 지워 분단을 확정한다
목적 — 새로 뜨는 노드가 7800 으로 JOIN 을 보내다 실패하게 만들어 분단을 확실히 만든다.
date '+%H:%M:%S 재시작'
kubectl -n keycloak-lab delete pod keycloak-0
kubectl -n keycloak-lab get pods -o wide | grep keycloak
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
echo "$K0"
예상 결과 — 실측은 이렇다(observed, 08-restart-forced-partition.txt).
재시작 시각: 11:46:07
pod "keycloak-0" deleted from keycloak-lab namespace
keycloak-0 false 10.42.1.67 2026-09-04T02:44:23Z
왜 필요한가 — StatefulSet 이 같은 이름으로 곧바로 다시 만들지만 주소는 바뀐다. 10.42.1.43 에서 10.42.1.67 로 바뀌었고, 새 주소를 다시 잡지 않으면 뒤의 모든 curl 이 아무 데도 안 닿는다.
echo "$K0" 가 빈 줄이면 파드에 아직 주소가 붙지 않은 것이다. 이 파드는 분단 때문에 Ready 가 되지 않으므로 wait --for=condition=Ready 로 기다리면 안 되고, 첫 줄의 get pods -o wide 에 IP 가 찍힐 때까지 ② 를 다시 친다. 빈 값을 그대로 두고 넘어가면 http://:8080 으로 요청이 나가고 그 실패를 분단으로 읽는다.
문제가 생기면 — 파드가 Pending 에서 안 넘어가면 노드 상태부터 본다.
관찰
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
실측은 이렇다(observed).
keycloak-0: 11:45:27=1 11:45:57=1 11:46:27=1 11:46:57=1 11:47:27=1
keycloak-1: ... 11:43:57=2 11:44:27=1 11:44:57=1 ... 11:47:27=1
양쪽 다 1 이다. 서로를 멤버로 안 세고 있고, 로그가 까닭을 말한다.
kubectl -n keycloak-lab logs keycloak-0 | grep -E "GMS|ISPN000094" | tail -20
실측은 이렇다(observed).
GMS: JOIN(keycloak-0-26403) sent to keycloak-1-48749 timed out ← 10회
GMS: too many JOIN attempts (10): becoming singleton ← 포기
ISPN000094: new cluster view [keycloak-0-26403|0] (1) [keycloak-0-26403]
새로 뜬 keycloak-0 은 DB 에서 keycloak-1 을 찾았고 주소도 안다. 그런데 JOIN 메시지가 7800 으로 안 간다. 디스커버리는 살아 있고 트랜스포트만 죽었다.
split brain 은 DB 한 줄로 확인된다.
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select name, ip, coord from jgroups_ping order by name"
실측은 이렇다(observed, 06-partition-observed.txt).
name | ip | coord
------------------+-----------------+-------
keycloak-0-26403 | 10.42.1.67:7800 | t ← 코디네이터
keycloak-1-48749 | 10.42.0.35:7800 | t ← 코디네이터
coord = t 가 둘이다. 서로를 못 보니까 각자 자기가 대장이라고 생각한다. 분단을 확인하는 가장 짧은 명령이 이 한 줄이다.
분단된 노드는 스스로 트래픽에서 빠진다.
kubectl -n keycloak-lab get pods -o custom-columns=\
NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount \
| grep keycloak
실측은 keycloak-0 false 0, keycloak-1 true 0 이다(observed, 11-service-impact.txt). 까닭은 헬스 본문에 있다.
kubectl -n keycloak-lab describe pod keycloak-0 | grep -A5 Conditions
실측은 이렇다(observed).
{ "status": "DOWN",
"checks": [
{ "name": "Keycloak cluster health check", "status": "DOWN",
"data": { "Failing since": "2026-09-04 02:45:14,251" } },
{ "name": "Keycloak database connections async health check", "status": "UP" } ] }
Keycloak 은 클러스터 분단을 readiness 로 신고한다. DB 는 UP 인데 클러스터가 DOWN 이고, 쿠버네티스가 그 신고를 받아 처리한다.
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
실측은 ready 주소: [10.42.0.35], notReady : [10.42.1.67] 다(observed). kubectl get endpoints 는 v1.33 부터 deprecated 라 경고가 뜨므로 쓰지 않는다.
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
실측은 정문이 HTTP 200, 토큰 발급도 HTTP 200 이다(observed). 분단된 노드가 스스로 로드밸런서에서 빠졌고 서비스는 계속됐다. liveness 였다면 재시작을 반복했을 텐데 재시작해도 안 나아지는 문제이므로 readiness 로 격리하는 쪽이 맞는 신호다. 다만 비대칭이라서 살았다 — keycloak-1 은 멤버가 하나 줄어든 정상적인 사건이라 Ready 를 유지했고, keycloak-0 은 합류 자체를 못 해 DOWN 이 됐다. 양쪽이 동시에 DOWN 이 되는 경로가 있다면 전면 장애이고, 그것이 A-5 의 주제다.
본 시험은 Service 를 쓰면 안 된다. keycloak-0 이 NotReady 라 Service 로 보내면 전부 keycloak-1 로 간다. 새 주소로 임시 파드를 다시 띄우고 파드 주소로 직접 친다.
keycloak-0 을 지웠으므로 $K0 가 낡았다. 두 주소를 다시 잡고 그 값으로 파드를 띄운다. $PW 도 여기서 다시 넣는다 — 앞의 포트 시험 파드에는 안 넣었다.
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
kubectl -n keycloak-lab run kc-probe --rm -it --restart=Never \
--image=curlimages/curl:8.11.1 \
--env="K0=$K0" --env="K1=$K1" \
--env="PW=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)" \
--command -- sh
TOK=/realms/master/protocol/openid-connect/token
# [1] keycloak-0 에서 로그인
R=$(curl -s -X POST "http://$K0:8080$TOK" \
-d grant_type=password -d client_id=admin-cli \
-d username=admin -d "password=$PW")
RT=$(echo "$R" | sed -n 's/.*"refresh_token":"\([^"]*\)".*/\1/p')
AT=$(echo "$R" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
echo "$AT" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null; echo # sid 를 적어 둔다
# [2] keycloak-1 에서 refresh
R=$(curl -s -w '\n%{http_code}' -X POST "http://$K1:8080$TOK" \
-d grant_type=refresh_token -d client_id=admin-cli -d "refresh_token=$RT")
echo "$R" | tail -1
RT=$(echo "$R" | sed -n 's/.*"refresh_token":"\([^"]*\)".*/\1/p')
# [3] keycloak-1 에서 로그아웃
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
"http://$K1:8080/realms/master/protocol/openid-connect/logout" \
-d client_id=admin-cli -d "refresh_token=$RT"
# [4] keycloak-0 에서 재갱신 시도
curl -s -w '\n%{http_code}\n' -X POST "http://$K0:8080$TOK" \
-d grant_type=refresh_token -d client_id=admin-cli -d "refresh_token=$RT"
네 단계를 다 쳤으면 파드 셸에서 나온다. 뒤의 명령은 전부 [kc-lab-1] 이고, 나가지 않으면 kubectl 도 psql 도 없는 curlimages/curl 안에서 치게 된다.
exit
실측은 이렇다(observed, 09-cross-node-under-partition.txt).
[1] keycloak-0 로그인 sid=nShl5TaBrZnKStDqaspjgmJB
[2] keycloak-1 에서 refresh HTTP 200 ← 예측대로
[3] keycloak-1 에서 로그아웃 HTTP 204
[4] keycloak-0 에서 재갱신 시도 HTTP 200 ← 400 이어야 했다
[2] 에서 세션 공유는 예측이 맞았다. 클러스터가 갈라졌는데도 한쪽에서 만든 세션을 반대쪽이 갱신했으니 세션은 7800 으로 다니지 않는다. [4] 에서 로그아웃 전파는 예측이 틀렸다. 대조군에서 400 이던 곳이 200 이다.
[4] 의 200 이 로그아웃이 아예 안 됐다는 뜻인지 확인한다. sid 는 [1] 에서 JWT payload 를 풀어 화면에 찍고 적어 둔 그 값이다. 로그인할 때마다 새로 생기므로 자기 실행의 값을 넣는다.
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select user_session_id, offline_flag, last_session_refresh
from offline_user_session where user_session_id='{{SID}}'"
이 실험대의 값은 nShl5TaBrZnKStDqaspjgmJB 였다(observed).
실측은 이렇다(observed, 10-logout-not-propagated.txt).
user_session_id | offline_flag | last_session_refresh
-----------------+--------------+----------------------
(0 rows) ← DB 행은 삭제되었다
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_statistics_approximate_entries_unique{cache="sessions"}'
실측은 keycloak-1 kc-lab-1 = 0, keycloak-0 kc-lab-2 = 1 이다(observed). 캐시에는 그 세션이 있다.
keycloak-1 로그아웃
│
├──▶ PostgreSQL 행 삭제 ✔ 되었다
│
└──▶ keycloak-0 에게 "캐시에서 지워라" ✗ 7800 이 막혀 못 갔다
│
keycloak-0 은 자기 캐시로 200 을 준다 ◀────────────┘
룩어사이드 캐시는 읽을 때 DB 와 대조하지 않는다. 세션 조회는 PostgreSQL 을 타고 세션 무효화는 클러스터 메시지(7800)를 타므로, 7800 을 막으면 조회는 정상이고 무효화만 전파되지 않는다. 실제 사용자도 로그아웃이 안 되는지는 따로 답이 있다 — 아니다. 파드 주소로 직접 쳤기 때문이고, 실제 사용자는 nginx → Traefik → Service 를 거치는데 NotReady 인 keycloak-0 은 거기서 빠져 있다.
복구와 원상복구 확인표
1. 정책을 지우고 재형성을 기다린다
목적 — 7800 을 다시 열어 두 노드가 하나의 뷰로 합쳐지게 한다.
date '+%H:%M:%S 해제'
kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
예상 결과 — 실측은 이렇다(observed, 12-recovery.txt).
해제 시각: 11:49:58
networkpolicy.networking.k8s.io "a1-block-jgroups-transport" deleted from keycloak-lab namespace
+30초 keycloak-0=1 keycloak-1=1 | Ready 파드 2 개
+60초 keycloak-0=1 keycloak-1=1 | Ready 파드 2 개
+90초 keycloak-0=2 keycloak-1=2 ← 재형성
왜 필요한가 — 90초 만에 자동으로 다시 붙었고 사람 손이 필요 없었다. 누가 붙였는지는 카운터가 말한다.
문제가 생기면 — 2~3분이 지나도 1 이면 MERGE3 주기 밖이거나 정책이 안 지워진 것이므로 get networkpolicy 부터 본다.
2. 누가 붙였는지 카운터로 확인한다
목적 — 재형성이 저절로 일어난 것인지 확인한다.
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_merge3_get_num_merge_events'
예상 결과 — 실측은 merge_events keycloak-0 = 1, merge_events keycloak-1 = 1 이다(observed). 주입 전에 0.0 이던 값이 1 이다.
왜 필요한가 — MERGE3 는 split brain 을 감지해 갈라진 뷰를 병합하는 JGroups 프로토콜이고, 0 → 1 로 오른 카운터가 그 프로토콜이 실제로 일했다고 말한다.
문제가 생기면 — 값이 그대로 0 이면 재형성이 다른 경로로 일어났거나 아직 안 일어난 것이므로 vendor_cluster_size 를 다시 본다.
코디네이터도 하나로 돌아온다. 실측은 이렇다(observed).
keycloak-0-26403 | 10.42.1.67:7800 | t
keycloak-1-48749 | 10.42.0.35:7800 | f ← 코디네이터가 하나로 돌아왔다
코디네이터가 keycloak-1 에서 keycloak-0 으로 넘어갔다. 코디네이터는 특권이 아니라 역할이며 병합 시 재선출되므로 주입 전과 달라도 정상이다.
| 항목 | 명령 | 돌아왔을 때 |
|---|---|---|
| 정책 | kubectl -n keycloak-lab get networkpolicy |
No resources found |
| 파드 | kubectl -n keycloak-lab get pods -o wide |
keycloak 둘 다 1/1 Running |
| 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), 양쪽 동일 |
| 디스커버리 | psql -c "select name, ip, coord from jgroups_ping order by name" |
coord = t 가 하나 |
| 지표 | vendor_cluster_size |
양쪽 2 |
| 임시 파드 | kubectl -n keycloak-lab get pod kc-probe |
NotFound (없어야 정상) |
| 밖 | curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master |
200 |
conntrack 은 지운 채로 두면 된다. 표는 새 패킷이 오면 다시 채워진다.
막히면
| 증상 | 원인 | 확인 |
|---|---|---|
| 정책을 걸었는데 지표가 안 변한다 | conntrack 의 ESTABLISHED 가 먼저 통과시킨다 | sudo conntrack -L 2>/dev/null | grep 7800 |
conntrack -D 가 0 flow entries |
튜플이 틀렸다. --dport 만으로는 0건 |
-L 출력의 src/dst/sport/dport 를 그대로 옮긴다 |
| conntrack 을 지웠는데도 계속 2 | 이 실험은 그것만으로 분단되는지 판정 못 했다 | 정책이 걸린 채 파드를 재시작한다 |
파드가 재시작을 반복한다 (RESTARTS 증가) |
9000 을 안 열었다. readiness 실패 → kubelet 이 죽인다 | describe pod 의 Events. 매니페스트에 9000 이 있는지 |
kubectl exec keycloak-0 -- curl 이 exit 127 |
Keycloak 이미지에 curl 도 wget 도 없다 | 임시 curl 파드를 띄우거나 Prometheus 에 묻는다 |
refresh 가 계속 keycloak-1 로만 간다 |
Service 로 보냈다. NotReady 파드는 빠진다 | 파드 IP 로 직접 |
| 재시작 뒤 아무 데도 안 닿는다 | 파드 IP 가 바뀌었다 (10.42.1.43 → 10.42.1.67) |
get pod -o jsonpath='{.status.podIP}' 다시 |
kubectl get endpoints 가 경고를 찍는다 |
v1.33 부터 deprecated | get endpointslice -l kubernetes.io/service-name=... |
로그인이 401/400 |
비밀번호가 안 넘어갔다 | 파드 안에서 echo ${#PW} — 0 이면 --env 가 빈 값 |
| 값이 빈 문자열인데 「변했다」로 읽힌다 | 원래 실행이 이 실수를 했다 | 빈 값은 「측정 실패」다. 판정 조건에서 빼고 다시 잰다 |
| 복구 후 2~3분이 지나도 1 | MERGE3 주기 밖이거나 정책이 안 지워졌다 | get networkpolicy 로 먼저 확인 |
무엇이 관측이고 무엇이 아닌가
이 절차의 숫자는 2026-09-04 11:38–11:52 KST 에 돈 한 번의 실행에서 나왔다(observed).
- (observed) 차단
11:38:08·재시작11:46:07·해제11:49:58, 25분 내내 2 이던vendor_cluster_size,/proc/net/tcp6의01, conntrack 네 줄,coord = t가 둘, 분단 중 교차 refresh200과 로그아웃 후200, DB 행 0 과 캐시 1, 90초 재형성,merge_events가0 → 1. - (unknown)
tr ',' '\n' | grep -E로 자른 Prometheus 출력과jq형태.jq는 이 실험대에 아예 없다. - (unknown)
ssh kc-lab-2로 들어가서conntrack -L을 따로 치는 세 줄 형태. 이 실험대는ssh kc-lab-2 '...'한 줄로 쳤다. - (unknown) 매니페스트가 놓인 체크아웃의 위치와 그 디렉터리로 옮기는 명령, 그리고 파일이 없을 때 여는 에디터 명령. 원 가이드는
cat과kubectl apply를 저장소 상대경로로만 적는다. - 이 실험이 판정하지 못한 것 — conntrack 삭제만으로 분단이 만들어지는지. 해설 문서가 「3분 뒤 분단」이라고 썼다가 정정했고, 실제 하락은 파드 재시작 4초 뒤였다.
- 이 실험이 재지 않은 것 —
keycloak-0캐시에 있던 낡은 엔트리가 병합 후 어떻게 되는지. 궁금하면 재형성 뒤에vendor_statistics_approximate_entries_unique{cache="sessions"}를 다시 본다. - 가이드가 스크립트를 안 쓰는 까닭도 측정 실패에서 나왔다(observed). 원래 실행은 임시 파드를 20초마다 띄워 지표를 긁었고
+20초 suspected(k0 k1) = []처럼 빈 값과 개수가 안 맞는 값이 섞였다. 판정 조건이[ "$R" != "0.0 0.0 " ]이어서 빈 문자열을 변화로 읽고 즉시 빠져나왔다.