Files
keycloak-pattern/docs/experiment-a7-volatile-comparison.md
T
DongHyeonkaandClaude Opus 5 b9f4ef7bc2 docs(a7a): the volatile refresh failure is a client-scope lookup, not REVOKED_TOKEN
A-7 left the cause as a hypothesis and said to confirm it with the statement
logging A-3 used. Doing that shows the hypothesis was wrong.

Login issues no SQL at all in volatile mode. Refresh issues exactly one
statement, and it is not the one A-7 guessed:

  select cscme1_0.SCOPE_ID from CLIENT_SCOPE_CLIENT cscme1_0
   where cscme1_0.CLIENT_ID=$1 and cscme1_0.DEFAULT_SCOPE=$2
   parameters: $1 = '131a9912-...', $2 = 'f'

REVOKED_TOKEN never appears. DEFAULT_SCOPE='f' means this is the optional
client scope list, which refresh needs because it recomputes which scopes go
into the new access token.

The larger finding is that A-7's table was conditional and did not say so.
The single statement is issued once and then cached, so the same
configuration produces three different outcomes depending only on cache
warmth — all three reproduced:

  fully cold      login 400  (select ce1_0.ID from CLIENT ...)
  CLIENT warm     login 200, refresh 500  <- what A-7 measured
  fully warm      login 200, refresh 200

So "volatile means you can log in without a database" is also conditional:
on a cold start the client lookup itself fails. In each case the Keycloak
log names the failing SQL directly, so this is determined rather than
inferred.

A-7 keeps its original wording with a correction banner above it, so what
was mis-guessed and why stays visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 20:27:23 +09:00

