Files
document-haness/docs/keycloak-session-store/tech-log-studio/when-the-measurement-lies/case/case-seventy-six-failures-that-were-not-the-servers.md
T
DongHyeonkaandClaude Opus 5 2109f726fe feat(pipeline): keycloak-session-store 25편·virtualization 59편을 S3→S5→S6 으로 돌린다
기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만
있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문
종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다.

SSOT 결함 둘을 고쳤다.

- kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이
  「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽
  `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다.
- virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데
  원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다.

기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은
둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더
든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다).

계약을 셋 고쳤다.

- kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/**
  28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는
  것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온
  것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수
  대조했을 때 가장 가까운 커밋도 28개가 어긋났다).
- virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고
  재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다.
- kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고
  넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다.

style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져
(실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고,
engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고
있었다(Question 기록에서 11.94 → 3.86).

verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:01:55 +09:00

9.6 KiB

kind, slug, title, topic, topicName, project, status, lastVerifiedOn, sourceRevision, source, assets, evidence
kind slug title topic topicName project status lastVerifiedOn sourceRevision source assets evidence
CASE seventy-six-failures-that-were-not-the-servers 실패 76 건이 서버 탓이 아니었다 — 대조군이 오보를 막았다 when-the-measurement-lies 주입이 걸렸는지 무엇으로 아는가 keycloak-session-store 게시 전 2026-09-04 cdac9b8178391311d8eca1ebc6cac15bb62d79af
final/document.md#결정이-지켜지는지-확인하는-방법-대조군-없이는
key file
measurement-control ../../../final/assets/measurement-control/measurement-control.svg
../../../final/evidence/raw/d4-certificate-renewal__05-control-no-injection.txt
../../../final/evidence/raw/d4-certificate-renewal__11-inflight-full.txt

실패 76 건이 서버 탓이 아니었다 — 대조군이 오보를 막았다

nginx reload 중 감시 로그에 찍힌 실패 76건은 서버에 닿지 않은 요청이었다. 연결수가 0 이고 소요 시간이 50µs 였다. 같은 순간 대조 폴링 49건은 전부 200 이었고, 같은 조건을 100번 반복해도 재현되지 않았다. 주입 전에 대조군 900건을 잡아 두지 않았다면 「갱신 중 대규모 요청 실패」로 적었다.

관계

  • 예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다 그 기준의 세 번째 규칙을 이 확인이 실제로 써 본 결과다.
  • up 지표는 살아 있지만 쓸모없는 상태를 보지 못한다 이 실험대에서 한 지표만 보고 판정한 다른 경우이고, 방향이 반대다. 그쪽은 이상을 못 봤고 이쪽은 없는 이상을 봤다.
  • 두 시계에서 온 값을 빼지 않는다 같은 D-4 에서 나온 다른 측정 결함이다. 감시를 돌린 기계와 실험대의 시계가 106초 달랐다.

문제

인증서 갱신 중 nginx reload 가 진행 중이던 요청을 끊는지 확인해야 했다. 새 연결만 폴링하면 TLS 핸드셰이크가 매번 새로 일어나서 「새 연결을 받아주는가」만 재게 되므로, reload 순간에 실제로 전송 중인 요청을 따로 만들어 감시했다.

그 감시 로그에 실패가 76건 남았다. 이 값만 보면 reload 가 진행 중 요청을 대량으로 끊은 것으로 읽힌다.

결론

감시 로그의 실패 : 76건 같은 순간 대조 폴링 : 49건 전부 200 실패한 요청의 연결수 : 0 실패한 요청의 소요 시간 : 50µs 같은 조건 재현 : 0/100 주입 전 대조군 : 900건 전부 200, 오류 0 서버 쪽 원인으로 귀속 : x

TCP 연결 시도조차 없었고 소요 시간이 DNS 조회보다 짧았으므로 요청이 서버에 닿지 않았다. 같은 순간 다른 경로로 들어간 요청은 전부 200 을 받았다. 훅을 넣고 다시 검증한 D-4a 에서 reload 자체는 새 연결 8856건 전부 200 이었고, 전송 12초째에 reload 를 맞은 42초짜리 요청은 845361바이트를 온전히 받았다.

검증 환경

실험 : D-4 인증서 갱신 중 nginx reload 호스트 TLS 종단 : nginx, Let's Encrypt 인증서 주입 전 대조군 : 0.2초 간격 900회 = 180초 대조군 응답시간 : 최소 67 중앙 98 p95 195 최대 1121 평균 106.9 (ms) 진행 중 요청 : 845KB 번들을 20k/s 로 받아 한 요청을 42초 동안 유지 관측 지점 : 외부 curl · Prometheus 지표 · PostgreSQL 직접 조회 시계 : test-server 는 NTP 가 꺼져 있어 106초 빠르다

재현 조건

  1. 주입 전에 평시 오류율을 잰다. 0.2초 간격으로 900회 폴링하고 상태코드 분포와 응답시간, TLS 핸드셰이크 횟수를 기록한다.

  2. reload 순간에 전송 중인 요청이 있도록 만든다. 845KB 번들을 20k/s 로 내려받아 요청 하나를 42초 동안 유지한다.

  3. 새 연결 폴링과 진행 중 요청 감시를 각각 다른 프로세스로 띄우고 같은 시간대에 돌린다.

  4. 인증서를 강제 갱신하고 nginx 를 reload 한다.

  5. 실패가 나오면 그 순간의 대조 폴링 결과, 연결수, 소요 시간을 함께 읽는다.

  6. 같은 조건을 100회 반복해 재현되는지 본다.

본문

주입 전에 평시를 먼저 쟀다

D-4 의 물음은 nginx reload 중 진행 중이던 요청이 어떻게 되는가였다. 주입에 쓸 강제 갱신은 진짜 인증서를 발급하는 명령이라 되돌릴 수 없고 Let's Encrypt 의 발급 한도(주당 중복 인증서 5장)를 한 장 깎는다. 그래서 이 실험 전체에서 한 번만 쓰기로 했고, 그 한 번을 헛되게 쓰지 않으려면 잴 것을 주입 전에 전부 재 둬야 했다. 답하기 전에 주입 없는 상태의 오류율부터 잡은 것이 그래서다 — 0.2초 간격으로 900회, 180초 동안 폴링해서 900건 전부 200 이고 오류가 0 이라는 것을 확인했다. 중앙값은 98ms, p95 는 195ms 였다.

이 폴링에는 한계가 하나 있었다. TLS 핸드셰이크가 900회 전부 일어나서 매 요청이 새 연결이었고, 그래서 이 폴링이 재는 것은 「새 연결을 받아주는가」다. 계획서가 물은 것은 진행 중이던 요청이므로 reload 순간에 실제로 전송 중인 요청이 있어야 했다. 845KB 짜리 번들을 20k/s 로 일부러 느리게 받아 요청 하나를 42초 동안 유지하는 감시를 따로 띄웠다.

76건이 한 초에 몰려 찍혔다

감시 로그는 146줄이었고 그중 76줄이 코드=000 이었다. 나머지 70줄은 코드=200바이트=845361 이었다.

08:14:22 코드=200 바이트=845361 시간=42.338482 연결수=1
08:15:04 코드=000 바이트=0 시간=0.001148 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000051 연결수=0
08:15:04 코드=000 바이트=0 시간=0.000056 연결수=0
08:15:04 코드=200 바이트=845361 시간=42.236496 연결수=1

76줄의 시각이 전부 08:15:04 로 같고, 같은 초의 다음 줄은 다시 코드=200 에 42.236496초다. 42초씩 걸리던 요청이 한 초 안에 76번 끝났다. 실패가 즉시 돌아오니 다음 요청을 바로 띄우는 감시 루프가 그 1초 동안 폭주했다.

76건을 서버 탓으로 적지 않은 근거 네 가지

76건을 reload 탓으로 적기 전에 네 가지를 봤다.

근거
같은 순간 폴링 49건 전부 200
연결수 0 — TCP 연결 시도조차 못 했다
소요 시간 50µs — DNS 조회보다 짧다
재현 0/100

연결수가 0 이므로 서버까지 패킷이 가지 않았다. 소요 시간 50µs 는 이름 풀이 한 번보다 짧아서 요청이 네트워크로 나간 시간이 아니다. 같은 순간 다른 프로세스가 보낸 49건은 전부 200 을 받았으므로 그 시각에 서버는 요청을 처리하고 있었다. 같은 조건으로 100번 더 돌려도 한 번도 다시 나오지 않았다.

대조군이 없었다면 무엇이 됐나

주입 전 대조군과 대비될 때만 귀속이 성립하고, 대조군이 없으면 같은 관측이 오보로 이어지는 구성

같은 관측이 두 갈래로 갈린다. 평시 오류율을 아는 쪽에서는 76건을 대조군 900건과 견주어 서버 바깥 원인으로 돌릴 수 있고, 모르는 쪽에서는 「갱신 중 대규모 요청 실패」로 적게 된다.

이 실험대에서 대조군 규칙은 이미 두 번 어겨졌다. A-6 에서는 대조군이 −41% 인데도 「영향 없음」이라고 적었고, A-8 에서는 표본 9개로 무중단을 주장했다. 둘 다 나중에 고쳤다. 세 번째가 이 76건인데, 이번에는 대조군을 먼저 잡아 둔 덕분에 기록에 들어가기 전에 걸렸다.

같은 대조를 문서 전체에 돌렸다

기록을 다 쓴 뒤 본문의 주장과 증거 파일을 하나씩 맞춰 봤다. 어긋난 곳이 여섯 군데 나왔다.

어디 무엇이 어긋났나
C-1 본문은 「세션 0」인데 증거는 4
C-2 exit code 1 인 명령의 성공 읽기를 실었다
A-1 4초 전 파드 재시작이 만든 분단을 conntrack 공으로 돌렸다
A-2 첫 측정의 000000{"error":"HTTP 401"}401 을 숨겼다
A-3 wal_writer_delay 를 재지 않고 단언했다 (실측 200ms, 로그인율도 19/s 가 아니라 14/s)
D-1 본문은 RTO 30초, 자기 타임라인은 41초

전부 고치면서 무엇이 어긋났는지를 표로 남겼다. 76건과 이 여섯 건은 같은 방향으로 틀린다 — 숫자 하나를 그 숫자가 나온 조건과 떼어 놓고 읽으면 본문이 자기 증거와 다른 말을 하게 된다.

이 확인이 말하지 않는 것

76건이 서버 탓이 아니라는 것까지가 이 확인의 범위다. 감시를 돌린 쪽에서 무엇이 그 한 초 동안 76번 즉시 실패했는지는 특정하지 못했다. 감시를 -s 로 돌려 curl 의 오류 메시지를 버렸고 종료 코드도 남기지 않았기 때문이고, 재현이 0/100 이라 같은 조건을 다시 만들어 좁힐 수도 없었다. 장치가 왜 실패했는지를 남기지 않은 탓에, 이 76건을 서버 밖으로 돌린 근거는 같은 시각 대조 폴링이 멀쩡했다는 사실 하나였다.

그래서 감시 자체를 바꿨다. curl 의 종료 코드까지 함께 적고 실패하면 1초 쉬어 루프 폭주를 막는 형태로 갈아끼웠고, 같은 로그의 뒷부분에는 그 뒤로 curl종료=0 이 줄마다 붙어 있다. 다음에 같은 일이 생기면 6이 이름 풀이, 7이 연결, 35가 TLS 라는 식으로 종료 코드가 바로 답한다.