Files
keycloak-pattern/docs/experiment-a5-asymmetric-partition.md
T
DongHyeonkaandClaude Opus 5 e0d27d47ce docs: correct the places where documents contradicted their own evidence
An independent audit found ten documents printing values their evidence files do not contain. C-1 printed a session count of 0 where the evidence says 4, C-2 printed a success readback for a command that exited 1, and A-1 credited the conntrack flush with a split that the timestamps attribute to a pod restart four seconds earlier.

Also measured wal_writer_delay, which A-3 had asserted as matching without ever querying it, relabelled the A-6 control that moved 41 percent, noted A-8's nine-sample resolution, corrected D-1's RTO to the 41 seconds its own timeline shows, and added a correction banner to D-2. Every experiment document now links its evidence files with their real collection times, and the duplicate screenshots are documented as duplicates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:35:49 +09:00

13 KiB
Raw Blame History

A-5 — 한쪽 방향만 끊기면 어떻게 되는가 (split brain)

브랜치 feature/keycloak-a5-asymmetric-partition · 증거 docs/evidence/a5-asymmetric-partition/ · 2026-09-04 12:2812:46 KST

선행: A-1 — 이 실험은 A-1 이 열어둔 질문에 답한다.

A-1 의 열린 질문"양쪽이 동시에 NotReady 가 되는 경로가 있다면 전면 장애다. A-5 에서 이어서 본다."


0. 결론부터

물음
비대칭(한 방향) 차단이 클러스터를 가르는가 아니다. 열린 방향으로 재연결한다
양방향 완전 차단은 가르는가 그렇다. 양쪽 모두 멤버 1개
그때 양쪽이 다 NotReady 가 되는가 아니다. 한쪽만 DOWN 이다
그래서 서비스는 계속된다. 외부 200 유지

전면 장애 경로는 없었다. 코디네이터 쪽이 항상 살아남는다.

그리고 주입을 세 번 실패했다. 세 번 모두 다른 이유였고, 셋 다 "아무 일도 없었다"로 보였다.


1. 주입을 세 번 실패했다

A-1 에서 NetworkPolicy 가 conntrack 의 ESTABLISHED 승인에 막혀 기존 연결을 못 끊는다는 것을 배웠다. 그래서 이번에는 iptables 로 갔는데, 또 다른 벽이 세 개 있었다.

실패 ① — iptables -I FORWARD 1 은 최상단에 남지 않는다

ssh kc-lab-2 'sudo iptables -I FORWARD 1 -p tcp -d 10.42.1.77 --dport 7800 -j DROP'
num   pkts bytes target
1      232  377K KUBE-ROUTER-FORWARD  /* kube-router netpol */   ← 다시 1번이 되었다
2        0     0 DROP  tcp dpt:57800
3        0     0 DROP  tcp dpt:7800                              ← 0 패킷

kube-router 가 주기적으로 자기 체인을 최상단에 다시 삽입한다. 내가 1번에 넣어도 곧 2번, 3번으로 밀려난다.

직접 넣은 iptables 규칙은 CNI 가 관리하는 체인과 경쟁한다. 넣는 것으로 끝이 아니라 패킷 카운터로 확인해야 한다.

실패 ② — 그래도 0 패킷: 연결 방향을 잘못 짚었다

raw 테이블로 옮겼는데도 0 패킷이었다.

ssh kc-lab-2 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d 10.42.1.77 --dport 7800 -j DROP'

실제 연결을 봤더니

ESTABLISHED src=10.42.1.77 dst=10.42.0.42 sport=60485 dport=7800
            ────────────            ────────────────────
            keycloak-0 가 클라이언트   keycloak-1 이 서버

A-1 때와 방향이 반대였다. 파드가 재시작되면서 누가 먼저 연결을 걸었는지가 바뀌었다.

JGroups 의 TCP 연결 방향은 고정이 아니다. 먼저 뜬 쪽, 먼저 JOIN 을 건 쪽에 따라 달라진다. 가정하지 말고 conntrack -L 로 봐야 한다.

성공 — raw 테이블 PREROUTING, 수신측 노드에

ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d 10.42.0.42 --dport 7800  -j DROP
              sudo iptables -t raw -I PREROUTING 1 -p tcp -d 10.42.0.42 --dport 57800 -j DROP'
   pkts bytes target
     19  1096 DROP  tcp dpt:57800
     21  3058 DROP  tcp dpt:7800     ← 걸린다

개념 — netfilter 처리 순서

   패킷 도착
     │
     ├─▶ raw     PREROUTING     ← conntrack 보다 먼저. NOTRACK·DROP 용
     │
     ├─▶ conntrack 조회/생성    ← 여기서 ESTABLISHED 가 결정된다
     │
     ├─▶ mangle  PREROUTING
     ├─▶ nat     PREROUTING
     ├─▶ filter  FORWARD        ← NetworkPolicy·kube-router 가 여기 있다
     └─▶ 목적지 파드
