Files
keycloak-pattern/docs/guides/experiments/a1-jgroups-transport-block.md
T
DongHyeonkaandClaude Opus 5 6f6ab86345 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>
2026-09-07 18:29:00 +09:00

46 KiB
Raw Blame History

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 에서 친다. kubectlsudo 로 쓴다 (kubeconfig 를 사용자 홈에 복사해 뒀다면 sudo 는 빼도 된다).
  • kc-lab-2 에는 ssh kc-lab-2 로 붙는다. conntrack 은 두 노드 모두에서 봐야 한다.
  • 터미널 두 개를 열어 두면 편하다. 하나는 임시 curl 파드용(붙잡고 있어야 한다), 하나는 관찰용.

주의 — 이건 상태를 부수는 실험이다

Keycloak 클러스터를 실제로 분단시키고 파드를 재시작한다. 실험대에서만 한다. 전 구간 약 30분이고, 되돌리는 방법은 매 단계에 적어 두었다. 중간에 그만두려면 5. 복구 의 첫 명령 하나면 된다.

표시 규약

표시
실측 2026-09-04 11:3811: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/1
  • RESTARTS0 — 뒤에서 이 값이 오르면 주입이 엉뚱한 것을 건드린 것이다
  • NODE 가 서로 다르다 — 같은 노드에 있으면 이 실험은 성립하지 않는다 (파드 간 통신이 노드를 안 넘어간다)
  • IP 두 개를 적어 둔다. 뒤에서 계속 쓴다

이 결과가 의미하는 것keycloak-0kc-lab-2, keycloak-1kc-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

실측01-baseline-cluster.txt

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 컨테이너에는 curlwget 도 없다(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 이 없는 것. 그게 전부다. PodSelectorapp=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 가 오르고 READY0/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=$?"

실측03 · 05

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 이든 「안 닿는다」이고, 정책은 걸린 것이다.

임시 파드는 정책에 안 잡힌다. podSelectorapp=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'

실측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/nullconntrack 이 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). RESTARTSstartTime 이 이미 바뀌어 있으면 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

실측11-service-impact.txt

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 -D0 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 -- curlexit 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 는 기존 연결을 못 끊는다