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

309 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# D-1 — 백업이 있다와 복구해봤다는 다르다
브랜치 `feature/keycloak-d1-backup-restore` ·
증거 [`docs/evidence/d1-backup-restore/`](evidence/d1-backup-restore/) ·
2026-09-04 16:5517:05 KST
선행: [`A-2`](experiment-a2-database-loss.md) · [`A-3`](experiment-a3-database-crash.md)
---
## 구조
![D-1 구조 — 파괴와 복구, 그리고 캐시가 가린 것](diagrams/d1-backup-restore.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](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. 백업
```bash
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. 파괴
```bash
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. 복구
```bash
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`](evidence/d1-backup-restore/01-backup.txt) | 터미널 원문 |
| [`02-destruction.txt`](evidence/d1-backup-restore/02-destruction.txt) | 터미널 원문 |
| [`03-restore.txt`](evidence/d1-backup-restore/03-restore.txt) | 터미널 원문 |
파일별 상세는 [`evidence/d1-backup-restore/README.md`](evidence/d1-backup-restore/README.md).
## 6. 재현 절차 (명령어)
```bash
# 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 를 통과시킨다** |