Files
keycloak-pattern/docs/experiment-a8-rolling-restart.md
T
DongHyeonkaandClaude Opus 5 cdac9b8178 docs: give the twelve experiments that had no architecture diagram one
An audit against the standard the series set — concepts, procedure,
commands, architecture diagram, evidence table, terminal output — found the
three new experiments met it while twelve of the original ones had no
diagram at all: A-0, A-1, A-3, A-4, A-5, A-6, A-8, B-0, B-2, B-7, C-2, D-2.

Each now has one drawn from what that experiment actually found, not filler:
A-0 shows sharing going through PostgreSQL rather than between the caches;
A-3 the gap between the 200 and the WAL flush, with both failed injections;
A-5 the three silent injection failures; A-6 the two places latency is
multiplied; B-0 the repository keyed by principal with no session id; B-2
the primary key that causes the overwrite; D-2 why the rolling update
stopped the accident halfway.

Also corrected the index's stale claim of 11 experiments without a
screenshot — it is 14, and the reason is recorded: those experiments were
measured from terminals, the database and logs, and the observability stack
does not scrape Redis, the BFF or PostgreSQL, so there is no console to
photograph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 21:44:36 +09:00

9.2 KiB
Raw Blame History

A-8 — 배포할 때마다 로그아웃되는가

브랜치 feature/keycloak-a8-rolling-restart · 증거 docs/evidence/a8-rolling-restart/ · 2026-09-04 13:3813:41 KST

운영에서 가장 자주 겪는 일이다. 장애가 아니라 정상 작업인데도 사용자가 로그아웃되면 그건 사고다.


구조

A-8 — 사라지는 캐시와 남는 세션

다이어그램 규약은 diagrams/_style.md. 실험대 전체 구조는 diagrams/lab-topology.svg.


0. 결론부터

확인 결과
재시작 중 서비스 중단 없음. 전 구간 200
재시작 전 발급한 refresh token 여전히 통한다 (200)
DB 세션 수 151 → 151 그대로
세션 캐시 0 으로 초기화
클러스터 자동 재형성 (cluster_size = 2)

세션은 살아남고 캐시만 사라진다. 이것이 persistent-user-sessions 를 켜는 진짜 이유다.


1. 방법

# 1. 재시작 전 로그인 — 토큰을 상주 파드 안에 보관한다
kubectl -n keycloak-lab run a8-probe --image=curlimages/curl:8.11.1 \
  --restart=Never --command -- sleep 3600
kubectl -n keycloak-lab exec a8-probe -- sh -c '<로그인 후 /tmp/rt, /tmp/sid 에 저장>'

# 2. 재시작하면서 5초 간격으로 외부 진입점을 찍는다
kubectl -n keycloak-lab rollout restart statefulset/keycloak
( for i in $(seq 1 48); do
    curl -s -o /dev/null -w '%{http_code} ' --max-time 4 https://auth.hyeonworks.com/realms/master
    sleep 5
  done ) &
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=420s

탐침 파드가 StatefulSet 밖에 있어야 재시작을 넘어 토큰을 들고 있을 수 있다.


2. 가용성 — 무중단이었다

statefulset.apps/keycloak restarted
200 Waiting for partitioned roll out to finish: 0 out of 2 new pods have been updated...
    Waiting for 1 pods to be ready...
200 200 200 200 Waiting for partitioned roll out to finish: 1 out of 2 new pods have been updated...
    Waiting for 1 pods to be ready...
200 200 200 200 partitioned roll out complete: 2 new pods have been updated...

9번 찍어서 9번 다 200.

표본은 9개다. 5초 간격으로 찍었으므로 5초보다 짧은 끊김은 이 측정으로 잡히지 않는다. 실제로 후속 작업에서 1초 간격·3초 타임아웃으로 재보니 롤백 전환 순간에 000 이 한 번 잡혔다 (followup 2절). "무중단" 은 관측 해상도에 달려 있으며, 여기서는 "5초 해상도에서 끊김이 관측되지 않았다" 까지가 정확한 서술이다.

왜 무중단이 되는가

   StatefulSet 롤링 재시작
     │
     ├─ keycloak-1 종료 → Service 엔드포인트에서 빠짐
     │     └─ 이 동안 keycloak-0 이 전부 받는다
     ├─ keycloak-1 기동 → readiness UP → 엔드포인트 복귀
     │
     └─ keycloak-0 종료 → ... (반복)

한 번에 하나씩 내리므로 항상 최소 하나는 Ready 다. readiness 프로브가 이 전환을 정확히 맞춰준다 — A-2 에서 본 그 메커니즘이 여기서는 정상 작업을 안전하게 만든다.

다만 이 실험대는 파드가 2개다. replica 1 이면 반드시 끊긴다. 무중단은 공짜가 아니라 replica ≥ 2 와 readiness 의 조합이다.


3. 세션 생존

=== 재시작 전 발급한 refresh token 이 아직 통하는가 ===
  대상 sid: XLcgQWRiJrTkuNZcJsNeT_2j
  keycloak-0 에서 refresh  HTTP 200

=== DB 에 그 세션이 남아 있는가 ===
     user_session_id      | created_on | last_session_refresh
--------------------------+------------+----------------------
 XLcgQWRiJrTkuNZcJsNeT_2j | 1788495513 |           1788495577

  전체 온라인 세션: 151 (재시작 전 151)