305 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# A-7 — 옛 방식(volatile)이었다면 무엇이 달라지는가
브랜치 `feature/keycloak-a7-volatile-comparison` ·
증거 [`docs/evidence/a7-volatile-comparison/`](evidence/a7-volatile-comparison/) ·
2026-09-04 13:4514:15 KST
**A층의 결론 전체가 "Keycloak 26 기본값"이라는 전제 위에 있다.**
전제를 뒤집어 같은 실험을 반복한 것이 이 실험이다.
---
## 구조
![A-7 구조 — 두 모드의 데이터 흐름과 뒤집힌 결과](diagrams/a7-volatile-inversion.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
---
## 0. 결론부터 — 비교표
| 실험 | persistent (KC 26 기본) | **volatile (KC 24 이전 방식)** |
|---|---|---|
| **A-0** 세션 저장 위치 | DB 에 행이 생긴다 | **DB 0건.** 메모리에만 |
| **A-1** 7800 차단 후 교차 노드 refresh | **`200`** — 안 깨진다 | **`400 Session not active`** — 깨진다 |
| **A-8** 롤링 재시작 후 refresh | **`200`** — 세션 생존 | **`400 Session not active`** — 전원 로그아웃 |
| **A-2** DB 정지 중 새 로그인 | `500` | **`200`** — 된다 |
| A-2 DB 정지 중 refresh | `500` | `500` |
**세 개가 정반대로 뒤집혔다.** 예측한 그대로다.
> **★ 이 표에는 조건이 빠져 있다 (A-7a).** volatile 행의 A-2 결과는
> **캐시 온도에 따라 달라진다.** 완전 냉시동이면 로그인도 `400` 이고,
> 캐시가 완전히 더우면 refresh 도 `200` 이다. 여기 적힌 값은 그 사이의
> 한 상태다 — 셋 다 [A-7a](experiment-a7a-volatile-cause.md) 에서 재현했다.
> **"세션 공유는 7800 을 안 탄다"는 A-1 의 결론은 버전에 달린 사실이다.**
> 인터넷 자료 대부분이 24 이전 기준이므로 **거기서는 통념이 맞다.**
> 틀린 것은 자료가 아니라 **버전을 확인하지 않고 적용하는 것**이다.
---
## 1. 전환 방법
```bash
# 이 버전에서 끌 수 있는지부터 확인한다
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kc.sh build --help-all \
| tr ',' '\n' | grep -i persistent
# persistent-user-sessions[:v1] ← 목록에 있다
```
```yaml
# deploy/lab/k8s/keycloak-cluster.yaml
args: ["start", "--features-disabled=persistent-user-sessions"]
```
```bash
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
```
**빌드 옵션이므로 기동 시 재빌드가 일어나 평소보다 오래 걸린다.**
---
## 2. A-0 재실행 — 세션이 DB 에 안 들어간다
```
=== keycloak-0 에만 로그인 5회 ===
keycloak-0 sessions 캐시 5.0 건
keycloak-1 sessions 캐시 0.0 건
=== DB 에는 들어갔는가 (persistent 였을 때는 5건) ===
offline_flag | count
--------------+-------
(0 rows) ← 0 건
=== 교차 노드 세션은 되는가 ===
keycloak-0 로그인 → keycloak-1 에서 refresh HTTP 200
```
**DB 는 비어 있는데 교차 노드가 된다.** persistent 때와 겉보기 결과가 같지만
**경로가 완전히 다르다** — 이제는 DB 가 아니라 **클러스터를 타고** 있다.
> 캐시 엔트리 수가 `5 / 0` 인 것은 persistent 때와 같다.
> `approximate_entries_unique` 는 **그 노드가 소유한 엔트리**만 세므로,
> 백업본을 들고 있어도 0 으로 보인다. **이 지표만으로는 두 모드를 구분할 수
> 없다** — 구분하려면 7800 을 끊어봐야 한다. 그게 다음 절이다.
---
## 3. A-1 재실행 — **정반대다**
A-5 에서 확립한 `raw` 테이블 양방향 차단을 그대로 썼다.
```
[대조군] 차단 전 교차 노드 refresh HTTP 200
차단 적용 → 분단 성립
+25초 cluster_size(k0 k1) = [2.0 2.0]
+50초 cluster_size(k0 k1) = [1.0 ...] ← 갈라졌다
=== 분단 상태에서 ===
keycloak-0 로그인 → keycloak-0 에서 refresh HTTP 200 ← 대조군
keycloak-0 로그인 → keycloak-1 에서 refresh HTTP 400 ← 시험군
{"error":"invalid_grant","error_description":"Session not active"}
```
**대조군이 200 인 것이 중요하다.** 차단이 모든 것을 망가뜨린 게 아니라
**교차 노드만** 끊었다는 증거다.
```
persistent : 세션 ── PostgreSQL ──▶ 양쪽이 본다 7800 무관
volatile : 세션 ── 클러스터(7800) ─▶ 상대에게 간다 7800 필수
```
**같은 주입, 같은 관측, 정반대 결과.** 이 한 쌍이 A층 전체의 근거다.
---
## 4. A-8 재실행 — 배포가 곧 로그아웃
```
=== 재시작 전 로그인 ===
sid = aVwYnzKZFFvMqD3bpSeiILuM
=== 롤링 재시작 ===
partitioned roll out complete: 2 new pods have been updated...
=== ★ 재시작 전 토큰이 아직 통하는가 (persistent 였을 때는 200) ===
keycloak-0 에서 refresh HTTP 400
{"error":"invalid_grant","error_description":"Session not active"}
```
**배포할 때마다 전원 로그아웃된다.**
| | persistent | volatile |
|---|---|---|
| 배포 | 자유롭다 | **모든 사용자가 다시 로그인** |
| 파드 재시작 (OOM, 노드 교체) | 무해 | **그 노드가 처리하던 세션 소멸** |
| 무중단 여부 | 무중단 (A-8) | 접속은 되지만 **로그인 상태가 사라진다** |
> **A-8 에서 "이것이 persistent 를 켜는 진짜 이유"라고 썼는데, 여기서 증명된다.**
> 24 이전 버전을 쓰는 곳에서 "배포하면 로그아웃된다"가 당연하게 여겨졌던 이유다.
---
## 5. A-2 재실행 — 예상 못 한 비대칭
```
① 캐시를 가진 노드에서 refresh HTTP 500
② 새 로그인 HTTP 200 ← persistent 에서는 500 이었다
```
**새 로그인은 되는데 refresh 가 안 된다.** 순서가 거꾸로다.
### 왜 새 로그인이 되는가
```
로그인에 필요한 것
├─ realm 설정 → Infinispan `realms` 캐시에 있다
├─ 사용자 자격 → `users` 캐시에 있다
└─ 세션 저장 → volatile 이므로 메모리
→ DB 없이 완결된다
```
**A-2 에서 persistent 로 했을 때 로그인이 실패한 이유는 "세션을 DB 에 써야
해서"였다.** 그 쓰기가 없어지니 로그인이 통과한다.
### refresh 가 500 인 이유 — 가설
> **★ 정정 (A-7a)** — 이 가설은 **틀렸다.** 문장 로깅으로 확정한 결과
> 원인은 `REVOKED_TOKEN` 이 아니라 **`CLIENT_SCOPE_CLIENT` 조회**였다.
> 더 중요하게는 **위 표 자체가 캐시 온도에 따라 400/500/200 으로 갈린다.**
> [A-7a 문서](experiment-a7a-volatile-cause.md) 참조.
아래는 정정 전 원문이다 — 무엇을 어떻게 잘못 짚었는지 남긴다.
**측정은 확실하지만 원인은 확정하지 못했다.** 유력한 후보는
`REVOKED_TOKEN` 테이블이다 — refresh token 회전에서 **이미 쓴 토큰인지**
확인하려면 그 테이블을 봐야 하고, 그 경로는 캐시되지 않는다.
```
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -c "\dt" | grep revoked
public | revoked_token | table | keycloak
```
**확정하려면 A-3 에서 쓴 문장 로깅을 켜고 다시 재현해야 한다.** 여기서는
**가설로 남긴다.****A-7a 에서 그대로 실행해 확정했고, 가설은 틀렸다.**
> **volatile 이 "DB 없이 돌아간다"는 뜻은 아니다.**
> realm·사용자·클라이언트·취소 토큰은 **여전히 DB 에 있다.**
> 세션만 메모리로 옮긴 것이다.
---
## 6. 원복
```bash
# args 를 되돌린다
args: ["start"]
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
```
```
=== [검증] persistent 로 돌아왔는가 ===
["start"]
DB 온라인 세션: 1 건 (로그인 1회 후 → persistent 복귀 확인)
외부 진입점 HTTP 200
```
**전환 자체는 설정 한 줄이고 되돌리기도 한 줄이다.** 다만 전환 시점에
**기존 세션은 전부 사라진다** (저장 위치가 바뀌므로).
---
## 7. 개념
### 두 모드의 데이터 흐름
```
persistent (KC 25+, 26 기본)
로그인 ──▶ PostgreSQL (진실) + 로컬 캐시 (사본)
조회 ──▶ 캐시에 없으면 DB
공유 ──▶ 같은 DB 를 보는 것
volatile (KC 24 이전)
로그인 ──▶ Infinispan (진실)
조회 ──▶ 클러스터에서 찾는다
공유 ──▶ 7800 을 통한 복제
```
### 무엇을 맞바꾸는가
| | persistent | volatile |
|---|---|---|
| 재시작 내구성 | **있다** | 없다 |
| 7800 의존 | 낮다 (무효화만) | **높다 (세션 자체)** |
| DB 부하 | **로그인·refresh 마다 쓰기** | 세션 관련 없음 |
| 노드 확장 | DB 가 병목 | **복제 트래픽이 N² 로 증가** |
| 지연 민감도 | **DB 왕복에 민감** (A-6) | 클러스터 왕복에 민감 |
**26 이 기본을 바꾼 이유가 이 표에 있다** — 운영에서 가장 아픈 것이
"배포하면 로그아웃"이었기 때문이다.
---
---
## 증거 파일
**증거 수집 시각: 2026-09-04 13:22 13:32 KST** (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)
| 파일 | 종류 |
|---|---|
| [`01-switch-to-volatile.txt`](evidence/a7-volatile-comparison/01-switch-to-volatile.txt) | 터미널 원문 |
| [`02-a0-rerun.txt`](evidence/a7-volatile-comparison/02-a0-rerun.txt) | 터미널 원문 |
| [`03-a8-rerun-restart.txt`](evidence/a7-volatile-comparison/03-a8-rerun-restart.txt) | 터미널 원문 |
| [`04-a1-rerun-partition.txt`](evidence/a7-volatile-comparison/04-a1-rerun-partition.txt) | 터미널 원문 |
| [`05-a2-rerun-db-loss.txt`](evidence/a7-volatile-comparison/05-a2-rerun-db-loss.txt) | 터미널 원문 |
| [`06-restore-persistent.txt`](evidence/a7-volatile-comparison/06-restore-persistent.txt) | 터미널 원문 |
파일별 상세는 [`evidence/a7-volatile-comparison/README.md`](evidence/a7-volatile-comparison/README.md).
## 8. 재현 절차 (명령어)
```bash
# 1. 끌 수 있는지 확인
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kc.sh build --help-all \
| tr ',' '\n' | grep -i persistent
# 2. 세션을 비우고 전환 (비교 기준을 맞추기 위해)
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "delete from offline_user_session"
# args: ["start", "--features-disabled=persistent-user-sessions"]
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=500s
# 3. volatile 확인 — 로그인 후 DB 가 비어 있어야 한다
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select offline_flag, count(*) from offline_user_session group by offline_flag"
# 4. A-1 / A-8 을 그대로 반복한다 (증거 파일 참조)
# 5. 원복
# args: ["start"]
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
```
---
## 9. 이 실험이 A층에 남기는 것
| | |
|---|---|
| **A-0~A-6 의 결론은 모두 "26 기본값" 조건부다** | 버전이 다르면 답이 다르다 |
| **A-1 이 통념과 어긋난 이유가 확정됐다** | 통념은 24 이전에서 맞다 |
| **버전 확인이 1순위** | `kc.sh --version` 을 먼저 본다 |
| **volatile 이 DB 독립을 뜻하지 않는다** | realm·사용자·취소 토큰은 여전히 DB |
### 미해결로 남긴 것
**volatile 에서 refresh 만 500 이 되는 이유**`REVOKED_TOKEN` 조회 가설을
세웠지만 확정하지 못했다. A-3 의 문장 로깅 기법으로 재현하면 답이 나온다.