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>
550 lines
21 KiB
Markdown
550 lines
21 KiB
Markdown
# A-1 — 노드 간 통신(TCP 7800)을 끊으면 무엇이 깨지는가
|
||
|
||
브랜치 `feature/keycloak-a1-jgroups-transport-block` ·
|
||
증거 [`docs/evidence/a1-jgroups-transport-block/`](evidence/a1-jgroups-transport-block/) ·
|
||
2026-09-04 11:38–11:52 KST · Keycloak 26.7.0 / Infinispan 16.0.12
|
||
|
||
맥락은 [`session-lab-prerequisites.md`](session-lab-prerequisites.md),
|
||
기준선은 [`experiment-00-session-replication.md`](experiment-00-session-replication.md).
|
||
|
||
---
|
||
|
||
## 0. 결론부터
|
||
|
||
| 예측 | 결과 |
|
||
|---|---|
|
||
| 세션 공유는 **안 깨진다** | **맞다.** 교차 노드 refresh 가 `200` |
|
||
| 로그아웃 전파는 **안 깨진다** | **틀렸다.** `400` 이어야 할 것이 `200` |
|
||
| — | **NetworkPolicy 만으로는 분단이 일어나지 않는다** (예상 못 함) |
|
||
| — | **분단된 노드가 스스로 로드밸런서에서 빠진다** (예상 못 함) |
|
||
|
||
**예측 하나가 빗나갔고, 예상하지 못한 것이 둘 나왔다.** 그중 하나는
|
||
실험 방법 자체를 무효화할 뻔했다.
|
||
|
||
---
|
||
|
||
## 1. 왜 이 실험인가
|
||
|
||
A-0 에서 **세션은 Infinispan 복제가 아니라 PostgreSQL 로 공유된다**는 것을
|
||
측정했다. 그렇다면 통념과 정면으로 어긋난다.
|
||
|
||
| | |
|
||
|---|---|
|
||
| **통념** (Keycloak 24 이전 자료) | 세션은 7800 으로 복제된다 → **막으면 세션 공유가 깨진다** |
|
||
| **A-0 측정** | 세션은 DB 로 공유된다 → **막아도 안 깨진다** |
|
||
|
||
둘 중 하나는 틀렸고, 이 실험이 판정한다.
|
||
|
||
---
|
||
|
||
## 2. 기준선
|
||
|
||
```
|
||
=== [기준선 1] 클러스터 뷰 ===
|
||
keycloak-0: [keycloak-1-48749|5] (2) [keycloak-1-48749, keycloak-0-30843]
|
||
keycloak-1: [keycloak-1-48749|5] (2) [keycloak-1-48749, keycloak-0-30843]
|
||
|
||
=== [기준선 2] JGROUPS_PING ===
|
||
keycloak-0-30843 | 10.42.1.43:7800 | f
|
||
keycloak-1-48749 | 10.42.0.35:7800 | t ← 코디네이터는 하나
|
||
|
||
=== [기준선 4] JGroups 지표 (양쪽 동일) ===
|
||
fd_sock2_get_num_suspected_members 0.0
|
||
merge3_get_num_merge_events 0.0
|
||
nakack2_get_xmit_table_missing 0.0
|
||
```
|
||
|
||
**대조군** — 차단 전에 같은 절차를 그대로 한 번 돌린다.
|
||
|
||
```
|
||
=== [대조군] keycloak-0 로그인 → keycloak-1 에서 refresh ===
|
||
sid tAWs2gCPr6SOcD4jDR9-_CzB
|
||
keycloak-1 에서 refresh: 200
|
||
```
|
||
|
||
A-0 에서 배운 규칙이다 — **시험군만 재는 측정은 측정이 아니다.**
|
||
|
||
---
|
||
|
||
## 3. 주입 — NetworkPolicy 로 7800 만 막는다
|
||
|
||
```bash
|
||
kubectl apply -f deploy/lab/k8s/a1-block-jgroups-transport.yaml
|
||
```
|
||
|
||
```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 이 파드를 죽인다** — 엉뚱한 이유로 클러스터가 깨진다 |
|
||
|
||
적용 직후 확인했다.
|
||
|
||
```
|
||
파드 상태: keycloak-0 ready=true restarts=0
|
||
keycloak-1 ready=true restarts=0
|
||
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
|
||
```
|
||
|
||
**주입이 의도한 것만 건드렸음을 먼저 확인한 뒤에 결과를 해석한다.**
|
||
|
||
---
|
||
|
||
## 4. 문제 ① — **NetworkPolicy 만으로는 분단이 안 된다**
|
||
|
||
가장 중요한 발견이며, 하마터면 **실험 전체를 무효로 만들 뻔했다.**
|
||
|
||
차단 후 지표가 꿈쩍도 하지 않았다. 신규 연결은 분명히 막히는데.
|
||
|
||
```
|
||
=== 7800 신규 연결 ===
|
||
10.42.1.43:7800 curl exit=7 (연결 실패)
|
||
10.42.1.43:9000 curl exit=28 (연결됨, telnet 이라 대기 → 타임아웃)
|
||
```
|
||
|
||
그런데 파드 내부 소켓을 보니
|
||
|
||
```
|
||
=== /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)
|
||
```
|
||
|
||
**기존 연결이 멀쩡히 살아 있다.**
|
||
|
||
### 왜 그런가 — conntrack
|
||
|
||
```
|
||
패킷 도착
|
||
│
|
||
├─▶ [ conntrack: ESTABLISHED/RELATED 이면 ACCEPT ] ← 여기서 통과해버린다
|
||
│
|
||
└─▶ [ NetworkPolicy 규칙 평가 ] ← 여기까지 오지 않는다
|
||
```
|
||
|
||
리눅스 방화벽은 성능을 위해 **이미 성립한 연결을 먼저 통과**시킨다.
|
||
NetworkPolicy 는 그 뒤에 있으므로 **신규 연결(SYN)만** 걸러낸다.
|
||
|
||
```
|
||
=== conntrack 확인 ===
|
||
tcp 6 86398 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=40023 dport=7800 ... [ASSURED]
|
||
tcp 6 79982 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=50477 dport=57800 ... [ASSURED]
|
||
tcp 6 33 SYN_SENT src=10.42.1.58 dst=10.42.0.35 sport=34824 dport=7800 [UNREPLIED]
|
||
─────────────────────────────────────────────────────────────
|
||
신규 연결은 응답을 못 받는다 = 정책이 동작하고는 있다
|
||
```
|
||
|
||
> **운영적 함의 — NetworkPolicy 는 이미 붙어 있는 것을 떼어내지 못한다.**
|
||
> 보안 사고 대응으로 "지금 당장 이 통신을 끊어라"에 NetworkPolicy 를 적용하면,
|
||
> **새 연결만 막히고 진행 중인 연결은 계속된다.** 끊으려면 conntrack 을 지우거나
|
||
> 파드를 재시작해야 한다.
|
||
|
||
### 덤 — **57800 포트도 있다**
|
||
|
||
`sport=50477 dport=57800` — FD_SOCK2 는 **`bind_port + 50000`** 을 쓴다.
|
||
7800 만 막고 57800 을 열어두면 장애 감지 채널이 남는다.
|
||
이 실험의 허용 목록 방식은 **둘 다 자동으로 막았다** — 8080·9000 외 전부 거부이므로.
|
||
|
||
### 조치
|
||
|
||
```bash
|
||
# 정확한 튜플로 지정해야 지워진다. --dport 만으로는 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 # 역방향
|
||
```
|
||
|
||
**양쪽 노드에서, 양쪽 방향으로** 지워야 한다. 서버 쪽 노드에는 튜플이 뒤집혀
|
||
기록되어 있다.
|
||
|
||
### ★ 정정 — conntrack 삭제가 분단을 만들었다고 볼 근거가 없다
|
||
|
||
이 문서는 처음에 이렇게 썼다.
|
||
|
||
```
|
||
11:41 conntrack 삭제
|
||
11:44 cluster_size 2 → 1 ← 약 3분 뒤
|
||
```
|
||
|
||
**증거를 다시 보면 그 인과가 성립하지 않는다.**
|
||
|
||
| 시각 | 증거 |
|
||
|---|---|
|
||
| 11:41 | conntrack 삭제. 직후 `07-cluster-size.txt` 는 **11:45 까지 전부 `2`** |
|
||
| **11:44:23** | **`keycloak-0` 파드의 `startTime`** — 스스로 재시작했다 |
|
||
| 11:44:27 | `cluster_size` 2 → 1 |
|
||
| 11:46:07 | 내가 `delete pod` 를 실행 (이미 떨어진 뒤) |
|
||
|
||
**하락은 conntrack 삭제 3분 뒤가 아니라 파드 재시작 4초 뒤에 일어났다.**
|
||
같은 문서 6절이 "정책이 걸린 채 재시작되자" 라고 쓴 것이 맞고,
|
||
**4절의 "conntrack 삭제 → 3분 뒤 분단" 은 시각이 겹친 것을 인과로 읽은 것이다.**
|
||
|
||
conntrack 삭제 자체가 무의미했다는 뜻은 아니다 — 다만 **이 실험은
|
||
그것만으로 분단이 되는지 판정하지 못했다.** 판정한 것은 A-5 이고,
|
||
거기서 `raw` 테이블이 필요하다는 것이 드러났다.
|
||
|
||
> TCP 가 재전송 타임아웃으로 상대를 알아채는 것은 사실이지만,
|
||
> **이 실험에서 그 경로가 발동했다는 증거는 없다.**
|
||
|
||
---
|
||
|
||
## 5. 문제 ② — 계측 도구가 잘못됐다
|
||
|
||
임시 curl 파드로 20초마다 지표를 긁었더니 이런 결과가 나왔다.
|
||
|
||
```
|
||
+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` 은 매번 파드를
|
||
만들고 지우므로 느리고 경합이 있다.
|
||
|
||
게다가 첫 시도의 판정 조건이
|
||
|
||
```sh
|
||
[ "$R" != "0.0 0.0 " ] && echo "→ 변화 감지" && break
|
||
```
|
||
|
||
여서 **빈 문자열을 "변화"로 읽고 즉시 빠져나왔다.** A-0 에서 똑같은 실수를
|
||
했는데 또 했다.
|
||
|
||
> **임시 파드는 계측 도구가 아니다.** 15초마다 이미 긁고 있는 Prometheus 가
|
||
> 그러라고 있는 것이다.
|
||
|
||
```bash
|
||
kubectl -n observability port-forward svc/prometheus 19090:9090 &
|
||
curl -s "http://localhost:19090/api/v1/query_range?query=vendor_cluster_size&start=$START&end=$END&step=60"
|
||
```
|
||
|
||
그리고 이 과정에서 **`vendor_cluster_size`** 를 발견했다 — 멤버 수를 직접
|
||
알려주는 지표다. 처음부터 이걸 봤어야 했다.
|
||
|
||
```bash
|
||
curl -s "http://localhost:19090/api/v1/label/__name__/values" | grep -E "cluster|member|view"
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 진짜 분단이 일어난 순간
|
||
|
||
```
|
||
=== vendor_cluster_size ===
|
||
keycloak-1: 11:43:57=2 11:44:27=1 ... 11:51:28=2
|
||
keycloak-0: 11:43:57=2 (파드 교체) 11:45:27=1 ... 11:51:28=2
|
||
```
|
||
|
||

