feat: 가상화 문서들 추가

This commit is contained in:
DongHyeonka
2026-09-10 08:54:05 +09:00
parent e9f6a93327
commit 43e1aadef0
695 changed files with 153404 additions and 12754 deletions
@@ -136,7 +136,7 @@
"id": "k1-rev",
"from": "k1",
"to": "rev",
"label": "반대 방향으로 재연결",
"label": "재연결",
"kind": "request",
"evidence": [
{
@@ -32,11 +32,11 @@
"nodes": [
{
"id": "attacker",
"label": "밖에서 보낸 위조 헤더",
"label": "외부 위조 헤더",
"kind": "actor",
"role": "source",
"emphasis": "warning",
"description": "앱이 믿는 이름을 그대로 쓴다.",
"description": "밖에서 들어온 요청이 앱이 믿는 헤더 이름을 그대로 쓴다.",
"details": [
"X-Auth-Request-Roles"
],
@@ -126,7 +126,7 @@
"id": "lookup-session",
"from": "request",
"to": "app-session",
"label": "세션 id 로 조회",
"label": "조회",
"kind": "read",
"evidence": [
{
@@ -140,7 +140,7 @@
"id": "lookup-client",
"from": "request",
"to": "authorized-client",
"label": "principal 이름으로 조회",
"label": "조회",
"kind": "read",
"evidence": [
{
@@ -183,4 +183,4 @@
"metadata": {
"rationale": "이름이 비슷한 두 저장 대상을 조회 키로 갈랐다. B-1 과 B-2 의 결과가 이 분기에서 나온다."
}
}
}
@@ -32,11 +32,11 @@
"nodes": [
{
"id": "logout",
"label": "한 앱에서 로그아웃",
"label": "한 앱 로그아웃",
"kind": "actor",
"role": "source",
"emphasis": "primary",
"description": "Keycloak 세션이 끝난다.",
"description": "한 앱에서 로그아웃하면 Keycloak 세션이 끝난다.",
"details": [],
"evidence": [
{
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,341 @@
{
"version": "1.1",
"id": "cpu-io-passthrough-paths",
"title": "게스트가 하드웨어에 닿는 갈래는 셋이고 깊이가 다르다",
"question": "게스트의 CPU 실행·virtio I/O·직접 할당한 장치는 각각 어디까지 내려가고 호스트 유저공간을 지나는가",
"type": "architecture",
"direction": "TB",
"audience": [
"가상화 구조를 처음 읽는 사람",
"이 실험대가 무엇 위에서 도는지 알아야 하는 사람"
],
"summary": "CPU 는 QEMU 와 KVM 사이를 왕복하고, virtio 는 virtqueue 를 누가 소비하느냐에 따라 QEMU 나 커널 vhost 로 가며, 패스스루는 유저공간 드라이버가 IOMMU 보호 아래 장치에 직접 닿는다.",
"alt": "게스트·호스트 유저공간·호스트 커널·하드웨어 네 층을 가로지르는 세 갈래 경로도.",
"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",
"anchor": {
"kind": "heading",
"value": "이 층 아래의 구조 — 조사한 것",
"line": 1091
}
},
"composition": {
"profile": "component-flow",
"diagram_only": true,
"reference_ids": [
"payment-event-flow"
],
"rationale": "세 갈래가 각각 방향이 있는 경로다. comparison 은 관계선을 지워도 뜻이 남는 배치라 이 그림에서는 표가 되고, 깊이가 다르다는 주장이 자리로 드러나지 않는다.",
"focus_node": "qemu"
},
"groups": [],
"nodes": [
{
"id": "qemu",
"label": "QEMU",
"kind": "service",
"role": "source",
"shape": "box",
"emphasis": "primary",
"details": [
"/dev/kvm",
"KVM_CREATE_VM · KVM_CREATE_VCPU"
],
"description": "KVM 핸들을 열고 VM 과 vCPU 를 만드는 유저공간 프로세스. vcpu ioctl 은 그 vcpu 를 만든 스레드에서 낸다.",
"evidence": [
{
"start_line": 1102,
"end_line": 1108
}
],
"assumption": false
},
{
"id": "vfio",
"label": "VFIO",
"kind": "service",
"role": "service",
"shape": "box",
"details": [
"vfio-pci",
"IOMMU 그룹",
"/dev/vfio/vfio"
],
"description": "IOMMU 보호 아래 장치 접근을 유저공간에 여는 프레임워크. 소유 단위가 IOMMU 그룹이다.",
"evidence": [
{
"start_line": 1131,
"end_line": 1146
}
],
"assumption": false
},
{
"id": "pci-device",
"label": "물리 PCI 장치",
"kind": "service",
"role": "service",
"shape": "box",
"details": [
"게스트 직접 할당"
],
"description": "호스트 드라이버에서 떼어 vfio-pci 에 묶은 장치.",
"evidence": [
{
"start_line": 1131,
"end_line": 1133
},
{
"start_line": 1144,
"end_line": 1146
}
],
"assumption": false
},
{
"id": "iommu",
"label": "IOMMU",
"kind": "service",
"role": "sink",
"shape": "box",
"details": [
"DMA · 인터럽트 리매핑"
],
"description": "장치가 아무 메모리나 건드리지 못하게 막는 리매핑 장치.",
"evidence": [
{
"start_line": 1144,
"end_line": 1146
}
],
"assumption": false
},
{
"id": "kvm",
"label": "KVM",
"kind": "service",
"role": "service",
"shape": "box",
"details": [
"struct kvm_run",
"KVM_GET_VCPU_MMAP_SIZE"
],
"description": "mmap 한 공유 메모리로 나온 이유를 유저공간에 알리는 커널 쪽.",
"evidence": [
{
"start_line": 1105,
"end_line": 1108
}
],
"assumption": false
},
{
"id": "physical-cpu",
"label": "물리 CPU",
"kind": "service",
"role": "service",
"shape": "box",
"details": [
"VMX root · VMX non-root",
"VM entry · VM exit",
"guest-state 영역"
],
"description": "VM entry 때 guest-state 영역에서 상태를 싣고 VM exit 때 그리로 저장하는 프로세서.",
"evidence": [
{
"start_line": 1111,
"end_line": 1113
}
],
"assumption": false
},
{
"id": "guest-code",
"label": "게스트 코드",
"kind": "service",
"role": "sink",
"shape": "box",
"details": [
"vCPU"
],
"description": "VMX non-root 로 물리 CPU 에서 도는 게스트 명령.",
"evidence": [
{
"start_line": 1111,
"end_line": 1113
}
],
"assumption": false
},
{
"id": "virtio-driver",
"label": "게스트 virtio 드라이버",
"kind": "service",
"role": "sink",
"shape": "box",
"details": [
"virtio-pci · virtio-mmio",
"virtqueue"
],
"description": "게스트가 보는 반가상화 장치의 드라이버. 주고받는 통로가 virtqueue 다.",
"evidence": [
{
"start_line": 1116,
"end_line": 1118
}
],
"assumption": false
},
{
"id": "vhost",
"label": "vhost",
"kind": "service",
"role": "sink",
"shape": "box",
"details": [
"리눅스 커널 구현",
"virtqueue 공유"
],
"description": "virtqueue 를 QEMU 밖과 나누는 커널 구현. vhost-user 규약이 이것을 제어하는 ioctl 인터페이스를 보완한다.",
"evidence": [
{
"start_line": 1125,
"end_line": 1129
}
],
"assumption": false
}
],
"edges": [
{
"id": "qemu-device-fd",
"from": "qemu",
"to": "vfio",
"label": "장치 fd",
"kind": "request",
"style": "solid",
"evidence": [
{
"start_line": 1138,
"end_line": 1141
}
],
"assumption": false
},
{
"id": "vfio-binds-device",
"from": "vfio",
"to": "pci-device",
"label": "vfio-pci 결합",
"kind": "control",
"style": "solid",
"evidence": [
{
"start_line": 1144,
"end_line": 1146
}
],
"assumption": false
},
{
"id": "device-dma",
"from": "pci-device",
"to": "iommu",
"label": "DMA",
"kind": "data",
"style": "solid",
"evidence": [
{
"start_line": 1144,
"end_line": 1146
}
],
"assumption": false
},
{
"id": "qemu-kvm-run",
"from": "qemu",
"to": "kvm",
"label": "KVM_RUN",
"kind": "request",
"style": "solid",
"evidence": [
{
"start_line": 1104,
"end_line": 1106
}
],
"assumption": false
},
{
"id": "kvm-vm-entry",
"from": "kvm",
"to": "physical-cpu",
"label": "VM entry",
"kind": "request",
"style": "solid",
"evidence": [
{
"start_line": 1111,
"end_line": 1112
}
],
"assumption": false
},
{
"id": "cpu-runs-guest",
"from": "physical-cpu",
"to": "guest-code",
"label": "VMX non-root",
"kind": "data",
"style": "solid",
"evidence": [
{
"start_line": 1111,
"end_line": 1112
}
],
"assumption": false
},
{
"id": "qemu-virtqueue",
"from": "qemu",
"to": "virtio-driver",
"label": "virtqueue",
"kind": "data",
"style": "solid",
"evidence": [
{
"start_line": 1116,
"end_line": 1118
},
{
"start_line": 1128,
"end_line": 1128
}
],
"assumption": false
},
{
"id": "qemu-vhost-ioctl",
"from": "qemu",
"to": "vhost",
"label": "제어 ioctl",
"kind": "control",
"style": "dashed",
"evidence": [
{
"start_line": 1125,
"end_line": 1129
}
],
"assumption": false
}
],
"legend": [],
"metadata": {
"rationale": "세 갈래의 깊이 차이는 어느 층까지 선이 내려가느냐로만 보이므로 배치와 관계선이 함께 있어야 한다. 셋 중 패스스루는 이 실험대가 쓰지 않으며 본문이 그 사실을 적는다."
}
}
@@ -32,11 +32,11 @@
"nodes": [
{
"id": "rollback",
"label": "옛 버전으로 되돌리기",
"label": "옛 버전 롤백",
"kind": "process",
"role": "source",
"emphasis": "warning",
"description": "이미지 태그를 되돌린다.",
"description": "이미지 태그를 옛 버전으로 되돌린다.",
"details": [],
"evidence": [
{
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,306 @@
{
"version": "1.1",
"id": "guest-as-host-process",
"title": "게스트 두 대는 호스트에서 qemu 프로세스 두 개다",
"question": "호스트에서 게스트는 무엇으로 존재하고, 게스트가 보는 장치는 누가 만들어 주는가",
"type": "deployment",
"direction": "TB",
"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 는 이 그림에 넣지 않았고 본문이 맡는다.",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "0ae5674723f25dc85d5069529890f07be1a11b768b56901103fa3ea6ac3dcf55",
"anchor": {
"kind": "heading",
"value": "0층. 가상화 — 「바닥」 아래에 있는 것",
"line": 935
}
},
"composition": {
"profile": "component-flow",
"diagram_only": true,
"reference_ids": [
"payment-event-flow"
],
"rationale": "게스트가 호스트에서 어떻게 존재하는지는 libvirtd 에서 qemu 프로세스로, 다시 게스트 커널로 내려가는 한 방향 경로다. sequence 는 이 절에 시각 순서가 없어서 맞지 않고, orchestrator-workers 는 게스트 둘을 교체 가능한 워커로 그리는데 둘은 할당량도 역할도 다르다.",
"focus_node": "qemu-kc-lab-1"
},
"groups": [
{
"id": "host-zone",
"label": "호스트 test-server",
"kind": "system",
"role": "zone",
"description": "libvirtd 와 게스트 프로세스가 함께 도는 호스트 경계.",
"evidence": [
{
"start_line": 946,
"end_line": 948
},
{
"start_line": 956,
"end_line": 956
}
],
"assumption": false
},
{
"id": "guest-1-zone",
"label": "게스트 kc-lab-1",
"kind": "system",
"role": "zone",
"description": "qemu 프로세스 하나가 담는 Debian 게스트.",
"evidence": [
{
"start_line": 987,
"end_line": 990
},
{
"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
}
],
"assumption": false
}
],
"nodes": [
{
"id": "libvirtd",
"label": "libvirtd",
"kind": "service",
"role": "source",
"group": "host-zone",
"shape": "box",
"details": [
"qemu:///system"
],
"description": "게스트마다 qemu-system-x86_64 를 하나씩 띄우는 호스트 데몬. htop 트리 뷰에서 그 아래에 게스트 프로세스가 달린다.",
"evidence": [
{
"start_line": 946,
"end_line": 948
},
{
"start_line": 956,
"end_line": 956
}
],
"assumption": false
},
{
"id": "qemu-kc-lab-1",
"label": "qemu-system-x86_64",
"kind": "service",
"role": "service",
"group": "host-zone",
"shape": "box",
"emphasis": "primary",
"details": [
"kc-lab-1",
"vCPU 2",
"할당 3584MB",
"RSS 3765MB"
],
"description": "kc-lab-1 게스트 전체가 들어 있는 호스트 프로세스. vCPU 는 이 프로세스의 스레드이고 RSS 는 게스트가 터치한 페이지만큼이다.",
"evidence": [
{
"start_line": 946,
"end_line": 948
},
{
"start_line": 987,
"end_line": 987
},
{
"start_line": 1032,
"end_line": 1032
}
],
"assumption": false
},
{
"id": "qemu-kc-lab-2",
"label": "qemu-system-x86_64",
"kind": "service",
"role": "service",
"group": "host-zone",
"shape": "box",
"details": [
"kc-lab-2",
"할당 2560MB",
"RSS 2633MB"
],
"description": "kc-lab-2 게스트 전체가 들어 있는 호스트 프로세스. A-4 의 virsh destroy 가 끊는 것이 이 프로세스다.",
"evidence": [
{
"start_line": 946,
"end_line": 948
},
{
"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
}
],
"assumption": false
}
],
"edges": [
{
"id": "libvirtd-spawns-1",
"from": "libvirtd",
"to": "qemu-kc-lab-1",
"label": "프로세스 생성",
"kind": "control",
"style": "solid",
"evidence": [
{
"start_line": 946,
"end_line": 948
},
{
"start_line": 956,
"end_line": 956
}
],
"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",
"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
}
],
"assumption": false
}
],
"legend": [],
"metadata": {
"rationale": "배치도로 고른 이유는 이 절이 주장하는 것이 순서가 아니라 담김이기 때문이다 — 게스트 하나가 호스트 프로세스 하나 안에 있고, 게스트가 보는 장치는 그 프로세스가 내준다. 메모리를 읽는 곳 셋과 machine.slice·virbr0 는 본문 표와 문단이 맡는다."
}
}
@@ -90,9 +90,9 @@
"kind": "process",
"role": "target",
"emphasis": "warning",
"description": "클레임 변경이 반영되지 않는 이유.",
"description": "IdP 에서 클레임을 바꿔도 재인증까지 옛 값이 간다.",
"details": [
"재인증까지 옛 값"
"옛 클레임 값"
],
"evidence": [
{
@@ -56,7 +56,9 @@
"emphasis": "primary",
"description": "OFFLINE_USER_SESSION 에 세션 행을 보관한다. 두 노드가 같은 행을 본다.",
"details": [
"offline_flag='0'"
"OFFLINE_USER_SESSION",
"offline_flag='0'",
"두 노드 공용"
],
"evidence": [
{
@@ -98,7 +100,10 @@
}
],
"assumption": false,
"details": []
"details": [
"노드 둘 등록",
"세션 복제 아님"
]
}
],
"edges": [
@@ -163,4 +168,4 @@
"metadata": {
"rationale": "클러스터 형성과 세션 복제를 한 그림에서 분리했다. 발견(JGROUPS_PING)과 공유(OFFLINE_USER_SESSION)가 같은 데이터베이스 안의 다른 테이블이라는 점이 이 절의 오해가 생기는 자리다."
}
}
}
@@ -19,4 +19,4 @@ JGroups 는 한 방향이 막혀도 열린 방향으로 재연결한다. 그래
- **막은 방향 → keycloak-1:** 차단. Evidence: L226L232.
- **keycloak-0 → 막은 방향:** JGroups 메시지. Evidence: L226L232.
- **keycloak-1 → 열린 반대 방향:** 반대 방향으로 재연결. Evidence: L226L232.
- **keycloak-1 → 열린 반대 방향:** 재연결. Evidence: L226L232.
@@ -15,4 +15,4 @@ n3: "열린 반대 방향" {
}
n0 -> n1: "JGroups 메시지"
n1 -> n2: "차단"
n2 -> n3: "반대 방향으로 재연결"
n2 -> n3: "재연결"
@@ -8,5 +8,5 @@ digraph techviz {
n3 [label="열린 반대 방향", shape=box, style="rounded,filled"];
n0 -> n1 [label="JGroups 메시지", style=solid];
n1 -> n2 [label="차단", style=solid];
n2 -> n3 [label="반대 방향으로 재연결", style=solid];
n2 -> n3 [label="재연결", style=solid];
}
@@ -27,7 +27,7 @@
<mxPoint x="169.0" y="179.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_k1-rev" value="반대 방향으로 재연결" 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_rev">
<mxCell id="e_k1-rev" value="재연결" 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_rev">
<mxGeometry relative="1" as="geometry">
<mxPoint x="169.0" y="513.0" as="offset"/>
</mxGeometry>
@@ -292,11 +292,11 @@
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "반대 방향으로 재연결",
"text": "재연결",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "반대 방향으로 재연결",
"originalText": "재연결",
"autoResize": true,
"lineHeight": 1.25
},
@@ -2,7 +2,7 @@
"harness_version": "0.2.0",
"spec_id": "a5-partition-asymmetry",
"spec_version": "1.1",
"spec_sha256": "9b10dfa9cfb1f24e5b105da13bc8fde85130cc1cca2ddad54cd57c32bc2dfcf5",
"spec_sha256": "d971af080c366e171592b1cfe7e62120eb0f4e0b5193e241e018e9440a44536b",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
@@ -14,10 +14,10 @@
},
"outputs": [
"a5-partition-asymmetry.svg",
"a5-partition-asymmetry.drawio",
"a5-partition-asymmetry.mmd",
"a5-partition-asymmetry.d2",
"a5-partition-asymmetry.dot",
"a5-partition-asymmetry.drawio",
"a5-partition-asymmetry.excalidraw",
"a5-partition-asymmetry.alt.md"
],
@@ -7,4 +7,4 @@ flowchart TB
n3["열린 반대 방향"]
n0 -->|"JGroups 메시지"| n1
n1 -->|"차단"| n2
n2 -->|"반대 방향으로 재연결"| n3
n2 -->|"재연결"| n3
@@ -57,8 +57,8 @@
<rect class="edge-label-bg" x="123.2" y="165.0" width="91.7" height="22" rx="3" />
<text class="edge-label" x="169.0" y="180.0">JGroups 메시지</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="145.0,465.0 145.0,513.0 145.0,513.0 145.0,561.0" data-evidence="226-232" />
<rect class="edge-label-bg" x="123.2" y="499.0" width="91.7" height="22" rx="3" />
<text class="edge-label" x="169.0" y="514.0">반대 방향으로 재연결</text>
<rect class="edge-label-bg" x="147.0" y="499.0" width="44.0" height="22" rx="3" />
<text class="edge-label" x="169.0" y="514.0">재연결</text>
<g id="node-k0">
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="226-236" x="70.0" y="60.0" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="145.0" y="87.0">keycloak-0</text>

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

@@ -10,13 +10,13 @@ nginx 는 자기가 proxy_set_header 로 설정한 헤더만 덮어쓴다. 설
## Elements and evidence
- **밖에서 보낸 위조 헤더** (actor): 앱이 믿는 이름을 그대로 쓴다. Evidence: L393L400.
- **외부 위조 헤더** (actor): 밖에서 들어온 요청이 앱이 믿는 헤더 이름을 그대로 쓴다. Evidence: L393L400.
- **nginx** (gateway): 설정하지 않은 이름은 덮어쓰지 않는다. Evidence: L393L400.
- **oauth2-proxy** (gateway): 인증 결과를 헤더로 넣는다. Evidence: L393L400.
- **앱** (service): 헤더를 믿고 인가한다. Evidence: L401L408.
## Relationships
- **밖에서 보낸 위조 헤더 → nginx:** 위조 헤더. Evidence: L393L400.
- **외부 위조 헤더 → nginx:** 위조 헤더. Evidence: L393L400.
- **nginx → oauth2-proxy:** 미삭제 시 통과. Evidence: L393L400.
- **oauth2-proxy → 앱:** 인가 헤더. Evidence: L393L408.
@@ -1,7 +1,7 @@
# 지우지 않으면 통과한다
# Question: Edge 가 넣어주는 인가 헤더를 앱이 믿어도 되는가
direction: down
n0: "밖에서 보낸 위조 헤더" {
n0: "외부 위조 헤더" {
shape: person
}
n1: "nginx" {
@@ -2,7 +2,7 @@ digraph techviz {
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="밖에서 보낸 위조 헤더", shape=box, style="rounded,dashed,filled"];
n0 [label="외부 위조 헤더", shape=box, style="rounded,dashed,filled"];
n1 [label="nginx", shape=diamond, style="rounded,filled"];
n2 [label="oauth2-proxy", shape=diamond, style="rounded,filled"];
n3 [label="앱", shape=box, style="rounded,filled"];
@@ -5,7 +5,7 @@
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_attacker" value="밖에서 보낸 위조 헤더&lt;br/&gt;X-Auth-Request-Roles" tooltip="앱이 믿는 이름을 그대로 쓴다. | Evidence: L393-L400" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxCell id="n_attacker" value="외부 위조 헤더&lt;br/&gt;X-Auth-Request-Roles" tooltip="밖에서 들어온 요청이 앱이 믿는 헤더 이름을 그대로 쓴다. | Evidence: L393-L400" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;dashed=1;fillColor=#f5f7fa;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
<mxGeometry x="82.5" y="60.0" width="170.0" height="84.0" as="geometry"/>
</mxCell>
<mxCell id="n_nginx" value="nginx&lt;br/&gt;proxy_set_header ... &quot;&quot;" tooltip="설정하지 않은 이름은 덮어쓰지 않는다. | Evidence: L393-L400" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;rhombus;perimeter=rhombusPerimeter;fillColor=#fff7e8;strokeColor=#d97706;fillColor=#fffdf5;" vertex="1" parent="1">
@@ -361,11 +361,11 @@
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "밖에서 보낸 위조 헤더\nX-Auth-Request-Roles",
"text": "외부 위조 헤더\nX-Auth-Request-Roles",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "밖에서 보낸 위조 헤더\nX-Auth-Request-Roles",
"originalText": "외부 위조 헤더\nX-Auth-Request-Roles",
"autoResize": true,
"lineHeight": 1.25
},
@@ -2,7 +2,7 @@
"harness_version": "0.2.0",
"spec_id": "b4-header-trust-boundary",
"spec_version": "1.1",
"spec_sha256": "56747d5f7826ad63f202d8dc4d6a4f1323f12eeaedb4bf989b67d0bfab5f09cd",
"spec_sha256": "ad0e26787a9e508ddce08e196ceeaacf2dfe1ef2caa047537f4355b5e34739a1",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
@@ -14,10 +14,10 @@
},
"outputs": [
"b4-header-trust-boundary.svg",
"b4-header-trust-boundary.drawio",
"b4-header-trust-boundary.mmd",
"b4-header-trust-boundary.d2",
"b4-header-trust-boundary.dot",
"b4-header-trust-boundary.drawio",
"b4-header-trust-boundary.excalidraw",
"b4-header-trust-boundary.alt.md"
],
@@ -1,7 +1,7 @@
%% 지우지 않으면 통과한다
%% question: Edge 가 넣어주는 인가 헤더를 앱이 믿어도 되는가
flowchart TB
n0(["밖에서 보낸 위조 헤더"])
n0(["외부 위조 헤더"])
n1{"nginx"}
n2{"oauth2-proxy"}
n3["앱"]
@@ -61,7 +61,7 @@
<text class="edge-label" x="191.5" y="527.0">인가 헤더</text>
<g id="node-attacker">
<g class="actor-symbol emphasis-warning" data-evidence="393-400"><circle cx="167.5" cy="80.0" r="11.0" /><line x1="167.5" y1="96.0" x2="167.5" y2="105.0" /><line x1="149.5" y1="106.0" x2="185.5" y2="106.0" /><line x1="167.5" y1="105.0" x2="152.5" y2="122.0" /><line x1="167.5" y1="105.0" x2="182.5" y2="122.0" /></g>
<text class="node-label" x="167.5" y="137.0">밖에서 보낸 위조 헤더</text>
<text class="node-label" x="167.5" y="137.0">외부 위조 헤더</text>
</g>
<g id="node-nginx">
<polygon class="node-shape kind-gateway emphasis-warning role-control" data-evidence="393-400" points="167.5,240.0 265.0,275.5 167.5,311.0 70.0,275.5" />

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

@@ -19,6 +19,6 @@ Spring Security 의 자동 구성은 세션을 서블릿 컨테이너 메모리
## Relationships
- **OAuth2AuthorizedClient → PostgreSQL:** 저장. Evidence: L355L358.
- **브라우저 요청 → OAuth2AuthorizedClient:** principal 이름으로 조회. Evidence: L332L341.
- **브라우저 요청 → Application Session:** 세션 id 로 조회. Evidence: L326L331.
- **브라우저 요청 → OAuth2AuthorizedClient:** 조회. Evidence: L332L341.
- **브라우저 요청 → Application Session:** 조회. Evidence: L326L331.
- **Application Session → Redis:** 저장. Evidence: L347L351.
@@ -16,7 +16,7 @@ n3: "Redis" {
n4: "PostgreSQL" {
shape: cylinder
}
n0 -> n1: "세션 id 로 조회"
n0 -> n2: "principal 이름으로 조회"
n0 -> n1: "조회"
n0 -> n2: "조회"
n1 -> n3: "저장"
n2 -> n4: "저장"
@@ -7,8 +7,8 @@ digraph techviz {
n2 [label="OAuth2AuthorizedClient", shape=box, style="rounded,filled"];
n3 [label="Redis", shape=cylinder, style="rounded,filled"];
n4 [label="PostgreSQL", shape=cylinder, style="rounded,filled"];
n0 -> n1 [label="세션 id 로 조회", style=solid];
n0 -> n2 [label="principal 이름으로 조회", style=solid];
n0 -> n1 [label="조회", style=solid];
n0 -> n2 [label="조회", style=solid];
n1 -> n3 [label="저장", style=solid];
n2 -> n4 [label="저장", style=solid];
}
@@ -25,12 +25,12 @@
<mxPoint x="672.0" y="271.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_lookup-client" value="principal 이름으로 조회" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_request" target="n_authorized-client">
<mxCell id="e_lookup-client" value="조회" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_request" target="n_authorized-client">
<mxGeometry relative="1" as="geometry">
<mxPoint x="324.0" y="233.2" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_lookup-session" value="세션 id 로 조회" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_request" target="n_app-session">
<mxCell id="e_lookup-session" value="조회" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_request" target="n_app-session">
<mxGeometry relative="1" as="geometry">
<mxPoint x="329.2" y="152.8" as="offset"/>
</mxGeometry>
@@ -165,9 +165,9 @@
{
"id": "edge-label-lookup-client",
"type": "text",
"x": 256.0,
"x": 279.0,
"y": 221.25,
"width": 136,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
@@ -193,11 +193,11 @@
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "principal 이름으로 조회",
"text": "조회",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "principal 이름으로 조회",
"originalText": "조회",
"autoResize": true,
"lineHeight": 1.25
},
@@ -292,11 +292,11 @@
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "세션 id 로 조회",
"text": "조회",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "세션 id 로 조회",
"originalText": "조회",
"autoResize": true,
"lineHeight": 1.25
},
@@ -2,7 +2,7 @@
"harness_version": "0.2.0",
"spec_id": "bff-store-lookup-keys",
"spec_version": "1.1",
"spec_sha256": "43dcbb2dd166f749010bbc6c21cbd6765d700d103672a9a836c18a77c5dbf1ea",
"spec_sha256": "196f000c18bd775c5cfbdd1c571d723e4797bba6c32865e13e67197fb57faba1",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "609353e10bfd37a9bbb6a79ecf2a32f3d3c02d5d161879a14ad4713e49e7e5e8",
@@ -14,10 +14,10 @@
},
"outputs": [
"bff-store-lookup-keys.svg",
"bff-store-lookup-keys.drawio",
"bff-store-lookup-keys.mmd",
"bff-store-lookup-keys.d2",
"bff-store-lookup-keys.dot",
"bff-store-lookup-keys.drawio",
"bff-store-lookup-keys.excalidraw",
"bff-store-lookup-keys.alt.md"
],
@@ -6,8 +6,8 @@ flowchart LR
n2["OAuth2AuthorizedClient"]
n3[("Redis")]
n4[("PostgreSQL")]
n0 -->|"세션 id 로 조회"| n1
n0 -->|"principal 이름으로 조회"| n2
n0 -->|"조회"| n1
n0 -->|"조회"| n2
n1 -->|"저장"| n3
n2 -->|"저장"| n4
classDef external stroke-dasharray: 6 4
@@ -54,11 +54,11 @@
<rect class="edge-label-bg" x="650.0" y="257.0" width="44.0" height="22" rx="3" />
<text class="edge-label" x="672.0" y="272.0">저장</text>
<polyline class="edge kind-read style-solid emphasis-normal" points="220.0,202.0 300.0,202.0 300.0,264.5 380.0,264.5" data-evidence="332-341" />
<rect class="edge-label-bg" x="258.1" y="219.2" width="131.9" height="22" rx="3" />
<text class="edge-label" x="324.0" y="234.2">principal 이름으로 조회</text>
<rect class="edge-label-bg" x="302.0" y="219.2" width="44.0" height="22" rx="3" />
<text class="edge-label" x="324.0" y="234.2">조회</text>
<polyline class="edge kind-read style-solid emphasis-normal" points="220.0,184.0 305.2,184.0 305.2,121.5 390.5,121.5" data-evidence="326-331" />
<rect class="edge-label-bg" x="286.8" y="138.8" width="85.0" height="22" rx="3" />
<text class="edge-label" x="329.2" y="153.8">세션 id 로 조회</text>
<rect class="edge-label-bg" x="307.2" y="138.8" width="44.0" height="22" rx="3" />
<text class="edge-label" x="329.2" y="153.8">조회</text>
<polyline class="edge kind-write style-solid emphasis-normal" points="557.5,121.5 646.2,121.5 646.2,108.5 735.0,108.5" data-evidence="347-351" />
<rect class="edge-label-bg" x="648.2" y="101.0" width="44.0" height="22" rx="3" />
<text class="edge-label" x="670.2" y="116.0">저장</text>

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

@@ -10,7 +10,7 @@ C-1 에서 로그아웃이 다른 앱으로 퍼지지 않는 것을 관측했고
## Elements and evidence
- **한 앱에서 로그아웃** (actor): Keycloak 세션이 끝난다. Evidence: L458L465.
- **한 앱 로그아웃** (actor): 한 앱에서 로그아웃하면 Keycloak 세션이 끝난다. Evidence: L458L465.
- **backchannelLogoutUrl** (component): 클라이언트 설정. 비어 있었다. Evidence: L458L466.
- **앱의 수신 엔드포인트** (component): 소스에 없었다. Evidence: L458L466.
- **다른 앱의 세션** (component): 그대로 남는다. Evidence: L458L466.
@@ -19,4 +19,4 @@ C-1 에서 로그아웃이 다른 앱으로 퍼지지 않는 것을 관측했고
- **backchannelLogoutUrl → 앱의 수신 엔드포인트:** 호출 (주소 필요). Evidence: L458L466.
- **앱의 수신 엔드포인트 → 다른 앱의 세션:** 세션 삭제 (엔드포인트 필요). Evidence: L458L466.
- **한 앱에서 로그아웃 → backchannelLogoutUrl:** 통지 주소 조회. Evidence: L458L466.
- **한 앱 로그아웃 → backchannelLogoutUrl:** 통지 주소 조회. Evidence: L458L466.
@@ -1,7 +1,7 @@
# 백채널 로그아웃은 양쪽이 있어야 한다
# Question: IdP 쪽만 설정하면 로그아웃이 퍼지는가
direction: down
n0: "한 앱에서 로그아웃" {
n0: "한 앱 로그아웃" {
shape: person
}
n1: "backchannelLogoutUrl" {
@@ -2,7 +2,7 @@ digraph techviz {
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="한 앱에서 로그아웃", shape=box, style="rounded,dashed,filled"];
n0 [label="한 앱 로그아웃", shape=box, style="rounded,dashed,filled"];
n1 [label="backchannelLogoutUrl", shape=box, style="rounded,filled"];
n2 [label="앱의 수신 엔드포인트", shape=box, style="rounded,filled"];
n3 [label="다른 앱의 세션", shape=box, style="rounded,filled"];
@@ -5,7 +5,7 @@
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_logout" value="한 앱에서 로그아웃" tooltip="Keycloak 세션이 끝난다. | Evidence: L458-L465" 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">
<mxCell id="n_logout" value="한 앱 로그아웃" tooltip="한 앱에서 로그아웃하면 Keycloak 세션이 끝난다. | Evidence: L458-L465" 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="82.0" y="60.0" width="150.0" height="84.0" as="geometry"/>
</mxCell>
<mxCell id="n_kc-config" value="backchannelLogoutUrl&lt;br/&gt;주소 없음" tooltip="클라이언트 설정. 비어 있었다. | Evidence: L458-L466" 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">
@@ -361,11 +361,11 @@
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "한 앱에서 로그아웃",
"text": "한 앱 로그아웃",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "한 앱에서 로그아웃",
"originalText": "한 앱 로그아웃",
"autoResize": true,
"lineHeight": 1.25
},
@@ -2,7 +2,7 @@
"harness_version": "0.2.0",
"spec_id": "c2-backchannel-both-sides",
"spec_version": "1.1",
"spec_sha256": "dd700a72a2ec7d5427681b95610e9715385241a81c78e2e9dce2aafdee32b35c",
"spec_sha256": "7197b688cc0a3bdd859bda6c2bc4c32af3e562e2bd165d27d50e66aa63bc942d",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
@@ -14,10 +14,10 @@
},
"outputs": [
"c2-backchannel-both-sides.svg",
"c2-backchannel-both-sides.drawio",
"c2-backchannel-both-sides.mmd",
"c2-backchannel-both-sides.d2",
"c2-backchannel-both-sides.dot",
"c2-backchannel-both-sides.drawio",
"c2-backchannel-both-sides.excalidraw",
"c2-backchannel-both-sides.alt.md"
],
@@ -1,7 +1,7 @@
%% 백채널 로그아웃은 양쪽이 있어야 한다
%% question: IdP 쪽만 설정하면 로그아웃이 퍼지는가
flowchart TB
n0(["한 앱에서 로그아웃"])
n0(["한 앱 로그아웃"])
n1["backchannelLogoutUrl"]
n2["앱의 수신 엔드포인트"]
n3["다른 앱의 세션"]
@@ -61,7 +61,7 @@
<text class="edge-label" x="181.0" y="193.0">통지 주소 조회</text>
<g id="node-logout">
<g class="actor-symbol emphasis-primary" data-evidence="458-465"><circle cx="157.0" cy="80.0" r="11.0" /><line x1="157.0" y1="96.0" x2="157.0" y2="105.0" /><line x1="139.0" y1="106.0" x2="175.0" y2="106.0" /><line x1="157.0" y1="105.0" x2="142.0" y2="122.0" /><line x1="157.0" y1="105.0" x2="172.0" y2="122.0" /></g>
<text class="node-label" x="157.0" y="137.0">한 앱에서 로그아웃</text>
<text class="node-label" x="157.0" y="137.0">한 앱 로그아웃</text>
</g>
<g id="node-kc-config">
<rect class="node-shape kind-component emphasis-warning role-control" data-evidence="458-466" x="70.0" y="240.0" width="174.0" height="71.0" rx="7" />

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

@@ -0,0 +1,32 @@
# 게스트가 하드웨어에 닿는 갈래는 셋이고 깊이가 다르다
## Alternative text
게스트·호스트 유저공간·호스트 커널·하드웨어 네 층을 가로지르는 세 갈래 경로도.
## 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 와 나누는 길이 따로 있다. 앞의 둘은 이 실험대가 쓰고 패스스루는 쓰지 않는다.
## Elements and evidence
- **QEMU** (service): KVM 핸들을 열고 VM 과 vCPU 를 만드는 유저공간 프로세스. vcpu ioctl 은 그 vcpu 를 만든 스레드에서 낸다. Evidence: L1102L1108.
- **VFIO** (service): IOMMU 보호 아래 장치 접근을 유저공간에 여는 프레임워크. 소유 단위가 IOMMU 그룹이다. Evidence: L1131L1146.
- **물리 PCI 장치** (service): 호스트 드라이버에서 떼어 vfio-pci 에 묶은 장치. Evidence: L1131L1133, L1144L1146.
- **IOMMU** (service): 장치가 아무 메모리나 건드리지 못하게 막는 리매핑 장치. Evidence: L1144L1146.
- **KVM** (service): mmap 한 공유 메모리로 나온 이유를 유저공간에 알리는 커널 쪽. Evidence: L1105L1108.
- **물리 CPU** (service): VM entry 때 guest-state 영역에서 상태를 싣고 VM exit 때 그리로 저장하는 프로세서. Evidence: L1111L1113.
- **게스트 코드** (service): VMX non-root 로 물리 CPU 에서 도는 게스트 명령. Evidence: L1111L1113.
- **게스트 virtio 드라이버** (service): 게스트가 보는 반가상화 장치의 드라이버. 주고받는 통로가 virtqueue 다. Evidence: L1116L1118.
- **vhost** (service): virtqueue 를 QEMU 밖과 나누는 커널 구현. vhost-user 규약이 이것을 제어하는 ioctl 인터페이스를 보완한다. Evidence: L1125L1129.
## Relationships
- **물리 CPU → 게스트 코드:** VMX non-root. Evidence: L1111L1112.
- **물리 PCI 장치 → IOMMU:** DMA. Evidence: L1144L1146.
- **KVM → 물리 CPU:** VM entry. Evidence: L1111L1112.
- **QEMU → VFIO:** 장치 fd. Evidence: L1138L1141.
- **QEMU → KVM:** KVM_RUN. Evidence: L1104L1106.
- **QEMU → vhost:** 제어 ioctl. Evidence: L1125L1129.
- **QEMU → 게스트 virtio 드라이버:** virtqueue. Evidence: L1116L1118, L1128L1128.
- **VFIO → 물리 PCI 장치:** vfio-pci 결합. Evidence: L1144L1146.
@@ -0,0 +1,38 @@
# 게스트가 하드웨어에 닿는 갈래는 셋이고 깊이가 다르다
# Question: 게스트의 CPU 실행·virtio I/O·직접 할당한 장치는 각각 어디까지 내려가고 호스트 유저공간을 지나는가
direction: down
n0: "QEMU" {
shape: rectangle
}
n1: "VFIO" {
shape: rectangle
}
n2: "물리 PCI 장치" {
shape: rectangle
}
n3: "IOMMU" {
shape: rectangle
}
n4: "KVM" {
shape: rectangle
}
n5: "물리 CPU" {
shape: rectangle
}
n6: "게스트 코드" {
shape: rectangle
}
n7: "게스트 virtio 드라이버" {
shape: rectangle
}
n8: "vhost" {
shape: rectangle
}
n0 -> n1: "장치 fd"
n1 -> n2: "vfio-pci 결합"
n2 -> n3: "DMA"
n0 -> n4: "KVM_RUN"
n4 -> n5: "VM entry"
n5 -> n6: "VMX non-root"
n0 -> n7: "virtqueue"
n0 -> n8: "제어 ioctl"
@@ -0,0 +1,78 @@
<?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="cpu-io-passthrough-paths" name="게스트가 하드웨어에 닿는 갈래는 셋이고 깊이가 다르다">
<mxGraphModel dx="985" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="985" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_qemu" value="QEMU&lt;br/&gt;/dev/kvm&lt;br/&gt;KVM_CREATE_VM · KVM_CREATE_VCPU" tooltip="KVM 핸들을 열고 VM 과 vCPU 를 만드는 유저공간 프로세스. vcpu ioctl 은 그 vcpu 를 만든 스레드에서 낸다. | Evidence: L1102-L1108" 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="379.5" y="60.0" width="251.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="n_kvm" value="KVM&lt;br/&gt;struct kvm_run&lt;br/&gt;KVM_GET_VCPU_MMAP_SIZE" tooltip="mmap 한 공유 메모리로 나온 이유를 유저공간에 알리는 커널 쪽. | Evidence: L1105-L1108" 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="252.5" width="188.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="n_vfio" value="VFIO&lt;br/&gt;vfio-pci&lt;br/&gt;IOMMU 그룹&lt;br/&gt;/dev/vfio/vfio" tooltip="IOMMU 보호 아래 장치 접근을 유저공간에 여는 프레임워크. 소유 단위가 IOMMU 그룹이다. | Evidence: L1131-L1146" 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="318.0" y="244.0" width="150.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_vhost" value="vhost&lt;br/&gt;리눅스 커널 구현&lt;br/&gt;virtqueue 공유" tooltip="virtqueue 를 QEMU 밖과 나누는 커널 구현. vhost-user 규약이 이것을 제어하는 ioctl 인터페이스를 보완한다. | Evidence: L1125-L1129" 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="528.0" y="252.5" width="150.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="n_virtio-driver" value="게스트 virtio 드라이버&lt;br/&gt;virtio-pci · virtio-mmio&lt;br/&gt;virtqueue" tooltip="게스트가 보는 반가상화 장치의 드라이버. 주고받는 통로가 virtqueue 다. | Evidence: L1116-L1118" 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="738.0" y="252.5" width="202.0" height="88.0" as="geometry"/>
</mxCell>
<mxCell id="n_physical-cpu" value="물리 CPU&lt;br/&gt;VMX root · VMX non-root&lt;br/&gt;VM entry · VM exit&lt;br/&gt;guest-state 영역" tooltip="VM entry 때 guest-state 영역에서 상태를 싣고 VM exit 때 그리로 저장하는 프로세서. | Evidence: L1111-L1113" 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="302.5" y="445.0" width="195.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_pci-device" value="물리 PCI 장치&lt;br/&gt;게스트 직접 할당" tooltip="호스트 드라이버에서 떼어 vfio-pci 에 묶은 장치. | Evidence: L1131-L1133, L1144-L1146" 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="557.5" y="462.0" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_guest-code" value="게스트 코드&lt;br/&gt;vCPU" tooltip="VMX non-root 로 물리 CPU 에서 도는 게스트 명령. | Evidence: L1111-L1113" 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="325.0" y="646.0" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_iommu" value="IOMMU&lt;br/&gt;DMA · 인터럽트 리매핑" tooltip="장치가 아무 메모리나 건드리지 못하게 막는 리매핑 장치. | Evidence: L1144-L1146" 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="535.0" y="646.0" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="e_cpu-runs-guest" value="VMX non-root" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_physical-cpu" target="n_guest-code">
<mxGeometry relative="1" as="geometry">
<mxPoint x="424.0" y="598.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_device-dma" value="DMA" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_pci-device" target="n_iommu">
<mxGeometry relative="1" as="geometry">
<mxPoint x="621.2" y="561.5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_kvm-vm-entry" value="VM entry" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_kvm" target="n_physical-cpu">
<mxGeometry relative="1" as="geometry">
<mxPoint x="282.0" y="364.8" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_qemu-device-fd" value="장치 fd" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_qemu" target="n_vfio">
<mxGeometry relative="1" as="geometry">
<mxPoint x="444.5" y="168.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_qemu-kvm-run" value="KVM_RUN" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_qemu" target="n_kvm">
<mxGeometry relative="1" as="geometry">
<mxPoint x="321.0" y="172.2" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_qemu-vhost-ioctl" value="제어 ioctl" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_qemu" target="n_vhost">
<mxGeometry relative="1" as="geometry">
<mxPoint x="558.5" y="172.2" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_qemu-virtqueue" value="virtqueue" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_qemu" target="n_virtio-driver">
<mxGeometry relative="1" as="geometry">
<mxPoint x="685.5" y="172.2" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_vfio-binds-device" value="vfio-pci 결합" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_vfio" target="n_pci-device">
<mxGeometry relative="1" as="geometry">
<mxPoint x="512.8" y="377.5" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,31 @@
{
"harness_version": "0.2.0",
"spec_id": "cpu-io-passthrough-paths",
"spec_version": "1.1",
"spec_sha256": "981204dcf528aa1c3b0a400d3ea0d11fb5c26250adaf6e7929f9f1bf39b72f3a",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "0ae5674723f25dc85d5069529890f07be1a11b768b56901103fa3ea6ac3dcf55",
"anchor": {
"kind": "heading",
"value": "이 층 아래의 구조 — 조사한 것",
"line": 1091
}
},
"outputs": [
"cpu-io-passthrough-paths.svg",
"cpu-io-passthrough-paths.drawio",
"cpu-io-passthrough-paths.mmd",
"cpu-io-passthrough-paths.d2",
"cpu-io-passthrough-paths.excalidraw",
"cpu-io-passthrough-paths.alt.md"
],
"lint_issue_count": 5,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "component-flow",
"reference_ids": [
"payment-event-flow"
],
"diagram_only": true
}
@@ -0,0 +1,20 @@
%% 게스트가 하드웨어에 닿는 갈래는 셋이고 깊이가 다르다
%% question: 게스트의 CPU 실행·virtio I/O·직접 할당한 장치는 각각 어디까지 내려가고 호스트 유저공간을 지나는가
flowchart TB
n0["QEMU"]
n1["VFIO"]
n2["물리 PCI 장치"]
n3["IOMMU"]
n4["KVM"]
n5["물리 CPU"]
n6["게스트 코드"]
n7["게스트 virtio 드라이버"]
n8["vhost"]
n0 -->|"장치 fd"| n1
n1 -->|"vfio-pci 결합"| n2
n2 -->|"DMA"| n3
n0 -->|"KVM_RUN"| n4
n4 -->|"VM entry"| n5
n5 -->|"VMX non-root"| n6
n0 -->|"virtqueue"| n7
n0 -->|"제어 ioctl"| n8
@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="985" height="762" viewBox="0 0 985 762" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">게스트가 하드웨어에 닿는 갈래는 셋이고 깊이가 다르다</title>
<desc id="diagram-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 와 나누는 길이 따로 있다. 앞의 둘은 이 실험대가 쓰고 패스스루는 쓰지 않는다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;cpu-io-passthrough-paths&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;0ae5674723f25dc85d5069529890f07be1a11b768b56901103fa3ea6ac3dcf55&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;이 층 아래의 구조 — 조사한 것&quot;,&quot;line&quot;:1091}},&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" />
</marker>
<style>
:root { color-scheme: light; }
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
.canvas { fill: #ffffff; }
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
.group-label-bg { fill: #ffffff; }
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
.edge.style-dotted { stroke-dasharray: 2 5; }
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.edge.emphasis-muted { stroke: #9ca3af; }
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
.edge-label-bg { fill: #ffffff; }
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
.node-shape.assumption { stroke-dasharray: 4 4; }
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
.controller-led { fill: #4b5563; }
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
.node-detail { font-size: 11px; fill: #374151; }
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="985" height="762" />
<polyline class="edge kind-data style-solid emphasis-normal" points="400.0,550.0 400.0,598.0 400.0,598.0 400.0,646.0" data-evidence="1111-1112" />
<rect class="edge-label-bg" x="374.8" y="584.0" width="98.4" height="22" rx="3" />
<text class="edge-label" x="424.0" y="599.0">VMX non-root</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="632.5,533.0 632.5,589.5 610.0,589.5 610.0,646.0" data-evidence="1144-1146" />
<rect class="edge-label-bg" x="599.2" y="547.5" width="44.0" height="22" rx="3" />
<text class="edge-label" x="621.2" y="562.5">DMA</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="164.0,340.5 164.0,392.8 400.0,392.8 400.0,445.0" data-evidence="1111-1112" />
<rect class="edge-label-bg" x="246.2" y="350.8" width="71.6" height="22" rx="3" />
<text class="edge-label" x="282.0" y="365.8">VM entry</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="496.0,148.0 496.0,196.0 393.0,196.0 393.0,244.0" data-evidence="1138-1141" />
<rect class="edge-label-bg" x="418.8" y="154.0" width="51.5" height="22" rx="3" />
<text class="edge-label" x="444.5" y="169.0">장치 fd</text>
<polyline class="edge kind-request style-solid emphasis-normal" points="478.0,148.0 478.0,200.2 164.0,200.2 164.0,252.5" data-evidence="1104-1106" />
<rect class="edge-label-bg" x="288.6" y="158.2" width="64.9" height="22" rx="3" />
<text class="edge-label" x="321.0" y="173.2">KVM_RUN</text>
<polyline class="edge kind-control style-dashed emphasis-normal" points="514.0,148.0 514.0,200.2 603.0,200.2 603.0,252.5" data-evidence="1125-1129" />
<rect class="edge-label-bg" x="522.7" y="158.2" width="71.6" height="22" rx="3" />
<text class="edge-label" x="558.5" y="173.2">제어 ioctl</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="532.0,148.0 532.0,200.2 839.0,200.2 839.0,252.5" data-evidence="1116-1118,1128-1128" />
<rect class="edge-label-bg" x="646.4" y="158.2" width="78.3" height="22" rx="3" />
<text class="edge-label" x="685.5" y="173.2">virtqueue</text>
<polyline class="edge kind-control style-solid emphasis-normal" points="393.0,349.0 393.0,405.5 632.5,405.5 632.5,462.0" data-evidence="1144-1146" />
<rect class="edge-label-bg" x="466.9" y="363.5" width="91.7" height="22" rx="3" />
<text class="edge-label" x="512.8" y="378.5">vfio-pci 결합</text>
<g id="node-qemu">
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="1102-1108" x="379.5" y="60.0" width="251.0" height="88.0" rx="7" />
<text class="node-label" x="505.0" y="87.0">QEMU</text>
<line class="node-detail-divider" x1="393.5" y1="108.0" x2="616.5" y2="108.0" />
<text class="node-detail" x="395.5" y="125.0">/dev/kvm</text>
<text class="node-detail" x="395.5" y="141.0">KVM_CREATE_VM · KVM_CREATE_VCPU</text>
</g>
<g id="node-kvm">
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="1105-1108" x="70.0" y="252.5" width="188.0" height="88.0" rx="7" />
<text class="node-label" x="164.0" y="279.5">KVM</text>
<line class="node-detail-divider" x1="84.0" y1="300.5" x2="244.0" y2="300.5" />
<text class="node-detail" x="86.0" y="317.5">struct kvm_run</text>
<text class="node-detail" x="86.0" y="333.5">KVM_GET_VCPU_MMAP_SIZE</text>
</g>
<g id="node-vfio">
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="1131-1146" x="318.0" y="244.0" width="150.0" height="105.0" rx="7" />
<text class="node-label" x="393.0" y="271.0">VFIO</text>
<line class="node-detail-divider" x1="332.0" y1="292.0" x2="454.0" y2="292.0" />
<text class="node-detail" x="334.0" y="309.0">vfio-pci</text>
<text class="node-detail" x="334.0" y="325.0">IOMMU 그룹</text>
<text class="node-detail" x="334.0" y="341.0">/dev/vfio/vfio</text>
</g>
<g id="node-vhost">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="1125-1129" x="528.0" y="252.5" width="150.0" height="88.0" rx="7" />
<text class="node-label" x="603.0" y="279.5">vhost</text>
<line class="node-detail-divider" x1="542.0" y1="300.5" x2="664.0" y2="300.5" />
<text class="node-detail" x="544.0" y="317.5">리눅스 커널 구현</text>
<text class="node-detail" x="544.0" y="333.5">virtqueue 공유</text>
</g>
<g id="node-virtio-driver">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="1116-1118" x="738.0" y="252.5" width="202.0" height="88.0" rx="7" />
<text class="node-label" x="839.0" y="279.5">게스트 virtio 드라이버</text>
<line class="node-detail-divider" x1="752.0" y1="300.5" x2="926.0" y2="300.5" />
<text class="node-detail" x="754.0" y="317.5">virtio-pci · virtio-mmio</text>
<text class="node-detail" x="754.0" y="333.5">virtqueue</text>
</g>
<g id="node-physical-cpu">
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="1111-1113" x="302.5" y="445.0" width="195.0" height="105.0" rx="7" />
<text class="node-label" x="400.0" y="472.0">물리 CPU</text>
<line class="node-detail-divider" x1="316.5" y1="493.0" x2="483.5" y2="493.0" />
<text class="node-detail" x="318.5" y="510.0">VMX root · VMX non-root</text>
<text class="node-detail" x="318.5" y="526.0">VM entry · VM exit</text>
<text class="node-detail" x="318.5" y="542.0">guest-state 영역</text>
</g>
<g id="node-pci-device">
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="1131-1133,1144-1146" x="557.5" y="462.0" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="632.5" y="489.0">물리 PCI 장치</text>
<line class="node-detail-divider" x1="571.5" y1="510.0" x2="693.5" y2="510.0" />
<text class="node-detail" x="573.5" y="527.0">게스트 직접 할당</text>
</g>
<g id="node-guest-code">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="1111-1113" x="325.0" y="646.0" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="400.0" y="673.0">게스트 코드</text>
<line class="node-detail-divider" x1="339.0" y1="694.0" x2="461.0" y2="694.0" />
<text class="node-detail" x="341.0" y="711.0">vCPU</text>
</g>
<g id="node-iommu">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="1144-1146" x="535.0" y="646.0" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="610.0" y="673.0">IOMMU</text>
<line class="node-detail-divider" x1="549.0" y1="694.0" x2="671.0" y2="694.0" />
<text class="node-detail" x="551.0" y="711.0">DMA · 인터럽트 리매핑</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -10,7 +10,7 @@
## Elements and evidence
- **옛 버전으로 되돌리기** (process): 이미지 태그를 되돌린다. Evidence: L483L490.
- **옛 버전 롤백** (process): 이미지 태그를 옛 버전으로 되돌린다. Evidence: L483L490.
- **Liquibase 검증** (process): 새 버전이 남긴 체크섬을 거부한다. Evidence: L483L491.
- **StatefulSet 롤링 업데이트** (process): 첫 파드가 실패하면 거기서 멈춘다. Evidence: L492L497.
- **외부 서비스** (service): 남은 파드가 계속 200 을 낸다. Evidence: L492L498.
@@ -20,5 +20,5 @@
- **Liquibase 검증 → databasechangelog 행 수:** 판단 기준. Evidence: L499L505.
- **Liquibase 검증 → StatefulSet 롤링 업데이트:** 중단 지점. Evidence: L492L497.
- **옛 버전으로 되돌리기 → Liquibase 검증:** 기동 시 검증. Evidence: L483L491.
- **옛 버전 롤백 → Liquibase 검증:** 기동 시 검증. Evidence: L483L491.
- **StatefulSet 롤링 업데이트 → 외부 서비스:** 잔여 파드 응답. Evidence: L492L498.
@@ -1,7 +1,7 @@
# 방향에 따라 갈리는 업그레이드
# Question: 되돌릴 수 있는지는 무엇이 결정하는가
direction: down
n0: "옛 버전으로 되돌리기" {
n0: "옛 버전 롤백" {
shape: rectangle
}
n1: "Liquibase 검증" {
@@ -2,7 +2,7 @@ digraph techviz {
graph [rankdir=TB, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="옛 버전으로 되돌리기", shape=box, style="rounded,filled"];
n0 [label="옛 버전 롤백", shape=box, style="rounded,filled"];
n1 [label="Liquibase 검증", shape=box, style="rounded,filled"];
n2 [label="StatefulSet 롤링 업데이트", shape=box, style="rounded,filled"];
n3 [label="외부 서비스", shape=box, style="rounded,filled"];
@@ -5,7 +5,7 @@
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_rollback" value="옛 버전으로 되돌리기" tooltip="이미지 태그를 되돌린다. | Evidence: L483-L490" 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">
<mxCell id="n_rollback" value="옛 버전 롤백" tooltip="이미지 태그를 옛 버전으로 되돌린다. | Evidence: L483-L490" 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="199.0" y="60.0" width="150.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_liquibase" value="Liquibase 검증&lt;br/&gt;ValidationFailedException: 1 changesets check sum" tooltip="새 버전이 남긴 체크섬을 거부한다. | Evidence: L483-L491" 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">
@@ -460,11 +460,11 @@
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "옛 버전으로 되돌리기",
"text": "옛 버전 롤백",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "옛 버전으로 되돌리기",
"originalText": "옛 버전 롤백",
"autoResize": true,
"lineHeight": 1.25
},
@@ -2,7 +2,7 @@
"harness_version": "0.2.0",
"spec_id": "d2-upgrade-direction",
"spec_version": "1.1",
"spec_sha256": "7f7090d0f9f237909f548d3f67104bb98dba10e94b63ca7e6e0c684d92afe199",
"spec_sha256": "1900af5e51837783471656df7ad19888dbca5f323396175b7d4f1a23d8a6f444",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
@@ -14,10 +14,10 @@
},
"outputs": [
"d2-upgrade-direction.svg",
"d2-upgrade-direction.drawio",
"d2-upgrade-direction.mmd",
"d2-upgrade-direction.d2",
"d2-upgrade-direction.dot",
"d2-upgrade-direction.drawio",
"d2-upgrade-direction.excalidraw",
"d2-upgrade-direction.alt.md"
],
@@ -1,7 +1,7 @@
%% 방향에 따라 갈리는 업그레이드
%% question: 되돌릴 수 있는지는 무엇이 결정하는가
flowchart TB
n0["옛 버전으로 되돌리기"]
n0["옛 버전 롤백"]
n1["Liquibase 검증"]
n2["StatefulSet 롤링 업데이트"]
n3["외부 서비스"]
@@ -64,7 +64,7 @@
<text class="edge-label" x="334.2" y="498.5">잔여 파드 응답</text>
<g id="node-rollback">
<rect class="node-shape kind-process emphasis-warning role-source" data-evidence="483-490" x="199.0" y="60.0" width="150.0" height="64.0" rx="7" />
<text class="node-label" x="274.0" y="90.0">옛 버전으로 되돌리기</text>
<text class="node-label" x="274.0" y="90.0">옛 버전 롤백</text>
</g>
<g id="node-liquibase">
<rect class="node-shape kind-process emphasis-warning role-control" data-evidence="483-491" x="124.0" y="220.0" width="300.0" height="71.0" rx="7" />

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,27 @@
# 게스트 두 대는 호스트에서 qemu 프로세스 두 개다
## Alternative text
호스트 안에 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 는 이 그림에 넣지 않았고 본문이 맡는다.
## Elements and evidence
- **Boundary: 호스트 test-server** (system): libvirtd 와 게스트 프로세스가 함께 도는 호스트 경계. Evidence: L946L948, L956L956.
- **Boundary: 게스트 kc-lab-1** (system): qemu 프로세스 하나가 담는 Debian 게스트. Evidence: L987L990, L1039L1039.
- **Boundary: 게스트 kc-lab-2** (system): 나머지 qemu 프로세스가 담는 Debian 게스트. Evidence: L1033L1033, L1040L1040.
- **libvirtd** (service): 게스트마다 qemu-system-x86_64 를 하나씩 띄우는 호스트 데몬. htop 트리 뷰에서 그 아래에 게스트 프로세스가 달린다. Evidence: L946L948, L956L956.
- **qemu-system-x86_64** (service): kc-lab-1 게스트 전체가 들어 있는 호스트 프로세스. vCPU 는 이 프로세스의 스레드이고 RSS 는 게스트가 터치한 페이지만큼이다. Evidence: L946L948, L987L987, L1032L1032.
- **qemu-system-x86_64** (service): kc-lab-2 게스트 전체가 들어 있는 호스트 프로세스. A-4 의 virsh destroy 가 끊는 것이 이 프로세스다. Evidence: L946L948, L967L976, L1033L1033.
- **Debian 게스트 커널** (service): virtio 장치만 보는 게스트. free 가 읽는 값이 여기 있다. Evidence: L994L998, L1039L1039.
- **Debian 게스트 커널** (service): 같은 방식으로 붙은 두 번째 게스트. Evidence: L994L998, L1040L1040.
## Relationships
- **libvirtd → qemu-system-x86_64:** 프로세스 생성. Evidence: L946L948, L956L956.
- **libvirtd → qemu-system-x86_64:** 프로세스 생성. Evidence: L946L948, L956L956.
- **qemu-system-x86_64 → Debian 게스트 커널:** virtio 디스크 · virtio-net. Evidence: L987L990, L994L998.
- **qemu-system-x86_64 → Debian 게스트 커널:** virtio 디스크 · virtio-net. Evidence: L987L990, L994L998.
@@ -0,0 +1,28 @@
# 게스트 두 대는 호스트에서 qemu 프로세스 두 개다
# Question: 호스트에서 게스트는 무엇으로 존재하고, 게스트가 보는 장치는 누가 만들어 주는가
direction: down
g0: "호스트 test-server" {
n0: "libvirtd" {
shape: rectangle
}
n1: "qemu-system-x86_64" {
shape: rectangle
}
n2: "qemu-system-x86_64" {
shape: rectangle
}
}
g1: "게스트 kc-lab-1" {
n3: "Debian 게스트 커널" {
shape: rectangle
}
}
g2: "게스트 kc-lab-2" {
n4: "Debian 게스트 커널" {
shape: rectangle
}
}
g0.n0 -> g0.n1: "프로세스 생성"
g0.n0 -> g0.n2: "프로세스 생성"
g0.n1 -> g1.n3: "virtio 디스크 · virtio-net"
g0.n2 -> g2.n4: "virtio 디스크 · virtio-net"
@@ -0,0 +1,55 @@
<?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="guest-as-host-process" name="게스트 두 대는 호스트에서 qemu 프로세스 두 개다">
<mxGraphModel dx="680" dy="642" 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">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="g_host-zone" value="호스트 test-server" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="75.0" y="35.0" width="440.0" height="361.0" as="geometry"/>
</mxCell>
<mxCell id="g_guest-1-zone" value="게스트 kc-lab-1" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="40.0" y="420.0" width="255.0" height="177.0" as="geometry"/>
</mxCell>
<mxCell id="g_guest-2-zone" value="게스트 kc-lab-2" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="295.0" y="420.0" width="255.0" height="177.0" as="geometry"/>
</mxCell>
<mxCell id="n_libvirtd" value="libvirtd&lt;br/&gt;qemu:///system" tooltip="게스트마다 qemu-system-x86_64 를 하나씩 띄우는 호스트 데몬. htop 트리 뷰에서 그 아래에 게스트 프로세스가 달린다. | Evidence: L946-L948, L956-L956" 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="220.0" y="81.0" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_qemu-kc-lab-1" value="qemu-system-x86_64&lt;br/&gt;kc-lab-1&lt;br/&gt;vCPU 2&lt;br/&gt;할당 3584MB&lt;br/&gt;RSS 3765MB" tooltip="kc-lab-1 게스트 전체가 들어 있는 호스트 프로세스. vCPU 는 이 프로세스의 스레드이고 RSS 는 게스트가 터치한 페이지만큼이다. | Evidence: L946-L948, L987-L987, L1032-L1032" 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="105.0" y="248.0" width="160.0" height="122.0" as="geometry"/>
</mxCell>
<mxCell id="n_qemu-kc-lab-2" value="qemu-system-x86_64&lt;br/&gt;kc-lab-2&lt;br/&gt;할당 2560MB&lt;br/&gt;RSS 2633MB" tooltip="kc-lab-2 게스트 전체가 들어 있는 호스트 프로세스. A-4 의 virsh destroy 가 끊는 것이 이 프로세스다. | Evidence: L946-L948, L967-L976, L1033-L1033" 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="325.0" y="256.5" width="160.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_guest-kernel-1" value="Debian 게스트 커널&lt;br/&gt;enp1s0 · 192.168.122.11&lt;br/&gt;총 3423MB&lt;br/&gt;available 1959MB" tooltip="virtio 장치만 보는 게스트. free 가 읽는 값이 여기 있다. | Evidence: L994-L998, L1039-L1039" 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="466.0" width="195.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_guest-kernel-2" value="Debian 게스트 커널&lt;br/&gt;enp1s0 · 192.168.122.12&lt;br/&gt;총 2480MB&lt;br/&gt;available 1899MB" tooltip="같은 방식으로 붙은 두 번째 게스트. | Evidence: L994-L998, L1040-L1040" 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="325.0" y="466.0" width="195.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="e_libvirtd-spawns-1" value="프로세스 생성" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_libvirtd" target="n_qemu-kc-lab-1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="235.5" y="172.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_libvirtd-spawns-2" value="프로세스 생성" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_libvirtd" target="n_qemu-kc-lab-2">
<mxGeometry relative="1" as="geometry">
<mxPoint x="354.5" y="176.2" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_qemu1-virtio" value="virtio 디스크 · virtio-net" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_qemu-kc-lab-1" target="n_guest-kernel-1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="176.2" y="390.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_qemu2-virtio" value="virtio 디스크 · virtio-net" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_qemu-kc-lab-2" target="n_guest-kernel-2">
<mxGeometry relative="1" as="geometry">
<mxPoint x="413.8" y="385.8" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,961 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "group-host-zone",
"type": "rectangle",
"x": 75.0,
"y": 35.0,
"width": 440.0,
"height": 361.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#f8f9fa",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 870740741,
"version": 1,
"versionNonce": 412901763,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "group-label-host-zone",
"type": "text",
"x": 91.0,
"y": 41.0,
"width": 135,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1945047456,
"version": 1,
"versionNonce": 680659727,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 14,
"fontFamily": 5,
"text": "호스트 test-server",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "호스트 test-server",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "group-guest-1-zone",
"type": "rectangle",
"x": 40.0,
"y": 420.0,
"width": 255.0,
"height": 177.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#f8f9fa",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1946150759,
"version": 1,
"versionNonce": 1446291239,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "group-label-guest-1-zone",
"type": "text",
"x": 56.0,
"y": 426.0,
"width": 108,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 234158938,
"version": 1,
"versionNonce": 638600275,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 14,
"fontFamily": 5,
"text": "게스트 kc-lab-1",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "게스트 kc-lab-1",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "group-guest-2-zone",
"type": "rectangle",
"x": 295.0,
"y": 420.0,
"width": 255.0,
"height": 177.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#f8f9fa",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "dashed",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 27742558,
"version": 1,
"versionNonce": 176708121,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "group-label-guest-2-zone",
"type": "text",
"x": 311.0,
"y": 426.0,
"width": 108,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 746153233,
"version": 1,
"versionNonce": 1519863769,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 14,
"fontFamily": 5,
"text": "게스트 kc-lab-2",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "게스트 kc-lab-2",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-libvirtd-spawns-1",
"type": "arrow",
"x": 185.0,
"y": 152.0,
"width": 101.0,
"height": 96.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1250848425,
"version": 1,
"versionNonce": 1443824793,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
101.0,
0.0
],
[
101.0,
48.0
],
[
0.0,
48.0
],
[
0.0,
96.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-libvirtd",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-qemu-kc-lab-1",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-libvirtd-spawns-1",
"type": "text",
"x": 190.5,
"y": 160.0,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 124252151,
"version": 1,
"versionNonce": 186547372,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "프로세스 생성",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "프로세스 생성",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-libvirtd-spawns-2",
"type": "arrow",
"x": 304.0,
"y": 152.0,
"width": 101.0,
"height": 104.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 463063329,
"version": 1,
"versionNonce": 1576006883,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
0.0,
52.25
],
[
101.0,
52.25
],
[
101.0,
104.5
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-libvirtd",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-qemu-kc-lab-2",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-libvirtd-spawns-2",
"type": "text",
"x": 309.5,
"y": 164.25,
"width": 90,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 774873599,
"version": 1,
"versionNonce": 45588994,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "프로세스 생성",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "프로세스 생성",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-qemu1-virtio",
"type": "arrow",
"x": 167.5,
"y": 370.0,
"width": 17.5,
"height": 96.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1054083273,
"version": 1,
"versionNonce": 1609768765,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
17.5,
0.0
],
[
17.5,
48.0
],
[
0.0,
48.0
],
[
0.0,
96.0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-qemu-kc-lab-1",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-guest-kernel-1",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-qemu1-virtio",
"type": "text",
"x": 84.25,
"y": 378.0,
"width": 184,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1192631446,
"version": 1,
"versionNonce": 1279906491,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "virtio 디스크 · virtio-net",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "virtio 디스크 · virtio-net",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "edge-qemu2-virtio",
"type": "arrow",
"x": 405.0,
"y": 361.5,
"width": 17.5,
"height": 104.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": null,
"seed": 1173063830,
"version": 1,
"versionNonce": 1429081454,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"points": [
[
0.0,
0.0
],
[
0.0,
52.25
],
[
17.5,
52.25
],
[
17.5,
104.5
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "node-qemu-kc-lab-2",
"focus": 0,
"gap": 4
},
"endBinding": {
"elementId": "node-guest-kernel-2",
"focus": 0,
"gap": 4
},
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": true
},
{
"id": "edge-label-qemu2-virtio",
"type": "text",
"x": 321.75,
"y": 373.75,
"width": 184,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 158399087,
"version": 1,
"versionNonce": 1245508410,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 13,
"fontFamily": 5,
"text": "virtio 디스크 · virtio-net",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "virtio 디스크 · virtio-net",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-libvirtd",
"type": "rectangle",
"x": 220.0,
"y": 81.0,
"width": 150.0,
"height": 71.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1342418446,
"version": 1,
"versionNonce": 538925829,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-libvirtd",
"type": "text",
"x": 230.0,
"y": 91.0,
"width": 130.0,
"height": 51.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 547517362,
"version": 1,
"versionNonce": 605616290,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "libvirtd\nqemu:///system",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "libvirtd\nqemu:///system",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-qemu-kc-lab-1",
"type": "rectangle",
"x": 105.0,
"y": 248.0,
"width": 160.0,
"height": 122.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 151836861,
"version": 1,
"versionNonce": 221047748,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-qemu-kc-lab-1",
"type": "text",
"x": 115.0,
"y": 258.0,
"width": 140.0,
"height": 102.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 784659461,
"version": 1,
"versionNonce": 283887783,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "qemu-system-x86_64\nkc-lab-1\nvCPU 2\n할당 3584MB\nRSS 3765MB",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "qemu-system-x86_64\nkc-lab-1\nvCPU 2\n할당 3584MB\nRSS 3765MB",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-qemu-kc-lab-2",
"type": "rectangle",
"x": 325.0,
"y": 256.5,
"width": 160.0,
"height": 105.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1539760600,
"version": 1,
"versionNonce": 1076713752,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-qemu-kc-lab-2",
"type": "text",
"x": 335.0,
"y": 266.5,
"width": 140.0,
"height": 85.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 416935857,
"version": 1,
"versionNonce": 124918568,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "qemu-system-x86_64\nkc-lab-2\n할당 2560MB\nRSS 2633MB",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "qemu-system-x86_64\nkc-lab-2\n할당 2560MB\nRSS 2633MB",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-guest-kernel-1",
"type": "rectangle",
"x": 70.0,
"y": 466.0,
"width": 195.0,
"height": 105.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 324645886,
"version": 1,
"versionNonce": 234656840,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-guest-kernel-1",
"type": "text",
"x": 80.0,
"y": 476.0,
"width": 175.0,
"height": 85.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 383931435,
"version": 1,
"versionNonce": 838146256,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Debian 게스트 커널\nenp1s0 · 192.168.122.11\n총 3423MB\navailable 1959MB",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Debian 게스트 커널\nenp1s0 · 192.168.122.11\n총 3423MB\navailable 1959MB",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-guest-kernel-2",
"type": "rectangle",
"x": 325.0,
"y": 466.0,
"width": 195.0,
"height": 105.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1474124435,
"version": 1,
"versionNonce": 1748268600,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-guest-kernel-2",
"type": "text",
"x": 335.0,
"y": 476.0,
"width": 175.0,
"height": 85.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 516515521,
"version": 1,
"versionNonce": 1402202909,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "Debian 게스트 커널\nenp1s0 · 192.168.122.12\n총 2480MB\navailable 1899MB",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "Debian 게스트 커널\nenp1s0 · 192.168.122.12\n총 2480MB\navailable 1899MB",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -0,0 +1,31 @@
{
"harness_version": "0.2.0",
"spec_id": "guest-as-host-process",
"spec_version": "1.1",
"spec_sha256": "4095e88815b73ef3f87fca3e8a33d88f7d8534595d74f14f84986d71c63e2056",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "0ae5674723f25dc85d5069529890f07be1a11b768b56901103fa3ea6ac3dcf55",
"anchor": {
"kind": "heading",
"value": "0층. 가상화 — 「바닥」 아래에 있는 것",
"line": 935
}
},
"outputs": [
"guest-as-host-process.svg",
"guest-as-host-process.drawio",
"guest-as-host-process.mmd",
"guest-as-host-process.d2",
"guest-as-host-process.excalidraw",
"guest-as-host-process.alt.md"
],
"lint_issue_count": 0,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "component-flow",
"reference_ids": [
"payment-event-flow"
],
"diagram_only": true
}
@@ -0,0 +1,18 @@
%% 게스트 두 대는 호스트에서 qemu 프로세스 두 개다
%% question: 호스트에서 게스트는 무엇으로 존재하고, 게스트가 보는 장치는 누가 만들어 주는가
flowchart TB
subgraph g_host_zone["호스트 test-server"]
n0["libvirtd"]
n1["qemu-system-x86_64"]
n2["qemu-system-x86_64"]
end
subgraph g_guest_1_zone["게스트 kc-lab-1"]
n3["Debian 게스트 커널"]
end
subgraph g_guest_2_zone["게스트 kc-lab-2"]
n4["Debian 게스트 커널"]
end
n0 -->|"프로세스 생성"| n1
n0 -->|"프로세스 생성"| n2
n1 -->|"virtio 디스크 · virtio-net"| n3
n2 -->|"virtio 디스크 · virtio-net"| n4
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="642" viewBox="0 0 680 642" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">게스트 두 대는 호스트에서 qemu 프로세스 두 개다</title>
<desc id="diagram-description">위에서 아래로 읽는다. 호스트 test-server 경계 안에 libvirtd 가 있고 그 아래에 qemu-system-x86_64 프로세스가 게스트 수만큼 있다. 경계 밖 아래쪽에 게스트 kc-lab-1 과 kc-lab-2 가 각각 따로 있고, 게스트가 보는 디스크와 인터페이스는 자기를 담은 프로세스가 virtio 로 내준다. 프로세스에 적힌 RSS 와 게스트에 적힌 available 은 같은 메모리를 다른 껍질에서 읽은 값이다. machine.slice 와 virbr0 는 이 그림에 넣지 않았고 본문이 맡는다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;guest-as-host-process&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;0ae5674723f25dc85d5069529890f07be1a11b768b56901103fa3ea6ac3dcf55&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;0층. 가상화 — 「바닥」 아래에 있는 것&quot;,&quot;line&quot;:935}},&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" />
</marker>
<style>
:root { color-scheme: light; }
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
.canvas { fill: #ffffff; }
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
.group-label-bg { fill: #ffffff; }
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
.edge.style-dotted { stroke-dasharray: 2 5; }
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.edge.emphasis-muted { stroke: #9ca3af; }
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
.edge-label-bg { fill: #ffffff; }
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
.node-shape.assumption { stroke-dasharray: 4 4; }
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
.controller-led { fill: #4b5563; }
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
.node-detail { font-size: 11px; fill: #374151; }
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="680" height="642" />
<rect class="group-box" x="75.0" y="35.0" width="440.0" height="361.0" rx="8" />
<rect class="group-label-bg" x="89.0" y="25.0" width="127.0" height="22" />
<text class="group-label" x="99.0" y="40.0">호스트 test-server</text>
<rect class="group-box" x="40.0" y="420.0" width="255.0" height="177.0" rx="8" />
<rect class="group-label-bg" x="54.0" y="410.0" width="106.0" height="22" />
<text class="group-label" x="64.0" y="425.0">게스트 kc-lab-1</text>
<rect class="group-box" x="295.0" y="420.0" width="255.0" height="177.0" rx="8" />
<rect class="group-label-bg" x="309.0" y="410.0" width="106.0" height="22" />
<text class="group-label" x="319.0" y="425.0">게스트 kc-lab-2</text>
<polyline class="edge kind-control style-solid emphasis-normal" points="286.0,152.0 286.0,200.0 185.0,200.0 185.0,248.0" data-evidence="946-948,956-956" />
<rect class="edge-label-bg" x="203.1" y="158.0" width="64.9" height="22" rx="3" />
<text class="edge-label" x="235.5" y="173.0">프로세스 생성</text>
<polyline class="edge kind-control style-solid emphasis-normal" points="304.0,152.0 304.0,204.2 405.0,204.2 405.0,256.5" data-evidence="946-948,956-956" />
<rect class="edge-label-bg" x="322.1" y="162.2" width="64.9" height="22" rx="3" />
<text class="edge-label" x="354.5" y="177.2">프로세스 생성</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="185.0,370.0 185.0,418.0 167.5,418.0 167.5,466.0" data-evidence="987-990,994-998" />
<rect class="edge-label-bg" x="90.2" y="376.0" width="172.1" height="22" rx="3" />
<text class="edge-label" x="176.2" y="391.0">virtio 디스크 · virtio-net</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="405.0,361.5 405.0,413.8 422.5,413.8 422.5,466.0" data-evidence="987-990,994-998" />
<rect class="edge-label-bg" x="327.7" y="371.8" width="172.1" height="22" rx="3" />
<text class="edge-label" x="413.8" y="386.8">virtio 디스크 · virtio-net</text>
<g id="node-libvirtd">
<rect class="node-shape kind-service emphasis-normal role-source" data-evidence="946-948,956-956" x="220.0" y="81.0" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="295.0" y="108.0">libvirtd</text>
<line class="node-detail-divider" x1="234.0" y1="129.0" x2="356.0" y2="129.0" />
<text class="node-detail" x="236.0" y="146.0">qemu:///system</text>
</g>
<g id="node-qemu-kc-lab-1">
<rect class="node-shape kind-service emphasis-primary role-service" data-evidence="946-948,987-987,1032-1032" x="105.0" y="248.0" width="160.0" height="122.0" rx="7" />
<text class="node-label" x="185.0" y="275.0">qemu-system-x86_64</text>
<line class="node-detail-divider" x1="119.0" y1="296.0" x2="251.0" y2="296.0" />
<text class="node-detail" x="121.0" y="313.0">kc-lab-1</text>
<text class="node-detail" x="121.0" y="329.0">vCPU 2</text>
<text class="node-detail" x="121.0" y="345.0">할당 3584MB</text>
<text class="node-detail" x="121.0" y="361.0">RSS 3765MB</text>
</g>
<g id="node-qemu-kc-lab-2">
<rect class="node-shape kind-service emphasis-normal role-service" data-evidence="946-948,967-976,1033-1033" x="325.0" y="256.5" width="160.0" height="105.0" rx="7" />
<text class="node-label" x="405.0" y="283.5">qemu-system-x86_64</text>
<line class="node-detail-divider" x1="339.0" y1="304.5" x2="471.0" y2="304.5" />
<text class="node-detail" x="341.0" y="321.5">kc-lab-2</text>
<text class="node-detail" x="341.0" y="337.5">할당 2560MB</text>
<text class="node-detail" x="341.0" y="353.5">RSS 2633MB</text>
</g>
<g id="node-guest-kernel-1">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="994-998,1039-1039" x="70.0" y="466.0" width="195.0" height="105.0" rx="7" />
<text class="node-label" x="167.5" y="493.0">Debian 게스트 커널</text>
<line class="node-detail-divider" x1="84.0" y1="514.0" x2="251.0" y2="514.0" />
<text class="node-detail" x="86.0" y="531.0">enp1s0 · 192.168.122.11</text>
<text class="node-detail" x="86.0" y="547.0">총 3423MB</text>
<text class="node-detail" x="86.0" y="563.0">available 1959MB</text>
</g>
<g id="node-guest-kernel-2">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="994-998,1040-1040" x="325.0" y="466.0" width="195.0" height="105.0" rx="7" />
<text class="node-label" x="422.5" y="493.0">Debian 게스트 커널</text>
<line class="node-detail-divider" x1="339.0" y1="514.0" x2="506.0" y2="514.0" />
<text class="node-detail" x="341.0" y="531.0">enp1s0 · 192.168.122.12</text>
<text class="node-detail" x="341.0" y="547.0">총 2480MB</text>
<text class="node-detail" x="341.0" y="563.0">available 1899MB</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

@@ -13,7 +13,7 @@ Q1 은 저장소를 밖으로 빼면 다중 인스턴스와 재시작은 풀리
- **열린 질문 네 개** (component): 설계로는 답할 수 없었다. Evidence: L682L690.
- **저장소 선택** (process): 다중 인스턴스와 재시작은 여기서 풀린다. Evidence: L682L692.
- **스키마와 조회 키** (process): 덮어쓰기와 정리 누락은 여기가 원인이다. Evidence: L682L694.
- **세션 스냅샷** (process): 클레임 변경이 반영되지 않는 이유. Evidence: L682L696.
- **세션 스냅샷** (process): IdP 에서 클레임을 바꿔도 재인증까지 옛 값이 간다. Evidence: L682L696.
## Relationships
@@ -14,7 +14,7 @@
<mxCell id="n_schema" value="스키마와 조회 키&lt;br/&gt;키에 세션 id 없음" tooltip="덮어쓰기와 정리 누락은 여기가 원인이다. | Evidence: L682-L694" 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="75.0" y="394.0" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_snapshot" value="세션 스냅샷&lt;br/&gt;재인증까지 옛 값" tooltip="클레임 변경이 반영되지 않는 이유. | Evidence: L682-L696" 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">
<mxCell id="n_snapshot" value="세션 스냅샷&lt;br/&gt;옛 클레임 값" tooltip="IdP 에서 클레임을 바꿔도 재인증까지 옛 값이 간다. | Evidence: L682-L696" 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="75.0" y="561.0" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="e_q-s" value="1차 검토" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_questions" target="n_store-choice">
@@ -568,11 +568,11 @@
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "세션 스냅샷\n재인증까지 옛 값",
"text": "세션 스냅샷\n옛 클레임 값",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "세션 스냅샷\n재인증까지 옛 값",
"originalText": "세션 스냅샷\n옛 클레임 값",
"autoResize": true,
"lineHeight": 1.25
}
@@ -2,7 +2,7 @@
"harness_version": "0.2.0",
"spec_id": "open-questions-answered",
"spec_version": "1.1",
"spec_sha256": "176dac77a298b7d32cd7e1958a7468f496a070983e0ca7cbfc1af1a636e7579c",
"spec_sha256": "761fe849313e3653d0f161bb8162d43d64389b0ec6896d249dade786b9c7915d",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "1d44cba1905544d92f1d26ae36a8deb64a3db3914d6b488fd30d6ae7f8cfbabe",
@@ -14,10 +14,10 @@
},
"outputs": [
"open-questions-answered.svg",
"open-questions-answered.drawio",
"open-questions-answered.mmd",
"open-questions-answered.d2",
"open-questions-answered.dot",
"open-questions-answered.drawio",
"open-questions-answered.excalidraw",
"open-questions-answered.alt.md"
],
@@ -81,6 +81,6 @@
<rect class="node-shape kind-process emphasis-warning role-target" data-evidence="682-696" x="75.0" y="561.0" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="150.0" y="588.0">세션 스냅샷</text>
<line class="node-detail-divider" x1="89.0" y1="609.0" x2="211.0" y2="609.0" />
<text class="node-detail" x="91.0" y="626.0">재인증까지 옛 값</text>
<text class="node-detail" x="91.0" y="626.0"> 클레임</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

@@ -1,40 +1,40 @@
<?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="session-sharing-path" name="세션 공유가 실제로 지나는 경로">
<mxGraphModel dx="680" dy="364" 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="422" 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">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_keycloak-0" value="keycloak-0&lt;br/&gt;자기 처리분만 캐시" tooltip="로그인을 처리하고 자기 sessions 캐시에만 엔트리를 남긴다. | Evidence: L49-L51" 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="82.5" width="150.0" height="71.0" as="geometry"/>
<mxGeometry x="70.0" y="111.5" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_keycloak-1" value="keycloak-1&lt;br/&gt;refresh 시 DB 조회" tooltip="다른 노드가 만든 세션을 캐시로 받지 않고 데이터베이스에서 읽는다. | Evidence: L48-L51" 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="225.5" width="150.0" height="71.0" as="geometry"/>
<mxGeometry x="70.0" y="254.5" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_jgroups-ping" value="JGROUPS_PING" tooltip="노드가 서로를 발견하는 자리. 여기 등록되어 있다는 것과 세션이 복제된다는 것은 다른 사건이다. | Evidence: L44-L46" 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=#9ca3af;fillColor=#f9fafb;" vertex="1" parent="1">
<mxGeometry x="380.0" y="60.0" width="150.0" height="90.0" as="geometry"/>
<mxCell id="n_jgroups-ping" value="JGROUPS_PING&lt;br/&gt;노드 둘 등록&lt;br/&gt;세션 복제 아님" tooltip="노드가 서로를 발견하는 자리. 여기 등록되어 있다는 것과 세션이 복제된다는 것은 다른 사건이다. | Evidence: L44-L46" 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=#9ca3af;fillColor=#f9fafb;" vertex="1" parent="1">
<mxGeometry x="392.0" y="60.0" width="150.0" height="114.0" as="geometry"/>
</mxCell>
<mxCell id="n_postgres" value="PostgreSQL&lt;br/&gt;offline_flag=&#x27;0&#x27;" tooltip="OFFLINE_USER_SESSION 에 세션 행을 보관한다. 두 노드가 같은 행을 본다. | Evidence: L48-L52" 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="380.0" y="222.0" width="150.0" height="97.0" as="geometry"/>
<mxCell id="n_postgres" value="PostgreSQL&lt;br/&gt;OFFLINE_USER_SESSION&lt;br/&gt;offline_flag=&#x27;0&#x27;&lt;br/&gt;두 노드 공용" tooltip="OFFLINE_USER_SESSION 에 세션 행을 보관한다. 두 노드가 같은 행을 본다. | Evidence: L48-L52" 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="380.0" y="246.0" width="174.0" height="131.0" as="geometry"/>
</mxCell>
<mxCell id="e_k0-discovery" value="멤버 등록" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_keycloak-0" target="n_jgroups-ping">
<mxGeometry relative="1" as="geometry">
<mxPoint x="324.0" y="102.5" as="offset"/>
<mxPoint x="330.0" y="123.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_k0-writes" value="세션 INSERT" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_keycloak-0" target="n_postgres">
<mxGeometry relative="1" as="geometry">
<mxPoint x="324.0" y="194.2" as="offset"/>
<mxPoint x="324.0" y="229.2" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_k1-discovery" value="멤버 등록" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_keycloak-1" target="n_jgroups-ping">
<mxGeometry relative="1" as="geometry">
<mxPoint x="324.0" y="183.0" as="offset"/>
<mxPoint x="330.0" y="203.5" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_k1-reads" 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_keycloak-1" target="n_postgres">
<mxGeometry relative="1" as="geometry">
<mxPoint x="324.0" y="274.8" as="offset"/>
<mxPoint x="324.0" y="309.8" as="offset"/>
</mxGeometry>
</mxCell>
</root>
@@ -7,9 +7,9 @@
"id": "edge-k0-discovery",
"type": "arrow",
"x": 220.0,
"y": 96.0,
"width": 160.0,
"height": 13.0,
"y": 108.0,
"width": 172.0,
"height": 30.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -33,18 +33,18 @@
"points": [
[
0.0,
13.0
30.0
],
[
80.0,
13.0
86.0,
30.0
],
[
80.0,
86.0,
0.0
],
[
160.0,
172.0,
0.0
]
],
@@ -66,8 +66,8 @@
{
"id": "edge-label-k0-discovery",
"type": "text",
"x": 279.0,
"y": 90.5,
"x": 285.0,
"y": 111.0,
"width": 90,
"height": 24,
"angle": 0,
@@ -106,9 +106,9 @@
"id": "edge-k0-writes",
"type": "arrow",
"x": 220.0,
"y": 127.0,
"y": 156.0,
"width": 160.0,
"height": 134.5,
"height": 146.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -140,11 +140,11 @@
],
[
80.0,
134.5
146.5
],
[
160.0,
134.5
146.5
]
],
"lastCommittedPoint": null,
@@ -166,7 +166,7 @@
"id": "edge-label-k0-writes",
"type": "text",
"x": 279.0,
"y": 182.25,
"y": 217.25,
"width": 90,
"height": 24,
"angle": 0,
@@ -205,9 +205,9 @@
"id": "edge-k1-discovery",
"type": "arrow",
"x": 220.0,
"y": 114.0,
"width": 160.0,
"height": 138.0,
"y": 126.0,
"width": 172.0,
"height": 155.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -231,18 +231,18 @@
"points": [
[
0.0,
138.0
155.0
],
[
80.0,
138.0
86.0,
155.0
],
[
80.0,
86.0,
0.0
],
[
160.0,
172.0,
0.0
]
],
@@ -264,8 +264,8 @@
{
"id": "edge-label-k1-discovery",
"type": "text",
"x": 279.0,
"y": 171.0,
"x": 285.0,
"y": 191.5,
"width": 90,
"height": 24,
"angle": 0,
@@ -304,9 +304,9 @@
"id": "edge-k1-reads",
"type": "arrow",
"x": 220.0,
"y": 270.0,
"y": 299.0,
"width": 160.0,
"height": 9.5,
"height": 21.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -338,11 +338,11 @@
],
[
80.0,
9.5
21.5
],
[
160.0,
9.5
21.5
]
],
"lastCommittedPoint": null,
@@ -364,7 +364,7 @@
"id": "edge-label-k1-reads",
"type": "text",
"x": 279.0,
"y": 262.75,
"y": 297.75,
"width": 90,
"height": 24,
"angle": 0,
@@ -403,7 +403,7 @@
"id": "node-keycloak-0",
"type": "rectangle",
"x": 70.0,
"y": 82.5,
"y": 111.5,
"width": 150.0,
"height": 71.0,
"angle": 0,
@@ -433,7 +433,7 @@
"id": "node-label-keycloak-0",
"type": "text",
"x": 80.0,
"y": 92.5,
"y": 121.5,
"width": 130.0,
"height": 51.0,
"angle": 0,
@@ -472,7 +472,7 @@
"id": "node-keycloak-1",
"type": "rectangle",
"x": 70.0,
"y": 225.5,
"y": 254.5,
"width": 150.0,
"height": 71.0,
"angle": 0,
@@ -502,7 +502,7 @@
"id": "node-label-keycloak-1",
"type": "text",
"x": 80.0,
"y": 235.5,
"y": 264.5,
"width": 130.0,
"height": 51.0,
"angle": 0,
@@ -540,10 +540,10 @@
{
"id": "node-jgroups-ping",
"type": "rectangle",
"x": 380.0,
"x": 392.0,
"y": 60.0,
"width": 150.0,
"height": 90.0,
"height": 114.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#e7f5ff",
@@ -570,10 +570,10 @@
{
"id": "node-label-jgroups-ping",
"type": "text",
"x": 390.0,
"x": 402.0,
"y": 70.0,
"width": 130.0,
"height": 70.0,
"height": 94.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -598,11 +598,11 @@
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "JGROUPS_PING",
"text": "JGROUPS_PING\n노드 둘 등록\n세션 복제 아님",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "JGROUPS_PING",
"originalText": "JGROUPS_PING\n노드 둘 등록\n세션 복제 아님",
"autoResize": true,
"lineHeight": 1.25
},
@@ -610,9 +610,9 @@
"id": "node-postgres",
"type": "rectangle",
"x": 380.0,
"y": 222.0,
"width": 150.0,
"height": 97.0,
"y": 246.0,
"width": 174.0,
"height": 131.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#e7f5ff",
@@ -640,9 +640,9 @@
"id": "node-label-postgres",
"type": "text",
"x": 390.0,
"y": 232.0,
"width": 130.0,
"height": 77.0,
"y": 256.0,
"width": 154.0,
"height": 111.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
@@ -667,11 +667,11 @@
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "PostgreSQL\noffline_flag='0'",
"text": "PostgreSQL\nOFFLINE_USER_SESSION\noffline_flag='0'\n두 노드 공용",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "PostgreSQL\noffline_flag='0'",
"originalText": "PostgreSQL\nOFFLINE_USER_SESSION\noffline_flag='0'\n두 노드 공용",
"autoResize": true,
"lineHeight": 1.25
}
@@ -2,7 +2,7 @@
"harness_version": "0.2.0",
"spec_id": "session-sharing-path",
"spec_version": "1.1",
"spec_sha256": "347c9930c1720050bf968e27eb87993d3eaaa3139ac231d1b6f8f93d770dd148",
"spec_sha256": "e2e4de7934fcac11766554789e18049d2f9bf1dbadc76c946dee5f4e69e628c2",
"source_context": {
"document": "docs/keycloak-session-store/final/document.md",
"document_sha256": "609353e10bfd37a9bbb6a79ecf2a32f3d3c02d5d161879a14ad4713e49e7e5e8",
@@ -14,14 +14,14 @@
},
"outputs": [
"session-sharing-path.svg",
"session-sharing-path.drawio",
"session-sharing-path.mmd",
"session-sharing-path.d2",
"session-sharing-path.dot",
"session-sharing-path.drawio",
"session-sharing-path.excalidraw",
"session-sharing-path.alt.md"
],
"lint_issue_count": 1,
"lint_issue_count": 2,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "component-flow",
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="364" viewBox="0 0 680 364" role="img" aria-labelledby="diagram-title diagram-description">
<svg xmlns="http://www.w3.org/2000/svg" width="680" height="422" viewBox="0 0 680 422" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">세션 공유가 실제로 지나는 경로</title>
<desc id="diagram-description">keycloak-0 과 keycloak-1 은 JGROUPS_PING 테이블을 통해 서로를 발견하고 클러스터 뷰를 형성한다. 그러나 sessions 캐시의 엔트리는 노드 사이로 복제되지 않으며, 각 노드는 자기가 처리한 로그인만 캐시한다. 노드 A 로 로그인한 세션을 노드 B 가 아는 이유는 노드 B 가 PostgreSQL 의 OFFLINE_USER_SESSION 을 직접 읽기 때문이고, 이는 반대편 노드가 날린 SQL 을 문장 로깅으로 잡아 확인했다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;session-sharing-path&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;609353e10bfd37a9bbb6a79ecf2a32f3d3c02d5d161879a14ad4713e49e7e5e8&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;그런데 첫 실험에서 전제가 무너졌다&quot;,&quot;line&quot;:33}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
@@ -49,39 +49,44 @@
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="680" height="364" />
<polyline class="edge kind-write style-solid emphasis-normal" points="220.0,109.0 300.0,109.0 300.0,96.0 380.0,96.0" data-evidence="44-45" />
<rect class="edge-label-bg" x="298.2" y="88.5" width="51.5" height="22" rx="3" />
<text class="edge-label" x="324.0" y="103.5">멤버 등록</text>
<polyline class="edge kind-write style-solid emphasis-normal" points="220.0,127.0 300.0,127.0 300.0,261.5 380.0,261.5" data-evidence="49-52" />
<rect class="edge-label-bg" x="284.9" y="180.2" width="78.3" height="22" rx="3" />
<text class="edge-label" x="324.0" y="195.2">세션 INSERT</text>
<polyline class="edge kind-write style-solid emphasis-normal" points="220.0,252.0 300.0,252.0 300.0,114.0 380.0,114.0" data-evidence="44-45" />
<rect class="edge-label-bg" x="298.2" y="169.0" width="51.5" height="22" rx="3" />
<text class="edge-label" x="324.0" y="184.0">멤버 등록</text>
<polyline class="edge kind-read style-solid emphasis-normal" points="220.0,270.0 300.0,270.0 300.0,279.5 380.0,279.5" data-evidence="48-50" />
<rect class="edge-label-bg" x="284.9" y="260.8" width="78.3" height="22" rx="3" />
<text class="edge-label" x="324.0" y="275.8">세션 SELECT</text>
<rect class="canvas" width="680" height="422" />
<polyline class="edge kind-write style-solid emphasis-normal" points="220.0,138.0 306.0,138.0 306.0,108.0 392.0,108.0" data-evidence="44-45" />
<rect class="edge-label-bg" x="304.2" y="109.0" width="51.5" height="22" rx="3" />
<text class="edge-label" x="330.0" y="124.0">멤버 등록</text>
<polyline class="edge kind-write style-solid emphasis-normal" points="220.0,156.0 300.0,156.0 300.0,302.5 380.0,302.5" data-evidence="49-52" />
<rect class="edge-label-bg" x="284.9" y="215.2" width="78.3" height="22" rx="3" />
<text class="edge-label" x="324.0" y="230.2">세션 INSERT</text>
<polyline class="edge kind-write style-solid emphasis-normal" points="220.0,281.0 306.0,281.0 306.0,126.0 392.0,126.0" data-evidence="44-45" />
<rect class="edge-label-bg" x="304.2" y="189.5" width="51.5" height="22" rx="3" />
<text class="edge-label" x="330.0" y="204.5">멤버 등록</text>
<polyline class="edge kind-read style-solid emphasis-normal" points="220.0,299.0 300.0,299.0 300.0,320.5 380.0,320.5" data-evidence="48-50" />
<rect class="edge-label-bg" x="284.9" y="295.8" width="78.3" height="22" rx="3" />
<text class="edge-label" x="324.0" y="310.8">세션 SELECT</text>
<g id="node-keycloak-0">
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="49-51" x="70.0" y="82.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="145.0" y="109.5">keycloak-0</text>
<line class="node-detail-divider" x1="84.0" y1="130.5" x2="206.0" y2="130.5" />
<text class="node-detail" x="86.0" y="147.5">자기 처리분만 캐시</text>
<rect class="node-shape kind-service emphasis-primary role-source" data-evidence="49-51" x="70.0" y="111.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="145.0" y="138.5">keycloak-0</text>
<line class="node-detail-divider" x1="84.0" y1="159.5" x2="206.0" y2="159.5" />
<text class="node-detail" x="86.0" y="176.5">자기 처리분만 캐시</text>
</g>
<g id="node-keycloak-1">
<rect class="node-shape kind-service emphasis-primary role-target" data-evidence="48-51" x="70.0" y="225.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="145.0" y="252.5">keycloak-1</text>
<line class="node-detail-divider" x1="84.0" y1="273.5" x2="206.0" y2="273.5" />
<text class="node-detail" x="86.0" y="290.5">refresh 시 DB 조회</text>
<rect class="node-shape kind-service emphasis-primary role-target" data-evidence="48-51" x="70.0" y="254.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="145.0" y="281.5">keycloak-1</text>
<line class="node-detail-divider" x1="84.0" y1="302.5" x2="206.0" y2="302.5" />
<text class="node-detail" x="86.0" y="319.5">refresh 시 DB 조회</text>
</g>
<g id="node-jgroups-ping">
<rect class="node-shape kind-datastore emphasis-muted role-support" data-evidence="44-46" x="380.0" y="73.0" width="150.0" height="64.0" /><ellipse class="node-shape kind-datastore emphasis-muted role-support" cx="455.0" cy="73.0" rx="75.0" ry="13.0" /><path class="storage-bottom" d="M 380.0 137.0 A 75.0 13.0 0 0 0 530.0 137.0" />
<text class="node-label" x="455.0" y="103.0">JGROUPS_PING</text>
<rect class="node-shape kind-datastore emphasis-muted role-support" data-evidence="44-46" x="392.0" y="73.0" width="150.0" height="88.0" /><ellipse class="node-shape kind-datastore emphasis-muted role-support" cx="467.0" cy="73.0" rx="75.0" ry="13.0" /><path class="storage-bottom" d="M 392.0 161.0 A 75.0 13.0 0 0 0 542.0 161.0" />
<text class="node-label" x="467.0" y="100.0">JGROUPS_PING</text>
<line class="node-detail-divider" x1="406.0" y1="121.0" x2="528.0" y2="121.0" />
<text class="node-detail" x="408.0" y="138.0">노드 둘 등록</text>
<text class="node-detail" x="408.0" y="154.0">세션 복제 아님</text>
</g>
<g id="node-postgres">
<rect class="node-shape kind-datastore emphasis-primary role-store" data-evidence="48-52" x="380.0" y="235.0" width="150.0" height="71.0" /><ellipse class="node-shape kind-datastore emphasis-primary role-store" cx="455.0" cy="235.0" rx="75.0" ry="13.0" /><path class="storage-bottom" d="M 380.0 306.0 A 75.0 13.0 0 0 0 530.0 306.0" />
<text class="node-label" x="455.0" y="262.0">PostgreSQL</text>
<line class="node-detail-divider" x1="394.0" y1="283.0" x2="516.0" y2="283.0" />
<text class="node-detail" x="396.0" y="300.0">offline_flag=&#x27;0&#x27;</text>
<rect class="node-shape kind-datastore emphasis-primary role-store" data-evidence="48-52" x="380.0" y="259.0" width="174.0" height="105.0" /><ellipse class="node-shape kind-datastore emphasis-primary role-store" cx="467.0" cy="259.0" rx="87.0" ry="13.0" /><path class="storage-bottom" d="M 380.0 364.0 A 87.0 13.0 0 0 0 554.0 364.0" />
<text class="node-label" x="467.0" y="286.0">PostgreSQL</text>
<line class="node-detail-divider" x1="394.0" y1="307.0" x2="540.0" y2="307.0" />
<text class="node-detail" x="396.0" y="324.0">OFFLINE_USER_SESSION</text>
<text class="node-detail" x="396.0" y="340.0">offline_flag=&#x27;0&#x27;</text>
<text class="node-detail" x="396.0" y="356.0">두 노드 공용</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

@@ -930,6 +930,227 @@ lint 가 잡아낸 것 중 사람이 놓치기 쉬운 것 둘을 적어 둔다.
줄였다」)은 **systemd 를 설명하지 않고는 쓸 수 없는데**, 1층이 그 바닥을 채운다.
---
### 0층. 가상화 — 「바닥」 아래에 있는 것
1층을 이 실험대의 바닥이라고 적었는데, 정확히는 **호스트의** 바닥이다. 위
여덟 층 중 2층부터 위는 전부 게스트 두 대 안에서 돌고, 게스트는 호스트에서
프로세스다. 번호를 다시 매기는 대신 아래에 한 층을 더한다.
이 층을 건너뛰면 호스트에서 읽은 숫자를 게스트의 숫자로 읽게 되고, 그
착각은 8층까지 그대로 올라간다.
#### 게스트는 호스트에서 프로세스 하나다
**무엇인가.** libvirtd 가 게스트마다 `qemu-system-x86_64` 를 하나씩 띄운다.
호스트에서 보면 VM 은 특별한 무엇이 아니라 프로세스 두 개이고, VM 이 쓰는
메모리는 그 프로세스의 RSS 다. vCPU 도 마찬가지로 그 프로세스의 스레드라서
`htop` 에서 스레드를 켜 두면 게스트마다 두 줄씩 더 나온다.
```bash
export LIBVIRT_DEFAULT_URI=qemu:///system
ps -eo rss,args --sort=-rss | grep '[q]emu-system' # 호스트에서 본 VM
```
`htop` 에서는 `F5` 트리 뷰가 `libvirtd` 아래 `qemu-system` 이 달린 모양을
보여 주고, `u``libvirt-qemu` 를 고르면 VM 만 남는다.
![호스트에서 본 게스트 두 대](assets/guest-as-host-process/guest-as-host-process.svg)
호스트 경계 안에 있는 것은 libvirtd 와 프로세스 두 개뿐이고, 게스트가 보는
디스크와 인터페이스는 자기를 담은 프로세스가 내준다. 프로세스에 적힌 RSS 와
게스트에 적힌 available 은 같은 메모리를 다른 껍질에서 읽은 값이다. 그림에
`machine.slice``virbr0` 는 넣지 않았다 — 앞의 것은 1층에, 뒤의 것은 다음
항목에 있다.
**왜 여기 나오나.** A-4 의 노드 상실이 이 층에서 일어난다. `virsh destroy`
는 게스트에 ACPI 신호를 보내지 않고 프로세스를 끊으므로, 게스트 입장에서는
예고가 없다.
```
=== 워커 노드(kc-lab-2) 전원 차단 — virsh destroy 는 종료 신호가 없다 ===
차단 시각: 12:07:43
Domain 'kc-lab-2' destroyed
+45초 node=NotReady | keycloak-0=Running | 외부 HTTP 503
```
**없거나 틀리면.** 호스트에서 프로세스가 사라진 것과 게스트 안에서
서비스가 죽은 것을 같은 사건으로 읽게 된다. 4층의 축출 타이머가 45초 뒤에
움직이는 이유는 게스트가 죽었다고 말한 적이 없기 때문이다.
#### 디스크와 네트워크는 virtio 로 붙는다
**무엇인가.** 게스트는 실재하는 하드웨어 대신 반가상화 장치를 본다.
```bash
virt-install --name kc-lab-1 --memory 3584 --vcpus 2 \
--disk size=20,backing_store=/var/lib/libvirt/images/base.qcow2 \
--disk vol=default/seed-kc-lab-1.iso,device=disk,bus=virtio,readonly=on \
--network network=default,mac=52:54:00:aa:bb:11 \
--import --os-variant debian12 --noautoconsole
```
`--network network=default` 는 게스트를 `virbr0` 에 붙인다. libvirt 의 NAT
네트워크이고 대역은 `192.168.122.0/24` 이며, kc-lab-1 이 `.11`, kc-lab-2 가
`.12` 다. 「주입이 먹지 않는다」의 여섯 번째, `tc``eth0` 에 걸었는데
아무 일도 없었던 것도 장치 이름이 이 층에서 정해지기 때문이다 — Debian
게스트의 인터페이스는 `enp1s0` 다.
**없거나 틀리면 — 조용히 실패한다.** 시드 ISO 를 virtio 디스크가 아니라
SATA CD-ROM 으로 붙이면(`virt-install --cloud-init` 의 기본값이다) Debian
`genericcloud` 이미지는 그 장치를 못 본다. 크기를 줄이려고 물리 하드웨어
드라이버를 뺀 이미지라 AHCI 가 없다. cloud-init 은 데이터소스를 찾지 못한
채 오류를 남기지 않고 끝나고, 밖에서 보이는 증상은 hostname 이 `localhost`
로 남고 SSH 가 `Permission denied (publickey)` 로 거부되는 것뿐이다.
**확인.** 게스트에 들어갈 수 없을 때는 화면을 뜬다.
```bash
virsh domblklist kc-lab-1 # 붙은 디스크
virsh net-dhcp-leases default # 게스트 IP
virsh screenshot kc-lab-1 /tmp/kc1.ppm # 확장자와 무관하게 PNG 로 저장된다
```
`localhost login:` 이면 cloud-init 이 안 돌았고 `kc-lab-1 login:` 이면 돌았다.
#### 같은 메모리가 세 곳에서 다르게 보인다
**무엇인가.** 이 실험대에서 메모리를 읽는 곳은 셋이고, 셋이 다른 값을
내는 것이 정상이다.
| 어디서 | 무엇을 보나 |
|---|---|
| 호스트 `htop` | QEMU 프로세스의 RSS = 게스트 전체 |
| 게스트 `free -m` | 게스트 커널이 나눠 쓰는 값 |
| `kubectl top` | 파드·노드 단위 working set |
2026-09-03, Keycloak 을 올리기 전 호스트만 보면 남은 것이 없어 보였다.
```
lab host 총 7628MB · 사용 7189MB · 여유 439MB
├ qemu #1 RSS 3765MB kc-lab-1 (할당 3584MB) → 상한 도달
└ qemu #2 RSS 2633MB kc-lab-2 (할당 2560MB) → 상한 도달
```
같은 시각 게스트 안에는 여유가 있었다.
```
kc-lab-1 총 3423MB · used 1464 · buff/cache 2020 · available 1959MB
kc-lab-2 총 2480MB · used 580 · buff/cache 1714 · available 1899MB
```
**왜 이런가.** QEMU 의 RSS 는 게스트가 **터치한** 페이지만큼이다. 게스트가
페이지 캐시로 메모리를 채우면 RSS 도 할당 상한까지 올라가고, 상한에 닿으면
거기서 멈춘다. 위 두 프로세스가 그 상태였다. 그래서 게스트 안에 워크로드를
더 올려도 호스트 압박은 늘지 않는다 — 게스트의 페이지 캐시가 밀려날 뿐이다.
**없거나 틀리면.** 「호스트 여유 439MB」를 자원이 없다는 뜻으로 읽는다.
게스트 여유를 합치면 약 3.8GB 였고, 배포 예산은 약 2600Mi 였다.
**확인.**
```bash
ps -eo rss,args --sort=-rss | grep '[q]emu-system' # 호스트에서 본 VM
ssh kc-lab-1 free -m # 게스트 안 실제
kubectl top nodes # working set
```
#### 상한을 바꾸려면 껐다 켜야 한다
**무엇인가.** 호스트 메모리를 8GB 에서 12GB 로 물리 증설한 뒤, 게스트를 다시
만들지 않고 할당만 옮겼다.
```bash
virsh setmaxmem kc-lab-1 5120M --config
virsh setmem kc-lab-1 5120M --config
```
`setmaxmem` 이 상한이고 `setmem` 이 현재 할당이다. 현재값을 상한보다 크게
줄 수 없으므로 `setmaxmem` 이 먼저다. `--config` 는 다음 부팅부터,
`--live` 는 실행 중인 도메인에 즉시 적용된다. 다만 `setmaxmem --live`
대개 거부된다 — 게스트가 부팅할 때 메모리 맵을 정하기 때문이다.
**왜 여기 나오나.** 이 재배분이 1층의 `machine.slice` 아래에서 일어난다.
그리고 증설 전에는 관측 스택을 올릴 만큼 남지 않았다. 증설 뒤 kc-lab-1 이
2045Mi(41%), kc-lab-2 가 1131Mi(28%), 호스트 여유가 3957MB 였다.
**확인.**
```bash
virsh dominfo kc-lab-1 | grep -i memory
ssh kc-lab-1 free -m # 게스트가 실제로 인식한 값
```
#### swap 은 게스트에 두지 않는다
호스트에는 8GB 의 swap 이 있고 게스트에는 0MB 다. 이유는 셋이다.
k3s 와 kubelet 은 기본적으로 swap 을 거부하고, 호스트 swap 으로 QEMU 의
페이지가 밀리면 게스트 성능이 급락하며, 무엇보다 이 실험대가 재는 것이
**타이밍**이다. refresh 경쟁과 복제 지연을 재는 동안 swap 이 끼면 8층의
측정이 통째로 뜻을 잃는다.
#### 이 층 아래의 구조 — 조사한 것
여기까지는 이 실험대에서 읽은 값이다. 아래는 그 아래에 무엇이 있는지를
공식 문서에서 확인한 것이고 **이 실험대에서 잰 것이 아니다.** 세 갈래 중
앞의 둘은 이 실험대가 쓰고 셋째는 쓰지 않는다.
![게스트가 하드웨어에 닿는 세 갈래](assets/cpu-io-passthrough-paths/cpu-io-passthrough-paths.svg)
왼쪽부터 CPU · virtio I/O · 패스스루다. 셋의 차이는 호스트 유저공간을
지나는가와 몇 번 지나는가에 있다.
**CPU — 유저공간이 커널에 들어갔다 나온다.** `open("/dev/kvm")` 으로 KVM
핸들을 얻고, 시스템 ioctl 로 VM 을, VM ioctl 로 vCPU 를 만든다
(`KVM_CREATE_VM` · `KVM_CREATE_VCPU`). 게스트를 돌리는 것은 vCPU ioctl
`KVM_RUN` 이고, 커널은 vcpu fd 를 offset 0 으로 mmap 한 공유 메모리
(`struct kvm_run`)로 왜 나왔는지를 알린다. 크기는 `KVM_GET_VCPU_MMAP_SIZE`
로 묻는다. 문서에 이런 문장이 있다 — 「vcpu ioctl 은 그 vcpu 를 만든
스레드에서 내야 한다」. **앞에서 본 「vCPU 는 QEMU 프로세스의 스레드」가
여기서 나온다.** ([커널 KVM API 문서](https://docs.kernel.org/virt/kvm/api.html))
하드웨어 쪽 이름은 VMX 다. 프로세서는 VMX root 와 VMX non-root 로 나뉘어
돌고, VM entry 때 guest-state 영역에서 상태를 싣고 VM exit 때 그리로
저장한다. ([Intel SDM Vol. 3C](https://cdrdv2-public.intel.com/789585/326019-sdm-vol-3c.pdf))
**I/O — 게스트가 보는 장치는 규격이다.** virtio 는 「서로 다른 종류의
드라이버와 장치가 통신하는 규약을 정한 공개 표준」이고, 주고받는 통로는
virtqueue 라는 링 버퍼다. 게스트에 장치를 내보이는 전송 계층은 PCI · MMIO ·
CCW 이고 리눅스에서는 virtio-pci 와 virtio-mmio 가 그 드라이버다.
([커널 virtio 문서](https://docs.kernel.org/driver-api/virtio/virtio.html))
**앞의 「시드를 virtio 디스크로 붙인다」가 이 규격이다.** Debian
`genericcloud` 이미지가 AHCI 를 못 보는 것은 그 이미지에 물리 하드웨어
드라이버가 없기 때문이지 virtio 가 특별해서가 아니다.
virtqueue 를 QEMU 밖과 나누는 길이 따로 있다. vhost-user 문서는 그 규약이
「리눅스 커널의 vhost 구현을 제어하는 ioctl 인터페이스를 보완」하며 「같은
호스트의 유저공간 프로세스와 virtqueue 를 공유하는 제어 평면」이라고 적는다.
앞쪽이 QEMU 이고 뒤쪽이 virtqueue 를 소비하는 쪽이다.
([QEMU vhost-user 규약](https://www.qemu.org/docs/master/interop/vhost-user.html))
**패스스루 — 이 실험대는 쓰지 않는다.** VFIO 는 「IOMMU 로 보호되는
환경에서 장치 접근을 유저공간에 안전하게 여는, IOMMU 와 장치에 중립인
프레임워크」다. 소유의 단위는 장치가 아니라 IOMMU 그룹인데, 「시스템의 다른
모든 장치로부터 격리할 수 있는 장치 묶음」이 그룹이고 격리가 늘 장치 하나
단위로 되지는 않기 때문이다.
```
/dev/vfio/vfio 컨테이너를 연다
/dev/vfio/$GROUP 그룹을 열어 VFIO_GROUP_SET_CONTAINER 로 붙인다
VFIO_GROUP_GET_DEVICE_FD 장치 fd 를 받는다
VFIO_IOMMU_MAP_DMA 장치가 닿을 주소 범위를 매핑한다
```
호스트 드라이버에서 떼어 `vfio-pci` 에 묶는 것이 장치를 넘기는 방법이고,
IOMMU 가 DMA 와 인터럽트 리매핑으로 장치가 아무 메모리나 건드리지 못하게
막는다. ([커널 VFIO 문서](https://docs.kernel.org/driver-api/vfio.html))
**세 갈래의 차이는 깊이다.** CPU 는 `KVM_RUN` 으로 들어갔다 `struct kvm_run`
으로 나오는 왕복이 있고, virtio 는 virtqueue 를 누가 소비하느냐에 따라
왕복하는 곳이 달라지며, 패스스루는 유저공간 드라이버가 장치에 직접 닿는다.
**이 실험대가 잰 값은 앞의 두 갈래에서만 나온 것이다.** 패스스루는 이
실험대에 없으므로 여기 적은 것은 문서를 읽은 결과이고 측정이 아니다.
---
### 1층. 리눅스와 systemd — 이 실험대의 바닥
@@ -2131,6 +2352,15 @@ curl -s localhost:19090/api/v1/targets | jq -r '.data.activeTargets[].labels.job
전체를 무효화한다는 규격이다. 뒤엣것이 B-3 의 「이긴 요청의 토큰도 못 쓴다」를
설명한다.
**0층은 성격이 다르다.** 「이 층 아래의 구조」에 적은 것은 이 실험대에서 잰
것이 아니라 커널·Intel·QEMU 문서를 읽은 결과다. KVM 의 ioctl 층과
`struct kvm_run`, VMX root 와 non-root, virtio 의 virtqueue 와 전송 계층,
vhost-user 가 무엇을 보완하는지, VFIO 의 IOMMU 그룹과 DMA 리매핑이 그렇다.
**그중 패스스루는 이 실험대에 아예 없다** — 쓴 적이 없으므로 잰 값도 없고,
거기 적은 것은 전부 문서에서 옮긴 것이다. 반면 같은 0층의 앞쪽,
`qemu-system-x86_64` 의 RSS 와 게스트의 `available``virsh` 로 옮긴 메모리
할당은 이 실험대에서 읽은 값이다.
**한 항목만은 재지 않았다.** 1층의 `Restart=on-failure` 는 유닛 파일을 읽어
적었지 nginx 를 죽여 확인하지 않았다. 이 기록이 스물여섯 번 배운 것이
「설정이 그렇다고 그렇게 동작하지는 않는다」이므로 그대로 적어 둔다. 재려면
@@ -7,9 +7,9 @@
"revision": "cdac9b8178391311d8eca1ebc6cac15bb62d79af",
"verified": "이 커밋이 A-1 부터 D-4a 까지 실험 전량을 병합한 tip 이고 저장소 HEAD 다. git branch --contains 가 experiment 브랜치 26 개를 모두 낸다 (2026-09-07 확인)"
},
"ssotSha256": "ae0aee375daebc00d72590db8745ff9ef1018911785a80e9fd7b62f893dea19d",
"ssotSha256": "0ae5674723f25dc85d5069529890f07be1a11b768b56901103fa3ea6ac3dcf55",
"sourceRevision": "keycloak-session-lab@2026-09",
"generatedAt": "2026-09-07",
"generatedAt": "2026-09-08",
"candidateScope": {
"document": "final/document.md",
"sections": [
@@ -72,10 +72,6 @@
"OFFLINE_USER_SESSION",
"sessions 캐시"
],
"evidence": [
"evidence/raw/a1-jgroups-transport-block__09-cross-node-under-partition.txt",
"evidence/raw/a1-jgroups-transport-block__10-logout-not-propagated.txt"
],
"classification": "로그가 가리키는 원인과 실제 경로가 다르다는 것을 노드 B 의 SQL 문장 로깅으로 확정했고, 그 확정이 A-1 의 빗나간 예측까지 설명하며 닫힌다",
"missing-verification": "Infinispan 복제를 명시적으로 켠 구성에서는 재지 않았다. 이 결론은 persistent-user-sessions 가 켜진 26.7.0 기본값에 한정된다",
"relations": [
@@ -84,6 +80,14 @@
"reference:state-the-version-and-the-setting-with-the-result"
],
"kind": "case",
"ssot-assets": [
"session-sharing-path",
"a1-transport-vs-discovery"
],
"ssot-evidence": [
"raw/a1-jgroups-transport-block__09-cross-node-under-partition.txt",
"raw/a1-jgroups-transport-block__10-logout-not-propagated.txt"
],
"publication": "미작성"
},
{
@@ -98,10 +102,6 @@
"REVOKED_TOKEN",
"DEFAULT_SCOPE"
],
"evidence": [
"evidence/raw/a7a-volatile-cause__01-cause-determined.txt",
"evidence/raw/a7-volatile-comparison__05-a2-rerun-db-loss.txt"
],
"classification": "가설(REVOKED_TOKEN)을 문장 로깅으로 반증하고 실제 조회를 확정한 뒤, 캐시 온도 셋을 모두 재현해 표 하나로 닫히지 않는다는 결론까지 갔다",
"missing-verification": "캐시가 식는 시간을 재지 않았다. 냉·중간·온 세 상태를 만들어 확인했을 뿐 그 사이의 전이는 관측하지 않았다",
"relations": [
@@ -109,6 +109,13 @@
"reference:state-the-version-and-the-setting-with-the-result"
],
"kind": "case",
"ssot-assets": [
"cache-temperature-outcomes"
],
"ssot-evidence": [
"raw/a7a-volatile-cause__01-cause-determined.txt",
"raw/a7-volatile-comparison__05-a2-rerun-db-loss.txt"
],
"publication": "미작성"
},
{
@@ -121,10 +128,6 @@
"code": [
"persistent-user-sessions"
],
"evidence": [
"evidence/raw/a8-rolling-restart__01-restart-availability.txt",
"evidence/raw/a8-rolling-restart__02-session-survival.txt"
],
"classification": "재시작 전후의 DB 세션 수와 캐시 수를 따로 세어 무엇이 남고 무엇이 사라지는지 갈랐고, 그 설정을 켜는 이유로 닫힌다",
"missing-verification": "표본이 적어 무중단 주장을 처음에 과장했다가 고쳤다. 재시작 중 진행 중이던 요청은 재지 않았다",
"relations": [
@@ -132,6 +135,13 @@
"case:session-sharing-is-the-database-not-replication"
],
"kind": "case",
"ssot-assets": [
"a8-cache-vs-session"
],
"ssot-evidence": [
"raw/a8-rolling-restart__01-restart-availability.txt",
"raw/a8-rolling-restart__02-session-survival.txt"
],
"publication": "미작성"
}
],
@@ -151,6 +161,9 @@
"reference:state-the-version-and-the-setting-with-the-result"
],
"kind": "concept",
"ssot-assets": [
"version-conditional-results"
],
"publication": "미작성"
}
],
@@ -195,11 +208,6 @@
"SET LOCAL synchronous_commit TO OFF",
"wal_writer_delay"
],
"evidence": [
"evidence/raw/a3-database-crash__03-loss-measurement.txt",
"evidence/raw/a3-database-crash__07-loss-result.txt",
"evidence/raw/a3-database-crash__08-wal-settings.txt"
],
"classification": "153 건 중 149 건만 남은 것을 세어 확인하고 원인을 커밋 설정에서 확정했으며, 의도된 설계의 대가를 숫자로 닫았다",
"missing-verification": "synchronous_commit 을 켠 대조군을 돌리지 않았다. 유실이 0 이 되는지와 그때의 지연 비용은 재지 않았다",
"relations": [
@@ -208,6 +216,14 @@
"reference:most-of-an-outage-is-noticing"
],
"kind": "case",
"ssot-assets": [
"a3-commit-to-disk-gap"
],
"ssot-evidence": [
"raw/a3-database-crash__03-loss-measurement.txt",
"raw/a3-database-crash__07-loss-result.txt",
"raw/a3-database-crash__08-wal-settings.txt"
],
"publication": "미작성"
},
{
@@ -222,11 +238,6 @@
"tolerationSeconds",
"StatefulSet"
],
"evidence": [
"evidence/raw/a4-node-loss__02-worker-node-killed.txt",
"evidence/raw/a4-node-loss__03-state-during-loss.txt",
"evidence/raw/a4-node-loss__04-eviction-timing.txt"
],
"classification": "같은 복구 시간을 내는 두 장애의 원인이 다르다는 것을 갈랐고, 축출까지 5분 40초라는 내역으로 닫힌다",
"missing-verification": "저장소를 두 노드에 나눠 배치한 구성에서는 재지 않았다. 4a 의 결과는 이 실험대의 배치에 걸려 있다",
"relations": [
@@ -235,6 +246,15 @@
"reference:most-of-an-outage-is-noticing"
],
"kind": "case",
"ssot-assets": [
"a4-two-node-losses",
"lab-topology"
],
"ssot-evidence": [
"raw/a4-node-loss__02-worker-node-killed.txt",
"raw/a4-node-loss__03-state-during-loss.txt",
"raw/a4-node-loss__04-eviction-timing.txt"
],
"publication": "미작성"
},
{
@@ -248,10 +268,6 @@
"agroal 커넥션 풀",
"readiness probe"
],
"evidence": [
"evidence/raw/a6-latency-injection__02-delay-injected.txt",
"evidence/raw/a6-latency-injection__04-pool-under-load.txt"
],
"classification": "28 배가 어디서 왔는지를 왕복 누적과 풀 경합 두 단계로 분해했고, 느림이 장애로 승격되는 지점까지 관측해 닫았다",
"missing-verification": "지연 값을 200ms 한 점에서만 쟀다. 어느 지연부터 readiness 가 실패하는지는 재지 않았다",
"relations": [
@@ -259,6 +275,13 @@
"case:two-ways-to-lose-a-node"
],
"kind": "case",
"ssot-assets": [
"a6-latency-multiplication"
],
"ssot-evidence": [
"raw/a6-latency-injection__02-delay-injected.txt",
"raw/a6-latency-injection__04-pool-under-load.txt"
],
"publication": "미작성"
}
],
@@ -278,6 +301,9 @@
"concept:the-up-metric-cannot-see-alive-but-useless"
],
"kind": "concept",
"ssot-assets": [
"observation-points"
],
"publication": "미작성"
}
],
@@ -323,10 +349,6 @@
"AuthenticatedPrincipalOAuth2AuthorizedClientRepository",
"SPRING_SESSION_STORE_TYPE"
],
"evidence": [
"evidence/raw/b0-bff-redis-deploy__03-beans-analysis.txt",
"evidence/raw/b1-redis-session-store__03-redis-contents.txt"
],
"classification": "저장소를 붙이기 전에 자동구성이 무엇을 골랐는지 먼저 읽고, 그 조회 키 차이가 다음 실험의 결과를 만든다는 것까지 이어 닫았다",
"missing-verification": "Redis 를 붙인 상태에서 토큰 저장소만 따로 두는 조합은 B-2 에서 다시 쟀다. 여기서는 세션 쪽만 확인했다",
"relations": [
@@ -335,6 +357,10 @@
"decision:split-the-two-stores-and-design-each"
],
"kind": "case",
"ssot-evidence": [
"raw/b0-bff-redis-deploy__03-beans-analysis.txt",
"raw/b1-redis-session-store__03-redis-contents.txt"
],
"publication": "미작성"
},
{
@@ -348,9 +374,6 @@
"JdbcOAuth2AuthorizedClientService",
"PRIMARY KEY (client_registration_id, principal_name)"
],
"evidence": [
"evidence/raw/b0-bff-redis-deploy__02-autoconfiguration.txt"
],
"classification": "저장소를 나눠 옮긴 뒤 네 가지를 확인해 둘은 풀리고 둘은 남는 것을 갈랐고, 남은 둘의 원인이 저장소가 아니라 DDL 한 줄임을 확정했다",
"missing-verification": "세션 id 를 키에 넣은 스키마로 고쳐서 다시 재지 않았다. 덮어쓰기가 사라지는지는 확인하지 않았다",
"relations": [
@@ -360,6 +383,12 @@
"decision:split-the-two-stores-and-design-each"
],
"kind": "case",
"ssot-assets": [
"b2-primary-key-overwrite"
],
"ssot-evidence": [
"raw/b0-bff-redis-deploy__02-autoconfiguration.txt"
],
"publication": "미작성"
},
{
@@ -373,9 +402,6 @@
"revokeRefreshToken=true",
"refreshTokenMaxReuse=0"
],
"evidence": [
"evidence/raw/b0-bff-redis-deploy__01-deploy.txt"
],
"classification": "동시 5 건을 실제로 경합시켜 「하나는 성공한다」가 아니라 전부 못 쓰게 되는 것을 관측했고, 원인을 client session 삭제로 확정했다",
"missing-verification": "재시도 설계를 붙여서 다시 재지 않았다. 어떤 재시도가 이 상황을 복구하는지는 확인하지 않았다",
"relations": [
@@ -383,6 +409,12 @@
"decision:split-the-two-stores-and-design-each"
],
"kind": "case",
"ssot-assets": [
"b3-rotation-contention"
],
"ssot-evidence": [
"raw/b0-bff-redis-deploy__01-deploy.txt"
],
"publication": "미작성"
},
{
@@ -397,10 +429,6 @@
"NimbusJwtDecoder",
"JWKS kid"
],
"evidence": [
"evidence/raw/b5-redis-loss__04-persistence.txt",
"evidence/raw/b6-key-rotation__03-old-key-removed.txt"
],
"classification": "설정만 보면 되어 있는 것으로 읽히는 두 자리를 각각 실제로 죽여 보고 회전시켜 확인했으며, 설정과 매체를 나눠 봐야 한다는 결론으로 닫힌다",
"missing-verification": "볼륨을 붙인 Redis 로 다시 재지 않았다. 영속화가 실제로 동작하는지는 확인하지 않았다",
"relations": [
@@ -408,6 +436,13 @@
"reference:look-at-the-lookup-key-before-moving-the-store"
],
"kind": "case",
"ssot-assets": [
"b5-b6-storage-and-keys"
],
"ssot-evidence": [
"raw/b5-redis-loss__04-persistence.txt",
"raw/b6-key-rotation__03-old-key-removed.txt"
],
"publication": "미작성"
}
],
@@ -427,6 +462,9 @@
"decision:split-the-two-stores-and-design-each"
],
"kind": "concept",
"ssot-assets": [
"bff-store-lookup-keys"
],
"publication": "미작성"
}
],
@@ -494,9 +532,6 @@
"proxy_set_header X-Auth-Request-Roles",
"--cookie-refresh"
],
"evidence": [
"evidence/raw/b4-edge-authorization__01-header-handling.txt"
],
"classification": "위조 헤더를 실제로 보내 통과하는 것을 관측하고 원인을 지우지 않은 설정에서 확정했으며, 클레임 변경이 재인증까지 반영되지 않는 것도 같은 실험에서 닫았다",
"missing-verification": "--cookie-refresh 를 켠 구성에서는 재지 않았다. 클레임 반영 지연이 얼마나 줄어드는지는 확인하지 않았다",
"relations": [
@@ -504,6 +539,12 @@
"case:nobody-implemented-backchannel-logout"
],
"kind": "case",
"ssot-assets": [
"b4-header-trust-boundary"
],
"ssot-evidence": [
"raw/b4-edge-authorization__01-header-handling.txt"
],
"publication": "미작성"
},
{
@@ -517,9 +558,6 @@
"backchannelLogoutUrl",
"oidcLogout"
],
"evidence": [
"evidence/raw/b4-edge-authorization__01-header-handling.txt"
],
"classification": "네 가지를 순서대로 확인해 설정 결손과 기능 부재가 한 경로에 겹쳐 있는 것을 갈랐고, 한쪽만 고쳐서는 되지 않는다는 결론으로 닫힌다",
"missing-verification": "양쪽을 다 구현해서 전파가 되는지 확인하지 않았다. 원인 확정까지가 이 실험의 범위다",
"relations": [
@@ -527,6 +565,12 @@
"reference:clear-the-header-before-you-trust-it"
],
"kind": "case",
"ssot-assets": [
"c2-backchannel-both-sides"
],
"ssot-evidence": [
"raw/b4-edge-authorization__01-header-handling.txt"
],
"publication": "미작성"
},
{
@@ -541,10 +585,6 @@
"--cookie-refresh",
"refresh:disabled"
],
"evidence": [
"evidence/raw/b7a-orphan-session__01-orphan-lifecycle.txt",
"evidence/raw/b7-cookie-secret__01-deploy.txt"
],
"classification": "secret 교체가 티켓을 못 풀게 만든 상태에서 무엇을 지울지 고르는 방법을 TTL 역산으로 세우고 로그와 1 초 차로 교차검증해 닫았다",
"missing-verification": "--cookie-refresh 를 켠 구성에서는 이 역산이 성립하지 않는다. 그 경우의 정리 방법은 재지 않았다",
"relations": [
@@ -552,6 +592,13 @@
"reference:clear-the-header-before-you-trust-it"
],
"kind": "case",
"ssot-assets": [
"b7-cookie-session-tradeoff"
],
"ssot-evidence": [
"raw/b7a-orphan-session__01-orphan-lifecycle.txt",
"raw/b7-cookie-secret__01-deploy.txt"
],
"publication": "미작성"
}
],
@@ -598,12 +645,6 @@
"/etc/letsencrypt/renewal-hooks/",
"nginx 마스터·워커 PID"
],
"evidence": [
"evidence/raw/d4-certificate-renewal__07-renewal-hook-missing.txt",
"evidence/raw/d4-certificate-renewal__09-serial-timeline.txt",
"evidence/raw/d4-certificate-renewal__12-certbot-state.txt",
"evidence/raw/d4-certificate-renewal__13-verdict.txt"
],
"classification": "디스크 기록 시각과 실제 서빙 시각을 따로 재어 공백을 확정하고 원인 셋이 모두 비어 있는 것을 확인했으며, 88 일 잠복한다는 성질까지 닫았다",
"missing-verification": "실제 만료가 임박한 상태를 만들지 않았다. 이 결함이 만료로 드러나는 경로는 재지 않았다",
"relations": [
@@ -612,6 +653,15 @@
"question:does-the-renewal-timer-actually-renew"
],
"kind": "case",
"ssot-assets": [
"renewal-to-serving-gap"
],
"ssot-evidence": [
"raw/d4-certificate-renewal__07-renewal-hook-missing.txt",
"raw/d4-certificate-renewal__09-serial-timeline.txt",
"raw/d4-certificate-renewal__12-certbot-state.txt",
"raw/d4-certificate-renewal__13-verdict.txt"
],
"publication": "미작성"
},
{
@@ -625,11 +675,6 @@
"renewal-hooks/deploy/",
"types_hash 경고"
],
"evidence": [
"evidence/raw/d4a-deploy-hook__01-hook-verified.txt",
"evidence/raw/d4a-deploy-hook__02-certbot-with-hook.txt",
"evidence/raw/d4a-deploy-hook__03-after-state.txt"
],
"classification": "처방을 실제로 넣고 전후를 같은 방법으로 재어 확인했고, 훅 로그의 error 문구가 실패가 아니라는 것까지 갈라 닫았다",
"missing-verification": "실제 갱신 주기에서 훅이 도는 것은 확인하지 않았다. 강제 갱신으로만 검증했다",
"relations": [
@@ -638,6 +683,14 @@
"reference:judge-a-reload-by-the-worker-pid-not-the-log"
],
"kind": "case",
"ssot-assets": [
"d4a-hook-effect"
],
"ssot-evidence": [
"raw/d4a-deploy-hook__01-hook-verified.txt",
"raw/d4a-deploy-hook__02-certbot-with-hook.txt",
"raw/d4a-deploy-hook__03-after-state.txt"
],
"publication": "미작성"
},
{
@@ -652,9 +705,6 @@
"databasechangelog",
"StatefulSet 롤링 업데이트"
],
"evidence": [
"evidence/raw/d4-certificate-renewal__01-certificate-state.txt"
],
"classification": "롤백 실패를 관측하고 원인을 체크섬으로 확정한 뒤, 스키마가 움직이지 않은 방향은 실제로 성공하는 것까지 확인해 조건을 붙여 닫았다",
"missing-verification": "스키마가 크게 바뀌는 메이저 업그레이드에서는 재지 않았다. 두 패치 버전 사이만 확인했다",
"relations": [
@@ -662,6 +712,12 @@
"decision:put-the-reload-in-a-deploy-hook"
],
"kind": "case",
"ssot-assets": [
"d2-upgrade-direction"
],
"ssot-evidence": [
"raw/d4-certificate-renewal__01-certificate-state.txt"
],
"publication": "미작성"
}
],
@@ -752,10 +808,6 @@
"PID 1 SIGKILL",
"flannel VXLAN"
],
"evidence": [
"evidence/raw/a1-jgroups-transport-block__05-conntrack-problem.txt",
"evidence/raw/a6-latency-injection__03-flannel-injection.txt"
],
"classification": "아홉 건이 각각 다른 이유로 실패한 것을 하나씩 확정하고, 실패한 주입과 영향 없는 주입이 구별되지 않는다는 공통 원인으로 닫았다",
"missing-verification": "아홉 건 모두를 고친 뒤 다시 돌린 것은 아니다. 일부는 다른 주입 방법으로 우회했다",
"relations": [
@@ -764,6 +816,14 @@
"concept:the-up-metric-cannot-see-alive-but-useless"
],
"kind": "case",
"ssot-assets": [
"injection-verification",
"a5-partition-asymmetry"
],
"ssot-evidence": [
"raw/a1-jgroups-transport-block__05-conntrack-problem.txt",
"raw/a6-latency-injection__03-flannel-injection.txt"
],
"publication": "미작성"
},
{
@@ -777,10 +837,6 @@
"TCP 연결수 0",
"50µs"
],
"evidence": [
"evidence/raw/d4-certificate-renewal__05-control-no-injection.txt",
"evidence/raw/d4-certificate-renewal__11-inflight-full.txt"
],
"classification": "같은 순간의 대조 폴링과 연결수·소요 시간·재현율 넷으로 귀속을 뒤집었고, 대조군이 없었으면 무엇이 됐을지까지 닫았다",
"missing-verification": "클라이언트 쪽 원인을 특정하지 않았다. 서버 탓이 아니라는 것까지가 이 확인의 범위다",
"relations": [
@@ -789,6 +845,13 @@
"reference:never-subtract-values-from-two-clocks"
],
"kind": "case",
"ssot-assets": [
"measurement-control"
],
"ssot-evidence": [
"raw/d4-certificate-renewal__05-control-no-injection.txt",
"raw/d4-certificate-renewal__11-inflight-full.txt"
],
"publication": "미작성"
},
{
@@ -803,9 +866,6 @@
"kubectl run --rm -i",
"상주 탐침"
],
"evidence": [
"evidence/raw/a6-latency-injection__04-pool-under-load.txt"
],
"classification": "헤드라인 수치를 만든 명령 넷이 산문이던 것을 셸 표현식으로 바꾸고 실제로 돌려 확인했으며, 그 확인에서 한 건이 깨진 것까지 닫았다",
"missing-verification": "고친 명령을 다른 환경에서 돌려 보지 않았다. 이 실험대에서만 확인했다",
"relations": [
@@ -813,6 +873,12 @@
"reference:verify-the-injection-landed-separately-from-the-result"
],
"kind": "case",
"ssot-assets": [
"reproducibility-gap"
],
"ssot-evidence": [
"raw/a6-latency-injection__04-pool-under-load.txt"
],
"publication": "미작성"
}
],
@@ -1408,5 +1474,11 @@
"unwritten": 33,
"unlisted": 0,
"candidates": 42
},
"unassignedSsotAssets": {
"d3-secret-exposure": "D-3 의 base64 관측은 KEEP_IN_SSOT 로 두어 글감이 없다. 그림도 함께 남긴다",
"wrong-predictions": "틀린 예측 다섯을 한 자리에 모은 그림이라 특정 Case 에 붙지 않는다. 주제 전체의 그림이다",
"open-questions-answered": "앞 프로젝트가 남긴 열린 질문 넷에 답한 것을 모은 그림이라 이 프로젝트의 어느 한 글감에 붙지 않는다",
"not-applicable-conditions": "적용되지 않는 조건을 모은 그림이고 그것을 담는 것은 Reference 다. Reference 에는 본문이 없어 그림을 렌더링할 자리가 없다"
}
}