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,926 @@
|
||||
# A-6 재현 가이드 — 끊지 않고 200ms 만 넣어 22초를 만든다
|
||||
|
||||
해설 문서: [`docs/experiment-a6-latency-injection.md`](../../experiment-a6-latency-injection.md) ·
|
||||
증거 원문: [`docs/evidence/a6-latency-injection/`](../../evidence/a6-latency-injection/)
|
||||
|
||||
## 이 가이드가 끝나면
|
||||
|
||||
당신 터미널에서 이것들을 **직접 본다.**
|
||||
|
||||
| 보게 되는 것 | 어디서 |
|
||||
|---|---|
|
||||
| `eth0` 이라는 인터페이스가 **없다**는 것 | `ip -brief link` |
|
||||
| 스크립트가 **「적용완료」를 찍었는데 아무것도 안 걸린 것** | `tc -s qdisc` 카운터 |
|
||||
| `enp1s0` 에서는 **파드 IP 가 안 보이는** 것 | VXLAN 캡슐화 |
|
||||
| 200ms 가 **1,872ms** 가 되는 것 | 두 노드 응답 시간 비교 |
|
||||
| 동시 20건이 **22.2초**까지 계단으로 늘어나는 것 | 상주 탐침이 모은 파일 |
|
||||
| 커넥션 획득에 **20초**를 기다린 요청 | `agroal_blocking_time_max_milliseconds` |
|
||||
| **readiness 프로브가 같은 줄에 서서** 타임아웃되는 것 | `kubectl get events` |
|
||||
| 예측했던 낙관적 락 충돌이 **0건**인 것 | Keycloak 로그 |
|
||||
|
||||
## 전제
|
||||
|
||||
- [`05-keycloak`](../05-keycloak/) · [`06-observability`](../06-observability/) 가 끝나 있다.
|
||||
- [`A-5`](a5-asymmetric-partition.md) 를 먼저 해 두면 좋다. **「주입을 넣은 것과
|
||||
걸린 것은 다르다」가 여기서 세 번째로 나온다.**
|
||||
- `kubectl` 은 **`kc-lab-1` 에서 `sudo`** 로 친다.
|
||||
- `tc` 는 **`kc-lab-2` 에서** 친다(`ssh kc-lab-2`). postgres 가 그 노드에 있다.
|
||||
- 터미널 두 개면 편하다. 하나는 부하·측정, 하나는 이벤트 관찰.
|
||||
|
||||
## 주의 — 이건 상태를 부수는 실험이다
|
||||
|
||||
Keycloak 한 대를 **느려지게** 만든다. 파드가 재시작될 수 있고 readiness 가
|
||||
빠진다. **실험대에서만 한다.** 전 구간 약 30분이다.
|
||||
|
||||
중간에 그만두려면 한 줄이면 된다.
|
||||
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo tc qdisc del dev flannel.1 root'
|
||||
```
|
||||
|
||||
## 표시 규약
|
||||
|
||||
| 표시 | 뜻 |
|
||||
|---|---|
|
||||
| **실측** | 2026-09-04 13:10–13:35 KST 실행 기록의 **출력 원문**. 증거 파일에 그대로 있다 |
|
||||
| **형태** | 값이 매번 달라지는 출력. 모양만 보이고 숫자는 당신 것과 다르다 |
|
||||
| **미검증** | 손으로 치기 좋게 이 가이드에서 고친 형태. 원래 실행은 스크립트로 했다 |
|
||||
|
||||
IP·파드 이름·인터페이스 이름은 **당신 환경에서 다를 수 있다.** 자리표시자
|
||||
(`<...>`)를 쓰지 않는 대신, 그 값을 뽑는 명령을 먼저 적는다.
|
||||
|
||||
---
|
||||
|
||||
# 0. 왜 이 실험을 하는가
|
||||
|
||||
A-2 는 DB 를 **완전히** 세웠고, A-4 는 기계를 **통째로** 껐다. 둘 다 즉시
|
||||
드러났다. `503` 이 나오고 `up` 이 0 이 됐다.
|
||||
|
||||
**실제 장애의 대부분은 그렇지 않다. 느려지기만 한다.** 그리고 느려짐은
|
||||
사망보다 **진단하기 어렵다 — 헬스체크가 통과하기 때문이다.**
|
||||
|
||||
이 실험이 묻는 것은 하나다.
|
||||
|
||||
```
|
||||
200밀리초를 넣으면 애플리케이션은 200밀리초 느려지는가?
|
||||
```
|
||||
|
||||
답은 **아니다.** 두 군데에서 곱해진다.
|
||||
|
||||
---
|
||||
|
||||
# 1. 설계 — 왜 이 배치가 그대로 A/B 실험이 되는가
|
||||
|
||||
## 1-1. 무엇이 어느 노드에 있나
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
**실측** — [`01-baseline.txt`](../../evidence/a6-latency-injection/01-baseline.txt)
|
||||
```
|
||||
postgres 10.42.1.76 (kc-lab-2)
|
||||
keycloak-0 10.42.1.77 (kc-lab-2) → DB 와 같은 노드, cni0 로 직행
|
||||
keycloak-1 10.42.0.42 (kc-lab-1) → DB 와 다른 노드, VXLAN 을 건넌다 ← 여기에 지연을 건다
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **postgres 와 `keycloak-0` 이 같은 노드**인가.
|
||||
|
||||
**이 결과가 의미하는 것**
|
||||
|
||||
```
|
||||
kc-lab-2 kc-lab-1
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ postgres │ │ keycloak-1 │
|
||||
│ keycloak-0 │ │ │
|
||||
│ └─ cni0 로 직행 │◀─ VXLAN ──▶│ └─ 오버레이 경유 │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
지연 없음 여기만 느려진다
|
||||
```
|
||||
|
||||
**postgres 가 보내는 패킷 중 노드를 건너가는 것만** 지연시키면
|
||||
`keycloak-1` 의 DB 접근만 느려지고 `keycloak-0` 은 그대로다.
|
||||
**대조군이 같은 실험 안에 있다.** 파드를 두 개 더 띄울 필요도, 다른 시간대와
|
||||
비교할 필요도 없다.
|
||||
|
||||
> **배치가 다르면 이 실험은 성립하지 않는다.** 두 Keycloak 이 모두 DB 와 다른
|
||||
> 노드에 있으면 대조군이 없고, 모두 같은 노드에 있으면 시험군이 없다.
|
||||
> 먼저 확인한다.
|
||||
|
||||
변수로 잡아 둔다.
|
||||
```bash
|
||||
K0=$(sudo kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
|
||||
K1=$(sudo kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
|
||||
PG=$(sudo kubectl -n keycloak-lab get pod -l app=postgres -o jsonpath='{.items[0].status.podIP}')
|
||||
echo "K0=$K0 K1=$K1 PG=$PG"
|
||||
```
|
||||
|
||||
## 1-2. 상주 탐침 파드를 먼저 띄운다
|
||||
|
||||
Keycloak 컨테이너에는 `curl` 도 `wget` 도 없다(`exit 127`). 그리고 이 실험은
|
||||
**같은 요청을 수십 번 반복**해야 하므로 파드를 매번 만들면 안 된다.
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab run a6-probe --image=curlimages/curl:8.11.1 \
|
||||
--restart=Never \
|
||||
--env="K0=$K0" --env="K1=$K1" \
|
||||
--env="PW=$(sudo kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d)" \
|
||||
--command -- sleep 1800
|
||||
sudo kubectl -n keycloak-lab wait --for=condition=Ready pod/a6-probe --timeout=120s
|
||||
```
|
||||
|
||||
**되돌리기**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab delete pod a6-probe --ignore-not-found
|
||||
```
|
||||
|
||||
**확인** — 값이 들어갔나. **비밀번호는 길이만 본다**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- sh -c 'echo "K0=$K0 K1=$K1 PW=${#PW}자"'
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
K0=10.42.1.77 K1=10.42.0.42 PW=32자
|
||||
```
|
||||
|
||||
`PW=0자` 면 시크릿이 안 넘어간 것이다. 그 상태로 재면 **전부 401 을 재게 된다.**
|
||||
|
||||
> **★ `kubectl run --rm -i` 로 부하를 주면 안 된다.**
|
||||
> 원 실행이 그렇게 했다가 **동시 20건의 출력을 잃었다.** 파드가 만들어지고
|
||||
> 지워지는 사이에 stdout 을 붙잡는 경주가 되고, 20줄 중 일부만 도착하거나
|
||||
> 아예 끊긴다. **결과는 파드 안 파일에 모으고 끝나면 한 번에 꺼낸다.**
|
||||
> 이 가이드의 모든 부하 명령이 그 형태다.
|
||||
|
||||
> **탐침의 `K0`/`K1` 은 만들 때 고정된다.** Keycloak 파드가 재시작되면 IP 가
|
||||
> 바뀌고 탐침의 값은 낡는다. 그때는 탐침을 지우고 다시 만든다.
|
||||
> 이걸 놓치면 **「아무 데도 안 닿음」을 「지연」으로 착각한다.**
|
||||
|
||||
---
|
||||
|
||||
# 2. 기준선 — 주입 전에 같은 명령으로 먼저 잰다
|
||||
|
||||
## 2-1. 요청 하나를 눈으로 본다
|
||||
|
||||
먼저 **읽는 형태**로 한 번 친다. 시간이 어디서 드는지 봐야 나중에 무엇이
|
||||
변했는지 안다.
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
curl -s -o /dev/null \
|
||||
-w "connect %{time_connect} ttfb %{time_starttransfer} total %{time_total}\n" \
|
||||
-X POST "http://$K1:8080/realms/master/protocol/openid-connect/token" \
|
||||
-d grant_type=password -d client_id=admin-cli -d username=admin -d "password=$PW"'
|
||||
```
|
||||
**형태**
|
||||
```
|
||||
connect 0.001 ttfb 0.065 total 0.066
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `connect` 와 `ttfb` 의 차이.
|
||||
|
||||
| 값 | 무엇의 시간인가 |
|
||||
|---|---|
|
||||
| `time_connect` | 탐침 → Keycloak **TCP 연결**. 이 실험에서 **거의 안 변한다** |
|
||||
| `time_starttransfer` | 첫 바이트까지 = **Keycloak 이 DB 와 대화한 시간**. 여기가 폭발한다 |
|
||||
|
||||
**이 결과가 의미하는 것** — 지연은 **탐침과 Keycloak 사이**가 아니라
|
||||
**Keycloak 과 DB 사이**에 넣는다. 그래서 `connect` 는 그대로고 `ttfb` 만 는다.
|
||||
주입 후에 이 두 값을 다시 보면 **어디에 지연이 걸렸는지 한눈에 판정된다.**
|
||||
|
||||
응답이 `401` 이나 `400` 이면 `-o /dev/null` 을 빼고 본문을 본다.
|
||||
|
||||
## 2-2. 반복해서 평균을 낸다
|
||||
|
||||
**확인** — 20회, 원본을 파일에 모은다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
rm -f /tmp/base-k1 ; i=0
|
||||
while [ $i -lt 20 ]; do
|
||||
curl -s -o /dev/null -w "%{time_total}\n" \
|
||||
-X POST "http://$K1:8080/realms/master/protocol/openid-connect/token" \
|
||||
-d grant_type=password -d client_id=admin-cli -d username=admin -d "password=$PW" \
|
||||
>> /tmp/base-k1
|
||||
i=$((i+1))
|
||||
done'
|
||||
```
|
||||
**원본을 먼저 본다.** 평균만 보면 한 건이 튄 것을 놓친다.
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- cat /tmp/base-k1
|
||||
```
|
||||
그 다음 줄여서 본다.
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- cat /tmp/base-k1 \
|
||||
| awk '{s+=$1} END {printf "%d회 평균 %.0f ms\n", NR, s*1000/NR}'
|
||||
```
|
||||
|
||||
`$K1` 을 `$K0` 로 바꿔 **대조군도 똑같이** 잰다.
|
||||
|
||||
**실측** — [`01-baseline.txt`](../../evidence/a6-latency-injection/01-baseline.txt)
|
||||
```
|
||||
=== 기준선 지연 — 각 노드에서 로그인 20회 ===
|
||||
keycloak-0 평균 70 ms
|
||||
keycloak-1 평균 66 ms
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — 두 값이 **비슷한 것**. 지금 `keycloak-1` 이 오히려
|
||||
4ms 빠르다. **VXLAN 을 건너는 쪽이 더 빠를 수도 있는 수준의 차이**이며,
|
||||
그래서 뒤에 나올 28배가 의심의 여지 없이 주입 탓이 된다.
|
||||
|
||||
> **횟수를 주입 전후로 똑같이 맞춘다.** 기준선은 20회로 쟀고, 해설 문서의
|
||||
> 재현 절차에는 15회로 적혀 있다. 횟수가 다르면 평균도 달라진다.
|
||||
> **비교할 두 값은 같은 명령으로 만든다.**
|
||||
|
||||
## 2-3. 커넥션 풀 지표에 무엇이 있나
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- sh -c \
|
||||
'curl -s "http://$K1:9000/metrics" | grep "^agroal_"'
|
||||
```
|
||||
**실측** — [`01-baseline.txt`](../../evidence/a6-latency-injection/01-baseline.txt)
|
||||
```
|
||||
agroal_acquire_count_total
|
||||
agroal_active_count
|
||||
agroal_available_count
|
||||
agroal_awaiting_count
|
||||
agroal_blocking_time_average_milliseconds
|
||||
agroal_blocking_time_max_milliseconds
|
||||
agroal_blocking_time_total_milliseconds
|
||||
agroal_creation_count_total
|
||||
agroal_creation_time_average_milliseconds
|
||||
agroal_creation_time_max_milliseconds
|
||||
agroal_creation_time_total_milliseconds
|
||||
agroal_destroy_count_total
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `agroal_*` 이 **JDBC 커넥션 풀** 지표다
|
||||
(Agroal 은 Quarkus 의 풀 구현이다). 이 실험의 핵심 증거가 여기서 나온다.
|
||||
|
||||
> **당신 출력은 이보다 길 것이다.** 위 목록은 알파벳순으로 `destroy_count_total`
|
||||
> 에서 끊겨 있다 — 원 실행이 앞부분만 남긴 것이다. 실제로는 뒤에
|
||||
> `agroal_max_used_count` 같은 것이 더 있고, 6-4 에서 그 값을 쓴다.
|
||||
> **증거 파일이 짧다고 지표가 없는 것이 아니다.**
|
||||
|
||||
| 지표 | 무엇을 말하는가 |
|
||||
|---|---|
|
||||
| `blocking_time_max` | **커넥션을 받으려고 가장 오래 기다린 시간** |
|
||||
| `max_used_count` | 풀이 최대 몇 개까지 늘었나 |
|
||||
| `awaiting_count` | **지금** 줄 서 있는 요청 수 |
|
||||
| `active_count` | **지금** 쓰이고 있는 커넥션 수 |
|
||||
|
||||
**`awaiting_count` 와 `active_count` 는 순간값이다.** 부하가 끝나면 0 으로
|
||||
돌아간다 — **부하 중에 읽어야 보인다.** `blocking_time_max` 는 누적이라
|
||||
나중에 읽어도 남아 있다.
|
||||
|
||||
지금 값을 적어 둔다. 나중에 오른 것을 보려면 지금 값이 필요하다.
|
||||
|
||||
---
|
||||
|
||||
# 3. 주입 시도 ① — `eth0` (그런 인터페이스가 없다)
|
||||
|
||||
**일부러 실패하는 단계다.** 이 실패의 모양이 이 실험이 남긴 가장 실용적인
|
||||
교훈이다.
|
||||
|
||||
## 3-1. 넣어 본다
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo tc qdisc add dev eth0 root handle 1: prio'
|
||||
```
|
||||
**실측** — [`02-delay-injected.txt`](../../evidence/a6-latency-injection/02-delay-injected.txt)
|
||||
```
|
||||
Cannot find device "eth0"
|
||||
```
|
||||
|
||||
한 줄이면 끝날 일이다. **그런데 원 실행은 이걸 스크립트로 돌렸다.**
|
||||
|
||||
**실측** — 같은 파일, 원문 그대로
|
||||
```
|
||||
=== 주입: postgres(10.42.1.76) 가 보내는 패킷만 200ms 지연 (kc-lab-2 eth0) ===
|
||||
prio qdisc 로 밴드를 나누고, u32 필터로 출발지 IP 가 postgres 인 것만 3번 밴드로 보낸다
|
||||
Cannot find device "eth0"
|
||||
Cannot find device "eth0"
|
||||
적용완료
|
||||
Cannot find device "eth0"
|
||||
Cannot find device "eth0"
|
||||
주입: 13:14:55
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`적용완료` 가 에러 사이에 끼어 있다.**
|
||||
|
||||
**이 결과가 의미하는 것** — **「적용완료」는 스크립트가 찍은 글자이지 커널이
|
||||
한 말이 아니다.** `tc` 는 네 번 다 실패했는데 스크립트는 그대로 다음 절로
|
||||
넘어갔고, 문서에는 시각까지 찍혔다.
|
||||
|
||||
> **명령의 성공을 「에러가 안 보인다」로 판정하면 안 된다.**
|
||||
> 에러는 보였는데 그 사이에 성공 메시지가 있었을 뿐이다.
|
||||
> 손으로 한 줄씩 치면 이 실수를 할 수 없다 — **이 가이드에 스크립트가 없는
|
||||
> 이유다.**
|
||||
|
||||
그리고 그 상태에서 잰 「검증」이 이랬다.
|
||||
|
||||
**실측** — 같은 파일
|
||||
```
|
||||
=== [검증] 지연이 실제로 걸렸는가 — 두 노드 비교 ===
|
||||
keycloak-0 평균 43 ms 최대 64 ms
|
||||
keycloak-1 평균 47 ms 최대 70 ms
|
||||
```
|
||||
|
||||
**두 노드가 여전히 같다. 이것이 「안 걸렸다」는 신호였다.** 검증 절이 값을
|
||||
찍기만 하고 **판정하지 않으면** 이렇게 그냥 지나간다.
|
||||
|
||||
## 3-2. 인터페이스 이름을 확인한다
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
ssh kc-lab-2 'ip -brief link'
|
||||
```
|
||||
**실측** — [`03-flannel-injection.txt`](../../evidence/a6-latency-injection/03-flannel-injection.txt)
|
||||
```
|
||||
flannel.1 UNKNOWN a6:b2:62:04:c1:a4 <BROADCAST,MULTICAST,UP,LOWER_UP>
|
||||
cni0 UP 5a:77:1a:e2:b0:a4 <BROADCAST,MULTICAST,UP,LOWER_UP>
|
||||
```
|
||||
게스트의 물리 인터페이스는 `enp1s0` 이다.
|
||||
|
||||
**어디를 봐야 하는가** — **`eth0` 이 없다.**
|
||||
|
||||
| 이름 | 무엇 |
|
||||
|---|---|
|
||||
| `enp1s0` | **게스트의 물리(가상) NIC.** 노드 간 실제 트래픽이 나가는 곳 |
|
||||
| `flannel.1` | **VXLAN 터널.** 노드를 건너는 파드 트래픽이 여기로 들어간다 |
|
||||
| `cni0` | **노드 안 브리지.** 같은 노드 파드끼리는 여기서 끝난다 |
|
||||
|
||||
Debian 클라우드 이미지는 **예측 가능한 인터페이스 이름**을 쓴다.
|
||||
|
||||
```
|
||||
enp1s0
|
||||
│ │ └─ s0 : slot 0
|
||||
│ └──── p1 : PCI bus 1
|
||||
└────── en : ethernet
|
||||
```
|
||||
|
||||
이름이 **하드웨어 위치에서** 나오므로 NIC 순서가 바뀌어도 이름이 안 바뀐다.
|
||||
그 대신 `eth0` 이라고 적힌 인터넷의 모든 예제가 안 돈다.
|
||||
|
||||
> `flannel.1` 의 상태가 `UNKNOWN` 인 것은 정상이다. 터널 장치는 캐리어 개념이
|
||||
> 없어서 `UP` 대신 `UNKNOWN` 으로 보고한다. **고장이 아니다.**
|
||||
|
||||
---
|
||||
|
||||
# 4. 주입 시도 ② — `enp1s0` (파드 IP 가 안 보인다)
|
||||
|
||||
`eth0` 을 `enp1s0` 으로 고치면 될 것 같다. **안 된다.** 이유가 이 실험의 핵심
|
||||
개념이다.
|
||||
|
||||
## 4-1. 무엇이 문제인가
|
||||
|
||||
노드 간 파드 통신은 **flannel VXLAN 으로 캡슐화**된다.
|
||||
|
||||
```
|
||||
원래 패킷: src=10.42.1.76(postgres) dst=10.42.0.42(keycloak-1)
|
||||
│
|
||||
▼ flannel.1 에서 캡슐화
|
||||
실제 패킷: src=192.168.122.12(노드) dst=192.168.122.11(노드) UDP 8472
|
||||
└─ 안쪽에 원래 패킷이 통째로 들어 있다
|
||||
│
|
||||
▼
|
||||
enp1s0 로 나간다
|
||||
```
|
||||
|
||||
**`enp1s0` 에서 `match ip src 10.42.1.76` 은 절대 일치하지 않는다.**
|
||||
그 IP 는 페이로드 안에 있고, 헤더에는 노드 IP 만 있다.
|
||||
|
||||
## 4-2. 눈으로 확인한다
|
||||
|
||||
**확인** — 실제로 무엇이 나가는지 본다. **미검증**
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo tcpdump -i enp1s0 -n -c 5 udp port 8472'
|
||||
```
|
||||
노드 IP 사이의 UDP 8472 만 보이고 `10.42.x.x` 는 안 보인다.
|
||||
|
||||
같은 시간에 터널 쪽을 보면 파드 IP 가 보인다. **미검증**
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo tcpdump -i flannel.1 -n -c 5 host 10.42.1.76'
|
||||
```
|
||||
|
||||
> **원 실행에는 이 확인이 없다.** `eth0` 실패 뒤 곧바로 `flannel.1` 로 갔다.
|
||||
> 그래서 「`enp1s0` 에 걸면 0 패킷」이라는 **출력 원문은 이 실험에 없다** —
|
||||
> 구조에서 나온 결론이다. 당신이 직접 보고 싶으면 위 `tcpdump` 두 줄이면 된다.
|
||||
|
||||
**이 결과가 의미하는 것** — **오버레이 네트워크에서는 「어느 인터페이스에
|
||||
거는가」가 「무엇을 볼 수 있는가」를 정한다.**
|
||||
|
||||
| 인터페이스 | 파드 IP 가 보이나 | 무엇을 지연시키게 되나 |
|
||||
|---|---|---|
|
||||
| `cni0` | 보인다 | **같은 노드 안** 통신만 |
|
||||
| **`flannel.1`** | **보인다 (캡슐화 직전)** | **노드를 건너는** 파드 통신 |
|
||||
| `enp1s0` | **안 보인다** | 노드 간 **모든** 것 (SSH·k3s 포함) |
|
||||
|
||||
`enp1s0` 에 `netem` 을 root 로 걸면 **`kubectl` 도 SSH 도 같이 느려진다.**
|
||||
그러면 무엇이 원인인지 못 가린다.
|
||||
|
||||
---
|
||||
|
||||
# 5. 주입 성공 — `flannel.1` 에 건다
|
||||
|
||||
## 5-1. 거는 순서
|
||||
|
||||
**되돌리기** — 먼저 읽어 둔다. 이 한 줄이 세 가지를 다 지운다
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo tc qdisc del dev flannel.1 root'
|
||||
```
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
ssh kc-lab-2 "sudo tc qdisc add dev flannel.1 root handle 1: prio"
|
||||
ssh kc-lab-2 "sudo tc qdisc add dev flannel.1 parent 1:3 handle 30: netem delay 200ms"
|
||||
ssh kc-lab-2 "sudo tc filter add dev flannel.1 protocol ip parent 1:0 prio 3 \
|
||||
u32 match ip src $PG/32 flowid 1:3"
|
||||
date '+%H:%M:%S 주입'
|
||||
```
|
||||
|
||||
**한 줄씩 친다.** 앞 줄이 실패하면 뒤 줄은 붙을 곳이 없어서 다른 에러를 낸다.
|
||||
|
||||
## 5-2. 개념 — `tc` 의 계층 구조
|
||||
|
||||
```
|
||||
qdisc (큐 규율) 인터페이스에 붙는 패킷 스케줄러
|
||||
├─ prio 우선순위 밴드 3개로 나눈다
|
||||
│ ├─ 1:1 (기본)
|
||||
│ ├─ 1:2 (기본)
|
||||
│ └─ 1:3 ← 여기에 netem 을 붙인다
|
||||
└─ filter 어떤 패킷을 어느 밴드로 보낼지
|
||||
```
|
||||
|
||||
**`netem` 을 root 에 바로 붙이면 모든 트래픽이 느려진다.**
|
||||
`prio` + `filter` 를 쓰면 **고른 트래픽만** 느려진다. 이 실험은
|
||||
**postgres 가 보내는 것만** 골라야 하므로 세 단계가 필요하다.
|
||||
|
||||
세 줄이 하는 일을 나눠 읽으면 이렇다.
|
||||
|
||||
| 줄 | 하는 일 |
|
||||
|---|---|
|
||||
| `qdisc ... root handle 1: prio` | 밴드 3개짜리 분류기를 만든다 |
|
||||
| `qdisc ... parent 1:3 handle 30: netem delay 200ms` | 3번 밴드에 **200ms 지연**을 붙인다 |
|
||||
| `filter ... match ip src $PG/32 flowid 1:3` | **출발지가 postgres 인 패킷**을 3번 밴드로 보낸다 |
|
||||
|
||||
## 5-3. ★ 걸렸는지 카운터로 확인한다 — 그리고 0 을 오해하지 않는다
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo tc -s qdisc show dev flannel.1'
|
||||
```
|
||||
**실측** — [`03-flannel-injection.txt`](../../evidence/a6-latency-injection/03-flannel-injection.txt) · **넣은 직후**
|
||||
```
|
||||
qdisc prio 1: root refcnt 2 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
|
||||
Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0)
|
||||
backlog 0b 0p requeues 0
|
||||
qdisc netem 30: parent 1:3 limit 1000 delay 200ms
|
||||
Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0)
|
||||
backlog 0b 0p requeues 0
|
||||
```
|
||||
|
||||
**`Sent 0 pkt` 이다. 그런데 이건 실패가 아니다.**
|
||||
|
||||
A-5 에서 `pkts 0` 은 「규칙이 안 걸렸다」였다. **여기서는 다르다** —
|
||||
아직 **아무 패킷도 지나가지 않았을 뿐**이다. postgres 는 요청이 있어야 답한다.
|
||||
|
||||
**하기** — 트래픽을 한 번 만든다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
curl -s -o /dev/null -w "%{time_total}\n" \
|
||||
-X POST "http://$K1:8080/realms/master/protocol/openid-connect/token" \
|
||||
-d grant_type=password -d client_id=admin-cli -d username=admin -d "password=$PW"'
|
||||
```
|
||||
|
||||
**확인** — 다시 센다
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo tc -s qdisc show dev flannel.1 | grep -A2 netem'
|
||||
```
|
||||
**실측** — 같은 파일
|
||||
```
|
||||
=== [검증] 필터에 패킷이 걸리는가 ===
|
||||
qdisc netem 30: parent 1:3 limit 1000 delay 200ms
|
||||
Sent 18388 bytes 150 pkt (dropped 0, overlimits 0 requeues 0)
|
||||
backlog 0b 0p requeues 0
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`150 pkt`.** 실제로 지연 밴드를 통과했다.
|
||||
|
||||
> **판정표 — `netem` 의 `Sent`**
|
||||
>
|
||||
> | 상태 | 뜻 | 할 일 |
|
||||
> |---|---|---|
|
||||
> | 부하 전 `0 pkt` | 아직 트래픽이 없다 | 요청을 한 번 보내고 다시 센다 |
|
||||
> | **부하 후에도 `0 pkt`** | **필터가 아무것도 못 잡았다** | IP·인터페이스·방향을 다시 본다 |
|
||||
> | `pkt` 이 는다 | 걸렸다 | 관찰로 넘어간다 |
|
||||
> | `dropped` 가 는다 | `limit 1000` 을 넘겼다 | 부하를 줄이거나 `limit` 을 올린다 |
|
||||
|
||||
**A-1·A-5 와 같은 교훈이 세 번째로 나왔다. 주입을 넣은 것과 걸린 것은 다르다.**
|
||||
|
||||
필터 자체도 볼 수 있다. **미검증**
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo tc filter show dev flannel.1'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 6. 효과를 관찰한다
|
||||
|
||||
## 6-1. 단일 요청 — 지연은 곱해진다
|
||||
|
||||
**확인** — 2-1 과 **똑같은 명령**을 다시 친다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
curl -s -o /dev/null \
|
||||
-w "connect %{time_connect} ttfb %{time_starttransfer} total %{time_total}\n" \
|
||||
-X POST "http://$K1:8080/realms/master/protocol/openid-connect/token" \
|
||||
-d grant_type=password -d client_id=admin-cli -d username=admin -d "password=$PW"'
|
||||
```
|
||||
|
||||
**`connect` 는 그대로인데 `ttfb` 만 폭발**하는지 본다. 그러면 지연이
|
||||
**의도한 구간에** 걸린 것이다.
|
||||
|
||||
그 다음 2-2 와 똑같이 반복해서 두 노드를 잰다.
|
||||
|
||||
**실측** — [`03-flannel-injection.txt`](../../evidence/a6-latency-injection/03-flannel-injection.txt)
|
||||
```
|
||||
=== 두 노드 지연 비교 (기준선: k0=70ms k1=66ms) ===
|
||||
keycloak-0 평균 41 ms 최대 57 ms
|
||||
keycloak-1 평균 1872 ms 최대 1887 ms
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — `keycloak-1` 이 **66 → 1,872 ms, 28배.**
|
||||
|
||||
> **★ 대조군도 변했다.** `keycloak-0` 은 기준선 70ms 에서 41ms 로 **41%
|
||||
> 빨라졌다.** 주입과 무관한 변동(JIT 워밍업, 캐시)이며, 해설 문서가 처음에
|
||||
> 「영향 없음」이라고 쓴 것은 **부정확했다.** 자릿수가 달라 결론은 유지되지만,
|
||||
> **대조군이 안 변한다고 가정하면 안 된다.** 당신 실행에서도 볼 것이다.
|
||||
|
||||
## 6-2. 왜 200ms 가 1,872ms 가 되는가
|
||||
|
||||
A-0 에서 잡은 로그인 트랜잭션의 SQL 이 답이다.
|
||||
|
||||
```
|
||||
BEGIN
|
||||
select ... from OFFLINE_USER_SESSION ...
|
||||
select VERSION ... for no key update skip locked
|
||||
select ... from OFFLINE_CLIENT_SESSION ...
|
||||
select VERSION ... for no key update skip locked
|
||||
insert into OFFLINE_USER_SESSION ...
|
||||
insert into OFFLINE_CLIENT_SESSION ...
|
||||
SET LOCAL synchronous_commit TO OFF
|
||||
COMMIT
|
||||
```
|
||||
|
||||
**왕복이 아홉 번이다.**
|
||||
|
||||
```
|
||||
200 ms × 9 왕복 ≈ 1,800 ms 실측 1,872 ms
|
||||
```
|
||||
|
||||
> **★ `9` 는 SQL 목록을 센 것이고 패킷을 추적한 값이 아니다.** 자릿수가 맞는다는
|
||||
> 것까지가 이 계산이 말할 수 있는 범위이며, **왕복 수를 확정하려면 `tc -s` 의
|
||||
> 패킷 수를 요청 수로 나누거나 패킷 캡처가 필요하다.**
|
||||
|
||||
> **네트워크 지연은 왕복 횟수만큼 증폭된다.**
|
||||
> 「DB 가 200ms 느려졌다」는 「애플리케이션이 200ms 느려졌다」가 아니다.
|
||||
> **쿼리 수를 줄이는 것이 지연 환경에서 결정적인 이유**가 이것이다.
|
||||
|
||||
## 6-3. 동시 부하 — 여기서 진짜 고장이 난다
|
||||
|
||||
**여기가 이 실험의 본 시험이다.** 순차로 20번 돌리면 큐잉이 재현되지 않는다.
|
||||
**동시에** 20건을 보내야 한다.
|
||||
|
||||
**하기** — 백그라운드로 띄우고 `wait`. 결과는 파드 안 파일에 모은다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- sh -c '
|
||||
rm -f /tmp/load ; i=0
|
||||
while [ $i -lt 20 ]; do
|
||||
( curl -s -o /dev/null -w "%{http_code} %{time_total}\n" --max-time 60 \
|
||||
-X POST "http://$K1:8080/realms/master/protocol/openid-connect/token" \
|
||||
-d grant_type=password -d client_id=admin-cli -d username=admin -d "password=$PW" \
|
||||
>> /tmp/load ) &
|
||||
i=$((i+1))
|
||||
done
|
||||
wait'
|
||||
```
|
||||
|
||||
**확인** — 다 모였는지부터 센다
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- cat /tmp/load > /tmp/load.txt
|
||||
wc -l /tmp/load.txt
|
||||
```
|
||||
|
||||
**`20` 이 아니면 수집이 샌 것이다.** 그 상태의 숫자는 해석하지 않는다.
|
||||
|
||||
**확인** — 원본을 본다
|
||||
```bash
|
||||
cat /tmp/load.txt
|
||||
```
|
||||
그 다음 상태 코드와 시간을 나눠 본다.
|
||||
```bash
|
||||
awk '{print $1}' /tmp/load.txt | sort | uniq -c
|
||||
awk '{print $2}' /tmp/load.txt | sort -g
|
||||
```
|
||||
|
||||
**실측** — [`04-pool-under-load.txt`](../../evidence/a6-latency-injection/04-pool-under-load.txt)
|
||||
```
|
||||
=== 동시 부하 20건을 keycloak-1 에 — 커넥션 풀이 견디는가 ===
|
||||
1 200 1.911191
|
||||
1 200 1.913766
|
||||
1 200 1.958374
|
||||
1 200 1.981620
|
||||
1 200 10.539402
|
||||
1 200 11.951943
|
||||
1 200 13.351102
|
||||
1 200 14.785832
|
||||
1 200 16.189533
|
||||
1 200 17.625166
|
||||
1 200 19.053724
|
||||
1 200 20.495883
|
||||
1 200 21.905932
|
||||
1 200 22.228466
|
||||
1 200 22.230871
|
||||
1 200 3.441366
|
||||
1 200 4.841075
|
||||
1 200 6.257489
|
||||
1 200 7.704608
|
||||
1 200 9.104792
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가 — 두 가지다.**
|
||||
|
||||
**① 순서가 이상하다.** `10.5` 가 `3.4` 보다 앞에 있다. 원 실행이 `sort` 를
|
||||
**사전순**으로 썼기 때문이다(맨 앞의 `1` 은 `uniq -c` 가 붙인 개수다).
|
||||
문자열로 정렬하면 `"10.5" < "3.4"` 다.
|
||||
|
||||
```bash
|
||||
sort /tmp/load.txt # 사전순 — 10.5 가 3.4 앞에 온다
|
||||
sort -g /tmp/load.txt # 수치순 — 이걸 써야 한다
|
||||
```
|
||||
|
||||
**시간 값을 정렬할 때는 `sort -g`.** 이걸 놓치면 「최대값」을 잘못 읽는다.
|
||||
|
||||
**② 숫자를 순서대로 놓으면 계단이다.**
|
||||
|
||||
```
|
||||
1.9 → 3.4 → 4.8 → 6.2 → 7.7 → 9.1 → 10.5 → ... → 22.2
|
||||
──── ──── ──── ────
|
||||
약 1.4초 간격 — 앞 요청이 커넥션을 놓아줄 때까지 줄을 선다
|
||||
```
|
||||
|
||||
**전부 성공(200)했지만 응답 시간이 1.9초에서 22.2초까지 늘어난다.**
|
||||
**전형적인 큐잉이다.** 커넥션 수는 유한하고, 각 요청이 커넥션을 1.9초씩
|
||||
붙잡으므로 뒤에 온 요청은 그만큼 기다린다.
|
||||
|
||||
> **`200` 만 보는 감시는 이 장애를 못 본다.** 상태 코드는 전부 정상이다.
|
||||
> **응답 시간 분포를 봐야 한다.**
|
||||
|
||||
## 6-4. 커넥션 풀 지표가 증언한다
|
||||
|
||||
**부하가 끝나자마자** 읽는다. 늦으면 순간값이 0 으로 돌아간다.
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab exec a6-probe -- sh -c \
|
||||
'curl -s "http://$K1:9000/metrics" | grep -E "^agroal_(blocking_time|max_used|acquire|active|available|awaiting)"'
|
||||
```
|
||||
**실측** — [`04-pool-under-load.txt`](../../evidence/a6-latency-injection/04-pool-under-load.txt)
|
||||
```
|
||||
=== 부하 직후 커넥션 풀 ===
|
||||
agroal_blocking_time_max_milliseconds 20000.0
|
||||
agroal_max_used_count 19.0
|
||||
agroal_acquire_count_total 672.0
|
||||
agroal_active_count 0.0
|
||||
agroal_awaiting_count 0.0
|
||||
agroal_blocking_time_average_milliseconds 281.0
|
||||
agroal_available_count 19.0
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가**
|
||||
|
||||
| 값 | 읽는 법 |
|
||||
|---|---|
|
||||
| `blocking_time_max 20000.0` | **커넥션을 받으려고 20초를 기다린 요청이 있었다** |
|
||||
| `max_used_count 19.0` | 풀이 19개까지 늘어났다 |
|
||||
| `blocking_time_average 281.0` | 평균은 0.3초. **평균만 보면 아무 일도 없어 보인다** |
|
||||
| `active_count 0.0` · `awaiting_count 0.0` | **순간값. 부하가 끝나서 0 이다** |
|
||||
|
||||
**평균과 최대의 간격이 이 장애의 모양이다.** 평균 281ms 짜리 그래프에서는
|
||||
아무도 20초를 보지 못한다.
|
||||
|
||||
Grafana 에서 같은 것을 그림으로 본다 —
|
||||
[`a6-connection-pool-blocking.png`](../../evidence/a6-latency-injection/a6-connection-pool-blocking.png).
|
||||
|
||||
## 6-5. 그리고 헬스체크가 무너진다
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab get events --sort-by=.lastTimestamp | tail -20
|
||||
sudo kubectl -n keycloak-lab get pods
|
||||
```
|
||||
**실측** — [`04-pool-under-load.txt`](../../evidence/a6-latency-injection/04-pool-under-load.txt)
|
||||
```
|
||||
keycloak-0 1/1 Running 0 60m
|
||||
keycloak-1 1/1 Running 1 (51m ago) 3h24m
|
||||
52m Normal TaintManagerEviction pod/keycloak-1 Cancelling deletion of Pod keycloak-lab/keycloak-1
|
||||
32m Warning Unhealthy pod/keycloak-1 Readiness probe failed: HTTP probe failed with statuscode: 503
|
||||
89s Warning Unhealthy pod/keycloak-1 Readiness probe failed: Get "http://10.42.0.42:9000/health/ready": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`89s` 짜리 줄.** 그것이 지금 주입의 결과다.
|
||||
`32m`·`52m` 짜리는 **A-4 의 잔재**다(노드를 껐다 켠 흔적).
|
||||
|
||||
> **이벤트를 볼 때는 `Age` 를 먼저 본다.** 이벤트 목록은 한 시간 전 것까지
|
||||
> 섞여 있다. 방금 일어난 일만 골라야 한다.
|
||||
|
||||
**두 실패의 차이가 중요하다.**
|
||||
|
||||
| 메시지 | 무슨 일 |
|
||||
|---|---|
|
||||
| `HTTP probe failed with statuscode: 503` | Keycloak 이 **답은 했다.** 스스로 DOWN 이라고 말했다 |
|
||||
| **`context deadline exceeded`** | **답 자체를 못 했다.** 프로브가 줄에서 기다리다 끝났다 |
|
||||
|
||||
**readiness 프로브 자체가 타임아웃됐다.** 헬스체크도 같은 커넥션 풀 줄에 선다.
|
||||
|
||||
## 6-6. 연쇄 고장의 모양
|
||||
|
||||
```
|
||||
DB 가 느려진다
|
||||
↓
|
||||
요청이 커넥션을 오래 붙잡는다
|
||||
↓
|
||||
커넥션 풀이 고갈된다
|
||||
↓
|
||||
새 요청이 줄을 선다 (최대 20초)
|
||||
↓
|
||||
헬스체크도 줄에 선다 → 타임아웃 → NotReady
|
||||
↓
|
||||
그 노드가 로드밸런서에서 빠진다
|
||||
↓
|
||||
★ 남은 노드로 트래픽이 몰린다 → 그 노드도 같은 길을 간다
|
||||
```
|
||||
|
||||
**마지막 화살표가 무서운 부분이다. 느려짐은 전파된다.**
|
||||
A-2(DB 완전 정지)는 즉시 503 으로 드러나 오히려 명확했지만,
|
||||
**느려짐은 살아 있는 노드를 하나씩 무너뜨린다.**
|
||||
|
||||
## 6-7. 빗나간 예측 — 낙관적 락 충돌은 늘지 않았다
|
||||
|
||||
계획서에는 이렇게 적혀 있었다.
|
||||
|
||||
> **낙관적 락 충돌 증가** — 트랜잭션이 길어져 `VERSION` 충돌이 늘어야 한다
|
||||
|
||||
**확인** — 지연 구간의 로그를 센다. **미검증** (원 실행의 정확한 패턴은 기록에 없다)
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab logs keycloak-1 --since=20m \
|
||||
| grep -icE 'optimistic|StaleState|version.*conflict'
|
||||
```
|
||||
**실측** — [`05-recovery.txt`](../../evidence/a6-latency-injection/05-recovery.txt)
|
||||
```
|
||||
=== 낙관적 락 충돌이 늘었는가 — 지연 중 로그 ===
|
||||
관련 로그 줄수: 0
|
||||
```
|
||||
|
||||
**하나도 없었다.** 이유가 명확하다.
|
||||
|
||||
```
|
||||
로그인 → 매번 새 세션 행을 INSERT → 다툴 상대가 없다
|
||||
refresh → 같은 세션 행을 UPDATE → 여기서 다툰다
|
||||
```
|
||||
|
||||
**충돌은 같은 행을 동시에 고칠 때만 일어난다.** 로그인 부하로는 재현되지
|
||||
않는다. 이건 **B-3(refresh 토큰 경쟁)의 영역**이며, 거기서 지연을 함께 주면
|
||||
충돌률이 올라갈 것이다.
|
||||
|
||||
> 예측을 적어 두지 않았다면 「충돌이 없네」 하고 넘어갔을 것이다.
|
||||
> **빗나간 예측이 다음 실험의 설계를 정해 준다.**
|
||||
|
||||
---
|
||||
|
||||
# 7. 복구
|
||||
|
||||
## 7-1. 지운다
|
||||
|
||||
**하기**
|
||||
```bash
|
||||
date '+%H:%M:%S 해제'
|
||||
ssh kc-lab-2 'sudo tc qdisc del dev flannel.1 root'
|
||||
```
|
||||
|
||||
**확인**
|
||||
```bash
|
||||
ssh kc-lab-2 'sudo tc qdisc show dev flannel.1'
|
||||
```
|
||||
**실측** — [`05-recovery.txt`](../../evidence/a6-latency-injection/05-recovery.txt)
|
||||
```
|
||||
=== 지연 해제 ===
|
||||
해제완료
|
||||
qdisc noqueue 0: root refcnt 2
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — **`noqueue`.** `prio` 도 `netem` 도 없다.
|
||||
`root` 를 지우면 그 아래 자식 qdisc 와 filter 가 **같이** 사라진다.
|
||||
|
||||
## 7-2. 즉시 회복하는지 본다
|
||||
|
||||
**확인** — **2-2 의 반복 측정 명령을 그대로 다시 친다.** 그 명령의 첫 줄이
|
||||
`rm -f /tmp/base-k1` 이므로 파일은 새로 만들어진다. 두 노드 다 잰다.
|
||||
|
||||
같은 명령이어야 비교가 된다. 다른 명령으로 잰 값은 기준선과 나란히 놓을 수 없다.
|
||||
|
||||
**실측** — 같은 파일
|
||||
```
|
||||
=== 회복 확인 ===
|
||||
keycloak-0 평균 43 ms
|
||||
keycloak-1 평균 51 ms
|
||||
keycloak-0 1/1 Running 0 61m
|
||||
keycloak-1 1/1 Running 1 (52m ago) 3h24m
|
||||
```
|
||||
|
||||
**파드 재시작 없이 즉시 회복.** `RESTARTS` 가 안 늘었다 — 이 실험은
|
||||
readiness 를 흔들었을 뿐 파드를 죽이지는 않았다. **커넥션 풀도 스스로
|
||||
정상화됐다.**
|
||||
|
||||
## 7-3. 원상복구 확인표
|
||||
|
||||
| 항목 | 명령 | 돌아왔을 때 |
|
||||
|---|---|---|
|
||||
| qdisc | `ssh kc-lab-2 'sudo tc qdisc show dev flannel.1'` | `noqueue` |
|
||||
| (물리 쪽도) | `ssh kc-lab-2 'sudo tc qdisc show dev enp1s0'` | 시도 ① 잔재가 없어야 한다 |
|
||||
| 응답 시간 | 2-2 의 반복 측정 | 기준선과 같은 자릿수 |
|
||||
| 파드 | `sudo kubectl -n keycloak-lab get pods` | 둘 다 `1/1 Running` |
|
||||
| Service | `sudo kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak` | ready 주소 **둘** |
|
||||
| 풀 | `agroal_awaiting_count` · `agroal_active_count` | `0` |
|
||||
| 탐침 파드 | `sudo kubectl -n keycloak-lab get pod a6-probe` | 지웠으면 `NotFound` |
|
||||
| 밖 | `curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master` | `200` |
|
||||
|
||||
탐침을 지운다.
|
||||
```bash
|
||||
sudo kubectl -n keycloak-lab delete pod a6-probe --ignore-not-found
|
||||
```
|
||||
|
||||
> `agroal_blocking_time_max_milliseconds` 는 **누적이라 20000 인 채로 남는다.**
|
||||
> 파드를 재시작해야 0 이 된다. **그대로 두는 편이 낫다** — 「이 노드가 한 번
|
||||
> 20초를 기다린 적이 있다」는 기록이다.
|
||||
|
||||
---
|
||||
|
||||
# 막히면
|
||||
|
||||
전부 이 실험대가 **실제로 겪은** 증상이다. 지어낸 것은 없다.
|
||||
|
||||
| 증상 | 원인 | 확인 |
|
||||
|---|---|---|
|
||||
| `Cannot find device "eth0"` | **이 게스트의 NIC 는 `enp1s0` 이다** | `ip -brief link` — 3-2 |
|
||||
| 스크립트가 「적용완료」인데 지연이 없다 | **성공 메시지는 스크립트가 찍은 것** | `tc -s qdisc` 카운터 — 3-1 |
|
||||
| `enp1s0` 에 걸었는데 안 걸린다 | **VXLAN 안에 파드 IP 가 숨어 있다** | `flannel.1` 에 건다 — 4절 |
|
||||
| `Sent 0 pkt` | 부하 **전**이면 정상. 부하 **후**면 필터가 틀렸다 | 요청 한 번 보내고 다시 센다 — 5-3 |
|
||||
| 지연이 양쪽 다 늘었다 | `netem` 을 `root` 에 직접 붙였다 | `prio` + `filter` 로 골라 낸다 — 5-2 |
|
||||
| `kubectl` 이나 SSH 까지 느려졌다 | `enp1s0` 에 걸었다 | `tc qdisc del dev enp1s0 root` |
|
||||
| 20줄 중 몇 줄만 온다 | **`kubectl run --rm -i` 로 동시 실행하면 stdout 이 샌다** | 상주 파드 + 파일 — 1-2 |
|
||||
| 최대값이 `9.1` 로 보인다 | `sort` 가 **사전순**이다 | `sort -g` — 6-3 |
|
||||
| `blocking_time` 이 0 이다 | 부하가 끝나고 한참 뒤에 읽었다 | **부하 직후**에 읽는다 — 6-4 |
|
||||
| `awaiting_count` 가 늘 0 이다 | **순간값이다** | 부하가 도는 **중에** 읽는다 |
|
||||
| 로그인이 전부 `401` | `PW` 가 안 넘어갔다 | `exec a6-probe -- sh -c 'echo ${#PW}'` — 1-2 |
|
||||
| 갑자기 아무 데도 안 닿는다 | **파드 IP 가 바뀌었다** | 탐침을 지우고 다시 만든다 — 1-2 |
|
||||
| 이벤트가 과거 것과 섞인다 | 이벤트는 한 시간 전 것도 남는다 | `Age` 를 먼저 본다 — 6-5 |
|
||||
| 대조군도 값이 변했다 | **정상이다.** JIT·캐시 변동 | 자릿수로 판정한다 — 6-1 |
|
||||
| `dropped` 가 늘어난다 | `netem` 의 `limit 1000` 을 넘겼다 | 부하를 줄이거나 `limit` 을 올린다 |
|
||||
|
||||
---
|
||||
|
||||
# 이 실험이 남기는 관측 숙제
|
||||
|
||||
**지금 관측 스택에는 지연 분포 지표가 없다.** `agroal_blocking_time_*` 은
|
||||
있지만 히스토그램이 아니라 **평균과 최대뿐**이다. 6-4 에서 본 대로
|
||||
평균 281ms 와 최대 20,000ms 사이에 무엇이 있었는지는 알 수 없다.
|
||||
|
||||
```promql
|
||||
# 있으면 좋았을 것
|
||||
histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m]))
|
||||
```
|
||||
|
||||
| 알게 된 것 | 함의 |
|
||||
|---|---|
|
||||
| 지연은 **왕복 횟수만큼 곱해진다** | DB 지연 대책은 「쿼리 수 줄이기」가 먼저다 |
|
||||
| 커넥션 풀에서 **한 번 더 곱해진다** | 풀 크기와 타임아웃이 장애 반경을 정한다 |
|
||||
| **헬스체크도 줄에 선다** | 프로브 타임아웃이 풀 대기보다 짧아야 격리가 제때 된다 |
|
||||
| 느려짐은 **전파된다** | 노드를 빼면 남은 노드가 더 빨리 무너진다 |
|
||||
| `up` 도 readiness 도 **늦게 반응** | **응답 시간 분포(p95/p99)를 봐야 한다** |
|
||||
|
||||
---
|
||||
|
||||
# 다음
|
||||
|
||||
| 실험 | A-6 이 남긴 질문 |
|
||||
|---|---|
|
||||
| [B-3](../../experiment-b3-refresh-token-contention.md) refresh 경쟁 | **지연을 함께 주면 낙관적 락 충돌이 재현될 것** — 여기서는 안 됐다 |
|
||||
| [B-1](../../experiment-b1-redis-session-store.md) 저장소 지연 | **같은 기법을 Redis 앞에 쓴다.** `flannel.1` · `prio` · `filter` 그대로 |
|
||||
| [A-4](a4-node-loss.md) 노드 상실 | 거기서는 `up=0` 이 정확했다. **여기서는 `up=1` 인 채로 무너진다** |
|
||||
| 관측 보완 | **응답 시간 히스토그램**이 없다 |
|
||||
| 전부 | **주입이 걸렸는지 카운터로 먼저 확인한다.** 세 실험 연속으로 같은 교훈 |
|
||||
Reference in New Issue
Block a user