docs(guides): reproduction guides for all 26 experiments
Written by subagents running under the writing-practitioner-guides skill,
one guide per experiment, 22,566 lines. Each walks a reader from baseline
capture through injection, injection verification, observation and recovery.
Section 3 carries the weight in most of them. Injection failed silently nine
times in this lab, and a failed injection looks exactly like no effect — so
the guides verify the target is actually in the intended state before
reading any result. A-4 makes virsh list the only proof because the node
reads Ready for 40 seconds after the machine is off; A-5 makes the packet
counter the sole go/no-go because a rule on the wrong node produces an empty
result that reads like a finding; A-6 quotes the run where 적용완료 was
printed between four Cannot find device "eth0" lines.
The traps the guides are built around are ones that invert a conclusion
rather than merely annoy:
A-0 emptying the session table without a restart leaves cache entries
that get counted as replication arriving
A-2 dropping -o /dev/null fuses body and status into one string
A-3 presence of "ready to accept connections" instead of its timestamp
B-2 row count alone reads an UPDATE as nothing having happened
B-4 tr ',' '\n' splits ["admin","editor"] so only admin is seen
B-7 no login screen means the cookie died and SSO re-authenticated
C-1 counting sessions without joining realm counts your own kcadm one
D-1 kubectl exec without -i restores nothing and still exits 0
D-4a "ran with error output" is what success looks like
Every quoted block is copied from docs/evidence/ and marked 실측; reshaped
commands are marked 미검증 rather than passed off as measured. Where a source
document carries a ★ correction the guides follow the corrected claim — A-7's
REVOKED_TOKEN hypothesis, C-1's session count, B-2's schema attribution.
Two hazards are stated rather than smoothed over: B-6 deletes a key that
cannot be recreated, and D-1/D-4 need host sudo, which asks for a password,
so those steps say a person must type them.
Audit over all 26: 672 interpretation pairs, 486 evidence citations, 117
undo sections, and zero occurrences of the patterns the skill forbids —
no python data processing, no deprecated kubectl get endpoints, no
placeholders, no bare kcadm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8062cc9a19
commit
6f6ab86345
@@ -0,0 +1,883 @@
|
||||
# B-5 재현 가이드 — Redis 를 내리고 파드가 `Ready` 인 채로 계속 실패하는 것을 본다
|
||||
|
||||
해설 문서: [`docs/experiment-b5-redis-loss-persistence.md`](../../experiment-b5-redis-loss-persistence.md) ·
|
||||
증거 원문: [`docs/evidence/b5-redis-loss/`](../../evidence/b5-redis-loss/)
|
||||
|
||||
## 이 가이드가 끝나면
|
||||
|
||||
당신 터미널에서 이것들을 **직접 본다.**
|
||||
|
||||
| 보게 되는 것 | 어디서 |
|
||||
|---|---|
|
||||
| 오류가 아니라 **멈추는** 것 (`HTTP 000`) | 밖에서 `curl` |
|
||||
| `/actuator/health` 는 `503` 인데 **파드는 `1/1 Ready`** | health 그룹별 응답 |
|
||||
| Service 엔드포인트에 **두 파드가 그대로** 남아 있는 것 | `endpointslice` |
|
||||
| 손대지 않아도 **재시작 0회로 회복**하는 것 | `get pods` · Lettuce |
|
||||
| **AOF 를 켰는데 재시작 후 `dbsize 0`** 인 것 | 볼륨 없는 `/data` |
|
||||
| 볼륨 위에서는 **살아남는** 것 | PVC 를 붙인 뒤 같은 시험 |
|
||||
|
||||
## 전제
|
||||
|
||||
- [`05-keycloak`](../05-keycloak/) · [`06-observability`](../06-observability/) 가 끝나 있다.
|
||||
- [`B-1`](b1-redis-session-store.md) · [`B-2`](b2-multi-instance-session.md)
|
||||
가 끝나 **세션은 Redis, 토큰은 PostgreSQL** 로 나뉘어 있다.
|
||||
나뉘어 있어야 **각각 죽여볼 수 있다** — 이 실험은 Redis 만 죽인다.
|
||||
- 명령은 **`kc-lab-1` 에서** 친다. `kubectl` 은 `sudo` 로 쓴다.
|
||||
- 브라우저로 `https://app1.hyeonworks.com/` 에 **로그인해 둔다**
|
||||
(`labuser` / `labpass`). Redis 에 세션이 하나는 있어야 「잃는 것」이 보인다.
|
||||
- Redis 는 `redis.keycloak-lab.svc:6379`, 파드는 `kc-lab-2` 에 고정되어 있다.
|
||||
|
||||
## 주의 — 이건 저장소를 지우는 실험이다
|
||||
|
||||
Redis 를 0대로 내리고, 나중에 **볼륨 없이 파드를 지운다.** 그 안의 세션은
|
||||
**돌아오지 않는다.** 로그인한 사용자는 전부 로그아웃된다. **실험대에서만 한다.**
|
||||
전 구간 약 30분. 중간에 그만두려면 [5-1](#5-1-되돌린다) 의 한 줄이면 된다.
|
||||
|
||||
## 표시 규약
|
||||
|
||||
| 표시 | 뜻 |
|
||||
|---|---|
|
||||
| **실측** | 2026-09-04 14:24–14:28 KST 실행 기록의 **출력 원문**. 증거 파일에 그대로 있다 |
|
||||
| **형태** | 값이 매번 달라지는 출력. 모양만 보이고 IP·시각은 당신 것과 다르다 |
|
||||
| **미검증** | 손으로 치기 좋게 이 가이드에서 고친 형태. 원래 실행은 스크립트였다 |
|
||||
|
||||
**이 실험대는 그 뒤로 바뀌었다.** 지금 매니페스트
|
||||
([`bff-redis.yaml`](../../../deploy/lab/k8s/bff-redis.yaml))에는 **B-5 의 결론이
|
||||
이미 반영되어** PVC 와 `--appendonly yes` 가 들어 있다. 그래서 6절은
|
||||
「볼륨 없는 상태를 다시 만드는」 단계부터 시작한다.
|
||||
|
||||
---
|
||||
|
||||
# 0. 왜 이 실험을 하는가
|
||||
|
||||
A-2 에서 Keycloak 의 PostgreSQL 을 내렸다. 그때는 이렇게 됐다.
|
||||
|
||||
```
|
||||
DB 정지 → 헬스체크 실패 → 파드 NotReady → Service 에서 빠짐 → 밖에서 503
|
||||
```
|
||||
|
||||
**명확한 실패였다.** 503 은 「지금 안 된다」고 말하고, 클라이언트는 재시도든
|
||||
포기든 결정할 수 있다.
|
||||
|
||||
| | 예측 |
|
||||
|---|---|
|
||||
| 통념 | 의존 저장소가 죽으면 **헬스체크가 알아서 파드를 빼 준다** |
|
||||
| B-5 가 재는 것 | 진짜 그런가. **그리고 이번에는 무엇을 보고 판단하는가** |
|
||||
|
||||
그리고 두 번째 질문이 붙는다.
|
||||
|
||||
```
|
||||
Redis 를 다시 띄우면 → 세션이 남아 있나?
|
||||
```
|
||||
|
||||
**「영속화를 켜 두면 남는다」가 통념이다.** 이 실험은 그 통념이 쿠버네티스에서
|
||||
어떻게 어긋나는지를 잰다. 그래서 **1-3 이 이 가이드에서 가장 중요한 절**이다 —
|
||||
영속화를 논하기 전에 **`/data` 가 무엇인지부터** 본다.
|
||||
|
||||
---
|
||||
|
||||
# 1. 기준선 — Redis 를 내리기 전에
|
||||
|
||||
```
|
||||
파드 → Redis 내용 · 영속화 설정 → ★ /data 가 볼륨인가 → 세 경로 → health 그룹
|
||||
```
|
||||
|
||||
## 1-1. 파드와 노드
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
NAME READY STATUS RESTARTS AGE IP NODE
|
||||
bff-555df79c97-6j86w 1/1 Running 0 17m 10.42.0.52 kc-lab-1
|
||||
bff-555df79c97-vgg6g 1/1 Running 0 16m 10.42.1.124 kc-lab-2
|
||||
postgres-... 1/1 Running 0 5d ... kc-lab-2
|
||||
redis-... 1/1 Running 0 3d ... kc-lab-2
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가**
|
||||
|
||||
- `bff` 가 **둘 다** `1/1`, `RESTARTS` 가 `0`
|
||||
- **Redis 는 하나다.** replica 가 없다 — 그래서 0으로 내리면 전면 정지다
|
||||
- Redis 와 PostgreSQL 이 **같은 노드**(`kc-lab-2`)다. 매니페스트가
|
||||
`nodeSelector` 로 고정한다 — A-4(노드 상실)에서 **두 저장소가 한꺼번에**
|
||||
없어지게 하려는 배치다
|
||||
|
||||
`10.42.0.52` 와 `10.42.1.124` 는 [`03-health-groups.txt`](../../evidence/b5-redis-loss/03-health-groups.txt)
|
||||
에 남은 실제 BFF 파드 IP 다. **4-3 에서 이 두 주소가 다시 나온다.**
|
||||
|
||||
## 1-2. Redis 가 지금 무엇을 들고 있나
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli ping
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get save
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get appendonly
|
||||
```
|
||||
**실측** — [`01-baseline.txt`](../../evidence/b5-redis-loss/01-baseline.txt)
|
||||
```
|
||||
=== 기준선 ===
|
||||
Redis 키: 1
|
||||
PostgreSQL 토큰: 1 행
|
||||
Redis 영속화 설정:
|
||||
save = save
|
||||
appendonly no
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — 세 가지.
|
||||
|
||||
| 값 | 그때 | 뜻 |
|
||||
|---|---|---|
|
||||
| 키 수 | `1` | 로그인 세션 하나 |
|
||||
| `save` | 빈 값 | **RDB 스냅샷이 꺼져 있다** |
|
||||
| `appendonly` | `no` | **AOF 도 꺼져 있다** |
|
||||
|
||||
**이 결과가 의미하는 것** — 그때는 **영속화가 아예 꺼져 있었다.**
|
||||
지금 당신 환경은 다를 것이다 — 매니페스트가 `--appendonly yes` 로 시작하므로
|
||||
`appendonly yes` 가 나온다. **그 차이가 6절의 출발점이다.**
|
||||
|
||||
> `save` 출력의 값이 비어 있는 것과 키가 없는 것은 다르다.
|
||||
> `config get save` 는 항상 두 줄(이름·값)을 돌려주고, 값 줄이 비어 있으면
|
||||
> 「스냅샷 조건 없음」이다. 증거의 `save = save` 는 그 두 줄이 한 줄로
|
||||
> 붙어 찍힌 모양이다.
|
||||
|
||||
## 1-3. ★ `/data` 가 볼륨인가 — 영속화를 말하기 전에 여기부터 본다
|
||||
|
||||
**이 절을 건너뛰면 6절의 결과를 오해한다.** 「AOF 를 켰는데 안 남는다」를
|
||||
「Redis 가 이상하다」로 읽게 된다.
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pod -l app=redis \
|
||||
-o jsonpath='{.items[0].spec.volumes}'; echo
|
||||
```
|
||||
**형태** — 지금 매니페스트 기준
|
||||
```json
|
||||
[{"name":"data","persistentVolumeClaim":{"claimName":"redis-data"}}]
|
||||
```
|
||||
|
||||
**확인** — 그 볼륨이 `/data` 에 붙어 있나
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pod -l app=redis \
|
||||
-o jsonpath='{.items[0].spec.containers[0].volumeMounts}'; echo
|
||||
sudo kubectl -n keycloak-lab get pvc
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
|
||||
redis-data Bound pvc-... 1Gi RWO local-path 3d
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — 세 가지가 **전부** 성립해야 한다.
|
||||
|
||||
```
|
||||
① volumes 에 항목이 있다 ← 없으면 컨테이너 파일시스템이다
|
||||
② volumeMounts 의 mountPath 가 /data ← 다른 데 붙었으면 소용없다
|
||||
③ PVC 가 Bound ← Pending 이면 파드가 안 뜬다
|
||||
```
|
||||
|
||||
**이 결과가 의미하는 것** — 셋 중 하나라도 빠지면 **`appendonly yes` 는
|
||||
장식이다.** 파일은 만들어지고 로그도 정상인데 재시작하면 사라진다.
|
||||
6절에서 그것을 직접 만든다.
|
||||
|
||||
> **개념 — 컨테이너 파일시스템은 컨테이너와 함께 죽는다.**
|
||||
>
|
||||
> ```
|
||||
> /data 가 볼륨이 아니다 → 이미지 위의 쓰기 가능 레이어에 쓴다
|
||||
> → 컨테이너가 없어지면 그 레이어도 없어진다
|
||||
> ```
|
||||
>
|
||||
> Redis 는 이것을 모른다. `appendonly yes` 를 켜면 성실히 `/data` 에
|
||||
> `appendonlydir` 을 만들고 매 쓰기를 기록한다. **거짓말이 아니라 정말로
|
||||
> 기록한다.** 다만 그 디렉터리가 어디 있는지를 모를 뿐이다.
|
||||
>
|
||||
> `emptyDir` 도 마찬가지다 — 컨테이너 재시작은 견디지만 **파드가 없어지면
|
||||
> 같이 없어진다.** 「볼륨을 붙였다」와 「영속 볼륨을 붙였다」는 다르다.
|
||||
|
||||
## 1-4. 세 경로를 정상 상태에서 잰다
|
||||
|
||||
**주입 후에 볼 것을 주입 전에 똑같은 명령으로 먼저 봐 둔다.**
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
for p in / /bff/token-boundary /actuator/health; do
|
||||
curl -s -o /dev/null -w "$p %{http_code}\n" --max-time 10 "https://app1.hyeonworks.com$p"
|
||||
done
|
||||
```
|
||||
**실측** — [`01-baseline.txt`](../../evidence/b5-redis-loss/01-baseline.txt) 는 첫 줄만 남겼다
|
||||
```
|
||||
=== 외부 진입점 정상 확인 ===
|
||||
https://app1.hyeonworks.com/ HTTP 200
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`000` 이 아닌 것.** 그게 판정 기준의 전부다.
|
||||
|
||||
| 경로 | 정상일 때 | 왜 |
|
||||
|---|---|---|
|
||||
| `/` | `200` | `permitAll` 정적 페이지. **Redis 를 안 탄다** |
|
||||
| `/bff/token-boundary` | `200` 또는 로그인으로 보내는 `3xx` | 세션이 필요하다 — **Redis 를 탄다** |
|
||||
| `/actuator/health` | `200` | 모든 지표의 합 |
|
||||
|
||||
**미검증** — 셸의 `curl` 에는 로그인 쿠키가 없으므로 두 번째는 보통 `3xx` 다.
|
||||
**`200` 이든 `3xx` 든 상관없다** — 이 실험이 보는 것은 **응답이 오는가**이고,
|
||||
`3xx` 를 만드는 과정에서도 BFF 는 세션을 만들려고 **Redis 를 건드린다.**
|
||||
|
||||
> **`--max-time` 을 반드시 붙인다.** 4-1 에서 이 요청은 **응답이 안 온다.**
|
||||
> 타임아웃이 없으면 터미널이 붙잡힌 채로 있고, 그 상태를 「멈춤」이 아니라
|
||||
> 「내 터미널이 이상함」으로 읽게 된다.
|
||||
|
||||
## 1-5. health 그룹을 미리 본다
|
||||
|
||||
**4-2 의 놀라움은 기준선을 봐 둬야 놀라움이 된다.**
|
||||
|
||||
**확인** — `/actuator/**` 는 이 실험대에서 열려 있다(운영에서는 절대 안 연다)
|
||||
```bash
|
||||
curl -s https://app1.hyeonworks.com/actuator/health; echo
|
||||
curl -s https://app1.hyeonworks.com/actuator/health/readiness; echo
|
||||
curl -s https://app1.hyeonworks.com/actuator/health/liveness; echo
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **첫 번째 응답의 본문에 `redis` 항목이 있는지**,
|
||||
그리고 **두 번째 응답에는 없는지.** 세 응답이 서로 다른 것을 본다는 것이
|
||||
이 절의 전부다.
|
||||
|
||||
**실측** — 정지 후의 값은 [`03-health-groups.txt`](../../evidence/b5-redis-loss/03-health-groups.txt)
|
||||
에 있고, 그 본문 항목 자리는 **비어 있다**
|
||||
```
|
||||
=== /actuator/health 본문 (Redis 항목이 있는가) ===
|
||||
|
||||
|
||||
=== /actuator/health/readiness 본문 ===
|
||||
{"status":"UP"}
|
||||
```
|
||||
|
||||
**첫 번째 칸이 비어 있는 것은 측정 실패다.** 파드 안에서 본문을 받아오려다
|
||||
못 받았다. **당신은 지금 밖에서 직접 재 두는 것이 낫다** — 뒤에서 이 자리를
|
||||
비교하게 된다.
|
||||
|
||||
> **개념 — Spring Boot 의 health group.**
|
||||
>
|
||||
> ```
|
||||
> /actuator/health 모든 지표의 합 ← redis 지표가 여기 있다
|
||||
> /actuator/health/readiness readiness 그룹 ← 기본값은 readinessState 뿐
|
||||
> /actuator/health/liveness liveness 그룹
|
||||
> ```
|
||||
>
|
||||
> **`redis` 헬스 지표는 자동으로 readiness 그룹에 들어가지 않는다.**
|
||||
> 그리고 kubelet 이 보는 것은 매니페스트가 지정한 경로 —
|
||||
> `readinessProbe.httpGet.path: /actuator/health/readiness` — 다.
|
||||
>
|
||||
> **전체는 DOWN 인데 readiness 는 UP 인 상태**가 성립하고, 4-2 가 그것이다.
|
||||
|
||||
**확인** — kubelet 이 실제로 무엇을 보는지 매니페스트에서 확인한다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get deploy bff \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].readinessProbe.httpGet.path}'; echo
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
/actuator/health/readiness
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 2. 주입 ① — Redis 를 0대로 내린다
|
||||
|
||||
여기부터 상태가 바뀐다. **되돌리는 명령을 먼저 읽어 둔다.**
|
||||
|
||||
**되돌리기**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab scale deployment/redis --replicas=1
|
||||
```
|
||||
|
||||
## 2-1. 왜 `scale --replicas=0` 인가
|
||||
|
||||
| 방법 | 만들어지는 상태 |
|
||||
|---|---|
|
||||
| `delete pod` | Deployment 가 **즉시 새로 만든다.** 몇 초짜리 공백이라 관찰할 시간이 없다 |
|
||||
| **`scale --replicas=0`** | **없는 상태가 유지된다.** 내가 되돌릴 때까지 |
|
||||
| NetworkPolicy 로 6379 차단 | 「연결 거부」와 「응답 없음」이 섞인다. A-1 에서 본 대로 **기존 연결은 안 끊긴다** |
|
||||
|
||||
**「저장소가 없어진 상태」를 안정적으로 유지하는 것이 목적**이므로 두 번째다.
|
||||
그리고 이 방법은 **파드가 사라지므로 주입 여부를 눈으로 확인하기 쉽다**(3-1).
|
||||
|
||||
## 2-2. 적용
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
date '+%H:%M:%S 정지'
|
||||
sudo kubectl -n keycloak-lab scale deployment/redis --replicas=0
|
||||
```
|
||||
**실측** — [`02-redis-down.txt`](../../evidence/b5-redis-loss/02-redis-down.txt)
|
||||
```
|
||||
=== ① Redis 정지 ===
|
||||
정지: 14:26:30
|
||||
deployment.apps/redis scaled
|
||||
삭제 완료
|
||||
```
|
||||
|
||||
**시각을 반드시 적어 둔다.** 5절에서 「언제부터 회복됐나」를 붙일 때 쓴다.
|
||||
|
||||
---
|
||||
|
||||
# 3. 주입이 실제로 걸렸는지 확인한다
|
||||
|
||||
**결과를 해석하기 전에, 주입이 의도한 것만 건드렸는지 먼저 본다.**
|
||||
|
||||
## 3-1. Redis 파드가 정말 없나
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pods -l app=redis
|
||||
sudo kubectl -n keycloak-lab get deploy redis
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
No resources found in keycloak-lab namespace.
|
||||
|
||||
NAME READY UP-TO-DATE AVAILABLE AGE
|
||||
redis 0/0 0 0 3d
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `0/0`. `1/1` 이면 스케일이 안 먹었거나 다른
|
||||
네임스페이스를 건드린 것이고, 그 상태에서 재는 것은 전부 무의미하다.
|
||||
|
||||
## 3-2. BFF 가 정말 못 붙고 있나
|
||||
|
||||
**응답이 없는 것과 붙지 못하는 것은 다르다.** 로그가 이유를 말한다.
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab logs -l app=bff --tail=40 | grep -iE 'redis|connect|netty' | tail -10
|
||||
```
|
||||
**실측** — [`02-redis-down.txt`](../../evidence/b5-redis-loss/02-redis-down.txt)
|
||||
```
|
||||
=== BFF 로그 ===
|
||||
at java.base/sun.nio.ch.Net.pollConnect(Native Method) ~[na:na]
|
||||
at java.base/sun.nio.ch.Net.pollConnectNow(Unknown Source) ~[na:na]
|
||||
at java.base/sun.nio.ch.SocketChannelImpl.finishConnect(Unknown Source) ~[na:na]
|
||||
at io.netty.channel.socket.nio.NioSocketChannel.doFinishConnect(NioSocketChannel.java:336) ~[netty-transport-4.1.135.Final.jar!/:4.1.135.Final]
|
||||
at io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe.finishConnect(AbstractNioChannel.java:339) ~[netty-transport-4.1.135.Final.jar!/:4.1.135.Final]
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`pollConnect` · `finishConnect`.** 연결을 **맺는
|
||||
중**이라는 뜻이다.
|
||||
|
||||
**이 결과가 의미하는 것** — 이미 실패한 것이 아니라 **아직 시도 중**이다.
|
||||
Lettuce(Netty 기반 Redis 클라이언트)가 재연결을 시도하며 타임아웃을 기다린다.
|
||||
**4-1 의 `000` 이 여기서 나온다.**
|
||||
|
||||
## 3-3. 엉뚱한 것을 죽이지 않았나
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pods
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`bff` 두 개의 `RESTARTS` 가 여전히 0**, 그리고
|
||||
**postgres 가 살아 있는 것.** postgres 까지 내렸다면 이건 B-5 가 아니라
|
||||
전면 장애를 재는 것이다.
|
||||
|
||||
**실측** — [`02-redis-down.txt`](../../evidence/b5-redis-loss/02-redis-down.txt)
|
||||
```
|
||||
=== 파드 상태 — readiness 가 Redis 를 보는가 ===
|
||||
bff-555df79c97-6j86w 1/1 Running 0 17m
|
||||
bff-555df79c97-vgg6g 1/1 Running 0 16m
|
||||
```
|
||||
|
||||
**여기서 이미 답이 절반 나와 있다** — Redis 가 없는데 **`1/1`** 이다.
|
||||
|
||||
---
|
||||
|
||||
# 4. 효과를 관찰한다
|
||||
|
||||
## 4-1. `000` 은 오류가 아니라 멈춤이다
|
||||
|
||||
**확인** — 1-4 와 **똑같은 명령**
|
||||
```bash
|
||||
for p in / /bff/token-boundary /actuator/health; do
|
||||
curl -s -o /dev/null -w "$p %{http_code}\n" --max-time 10 "https://app1.hyeonworks.com$p"
|
||||
done
|
||||
```
|
||||
**실측** — [`02-redis-down.txt`](../../evidence/b5-redis-loss/02-redis-down.txt)
|
||||
```
|
||||
=== 로그인한 사용자의 다음 요청은 어떻게 되는가 ===
|
||||
/ HTTP 200
|
||||
/bff/token-boundary HTTP 000
|
||||
/actuator/health HTTP 503
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — 세 값이 **서로 다르다.**
|
||||
|
||||
| 코드 | 뜻 |
|
||||
|---|---|
|
||||
| `200` | 정적 페이지는 산다 — **Redis 를 안 타는 경로** |
|
||||
| **`000`** | **응답 자체를 못 받았다.** curl 이 기다리다 포기했다 |
|
||||
| `503` | 헬스 엔드포인트는 **대답은 한다** — 다만 DOWN 이라고 |
|
||||
|
||||
**이 결과가 의미하는 것** — **오류를 돌려주는 것이 아니라 매달려 있다.**
|
||||
|
||||
```
|
||||
빠른 실패: 요청 → 즉시 503 → 사용자는 오류 화면을 본다. 재시도할지 정할 수 있다
|
||||
느린 실패: 요청 → ………… → 사용자는 멈춘 화면을 본다. 아무것도 정할 수 없다
|
||||
```
|
||||
|
||||
**「빨리 실패하기(fail fast)」가 안 되어 있다.** A-6(지연 주입)에서 본 것과
|
||||
같은 문제다 — **느린 실패가 빠른 실패보다 나쁘다.** 브라우저 탭도, 그 앞의
|
||||
로드밸런서도, 그 앞의 사용자도 전부 붙잡힌다.
|
||||
|
||||
**응답 본문도 비어 있다.**
|
||||
|
||||
**실측** — 같은 파일
|
||||
```
|
||||
--- token-boundary 응답 본문 ---
|
||||
|
||||
|
||||
```
|
||||
**본문이 없다는 것은 「오류 페이지조차 못 만들었다」**는 뜻이다.
|
||||
|
||||
> **고치려면 클라이언트에 타임아웃을 건다.** Lettuce 의 연결·명령 타임아웃을
|
||||
> 짧게 잡으면 `000` 이 `500` 이 된다. **500 이 000 보다 낫다** — 적어도
|
||||
> 말은 하기 때문이다.
|
||||
|
||||
## 4-2. ★ 그런데 파드는 `Ready` 를 유지한다
|
||||
|
||||
**이것이 이 실험의 가장 중요한 발견이다.**
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
curl -s -o /dev/null -w 'health %{http_code}\n' --max-time 10 https://app1.hyeonworks.com/actuator/health
|
||||
curl -s -o /dev/null -w 'readiness %{http_code}\n' --max-time 10 https://app1.hyeonworks.com/actuator/health/readiness
|
||||
curl -s -o /dev/null -w 'liveness %{http_code}\n' --max-time 10 https://app1.hyeonworks.com/actuator/health/liveness
|
||||
curl -s https://app1.hyeonworks.com/actuator/health/readiness; echo
|
||||
```
|
||||
**실측** — [`03-health-groups.txt`](../../evidence/b5-redis-loss/03-health-groups.txt)
|
||||
```
|
||||
=== health 그룹별 응답 — 왜 파드는 Ready 인가 ===
|
||||
/actuator/health HTTP server
|
||||
/actuator/health/readiness HTTP 200
|
||||
/actuator/health/liveness HTTP 200
|
||||
|
||||
=== /actuator/health 본문 (Redis 항목이 있는가) ===
|
||||
|
||||
|
||||
=== /actuator/health/readiness 본문 ===
|
||||
{"status":"UP"}
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `readiness` 가 **`200` 이고 `{"status":"UP"}`**.
|
||||
|
||||
> **첫 줄의 `HTTP server` 는 상태 코드가 아니다 — 측정이 실패한 것이다.**
|
||||
> 값이 들어와야 할 자리에 엉뚱한 문자열이 들어와 있다. `503` 이라는 값은
|
||||
> [`02-redis-down.txt`](../../evidence/b5-redis-loss/02-redis-down.txt) 쪽
|
||||
> 측정에서 나왔다.
|
||||
>
|
||||
> **빈 값이나 이상한 값을 「측정 결과」로 읽지 않는다.** 그건 「측정 실패」다.
|
||||
> A-1 에서도 빈 문자열을 「변화」로 읽어 판정이 틀어진 적이 있다.
|
||||
> 이상하면 그 자리에서 다시 친다 — 손으로 하나씩 치는 이유가 이것이다.
|
||||
|
||||
**이 결과가 의미하는 것** — 전체 상태는 DOWN 인데 **kubelet 이 보는 그룹은 UP**
|
||||
이다. 그래서 **파드를 빼지 않는다.**
|
||||
|
||||
```
|
||||
/actuator/health redis: DOWN → 전체 DOWN → 503
|
||||
/actuator/health/readiness readinessState 만 → UP → kubelet: "정상"
|
||||
```
|
||||
|
||||
## 4-3. Service 엔드포인트에 둘 다 남아 있다
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=bff \
|
||||
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||||
```
|
||||
**실측** — [`03-health-groups.txt`](../../evidence/b5-redis-loss/03-health-groups.txt)
|
||||
```
|
||||
=== Service 엔드포인트 — 트래픽을 계속 받는가 ===
|
||||
ready: [10.42.0.52 10.42.1.124]
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **두 주소가 그대로 ready 다.** 1-1 에서 본 그 두 IP.
|
||||
|
||||
**이 결과가 의미하는 것** — **두 파드가 계속 트래픽을 받으며 계속 실패한다.**
|
||||
어느 replica 로 가도 결과는 같으므로 **재시도해도 소용없다.**
|
||||
|
||||
> **`kubectl get endpoints` 는 쓰지 않는다.** v1.33 부터 deprecated 라 경고가
|
||||
> 뜬다. 해설 문서 5절의 재현 절차에는 옛 형태(`get endpoints bff`)가 실려
|
||||
> 있다 — `endpointslice` 로 본다.
|
||||
|
||||
## 4-4. A-2 와의 대비가 이 실험의 결론이다
|
||||
|
||||
| | A-2 (Keycloak · DB 상실) | **B-5 (BFF · Redis 상실)** |
|
||||
|---|---|---|
|
||||
| 의존 대상 헬스 지표 | **readiness 에 포함** | **포함 안 됨** |
|
||||
| 파드 상태 | **NotReady** | **Ready 유지** |
|
||||
| Service 엔드포인트 | **비었다** | 둘 다 남는다 |
|
||||
| 외부 응답 | **503** (즉시, 명확) | **000** (멈춤) |
|
||||
|
||||
**Keycloak 은 자기 의존성을 readiness 에 넣었고, 이 BFF 는 안 넣었다.**
|
||||
어느 쪽이 옳은지는 상황에 달렸다.
|
||||
|
||||
| readiness 에 넣으면 | 넣지 않으면 |
|
||||
|---|---|
|
||||
| 의존 대상이 죽으면 **전 파드가 빠진다** → 전면 장애 | 파드가 남아 **실패를 계속 서빙한다** |
|
||||
| 부분 기능이라도 살릴 수 없다 | 부분 기능(정적 페이지 등)은 살아 있다 |
|
||||
| A-2 처럼 **명확한 503** | **멈춤** — 진단이 어렵다 |
|
||||
|
||||
**의도적으로 골라야 하는 설정이며, 기본값에 맡기면 후자가 된다.**
|
||||
넣기로 정했다면 명시한다.
|
||||
|
||||
```yaml
|
||||
management:
|
||||
endpoint:
|
||||
health:
|
||||
group:
|
||||
readiness:
|
||||
include: readinessState, redis # 넣으려면 명시해야 한다
|
||||
```
|
||||
|
||||
> **liveness 에는 넣지 않는다.** liveness 가 실패하면 kubelet 이 파드를
|
||||
> **죽인다.** Redis 가 없어서 죽인 파드는 다시 떠도 Redis 가 없으므로 또
|
||||
> 죽는다 — **재시작해도 안 나아지는 문제에 재시작을 거는 것**이다.
|
||||
> 5-2 가 그 반대 증거다.
|
||||
|
||||
---
|
||||
|
||||
# 5. 복구 ① — 되돌리고 자동 회복을 본다
|
||||
|
||||
## 5-1. 되돌린다
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
date '+%H:%M:%S 복구'
|
||||
sudo kubectl -n keycloak-lab scale deployment/redis --replicas=1
|
||||
sudo kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
```
|
||||
**실측** — [`04-persistence.txt`](../../evidence/b5-redis-loss/04-persistence.txt)
|
||||
```
|
||||
=== 복구 ===
|
||||
deployment.apps/redis scaled
|
||||
deployment "redis" successfully rolled out
|
||||
```
|
||||
|
||||
## 5-2. 손대지 않고 회복하는지 본다
|
||||
|
||||
**BFF 를 재시작하고 싶은 충동을 참는다.** 재시작하면 「스스로 회복하는가」를
|
||||
영영 알 수 없다.
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
for p in /actuator/health /bff/token-boundary; do
|
||||
curl -s -o /dev/null -w "$p %{http_code}\n" --max-time 10 "https://app1.hyeonworks.com$p"
|
||||
done
|
||||
sudo kubectl -n keycloak-lab get pods -l app=bff
|
||||
```
|
||||
**실측** — [`04-persistence.txt`](../../evidence/b5-redis-loss/04-persistence.txt)
|
||||
```
|
||||
/actuator/health HTTP 200
|
||||
/bff/token-boundary HTTP 302
|
||||
BFF 재시작 필요했나: 0,0 회 재시작
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`재시작 0,0`.**
|
||||
|
||||
**이 결과가 의미하는 것** — **Lettuce 가 스스로 재연결했다.** A-2 에서
|
||||
Keycloak 의 커넥션 풀이 그랬던 것과 같다. **liveness 를 Redis 에 걸었다면
|
||||
파드가 재시작됐을 것**이고, 회복이 더 늦어졌을 것이다.
|
||||
|
||||
**`302` 는 실패가 아니다.** 세션이 사라졌으므로 로그인으로 보내는 것이다.
|
||||
**Redis 가 비었으므로 로그인 상태가 없다 — 사용자는 로그아웃된다.**
|
||||
|
||||
> **여기가 6절로 넘어가는 다리다.** 「Redis 를 다시 띄웠는데 왜 세션이
|
||||
> 없나」가 다음 질문이고, 답은 「영속화가 없었으니까」다. 그럼 켜면 되나?
|
||||
|
||||
---
|
||||
|
||||
# 6. 주입 ② — 영속화를 켜고 파드를 지운다
|
||||
|
||||
## 6-1. 볼륨이 없던 상태를 다시 만든다
|
||||
|
||||
**지금 실험대에는 이미 PVC 가 붙어 있다**(1-3 에서 확인했다). 원래 측정
|
||||
당시에는 없었다. **볼륨을 떼야 그때를 재현한다.**
|
||||
|
||||
**되돌리기** — **먼저 읽어 둔다**
|
||||
```bash
|
||||
sudo kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
||||
sudo kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
```
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab patch deployment redis --type=json \
|
||||
-p '[{"op":"remove","path":"/spec/template/spec/containers/0/volumeMounts"},
|
||||
{"op":"remove","path":"/spec/template/spec/volumes"}]'
|
||||
sudo kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
```
|
||||
**미검증** — 원래 실행은 **반대 순서**였다(볼륨 없는 상태에서 시작해 PVC 를
|
||||
붙였다). 지금 실험대에서 같은 관찰을 하려면 이 방향이 된다.
|
||||
|
||||
**확인** — 1-3 과 **똑같은 명령**으로 떨어진 것을 본다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pod -l app=redis \
|
||||
-o jsonpath='{.items[0].spec.volumes}'; echo
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **빈 줄이 나와야 한다.** 여기서 여전히 PVC 가 보이면
|
||||
패치가 안 먹은 것이고, 그 상태로 6-3 을 하면 **당연히 살아남는다** — 그리고
|
||||
그걸 「영속화가 잘 된다」로 오독한다.
|
||||
|
||||
> **PVC 자체는 지우지 않는다.** Deployment 에서 참조만 뗐다. 6-5 에서
|
||||
> `apply` 로 되돌리면 같은 PVC 에 다시 붙는다. **PVC 를 지우면
|
||||
> `local-path` 프로비저너가 노드의 디렉터리까지 지운다.**
|
||||
|
||||
## 6-2. AOF 를 켜고 키를 심는다
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli config set appendonly yes
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get appendonly
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli set b5:aof "written-with-aof"
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- ls -la /data
|
||||
```
|
||||
**실측** — [`04-persistence.txt`](../../evidence/b5-redis-loss/04-persistence.txt)
|
||||
```
|
||||
--- AOF 를 켜고 다시 심는다 (영속화가 켜져 있으면 살아남는가) ---
|
||||
appendonly yes
|
||||
total 12
|
||||
drwxr-xr-x 3 redis redis 4096 Sep 4 05:26 .
|
||||
drwxr-xr-x 1 root root 4096 Sep 4 05:26 ..
|
||||
drwx------ 2 redis redis 4096 Sep 4 05:26 appendonlydir
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`appendonlydir` 이 실제로 만들어졌다.**
|
||||
|
||||
**이 결과가 의미하는 것** — **Redis 는 시킨 대로 했다.** 설정도 `yes` 고
|
||||
디렉터리도 있고 파일도 쓰인다. **여기서 「영속화가 켜졌다」고 결론 내리면
|
||||
틀린다** — 어디에 쓰는지를 안 봤기 때문이다. 1-3 에서 이미 본 대로 지금
|
||||
`/data` 는 **컨테이너 파일시스템**이다.
|
||||
|
||||
## 6-3. 파드를 지운다
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab delete pod -l app=redis
|
||||
sudo kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli get b5:aof
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get appendonly
|
||||
```
|
||||
**실측** — [`04-persistence.txt`](../../evidence/b5-redis-loss/04-persistence.txt)
|
||||
```
|
||||
--- 파드를 지운다 ---
|
||||
deployment "redis" successfully rolled out
|
||||
재기동 후:
|
||||
dbsize: 0
|
||||
b5:probe
|
||||
b5:aof
|
||||
appendonly no
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **두 가지가 같이 사라졌다.**
|
||||
|
||||
| 사라진 것 | 왜 |
|
||||
|---|---|
|
||||
| **데이터** | `/data` 가 컨테이너 파일시스템이었다 — 컨테이너와 함께 없어졌다 |
|
||||
| **설정** | `CONFIG SET` 은 **런타임 전용**이다. 재기동하면 매니페스트의 `args` 가 이긴다 |
|
||||
|
||||
**이 결과가 의미하는 것**
|
||||
|
||||
> **쿠버네티스에서 영속화 설정만 켜는 것은 장식이다.**
|
||||
> `appendonly yes` 를 켜고 안심하는 것이 가장 위험하다 — **파일은 만들어지고
|
||||
> 로그도 정상이며, 사라지는 것은 재시작 순간뿐**이다. 그리고 재시작은
|
||||
> 노드 정비·이미지 갱신·OOM 어느 것으로든 일어난다.
|
||||
|
||||
**설정이 되돌아간 것도 따로 중요하다.** `CONFIG SET` 으로 고친 값은
|
||||
`CONFIG REWRITE` 를 하지 않으면 파일에 안 남고, 컨테이너에서는 그 파일 자체가
|
||||
안 남는다. **런타임 설정으로 영속 동작을 정하려는 시도는 두 겹으로 실패한다.**
|
||||
|
||||
## 6-4. 볼륨을 되돌리고 같은 시험을 다시 한다
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
sudo kubectl apply -f deploy/lab/k8s/bff-redis.yaml
|
||||
sudo kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
```
|
||||
|
||||
**확인** — 1-3 과 **똑같은 명령**으로 볼륨이 돌아온 것을 본다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pod -l app=redis \
|
||||
-o jsonpath='{.items[0].spec.volumes}'; echo
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli config get appendonly
|
||||
```
|
||||
|
||||
**하기** — 키를 심고 다시 지운다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli set b5:pvc "written-on-pvc"
|
||||
sudo kubectl -n keycloak-lab delete pod -l app=redis
|
||||
sudo kubectl -n keycloak-lab rollout status deployment/redis --timeout=180s
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli dbsize
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli get b5:pvc
|
||||
```
|
||||
**실측** — [`04-persistence.txt`](../../evidence/b5-redis-loss/04-persistence.txt)
|
||||
```
|
||||
=== 영속 볼륨 위에서 다시 시험 ===
|
||||
appendonly yes
|
||||
키 심음: written-on-pvc
|
||||
sed: -e expression #1, char 8: unknown option to 's'
|
||||
|
||||
--- 파드를 지운다 ---
|
||||
deployment "redis" successfully rolled out
|
||||
재기동 후:
|
||||
dbsize: 1
|
||||
b5:pvc written-on-pvc
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`dbsize: 1` 과 `written-on-pvc`.** 살아남았다.
|
||||
|
||||
> 중간의 `sed: -e expression #1, char 8: unknown option to 's'` 는
|
||||
> **원래 실행의 스크립트가 낸 오류**이고 측정과는 무관하다. 값에 `/` 가
|
||||
> 들어간 문자열을 `sed 's/.../.../'` 에 그대로 넣으면 이렇게 된다.
|
||||
> **증거 파일에 남은 오류를 지우지 않은 것**은, 그것이 「이 줄은 스크립트가
|
||||
> 만든 것」이라는 표시이기 때문이다.
|
||||
|
||||
## 6-5. 순서가 있다
|
||||
|
||||
| 구성 | 파드 삭제 후 |
|
||||
|---|---|
|
||||
| AOF **끔**, 볼륨 없음 | 전부 소실 |
|
||||
| AOF **켬**, 볼륨 없음 | **전부 소실** (설정은 켰는데) |
|
||||
| AOF **켬**, **PVC** | **생존** |
|
||||
|
||||
**볼륨이 먼저고 설정이 나중이다.** 순서를 바꾸면 두 번째 줄이 된다 —
|
||||
그리고 두 번째 줄은 **첫 번째 줄과 결과가 같은데 안심하고 있다는 점에서
|
||||
더 나쁘다.**
|
||||
|
||||
### `appendfsync` 는 여전히 트레이드오프다
|
||||
|
||||
```
|
||||
appendfsync everysec ← 기본값
|
||||
```
|
||||
|
||||
| 설정 | 잃는 양 | 비용 |
|
||||
|---|---|---|
|
||||
| `always` | 없음 | 쓰기마다 fsync — 느리다 |
|
||||
| **`everysec`** | **최대 1초** | 기본값 |
|
||||
| `no` | OS 에 맡김 | 가장 빠름 |
|
||||
|
||||
**세션 저장소에서 1초를 잃는다는 것은 그 사이 로그인한 사용자가 다시
|
||||
로그인해야 한다는 뜻이다.** A-3 에서 본 PostgreSQL 의 `synchronous_commit OFF`
|
||||
와 **같은 모양의 맞바꿈**이고, 거기서 Keycloak 이 같은 판단을 했다.
|
||||
|
||||
### PVC 도 노드에 못박힌다
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl get pvc -n keycloak-lab redis-data -o jsonpath='{.spec.storageClassName}'; echo
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
local-path
|
||||
```
|
||||
|
||||
**`local-path` 는 노드의 디렉터리다.** A-4 에서 본 것과 같다 — **노드가 죽으면
|
||||
볼륨도 함께 접근 불가**가 되고, 파드는 다른 노드로 못 옮겨간다.
|
||||
|
||||
**영속화는 재시작을 견디게 하지만 노드 상실을 견디게 하지는 않는다.**
|
||||
|
||||
---
|
||||
|
||||
# 7. 복구 ② · 원상복구 확인표
|
||||
|
||||
## 7-1. 실험이 심은 키를 지운다
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli del b5:aof b5:pvc b5:probe
|
||||
sudo kubectl -n keycloak-lab exec deploy/redis -- redis-cli --scan
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `b5:` 로 시작하는 키가 없는 것. **`FLUSHALL` 은
|
||||
치지 않는다** — BFF 세션과 oauth2-proxy 세션이 같은 Redis 에 있다.
|
||||
|
||||
## 7-2. 원상복구 확인표
|
||||
|
||||
| 항목 | 명령 | 돌아왔을 때 |
|
||||
|---|---|---|
|
||||
| Redis | `sudo kubectl -n keycloak-lab get deploy redis` | `1/1` |
|
||||
| 볼륨 | `… get pod -l app=redis -o jsonpath='{.items[0].spec.volumes}'` | `persistentVolumeClaim` 이 보인다 |
|
||||
| PVC | `sudo kubectl -n keycloak-lab get pvc` | `redis-data` `Bound` |
|
||||
| 영속화 | `… exec deploy/redis -- redis-cli config get appendonly` | `yes` |
|
||||
| BFF | `sudo kubectl -n keycloak-lab get pods -l app=bff` | 둘 다 `1/1`, `RESTARTS 0` |
|
||||
| 엔드포인트 | `… get endpointslice -l kubernetes.io/service-name=bff` | ready 주소 **둘** |
|
||||
| 실험 키 | `… redis-cli --scan` | `b5:*` 없음 |
|
||||
| 밖 | `curl -s -o /dev/null -w '%{http_code}\n' --max-time 10 https://app1.hyeonworks.com/` | `200` |
|
||||
|
||||
**로그인 세션은 돌아오지 않는다.** 브라우저에서 다시 로그인하는 것이 복구다.
|
||||
|
||||
---
|
||||
|
||||
# 막히면
|
||||
|
||||
전부 이 실험대가 **실제로 겪은** 증상이거나, 그 기록에서 곧바로 따라 나오는 것이다.
|
||||
|
||||
| 증상 | 원인 | 확인 |
|
||||
|---|---|---|
|
||||
| `curl` 이 안 끝나고 터미널이 붙잡힌다 | **그게 이 실험의 결과다.** `000` 이 되는 과정이다 | `--max-time` 을 붙인다 — 1-4 |
|
||||
| `000` 을 서버 오류로 읽는다 | `000` 은 **응답을 못 받았다**는 curl 의 표기다 | `400`/`503` 과 구별한다 — 4-1 |
|
||||
| **AOF 를 켰는데 안 남는다** | **`/data` 가 볼륨이 아니다** | 결론 내리기 전에 1-3 을 먼저 |
|
||||
| 6-3 에서 데이터가 **살아남는다** | 볼륨 제거 패치가 안 먹었다 | `get pod … spec.volumes` 가 **비어야** 한다 — 6-1 |
|
||||
| `config set` 한 값이 재기동 후 사라진다 | **런타임 전용이다.** 매니페스트 `args` 가 이긴다 | 6-3 |
|
||||
| `/actuator/health` 응답 자리에 이상한 문자열 | **측정 실패다.** 값이 아니다 (`HTTP server`) | 그 자리에서 다시 친다 — 4-2 |
|
||||
| 파드가 `NotReady` 가 되기를 기다린다 | **안 된다.** `redis` 지표가 readiness 그룹에 없다 | 4-2 · 4-4 |
|
||||
| `kubectl get endpoints` 가 경고를 찍는다 | v1.33 부터 deprecated | `get endpointslice -l kubernetes.io/service-name=bff` |
|
||||
| 회복 후 로그인이 풀려 있다 | **정상이다.** Redis 가 비었으니 세션이 없다 | `302` 는 실패가 아니다 — 5-2 |
|
||||
| BFF 를 재시작해 버렸다 | 「스스로 회복하는가」를 못 재게 된다 | 다시 2절부터. 손대지 않고 기다린다 |
|
||||
| PVC 가 `Pending` | `local-path` 프로비저너가 없거나 노드가 안 맞는다 | `describe pvc redis-data` 의 Events |
|
||||
| 다른 실험이 갑자기 깨진다 | **`FLUSHALL` 을 쳤다.** 같은 Redis 를 나눠 쓴다 | 접두어로만 지운다 — 7-1 |
|
||||
|
||||
---
|
||||
|
||||
# 이 실험이 관측에 남긴 숙제
|
||||
|
||||
**Grafana 에 이 실험의 그래프가 없다.** 안 찍은 것이 아니라 **지표가 없다.**
|
||||
|
||||
**실측** — [`04-observability-gap.txt`](../../evidence/followup/04-observability-gap.txt)
|
||||
(후속 조사)
|
||||
```
|
||||
=== B층 구성 요소의 지표가 있는가 ===
|
||||
redis_up 시계열 0개
|
||||
redis_connected_clients 시계열 0개
|
||||
pg_up 시계열 0개
|
||||
pg_stat_database_numbackends 시계열 0개
|
||||
```
|
||||
|
||||
Prometheus 가 긁는 대상에 **Redis·PostgreSQL·BFF 가 애초에 없다.**
|
||||
A층이 Grafana 증거를 남길 수 있었던 것은 Keycloak 이 `/metrics` 를 내놓고
|
||||
그것을 scrape 대상에 넣어 뒀기 때문이다.
|
||||
|
||||
> **관측은 「나중에 붙이는 것」이 아니라 실험 설계에 포함되어야 한다.**
|
||||
> 이 실험에서 「Redis 가 언제 끊겼고 언제 붙었나」를 초 단위로 보고 싶다면
|
||||
> `redis_exporter` 가 먼저 있어야 하고, 그건 실험이 끝난 뒤에는 못 만든다.
|
||||
|
||||
| 대상 | 방법 |
|
||||
|---|---|
|
||||
| Redis | `redis_exporter` 사이드카 또는 Deployment |
|
||||
| PostgreSQL | `postgres_exporter` |
|
||||
| BFF | 이미 actuator 가 있다 — `/actuator/prometheus` 노출 + scrape 추가 |
|
||||
| 파드 readiness | `kube-state-metrics` (A-2 에서 이미 찾은 항목) |
|
||||
|
||||
---
|
||||
|
||||
# 다음
|
||||
|
||||
| 실험 | B-5 가 남긴 질문 |
|
||||
|---|---|
|
||||
| [B-3](b3-refresh-token-contention.md) refresh 경쟁 | **Redis lock 을 쓴다면 여기서 갱신이 멈춘다.** DB 행 잠금이 유리한 이유가 이 실험으로 보강된다 |
|
||||
| B-6 암호화 key 교체 | Redis 가 이제 영속적이므로 **key 를 바꾸면 옛 데이터가 남아 있다** |
|
||||
| D-1 백업·복구 | `local-path` PVC 는 **노드에 묶여 있다** — 노드가 안 돌아오면 백업뿐 |
|
||||
| 구성 | **readiness 그룹에 무엇을 넣을지 명시적으로 정한다.** 기본값은 결정이 아니다 |
|
||||
| 구성 | Redis 클라이언트에 **타임아웃**을 걸어 `000` 을 `500` 으로 바꾼다 |
|
||||
Reference in New Issue
Block a user