기반 가이드 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>
45 KiB
id, kind, slug, title, topic, topicName, project, status, studio, pinnedVersions, source, sourceRevision
| id | kind | slug | title | topic | topicName | project | status | studio | pinnedVersions | source | sourceRevision | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cf2e783c-424b-4167-aa27-3bd6b5f46ee2 | SETUP | reproduce-a6-latency-injection | flannel.1 에 200ms 를 넣고 커넥션 풀이 고갈되는 것을 본다 | losing-a-node-or-the-store | PostgreSQL 을 내리고 노드 전원을 뽑았을 때 | keycloak-session-store | 게시 전 | https://hyeonworks.com/studio/documents/cf2e783c-424b-4167-aa27-3bd6b5f46ee2/edit |
|
|
cdac9b8178391311d8eca1ebc6cac15bb62d79af |
flannel.1 에 200ms 를 넣고 커넥션 풀이 고갈되는 것을 본다
postgres 가 보내는 패킷 중 노드를 건너가는 것만 200밀리초 지연시켜 Keycloak 한 대의 JDBC 커넥션 풀이 마르는 것까지 따라가는 절차다. 주입 셋 중 둘은 일부러 실패시키고, 걸렸는지는 tc -s 카운터로만 판정한다. 전 구간 약 30분.
관계
- 200 밀리초를 넣었더니 응답이 22.2 초가 됐다 이 절차가 만드는 사건이고, 왜 200밀리초가 22초가 되는지는 그 기록이 결론으로 갖는다.
- 주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다 여기서 일부러 밟는 실패 둘이 그 아홉 건에 들어 있다.
- 산문으로 적힌 측정 장치를 실행 가능하게 고쳤더니 한 건이 깨졌다 스크립트가 찍은 「적용완료」를 커널의 답으로 읽은 것이 그 기록이 다루는 실패와 같은 종류다.
- 예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다
대조군을 같은 클러스터 안에 두고
tc -s로 주입을 먼저 판정하는 이 순서가 그 기준을 따른다.
본문
읽기 전에 — 어디서 치는가
kubectl 은 [lab host] 에서 친다. tc 는 노드 자체를 건드리는 명령이라 kc-lab-2 에서 치고, postgres 가 그 노드에 있다. 아래 명령들은 가이드가 실제로 친 한 줄 형태 그대로이므로 ssh kc-lab-2 '...' 가 붙어 있다. 코드블록마다 어느 셸인지 붙여 두었다.
원 가이드는 이 명령들을 kc-lab-1 에서 치라고 적었다. 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 sudo 없는 kubectl 이 permission denied 로 막힌다 — kubeconfig 는 lab host 의 ~/.kube/config 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 kubectl 블록의 기계 이름을 [lab host] 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
터미널은 둘을 연다. 하나는 부하와 측정, 하나는 이벤트 관찰이다.
| 무엇 | 값 |
|---|---|
| 네임스페이스 | keycloak-lab · 관측 스택은 observability |
| 주입 지점 | flannel.1(VXLAN 터널). eth0 은 없고 enp1s0 에는 파드 IP 가 안 보인다 |
| 주입 값 | netem delay 200ms 를 prio 의 3번 밴드에 |
| 고르는 기준 | u32 match ip src $PG/32 — postgres 가 보내는 패킷만 |
| 탐침 파드 | a6-probe — curlimages/curl:8.11.1, sleep 1800, --restart=Never |
| 부하 | 동시 20건. 결과는 파드 안 /tmp/load 에 모은다 |
| 걸리는 시간 | 전 구간 약 30분 |
이 실험이 가르는 것
A-2 는 DB 를 완전히 세웠고 A-4 는 기계를 통째로 껐다. 둘 다 즉시 드러났다 — 503 이 나오고 up 이 0 이 됐다. 실제 장애의 대부분은 느려지기만 하고, 느려짐은 헬스체크가 통과하므로 사망보다 진단하기 어렵다.
묻는 것은 하나다.
200밀리초를 넣으면 애플리케이션은 200밀리초 느려지는가?
답은 아니고, 두 군데에서 곱해진다. 끝나면 이것들을 자기 화면에서 본다 — eth0 이라는 인터페이스가 없다는 것은 ip -brief link 에서 보고, 스크립트가 「적용완료」를 찍었는데 아무것도 안 걸린 것은 tc -s qdisc 카운터에서 본다. enp1s0 에서 파드 IP(Internet Protocol 주소)가 안 보이는 것은 VXLAN(Virtual Extensible LAN, 가상 확장 랜) 캡슐화에서, 200ms 가 1,872ms 가 되는 것은 두 노드 응답 시간 비교에서 드러난다. 동시 20건이 22.2초까지 계단으로 늘어나는 것은 상주 탐침이 모은 파일에서, 커넥션 획득에 20초를 기다린 요청은 agroal_blocking_time_max_milliseconds 에서 나온다. readiness 프로브가 같은 줄에 서서 타임아웃되는 것은 kubectl get events 에 찍히고, 예측했던 낙관적 락 충돌이 0건인 것은 Keycloak 로그에서 확인한다.
전제와 되돌리기
05-keycloak과06-observability가 끝나 있다.- A-5 를 먼저 해 두면 좋다. 「주입을 넣은 것과 걸린 것은 다르다」가 여기서 세 번째로 나온다.
tc는kc-lab-2에서 친다(ssh kc-lab-2). postgres 가 그 노드에 있다.
:::warning
이 절차는 Keycloak 한 대를 느려지게 만든다. 파드가 재시작될 수 있고 readiness 가 빠진다. 실험대에서만 한다.
:::
1. 중단하는 방법을 먼저 읽어 둔다
목적 — 어느 단계에서든 flannel.1 에 건 것을 한 줄로 전부 걷어낼 수 있게 해 둔다.
지금 치는 명령이 아니다. 아래 한 줄은 중간에 그만둘 때 치는 것이고, 여기서는 어디 있는지만 봐 둔다. 아직 아무것도 걸지 않았으므로 지금 치면 지울 것이 없다.
ssh kc-lab-2 'sudo tc qdisc del dev flannel.1 root'
지금 쳐서 확인할 것은 걸린 것이 없다는 쪽이다.
ssh kc-lab-2 'sudo tc qdisc show dev flannel.1'
예상 결과 — qdisc noqueue 0: root refcnt 2 다. 이 값이 주입 전의 출발점이고, 걷어낸 뒤에 같은 줄이 다시 나오면 원상복구된 것이다. netem 이 지금 보이면 앞 실험이 안 걷고 끝낸 것이므로 위의 del 을 먼저 친다. 걸린 것이 없는 상태에서 del 을 쳤을 때의 출력은 이 실험대에 기록이 없다.
왜 필요한가 — 이 한 줄이 세 가지를 다 지운다. prio qdisc 와 그 아래 netem 과 filter 이고, root 를 지우면 자식이 함께 사라진다.
문제가 생기면 — tc 는 노드 자체를 건드리는 명령이라 게스트 셸이 필요하다. 따라 하는 사람은 ssh kc-lab-2 로 먼저 붙고 원격 셸에서 sudo tc ... 를 칠 수 있고, 그러면 한 줄에 SSH(Secure Shell, 원격 셸) 접속과 원격 셸의 인용이 겹치지 않는다. 이 두 단계 형태는 이 실험대에서 치지 않았다.
주입 전에 같은 명령으로 먼저 본다
배치를 먼저 확인해야 이 절차가 성립한다. 대조군이 같은 클러스터 안에 있는 설계이기 때문이다. 순서는 이렇다.
파드 배치 → 상주 탐침 → 단일 요청 → 20회 반복 → 커넥션 풀 지표
1. postgres 와 두 Keycloak 이 어느 노드에 있나
무엇을 확인하는가 — postgres 와 keycloak-0 이 같은 노드이고 keycloak-1 만 노드를 건너는지.
kubectl -n keycloak-lab get pods -o wide
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}')
PG=$(kubectl -n keycloak-lab get pod -l app=postgres -o jsonpath='{.items[0].status.podIP}')
echo "K0=$K0 K1=$K1 PG=$PG"
출력에서 답이 되는 것 — 세 파드의 NODE 열이다. 실측은 이렇다.
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-1 의 DB 접근만 느려지고 keycloak-0 은 그대로다.
kc-lab-2 kc-lab-1
┌──────────────────┐ ┌──────────────────┐
│ postgres │ │ keycloak-1 │
│ keycloak-0 │ │ │
│ └─ cni0 로 직행 │◀─ VXLAN ──▶│ └─ 오버레이 경유 │
└──────────────────┘ └──────────────────┘
지연 없음 여기만 느려진다
대조군이 같은 실험 안에 있으므로 파드를 두 개 더 띄울 필요도, 다른 시간대와 비교할 필요도 없다. 배치가 다르면 이 절차는 성립하지 않는다 — 두 Keycloak 이 모두 DB 와 다른 노드에 있으면 대조군이 없고, 모두 같은 노드에 있으면 시험군이 없다.
2. 상주 탐침을 띄운다
목적 — 같은 요청을 수십 번 반복할 수 있는 파드를 하나 띄우고, 비밀번호를 값으로 찍지 않고 넘긴다.
kubectl -n keycloak-lab run a6-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 1800
kubectl -n keycloak-lab wait --for=condition=Ready pod/a6-probe --timeout=120s
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자
왜 필요한가 — Keycloak 컨테이너에는 curl 도 wget 도 없다(exit 127). 비밀번호는 명령 치환으로 넘어가므로 화면에 안 나오고, 확인할 때도 길이만 본다. PW=0자 면 시크릿이 안 넘어간 것이고 그 상태로 재면 전부 401 을 재게 된다.
문제가 생기면 — 탐침의 K0 와 K1 은 만들 때 고정된다. Keycloak 파드가 재시작되면 IP 가 바뀌고 탐침의 값이 낡으므로, 그때는 탐침을 지우고 다시 만든다. 이걸 놓치면 「아무 데도 안 닿음」을 「지연」으로 읽는다. 그리고 부하를 kubectl run --rm -i 로 주면 안 된다 — 원 실행이 그렇게 했다가 동시 20건의 출력을 잃었다. 파드가 만들어지고 지워지는 사이에 stdout 을 붙잡는 경주가 되고, 20줄 중 일부만 도착하거나 아예 끊긴다.
3. 요청 하나를 읽는 형태로 먼저 친다
무엇을 확인하는가 — 시간이 연결에 드는지 첫 바이트까지 드는지.
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 다. 모양은 이렇고 숫자는 환경마다 다르다.
connect 0.001 ttfb 0.065 total 0.066
| 값 | 무엇의 시간인가 |
|---|---|
time_connect |
탐침에서 Keycloak 까지의 TCP 연결. 이 절차에서 거의 안 변한다 |
time_starttransfer |
첫 바이트까지 = Keycloak 이 DB 와 대화한 시간. 여기가 폭발한다 |
이 결과가 뜻하는 것 — 지연은 탐침과 Keycloak 사이가 아니라 Keycloak 과 DB 사이에 넣으므로 connect 는 그대로고 ttfb 만 는다. 주입 후에 이 두 값을 다시 보면 어디에 지연이 걸렸는지 한눈에 판정된다. 응답이 401 이나 400 이면 -o /dev/null 을 빼고 본문을 본다.
4. 20회를 반복해 원본을 파일에 모은다
무엇을 확인하는가 — 주입 전 두 노드의 응답 시간.
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'
kubectl -n keycloak-lab exec a6-probe -- cat /tmp/base-k1
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 로 바꿔 대조군도 똑같이 잰다. /tmp/base-k1 이라는 출력 파일 이름도 같이 바꾼다 — ① 은 그 경로에 덮어쓰므로, 변수만 바꾸고 다시 돌리면 keycloak-1 에서 잰 20회가 지워지고 keycloak-0 의 20회가 그 파일을 차지한다. 화면에는 아무 경고도 안 뜬다. 뒤에서 두 노드의 평균을 견줄 때 keycloak-1 쪽이 이미 없다.
출력에서 답이 되는 것 — 두 노드의 평균이다.
=== 기준선 지연 — 각 노드에서 로그인 20회 ===
keycloak-0 평균 70 ms
keycloak-1 평균 66 ms
이 결과가 뜻하는 것 — 두 값이 비슷하고 지금은 keycloak-1 이 오히려 4ms 빠르다. VXLAN 을 건너는 쪽이 더 빠를 수도 있는 수준의 차이이므로, 뒤에 나올 28배가 의심의 여지 없이 주입 탓이 된다. 평균만 보면 한 건이 튄 것을 놓치므로 ② 를 건너뛰지 않는다. 그리고 횟수를 주입 전후로 똑같이 맞춘다 — 이 측정은 20회로 쟀는데 해설 문서의 재현 절차에는 15회로 적혀 있고, 횟수가 다르면 평균도 달라진다.
5. 커넥션 풀 지표에 무엇이 있는지 미리 본다
무엇을 확인하는가 — agroal_* 지표의 이름과 지금 값.
kubectl -n keycloak-lab exec a6-probe -- sh -c \
'curl -s "http://$K1:9000/metrics" | grep "^agroal_"'
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
출력에서 답이 되는 것 — 열두 줄 가운데 뒤에서 쓰는 넷이다.
| 지표 | 무엇을 말하는가 |
|---|---|
blocking_time_max |
커넥션을 받으려고 가장 오래 기다린 시간 |
max_used_count |
풀이 최대 몇 개까지 늘었나 |
awaiting_count |
지금 줄 서 있는 요청 수 |
active_count |
지금 쓰이고 있는 커넥션 수 |
이 결과가 뜻하는 것 — agroal_* 이 JDBC 커넥션 풀 지표다(Agroal 은 Quarkus 의 풀 구현이다). awaiting_count 와 active_count 는 순간값이라 부하가 끝나면 0 으로 돌아가므로 부하 중에 읽어야 보이고, blocking_time_max 는 누적이라 나중에 읽어도 남는다. 위 목록은 알파벳순으로 destroy_count_total 에서 끊겨 있는데 원 실행이 앞부분만 남긴 것이고, 실제로는 뒤에 agroal_max_used_count 같은 것이 더 있다. 증거 파일이 짧다고 지표가 없는 것은 아니다.
주입
주입은 세 번이고 앞의 둘은 일부러 실패한다.
1. 시도 ① — eth0
목적 — 인터넷 예제가 전부 쓰는 이름을 그대로 써 보고 무엇이 나오는지 본다.
ssh kc-lab-2 'sudo tc qdisc add dev eth0 root handle 1: prio'
예상 결과
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 는 네 번 다 실패했는데 스크립트는 그대로 다음 절로 넘어갔고 문서에는 시각까지 찍혔다. 명령의 성공을 「에러가 안 보인다」로 판정하면 안 되고, 손으로 한 줄씩 치면 이 실수를 할 수 없다.
문제가 생기면 — 인터페이스 이름을 확인한다.
ssh kc-lab-2 'ip -brief link'
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>
게스트의 물리 NIC(Network Interface Card, 네트워크 카드) 이름은 enp1s0 이고 eth0 이 없다.
| 이름 | 무엇 |
|---|---|
enp1s0 |
게스트의 물리(가상) NIC(Network Interface Card, 네트워크 카드). 노드 간 실제 트래픽이 나가는 곳 |
flannel.1 |
VXLAN 터널. 노드를 건너는 파드 트래픽이 여기로 들어간다 |
cni0 |
노드 안 브리지. 같은 노드 파드끼리는 여기서 끝난다 |
Debian 클라우드 이미지는 예측 가능한 인터페이스 이름을 쓴다.
enp1s0
│ │ └─ s0 : slot 0
│ └──── p1 : PCI bus 1
└────── en : ethernet
이름이 하드웨어 위치에서 나오므로 NIC 순서가 바뀌어도 이름이 안 바뀌고, 그 대신 eth0 이라고 적힌 인터넷의 모든 예제가 안 돈다. flannel.1 의 상태가 UNKNOWN 인 것은 정상이다 — 터널 장치는 캐리어 개념이 없어서 UP 대신 UNKNOWN 으로 보고한다.
2. 시도 ② — enp1s0. 거는 명령이 없다
목적 — 이름만 고치면 되는지 확인한다.
이 절에는 주입 명령이 없다. enp1s0 로 tc 를 거는 줄은 원본 가이드에 없다(unknown) — 원 실행은 eth0 이 실패한 뒤 곧바로 flannel.1 로 갔다. 여기서 실제로 칠 수 있는 것은 아래 미검증 tcpdump 두 줄뿐이고, 왜 enp1s0 이 답이 아닌지는 구조에서 나온다. 두 줄을 건너뛰어도 3번으로 넘어가는 데 지장이 없다.
노드 간 파드 통신은 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 만 있다. 눈으로 확인하는 두 줄을 가이드가 미검증으로 표시했다.
ssh kc-lab-2 'sudo tcpdump -i enp1s0 -n -c 5 udp port 8472'
ssh kc-lab-2 'sudo tcpdump -i flannel.1 -n -c 5 host 10.42.1.76'
예상 결과 — 앞쪽에서는 노드 IP 사이의 UDP 8472 만 보이고 10.42.x.x 는 안 보인다. 뒤쪽 터널에서는 파드 IP 가 보인다.
| 인터페이스 | 파드 IP 가 보이나 | 무엇을 지연시키게 되나 |
|---|---|---|
cni0 |
보인다 | 같은 노드 안 통신만 |
flannel.1 |
보인다 (캡슐화 직전) | 노드를 건너는 파드 통신 |
enp1s0 |
안 보인다 | 노드 간 모든 것 (SSH·k3s 포함) |
왜 필요한가 — enp1s0 에 netem 을 root 로 걸면 kubectl 도 SSH 도 같이 느려져서 무엇이 원인인지 못 가린다.
문제가 생기면 — 원 실행에는 이 확인이 없다. eth0 실패 뒤 곧바로 flannel.1 로 갔으므로 「enp1s0 에 걸면 0 패킷」이라는 출력 원문은 이 실험에 없고, 구조에서 나온 결론이다.
3. 성공한 주입 — flannel.1 에 세 줄
목적 — postgres 가 보내는 패킷만 골라 200ms 지연시킨다.
2026-09-17 에 새 실험대에서 이 세 줄을 쳐서 같은 수를 받았다(observed). 넣기 전과 뒤를 같은 명령으로 쟀다.
넣기 전 200 0.067946s 200 0.064262s
넣은 뒤 200 1.905504s 200 1.903161s 200 1.860396s 200 1.867929s 200 1.872740s
되돌린 뒤 200 0.067775s 200 0.043802s 200 0.045763s
200ms 를 넣었는데 1.87초가 된다. 문서가 적은 1,872ms 와 28배가 그대로 나왔다. 그 사이 netem 카운터는 83 pkt 이었고, 되돌리자 qdisc noqueue 로 돌아가며 시간도 원래대로였다.
시도 ① 과 ② 의 관측도 다시 나왔다(observed).
sudo tc qdisc add dev eth0 root handle 1: prio → Cannot find device "eth0"
enp1s0 의 헤더 192.168.122.12.53654 > 192.168.122.11.8472: OTV … ← 노드 IP 뿐
flannel.1 의 헤더 10.42.0.14.52916 > 10.42.1.29.5432 … ← 파드 IP 가 보인다
문서가 미검증으로 둔 세 줄도 전부 돌았다(observed) — tcpdump 두 줄과 tc filter show. 필터 목록은 이렇게 나온다.
filter parent 1: protocol ip pref 3 u32 chain 0 fh 800::800 order 2048 key ht 800 bkt 0 *flowid 1:3 not_in_hw
match 0a2a011d/ffffffff at 12
0a2a011d 가 10.42.1.29 를 16진수로 적은 것이고, 그 값이 지금 postgres 의 IP 와 같은지가 필터가 제 대상을 고르고 있다는 증거다.
tcpdump 에는 시간 제한을 걸어 둔다. -c 5 는 패킷 다섯 개를 받아야 끝나므로, 그 조건에 맞는 트래픽이 없으면 명령이 안 끝난다. sudo timeout 12 tcpdump … 로 감싸면 조용한 구간에서도 프롬프트가 돌아온다.
한 줄씩 친다. 앞 줄이 실패하면 뒤 줄은 붙을 곳이 없어서 다른 에러를 낸다.
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 주입'
예상 결과 — 세 줄 다 아무것도 찍지 않는다. 걸렸는지는 다음 절의 카운터가 답한다.
왜 필요한가 — 세 줄이 나뉘어 있는 까닭은 tc 의 계층 구조다.
qdisc (큐 규율) 인터페이스에 붙는 패킷 스케줄러
├─ prio 우선순위 밴드 3개로 나눈다
│ ├─ 1:1 (기본)
│ ├─ 1:2 (기본)
│ └─ 1:3 ← 여기에 netem 을 붙인다
└─ filter 어떤 패킷을 어느 밴드로 보낼지
| 줄 | 하는 일 |
|---|---|
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번 밴드로 보낸다 |
netem 을 root 에 바로 붙이면 모든 트래픽이 느려진다. prio 와 filter 를 쓰면 고른 트래픽만 느려지고, 이 절차는 postgres 가 보내는 것만 골라야 하므로 세 단계가 필요하다.
문제가 생기면 — 지연이 양쪽 다 늘었으면 netem 을 root 에 직접 붙인 것이다. 중단 명령으로 걷어내고 세 줄을 다시 친다.
주입 검증
1. 시도 ① 은 값을 찍기만 하고 판정하지 않아 그냥 지나갔다
시도 ① 은 에러를 냈는데도 그대로 넘어갔고, 그 상태에서 잰 「검증」이 이랬다.
=== [검증] 지연이 실제로 걸렸는가 — 두 노드 비교 ===
keycloak-0 평균 43 ms 최대 64 ms
keycloak-1 평균 47 ms 최대 70 ms
두 노드가 여전히 같고, 그것이 「안 걸렸다」는 신호였다. 검증 절이 값을 찍기만 하고 판정하지 않으면 이렇게 지나간다.
2. 성공한 주입 뒤에는 카운터를 본다
ssh kc-lab-2 'sudo tc -s qdisc show dev flannel.1'
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 는 요청이 있어야 답하므로 트래픽을 한 번 만든다.
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"'
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 이므로 실제로 지연 밴드를 통과했다.
| 상태 | 뜻 | 할 일 |
|---|---|---|
부하 전 0 pkt |
아직 트래픽이 없다 | 요청을 한 번 보내고 다시 센다 |
부하 후에도 0 pkt |
필터가 아무것도 못 잡았다 | IP·인터페이스·방향을 다시 본다 |
pkt 이 는다 |
걸렸다 | 관찰로 넘어간다 |
dropped 가 는다 |
limit 1000 을 넘겼다 |
부하를 줄이거나 limit 을 올린다 |
A-1 과 A-5 에서 나온 것과 같은 교훈이 세 번째로 나왔다 — 주입을 넣은 것과 걸린 것은 다르다. 필터 자체를 보는 줄은 가이드가 미검증으로 표시했다.
ssh kc-lab-2 'sudo tc filter show dev flannel.1'
관찰
1. 단일 요청 — connect 는 그대로고 ttfb 만 폭발한다
주입 전에 친 것과 똑같은 명령을 다시 친다.
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"'
그다음 20회 반복으로 두 노드를 잰다.
=== 두 노드 지연 비교 (기준선: k0=70ms k1=66ms) ===
keycloak-0 평균 41 ms 최대 57 ms
keycloak-1 평균 1872 ms 최대 1887 ms
keycloak-1 이 66ms 에서 1,872ms 로 28배가 됐다. 대조군도 변했다 — keycloak-0 은 70ms 에서 41ms 로 41% 빨라졌다. 주입과 무관한 변동(JIT(Just-In-Time 컴파일) 워밍업, 캐시)이며, 해설 문서가 처음에 「영향 없음」이라고 쓴 것은 부정확했다. 자릿수가 달라 결론은 유지되지만 대조군이 안 변한다고 가정하면 안 된다.
왜 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 느려졌다」가 아니고, 쿼리 수를 줄이는 것이 지연 환경에서 결정적인 까닭이 여기 있다.
2. 동시 부하가 이 절차의 본 시험이다
순차로 20번 돌리면 큐잉이 재현되지 않는다. 백그라운드로 띄우고 wait 하며, 결과는 파드 안 파일에 모은다.
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'
kubectl -n keycloak-lab exec a6-probe -- cat /tmp/load > /tmp/load.txt
wc -l /tmp/load.txt
cat /tmp/load.txt
awk '{print $1}' /tmp/load.txt | sort | uniq -c
awk '{print $2}' /tmp/load.txt | sort -g
20 이 아니면 수집이 샌 것이고, 그 상태의 숫자는 해석하지 않는다.
=== 동시 부하 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" 다.
sort /tmp/load.txt # 사전순 — 10.5 가 3.4 앞에 온다
sort -g /tmp/load.txt # 수치순 — 이걸 써야 한다
원 가이드의 저 처방은 이 파일에서 안 듣는다. 각 줄이 200 1.911191 처럼 전부 같은 200 으로 시작해서, sort -g 는 줄 앞머리 수치를 비교하다 전부 동률이 되고 마지막에 바이트 순으로 떨어진다. sort 와 출력이 똑같다. 정렬할 열을 짚어야 듣는다.
sort -g -k2 /tmp/load.txt
위 ③ 의 awk '{print $2}' /tmp/load.txt | sort -g 도 제대로 돈다 — awk 가 둘째 열만 남기기 때문이다. 최대값 하나만 볼 때는 거기에 | tail -1 을 붙인다.
시간 값을 정렬할 때는 열을 짚는다. 이걸 놓치면 최대값을 잘못 읽고, 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 만 보는 감시는 이 장애를 못 본다.
3. 커넥션 풀 지표는 부하가 끝나자마자 읽는다
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)"'
=== 부하 직후 커넥션 풀 ===
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초를 보지 못한다.
이 관측 스택에는 히스토그램 지표가 없어서 그 사이의 분포는 안 나온다. 가이드가 남겨 둔 쿼리는 다음 한 줄이다.
# 있으면 좋았을 것
histogram_quantile(0.99, rate(http_server_requests_seconds_bucket[5m]))
4. 헬스체크가 같은 줄에 선다
kubectl -n keycloak-lab get events --sort-by=.lastTimestamp | tail -20
kubectl -n keycloak-lab get pods
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 프로브 자체가 타임아웃됐다. 헬스체크도 같은 커넥션 풀 줄에 서므로 연쇄가 이렇게 된다.
DB 가 느려진다
↓
요청이 커넥션을 오래 붙잡는다
↓
커넥션 풀이 고갈된다
↓
새 요청이 줄을 선다 (최대 20초)
↓
헬스체크도 줄에 선다 → 타임아웃 → NotReady
↓
그 노드가 로드밸런서에서 빠진다
↓
★ 남은 노드로 트래픽이 몰린다 → 그 노드도 같은 길을 간다
마지막 화살표에서 느려짐이 전파된다. A-2(DB 완전 정지)는 즉시 503 으로 드러나 오히려 명확했고, 느려짐은 살아 있는 노드를 하나씩 무너뜨린다.
해설 문서는 이 연쇄에서 구성 규칙 두 가지를 끌어냈다.
| 알게 된 것 | 구성에서 무엇을 정하나 |
|---|---|
| 커넥션 풀에서 한 번 더 곱해진다 | 풀 크기와 타임아웃이 장애 반경을 정한다 |
| 헬스체크도 줄에 선다 | 프로브 타임아웃이 풀 대기보다 짧아야 격리가 제때 된다 |
헬스체크 쪽이 이 절차에서 실제로 일어난 일이다.
5. 빗나간 예측도 하나 남았다
계획서에는 이렇게 적혀 있었다.
낙관적 락 충돌 증가 — 트랜잭션이 길어져
VERSION충돌이 늘어야 한다
지연 구간의 로그를 세는 줄을 가이드가 미검증으로 표시했다. 원 실행의 정확한 패턴이 기록에 없다.
kubectl -n keycloak-lab logs keycloak-1 --since=20m \
| grep -icE 'optimistic|StaleState|version.*conflict'
=== 낙관적 락 충돌이 늘었는가 — 지연 중 로그 ===
관련 로그 줄수: 0
하나도 없었고 까닭이 명확하다.
로그인 → 매번 새 세션 행을 INSERT → 다툴 상대가 없다
refresh → 같은 세션 행을 UPDATE → 여기서 다툰다
충돌은 같은 행을 동시에 고칠 때만 일어나므로 로그인 부하로는 재현되지 않는다. 예측이 빗나간 뒤에야 연산이 INSERT 라는 것이 보였고, 틀린 이유가 락 구현이 아니라 연산의 종류에 있었다. B-3(refresh 토큰 경쟁)의 영역이고, 거기서 지연을 함께 주면 충돌률이 올라갈 것이라고 가이드는 적는다. 예측을 적어 두지 않았다면 「충돌이 없네」 하고 넘어갔다.
복구와 원상복구 확인표
1. 지연을 걷어낸다
목적 — flannel.1 의 root qdisc 를 지워 prio 와 netem 과 filter 를 한꺼번에 없앤다.
date '+%H:%M:%S 해제'
ssh kc-lab-2 'sudo tc qdisc del dev flannel.1 root'
ssh kc-lab-2 'sudo tc qdisc show dev flannel.1'
예상 결과
=== 지연 해제 ===
해제완료
qdisc noqueue 0: root refcnt 2
noqueue 이므로 prio 도 netem 도 없다.
왜 필요한가 — root 를 지우면 그 아래 자식 qdisc 와 filter 가 같이 사라진다. 하나씩 지우면 filter 를 빠뜨리기 쉽다.
문제가 생기면 — 시도 ① 이 enp1s0 에 무언가 남겼을 수 있으므로 그쪽도 본다.
2. 회복을 같은 명령으로 확인한다
목적 — 주입 전과 같은 20회 반복 측정을 다시 쳐서 자릿수가 돌아왔는지 본다.
회복은 20회 반복 측정 명령을 그대로 다시 쳐서 본다. 그 명령의 첫 줄이 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 를 흔들었을 뿐 파드를 죽이지는 않았고, 커넥션 풀도 스스로 정상화됐다.
왜 필요한가 — agroal_blocking_time_max_milliseconds 는 누적이라 20000 인 채로 남는다. 파드를 재시작해야 0 이 되는데 그대로 두는 편이 낫다 — 이 노드가 한 번 20초를 기다린 적이 있다는 기록이다.
문제가 생기면 — 탐침 파드를 지운다.
kubectl -n keycloak-lab delete pod a6-probe --ignore-not-found
| 항목 | 명령 | 돌아왔을 때 |
|---|---|---|
| qdisc | ssh kc-lab-2 'sudo tc qdisc show dev flannel.1' |
noqueue |
| (물리 쪽도) | ssh kc-lab-2 'sudo tc qdisc show dev enp1s0' |
시도 ① 잔재가 없어야 한다 |
| 응답 시간 | 20회 반복 측정 | 주입 전과 같은 자릿수 |
| 파드 | kubectl -n keycloak-lab get pods |
둘 다 1/1 Running |
| Service | kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak |
ready 주소 둘 |
| 풀 | agroal_awaiting_count · agroal_active_count |
0 |
| 탐침 파드 | 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 |
막히면
이 표는 전부 이 실험대가 실제로 겪은 증상이고 지어낸 것은 없다.
| 증상 | 원인 | 확인 |
|---|---|---|
Cannot find device "eth0" |
이 게스트의 NIC 는 enp1s0 이다 |
ip -brief link |
| 스크립트가 「적용완료」인데 지연이 없다 | 성공 메시지는 스크립트가 찍은 것 | tc -s qdisc 카운터 |
enp1s0 에 걸었는데 안 걸린다 |
VXLAN 안에 파드 IP 가 숨어 있다 | flannel.1 에 건다 |
Sent 0 pkt |
부하 전이면 정상. 부하 후면 필터가 틀렸다 | 요청 한 번 보내고 다시 센다 |
| 지연이 양쪽 다 늘었다 | netem 을 root 에 직접 붙였다 |
prio + filter 로 골라 낸다 |
kubectl 이나 SSH 까지 느려졌다 |
enp1s0 에 걸었다 |
tc qdisc del dev enp1s0 root |
| 20줄 중 몇 줄만 온다 | kubectl run --rm -i 로 동시 실행하면 stdout 이 샌다 |
상주 파드 + 파일 |
최대값이 9.1 로 보인다 |
sort 가 사전순이고, sort -g 만 붙여도 줄이 전부 200 으로 시작해 안 듣는다 |
sort -g -k2, 또는 ③ 의 awk 형태 |
blocking_time 이 0 이다 |
부하가 끝나고 한참 뒤에 읽었다 | 부하 직후에 읽는다 |
awaiting_count 가 늘 0 이다 |
순간값이다 | 부하가 도는 중에 읽는다 |
로그인이 전부 401 |
PW 가 안 넘어갔다 |
exec a6-probe -- sh -c 'echo ${#PW}' |
| 갑자기 아무 데도 안 닿는다 | 파드 IP 가 바뀌었다 | 탐침을 지우고 다시 만든다 |
| 이벤트가 과거 것과 섞인다 | 이벤트는 한 시간 전 것도 남는다 | Age 를 먼저 본다 |
| 대조군도 값이 변했다 | 정상이다. JIT 와 캐시 변동 | ���릿수로 판정한다 |
dropped 가 늘어난다 |
netem 의 limit 1000 을 넘겼다 |
부하를 줄이거나 limit 을 올린다 |
무엇이 관측이고 무엇이 아닌가
- (observed) 파드와 postgres 의 노드 배치, 주입 전 평균
70 ms와66 ms,Cannot find device "eth0"네 줄 사이에 낀적용완료와 주입 시각13:14:55, 그 상태의 「검증」 값43 ms와47 ms,ip -brief link의flannel.1과cni0, 넣은 직후의Sent 0 bytes 0 pkt와 부하 뒤의Sent 18388 bytes 150 pkt, 주입 뒤41 ms와1872 ms, 동시 20건의 스무 줄 전부와22.230871까지의 계단,blocking_time_max 20000.0과max_used_count 19.0과acquire_count_total 672.0과blocking_time_average 281.0, 이벤트 세 줄과89s·32m·52m, 낙관적 락 로그0, 해제 뒤noqueue와43 ms와51 ms,agroal_*지표 이름 열두 개. - (unknown)
enp1s0과flannel.1에 각각 거는tcpdump두 줄,tc filter show, 낙관적 락 로그를 세는grep -icE줄. 가이드가 셋 다 미검증으로 표시했다.ssh kc-lab-2로 들어가 원격 셸에서tc를 치는 두 단계 형태도 이 실험대에서 치지 않았다. - 구조에서 나온 결론이고 출력이 없는 것 — 「
enp1s0에 걸면 0 패킷」. 원 실행은eth0실패 뒤 곧바로flannel.1로 갔다. - 센 것이고 잰 것이 아닌 것 — 왕복
9는 A-0 이 잡은 SQL 목록을 센 값이고 패킷을 추적한 값이 아니다.200 ms × 9 ≈ 1,800 ms와 실측1,872 ms의 자릿수가 맞는다는 것까지가 이 계산의 범위다. - 증거 파일이 잘려 있는 것 —
agroal_*목록이 알파벳순으로destroy_count_total에서 끊겨 있다. 뒤에 쓰는agroal_max_used_count는 그 목록에 안 보이지만 부하 뒤 출력에는 있다. - 처음 쓴 것이 부정확했던 곳 — 해설 문서의 「대조군 영향 없음」. 대조군은
70 ms에서41 ms로 41% 빨라졌다. - 이 절차가 재지 않은 것 — 응답 시간 분포. 관측 스택에 히스토그램 지표가 없어 평균
281ms와 최대20,000ms사이에 무엇이 있었는지는 모른다. 낙관적 락 충돌도 로그인 부하로는 재현되지 않아 B-3 으로 넘겼다.