Files
keycloak-pattern/docs/experiment-d1-backup-restore.md
DongHyeonkaandClaude Opus 5 df5af95cb3 docs: D-1 — an empty database still answered 200
Dropping the schema left Keycloak serving realm metadata and JWKS from its Infinispan cache, so the front door stayed at 200 while only the paths that read the database failed. That is a different shape from A-2, where the connection itself broke and readiness pulled the pods out of the Service; here the connection is fine and the tables are simply gone, which the health check does not notice.

Restoring the pg_dump took one second with zero errors and no pod restart, and the row counts matched the backup exactly, sessions included. The real RPO is the backup interval plus the synchronous_commit loss measured in A-3, and this dump sits in the host's /tmp, which is the same failure domain as the thing it protects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 14:59:56 +09:00

8.0 KiB
Raw Permalink 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


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