refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "노드가 서로를 찾는 경로와 실제로 이야기하는 경로가 다르다. 발견은 PostgreSQL 의 JGROUPS_PING 테이블을 쓰고 전송은 TCP 7800 을 쓴다. 7800 만 막으면 둘 다 데이터베이스에 등록된 채로 남아 서로 존재한다고 믿지만 메시지는 오가지 않는다. 세션은 데이터베이스에 있으므로 교차 노드 refresh 는 200 을 유지하고, 로그아웃 무효화 통지는 7800 을 타므로 400 이어야 할 것이 200 이 된다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "A-1 · JGroups 전송(TCP 7800) 차단",
"line": 185
"line": 485
}
},
"composition": {
@@ -40,8 +40,8 @@
"details": [],
"evidence": [
{
"start_line": 183,
"end_line": 189
"start_line": 485,
"end_line": 489
}
],
"assumption": false
@@ -58,8 +58,8 @@
],
"evidence": [
{
"start_line": 191,
"end_line": 196
"start_line": 491,
"end_line": 495
}
],
"assumption": false
@@ -76,8 +76,8 @@
],
"evidence": [
{
"start_line": 183,
"end_line": 189
"start_line": 485,
"end_line": 489
}
],
"assumption": false
@@ -94,8 +94,8 @@
],
"evidence": [
{
"start_line": 186,
"end_line": 190
"start_line": 487,
"end_line": 490
}
],
"assumption": false
@@ -110,8 +110,8 @@
"kind": "request",
"evidence": [
{
"start_line": 186,
"end_line": 190
"start_line": 487,
"end_line": 490
}
],
"assumption": false
@@ -124,8 +124,8 @@
"kind": "blocked",
"evidence": [
{
"start_line": 186,
"end_line": 190
"start_line": 487,
"end_line": 490
}
],
"assumption": false
@@ -138,8 +138,8 @@
"kind": "read",
"evidence": [
{
"start_line": 186,
"end_line": 189
"start_line": 487,
"end_line": 489
}
],
"assumption": false
@@ -149,4 +149,4 @@
"metadata": {
"rationale": "발견과 전송을 분리해 그렸다. A-1 의 예측이 하나만 맞은 이유가 이 분기에 있다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "Keycloak 은 트랜잭션마다 SET LOCAL synchronous_commit TO OFF 를 건다. PostgreSQL 은 COMMIT 을 WAL 디스크 기록을 기다리지 않고 즉시 반환하고, 클라이언트는 200 과 토큰을 받는다. 실측된 wal_writer_delay 는 200ms 이며 그 사이에 프로세스가 죽으면 그만큼이 사라진다. 153건 중 4건이 그렇게 유실됐다. 버그가 아니라 의도된 설계이고 그 대가를 숫자로 확인한 것이다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "A-2 · A-3 — DB 가 멈출 때와 죽을 때",
"line": 201
"line": 506
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 207,
"end_line": 213
"start_line": 513,
"end_line": 518
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 211,
"end_line": 215
"start_line": 516,
"end_line": 518
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 211,
"end_line": 215
"start_line": 516,
"end_line": 518
}
],
"assumption": false
@@ -96,8 +96,8 @@
],
"evidence": [
{
"start_line": 207,
"end_line": 215
"start_line": 513,
"end_line": 518
}
],
"assumption": false
@@ -112,8 +112,8 @@
"kind": "request",
"evidence": [
{
"start_line": 207,
"end_line": 211
"start_line": 513,
"end_line": 516
}
],
"assumption": false
@@ -126,8 +126,8 @@
"kind": "write",
"evidence": [
{
"start_line": 211,
"end_line": 215
"start_line": 516,
"end_line": 518
}
],
"assumption": false
@@ -140,8 +140,8 @@
"kind": "write",
"evidence": [
{
"start_line": 211,
"end_line": 215
"start_line": 516,
"end_line": 518
}
],
"assumption": false
@@ -151,4 +151,4 @@
"metadata": {
"rationale": "성공 응답과 영속화를 두 사건으로 분리했다. RPO 가 0 이 아닌 이유가 그 사이에 있다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "kc-lab-2 를 잃으면 keycloak-1 은 살아 있지만 PostgreSQL 이 같이 죽어 외부 응답이 503 이 된다. kc-lab-1 을 잃으면 keycloak-0 은 계속 돌고 있는데 traefik 과 API 서버가 사라져 연결 자체가 안 된다. 워크로드가 멀쩡한데 도달할 수 없어 장애다. 진입점이 단일 노드에 있으면 워크로드 이중화는 의미가 없다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "A-4 · 노드 상실 — 둘 다 전면 장애지만 이유가 다르다",
"line": 223
"line": 533
}
},
"composition": {
@@ -40,8 +40,8 @@
"details": [],
"evidence": [
{
"start_line": 221,
"end_line": 228
"start_line": 533,
"end_line": 538
}
],
"assumption": false
@@ -58,8 +58,8 @@
],
"evidence": [
{
"start_line": 221,
"end_line": 231
"start_line": 533,
"end_line": 540
}
],
"assumption": false
@@ -76,8 +76,8 @@
],
"evidence": [
{
"start_line": 221,
"end_line": 231
"start_line": 533,
"end_line": 540
}
],
"assumption": false
@@ -94,8 +94,8 @@
],
"evidence": [
{
"start_line": 221,
"end_line": 233
"start_line": 533,
"end_line": 543
}
],
"assumption": false
@@ -110,8 +110,8 @@
"kind": "request",
"evidence": [
{
"start_line": 221,
"end_line": 228
"start_line": 533,
"end_line": 538
}
],
"assumption": false
@@ -124,8 +124,8 @@
"kind": "request",
"evidence": [
{
"start_line": 221,
"end_line": 228
"start_line": 533,
"end_line": 538
}
],
"assumption": false
@@ -138,8 +138,8 @@
"kind": "read",
"evidence": [
{
"start_line": 221,
"end_line": 233
"start_line": 533,
"end_line": 543
}
],
"assumption": false
@@ -149,4 +149,4 @@
"metadata": {
"rationale": "저장소 상실과 진입 경로 상실을 한 그림에서 갈랐다. 복구 시간이 같아도 대비가 다르다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "JGroups 는 한 방향이 막혀도 열린 방향으로 재연결한다. 그래서 비대칭 차단으로는 cluster_size 가 2 로 유지된다. 양방향을 다 막으면 양쪽 모두 멤버 1개가 되지만 한쪽만 DOWN 이 된다. 코디네이터 쪽이 살아남고 분단된 쪽은 스스로 readiness 를 떨어뜨려 로드밸런서에서 빠지므로 외부 응답은 200 을 유지한다. 전면 장애 경로가 없다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "A-5 · 비대칭 분단 — 전면 장애 경로가 없다",
"line": 246
"line": 576
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 226,
"end_line": 236
"start_line": 536,
"end_line": 545
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 226,
"end_line": 236
"start_line": 536,
"end_line": 545
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 226,
"end_line": 236
"start_line": 536,
"end_line": 545
}
],
"assumption": false
@@ -96,8 +96,8 @@
],
"evidence": [
{
"start_line": 226,
"end_line": 232
"start_line": 536,
"end_line": 542
}
],
"assumption": false
@@ -112,8 +112,8 @@
"kind": "request",
"evidence": [
{
"start_line": 226,
"end_line": 232
"start_line": 536,
"end_line": 542
}
],
"assumption": false
@@ -126,8 +126,8 @@
"kind": "blocked",
"evidence": [
{
"start_line": 226,
"end_line": 232
"start_line": 536,
"end_line": 542
}
],
"assumption": false
@@ -140,8 +140,8 @@
"kind": "request",
"evidence": [
{
"start_line": 226,
"end_line": 232
"start_line": 536,
"end_line": 542
}
],
"assumption": false
@@ -151,4 +151,4 @@
"metadata": {
"rationale": "방향 하나를 막는 것과 둘을 막는 것의 차이를 그렸다. 주입이 세 번 실패한 곳이기도 하다."
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -13,11 +13,11 @@
"long_description": "주입한 지연은 200ms 인데 로그인 응답이 1,872ms 가 됐다. 로그인 한 번이 데이터베이스 왕복을 여러 번 하므로 왕복 횟수만큼 더해진다. 동시 20건에서는 22.2초가 됐다. 요청이 커넥션을 오래 붙들어 뒤의 요청이 풀에서 대기하기 때문이며 커넥션 획득 대기 최대가 20,000ms 였다. 마지막으로 readiness 프로브가 타임아웃으로 실패해 느린 노드가 로드밸런서에서 빠진다. 느림이 장애로 승격된다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "A-6 · 지연 주입 — 200밀리초가 22초가 된다",
"line": 255
"line": 590
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 253,
"end_line": 258
"start_line": 590,
"end_line": 593
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 259,
"end_line": 263
"start_line": 594,
"end_line": 598
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 264,
"end_line": 268
"start_line": 600,
"end_line": 602
}
],
"assumption": false
@@ -96,8 +96,8 @@
],
"evidence": [
{
"start_line": 269,
"end_line": 273
"start_line": 604,
"end_line": 605
}
],
"assumption": false
@@ -112,8 +112,8 @@
"kind": "request",
"evidence": [
{
"start_line": 253,
"end_line": 263
"start_line": 590,
"end_line": 598
}
],
"assumption": false
@@ -126,8 +126,8 @@
"kind": "request",
"evidence": [
{
"start_line": 259,
"end_line": 268
"start_line": 594,
"end_line": 602
}
],
"assumption": false
@@ -140,8 +140,8 @@
"kind": "request",
"evidence": [
{
"start_line": 264,
"end_line": 273
"start_line": 600,
"end_line": 605
}
],
"assumption": false
@@ -151,4 +151,4 @@
"metadata": {
"rationale": "왕복 누적과 풀 경합을 분리해 그렸다. 두 단계를 하나로 보면 28배가 어디서 왔는지 설명되지 않는다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
"title": "재시작이 지우는 것과 남기는 것",
"question": "롤링 재시작 뒤에도 로그인이 유지되는 이유는 무엇인가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"무중단 배포를 설계하는 운영 엔지니어"
],
@@ -13,11 +13,11 @@
"long_description": "롤링 재시작은 한 번에 한 파드씩 바꾼다. 파드가 죽으면 그 노드의 Infinispan sessions 캐시는 0 으로 초기화된다. 그러나 PostgreSQL 의 세션 행은 151개가 그대로 남았고 재시작 전에 발급한 refresh token 이 여전히 200 을 받는다. 전 구간 서비스 중단이 없었다. volatile 모드였다면 같은 재시작에서 400 Session not active 가 되어 전원 로그아웃이 된다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "A-8 · 롤링 재시작 — 세션은 살아남고 캐시만 사라진다",
"line": 272
"line": 612
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 270,
"end_line": 274
"start_line": 612,
"end_line": 614
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 270,
"end_line": 278
"start_line": 612,
"end_line": 618
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 270,
"end_line": 278
"start_line": 612,
"end_line": 618
}
],
"assumption": false
@@ -96,8 +96,8 @@
],
"evidence": [
{
"start_line": 270,
"end_line": 278
"start_line": 612,
"end_line": 618
}
],
"assumption": false
@@ -112,8 +112,8 @@
"kind": "write",
"evidence": [
{
"start_line": 270,
"end_line": 278
"start_line": 612,
"end_line": 618
}
],
"assumption": false
@@ -126,8 +126,8 @@
"kind": "read",
"evidence": [
{
"start_line": 270,
"end_line": 278
"start_line": 612,
"end_line": 618
}
],
"assumption": false
@@ -140,8 +140,8 @@
"kind": "read",
"evidence": [
{
"start_line": 270,
"end_line": 278
"start_line": 612,
"end_line": 618
}
],
"assumption": false
@@ -151,4 +151,4 @@
"metadata": {
"rationale": "캐시와 세션을 분리해 그렸다. 이 분리가 없으면 재시작 후 로그인이 유지되는 이유를 설명할 수 없다."
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
"title": "덮어쓰기를 만드는 기본키",
"question": "같은 사용자의 두 브라우저가 서로의 토큰을 지우는 이유는 무엇인가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"BFF 의 토큰 저장을 설계하는 백엔드 엔지니어"
],
@@ -13,11 +13,11 @@
"long_description": "토큰을 PostgreSQL 로 옮겨 다중 인스턴스 문제는 풀렸다. 그러나 기본키가 client_registration_id 와 principal_name 의 조합이고 세션 id 가 들어 있지 않다. 같은 사용자의 두 세션이 같은 행을 쓰므로 나중 로그인이 앞의 토큰을 덮어쓴다. 그리고 로그아웃하면 Redis 세션은 0 키로 정리되지만 PostgreSQL 에는 평문 refresh token 이 한 행 그대로 남는다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "B-2 · 저장소를 나눠 풀자 다른 두 문제가 남았다",
"line": 365
"line": 771
}
},
"composition": {
@@ -40,8 +40,8 @@
"details": [],
"evidence": [
{
"start_line": 360,
"end_line": 366
"start_line": 745,
"end_line": 747
}
],
"assumption": false
@@ -56,8 +56,8 @@
"details": [],
"evidence": [
{
"start_line": 360,
"end_line": 366
"start_line": 745,
"end_line": 747
}
],
"assumption": false
@@ -74,8 +74,8 @@
],
"evidence": [
{
"start_line": 370,
"end_line": 377
"start_line": 776,
"end_line": 781
}
],
"assumption": false
@@ -92,8 +92,8 @@
],
"evidence": [
{
"start_line": 378,
"end_line": 384
"start_line": 789,
"end_line": 794
}
],
"assumption": false
@@ -108,8 +108,8 @@
"kind": "write",
"evidence": [
{
"start_line": 370,
"end_line": 377
"start_line": 776,
"end_line": 781
}
],
"assumption": false
@@ -122,8 +122,8 @@
"kind": "write",
"evidence": [
{
"start_line": 370,
"end_line": 377
"start_line": 776,
"end_line": 781
}
],
"assumption": false
@@ -136,8 +136,8 @@
"kind": "blocked",
"evidence": [
{
"start_line": 378,
"end_line": 384
"start_line": 789,
"end_line": 794
}
],
"assumption": false
@@ -147,4 +147,4 @@
"metadata": {
"rationale": "저장소가 아니라 스키마가 원인이라는 것을 키로 보이게 그렸다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "revokeRefreshToken 을 켜고 refreshTokenMaxReuse 를 0 으로 둔 상태에서 같은 refresh token 으로 동시에 5건을 보냈다. 순차로 돌리면 재현되지 않으며 백그라운드로 띄우고 wait 해야 경합이 생긴다. 이긴 요청이 받은 새 토큰조차 쓸 수 없었다. Keycloak 이 경쟁을 감지하면 client session 을 지우기 때문이다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "B-3 · Refresh Token Rotation 경쟁 (Q2)",
"line": 391
"line": 806
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 369,
"end_line": 375
"start_line": 776,
"end_line": 781
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 369,
"end_line": 377
"start_line": 776,
"end_line": 781
}
],
"assumption": false
@@ -76,8 +76,8 @@
"details": [],
"evidence": [
{
"start_line": 378,
"end_line": 384
"start_line": 789,
"end_line": 794
}
],
"assumption": false
@@ -92,8 +92,8 @@
"details": [],
"evidence": [
{
"start_line": 378,
"end_line": 384
"start_line": 789,
"end_line": 794
}
],
"assumption": false
@@ -108,8 +108,8 @@
"kind": "request",
"evidence": [
{
"start_line": 369,
"end_line": 377
"start_line": 776,
"end_line": 781
}
],
"assumption": false
@@ -122,8 +122,8 @@
"kind": "blocked",
"evidence": [
{
"start_line": 378,
"end_line": 384
"start_line": 789,
"end_line": 794
}
],
"assumption": false
@@ -136,8 +136,8 @@
"kind": "blocked",
"evidence": [
{
"start_line": 378,
"end_line": 384
"start_line": 789,
"end_line": 794
}
],
"assumption": false
@@ -147,4 +147,4 @@
"metadata": {
"rationale": "실패가 진 요청에만 오지 않는다는 것을 그렸다. 재시도 설계가 여기서 갈린다."
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,23 +1,23 @@
{
"version": "1.1",
"id": "b4-header-trust-boundary",
"title": "지우지 않으면 통과한다",
"question": "Edge 가 넣어주는 인가 헤더를 앱이 믿어도 되는가",
"title": "헤더 도착과 인가 우회는 다르다",
"question": "같은 위조 헤더가 도착하는 것과 인가를 우회하는 것은 같은가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"Forward-Auth 구조를 운영하는 엔지니어"
],
"summary": "nginx 는 자기가 설정하지 않은 동명 헤더를 덮어쓰지 않는다. 먼저 지워야 한다.",
"alt": "밖에서 들어온 위조 헤더가 프록시를 그대로 통과해 앱에 닿는 구성. 프록시가 그 이름을 설정할 때만 덮어쓴다.",
"long_description": "nginx 는 자기가 proxy_set_header 로 설정한 헤더만 덮어쓴다. 설정하지 않은 이름은 클라이언트가 보낸 값이 그대로 지나간다. 그래서 X-Auth-Request-Roles 같은 헤더를 앱이 믿으려면 프록시가 그 이름을 먼저 빈 값으로 지워야 한다. 그리고 IdP 에서 클레임을 바꿔도 반영되지 않는다. 12회 요청 6초 동안 옛 값이 갔고 세션을 지워 재인증한 뒤에야 새 값이 왔다. 세션은 로그인 시점의 스냅샷이다.",
"summary": "동명 위조 헤더는 permitAll echo 까지 도착했지만 JWT 보호 경로는 401 이었다. 헤더 삭제는 이 실험대에서 적용하지 않은 미검증 처방이다.",
"alt": "외부 위조 헤더가 nginx 를 지나 permitAll echo 에서는 200으로 도착하지만 JWT 보호 경로에서는 401로 막히는 분기.",
"long_description": "위조 헤더는 app1.hyeonworks.com/api 의 permitAll echo 앱까지 그대로 도착했다. 같은 헤더로 JWT 를 요구하는 /api/me 와 /api/protected 를 호출하면 401이었다. 따라서 헤더 도착과 인가 우회는 다른 사건이다. proxy_set_header 로 먼저 지우는 것은 문서에 적힌 처방이지만 이 실험대에서는 적용하지 않아 미검증이다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "B-4 · Edge 인가의 범위 (Q4)",
"line": 401
"line": 821
}
},
"composition": {
@@ -36,14 +36,14 @@
"kind": "actor",
"role": "source",
"emphasis": "warning",
"description": "밖에서 들어온 요청이 앱이 믿는 헤더 이름을 그대로 쓴다.",
"description": "앱이 신뢰할 수 있는 이름과 같은 헤더를 보낸다.",
"details": [
"X-Auth-Request-Roles"
],
"evidence": [
{
"start_line": 393,
"end_line": 400
"start_line": 823,
"end_line": 830
}
],
"assumption": false
@@ -54,50 +54,50 @@
"kind": "gateway",
"role": "control",
"emphasis": "warning",
"description": "설정하지 않은 이름은 덮어쓰지 않는다.",
"description": "설정하지 않은 동명 헤더를 덮어쓰지 않는다.",
"details": [
"proxy_set_header ... \"\""
"동명 헤더 미삭제"
],
"evidence": [
{
"start_line": 393,
"end_line": 400
"start_line": 829,
"end_line": 830
}
],
"assumption": false
},
{
"id": "proxy",
"label": "oauth2-proxy",
"kind": "gateway",
"role": "control",
"id": "echo",
"label": "permitAll echo",
"kind": "service",
"role": "target",
"emphasis": "normal",
"description": "인증 결과를 헤더로 넣는다.",
"description": "위조 헤더가 도착하는 것을 관측한 경로다.",
"details": [
"--set-xauthrequest"
"/api/echo · HTTP 200"
],
"evidence": [
{
"start_line": 393,
"end_line": 400
"start_line": 825,
"end_line": 840
}
],
"assumption": false
},
{
"id": "app",
"label": "",
"id": "protected",
"label": "JWT 보호 경로",
"kind": "service",
"role": "target",
"emphasis": "primary",
"description": "헤더를 믿고 인가한다.",
"description": "같은 위조 헤더만으로는 통과하지 못했다.",
"details": [
"로그인 시점 스냅샷"
"/api/me · /api/protected · 401"
],
"evidence": [
{
"start_line": 401,
"end_line": 408
"start_line": 832,
"end_line": 840
}
],
"assumption": false
@@ -112,8 +112,22 @@
"kind": "request",
"evidence": [
{
"start_line": 393,
"end_line": 400
"start_line": 823,
"end_line": 830
}
],
"assumption": false
},
{
"id": "n-e",
"from": "nginx",
"to": "echo",
"label": "그대로 전달",
"kind": "request",
"evidence": [
{
"start_line": 825,
"end_line": 840
}
],
"assumption": false
@@ -121,27 +135,13 @@
{
"id": "n-p",
"from": "nginx",
"to": "proxy",
"label": "미삭제 시 통과",
"to": "protected",
"label": "같은 헤더",
"kind": "request",
"evidence": [
{
"start_line": 393,
"end_line": 400
}
],
"assumption": false
},
{
"id": "p-a",
"from": "proxy",
"to": "app",
"label": "인가 헤더",
"kind": "request",
"evidence": [
{
"start_line": 393,
"end_line": 408
"start_line": 832,
"end_line": 840
}
],
"assumption": false
@@ -149,6 +149,6 @@
],
"legend": [],
"metadata": {
"rationale": "위조 경로와 정상 경로가 같은 헤더 이름을 쓴다는 것을 그렸다. 지우는 단계가 없으면 둘을 구별할 수 없다."
"rationale": "위조 헤더의 도착과 인가 우회를 한 경로로 합치지 않고, 같은 입력이 permitAll 과 JWT 보호 경로에서 갈리는 것을 그렸다. 헤더 삭제 처방은 미검증이므로 노드로 확정하지 않았다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,23 +1,23 @@
{
"version": "1.1",
"id": "b5-b6-storage-and-keys",
"title": "볼륨 없는 영속화와 유예 없는 회전",
"question": "설정만으로 영속화와 무중단 키 교체가 되는가",
"title": "볼륨 없는 영속화와 replica별 키 회전",
"question": "설정을 켰다는 사실만으로 영속화와 일관된 키 회전을 기대할 수 있는가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"저장소와 키 회전을 운영하는 엔지니어"
],
"summary": "볼륨이 없으면 appendonly 설정은 장식이고, JWKS 캐시는 모르는 kid 를 만나면 곧바로 다시 가져온다.",
"alt": "Redis 의 데이터 디렉터리가 컨테이너 파일시스템일 때 영속화 설정이 무의미해지는 구성과, 새 kid 를 만난 검증기가 JWKS 를 재조회하는 구성.",
"long_description": "redis-cli config set appendonly yes 를 켜도 아무것도 달라지지 않았다. /data 가 컨테이너 파일시스템이라 컨테이너와 함께 죽기 때문이다. appendonlydir 이 만들어졌다가 그대로 버려진다. 볼륨 없는 영속화 설정은 장식이다. realm 키를 회전했을 때는 JWKS 캐시의 유예 구간을 기대했는데 없었다. NimbusJwtDecoder 는 모르는 kid 를 만나면 JWKS 를 다시 가져온다.",
"summary": "Redis AOF 는 볼륨이 없으면 컨테이너와 함께 사라진다. 키 회전 뒤에는 replica별 JWKS cache 상태가 달라 같은 옛 토큰이 401과 200을 번갈아 받았다.",
"alt": "운영 변경에서 Redis AOF는 볼륨 없는 데이터 디렉터리로 이어져 소멸하고, realm 키 회전은 replica별 JWKS cache 차이로 401과 200이 갈리는 두 경로.",
"long_description": "B-5에서는 appendonly yes 설정이 적용돼도 /data가 컨테이너 파일시스템이라 재시작 때 사라졌다. B-6에서는 옛 키를 지운 뒤 같은 토큰을 반복 요청하자 401과 200이 교대로 나왔다. echo replica 둘의 JWKS cache가 독립적이라 한쪽은 새 목록을 받아 옛 키를 잃었고 다른 쪽은 아직 들고 있었기 때문이다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "B-5 · B-6 — 저장소 상실과 키 회전",
"line": 415
"line": 859
}
},
"composition": {
@@ -31,71 +31,91 @@
"groups": [],
"nodes": [
{
"id": "config",
"label": "appendonly yes",
"id": "change",
"label": "운영 변경",
"kind": "process",
"role": "source",
"emphasis": "warning",
"description": "설정은 적용된다.",
"emphasis": "normal",
"description": "저장 설정과 realm 키 회전을 각각 수행한다.",
"details": [
"redis-cli config set"
"B-5 · B-6"
],
"evidence": [
{
"start_line": 406,
"end_line": 412
"start_line": 861,
"end_line": 872
}
],
"assumption": false
},
{
"id": "datadir",
"label": "/data",
"label": "Redis /data",
"kind": "datastore",
"role": "control",
"emphasis": "warning",
"description": "컨테이너 파일시스템이다.",
"description": "AOF가 기록되는 컨테이너 파일시스템이다.",
"details": [
"appendonlydir"
"appendonly yes · 볼륨 없음"
],
"evidence": [
{
"start_line": 406,
"end_line": 412
"start_line": 861,
"end_line": 863
}
],
"assumption": false
},
{
"id": "container",
"label": "컨테이너",
"id": "lost",
"label": "Redis 재시작",
"kind": "component",
"role": "target",
"emphasis": "warning",
"description": "죽으면 /data 도 같이 사라진다.",
"description": "볼륨이 없어 데이터가 함께 사라진다.",
"details": [
"볼륨 없음"
"영속화 실패"
],
"evidence": [
{
"start_line": 406,
"end_line": 412
"start_line": 861,
"end_line": 863
}
],
"assumption": false
},
{
"id": "volume",
"label": "PersistentVolume",
"kind": "datastore",
"role": "target",
"emphasis": "primary",
"description": "여기 있어야 설정이 뜻을 갖는다.",
"details": [],
"id": "replicas",
"label": "echo replica 2개",
"kind": "service",
"role": "control",
"emphasis": "warning",
"description": "JWKS cache를 인스턴스마다 따로 가진다.",
"details": [
"replica별 cache"
],
"evidence": [
{
"start_line": 406,
"end_line": 412
"start_line": 868,
"end_line": 872
}
],
"assumption": false
},
{
"id": "mixed",
"label": "교차 응답",
"kind": "component",
"role": "target",
"emphasis": "primary",
"description": "같은 옛 토큰의 검증 결과가 replica에 따라 갈린다.",
"details": [
"401 · 200 · 반복"
],
"evidence": [
{
"start_line": 868,
"end_line": 877
}
],
"assumption": false
@@ -104,42 +124,56 @@
"edges": [
{
"id": "c-d",
"from": "config",
"from": "change",
"to": "datadir",
"label": "AOF 기록",
"label": "AOF 설정",
"kind": "write",
"evidence": [
{
"start_line": 406,
"end_line": 412
"start_line": 861,
"end_line": 863
}
],
"assumption": false
},
{
"id": "d-c",
"id": "d-l",
"from": "datadir",
"to": "container",
"label": "컨테이너와 함께 소멸",
"to": "lost",
"label": "볼륨 없음",
"kind": "blocked",
"evidence": [
{
"start_line": 406,
"end_line": 412
"start_line": 861,
"end_line": 863
}
],
"assumption": false
},
{
"id": "d-v",
"from": "datadir",
"to": "volume",
"label": "볼륨 마운트",
"kind": "write",
"id": "c-r",
"from": "change",
"to": "replicas",
"label": "키 회전",
"kind": "request",
"evidence": [
{
"start_line": 406,
"end_line": 412
"start_line": 865,
"end_line": 872
}
],
"assumption": false
},
{
"id": "r-m",
"from": "replicas",
"to": "mixed",
"label": "cache 상태",
"kind": "request",
"evidence": [
{
"start_line": 868,
"end_line": 877
}
],
"assumption": false
@@ -147,6 +181,6 @@
],
"legend": [],
"metadata": {
"rationale": "설정과 매체를 분리해 그렸다. 설정만 보면 두 경우 모두 되어 있는 것으로 읽힌다."
"rationale": "B-5와 B-6를 같은 실패로 합치지 않고, 설정이 실제 매체와 인스턴스별 cache에 닿을 때 각각 어떤 결과가 나는지 두 갈래로 연결했다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,16 +8,16 @@
"audience": [
"Edge 인증 프록시를 운영하는 엔지니어"
],
"summary": "서버 상태가 없어 replica 간 공유 문제가 생기지 않는다. 대신 secret 이 단수라 겹침 구간을 만들 수 없다.",
"summary": "cookie-only 모드는 공유할 server state가 없지만 Redis session store를 켜면 쿠키에는 ticket, 서버에는 session이 남는다. secret 회전은 ticket을 무효화해 Redis session을 고아로 만들 수 있고, refresh가 꺼져 있을 때 TTL로 생성 시각을 역산할 수 있다.",
"alt": "세션이 쿠키에 담기고 replica 는 같은 Secret 만 읽는 구성. Redis 저장소를 켜면 쿠키에 티켓만 남고 서버에 세션이 생긴다.",
"long_description": "oauth2-proxy 는 세션 전체 쿠키에 담고 replica 같은 k8s Secret 을 읽을 뿐이다. 공유할 서버 상태가 없으니 콜백이 다른 replica 로 가도 된다. 대신 --cookie-secret 이 단수라 옛 secret 도 당분간 받아준다가 불가능하고 교체하는 순간 모든 쿠키가 한꺼번에 무효가 된다. Redis 저장소를 켜면 쿠키에는 티켓만 남는데, 티켓 안에 세션 id 가 있으므로 secret 을 바꾸면 어느 Redis 키를 지울지도 모르게 된다.",
"long_description": "cookie-only 모드에서는 세션 전체 쿠키에 있어 replica 같은 secret만 읽으면 된다. Redis session store를 켜면 쿠키에는 ticket만 남고 실제 session은 Redis에 생긴다. secret을 바꾸면 ticket을 풀 수 없어 어느 Redis key를 지울지 알 수 없고 고아 session이 남는다. refresh:disabled에서는 TTL이 생성 시각의 함수라 회전 시각과 비교해 고아를 골라낼 수 있지만 --cookie-refresh를 켜면 이 역산은 성립하지 않는다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "B-7 · B-7a — 쿠키에 담는 세션, 그리고 그 대가",
"line": 424
"line": 884
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 419,
"end_line": 428
"start_line": 863,
"end_line": 867
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 429,
"end_line": 438
"start_line": 923,
"end_line": 932
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 429,
"end_line": 440
"start_line": 923,
"end_line": 932
}
],
"assumption": false
@@ -96,8 +96,8 @@
],
"evidence": [
{
"start_line": 441,
"end_line": 452
"start_line": 935,
"end_line": 946
}
],
"assumption": false
@@ -112,8 +112,8 @@
"kind": "write",
"evidence": [
{
"start_line": 419,
"end_line": 432
"start_line": 919,
"end_line": 925
}
],
"assumption": false
@@ -126,8 +126,8 @@
"kind": "read",
"evidence": [
{
"start_line": 429,
"end_line": 438
"start_line": 923,
"end_line": 932
}
],
"assumption": false
@@ -140,8 +140,8 @@
"kind": "read",
"evidence": [
{
"start_line": 441,
"end_line": 452
"start_line": 935,
"end_line": 946
}
],
"assumption": false
@@ -151,4 +151,4 @@
"metadata": {
"rationale": "쿠키 저장과 Redis 저장을 한 축에 놓았다. 옮기는 순간 지울 수 없는 상태가 생긴다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "Spring Security 의 자동 구성은 세션을 서블릿 컨테이너 메모리에, authorized client 를 InMemoryOAuth2AuthorizedClientService 에 둔다. 조회 경로가 다른데 이름이 비슷해 하나로 오해하기 쉽다. AuthenticatedPrincipalOAuth2AuthorizedClientRepository 는 principal 이름으로 찾고 조회 키에 session id 가 없다. 그래서 SPRING_SESSION_STORE_TYPE 을 redis 로 바꿔 세션을 옮겨도 토큰은 인스턴스 메모리에 남는다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "609353e10bfd37a9bbb6a79ecf2a32f3d3c02d5d161879a14ad4713e49e7e5e8",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "B-1 · 세션만 Redis 로 옮기면 — 반쪽만 옮겨진다",
"line": 345
"line": 742
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 326,
"end_line": 331
"start_line": 719,
"end_line": 722
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 335,
"end_line": 341
"start_line": 727,
"end_line": 732
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 335,
"end_line": 341
"start_line": 727,
"end_line": 732
}
],
"assumption": false
@@ -96,8 +96,8 @@
],
"evidence": [
{
"start_line": 347,
"end_line": 351
"start_line": 744,
"end_line": 747
}
],
"assumption": false
@@ -114,8 +114,8 @@
],
"evidence": [
{
"start_line": 355,
"end_line": 358
"start_line": 773,
"end_line": 776
}
],
"assumption": false
@@ -130,8 +130,8 @@
"kind": "read",
"evidence": [
{
"start_line": 326,
"end_line": 331
"start_line": 719,
"end_line": 722
}
],
"assumption": false
@@ -144,8 +144,8 @@
"kind": "read",
"evidence": [
{
"start_line": 332,
"end_line": 341
"start_line": 725,
"end_line": 732
}
],
"assumption": false
@@ -158,8 +158,8 @@
"kind": "write",
"evidence": [
{
"start_line": 347,
"end_line": 351
"start_line": 744,
"end_line": 747
}
],
"assumption": false
@@ -172,8 +172,8 @@
"kind": "write",
"evidence": [
{
"start_line": 355,
"end_line": 358
"start_line": 773,
"end_line": 776
}
],
"assumption": false
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "C-1 에서 로그아웃이 다른 앱으로 퍼지지 않는 것을 관측했고 원인은 단순했다. 두 클라이언트 모두 backchannelLogoutUrl 이 비어 있어 Keycloak 은 부를 주소를 몰랐고, BFF 소스에는 oidcLogout 설정이 없어 주소를 알려줘도 받을 곳이 없었다. Keycloak 파드에서 앱 URL 로 요청하면 200 이 돌아오므로 네트워크 문제도 아니다. 설정이 빠졌다와 기능이 없다는 다르게 고쳐야 하는데 여기는 둘 다였다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "C층 — SSO 와 로그아웃 전파",
"line": 464
"line": 964
}
},
"composition": {
@@ -40,8 +40,8 @@
"details": [],
"evidence": [
{
"start_line": 458,
"end_line": 465
"start_line": 952,
"end_line": 957
}
],
"assumption": false
@@ -58,8 +58,8 @@
],
"evidence": [
{
"start_line": 458,
"end_line": 466
"start_line": 952,
"end_line": 966
}
],
"assumption": false
@@ -76,8 +76,8 @@
],
"evidence": [
{
"start_line": 458,
"end_line": 466
"start_line": 952,
"end_line": 966
}
],
"assumption": false
@@ -94,8 +94,8 @@
],
"evidence": [
{
"start_line": 458,
"end_line": 466
"start_line": 952,
"end_line": 966
}
],
"assumption": false
@@ -110,8 +110,8 @@
"kind": "request",
"evidence": [
{
"start_line": 458,
"end_line": 466
"start_line": 952,
"end_line": 966
}
],
"assumption": false
@@ -124,8 +124,8 @@
"kind": "blocked",
"evidence": [
{
"start_line": 458,
"end_line": 466
"start_line": 952,
"end_line": 966
}
],
"assumption": false
@@ -138,8 +138,8 @@
"kind": "blocked",
"evidence": [
{
"start_line": 458,
"end_line": 466
"start_line": 952,
"end_line": 966
}
],
"assumption": false
@@ -149,4 +149,4 @@
"metadata": {
"rationale": "IdP 쪽 결손과 앱 쪽 결손을 한 경로 위에 나란히 놓았다. 하나만 고치면 여전히 안 된다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "volatile 모드에서 로그인은 SQL 을 0개 쏜다. refresh 는 딱 한 문장을 쏘는데 CLIENT_SCOPE_CLIENT 의 선택적 스코프 조회이며, 그것도 첫 번째만 쏘고 이후 캐시된다. 그래서 DB 를 세웠을 때 완전 냉시동이면 클라이언트 조회부터 실패해 로그인이 400 이고, CLIENT 캐시만 더우면 refresh 가 500 이며, 완전히 더우면 둘 다 200 이다. A-7 이 표에 적은 것은 그 사이의 한 상태였다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "A-7 · A-7a — 전부 뒤집는 설정 하나, 그리고 그 표에도 조건이 있었다",
"line": 283
"line": 644
}
},
"composition": {
@@ -40,8 +40,8 @@
"details": [],
"evidence": [
{
"start_line": 284,
"end_line": 296
"start_line": 646,
"end_line": 657
}
],
"assumption": false
@@ -58,8 +58,8 @@
],
"evidence": [
{
"start_line": 306,
"end_line": 312
"start_line": 667,
"end_line": 673
}
],
"assumption": false
@@ -76,8 +76,8 @@
],
"evidence": [
{
"start_line": 284,
"end_line": 292
"start_line": 646,
"end_line": 652
}
],
"assumption": false
@@ -94,8 +94,8 @@
],
"evidence": [
{
"start_line": 296,
"end_line": 302
"start_line": 657,
"end_line": 662
}
],
"assumption": false
@@ -110,8 +110,8 @@
"kind": "read",
"evidence": [
{
"start_line": 306,
"end_line": 312
"start_line": 667,
"end_line": 673
}
],
"assumption": false
@@ -124,8 +124,8 @@
"kind": "read",
"evidence": [
{
"start_line": 284,
"end_line": 292
"start_line": 646,
"end_line": 652
}
],
"assumption": false
@@ -138,8 +138,8 @@
"kind": "read",
"evidence": [
{
"start_line": 296,
"end_line": 302
"start_line": 657,
"end_line": 662
}
],
"assumption": false
@@ -149,4 +149,4 @@
"metadata": {
"rationale": "세 결과를 나열하는 대신 그 결과를 만드는 조회 두 개를 그렸다. 캐시가 그 조회를 삼키면 결과가 바뀐다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
"title": "게스트가 하드웨어에 닿는 갈래는 셋이고 깊이가 다르다",
"question": "게스트의 CPU 실행·virtio I/O·직접 할당한 장치는 각각 어디까지 내려가고 호스트 유저공간을 지나는가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"가상화 구조를 처음 읽는 사람",
"이 실험대가 무엇 위에서 도는지 알아야 하는 사람"
@@ -14,11 +14,11 @@
"long_description": "위에서 아래로 읽는다. 맨 위 QEMU 에서 갈래 셋이 갈린다. 왼쪽 CPU 갈래는 KVM_RUN 으로 커널의 KVM 에 들어가고 KVM 이 VM entry 로 물리 CPU 에 올리며 게스트 코드가 VMX non-root 로 거기서 돈다. 물리 CPU 상자에 VM exit 이 함께 적혀 있고 나온 이유는 KVM 상자의 struct kvm_run 으로 유저공간에 돌아간다. 가운데 패스스루 갈래는 QEMU 가 VFIO 로 장치 fd 를 받고 vfio-pci 가 장치를 묶으며 그 장치의 DMA 가 IOMMU 를 지난다. 오른쪽 virtio 갈래는 virtqueue 가 게스트 드라이버에 닿고 같은 virtqueue 를 커널 vhost 와 나누는 길이 따로 있다. 앞의 둘은 이 실험대가 쓰고 패스스루는 쓰지 않는다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "0ae5674723f25dc85d5069529890f07be1a11b768b56901103fa3ea6ac3dcf55",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "이 층 아래의 구조 — 조사한 것",
"line": 1091
"line": 3430
}
},
"composition": {
@@ -46,8 +46,8 @@
"description": "KVM 핸들을 열고 VM 과 vCPU 를 만드는 유저공간 프로세스. vcpu ioctl 은 그 vcpu 를 만든 스레드에서 낸다.",
"evidence": [
{
"start_line": 1102,
"end_line": 1108
"start_line": 3441,
"end_line": 3447
}
],
"assumption": false
@@ -66,8 +66,8 @@
"description": "IOMMU 보호 아래 장치 접근을 유저공간에 여는 프레임워크. 소유 단위가 IOMMU 그룹이다.",
"evidence": [
{
"start_line": 1131,
"end_line": 1146
"start_line": 3470,
"end_line": 3485
}
],
"assumption": false
@@ -84,12 +84,12 @@
"description": "호스트 드라이버에서 떼어 vfio-pci 에 묶은 장치.",
"evidence": [
{
"start_line": 1131,
"end_line": 1133
"start_line": 3470,
"end_line": 3472
},
{
"start_line": 1144,
"end_line": 1146
"start_line": 3483,
"end_line": 3485
}
],
"assumption": false
@@ -106,8 +106,8 @@
"description": "장치가 아무 메모리나 건드리지 못하게 막는 리매핑 장치.",
"evidence": [
{
"start_line": 1144,
"end_line": 1146
"start_line": 3483,
"end_line": 3485
}
],
"assumption": false
@@ -125,8 +125,8 @@
"description": "mmap 한 공유 메모리로 나온 이유를 유저공간에 알리는 커널 쪽.",
"evidence": [
{
"start_line": 1105,
"end_line": 1108
"start_line": 3444,
"end_line": 3447
}
],
"assumption": false
@@ -145,8 +145,8 @@
"description": "VM entry 때 guest-state 영역에서 상태를 싣고 VM exit 때 그리로 저장하는 프로세서.",
"evidence": [
{
"start_line": 1111,
"end_line": 1113
"start_line": 3450,
"end_line": 3452
}
],
"assumption": false
@@ -163,8 +163,8 @@
"description": "VMX non-root 로 물리 CPU 에서 도는 게스트 명령.",
"evidence": [
{
"start_line": 1111,
"end_line": 1113
"start_line": 3450,
"end_line": 3452
}
],
"assumption": false
@@ -182,8 +182,8 @@
"description": "게스트가 보는 반가상화 장치의 드라이버. 주고받는 통로가 virtqueue 다.",
"evidence": [
{
"start_line": 1116,
"end_line": 1118
"start_line": 3455,
"end_line": 3457
}
],
"assumption": false
@@ -201,8 +201,8 @@
"description": "virtqueue 를 QEMU 밖과 나누는 커널 구현. vhost-user 규약이 이것을 제어하는 ioctl 인터페이스를 보완한다.",
"evidence": [
{
"start_line": 1125,
"end_line": 1129
"start_line": 3464,
"end_line": 3468
}
],
"assumption": false
@@ -218,8 +218,8 @@
"style": "solid",
"evidence": [
{
"start_line": 1138,
"end_line": 1141
"start_line": 3477,
"end_line": 3480
}
],
"assumption": false
@@ -233,8 +233,8 @@
"style": "solid",
"evidence": [
{
"start_line": 1144,
"end_line": 1146
"start_line": 3483,
"end_line": 3485
}
],
"assumption": false
@@ -248,8 +248,8 @@
"style": "solid",
"evidence": [
{
"start_line": 1144,
"end_line": 1146
"start_line": 3483,
"end_line": 3485
}
],
"assumption": false
@@ -263,8 +263,8 @@
"style": "solid",
"evidence": [
{
"start_line": 1104,
"end_line": 1106
"start_line": 3443,
"end_line": 3445
}
],
"assumption": false
@@ -278,8 +278,8 @@
"style": "solid",
"evidence": [
{
"start_line": 1111,
"end_line": 1112
"start_line": 3450,
"end_line": 3451
}
],
"assumption": false
@@ -293,8 +293,8 @@
"style": "solid",
"evidence": [
{
"start_line": 1111,
"end_line": 1112
"start_line": 3450,
"end_line": 3451
}
],
"assumption": false
@@ -308,12 +308,12 @@
"style": "solid",
"evidence": [
{
"start_line": 1116,
"end_line": 1118
"start_line": 3455,
"end_line": 3457
},
{
"start_line": 1128,
"end_line": 1128
"start_line": 3467,
"end_line": 3467
}
],
"assumption": false
@@ -327,8 +327,8 @@
"style": "dashed",
"evidence": [
{
"start_line": 1125,
"end_line": 1129
"start_line": 3464,
"end_line": 3468
}
],
"assumption": false
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
"title": "방향에 따라 갈리는 업그레이드",
"question": "되돌릴 수 있는지는 무엇이 결정하는가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"버전 업그레이드를 계획하는 운영 엔지니어"
],
@@ -13,11 +13,11 @@
"long_description": "26.7.0 에서 26.7.3 으로 올리는 것은 무중단이었고 87회 요청이 전부 200 이었다. 되돌리기는 Liquibase 가 체크섬 검증에서 막았다. 새 버전이 남긴 체크섬을 옛 버전이 거부하기 때문이다. 그런데 서비스는 살아 있었다. StatefulSet 롤링 업데이트가 첫 파드에서 멈추고 나머지를 건드리지 않았기 때문이다. 롤백 불가는 조건부이며 판단 기준은 databasechangelog 의 행 수가 업그레이드 전후로 같은가이다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "D-1 · D-2 — 백업과 업그레이드",
"line": 481
"line": 986
}
},
"composition": {
@@ -40,8 +40,8 @@
"details": [],
"evidence": [
{
"start_line": 483,
"end_line": 490
"start_line": 988,
"end_line": 995
}
],
"assumption": false
@@ -58,8 +58,8 @@
],
"evidence": [
{
"start_line": 483,
"end_line": 491
"start_line": 988,
"end_line": 996
}
],
"assumption": false
@@ -76,8 +76,8 @@
],
"evidence": [
{
"start_line": 492,
"end_line": 497
"start_line": 997,
"end_line": 999
}
],
"assumption": false
@@ -92,8 +92,8 @@
"details": [],
"evidence": [
{
"start_line": 492,
"end_line": 498
"start_line": 997,
"end_line": 1003
}
],
"assumption": false
@@ -110,8 +110,8 @@
],
"evidence": [
{
"start_line": 499,
"end_line": 505
"start_line": 1006,
"end_line": 1008
}
],
"assumption": false
@@ -126,8 +126,8 @@
"kind": "request",
"evidence": [
{
"start_line": 483,
"end_line": 491
"start_line": 988,
"end_line": 996
}
],
"assumption": false
@@ -140,8 +140,8 @@
"kind": "blocked",
"evidence": [
{
"start_line": 492,
"end_line": 497
"start_line": 997,
"end_line": 999
}
],
"assumption": false
@@ -154,8 +154,8 @@
"kind": "request",
"evidence": [
{
"start_line": 492,
"end_line": 498
"start_line": 997,
"end_line": 1003
}
],
"assumption": false
@@ -168,8 +168,8 @@
"kind": "read",
"evidence": [
{
"start_line": 499,
"end_line": 505
"start_line": 1006,
"end_line": 1008
}
],
"assumption": false
@@ -179,4 +179,4 @@
"metadata": {
"rationale": "체크섬 검증과 롤링 업데이트를 함께 그렸다. 하나는 막고 하나는 피해를 줄인다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
"title": "base64 는 암호화가 아니다",
"question": "Secret 에 넣으면 값이 가려지는가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"쿠버네티스에 비밀을 두는 엔지니어"
],
@@ -13,11 +13,11 @@
"long_description": "kubectl get secret -o yaml 이 보여주는 base64 는 인코딩이지 암호화가 아니다. etcd 에 평문으로 있다. 그리고 파드 안에서 env 를 grep 하면 그대로 나온다. 값을 Secret 에 넣었다는 것과 값이 가려졌다는 것은 다른 사건이다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "D-3 · 비밀",
"line": 504
"line": 1015
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 495,
"end_line": 501
"start_line": 1002,
"end_line": 1006
}
],
"assumption": false
@@ -58,8 +58,8 @@
"details": [],
"evidence": [
{
"start_line": 495,
"end_line": 501
"start_line": 1002,
"end_line": 1006
}
],
"assumption": false
@@ -76,8 +76,8 @@
],
"evidence": [
{
"start_line": 495,
"end_line": 501
"start_line": 1002,
"end_line": 1006
}
],
"assumption": false
@@ -92,8 +92,8 @@
"kind": "write",
"evidence": [
{
"start_line": 495,
"end_line": 501
"start_line": 1002,
"end_line": 1006
}
],
"assumption": false
@@ -106,8 +106,8 @@
"kind": "write",
"evidence": [
{
"start_line": 495,
"end_line": 501
"start_line": 1002,
"end_line": 1006
}
],
"assumption": false
@@ -117,4 +117,4 @@
"metadata": {
"rationale": "인코딩과 암호화를 갈랐다. 두 경로 모두 끝이 평문이다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "훅이 없을 때 새 인증서가 디스크에 기록된 08:20:27 과 실제로 서빙된 08:58:52 사이가 2305초였고, 그것도 사람이 nginx -s reload 를 쳤기 때문이다. deploy 훅을 넣자 발급에서 서빙까지 1~2초가 됐다. 판정은 문구가 아니라 워커 PID 로 한다. certbot 이 Hook 'deploy-hook' ran with error output 이라고 찍지만 실패가 아니며 nginx 의 types_hash 경고가 stderr 로 나갔을 뿐이다. 로그에서 error 를 grep 하는 감시는 성공한 훅을 실패로 오독한다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "D-4 · D-4a — 인증서, 그리고 이 실험대 최대의 발견",
"line": 509
"line": 1025
}
},
"composition": {
@@ -40,8 +40,8 @@
"details": [],
"evidence": [
{
"start_line": 560,
"end_line": 570
"start_line": 1077,
"end_line": 1088
}
],
"assumption": false
@@ -58,8 +58,8 @@
],
"evidence": [
{
"start_line": 560,
"end_line": 575
"start_line": 1077,
"end_line": 1092
}
],
"assumption": false
@@ -76,8 +76,8 @@
],
"evidence": [
{
"start_line": 571,
"end_line": 580
"start_line": 1089,
"end_line": 1097
}
],
"assumption": false
@@ -94,8 +94,8 @@
],
"evidence": [
{
"start_line": 560,
"end_line": 580
"start_line": 1077,
"end_line": 1097
}
],
"assumption": false
@@ -110,8 +110,8 @@
"kind": "request",
"evidence": [
{
"start_line": 560,
"end_line": 575
"start_line": 1077,
"end_line": 1092
}
],
"assumption": false
@@ -124,8 +124,8 @@
"kind": "request",
"evidence": [
{
"start_line": 560,
"end_line": 580
"start_line": 1077,
"end_line": 1097
}
],
"assumption": false
@@ -138,8 +138,8 @@
"kind": "request",
"evidence": [
{
"start_line": 571,
"end_line": 580
"start_line": 1089,
"end_line": 1097
}
],
"assumption": false
@@ -149,4 +149,4 @@
"metadata": {
"rationale": "훅의 유무를 한 축에 놓고, 판정 신호를 워커 PID 로 명시했다. 로그 문구로 판정하면 틀린다."
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -13,11 +13,11 @@
"long_description": "왼쪽부터 kubectl, keycloak-0-60375, keycloak-1-36736, JGROUPS_PING, keycloak-0-16105 다섯 참여자가 생명선으로 선다. 1번 메시지에서 kubectl 이 keycloak-0-60375 를 --grace-period=0 --force 로 끊는다. 2번에서 07:40:28 에 남은 노드 keycloak-1-36736 의 뷰에서 그 노드가 빠진다. 3번이 이 그림의 중심이다 — 같은 07:40:28 에 JGROUPS_PING 의 행을 지우는 화살표가 keycloak-1-36736 에서 나온다. 강제 종료로 이미 끊긴 keycloak-0-60375 의 생명선에서는 테이블로 가는 화살표가 없다. 4번에서 20초 뒤 07:40:48 에 keycloak-0-16105 가 합류한다. 돌아온 노드의 이름은 접두사 keycloak-0 만 같고 접미사가 다르다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "88a081390dff22cd4171d5f9b534e46a37f6295c3b2652bc4c6fff0b578d5c6b",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "관측 (observed)",
"line": 892
"line": 2963
}
},
"composition": {
@@ -39,8 +39,8 @@
"description": "파드를 정상 종료와 강제 종료로 끊는 쪽.",
"evidence": [
{
"start_line": 886,
"end_line": 888
"start_line": 2957,
"end_line": 2959
}
],
"assumption": false
@@ -57,8 +57,8 @@
"description": "강제 종료로 끊긴 노드. 자기 행을 지우고 나갈 틈이 없다.",
"evidence": [
{
"start_line": 898,
"end_line": 901
"start_line": 2969,
"end_line": 2972
}
],
"assumption": false
@@ -75,8 +75,8 @@
"description": "남아 있는 코디네이터. 정리 주체다.",
"evidence": [
{
"start_line": 914,
"end_line": 916
"start_line": 2985,
"end_line": 2987
}
],
"assumption": false
@@ -89,8 +89,8 @@
"description": "노드 행이 등록되는 발견 테이블.",
"evidence": [
{
"start_line": 888,
"end_line": 888
"start_line": 2959,
"end_line": 2959
}
],
"assumption": false
@@ -106,8 +106,8 @@
"description": "복구 뒤 올라온 노드. 접두사만 같다.",
"evidence": [
{
"start_line": 918,
"end_line": 920
"start_line": 2989,
"end_line": 2991
}
],
"assumption": false
@@ -123,8 +123,8 @@
"order": 1,
"evidence": [
{
"start_line": 886,
"end_line": 888
"start_line": 2957,
"end_line": 2959
}
],
"assumption": false
@@ -139,8 +139,8 @@
"order": 2,
"evidence": [
{
"start_line": 906,
"end_line": 907
"start_line": 2977,
"end_line": 2978
}
],
"assumption": false
@@ -155,12 +155,12 @@
"order": 3,
"evidence": [
{
"start_line": 897,
"end_line": 898
"start_line": 2968,
"end_line": 2969
},
{
"start_line": 914,
"end_line": 916
"start_line": 2985,
"end_line": 2987
}
],
"assumption": false
@@ -175,8 +175,8 @@
"order": 4,
"evidence": [
{
"start_line": 908,
"end_line": 909
"start_line": 2979,
"end_line": 2980
}
],
"assumption": false
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,31 +4,31 @@
"title": "게스트 두 대는 호스트에서 qemu 프로세스 두 개다",
"question": "호스트에서 게스트는 무엇으로 존재하고, 게스트가 보는 장치는 누가 만들어 주는가",
"type": "deployment",
"direction": "TB",
"direction": "LR",
"audience": [
"실험대를 운영하는 사람",
"호스트와 게스트의 측정값을 대조하는 사람"
],
"summary": "libvirtd 가 게스트마다 qemu-system-x86_64 를 띄우고, 게스트가 보는 디스크와 네트워크는 그 프로세스가 virtio 로 내주며, 두 프로세스는 machine.slice 아래에서 메모리 상한을 받는다.",
"alt": "호스트 안에 libvirtd·machine.slice·virbr0 와 qemu 프로세스 두 개가 있고, 프로세스가 게스트 하나씩을 담는 배치도.",
"long_description": "위에서 아래로 읽는다. 호스트 test-server 경계 안에 libvirtd 있고 그 아래에 qemu-system-x86_64 프로세스가 게스트 수만큼 있다. 경계 밖 아래쪽에 게스트 kc-lab-1 과 kc-lab-2 가 각각 따로 있고, 게스트가 보는 디스크와 인터페이스는 자기를 담은 프로세스 virtio 로 내준다. 프로세스에 적힌 RSS 와 게스트에 적힌 available 은 같은 메모리를 다른 껍질에서 읽은 값이다. machine.slice 와 virbr0 는 이 그림에 넣지 않았고 본문이 맡는다.",
"summary": "호스트에서는 게스트 두 대가 qemu-system-x86_64 프로세스 두 개로 존재하고, 각 프로세스가 virtio 장치를 게스트 커널에 내준다.",
"alt": "호스트 영역의 libvirtd qemu 프로세스 두 개를 만들고, 프로세스 집합이 virtio 경계를 넘어 Debian 게스트 커널 두 개를 제공하는 구조.",
"long_description": "호스트 test-server에 libvirtd가 게스트마다 qemu-system-x86_64 프로세스를 하나씩 띄운다. kc-lab-1과 kc-lab-2는 호스트에서 qemu 프로세스 두 개로 보이며 RSS는 호스트 관점의 값이다. 각 프로세스 virtio 디스크와 네트워크를 게스트에 제공하고, 게스트 안에서는 Debian 커널이 자기 메모리와 인터페이스를 본다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "0ae5674723f25dc85d5069529890f07be1a11b768b56901103fa3ea6ac3dcf55",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "0층. 가상화 — 「바닥」 아래에 있는 것",
"line": 935
"line": 3274
}
},
"composition": {
"profile": "component-flow",
"profile": "two-zone-pipeline",
"diagram_only": true,
"reference_ids": [
"payment-event-flow"
"localization-pipeline"
],
"rationale": "게스트가 호스트에서 어떻게 존재하는지는 libvirtd 에서 qemu 프로세스로, 다시 게스트 커널로 내려가는 한 방향 경로다. sequence 는 이 절에 시각 순서가 없어서 맞지 않고, orchestrator-workers 는 게스트 둘을 교체 가능한 워커로 그리는데 둘은 할당량도 역할도 다르다.",
"focus_node": "qemu-kc-lab-1"
"rationale": "호스트에서 관리되는 qemu 프로세스 집합과 게스트 커널 집합을 두 영역으로 나누고 virtio 경계만 보인다.",
"focus_node": "qemu-pairs"
},
"groups": [
{
@@ -36,51 +36,29 @@
"label": "호스트 test-server",
"kind": "system",
"role": "zone",
"description": "libvirtd 게스트 프로세스가 함께 도는 호스트 경계.",
"description": "libvirtd와 qemu-system-x86_64 프로세스가 도는 영역.",
"evidence": [
{
"start_line": 946,
"end_line": 948
},
{
"start_line": 956,
"end_line": 956
"start_line": 3285,
"end_line": 3295
}
],
"assumption": false
},
{
"id": "guest-1-zone",
"label": "게스트 kc-lab-1",
"id": "guest-zone",
"label": "게스트 VM",
"kind": "system",
"role": "zone",
"description": "qemu 프로세스 하나가 담는 Debian 게스트.",
"description": "kc-lab-1과 kc-lab-2의 Debian 커널이 도는 영역.",
"evidence": [
{
"start_line": 987,
"end_line": 990
"start_line": 3326,
"end_line": 3337
},
{
"start_line": 1039,
"end_line": 1039
}
],
"assumption": false
},
{
"id": "guest-2-zone",
"label": "게스트 kc-lab-2",
"kind": "system",
"role": "zone",
"description": "나머지 qemu 프로세스가 담는 Debian 게스트.",
"evidence": [
{
"start_line": 1033,
"end_line": 1033
},
{
"start_line": 1040,
"end_line": 1040
"start_line": 3378,
"end_line": 3379
}
],
"assumption": false
@@ -93,129 +71,63 @@
"kind": "service",
"role": "source",
"group": "host-zone",
"shape": "box",
"emphasis": "normal",
"description": "게스트마다 qemu-system-x86_64 프로세스를 띄운다.",
"details": [
"qemu:///system"
],
"description": "게스트마다 qemu-system-x86_64 를 하나씩 띄우는 호스트 데몬. htop 트리 뷰에서 그 아래에 게스트 프로세스가 달린다.",
"evidence": [
{
"start_line": 946,
"end_line": 948
},
{
"start_line": 956,
"end_line": 956
"start_line": 3285,
"end_line": 3295
}
],
"assumption": false
},
{
"id": "qemu-kc-lab-1",
"label": "qemu-system-x86_64",
"id": "qemu-pairs",
"label": "qemu 프로세스 2개",
"kind": "service",
"role": "service",
"group": "host-zone",
"shape": "box",
"emphasis": "primary",
"description": "각 프로세스 하나가 게스트 하나를 담는다.",
"details": [
"kc-lab-1",
"vCPU 2",
"할당 3584MB",
"RSS 3765MB"
"kc-lab-1 · 3584MB · RSS 3765MB",
"kc-lab-2 · 2560MB · RSS 2633~2670MB"
],
"description": "kc-lab-1 게스트 전체가 들어 있는 호스트 프로세스. vCPU 는 이 프로세스의 스레드이고 RSS 는 게스트가 터치한 페이지만큼이다.",
"evidence": [
{
"start_line": 946,
"end_line": 948
"start_line": 3326,
"end_line": 3326
},
{
"start_line": 987,
"end_line": 987
},
{
"start_line": 1032,
"end_line": 1032
"start_line": 3371,
"end_line": 3372
}
],
"assumption": false
},
{
"id": "qemu-kc-lab-2",
"label": "qemu-system-x86_64",
"id": "guest-kernels",
"label": "Debian 게스트 커널 2개",
"kind": "service",
"role": "service",
"group": "host-zone",
"shape": "box",
"role": "target",
"group": "guest-zone",
"emphasis": "primary",
"description": "게스트는 qemu가 내준 virtio 장치와 자기 메모리만 본다.",
"details": [
"kc-lab-2",
"할당 2560MB",
"RSS 2633MB"
"kc-lab-1 · 192.168.122.11 · available 1959MB",
"kc-lab-2 · 192.168.122.12 · available 1899MB"
],
"description": "kc-lab-2 게스트 전체가 들어 있는 호스트 프로세스. A-4 의 virsh destroy 가 끊는 것이 이 프로세스다.",
"evidence": [
{
"start_line": 946,
"end_line": 948
"start_line": 3333,
"end_line": 3337
},
{
"start_line": 967,
"end_line": 976
},
{
"start_line": 1033,
"end_line": 1033
}
],
"assumption": false
},
{
"id": "guest-kernel-1",
"label": "Debian 게스트 커널",
"kind": "service",
"role": "sink",
"group": "guest-1-zone",
"shape": "box",
"details": [
"enp1s0 · 192.168.122.11",
"총 3423MB",
"available 1959MB"
],
"description": "virtio 장치만 보는 게스트. free 가 읽는 값이 여기 있다.",
"evidence": [
{
"start_line": 994,
"end_line": 998
},
{
"start_line": 1039,
"end_line": 1039
}
],
"assumption": false
},
{
"id": "guest-kernel-2",
"label": "Debian 게스트 커널",
"kind": "service",
"role": "sink",
"group": "guest-2-zone",
"shape": "box",
"details": [
"enp1s0 · 192.168.122.12",
"총 2480MB",
"available 1899MB"
],
"description": "같은 방식으로 붙은 두 번째 게스트.",
"evidence": [
{
"start_line": 994,
"end_line": 998
},
{
"start_line": 1040,
"end_line": 1040
"start_line": 3378,
"end_line": 3379
}
],
"assumption": false
@@ -223,77 +135,29 @@
],
"edges": [
{
"id": "libvirtd-spawns-1",
"id": "spawn",
"from": "libvirtd",
"to": "qemu-kc-lab-1",
"to": "qemu-pairs",
"label": "프로세스 생성",
"kind": "control",
"style": "solid",
"evidence": [
{
"start_line": 946,
"end_line": 948
},
{
"start_line": 956,
"end_line": 956
"start_line": 3285,
"end_line": 3295
}
],
"assumption": false
},
{
"id": "libvirtd-spawns-2",
"from": "libvirtd",
"to": "qemu-kc-lab-2",
"label": "프로세스 생성",
"kind": "control",
"style": "solid",
"evidence": [
{
"start_line": 946,
"end_line": 948
},
{
"start_line": 956,
"end_line": 956
}
],
"assumption": false
},
{
"id": "qemu1-virtio",
"from": "qemu-kc-lab-1",
"to": "guest-kernel-1",
"label": "virtio 디스크 · virtio-net",
"id": "virtio",
"from": "qemu-pairs",
"to": "guest-kernels",
"label": "virtio 장치",
"kind": "data",
"style": "solid",
"evidence": [
{
"start_line": 987,
"end_line": 990
},
{
"start_line": 994,
"end_line": 998
}
],
"assumption": false
},
{
"id": "qemu2-virtio",
"from": "qemu-kc-lab-2",
"to": "guest-kernel-2",
"label": "virtio 디스크 · virtio-net",
"kind": "data",
"style": "solid",
"evidence": [
{
"start_line": 987,
"end_line": 990
},
{
"start_line": 994,
"end_line": 998
"start_line": 3326,
"end_line": 3337
}
],
"assumption": false
@@ -301,6 +165,6 @@
],
"legend": [],
"metadata": {
"rationale": "배치도로 고른 이유는 이 절이 주장하는 것이 순서가 아니라 담김이기 때문이다 — 게스트 하나가 호스트 프로세스 하나 안에 있고, 스트가 보는 장치는 그 프로세스가 내준다. 메모리를 읽는 곳 셋과 machine.slice·virbr0 는 본문 표와 문단이 맡는다."
"rationale": "개별 VM마다 같은 qemu→guest 경로를 두 번 반복하지 않고, 스트 프로세스 집합과 게스트 커널 집합의 대응을 한 경계로 묶었다."
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "이 실험대에서 아홉 번의 주입이 조용히 실패했다. conntrack 이 ESTABLISHED 연결을 통과시키고, kubectl delete --force 가 크래시가 아니며, PID 1 이 SIGKILL 을 무시하고, kube-router 가 iptables 체인을 다시 끼워 넣는다. 모두 아무 일도 없었다로 보였다. 그래서 주입 뒤에는 결과를 보기 전에 대상이 실제로 그 상태인지를 cluster_size, 워커 PID, conntrack 표, 패킷 카운터로 확인한다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "주입이 먹지 않는다 — 아홉 번, 전부 조용히",
"line": 113
"line": 296
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 109,
"end_line": 112
"start_line": 292,
"end_line": 294
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 128,
"end_line": 133
"start_line": 311,
"end_line": 316
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 128,
"end_line": 131
"start_line": 311,
"end_line": 313
}
],
"assumption": false
@@ -94,8 +94,8 @@
"kind": "request",
"evidence": [
{
"start_line": 128,
"end_line": 133
"start_line": 311,
"end_line": 316
}
],
"assumption": false
@@ -108,8 +108,8 @@
"kind": "request",
"evidence": [
{
"start_line": 128,
"end_line": 133
"start_line": 311,
"end_line": 316
}
],
"assumption": false
@@ -119,4 +119,4 @@
"metadata": {
"rationale": "아홉 번의 실패가 전부 같은 자리에서 생겼다. 주입과 관측 사이가 비어 있었다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
"title": "실험대의 구성",
"question": "이 실험들은 어떤 기계 위에서 돌았는가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"같은 실험을 재현하려는 백엔드 엔지니어"
],
@@ -13,20 +13,20 @@
"long_description": "베어메탈 test-server 위에 VM 두 대를 올렸다. kc-lab-1 이 k3s server 이자 컨트롤 플레인이고 keycloak-1 이 여기 있다. kc-lab-2 는 agent 이며 keycloak-0 과 PostgreSQL, Redis 가 함께 있다. 저장소가 한 노드에 몰려 있어 그 노드를 잃으면 DB 도 같이 잃는다. 호스트 nginx 가 Let's Encrypt 인증서로 TLS 를 끝내고 traefik 으로 넘기며, 인증서 하나에 auth·app1·app2 세 이름이 SAN 으로 들어 있다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "실험대",
"line": 85
"line": 120
}
},
"composition": {
"profile": "query-fanout",
"profile": "component-flow",
"diagram_only": true,
"reference_ids": [
"metrics-query-fanout"
"payment-event-flow"
],
"rationale": "호스트 nginx 에서 들어온 요청이 traefik 을 거쳐 두 Keycloak 노드로 갈라지는 것이 이 절의 지배적 구조다. 하나의 진입점에서 여러 백엔드로 퍼지는 모양이므로 query-fanout 을 골랐다."
"rationale": "호스트 nginx에서 traefik과 Keycloak, 저장소로 이어지는 실제 배치 경로가 지배적 질문이다. current context가 선택한 component-flow 문법을 사용한다."
},
"groups": [],
"nodes": [
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 88,
"end_line": 92
"start_line": 123,
"end_line": 127
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 85,
"end_line": 87
"start_line": 120,
"end_line": 122
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 85,
"end_line": 86
"start_line": 120,
"end_line": 120
}
],
"assumption": false
@@ -96,8 +96,8 @@
],
"evidence": [
{
"start_line": 86,
"end_line": 87
"start_line": 122,
"end_line": 122
}
],
"assumption": false
@@ -114,8 +114,8 @@
],
"evidence": [
{
"start_line": 86,
"end_line": 87
"start_line": 122,
"end_line": 122
}
],
"assumption": false
@@ -130,8 +130,8 @@
"kind": "request",
"evidence": [
{
"start_line": 88,
"end_line": 92
"start_line": 123,
"end_line": 127
}
],
"assumption": false
@@ -144,8 +144,8 @@
"kind": "request",
"evidence": [
{
"start_line": 85,
"end_line": 87
"start_line": 120,
"end_line": 122
}
],
"assumption": false
@@ -158,8 +158,8 @@
"kind": "request",
"evidence": [
{
"start_line": 85,
"end_line": 87
"start_line": 120,
"end_line": 122
}
],
"assumption": false
@@ -172,8 +172,8 @@
"kind": "read",
"evidence": [
{
"start_line": 86,
"end_line": 87
"start_line": 122,
"end_line": 122
}
],
"assumption": false
@@ -183,4 +183,4 @@
"metadata": {
"rationale": "저장소가 kc-lab-2 한 곳에 몰린 것을 그림에서 읽히게 배치했다. A-4 의 두 결과가 이 배치에서 갈린다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
"title": "대조군 없이는 귀속할 수 없다",
"question": "관측된 이상을 무엇의 탓으로 돌릴 수 있는가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"장애 실험의 결과를 해석하는 엔지니어"
],
@@ -13,11 +13,11 @@
"long_description": "D-4 에서 주입 전에 900건을 재서 오류 0을 확인했다. 그래서 갱신 중 비200 이 한 번 나와도 갱신 탓으로 귀속할 수 있었다. 이 규칙을 어긴 사례가 있었다. A-6 에서 −41% 인 대조군을 영향 없음이라고 적었고 A-8 에서 표본 9개로 무중단을 주장했다. 가장 최근에는 in-flight 감시에서 76건이 실패했는데 같은 순간 폴링 49건이 전부 200 이었고 연결수가 0이었으며 재현이 0/100 이었다. 대조군이 오보를 막았다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "측정이 거짓말하는 자리들",
"line": 594
"value": "측정이 거짓말할 때",
"line": 1122
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 596,
"end_line": 606
"start_line": 1124,
"end_line": 1134
}
],
"assumption": false
@@ -58,8 +58,8 @@
"details": [],
"evidence": [
{
"start_line": 607,
"end_line": 618
"start_line": 1138,
"end_line": 1145
}
],
"assumption": false
@@ -76,8 +76,8 @@
],
"evidence": [
{
"start_line": 596,
"end_line": 618
"start_line": 1124,
"end_line": 1145
}
],
"assumption": false
@@ -94,8 +94,8 @@
],
"evidence": [
{
"start_line": 607,
"end_line": 618
"start_line": 1138,
"end_line": 1145
}
],
"assumption": false
@@ -110,8 +110,8 @@
"kind": "read",
"evidence": [
{
"start_line": 596,
"end_line": 610
"start_line": 1124,
"end_line": 1141
}
],
"assumption": false
@@ -124,8 +124,8 @@
"kind": "request",
"evidence": [
{
"start_line": 596,
"end_line": 618
"start_line": 1124,
"end_line": 1145
}
],
"assumption": false
@@ -138,8 +138,8 @@
"kind": "blocked",
"evidence": [
{
"start_line": 607,
"end_line": 618
"start_line": 1138,
"end_line": 1145
}
],
"assumption": false
@@ -149,4 +149,4 @@
"metadata": {
"rationale": "대조군을 관측과 귀속 사이에 놓았다. 그 자리가 비면 같은 관측이 두 가지로 읽힌다."
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@
"title": "이 기록이 적용되지 않는 조건",
"question": "이 결론들을 언제 그대로 쓰면 안 되는가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"이 기록을 자기 환경에 옮기려는 엔지니어"
],
@@ -13,11 +13,11 @@
"long_description": "Keycloak 26 미만이면 persistent-user-sessions 가 기본이 아니라 A층 결론 중 셋이 뒤집힌다. volatile 에서는 캐시가 더운지 찬지가 DB 정지 시의 동작을 가른다. 인스턴스가 하나면 B층 질문 자체가 생기지 않는다. oauth2-proxy 에서 cookie-refresh 를 켜면 TTL 역산 정리 규칙이 무너진다. 그리고 이 실험대는 106초 시계 왜곡이 있었고 여기 적힌 수치는 그것을 보정한 값이다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "이 기록이 적용되지 않는 조건",
"line": 689
"line": 1235
}
},
"composition": {
@@ -31,19 +31,37 @@
"groups": [],
"nodes": [
{
"id": "version",
"label": "Keycloak 버전",
"kind": "component",
"id": "scope",
"label": "적용 범위 확인",
"kind": "process",
"role": "source",
"emphasis": "warning",
"description": "26 미만이면 세션의 거처가 다르다.",
"emphasis": "primary",
"description": "이 기록의 결론을 옮기기 전에 환경 조건을 확인한다.",
"details": [
"A-1 · A-2 · A-8"
"환경 전제"
],
"evidence": [
{
"start_line": 684,
"end_line": 690
"start_line": 1235,
"end_line": 1242
}
],
"assumption": false
},
{
"id": "version",
"label": "Keycloak 버전",
"kind": "component",
"role": "target",
"emphasis": "warning",
"description": "26 미만이면 A층 결론 중 셋이 뒤집힌다.",
"details": [
"persistent-user-sessions"
],
"evidence": [
{
"start_line": 1237,
"end_line": 1238
}
],
"assumption": false
@@ -52,16 +70,16 @@
"id": "cache",
"label": "캐시 온도",
"kind": "component",
"role": "control",
"role": "target",
"emphasis": "warning",
"description": "volatile 에서 DB 정지 시의 동작을 가른다.",
"description": "volatile DB 정지 결과를 바꾼다.",
"details": [
"400 · 500 · 200"
"cold · warm"
],
"evidence": [
{
"start_line": 684,
"end_line": 692
"start_line": 1239,
"end_line": 1239
}
],
"assumption": false
@@ -70,14 +88,34 @@
"id": "instances",
"label": "인스턴스 수",
"kind": "component",
"role": "control",
"role": "target",
"emphasis": "normal",
"description": "하나면 B층 질문이 생기지 않는다.",
"details": [],
"description": "단일 인스턴스면 B층 질문이 생기지 않는다.",
"details": [
"1 · 2+"
],
"evidence": [
{
"start_line": 684,
"end_line": 694
"start_line": 1240,
"end_line": 1240
}
],
"assumption": false
},
{
"id": "cookie-refresh",
"label": "cookie refresh",
"kind": "component",
"role": "target",
"emphasis": "warning",
"description": "켜면 B-7a의 TTL 역산이 성립하지 않는다.",
"details": [
"--cookie-refresh"
],
"evidence": [
{
"start_line": 1241,
"end_line": 1241
}
],
"assumption": false
@@ -88,14 +126,14 @@
"kind": "component",
"role": "target",
"emphasis": "warning",
"description": "이 수치들은 106초 보정을 거친 값이다.",
"description": "이 실험대 수치는 106초 왜곡 보정을 전제로 한다.",
"details": [
"NTP 미동기 환경"
"NTP · 106초"
],
"evidence": [
{
"start_line": 684,
"end_line": 696
"start_line": 1242,
"end_line": 1242
}
],
"assumption": false
@@ -103,43 +141,71 @@
],
"edges": [
{
"id": "v-c",
"from": "version",
"id": "scope-version",
"from": "scope",
"to": "version",
"label": "버전 확인",
"kind": "request",
"evidence": [
{
"start_line": 1237,
"end_line": 1237
}
],
"assumption": false
},
{
"id": "scope-cache",
"from": "scope",
"to": "cache",
"label": "선행 조건",
"label": "캐시 확인",
"kind": "request",
"evidence": [
{
"start_line": 684,
"end_line": 690
"start_line": 1239,
"end_line": 1239
}
],
"assumption": false
},
{
"id": "c-i",
"from": "cache",
"id": "scope-instances",
"from": "scope",
"to": "instances",
"label": "다음 조건",
"label": "개수 확인",
"kind": "request",
"evidence": [
{
"start_line": 684,
"end_line": 694
"start_line": 1240,
"end_line": 1240
}
],
"assumption": false
},
{
"id": "i-cl",
"from": "instances",
"to": "clock",
"label": "수치 해석",
"id": "scope-cookie-refresh",
"from": "scope",
"to": "cookie-refresh",
"label": "설정 확인",
"kind": "request",
"evidence": [
{
"start_line": 684,
"end_line": 696
"start_line": 1241,
"end_line": 1241
}
],
"assumption": false
},
{
"id": "scope-clock",
"from": "scope",
"to": "clock",
"label": "시계 확인",
"kind": "request",
"evidence": [
{
"start_line": 1242,
"end_line": 1242
}
],
"assumption": false
@@ -147,6 +213,6 @@
],
"legend": [],
"metadata": {
"rationale": "적용 조건을 목록이 아니라 무엇을 무효로 만드는가로 이었다."
"rationale": "다섯 조건을 임의의 순서로 직렬화하지 않고, 같은 적용 범위 확인에서 각각 갈라지는 독립 전제로 그렸다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "처음에는 외부 curl 로만 쟀다. A-1 에서 7800 을 끊었는데 외부 응답이 전부 200 이었다. 장애가 없어서가 아니라 분단된 노드가 readiness 실패로 스스로 로드밸런서에서 빠졌기 때문이다. 그래서 관측 지점을 셋으로 늘렸다. up 지표도 신뢰할 수 없었다 — A-2 에서 503 이 나는 동안에도 1이었다. 프로세스가 살아 있고 metrics 가 응답하면 up 은 1이며 살아 있지만 쓸모없는 상태를 못 본다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "관측을 어디에 둘 것인가",
"line": 140
"line": 328
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 141,
"end_line": 146
"start_line": 330,
"end_line": 332
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 143,
"end_line": 147
"start_line": 331,
"end_line": 333
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 143,
"end_line": 147
"start_line": 331,
"end_line": 333
}
],
"assumption": false
@@ -96,8 +96,8 @@
],
"evidence": [
{
"start_line": 143,
"end_line": 147
"start_line": 331,
"end_line": 333
}
],
"assumption": false
@@ -112,8 +112,8 @@
"kind": "read",
"evidence": [
{
"start_line": 143,
"end_line": 147
"start_line": 331,
"end_line": 333
}
],
"assumption": false
@@ -126,8 +126,8 @@
"kind": "read",
"evidence": [
{
"start_line": 143,
"end_line": 147
"start_line": 331,
"end_line": 333
}
],
"assumption": false
@@ -140,8 +140,8 @@
"kind": "read",
"evidence": [
{
"start_line": 143,
"end_line": 147
"start_line": 331,
"end_line": 333
}
],
"assumption": false
@@ -151,4 +151,4 @@
"metadata": {
"rationale": "세 지점이 서로 다른 층을 본다는 것과, 각 지점이 못 보는 것을 함께 담았다."
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,20 +4,20 @@
"title": "열린 질문 네 개가 닿은 곳",
"question": "설계로 못 푼 질문들은 어디에서 답을 얻었는가",
"type": "architecture",
"direction": "TB",
"direction": "LR",
"audience": [
"같은 질문을 안고 있는 백엔드 엔지니어"
],
"summary": "네 질문 중 셋은 저장소가 아니라 스키마와 조회 키에서 답이 나왔고, 하나는 세션 스냅샷이라는 성질에서 나왔다.",
"summary": "Q1은 외부 저장소와 스키마, Q2는 client session 삭제, Q3는 서로 다른 조회 키, Q4는 헤더 도착과 인가의 구분 및 재인증 전 세션 스냅샷에서 답이 나왔다.",
"alt": "네 개의 열린 질문이 각각 어느 실험 층에 닿아 어떤 구조적 원인으로 이어지는지 보이는 구성.",
"long_description": "Q1 은 저장소를 밖으로 빼면 다중 인스턴스와 재시작은 풀리지만 덮어쓰기와 로그아웃 정리는 남는다는 답을 얻었고 원인은 기본키에 세션 id 가 없다는 것이었다. Q2 는 이긴 요청의 토큰조차 못 쓴다는 답이다. Q3 은 세션과 토큰의 조회 키가 다르므로 각각 결정해야 한다는 답이다. Q4 프록시가 헤더를 먼저 지워야 하고 IdP 의 클레임 변경은 재인증 전까지 반영되지 않는다는 답이다.",
"long_description": "Q1은 저장소를 밖으로 빼면 다중 인스턴스와 재시작은 풀리지만 기본키에 session id가 없어 덮어쓰기와 정리 문제가 남는다는 답이다. Q2는 refresh 경쟁이 감지되면 client session이 지워져 이긴 요청의 새 token도 쓸 수 없다는 답이다. Q3는 Application Session과 OAuth2AuthorizedClient의 조회 키가 달라 각각 저장소를 결정해야 한다는 답이다. Q4는 위조 헤더가 permitAll echo까지 도착해도 JWT 보호 경로는 401이었고, 헤더 삭제 처방은 미적용이며 IdP claim 변경은 재인증 전까지 반영되지 않는다는 답이다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "열린 질문 네 개에 대한 답",
"line": 680
"line": 1221
}
},
"composition": {
@@ -36,68 +36,86 @@
"kind": "component",
"role": "source",
"emphasis": "primary",
"description": "설계로는 답할 수 없었다.",
"description": "설계만으로는 닫히지 않았던 Q1부터 Q4다.",
"details": [
"Q1 · Q2 · Q3 · Q4"
],
"evidence": [
{
"start_line": 682,
"end_line": 690
"start_line": 1223,
"end_line": 1228
}
],
"assumption": false
},
{
"id": "store-choice",
"label": "저장소 선택",
"kind": "process",
"role": "control",
"emphasis": "normal",
"description": "다중 인스턴스와 재시작은 여기서 풀린다.",
"details": [
"Redis · PostgreSQL"
],
"evidence": [
{
"start_line": 682,
"end_line": 692
}
],
"assumption": false
},
{
"id": "schema",
"label": "스키마와 조회 키",
"kind": "process",
"role": "control",
"emphasis": "warning",
"description": "덮어쓰기와 정리 누락은 여기가 원인이다.",
"details": [
"키에 세션 id 없음"
],
"evidence": [
{
"start_line": 682,
"end_line": 694
}
],
"assumption": false
},
{
"id": "snapshot",
"label": "세션 스냅샷",
"id": "q1",
"label": "Q1 · 저장소와 스키마",
"kind": "process",
"role": "target",
"emphasis": "warning",
"description": "IdP 에서 클레임을 바꿔도 재인증까지 옛 값이 간다.",
"description": "①②는 외부 저장소로 풀리고 ③④는 기본키 문제로 남는다.",
"details": [
"옛 클레임 값"
"외부 저장소 · 세션 id 없는 PK"
],
"evidence": [
{
"start_line": 682,
"end_line": 696
"start_line": 1225,
"end_line": 1225
}
],
"assumption": false
},
{
"id": "q2",
"label": "Q2 · 회전 경쟁",
"kind": "process",
"role": "target",
"emphasis": "warning",
"description": "경쟁이 감지되면 client session이 지워진다.",
"details": [
"이긴 새 token도 사용 불가"
],
"evidence": [
{
"start_line": 1226,
"end_line": 1226
}
],
"assumption": false
},
{
"id": "q3",
"label": "Q3 · 조회 키 분리",
"kind": "process",
"role": "target",
"emphasis": "normal",
"description": "Session과 AuthorizedClient는 각각 결정해야 한다.",
"details": [
"session id · principal"
],
"evidence": [
{
"start_line": 1227,
"end_line": 1227
}
],
"assumption": false
},
{
"id": "q4",
"label": "Q4 · Edge 신뢰 경계",
"kind": "process",
"role": "target",
"emphasis": "warning",
"description": "헤더 도착과 인가 우회는 다르고 claim은 재인증까지 snapshot이다.",
"details": [
"permitAll 200 · JWT 401 · 삭제 처방 미검증"
],
"evidence": [
{
"start_line": 1228,
"end_line": 1228
}
],
"assumption": false
@@ -105,43 +123,57 @@
],
"edges": [
{
"id": "q-s",
"id": "questions-q1",
"from": "questions",
"to": "store-choice",
"label": "1차 검토",
"to": "q1",
"label": "",
"kind": "request",
"evidence": [
{
"start_line": 682,
"end_line": 692
"start_line": 1225,
"end_line": 1225
}
],
"assumption": false
},
{
"id": "s-sc",
"from": "store-choice",
"to": "schema",
"label": "미해결 잔여",
"id": "questions-q2",
"from": "questions",
"to": "q2",
"label": "",
"kind": "request",
"evidence": [
{
"start_line": 682,
"end_line": 694
"start_line": 1226,
"end_line": 1226
}
],
"assumption": false
},
{
"id": "sc-sn",
"from": "schema",
"to": "snapshot",
"label": "최종 잔여",
"id": "questions-q3",
"from": "questions",
"to": "q3",
"label": "",
"kind": "request",
"evidence": [
{
"start_line": 682,
"end_line": 696
"start_line": 1227,
"end_line": 1227
}
],
"assumption": false
},
{
"id": "questions-q4",
"from": "questions",
"to": "q4",
"label": "답",
"kind": "request",
"evidence": [
{
"start_line": 1228,
"end_line": 1228
}
],
"assumption": false
@@ -149,6 +181,6 @@
],
"legend": [],
"metadata": {
"rationale": "네 질문을 나열하는 대신 공통 원인으로 모았다. 저장소 선택으로 풀리지 않는 것들이 한자리에 있다."
"rationale": "네 질문을 하나의 순차 원인으로 만들지 않고, 각 질문이 실험에서 얻은 서로 다른 답으로 갈라지는 구조를 그렸다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "certbot renew 는 archive 디렉터리에 새 인증서를 쓰고 live 심볼릭 링크가 그것을 가리키게 한다. nginx 는 ssl_certificate 가 가리키는 파일을 기동 시점에 한 번 읽어 메모리에 보관하므로, 경로가 그대로여도 reload 없이는 옛 인증서를 계속 서빙한다. 이 실험대에서는 certbot-renew.service 의 ExecStartPost, renewal-hooks 의 세 디렉터리, certbot 의 nginx 플러그인이 모두 비어 있어 2305초 동안 옛 인증서가 서빙됐다. deploy 훅 하나를 넣자 같은 구간이 1~2초가 됐다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "609353e10bfd37a9bbb6a79ecf2a32f3d3c02d5d161879a14ad4713e49e7e5e8",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "D-4 · D-4a — 인증서, 그리고 이 실험대 최대의 발견",
"line": 497
"line": 1025
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 499,
"end_line": 504
"start_line": 1027,
"end_line": 1032
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 536,
"end_line": 540
"start_line": 1066,
"end_line": 1067
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 527,
"end_line": 533
"start_line": 1055,
"end_line": 1062
}
],
"assumption": false
@@ -96,8 +96,8 @@
],
"evidence": [
{
"start_line": 541,
"end_line": 548
"start_line": 1077,
"end_line": 1085
}
],
"assumption": false
@@ -112,8 +112,8 @@
"kind": "write",
"evidence": [
{
"start_line": 504,
"end_line": 510
"start_line": 1032,
"end_line": 1038
}
],
"assumption": false
@@ -126,8 +126,8 @@
"kind": "blocked",
"evidence": [
{
"start_line": 536,
"end_line": 540
"start_line": 1066,
"end_line": 1067
}
],
"assumption": false
@@ -140,8 +140,8 @@
"kind": "request",
"evidence": [
{
"start_line": 527,
"end_line": 533
"start_line": 1055,
"end_line": 1062
}
],
"assumption": false
@@ -154,8 +154,8 @@
"kind": "request",
"evidence": [
{
"start_line": 556,
"end_line": 562
"start_line": 1094,
"end_line": 1099
}
],
"assumption": false
@@ -165,4 +165,4 @@
"metadata": {
"rationale": "갱신과 서빙을 두 사건으로 분리하고 그 사이에 reload 를 놓았다. 이 그림의 요지는 그 자리가 비어 있을 수 있다는 것이다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "절차를 명령으로 적었지만 측정 장치 자체가 산문인 자리가 남아 있었다. 22.2초라는 헤드라인을 만든 부하 생성기가 실행 가능한 형태가 아니었고, RPO 측정의 로그인 루프와 sid 대조도 그랬으며, A-8 은 파일 하나에 쓰고 다른 파일을 읽고 있었다. 전부 셸 표현식으로 바꾸고 실제로 돌려서 확인했다. 그 확인에서 A-6 의 부하 생성기가 깨졌다. 일회성 파드의 출력 스트림이 유실됐고 상주 탐침에 파일로 모아 20/20 을 확인했다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "재현 가능성을 어떻게 보장했나",
"line": 660
"line": 1196
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 598,
"end_line": 610
"start_line": 1126,
"end_line": 1141
}
],
"assumption": false
@@ -58,8 +58,8 @@
"details": [],
"evidence": [
{
"start_line": 598,
"end_line": 614
"start_line": 1126,
"end_line": 1145
}
],
"assumption": false
@@ -76,8 +76,8 @@
],
"evidence": [
{
"start_line": 615,
"end_line": 622
"start_line": 1154,
"end_line": 1157
}
],
"assumption": false
@@ -94,8 +94,8 @@
],
"evidence": [
{
"start_line": 615,
"end_line": 622
"start_line": 1154,
"end_line": 1157
}
],
"assumption": false
@@ -110,8 +110,8 @@
"kind": "request",
"evidence": [
{
"start_line": 598,
"end_line": 614
"start_line": 1126,
"end_line": 1145
}
],
"assumption": false
@@ -124,8 +124,8 @@
"kind": "request",
"evidence": [
{
"start_line": 615,
"end_line": 622
"start_line": 1154,
"end_line": 1157
}
],
"assumption": false
@@ -138,8 +138,8 @@
"kind": "request",
"evidence": [
{
"start_line": 615,
"end_line": 622
"start_line": 1154,
"end_line": 1157
}
],
"assumption": false
@@ -149,4 +149,4 @@
"metadata": {
"rationale": "산문에서 명령으로, 명령에서 실행 확인으로 두 단계를 그렸다. 두 번째 단계에서 한 건이 깨졌다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -8,16 +8,16 @@
"audience": [
"Keycloak 을 다중 노드로 운영하는 백엔드 엔지니어"
],
"summary": "클러스터 뷰는 형성되지만 세션 엔트리는 노드 사이를 건너가지 않는다. 두 노드가 같은 답을 내놓는 경로는 PostgreSQL 이다.",
"alt": "keycloak-0 과 keycloak-1 이 각자 캐시를 갖고 PostgreSQL 을 함께 읽는 구성. 두 캐시 사이에는 세션 복제 경로가 없다.",
"long_description": "keycloak-0 과 keycloak-1 은 JGROUPS_PING 테이블을 통해 서로를 발견하고 클러스터 뷰를 형성한다. 그러나 sessions 캐시의 엔트리는 노드 사이로 복제지 않으며, 각 노드는 자기가 처리한 로그인만 캐시한다. 노드 A 로 로그인한 세션을 노드 B 가 아는 이유는 노드 B 가 PostgreSQL OFFLINE_USER_SESSION 을 직접 읽기 때문이고, 이는 반대편 노드가 날린 SQL 을 문장 로깅으로 잡아 확인했다.",
"summary": "두 Keycloak 노드가 서로를 발견하는 경로는 JGROUPS_PING이고, 다른 노드의 세션을 아는 실제 공유 경로는 PostgreSQL OFFLINE_USER_SESSION이다.",
"alt": "Keycloak replica 두 개에서 JGROUPS_PING으로는 멤버 발견이, OFFLINE_USER_SESSION으로는 세션 저장과 조회가 갈라지는 두 경로.",
"long_description": "keycloak-0과 keycloak-1은 JGROUPS_PING을 통해 서로를 발견하지만 sessions cache 엔트리를 서로 복제지 않는다. 다른 노드가 만든 세션을 알아야 할 때는 PostgreSQL OFFLINE_USER_SESSION을 직접 읽는다. discovery와 session sharing은 같은 데이터베이스 안에서도 다른 책임이다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "609353e10bfd37a9bbb6a79ecf2a32f3d3c02d5d161879a14ad4713e49e7e5e8",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "그런데 첫 실험에서 전제가 무너졌다",
"line": 33
"line": 64
}
},
"composition": {
@@ -26,139 +26,93 @@
"reference_ids": [
"payment-event-flow"
],
"rationale": "세 구성 요소 사이에서 어느 경로가 실제로 존재하고 어느 경로가 존재하지 않는지가 이 절의 지배적 질문이다. 시간 순서가 아니라 경로의 유무가 핵심이므로 component-flow 를 골랐다."
"rationale": "두 replica를 반복해서 그리지 않고 Keycloak replica 집합에서 발견 테이블과 세션 테이블로 갈리는 두 책임을 분리한다."
},
"groups": [],
"nodes": [
{
"id": "keycloak-0",
"label": "keycloak-0",
"id": "replicas",
"label": "Keycloak replica 2개",
"kind": "service",
"role": "source",
"emphasis": "primary",
"description": "로그인을 처리하고 자기 sessions 캐시에만 엔트리를 남긴다.",
"description": "keycloak-0과 keycloak-1은 각자 sessions cache를 갖고 같은 PostgreSQL을 사용한다.",
"details": [
"자기 처리분만 캐시"
"keycloak-0 · keycloak-1",
"sessions cache는 각자"
],
"evidence": [
{
"start_line": 49,
"end_line": 51
"start_line": 75,
"end_line": 81
}
],
"assumption": false
},
{
"id": "postgres",
"label": "PostgreSQL",
"kind": "datastore",
"role": "store",
"emphasis": "primary",
"description": "OFFLINE_USER_SESSION 에 세션 행을 보관한다. 두 노드가 같은 행을 본다.",
"details": [
"OFFLINE_USER_SESSION",
"offline_flag='0'",
"두 노드 공용"
],
"evidence": [
{
"start_line": 48,
"end_line": 52
}
],
"assumption": false
},
{
"id": "keycloak-1",
"label": "keycloak-1",
"kind": "service",
"role": "target",
"emphasis": "primary",
"description": "다른 노드가 만든 세션을 캐시로 받지 않고 데이터베이스에서 읽는다.",
"details": [
"refresh 시 DB 조회"
],
"evidence": [
{
"start_line": 48,
"end_line": 51
}
],
"assumption": false
},
{
"id": "jgroups-ping",
"id": "discovery",
"label": "JGROUPS_PING",
"kind": "datastore",
"role": "support",
"emphasis": "muted",
"description": "노드가 서로를 발견하는 자리. 여기 등록되어 있다는 것과 세션이 복제된다는 것은 다른 사건이다.",
"evidence": [
{
"start_line": 44,
"end_line": 46
}
],
"assumption": false,
"role": "target",
"emphasis": "normal",
"description": "노드 discovery와 membership을 위한 테이블이다. 세션 엔트리 복제 경로가 아니다.",
"details": [
"노드 둘 등록",
"세션 복제 아님"
]
"discovery만 담당"
],
"evidence": [
{
"start_line": 75,
"end_line": 75
}
],
"assumption": false
},
{
"id": "session-store",
"label": "OFFLINE_USER_SESSION",
"kind": "datastore",
"role": "target",
"emphasis": "primary",
"description": "다른 노드가 만든 세션을 직접 읽는 실제 공유 경로다.",
"details": [
"keycloak-0 INSERT",
"keycloak-1 SELECT"
],
"evidence": [
{
"start_line": 78,
"end_line": 81
}
],
"assumption": false
}
],
"edges": [
{
"id": "k0-writes",
"from": "keycloak-0",
"to": "postgres",
"label": "세션 INSERT",
"id": "discovery-path",
"from": "replicas",
"to": "discovery",
"label": "멤버 발견",
"kind": "write",
"evidence": [
{
"start_line": 49,
"end_line": 52
"start_line": 75,
"end_line": 75
}
],
"assumption": false
},
{
"id": "k1-reads",
"from": "keycloak-1",
"to": "postgres",
"label": "세션 SELECT",
"kind": "read",
"id": "session-path",
"from": "replicas",
"to": "session-store",
"label": "세션 저장 · 조회",
"kind": "data",
"evidence": [
{
"start_line": 48,
"end_line": 50
}
],
"assumption": false
},
{
"id": "k0-discovery",
"from": "keycloak-0",
"to": "jgroups-ping",
"label": "멤버 등록",
"kind": "write",
"evidence": [
{
"start_line": 44,
"end_line": 45
}
],
"assumption": false
},
{
"id": "k1-discovery",
"from": "keycloak-1",
"to": "jgroups-ping",
"label": "멤버 등록",
"kind": "write",
"evidence": [
{
"start_line": 44,
"end_line": 45
"start_line": 78,
"end_line": 81
}
],
"assumption": false
@@ -166,6 +120,6 @@
],
"legend": [],
"metadata": {
"rationale": "클러스터 형성과 세션 복제를 한 그림에서 분리했다. 발견(JGROUPS_PING)과 공유(OFFLINE_USER_SESSION)가 같은 데이터베이스 안의 다른 테이블이라는 점이 이 절의 오해가 생기는 자리다."
"rationale": "replica 두 개를 각각 두 저장소와 연결하면 선만 교차하고 핵심이 흐려진다. replica 집합에서 발견과 세션 공유 책임이 갈라지는 것으로 압축했다."
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "Keycloak 26 은 persistent-user-sessions 가 기본값이라 세션을 PostgreSQL 에 쓴다. 24 이전은 메모리에 두고 Infinispan 으로 복제했다. 그래서 7800 을 끊었을 때, 롤링 재시작했을 때, DB 를 세웠을 때의 결과가 세 항목 모두 정반대로 나온다. 이 실험대의 A층 결론 전체가 이 설정 위에 서 있다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "그리고 이 결론에는 버전 조건이 붙어 있었다",
"line": 64
"line": 94
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 62,
"end_line": 66
"start_line": 92,
"end_line": 96
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 62,
"end_line": 66
"start_line": 92,
"end_line": 96
}
],
"assumption": false
@@ -78,8 +78,8 @@
],
"evidence": [
{
"start_line": 67,
"end_line": 73
"start_line": 97,
"end_line": 103
}
],
"assumption": false
@@ -96,8 +96,8 @@
],
"evidence": [
{
"start_line": 67,
"end_line": 73
"start_line": 97,
"end_line": 103
}
],
"assumption": false
@@ -112,8 +112,8 @@
"kind": "write",
"evidence": [
{
"start_line": 62,
"end_line": 68
"start_line": 92,
"end_line": 97
}
],
"assumption": false
@@ -126,8 +126,8 @@
"kind": "write",
"evidence": [
{
"start_line": 62,
"end_line": 68
"start_line": 92,
"end_line": 97
}
],
"assumption": false
@@ -137,4 +137,4 @@
"metadata": {
"rationale": "설정 하나가 세션의 거처를 바꾸고, 그 거처가 장애 결과를 결정한다는 인과를 한 그림에 담았다."
}
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13,11 +13,11 @@
"long_description": "이 실험대에서 예측 다섯 개가 틀렸고 주입이 아홉 번 조용히 실패했다. A-1 이 틀리지 않았다면 A-0 의 인과 설명이 잘못된 채로 남았을 것이고 A-7 의 가설이 확정되지 않았다면 volatile 이면 이렇다는 표가 조건 없이 유통됐을 것이다. 그래서 규칙이 셋이다. 예측을 먼저 적고, 주입이 걸렸는지를 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "결국 지키려던 것은 무엇이었나",
"line": 706
"line": 1257
}
},
"composition": {
@@ -42,8 +42,8 @@
],
"evidence": [
{
"start_line": 744,
"end_line": 752
"start_line": 1304,
"end_line": 1310
}
],
"assumption": false
@@ -60,8 +60,8 @@
],
"evidence": [
{
"start_line": 744,
"end_line": 752
"start_line": 1304,
"end_line": 1310
}
],
"assumption": false
@@ -76,8 +76,8 @@
"details": [],
"evidence": [
{
"start_line": 744,
"end_line": 753
"start_line": 1304,
"end_line": 1310
}
],
"assumption": false
@@ -92,8 +92,8 @@
"details": [],
"evidence": [
{
"start_line": 744,
"end_line": 753
"start_line": 1304,
"end_line": 1310
}
],
"assumption": false
@@ -108,8 +108,8 @@
"kind": "request",
"evidence": [
{
"start_line": 744,
"end_line": 752
"start_line": 1304,
"end_line": 1310
}
],
"assumption": false
@@ -122,8 +122,8 @@
"kind": "request",
"evidence": [
{
"start_line": 744,
"end_line": 752
"start_line": 1304,
"end_line": 1310
}
],
"assumption": false
@@ -136,8 +136,8 @@
"kind": "request",
"evidence": [
{
"start_line": 744,
"end_line": 753
"start_line": 1304,
"end_line": 1310
}
],
"assumption": false
@@ -147,4 +147,4 @@
"metadata": {
"rationale": "세 규칙을 순서로 놓았다. 각 단계가 빠졌을 때 어떤 방식으로 틀리는지가 이 실험대의 실제 이력이다."
}
}
}
@@ -10,13 +10,13 @@
## Elements and evidence
- **keycloak-0** (service): 로그아웃을 처리한 노드. Evidence: L183L189.
- **TCP 7800** (channel): 무효화 통지가 지나는 길. NetworkPolicy 로 막았다. Evidence: L191L196.
- **keycloak-1** (service): 통지를 못 받아 세션이 죽은 것을 모른다. Evidence: L183L189.
- **PostgreSQL** (datastore): 세션이 여기 있어 7800 과 무관하다. Evidence: L186L190.
- **keycloak-0** (service): 로그아웃을 처리한 노드. Evidence: L485L489.
- **TCP 7800** (channel): 무효화 통지가 지나는 길. NetworkPolicy 로 막았다. Evidence: L491L495.
- **keycloak-1** (service): 통지를 못 받아 세션이 죽은 것을 모른다. Evidence: L485L489.
- **PostgreSQL** (datastore): 세션이 여기 있어 7800 과 무관하다. Evidence: L487L490.
## Relationships
- **keycloak-0 → TCP 7800:** 무효화 통지. Evidence: L186L190.
- **keycloak-1 → PostgreSQL:** 세션 SELECT. Evidence: L186L189.
- **TCP 7800 → keycloak-1:** 차단. Evidence: L186L190.
- **keycloak-0 → TCP 7800:** 무효화 통지. Evidence: L487L490.
- **keycloak-1 → PostgreSQL:** 세션 SELECT. Evidence: L487L489.
- **TCP 7800 → keycloak-1:** 차단. Evidence: L487L490.
@@ -1,35 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="a1-transport-vs-discovery" name="발견과 전송은 다른 경로다">
<mxGraphModel dx="680" dy="696" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<mxGraphModel dx="680" dy="1218" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1218" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_k0" value="keycloak-0" tooltip="로그아웃을 처리한 노드. | Evidence: L183-L189" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="82.0" y="60.0" width="150.0" height="64.0" as="geometry"/>
<mxCell id="n_k0" value="keycloak-0" tooltip="로그아웃을 처리한 노드. | Evidence: L485-L489" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="38.5" y="145.0" width="150.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_transport" value="TCP 7800&lt;br/&gt;허용 포트 8080 · 9000" tooltip="무효화 통지가 지나는 길. NetworkPolicy 로 막았다. | Evidence: L191-L196" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="80.5" y="220.0" width="153.0" height="71.0" as="geometry"/>
<mxCell id="n_transport" value="TCP 7800&lt;br/&gt;허용 포트 8080 · 9000" tooltip="무효화 통지가 지나는 길. NetworkPolicy 로 막았다. | Evidence: L491-L495" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="35.0" y="455.0" width="153.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_k1" value="keycloak-1&lt;br/&gt;refresh 200 (기대 400)" tooltip="통지를 못 받아 세션이 죽은 것을 모른다. | Evidence: L183-L189" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="70.0" y="387.0" width="174.0" height="71.0" as="geometry"/>
<mxCell id="n_k1" value="keycloak-1&lt;br/&gt;refresh 200 (기대 400)" tooltip="통지를 못 받아 세션이 죽은 것을 모른다. | Evidence: L485-L489" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="35.0" y="768.0" width="174.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_db" value="PostgreSQL&lt;br/&gt;교차 노드 refresh 200" tooltip="세션이 여기 있어 7800 과 무관하다. | Evidence: L186-L190" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=cylinder3;boundedLbl=1;backgroundOutline=1;fillColor=#eef6fb;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="80.5" y="554.0" width="153.0" height="97.0" as="geometry"/>
<mxCell id="n_db" value="PostgreSQL&lt;br/&gt;교차 노드 refresh 200" tooltip="세션이 여기 있어 7800 과 무관하다. | Evidence: L487-L490" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=cylinder3;boundedLbl=1;backgroundOutline=1;fillColor=#eef6fb;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="35.0" y="1102.0" width="153.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="e_k0-t" value="무효화 통지" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_k0" target="n_transport">
<mxGeometry relative="1" as="geometry">
<mxPoint x="181.0" y="172.0" as="offset"/>
<mxPoint x="112.5" y="304.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_k1-db" value="세션 SELECT" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_k1" target="n_db">
<mxGeometry relative="1" as="geometry">
<mxPoint x="181.0" y="506.0" as="offset"/>
<mxPoint x="116.8" y="942.5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_t-k1" value="차단" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_transport" target="n_k1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="181.0" y="339.0" as="offset"/>
<mxPoint x="116.8" y="619.0" as="offset"/>
</mxGeometry>
</mxCell>
</root>
@@ -6,10 +6,10 @@
{
"id": "edge-k0-t",
"type": "arrow",
"x": 157.0,
"y": 124.0,
"width": 0.0,
"height": 96.0,
"x": 111.5,
"y": 209.0,
"width": 2.0,
"height": 246.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -32,20 +32,20 @@
"locked": false,
"points": [
[
0.0,
2.0,
0.0
],
[
0.0,
48.0
2.0,
123.0
],
[
0.0,
48.0
123.0
],
[
0.0,
96.0
246.0
]
],
"lastCommittedPoint": null,
@@ -66,8 +66,8 @@
{
"id": "edge-label-k0-t",
"type": "text",
"x": 136.0,
"y": 160.0,
"x": 67.5,
"y": 292.0,
"width": 90,
"height": 24,
"angle": 0,
@@ -105,10 +105,10 @@
{
"id": "edge-k1-db",
"type": "arrow",
"x": 157.0,
"y": 458.0,
"width": 0.0,
"height": 96.0,
"x": 111.5,
"y": 839.0,
"width": 10.5,
"height": 263.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -131,20 +131,20 @@
"locked": false,
"points": [
[
0.0,
10.5,
0.0
],
[
0.0,
48.0
10.5,
131.5
],
[
0.0,
48.0
131.5
],
[
0.0,
96.0
263.0
]
],
"lastCommittedPoint": null,
@@ -165,8 +165,8 @@
{
"id": "edge-label-k1-db",
"type": "text",
"x": 136.0,
"y": 494.0,
"x": 71.75,
"y": 930.5,
"width": 90,
"height": 24,
"angle": 0,
@@ -204,10 +204,10 @@
{
"id": "edge-t-k1",
"type": "arrow",
"x": 157.0,
"y": 291.0,
"width": 0.0,
"height": 96.0,
"x": 111.5,
"y": 526.0,
"width": 10.5,
"height": 242.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -235,15 +235,15 @@
],
[
0.0,
48.0
121.0
],
[
0.0,
48.0
10.5,
121.0
],
[
0.0,
96.0
10.5,
242.0
]
],
"lastCommittedPoint": null,
@@ -264,8 +264,8 @@
{
"id": "edge-label-t-k1",
"type": "text",
"x": 136.0,
"y": 327.0,
"x": 71.75,
"y": 607.0,
"width": 90,
"height": 24,
"angle": 0,
@@ -303,8 +303,8 @@
{
"id": "node-k0",
"type": "rectangle",
"x": 82.0,
"y": 60.0,
"x": 38.5,
"y": 145.0,
"width": 150.0,
"height": 64.0,
"angle": 0,
@@ -333,8 +333,8 @@
{
"id": "node-label-k0",
"type": "text",
"x": 92.0,
"y": 70.0,
"x": 48.5,
"y": 155.0,
"width": 130.0,
"height": 44.0,
"angle": 0,
@@ -372,8 +372,8 @@
{
"id": "node-transport",
"type": "rectangle",
"x": 80.5,
"y": 220.0,
"x": 35.0,
"y": 455.0,
"width": 153.0,
"height": 71.0,
"angle": 0,
@@ -402,8 +402,8 @@
{
"id": "node-label-transport",
"type": "text",
"x": 90.5,
"y": 230.0,
"x": 45.0,
"y": 465.0,
"width": 133.0,
"height": 51.0,
"angle": 0,
@@ -441,8 +441,8 @@
{
"id": "node-k1",
"type": "rectangle",
"x": 70.0,
"y": 387.0,
"x": 35.0,
"y": 768.0,
"width": 174.0,
"height": 71.0,
"angle": 0,
@@ -471,8 +471,8 @@
{
"id": "node-label-k1",
"type": "text",
"x": 80.0,
"y": 397.0,
"x": 45.0,
"y": 778.0,
"width": 154.0,
"height": 51.0,
"angle": 0,
@@ -510,10 +510,10 @@
{
"id": "node-db",
"type": "rectangle",
"x": 80.5,
"y": 554.0,
"x": 35.0,
"y": 1102.0,
"width": 153.0,
"height": 97.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#e7f5ff",
@@ -540,10 +540,10 @@
{
"id": "node-label-db",
"type": "text",
"x": 90.5,
"y": 564.0,
"x": 45.0,
"y": 1112.0,
"width": 133.0,
"height": 77.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -2,14 +2,14 @@
"harness_version": "0.2.0",
"spec_id": "a1-transport-vs-discovery",
"spec_version": "1.1",
"spec_sha256": "f54cae81276a31a8b73961148cadf3ae5cd256bdc38a9c002997fce63c243ebf",
"spec_sha256": "c62dd7f9d01772578e1cbfa1eef70d63eae4a68555e0fc4c71bcab862eff2c2a",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
"document_sha256": "28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560",
"anchor": {
"kind": "heading",
"value": "A-1 · JGroups 전송(TCP 7800) 차단",
"line": 185
"line": 485
}
},
"outputs": [
@@ -21,7 +21,7 @@
"a1-transport-vs-discovery.excalidraw",
"a1-transport-vs-discovery.alt.md"
],
"lint_issue_count": 0,
"lint_issue_count": 1,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "component-flow",
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="696" viewBox="0 0 680 696" role="img" aria-labelledby="diagram-title diagram-description">
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="1218" viewBox="0 0 680 1218" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">발견과 전송은 다른 경로다</title>
<desc id="diagram-description">노드가 서로를 찾는 경로와 실제로 이야기하는 경로가 다르다. 발견은 PostgreSQL 의 JGROUPS_PING 테이블을 쓰고 전송은 TCP 7800 을 쓴다. 7800 만 막으면 둘 다 데이터베이스에 등록된 채로 남아 서로 존재한다고 믿지만 메시지는 오가지 않는다. 세션은 데이터베이스에 있으므로 교차 노드 refresh 는 200 을 유지하고, 로그아웃 무효화 통지는 7800 을 타므로 400 이어야 할 것이 200 이 된다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;a1-transport-vs-discovery&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/keycloak-session-store/final/document.md&quot;,&quot;document_sha256&quot;:&quot;1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;A-1 · JGroups 전송(TCP 7800) 차단&quot;,&quot;line&quot;:185}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;a1-transport-vs-discovery&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/keycloak-session-store/final/document.md&quot;,&quot;document_sha256&quot;:&quot;28aef96a2bbb94fbb10ade26a71238fee62a5a4d9fa6e7749ae98cfd0a65e560&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;A-1 · JGroups 전송(TCP 7800) 차단&quot;,&quot;line&quot;:485}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
@@ -49,36 +49,36 @@
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="680" height="696" />
<polyline class="edge kind-request style-solid emphasis-normal" points="157.0,124.0 157.0,172.0 157.0,172.0 157.0,220.0" data-evidence="186-190" />
<rect class="edge-label-bg" x="151.9" y="158.0" width="58.2" height="22" rx="3" />
<text class="edge-label" x="181.0" y="173.0">무효화 통지</text>
<polyline class="edge kind-read style-solid emphasis-normal" points="157.0,458.0 157.0,506.0 157.0,506.0 157.0,554.0" data-evidence="186-189" />
<rect class="edge-label-bg" x="141.8" y="492.0" width="78.3" height="22" rx="3" />
<text class="edge-label" x="181.0" y="507.0">세션 SELECT</text>
<polyline class="edge kind-blocked style-solid emphasis-normal" points="157.0,291.0 157.0,339.0 157.0,339.0 157.0,387.0" data-evidence="186-190" />
<rect class="edge-label-bg" x="159.0" y="325.0" width="44.0" height="22" rx="3" />
<text class="edge-label" x="181.0" y="340.0">차단</text>
<rect class="canvas" width="680" height="1218" />
<polyline class="edge kind-request style-solid emphasis-normal" points="113.5,209.0 113.5,332.0 111.5,332.0 111.5,455.0" data-evidence="487-490" />
<rect class="edge-label-bg" x="83.4" y="290.0" width="58.2" height="22" rx="3" />
<text class="edge-label" x="112.5" y="305.0">무효화 통지</text>
<polyline class="edge kind-read style-solid emphasis-normal" points="122.0,839.0 122.0,970.5 111.5,970.5 111.5,1102.0" data-evidence="487-489" />
<rect class="edge-label-bg" x="77.6" y="928.5" width="78.3" height="22" rx="3" />
<text class="edge-label" x="116.8" y="943.5">세션 SELECT</text>
<polyline class="edge kind-blocked style-solid emphasis-normal" points="111.5,526.0 111.5,647.0 122.0,647.0 122.0,768.0" data-evidence="487-490" />
<rect class="edge-label-bg" x="94.8" y="605.0" width="44.0" height="22" rx="3" />
<text class="edge-label" x="116.8" y="620.0">차단</text>
<g id="node-k0">
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="183-189" x="82.0" y="60.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="157.0" y="90.0">keycloak-0</text>
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="485-489" x="38.5" y="145.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="113.5" y="175.0">keycloak-0</text>
</g>
<g id="node-transport">
<rect class="node-shape kind-channel emphasis-warning role-control" data-evidence="191-196" x="80.5" y="220.0" width="153.0" height="71.0" rx="7" />
<text class="node-label" x="157.0" y="247.0">TCP 7800</text>
<line class="node-detail-divider" x1="94.5" y1="268.0" x2="219.5" y2="268.0" />
<text class="node-detail" x="96.5" y="285.0">허용 포트 8080 · 9000</text>
<rect class="node-shape kind-channel emphasis-warning role-control" data-evidence="491-495" x="35.0" y="455.0" width="153.0" height="71.0" rx="7" />
<text class="node-label" x="111.5" y="482.0">TCP 7800</text>
<line class="node-detail-divider" x1="49.0" y1="503.0" x2="174.0" y2="503.0" />
<text class="node-detail" x="51.0" y="520.0">허용 포트 8080 · 9000</text>
</g>
<g id="node-k1">
<rect class="node-shape kind-service emphasis-primary role-target" data-evidence="183-189" x="70.0" y="387.0" width="174.0" height="71.0" rx="7" />
<text class="node-label" x="157.0" y="414.0">keycloak-1</text>
<line class="node-detail-divider" x1="84.0" y1="435.0" x2="230.0" y2="435.0" />
<text class="node-detail" x="86.0" y="452.0">refresh 200 (기대 400)</text>
<rect class="node-shape kind-service emphasis-primary role-target" data-evidence="485-489" x="35.0" y="768.0" width="174.0" height="71.0" rx="7" />
<text class="node-label" x="122.0" y="795.0">keycloak-1</text>
<line class="node-detail-divider" x1="49.0" y1="816.0" x2="195.0" y2="816.0" />
<text class="node-detail" x="51.0" y="833.0">refresh 200 (기대 400)</text>
</g>
<g id="node-db">
<rect class="node-shape kind-datastore emphasis-primary role-store" data-evidence="186-190" x="80.5" y="567.0" width="153.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-primary role-store" cx="157.0" cy="567.0" rx="76.5" ry="13.0" /><path class="storage-bottom" d="M 80.5 638.0 A 76.5 13.0 0 0 0 233.5 638.0" />
<text class="node-label" x="157.0" y="594.0">PostgreSQL</text>
<line class="node-detail-divider" x1="94.5" y1="615.0" x2="219.5" y2="615.0" />
<text class="node-detail" x="96.5" y="632.0">교차 노드 refresh 200</text>
<rect class="node-shape kind-datastore emphasis-primary role-store" data-evidence="487-490" x="35.0" y="1113.8" width="153.0" height="47.3" /><ellipse class="node-shape kind-datastore emphasis-primary role-store" cx="111.5" cy="1113.8" rx="76.5" ry="11.8" /><path class="storage-bottom" d="M 35.0 1161.2 A 76.5 11.8 0 0 0 188.0 1161.2" />
<text class="node-label" x="111.5" y="1129.0">PostgreSQL</text>
<line class="node-detail-divider" x1="49.0" y1="1150.0" x2="174.0" y2="1150.0" />
<text class="node-detail" x="51.0" y="1167.0">교차 노드 refresh 200</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

@@ -10,13 +10,13 @@ Keycloak 은 트랜잭션마다 SET LOCAL synchronous_commit TO OFF 를 건다.
## Elements and evidence
- **클라이언트** (actor): 200 과 토큰을 받는다. Evidence: L207L213.
- **Keycloak** (service): 트랜잭션마다 synchronous_commit 을 끈다. Evidence: L211L215.
- **PostgreSQL** (datastore): COMMIT 을 즉시 반환한다. WAL 은 아직 메모리에 있다. Evidence: L211L215.
- **디스크의 WAL** (datastore): 여기 닿아야 살아남는다. Evidence: L207L215.
- **클라이언트** (actor): 200 과 토큰을 받는다. Evidence: L513L518.
- **Keycloak** (service): 트랜잭션마다 synchronous_commit 을 끈다. Evidence: L516L518.
- **PostgreSQL** (datastore): COMMIT 을 즉시 반환한다. WAL 은 아직 메모리에 있다. Evidence: L516L518.
- **디스크의 WAL** (datastore): 여기 닿아야 살아남는다. Evidence: L513L518.
## Relationships
- **클라이언트 → Keycloak:** 로그인 요청. Evidence: L207L211.
- **Keycloak → PostgreSQL:** 세션 INSERT · COMMIT. Evidence: L211L215.
- **PostgreSQL → 디스크의 WAL:** 지연된 WAL 기록. Evidence: L211L215.
- **클라이언트 → Keycloak:** 로그인 요청. Evidence: L513L516.
- **Keycloak → PostgreSQL:** 세션 INSERT · COMMIT. Evidence: L516L518.
- **PostgreSQL → 디스크의 WAL:** 지연된 WAL 기록. Evidence: L516L518.
@@ -1,35 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="a3-commit-to-disk-gap" name="200 과 디스크 사이의 빈 구간">
<mxGraphModel dx="680" dy="742" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<mxGraphModel dx="680" dy="1358" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1358" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_client" value="클라이언트&lt;br/&gt;성공 응답 153건" tooltip="200 과 토큰을 받는다. | Evidence: L207-L213" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="134.5" y="60.0" width="150.0" height="84.0" as="geometry"/>
<mxCell id="n_client" value="클라이언트&lt;br/&gt;성공 응답 153건" tooltip="200 과 토큰을 받는다. | Evidence: L513-L518" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="35.0" y="145.0" width="150.0" height="84.0" as="geometry"/>
</mxCell>
<mxCell id="n_keycloak" value="Keycloak&lt;br/&gt;SET LOCAL synchronous_commit TO OFF" tooltip="트랜잭션마다 synchronous_commit 을 끈다. | Evidence: L211-L215" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="70.0" y="240.0" width="279.0" height="71.0" as="geometry"/>
<mxCell id="n_keycloak" value="Keycloak&lt;br/&gt;SET LOCAL synchronous_commit TO OFF" tooltip="트랜잭션마다 synchronous_commit 을 끈다. | Evidence: L516-L518" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="41.5" y="455.0" width="279.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_pg" value="PostgreSQL&lt;br/&gt;wal_writer_delay 200ms" tooltip="COMMIT 을 즉시 반환한다. WAL 은 아직 메모리에 있다. | Evidence: L211-L215" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=cylinder3;boundedLbl=1;backgroundOutline=1;fillColor=#eef6fb;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="115.5" y="407.0" width="188.0" height="97.0" as="geometry"/>
<mxCell id="n_pg" value="PostgreSQL&lt;br/&gt;wal_writer_delay 200ms" tooltip="COMMIT 을 즉시 반환한다. WAL 은 아직 메모리에 있다. | Evidence: L516-L518" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=cylinder3;boundedLbl=1;backgroundOutline=1;fillColor=#eef6fb;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="41.5" y="894.0" width="188.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_disk" value="디스크의 WAL&lt;br/&gt;DB 존재 149건" tooltip="여기 닿아야 살아남는다. | Evidence: L207-L215" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=cylinder3;boundedLbl=1;backgroundOutline=1;fillColor=#eef6fb;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="134.5" y="600.0" width="150.0" height="97.0" as="geometry"/>
<mxCell id="n_disk" value="디스크의 WAL&lt;br/&gt;DB 존재 149건" tooltip="여기 닿아야 살아남는다. | Evidence: L513-L518" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;shape=cylinder3;boundedLbl=1;backgroundOutline=1;fillColor=#eef6fb;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="41.5" y="1242.0" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="e_c-k" value="로그인 요청" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_client" target="n_keycloak">
<mxGeometry relative="1" as="geometry">
<mxPoint x="233.5" y="192.0" as="offset"/>
<mxPoint x="145.5" y="314.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_k-pg" value="세션 INSERT · COMMIT" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_keycloak" target="n_pg">
<mxGeometry relative="1" as="geometry">
<mxPoint x="233.5" y="359.0" as="offset"/>
<mxPoint x="158.2" y="682.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_pg-disk" value="지연된 WAL 기록" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_pg" target="n_disk">
<mxGeometry relative="1" as="geometry">
<mxPoint x="233.5" y="552.0" as="offset"/>
<mxPoint x="126.0" y="1075.5" as="offset"/>
</mxGeometry>
</mxCell>
</root>

Some files were not shown because too many files have changed in this diff Show More