last_session_refreshcreated_on 보다 64초 뒤다. 재시작 후의 refresh 가 실제로 DB 에 기록되었다는 뜻이다 — 응답 코드만 200 인 게 아니라 쓰기까지 정상이다.

파드가 통째로 바뀌었는데(44초/66초 나이) 세션은 그대로다.


4. 캐시는 사라진다

  keycloak-0  sessions 캐시 0.0 건 / cluster_size 2.0
  keycloak-1  sessions 캐시 1.0 건 / cluster_size 2.0

캐시 초기화와 클러스터 재형성

캐시는 프로세스 메모리이므로 재시작에 사라진다. keycloak-1 의 1건은 방금 refresh 를 처리하며 새로 담은 것이다.

   재시작 전:  캐시 N건  +  DB 151건
   재시작 후:  캐시 0건  +  DB 151건      ← 진실은 DB 에 있다

A-0 의 모델이 그대로 확인된다. 캐시가 통째로 날아가도 정확성은 유지되고 첫 접근만 느려진다 (룩어사이드 캐시의 성질).


5. 이것이 persistent-user-sessions 를 켜는 진짜 이유다

persistent (KC 26 기본) volatile (KC 24 이전 방식)
롤링 재시작 후 세션 유지 전원 로그아웃
배포 빈도 자유롭다 배포가 곧 사고다
대가 DB 쓰기 (A-6 에서 본 지연) 없음

A-7 에서 volatile 로 바꿔 같은 실험을 반복하면 여기가 정반대가 될 것이다. 그 비교가 이 실험의 짝이다.



개념

StatefulSet 롤링 재시작의 무중단 조건

   한 번에 하나씩 내린다  +  readiness 로 전환 시점을 맞춘다
        └─ 항상 최소 하나는 Ready 다

두 가지가 다 있어야 성립한다. replica 1 이면 반드시 끊기고, readiness 프로브가 없으면 아직 기동 중인 파드로 트래픽이 간다.

룩어사이드 캐시가 재시작을 견디는 이유

재시작 후
캐시 (프로세스 메모리) 사라진다
DB (진실의 원천) 남는다
정확성 유지된다 — 첫 접근만 느려진다

A-0 에서 세운 모델이 여기서 그대로 확인된다.



증거 파일

증거 수집 시각: 2026-09-04 13:19 13:20 KST (파일 mtime 기준. 문서 상단의 시각 표기는 작성 시점이라 다를 수 있다.)

파일 종류
01-restart-availability.txt 터미널 원문
02-session-survival.txt 터미널 원문
a8-cache-reset-cluster-reformed.png 스크린샷

파일별 상세는 evidence/a8-rolling-restart/README.md.

6. 재현 절차 (명령어)

# 상주 탐침 (StatefulSet 밖에 있어야 한다)
kubectl -n keycloak-lab run a8-probe --image=curlimages/curl:8.11.1 \
  --restart=Never --command -- sleep 3600
kubectl -n keycloak-lab wait --for=condition=Ready pod/a8-probe --timeout=120s

# 변수 준비
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
PW=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
       -o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)

# 로그인하고 refresh token 을 /tmp/rt 에, sid 를 /tmp/sid 에 보관.
# ★ 아래 5번이 읽는 파일과 같은 이름이어야 한다 — 처음 문서는 /tmp/tok 에 쓰고
#   /tmp/rt 를 읽어서 빈 문자열을 보냈고, 그래도 400 이 아니라 통과한 것처럼 보였다.
kubectl -n keycloak-lab exec a8-probe -- sh -c \
  "curl -s -X POST http://$K0:8080/realms/master/protocol/openid-connect/token \
     -d grant_type=password -d client_id=admin-cli -d username=admin -d password=$PW > /tmp/tok
   sed -n 's/.*\"refresh_token\":\"\\([^\"]*\\)\".*/\\1/p' /tmp/tok > /tmp/rt
   sed -n 's/.*\"access_token\":\"\\([^\"]*\\)\".*/\\1/p' /tmp/tok | cut -d. -f2 \
     | sed 's/\$/==/' | base64 -d 2>/dev/null \
     | sed -n 's/.*\"sid\":\"\\([^\"]*\\)\".*/\\1/p' > /tmp/sid
   echo \"rt \$(wc -c < /tmp/rt) bytes / sid \$(cat /tmp/sid)\""


# 재시작 + 가용성 감시
kubectl -n keycloak-lab rollout restart statefulset/keycloak
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=420s

# 세션 생존 확인
kubectl -n keycloak-lab exec a8-probe -- sh -c \
  'curl -s -o /dev/null -w "%{http_code}\n" -X POST http://'$K0':8080/realms/master/protocol/openid-connect/token \
     -d grant_type=refresh_token -d client_id=admin-cli -d refresh_token=$(cat /tmp/rt)'

# DB 대조
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
  -c "select user_session_id, created_on, last_session_refresh from offline_user_session
       where offline_flag='0' and user_session_id='$(kubectl -n keycloak-lab exec a8-probe -- cat /tmp/sid)'"

7. 다음 실험에 남기는 것

실험 이 실험이 준 것
A-7 volatile 비교 이 실험을 그대로 반복하면 정반대 결과가 나와야 한다
D-2 버전 업그레이드 롤링 재시작이 안전하다는 것이 업그레이드의 전제
구성 무중단은 replica ≥ 2 + readiness 의 조합이다