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>
46 KiB
A-1 재현 가이드 — 7800 을 끊고 무엇이 깨지는지 직접 본다
해설 문서: docs/experiment-a1-jgroups-transport-block.md ·
증거 원문: docs/evidence/a1-jgroups-transport-block/
이 가이드가 끝나면
당신 터미널에서 이것들을 직접 본다.
| 보게 되는 것 | 어디서 |
|---|---|
| NetworkPolicy 를 걸었는데 클러스터가 안 깨지는 상태 | conntrack -L · vendor_cluster_size |
coord = t 가 두 줄인 split brain |
PostgreSQL JGROUPS_PING |
분단인데도 교차 노드 refresh 가 200 |
임시 curl 파드 |
로그아웃했는데 반대편이 200 을 주는 상태 |
같은 파드 |
| 분단된 노드가 스스로 Service 에서 빠지는 것 | endpointslice |
| 90초 만에 자동으로 다시 붙는 것 | merge3_get_num_merge_events |
전제
05-keycloak·06-observability가 끝나 있다.- 명령은
kc-lab-1에서 친다.kubectl은sudo로 쓴다 (kubeconfig 를 사용자 홈에 복사해 뒀다면sudo는 빼도 된다). kc-lab-2에는ssh kc-lab-2로 붙는다. conntrack 은 두 노드 모두에서 봐야 한다.- 터미널 두 개를 열어 두면 편하다. 하나는 임시 curl 파드용(붙잡고 있어야 한다), 하나는 관찰용.
주의 — 이건 상태를 부수는 실험이다
Keycloak 클러스터를 실제로 분단시키고 파드를 재시작한다. 실험대에서만 한다. 전 구간 약 30분이고, 되돌리는 방법은 매 단계에 적어 두었다. 중간에 그만두려면 5. 복구 의 첫 명령 하나면 된다.
표시 규약
| 표시 | 뜻 |
|---|---|
| 실측 | 2026-09-04 11:38–11:52 KST 실행 기록의 출력 원문. 증거 파일에 그대로 있다 |
| 형태 | 값이 매번 달라지는 출력. 모양만 보이고 숫자는 당신 것과 다르다 |
| 미검증 | 손으로 치기 좋게 이 가이드에서 고친 형태. 원래 실행은 스크립트로 했다 |
IP·파드 이름·sid 는 당신 환경에서 다르다. 이 문서는 자리표시자(<...>)를 쓰지
않는 대신, 그 값을 뽑는 명령을 먼저 적는다. 예시로 실린 값은 전부 위 실행 기록의
실제 값이다.
0. 왜 이 실험을 하는가
A-0 이 「세션은 Infinispan 복제가 아니라 PostgreSQL 로 공유된다」를 측정했다. 그런데 Keycloak 24 이전 자료는 「세션은 7800 으로 복제된다」고 말한다.
| 예측 | |
|---|---|
| 통념 | 7800 을 막으면 세션 공유가 깨진다 |
| A-0 모델 | 7800 을 막아도 안 깨진다 |
둘 중 하나는 틀렸고, 7800 만 끊어 보면 판정된다. 그게 이 실험이다.
핵심은 두 가지를 분리해서 끊는 것이다.
디스커버리 노드가 서로를 어떻게 찾는가 → PostgreSQL 의 JGROUPS_PING 테이블
트랜스포트 실제로 어떻게 말하는가 → TCP 7800
트랜스포트만 막으면 DB 에는 둘 다 등록되어 있는데 메시지는 안 가는 상태가 된다. 단일 노드에서는 만들 수 없는 고장이고, 이 실험대가 VM 두 대인 이유다.
1. 기준선 — 아무것도 넣기 전에
시험군만 재는 측정은 측정이 아니다. 차단 후에 볼 것을 차단 전에 똑같은 명령으로 먼저 봐 둔다. 그래야 나중에 「원래 그랬던 것」과 「내가 바꾼 것」이 구별된다.
넓은 것부터 좁혀 간다.
노드 → 파드 → 정책 → 클러스터 뷰(로그) → 디스커버리(DB) → 지표(Prometheus) → 대조군 시험
1-1. 노드와 파드
확인
sudo kubectl get nodes
형태
NAME STATUS ROLES AGE VERSION
kc-lab-1 Ready control-plane,master 12d v1.33.x+k3s1
kc-lab-2 Ready <none> 12d v1.33.x+k3s1
둘 다 Ready 여야 한다. 여기서부터 어긋나면 이 실험의 결과는 전부 무의미하다.
확인
sudo kubectl -n keycloak-lab get pods -o wide
형태
NAME READY STATUS RESTARTS AGE IP NODE
bff-... 1/1 Running 0 3d 10.42.0.41 kc-lab-1
keycloak-0 1/1 Running 0 2d 10.42.1.43 kc-lab-2
keycloak-1 1/1 Running 0 2d 10.42.0.35 kc-lab-1
oauth2-proxy-... 1/1 Running 0 3d 10.42.0.44 kc-lab-1
postgres-... 1/1 Running 0 5d 10.42.0.22 kc-lab-1
redis-... 1/1 Running 0 3d 10.42.0.23 kc-lab-1
어디를 봐야 하는가
READY가 둘 다1/1RESTARTS가0— 뒤에서 이 값이 오르면 주입이 엉뚱한 것을 건드린 것이다NODE가 서로 다르다 — 같은 노드에 있으면 이 실험은 성립하지 않는다 (파드 간 통신이 노드를 안 넘어간다)IP두 개를 적어 둔다. 뒤에서 계속 쓴다
이 결과가 의미하는 것 — keycloak-0 은 kc-lab-2, keycloak-1 은 kc-lab-1
에 있다. 파드 번호와 노드 번호가 어긋난다. 뒤에서 conntrack 을 볼 때 이걸
헷갈리면 엉뚱한 노드를 뒤지게 된다.
IP 는 변수로 잡아 둔다. 파드가 재시작되면 바뀌므로 그때 다시 잡는다.
K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(sudo kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
echo "$K0 $K1"
실측
10.42.1.43 10.42.0.35
1-2. 기존 NetworkPolicy 가 없다는 것
확인
sudo kubectl -n keycloak-lab get networkpolicy
No resources found in keycloak-lab namespace.
왜 확인하나 — 이미 정책이 하나라도 걸려 있으면 결과가 그것과 섞인다. NetworkPolicy 는 합집합으로 허용되므로 두 개가 겹치면 무엇이 열려 있는지 한눈에 안 보인다. 비어 있어야 「내가 넣은 것만이 원인」이라고 말할 수 있다.
1-3. 클러스터 뷰 — 로그가 말하는 것
확인
sudo kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
sudo kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
실측
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-1-48749|5] (2) [keycloak-1-48749, keycloak-0-30843]
└── 코디네이터 ──┘ │ │ └────── 멤버 목록 ──────┘
│ └─ 멤버 수
└─ 뷰 ID (바뀔 때마다 1 증가)
이 결과가 의미하는 것 — 뷰 ID 5, 멤버 2, 그리고 양쪽이 완전히 같은 줄을
찍고 있다. 이게 「하나의 클러스터」다. 두 줄이 달라지면 그때가 분단이다.
keycloak-0-30843 의 뒤 숫자는 JGroups 가 붙인 것이고 파드가 재시작되면
바뀐다. 나중에 keycloak-0-26403 이 나오면 같은 파드의 새 인스턴스다.
1-4. 디스커버리 — DB 가 말하는 것
확인
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select name, ip, coord from jgroups_ping order by name"
실측
name | ip | coord
------------------+-----------------+-------
keycloak-0-30843 | 10.42.1.43:7800 | f
keycloak-1-48749 | 10.42.0.35:7800 | t
(2 rows)
어디를 봐야 하는가 — coord 열에 t 가 정확히 하나.
이 결과가 의미하는 것 — 두 노드가 서로를 찾을 수 있고, 코디네이터가 하나로 합의되어 있다. 이 테이블은 **「지금 등록되어 있다」**만 말한다. 실제로 메시지가 오가는지는 말하지 않는다 — 이 실험이 갈라놓을 지점이 정확히 여기다.
셋이 서로 다른 것을 본다. 로그 = 「그때 그렇게 보였다」, 테이블 = 「지금 등록되어 있다」, 지표 = 「지금 그 노드가 그렇게 안다」. A-1 에서 이 셋이 갈린다.
1-5. 지표 — 각 노드가 자기가 아는 멤버 수를 말한다
Keycloak 컨테이너에는 curl 도 wget 도 없다(exit 127). 그래서 밖에서
Prometheus 에 묻는 것이 가장 짧다. 15초마다 이미 긁고 있다.
확인
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
한 줄짜리 JSON 이 통째로 나온다. 처음 한 번은 그대로 본다. 어떤 라벨이 붙어 있는지 알아야 다음부터 무엇으로 걸러야 할지 안다.
형태
{"status":"success","data":{"resultType":"vector","result":[
{"metric":{"__name__":"vendor_cluster_size","cache_manager":"keycloak","job":"keycloak","node":"kc-lab-1","pod":"keycloak-1"},"value":[1757037600.123,"2"]},
{"metric":{"__name__":"vendor_cluster_size","cache_manager":"keycloak","job":"keycloak","node":"kc-lab-2","pod":"keycloak-0"},"value":[1757037600.123,"2"]}]}}
라벨을 보고 나면 읽기 좋게 자른다. 미검증
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
jq 가 깔려 있다면 이쪽이 낫다. 미검증
sudo 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 인 것을 봐 둔다. 나중에 오르는지 보려면 지금 값이 필요하다.
확인
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_merge3_get_num_merge_events'
실측 — 02-control-before-block.txt
vendor_jgroups_merge3_get_num_merge_events 0.0 (양쪽 노드)
vendor_jgroups_fd_sock2_get_num_suspected_members 0.0 (양쪽 노드)
1-6. 대조군 — 차단 전에 본 시험을 한 번 그대로 돌린다
이 절을 건너뛰면 뒤의 숫자는 아무 의미가 없다. A-0 에서 배운 규칙이다.
왜 임시 파드를 쓰나
- Keycloak 이미지에
curl이 없다 → 파드 안에서는 못 친다 - 토큰을 단계 사이로 넘겨야 한다 → 한 셸 안에서 다 해야 한다
- Service 로 가면 어느 노드가 처리했는지 알 수 없다 → 파드 IP 로 직접 친다. 이 실험의 질문 자체가 「어느 노드인가」다
하기 — 임시 파드를 띄우고 그 안의 셸에 들어간다
sudo 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=$(sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)" \
--command -- sh
되돌리기 — 셸에서 exit 하면 --rm 이 파드를 지운다.
비밀번호를 화면에 찍지 않는다. 명령 치환으로 넘기므로 값은 터미널에도 셸 히스토리에도 남지 않는다(히스토리에는
$(...)문자열만 남는다). 길이만 확인하고 싶으면 밖에서:sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \ -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
여기부터는 파드 안 셸이다. 프롬프트가 / $ 로 바뀐다.
하기 ① — keycloak-0 에서 로그인한다. 이 노드가 세션의 출생지다
TOK=/realms/master/protocol/openid-connect/token
curl -s -X POST "http://$K0:8080$TOK" \
-d grant_type=password -d client_id=admin-cli \
-d username=admin -d "password=$PW"
형태 — 한 줄 JSON 이 나온다. 한 번은 통째로 본다
{"access_token":"eyJhbGciOi...","expires_in":60,"refresh_expires_in":1800,
"refresh_token":"eyJhbGciOi...","token_type":"Bearer","scope":"profile email"}
어디를 봐야 하는가 — expires_in 60, refresh_expires_in 1800.
access token 은 60초짜리고 그동안은 서버에 안 물어본다. 그래서 이 실험의
탐침은 access token 이 아니라 refresh 다 — refresh 는 노드가 세션 저장소를
실제로 뒤져야 답할 수 있다.
하기 ② — 토큰을 변수에 담는다
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" 로 에러 본문을 본다.
하기 ③ — 이 세션의 sid 를 확인한다. JWT 의 가운데 토막이 클레임이다 미검증
echo "$AT" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null; echo
형태
{"exp":1757037660,"iat":1757037600,"jti":"...","iss":"http://10.42.1.43:8080/realms/master",
"sub":"...","typ":"Bearer","azp":"admin-cli","sid":"tAWs2gCPr6SOcD4jDR9-_CzB",...}
"sid" 값을 적어 둔다. 뒤에서 DB 를 직접 뒤질 때 이 값이 필요하다.
base64 패딩 때문에 끝이 깨져 보일 수 있다(
2>/dev/null이 그 불평을 지운다).sid는 앞쪽에 있어서 대개 보인다. 그래도 안 보이면 sid 없이 진행하고, DB 확인은 4-6 의 개수 세기로 대신한다.
하기 ④ — 대조군 본 시험. 반대편 노드에서 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')
실측 — 02-control-before-block.txt
sid tAWs2gCPr6SOcD4jDR9-_CzB
keycloak-1 에서 refresh: 200
이 결과가 의미하는 것 — 차단 전에는 교차 노드 refresh 가 된다. 이 200 이 기준선이다. 차단 후에도 200 이면 「원래 되던 게 그대로 되는 것」이고, 차단 후 400 이면 「내가 깨뜨린 것」이다. 대조군 없이는 이 구별이 안 된다.
refresh token 은 회전한다. 갱신할 때마다 새 것이 나오므로 매번
RT를 다시 담는다. 옛 것을 계속 쓰면 나중에 나오는 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
이 결과가 의미하는 것 — keycloak-1 에서 로그아웃하면 keycloak-0 에서도
갱신이 막힌다. 무효화가 전파된다.
이 400 은 A-0 에서 측정한 값이다. A-1 의 대조군 기록 (
02-control-before-block.txt) 에는 refresh 200 만 있고 로그아웃 단계는 없다. 당신은 지금 직접 재 두는 것이 낫다 — 뒤에서 이 자리가 200 으로 바뀌는 것이 이 실험의 결론이다.
exit 로 파드에서 나온다.
2. 주입 — NetworkPolicy 로 7800 만 막는다
여기부터 상태가 바뀐다. 되돌리는 명령을 먼저 읽어 둔다.
되돌리기
sudo kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
2-1. 무엇을 적용하는가 — 먼저 읽는다
확인
cat deploy/lab/k8s/a1-block-jgroups-transport.yaml
핵심은 이 부분이다.
spec:
podSelector: { matchLabels: { app: keycloak } }
policyTypes: [Ingress]
ingress:
- ports:
- { port: 8080, protocol: TCP } # HTTP — 열어둔다
- { port: 9000, protocol: TCP } # health+metrics — 열어둔다
# 7800 은 일부러 없다
개념 — NetworkPolicy 는 방화벽이 아니라 허용 목록이다
「7800 을 거부」라고 쓸 방법이 없다. 파드가 policyTypes: [Ingress] 를 가진
정책에 선택되는 순간 모든 인바운드가 거부되고, 규칙에 적힌 것만 통과한다.
그래서 7800 은 빠뜨림으로써 막힌다.
이 구조 때문에 두 허용 규칙이 결정적이다. 잘못 쓰면 분단된 클러스터가 아니라 죽은 Keycloak 을 측정하게 된다.
| 포트 | 빼면 |
|---|---|
| 8080 | Traefik·상대 노드의 REST 호출이 전부 끊긴다 |
| 9000 | readiness 프로브가 실패해 kubelet 이 파드를 죽인다 — 엉뚱한 이유로 클러스터가 깨진다 |
덤으로 57800 도 막힌다. FD_SOCK2(장애 감지 채널)는 bind_port + 50000 을
쓴다. 손으로 「7800 거부」 규칙을 쓰면 이걸 빠뜨리기 쉽지만, 허용 목록 방식은
8080·9000 외 전부 거부이므로 자동으로 같이 막힌다.
2-2. 적용
하기
sudo kubectl apply -f deploy/lab/k8s/a1-block-jgroups-transport.yaml
date '+%H:%M:%S 적용'
실측 — 03-block-applied.txt
networkpolicy.networking.k8s.io/a1-block-jgroups-transport created
적용 시각: 11:38:08
시각을 반드시 적어 둔다. 뒤에서 Prometheus 로 「언제부터 변했나」를 볼 때 이 시각이 없으면 인과를 못 붙인다. 실제로 이 실험은 시각이 겹친 것을 인과로 잘못 읽었다가 나중에 정정했다 — 해설 문서 4절의 ★ 정정.
3. 주입이 실제로 걸렸는지 확인한다
결과를 해석하기 전에, 주입이 의도한 것만 건드렸는지 먼저 본다. 이 실험이 남긴 가장 큰 교훈이 여기 있다.
3-1. 정책이 어떤 파드를 잡았나
확인
sudo kubectl -n keycloak-lab get networkpolicy
sudo kubectl -n keycloak-lab describe networkpolicy a1-block-jgroups-transport
형태
PodSelector: app=keycloak
Allowing ingress traffic:
To Port: 8080/TCP
To Port: 9000/TCP
From: <any> (traffic not restricted by source)
Policy Types: Ingress
어디를 봐야 하는가 — To Port 목록에 7800 이 없는 것. 그게 전부다.
PodSelector 가 app=keycloak 인 것도 확인한다. 오타로 아무 파드도 안 잡히면
정책은 걸렸는데 아무 일도 안 일어난다.
3-2. 엉뚱한 것을 죽이지 않았나
확인
sudo kubectl -n keycloak-lab get pods -o wide | grep keycloak
실측
파드 상태: keycloak-0 ready=true restarts=0
keycloak-1 ready=true restarts=0
어디를 봐야 하는가 — RESTARTS 가 여전히 0.
이 결과가 의미하는 것 — 9000 을 제대로 열어 둬서 readiness 프로브가 살아 있다.
여기서 RESTARTS 가 오르고 READY 가 0/1 이면 9000 을 막은 것이고,
그 상태에서 무엇을 재든 「분단된 클러스터」가 아니라 「죽은 파드」를 재는 것이다.
즉시 정책을 지우고 매니페스트를 다시 본다.
3-3. 열어 둔 포트는 살아 있나 · 막은 포트는 죽었나
하기 — 임시 파드를 다시 띄운다
sudo 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=$?"
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 (연결 실패)
어디를 봐야 하는가 — 8080·9000 은 200, 7800 은 curl 종료코드.
| curl exit | 뜻 |
|---|---|
7 |
연결 자체가 안 됨 |
28 |
--max-time 초과 = SYN 이 조용히 버려지고 있음 |
0 |
닿았다 — 정책이 안 걸린 것이다 |
7 이든 28 이든 「안 닿는다」이고, 정책은 걸린 것이다.
임시 파드는 정책에 안 잡힌다.
podSelector가app=keycloak이라 이 파드의 인바운드는 제한되지 않는다. 그런데도 7800 에 못 닿는 이유는 정책이 목적지(Keycloak 파드)의 인바운드를 막기 때문이다. 출발지가 무엇이든 상관없다.
exit 으로 나온다.
3-4. ★ 그런데 클러스터가 안 깨졌다
확인
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
실측 — 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
신규 연결은 분명히 막히는데 클러스터는 25분 내내 2 다. 여기서 「실험 실패」라고 결론 내리면 틀린다. 파드 안 소켓을 본다.
확인 — Keycloak 이미지에는 ss 도 없다. /proc 을 직접 읽는다
sudo kubectl -n keycloak-lab exec keycloak-0 -- cat /proc/net/tcp6 | grep 1E78
실측
=== /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.
이 결과가 의미하는 것 — 기존 연결이 멀쩡히 살아 있다.
3-5. 왜 그런가 — conntrack
패킷 도착
│
├─▶ [ conntrack: ESTABLISHED/RELATED 이면 ACCEPT ] ← 여기서 통과해버린다
│
└─▶ [ NetworkPolicy 규칙 평가 ] ← 여기까지 오지 않는다
리눅스 방화벽은 성능을 위해 이미 성립한 연결을 먼저 통과시킨다. NetworkPolicy 는 그 뒤에 있으므로 신규 연결(SYN)만 걸러낸다.
확인 — 두 노드 모두에서 본다
sudo conntrack -L 2>/dev/null | grep 7800
ssh kc-lab-2 'sudo conntrack -L 2>/dev/null | grep 7800'
--- 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 도 있다. FD_SOCK2 채널이며, 이것도 ESTABLISHED 로 살아 있다.
운영적 함의 — NetworkPolicy 는 이미 붙어 있는 것을 떼어내지 못한다. 보안 사고 대응으로 「지금 당장 이 통신을 끊어라」에 NetworkPolicy 를 걸면 새 연결만 막히고 진행 중인 연결은 계속된다. 끊으려면 conntrack 을 지우거나 파드를 재시작해야 한다.
3-6. conntrack 항목을 지운다
위 출력의 값을 그대로 넣는다. 튜플이 정확해야 지워진다.
하기 — kc-lab-1 에서
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 에서도 같은 일을 한다. 서버 쪽 노드에는 튜플이 뒤집혀 기록되어 있다.
형태
conntrack v1.4.7 (conntrack-tools): 1 flow entries have been deleted.
어디를 봐야 하는가 — 삭제 건수. 0 flow entries have been deleted 면
튜플이 틀린 것이다. --dport 7800 만 주면 0 건이 나온다 — 실제로 원래
실행에서 그렇게 나왔다.
확인 — 다시 세어 본다
sudo conntrack -L 2>/dev/null | grep -c 7800
ssh kc-lab-2 'sudo conntrack -L 2>/dev/null | grep -c 7800'
★ 여기서 정직하게 알아 둘 것
원래 실행에서 conntrack 을 지운 뒤에도 vendor_cluster_size 는 계속 2 였다.
해설 문서는 처음에 「conntrack 삭제 → 3분 뒤 분단」이라고 썼다가 증거를 다시 보고
정정했다. 실제 하락은 파드가 재시작된 4초 뒤에 일어났다.
즉, 이 절만으로 분단이 만들어지는지는 이 실험이 판정하지 못했다. 확실하게 분단을 만드는 방법은 다음 절이다.
3-7. 정책이 걸린 채 파드를 재시작한다 — 이게 분단을 만든다
하기
date '+%H:%M:%S 재시작'
sudo kubectl -n keycloak-lab delete pod keycloak-0
실측 — 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 이 같은 이름으로 곧바로 다시 만든다. 별도 조치 없음.
정책이 걸린 채 파드가 스스로 재시작하는 일도 있다. 원래 실행에서 실제로 그랬다(
startTime 11:44:23).RESTARTS나startTime이 이미 바뀌어 있으면delete를 칠 필요도 없다.
확인 — 새 파드가 뜨고 IP 가 바뀐 것을 본다
sudo kubectl -n keycloak-lab get pods -o wide | grep keycloak
K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
echo "$K0"
실측
10.42.1.67 ← 10.42.1.43 에서 바뀌었다
★ IP 가 바뀌었다는 것을 반드시 반영한다. 아까 띄운 임시 파드의 K0 환경변수는
낡았다. 뒤에서 파드를 다시 띄울 때 새 IP 로 띄운다. 이걸 놓치면 「아무 데도 안 닿음」을
「분단」으로 착각한다.
4. 효과를 관찰한다
4-1. 클러스터 크기가 떨어졌나
확인 — 2~4분에 걸쳐 몇 번 친다
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
실측
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. 서로를 멤버로 안 세고 있다.
4-2. 로그가 이유를 말한다
확인
sudo kubectl -n keycloak-lab logs keycloak-0 | grep -E "GMS|ISPN000094" | tail -20
실측
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]
sudo kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
실측
ISPN000094: [keycloak-1-48749|6] (1) [keycloak-1-48749]
이 결과가 의미하는 것 — 새로 뜬 keycloak-0 은 DB 에서 keycloak-1 을
찾았다. 주소도 안다. 그런데 JOIN 메시지가 7800 으로 안 간다. 10번 시도하고
포기해서 혼자가 됐다. 디스커버리는 살아 있고 트랜스포트만 죽은 상태다.
4-3. split brain 을 DB 한 줄로 확인한다
확인
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select name, ip, coord from jgroups_ping order by name"
실측 — 06
name | ip | coord
------------------+-----------------+-------
keycloak-0-26403 | 10.42.1.67:7800 | t ← 코디네이터
keycloak-1-48749 | 10.42.0.35:7800 | t ← 코디네이터
어디를 봐야 하는가 — coord = t 가 둘.
이 결과가 의미하는 것 — 교과서적인 split brain 이다. 서로를 못 보니까 각자
자기가 대장이라고 생각한다. 기준선(1-4)에서 t 가 하나였던 것과 대조한다.
분단을 확인하는 가장 짧은 명령이 이것이다.
4-4. 분단된 노드가 스스로 빠진다 — 예상 못 한 발견
확인
sudo 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
왜 keycloak-0 만 false 인가 — 물어본다.
sudo kubectl -n keycloak-lab describe pod keycloak-0 | grep -A5 Conditions
실측 — 헬스 응답 원문
{ "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 이다. 그리고 쿠버네티스가 그 신고를 받아 처리한다.
확인 — Service 에서 빠졌는지
sudo 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] ← keycloak-1 만 트래픽을 받는다
notReady : [10.42.1.67] ← keycloak-0 은 제외되었다
kubectl get endpoints는 쓰지 않는다. v1.33 부터 deprecated 라 경고가 뜬다.endpointslice를 본다.
확인 — 밖에서는 멀쩡한가
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
실측
https://auth.hyeonworks.com/realms/master HTTP 200
토큰 발급 HTTP 200
이 결과가 의미하는 것 — 분단된 노드가 스스로 로드밸런서에서 빠졌고, 서비스는 계속됐다. liveness 였다면 재시작을 반복했을 텐데, 재시작해도 안 나아지는 문제이므로 readiness(격리)가 맞는 신호다.
다만 비대칭이라서 살았다.
keycloak-1은 「멤버가 하나 줄어든」 정상적인 사건이라 Ready 를 유지했고,keycloak-0은 합류 자체를 못 해 DOWN 이 됐다. 양쪽이 동시에 DOWN 이 되는 경로가 있다면 전면 장애다 — A-5 의 주제.
4-5. 본 시험 — 분단 상태에서 세션은 어떻게 되는가
Service 를 쓰면 안 된다. keycloak-0 이 NotReady 라 Service 로 보내면 전부
keycloak-1 로 간다. 파드 IP 로 직접 친다.
하기 — 새 IP 로 임시 파드를 다시 띄운다
K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(sudo kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
sudo 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=$(sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)" \
--command -- sh
파드 안에서 1-6 과 똑같은 순서를 다시 한다.
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"
실측 — 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] 로그아웃 전파 — 예측이 틀렸다. 대조군(1-6)에서 400 이던 자리가 200 이다. 로그아웃한 세션이 반대편에서 살아 있다.
exit 으로 나온다.
4-6. 기제 확정 — DB 는 지워졌는데 캐시가 답한다
[4] 의 200 이 「로그아웃이 아예 안 됐다」는 뜻인지 확인해야 한다. DB 를 본다. sid 는 위 [1] 에서 적어 둔 값이다.
확인
sudo 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='nShl5TaBrZnKStDqaspjgmJB'"
실측 — 10-logout-not-propagated.txt
user_session_id | offline_flag | last_session_refresh
-----------------+--------------+----------------------
(0 rows) ← DB 행은 삭제되었다
sid 를 못 뽑았다면 개수로 본다. 로그인 전후·로그아웃 전후로 세 번 친다.
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \ -c "select offline_flag, count(*) from offline_user_session group by 1"관리 API 호출도 세션을 만들기 때문에 개수는 노이즈가 있다. sid 로 보는 편이 정확하다.
확인 — 노드별 세션 캐시 엔트리
sudo 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 ← 캐시에는 남아 있다
중괄호 때문에
wget이 싫어하면 라벨을 빼고 걸러 낸다. 미검증sudo kubectl -n observability exec deploy/prometheus -- \ wget -qO- 'localhost:9090/api/v1/query?query=vendor_statistics_approximate_entries_unique' \ | tr ',' '\n' | grep -E '"cache":|"pod":|^"[0-9]'
이 결과가 의미하는 것
keycloak-1 로그아웃
│
├──▶ PostgreSQL 행 삭제 ✔ 되었다
│
└──▶ keycloak-0 에게 "캐시에서 지워라" ✗ 7800 이 막혀 못 갔다
│
keycloak-0 은 자기 캐시로 200 을 준다 ◀────────────┘
룩어사이드 캐시는 읽을 때 DB 와 대조하지 않는다. 캐시에 있으면 그걸로 답한다. 캐시 무효화는 클러스터 메시지(7800)를 타고 간다.
| 세션 조회 | 세션 무효화 | |
|---|---|---|
| 경로 | PostgreSQL | 클러스터 메시지 (7800) |
| 7800 차단 시 | 정상 | 전파되지 않음 |
그럼 실제 사용자도 로그아웃이 안 되나? 아니다. 당신은 Service 를 우회해 파드 IP 로 직접 쳤다. 실제 사용자는 nginx → Traefik → Service 를 거치고, NotReady 인
keycloak-0은 거기서 빠져 있다. 정문으로 들어오면 낡은 캐시에 닿지 않는다. 4-4 의 readiness 게이트가 막는다.
5. 복구
5-1. 정책을 지운다
하기
date '+%H:%M:%S 해제'
sudo kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
실측 — 12-recovery.txt
해제 시각: 11:49:58
networkpolicy.networking.k8s.io "a1-block-jgroups-transport" deleted from keycloak-lab namespace
5-2. 자동으로 다시 붙는지 본다
확인 — 30초 간격으로 몇 번 친다
sudo kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
실측
+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 이면 막히면 표를 본다.
5-3. 누가 붙였나 — MERGE3
확인
sudo 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
어디를 봐야 하는가 — 기준선(1-5)에서 0.0 이던 값이 1 이다.
이 결과가 의미하는 것 — MERGE3 는 split brain 을 감지해 갈라진 뷰를 병합하는
JGroups 프로토콜이다. 주기적으로 다른 코디네이터의 존재를 확인하고, 발견하면
병합을 개시한다. 지표가 0 → 1 로 오른 것이 「MERGE3 가 실제로 일했다」는 증거다.
5-4. 코디네이터가 하나로 돌아왔나
확인
sudo kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select name, ip, coord from jgroups_ping order by name"
실측
keycloak-0-26403 | 10.42.1.67:7800 | t
keycloak-1-48749 | 10.42.0.35:7800 | f ← 코디네이터가 하나로 돌아왔다
코디네이터가 keycloak-1 에서 keycloak-0 으로 넘어갔다. 코디네이터는
특권이 아니라 역할이며 병합 시 재선출된다. 기준선과 달라도 정상이다.
5-5. 원상복구 확인표
| 항목 | 명령 | 돌아왔을 때 |
|---|---|---|
| 정책 | sudo kubectl -n keycloak-lab get networkpolicy |
No resources found |
| 파드 | sudo kubectl -n keycloak-lab get pods -o wide |
keycloak 둘 다 1/1 Running |
| 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), 양쪽 동일 |
| 디스커버리 | 위 5-4 | coord = t 가 하나 |
| 지표 | vendor_cluster_size |
양쪽 2 |
| 임시 파드 | sudo 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 |
임시 파드가 남아 있으면 (--rm 이 안 먹은 경우):
sudo kubectl -n keycloak-lab delete pod kc-probe --ignore-not-found
conntrack 은 지운 채로 두면 된다. 표는 새 패킷이 오면 다시 채워진다.
이 실험이 재지 않은 것 — 4-6 에서
keycloak-0캐시에 남아 있던 낡은 엔트리가 병합 후 어떻게 되는지는 측정하지 않았다. 궁금하면 5-2 뒤에vendor_statistics_approximate_entries_unique{cache="sessions"}를 다시 본다.
막히면
전부 이 실험대가 실제로 겪은 증상이다. 지어낸 것은 없다.
| 증상 | 원인 | 확인 |
|---|---|---|
| 정책을 걸었는데 지표가 안 변한다 | conntrack 의 ESTABLISHED 가 먼저 통과시킨다 | sudo conntrack -L 2>/dev/null | grep 7800 — 3-5 |
conntrack -D 가 0 flow entries |
튜플이 틀렸다. --dport 만으로는 0건 |
-L 출력의 src/dst/sport/dport 를 그대로 옮긴다 |
| conntrack 을 지웠는데도 계속 2 | 이 실험은 그것만으로 분단되는지 판정 못 했다 | 정책이 걸린 채 파드를 재시작한다 — 3-7 |
파드가 재시작을 반복한다 (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 로 직접 — 4-5 |
| 재시작 뒤 아무 데도 안 닿는다 | 파드 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 가 빈 값 |
| 값이 빈 문자열인데 「변했다」로 읽힌다 | 원래 실행이 이 실수를 했다 (03) |
빈 값은 「측정 실패」다. 판정 조건에서 빼고 다시 잰다 |
| 복구 후 2~3분이 지나도 1 | MERGE3 주기 밖이거나 정책이 안 지워졌다 | get networkpolicy 로 먼저 확인 |
왜 이 가이드에는 스크립트가 없나
원래 실행은 임시 파드를 20초마다 띄워 지표를 긁는 스크립트로 관찰했다.
그 결과가 이렇다. — 06-partition-observed.txt
+20초 suspected(k0 k1) = []
+60초 suspected(k0 k1) = [0.0 0.0 0.0 0.0 ]
+140초 suspected(k0 k1) = [0.0 ]
빈 값과 개수가 안 맞는 값이 섞였다. kubectl run --rm 은 매번 파드를 만들고
지우므로 느리고 경합이 있다. 게다가 판정 조건이 [ "$R" != "0.0 0.0 " ] 이어서
빈 문자열을 「변화」로 읽고 즉시 빠져나왔다.
임시 파드는 계측 도구가 아니다. 15초마다 이미 긁고 있는 Prometheus 가 그러라고 있는 것이다.
그래서 이 가이드는 관찰을 한 번에 하나씩 손으로 친다. 값이 이상하면 그 자리에서 보이고, 다시 칠 수 있고, 무엇을 봤는지 남는다.
다음
| 실험 | A-1 이 남긴 질문 |
|---|---|
| A-5 비대칭 파티션 | 양쪽이 동시에 NotReady 가 되는 경로가 있는가. 여기서는 비대칭이라 살았다 |
| A-2 DB 정지 | 캐시가 DB 와 대조하지 않으므로 캐시에 있는 세션은 DB 없이도 읽힐 수 있다 |
| A-7 volatile 비교 | 같은 주입에서 세션 공유가 깨져야 한다. A-1 이 그 대조군 |
| 전부 | 주입이 실제로 걸렸는지 먼저 확인한다. NetworkPolicy 는 기존 연결을 못 끊는다 |