기반 가이드 7단계로 실험대를 철거하고 다시 세운 뒤 virtualization setup 9편과 keycloak-session-store 26편을 순서대로 밟았다. 24편은 끝까지, 11편은 되는 데까지 밟았고 밟은 범위를 편마다 적었다. 명령이 못 도는 것을 고쳤다. - kubectl 을 `kc-lab-1` 에서 치라고 적었는데 그 기계에 kubeconfig 가 없다. 라벨 639개와 각 편의 「어디서 치는가」를 `[lab host]` 로 옮겼다 - `-o custom-columns=…[0]…` 이 zsh 에서 글로브로 읽혀 안 돈다. 28곳에 따옴표 - busybox `sed` 가 끝 개행을 안 붙여 A-3 의 측정이 언제나 0 이었다 - `--token-file ~/node-token` 뒤에 그 파일을 지우면 k3s agent 가 재부팅을 못 견딘다. `/etc/rancher/node-token` 으로 옮기는 처방을 재서 넣었다 - 게스트에 없는 도구를 전제로 한 명령 넷 — `conntrack`·`dig`·`strings`·`nginx -v` - `echo` 와 JWT 헤더가 `"이름" : [ 값 ]` 으로 찍는데 문서는 공백 없이 옮겨 적어 그 실측으로 만든 grep·sed 가 한 줄도 못 잡는다 - B-0 이 `directAccessGrantsEnabled` 와 계정 완성을 빠뜨려 B-3 이 못 돈다 - D-4·D-4a 가 `test-server` 와 `certbot-renew.*` 를 가리키는데 실제로는 `kc-lab-edge` 의 `certbot.service` 다 - `virsh setmaxmem --config` 를 `dominfo` 로 판정하면 틀린다. `--inactive` 로 - `LIBVIRT_DEFAULT_URI` 를 rc 에만 넣으면 `ssh host '명령'` 에서 안 먹는다 결과가 조건부인 것을 갈랐다. - readiness 는 즉시 안 뒤집힌다. A-1·A-2 의 60초 창을 적었다 - 03 의 층 ②③ `301` 은 04 이후의 값이고 그 단계에서는 `404` 다 - A-0 의 로그 필터를 요청 직후에 치면 정반대 결론이 나온다 - A-5 의 한 방향 차단은 잠깐 `1` 이었다 `2` 로 돌아온다 증거는 두 프로젝트의 `evidence/raw/` 에 99벌을 README 와 함께 남겼다. 비밀은 길이만 적었고 화면에 찍힌 토큰은 가렸다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
724 lines
39 KiB
Markdown
724 lines
39 KiB
Markdown
---
|
||
id: 0b64d23a-f82a-44b4-ad54-e079578977c4
|
||
kind: SETUP
|
||
slug: reproduce-a8-rolling-restart
|
||
title: 롤링 재시작을 걸고 재시작 전 토큰이 통하는지 본다
|
||
topic: session-custody-across-nodes
|
||
topicName: Keycloak 두 노드가 같은 세션을 읽는 경로
|
||
project: keycloak-session-store
|
||
status: 게시 전
|
||
studio: "https://hyeonworks.com/studio/documents/0b64d23a-f82a-44b4-ad54-e079578977c4/edit"
|
||
pinnedVersions:
|
||
- name: Keycloak
|
||
version: 26.7.0
|
||
- name: curlimages/curl
|
||
version: 8.11.1
|
||
source:
|
||
- final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-8
|
||
sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||
---
|
||
|
||
# 롤링 재시작을 걸고 재시작 전 토큰이 통하는지 본다
|
||
|
||
`rollout restart` 로 파드 둘을 교체한 뒤에도 토큰이 아직 통하는지 보는 절차다. 로그인해서 refresh token 과 `sid` 를 탐침 파드 안 파일에 담아 두고 교체한 다음 그 토큰을 쓴다. 외부 응답 시계열도 함께 잰다. 전 구간 15~20분이고 되돌릴 것이 없다.
|
||
|
||
## 관계
|
||
|
||
- **롤링 재시작은 세션을 남기고 캐시만 지웠다**
|
||
이 절차가 재는 것을 그 기록이 결론으로 적는다.
|
||
- **persistent-user-sessions 가 세션의 거처를 정한다**
|
||
세션이 살아남는 까닭이 그 기능이고, 그것이 꺼져 있으면 이 절차는 정반대 답을 낸다.
|
||
- **산문으로 적힌 측정 장치를 실행 가능하게 고쳤더니 한 건이 깨졌다**
|
||
여기의 가용성 루프가 그 점검에서 실행 가능한 형태로 고쳐진 명령 가운데 하나다.
|
||
- **persistent-user-sessions 를 끄고 A층 결론 넷을 다시 잰다**
|
||
같은 시험을 옛 기본값 위에서 치면 `200` 이 `400 Session not active` 로 바뀐다.
|
||
- **세션을 공유하는 것이 Infinispan 인지 PostgreSQL 인지 손으로 가른다**
|
||
「세션은 DB 에 있고 캐시는 사본」이라는 모델을 거기서 세웠고, 여기서 파드를 통째로 갈아 그 모델을 확인한다.
|
||
|
||
## 본문
|
||
|
||
<!-- body:start -->
|
||
|
||
## 읽기 전에 — 어디서 치는가
|
||
|
||
명령은 전부 `[lab host]` 에서 `kubectl` 로 친다. 노드 자체를 건드리는 명령이 없어서 게스트에 들어갈 일이 없다. `kubectl` 에 `sudo` 를 붙이지 않는다 — root 홈에는 `~/.kube/config` 가 없어서 `localhost:8080` 으로 붙으려다 `connection refused` 로 끝난다.
|
||
|
||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||
|
||
터미널은 둘을 연다. 하나는 가용성 감시용이라 루프가 도는 동안 붙잡혀 있고, 하나는 재시작과 관찰용이다.
|
||
|
||
| 무엇 | 값 |
|
||
|---|---|
|
||
| 네임스페이스 | `keycloak-lab` · 관측 스택은 `observability` |
|
||
| 대상 | StatefulSet `keycloak` 파드 둘 · Deployment `postgres` 하나 |
|
||
| 탐침 파드 | `a8-probe` — `curlimages/curl:8.11.1`, `sleep 7200`, `--restart=Never` |
|
||
| 전제 args | `["start"]` — 플래그가 붙어 있으면 이 절차가 아니다 |
|
||
| 가용성 루프 | 5초 간격 48회 · `--max-time 4` · 외부 진입점으로 |
|
||
| 무중단의 전제 | replica 2 와 readiness 프로브 |
|
||
| 도구 | `jq` 가 이 실험대에 없다. Prometheus 출력은 `tr` 과 `grep` 으로 자른다 |
|
||
|
||
## 이 실험이 가르는 것
|
||
|
||
운영에서 가장 자주 겪는 작업이다. 장애가 아니라 정상 배포인데도 사용자가 로그아웃되면 그건 사고다.
|
||
|
||
```text
|
||
배포한다 → 파드가 교체된다 → 프로세스 메모리가 사라진다
|
||
│
|
||
└─ 세션이 거기 있었다면?
|
||
```
|
||
|
||
A-0 은 「세션의 진실은 PostgreSQL 에 있고 Infinispan 캐시는 사본」이라는 모델을 세웠다. 그 모델이 맞다면 파드를 통째로 갈아도 세션은 살아야 하고, 틀리다면 배포가 곧 전원 로그아웃이다.
|
||
|
||
| | 예측 |
|
||
|---|---|
|
||
| A-0 모델 (persistent) | 재시작해도 세션 생존 |
|
||
| 옛 방식 (volatile) | 재시작하면 전원 로그아웃 |
|
||
|
||
둘 중 하나는 틀렸고, 재시작 전에 받은 토큰을 재시작 후에 써 보면 판정된다. 그리고 이 절차는 가용성도 같이 잰다 — 세션이 살아도 재시작 중에 서비스가 끊기면 그것대로 문제가 된다.
|
||
|
||
이 절차를 끝까지 치면 여섯을 손으로 보게 된다. 파드가 전부 교체되는 동안 외부가 계속 `200` 인 것, 재시작 전에 발급한 토큰이 재시작 후에도 통하는 것, DB 세션 수가 그대로인 것, 캐시만 0 으로 비워지는 것, 클러스터가 스스로 다시 붙는 것, 그리고 「무중단」이 관측 해상도에 달려 있다는 것.
|
||
|
||
## 전제와 되돌리기
|
||
|
||
앞선 구축 단계 `05-keycloak` 과 `06-observability` 가 끝나 있어야 한다. A-0 을 먼저 하면 좋다 — 「세션은 DB 에 있고 캐시는 사본이다」라는 모델이 여기서 그대로 확인된다.
|
||
|
||
이건 파괴적이지 않다. 그래서 더 조심한다. `rollout restart` 는 정상 작업이고 되돌릴 것이 없으며 잘못돼도 클러스터가 스스로 회복한다. 그 대신 함정이 다르다 — 재는 것이 「안 깨졌나」라서 측정을 잘못하면 안 깨진 것처럼 보이기가 너무 쉽고, 원래 실행이 실제로 그랬다.
|
||
|
||
다른 실험과 겹치지 않게 한다. 롤링 재시작 중에 다른 주입이 들어가 있으면 무엇 때문에 무엇이 일어났는지 구별되지 않는다.
|
||
|
||
정말 되돌려야 하면 이 명령이 있다. 다만 중간에 `rollout status` 를 `Ctrl-C` 로 끊어도 롤아웃 자체는 계속 진행되므로 끝날 때까지 두는 편이 낫다.
|
||
|
||
```bash label="[lab host] 직전 리비전으로 되돌린다"
|
||
kubectl -n keycloak-lab rollout undo statefulset/keycloak
|
||
```
|
||
|
||
## 주입 전에 같은 명령으로 먼저 본다
|
||
|
||
```text
|
||
파드·나이 → args → DB 세션 수 → 상주 탐침 → 토큰 확보 → 대조군 시험 → 캐시·클러스터
|
||
```
|
||
|
||
### 1. 파드와 나이와 replica 수를 적어 둔다
|
||
|
||
**목적** — 재시작 전의 `AGE` 를 확보하고 replica 가 2 인지 확인한다.
|
||
|
||
```bash label="[lab host] 파드를 노드와 함께 넓게 본다"
|
||
kubectl -n keycloak-lab get pods -o wide
|
||
```
|
||
|
||
**예상 결과** — 모양은 이렇고 값은 환경마다 다르다.
|
||
|
||
```text
|
||
NAME READY STATUS RESTARTS AGE IP NODE
|
||
keycloak-0 1/1 Running 0 2d 10.42.1.94 kc-lab-2
|
||
keycloak-1 1/1 Running 0 2d 10.42.0.45 kc-lab-1
|
||
postgres-7b474b88c8-t6rrf 1/1 Running 0 5d 10.42.0.22 kc-lab-1
|
||
```
|
||
|
||
`READY` 가 둘 다 `1/1`, `RESTARTS` 가 `0`, 그리고 `AGE` 를 적어 둔다. 재시작 후 이 값이 초 단위로 바뀌는 것으로 「정말 재시작됐다」를 판정한다. `keycloak` 파드가 둘인 것도 함께 본다. 그것이 무중단의 전제이고 하나면 반드시 끊긴다.
|
||
|
||
**왜 필요한가** — `rollout restart` 는 파드를 삭제하고 새로 만들기 때문에 `RESTARTS` 가 안 오른다. 재시작 여부를 `RESTARTS` 로 보면 아무 일도 안 일어났다고 읽게 된다.
|
||
|
||
**문제가 생기면** — `keycloak` 파드가 하나뿐이면 이 절차의 가용성 측정은 성립하지 않는다.
|
||
|
||
```bash label="[lab host] 두 파드 IP 를 변수에 담는다"
|
||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||
echo "$K0 $K1"
|
||
```
|
||
|
||
### 2. args 가 기본값인지 확인한다
|
||
|
||
**목적** — `persistent-user-sessions` 가 켜져 있는 상태에서 재는지 본다.
|
||
|
||
```bash label="[lab host] 컨테이너 args 를 그대로 찍는다"
|
||
kubectl -n keycloak-lab get statefulset keycloak \
|
||
-o jsonpath='{.spec.template.spec.containers[0].args}' ; echo
|
||
```
|
||
|
||
**예상 결과**
|
||
|
||
```text
|
||
["start"]
|
||
```
|
||
|
||
**왜 필요한가** — 플래그가 없으므로 `persistent-user-sessions` 가 기본으로 켜져 있다. `--features-disabled=persistent-user-sessions` 가 붙어 있으면 이 절차는 정반대 결과를 낸다.
|
||
|
||
**문제가 생기면** — 플래그가 보이면 앞 실험이 원복하지 않고 끝냈다. 그것부터 되돌린 뒤에 시작한다.
|
||
|
||
### 3. DB 세션 수를 적어 둔다
|
||
|
||
**목적** — 재시작 후에 견줄 값을 확보한다.
|
||
|
||
```bash label="[lab host] 온라인 세션과 offline token 을 나눠 센다"
|
||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "select offline_flag, count(*) from offline_user_session group by offline_flag"
|
||
```
|
||
|
||
**예상 결과**
|
||
|
||
```text
|
||
DB 세션 수: 151
|
||
```
|
||
|
||
**왜 필요한가** — 재시작 후 같은 값이 나오는지가 뒤의 판정에 들어간다. 숫자는 환경마다 다르고 관리 API 호출도 세션을 만들기 때문에 개수에는 소음이 섞인다. 그래서 이 절차는 개수 말고 특정 `sid` 하나를 따로 추적한다.
|
||
|
||
**문제가 생기면** — `(0 rows)` 가 나오면 세션이 없거나 volatile 이다. 2번으로 돌아간다.
|
||
|
||
### 4. 상주 탐침 파드를 StatefulSet 밖에 띄운다
|
||
|
||
**목적** — 재시작을 넘어 토큰을 들고 있을 장치를 만든다.
|
||
|
||
```text
|
||
토큰을 어디에 두나
|
||
├─ Keycloak 파드 안 → 같이 죽는다. 못 쓴다
|
||
├─ 내 셸 변수 → 되지만 화면·히스토리에 남는다
|
||
└─ 단독 탐침 파드의 /tmp → StatefulSet 과 무관하게 산다 ★
|
||
```
|
||
|
||
```bash label="[lab host] ① 탐침을 띄우고 Ready 를 기다린다"
|
||
kubectl -n keycloak-lab run a8-probe --image=curlimages/curl:8.11.1 \
|
||
--restart=Never \
|
||
--env="K0=$K0" --env="K1=$K1" \
|
||
--env="PW=$(kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)" \
|
||
--command -- sleep 7200
|
||
kubectl -n keycloak-lab wait --for=condition=Ready pod/a8-probe --timeout=120s
|
||
```
|
||
|
||
비밀번호는 명령 치환으로 넘어가므로 터미널에도 셸 히스토리에도 값이 남지 않는다. 길이만 본다.
|
||
|
||
```bash label="[lab host] ② 비밀번호의 길이만 센다"
|
||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||
```
|
||
|
||
```text
|
||
19
|
||
```
|
||
|
||
```bash label="[lab host] ③ 탐침 안에 값이 들어갔는지 본다"
|
||
kubectl -n keycloak-lab exec a8-probe -- sh -c 'echo "K0=$K0 K1=$K1 PW길이=${#PW}"'
|
||
```
|
||
|
||
**예상 결과** — 두 IP 가 보이고 `PW길이` 가 0 이 아니다.
|
||
|
||
**왜 필요한가** — Keycloak 컨테이너에는 `curl` 도 `wget` 도 없어서 `kubectl exec keycloak-0 -- curl` 은 `exit 127` 로 끝난다.
|
||
|
||
**문제가 생기면** — `PW길이=0` 이면 `--env` 가 빈 값을 받았다. 파드를 지우고 ① 부터 다시 한다. `--rm` 이 없는 상주 파드라 지우지 않으면 같은 이름이 그대로 있어 ① 이 `AlreadyExists` 로 거절되고, 이 절차를 두 번째 칠 때도 같은 곳에서 걸린다.
|
||
|
||
```bash label="[lab host] ④ 탐침을 지우고 ① 로 돌아간다"
|
||
kubectl -n keycloak-lab delete pod a8-probe --ignore-not-found
|
||
```
|
||
|
||
### 5. 토큰과 sid 를 파드 안에 담고 길이를 확인한다
|
||
|
||
**목적** — 재시작을 넘겨 쓸 값을 파일에 남기고, 그 파일이 비어 있지 않은지 본다.
|
||
|
||
이 단계에 이 실험의 함정이 있다.
|
||
|
||
```bash label="[lab host] ① 로그인해서 refresh token 과 sid 를 파일로 남긴다"
|
||
kubectl -n keycloak-lab exec a8-probe -- sh -c \
|
||
'curl -s -X POST "http://$K0:8080/realms/master/protocol/openid-connect/token" \
|
||
-d grant_type=password -d client_id=admin-cli \
|
||
-d username=admin -d "password=$PW" > /tmp/tok
|
||
sed -n "s/.*\"refresh_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok > /tmp/rt
|
||
sed -n "s/.*\"access_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok \
|
||
| cut -d. -f2 | base64 -d 2>/dev/null \
|
||
| sed -n "s/.*\"sid\":\"\([^\"]*\)\".*/\1/p" > /tmp/sid
|
||
echo "rt $(wc -c < /tmp/rt) bytes / sid $(cat /tmp/sid)"'
|
||
```
|
||
|
||
**예상 결과**
|
||
|
||
```text
|
||
=== [1] 재시작 전 로그인 — 토큰을 파드 안에 보관 ===
|
||
sid = XLcgQWRiJrTkuNZcJsNeT_2j
|
||
```
|
||
|
||
두 값이 다 채워졌는지 본다.
|
||
|
||
| 출력 | 뜻 |
|
||
|---|---|
|
||
| `rt 1188 bytes / sid XLcg...` | 정상 |
|
||
| `rt 1 bytes` | 빈 문자열에 개행만. 파싱 실패 |
|
||
| `sid` 가 비어 있음 | base64 패딩 때문에 잘렸다. sid 없이 진행하고 판정은 개수로 본다 |
|
||
|
||
**왜 필요한가** — 원래 실행이 실제로 빠진 함정이 여기 있다. 첫 재현 절차는 `/tmp/tok` 에 쓰고 `/tmp/rt` 를 읽었는데 `/tmp/rt` 를 만드는 줄이 빠져 있었다. 그러면 빈 문자열이 `refresh_token=` 으로 전송되는데, 그래도 `400` 이 아니라 통과한 것처럼 보였고 아무 에러도 안 났다. 이 실험의 판정이 「재시작 후 refresh 가 `200` 인가」이므로, 빈 토큰을 보내고 받은 응답을 「세션이 살아 있다」로 읽으면 결론이 통째로 거짓이 된다. `wc -c` 한 번이 이 시험 전체를 지킨다.
|
||
|
||
못 미더우면 파일을 직접 본다.
|
||
|
||
```bash label="[lab host] ② 파일 크기와 앞 40바이트를 본다"
|
||
kubectl -n keycloak-lab exec a8-probe -- ls -l /tmp/tok /tmp/rt /tmp/sid
|
||
kubectl -n keycloak-lab exec a8-probe -- head -c 40 /tmp/rt ; echo
|
||
```
|
||
|
||
```text
|
||
-rw-r--r-- 1 curl_use curl_gro 1188 Sep 4 13:19 /tmp/rt
|
||
eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldU
|
||
```
|
||
|
||
`/tmp/rt` 의 크기가 네 자리이고 내용이 `eyJ` 로 시작한다. `eyJ` 는 base64 로 인코딩된 `{"` 이고 JWT 는 전부 이렇게 시작한다.
|
||
|
||
**문제가 생기면** — `rt 1 bytes` 면 `cat /tmp/tok` 으로 응답 본문을 본다.
|
||
|
||
### 6. 대조군 — 재시작 전에 refresh 가 되는 것을 본다
|
||
|
||
**목적** — 뒤의 `200` 이 무엇과 견준 값인지 확보한다.
|
||
|
||
```bash label="[lab host] ① 같은 노드에서 갱신해 본다"
|
||
kubectl -n keycloak-lab exec a8-probe -- sh -c \
|
||
'curl -s -o /dev/null -w "%{http_code}\n" -X POST \
|
||
"http://$K0:8080/realms/master/protocol/openid-connect/token" \
|
||
-d grant_type=refresh_token -d client_id=admin-cli \
|
||
-d "refresh_token=$(cat /tmp/rt)"'
|
||
```
|
||
|
||
**예상 결과**
|
||
|
||
```text
|
||
200
|
||
```
|
||
|
||
이 refresh 로 토큰이 회전했다. `/tmp/rt` 의 값은 이제 이미 쓴 토큰이라 다시 채워야 하고, 안 채우면 뒤의 `400` 이 재시작 때문인지 재사용 때문인지 구별되지 않는다. 5번의 ① 과 같은 명령을 그대로 다시 친다.
|
||
|
||
```bash label="[lab host] ② 다시 로그인해서 두 파일을 새로 만든다"
|
||
kubectl -n keycloak-lab exec a8-probe -- sh -c \
|
||
'curl -s -X POST "http://$K0:8080/realms/master/protocol/openid-connect/token" \
|
||
-d grant_type=password -d client_id=admin-cli \
|
||
-d username=admin -d "password=$PW" > /tmp/tok
|
||
sed -n "s/.*\"refresh_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok > /tmp/rt
|
||
sed -n "s/.*\"access_token\":\"\([^\"]*\)\".*/\1/p" /tmp/tok \
|
||
| cut -d. -f2 | base64 -d 2>/dev/null \
|
||
| sed -n "s/.*\"sid\":\"\([^\"]*\)\".*/\1/p" > /tmp/sid
|
||
echo "rt $(wc -c < /tmp/rt) bytes / sid $(cat /tmp/sid)"'
|
||
```
|
||
|
||
**왜 필요한가** — ② 가 찍은 `sid` 가 최종 추적 대상이다. 7번부터 끝까지 이 값을 쓰므로 적어 둔다. ① 의 `200` 은 대조군이고, 그 대조군을 잡느라 소비한 토큰을 ② 가 메운다.
|
||
|
||
**문제가 생기면** — ① 에서 `400` 이 나오면 5번의 파일 확인으로 돌아간다. ② 의 출력이 `rt 1 bytes` 면 5번의 ② 로 파일을 직접 본다.
|
||
|
||
### 7. 그 세션이 지금 DB 에 있는지 sid 로 본다
|
||
|
||
**목적** — 재시작 전의 행 상태를 기록한다.
|
||
|
||
원 가이드의 질의는 `sid` 를 셸 치환으로 집어넣어 `psql -c` 문자열 안에 `kubectl exec` 이 한 번 더 들어간다. 따라 하는 사람은 방금 적어 둔 `sid` 를 그대로 친다 — 앞 명령이 이미 그 값을 화면에 보여 줬고, 명령 하나가 한 가지 일만 한다. 이 두 단계 형태는 이 실험대에서 치지 않았다(unknown).
|
||
|
||
```bash label="[lab host] ① sid 를 화면에서 읽는다"
|
||
kubectl -n keycloak-lab exec a8-probe -- cat /tmp/sid
|
||
```
|
||
|
||
```bash label="[lab host] ② 읽은 값을 그대로 넣어 행을 찾는다"
|
||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "select user_session_id, created_on, last_session_refresh from offline_user_session
|
||
where offline_flag='0' and user_session_id='{{SID}}'"
|
||
```
|
||
|
||
`sid` 는 로그인할 때마다 새로 생긴다. 이 실험대의 값은 `XLcgQWRiJrTkuNZcJsNeT_2j` 였다(observed).
|
||
|
||
**예상 결과** — 모양은 이렇고 값은 환경마다 다르다.
|
||
|
||
```text
|
||
user_session_id | created_on | last_session_refresh
|
||
--------------------------+------------+----------------------
|
||
XLcgQWRiJrTkuNZcJsNeT_2j | 1788495513 | 1788495513
|
||
(1 row)
|
||
```
|
||
|
||
행이 1개 있고 `created_on` 과 `last_session_refresh` 가 같다. 아직 갱신한 적이 없다.
|
||
|
||
**왜 필요한가** — 재시작 후에 이 행이 그대로 있고 `last_session_refresh` 만 올라가는 것이 뒤의 판정이다.
|
||
|
||
**문제가 생기면** — `(0 rows)` 가 나오면 `sid` 를 잘못 옮겼거나 그 세션이 이미 사라졌다. 5번부터 다시 한다.
|
||
|
||
### 8. 캐시와 클러스터 크기를 미리 본다
|
||
|
||
**목적** — 재시작 후 0 이 되는 값을 먼저 확보한다.
|
||
|
||
```bash label="[lab host] ① 한 줄짜리 JSON 을 통째로 본다"
|
||
kubectl -n observability exec deploy/prometheus -- \
|
||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
|
||
```
|
||
|
||
모양은 이렇고 값은 환경마다 다르다.
|
||
|
||
```json
|
||
{"status":"success","data":{"resultType":"vector","result":[
|
||
{"metric":{"__name__":"vendor_cluster_size","cache_manager":"keycloak","job":"keycloak","node":"kc-lab-1","pod":"keycloak-1"},"value":[1757040000.1,"2"]},
|
||
{"metric":{"__name__":"vendor_cluster_size","cache_manager":"keycloak","job":"keycloak","node":"kc-lab-2","pod":"keycloak-0"},"value":[1757040000.1,"2"]}]}}
|
||
```
|
||
|
||
라벨을 보고 나면 읽기 좋게 자른다. 아래 형태는 가이드가 미검증으로 표시한 줄이다(unknown).
|
||
|
||
```bash label="[lab host] ② 파드와 값만 세로로 늘어놓는다"
|
||
kubectl -n observability exec deploy/prometheus -- \
|
||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
|
||
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
|
||
```
|
||
|
||
**예상 결과** — 결과가 두 줄이고 값이 둘 다 `2` 다. 세션 캐시 엔트리 수도 같은 형태로 보면 0 이 아닌 값이 나온다.
|
||
|
||
**왜 필요한가** — 재시작 후 캐시가 0 이 되고 클러스터 크기가 다시 `2` 로 돌아오는 것이 뒤의 판정이다.
|
||
|
||
**문제가 생기면** — 빈 결과가 오면 0 이 아니라 그런 지표가 없다. Prometheus 의 스크레이프 대상 목록으로 돌아간다.
|
||
|
||
## 주입
|
||
|
||
### 9. 가용성 감시를 먼저 띄우고 재시작한다
|
||
|
||
**목적** — 재시작 중 외부 응답을 5초 간격으로 기록하면서 파드를 교체하고, 그 사이에 엔드포인트가 어떻게 움직이는지 본다.
|
||
|
||
두 번째 터미널에서 루프를 돌린다. 재시작보다 먼저 시작해야 끊김 구간을 놓치지 않는다.
|
||
|
||
```bash label="[kc-lab-1 · 두 번째 터미널] ① 5초 간격으로 48번 외부를 친다"
|
||
for i in $(seq 1 48); do
|
||
printf '%s ' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 4 \
|
||
https://auth.hyeonworks.com/realms/master)"
|
||
sleep 5
|
||
done
|
||
echo
|
||
```
|
||
|
||
숫자가 5초마다 하나씩 붙는다. `200` 이 아닌 값이 보이면 거기가 끊김이다. 여기서 `-w '%{http_code}'` 를 쓰는 까닭은 48번 반복해서 견줄 값만 필요하기 때문이다. 무엇이 잘못됐는지 알아보려면 그때 `curl -v` 로 한 번 보면 된다. `--max-time 4` 는 5초 간격보다 짧게 잡은 것인데, 타임아웃이 간격보다 길면 요청이 밀려 시계열이 어긋난다.
|
||
|
||
첫 번째 터미널에서 재시작한다. `rollout restart` 는 바로 돌아오고, 파드 교체는 그 뒤에 백그라운드로 진행된다.
|
||
|
||
```bash label="[lab host] ② 시각을 남기고 롤링 재시작을 건다"
|
||
date '+%H:%M:%S 재시작'
|
||
kubectl -n keycloak-lab rollout restart statefulset/keycloak
|
||
```
|
||
|
||
**엔드포인트는 여기서 봐야 보인다.** 파드가 서비스에서 빠졌다 돌아오는 것은 롤아웃이 도는 동안에만 나타나고, 끝난 뒤에 치면 ready 주소가 늘 둘로 나온다. 두 번째 터미널은 ① 의 루프에 붙잡혀 있으므로 이 터미널에서 몇 번 반복해서 친다. 찍힌 것을 어떻게 읽는지는 15번에서 적는다.
|
||
|
||
```bash label="[lab host] ③ 롤아웃이 도는 동안 엔드포인트를 몇 번 본다"
|
||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
||
-o "custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||
```
|
||
|
||
그다음 롤아웃이 끝날 때까지 기다린다. 이 명령은 끝날 때까지 터미널을 붙잡는다.
|
||
|
||
```bash label="[lab host] ④ 롤아웃이 끝날 때까지 기다린다"
|
||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=420s
|
||
```
|
||
|
||
**예상 결과** — 아래는 두 터미널의 출력이 한 파일에 섞여 기록된 것이다. `200` 이 가용성 루프, `Waiting for...` 가 `rollout status` 다. 이 실험대는 ②④ 를 한 블록으로 연달아 쳤고 아래는 그때의 출력이다. 사이에 ③ 을 끼우면 `rollout status` 가 그만큼 늦게 시작하므로 `Waiting for` 줄 수가 이와 다를 수 있다.
|
||
|
||
```text
|
||
statefulset.apps/keycloak restarted
|
||
200 Waiting for partitioned roll out to finish: 0 out of 2 new pods have been updated...
|
||
Waiting for 1 pods to be ready...
|
||
Waiting for 1 pods to be ready...
|
||
Waiting for 1 pods to be ready...
|
||
200 200 200 200 Waiting for partitioned roll out to finish: 1 out of 2 new pods have been updated...
|
||
Waiting for 1 pods to be ready...
|
||
Waiting for 1 pods to be ready...
|
||
Waiting for 1 pods to be ready...
|
||
200 200 200 200 partitioned roll out complete: 2 new pods have been updated...
|
||
```
|
||
|
||
`0 out of 2` 에서 `1 out of 2` 를 거쳐 `complete` 로 한 번에 하나씩 가고 그 사이사이에 `200` 이 계속 찍힌다.
|
||
|
||
**왜 필요한가** — ① 을 ② 보다 늦게 띄우면 첫 파드가 내려가는 구간을 통째로 놓친다. ③ 도 마찬가지로 ④ 뒤로 밀면 놓친다. 시각도 반드시 적어 둔다.
|
||
|
||
**문제가 생기면** — ④ 가 타임아웃이면 파드가 Ready 를 못 받고 있다. `describe pod` 의 Events 와 `logs --previous` 를 본다. ④ 를 `Ctrl-C` 로 끊어도 롤아웃 자체는 계속 진행된다.
|
||
|
||
## 주입 검증
|
||
|
||
### 10. 파드가 진짜 바뀌었는지 AGE 로 본다
|
||
|
||
**목적** — 「세션이 살아남았다」가 의미를 갖는 조건을 확인한다.
|
||
|
||
```bash label="[lab host] 파드 나이와 재시작 카운터를 본다"
|
||
kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||
```
|
||
|
||
**예상 결과**
|
||
|
||
```text
|
||
=== [6] 파드 나이 — 정말 재시작되었나 ===
|
||
keycloak-0 1/1 Running 0 44s
|
||
keycloak-1 1/1 Running 0 66s
|
||
```
|
||
|
||
세 가지를 본다. `AGE` 가 초 단위인 것(앞에서 `2d` 였던 것이 `44s` 다), 두 나이가 다른 것(`44s` 와 `66s` 의 22초 차이가 롤링의 간격이고, 둘이 같으면 동시에 내려간 것이라 무중단이 아니다), 그리고 `RESTARTS` 가 여전히 `0` 인 것.
|
||
|
||
**왜 필요한가** — `rollout restart` 는 파드를 지우고 새로 만들므로 재시작 카운터가 새 파드에서 0 부터 시작한다. 판정에 `RESTARTS` 를 쓰면 안 된다는 것이 여기서 드러난다.
|
||
|
||
**문제가 생기면** — `AGE` 가 예전 값이면 롤아웃이 안 끝났다. 9번의 ④ 로 돌아간다.
|
||
|
||
파드 IP 가 바뀌었으므로 다시 잡는다. 탐침 파드는 다시 띄우지 않는다 — `/tmp/rt` 와 `/tmp/sid` 가 같이 사라진다. 탐침 안의 `K0` 환경변수는 낡았으므로 새 IP 를 명령줄로 넘긴다.
|
||
|
||
```bash label="[lab host] 새 파드 IP 를 다시 잡는다"
|
||
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||
echo "$K0 $K1"
|
||
```
|
||
|
||
### 11. 가용성 시계열을 읽고 표본 수를 센다
|
||
|
||
**목적** — 끊김이 관측됐는지 보고, 그 관측이 무엇까지 말할 수 있는지 정한다.
|
||
|
||
두 번째 터미널의 출력을 읽는다.
|
||
|
||
```text
|
||
200 200 200 200 200 200 200 200 200
|
||
```
|
||
|
||
**예상 결과** — `200` 이 9개이고 비200 이 없다.
|
||
|
||
루프는 48회로 잡았는데 남은 표본은 9개다. 원 기록이 그 차이를 설명하지 않는다(unknown) — 루프를 중간에 끊었는지, 기록에 앞부분만 옮겼는지 알 수 없다. 표본 수를 셀 때는 루프 횟수가 아니라 화면에 실제로 찍힌 개수를 센다.
|
||
|
||
**왜 필요한가** — 「무중단」이라고 쓰기 전에 표본 수를 본다.
|
||
|
||
```text
|
||
9개 표본 × 5초 간격 = 약 45초를 9번 들여다본 것
|
||
│
|
||
└─ 5초보다 짧은 끊김은 이 측정으로 잡히지 않는다
|
||
```
|
||
|
||
실제로 더 촘촘히 재니 끊김이 나왔다. 후속 작업에서 1초 간격과 3초 타임아웃으로 다른 전환을 재 본 값이 이렇다.
|
||
|
||
```text
|
||
200 ×24 000 200 ×19
|
||
```
|
||
|
||
`000` 은 서버 오류가 아니라 `--max-time 3` 타임아웃이다. 파드 전환 순간 요청 하나가 3초를 넘겼다.
|
||
|
||
| 쓰면 안 되는 문장 | 정확한 문장 |
|
||
|---|---|
|
||
| 「무중단이었다」 | 「5초 해상도에서 끊김이 관측되지 않았다」 |
|
||
|
||
더 촘촘히 보고 싶으면 루프를 이렇게 바꾼다. 가이드가 미검증으로 표시한 형태다(unknown).
|
||
|
||
```bash label="[kc-lab-1 · 두 번째 터미널] 1초 간격 150회로 더 촘촘히 잰다"
|
||
for i in $(seq 1 150); do
|
||
printf '%s ' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 \
|
||
https://auth.hyeonworks.com/realms/master)"
|
||
sleep 1
|
||
done
|
||
echo
|
||
```
|
||
|
||
**문제가 생기면** — 루프가 전부 `000` 이면 잘못된 URL 을 치고 있다. `curl -v` 로 한 번 본다.
|
||
|
||
## 관찰
|
||
|
||
### 12. 본 시험 — 재시작 전 토큰이 아직 통하는가
|
||
|
||
**목적** — 파드 안에 보관해 둔 토큰을 새 파드 IP 로 보낸다.
|
||
|
||
셸 인용이 세 겹이 되는 형태이고, 가이드는 여기에 다른 형태를 제시하지 않는다. 탐침을 다시 띄우면 토큰이 사라지기 때문이다.
|
||
|
||
```bash label="[lab host] 재시작 전 토큰으로 갱신을 시도한다"
|
||
kubectl -n keycloak-lab exec a8-probe -- sh -c \
|
||
'curl -s -w "\n%{http_code}\n" -X POST \
|
||
"http://'"$K0"':8080/realms/master/protocol/openid-connect/token" \
|
||
-d grant_type=refresh_token -d client_id=admin-cli \
|
||
-d "refresh_token=$(cat /tmp/rt)"'
|
||
```
|
||
|
||
**예상 결과**
|
||
|
||
```text
|
||
=== [3] 재시작 전 발급한 refresh token 이 아직 통하는가 ===
|
||
대상 sid: XLcgQWRiJrTkuNZcJsNeT_2j
|
||
keycloak-0 에서 refresh HTTP 200
|
||
```
|
||
|
||
`200` 이고 본문에 새 토큰이 들어 있다. 파드가 통째로 바뀌었는데 세션이 살아 있다. 새로 뜬 프로세스는 이 세션을 메모리에서 알던 것이 아니라 DB 에서 읽었다.
|
||
|
||
**왜 필요한가** — `400` 이 나왔다면 먼저 의심할 것은 결론이 아니라 토큰이다. 대조군 시험 뒤에 `/tmp/rt` 를 다시 안 채웠거나, `rt 1 bytes` 를 놓쳤거나, args 에 `--features-disabled=persistent-user-sessions` 가 있거나 셋 중 하나다. 셋 다 아니면 그때 결론을 의심한다.
|
||
|
||
**문제가 생기면** — 아무 데도 안 닿으면 파드 IP 가 바뀐 것을 명령에 반영하지 않았다. 10번의 IP 잡기를 다시 한다.
|
||
|
||
**2026-09-17 실측**(observed) — 롤링 재시작 전후로 같은 토큰을 썼다.
|
||
|
||
```text
|
||
재시작 전 대조군 200
|
||
재시작 (파드 둘 교체) AGE 31s · 63s · IP 10.42.1.47→.49, 10.42.0.20→.21
|
||
재시작 뒤 같은 refresh 200 · session_state 가 같은 sid
|
||
DB 행 created_on 1789625835 그대로 · last_session_refresh 1789625835 → 1789625952
|
||
캐시 재시작 전후 모두 keycloak-0/1 sessions 0
|
||
클러스터 다시 2
|
||
```
|
||
|
||
**`created_on` 은 그대로이고 `last_session_refresh` 만 117초 뒤로 갔다.** 새로 만든 세션이 아니라 **남아 있던 행을 새 파드가 읽어서 갱신한 것**이고, 그것이 이 편의 결론이다.
|
||
|
||
**탐침 안의 `$K0` 는 재시작 뒤에 낡는다.** 2026-09-17 에 낡은 값으로 쳐서 `000` 을 받았다(observed) — 문서가 미리 적어 둔 그 함정이다. 새 IP 를 명령줄로 넘겨 다시 쳤다.
|
||
|
||
```text
|
||
새 K0=10.42.1.49 (탐침 안의 낡은 값=10.42.1.47)
|
||
```
|
||
|
||
### 13. DB 행의 두 시각을 견준다
|
||
|
||
**목적** — 응답 코드만이 아니라 쓰기까지 정상인지 본다.
|
||
|
||
적어 둔 `sid` 를 넣어 7번의 ② 와 같은 질의를 다시 친다.
|
||
|
||
```bash label="[lab host] ① 같은 행을 다시 찾는다"
|
||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-c "select user_session_id, created_on, last_session_refresh from offline_user_session
|
||
where offline_flag='0' and user_session_id='{{SID}}'"
|
||
```
|
||
|
||
**예상 결과**
|
||
|
||
```text
|
||
=== [4] DB 에 그 세션이 남아 있는가 ===
|
||
user_session_id | created_on | last_session_refresh
|
||
--------------------------+------------+----------------------
|
||
XLcgQWRiJrTkuNZcJsNeT_2j | 1788495513 | 1788495577
|
||
(1 row)
|
||
```
|
||
|
||
두 숫자의 차이를 본다.
|
||
|
||
```text
|
||
1788495577 - 1788495513 = 64초
|
||
│ │
|
||
│ └─ 재시작 전에 세션이 만들어진 시각
|
||
└─ 재시작 후의 refresh 가 기록된 시각
|
||
```
|
||
|
||
두 값은 유닉스 시각(초)이라 사람이 읽는 형태로 보려면 이렇게 친다.
|
||
|
||
```bash label="[lab host] ② 두 유닉스 시각을 사람이 읽는 형태로 바꾼다"
|
||
date -d @1788495513 ; date -d @1788495577
|
||
```
|
||
|
||
**왜 필요한가** — `200` 만 봤다면 「캐시에 뭔가 남아서 답한 것 아닌가」를 배제할 수 없다. A-1 에서 실제로 그런 일이 있었다. 여기서는 새 파드가 DB 에서 세션을 읽었고 갱신 시각을 DB 에 되썼으므로 그 가능성이 없다.
|
||
|
||
**문제가 생기면** — `last_session_refresh` 가 안 올랐으면 본 시험을 하기 전에 조회했다. 순서는 refresh 를 먼저 하고 조회한다.
|
||
|
||
전체 세션 수도 함께 본다.
|
||
|
||
```bash label="[lab host] ③ 온라인 세션 전체를 센다"
|
||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||
-tAc "select count(*) from offline_user_session where offline_flag='0'"
|
||
```
|
||
|
||
```text
|
||
전체 온라인 세션: 151 (재시작 전 151)
|
||
```
|
||
|
||
3번에서 적어 둔 값과 같다. 한 건도 안 잃었다. `sid` 하나가 살아남은 것과 전체가 살아남은 것은 다른 주장이라 둘 다 본다. 관리 API 호출이 세션을 만들기 때문에 몇 건 늘어날 수는 있고, 크게 줄었다면 그게 문제가 된다.
|
||
|
||
### 14. 캐시가 비워지고 클러스터가 다시 붙는 것을 본다
|
||
|
||
**목적** — 재시작이 무엇을 지우고 무엇을 남겼는지 가른다.
|
||
|
||
```bash label="[lab host] 캐시 엔트리 수와 클러스터 크기를 이어서 본다"
|
||
kubectl -n observability exec deploy/prometheus -- \
|
||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_statistics_approximate_entries_unique' \
|
||
| tr ',' '\n' | grep -E '"cache":|"pod":|^"[0-9]'
|
||
kubectl -n observability exec deploy/prometheus -- \
|
||
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
|
||
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
|
||
```
|
||
|
||
**예상 결과**
|
||
|
||
```text
|
||
=== [5] 캐시는 어떻게 되었는가 ===
|
||
keycloak-0 sessions 캐시 0.0 건 / cluster_size 2.0
|
||
keycloak-1 sessions 캐시 1.0 건 / cluster_size 2.0
|
||
```
|
||
|
||
세 가지를 본다. 캐시가 0 인 것(프로세스 메모리라 재시작에 사라졌다), `keycloak-1` 의 1건(방금 refresh 를 처리하며 새로 담은 값이므로 0 이 아니라고 「캐시가 살아남았다」로 읽지 않는다), 그리고 `cluster_size` 가 다시 `2` 인 것.
|
||
|
||
**왜 필요한가** — A-0 의 모델이 여기서 그대로 확인된다.
|
||
|
||
```text
|
||
재시작 전: 캐시 N건 + DB 151건
|
||
재시작 후: 캐시 0건 + DB 151건 ← 진실은 DB 에 있다
|
||
```
|
||
|
||
캐시가 통째로 날아가도 정확성은 유지되고 첫 접근만 느려진다. 룩어사이드 캐시의 성질이다.
|
||
|
||
**문제가 생기면** — `cluster_size` 가 `1` 에서 안 올라오면 클러스터가 다시 안 붙었다. 파드 로그에서 멤버 수를 본다.
|
||
|
||
### 15. 무중단이 되는 까닭을 엔드포인트에서 본다
|
||
|
||
**목적** — 파드가 서비스에서 언제 빠지고 언제 돌아오는지 본다.
|
||
|
||
```text
|
||
StatefulSet 롤링 재시작
|
||
│
|
||
├─ keycloak-1 종료 → Service 엔드포인트에서 빠짐
|
||
│ └─ 이 동안 keycloak-0 이 전부 받는다
|
||
├─ keycloak-1 기동 → readiness UP → 엔드포인트 복귀
|
||
│
|
||
└─ keycloak-0 종료 → ... (반복)
|
||
```
|
||
|
||
실제로 그렇게 움직이는지는 재시작 중에 쳐야 보인다. 그 명령이 9번의 ③ 이므로 여기서 읽는 것은 그때 화면에 찍힌 값이다. 지금 다시 쳐도 롤아웃이 이미 끝났으므로 ready 주소는 둘로만 나온다.
|
||
|
||
**예상 결과** — 재시작 중에는 ready 주소가 하나로 줄었다가 둘로 돌아온다.
|
||
|
||
**왜 필요한가** — 한 번에 하나씩 내리므로 항상 최소 하나는 Ready 이고, readiness 프로브가 이 전환을 맞춰 준다. A-2 에서 장애를 격리하는 장치로 본 그 메커니즘이 여기서는 정상 작업을 안전하게 만든다.
|
||
|
||
| 무중단의 조건 | 빠지면 |
|
||
|---|---|
|
||
| replica ≥ 2 | 하나뿐이면 내리는 동안 아무도 안 받는다 |
|
||
| readiness 프로브 | 아직 기동 중인 파드로 트래픽이 간다 |
|
||
|
||
둘 다 있어야 성립하고, 이 실험대는 파드가 2개라서 됐다.
|
||
|
||
**문제가 생기면** — `kubectl get endpoints` 는 쓰지 않는다. v1.33 부터 deprecated 라 경고가 뜨므로 `endpointslice` 를 본다.
|
||
|
||
## 복구와 원상복구 확인표
|
||
|
||
주입이 정상 작업이었으므로 되돌릴 것이 없다. 정리만 한다.
|
||
|
||
```bash label="[lab host] 탐침 파드를 지운다"
|
||
kubectl -n keycloak-lab delete pod a8-probe --ignore-not-found
|
||
```
|
||
|
||
남겨 두면 7200초 뒤에 스스로 끝나지만, 그 안에 다른 실험을 하면 네임스페이스에 정체 모를 파드가 하나 있는 상태가 된다. 지운다.
|
||
|
||
| 항목 | 명령 | 돌아왔을 때 |
|
||
|---|---|---|
|
||
| 파드 | `kubectl -n keycloak-lab get pods -o wide` | `keycloak` 둘 다 `1/1 Running` |
|
||
| Service | `kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak` | ready 주소 둘 |
|
||
| 클러스터 뷰 | `kubectl -n keycloak-lab logs keycloak-0 \| grep ISPN000094 \| tail -1` | 멤버 `(2)` |
|
||
| 지표 | `vendor_cluster_size` | 양쪽 `2` |
|
||
| 세션 | `psql -tAc "select count(*) from offline_user_session where offline_flag='0'"` | 재시작 전과 비슷한 값 |
|
||
| 탐침 파드 | `kubectl -n keycloak-lab get pod a8-probe` | `NotFound` |
|
||
| 밖 | `curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master` | `200` |
|
||
|
||
## 막히면
|
||
|
||
아래는 이 실험대가 실제로 겪은 증상이다. 마지막 줄만 A-2·A-3 에서 겪은 것을 옮겼다 — 탐침 파드를 같은 방식으로 띄우므로 여기서도 그대로 걸린다.
|
||
|
||
| 증상 | 원인 | 확인 |
|
||
|---|---|---|
|
||
| refresh 가 `200` 인데 뭔가 이상하다 | `/tmp/rt` 가 비어 있다. 빈 토큰인데 통과한 것처럼 보인다 | `wc -c < /tmp/rt` |
|
||
| refresh 가 `400 Session not active` | 대조군 시험 뒤에 `/tmp/rt` 를 안 채웠다. 이미 쓴 토큰이다 | 새로 로그인해서 다시 담는다 |
|
||
| refresh 가 `400` 인데 토큰은 맞다 | args 가 volatile 이다 | `get statefulset ... args`. 그건 A-7 |
|
||
| 재시작 후 아무 데도 안 닿는다 | 파드 IP 가 바뀌었다 | `get pod -o jsonpath='{.status.podIP}'` 다시 |
|
||
| 탐침을 다시 띄웠더니 토큰이 없다 | `/tmp/rt` 가 파드와 함께 사라졌다 | 탐침은 재시작 내내 유지한다 |
|
||
| `RESTARTS` 가 0 이라 재시작이 안 된 것 같다 | `rollout restart` 는 파드를 교체한다 | `AGE` 로 본다 |
|
||
| `rollout status` 가 타임아웃 | 파드가 Ready 를 못 받는다 | `describe pod` 의 Events, `logs --previous` |
|
||
| 가용성 루프에 `000` 이 섞인다 | `--max-time` 초과. 서버 오류가 아니다 | 간격보다 짧은 타임아웃인지 |
|
||
| 가용성 루프가 전부 `000` | 루프가 잘못된 URL 을 친다 | `curl -v` 로 한 번 본다 |
|
||
| 세션 수가 크게 줄었다 | 다른 실험이 세션을 지웠거나 volatile 이다 | args 와 DB 세션 수를 다시 |
|
||
| DB 행의 `last_session_refresh` 가 안 올랐다 | 본 시험을 하기 전에 조회했다 | 순서: refresh → 조회 |
|
||
| `kubectl get endpoints` 가 경고를 찍는다 | v1.33 부터 deprecated | `get endpointslice -l kubernetes.io/service-name=...` |
|
||
| `kubectl exec keycloak-0 -- curl` 이 `exit 127` | Keycloak 이미지에 curl 도 wget 도 없다 | 탐침 파드를 쓴다 |
|
||
| `a8-probe` 를 다시 못 만든다 | 앞선 실행의 파드가 그 이름으로 남아 있다 | `delete pod a8-probe --ignore-not-found` |
|
||
|
||
## 무엇이 관측이고 무엇이 아닌가
|
||
|
||
이 실험대가 실제로 본 것(observed)은 재시작 전 DB 세션 `151` 과 `sid = XLcgQWRiJrTkuNZcJsNeT_2j`, 비밀번호 길이 `19`, `rollout status` 와 가용성 루프가 섞인 출력 전문, 재시작 뒤 파드 나이 `44s` 와 `66s` 및 `RESTARTS 0`, 가용성 시계열의 `200` 아홉 개, 재시작 전 토큰의 `HTTP 200`, DB 행의 `1788495513` 에서 `1788495577` 로의 변화, 전체 세션 `151 (재시작 전 151)`, 캐시 `0.0` 과 `1.0` 및 `cluster_size 2.0`, 후속 작업의 `200 ×24 000 200 ×19` 다.
|
||
|
||
가이드가 미검증으로 표시한 것(unknown)은 `tr ',' '\n' | grep -E` 로 자른 Prometheus 출력과 1초 간격·3초 타임아웃 루프다. `sid` 를 화면에서 읽어 질의에 직접 넣는 두 단계 형태도 이 실험대에서 치지 않았다.
|
||
|
||
원래 실행이 실제로 빠졌던 곳이 하나 있다. 첫 재현 절차에 `/tmp/rt` 를 만드는 줄이 없어서 빈 문자열이 `refresh_token=` 으로 전송됐는데, `400` 이 아니라 통과한 것처럼 보였고 아무 에러도 안 났다.
|
||
|
||
해상도에 걸린 주장이 하나 있다. 「무중단」이 아니라 「5초 해상도에서 끊김이 관측되지 않았다」이고 표본은 9개다. 1초 간격으로 잰 후속 작업은 다른 조건에서 `000` 을 하나 잡았다. 루프를 48회로 돌렸는데 표본이 9개인 까닭은 원 기록에 없어서 여기서도 못 적는다(unknown).
|
||
|
||
이 절차가 재지 않은 것이 셋이다. replica 1 에서 어떻게 되는지(반드시 끊긴다고 적었지만 재지 않았다), 5초보다 짧은 끊김, 그리고 캐시가 0 에서 다시 차는 데 걸리는 시간이다. 「첫 접근만 느려진다」고 썼지만 그 느림을 재지 않았고, A-6 이 인접한 주제다.
|
||
|
||
<!-- body:end -->
|