docs: give the twelve experiments that had no architecture diagram one

An audit against the standard the series set — concepts, procedure,
commands, architecture diagram, evidence table, terminal output — found the
three new experiments met it while twelve of the original ones had no
diagram at all: A-0, A-1, A-3, A-4, A-5, A-6, A-8, B-0, B-2, B-7, C-2, D-2.

Each now has one drawn from what that experiment actually found, not filler:
A-0 shows sharing going through PostgreSQL rather than between the caches;
A-3 the gap between the 200 and the WAL flush, with both failed injections;
A-5 the three silent injection failures; A-6 the two places latency is
multiplied; B-0 the repository keyed by principal with no session id; B-2
the primary key that causes the overwrite; D-2 why the rolling update
stopped the accident halfway.

Also corrected the index's stale claim of 11 experiments without a
screenshot — it is 14, and the reason is recorded: those experiments were
measured from terminals, the database and logs, and the observability stack
does not scrape Redis, the BFF or PostgreSQL, so there is no console to
photograph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-04 21:44:36 +09:00
co-authored by Claude Opus 5
parent a2f749292a
commit cdac9b8178
26 changed files with 721 additions and 1 deletions
+48
View File
@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 400" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">A-0 — 세션을 공유하는 것은 Infinispan 이 아니라 PostgreSQL 이다</text>
<text x="20" y="44" class="s">클러스터가 형성됐다는 것과 세션이 복제된다는 것은 다른 얘기였다</text>
<rect x="30" y="76" width="330" height="120" class="box"/>
<text x="46" y="98" class="tb">keycloak-0 (kc-lab-2)</text>
<rect x="46" y="112" width="298" height="34" class="box"/>
<text x="58" y="134" class="t">Infinispan sessions 캐시</text>
<text x="46" y="166" class="s">자기가 처리한 로그인만 들어 있다</text>
<text x="46" y="184" class="s">entries_unique 는 노드마다 다르다</text>
<rect x="500" y="76" width="330" height="120" class="box"/>
<text x="516" y="98" class="tb">keycloak-1 (kc-lab-1)</text>
<rect x="516" y="112" width="298" height="34" class="box"/>
<text x="528" y="134" class="t">Infinispan sessions 캐시</text>
<text x="516" y="166" class="s">여기에도 자기 것만 있다</text>
<text x="516" y="184" class="s">상대 세션은 들어오지 않는다</text>
<path d="M362 130 L498 130" class="r" stroke-dasharray="5 3" marker-end="url(#ar)"/>
<path d="M498 146 L362 146" class="r" stroke-dasharray="5 3" marker-end="url(#ar)"/>
<text x="430" y="122" class="tb" text-anchor="middle" fill="#cf222e">복제 없음</text>
<text x="430" y="170" class="s" text-anchor="middle" fill="#cf222e">세션 엔트리는 노드 사이를 건너가지 않는다</text>
<rect x="300" y="250" width="260" height="76" class="ok"/>
<text x="316" y="272" class="tb">PostgreSQL</text>
<text x="316" y="292" class="s">OFFLINE_USER_SESSION (offline_flag='0')</text>
<text x="316" y="310" class="s">두 노드가 같은 행을 본다</text>
<path d="M150 198 L330 246" class="g" marker-end="url(#ag)"/>
<path d="M710 198 L530 246" class="g" marker-end="url(#ag)"/>
<text x="180" y="232" class="s">SELECT / INSERT</text>
<text x="600" y="232" class="s">SELECT / INSERT</text>
<rect x="30" y="346" width="800" height="40" class="box"/>
<text x="46" y="370" class="s">근거 — 노드 A 로 로그인하고 노드 B 로 refresh 했을 때, 반대편 노드가 날린 SQL 을 문장 로깅으로 직접 잡았다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+47
View File
@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 430" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">A-1 — 7800 을 막아도 세션 공유는 안 깨진다. 깨지는 것은 로그아웃 전파다</text>
<text x="20" y="44" class="s">예측 하나가 빗나갔고, 예상 못 한 것이 둘 나왔다</text>
<rect x="30" y="72" width="300" height="104" class="box"/>
<text x="46" y="94" class="tb">keycloak-0</text>
<text x="46" y="116" class="s">로그인 처리 · 세션을 DB 에 쓴다</text>
<rect x="530" y="72" width="300" height="104" class="box"/>
<text x="546" y="94" class="tb">keycloak-1</text>
<text x="546" y="116" class="s">DB 를 읽어 같은 세션을 안다</text>
<rect x="352" y="86" width="156" height="34" class="dead"/>
<text x="430" y="107" class="t" text-anchor="middle" fill="#cf222e">TCP 7800 차단</text>
<text x="430" y="140" class="s" text-anchor="middle">NetworkPolicy 는 허용목록이다</text>
<text x="430" y="156" class="s" text-anchor="middle">8080·9000 만 열고 7800 은 누락시킨다</text>
<rect x="300" y="206" width="260" height="64" class="ok"/>
<text x="316" y="228" class="tb">PostgreSQL</text>
<text x="316" y="248" class="s">세션은 여기 있다 → 교차 노드 refresh 200</text>
<path d="M150 178 L320 202" class="g" marker-end="url(#ag)"/>
<path d="M710 178 L540 202" class="g" marker-end="url(#ag)"/>
<rect x="30" y="290" width="390" height="76" class="dead"/>
<text x="46" y="312" class="tb">깨진 것 — 로그아웃 전파</text>
<text x="46" y="332" class="s">무효화 통지가 7800 을 탄다</text>
<text x="46" y="350" class="s">400 이어야 할 refresh 가 200 을 반환했다</text>
<rect x="440" y="290" width="390" height="76" class="warn"/>
<text x="456" y="312" class="tb">★ 주입이 먹지 않았다 — conntrack</text>
<text x="456" y="332" class="s">ESTABLISHED 연결은 규칙 평가를 건너뛴다</text>
<text x="456" y="350" class="s">cluster_size 가 25분간 2 로 남았다 → 삭제 후에야 갈렸다</text>
<text x="30" y="392" class="s">그리고 예상 못 한 둘째 — 분단된 노드가 readiness 실패로 스스로 로드밸런서에서 빠진다</text>
<text x="30" y="410" class="s">덕분에 외부에서는 장애가 보이지 않는다. 관측 지점을 밖에만 두면 이 실험은 「아무 일도 없음」으로 보인다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+55
View File
@@ -0,0 +1,55 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 400" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">A-3 — 200 을 받은 로그인 153건 중 4건이 DB 에 없다</text>
<text x="20" y="44" class="s">버그가 아니라 synchronous_commit OFF 의 대가를 실측한 것이다</text>
<rect x="30" y="74" width="180" height="70" class="box"/>
<text x="46" y="96" class="tb">클라이언트</text>
<text x="46" y="116" class="s">로그인 요청</text>
<text x="46" y="134" class="s">200 + 토큰 수신</text>
<rect x="250" y="74" width="200" height="70" class="box"/>
<text x="266" y="96" class="tb">Keycloak</text>
<text x="266" y="116" class="s">트랜잭션마다</text>
<text x="266" y="134" class="s">SET LOCAL synchronous_commit OFF</text>
<rect x="490" y="74" width="180" height="70" class="warn"/>
<text x="506" y="96" class="tb">PostgreSQL</text>
<text x="506" y="116" class="s">COMMIT 을 즉시 반환</text>
<text x="506" y="134" class="s">WAL 은 아직 메모리에</text>
<rect x="710" y="74" width="120" height="70" class="box"/>
<text x="726" y="96" class="tb">디스크</text>
<text x="726" y="116" class="s">wal_writer_delay</text>
<text x="726" y="134" class="s">200ms 뒤 기록</text>
<path d="M212 108 L246 108" class="k" marker-end="url(#a)"/>
<path d="M452 108 L486 108" class="k" marker-end="url(#a)"/>
<path d="M672 108 L706 108" class="r" stroke-dasharray="4 3" marker-end="url(#ar)"/>
<text x="676" y="164" class="s" fill="#cf222e">이 구간이 비어 있다</text>
<rect x="620" y="180" width="210" height="40" class="dead"/>
<text x="636" y="205" class="t" fill="#cf222e">✂ 여기서 강제 종료</text>
<rect x="30" y="250" width="380" height="96" class="dead"/>
<text x="46" y="272" class="tb">결과 — RPO 는 0 이 아니다</text>
<text x="46" y="296" class="t">클라이언트가 200 받은 로그인 153 건</text>
<text x="46" y="316" class="t">그중 DB 에 실제로 존재 149 건</text>
<text x="46" y="336" class="t" fill="#cf222e">★ 유실 4 건</text>
<rect x="430" y="250" width="400" height="96" class="box"/>
<text x="446" y="272" class="tb">주입도 두 번 실패했다</text>
<text x="446" y="294" class="s">kubectl delete --force 는 크래시가 아니다 — 런타임이 SIGTERM 을 보내</text>
<text x="446" y="310" class="s">PostgreSQL 이 정상 플러시했고 유실이 0 이었다</text>
<text x="446" y="332" class="s">kill -9 1 도 무시된다 — PID 1 은 자기 네임스페이스의 SIGKILL 을 받지 않는다</text>
<text x="30" y="372" class="s">백엔드 프로세스를 죽여 postmaster 가 reinitialize 하게 만들자 비로소 "not properly shut down / redo starts" 가 찍혔다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+52
View File
@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 420" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">A-4 — 둘 다 전면 장애지만 이유가 다르다</text>
<text x="20" y="44" class="s">그리고 죽은 파드가 산 파드보다 건강해 보인다</text>
<rect x="30" y="70" width="390" height="150" class="box"/>
<text x="46" y="92" class="tb">4a — 워커(kc-lab-2) 상실</text>
<rect x="46" y="104" width="170" height="46" class="dead"/>
<text x="58" y="124" class="t">keycloak-0</text>
<text x="58" y="142" class="s">PostgreSQL 도 여기 있었다</text>
<rect x="236" y="104" width="170" height="46" class="warn"/>
<text x="248" y="124" class="t">keycloak-1 살아있음</text>
<text x="248" y="142" class="s">그러나 DB 가 없다</text>
<text x="46" y="172" class="t">외부 응답 <tspan fill="#cf222e" font-weight="600">503</tspan> · kubectl 정상</text>
<text x="46" y="194" class="s">DB 가 같이 죽어서 장애다. 남은 파드는 돌지만 아무것도 못 한다</text>
<text x="46" y="212" class="s">PVC 가 local-path 라 다른 노드로 재배치되지 않는다</text>
<rect x="440" y="70" width="390" height="150" class="box"/>
<text x="456" y="92" class="tb">4b — 컨트롤 플레인(kc-lab-1) 상실</text>
<rect x="456" y="104" width="170" height="46" class="ok"/>
<text x="468" y="124" class="t">keycloak-0</text>
<text x="468" y="142" class="s">★ 계속 돌고 있다</text>
<rect x="646" y="104" width="170" height="46" class="dead"/>
<text x="658" y="124" class="t">API 서버 · traefik</text>
<text x="658" y="142" class="s">들어갈 길이 없다</text>
<text x="456" y="172" class="t">외부 응답 <tspan fill="#cf222e" font-weight="600">000</tspan> · kubectl <tspan fill="#cf222e">불통</tspan></text>
<text x="456" y="194" class="s">워크로드는 멀쩡한데 도달할 수 없어 장애다</text>
<text x="456" y="212" class="s">진입점이 단일 노드에 있으면 워크로드 이중화는 의미가 없다</text>
<rect x="30" y="242" width="800" height="86" class="warn"/>
<text x="46" y="264" class="tb">★ 예상하지 못한 것 셋</text>
<text x="46" y="286" class="s">1. 죽은 파드가 산 파드보다 건강해 보인다 — kubelet 이 사라져 상태가 갱신되지 않으니 Running 으로 남는다</text>
<text x="46" y="304" class="s">2. StatefulSet 은 Terminating 파드의 대체를 만들지 않는다 — 이름이 같아야 하므로 지워지기를 기다린다</text>
<text x="46" y="322" class="s">3. node-monitor-grace-period 40초 + tolerationSeconds 300초 = 축출까지 5분 40초</text>
<rect x="30" y="348" width="800" height="46" class="ok"/>
<text x="46" y="370" class="tb">복구는 둘 다 virsh start 이후 60초</text>
<text x="46" y="388" class="s">장애 시간의 대부분은 복구가 아니라 "누가 죽은 것을 알아채는 데" 걸린 시간이다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+49
View File
@@ -0,0 +1,49 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 420" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">A-5 — 비대칭 차단은 클러스터를 가르지 못한다. 그리고 갈라도 서비스는 계속된다</text>
<text x="20" y="44" class="s">주입을 세 번 실패했고, 세 번 모두 「아무 일도 없었다」로 보였다</text>
<rect x="30" y="72" width="390" height="112" class="box"/>
<text x="46" y="94" class="tb">① 한 방향만 차단</text>
<rect x="46" y="106" width="130" height="34" class="box"/>
<text x="58" y="128" class="t">keycloak-0</text>
<rect x="290" y="106" width="116" height="34" class="box"/>
<text x="302" y="128" class="t">keycloak-1</text>
<path d="M178 116 L286 116" class="r" stroke-dasharray="4 3" marker-end="url(#ar)"/>
<path d="M286 132 L178 132" class="g" marker-end="url(#ag)"/>
<text x="46" y="164" class="s" fill="#1a7f37">열린 방향으로 재연결한다 → cluster_size 2 유지. 가르지 못한다</text>
<rect x="440" y="72" width="390" height="112" class="box"/>
<text x="456" y="94" class="tb">② 양방향 완전 차단</text>
<rect x="456" y="106" width="130" height="34" class="dead"/>
<text x="468" y="128" class="t">keycloak-0</text>
<rect x="700" y="106" width="116" height="34" class="ok"/>
<text x="712" y="128" class="t">keycloak-1</text>
<path d="M588 116 L696 116" class="r" stroke-dasharray="4 3" marker-end="url(#ar)"/>
<path d="M696 132 L588 132" class="r" stroke-dasharray="4 3" marker-end="url(#ar)"/>
<text x="456" y="164" class="s">양쪽 모두 멤버 1개. 그런데 <tspan fill="#cf222e" font-weight="600">한쪽만 DOWN</tspan> 이 된다 — 코디네이터 쪽이 살아남는다</text>
<rect x="30" y="204" width="800" height="48" class="ok"/>
<text x="46" y="226" class="tb">그래서 전면 장애 경로가 없다 — 외부 응답 200 유지</text>
<text x="46" y="244" class="s">분단된 쪽이 스스로 readiness 를 떨어뜨려 로드밸런서에서 빠지고, 남은 쪽이 계속 응답한다</text>
<rect x="30" y="272" width="800" height="130" class="warn"/>
<text x="46" y="294" class="tb">★ 세 번의 주입 실패 — 전부 「조용히」 실패했다</text>
<text x="46" y="318" class="s">1. iptables -I FORWARD 1 이 무시됐다 — kube-router 가 자기 체인을 FORWARD 맨 위에 다시 끼워 넣는다 (패킷 0)</text>
<text x="46" y="338" class="s">2. 방향이 뒤집혀 있었다 — JGroups 의 client/server 역할은 재시작마다 바뀐다. raw 규칙이 엉뚱한 노드에 걸렸다</text>
<text x="46" y="358" class="s">3. dev eth0 이 없다 — Debian 은 enp1s0 이고, 게다가 flannel VXLAN 이 이미 캡슐화해 파드 IP 가 안 보인다</text>
<text x="46" y="382" class="s" fill="#cf222e">해결: raw 테이블 PREROUTING(conntrack 보다 먼저) + flannel.1 인터페이스에서 필터</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