|
||
|
||
정책이 걸린 채 `keycloak-0` 이 재시작되자, 로그가 정확히 말해준다.
|
||
|
||
```
|
||
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-1` 쪽도 혼자가 되었다.
|
||
|
||
```
|
||
ISPN000094: [keycloak-1-48749|6] (1) [keycloak-1-48749]
|
||
```
|
||
|
||
### **DB 에는 둘 다 있는데 클러스터는 안 붙는다** — 예측한 그 상태
|
||
|
||
```
|
||
=== JGROUPS_PING ===
|
||
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 이며, **데이터베이스 한 줄로
|
||
확인된다.** 디스커버리(DB)는 살아 있고 트랜스포트(7800)만 죽은 상태다.
|
||
|
||
**단일 노드에서는 만들 수 없는 고장**이며, 이 실험대를 2 VM 으로 만든 이유다.
|
||
|
||
---
|
||
|
||
## 7. 본 시험 — 분단 상태에서 세션은 어떻게 되는가
|
||
|
||
```
|
||
[1] keycloak-0 로그인 sid=nShl5TaBrZnKStDqaspjgmJB
|
||
[2] keycloak-1 에서 refresh HTTP 200 ← 예측대로
|
||
[3] keycloak-1 에서 로그아웃 HTTP 204
|
||
[4] keycloak-0 에서 재갱신 시도 HTTP 200 ← 400 이어야 했다
|
||
```
|
||
|
||
### [2] 세션 공유 — **예측이 맞았다**
|
||
|
||
클러스터가 갈라졌는데도 **한쪽에서 만든 세션을 반대쪽이 갱신했다.**
|
||
A-0 의 모델이 맞고, **통념이 틀렸다.** 세션은 7800 으로 다니지 않는다.
|
||
|
||
### [4] 로그아웃 전파 — **예측이 틀렸다**
|
||
|
||
A-0 에서는 같은 절차가 `400 invalid_grant / Session not active` 였다.
|
||
분단 상태에서는 `200` 이다. **로그아웃한 세션이 반대편에서 살아 있다.**
|
||
|
||
기제를 확정했다.
|
||
|
||
```
|
||
=== 그 sid 가 DB 에 남아 있는가 ===
|
||
user_session_id | offline_flag | last_session_refresh
|
||
-----------------+--------------+----------------------
|
||
(0 rows) ← DB 행은 삭제되었다
|
||
|
||
=== 노드별 세션 캐시 엔트리 ===
|
||
keycloak-1 kc-lab-1 = 0
|
||
keycloak-0 kc-lab-2 = 1 ← 캐시에는 남아 있다
|
||
```
|
||
|
||
```
|
||
keycloak-1 로그아웃
|
||
│
|
||
├──▶ PostgreSQL 행 삭제 ✔ 되었다
|
||
│
|
||
└──▶ keycloak-0 에게 "캐시에서 지워라" ✗ 7800 이 막혀 못 갔다
|
||
│
|
||
keycloak-0 은 자기 캐시로 200 을 준다 ◀────────────┘
|
||
```
|
||
|
||
### **A-0 의 결론을 정정한다**
|
||
|
||
A-0 에서 나는 이렇게 썼다.
|
||
|
||
> 로그아웃과 함께 DB 행이 사라졌다 → 무효화가 DB 삭제로 전파된다
|
||
|
||
**그 인과는 틀렸다.** DB 행 삭제는 일어나지만, **반대편 노드는 DB 를 다시
|
||
읽지 않는다.** 자기 캐시에 있으면 그걸로 답한다.
|
||
|
||
> **룩어사이드 캐시는 읽을 때 DB 와 대조하지 않는다.**
|
||
> 캐시 무효화는 **클러스터 메시지(7800)를 타고** 간다.
|
||
|
||
A-0 에서 400 이 나온 것은 DB 덕분이 아니라 **그때는 7800 이 살아 있어서**였다.
|
||
두 실험을 붙여야 비로소 정확한 그림이 나온다.
|
||
|
||
| | 세션 **조회** | 세션 **무효화** |
|
||
|---|---|---|
|
||
| 경로 | PostgreSQL | **클러스터 메시지 (7800)** |
|
||
| 7800 차단 시 | 정상 | **전파되지 않음** |
|
||
|
||
---
|
||
|
||
## 8. 그런데 안전장치가 있었다 — 예상 못 한 발견
|
||
|
||
`keycloak-0` 이 `Ready=false` 였다. 이유를 물었더니
|
||
|
||
```json
|
||
{ "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 로 신고한다.** 그리고 쿠버네티스가
|
||
그 신고를 받아 처리했다.
|
||
|
||
```
|
||
=== Service 엔드포인트 ===
|
||
ready 주소: [10.42.0.35] ← keycloak-1 만 트래픽을 받는다
|
||
notReady : [10.42.1.67] ← keycloak-0 은 제외되었다
|
||
|
||
=== 외부 진입점 ===
|
||
https://auth.hyeonworks.com/realms/master HTTP 200
|
||
토큰 발급 HTTP 200
|
||
```
|
||
|
||
**분단된 노드가 스스로 로드밸런서에서 빠졌고, 서비스는 계속되었다.**
|
||
|
||
### 그래서 7절의 로그아웃 우회는 어떻게 봐야 하나
|
||
|
||
| | |
|
||
|---|---|
|
||
| 내가 한 것 | Service 를 우회해 **파드 IP 로 직접** 호출 |
|
||
| 실제 사용자 | nginx → Traefik → **Service** → Ready 인 파드만 |
|
||
|
||
**정문으로 들어오면 낡은 캐시에 닿지 않는다.** readiness 게이트가 막는다.
|
||
|
||
> 다만 이건 **비대칭이라서 살았다.** `keycloak-1` 은 원래 뷰에서 멤버가 하나
|
||
> 줄어든 정상적인 사건이라 Ready 를 유지했고, `keycloak-0` 은 합류 자체를
|
||
> 못 해 DOWN 이 되었다. **양쪽이 동시에 DOWN 이 되는 경로가 있다면 전면 장애다.**
|
||
> A-5(비대칭 파티션)에서 이어서 본다.
|
||
|
||
---
|
||
|
||
## 9. 복구
|
||
|
||
```bash
|
||
kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
|
||
```
|
||
|
||
```
|
||
+30초 keycloak-0=1 keycloak-1=1
|
||
+60초 keycloak-0=1 keycloak-1=1
|
||
+90초 keycloak-0=2 keycloak-1=2 ← 재형성
|
||
```
|
||
|
||
**90초 만에 자동으로 다시 붙었다. 사람 손이 필요 없었다.**
|
||
|
||
```
|
||
=== MERGE3 가 합쳤는가 ===
|
||
merge_events keycloak-0 = 1
|
||
merge_events keycloak-1 = 1
|
||
```
|
||
|
||
**MERGE3 가 한 일이다.** split brain 을 감지해 뷰를 병합하는 프로토콜이며,
|
||
지표가 `0 → 1` 로 올라간 것이 그 증거다.
|
||
|
||
```
|
||
=== JGROUPS_PING ===
|
||
keycloak-0-26403 | 10.42.1.67:7800 | t
|
||
keycloak-1-48749 | 10.42.0.35:7800 | f ← 코디네이터가 하나로 돌아왔다
|
||
```
|
||
|
||
**코디네이터가 keycloak-1 에서 keycloak-0 으로 넘어갔다.** 코디네이터는
|
||
특권이 아니라 역할이며, 병합 시 재선출된다.
|
||
|
||
---
|
||
|
||
## 10. 개념 정리
|
||
|
||
### conntrack — 연결 추적
|
||
|
||
리눅스 커널이 **진행 중인 연결을 기억**하는 표. 패킷마다 규칙을 다시 평가하지
|
||
않기 위해 존재한다.
|
||
|
||
| 상태 | 뜻 |
|
||
|---|---|
|
||
| `NEW` | 첫 패킷(SYN) |
|
||
| **`ESTABLISHED`** | **양방향 통신이 성립함 — 규칙 평가를 건너뛴다** |
|
||
| `[ASSURED]` | 충분히 오래된 연결. 표가 꽉 차도 안 지워진다 |
|
||
| `SYN_SENT [UNREPLIED]` | 보냈는데 답이 없음 = **차단되고 있다** |
|
||
|
||
```bash
|
||
sudo conntrack -L | grep 7800
|
||
sudo conntrack -D -p tcp -s <src> -d <dst> --sport <sp> --dport <dp>
|
||
```
|
||
|
||
### FD_SOCK2 와 포트 규약
|
||
|
||
| 프로토콜 | 포트 | 하는 일 |
|
||
|---|---|---|
|
||
| TCP (트랜스포트) | **7800** | 클러스터 메시지 |
|
||
| **FD_SOCK2** | **57800** = 7800 + 50000 | 소켓으로 상대 생존 감시 |
|
||
|
||
**방화벽 규칙을 손으로 쓸 때 57800 을 빠뜨리기 쉽다.**
|
||
|
||
### MERGE3
|
||
|
||
split brain 이 생긴 뒤 **갈라진 뷰를 다시 합치는** JGroups 프로토콜.
|
||
주기적으로 다른 코디네이터의 존재를 확인하고, 발견하면 병합을 개시한다.
|
||
|
||
```promql
|
||
vendor_jgroups_merge3_get_num_merge_events
|
||
```
|
||
|
||
### readiness 프로브와 Service 엔드포인트
|
||
|
||
```
|
||
readiness 실패 → 파드가 Service 의 notReadyAddresses 로 이동
|
||
→ kube-proxy 가 그 파드로 라우팅하지 않음
|
||
→ 살아 있지만 트래픽은 안 받음
|
||
```
|
||
|
||
**liveness 와 다르다.** liveness 실패는 **재시작**, readiness 실패는
|
||
**격리**다. 클러스터 분단처럼 "재시작해도 안 나아지는" 문제에는 readiness 가
|
||
맞는 신호다.
|
||
|
||
---
|
||
|
||
---
|
||
|
||
## 증거 파일
|
||
|
||
**증거 수집 시각: 2026-09-04 11:34 – 11:50 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
|
||
|
||
| 파일 | 종류 |
|
||
|---|---|
|
||
| [`01-baseline-cluster.txt`](evidence/a1-jgroups-transport-block/01-baseline-cluster.txt) | 터미널 원문 |
|
||
| [`02-control-before-block.txt`](evidence/a1-jgroups-transport-block/02-control-before-block.txt) | 터미널 원문 |
|
||
| [`03-block-applied.txt`](evidence/a1-jgroups-transport-block/03-block-applied.txt) | 터미널 원문 |
|
||
| [`04-after-block-state.txt`](evidence/a1-jgroups-transport-block/04-after-block-state.txt) | 터미널 원문 |
|
||
| [`05-conntrack-problem.txt`](evidence/a1-jgroups-transport-block/05-conntrack-problem.txt) | 터미널 원문 |
|
||
| [`06-partition-observed.txt`](evidence/a1-jgroups-transport-block/06-partition-observed.txt) | 터미널 원문 |
|
||
| [`07-cluster-size.txt`](evidence/a1-jgroups-transport-block/07-cluster-size.txt) | 터미널 원문 |
|
||
| [`08-restart-forced-partition.txt`](evidence/a1-jgroups-transport-block/08-restart-forced-partition.txt) | 터미널 원문 |
|
||
| [`09-cross-node-under-partition.txt`](evidence/a1-jgroups-transport-block/09-cross-node-under-partition.txt) | 터미널 원문 |
|
||
| [`10-logout-not-propagated.txt`](evidence/a1-jgroups-transport-block/10-logout-not-propagated.txt) | 터미널 원문 |
|
||
| [`11-service-impact.txt`](evidence/a1-jgroups-transport-block/11-service-impact.txt) | 터미널 원문 |
|
||
| [`12-recovery.txt`](evidence/a1-jgroups-transport-block/12-recovery.txt) | 터미널 원문 |
|
||
| [`a1-cluster-size-partition-recovery.png`](evidence/a1-jgroups-transport-block/a1-cluster-size-partition-recovery.png) | 스크린샷 |
|
||
|
||
파일별 상세는 [`evidence/a1-jgroups-transport-block/README.md`](evidence/a1-jgroups-transport-block/README.md).
|
||
|
||
## 11. 재현 절차 (명령어)
|
||
|
||
```bash
|
||
# 0. 기준선
|
||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "select name, ip, coord from jgroups_ping order by name"
|
||
kubectl -n observability port-forward svc/prometheus 19090:9090 &
|
||
curl -s "http://localhost:19090/api/v1/query?query=vendor_cluster_size"
|
||
|
||
# 1. 차단
|
||
kubectl apply -f deploy/lab/k8s/a1-block-jgroups-transport.yaml
|
||
|
||
# 2. 주입이 의도한 것만 건드렸는지 확인 (8080/9000 은 살아 있어야 한다)
|
||
kubectl -n keycloak-lab get pods -o wide | grep keycloak # restarts=0 확인
|
||
|
||
# 3. 기존 연결이 남아 있음을 확인 — 이걸 안 하면 실험이 무효다
|
||
ssh kc-lab-1 'sudo conntrack -L | grep 7800'
|
||
|
||
# 4. conntrack 삭제 (양쪽 노드, 양쪽 방향). 반영까지 약 3분
|
||
ssh kc-lab-1 'sudo conntrack -D -p tcp -s <k1ip> -d <k0ip> --sport <sp> --dport 7800'
|
||
ssh kc-lab-2 'sudo conntrack -D -p tcp -s <k0ip> -d <k1ip> --sport 7800 --dport <sp>'
|
||
|
||
# 5. 분단 확인
|
||
curl -s "http://localhost:19090/api/v1/query?query=vendor_cluster_size"
|
||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "select name, coord from jgroups_ping" # coord=t 가 둘이면 split brain
|
||
|
||
# 6. 복구
|
||
kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
|
||
curl -s "http://localhost:19090/api/v1/query?query=vendor_jgroups_merge3_get_num_merge_events"
|
||
```
|
||
|
||
---
|
||
|
||
## 12. 다음 실험에 남기는 것
|
||
|
||
| 실험 | 이 실험이 준 것 |
|
||
|---|---|
|
||
| **A-5** 비대칭 파티션 | **양쪽이 동시에 NotReady 가 되는 경로가 있는가.** 여기서는 비대칭이라 살았다 |
|
||
| **A-2** DB 정지 | 캐시가 DB 와 대조하지 않는다는 사실 → **캐시에 있는 세션은 DB 없이도 읽힐 수 있다** |
|
||
| **A-7** volatile 비교 | 같은 주입에서 세션 공유가 **깨져야** 한다. 이 실험이 그 대조군 |
|
||
| 전체 | **주입이 실제로 걸렸는지 먼저 확인한다.** NetworkPolicy 는 기존 연결을 못 끊는다 |
|