Files
document-haness/docs/keycloak-session-store/tech-log-studio/losing-a-node-or-the-store/setup/setup-reproduce-a6-latency-injection.md
T
DongHyeonkaandClaude Opus 5 024362d096 fix(setup): 실험대를 새로 세워 setup 35편을 밟고 어긋난 명령과 결과를 고친다
기반 가이드 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>
2026-09-17 15:59:42 +09:00

45 KiB
Raw Blame History

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
name version
Keycloak 26.7.0
name version
curlimages/curl 8.11.1
final/document.md#a층-재현-절차-열-편을-직접-치는-순서-a-6
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 없는 kubectlpermission denied 로 막힌다 — kubeconfig 는 lab host 의 ~/.kube/config 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 kubectl 블록의 기계 이름을 [lab host] 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.

터미널은 둘을 연다. 하나는 부하와 측정, 하나는 이벤트 관찰이다.

무엇
네임스페이스 keycloak-lab · 관측 스택은 observability
주입 지점 flannel.1(VXLAN 터널). eth0 은 없고 enp1s0 에는 파드 IP 가 안 보인다
주입 값 netem delay 200msprio 의 3번 밴드에
고르는 기준 u32 match ip src $PG/32 — postgres 가 보내는 패킷만
탐침 파드 a6-probecurlimages/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-keycloak06-observability 가 끝나 있다.
  • A-5 를 먼저 해 두면 좋다. 「주입을 넣은 것과 걸린 것은 다르다」가 여기서 세 번째로 나온다.
  • tckc-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 컨테이너에는 curlwget 도 없다(exit 127). 비밀번호는 명령 치환으로 넘어가므로 화면에 안 나오고, 확인할 때도 길이만 본다. PW=0자 면 시크릿이 안 넘어간 것이고 그 상태로 재면 전부 401 을 재게 된다.

문제가 생기면 — 탐침의 K0K1 은 만들 때 고정된다. 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"'

출력에서 답이 되는 것connectttfb 다. 모양은 이렇고 숫자는 환경마다 다르다.

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_countactive_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. 거는 명령이 없다

목적 — 이름만 고치면 되는지 확인한다.

이 절에는 주입 명령이 없다. enp1s0tc 를 거는 줄은 원본 가이드에 없다(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 포함)

왜 필요한가enp1s0netem 을 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

0a2a011d10.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 에 바로 붙이면 모든 트래픽이 느려진다. priofilter 를 쓰면 고른 트래픽만 느려지고, 이 절차는 postgres 가 보내는 것만 골라야 하므로 세 단계가 필요하다.

문제가 생기면 — 지연이 양쪽 다 늘었으면 netemroot 에 직접 붙인 것이다. 중단 명령으로 걷어내고 세 줄을 다시 친다.

주입 검증

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.53.4 보다 앞에 있는데 원 실행이 sort 를 사전순으로 썼기 때문이다(맨 앞의 1uniq -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 짜리 줄이 지금 주입의 결과이고 32m52m 짜리는 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.1root qdisc 를 지워 prionetem 과 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 이므로 prionetem 도 없다.

왜 필요한가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 부하 전이면 정상. 부하 후면 필터가 틀렸다 요청 한 번 보내고 다시 센다
지연이 양쪽 다 늘었다 netemroot 에 직접 붙였다 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 가 늘어난다 netemlimit 1000 을 넘겼다 부하를 줄이거나 limit 을 올린다

무엇이 관측이고 무엇이 아닌가

  • (observed) 파드와 postgres 의 노드 배치, 주입 전 평균 70 ms66 ms, Cannot find device "eth0" 네 줄 사이에 낀 적용완료 와 주입 시각 13:14:55, 그 상태의 「검증」 값 43 ms47 ms, ip -brief linkflannel.1cni0, 넣은 직후의 Sent 0 bytes 0 pkt 와 부하 뒤의 Sent 18388 bytes 150 pkt, 주입 뒤 41 ms1872 ms, 동시 20건의 스무 줄 전부와 22.230871 까지의 계단, blocking_time_max 20000.0max_used_count 19.0acquire_count_total 672.0blocking_time_average 281.0, 이벤트 세 줄과 89s·32m·52m, 낙관적 락 로그 0, 해제 뒤 noqueue43 ms51 ms, agroal_* 지표 이름 열두 개.
  • (unknown) enp1s0flannel.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 으로 넘겼다.