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>
This commit is contained in:
DongHyeonka
2026-09-17 11:01:55 +09:00
co-authored by Claude Opus 5
parent d473609e0a
commit 2109f726fe
574 changed files with 159654 additions and 1551 deletions
@@ -0,0 +1,103 @@
---
kind: REFERENCE
slug: never-subtract-values-from-two-clocks
title: 두 시계에서 온 값을 빼지 않는다
topic: when-the-measurement-lies
topicName: 주입이 걸렸는지 무엇으로 아는가
project: keycloak-session-store
status: 게시 전
source:
- final/document.md#결정이-지켜지는지-확인하는-방법-두-시계
sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
evidence:
- ../../../final/evidence/raw/d4a-deploy-hook__01-hook-verified.txt
- ../../../final/evidence/raw/followup__03-b4-role-propagation.txt
---
# 두 시계에서 온 값을 빼지 않는다
서로 다른 기계에서 온 타임스탬프는 빼기 전에 두 시계가 같은지 확인한다. D-4a 에서 test-server 는 dev 머신보다 106초 빨랐다. 보정하지 않고 계산한 D-4 의 공백은 106초 짧았고(2199 → 2305초), 1~2초를 재는 D-4a 에서는 그냥 뺀 값이 107초로 나와 참값보다 약 106초 어긋났다.
## 관계
- **실패 76 건이 서버 탓이 아니었다 — 대조군이 오보를 막았다**
같은 D-4 의 값이다. 그 실험이 적어 둔 공백 2199초가 이 보정으로 2305초가 됐다.
- **산문으로 적힌 측정 장치를 실행 가능하게 고쳤더니 한 건이 깨졌다**
측정값이 아니라 재는 쪽이 틀린 다른 경우다. 거기서는 적어 둔 명령이 실행되지 않았고, 여기서는 실행된 명령이 남긴 시각이 어긋나 있었다.
## 목적
보정하지 않고 뺀 값이 자릿수만 어긋난다고 읽는 것을 막는다.
D-4a 에서 갱신부터 서빙까지 1~2초를 재려다 걸렸다. test-server 는 NTP 동기가 꺼져 있고 dev 머신보다 106초 빠르다. 그 사실을 적지 않고 뺀 D-4 의 공백은 2199초였는데 보정하면 2305초, 38분 25초다. 틀린 값이 106초 짧게 나왔다.
같은 왜곡이 D-4a 에서는 결과를 통째로 바꾼다. 그냥 빼면 훅이 발급보다 107초 뒤로 보이는데 참값은 1~2초라 약 106초가 어긋나고, 보정을 반대쪽에 걸면 음수가 나와 훅이 갱신보다 먼저 돈 것이 된다. 자릿수만이 아니라 두 사건의 선후까지 뒤집힌다.
## 규칙
### 1. 빼기 전에 두 값이 같은 시계에서 왔는지 확인한다
D-4 의 2199초는 archive/cert2.pem 의 mtime 과 일련번호 관측 시각을 그대로 뺀 값이다. 앞은 test-server 시계이고 뒤는 dev 시계다. 양쪽 다 UTC 로 적혀 있어 뺄 때는 같은 자에서 읽은 것처럼 보인다. 갈리는 단위는 시간대가 아니라 기계다.
차를 셸이 대신 빼게 하지 않는다. 두 값을 각각 찍어 놓고 눈으로 빼면 어느 값이 어느 기계에서 왔는지가 출력에 남지만, 명령 안에서 빼 버리면 결과 한 줄만 남고 그 정보가 사라진다.
D-4 의 가이드에는 이 편에만 있는 시각 표기 규약이 하나 있다. 시각마다 어느 시계인지를 붙여 `08:58:52 (dev)``17:22:13 KST (ts)` 로 적고, 보정한 값은 `08:20:27 (실제)` 로 적는다. 두 기계의 시계를 섞어 빼는 바람에 숫자를 한 번 틀린 뒤에 붙은 규약이다.
### 2. 어느 시계가 밀렸는지는 제3의 기준으로 가른다
dev 머신은 Google 및 Let's Encrypt ACME 응답과 0초 차였고, test-server 는 Google 기준으로 -105초였다. 두 기계만 맞대면 차이만 나오고 어느 쪽이 맞는지는 안 나온다. test-server 의 timedatectl 은 NTP=no · NTPSynchronized=no 였다.
### 3. 왜곡값은 여러 번 재서 흔들리는지 본다
ssh 왕복으로 잰 왜곡은 3회 모두 +106.1초였다. 흔들리면 네트워크 지연이 섞인 것이고 안정적이면 왜곡이다. 이 실험대가 쓴 106초는 이 106.1초를 반올림한 값이다. Google 비교 쪽의 -105초와는 1초 넘게 갈렸다 — 둘 다 test-server 가 앞섰다는 것은 같지만, 보정에 넣은 것은 흔들림을 확인한 ssh 왕복 쪽이다.
### 4. 왜곡은 주입하기 전에 재 둔다
D-4 와 D-4a 의 재현 절차는 인증서를 강제 갱신하기 전에 시계부터 잰다. 주입이 끝난 뒤에는 그때 그 시계가 얼마나 어긋나 있었는지를 되짚을 방법이 없기 때문이다. D-4 는 시계를 재지 않고 2199초를 적었고, 나중에 D-4a 가 시계를 재면서 그 값이 2305초로 정정됐다.
강제 갱신은 진짜 인증서를 발급해 되돌릴 수 없으므로 이 실험 전체에서 한 번만 쓴다. 그 한 번을 헛되이 쓰지 않으려고 주입 전에 잴 것을 여덟 칸으로 적어 두었고, 시계는 그중 일곱 번째다.
관측도 한 기계에서만 한다. 이 실험은 전부 dev 머신에서 관측했고, 호스트에서만 알 수 있는 파일 mtime 과 훅 로그만 보정해서 썼다. 밖에서 본 것이 이 실험의 답이고, dev 가 이 실험대에서 유일하게 정확한 시계였기 때문이다.
### 5. 보정한 결과는 독립된 시계로 교차검증한다
새 인증서의 SCT 는 CT 로그가 자기 시계로 서명한 시각이라 test-server 것도 dev 머신 것도 아니다. 106초를 뺀 타임라인에서 훅의 nginx -t 는 발급 1초 뒤에 놓였다. 보정이 틀린 방향이었으면 훅이 발급보다 앞에 왔을 것이다.
다만 훅 시각은 초 단위로만 남은 로그 값이고 SCT 에는 밀리초가 붙어 있다. 이 실험대가 적은 「정확히 1초 앞」은 그 반올림을 거친 표현이라 차가 1.000초라는 뜻은 아니다.
인증서에 적힌 notBefore 는 발급 시각을 재는 기준으로 쓸 수 없다. Let's Encrypt 가 notBefore 를 정확히 한 시간 백데이트하므로 그대로 발급 시각으로 읽으면 한 시간을 잃고, 한 시간을 더한 값도 발급 시각이 아니다. 이 실험대의 두 인증서에서 SCT 는 그 값보다 약 89초 앞섰다.
### 6. 같은 왜곡이라도 다른 실험에서 잰 값을 섞어 쓰지 않는다
B-4 는 타임스탬프 둘의 차로 약 107초를 어림했고, D-4a 는 ACME 응답을 제3의 기준으로 두고 106초를 다시 쟀다. 둘 다 test-server 가 앞선 양이다. 보정에 쓸 값은 D-4a 의 106초이고, 107초는 B-4 의 12회를 읽기 위한 어림이다.
### 7. 음수 지연이 나오면 계산이 아니라 시계를 의심한다
D-4 와 D-4a 의 「막히면」 표는 공백이나 지연이 음수로 나오는 것을 시계 재는 절차로 돌아갈 신호로 적어 두었다. 선후가 뒤집힌 결과는 자릿수만 어긋난 값과 달리 바로 걸린다.
## 적용 조건
- 서로 다른 기계에서 온 타임스탬프의 차이를 잴 때
- 로그 시각과 외부 서비스가 찍은 시각을 견줄 때. D-4a 가 견준 것은 훅 로그와 CT 로그의 SCT 다
- NTP 동기를 확인하지 않은 호스트가 측정에 끼어 있을 때. test-server 가 그랬다
- 앞선 실험이 적어 둔 구간 값을 그대로 인용할 때. D-4 의 2199초가 D-4a 때문에 정정됐다
## 예외
- 같은 기계의 단조 시계로만 잰 구간에는 걸리지 않는다
- 재는 구간이 왜곡보다 훨씬 길면 값은 틀려도 결론은 남는다. D-4 에서는 오차가 106초여서 결론이 안 바뀌었고, 1~2초를 재는 D-4a 에서는 결과가 완전히 뒤집혔다
- 근거는 D-4·D-4a 한 쌍이다. B-4 에서도 같은 두 기계가 약 107초 어긋나 클레임 반영 시각이 변경 시각보다 앞서 보였는데, 그쪽은 어림으로 재고 넘어갔다. 이 실험대 밖의 측정으로 넓혀 확인한 적은 없다
## 예시
- test-server : NTP=no · NTPSynchronized=no · dev 머신보다 106초 빠름
- dev 머신 → Google : 0초 차. dev 머신 → Let's Encrypt ACME : 0초 차
- ssh 왕복 왜곡 3회 : +106.1 / +106.1 / +106.1초. 안정적
- test-server → Google : -105초. 같은 왜곡을 다른 방법으로 잰 값이다
- 보정식 : 실제 시각 = test-server 시계 106초. dev 시계는 보정하지 않는다
- D-4 의 공백 : 보정 전 2199초, 보정 후 2305초 = 38분 25초
- 보정하지 않은 D-4a : 그냥 빼면 107초, 참값 1~2초보다 약 106초 어긋난다. 보정을 반대쪽에 걸면 음수가 된다
- 보정한 D-4a 타임라인 : 발급 다음 초에 훅 nginx -t 와 새 워커 37252 기동, 그다음 초에 훅 nginx -s reload. 발급에서 서빙까지 1~2초
- SCT 는 밀리초까지 찍히고 훅 로그는 초 단위다. 「정확히 1초 앞」은 그 차이를 반올림한 표현이다
- D-4a 의 새 인증서 : notBefore 는 `Sep 4 11:29:18`, 한 시간을 더하면 `12:29:18`, SCT 는 `12:27:49.05` 다. 약 88.9초 앞선다
- B-4 의 약 107초 : 타임스탬프 둘의 차로 어림한 별개 값이다
@@ -0,0 +1,94 @@
---
kind: REFERENCE
slug: verify-the-injection-landed-separately-from-the-result
title: 예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다
topic: when-the-measurement-lies
topicName: 주입이 걸렸는지 무엇으로 아는가
project: keycloak-session-store
status: 게시 전
sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
source:
- final/document.md#결국-지키려던-것은-무엇이었나
- final/document.md#문제를-어렵게-만든-제약-주입이-먹지-않는다
- final/document.md#결정이-지켜지는지-확인하는-방법-대조군-없이는
evidence:
- ../../../final/evidence/raw/a1-jgroups-transport-block__07-cluster-size.txt
- ../../../final/evidence/raw/a6-latency-injection__03-flannel-injection.txt
- ../../../final/evidence/raw/a8-rolling-restart__01-restart-availability.txt
- ../../../final/evidence/raw/d4-certificate-renewal__05-control-no-injection.txt
- ../../../final/evidence/raw/d4-certificate-renewal__08-inflight-artifact.txt
---
# 예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다
상태를 일부러 망가뜨리는 실험은 예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다. 이 실험대가 위반 사례로 적은 둘은 전부 대조군 규칙이다 — A-6 의 −41% 대조군과 A-8 의 표본 9개다.
## 관계
- **주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다**
두 번째 규칙이 이 아홉 건에서 나왔다. 아홉 건은 규칙을 세우게 만든 실패이고, 규칙을 세운 뒤의 위반 사례가 아니다.
- **실패 76 건이 서버 탓이 아니었다 — 대조군이 오보를 막았다**
세 번째 규칙을 지켜서 오보를 막은 가장 최근 실험이다. 대조군과 같은 순간의 폴링이 없었으면 측정 장치의 아티팩트가 서버 장애로 기록됐다.
- **up 지표는 살아 있지만 쓸모없는 상태를 보지 못한다**
A-2 에서 장애가 실제로 일어나는 동안 이 지표는 1 이었다. 주입이 걸린 것을 확인해도 관측 도구가 그 상태를 보여 주지 않는 경우가 남는다.
## 목적
주입 실험의 결과는 세 자리에서 틀릴 수 있다. 첫째, 무엇을 예상했는지를 결과를 본 뒤에 적으면 예측과 결과의 차이가 사라진다. 둘째, 주입이 걸리지 않으면 관측에 아무 변화도 나타나지 않아 영향이 없는 경우와 구별되지 않는다. 셋째, 평시 값을 모르면 변화가 주입 탓인지 다른 이유인지 가를 수 없다. 뒤의 두 자리에서 실제로 문제가 났다 — 주입은 아홉 번 조용히 실패했고, 대조군 규칙은 두 번 어겼다. 첫째 자리를 어긴 사례는 이 실험대의 기록에 없다.
그 기록은 세 번째가 가장 자주 어겨졌고 치른 값도 가장 컸다고 적는데, 그 문장은 세 규칙의 위반 횟수를 견준 수치가 아니라 기록을 쓴 사람의 판단이다. 숫자로는 세 번째 규칙을 어긴 사례 둘만 남아 있다.
## 규칙
### 1. 예측을 먼저 적는다
주입하기 전에 무엇이 일어날 것으로 보는지 적어 둔다. 결과를 보고 나면 무엇을 예상했는지 정직하게 쓸 수 없다.
이 실험대가 그 예측을 적어 둔 곳은 A-0 의 「9. 다음 실험에 대한 예측」이다. A-0 이 평시를 재고 난 직후, 아직 아무것도 주입하기 전에 쓴 표이고, 뒤따르는 절들의 「맞다 / 틀렸다」는 전부 그 표와의 대조다. 표 앞에는 「예측이 빗나가면 그것이야말로 배울 거리다」라고 적혀 있다.
표에 적힌 실험 번호는 옛 로드맵의 것이라 지금 번호와 어긋난다. 당시의 `B-5` 가 최종 B-3 이고 `A-3 노드 상실` 이 최종 A-4 인데, 번호를 고쳐 적지 않고 원문 그대로 두었다 — 예측을 언제 썼는지가 번호에 남아 있기 때문이다.
틀린 예측은 지우지 않고 남긴다. 이 실험대에서 빗나간 예측은 다섯이다. B-4 에서는 nginx 가 동명 헤더를 덮어쓸 것으로 봤지만 덮어쓰지 않았고, A-7 에서는 refresh 의 500 이 REVOKED_TOKEN 때문이라고 봤지만 CLIENT_SCOPE_CLIENT 였다. 그중 A-1 의 예측이 틀리지 않았다면 A-0 의 인과 설명이 잘못된 채로 남았을 것이다.
A-2 의 up 이 1 이던 것은 그 다섯에 들어가지 않는다. 원본 가이드가 그 줄의 예측 칸을 비워 두고 「관측의 함정」이라고 적었으므로, 미리 적어 둔 예측이 빗나간 것이 아니라 예측한 적 없이 튀어나온 관측이다.
이 규칙을 어긴 사례는 이 실험대의 기록에 적혀 있지 않다.
### 2. 주입이 걸렸는지를 결과와 따로 확인한다
「아무 일도 없었다」는 「영향이 없다」와 구별되지 않는다. 주입 뒤에는 대상이 실제로 그 상태인지를 확인하는 단계를 하나 더 둔다. 보는 값은 결과가 아니라 상태다 — cluster_size, 워커 PID(프로세스 식별 번호), conntrack 표, 패킷 카운터.
주입 명령이 오류 없이 끝난 것은 걸렸다는 뜻이 아니다. NetworkPolicy 로 7800 을 막았을 때 ESTABLISHED 연결은 conntrack 때문에 규칙 평가를 건너뛰었고, cluster_size 는 25분간 2 로 남았다. 명령 자체는 정상 종료했다.
실패한 주입이 깨끗한 결과를 내기도 한다. A-3 은 DB 를 세 번 죽여 두 번 실패했는데, 그 두 번이 모두 「유실 0건」을 냈다. 무엇을 보면 걸린 것인지 신호를 미리 정해 두지 않았다면 첫 번째 결과를 그대로 답으로 적었을 것이고 결론은 정반대가 됐을 것이다.
재현 가이드 26편이 이 규칙을 절 구조로 갖고 있다. 편마다 주입 전에 평시를 잡는 절이 먼저 오고, 주입 다음에 「주입 검증」 절이 따로 서고, 그 뒤에 관찰과 복구가 온다.
이 규칙의 근거인 아홉 번의 실패는 규칙을 세우기 전에 일어났다. 규칙을 세운 뒤에 이 규칙을 어긴 사례는 따로 적혀 있지 않다.
### 3. 대조군 없이 귀속하지 않는다
평시를 모르면 이상을 해석할 수 없다. 주입 중에 비200 이 한 번 나왔을 때 평시 오류율을 모르면 그것이 주입 탓인지 알 수 없으므로, D-4 에서는 주입 전에 900건을 재서 오류가 0 이라는 것부터 확인했다.
대조군이 변하지 않는다고 가정하지 않는다. A-6 에서는 지연을 주입하지 않은 노드 쪽이 오히려 빨라졌다.
이 실험대가 어긴 사례로 적은 둘이 전부 이 규칙에 걸려 있다. D-4 의 in-flight 76건은 반대로 읽어야 한다 — 거기서는 대조군이 있어서 오보를 막았다.
## 적용 조건
- 상태를 일부러 망가뜨리고 그 영향을 재는 실험 전부에 걸린다.
- 이 실험대가 적용 범위로 말하는 곳은 아홉 번의 실패 이후 이 실험대에서 돌린 실험까지다. 다른 실험실이나 다른 프로젝트까지 넓힌 근거는 이 실험대의 기록에 없다.
- 주입 명령이 오류 없이 끝났을 때도 두 번째 규칙이 걸린다. 종료 코드는 대상의 상태를 말해 주지 않는다.
- 대조군은 주입한 뒤가 아니라 주입 전에 잰다. 주입 뒤에 잰 값에는 이미 주입의 영향이 섞여 있다.
## 예외
- 주입 없이 평시를 관측하는 측정에는 첫 두 규칙이 걸리지 않는다. 망가뜨릴 대상이 없으므로 예측과 주입 확인이 성립하지 않는다.
- 대조군 규칙은 평시 관측에도 걸린다. 무엇을 재든 그 값이 평소 값인지 아닌지를 가르려면 비교할 구간이 필요하다.
## 예시
- A-6 지연 주입 : 지연을 넣지 않은 대조군 노드가 70 ms 에서 41 ms 로 빨라졌는데(−41%) 해설 문서는 처음에 영향 없음이라고 적었다. JIT(just-in-time 컴파일) 워밍업이나 캐시처럼 주입과 무관한 변동이었고, 자릿수가 달라 결론 자체는 유지됐다. 나중에 고쳤다.
- A-8 롤링 재시작 : 5초 간격 표본 9개로 무중단을 주장했다. 고친 문장은 「5초 해상도에서 끊김이 관측되지 않았다」다. 뒤에 1초 간격에 3초 타임아웃으로 D-2 롤백 전환을 재자 파드가 바뀌는 순간 요청 하나가 3초를 넘겼다.
- D-4 in-flight 감시 : 76건이 실패했다. 그대로 적었으면 「갱신 중 대규모 요청 실패」라는 오보가 됐을 것이다. 같은 순간의 폴링은 49건 전부 200 이었고, 실패한 76건은 연결수 0 에 소요 시간 50µs 였고 재현이 0/100 이었다.
- A-1 전송 차단 : 주입을 넣은 뒤 cluster_size 를 따로 읽었고, 25분간 2 로 남아 있는 것을 보고 주입이 걸리지 않았다고 판정했다.