어디에 넣는가 기존 연결을 끊는가 CNI 와 경쟁하는가
NetworkPolicy (filter) 못 끊는다 — conntrack 이 먼저 통과시킨다 없음
filter FORWARD 직접 순서에 따라 경쟁한다 (kube-router 가 밀어낸다)
raw PREROUTING 끊는다 없다 — CNI 가 안 쓰는 테이블

진짜 네트워크 분단을 흉내내려면 raw 테이블이 맞다.


2. 비대칭 차단 — 클러스터가 갈라지지 않았다

한 방향(→ keycloak-1:7800)만 막고 관찰했다.

  +25초  keycloak-0:1/1 keycloak-1:1/1 | 외부 200
  +75초  keycloak-0:1/1 keycloak-1:0/1 | 외부 200      ← 잠깐 흔들림
  +100초 keycloak-0:1/1 keycloak-1:1/1 | 외부 200      ← 회복
  ...
  +200초 keycloak-0:1/1 keycloak-1:1/1 | 외부 200

주입 이후 뷰 변화가 하나도 없었다.

kubectl -n keycloak-lab logs keycloak-0 --since=20m | grep ISPN000094 | awk '$2 >= "03:33:58"'
# (아무것도 안 나온다)
  뷰 ID 이력:  |13] (2)  ← 주입 전부터 지금까지 그대로
  suspected  = 0

맥락 하나가 빠져 있었다06-view-history-and-cleanup.txt 를 보면 뷰 13 은 주입(03:33:58)보다 9초 앞선 03:33:49 의 MergeView 로 만들어졌고, 그 직전에는 |12] (1) — 즉 막 분단됐다가 합쳐진 직후였다. merge_events = 1.0 도 그 병합의 것이다.

"주입 전부터 그대로" 는 맞지만, 그 "전" 이 9초였다. 앞선 실패한 주입 시도들이 만든 흔들림이고, 주입 이후 뷰가 변하지 않았다는 결론 자체는 유지된다.

왜 안 갈라졌는가 — 연결 방향이 뒤집혔다

차단 전:  keycloak-0 → keycloak-1:7800     ← 내가 막은 방향
차단 후:  keycloak-1 → keycloak-0:7800     ← 열린 방향으로 다시 붙었다
ESTABLISHED src=10.42.0.42 dst=10.42.1.77 sport=48473 dport=7800
            ────────────            ─────────────
            keycloak-1 이 클라이언트   keycloak-0 이 서버

JGroups 는 막힌 연결이 죽자 반대 방향으로 새로 연결했고, 그 사이 FD_SOCK2 가 상대를 의심하기 전에 복구가 끝났다. suspected = 0 이 그 증거다.

한 방향만 막는 것으로는 JGroups 를 가를 수 없다. 두 노드는 서로에게 연결을 걸 수 있으므로, 한쪽 길이 막히면 다른 길로 간다.

운영적으로는 좋은 소식이다 — 단방향 방화벽 오설정은 자가 치유된다. 반대로 분단을 재현하려는 실험자에게는 함정이다.


3. 양방향 차단 — 갈라졌지만 전면 장애는 아니다

양쪽 노드 모두에 규칙을 넣었다.

  +75초   keycloak-0:1/1 keycloak-1:1/1 | ready=[10.42.0.42 10.42.1.77] 외부 200
  +100초  keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77]            외부 200
  ...
  +225초  keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77]            외부 200
=== 뷰 ===
  keycloak-0  [keycloak-0-24309|14] (1) [keycloak-0-24309]
  keycloak-1  [keycloak-1-45480|14] (1) [keycloak-1-45480]

=== JGROUPS_PING ===
 keycloak-0-24309 | 10.42.1.77:7800 | t     ← 코디네이터
 keycloak-1-45480 | 10.42.0.42:7800 | t     ← 코디네이터

coord = t 가 둘. 완전한 split brain 이다.

cluster_size 가 갈라졌다 합쳐진다

그런데 한쪽만 DOWN 이다