+51
View File
@@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 400" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">A-6 — 200밀리초가 22초가 된다</text>
<text x="20" y="44" class="s">지연은 왕복 횟수만큼 곱해지고, 커넥션 풀에서 한 번 더 곱해진다</text>
<rect x="30" y="76" width="180" height="60" class="ok"/>
<text x="46" y="98" class="tb">대조군 keycloak-0</text>
<text x="46" y="120" class="s">DB 와 같은 노드 · 66ms</text>
<rect x="250" y="76" width="180" height="60" class="dead"/>
<text x="266" y="98" class="tb">시험군 keycloak-1</text>
<text x="266" y="120" class="s">tc netem delay 200ms</text>
<text x="230" y="112" class="tb" fill="#cf222e"></text>
<rect x="470" y="76" width="180" height="60" class="warn"/>
<text x="486" y="98" class="tb">단일 요청</text>
<text x="486" y="120" class="t">1,872 ms <tspan class="s">(28배)</tspan></text>
<rect x="690" y="76" width="140" height="60" class="dead"/>
<text x="706" y="98" class="tb">동시 20건</text>
<text x="706" y="120" class="t" fill="#cf222e">22.2 초</text>
<path d="M432 106 L466 106" class="k" marker-end="url(#a)"/>
<path d="M652 106 L686 106" class="r" marker-end="url(#ar)"/>
<text x="20" y="176" class="tb">왜 200ms 가 1,872ms 가 되는가 — 왕복 횟수</text>
<rect x="30" y="190" width="800" height="46" class="box"/>
<text x="46" y="210" class="s">로그인 한 번은 DB 왕복을 여러 번 한다: 클라이언트 조회 · 사용자 조회 · 세션 INSERT · 커밋 …</text>
<text x="46" y="228" class="s">왕복마다 200ms 가 더해진다. 애플리케이션은 아무것도 잘못하지 않았다</text>
<text x="20" y="266" class="tb">왜 1,872ms 가 22.2초가 되는가 — 커넥션 풀</text>
<rect x="30" y="280" width="390" height="76" class="dead"/>
<text x="46" y="302" class="s">요청이 커넥션을 오래 붙들고 있으면</text>
<text x="46" y="320" class="s">뒤의 요청은 풀에서 대기한다</text>
<text x="46" y="342" class="t" fill="#cf222e">agroal 커넥션 획득 대기 최대 20,000 ms</text>
<rect x="440" y="280" width="390" height="76" class="warn"/>
<text x="456" y="302" class="tb">그리고 파드가 죽는다</text>
<text x="456" y="322" class="s">readiness 프로브가 타임아웃으로 실패 →</text>
<text x="456" y="342" class="s">느린 노드가 로드밸런서에서 빠진다. 느림이 장애로 승격된다</text>
<text x="30" y="384" class="s">측정 장치 주의 — 동시 20건을 kubectl run --rm -i 로 돌리면 출력 스트림이 유실된다. 상주 탐침에 파일로 모아야 한다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

