Files
keycloak-pattern/docs/experiment-d1-backup-restore.md
T
DongHyeonkaandClaude Opus 5 e0d27d47ce docs: correct the places where documents contradicted their own evidence
An independent audit found ten documents printing values their evidence files do not contain. C-1 printed a session count of 0 where the evidence says 4, C-2 printed a success readback for a command that exited 1, and A-1 credited the conntrack flush with a split that the timestamps attribute to a pod restart four seconds earlier.

Also measured wal_writer_delay, which A-3 had asserted as matching without ever querying it, relabelled the A-6 control that moved 41 percent, noted A-8's nine-sample resolution, corrected D-1's RTO to the 41 seconds its own timeline shows, and added a correction banner to D-2. Every experiment document now links its evidence files with their real collection times, and the duplicate screenshots are documented as duplicates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 16:35:49 +09:00

10 KiB
Raw Blame History

D-1 — 백업이 있다와 복구해봤다는 다르다

브랜치 feature/keycloak-d1-backup-restore · 증거 docs/evidence/d1-backup-restore/ · 2026-09-04 16:5517:05 KST

선행: A-2 · A-3


구조

D-1 구조 — 파괴와 복구, 그리고 캐시가 가린 것

다이어그램 규약은 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 41초 (14:59:47 파괴 → 15:00:28 서비스 확인)
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 = 41초  (이 규모에서는 대부분이 사람의 판단 시간이다)

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 가 노드에 묶인 것을 봤듯, 백업도 같은 장애 도메인에 있으면 백업이 아니다.



개념

pg_dump 의 두 옵션

옵션 없으면
--clean 복구 시 기존 객체와 충돌
--if-exists 없는 객체 DROP 에서 오류가 쏟아진다

"DB 가 살아 있다" 와 "데이터가 있다" 는 다르다

   A-2  DB 프로세스 정지  →  커넥션 실패  →  readiness DOWN  →  파드가 Service 에서 빠짐
   D-1  스키마만 삭제      →  커넥션 정상  →  readiness UP    →  ★ 파드가 그대로 트래픽을 받는다

헬스체크는 커넥션만 본다. 그래서 빈 데이터베이스를 통과시킨다. 그리고 Keycloak 이 realm 캐시로 일부를 계속 서빙해 부분적으로만 깨진다.

RPO 는 두 겹이다

   ① 마지막 덤프 이후의 변경        ← 백업 주기가 정한다
   ② synchronous_commit OFF 손실    ← A-3 에서 측정한 수백 ms
   실제 RPO = ① + ②

백업 주기만 보고 RPO 를 말하면 ②를 빠뜨린다.

백업의 장애 도메인

이번 덤프는 test-server:/tmp 에 있었다. 호스트가 죽으면 같이 사라진다. A-4 에서 PVC 가 노드에 묶인 것과 같은 문제이며, 같은 장애 도메인에 있는 백업은 백업이 아니다.



증거 파일

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

파일 종류
01-backup.txt 터미널 원문
02-destruction.txt 터미널 원문
03-restore.txt 터미널 원문

파일별 상세는 evidence/d1-backup-restore/README.md.

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 를 통과시킨다