docs: A-5 — a one-way block heals itself and a full split still leaves one node serving
Three injections failed first: kube-router keeps reinserting its chain above a hand-placed FORWARD rule, the JGroups connection direction had reversed since A-1, and only the raw table runs ahead of conntrack. Each failure looked like nothing happening. Blocking one direction never partitioned the cluster because JGroups reconnected the other way before failure detection fired. Blocking both produced a real split brain with two coordinators in JGROUPS_PING, yet only the non-coordinator node reported DOWN, so the Service kept an endpoint and the front door stayed at 200. That answers the question A-1 left open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d0666c5ba0
commit
2fa77a899e
@@ -0,0 +1,42 @@
|
||||
=== A-5 기준선 ===
|
||||
keycloak-0=10.42.1.77 (kc-lab-2) keycloak-1=10.42.0.42 (kc-lab-1)
|
||||
keycloak-0 1/1 Running 0 11m
|
||||
keycloak-1 1/1 Running 1 (2m48s ago) 155m
|
||||
postgres-7b474b88c8-9cmsv 1/1 Running 0 14m
|
||||
Traceback (most recent call last):
|
||||
File "<string>", line 3, in <module>
|
||||
for r in json.load(sys.stdin)["data"]["result"]: print(f" cluster_size {r["metric"].get("pod"):12} = {r["value"][1]}")
|
||||
~~~~~~~~~^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/json/__init__.py", line 298, in load
|
||||
return loads(fp.read(),
|
||||
cls=cls, object_hook=object_hook,
|
||||
parse_float=parse_float, parse_int=parse_int,
|
||||
parse_constant=parse_constant, object_pairs_hook=object_pairs_hook, **kw)
|
||||
File "/usr/lib/python3.14/json/__init__.py", line 352, in loads
|
||||
return _default_decoder.decode(s)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~^^^
|
||||
File "/usr/lib/python3.14/json/decoder.py", line 345, in decode
|
||||
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
|
||||
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/usr/lib/python3.14/json/decoder.py", line 363, in raw_decode
|
||||
raise JSONDecodeError("Expecting value", s, err.value) from None
|
||||
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
|
||||
|
||||
=== 주입: keycloak-0 으로 들어가는 7800/57800 만 DROP (한 방향) ===
|
||||
A-1 의 NetworkPolicy 는 conntrack ESTABLISHED 에 막혀 기존 연결을 못 끊었다.
|
||||
FORWARD 최상단에 넣으면 conntrack 승인보다 먼저 평가된다.
|
||||
Chain FORWARD (policy ACCEPT)
|
||||
num target prot opt source destination
|
||||
1 DROP 6 -- 0.0.0.0/0 10.42.1.77 tcp dpt:57800
|
||||
2 DROP 6 -- 0.0.0.0/0 10.42.1.77 tcp dpt:7800
|
||||
주입 시각: 12:28:23
|
||||
|
||||
=== 관찰 — 어느 쪽이 먼저 상대를 의심하는가 ===
|
||||
+25초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+50초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+75초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+100초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+125초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+150초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+175초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+200초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
@@ -0,0 +1,16 @@
|
||||
=== 실패한 규칙 제거 ===
|
||||
제거완료
|
||||
|
||||
=== raw 테이블 PREROUTING 에 넣는다 — conntrack 보다 먼저 실행된다 ===
|
||||
netfilter 순서: raw PREROUTING → conntrack → mangle → nat → filter
|
||||
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
|
||||
num pkts bytes target prot opt in out source destination
|
||||
1 0 0 DROP 6 -- * * 0.0.0.0/0 10.42.1.77 tcp dpt:57800
|
||||
2 0 0 DROP 6 -- * * 0.0.0.0/0 10.42.1.77 tcp dpt:7800
|
||||
주입: 12:32:35
|
||||
|
||||
=== [검증] 이번엔 패킷이 걸렸는가 ===
|
||||
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
|
||||
pkts bytes target prot opt in out source destination
|
||||
0 0 DROP 6 -- * * 0.0.0.0/0 10.42.1.77 tcp dpt:57800
|
||||
0 0 DROP 6 -- * * 0.0.0.0/0 10.42.1.77 tcp dpt:7800
|
||||
@@ -0,0 +1,18 @@
|
||||
=== keycloak-1(수신측)으로 들어가는 7800/57800 만 DROP — kc-lab-1 에 넣는다 ===
|
||||
주입: 12:33:58
|
||||
|
||||
=== [검증] 패킷이 걸리는가 ===
|
||||
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
|
||||
pkts bytes target prot opt in out source destination
|
||||
0 0 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:57800
|
||||
19 2938 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:7800
|
||||
|
||||
=== 관찰 ===
|
||||
+25초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+50초 - | 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
|
||||
+125초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+150초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+175초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
+200초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
|
||||
@@ -0,0 +1,19 @@
|
||||
=== 지금 연결 방향은? (차단은 → 10.42.0.42:7800 만) ===
|
||||
tcp 6 299 ESTABLISHED src=10.42.0.42 dst=10.42.1.77 sport=48473 dport=7800 src=10.42.1.77 dst=10.42.0.42 sport=7800 dport=48473
|
||||
tcp 6 86232 ESTABLISHED src=10.42.0.42 dst=10.42.1.77 sport=44205 dport=57800 src=10.42.1.77 dst=10.42.0.42 sport=57800 dport=44205
|
||||
|
||||
=== 차단 규칙 누적 카운터 ===
|
||||
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
|
||||
pkts bytes target prot opt in out source destination
|
||||
19 1096 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:57800
|
||||
21 3058 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:7800
|
||||
|
||||
=== 클러스터 멤버 (파드에서 직접) ===
|
||||
keycloak-0 cluster_size=2.0
|
||||
keycloak-1 cluster_size=2.0
|
||||
|
||||
=== 최근 뷰 로그 ===
|
||||
keycloak-0: [keycloak-0-24309(v=16.0.12)|12] (1) [keycloak-0-24309(v=16.0.12)]
|
||||
|
||||
keycloak-1: [keycloak-1-45480(v=16.0.12)|12] (1) [keycloak-1-45480(v=16.0.12)]
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
=== 뷰 변화 전체 이력 (차단 12:33:58 전후) ===
|
||||
--- keycloak-0 ---
|
||||
2026-09-04 03:33:49 | during merge: CacheTopology{id=37, phase=NO_REBALANCE, rebalanceId=11, currentCH=DefaultConsistentHash{ns=25
|
||||
2026-09-04 03:33:49,398 INFO [org.infinispan.CLUSTER] (non-blocking-thread--p2-t1) [Context=offlineSessions] ISPN100007: After me
|
||||
2026-09-04 03:33:49 | during merge: CacheTopology{id=37, phase=NO_REBALANCE, rebalanceId=11, currentCH=DefaultConsistentHash{ns=25
|
||||
2026-09-04 03:33:49,399 INFO [org.infinispan.CLUSTER] (non-blocking-thread--p2-t1) [Context=loginFailures] ISPN100007: After merg
|
||||
2026-09-04 03:33:49 | during merge: CacheTopology{id=37, phase=NO_REBALANCE, rebalanceId=11, currentCH=DefaultConsistentHash{ns=25
|
||||
2026-09-04 03:33:49,402 INFO [org.infinispan.CLUSTER] (non-blocking-thread--p2-t1) [Context=actionTokens] ISPN100007: After merge
|
||||
--- keycloak-1 ---
|
||||
2026-09-04 03:32:59,874 INFO [org.infinispan.CLUSTER] (non-blocking-thread--p2-t2) [Context=work] ISPN100007: After merge (or coo
|
||||
2026-09-04 03:32:59,879 INFO [org.infinispan.CLUSTER] (non-blocking-thread--p2-t2) [Context=clientSessions] ISPN100007: After mer
|
||||
2026-09-04 03:32:59,879 INFO [org.infinispan.CLUSTER] (non-blocking-thread--p2-t1) [Context=offlineSessions] ISPN100007: After me
|
||||
2026-09-04 03:32:59,883 INFO [org.infinispan.CLUSTER] (non-blocking-thread--p2-t1) [Context=loginFailures] ISPN100007: After merg
|
||||
2026-09-04 03:32:59,884 INFO [org.infinispan.CLUSTER] (non-blocking-thread--p2-t2) [Context=actionTokens] ISPN100007: After merge
|
||||
2026-09-04 03:33:49 | MergeView::[keycloak-0-24309(v=16.0.12)|13] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(v=16.0.12)],
|
||||
|
||||
=== MERGE3 병합 이벤트 ===
|
||||
keycloak-0 merge_events=1.0 suspected=0.0
|
||||
keycloak-1 merge_events=1.0 suspected=0.0
|
||||
|
||||
=== 차단 해제 ===
|
||||
해제완료
|
||||
keycloak-0 cluster_size=2.0
|
||||
warning: couldn't attach to pod/a5-f, falling back to streaming logs: unable to upgrade connection: container a5-f not found in pod a5-f_keycloak-lab
|
||||
keycloak-0 cluster_size=2.0
|
||||
keycloak-1 cluster_size=2.0
|
||||
keycloak-0 1/1 Running 0 22m
|
||||
keycloak-1 1/1 Running 1 (13m ago) 166m
|
||||
@@ -0,0 +1,17 @@
|
||||
=== 양방향 차단 — 두 노드 모두에 raw DROP ===
|
||||
주입: 12:40:25
|
||||
+25초 keycloak-0:1/1 keycloak-1:1/1 | ready=[10.42.0.42 10.42.1.77] 외부 200
|
||||
+50초 keycloak-0:1/1 keycloak-1:1/1 | ready=[10.42.0.42 10.42.1.77] 외부 200
|
||||
+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
|
||||
+125초 keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77] 외부 200
|
||||
+150초 keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77] 외부 200
|
||||
+175초 keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77] 외부 200
|
||||
+200초 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(v=16.0.12)|14] (1) [keycloak-0-24309(v=16.0.12)]
|
||||
|
||||
keycloak-1 [keycloak-1-45480(v=16.0.12)|14] (1) [keycloak-1-45480(v=16.0.12)]
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
=== 왜 한쪽만 DOWN 인가 — 코디네이터 여부 확인 ===
|
||||
name | ip | coord
|
||||
------------------+-----------------+-------
|
||||
keycloak-0-24309 | 10.42.1.77:7800 | t
|
||||
keycloak-1-45480 | 10.42.0.42:7800 | t
|
||||
(2 rows)
|
||||
|
||||
=== 양쪽 헬스체크 상세 ===
|
||||
--- keycloak-0 ---
|
||||
{"status":"UP","checks":[{"name":"GracefulShutdown","status":"UP"}
|
||||
{"name":"KeycloakInitialized","status":"UP"}
|
||||
{"name":"Keycloakclusterhealthcheck","status":"UP"}
|
||||
|
||||
--- keycloak-1 ---
|
||||
{"status":"DOWN","checks":[{"name":"GracefulShutdown","status":"UP"}
|
||||
{"name":"Keycloakdatabaseconnectionsasynchealthcheck","status":"UP"}
|
||||
{"name":"KeycloakInitialized","status":"UP"}
|
||||
|
||||
warning: couldn't attach to pod/a5-h, falling back to streaming logs: Internal error occurred: Internal error occurred: error attaching to container: container is in CONTAINER_EXITED state
|
||||
--- keycloak-0 ---
|
||||
{"status":"UP","checks":[{"name":"GracefulShutdown","status":"UP"}
|
||||
{"name":"KeycloakInitialized","status":"UP"}
|
||||
{"name":"Keycloakclusterhealthcheck","status":"UP"}
|
||||
|
||||
--- keycloak-1 ---
|
||||
{"status":"DOWN","checks":[{"name":"GracefulShutdown","status":"UP"}
|
||||
{"name":"Keycloakdatabaseconnectionsasynchealthcheck","status":"UP"}
|
||||
{"name":"KeycloakInitialized","status":"UP"}
|
||||
|
||||
|
||||
=== 차단 해제 ===
|
||||
해제: 12:44:37
|
||||
+25초 keycloak-0:1/1 keycloak-1:0/1
|
||||
+50초 keycloak-0:1/1 keycloak-1:1/1
|
||||
→ 복구 완료
|
||||
keycloak-0 MergeView::[keycloak-0-24309(v=16.0.12)|15] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(
|
||||
|
||||
keycloak-1 MergeView::[keycloak-0-24309(v=16.0.12)|15] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# A-5 — 비대칭 파티션 증거
|
||||
|
||||
2026-09-04 12:28–12:46 KST
|
||||
해설: [`docs/experiment-a5-asymmetric-partition.md`](../../experiment-a5-asymmetric-partition.md)
|
||||
|
||||
| 파일 | 무엇을 보여주는가 |
|
||||
|---|---|
|
||||
| `01-injection.txt` | 첫 시도 — `iptables -I FORWARD 1` |
|
||||
| `02-injection-verify.txt` | **실패 확인** — kube-router 가 자기 체인을 위로 재삽입, DROP 규칙 **0 패킷** |
|
||||
| `03-raw-table-injection.txt` | `raw` 테이블로 이동. 그래도 0 패킷 |
|
||||
| `04-correct-direction.txt` | **연결 방향이 반대였다.** 수신측 노드로 옮기니 **19/21 패킷 차단** |
|
||||
| `05-reconnect-observed.txt` | **핵심** — 연결이 **열린 방향으로 뒤집혀 재연결**. 뷰 변화 없음, `suspected=0` |
|
||||
| `06-view-history-and-cleanup.txt` | 뷰 ID 이력. 주입 이후 변화 없음 확인 |
|
||||
| `07-bidirectional-block.txt` | 양방향 차단 → **뷰 14, 멤버 1개씩.** 그런데 `ready=[10.42.1.77]` 로 **한쪽은 남고 외부 200** |
|
||||
| `08-coordinator-and-recovery.txt` | **`coord=t` 가 둘**(split brain). keycloak-0 `UP` / keycloak-1 `DOWN`. 해제 후 50초, `MergeView |15] (2)` |
|
||||
| `a5-cluster-size-bidirectional-block.png` | Grafana — `vendor_cluster_size` 가 갈라졌다 합쳐지는 구간 |
|
||||
|
||||
## 핵심 네 줄
|
||||
|
||||
1. **주입을 세 번 실패했다** — CNI 체인 경쟁, 연결 방향 오판, conntrack. 셋 다 "아무 일 없음"으로 보였다.
|
||||
2. **단방향 차단으로는 못 가른다.** JGroups 가 열린 방향으로 재연결한다 (자가 치유).
|
||||
3. **양방향이면 갈라진다** — `coord=t` 가 둘.
|
||||
4. **그래도 전면 장애가 아니다.** 코디네이터 쪽만 UP 을 유지해 서비스가 계속된다 — A-1 의 열린 질문에 대한 답.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Reference in New Issue
Block a user