+47
View File
@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 380" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">A-8 — 세션은 살아남고 캐시만 사라진다</text>
<text x="20" y="44" class="s">이것이 persistent-user-sessions 를 켜는 진짜 이유다</text>
<rect x="30" y="74" width="800" height="30" class="box"/>
<text x="46" y="94" class="tb">kubectl rollout restart statefulset/keycloak — 한 번에 한 파드씩</text>
<rect x="30" y="124" width="250" height="110" class="dead"/>
<text x="46" y="146" class="tb">사라지는 것 — 메모리</text>
<text x="46" y="168" class="s">Infinispan sessions 캐시</text>
<text x="46" y="188" class="t">entries_unique → <tspan fill="#cf222e" font-weight="600">0</tspan></text>
<text x="46" y="212" class="s">파드와 함께 없어진다</text>
<rect x="305" y="124" width="250" height="110" class="ok"/>
<text x="321" y="146" class="tb">남는 것 — PostgreSQL</text>
<text x="321" y="168" class="s">OFFLINE_USER_SESSION</text>
<text x="321" y="188" class="t">151 → <tspan fill="#1a7f37" font-weight="600">151</tspan> 그대로</text>
<text x="321" y="212" class="s">재시작과 무관하다</text>
<rect x="580" y="124" width="250" height="110" class="ok"/>
<text x="596" y="146" class="tb">그래서 사용자는</text>
<text x="596" y="168" class="s">재시작 전 발급한 refresh token</text>
<text x="596" y="188" class="t">여전히 <tspan fill="#1a7f37" font-weight="600">200</tspan></text>
<text x="596" y="212" class="s">전 구간 중단 없음</text>
<path d="M282 178 L301 178" class="k" marker-end="url(#a)"/>
<path d="M557 178 L576 178" class="k" marker-end="url(#a)"/>
<rect x="30" y="254" width="800" height="52" class="box"/>
<text x="46" y="276" class="tb">클러스터는 자동 재형성된다</text>
<text x="46" y="296" class="s">JGROUPS_PING 테이블에 다시 등록되고 cluster_size 가 2 로 돌아온다 — 사람이 할 일이 없다</text>
<rect x="30" y="322" width="800" height="44" class="warn"/>
<text x="46" y="344" class="s">★ A-7 과 대조 — volatile 이면 같은 재시작에서 refresh 가 400 Session not active 가 된다.</text>
<text x="46" y="360" class="s">세션이 메모리에만 있었으므로 캐시가 사라지는 순간 세션도 사라진다. 이 실험의 결론은 버전 조건부다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+45
View File
@@ -0,0 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 400" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">B-0 — 아무것도 설정하지 않으면 Spring 이 무엇을 고르는가</text>
<text x="20" y="44" class="s">추측이 맞았지만, 추측으로 두면 안 되는 이유가 둘째 줄에 있다</text>
<rect x="30" y="72" width="800" height="118" class="box"/>
<text x="46" y="94" class="tb">/actuator/beans 가 실제로 답한 것</text>
<text x="46" y="118" class="t">authorizedClientService → InMemoryOAuth2AuthorizedClientService</text>
<text x="46" y="138" class="t">authorizedClientRepository → AuthenticatedPrincipalOAuth2AuthorizedClientRepository</text>
<text x="46" y="158" class="t">SessionRepository → 없음 (서블릿 컨테이너 in-memory)</text>
<text x="46" y="178" class="t" fill="#cf222e">Redis / Spring Session → 없음</text>
<rect x="30" y="212" width="390" height="150" class="warn"/>
<text x="46" y="234" class="tb">★ 이름이 곧 설명이다</text>
<text x="46" y="256" class="s">AuthenticatedPrincipal…Repository 는</text>
<text x="46" y="276" class="t">principal 이름으로 찾는다</text>
<rect x="46" y="288" width="358" height="30" class="dead"/>
<text x="58" y="308" class="s">조회 키에 session id 가 없다</text>
<text x="46" y="336" class="s">그래서 세션 저장소를 Redis 로 옮겨도</text>
<text x="46" y="354" class="s">토큰은 따라오지 않는다 — B-1 이 겪는 문제의 뿌리</text>
<rect x="440" y="212" width="390" height="150" class="box"/>
<text x="456" y="234" class="tb">두 개가 서로 다른 것을 저장한다</text>
<rect x="456" y="248" width="358" height="40" class="box"/>
<text x="468" y="266" class="t">Application Session</text>
<text x="468" y="282" class="s">누가 로그인했는지 · 세션 id 로 찾는다</text>
<rect x="456" y="298" width="358" height="40" class="box"/>
<text x="468" y="316" class="t">OAuth2AuthorizedClient</text>
<text x="468" y="332" class="s">access / refresh token · principal 이름으로 찾는다</text>
<text x="456" y="356" class="s">이 둘을 하나로 생각하면 B-1·B-2 의 결과를 해석할 수 없다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,55 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 440" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">B-2 — 저장소를 나눠 문제를 풀자 다른 두 문제가 남았다</text>
<text x="20" y="44" class="s">Q1 의 네 항목 중 둘이 통과, 둘이 실패</text>
<rect x="30" y="72" width="240" height="90" class="box"/>
<text x="46" y="94" class="tb">bff 인스턴스 2개</text>
<text x="46" y="116" class="s">어느 쪽으로 요청이 가도 된다</text>
<text x="46" y="136" class="s">enableServiceLinks: false</text>
<text x="46" y="154" class="s">(REDIS_PORT 충돌 회피)</text>
<rect x="330" y="72" width="230" height="90" class="ok"/>
<text x="346" y="94" class="tb">Redis</text>
<text x="346" y="116" class="s">Application Session</text>
<text x="346" y="136" class="s">세션 id 로 조회</text>
<text x="346" y="154" class="s">로그아웃 시 정리됨</text>
<rect x="600" y="72" width="230" height="90" class="warn"/>
<text x="616" y="94" class="tb">PostgreSQL</text>
<text x="616" y="116" class="s">OAuth2AuthorizedClient</text>
<text x="616" y="136" class="s">principal 이름으로 조회</text>
<text x="616" y="154" class="s">로그아웃해도 남는다</text>
<path d="M272 108 L326 108" class="g" marker-end="url(#ag)"/>
<path d="M562 108 L596 108" class="g" marker-end="url(#ag)"/>
<rect x="30" y="188" width="390" height="100" class="ok"/>
<text x="46" y="210" class="tb">통과</text>
<text x="46" y="234" class="t">① 다른 인스턴스로 요청해도 된다</text>
<text x="46" y="256" class="t">② 재시작 후에도 로그인 유지</text>
<text x="46" y="278" class="s">저장소를 밖으로 뺐으니 당연한 결과다</text>
<rect x="440" y="188" width="390" height="100" class="dead"/>
<text x="456" y="210" class="tb">실패</text>
<text x="456" y="234" class="t" fill="#cf222e">③ 같은 사용자의 다른 브라우저가 덮어쓴다</text>
<text x="456" y="256" class="t" fill="#cf222e">④ 로그아웃해도 한쪽만 정리된다</text>
<text x="456" y="278" class="s">둘 다 저장소 선택의 문제가 아니라 스키마의 문제다</text>
<rect x="30" y="308" width="800" height="70" class="warn"/>
<text x="46" y="330" class="tb">★ 뿌리는 DDL 한 줄이다</text>
<text x="46" y="352" class="t">PRIMARY KEY (client_registration_id, principal_name)</text>
<text x="46" y="370" class="s">세션 id 가 키에 없다 → 같은 사용자의 두 세션이 같은 행을 쓴다 → 나중 로그인이 앞의 토큰을 덮어쓴다</text>
<rect x="30" y="392" width="800" height="40" class="dead"/>
<text x="46" y="416" class="s">로그아웃 후: Redis 세션 0 키 (정리됨) · PostgreSQL 토큰 1 행 — 평문 refresh token 이 그대로 남는다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+52
View File
@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 420" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">B-7 — BFF 와 정반대의 성질: 공유할 상태가 없고, 대신 겹침 구간도 없다</text>
<text x="20" y="44" class="s">oauth2-proxy 는 세션을 쿠키에 담는다</text>
<rect x="30" y="72" width="380" height="120" class="box"/>
<text x="46" y="94" class="tb">replica 는 무엇을 공유하나</text>
<rect x="46" y="106" width="160" height="34" class="box"/>
<text x="58" y="128" class="t">oauth2-proxy A</text>
<rect x="250" y="106" width="146" height="34" class="box"/>
<text x="262" y="128" class="t">oauth2-proxy B</text>
<rect x="106" y="152" width="230" height="30" class="ok"/>
<text x="118" y="172" class="s">같은 k8s Secret 을 읽는다</text>
<path d="M126 142 L180 150" class="g"/>
<path d="M316 142 L262 150" class="g"/>
<text x="46" y="196" class="s"> </text>
<rect x="440" y="72" width="390" height="120" class="ok"/>
<text x="456" y="94" class="tb">그래서 얻는 것</text>
<text x="456" y="118" class="s">공유할 서버 상태가 없다 → 콜백이 다른 replica 로 가도 된다</text>
<text x="456" y="140" class="s">replica 를 늘려도 세션 저장소 고민이 없다</text>
<text x="456" y="164" class="s">BFF 가 Redis·PostgreSQL 로 풀던 문제가 아예 생기지 않는다</text>
<rect x="30" y="212" width="380" height="106" class="dead"/>
<text x="46" y="234" class="tb">★ 대신 겹침 구간을 만들 수 없다</text>
<text x="46" y="258" class="t">--cookie-secret 은 단수다</text>
<text x="46" y="280" class="s">「옛 secret 도 당분간 받아준다」가 불가능하다</text>
<text x="46" y="300" class="s">교체하는 순간 모든 쿠키가 한꺼번에 무효가 된다</text>
<rect x="440" y="212" width="390" height="106" class="warn"/>
<text x="456" y="234" class="tb">교체하면 벌어지는 일</text>
<text x="456" y="258" class="s">옛 쿠키 → session ticket cookie failed validation</text>
<text x="456" y="278" class="s">Keycloak SSO 가 살아 있으면 로그인 화면 없이 조용히 재인증</text>
<text x="456" y="300" class="s" fill="#cf222e">서버 쪽 세션은 고아로 남는다 — 티켓을 못 풀어 지우지도 못한다</text>
<rect x="30" y="338" width="800" height="66" class="ok"/>
<text x="46" y="360" class="tb">→ B-7a 가 이어받았다</text>
<text x="46" y="382" class="s">「지울 수 없다」는 oauth2-proxy 의 한계일 뿐이었다. 운영자는 지울 수 있고, TTL 역산으로 고아만 골라낼 수 있다</text>
<text x="46" y="398" class="s">그리고 고아는 생성 후 정확히 1시간에 사라진다 — TTL 이 요청으로 갱신되지 않기 때문이다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+44
View File
@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 400" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">C-2 — 로그아웃이 안 퍼지는 이유는 단순했다. 아무도 구현하지 않았다</text>
<text x="20" y="44" class="s">C-1 이 관측한 현상의 원인</text>
<text x="20" y="76" class="tb">백채널 로그아웃이 동작하려면 양쪽이 다 있어야 한다</text>
<rect x="30" y="94" width="380" height="110" class="dead"/>
<text x="46" y="116" class="tb">① IdP 쪽 — Keycloak 클라이언트 설정</text>
<text x="46" y="140" class="t">backchannelLogoutUrl</text>
<text x="46" y="162" class="t" fill="#cf222e">두 클라이언트 모두 비어 있다</text>
<text x="46" y="186" class="s">Keycloak 은 부를 주소를 모른다</text>
<rect x="440" y="94" width="390" height="110" class="dead"/>
<text x="456" y="116" class="tb">② 앱 쪽 — 수신 엔드포인트</text>
<text x="456" y="140" class="t">/logout/connect/back-channel/{registrationId}</text>
<text x="456" y="162" class="t" fill="#cf222e">BFF 소스에 oidcLogout 설정이 없다</text>
<text x="456" y="186" class="s">주소를 알려줘도 받을 곳이 없다</text>
<rect x="30" y="228" width="800" height="60" class="warn"/>
<text x="46" y="250" class="tb">★ 그래서 IdP 쪽만 설정하면 되는 줄 알면 틀린다</text>
<text x="46" y="272" class="s">backchannelLogoutUrl 을 넣어 봤지만 앱 세션은 그대로 남았다. 받는 쪽이 없기 때문이다</text>
<rect x="30" y="308" width="380" height="72" class="ok"/>
<text x="46" y="330" class="tb">네트워크 문제가 아니라는 확인</text>
<text x="46" y="352" class="s">Keycloak 파드에서 앱 URL 로 요청 → HTTP 200</text>
<text x="46" y="372" class="s">닿기는 한다. 닿아도 처리할 코드가 없을 뿐이다</text>
<rect x="440" y="308" width="390" height="72" class="box"/>
<text x="456" y="330" class="tb">남기는 것</text>
<text x="456" y="352" class="s">「설정이 빠졌다」와 「기능이 없다」는 다르게 고쳐야 한다</text>
<text x="456" y="372" class="s">여기는 둘 다였고, 확인 순서를 바꿨다면 한쪽만 고치고 끝냈을 것이다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+48
View File
@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 860 420" font-family="-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,sans-serif">
<style>
.box{fill:#fff;stroke:#8c959f;stroke-width:1.4;rx:6}
.dead{fill:#fff5f5;stroke:#cf222e;stroke-width:1.6;stroke-dasharray:5 3;rx:6}
.ok{fill:#f6fdf6;stroke:#1a7f37;stroke-width:1.6;rx:6}
.warn{fill:#fff8f0;stroke:#bf8700;stroke-width:1.5;rx:6}
.t{font-size:12px;fill:#1f2328}
.tb{font-size:12.5px;fill:#1f2328;font-weight:600}
.s{font-size:10.5px;fill:#656d76}
.ttl{font-size:14px;fill:#1f2328;font-weight:600}
.r{stroke:#cf222e;stroke-width:1.6;fill:none}
.g{stroke:#1a7f37;stroke-width:1.6;fill:none}
.k{stroke:#8c959f;stroke-width:1.3;fill:none}
</style>
<defs>
<marker id="a" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#8c959f"/></marker>
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#cf222e"/></marker>
<marker id="ag" markerWidth="8" markerHeight="8" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 z" fill="#1a7f37"/></marker>
</defs>
<text x="20" y="26" class="ttl">D-2 — 롤백은 안 된다. 다만 롤링 업데이트가 사고를 절반에서 멈춰줬다</text>
<text x="20" y="44" class="s">그리고 이 결론은 조건부였다 — 스키마가 안 바뀌면 롤백된다(후속에서 정밀화)</text>
<rect x="30" y="72" width="380" height="126" class="box"/>
<text x="46" y="94" class="tb">앞으로 (26.7.0 → 26.7.3)</text>
<text x="46" y="118" class="t" fill="#1a7f37">된다. 무중단</text>
<text x="46" y="140" class="s">87회 요청 전부 200</text>
<text x="46" y="160" class="s">databasechangelog 210 → 210 (스키마 변경 없음)</text>
<text x="46" y="180" class="s">세션 유지 · Infinispan 16.0.12 → 16.0.14</text>
<rect x="440" y="72" width="390" height="126" class="dead"/>
<text x="456" y="94" class="tb">뒤로 (스키마가 이미 움직였을 때)</text>
<text x="456" y="118" class="t" fill="#cf222e">안 된다</text>
<text x="456" y="140" class="s">liquibase ValidationFailedException:</text>
<text x="456" y="158" class="s">1 changesets check sum</text>
<text x="456" y="180" class="s">새 버전이 남긴 체크섬을 옛 버전이 거부한다</text>
<rect x="30" y="222" width="800" height="72" class="ok"/>
<text x="46" y="244" class="tb">★ 그런데 서비스는 살아 있었다 — StatefulSet 롤링 업데이트 덕분이다</text>
<text x="46" y="266" class="s">한 번에 한 파드씩 바꾼다. 첫 파드가 기동에 실패하면 거기서 멈추고 나머지는 건드리지 않는다</text>
<text x="46" y="286" class="s">그래서 남은 파드가 외부 200 을 계속 냈다. 「롤백 계획」이 없어도 사고가 전면화되지 않았다</text>
<rect x="30" y="314" width="380" height="90" class="warn"/>
<text x="46" y="336" class="tb">그러면 무엇으로 판단하나</text>
<text x="46" y="358" class="t">select count(*) from databasechangelog</text>
<text x="46" y="380" class="s">업그레이드 전후 이 수가 같으면 롤백 가능</text>
<text x="46" y="398" class="s">늘었으면 스키마가 움직였다 = 롤백 불가</text>
<rect x="440" y="314" width="390" height="90" class="box"/>
<text x="456" y="336" class="tb">전제 — 백업이 먼저다 (D-1)</text>
<text x="456" y="358" class="s">롤백이 막히는 상황에서 되돌릴 방법은 덤프 복원뿐이다</text>
<text x="456" y="380" class="s">D-1 이 그 절차를 재고, D-2 가 그 절차가 필요한 이유를 만든다</text>
<text x="456" y="398" class="s">순서를 바꾸면 되돌릴 수 없는 상태에서 백업을 배우게 된다</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -16,6 +16,15 @@
---
## 구조
![A-0 — 세션 공유는 PostgreSQL 을 거친다](diagrams/a0-session-replication.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 물음 | 답 |
@@ -9,6 +9,15 @@
---
## 구조
![A-1 — 7800 차단과 그때 깨지는 것](diagrams/a1-transport-block.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 예측 | 결과 |
+9
View File
@@ -9,6 +9,15 @@
---
## 구조
![A-3 — 200 과 디스크 사이의 빈 구간](diagrams/a3-database-crash.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
```
+9
View File
@@ -17,6 +17,15 @@
---
## 구조
![A-4 — 두 가지 노드 상실과 서로 다른 이유](diagrams/a4-node-loss.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| | 4a 워커 상실 | 4b 컨트롤 플레인 상실 |
@@ -12,6 +12,15 @@
---
## 구조
![A-5 — 비대칭·양방향 차단과 세 번의 주입 실패](diagrams/a5-asymmetric-partition.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 물음 | 답 |
+9
View File
@@ -9,6 +9,15 @@
---
## 구조
![A-6 — 지연이 곱해지는 두 단계](diagrams/a6-latency-injection.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 측정 | 값 |
+9
View File
@@ -9,6 +9,15 @@
---
## 구조
![A-8 — 사라지는 캐시와 남는 세션](diagrams/a8-rolling-restart.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 확인 | 결과 |
+9
View File
@@ -11,6 +11,15 @@
---
## 구조
![B-0 — 자동구성이 고른 것과 그 조회 키](diagrams/b0-autoconfiguration.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
```
@@ -10,6 +10,15 @@
---
## 구조
![B-2 — 저장소 분리 후 남은 두 문제](diagrams/b2-store-split-consequences.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
B-1 이 남긴 문제(세션만 공유되고 토큰은 안 됨)를 **JDBC 로 옮겨 해결했다.**
@@ -12,6 +12,15 @@
---
## 구조
![B-7 — 쿠키 세션의 성질과 회전의 대가](diagrams/b7-cookie-secret.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 물음 | 답 |
+9
View File
@@ -8,6 +8,15 @@
---
## 구조
![C-2 — 백채널 로그아웃에 필요한 양쪽](diagrams/c2-backchannel-logout.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 확인 | 결과 |
+9
View File
@@ -23,6 +23,15 @@
---
## 구조
![D-2 — 방향에 따라 갈리는 결과](diagrams/d2-version-upgrade.svg)
> 다이어그램 규약은 [`diagrams/_style.md`](diagrams/_style.md).
> 실험대 전체 구조는 [`diagrams/lab-topology.svg`](diagrams/lab-topology.svg).
---
## 0. 결론부터
| 확인 | 결과 |
@@ -10,6 +10,18 @@
---
## 구조
이 문서는 세 실험의 후속이므로 각자의 구조도를 참조한다.
| 항목 | 구조도 |
|---|---|
| D-2 정방향 업그레이드 | [`d2-version-upgrade.svg`](diagrams/d2-version-upgrade.svg) |
| B-4 ③ role 반영 | [`b4-header-forgery.svg`](diagrams/b4-header-forgery.svg) |
| D-4 강제 갱신 | [`d4-renewal-gap.svg`](diagrams/d4-renewal-gap.svg) · [`d4a-hook-timeline.svg`](diagrams/d4a-hook-timeline.svg) |
---
## 0. 결론부터
| 항목 | 결과 |
+8 -1
View File
@@ -41,7 +41,14 @@
| `diagrams/*.svg` | 실험별 구조도 12개 |
| `evidence/*/*.png` | Grafana · 브라우저 스크린샷 |
**스크린샷이 없는 실험은 11개다**`A-3 A-7 B-3 B-4 B-5 B-6 C-2 D-1 D-3 D-4 후속`.
**스크린샷이 없는 실험은 14개다**`A-3 A-7 A-7a B-3 B-4 B-5 B-6 C-2 D-1 D-3 D-4 D-4a 후속 멀티노드`.
없는 이유는 **관측 대상에 그 지표가 없기 때문**이지 안 찍어서가 아니다 —
Prometheus 는 keycloak·kubelet·node-exporter·prometheus 만 긁는다
([`followup/04-observability-gap.txt`](evidence/followup/04-observability-gap.txt)).
터미널·DB·로그로 측정한 실험은 브라우저에 띄울 화면 자체가 없다.
**구조도(SVG)는 28개이고, 실험 문서 29개 중 28개가 하나 이상을 싣는다.**
싣지 않는 하나는 후속 요약 문서이며, 세 실험의 구조도를 표로 참조한다.
그중 B층은 **Prometheus 가 Redis·BFF·PostgreSQL 을 긁지 않아** 만들 수가 없다 —
[`followup/04-observability-gap.txt`](evidence/followup/04-observability-gap.txt) 에 측정해 두었다.