--- keycloak-0 ---
{"status":"UP",   ... {"name":"Keycloak cluster health check","status":"UP"}

--- keycloak-1 ---
{"status":"DOWN", ... (cluster health check 가 DOWN)
keycloak-0 keycloak-1
분단 전 역할 코디네이터 (뷰 13 의 발행자) 일반 멤버
분단 후 자기 인식 "멤버가 하나 나갔다" — 정상 사건 "코디네이터를 잃었다" — 비정상
헬스체크 UP DOWN
Service 엔드포인트 남는다 빠진다

A-1 의 열린 질문에 대한 답 — 양쪽이 동시에 NotReady 가 되는 경로는 없었다.

Keycloak 의 클러스터 헬스체크는 비대칭이다. 코디네이터였던 쪽은 자기가 정상이라고 보고, 잃은 쪽만 DOWN 이 된다. 그래서 완전 분단조차 용량 저하로 끝나고 전면 장애가 되지 않는다.

A-2(DB 상실)에서 양쪽이 동시에 DOWN 이 된 것과 대조된다. DB 는 모두가 의존하는 하나지만, 클러스터 멤버십은 서로 상대적이기 때문이다.


4. 복구

ssh kc-lab-1 'sudo iptables -t raw -F PREROUTING'
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
  +25초  keycloak-0:1/1 keycloak-1:0/1
  +50초  keycloak-0:1/1 keycloak-1:1/1
  → 복구 완료

MergeView::[keycloak-0-24309|15] (2) [keycloak-0-24309, keycloak-1-45480]

50초, 사람 개입 없음. MergeView 로 뷰 15 가 발행되며 병합됐다.

각 캐시마다 재분배 로그가 남는다.

[Context=work]            ISPN100007: After merge (or coordinator change) ...
[Context=clientSessions]  ISPN100007: After merge ...
[Context=offlineSessions] ISPN100007: After merge ...
[Context=loginFailures]   ISPN100007: After merge ...
[Context=actionTokens]    ISPN100007: After merge ...

ISPN100007 은 병합 후 캐시별 토폴로지 재계산이다. 캐시가 여럿이므로 로그도 캐시 수만큼 나온다.


5. 개념

MergeView 와 뷰 ID

[keycloak-0-24309|13] (2)   ← 정상
[keycloak-0-24309|14] (1)   ← 분단. 양쪽이 각자 14 를 발행
MergeView::[...|15] (2)     ← 병합. 뷰 ID 는 계속 증가한다

뷰 ID 는 단조 증가하므로 "언제 몇 번 갈라졌는지"를 로그만으로 셀 수 있다.

코디네이터의 비대칭성

JGroups 코디네이터는 가장 오래된 멤버다. 분단이 나면

   코디네이터 쪽:  "멤버가 나갔다"        → 정상 처리, 계속 코디네이터
   나머지 쪽:      "코디네이터가 사라졌다"  → 새 코디네이터를 자기로 선출

둘 다 자기가 코디네이터라고 믿는 상태가 split brain 이고, JGROUPS_PING.coord 컬럼에 그대로 드러난다.

실험자를 위한 규칙

상황 확인 방법
규칙을 넣었는데 안 걸린다 iptables -L -n -v패킷 카운터
방향을 모르겠다 conntrack -L | grep <포트>
CNI 가 밀어낸다 raw 테이블을 쓴다
갈라졌는지 알고 싶다 vendor_cluster_size, ISPN000094, JGROUPS_PING.coord


증거 파일

증거 수집 시각: 2026-09-04 12:29 16:34 KST (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)

파일 종류
01-injection.txt 터미널 원문
02-injection-verify.txt 터미널 원문
03-raw-table-injection.txt 터미널 원문
04-correct-direction.txt 터미널 원문
05-reconnect-observed.txt 터미널 원문
06-view-history-and-cleanup.txt 터미널 원문
07-bidirectional-block.txt 터미널 원문
08-coordinator-and-recovery.txt 터미널 원문
a5-cluster-size-bidirectional-block.png 스크린샷

파일별 상세는 evidence/a5-asymmetric-partition/README.md.

6. 재현 절차 (명령어)

# 1. 지금 연결이 어느 방향인지 먼저 본다 — 가정하면 실패한다
ssh kc-lab-1 'sudo conntrack -L | grep 7800'

# 2. 수신측 노드의 raw PREROUTING 에 넣는다 (filter 는 CNI 와 경쟁한다)
ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d <수신 파드IP> --dport 7800  -j DROP'
ssh kc-lab-1 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d <수신 파드IP> --dport 57800 -j DROP'

# 3. 걸렸는지 카운터로 확인 — 0 이면 해석 금지
ssh kc-lab-1 'sudo iptables -t raw -L PREROUTING -n -v'

# 4. 양방향으로 하려면 반대 노드에도 (한 방향만으로는 자가 치유된다)
ssh kc-lab-2 'sudo iptables -t raw -I PREROUTING 1 -p tcp -d <반대 파드IP> --dport 7800 -j DROP'

# 5. 분단 확인
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
  -c "select name, coord from jgroups_ping"          # coord=t 가 둘이면 split brain
kubectl -n keycloak-lab get endpoints keycloak -o jsonpath='{.subsets[*].addresses[*].ip}'

# 6. 해제
ssh kc-lab-1 'sudo iptables -t raw -F PREROUTING'
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'

7. 다음 실험에 남기는 것

실험 이 실험이 준 것
A-6 지연 주입 tc netem 도 같은 함정 — 주입이 걸렸는지 먼저 확인
A-7 volatile 비교 여기서는 분단에도 서비스가 계속됐다. volatile 이면 세션이 갈라진다
운영 단방향 방화벽 오설정은 자가 치유된다. 양방향이어야 사고가 된다
운영 완전 분단조차 전면 장애가 아니다 — 코디네이터 쪽이 살아남는다