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>
8.2 KiB
D-1 — 백업이 있다와 복구해봤다는 다르다
브랜치 feature/keycloak-d1-backup-restore ·
증거 docs/evidence/d1-backup-restore/ ·
2026-09-04 16:55–17:05 KST
구조
다이어그램 규약은
diagrams/_style.md. 실험대 전체 구조는diagrams/lab-topology.svg.
0. 결론부터
| 측정 | 값 |
|---|---|
| 덤프 크기 / 시간 | 395KB · 1초 미만 (101개 테이블) |
| 복구 시간 | 1초 (15:00:12 → 15:00:13), 오류 0건 |
| 서비스 회복 | 재시작 없이 15초 이내 (restarts=0) |
| 데이터 일치 | 완전 일치 — realms 2 / clients 15 / users 2 / sessions 3 / authclients 1 |
| RTO | 약 30초 (파괴 감지부터 서비스 복귀까지) |
| RPO | 마지막 덤프 시점 + A-3 의 synchronous_commit OFF 손실 |
그리고 예상 못 한 것 — 스키마를 통째로 지웠는데 서비스가 200 을 계속 냈다.
1. 백업
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > /tmp/keycloak-backup.sql
크기: 394945 bytes (6956 줄)
CREATE TABLE: 101 개
offline_user_session 언급: 13
세션도 덤프에 들어간다.
COPY public.offline_user_session (user_session_id, user_id, realm_id, created_on, offline_flag, data, ...)
E1q5xI7tt4U_WhZpW7rEPIF2 48b37d33-... 7845f394-... 1788500836 0 {"ipAddr...
| 옵션 | 뜻 |
|---|---|
--clean |
복구 시 기존 객체를 DROP 하고 다시 만든다 |
--if-exists |
없는 객체를 DROP 할 때 오류를 내지 않는다 |
두 옵션이 없으면 "이미 존재한다" 오류가 쏟아진다.
2. 파괴
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
DROP SCHEMA
CREATE SCHEMA
남은 테이블: 0
★ 그런데 서비스가 살아 있었다
https://auth.hyeonworks.com/realms/master HTTP 200
https://app1.hyeonworks.com/ HTTP 200
keycloak-0 / keycloak-1 1/1 Running
데이터베이스가 통째로 비었는데 200 이다.
Keycloak 이 realm 정보를 Infinispan realms 캐시에서 서빙하기 때문이다
(A-0 에서 그 캐시에 57개 엔트리가 있는 것을 봤다).
무엇이 깨지고 무엇이 안 깨지는가
/protocol/openid-connect/certs HTTP 200 ← realm 키는 캐시에 있다
/.well-known/openid-configuration HTTP 500 ← 이건 DB 를 본다
토큰 발급 HTTP 400
부분적으로만 깨진다. 헬스체크는 통과하고, 일부 엔드포인트는 정상이며, 로그인만 안 된다.
KEYCLOAK_JDBC_PING2: Failed to fetch the cluster members from the database
A-2(DB 프로세스 정지)와 다른 모양이다. 거기서는 커넥션이 아예 안 돼 readiness 가 DOWN 이 되고 전 파드가 Service 에서 빠졌다. 여기서는 커넥션은 되고 테이블만 없다 — 헬스체크가 통과해버린다.
"DB 가 살아 있다"와 "데이터가 있다"는 다르다. 헬스체크는 앞의 것만 본다.
3. 복구
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak \
< /tmp/keycloak-backup.sql
시작: 15:00:12
완료: 15:00:13
오류 줄: 0
1초, 오류 없음.
복구 후: realms 2 | clients 15 | users 2 | sessions 3 | authclients 1
백업 시: realms 2 | clients 15 | users 2 | sessions 3 | authclients 1
완전히 일치한다.
서비스는 재시작 없이 돌아왔다
+15초 well-known=200 토큰발급=200
→ 재시작 없이 회복
keycloak-0 restarts=0
keycloak-1 restarts=0
커넥션 풀이 이미 붙어 있었으므로 테이블이 돌아오자마자 동작했다. A-2 에서 본 것과 같은 자가 회복이다.
세션도 살아났다
user_session_id | realm
--------------------------+-------------------
E1q5xI7tt4U_WhZpW7rEPIF2 | master
2ap3DyRiBF8OdMiqCodsJ0mp | master
Zsk4QcgXf_qgyMKzde5AG-Fz | master
vsDgCVo12-qX0CC63ZmYzbYF | keycloak-patterns
persistent-user-sessions 덕분에 세션이 백업 대상이 된다 (A-0).
volatile 이었다면 세션은 애초에 DB 에 없으므로 복구해도 전원 재로그인이다.
4. RTO 와 RPO
14:59:47 파괴
15:00:12 복구 시작
15:00:13 복구 완료
~15:00:28 서비스 정상 확인
RTO ≈ 30초 (이 규모에서는 대부분이 사람의 판단 시간이다)
RPO 는 두 겹이다
① 마지막 덤프 이후의 모든 변경 ← 백업 주기가 정한다
② A-3 에서 측정한 synchronous_commit 손실 ← 수백 ms
실제 RPO = ① + ②
A-3 에서 "153건 중 4건 유실"을 측정한 것이 여기에 더해진다. 백업 주기만 보고 RPO 를 말하면 ②를 빠뜨린다.
이 실험대의 규모는 현실적이지 않다
| 이 실험대 | 운영 | |
|---|---|---|
| 덤프 크기 | 395KB | GB~TB |
| 복구 시간 | 1초 | 분~시간 |
| 세션 수 | 3 | 수만 |
복구가 1초인 것은 데이터가 작기 때문이며, 절차가 맞다는 것만 확인된다. 시간은 규모에 따라 완전히 달라진다.
5. 이 실험이 검증한 것과 못 한 것
| ✔ 덤프에 필요한 것이 다 들어간다 | realm·client·user·session·authorized client |
| ✔ 복구 절차가 동작한다 | --clean --if-exists 로 오류 0 |
| ✔ 서비스가 자가 회복한다 | 재시작 불필요 |
| ✘ 노드가 죽은 경우 | A-4 에서 본 대로 PVC 가 노드에 묶여 있다. 노드가 안 돌아오면 덤프가 유일한 길인데, 덤프를 어디에 두느냐가 문제가 된다 |
| ✘ 대규모 복구 시간 | 데이터가 작아 측정 의미가 없다 |
| ✘ 백업 자동화·보존·검증 | 이번엔 손으로 한 번 떴다 |
가장 중요한 미검증 항목이 "덤프를 어디에 두는가" 다. 이번 덤프는
test-server:/tmp에 있다. 호스트가 죽으면 같이 사라진다. A-4 에서 PVC 가 노드에 묶인 것을 봤듯, 백업도 같은 장애 도메인에 있으면 백업이 아니다.
6. 재현 절차 (명령어)
# 1. 백업 — --clean --if-exists 가 없으면 복구 때 오류가 쏟아진다
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
--clean --if-exists > keycloak-backup.sql
# 2. 무엇이 들어갔는지 확인 (세션이 있어야 한다)
grep -c '^CREATE TABLE' keycloak-backup.sql
grep -A3 'COPY public.offline_user_session' keycloak-backup.sql
# 3. 파괴
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
# 4. ★ 무엇이 깨지는지 확인 — 전부 깨지지 않는다
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/keycloak-patterns/.well-known/openid-configuration
# 5. 복구
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak < keycloak-backup.sql
# 6. 데이터 대조 — 백업 시점의 수치와 같아야 한다
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
"select (select count(*) from realm), (select count(*) from client),
(select count(*) from user_entity),
(select count(*) from offline_user_session where offline_flag='0')"
7. 다음 실험에 남기는 것
| 실험 | 이 실험이 준 것 |
|---|---|
| D-2 버전 업그레이드 | 백업이 전제다. 스키마 마이그레이션은 되돌리기 어렵다 |
| 운영 | 덤프를 다른 장애 도메인에 둔다 |
| 관측 | "DB 가 살아 있다"만 보는 헬스체크는 빈 DB 를 통과시킨다 |