The forward upgrade to 26.7.3 was zero downtime across 87 samples, and since databasechangelog stayed at 210 the rollback to 26.7.0 also succeeded, which narrows D-2's conclusion: rolling back fails when the schema moved, not because of the version number. The row count is the check. Role changes never reach the upstream through request repetition; the session is a snapshot taken at login and only a new session picks up the new claim. Auditing the docs also surfaced that Prometheus scrapes only keycloak, kubelet, node-exporter and itself, so the B-layer experiments have no metrics to screenshot rather than missing screenshots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9.6 KiB
A-7 — 옛 방식(volatile)이었다면 무엇이 달라지는가
브랜치 feature/keycloak-a7-volatile-comparison ·
증거 docs/evidence/a7-volatile-comparison/ ·
2026-09-04 13:45–14:15 KST
A층의 결론 전체가 "Keycloak 26 기본값"이라는 전제 위에 있다. 전제를 뒤집어 같은 실험을 반복한 것이 이 실험이다.
구조
다이어그램 규약은
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 |
세 개가 정반대로 뒤집혔다. 예측한 그대로다.
"세션 공유는 7800 을 안 탄다"는 A-1 의 결론은 버전에 달린 사실이다. 인터넷 자료 대부분이 24 이전 기준이므로 거기서는 통념이 맞다. 틀린 것은 자료가 아니라 버전을 확인하지 않고 적용하는 것이다.
1. 전환 방법
# 이 버전에서 끌 수 있는지부터 확인한다
kubectl -n keycloak-lab exec keycloak-0 -- /opt/keycloak/bin/kc.sh build --help-all \
| tr ',' '\n' | grep -i persistent
# persistent-user-sessions[:v1] ← 목록에 있다
# deploy/lab/k8s/keycloak-cluster.yaml
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
빌드 옵션이므로 기동 시 재빌드가 일어나 평소보다 오래 걸린다.
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 인 이유 — 가설
측정은 확실하지만 원인은 확정하지 못했다. 유력한 후보는
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 에서 쓴 문장 로깅을 켜고 다시 재현해야 한다. 여기서는 가설로 남긴다.
volatile 이 "DB 없이 돌아간다"는 뜻은 아니다. realm·사용자·클라이언트·취소 토큰은 여전히 DB 에 있다. 세션만 메모리로 옮긴 것이다.
6. 원복
# 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 이 기본을 바꾼 이유가 이 표에 있다 — 운영에서 가장 아픈 것이 "배포하면 로그아웃"이었기 때문이다.
8. 재현 절차 (명령어)
# 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 의 문장 로깅 기법으로 재현하면 답이 나온다.