Twenty experiments across four layers, each with a topology diagram marking where the fault goes in, the metrics to watch, a falsifiable prediction written before the run, and a pass/fail rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
796 lines
34 KiB
Markdown
796 lines
34 KiB
Markdown
# 실험 실행 계획 — 구조 · 주입 지점 · 확인 항목
|
|
|
|
[`session-store-lab-roadmap.md`](session-store-lab-roadmap.md)가 **무엇을 할지**의
|
|
지도라면, 이 문서는 **각 실험을 어떤 구조에서 어디를 건드려 무엇을 볼지**를
|
|
미리 확정해둔 것이다.
|
|
|
|
**실험 전에 예측을 적어둔다.** 결과를 보고 나서 "그럴 줄 알았다"고 쓰면
|
|
아무것도 배우지 못한다. 예측이 빗나갔을 때가 배울 거리다.
|
|
|
|
맥락이 안 잡히면 [`session-lab-prerequisites.md`](session-lab-prerequisites.md)
|
|
먼저.
|
|
|
|
---
|
|
|
|
## 1. 기준 구조 — 모든 실험이 여기서 출발한다
|
|
|
|
```
|
|
[개발 노트북] 브라우저 · kubectl · Playwright
|
|
│
|
|
│ https (tailnet 100.x, split DNS)
|
|
▼
|
|
┌──────────────────────────────────────────────────────────────┐
|
|
│ test-server Arch Linux · 12GB RAM · WiFi only │
|
|
│ │
|
|
│ nginx :443 ← TLS 종료 (Let's Encrypt, DNS-01) │
|
|
│ auth.hyeonworks.com / app1 / app2 │
|
|
└───────────────────────────┬───────────────────────────────────┘
|
|
│ http, libvirt NAT (virbr0)
|
|
┌───────────────┴────────────────┐
|
|
▼ ▼
|
|
┌───────────────────────┐ ┌───────────────────────┐
|
|
│ kc-lab-1 5120MB │ │ kc-lab-2 4096MB │
|
|
│ k3s server │ │ k3s agent │
|
|
│ 10.42.0.0/24 │ │ 10.42.1.0/24 │
|
|
│ │ │ │
|
|
│ traefik ← 진입점 │ │ │
|
|
│ coredns │ │ │
|
|
│ metrics-server │ │ │
|
|
│ local-path-prov. │ │ │
|
|
│ │◀ 7800 ▶│ │
|
|
│ keycloak-1 │ JGroups│ keycloak-0 │
|
|
│ │ │ postgres (PVC) │
|
|
│ prometheus (PVC) │ │ │
|
|
│ grafana │ │ │
|
|
└───────────────────────┘ └───────────────────────┘
|
|
▲ ▲
|
|
│ 관측 스택은 여기 고정 │ 장애 주입은 여기
|
|
│ (nodeSelector 로 못박음) │
|
|
```
|
|
|
|
### 배치 규칙 — 왜 이렇게 두었는가
|
|
|
|
| 규칙 | 이유 |
|
|
|---|---|
|
|
| 관측 스택은 **kc-lab-1 고정** | 관측 시스템이 관측 대상과 같이 죽으면 기록이 없다 |
|
|
| 장애 주입은 **kc-lab-2** | server 를 죽이면 노드 상실이 아니라 **컨트롤 플레인 상실**이다 |
|
|
| postgres 는 **kc-lab-2** | DB 손실과 노드 손실을 한 실험에서 겹쳐 볼 수 있다 |
|
|
| PVC 는 노드에 못박힘 | `local-path` 프로비저너. **결함이 아니라 조건** |
|
|
|
|
### 공통 관측 지점
|
|
|
|
이후 모든 실험에서 이 표의 것들을 본다.
|
|
|
|
| 무엇 | 어디서 | 값의 뜻 |
|
|
|---|---|---|
|
|
| **`up`** | Prometheus | **0이라는 값으로 남는다.** 죽은 시각을 사후에 특정할 수 있는 유일한 지표 |
|
|
| `vendor_statistics_approximate_entries_unique{cache="sessions"}` | KC :9000 | 노드별 세션 캐시 보유량 |
|
|
| `vendor_jgroups_fd_sock2_get_num_suspected_members` | KC :9000 | **상대를 의심하기 시작한 순간** |
|
|
| `vendor_jgroups_merge3_get_views` | KC :9000 | split brain 후 재병합 |
|
|
| `ISPN000094` | `kubectl logs` | 새 클러스터 뷰(멤버 명단) |
|
|
| `JGROUPS_PING` 테이블 | PostgreSQL | 디스커버리 등록 상태 |
|
|
| `offline_user_session` (`offline_flag='0'`) | PostgreSQL | **온라인 세션의 진실** |
|
|
| `node_load1`, `node_memory_*` | node-exporter | 기계가 죽었나 프로세스가 죽었나 |
|
|
|
|
---
|
|
|
|
## 2. 전체 목록
|
|
|
|
| # | 실험 | 답하는 실무 질문 | 상태 | 선행 |
|
|
|---|---|---|---|---|
|
|
| **A-0** | 세션 복제 확인 | 정상일 때 세션은 어디 있나 | ✅ | — |
|
|
| **A-1** | TCP 7800 차단 | 노드 간 통신이 끊기면? | 다음 | A-0 |
|
|
| **A-2** | DB 정상 정지 | **DB가 죽으면?** | | A-0 |
|
|
| **A-3** | DB 강제 종료 | 복구 시 무엇을 잃나 (RPO) | | A-2 |
|
|
| **A-4** | 노드 전원 차단 | **Keycloak 한 대가 죽으면?** | | A-0 |
|
|
| **A-5** | 비대칭 파티션 | 한쪽만 안 들리면? (split brain) | | A-1 |
|
|
| **A-6** | 지연 주입 (netem) | 느려지기만 하면? | | A-1 |
|
|
| **A-7** | volatile 모드 비교 | 옛 방식(KC≤24)은 뭐가 다른가 | | A-1·A-2·A-4 |
|
|
| **A-8** | 롤링 재시작 | 배포하면 로그아웃되나 | | A-0 |
|
|
| **B-1** | BFF + Redis 저장소 | Redis vs JDBC 어느 쪽인가 → **Q3** | | 배포 필요 |
|
|
| **B-2** | BFF 다중 인스턴스 | 인스턴스 늘리면? → **Q1** | | B-1 |
|
|
| **B-3** | Refresh 토큰 경쟁 | 동시 갱신하면? → **Q2** | | B-1 |
|
|
| **B-4** | Edge 인가 범위 | 인가를 Edge에 얼마나? → **Q4** | | B-1 |
|
|
| **B-5** | Redis 상실 | Redis가 죽으면? | | B-1 |
|
|
| **B-6** | Redis 영속화 비교 | AOF/RDB 유무 차이 | | B-5 |
|
|
| **C-1** | 다중 앱 SSO | **SSO 붙이면 뭐가 달라지나** | | B-1 |
|
|
| **C-2** | 백채널 로그아웃 | 로그아웃이 전 앱에 퍼지나 | | C-1 |
|
|
| **D-1** | 백업·복구 리허설 | **복구해본 적 있나** | | A-2 |
|
|
| **D-2** | 버전 업그레이드 | 올릴 때 세션·스키마는? | | D-1 |
|
|
| **D-3** | 비밀 관리 | Secret은 정말 감춰지나 | | — |
|
|
| **D-4** | 인증서 갱신 | 갱신 중 무중단인가 | | — |
|
|
|
|
---
|
|
|
|
# A층 — Keycloak 자체
|
|
|
|
## A-1. TCP 7800 차단
|
|
|
|
### 답하려는 질문
|
|
> 노드 사이 네트워크가 끊기면 로그인한 사용자는 어떻게 되는가?
|
|
|
|
**이 실험이 A-0 다음인 이유** — 통념과 측정이 정면으로 어긋나 있다.
|
|
둘 중 하나는 틀렸고, 이 실험이 판정한다.
|
|
|
|
| | |
|
|
|---|---|
|
|
| **통념** (KC 24 이전 자료) | 세션은 7800으로 복제된다 → **막으면 세션 공유가 깨진다** |
|
|
| **A-0 측정** | 세션은 DB로 공유된다 → **막아도 안 깨진다** |
|
|
|
|
### 구조 — 주입 지점
|
|
|
|
```
|
|
kc-lab-1 kc-lab-2
|
|
┌────────────────┐ ┌────────────────┐
|
|
│ keycloak-1 │ ✂✂✂ 7800 차단 ✂✂✂ │ keycloak-0 │
|
|
│ │ (NetworkPolicy) │ │
|
|
└───────┬────────┘ └───────┬────────┘
|
|
│ │
|
|
│ 둘 다 DB 는 그대로 본다 │
|
|
└──────────────┬──────────────────────┘
|
|
▼
|
|
┌───────────────┐
|
|
│ postgres │ ← 여기는 안 건드린다
|
|
│ JGROUPS_PING │ 디스커버리는 계속 된다
|
|
│ 세션 테이블 │
|
|
└───────────────┘
|
|
```
|
|
|
|
**핵심**: 디스커버리(DB)는 살리고 **트랜스포트(7800)만** 끊는다.
|
|
그래서 "DB엔 서로 등록되는데 클러스터는 안 붙는" 상태가 만들어진다.
|
|
**단일 노드에서는 재현 불가능한 고장**이며, 이 실험대를 2노드로 만든 이유다.
|
|
|
|
### 주입 방법
|
|
|
|
```yaml
|
|
# NetworkPolicy 로 파드 간 7800 만 막는다. IP 가 아니라 라벨로 지정한다
|
|
# (파드 IP 는 재시작마다 바뀐다 — 2홉 실험에서 확인)
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: NetworkPolicy
|
|
metadata: { name: block-jgroups, namespace: keycloak-lab }
|
|
spec:
|
|
podSelector: { matchLabels: { app: keycloak } }
|
|
policyTypes: [Ingress]
|
|
ingress:
|
|
- from: [{ podSelector: { matchLabels: { app: keycloak } } }]
|
|
ports: [{ port: 7800, protocol: TCP }] # ← 이 규칙을 뺀 채 적용하면 차단
|
|
```
|
|
|
|
### 확인할 것
|
|
|
|
| 관측 | 예상 |
|
|
|---|---|
|
|
| `fd_sock2_get_num_suspected_members` | **0 → 1** (가장 먼저 움직일 것) |
|
|
| `ISPN000094` 로그 | 각자 **멤버 1개짜리 뷰** 발행 (split brain) |
|
|
| `JGROUPS_PING` 테이블 | **양쪽 다 남아 있다** (디스커버리는 살아 있으므로) |
|
|
| **교차 노드 refresh** | **200 유지** ← 이게 판정의 핵심 |
|
|
| 로그아웃 전파 | **?** — DB 삭제로 되면 유지, 클러스터 메시지면 깨진다 |
|
|
| `sessions` 캐시 엔트리 | 변화 없음 |
|
|
|
|
### 예측
|
|
|
|
```
|
|
세션 공유 안 깨진다 (A-0 근거: 세션은 7800 으로 안 다닌다)
|
|
로그아웃 안 깨진다 (DB 행 삭제로 전파되므로)
|
|
realm 설정 변경 전파 깨진다 (work 캐시는 클러스터 메시지를 쓴다)
|
|
로그인 실패 횟수 집계 깨진다 (loginFailures 캐시)
|
|
```
|
|
|
|
### 판정 기준
|
|
|
|
| 결과 | 뜻 |
|
|
|---|---|
|
|
| 교차 refresh 가 **200** | A-0 모델이 맞다. 통념이 틀렸다 |
|
|
| 교차 refresh 가 **400/401** | **A-0 해석이 틀렸다.** 무엇을 놓쳤는지 다시 판다 |
|
|
|
|
### 복구
|
|
```bash
|
|
kubectl -n keycloak-lab delete networkpolicy block-jgroups
|
|
# MERGE3 가 뷰를 다시 합치는지 확인 — merge3_get_views 증가
|
|
```
|
|
|
|
---
|
|
|
|
## A-2. DB 정상 정지
|
|
|
|
### 답하려는 질문
|
|
> **PostgreSQL이 죽으면 어떻게 되는가?** 로그인한 사람은? 새 로그인은?
|
|
|
|
### 구조
|
|
|
|
```
|
|
kc-lab-1 kc-lab-2
|
|
┌────────────────┐ ┌────────────────┐
|
|
│ keycloak-1 │◀────── 7800 ──────▶│ keycloak-0 │
|
|
│ 캐시: 세션 N개 │ (살아 있다) │ 캐시: 세션 M개 │
|
|
└───────┬────────┘ └───────┬────────┘
|
|
│ │
|
|
└──────────────┬──────────────────────┘
|
|
▼
|
|
┌───────────────┐
|
|
│ postgres ✗ │ ← replicas=0 으로 정지
|
|
└───────────────┘
|
|
|
|
질문: 캐시에 세션을 들고 있는 노드는 그 세션을 계속 쓸 수 있는가?
|
|
```
|
|
|
|
**A-0에서 얻은 예측의 시험대다.** DB가 진실의 원천이라면, **캐시에 있어도
|
|
못 쓴다**는 것이 논리적 귀결이다. 하지만 룩어사이드 캐시라면 읽기는 될
|
|
수도 있다 — **이게 갈리는 지점**이다.
|
|
|
|
### 주입 방법
|
|
```bash
|
|
kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
|
```
|
|
|
|
### 확인할 것 — **세 가지 경로를 구분해서 본다**
|
|
|
|
| 경로 | 무엇을 보는가 | 예상 |
|
|
|---|---|---|
|
|
| **① 캐시 있는 노드에서 refresh** | 캐시가 DB를 대신할 수 있나 | **실패** (쓰기가 필요) |
|
|
| **② 캐시 없는 노드에서 refresh** | 완전히 DB 의존 | **실패** |
|
|
| **③ 새 로그인** | 쓰기 경로 | **실패** |
|
|
| ④ 이미 발급된 access token 검증 | 서명만 보면 되는 경로 | **성공** (60초 동안) |
|
|
|
|
**④가 중요하다.** DB가 죽어도 **access token 수명 동안은 리소스 서버가
|
|
정상 동작한다.** 이게 "짧은 access token + 긴 refresh token" 구조가
|
|
장애에 주는 유예 시간이다.
|
|
|
|
| 관측 | 예상 |
|
|
|---|---|
|
|
| `up{job="keycloak"}` | **1 유지** — 프로세스는 살아 있다 |
|
|
| KC `/health/ready` | **DOWN** 으로 바뀌는가? |
|
|
| KC 로그 | 커넥션 풀 고갈 / `FATAL: the database system is shutting down` |
|
|
| 에러 응답 코드 | 500? 503? — **클라이언트가 재시도할지 결정하는 값** |
|
|
|
|
### 예측
|
|
```
|
|
로그인 실패
|
|
refresh 실패 (캐시 유무 무관)
|
|
기존 access token 검증 성공 — 만료까지
|
|
KC 프로세스 살아 있음 (up=1) — 헬스체크만 DOWN
|
|
```
|
|
|
|
### 판정 기준
|
|
`up=1`인데 기능은 전멸 → **"프로세스 살아 있음"이 헬스체크로 부족하다는 증명.**
|
|
`/health/ready`가 DB를 보는지 여부가 롤링 배포 안전성을 좌우한다.
|
|
|
|
### 복구
|
|
```bash
|
|
kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
|
# 자동 복구되는가, Keycloak 재시작이 필요한가 ← 이것도 측정 대상
|
|
```
|
|
|
|
---
|
|
|
|
## A-3. DB 강제 종료 — 무엇을 잃는가 (RPO)
|
|
|
|
### 답하려는 질문
|
|
> 정상 종료가 아니라 **전원이 나가면** 무엇을 잃는가?
|
|
|
|
**A-0에서 발견한 `SET LOCAL synchronous_commit TO OFF`의 시험대다.**
|
|
|
|
```sql
|
|
update OFFLINE_USER_SESSION set LAST_SESSION_REFRESH=$1, VERSION=$2 where ...
|
|
SET LOCAL synchronous_commit TO OFF ← WAL 플러시를 기다리지 않고 커밋
|
|
COMMIT
|
|
```
|
|
|
|
### 구조
|
|
|
|
```
|
|
┌────────────────┐
|
|
│ keycloak-0 │ refresh 를 초당 여러 번 날린다
|
|
└───────┬────────┘
|
|
│ UPDATE LAST_SESSION_REFRESH
|
|
▼
|
|
┌───────────────────────────────┐
|
|
│ postgres │
|
|
│ 메모리 (WAL 버퍼) ← 여기까지만 쓰고 COMMIT 응답 │
|
|
│ ↓ 아직 안 내려감 │
|
|
│ 디스크 (WAL) │ ✗ SIGKILL
|
|
└───────────────────────────────┘
|
|
이 사이의 갱신이 사라진다
|
|
```
|
|
|
|
### 주입 방법
|
|
```bash
|
|
# 정상 종료가 아니라 강제 종료. 컨테이너에 SIGKILL
|
|
kubectl -n keycloak-lab delete pod -l app=postgres --grace-period=0 --force
|
|
```
|
|
|
|
### 확인할 것
|
|
|
|
| 절차 | 무엇을 보는가 |
|
|
|---|---|
|
|
| 1. refresh 를 반복하며 매번 `LAST_SESSION_REFRESH` 를 기록 | 기준값 |
|
|
| 2. 마지막 성공 시각을 클라이언트 쪽에 남긴다 | **클라이언트가 믿는 값** |
|
|
| 3. 강제 종료 → 재기동 | |
|
|
| 4. DB의 `LAST_SESSION_REFRESH` 를 다시 읽는다 | **2와 4의 차이 = 잃은 것** |
|
|
| 5. postgres 로그의 crash recovery 메시지 | `database system was not properly shut down` |
|
|
|
|
### 예측
|
|
```
|
|
잃는 양 수백 밀리초 분량의 LAST_SESSION_REFRESH 갱신
|
|
잃지 않는 것 로그인·로그아웃 자체 (다른 트랜잭션일 가능성 — 확인 필요)
|
|
crash recovery 자동 실행되고 성공
|
|
```
|
|
|
|
**"COMMIT 응답을 받았는데 없어졌다"를 실제로 관측하는 것이 목표다.**
|
|
이건 버그가 아니라 **의도된 트레이드오프**이며, 그걸 눈으로 봐야
|
|
"세션 갱신은 잃어도 되는 쓰기"라는 설계 판단을 체감할 수 있다.
|
|
|
|
---
|
|
|
|
## A-4. 노드 전원 차단
|
|
|
|
### 답하려는 질문
|
|
> **Keycloak 한 대가 통째로 죽으면** 그 노드가 처리하던 사용자는 어떻게 되는가?
|
|
|
|
### 구조
|
|
|
|
```
|
|
kc-lab-1 (살아 있다) kc-lab-2 ✗ 전원 차단
|
|
┌────────────────────┐ ┌────────────────────┐
|
|
│ traefik ← 진입점 │ │ keycloak-0 │
|
|
│ coredns │ │ 캐시 세션 M개 ✗ │
|
|
│ keycloak-1 │ │ postgres ✗ │ ← 같이 죽는다
|
|
│ prometheus │ │ │
|
|
└────────────────────┘ └────────────────────┘
|
|
|
|
⚠ postgres 가 kc-lab-2 에 있으므로 이 실험은 A-2 와 겹친다.
|
|
순수한 "노드 상실"을 보려면 postgres 를 먼저 kc-lab-1 로 옮긴다.
|
|
```
|
|
|
|
**중요** — `virsh destroy`는 프로세스 kill이 아니라 **전원 차단**이다.
|
|
게스트에 종료 신호가 가지 않으므로 진짜 노드 상실이다.
|
|
|
|
### 두 판본으로 나눠 실행한다
|
|
|
|
| 판본 | 배치 | 보는 것 |
|
|
|---|---|---|
|
|
| **4a. 순수 노드 상실** | postgres 를 kc-lab-1 로 옮긴 뒤 | Keycloak 한 대만 잃었을 때 |
|
|
| **4b. 복합 장애** | 현재 배치 그대로 | **노드 + DB 동시 상실** — 더 현실적 |
|
|
|
|
### 주입 방법
|
|
```bash
|
|
virsh destroy kc-lab-2 # ACPI 종료가 아니라 전원 차단
|
|
```
|
|
|
|
### 확인할 것
|
|
|
|
| 관측 | 예상 (4a) |
|
|
|---|---|
|
|
| `up{node="kc-lab-2"}` | **0** — 언제 죽었는지 기록됨 |
|
|
| 죽은 노드의 마지막 샘플 | node-exporter 가 DaemonSet 인 이유 |
|
|
| kubectl 노드 상태 | `Ready` → `NotReady` (기본 40초 후) |
|
|
| **죽은 노드가 만든 세션의 refresh** | **성공** ← 판정 핵심. DB 에 있으므로 |
|
|
| `ISPN000094` | 남은 노드가 멤버 1개 뷰 발행 |
|
|
| `JGROUPS_PING` | 죽은 노드 행이 **남아 있다** (지울 주체가 없다) |
|
|
| 파드 재배치 | PVC 때문에 postgres 는 **재배치 못 한다** |
|
|
|
|
### 예측
|
|
```
|
|
4a 세션 살아남는다. 남은 노드가 DB 에서 읽어 계속 서비스한다
|
|
4b 전면 장애 — DB 가 같이 죽었으므로 A-2 와 같아진다
|
|
```
|
|
|
|
### 복구
|
|
```bash
|
|
virsh start kc-lab-2
|
|
# JGROUPS_PING 의 유령 행이 어떻게 정리되는지 관찰 — 자동인가 수동인가
|
|
```
|
|
|
|
---
|
|
|
|
## A-5. 비대칭 파티션 (split brain)
|
|
|
|
### 답하려는 질문
|
|
> **한쪽 방향만** 끊기면? 서로 상대가 죽었다고 생각하면?
|
|
|
|
가장 고약한 장애다. 완전 단절보다 **부분 단절**이 훨씬 흔하고 진단이 어렵다.
|
|
|
|
### 구조
|
|
|
|
```
|
|
kc-lab-1 kc-lab-2
|
|
┌────────────────┐ ──────────▶ ┌────────────────┐
|
|
│ keycloak-1 │ 나가는 건 됨 │ keycloak-0 │
|
|
│ │ │ │
|
|
│ │ ◀━━✗━━━━━━ │ │
|
|
│ │ 들어오는 건 막힘 │ │
|
|
└────────────────┘ └────────────────┘
|
|
|
|
kc-lab-1 → "상대가 응답이 없네, 죽었나보다"
|
|
kc-lab-2 → "나는 잘 보내고 있는데?"
|
|
```
|
|
|
|
### 주입 방법
|
|
```bash
|
|
# 게스트 안에서 nftables 로 인바운드만 차단 (커널이 분리돼 있어 가능)
|
|
ssh kc-lab-2 'sudo nft add rule inet filter input tcp dport 7800 drop'
|
|
```
|
|
|
|
### 확인할 것
|
|
|
|
| 관측 | 보는 이유 |
|
|
|---|---|
|
|
| 양쪽의 `ISPN000094` 뷰 | **서로 다른 명단을 갖는가** |
|
|
| `coord` 컬럼 | **양쪽 다 코디네이터가 되는가** (진짜 split brain) |
|
|
| `fd_sock2_suspected` | 어느 쪽이 먼저 의심하는가 |
|
|
| 세션 동작 | DB 는 멀쩡하므로 **영향 없어야 한다** |
|
|
| 복구 후 `merge3_get_views` | MERGE3 가 합치는가, 재시작이 필요한가 |
|
|
|
|
---
|
|
|
|
## A-6. 지연 주입 (tc netem)
|
|
|
|
### 답하려는 질문
|
|
> **끊기지 않고 느려지기만** 하면? — 실제 장애의 대부분이 이 모양이다
|
|
|
|
### 구조
|
|
|
|
```
|
|
keycloak ──▶ [ tc netem: +200ms ] ──▶ postgres
|
|
지연만 넣는다
|
|
```
|
|
|
|
### 주입 방법
|
|
```bash
|
|
ssh kc-lab-2 'sudo tc qdisc add dev eth0 root netem delay 200ms'
|
|
```
|
|
|
|
### 확인할 것
|
|
|
|
| 관측 | 왜 |
|
|
|---|---|
|
|
| refresh 응답 시간 분포 | 200ms 가 그대로 더해지는가, 증폭되는가 |
|
|
| **DB 커넥션 풀 사용률** | 느려지면 커넥션이 반환 안 돼 **고갈된다** |
|
|
| 타임아웃 발생 지점 | 어느 계층이 먼저 포기하는가 |
|
|
| **낙관적 락 충돌 증가** | 트랜잭션이 길어져 `VERSION` 충돌이 늘어야 한다 |
|
|
|
|
**여기서 A-0의 낙관적 락 발견이 다시 쓰인다** — 지연이 길수록 읽고 쓰는
|
|
사이가 벌어지므로 충돌 확률이 오른다.
|
|
|
|
---
|
|
|
|
## A-7. volatile 모드 비교 — 옛 방식은 뭐가 다른가
|
|
|
|
### 답하려는 질문
|
|
> Keycloak 24 이전 방식(메모리가 진실의 원천)이었다면 지금까지의 결론이
|
|
> 어떻게 달라지는가?
|
|
|
|
**A-0의 모든 결론이 "26 기본값"이라는 전제 위에 있다.**
|
|
전제를 뒤집어 확인하는 것이 이 실험이다.
|
|
|
|
### 구조
|
|
|
|
```
|
|
현재 (persistent, KC 26 기본) 비교군 (volatile)
|
|
┌────────────────┐ ┌────────────────┐
|
|
│ keycloak │ │ keycloak │
|
|
│ 캐시(로컬) │ │ 캐시 ←──────┼── 진실의 원천
|
|
└───────┬────────┘ └───────┬────────┘
|
|
│ 진실의 원천 │ ✗ 세션 저장 안 함
|
|
▼ ▼
|
|
[ PostgreSQL ] [ PostgreSQL ]
|
|
(realm 설정만)
|
|
```
|
|
|
|
### 설정 변경
|
|
```yaml
|
|
args: ["start", "--features-disabled=persistent-user-sessions"]
|
|
```
|
|
|
|
### 확인할 것 — **A-0부터 A-4를 전부 다시 돌린다**
|
|
|
|
| 실험 | persistent (측정 완료) | volatile (예측) |
|
|
|---|---|---|
|
|
| A-0 캐시 위치 | 각자 자기 것만 | **양쪽에 복제되어야 한다** |
|
|
| **A-1 7800 차단** | 안 깨짐 | **치명적** — 정반대 |
|
|
| A-2 DB 정지 | 전면 장애 | **세션은 살아남음** |
|
|
| A-4 노드 상실 | 세션 생존 | 복제됐다면 생존 |
|
|
| A-8 롤링 재시작 | 세션 유지 | **전부 로그아웃** |
|
|
|
|
**A-1이 정반대로 뒤집히는 것을 보는 게 이 실험의 목적이다.**
|
|
같은 주입, 같은 관측, 정반대 결과 — 그래야 "버전 확인이 1순위"라는 교훈이
|
|
말이 아니라 데이터가 된다.
|
|
|
|
---
|
|
|
|
## A-8. 롤링 재시작 — 배포하면 로그아웃되는가
|
|
|
|
### 답하려는 질문
|
|
> **운영에서 가장 자주 겪는 것.** 배포할 때마다 사용자가 로그아웃되는가?
|
|
|
|
### 구조
|
|
|
|
```
|
|
시각 →
|
|
keycloak-0 ████████──✗ 재시작 ──████████
|
|
keycloak-1 ████████████████──✗ 재시작 ──████
|
|
|
|
사용자 세션 ────────────────────────────────▶ 유지되는가?
|
|
```
|
|
|
|
### 주입 방법
|
|
```bash
|
|
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
|
```
|
|
|
|
### 확인할 것
|
|
| 관측 | 예상 (persistent) |
|
|
|---|---|
|
|
| 재시작 전후 `sid` | **동일** |
|
|
| 재시작 중 refresh | 일시 실패 후 성공 (파드 전환 시점) |
|
|
| 재시작 후 캐시 엔트리 | **0에서 다시 시작** — 캐시는 사라진다 |
|
|
| 브라우저 SSO 쿠키 | 유지 |
|
|
|
|
**A-7과 짝이다.** volatile 이었다면 여기서 전원 로그아웃된다.
|
|
이게 **persistent 를 켜는 진짜 이유**다.
|
|
|
|
---
|
|
|
|
# B층 — 애플리케이션 세션 (Redis)
|
|
|
|
**선행: 배포가 필요하다.** 현재 실험대에 BFF도 Redis도 없다.
|
|
|
|
```
|
|
지금 B층 실험에 필요한 것
|
|
┌──────────────┐ ┌──────────────────────────┐
|
|
│ keycloak x2 │ │ keycloak x2 │
|
|
│ postgres │ │ postgres │
|
|
│ echo (헤더용) │ + │ bff x2 ← 새로 배포 │
|
|
│ │ │ redis ← 새로 배포 │
|
|
│ │ │ resource-server │
|
|
└──────────────┘ └──────────────────────────┘
|
|
```
|
|
|
|
BFF 구현은 `develop-keycloak-pattern3`에 있으므로 가져온다.
|
|
```bash
|
|
git checkout develop-keycloak-pattern3 -- bff/
|
|
```
|
|
|
|
**자원 예산 확인 필요** — 호스트 12GB 중 현재 여유 약 4GB.
|
|
Redis(~64Mi) + BFF 2개(~512Mi) 정도면 들어가지만, **A층 실험을 마친 뒤**
|
|
배포하는 편이 안전하다.
|
|
|
|
## B-1. BFF 저장소 결정 → Q3
|
|
|
|
### 구조
|
|
|
|
```
|
|
브라우저 ──쿠키(SESSION)──▶ ┌─────────┐ ┌──────────────┐
|
|
│ bff-0 │─────────▶│ │
|
|
├─────────┤ │ Redis │
|
|
│ bff-1 │─────────▶│ │
|
|
└─────────┘ └──────────────┘
|
|
│ 두 가지가 들어간다
|
|
│ ① Application Session
|
|
│ ② OAuth2AuthorizedClient
|
|
▼
|
|
[ Keycloak ]
|
|
```
|
|
|
|
### 확인할 것 — **조회 키가 다르다는 것이 핵심**
|
|
|
|
| 상태 | 조회 키 | 결과 |
|
|
|---|---|---|
|
|
| Application Session | **session ID** | 브라우저마다 별개 |
|
|
| OAuth2AuthorizedClient | **registration + principal name** | **같은 사용자의 여러 브라우저가 공유** |
|
|
|
|
5단계 — 로그인 유지·재시작 복구 / **refresh token 이 평문으로 저장되는가** /
|
|
**session TTL ≠ token 만료** / logout 후 잔여 항목 / 저장소 끊김 시 오류 형태
|
|
|
|
## B-2. 다중 인스턴스 운영 → Q1
|
|
|
|
### 구조
|
|
```
|
|
nginx ip_hash 주석 → 스티키 유무를 같은 구성에서 비교
|
|
│
|
|
├──▶ bff-0 ──┐
|
|
└──▶ bff-1 ──┴──▶ Redis
|
|
```
|
|
|
|
**확인 핵심** — **authorized client 덮어쓰기.** "Redis만 붙이면 해결"이라는
|
|
착각을 깨는 항목이다. 조회 키에 session ID가 없어서 생긴다.
|
|
|
|
## B-3. Refresh Token 경쟁 → Q2
|
|
|
|
### 구조
|
|
```
|
|
access token 만료 직후, 동시에 두 요청
|
|
│ │
|
|
▼ ▼
|
|
bff-0 bff-1
|
|
│ │
|
|
└────── 같은 refresh token ──────▶ Keycloak
|
|
│
|
|
한쪽만 성공, 다른 쪽은?
|
|
```
|
|
|
|
**★ B-1 이후여야 재현된다.** 저장소가 process-local이면 두 replica가 같은
|
|
항목을 보지 않아 **경쟁 자체가 성립하지 않는다.**
|
|
|
|
**A-0에서 낙관적 락(`VERSION`)과 `SKIP LOCKED`를 이미 확인했다.**
|
|
Keycloak 쪽 동작을 알고 시작하므로, BFF 쪽 재시도 설계에 집중한다.
|
|
|
|
판정 기준 — *실패가 사용자에게 노출되면 lock, 노출 안 되면 재시도.*
|
|
|
|
## B-4. Edge 인가 범위 → Q4
|
|
|
|
### 구조
|
|
```
|
|
브라우저 ──▶ nginx ──▶ [auth_request] ──▶ BFF
|
|
│ │
|
|
│◀── X-Auth-Request-Roles ──┘
|
|
│
|
|
└──▶ Resource Server ← 헤더만 믿는가?
|
|
```
|
|
|
|
**2홉 실험의 교훈이 여기서 결정적이다** — 헤더 위조가 통하면
|
|
그것은 쿠키 속성이 아니라 **신원 위조**다. NetworkPolicy 패턴을 재사용한다.
|
|
|
|
확인 — 다중 값 구분자·escaping / 헤더 크기 상한 / role 변경 반영 시점 /
|
|
upstream 이 값과 service identity 까지 보는가
|
|
|
|
## B-5 · B-6. Redis 상실과 영속화
|
|
|
|
```
|
|
B-5 Redis 파드 정지 → 세션 전멸? 로그인 화면?
|
|
B-6 appendonly yes/no 비교 → 재시작 후 무엇이 남는가
|
|
```
|
|
|
|
**A-2·A-3의 Redis 판본이다.** 같은 질문을 다른 저장소에 던진다.
|
|
|
|
---
|
|
|
|
# C층 — SSO
|
|
|
|
## C-1. 다중 앱 SSO
|
|
|
|
### 답하려는 질문
|
|
> **당신 원래 질문**: "SSO를 추가하면 어떻게 달라지는가"
|
|
|
|
### 구조
|
|
|
|
```
|
|
┌──────────────────────────┐
|
|
│ Keycloak SSO 세션 │
|
|
│ 쿠키 KEYCLOAK_IDENTITY │
|
|
└────────┬─────────────────┘
|
|
│
|
|
┌──────────────┴──────────────┐
|
|
▼ ▼
|
|
┌──────────────┐ ┌──────────────┐
|
|
│ app1 │ │ app2 │
|
|
│ (BFF 세션) │ │ (BFF 세션) │
|
|
└──────────────┘ └──────────────┘
|
|
|
|
app1 로그인 → app2 는 로그인 화면 없이 통과
|
|
Keycloak 세션 소멸 → 양쪽 다 재로그인
|
|
```
|
|
|
|
### 확인할 것
|
|
| 항목 | 보는 것 |
|
|
|---|---|
|
|
| app1 로그인 후 app2 접속 | 로그인 화면 없이 통과하는가 |
|
|
| **A-2(DB 정지)를 겹친다** | **장애 반경이 앱 하나에서 전체로 커지는가** |
|
|
| SSO 세션 vs 앱 세션 수명 | 어긋나면 무슨 일이 생기는가 |
|
|
| 노드 상실 중 SSO | 새 앱 진입이 되는가 |
|
|
|
|
**이게 SSO의 대가다** — 편의를 얻는 대신 **단일 장애점이 하나 늘어난다.**
|
|
|
|
## C-2. 백채널 로그아웃
|
|
|
|
```
|
|
Keycloak ──logout token(sid)──▶ app1 /backchannel-logout
|
|
└─────────────────────▶ app2 /backchannel-logout
|
|
```
|
|
|
|
**A-0에서 확인한 `sid`가 여기서 쓰인다.** 토큰·DB·관리 API에서 같았던 그
|
|
문자열이 로그아웃 전파의 키다.
|
|
|
|
확인 — 한 앱이 응답 안 하면? 부분 실패 시 재시도는? 역인덱스는 어디에?
|
|
|
|
---
|
|
|
|
# D층 — 운영 역량
|
|
|
|
## D-1. 백업과 복구 리허설
|
|
|
|
> **"백업이 있다"와 "복구해봤다"는 완전히 다르다.**
|
|
|
|
```
|
|
1. pg_dump 로 전체 덤프
|
|
2. 일부러 파괴 (PVC 삭제 또는 DROP DATABASE)
|
|
3. 덤프에서 복구
|
|
4. 로그인이 되는가 / 기존 세션이 살아나는가
|
|
5. 걸린 시간 = RTO, 잃은 데이터 = RPO
|
|
```
|
|
|
|
**A-3에서 측정한 RPO와 합쳐진다.** `synchronous_commit OFF` 때문에
|
|
백업 시점 이후뿐 아니라 **크래시 직전 수백 ms도 잃는다.**
|
|
|
|
## D-2. 버전 업그레이드
|
|
|
|
```
|
|
26.7.0 → 다음 마이너
|
|
│
|
|
├── DB 스키마 자동 마이그레이션 ← 실패하면 되돌리기 어렵다
|
|
├── 롤링 중 세션 유지되는가 ← A-8 과 연결
|
|
└── 롤백 가능한가 ← 스키마가 바뀐 뒤에는 구버전이 못 뜬다
|
|
```
|
|
|
|
**D-1이 선행이어야 한다.** 백업 없이 스키마 마이그레이션을 돌리지 않는다.
|
|
|
|
## D-3. 비밀 관리
|
|
`kubectl get secret -o yaml` → base64 디코드가 한 줄로 되는 것을 **직접 본다.**
|
|
"Secret은 암호화가 아니다"를 문장이 아니라 화면으로.
|
|
|
|
## D-4. 인증서 갱신
|
|
```bash
|
|
sudo certbot renew --force-renewal # 90일을 안 기다린다
|
|
```
|
|
nginx reload 중 **진행 중이던 요청**은 어떻게 되는가.
|
|
|
|
---
|
|
|
|
## 실행 순서와 의존 관계
|
|
|
|
```
|
|
A-0 ✅
|
|
│
|
|
├─▶ A-1 (7800) ─────┬─▶ A-5 (비대칭)
|
|
│ └─▶ A-6 (지연)
|
|
│
|
|
├─▶ A-2 (DB 정지) ──▶ A-3 (강제 종료) ──▶ D-1 (백업/복구) ──▶ D-2 (업그레이드)
|
|
│
|
|
├─▶ A-4 (노드 상실)
|
|
│
|
|
├─▶ A-8 (롤링 재시작)
|
|
│
|
|
└─▶ A-7 (volatile 비교) ← A-1·A-2·A-4·A-8 을 다 한 뒤 전부 재실행
|
|
│
|
|
▼
|
|
[ BFF + Redis 배포 ]
|
|
│
|
|
├─▶ B-1 (저장소) ──┬─▶ B-2 (다중 인스턴스)
|
|
│ ├─▶ B-3 (refresh 경쟁)
|
|
│ ├─▶ B-4 (edge 인가)
|
|
│ └─▶ B-5 (Redis 상실) ─▶ B-6 (영속화)
|
|
│
|
|
└─▶ C-1 (SSO) ─▶ C-2 (백채널 로그아웃)
|
|
|
|
D-3 (비밀), D-4 (인증서) 는 언제든 독립적으로
|
|
```
|
|
|
|
**A-7(volatile)을 A층 마지막에 두는 이유** — 앞의 실험들을 전부 마친 뒤
|
|
설정 하나만 바꿔 재실행하면, **같은 주입에 대한 정반대 결과**를 한 벌로
|
|
얻는다. 이게 이 실험대에서 가장 값진 산출물이 될 가능성이 높다.
|
|
|
|
---
|
|
|
|
## 공통 판정 규칙
|
|
|
|
A-0에서 비싸게 배운 것들. 모든 실험에 적용한다.
|
|
|
|
| 규칙 | 왜 |
|
|
|---|---|
|
|
| **대조군을 먼저 잰다** | 반대편 응답은 발급 노드 응답과 나란히 놓기 전엔 의미 없다 |
|
|
| **개수가 아니라 식별자로 추적** | 실험 도구 자신이 만든 잡음에 오염된다 |
|
|
| **저장소를 실제로 건드리는 탐침** | userinfo 는 세션을 몰라도 통과한다 |
|
|
| **계측 코드가 자기 실패를 말하게** | 스냅샷이 비면 델타가 조용히 0이 된다 |
|
|
| **예측을 먼저 적는다** | 사후에 "그럴 줄 알았다"는 아무것도 아니다 |
|
|
| **증거는 원본 그대로** | 터미널 출력과 브라우저 스크린샷. 재구성하지 않는다 |
|
|
|
|
### 실험마다 남기는 것
|
|
|
|
```
|
|
docs/experiment-NN-<이름>.md 해설 · 예측 · 결과 · 개념 · 함정
|
|
docs/evidence/<이름>/README.md 파일별 무엇을 보여주는지
|
|
docs/evidence/<이름>/*.txt 터미널 원문
|
|
docs/evidence/<이름>/*.png 브라우저 스크린샷 (Grafana / 관리 콘솔)
|
|
deploy/lab/scripts/experiment-*.sh 재현 스크립트
|
|
```
|