virtualization 의 setup 9편은 같은 커밋(2109f72)에서 한꺼번에 생겼는데 그때
Studio 반입이 7편만 덮고 둘을 빠뜨렸다. 둘 다 readiness 가 READY 이고 관문도
통과하는데 frontmatter 에 id 도 studio 도 없었다 — Studio 문서가 아예 없었다는
뜻이다. 빼놓을 이유가 없어서 만들었다.
- setup-tear-down-the-lab-and-know-what-survives -> 87e0138d-…
- setup-power-cycle-the-lab-and-reallocate-guest-memory -> 8a9ca3d4-…
주제는 새로 만들지 않고 같은 폴더의 기존 편에서 topicId·projectId 를 읽어
같은 값을 썼다. 둘 다 저장까지만 하고 게시하지 않았다(currentPublication: null).
pinnedVersions 의 40자 상한
- power-cycle 의 「기준 배치」 값이 51자여서 서버가 422 로 거절했다
(size must be between 1 and 40)
- 그 칸에 문장이 들어가 있었다. CLAUDE.md 는 Setup 에 대해 「명령이 본문에
들어가고 버전만 pinnedVersions 에 남는다」고 적는다
- 잘라내기 전에 그 설명이 본문에 있는지 먼저 봤다. 셋 다 있었다 — 63행이
호스트 RAM 증설, 176행이 2026-09-03, 215행이 2026-09-10 과의 날짜 엇갈림.
그래서 날짜만 남겼고 잃은 내용이 없다
- 저장소 전체를 훑어 40자를 넘는 값은 이것 하나뿐이었다
기록 두 편의 diff 는 frontmatter 뿐이고, tech-log-tree.json 은 파생 칸
(publication · studioId) 넷만 바뀌었다. 「게시됨」은 Studio 에 있다는 뜻이지
공개됐다는 뜻이 아니다.
관문: check_body PASS · check_prose error 0 · check_evidence 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 ·
check-studio-whitespace PASS
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8222 lines
790 KiB
JSON
8222 lines
790 KiB
JSON
{
|
|
"schemaVersion": 4,
|
|
"project": "virtualization",
|
|
"ssot": "final/document.md",
|
|
"ssotSha256": "9146d6428ebdf2ab49f0fbd160e9526db93f48ca357bc8d281e753a999771f50",
|
|
"sourceRevision": "no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다",
|
|
"generatedAt": "2026-09-17",
|
|
"sourceRepository": [
|
|
{
|
|
"name": "반입한 개념 문서 (제1~4부)",
|
|
"path": "/home/donghyeon/workspace/chat-gpt-container/document-haness/docs/virtualization/final/document.md",
|
|
"revision": null,
|
|
"verified": "이 프로젝트에는 분석한 저장소가 없다. 출발점은 사용자가 밖에서 쓴 문서 한 편이고, 체크아웃이 아니라 반입한 파일이 근거를 고정한다. 그래서 path 는 그 문서의 현재 자리인 final/document.md 다. 반입할 때의 원본(절 23개 · sha256 2b7b386de841f3e7b408066b39efaf183804b3d158d78d88e7fa0b7e00e08d69)은 source/kvm-cpu-virtualization-ssot.md 에 두었다가, 사용자가 §24~§28 을 더해 final/document.md 가 그 상위 집합이 된 뒤 지웠다 — heading marker 를 뺀 본문을 대조해 지워진 문장이 없음을 확인했다. 코드를 읽고 쓴 글이 아니라 고정할 커밋이 없어 revision 은 null 로 둔다. 지어내지 않는다."
|
|
},
|
|
{
|
|
"name": "실험대 저장소 (제5~6부)",
|
|
"path": "/home/donghyeon/workspace/keycloak-pattern",
|
|
"revision": "9465582b5d1630eb4ae7c4e078021486919bf6b6",
|
|
"verified": "제5부와 제6부의 근거인 기반 7단계 가이드(source/docs/guides/00-lab-host ~ 06-observability)와 설정 원본(source/deploy/lab/edge/)이 이 저장소에서 반입됐다. 반입할 때 적어 둔 source/.source-revision 이 이 커밋이고, git cat-file 로 저장소에 실재함을 확인했다. 커밋 제목은 「chore: 실행 환경 구성 문서 추가 및 수정」이고 날짜는 2026-09-10 이다 (2026-09-12 확인). 제1~4부는 이 저장소에서 오지 않았다 — 그쪽 출처는 위 항목이다. **다만 반입한 바이트가 이 커밋과 같지는 않다** (2026-09-17 대조) — 반입 14개 가운데 이 커밋과 같은 것은 3개이고 11개가 다르다. 같은 14개를 저장소의 **작업 트리**와 견주면 12개가 같다. 즉 반입은 커밋이 아니라 **그 시점의 작업 트리**(미커밋 수정이 있던 상태)에서 떠 온 것이다. 남은 2개(docs/session-lab-concepts.md · docs/guides/04-tls/README.md)는 반입 뒤 저장소가 더 고친 파일이고 지금도 git status 가 M 으로 낸다. **맞는 커밋을 찾아봤고 없다** — HEAD 에서 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 것도 9개가 어긋났다. 이 커밋은 「반입 시점의 HEAD」라는 뜻이지 「반입한 바이트가 이것이다」가 아니다"
|
|
}
|
|
],
|
|
"candidateScope": {
|
|
"document": "final/document.md",
|
|
"sections": [
|
|
"2. 전체 구조",
|
|
"3. 각 구성요소의 역할",
|
|
"4. vCPU와 vCPU Thread",
|
|
"5. Host Linux Scheduler와 실제 CPU",
|
|
"6. KVM_RUN과 Guest 실행",
|
|
"7. VM Entry와 VM Exit",
|
|
"8. 무엇이 실제로 VM Exit을 발생시키는가",
|
|
"9. VM Exit 이후 처리",
|
|
"10. Guest가 idle이면 물리 CPU는 어떻게 되는가",
|
|
"11. VM의 4 vCPU는 정확히 무엇을 의미하는가",
|
|
"12. CPU contention과 overcommit",
|
|
"13. Steal Time",
|
|
"14. 실제 Linux에서 확인할 수 있는 것",
|
|
"15. CPU 가상화 관점에서 장애를 보는 방법",
|
|
"16. 현재 Keycloak/K3s 실험과의 관계",
|
|
"17. 동시성 테스트와 부하 테스트를 분리해야 한다",
|
|
"18. Bare-metal K3s와 VM 기반 K3s의 차이",
|
|
"19. 이 SSOT에서 파생될 CONCEPT",
|
|
"20. 이 CONCEPT에서 파생되는 OPEN QUESTION",
|
|
"21. OPEN QUESTION에서 CASE가 만들어지는 흐름",
|
|
"22. 현재 단계의 핵심 Claim",
|
|
"23. 다음 단계",
|
|
"24. CPU 가상화 계층에서 발생할 수 있는 문제",
|
|
"25. CPU 문제를 계층별로 구분하는 진단표",
|
|
"26. 현재 Keycloak 실험에서 CPU 문제를 오판하지 않기 위한 기준",
|
|
"27. 문제 영역에서 파생되는 추가 OPEN QUESTION",
|
|
"28. CONCEPT -> OPEN QUESTION -> CASE 적용 기준",
|
|
"29. 이 문서에서 먼저 고정할 전체 구조",
|
|
"30. 일반 Linux의 Virtual Memory부터 시작한다",
|
|
"31. Page와 Physical Frame",
|
|
"32. Virtual Address = Page + Offset",
|
|
"33. Guest Page Table",
|
|
"34. MMU: 실제 주소 변환을 수행하는 CPU 하드웨어",
|
|
"35. TLB: 주소 변환 결과의 CPU Cache",
|
|
"36. Bare Metal과 VM의 차이",
|
|
"37. EPT(Extended Page Tables)",
|
|
"38. 왜 EPT가 필요한가",
|
|
"39. Shadow Page Table과 EPT의 의미",
|
|
"40. QEMU는 Guest RAM을 어떻게 준비하는가",
|
|
"41. KVM_SET_USER_MEMORY_REGION",
|
|
"42. Configured Memory와 실제 Physical RAM 사용량은 같지 않을 수 있다",
|
|
"43. Guest Page Table 자체도 메모리에 있다",
|
|
"44. 정상 Memory Access는 매번 VM Exit하지 않는다",
|
|
"45. Guest Page Fault",
|
|
"46. Page Fault의 대표적인 원인",
|
|
"47. EPT Violation",
|
|
"48. Guest Page Fault와 EPT Violation 비교",
|
|
"49. Host Page Fault도 별도로 존재한다",
|
|
"50. Huge Page가 필요한 이유",
|
|
"51. Huge Page와 TLB Coverage",
|
|
"52. VM에서 Huge Page를 볼 때 주의할 점",
|
|
"53. THP: Transparent Huge Pages",
|
|
"54. THP의 Trade-off",
|
|
"55. HugeTLB",
|
|
"56. THP와 HugeTLB 비교",
|
|
"57. Memory Overcommit",
|
|
"58. CPU Overcommit과 Memory Overcommit의 차이",
|
|
"59. Host Memory Pressure와 Reclaim",
|
|
"60. Host Swap이 VM에 미치는 영향",
|
|
"61. Guest Swap과 Host Swap",
|
|
"62. Memory Pressure와 Storage Contention의 연결",
|
|
"63. Swap Used만 보고 장애를 판단하면 안 된다",
|
|
"64. Ballooning이 필요한 이유",
|
|
"65. virtio-balloon 구조",
|
|
"66. Balloon Inflate",
|
|
"67. Balloon Page 반환의 의미",
|
|
"68. Balloon Deflate",
|
|
"69. Ballooning을 과도하게 하면 Guest가 압박을 받는다",
|
|
"70. Ballooning과 Memory Hotplug",
|
|
"71. OOM",
|
|
"72. Guest OOM과 Host OOM",
|
|
"73. NUMA",
|
|
"74. Local Memory와 Remote Memory",
|
|
"75. vCPU와 NUMA의 연결",
|
|
"76. vCPU Pinning만으로는 NUMA 최적화가 끝나지 않는다",
|
|
"77. Guest NUMA",
|
|
"78. NUMA는 실제 장비 topology부터 확인한다",
|
|
"79. 전체 Memory Virtualization 실행 경로",
|
|
"80. 전체 Memory Virtualization 관리 경로",
|
|
"81. CPU / Network / Storage / Memory 연결",
|
|
"82. 핵심 Claim Registry",
|
|
"83. 실제 환경에서 확인할 OPEN QUESTION",
|
|
"84. 권장 실험 순서",
|
|
"85. 실험 시 반드시 같이 기록할 것",
|
|
"86. 문제를 진단할 때의 분류",
|
|
"87. 최종 기준 그림",
|
|
"88. 결론",
|
|
"89. 문서 목적",
|
|
"90. virsh / libvirt / virtio 구분",
|
|
"91. virtio-net은 정확히 어디에 있는가",
|
|
"92. Frontend와 Backend",
|
|
"93. Guest OS는 왜 QEMU가 아니라 virtio-net을 사용하는가",
|
|
"94. 전체 네트워크 계층",
|
|
"95. Physical NIC의 역할",
|
|
"96. Linux Bridge의 역할",
|
|
"97. Routing의 역할",
|
|
"98. NAT의 역할",
|
|
"99. TAP의 역할",
|
|
"100. virtqueue의 역할",
|
|
"101. Guest TCP/IP Stack의 역할",
|
|
"102. Packet이 Keycloak까지 올라오는 과정",
|
|
"103. QEMU virtio Device Model의 역할",
|
|
"104. 왜 `TAP → vhost-net → QEMU → virtqueue`라고 일반화하면 안 되는가",
|
|
"105. Control Path와 Data Path",
|
|
"106. QEMU가 Userspace인데 packet이 QEMU를 안 거칠 수 있는 이유",
|
|
"107. vhost-net 최적화",
|
|
"108. vhost-net은 QEMU를 제거하지 않는다",
|
|
"109. Fast Path와 Slow/Control Path",
|
|
"110. Data Copy 최적화",
|
|
"111. Interrupt / Notification 최적화",
|
|
"112. Multi-Queue 최적화",
|
|
"113. Offload 최적화",
|
|
"114. Linux Bridge가 항상 Host TCP/IP Stack을 거치는 것은 아니다",
|
|
"115. Host Physical NIC로 나갈 때 virtio를 다시 거치지 않는다",
|
|
"116. 현재 Keycloak/K3s 테스트 환경과 연결",
|
|
"117. 이 구조에서 발생할 수 있는 문제",
|
|
"118. 실제 Linux에서 확인할 명령어",
|
|
"119. 실제 packet path 추적",
|
|
"120. Keycloak Refresh Token 실험과의 관계",
|
|
"121. 이 SSOT에서 파생될 CONCEPT",
|
|
"122. OPEN QUESTION",
|
|
"123. OPEN QUESTION → CASE",
|
|
"124. 핵심 Claim",
|
|
"125. 최종 기준 구조",
|
|
"126. 다음 실습 순서",
|
|
"127. 문서 목적",
|
|
"128. 전체 구조",
|
|
"129. Guest Application: `read()` / `write()`에서 시작",
|
|
"130. VFS: 공통 파일 인터페이스 계층",
|
|
"131. Filesystem(ext4/XFS): 파일 세계를 block 공간에 배치",
|
|
"132. inode",
|
|
"133. Page Cache: `write()`가 바로 SSD write는 아니다",
|
|
"134. Guest Block I/O Layer",
|
|
"135. `/dev/vda`: Guest가 보는 가상 Block Device",
|
|
"136. `/dev/vda`와 Filesystem 관계",
|
|
"137. virtio-blk: Guest의 가상 Block Device Driver",
|
|
"138. virtio-blk와 virtqueue",
|
|
"139. virtqueue의 실제 의미",
|
|
"140. VM Boundary를 넘으면 QEMU가 등장",
|
|
"141. QEMU가 물리 SSD를 직접 제어하는 것은 아니다",
|
|
"142. qcow2: Host에서는 파일, Guest에서는 디스크",
|
|
"143. qcow2 Virtual Size와 실제 Host 사용량",
|
|
"144. RAW Image",
|
|
"145. Host Block Device를 직접 backend로 사용 가능",
|
|
"146. 실제 연결 확인",
|
|
"147. VM에서는 Page Cache가 두 번 나타날 수 있다",
|
|
"148. `write()` 완료와 영속화는 다르다",
|
|
"149. Direct I/O",
|
|
"150. `fsync()`가 필요한 이유",
|
|
"151. FLUSH",
|
|
"152. 가장 위험한 상황: 거짓 완료",
|
|
"153. QEMU Cache Mode",
|
|
"154. `cache=none`",
|
|
"155. `cache=writeback`",
|
|
"156. `writeback = 위험`이라고 단정하면 안 되는 이유",
|
|
"157. Device-side Cache",
|
|
"158. Host Block Layer",
|
|
"159. 여러 VM이 하나의 NVMe를 공유하면",
|
|
"160. blk-mq: Multi-Queue Block Layer",
|
|
"161. I/O Scheduler",
|
|
"162. `none`",
|
|
"163. 실제 I/O Scheduler 확인",
|
|
"164. NVMe Driver와 Physical Device",
|
|
"165. NVMe와 SSD 구분",
|
|
"166. Storage I/O Completion",
|
|
"167. Storage Contention",
|
|
"168. CPU가 정상이어도 Storage 때문에 느릴 수 있다",
|
|
"169. Storage 관측 명령어",
|
|
"170. PostgreSQL 예시: WAL과 Durability",
|
|
"171. 성능과 Durability의 Trade-off",
|
|
"172. Storage Virtualization Canonical Flow",
|
|
"173. Network Virtualization과 비교",
|
|
"174. 핵심 Claim",
|
|
"175. 실제 테스트 서버에서 확인할 Open Questions",
|
|
"176. 권장 실습 흐름",
|
|
"177. 최종 요약",
|
|
"178. 이 부의 출처와 범위",
|
|
"179. 엣지를 물리 호스트에서 VM 으로 옮기면 무엇이 새로 필요해지나",
|
|
"180. nftables 는 앞 체인의 `accept` 로 뒤 체인의 `reject` 를 막지 못한다",
|
|
"181. qcow2 가 담는 것과 담지 않는 것",
|
|
"182. 이 구축에서 드러난 문서 결함의 공통 원인",
|
|
"183. 이 부에서 파생될 OPEN QUESTION",
|
|
"184. 이 부의 출처와 범위",
|
|
"185. 가이드 묶음이 스스로 정한 규약",
|
|
"186. 단계 00 — lab host 가상화 준비",
|
|
"187. 단계 01 — 게스트 세 대",
|
|
"188. 단계 02 — k3s server 와 agent",
|
|
"189. 단계 03 — 엣지 nginx 라우팅과 호스트 DNAT",
|
|
"190. 단계 04 — Let's Encrypt 와 인증서 갱신",
|
|
"191. 단계 05 — Keycloak 2노드와 PostgreSQL",
|
|
"192. 단계 06 — Prometheus 와 Grafana",
|
|
"193. 이 구축이 제1~4부의 어느 구조에 닿나",
|
|
"194. 이 부에서 파생될 OPEN QUESTION",
|
|
"195. 이 부의 출처와 범위",
|
|
"196. 이 문서가 무엇인가",
|
|
"197. 측정 환경",
|
|
"198. 자원 — 할당과 실사용은 다르다",
|
|
"199. 디스크 — 오버레이는 얼마나 쓰나",
|
|
"200. 부팅 — cloud-init 은 얼마나 걸리나",
|
|
"201. 네트워크 — DHCP 예약의 실제 동작",
|
|
"202. 철거 — 실제 출력 전문",
|
|
"203. 실측으로 드러난 함정 셋",
|
|
"204. 재구축할 때 무엇이 남아 있나",
|
|
"206. 이 부의 출처와 범위",
|
|
"207. `lab-edge-dnat.nft` — DNAT 파일이 자기 안에 적어 둔 네 가지",
|
|
"208. `lab-edge-dnat.service` — `ExecStartPost` 앞의 `-` 가 무엇을 봐주나",
|
|
"209. `nginx-keycloak-lab.conf` — 스티키 스위치와 신뢰 경계",
|
|
"210. `reload-nginx.sh` — `deploy/` 와 `post/` 를 가르는 한 줄",
|
|
"211. 이 부의 출처와 범위",
|
|
"212. \"이건 Arch라서 하는 건가?\"에 대한 답",
|
|
"213. 왜 호스트에 직접 깔지 않고 VM 2대인가",
|
|
"214. 전체 구조 한눈에 보기",
|
|
"215. VM 한 대의 디스크 구성",
|
|
"216. 설정 파일이 게스트에 도달하는 경로",
|
|
"217. 부팅할 때 일어나는 일",
|
|
"218. 실험대 전체 배치 (2026-09-03 구축 완료, 실측값)",
|
|
"219. 1층. 가상화",
|
|
"220. VT-x / AMD-V (하드웨어 가상화 확장)",
|
|
"221. KVM",
|
|
"222. QEMU",
|
|
"223. libvirt / virsh / libvirtd",
|
|
"224. 연결 URI — `qemu:///system` vs `qemu:///session`",
|
|
"225. 보조 그룹과 재로그인",
|
|
"226. 멱등성과 `&&` 단축 평가",
|
|
"227. systemd 소켓 활성화 (`libvirtd.socket`)",
|
|
"228. qcow2와 backing store (오버레이)",
|
|
"229. 왜 OS를 설치하지 않아도 VM이 뜨는가",
|
|
"230. 디스크 이미지를 \"복사한다\"는 것의 실제 원리",
|
|
"231. qcow2 파일 내부는 어떻게 생겼나 — 매핑표가 전부다",
|
|
"232. `qemu-img` 와 `qemu-system-x86_64` 는 다른 도구다",
|
|
"233. 오버레이는 Docker 레이어와 같은 아이디어다",
|
|
"234. 그래서 마이그레이션과 스냅샷이 된다",
|
|
"235. multipass, virt-install, virsh — 무엇이 다른가",
|
|
"236. 클라우드 이미지와 cloud-init",
|
|
"237. 확정된 함정: `--cloud-init` + Debian `genericcloud` 조합은 동작하지 않는다",
|
|
"238. 시드 ISO 를 굽는 세 명령이 각각 하는 일",
|
|
"239. 시드 디렉터리 구조와 파일명 규칙",
|
|
"240. 진단 도구: `virsh screenshot`",
|
|
"241. base 이미지가 무엇인지 확인하는 법",
|
|
"242. UEFI / OVMF (`edk2-ovmf`)",
|
|
"243. `--os-variant` / osinfo",
|
|
"244. 2층. 가상 네트워크",
|
|
"245. libvirt `default` 네트워크와 `virbr0`",
|
|
"246. dnsmasq (libvirt 내장 DHCP/DNS)",
|
|
"247. DHCP 예약 (`ip-dhcp-host`)과 MAC `52:54:00`",
|
|
"248. `--live --config`",
|
|
"249. NAT vs 브리지 vs macvtap",
|
|
"250. WiFi에서 브리지가 안 되는 이유",
|
|
"251. SSH 키는 \"머신\"이 아니라 \"홉\" 단위다",
|
|
"252. `~/.ssh/config`의 first-match-wins 규칙",
|
|
"253. `/etc/hosts`와 이름 해석 순서",
|
|
"254. 엣지를 물리 호스트에서 VM 으로 옮기면 무엇이 새로 필요해지나",
|
|
"255. nftables 는 앞 체인의 `accept` 로 뒤 체인의 `reject` 를 막지 못한다",
|
|
"256. 3층. 호스트 진입",
|
|
"257. 리버스 프록시와 `upstream`",
|
|
"258. 왜 TLS를 끊어서 내용을 보는가",
|
|
"259. `X-Forwarded-*`와 신뢰 경계",
|
|
"260. 스티키 세션",
|
|
"261. 진입점 자체가 죽으면 — 로드밸런서의 재귀 문제",
|
|
"262. `nginx -t`",
|
|
"263. 4층. TLS",
|
|
"264. ACME",
|
|
"265. 도메인 검증: HTTP-01 vs DNS-01",
|
|
"266. DNS-01 은 언제 쓰는가 — 네 가지 경우",
|
|
"267. `fullchain.pem` / `privkey.pem` / `cert.pem` / `chain.pem`",
|
|
"268. 공개 DNS에 사설 IP를 넣는 것",
|
|
"269. 5층. k3s",
|
|
"270. k3s server / agent / node-token",
|
|
"271. `--node-ip` / `--tls-san`",
|
|
"272. kubeconfig의 `127.0.0.1` 문제",
|
|
"273. agent 노드에는 kubeconfig가 없다 — `localhost:8080` 오류",
|
|
"274. Traefik (k3s 기본 ingress)",
|
|
"275. 호스트 nginx와 Traefik은 무엇이 다른가 — 둘 다 필요한 이유",
|
|
"276. servicelb (klipper-lb)",
|
|
"277. flannel VXLAN",
|
|
"278. NetworkPolicy와 k3s의 내장 컨트롤러",
|
|
"279. 매니페스트 읽는 법 — `deploy/lab/k8s/echo.yaml`을 예로",
|
|
"280. 무엇을 어디에 설치하는가",
|
|
"281. Docker를 lab host에 설치하면 안 되는 이유",
|
|
"282. 그러면 이미지는 어떻게 넣는가",
|
|
"283. 6층. Arch 특이사항",
|
|
"284. nginx 설정 구조 — `sites-available`은 nginx 기능이 아니다",
|
|
"285. 롤링 릴리스와 부분 업그레이드 금지",
|
|
"286. 패키지명 대응표",
|
|
"287. 없어서 오히려 편한 것",
|
|
"288. 게스트 배포판: Debian이란 무엇이고 Ubuntu와 무엇이 다른가",
|
|
"289. 7층. git",
|
|
"290. `.gitignore` 패턴 앵커링",
|
|
"291. 이미 추적 중인 파일은 무시되지 않는다",
|
|
"292. 8층. 패키지 저장소와 설치 원리",
|
|
"293. 저장소(repository)란 무엇인가",
|
|
"294. 설치는 다섯 단계로 진행된다",
|
|
"295. apt (Debian / Ubuntu)",
|
|
"296. pacman (Arch)",
|
|
"297. 왜 HTTP로 받아도 안전한가 — 서명 신뢰 사슬",
|
|
"298. 세 배포판 대조표",
|
|
"299. 이 실험대에서 어디에 나타나는가",
|
|
"300. 9층. `deploy/` — 무엇이 살아 있고 무엇이 참조인가",
|
|
"301. 전체 지도",
|
|
"302. 왜 적용하지 않는 것을 남겨두는가",
|
|
"303. `reverse-proxy/` — 1홉 계약의 원본",
|
|
"304. `tls/` — 같은 일을 하는 두 구현",
|
|
"305. `tunnel/` — 채택하지 않은 이유를 남긴 자산",
|
|
"306. `.example` 접미사 관례",
|
|
"307. 10층. 쿠버네티스 리소스 — 이 실험대에서 실제로 쓴 것들",
|
|
"308. 워크로드 세 종류 — 무엇을 언제 쓰는가",
|
|
"309. 저장소 — PVC · PV · StorageClass",
|
|
"310. Secret — 감춰지지 않는다",
|
|
"311. RBAC — ServiceAccount · ClusterRole · Binding",
|
|
"312. 배치 제어 — nodeSelector · 라벨 · taint",
|
|
"313. k3s server와 agent — 죽였을 때가 다르다",
|
|
"314. 11층. Keycloak 클러스터링 내부 — Infinispan과 JGroups",
|
|
"315. 두 층으로 되어 있다",
|
|
"316. 디스커버리와 트랜스포트는 다른 경로다",
|
|
"317. 코디네이터",
|
|
"318. 클러스터 뷰",
|
|
"319. 주요 JGroups 프로토콜 — 지표 이름에 그대로 나온다",
|
|
"320. 세션은 어디에 있는가 — 두 곳이되 역할이 다르다",
|
|
"321. 세션 쓰기 트랜잭션의 세 가지 설계 결정",
|
|
"322. 12층. 관측성 — Prometheus의 구조",
|
|
"323. 세 부분으로 되어 있다",
|
|
"324. exporter 패턴",
|
|
"325. 서비스 디스커버리 — 타깃을 적어두지 않는다",
|
|
"326. relabel — 걸러내고 이름을 붙인다",
|
|
"327. 메트릭 타입",
|
|
"328. `up` — 가장 중요한 합성 지표",
|
|
"329. TSDB와 보존 기간",
|
|
"330. 관측 시스템의 장애 도메인",
|
|
"331. 13층. 가상화 운영 — 실행 중 바꾸는 것들",
|
|
"332. VM 메모리 재배분 — 게스트를 다시 만들지 않는다",
|
|
"333. 안전한 종료 순서",
|
|
"334. 복구 순서 — 종료의 역순",
|
|
"335. qcow2 파일을 다른 물리 서버로 옮기면 무엇이 따라가나"
|
|
],
|
|
"excluded": [
|
|
"1. 이 문서의 범위",
|
|
"205. 관련 문서",
|
|
"336. 아직 기록하지 않은 개념",
|
|
"337. 이번에 채운 것 (2026-09-11)",
|
|
"338. 이번에 채운 것 (2026-09-04)"
|
|
],
|
|
"excludedAnchorPattern": "#(1-이-문서의-범위|205-관련-문서|336-아직-기록하지-않은-개념|337-이번에-채운-것-2026-09-11|338-이번에-채운-것-2026-09-04)$",
|
|
"note": "접어 넣은 분석이 아니라 처음부터 한 편으로 쓴 개념 문서라 제2부·제3부가 없다. 대신 이 문서 자신이 네 부다 — 제1부 CPU 가상화(§1~§28) · 제2부 메모리 가상화(§29~§88) · 제3부 네트워크 가상화(§89~§126) · 제4부 스토리지 가상화(§127~§177). 네 부 모두가 후보를 찾는 범위이고 sections 는 그 절 제목을 부 순서대로 적는다. 지금 sections 에 있는 것은 제1부 §2~§28 과 제2부 §29~§88 이고, 제3부와 제4부는 그 부를 분해할 때 같은 형식으로 뒤에 덧붙인다. §1 은 이 문서가 무엇을 다루고 무엇을 안 다루는지를 선언한 자리라 후보가 나오는 곳이 아니다 — 글감이 그 선언을 근거로 인용할 수는 있어서 concept 노드의 source 에는 들어 있고, excludedAnchorPattern 은 §1 만 가리키는 앵커로 올라온 글감을 막는다. 앵커 형식은 `final/document.md#<h2 절 제목 슬러그>` 하나로 통일한다. ── 제3부(네트워크 가상화 §89~§126)를 분해하면서 그 절 제목 38개를 sections 뒤에 덧붙였다. ── 제4부(스토리지 가상화 §127~§177)를 분해하면서 그 절 제목 51개를 다시 뒤에 덧붙였다. 이제 sections 에는 네 부가 모두 들어 있다 — 제1부 §2~§28 · 제2부 §29~§88 · 제3부 §89~§126 · 제4부 §127~§177, 합쳐 176개이고 남은 부는 없다. excluded 와 excludedAnchorPattern 은 §1 그대로다. ── 제5부(실험대에서 실제로 확인한 것 §178~§183)를 분해하면서 그 절 제목 6개를 sections 뒤에 덧붙였다. 이 부는 앞 네 부 뒤에 나중에 SSOT 로 들어온 것이라 그전까지 sections 에 없었다 — 지금은 176개에 6개를 더해 182개이고 SSOT 의 절 전부가 범위 안에 있다. 이 부는 처음에 §62 부터 다시 매겨져 있어 제2부와 번호가 여섯 개 겹쳤다. 2026-09-11 에 SSOT 의 제목 여섯 줄을 §178~§183 으로 고쳐 문서가 §1~§183 한 줄기로 이어진다. 앵커는 번호가 아니라 절 제목 슬러그라 그때도 충돌하지 않았지만, 사람이 「§64 를 봐」라고 부르면 제2부의 Ballooning 과 제5부의 nftables 중 어느 쪽인지 갈렸다. §178 은 이 부의 출처와 범위를 적은 자리지만 §1 처럼 excluded 로 빼지 않고 범위 안에 두었다 — 그 절이 선언만 하는 것이 아니라 이 실험대의 관측된 환경(호스트 사양·QEMU 11.1.1·libvirt 12.7.0·이더넷 없음·게스트 3대)을 함께 적고 있고 그것이 제5부 글감들의 전제이기 때문이다. 대신 후보 대장에서 출처·범위 쪽은 KEEP_IN_SSOT, 환경 쪽은 MERGE_INTO 로 갈랐다. excluded 와 excludedAnchorPattern 은 §1 그대로다. ── 제6부(실험대는 어떻게 세워졌나 §184~§194)를 분해하면서 그 절 제목 11개를 sections 뒤에 덧붙였다. 이 부도 제5부처럼 앞의 다섯 부 뒤에 나중에 SSOT 로 들어온 것이라 그전까지 sections 에 없었다 — 지금은 182개에 11개를 더해 193개이고 SSOT 의 절 전부가 범위 안에 있다. §184 는 §178 과 같은 기준으로 정했다 — 이 부의 출처와 범위를 적은 자리지만 §1 처럼 excluded 로 빼지 않고 범위 안에 두었다. 그 절이 선언만 하는 것이 아니라 이 부가 무엇을 어떻게 검증했는지(읽기 전용 확인은 돌아가는 실험대에서 실제로 실행했고, 만드는 명령은 다시 치면 실험대가 없어지므로 그때 친 것을 옮겼다)를 함께 적고 있고, 그것이 §186~§192 의 모든 관측이 어디까지 유효한지를 정하기 때문이다. 대신 후보 대장에서 출처·범위 쪽은 KEEP_IN_SSOT, 검증 방식 쪽은 MERGE_INTO 로 갈랐다 — §178 에서 출처·범위와 관측된 환경을 갈랐던 것과 같은 처분이다. excluded 와 excludedAnchorPattern 은 §1 그대로다. ── 제7·8·9부를 분해하면서 그 절 제목 140개를 sections 뒤에 덧붙였다. 세 부 모두 앞의 여섯 부 뒤에 나중에 SSOT 로 들어온 것이라 그전까지 sections 에 없었다 — SSOT 대조에서 `source/` 가 SSOT 에 안 담겨 있던 것이 드러나 11,878행이 17,512행으로, 부가 여섯에서 아홉으로, 절이 194개에서 338개로 늘었는데 sections 는 §2~§194 그대로였다. 지금은 193개에 140개를 더해 333개다. **제7·8·9부도 후보를 찾는 범위다** — 이 프로젝트에는 접어 넣은 모듈 분석이 없고, 세 부는 전부 밖에서 반입한 원본 문서의 전문이라 제5·6부와 성격이 같다. 근거로만 쓰는 부는 없다. §195·§206·§211(각 부의 출처와 범위)은 §178·§184 와 같은 기준으로 범위 안에 두었다 — 그 절들이 선언만 하는 것이 아니라 측정 규약(추정값 없음·없는 값은 「미측정」)과 대조 원장(실행되는 줄 49 는 이미 있었고 주석 59줄이 빠져 있었다)과 두 스냅샷의 날짜 차이를 함께 적고 있고, 그것이 그 부 글감들의 전제이기 때문이다. 대신 후보 대장에서 출처·범위 쪽은 KEEP_IN_SSOT, 규약·대조 쪽은 MERGE_INTO 로 갈랐다 — §178·§184 에서 쓴 것과 같은 처분이다. **excluded 가 넷 늘었다.** §205(관련 문서)는 링크 표이고 가리키는 대상 대부분이 `source/` 에 반입되지 않았다. §336(아직 기록하지 않은 개념)은 **문서에 없는 것**의 목록이라 정의상 후보가 나올 수 없고, §337·§338(이번에 채운 것)은 원본 문서의 편집 이력이다. 넷 다 §1 과 같은 자리 — 분석 재료이지 글감이 나오는 곳이 아니다. excludedAnchorPattern 을 다섯 앵커를 가리키는 하나로 넓혔다."
|
|
},
|
|
"assetLedger": {
|
|
"note": "final/assets/diagrams/ 의 그림 아홉 장. 부마다 개념 기록이 한 장씩 받았고, 배정되지 않은 그림은 없다. 개념 열하나 가운데 셋은 그림을 안 받았다 — huge-pages-in-a-vm 과 qemu-block-backend-forms 는 본문의 비교표가 답해 3단계가 청구하지 않았고, memory-pressure-reclaim-swap-oom 은 4단계가 근거 절 §62 로 그리려다 멈췄다(그 절이 「Guest와 Host가 동시에 memory pressure를 겪으면」이라고 적어 순서가 없는데, techviz 가 그 문맥에서 허용하는 profile 이 순서를 요구하는 sequence 하나뿐이었다. 순서를 지어내야 그려진다). ── 제5부(실험대 환경 구성)의 글감 일곱에는 그림을 배정하지 않았다. final/assets/diagrams 의 아홉 장은 제1~4부의 개념이 받은 것이고, 제5부가 그릴 대상(엣지 이동 전/후의 요청 경로, 같은 훅의 base 체인 평가 순서, qcow2 가 담는 것과 담지 않는 것)에 해당하는 그림은 아직 없다. 있는 것을 안 쓴 것이 아니라 없는 것이라 unassigned 도 그대로 비어 있다 — 만드는 것은 4단계의 일이고 배정도 그때 적는다. ── 4단계가 제5부의 셋 가운데 하나를 그렸다. case:nftables-accept-did-not-stop-the-libvirt-reject 가 nftables-forward-hook-chain-order 를 받았다 — 같은 훅에 붙은 base 체인 둘의 평가 순서와, insert 로 넣은 구멍이 체인 안에서 서는 자리다. 나머지 둘은 그리지 않기로 판정됐고 그래서 unassigned 에도 없다. concept:what-a-qcow2-file-carries 의 「파일 안/밖」은 관계선을 지워도 안쪽 넷·바깥쪽 넷의 목록이 남아 표이고, 그 표가 이미 그 기록 본문에 있다. 엣지 이동 전/후의 요청 경로는 decision 기록이 전/후 두 줄로 적어 옆 문단이 이미 말한다. 둘 다 그릴 대상이 없어진 것이므로 unassigned 의 「있는데 안 쓴 그림」과는 다른 상태다.",
|
|
"assigned": [
|
|
"vm-exit-handling-cycle",
|
|
"guest-memory-address-translation-path",
|
|
"page-fault-layers-in-a-vm",
|
|
"virtio-balloon-inflate-deflate",
|
|
"numa-vcpu-and-memory-placement",
|
|
"packet-control-and-data-paths",
|
|
"guest-block-io-to-virtqueue",
|
|
"write-completion-boundaries",
|
|
"vms-sharing-one-nvme",
|
|
"nftables-forward-hook-chain-order"
|
|
],
|
|
"unassigned": []
|
|
},
|
|
"contract": {
|
|
"decomposition": [
|
|
"글감을 찾는 입력은 final/document.md 하나다. 거기에 없는 근거는 먼저 SSOT 에 넣는다.",
|
|
"후보 전부는 candidates 에 처분과 함께 남고 PROMOTE 만 topics 로 올라간다.",
|
|
"없애고 관련 Case 나 Concept 의 한 절로 넣어도 이해·결정·재사용성이 그대로라면 독립 기록으로 만들지 않는다.",
|
|
"Topic 은 독자 질문 하나다. 그 물음에 답하지 않는 글감은 다른 Topic 으로 옮긴다.",
|
|
"Concept 은 Case·Decision·Question 을 먼저 고른 뒤 그것을 이해하는 데 필요한 것만 거꾸로 더한다."
|
|
],
|
|
"anchorStyle": "final/document.md#<h2 절 제목 슬러그>. 제목에서 ` * ( ) , : · — ? . / 를 공백으로 바꾸고 연속 공백을 - 로 접고 소문자로 내린 것이다.",
|
|
"readinessValues": [
|
|
"READY",
|
|
"OPEN",
|
|
"NEEDS_EVIDENCE",
|
|
"NEEDS_DECISION",
|
|
"BLOCKED"
|
|
],
|
|
"dispositionValues": {
|
|
"PROMOTE": "독립 Tech Log 로 쓴다",
|
|
"MERGE_INTO": "다른 기록의 한 절·표 행으로 흡수한다",
|
|
"KEEP_IN_SSOT": "분석에는 남기고 독립 기록으로 만들지 않는다 — 정상적인 성공 결과다",
|
|
"NEEDS_EVIDENCE": "주장에 아직 검증이 없다. 측정한 뒤에 다시 판정한다",
|
|
"NEEDS_DECISION": "방향이 그럴듯하지만 프로젝트가 정하지 않았다",
|
|
"BLOCKED": "원본이 불완전하거나 서로 어긋난다"
|
|
}
|
|
},
|
|
"note": "프로젝트 virtualization 의 SSOT 는 다섯 부다 — CPU(§1~§28) · 메모리(§29~§88) · 네트워크(§89~§126) · 스토리지(§127~§177) · 실험대(§178~§183). 다섯 부를 모두 분해했고 지금 주제는 다섯, 글감은 64개다 — cpu-virtualization 13 · memory-virtualization 20 · network-virtualization 10 · storage-virtualization 14 · lab-environment-build 7. 제1~4부에서 나온 주제 넷은 case 와 decision 이 0 이고 이유는 하나다 — 그 네 부에는 이 테스트 Host 에서 잰 값이 하나도 없고 측정이 없으면 Case 가 아니다. 제5부는 다르다 — 실험대를 실제로 세우면서 관측한 것이라 이 프로젝트의 첫 Case 와 첫 Decision 이 거기서 나왔다. 아래는 부마다 무엇을 어떻게 판단했는지를 분해한 순서대로 이어 적은 기록이다. ── 제1부(CPU 가상화 §1~§28)는 주제 하나에 concept 하나로 내려앉았다. 하나로 둔 근거는 SSOT 자신이다 — §19 가 이 실행 경로를 하나의 CONCEPT 로 관리하고 여러 문서로 과도하게 분할하지 않는다고 적었고, §28 이 CONCEPT 에서 확정할 범위를 「구조적으로 어떤 문제가 발생할 수 있는가 · 어떤 지표로 의심할 수 있는가 · 어느 계층에서 확인해야 하는가」로 그었다. 검사기는 그 시점에 「Topic 에 노드가 하나뿐이다」를 warn 으로 냈다. 그것이 그때 이 프로젝트의 상태였고 노드를 늘려서 지우지 않았다 — 뒤에 §20·§27 을 재판정해 question 열둘을 올리면서 그 warn 이 사라졌다. 이 SSOT 에는 이 테스트 Host 에서 잰 값이 하나도 없다. 그 상태에서 §20·§27 의 OPEN QUESTION 열둘을 question 글감으로 올렸다 — Question 은 아직 닫히지 않은 판단을 적는 종류라 측정이 없다는 것이 올리지 못할 이유가 아니고, 값이 나오면 그때 Case 가 된다. OQ 하나에 글감 하나이고 열둘의 known 에는 SSOT 가 서술한 것만 적었다. 측정이 필요한 Claim(SSOT-22-claims-12-13)은 그대로 NEEDS_EVIDENCE 다 — 그것은 「이 환경에서 실제로 그러하다」는 주장이라 재기 전에는 Case 도 Question 도 아니다. Topic 은 하나로 두었다. 열둘이 전부 「구조적으로 가능한 문제가 이 Host 에서 실제로 일어나는가」를 묻고 있어 cpu-virtualization 의 독자 질문 뒷부분(어느 계층의 문제인지 무엇을 보고 가르는가)에 그대로 걸린다. 주제를 나누면 개념과 그 개념이 낳은 물음이 갈라진다. final/assets 의 첫 그림은 vm-exit-handling-cycle 한 장이고 assetLedger 에 배정으로 적었다 — §9 가 그린 VM Exit 이후 제어권 경로다. 이 단계에서 final/document.md 의 frontmatter 를 떼고 heading 단계를 맞췄다 — 절이 h1 이면 앵커가 가리킬 절이 없다. 절을 h2, 그 아래를 h3·h4 로 내렸고 본문 문장은 한 글자도 바꾸지 않았다. ── 제2부(메모리 가상화 §29~§88)는 절 60개이고 주제 하나 (memory-virtualization) 에 글감 20개로 내려앉았다 — concept 여섯 · reference 둘 · question 열둘, case 와 decision 은 0 이다. concept 을 여섯으로 나눈 것은 절을 훑어서가 아니라 §83 의 OPEN QUESTION 을 먼저 고른 뒤 그 물음들이 읽히려면 무엇을 미리 알아야 하는지를 거꾸로 물어서다 — 주소 변환(OQ-2·3 이 필요로 한다) · page fault 세 계층(OQ-13·14) · huge page(OQ-4·5) · 메모리 압박(OQ-6·7·14) · ballooning(OQ-8·9) · NUMA(OQ-11·12) 여섯이 그렇게 나왔고 남는 개념은 없다. CPU 부가 §19 의 「하나의 CONCEPT 로 관리하고 여러 문서로 과도하게 분할하지 않는다」를 근거로 한 편으로 묶은 것과 달리 제2부에는 그런 지시가 없고, §52·§58·§61·§70·§72 처럼 「A 와 B 는 같지 않다」로 경계를 긋는 절이 계속 나와 한 편으로 묶으면 그 경계들이 한 글의 각주가 된다. case 가 0 인 이유는 제2부에도 이 테스트 Host 에서 잰 값이 하나도 없기 때문이다 — 측정이 없으면 Case 가 아니다. §82 의 CLAIM-MEM-01~18 은 전부 메커니즘 서술이라(「~할 수 있다」·「A 와 B 는 다르다」) 여섯 개념에 나눠 흡수했고, CPU 부의 Claim 12·13 처럼 「이 환경에서 실제로 그러하다」고 주장한 것이 없어 NEEDS_EVIDENCE 로 남길 것도 없었다. §83 의 OQ 열넷 가운데 열둘만 올렸다 — OQ-1(Host NUMA topology)과 OQ-10(vCPU 가 어느 Host CPU 에 있는가)은 CPU 부의 question:host-numa-topology 가 이미 묻고 있고 그 unknown 이 「두 VM 의 vCPU 와 memory 가 어느 node 에 배치되어 있는지」를 그대로 담고 있어 MERGE_INTO 로 두었다. 같은 한 번의 측정으로 닫히는 물음을 두 편으로 만들지 않는다. 그 질문이 「SSOT 는 이 확인에 쓸 명령을 적지 않았다」고 남긴 자리에 §83 이 적은 `lscpu` · `numactl --hardware` · `virsh vcpuinfo`/`vcpupin` 이 들어간다. 넘겨받은 쪽은 OQ-11(QEMU memory 가 어느 node 에 있는가 — `numastat -p` 는 CPU 부 어디에도 없다)과 OQ-12(remote access 가 latency 를 바꾸는가)다. reference 둘은 §85 와 §86 에서 나왔다 — 열두 Question 전부에 같이 걸리는 규칙이라 각 글에 되풀이하는 대신 한 편씩으로 두었다. 제2부의 그림은 아직 없다 — assetLedger 는 그대로다. ── 제3부(네트워크 가상화 §89~§126)를 분해했다. 절 38개이고 주제 하나(network-virtualization)에 글감 10개로 내려앉았다 — concept 하나 · reference 둘 · question 일곱, case 와 decision 은 0 이다. concept 을 하나로 둔 근거는 §121 이다. 그 절이 virsh 부터 Packet tracing 까지 스물한 요소를 한 목록으로 묶고 「이 요소들이 하나의 packet 실행 경로를 설명하므로 하나의 CONCEPT 로 관리한다」고 적었다. 제1부의 §19 가 CPU 실행 경로에 대해 같은 지시를 했고 그 부도 개념 하나였다. 제2부를 여섯으로 나눈 것은 거기에 그런 지시가 없었기 때문이지 절 수가 많아서가 아니다. 넷으로 쪼개는 안 — 경로 · backend 주체(§103~§109) · Host 쪽 배선(§95~§99·§114) · 최적화(§110~§113) — 을 놓고 봤지만 SSOT 가 반대로 적은 것을 뒤집을 근거를 SSOT 안에서 찾지 못해 기각했다. case 가 0 인 이유는 제3부에도 이 테스트 Host 에서 잰 값이 하나도 없기 때문이다 — 측정이 없으면 Case 가 아니다. §124 의 Claim 1~16 은 전부 메커니즘 서술이라 개념에 흡수했고, 「이 환경에서 실제로 그러하다」고 주장한 것은 §116 하나뿐이라 그것만 NEEDS_EVIDENCE 로 남겼다 — Host Nginx 아래 VM 두 대라는 앞부분은 §89 가 밝힌 실험 구성이지만 TAP → vhost-net → virtqueue 로 펼친 뒷부분은 OQ-1·2·3·6 이 답하기 전의 가정이다. CPU 부의 SSOT-22-claims-12-13 과 같은 처분이다. §122 의 OQ 일곱은 전부 올렸다 — 다른 부의 question 이 같은 측정으로 닫는 물음이 하나도 없었다. 가장 가까운 것이 OQ-7 과 CPU 부의 question:virtualization-layer-saturation-during-refresh 인데, OQ-7 이 재라고 적은 vhost thread 와 softirq 는 제1부·제2부 어디에도 나오지 않는다(§81 의 그림에 vhost 라는 낱말이 한 번 나올 뿐이다). 두 물음은 같은 실험 구간에서 서로 다른 스레드를 재므로 relations 로만 이었다. reference 둘은 §119 와 §120 에서 나왔다. §119 는 네 지점의 capture O/X 로 끊긴 구간을 좁히는 절차이고 §120 은 그것이 network 문제인지 애플리케이션·저장소 문제인지를 가르는 귀속 규칙이라 답하는 물음이 다르다. 둘 다 적용 조건과 예외를 SSOT 가 스스로 대 준다 — §113·§114 가 앞의 예외를, §102·§116 이 뒤의 예외를 댄다. 제1부에서 §14(명령 목록)와 §15(계층 분리)를 개념으로 흡수한 것과 판단이 갈리는데, 그때 적은 이유는 「붙일 Reference 가 없다」였고 제2부가 §85·§86 으로 기준을 바꿨다. 여기서는 제2부 쪽을 따랐고 §118 의 명령 목록은 §119 의 Reference 본문 표로 들어간다. 제3부의 그림은 아직 없다 — assetLedger 는 그대로다. ── 제4부(스토리지 가상화 §127~§177)를 분해했다. 절 51개이고 주제 하나(storage-virtualization)에 글감 14개로 내려앉았다 — concept 넷 · reference 셋 · question 일곱, case 와 decision 은 0 이다. concept 을 넷으로 나눈 것은 절을 훑어서가 아니라 §175 의 OQ 여덟을 먼저 고른 뒤 그 물음들이 읽히려면 무엇을 미리 알아야 하는지를 거꾸로 물어서다 — Guest 쪽 경로(OQ-1·OQ-8 이 필요로 한다) · backend 형태(OQ-1·OQ-2·OQ-3·OQ-5) · 완료의 네 가지 뜻(OQ-4·OQ-8) · Host block stack 과 경쟁(OQ-5·OQ-6·OQ-7) 넷이 그렇게 나왔고 남는 개념은 없다. 제1부와 제3부는 §19 와 §121 이 「하나의 CONCEPT 로 관리하고 여러 문서로 과도하게 분할하지 않는다」고 적어 한 편으로 묶었지만 제4부에는 그런 지시가 없고, §144·§148·§149·§152·§156·§157·§162·§165·§167·§168·§171 처럼 「A 는 B 가 아니다」로 경계를 긋는 절이 계속 나와 한 편으로 묶으면 그 경계들이 한 글의 각주가 된다 — 제2부를 여섯으로 나눈 것과 같은 이유다. 경로를 한 장으로 그린 §128·§172·§177 은 첫 개념에 흡수했다. 네 개념이 나눠 설명하는 지도라 어느 하나가 소유하지 않지만, 경로가 시작하는 글이 그림을 열고 나머지 셋이 자기 구간을 가리키는 편이 그림 하나를 다섯 번째 기록으로 만드는 것보다 낫다 — 제2부가 §79·§87 을 같은 이유로 첫 개념에 넣었다. case 가 0 인 이유는 제4부에도 이 테스트 Host 에서 잰 값이 하나도 없기 때문이다. §143 의 100 GB/3GB 도 §168 의 CPU 30%/latency 2초 도 SSOT 가 예시로 든 값이지 이 장비에서 잰 값이 아니다. §174 의 Claim 1~6 은 전부 메커니즘 서술이라(「~할 수 있다」·「A 와 B 는 같은 의미가 아니다」) 개념 둘과 Reference 하나에 나눠 흡수했고, 제1부의 Claim 12·13 이나 제3부의 §116 처럼 「이 환경에서 실제로 그러하다」고 주장한 절이 없어 NEEDS_EVIDENCE 로 남길 것도 없었다. §175 의 OQ 여덟 가운데 일곱만 올렸다 — OQ-2(qcow2 인가 RAW 인가)는 OQ-3 과 같은 한 번의 `qemu-img info` 출력으로 닫히고 형식은 그 출력의 첫 줄이라 MERGE_INTO 로 두었다. 제2부가 OQ-1 과 OQ-10 을 합친 것과 같은 판단이다. 다른 부의 question 과는 하나도 겹치지 않았다 — 가장 가까운 둘이 OQ-7 과 제1부의 question:vcpu-contention-under-two-vm-load, OQ-8 과 제2부의 question:host-major-fault-vs-storage-latency 인데 앞의 짝은 부하 형태가 같을 뿐 재는 자원이 CPU 와 storage 로 다르고, 뒤의 짝은 한쪽이 memory pressure 로 유도한 major fault 를 보고 다른 쪽이 애플리케이션의 `fsync()` 를 보므로 유도 방법도 계열도 다르다. 둘 다 relations 로만 이었다. reference 셋은 §168·§171·§145 에서 나왔다. §168 은 지연을 CPU 로 귀속하기 전에 storage 를 따로 재라는 자원 귀속 규칙이고, §171 은 빨라진 구성이 durability contract 를 지운 것은 아닌지 가르라는 평가 규칙이고, §145 는 Guest 안에서 본 disk 로 backend 를 단정하지 말라는 확정 규칙이라 셋이 답하는 물음이 다르다. 셋 다 적용 조건과 예외를 SSOT 가 스스로 대 준다 — §167·§169·§177 이 첫째의, §149·§154·§157 이 둘째의, §143·§144 가 셋째의 예외를 댄다. NEEDS_DECISION 하나를 남겼다 — cache mode 를 무엇으로 둘 것인가는 §153~§156 이 두 설정의 뜻만 갈라 놓고 방향을 정하지 않았고, 현재 값이 무엇인지도 OQ-4 가 아직 확인하지 않았다. 제4부의 그림은 아직 없다 — assetLedger 는 그대로다. ── 제5부(실험대에서 실제로 확인한 것 §178~§183)를 분해했다. 절 6개이고 주제 하나(lab-environment-build)에 글감 7개로 내려앉았다 — case 하나 · concept 하나 · reference 하나 · question 셋 · decision 하나. 앞 네 부와 성격이 다르다. 그 넷은 CPU·메모리·네트워크·스토리지가 어떻게 동작하는가를 적은 개념 문서였고 이 부는 그 위에 실험대 한 대를 실제로 세우면서 무엇이 새로 필요해졌고 어디서 막혔는가를 적는다. 그래서 여기서만 case 와 decision 이 0 이 아니다 — §180 는 증상(호스트에서 404, 밖에서 connection refused)·진단(libvirt `guest_input` 끝의 reject, 카운터 4 패킷이 밖에서 친 curl 횟수와 일치)·해결(체인 맨 앞에 insert, `ExecStartPost` 로 재삽입)이 한 절에서 닫히는 Case 이고, §179 은 「바꾼 이유는 성능이 아니라 더러워지는 층의 격리다」라는 근거와 일곱 줄의 감수한 비용을 SSOT 가 직접 적은 Decision 이다. 기술이 쓰였다는 사실을 왜 골랐는지로 바꾼 것이 아니라 SSOT 가 적어 둔 이유 문장을 그대로 받았다. concept 은 절을 훑어서가 아니라 §183 의 OQ 둘(virsh save 덤프 비용 · WiFi 로 qcow2 이동 시간)을 먼저 고른 뒤 거꾸로 물어서 하나만 나왔다 — 무엇이 파일에 따라오고 무엇이 안 따라오는지를 모르면 그 둘이 무엇을 재는지 말할 수 없다. §180 의 nftables base 체인 평가 규칙과 §179 의 OUTPUT↔FORWARD 전환은 Concept 으로 올리지 않았다 — 앞의 것은 그 Case 의 진단 자체라 떼면 Case 가 반쪽이 되고, 뒤의 것은 세 문장이라 한두 문장으로 Case 안에 설명되는 것에 해당한다. reference 는 §182 하나다. 결함 여섯을 따로 Case 로 쪼개지 않은 것은 여섯이 같은 물음에서 나와 같은 결론(틀린 것은 명령이 아니라 그 명령이 놓인 위치다)에 닿기 때문이고, 그 하나를 Case 가 아니라 Reference 로 둔 것은 원 프로젝트의 이름을 지워도 규칙이 남기 때문이다. question 셋은 §183 의 OQ 셋을 그대로 받았고 하나도 합치지 않았다 — OQ-2 와 OQ-3 은 같은 이식 계열이지만 한쪽은 같은 호스트의 RAM 덤프를, 한쪽은 다른 기계로 가는 파일 전송을 재므로 한 번의 측정으로 함께 닫히지 않는다. 제외는 12건이다 — KEEP_IN_SSOT 둘(§178 의 출처와 범위 선언, §181 의 온프렘→클라우드 반입 절차. 뒤의 것은 SSOT 가 스스로 external·코드 관측 아님이라고 표시했고 이 실험대에서 해 보지 않은 절차라 올리면 관측과 외부 지식이 같은 무게를 갖는다)과 MERGE_INTO 열이다. 제5부의 그림은 아직 없다 — assetLedger 는 그대로다. 한 가지를 남겨 둔다: §178 가 이 호스트의 VM 네트워크를 libvirt NAT(`virbr0`) 라고 observed 로 적어 제3부의 question:vm-network-mode-bridge-nat-or-routed 의 unknown 일부가 이미 답해졌다. 이번 단계의 범위가 「기존 글감을 고치지 않고 새 재료만 더한다」라 그 노드는 손대지 않았고 OPEN 그대로 두었다. 그 질문을 닫을지는 다음 재판정에서 본다. ── 제6부(실험대는 어떻게 세워졌나 §184~§194)를 분해했다. 절 11개이고 후보 65건에서 PROMOTE 7건이 나왔다 — 새 주제 build-completion-judgment 에 여섯(case 셋 · reference 둘 · question 하나), 기존 주제 lab-environment-build 에 decision 하나다. 주제를 새로 만든 이유는 독자 질문이 갈리기 때문이다. lab-environment-build 는 「무엇이 새로 필요해지고 어디서 막히는가」를 묻는데 제6부의 알맹이는 막힌 자리가 아니라 **막히지 않은 것처럼 보인 자리**다 — 설치 출력은 성공인데 agent 가 안 붙고, 검사기는 통과했는데 cloud-init 이 안 돌고, 갱신은 매번 SUCCESS 인데 옛 인증서가 38분 25초 동안 나갔다. 그 셋이 답하는 물음은 「끝났다는 것을 무엇으로 판정하나」 하나라 새 주제로 묶었다. decision 하나만 옛 주제로 보낸 것은 그것이 판정이 아니라 제약 때문에 새로 필요해진 것을 적기 때문이다 — 도메인이 CGNAT 대역이라 HTTP-01 이 성립하지 않아 DNS-01 과 Cloudflare API 토큰이 새로 필요해졌고, 그것은 엣지를 게스트로 옮기면서 certbot 이 따라간 §179 의 일곱 번째 줄과 같은 물음에 답한다. case 를 셋으로 나눈 것은 관측의 수가 아니라 물음의 수를 따른 것이다 — 빈 토큰(셸이 빈 값을 안 막는다), cloud-init(한 증상에 원인이 넷이고 전부 게스트 밖에 있다), 갱신 훅(받는 것과 서빙하는 것이 다르다)은 서로 다른 것을 묻고 서로 다른 결론에 닿는다. 반대로 §186~§192 에 흩어진 「출력을 상태로 읽었다」 열몇 건은 §191·§192 가 규칙으로 적어 둔 것이 있어 한 편의 reference 로 모았다 — 표의 행 하나가 될 것을 기록 하나로 만들지 않는다. concept 은 하나도 세우지 않았다. Case·Decision·Question 을 먼저 고른 뒤 「이것을 읽는 사람이 미리 알아야 하는 구조가 있나」를 물었을 때 나온 것이 전부 두세 문장으로 그 글 안에서 닫혔기 때문이다(Deployment→ReplicaSet 사슬, agent 의 kubeconfig 부재, lineage 라벨). 제외는 58건이다 — MERGE_INTO 42 · KEEP_IN_SSOT 12 · BLOCKED 3 · NEEDS_EVIDENCE 1. BLOCKED 셋은 SSOT 안에서 값이 갈리거나 원본이 없는 것이다(호스트 코어 수가 「16 코어」와 「논리 코어 8」로 갈린다, cloud-init 의 packages 에 certbot 이 있었는지가 01·03 과 04 사이에서 갈린다, 매니페스트 둘과 cloud-init 템플릿이 `source/` 에 반입되지 않았다). 재기 전에는 글감이 아니라 원본을 고친 뒤에 다시 판정한다. MERGE_INTO 42 가운데 일곱은 이미 쓰여 있는 기록으로 들어간다 — reference:verify-a-build-guide-in-execution-order 다섯, concept:what-a-qcow2-file-carries 하나, question:vm-configured-vs-current-memory 하나다. 이번 범위가 「기존 글감 64개를 건드리지 않는다」라 그 기록들은 손대지 않았고, 후보 대장의 target 이 다음 개정에서 무엇을 더해야 하는지를 가리킨다. 제6부의 그림은 아직 없다 — assetLedger 는 그대로다. ── 2026-09-16 · S2-H 제7·8·9부. SSOT 가 17,512행 아홉 부 338절이 된 뒤 새로 들어온 144개 절을 분해했다. 후보 90건을 더해 대장이 390건이고 글감 13개를 더해 91개다. 주제가 하나 늘어 일곱이다 — lab-entry-path-and-measurement-integrity(실험대의 진입 경로)는 lab-environment-build 가 묻는 「무엇이 새로 필요해지고 어디서 막히는가」와 다른 물음에 답한다: 요청이 지나는 길에 한 겹을 더하면 재려던 계약이 왜 깨지는가. 제9부 127절짜리 개념 사전에서 올린 글감은 넷뿐이다(qcow2 내부 · 클라우드 이미지 · L7 두 겹 · Docker 를 안 까는 결정과 VM 둘·DHCP 예약·터널 기각 셋). 나머지는 기존 기록으로 MERGE_INTO 하거나 KEEP_IN_SSOT 다 — §314~§330(Infinispan·JGroups·Prometheus)은 `keycloak-session-store` 가 측정으로 다루는 영역이라 §211 이 그은 경계(저쪽이 측정이고 이쪽이 정의)대로 KEEP_IN_SSOT 다.",
|
|
"topics": {
|
|
"cpu-virtualization": {
|
|
"topic": "cpu-virtualization",
|
|
"title": "CPU 가상화 — vCPU 가 물리 CPU 에서 실행되기까지",
|
|
"readerQuestion": "VM 에 준 vCPU 는 Host 의 어느 CPU 에서 언제 실행되고, Guest 안이 느릴 때 그것이 어느 계층의 문제인지 무엇을 보고 가르는가?",
|
|
"kinds": {
|
|
"case": [],
|
|
"concept": [
|
|
{
|
|
"title": "KVM 에서 vCPU 가 물리 CPU 에서 실행되기까지",
|
|
"kind": "concept",
|
|
"slug": "kvm-vcpu-to-physical-cpu",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#1-이-문서의-범위",
|
|
"final/document.md#2-전체-구조",
|
|
"final/document.md#3-각-구성요소의-역할",
|
|
"final/document.md#4-vcpu와-vcpu-thread",
|
|
"final/document.md#5-host-linux-scheduler와-실제-cpu",
|
|
"final/document.md#6-kvm_run과-guest-실행",
|
|
"final/document.md#7-vm-entry와-vm-exit",
|
|
"final/document.md#8-무엇이-실제로-vm-exit을-발생시키는가",
|
|
"final/document.md#9-vm-exit-이후-처리",
|
|
"final/document.md#10-guest가-idle이면-물리-cpu는-어떻게-되는가",
|
|
"final/document.md#11-vm의-4-vcpu는-정확히-무엇을-의미하는가",
|
|
"final/document.md#12-cpu-contention과-overcommit",
|
|
"final/document.md#13-steal-time",
|
|
"final/document.md#14-실제-linux에서-확인할-수-있는-것",
|
|
"final/document.md#15-cpu-가상화-관점에서-장애를-보는-방법",
|
|
"final/document.md#16-현재-keycloak-k3s-실험과의-관계",
|
|
"final/document.md#18-bare-metal-k3s와-vm-기반-k3s의-차이",
|
|
"final/document.md#19-이-ssot에서-파생될-concept",
|
|
"final/document.md#22-현재-단계의-핵심-claim",
|
|
"final/document.md#24-cpu-가상화-계층에서-발생할-수-있는-문제",
|
|
"final/document.md#25-cpu-문제를-계층별로-구분하는-진단표",
|
|
"final/document.md#26-현재-keycloak-실험에서-cpu-문제를-오판하지-않기-위한-기준",
|
|
"final/document.md#28-concept-->-open-question-->-case-적용-기준"
|
|
],
|
|
"basis-version": "x86_64 Intel VMX(VT-x) · Linux KVM(`kvm` · `kvm_intel`) · QEMU/libvirt · VM 안의 K3s cgroup CPU limit. SSOT 가 커널·QEMU·libvirt 버전을 한 번도 적지 않아 특정 버전에 고정하지 않는다 — 여기 적힌 것은 이 문서가 서술한 구성이고, 버전이 바뀌어 서술이 낡았는지는 §23 의 검증을 실제 Host 에서 돌릴 때 드러난다. AMD 는 `kvm_amd`/SVM 로 대응한다는 사실까지만 SSOT 에 있다.",
|
|
"classification": "§19 가 이 실행 경로를 하나의 CONCEPT 로 관리하고 여러 문서로 과도하게 분할하지 않는다고 적었다. virsh 에서 물리 CPU 까지가 한 인과 흐름이라 절 단위로 쪼개면 어느 조각도 혼자 서지 않는다 — vCPU thread 를 빼면 Host Scheduler 절이 무엇을 스케줄링하는지 말할 수 없고, VM Exit 을 빼면 Guest idle 이 왜 CPU 를 놓는지 말할 수 없다. §24 의 문제 열하나와 §25 의 진단표도 같은 경로 위의 자리들이라 이 글이 그 경로를 설명한 뒤라야 읽힌다. 근거는 문서가 서술한 메커니즘이고, 이 Host 에서 잰 값은 없다.",
|
|
"missing-verification": "이 테스트 Host 에서 잰 값이 하나도 없다. VM Exit 분포도, steal time 도, vCPU thread 가 실제로 logical CPU 사이를 옮겨 다니는지도 재지 않았다. §28 이 그은 경계대로 이 글은 「구조적으로 어떤 문제가 가능한가 · 어떤 지표로 의심하나 · 어느 계층에서 확인하나」까지만 말하고, 현재 서버에서 실제로 일어나는지는 사실로 확정하지 않는다. 재야 할 것은 §20·§27 의 OPEN QUESTION 열둘에 적혀 있고, 그 열둘은 이제 이 주제의 Question 글감 열둘이 되어 무엇을 재면 닫히는지를 각각 따로 적고 있다.",
|
|
"relations": [
|
|
"question:vcpu-thread-migration-without-pinning",
|
|
"question:vm-exit-distribution-by-workload",
|
|
"question:vcpu-contention-under-two-vm-load",
|
|
"question:throttling-vs-contention",
|
|
"question:is-production-on-a-hypervisor",
|
|
"question:host-numa-topology"
|
|
],
|
|
"ssot-assets": [
|
|
"vm-exit-handling-cycle"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/concept/concept-kvm-vcpu-to-physical-cpu.md",
|
|
"status": "초안",
|
|
"studioId": "5bcae89b-0873-4a2b-af4a-10e5234c085b",
|
|
"assets": [
|
|
"vm-exit-handling-cycle"
|
|
],
|
|
"assetFiles": [
|
|
"vm-exit-handling-cycle"
|
|
],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"reference": [],
|
|
"question": [
|
|
{
|
|
"title": "가상 머신 두 대에 동시에 부하를 주면 이 호스트에서 vCPU 경쟁이 실제로 생기는가",
|
|
"kind": "question",
|
|
"slug": "vcpu-contention-under-two-vm-load",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question-oq-1",
|
|
"final/document.md#12-cpu-contention과-overcommit",
|
|
"final/document.md#16-현재-keycloak-k3s-실험과의-관계"
|
|
],
|
|
"known": "§12 는 Host 12 logical CPU 에 8 vCPU VM 넷을 올린 예를 들어, 총 32 vCPU thread 가 12 logical CPU 위에서 Host CPU 시간을 두고 경쟁할 수 있고 그때 Guest application 이 느려진 원인이 application 이 아니라 Host CPU contention 일 수 있다고 적었다. §16 이 서술한 테스트 환경은 Physical Host 아래 Host Nginx 와 VM 두 대이고 각 VM 이 K3s 노드와 Keycloak 을 돌린다. §20 은 이 물음의 확인 대상으로 Host logical CPU 수 · 각 VM vCPU 수 · QEMU vCPU thread CPU 사용량 · Host run queue · Guest steal time 다섯을 적었다. 이 Host 의 logical CPU 수도 두 VM 의 vCPU 수도 SSOT 어디에도 적혀 있지 않다.",
|
|
"unknown": "이 Host 의 logical CPU 수와 두 VM 의 vCPU 합, 그리고 그 합이 logical CPU 를 넘는지. 넘든 넘지 않든 두 VM 에 동시에 부하를 걸었을 때 QEMU vCPU thread 사용량과 Host run queue 와 각 Guest 의 steal time 이 실제로 어떻게 움직이는지.",
|
|
"next-verification": "§20 의 확인 대상 다섯 가운데 정적인 둘 — Host logical CPU 수와 각 VM vCPU 수 — 을 먼저 적는다. 그 다음 VM 1 과 VM 2 에 동시에 CPU 부하를 걸고, 부하 전과 부하 중 두 시점에 Host 에서 `ps -eLo pid,tid,psr,pcpu,comm | grep qemu` (§14.7) 로 QEMU vCPU thread 의 CPU 사용량을, Host run queue 를, 각 Guest 에서 `top` (§14.8) 으로 `%st` 를 같은 시각에 기록한다.",
|
|
"decision-criterion": "두 VM 의 vCPU 합이 Host logical CPU 이하이고 동시 부하에서도 Guest `%st` 와 Host run queue 가 부하 전과 다르지 않으면, 이 Host 구성에서는 contention 이 관측되지 않는다고 적고 닫는다. `%st` 나 run queue 가 움직이면 그 부하 전후 표가 Case 가 되고 이 질문은 그 Case 가 닫는다. vCPU 수를 줄일지 VM 배치를 바꿀지는 그 Case 가 나온 뒤 Decision 으로 넘긴다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:steal-time-increase-under-two-cpu-bound-vms"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-vcpu-contention-under-two-vm-load.md",
|
|
"status": "초안",
|
|
"studioId": "eba4a877-276a-4050-9a97-d32614546c71",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "Keycloak 동시 refresh 실험 중 CPU 가상화 계층이 결과를 흔들 만큼 포화되는가",
|
|
"kind": "question",
|
|
"slug": "virtualization-layer-saturation-during-refresh",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question-oq-2",
|
|
"final/document.md#16-현재-keycloak-k3s-실험과의-관계",
|
|
"final/document.md#26-현재-keycloak-실험에서-cpu-문제를-오판하지-않기-위한-기준"
|
|
],
|
|
"known": "§16 은 Keycloak 실험 구조 아래에 KVM 계층이 하나 더 붙는다고 적고, 결과를 해석할 때 분리해야 할 원인으로 Keycloak refresh/session 동시성 · PostgreSQL contention/locking · Redis 상태 관리 · K3s resource scheduling · VM vCPU scheduling · Host CPU saturation · Nginx/LB 를 들었다. §26 은 같은 것을 Application/Auth · Storage · K3s · Guest · Virtualization · Host 여섯 경계로 다시 그렸다. §20 은 이 물음에서 Host CPU · Guest CPU · steal time · Keycloak latency · DB/Redis latency 를 동시에 관찰하라고 적었고 그 목적이 refresh 경쟁과 Host resource contention 을 분리하는 것이라고 밝혔다.",
|
|
"unknown": "refresh 경쟁 실험을 돌리는 동안 그 다섯 지표가 실제로 어떤 값을 갖는지, 그리고 가상화 계층 쪽 지표(Host CPU · steal time)가 실험 결과를 다르게 읽어야 할 만큼 움직이는지.",
|
|
"next-verification": "refresh 경쟁 실험을 돌리면서 §20 이 적은 다섯 지표를 같은 시각에 기록한다 — Guest 의 steal time 은 `top` (§14.8), Host 쪽 QEMU vCPU thread 는 `ps -eLo pid,tid,psr,pcpu,comm | grep qemu` (§14.7), Keycloak 과 DB/Redis latency 는 실험이 이미 재는 값을 그대로 쓴다. 실험을 걸기 전 같은 다섯을 한 번 찍어 기준값으로 둔다.",
|
|
"decision-criterion": "실험 구간의 Host CPU 와 steal time 이 기준값과 다르지 않으면, 그 실험 결과를 application/storage 문제로 읽어도 된다고 적고 닫는다. 둘 중 하나라도 움직이면 §26 의 여섯 경계를 분리해 다시 재고, refresh 경쟁 실험을 CPU 여유 상태에서 따로 돌릴지는 Decision 으로 넘긴다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:vcpu-contention-under-two-vm-load",
|
|
"question:throttling-vs-contention"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-virtualization-layer-saturation-during-refresh.md",
|
|
"status": "초안",
|
|
"studioId": "da3a7b56-5691-4a9f-90c5-3a1a5d0f0013",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "게스트가 유휴 상태일 때 vCPU 스레드는 이 테스트 환경에서 어떻게 보이는가",
|
|
"kind": "question",
|
|
"slug": "idle-vcpu-thread-appearance",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question-oq-3",
|
|
"final/document.md#10-guest가-idle이면-물리-cpu는-어떻게-되는가",
|
|
"final/document.md#14-실제-linux에서-확인할-수-있는-것-14-6"
|
|
],
|
|
"known": "§10 은 Guest 가 idle 이면 vCPU thread 가 block/sleep 상태로 들어가고 Host 는 그 CPU 시간을 다른 workload 에 쓸 수 있다고 적었다. §14.6 은 `ps -T -p <QEMU_PID>` 와 `top -H -p <QEMU_PID>` 로 vCPU 관련 thread 를 Host 에서 관찰할 수 있다고 하면서, 환경과 QEMU 버전에 따라 thread 이름이 다를 수 있다는 단서를 함께 달았다. §20 은 Guest idle 상태와 CPU workload 상태를 비교하라고 적고 확인 명령으로 `top -H -p <QEMU_PID>` 와 `ps -eLo pid,tid,psr,pcpu,stat,comm` 둘을 들었다.",
|
|
"unknown": "이 환경에서 QEMU 의 vCPU thread 가 어떤 이름으로 보이는지, idle 과 부하 상태에서 그 thread 의 `pcpu` 와 `stat` 가 실제로 어떻게 갈리는지.",
|
|
"next-verification": "VM 을 idle 로 둔 채 `top -H -p <QEMU_PID>` 와 `ps -eLo pid,tid,psr,pcpu,stat,comm` 을 찍는다. 같은 VM 에 CPU workload 를 걸고 같은 두 명령을 다시 찍어 두 출력을 나란히 둔다 (§20 이 적은 확인 명령 그대로).",
|
|
"decision-criterion": "idle 에서 vCPU thread 의 `pcpu` 가 0 에 가깝고 `stat` 가 sleep 계열이며 부하에서 실행 상태로 바뀌면, §10 의 서술이 이 환경에서도 성립하는 것으로 개념의 확인 사례에 흡수하고 닫는다. 두 상태에서 출력이 갈리지 않거나 §10 과 반대로 나오면 그 출력이 Case 가 된다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:vcpu-thread-migration-without-pinning"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-idle-vcpu-thread-appearance.md",
|
|
"status": "초안",
|
|
"studioId": "4a2d3332-81c3-44fa-a4fb-216a41abb504",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "실제 작업에서 주로 발생하는 VM Exit 은 무엇인가",
|
|
"kind": "question",
|
|
"slug": "vm-exit-distribution-by-workload",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question-oq-4",
|
|
"final/document.md#8-무엇이-실제로-vm-exit을-발생시키는가",
|
|
"final/document.md#14-실제-linux에서-확인할-수-있는-것-14-9"
|
|
],
|
|
"known": "§8 은 VM Exit 을 일으키는 동작으로 HLT · I/O Port 접근 · CPUID · Control Register 접근 · MSR 접근 · Exception · External Interrupt 를 들고, 무엇이 Exit 을 일으킬지는 root 권한이 아니라 VMCS 의 VM-Execution Control 설정과 동작 종류가 정한다고 적었다. §14.9 는 `sudo perf kvm stat live` 를 예로 들면서 지원되는 명령과 표시되는 Exit reason 이 kernel · perf 버전 · CPU architecture 및 설정에 따라 다를 수 있으니 `perf kvm --help` 를 함께 확인하고 필요하면 KVM tracepoint tracing 도 검토하라고 적었다. §20 의 비교 후보는 idle · CPU-bound workload · I/O-heavy workload · Keycloak 정상 요청 · Keycloak 부하 테스트 다섯이다.",
|
|
"unknown": "이 Host 에서 `perf kvm` 또는 KVM tracepoint 가 실제로 열리는지, 열린다면 §20 이 든 다섯 workload 의 Exit reason 분포가 서로 얼마나 다른지.",
|
|
"next-verification": "먼저 `perf kvm --help` 로 이 환경이 지원하는 하위 명령을 확인한다. 열리면 `sudo perf kvm stat live` 로 §20 의 비교 후보 다섯을 각각 돌리며 Exit reason 분포를 받아 적는다. 열리지 않으면 §14.9 가 말한 KVM tracepoint tracing 을 검토하고 그 결과도 기록한다.",
|
|
"decision-criterion": "다섯 workload 의 Exit reason 분포를 한 표로 놓을 수 있으면 그 표가 Case 가 되고 닫는다. `perf kvm` 도 tracepoint 도 이 환경에서 열리지 않으면, 「이 Host 에서는 Exit 분포를 관측할 수 없다」를 그대로 적고 §24.9 의 확인 항목에서 Exit reason 을 빼는 근거로 남긴 뒤 닫는다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:exit-distribution-for-keycloak-workload"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-vm-exit-distribution-by-workload.md",
|
|
"status": "초안",
|
|
"studioId": "61b7486f-5dab-41df-8f4f-e8fc21d88617",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "pinning 없이 vCPU 스레드는 호스트 논리 CPU 사이를 옮겨 다니는가",
|
|
"kind": "question",
|
|
"slug": "vcpu-thread-migration-without-pinning",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question-oq-5",
|
|
"final/document.md#5-host-linux-scheduler와-실제-cpu",
|
|
"final/document.md#14-실제-linux에서-확인할-수-있는-것-14-7"
|
|
],
|
|
"known": "§5 는 vCPU thread 도 다른 Host thread 와 같이 Linux Scheduler 가 실행할 logical CPU 를 정한다고 적었다. §14.7 은 `ps -eLo pid,tid,psr,pcpu,comm | grep qemu` 의 `PSR` 로 thread 가 최근 실행된 logical CPU 를 관찰할 수 있고, 이것이 vCPU 가 물리 CPU 에 영구 고정되어 있다는 뜻은 아니며 pinning 을 하지 않았다면 스케줄링에 따라 달라질 수 있다고 적었다. §20 은 `PSR` 과 scheduler tracing 으로 관찰하라고만 적었다.",
|
|
"unknown": "이 Host 에서 pinning 없이 같은 vCPU thread 의 `PSR` 이 시간에 따라 실제로 바뀌는지, 바뀐다면 부하가 있을 때와 없을 때가 다른지. 현재 두 VM 에 affinity 가 걸려 있는지도 SSOT 에 적혀 있지 않다.",
|
|
"next-verification": "`ps -eLo pid,tid,psr,pcpu,comm | grep qemu` 를 일정 간격으로 여러 번 찍어 같은 tid 의 `PSR` 이 바뀌는지 본다. Guest 가 idle 일 때와 CPU 부하가 있을 때를 나눠 각각 찍고, 같은 실행에서 그 VM 에 affinity 설정이 있는지도 함께 적는다 (§14.7 · §20).",
|
|
"decision-criterion": "관측 구간에서 같은 tid 의 `PSR` 이 바뀌면 이동한다는 사실을 개념의 확인 사례로 흡수하고 닫는다 — 그 이동이 성능에 영향을 주는지는 OQ-10 이 받는다. `PSR` 이 고정으로 나오면 affinity 설정을 먼저 확인하고, 설정이 없는데도 고정이면 그 관측이 Case 가 된다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:pinning-before-and-after",
|
|
"question:idle-vcpu-thread-appearance"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-vcpu-thread-migration-without-pinning.md",
|
|
"status": "초안",
|
|
"studioId": "cd58d35d-3a93-4682-82b8-b64ce3a8813c",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "운영 서버는 CPU 가상화 계층의 영향을 받는 구조인가",
|
|
"kind": "question",
|
|
"slug": "is-production-on-a-hypervisor",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question-oq-6",
|
|
"final/document.md#18-bare-metal-k3s와-vm-기반-k3s의-차이",
|
|
"final/document.md#25-cpu-문제를-계층별로-구분하는-진단표"
|
|
],
|
|
"known": "§18 은 bare-metal K3s 가 Host scheduler 로 바로 가는 것과 달리 VM 기반 K3s 에는 vCPU · QEMU thread · KVM/VMX 계층이 더 붙는다고 적었다. §20 은 두 경로를 `K3s -> Host Scheduler -> Physical CPU` 와 `K3s -> Guest -> vCPU -> Hypervisor -> Physical CPU` 로 나란히 적고, 구조에 따라 진단 지표가 달라진다고 밝혔다. §25 의 진단표에는 Virtualization·Host 계층에만 있는 행이 따로 있다. 운영 서버가 둘 중 어느 쪽인지는 SSOT 에 적혀 있지 않다.",
|
|
"unknown": "운영 서버가 bare-metal Host 에 직접 K3s 를 설치한 것인지, 상위 Hypervisor 나 Cloud VM 위에 있는지.",
|
|
"next-verification": "운영 서버의 구성 기록이나 도입 경로로 bare-metal 인지 Hypervisor/Cloud VM 위인지 확인한다. 서버에 붙을 수 있으면 §14.1~§14.3 의 `grep -E 'vmx|svm' /proc/cpuinfo` · `lsmod | grep kvm` · `ls -l /dev/kvm` 으로 그 장비가 VM 을 직접 돌리는 Host 인지부터 적고, Guest 안에서 `top` 의 `%st` (§14.8) 가 관측되는지를 함께 본다.",
|
|
"decision-criterion": "운영 서버가 bare-metal 로 확인되면 §25 의 Virtualization·Host 행을 운영 진단에서 빼도 된다고 적고 닫는다. Hypervisor 나 Cloud VM 위로 확인되면 그 사실이 운영 진단의 전제가 되고, §25 의 어느 행을 운영에서 쓸지는 그 뒤 Decision 이 정한다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-is-production-on-a-hypervisor.md",
|
|
"status": "초안",
|
|
"studioId": "2acec7d5-3eaf-4115-9d61-45647c4ec01e",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "가상 머신 두 대를 CPU-bound 로 만들면 게스트 steal time 은 얼마나 오르는가",
|
|
"kind": "question",
|
|
"slug": "steal-time-increase-under-two-cpu-bound-vms",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question-oq-7",
|
|
"final/document.md#13-steal-time",
|
|
"final/document.md#24-cpu-가상화-계층에서-발생할-수-있는-문제-24-4"
|
|
],
|
|
"known": "§13 은 steal time 이 Guest vCPU 에 실행할 작업이 있는데 다른 workload 때문에 즉시 실행되지 못하는 상황을 가리키는 단서이며, 그것 하나만으로 원인을 확정하지 말고 Host CPU saturation · run queue · affinity · workload 를 함께 확인해야 한다고 적었다. §24.4 는 steal time 증가를 Guest 에서 관측되는 문제로 놓고 우선 확인할 것을 `%st` 와 Host contention 으로 적었다. §27 은 이 물음에서 Host CPU utilization · run queue · QEMU vCPU thread · 각 Guest 의 `%st` 를 함께 측정하라고 적었다. 이 Host 에서 `%st` 를 실제로 잰 값은 없다.",
|
|
"unknown": "두 VM 을 동시에 CPU-bound 로 만들었을 때 각 Guest 의 `%st` 가 부하 전 대비 얼마나 오르는지, 그 증가가 Host run queue 와 같은 방향으로 움직이는지.",
|
|
"next-verification": "부하 전에 Host CPU utilization 과 run queue, `ps -eLo pid,tid,psr,pcpu,comm | grep qemu` (§14.7) 의 QEMU vCPU thread 사용량, 각 Guest 의 `top` (§14.8) `%st` 를 기준값으로 남긴다. 그 다음 VM 두 대를 동시에 CPU-bound 로 만들고 같은 네 항목을 같은 시각에 다시 기록한다 (§27 이 적은 측정 항목 그대로).",
|
|
"decision-criterion": "부하 전후의 `%st` 와 Host run queue 를 한 표로 놓을 수 있으면 그 표가 Case 가 되고 닫는다. `%st` 가 부하 전과 다르지 않으면 이 Host 구성에서는 두 VM 동시 부하가 steal time 으로 나타나는 경쟁을 만들지 않는다고 적고 OQ-1 과 함께 닫는다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:vcpu-contention-under-two-vm-load",
|
|
"question:throughput-vs-vcpu-count"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-steal-time-increase-under-two-cpu-bound-vms.md",
|
|
"status": "초안",
|
|
"studioId": "2d058fb5-01f1-4be8-80bb-0e377868573c",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "vCPU 를 늘릴수록 이 호스트에서 Keycloak 처리량도 계속 오르는가",
|
|
"kind": "question",
|
|
"slug": "throughput-vs-vcpu-count",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question-oq-8",
|
|
"final/document.md#11-vm의-4-vcpu는-정확히-무엇을-의미하는가",
|
|
"final/document.md#24-cpu-가상화-계층에서-발생할-수-있는-문제-24-6"
|
|
],
|
|
"known": "§11 은 4 vCPU 가 실행 컨텍스트 4개이지 물리 CPU 4개를 영구히 예약한 것이 아니라고 적었다. §24.6 은 vCPU 를 많이 준다고 항상 빨라지는 것은 아니며, Guest workload 가 그만큼의 병렬성을 쓰지 못하거나 Host 전체 CPU 에 비해 지나치게 많은 vCPU 를 주면 scheduling 대상만 늘 수 있으니 실제 workload 의 병렬성과 Host capacity 를 함께 확인해야 한다고 적었다. §27 은 비교 구성으로 2 vCPU / 4 vCPU / 8 vCPU 를 들었다.",
|
|
"unknown": "이 Host 에서 vCPU 를 2 → 4 → 8 로 올렸을 때 Keycloak 처리량과 latency 가 어디서부터 더 좋아지지 않는지. 좋아지지 않는다면 그것이 workload 의 병렬성 부족 때문인지 Host capacity 부족 때문인지.",
|
|
"next-verification": "VM 구성을 2 vCPU · 4 vCPU · 8 vCPU 로 바꿔 가며 같은 Keycloak 부하를 걸고 처리량과 latency 를 각각 기록한다. 세 실행 모두에서 Host logical CPU 수 대비 총 vCPU 와 Guest 의 `top` `%st` (§14.8) 를 함께 남겨, §24.6 이 말한 병렬성 부족과 Host capacity 부족을 갈라 둔다 (§27 이 든 비교 구성 그대로).",
|
|
"decision-criterion": "처리량이 더 오르지 않기 시작하는 vCPU 수가 나오면 그 지점을 이 workload 의 상한으로 적고 그 세 실행이 Case 가 된다. 세 구성이 모두 같은 방향으로 오르면 이 Host 에서는 상한에 닿지 않았다고 적고 닫는다. 어느 vCPU 수로 운영할지는 그 뒤 Decision 이 정한다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:steal-time-increase-under-two-cpu-bound-vms"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-throughput-vs-vcpu-count.md",
|
|
"status": "초안",
|
|
"studioId": "723d8930-9e82-4552-8376-038a6446ec71",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "cgroup CPU 제한과 호스트 vCPU 경쟁을 지표로 가를 수 있는가",
|
|
"kind": "question",
|
|
"slug": "throttling-vs-contention",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question-oq-9",
|
|
"final/document.md#24-cpu-가상화-계층에서-발생할-수-있는-문제-24-8",
|
|
"final/document.md#25-cpu-문제를-계층별로-구분하는-진단표"
|
|
],
|
|
"known": "§24.8 은 Host CPU 에 여유가 있어도 container cgroup 의 CPU limit 때문에 Keycloak Pod 실행이 제한될 수 있다고 적고, 「Host CPU 를 받지 못함」과 「Pod 가 자신의 CPU quota 를 초과함」을 구분해야 한다고 밝혔다. throttling 자체는 KVM 문제가 아니지만 VM 안에서 K3s 를 운영하는 지금 실험에서는 같은 application latency 로 관찰될 수 있어 진단 경계에 포함한다고도 적었다. §25 의 진단표는 CPU throttling 의 우선 확인 항목을 CPU limit · throttled time 으로, CPU contention 은 Host CPU · run queue · per-CPU usage 로 갈라 적었다. §27 은 둘을 의도적으로 재현해 Guest/Host/K3s 지표 차이를 비교하라고 적었다.",
|
|
"unknown": "같은 크기의 application latency 증가를 두 원인으로 각각 재현했을 때 §25 가 갈라 놓은 지표들이 실제로 갈리는지, 갈린다면 어느 지표가 가장 먼저 갈리는지.",
|
|
"next-verification": "같은 크기의 Keycloak latency 증가를 두 번 만든다 — 한 번은 Keycloak Pod 의 cgroup CPU limit 을 낮춰서, 한 번은 다른 VM 에 CPU 부하를 걸어서. 두 실행에서 Pod 의 throttled time 과 CPU limit, Guest CPU 와 `top` `%st` (§14.8), Host CPU 와 run queue 를 같은 항목으로 받아 적고 나란히 놓는다 (§27 · §25 의 두 행).",
|
|
"decision-criterion": "두 재현에서 지표 조합이 갈리면 그 대조표가 Case 가 되고, §25 의 두 행을 어떤 순서로 보는지를 정하는 근거가 된다. 갈리지 않으면 「이 지표들로는 두 원인이 구분되지 않는다」를 적고, 무엇을 더 봐야 하는지를 새 Question 으로 낸 뒤 이 질문은 닫는다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:virtualization-layer-saturation-during-refresh"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-throttling-vs-contention.md",
|
|
"status": "초안",
|
|
"studioId": "116b806c-b176-401c-9363-1489fffb721c",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "CPU pinning 전후로 Keycloak 지연과 vCPU 스케줄링 변동이 달라지는가",
|
|
"kind": "question",
|
|
"slug": "pinning-before-and-after",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question-oq-10",
|
|
"final/document.md#24-cpu-가상화-계층에서-발생할-수-있는-문제-24-7",
|
|
"final/document.md#14-실제-linux에서-확인할-수-있는-것-14-7"
|
|
],
|
|
"known": "§24.7 은 CPU pinning 이 특정 vCPU thread 를 특정 Host logical CPU 에 제한하는 것이고, 적절히 쓰면 scheduling 변동을 줄일 수 있지만 잘못 설정하면 특정 CPU 에 workload 가 집중될 수 있다고 적었다. 그래서 pinning 여부만 보지 말고 실제 per-CPU utilization 과 affinity 를 함께 확인해야 한다고 밝혔다. §14.7 은 `PSR` 이 thread 가 최근 실행된 logical CPU 를 보여 주고 pinning 을 하지 않았다면 스케줄링에 따라 달라질 수 있다고 적었다. §27 은 pinning 이 현재 workload 에서 실제 이점을 주는지는 실험으로 확인한다고 적었다.",
|
|
"unknown": "pinning 전후로 Keycloak latency 가 달라지는지, `PSR` 변동이 실제로 줄어드는지, 줄어드는 대신 §24.7 이 말한 대로 특정 CPU 로 workload 가 몰리는지.",
|
|
"next-verification": "pinning 없이 Keycloak 부하를 걸어 latency 와 `ps -eLo pid,tid,psr,pcpu,comm | grep qemu` (§14.7) 의 `PSR` 변동과 per-CPU 사용량을 남긴다. vCPU thread 를 고정한 뒤 같은 부하를 다시 걸어 같은 세 가지를 남기고 두 실행을 나란히 놓는다 (§27 · §24.7).",
|
|
"decision-criterion": "pinning 뒤 latency 가 좋아지고 per-CPU 사용량이 특정 CPU 로 몰리지 않으면, pinning 을 쓸지 말지를 Decision 으로 넘긴다 — 그 Decision 이 감수할 비용은 §24.7 이 적은 「잘못 설정하면 특정 CPU 에 workload 가 집중된다」이다. 두 실행에서 latency 차이가 없으면 이 workload 에서는 pinning 이 이점을 주지 않는다고 적고 닫는다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:vcpu-thread-migration-without-pinning"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-pinning-before-and-after.md",
|
|
"status": "초안",
|
|
"studioId": "62319064-17ef-4b2f-bf8b-e363cd0e36a6",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "Keycloak 작업의 VM Exit 분포는 idle · CPU-bound · I/O-bound 와 어떻게 다른가",
|
|
"kind": "question",
|
|
"slug": "exit-distribution-for-keycloak-workload",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question-oq-11",
|
|
"final/document.md#24-cpu-가상화-계층에서-발생할-수-있는-문제-24-9",
|
|
"final/document.md#14-실제-linux에서-확인할-수-있는-것-14-9"
|
|
],
|
|
"known": "§24.9 는 VM Exit 이 정상적인 가상화 동작이라 존재한다는 것 자체는 문제가 아니고, 특정 workload 에서 Exit 이 지나치게 빈번하고 처리 비용이 커질 때 성능에 영향을 줄 수 있다고 적었다. 볼 때는 Exit 횟수만이 아니라 Exit reason · workload 종류 · 처리 위치가 KVM 인지 QEMU userspace 인지 · application latency 와 Exit 증가가 함께 나타나는지를 같이 보라고 적었다. §14.9 는 지원되는 명령과 표시되는 Exit reason 이 kernel · perf 버전 · CPU architecture 및 설정에 따라 다를 수 있다고 단서를 달았다. §27 은 가능하면 `perf kvm` 또는 KVM tracepoint 로 Exit reason 분포를 비교하라고 적었다.",
|
|
"unknown": "Keycloak workload 구간의 Exit reason 분포가 idle · CPU-bound · I/O-bound 구간과 얼마나 다른지, 그 차이가 같은 구간의 Keycloak latency 와 같이 움직이는지.",
|
|
"next-verification": "OQ-4 에서 `perf kvm` 또는 KVM tracepoint 가 이 환경에서 열린다는 것을 먼저 확인한다. 열리면 idle · CPU-bound · I/O-bound · Keycloak 정상 요청 · Keycloak 부하 다섯 구간의 Exit reason 분포를 받고, 같은 구간의 Keycloak latency 를 나란히 적는다 (§27 · §24.9 가 함께 보라고 한 네 항목).",
|
|
"decision-criterion": "Keycloak 구간의 Exit 분포가 다른 구간과 다르고 그 차이가 latency 와 같이 움직이면 그 측정이 Case 가 되고 닫는다. 분포가 다르지 않거나 latency 와 따로 움직이면, 이 workload 에서 Exit overhead 는 조사 대상이 아니라고 적고 §25 의 「과도한 VM Exit」 행을 뒤로 미루는 근거로 남긴 뒤 닫는다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:vm-exit-distribution-by-workload"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-exit-distribution-for-keycloak-workload.md",
|
|
"status": "초안",
|
|
"studioId": "d103bb81-45df-402f-86e5-e41d66ed7f8d",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "이 호스트의 NUMA 토폴로지는 가상 머신 성능을 고려해야 할 구조인가",
|
|
"kind": "question",
|
|
"slug": "host-numa-topology",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question-oq-12",
|
|
"final/document.md#24-cpu-가상화-계층에서-발생할-수-있는-문제-24-11"
|
|
],
|
|
"known": "§24.11 은 멀티소켓 또는 NUMA 구조 Host 에서 vCPU 가 실행되는 NUMA node 와 VM memory 가 위치한 node 의 관계가 성능에 영향을 줄 수 있다고 적고, vCPU 가 Node 0 에서 실행되는데 필요한 memory 가 주로 Node 1 에 배치되어 있으면 remote memory access 가 발생할 수 있다는 예를 들었다. 같은 절이 이 문서에서는 문제의 존재와 CPU affinity 와의 관계까지만 기록하고 상세한 memory placement 와 NUMA tuning 은 메모리 가상화 CONCEPT 에서 다룬다고 범위를 그었다. §27 은 이 물음의 종료 기준을 스스로 적었다 — 단일 NUMA node 면 현재 실험에서 우선순위를 낮추고, 다중 NUMA node 면 vCPU/memory placement 를 별도 CASE 후보로 올린다.",
|
|
"unknown": "이 Host 가 단일 NUMA node 인지 다중 NUMA node 인지, 다중이라면 두 VM 의 vCPU 와 memory 가 어느 node 에 배치되어 있는지.",
|
|
"next-verification": "Host 의 NUMA topology 를 확인해 node 수를 적는다. 다중으로 나오면 §24.11 이 말한 vCPU 가 실행되는 node 와 VM memory 가 배치된 node 의 관계까지 같이 적는다 (§27 OQ-12). SSOT 는 이 확인에 쓸 명령을 적지 않았으므로 쓰는 명령과 그 출력을 함께 증거로 남긴다.",
|
|
"decision-criterion": "단일 NUMA node 로 나오면 §27 이 적은 대로 현재 실험에서 우선순위를 낮추고 닫는다. 다중 NUMA node 로 나오면 vCPU/memory placement 를 별도 CASE 후보로 올리고, 이 프로젝트가 메모리 가상화 SSOT 를 가질 때 §24.11 과 함께 그쪽으로 넘긴다.",
|
|
"relations": [
|
|
"concept:kvm-vcpu-to-physical-cpu"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "cpu-virtualization/question/question-host-numa-topology.md",
|
|
"status": "초안",
|
|
"studioId": "688c6c02-c1bd-4cd2-a615-2396db145386",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"decision": []
|
|
}
|
|
},
|
|
"memory-virtualization": {
|
|
"topic": "memory-virtualization",
|
|
"title": "메모리 가상화 — VM 에 준 RAM 이 실제로 있는 곳",
|
|
"readerQuestion": "VM 에 준 RAM 은 실제로 어디에 어떻게 있고, Guest 가 느리거나 죽을 때 그것이 어느 계층의 메모리 문제인지 무엇을 보고 가르는가?",
|
|
"kinds": {
|
|
"case": [],
|
|
"concept": [
|
|
{
|
|
"title": "Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA",
|
|
"kind": "concept",
|
|
"slug": "guest-memory-address-translation",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#29-이-문서에서-먼저-고정할-전체-구조",
|
|
"final/document.md#30-일반-linux의-virtual-memory부터-시작한다",
|
|
"final/document.md#31-page와-physical-frame",
|
|
"final/document.md#32-virtual-address-=-page-+-offset",
|
|
"final/document.md#33-guest-page-table",
|
|
"final/document.md#34-mmu-실제-주소-변환을-수행하는-cpu-하드웨어",
|
|
"final/document.md#35-tlb-주소-변환-결과의-cpu-cache",
|
|
"final/document.md#36-bare-metal과-vm의-차이",
|
|
"final/document.md#37-ept-extended-page-tables",
|
|
"final/document.md#38-왜-ept가-필요한가",
|
|
"final/document.md#39-shadow-page-table과-ept의-의미",
|
|
"final/document.md#40-qemu는-guest-ram을-어떻게-준비하는가",
|
|
"final/document.md#41-kvm_set_user_memory_region",
|
|
"final/document.md#42-configured-memory와-실제-physical-ram-사용량은-같지-않을-수-있다",
|
|
"final/document.md#43-guest-page-table-자체도-메모리에-있다",
|
|
"final/document.md#44-정상-memory-access는-매번-vm-exit하지-않는다",
|
|
"final/document.md#79-전체-memory-virtualization-실행-경로",
|
|
"final/document.md#80-전체-memory-virtualization-관리-경로",
|
|
"final/document.md#82-핵심-claim-registry",
|
|
"final/document.md#87-최종-기준-그림",
|
|
"final/document.md#88-결론"
|
|
],
|
|
"basis-version": "x86-64 Intel EPT(second-level address translation) · AMD 는 NPT 계열로 대응 · Linux KVM 의 `KVM_SET_USER_MEMORY_REGION` · QEMU/libvirt · 기본 page 4 KiB. SSOT 가 커널·QEMU·libvirt 버전을 한 번도 적지 않아 특정 버전에 고정하지 않는다 — 여기 적힌 것은 이 문서가 서술한 구성이고, 버전이 바뀌어 서술이 낡았는지는 §83 의 확인을 실제 Host 에서 돌릴 때 드러난다.",
|
|
"classification": "§29 가 이 부에서 먼저 고정할 구조로 GVA → GPA → HPA 를 놓았고 §87 이 같은 그림을 최종 기준으로 다시 그렸다. 이 경로는 제2부의 다른 다섯 글이 서는 바닥이다 — fault 가 어느 계층에서 났는지도, huge page 가 두 변환 단계 중 어디에 걸리는지도, NUMA 배치가 무엇을 옮기는지도 이 경로를 먼저 그려야 말이 된다. 실행 경로(§79)와 관리 경로(§80)를 갈라 놓는 것도 이 글의 일이다 — QEMU 가 backing 을 마련하고 KVM 이 region 을 등록하지만 매 접근을 처리하는 것은 CPU MMU 라는 것이 §41 과 §44 의 결론이다. 근거는 문서가 서술한 메커니즘이고 이 Host 에서 잰 값은 없다.",
|
|
"missing-verification": "이 테스트 Host 에서 잰 값이 하나도 없다. SSOT 제2부는 개념 서술이고 §83 이 OQ-1~OQ-14 로 아직 돌리지 않은 확인 목록을 적어 두었다. §88 이 그은 경계대로 이 글은 구조가 어떻게 동작하는지까지만 말하고 이 서버에서 실제로 어떤 값이 나오는지는 사실로 확정하지 않는다. 이 글에 대해서는 §83 OQ-2 와 OQ-3 이 재는 것 — VM 의 configured/current memory 와 QEMU process 의 Host resident memory — 이 §42 가 말한 세 값의 차이를 이 Host 에서 확정한다.",
|
|
"relations": [
|
|
"concept:page-fault-layers-in-a-vm",
|
|
"concept:huge-pages-in-a-vm",
|
|
"concept:memory-pressure-reclaim-swap-oom",
|
|
"concept:numa-locality-for-vcpu-and-memory",
|
|
"question:vm-configured-vs-current-memory",
|
|
"question:qemu-resident-memory-distribution",
|
|
"reference:memory-symptom-needs-layer-separation",
|
|
"concept:kvm-vcpu-to-physical-cpu"
|
|
],
|
|
"ssot-assets": [
|
|
"guest-memory-address-translation-path"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/concept/concept-guest-memory-address-translation.md",
|
|
"status": "초안",
|
|
"studioId": "2f2ffafc-91ff-4971-a6a8-7c0e7d844ca2",
|
|
"assets": [
|
|
"guest-memory-address-translation-path"
|
|
],
|
|
"assetFiles": [
|
|
"guest-memory-address-translation-path"
|
|
],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "VM 에서 page fault 는 세 계층에서 따로 일어난다",
|
|
"kind": "concept",
|
|
"slug": "page-fault-layers-in-a-vm",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#35-tlb-주소-변환-결과의-cpu-cache",
|
|
"final/document.md#45-guest-page-fault",
|
|
"final/document.md#46-page-fault의-대표적인-원인",
|
|
"final/document.md#47-ept-violation",
|
|
"final/document.md#48-guest-page-fault와-ept-violation-비교",
|
|
"final/document.md#49-host-page-fault도-별도로-존재한다",
|
|
"final/document.md#82-핵심-claim-registry",
|
|
"final/document.md#86-문제를-진단할-때의-분류"
|
|
],
|
|
"basis-version": "x86-64 Intel EPT 기준의 second-stage translation · Linux Guest/Host kernel 의 page fault 처리 · AMD 는 NPT 계열로 대응. SSOT 가 커널 버전을 적지 않아 특정 버전에 고정하지 않는다.",
|
|
"classification": "§48 의 비교표와 §49 의 세 갈래(Guest Page Fault · Host Page Fault · EPT 관련 사건)는 제2부에서 가장 여러 번 다시 쓰이는 구분이다 — §86 의 진단 분류가 그 위에 서 있고, §83 의 OQ-13 과 OQ-14 가 각각 Guest 쪽과 Host 쪽 fault 를 재라고 말한다. 정상 경로를 그리는 글과 갈라 둔 이유는 물음이 다르기 때문이다 — 그쪽은 정상일 때 무엇이 일어나는가를, 이쪽은 접근이 완료되지 않을 때 어느 계층이 그것을 받는가를 말한다. §35 가 TLB Miss 와 Page Fault 를 먼저 갈라 놓은 것도 여기 들어간다.",
|
|
"missing-verification": "이 테스트 Host 에서 잰 값이 하나도 없다. SSOT 제2부는 개념 서술이고 §83 이 OQ-1~OQ-14 로 아직 돌리지 않은 확인 목록을 적어 두었다. §88 이 그은 경계대로 이 글은 구조가 어떻게 동작하는지까지만 말하고 이 서버에서 실제로 어떤 값이 나오는지는 사실로 확정하지 않는다. Guest 쪽 fault 지표도 Host 쪽 major fault 도 재지 않았다. §46 의 다섯 원인 중 무엇이 이 환경에서 실제로 일어나는지는 OQ-13 이, Host 쪽과 storage latency 의 상관은 OQ-14 가 받는다.",
|
|
"relations": [
|
|
"concept:guest-memory-address-translation",
|
|
"concept:memory-pressure-reclaim-swap-oom",
|
|
"question:guest-page-fault-vs-workload",
|
|
"question:host-major-fault-vs-storage-latency",
|
|
"reference:memory-symptom-needs-layer-separation"
|
|
],
|
|
"ssot-assets": [
|
|
"page-fault-layers-in-a-vm"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/concept/concept-page-fault-layers-in-a-vm.md",
|
|
"status": "초안",
|
|
"studioId": "e80853fa-98b8-4cc3-a2df-427c7147794b",
|
|
"assets": [
|
|
"page-fault-layers-in-a-vm"
|
|
],
|
|
"assetFiles": [
|
|
"page-fault-layers-in-a-vm"
|
|
],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "Huge Page 를 VM 에서 볼 때 갈라지는 세 자리",
|
|
"kind": "concept",
|
|
"slug": "huge-pages-in-a-vm",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#50-huge-page가-필요한-이유",
|
|
"final/document.md#51-huge-page와-tlb-coverage",
|
|
"final/document.md#52-vm에서-huge-page를-볼-때-주의할-점",
|
|
"final/document.md#53-thp-transparent-huge-pages",
|
|
"final/document.md#54-thp의-trade-off",
|
|
"final/document.md#55-hugetlb",
|
|
"final/document.md#56-thp와-hugetlb-비교",
|
|
"final/document.md#82-핵심-claim-registry"
|
|
],
|
|
"basis-version": "Linux THP(`/sys/kernel/mm/transparent_hugepage/enabled`) · HugeTLB pool · x86-64 의 4 KiB / 2 MiB / 1 GiB page. §53 이 THP 정책은 kernel·distribution·Host 설정에 따라 다르므로 실제 시스템에서 확인하라고 적었고 SSOT 는 그 버전을 적지 않았다. §51 의 예시 숫자는 특정 CPU 의 실제 TLB 용량이 아니라는 단서가 원문에 붙어 있다.",
|
|
"classification": "§52 가 세운 세 자리 — Guest page table 단계 · Host backing · EPT 매핑 — 가 이 글이 존재하는 이유다. 「Huge Page 를 쓴다」는 말 하나로는 셋 중 어디를 말하는지 정해지지 않고, 그 구분이 없으면 OQ-4 가 읽은 THP 정책 값도 OQ-5 가 본 backing 설정도 무엇에 대한 값인지 말할 수 없다. THP 와 HugeTLB 의 비교(§56)와 THP 의 감수 비용(§54)도 같은 물음 아래 있어 함께 둔다.",
|
|
"missing-verification": "이 테스트 Host 에서 잰 값이 하나도 없다. SSOT 제2부는 개념 서술이고 §83 이 OQ-1~OQ-14 로 아직 돌리지 않은 확인 목록을 적어 두었다. §88 이 그은 경계대로 이 글은 구조가 어떻게 동작하는지까지만 말하고 이 서버에서 실제로 어떤 값이 나오는지는 사실로 확정하지 않는다. 이 Host 의 THP 정책도 HugePages_Total 도 VM 의 memory backing 설정도 읽지 않았다. §54 가 말한 compaction latency 영향은 측정이 필요하다고 원문이 직접 적었다.",
|
|
"relations": [
|
|
"concept:guest-memory-address-translation",
|
|
"question:host-thp-policy",
|
|
"question:vm-ram-backed-by-hugetlb",
|
|
"reference:record-the-conditions-with-every-memory-experiment"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/concept/concept-huge-pages-in-a-vm.md",
|
|
"status": "초안",
|
|
"studioId": "efce9a6e-5f5d-45ec-86e5-056d8cb1b8d2",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "Host 메모리 압박이 Guest 와 스토리지까지 내려가는 경로",
|
|
"kind": "concept",
|
|
"slug": "memory-pressure-reclaim-swap-oom",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#57-memory-overcommit",
|
|
"final/document.md#58-cpu-overcommit과-memory-overcommit의-차이",
|
|
"final/document.md#59-host-memory-pressure와-reclaim",
|
|
"final/document.md#60-host-swap이-vm에-미치는-영향",
|
|
"final/document.md#61-guest-swap과-host-swap",
|
|
"final/document.md#62-memory-pressure와-storage-contention의-연결",
|
|
"final/document.md#71-oom",
|
|
"final/document.md#72-guest-oom과-host-oom",
|
|
"final/document.md#81-cpu-network-storage-memory-연결",
|
|
"final/document.md#82-핵심-claim-registry"
|
|
],
|
|
"basis-version": "Linux memory reclaim(file-backed clean page · anonymous page) · swap · OOM Killer · cgroup memory limit · QEMU 의 Guest RAM backing. SSOT 가 커널 버전도 Host RAM 도 적지 않는다.",
|
|
"classification": "§58 이 이 부에서 가장 오해되기 쉬운 자리를 앞에 세웠다 — CPU 가 모자라면 scheduler 가 시간을 나누지만 RAM 이 모자라면 「지금 존재해야 하는 page 를 어디에 둘 것인가」가 남는다. 그 답이 reclaim · swap · ballooning · OOM 이고, §60 이 그 결과로 Guest 의 단순한 memory load 가 Host 의 swap-in I/O 대기로 바뀔 수 있다고 적었다. §62 와 §81 은 그 사슬을 storage contention 과 application latency 까지 이었다. OQ-6 · OQ-7 · OQ-14 셋이 이 경로 위에서만 읽힌다.",
|
|
"missing-verification": "이 테스트 Host 에서 잰 값이 하나도 없다. SSOT 제2부는 개념 서술이고 §83 이 OQ-1~OQ-14 로 아직 돌리지 않은 확인 목록을 적어 두었다. §88 이 그은 경계대로 이 글은 구조가 어떻게 동작하는지까지만 말하고 이 서버에서 실제로 어떤 값이 나오는지는 사실로 확정하지 않는다. 이 Host 의 RAM 도 swap 설정도 두 VM 의 configured memory 합도 SSOT 에 없다. 따라서 이 환경이 overcommit 상태인지조차 아직 사실이 아니다 — OQ-2 가 그 값을 받고, 압박이 실제로 Guest latency 를 밀어 올리는지는 OQ-7 이 받는다.",
|
|
"relations": [
|
|
"concept:guest-memory-address-translation",
|
|
"concept:page-fault-layers-in-a-vm",
|
|
"concept:virtio-balloon-memory-reclaim",
|
|
"question:swap-activity-in-guest-and-host",
|
|
"question:host-memory-pressure-vs-guest-latency",
|
|
"question:vm-configured-vs-current-memory",
|
|
"reference:memory-symptom-needs-layer-separation"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/concept/concept-memory-pressure-reclaim-swap-oom.md",
|
|
"status": "초안",
|
|
"studioId": "647d6d11-5bb2-4028-a530-b10c8925aa11",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "virtio-balloon 이 Guest 메모리를 회수하고 돌려주는 방식",
|
|
"kind": "concept",
|
|
"slug": "virtio-balloon-memory-reclaim",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#64-ballooning이-필요한-이유",
|
|
"final/document.md#65-virtio-balloon-구조",
|
|
"final/document.md#66-balloon-inflate",
|
|
"final/document.md#67-balloon-page-반환의-의미",
|
|
"final/document.md#68-balloon-deflate",
|
|
"final/document.md#69-ballooning을-과도하게-하면-guest가-압박을-받는다",
|
|
"final/document.md#70-ballooning과-memory-hotplug",
|
|
"final/document.md#82-핵심-claim-registry"
|
|
],
|
|
"basis-version": "virtio-balloon — Guest kernel 쪽 driver 와 QEMU 쪽 device 가 virtqueue 로 맞물린 구성 · libvirt 의 balloon target 설정. §67 이 Host 쪽 실제 release 동작은 QEMU/KVM 버전과 backing 종류·설정에 따라 달라질 수 있다고 적었고 SSOT 는 그 버전을 적지 않았다. `virtio-mem` 같은 다른 동적 memory 관리 방식이 있다는 사실까지만 §70 에 있다.",
|
|
"classification": "Host 는 Guest 안에서 어느 memory 가 중요한지 모른다는 것이 §64 의 출발점이고, 그래서 무작정 swap-out 하는 대신 Guest kernel 과 협력해 회수한다는 것이 이 장치의 존재 이유다. Guest 안의 driver 와 QEMU 쪽 device 가 virtqueue 로 맞물린 별도 메커니즘이라 압박 경로 안의 한 절로 넣으면 inflate 와 deflate 의 방향조차 놓을 자리가 없다. OQ-8 과 OQ-9 가 이 글을 필요로 한다.",
|
|
"missing-verification": "이 테스트 Host 에서 잰 값이 하나도 없다. SSOT 제2부는 개념 서술이고 §83 이 OQ-1~OQ-14 로 아직 돌리지 않은 확인 목록을 적어 두었다. §88 이 그은 경계대로 이 글은 구조가 어떻게 동작하는지까지만 말하고 이 서버에서 실제로 어떤 값이 나오는지는 사실로 확정하지 않는다. 이 프로젝트의 VM 에 virtio-balloon 이 붙어 있는지조차 SSOT 에 적혀 있지 않다 — 그것을 OQ-8 이 받고, target 을 바꿨을 때 Guest 가 쓸 수 있는 메모리가 어떻게 따라 움직이는지는 OQ-9 가 받는다. §67 이 남긴 「Host-side release 는 버전과 설정에 따라 다를 수 있다」도 재지 않았다.",
|
|
"relations": [
|
|
"concept:memory-pressure-reclaim-swap-oom",
|
|
"question:virtio-balloon-configured",
|
|
"question:balloon-target-vs-guest-available-memory",
|
|
"reference:memory-symptom-needs-layer-separation"
|
|
],
|
|
"ssot-assets": [
|
|
"virtio-balloon-inflate-deflate"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/concept/concept-virtio-balloon-memory-reclaim.md",
|
|
"status": "초안",
|
|
"studioId": "ebdf93d7-2dcb-4fc8-91b9-2364ac8196e6",
|
|
"assets": [
|
|
"virtio-balloon-inflate-deflate"
|
|
],
|
|
"assetFiles": [
|
|
"virtio-balloon-inflate-deflate"
|
|
],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "NUMA 에서는 vCPU 배치와 메모리 배치를 함께 본다",
|
|
"kind": "concept",
|
|
"slug": "numa-locality-for-vcpu-and-memory",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#73-numa",
|
|
"final/document.md#74-local-memory와-remote-memory",
|
|
"final/document.md#75-vcpu와-numa의-연결",
|
|
"final/document.md#76-vcpu-pinning만으로는-numa-최적화가-끝나지-않는다",
|
|
"final/document.md#77-guest-numa",
|
|
"final/document.md#78-numa는-실제-장비-topology부터-확인한다",
|
|
"final/document.md#81-cpu-network-storage-memory-연결",
|
|
"final/document.md#82-핵심-claim-registry"
|
|
],
|
|
"basis-version": "multi-socket NUMA x86-64 · Linux `lscpu` 와 `numactl --hardware` · `numastat -p <QEMU_PID>` · libvirt `virsh vcpupin` 과 `virsh vcpuinfo`. SSOT 가 이 Host 의 NUMA node 수도 커널 버전도 적지 않아 topology 는 측정 전까지 미정이다.",
|
|
"classification": "CPU 부의 개념이 vCPU 가 QEMU vCPU thread 로 Host scheduler 위에서 돈다는 데까지 갔고, 그 문서의 question:host-numa-topology 가 「상세한 memory placement 와 NUMA tuning 은 메모리 가상화 CONCEPT 에서 다룬다」고 범위를 넘겼다. 넘겨받은 자리가 여기다 — §75 가 thread 가 도는 node 와 backing page 가 있는 node 를 잇고, §76 이 vCPU pinning 만으로는 끝나지 않는다는 결론을 낸다. 주소 변환 개념과 갈라 둔 이유는 이것이 변환 방식이 아니라 배치 문제이고, 재는 명령도 종료 조건도 다르기 때문이다.",
|
|
"missing-verification": "이 테스트 Host 에서 잰 값이 하나도 없다. SSOT 제2부는 개념 서술이고 §83 이 OQ-1~OQ-14 로 아직 돌리지 않은 확인 목록을 적어 두었다. §88 이 그은 경계대로 이 글은 구조가 어떻게 동작하는지까지만 말하고 이 서버에서 실제로 어떤 값이 나오는지는 사실로 확정하지 않는다. 이 Host 가 단일 node 인지 다중 node 인지가 정해지지 않았고, 그것이 정해지기 전에는 §76 이 말한 어긋남이 이 환경에서 성립하는지조차 알 수 없다. topology 는 CPU 부의 question:host-numa-topology 가 받고, QEMU 메모리가 어느 node 에 있는지는 OQ-11 이, remote access 가 latency 를 바꾸는지는 OQ-12 가 받는다.",
|
|
"relations": [
|
|
"concept:guest-memory-address-translation",
|
|
"concept:kvm-vcpu-to-physical-cpu",
|
|
"question:host-numa-topology",
|
|
"question:qemu-memory-numa-placement",
|
|
"question:numa-remote-access-vs-workload-latency",
|
|
"reference:memory-symptom-needs-layer-separation"
|
|
],
|
|
"ssot-assets": [
|
|
"numa-vcpu-and-memory-placement"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/concept/concept-numa-locality-for-vcpu-and-memory.md",
|
|
"status": "초안",
|
|
"studioId": "68811272-5352-4409-aa20-7e22e601ede8",
|
|
"assets": [
|
|
"numa-vcpu-and-memory-placement"
|
|
],
|
|
"assetFiles": [
|
|
"numa-vcpu-and-memory-placement"
|
|
],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"reference": [
|
|
{
|
|
"title": "메모리 실험 결과에는 Host · VM · Workload 조건을 함께 남긴다",
|
|
"kind": "reference",
|
|
"slug": "record-the-conditions-with-every-memory-experiment",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#85-실험-시-반드시-같이-기록할-것",
|
|
"final/document.md#84-권장-실험-순서",
|
|
"final/document.md#83-실제-환경에서-확인할-open-question"
|
|
],
|
|
"classification": "§85 가 실험마다 남길 조건을 Host · VM · Workload 세 묶음으로 못박았다. 이유도 함께 적혀 있다 — 조건을 남기지 않으면 「Memory pressure 에서 느려졌다」는 결과를 다른 환경에 재사용하기 어렵다. 제2부의 열두 Question 전부가 같은 규칙에 걸리므로 각 글에 조건 목록을 되풀이하지 않고 여기 한 편으로 두고 가리킨다. 원 사건의 이름을 지워도 규칙이 남는다는 점에서 Case 요약이 아니다.",
|
|
"scope": "메모리 관련 측정을 남기는 모든 실험. Host 는 CPU model · core/thread 수 · RAM · NUMA topology · swap 설정 · kernel version · THP policy · physical storage, VM 은 vCPU · configured RAM · current RAM · memory backing 설정 · balloon device · guest swap · guest kernel, Workload 는 application · heap/memory 설정 · request concurrency · DB workload · 측정 시간을 적는다.",
|
|
"exceptions": "단일 값을 한 번 읽고 끝나는 확인(예: THP 정책 문자열 하나)에는 Workload 묶음이 비어 있어도 된다. 반대로 baseline 과 부하 구간을 비교하는 실험에서는 세 묶음을 두 시점 모두에 남긴다. SSOT 는 이 조건 목록을 이 프로젝트의 실제 장비 값으로 채운 적이 없다 — 목록이 규칙이고 값은 아직 없다.",
|
|
"relations": [
|
|
"concept:guest-memory-address-translation",
|
|
"concept:huge-pages-in-a-vm",
|
|
"concept:memory-pressure-reclaim-swap-oom",
|
|
"reference:memory-symptom-needs-layer-separation",
|
|
"question:host-memory-pressure-vs-guest-latency",
|
|
"question:numa-remote-access-vs-workload-latency"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/reference/reference-record-the-conditions-with-every-memory-experiment.md",
|
|
"status": "초안",
|
|
"studioId": "ded41b52-ec08-4231-a54c-86d6c80d38f0",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "메모리 증상 하나로 계층을 단정하지 않는다",
|
|
"kind": "reference",
|
|
"slug": "memory-symptom-needs-layer-separation",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#86-문제를-진단할-때의-분류",
|
|
"final/document.md#63-swap-used만-보고-장애를-판단하면-안-된다",
|
|
"final/document.md#78-numa는-실제-장비-topology부터-확인한다",
|
|
"final/document.md#88-결론",
|
|
"final/document.md#61-guest-swap과-host-swap",
|
|
"final/document.md#72-guest-oom과-host-oom"
|
|
],
|
|
"classification": "§86 은 memory latency 나 OOM 을 보고 한 번에 「메모리 부족」이라고 결론내지 말고 Guest Virtual Memory · Virtualization Translation · Host Memory · Dynamic Memory · NUMA 다섯 갈래로 먼저 가르라고 적었다. 같은 규칙이 SSOT 안에서 세 번 더 나온다 — §63 은 Swap Used 값 하나로, §78 은 topology 만 보고, §88 은 Guest 하나의 `free -h` 만 보고 판단하지 말라고 한다. 다섯 갈래는 이 주제의 여섯 개념을 가로지르는 라우팅이라 어느 개념 안에 넣어도 나머지 넷을 가리키지 못한다.",
|
|
"scope": "메모리 증상(latency 증가 · swap 관측 · OOM · fault 증가)을 원인으로 옮기려는 모든 판독. 가르는 축은 다섯이다 — Guest Virtual Memory(page fault · guest reclaim · guest swap · guest OOM), Virtualization Translation(EPT 관련 사건 · TLB pressure · huge-page/mapping 특성), Host Memory(host reclaim · host swap · host major fault · host OOM), Dynamic Memory(balloon target · guest pressure · hotplug/virtio-mem 여부), NUMA(vCPU placement · memory placement · remote access).",
|
|
"exceptions": "cgroup memory limit 이 걸린 환경에서는 Host 전체 RAM 이 남아 있어도 그 경계에서 OOM 이 나므로 「Host Memory 갈래가 아니다」로 바로 넘기면 안 된다(§72). 단일 NUMA node Host 에서는 NUMA 갈래의 우선순위를 낮춰도 된다고 §78 이 적었지만, 그것도 topology 를 잰 뒤의 이야기다. 이 규칙은 어느 갈래인지를 좁힐 뿐 원인을 확정하지 않는다 — 확정은 §85 의 조건을 남긴 측정이 한다.",
|
|
"relations": [
|
|
"concept:page-fault-layers-in-a-vm",
|
|
"concept:memory-pressure-reclaim-swap-oom",
|
|
"concept:virtio-balloon-memory-reclaim",
|
|
"concept:numa-locality-for-vcpu-and-memory",
|
|
"reference:record-the-conditions-with-every-memory-experiment",
|
|
"question:swap-activity-in-guest-and-host",
|
|
"question:guest-page-fault-vs-workload"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/reference/reference-memory-symptom-needs-layer-separation.md",
|
|
"status": "초안",
|
|
"studioId": "63fa33e5-9957-4d28-8d3c-c75735a73bd6",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"question": [
|
|
{
|
|
"title": "이 호스트의 가상 머신들은 설정한 메모리와 지금 잡고 있는 메모리가 얼마나 다른가",
|
|
"kind": "question",
|
|
"slug": "vm-configured-vs-current-memory",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-2",
|
|
"final/document.md#42-configured-memory와-실제-physical-ram-사용량은-같지-않을-수-있다",
|
|
"final/document.md#57-memory-overcommit"
|
|
],
|
|
"known": "§42 는 VM 에 16 GiB 를 설정했다고 해서 시작 순간 Host RAM 16 GiB 가 반드시 즉시 물리적으로 점유되는 것은 아니라고 적고, configured memory · Guest 가 현재 실제 사용하는 memory · Host 에서 현재 resident 한 physical memory 셋이 같지 않을 수 있다고 밝혔다. 그 차이를 만드는 것으로 Host 의 demand paging · backing 종류 · HugeTLB · memory locking · preallocation · overcommit 정책을 들었다. §57 은 configured 총량이 Host physical RAM 을 넘는 구성이 가능한 이유를 configured capacity 와 현재 working set/resident memory 가 같지 않을 수 있다는 데서 찾았다. §83 OQ-2 는 확인 명령으로 Host 쪽 `virsh dominfo <VM_NAME>` · `virsh dumpxml <VM_NAME>` · `virsh dommemstat <VM_NAME>` 와 Guest 쪽 `free -h` · `cat /proc/meminfo` 를 적고 Host 의 QEMU process 상태와 비교하라고 했다. 이 Host 의 physical RAM 도 VM 들의 configured memory 도 SSOT 어디에도 적혀 있지 않다.",
|
|
"unknown": "이 Host 의 physical RAM 과 각 VM 의 configured memory, 그리고 그 합이 Host RAM 을 넘는지. 넘든 넘지 않든 지금 각 Guest 가 실제로 쓰고 있는 memory 와 Host 에서 그 VM 몫으로 resident 한 memory 가 configured 값과 얼마나 벌어져 있는지.",
|
|
"next-verification": "각 VM 에 대해 Host 에서 `virsh dominfo <VM_NAME>` 로 configured/current memory 를, `virsh dumpxml <VM_NAME>` 로 memory backing 설정을, `virsh dommemstat <VM_NAME>` 로 balloon 계열 값을 적는다. 같은 시각에 각 Guest 에서 `free -h` 와 `cat /proc/meminfo` 를 찍고 Host 의 `free -h` 도 함께 남긴다 (§83 OQ-2). QEMU process 쪽 RSS 는 question:qemu-resident-memory-distribution 이 받는다. 기록할 조건은 reference:record-the-conditions-with-every-memory-experiment 를 따른다.",
|
|
"decision-criterion": "세 값(configured · Guest 사용량 · Host resident)을 VM 마다 한 표로 적으면 닫는다 — §42 가 말한 차이가 이 Host 에서 얼마인지가 답이다. configured 합이 Host RAM 을 넘으면 이 환경이 overcommit 상태라는 사실을 확정한 것이 되고, 그 상태에서 무엇이 일어나는지는 question:swap-activity-in-guest-and-host 와 question:host-memory-pressure-vs-guest-latency 가 받는다. 넘지 않으면 §57 의 시나리오는 이 환경에 적용되지 않는다고 적고 닫는다.",
|
|
"relations": [
|
|
"concept:guest-memory-address-translation",
|
|
"concept:memory-pressure-reclaim-swap-oom",
|
|
"question:qemu-resident-memory-distribution",
|
|
"reference:record-the-conditions-with-every-memory-experiment"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-vm-configured-vs-current-memory.md",
|
|
"status": "초안",
|
|
"studioId": "67285d51-7eaf-4540-9577-cfd0b273b40e",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "QEMU 프로세스가 실제로 붙잡고 있는 호스트 메모리는 얼마이고 어떻게 나뉘어 있는가",
|
|
"kind": "question",
|
|
"slug": "qemu-resident-memory-distribution",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-3",
|
|
"final/document.md#40-qemu는-guest-ram을-어떻게-준비하는가",
|
|
"final/document.md#41-kvm_set_user_memory_region",
|
|
"final/document.md#42-configured-memory와-실제-physical-ram-사용량은-같지-않을-수-있다"
|
|
],
|
|
"known": "§40 은 QEMU 가 Host userspace process 이고 Guest RAM 도 자신의 Host Virtual Address Space 에 마련한다고 적었다 — RAM 하드웨어에 「물리 주소 X 부터 8 GiB 를 달라」고 요구하는 것이 아니라 QEMU HVA → Host Page Table → HPA 로 관리된다. §41 은 그 영역과 Guest GPA 범위의 대응을 `KVM_SET_USER_MEMORY_REGION` 으로 KVM 에 등록한다고 적었다. §83 OQ-3 은 확인 명령으로 `ps -ef | grep qemu` 와 `ps -o pid,rss,vsz,cmd -p <QEMU_PID>` 를, 필요하면 `cat /proc/<QEMU_PID>/status` 와 `cat /proc/<QEMU_PID>/smaps_rollup` 을 적고 configured memory 와 RSS/anonymous/huge-page 상태를 비교하라고 했다.",
|
|
"unknown": "각 VM 의 QEMU process 가 지금 Host 에서 얼마나 resident 한지(RSS)와 virtual size(VSZ)가 얼마인지, 그 값이 configured memory 와 얼마나 벌어져 있는지, 그리고 그 backing 이 anonymous 인지 huge page 인지.",
|
|
"next-verification": "실행 중인 VM 마다 `ps -ef | grep qemu` 로 PID 를 찾고 `ps -o pid,rss,vsz,cmd -p <QEMU_PID>` 를 찍는다. 이어서 `cat /proc/<QEMU_PID>/status` 와 `cat /proc/<QEMU_PID>/smaps_rollup` 을 남겨 anonymous 와 huge page 항목을 본다 (§83 OQ-3). 같은 실행에서 question:vm-configured-vs-current-memory 가 적은 configured 값을 옆에 놓고 비교한다.",
|
|
"decision-criterion": "VM 마다 configured · RSS · VSZ 와 anonymous/huge-page 구성이 한 표에 적히면 닫는다. RSS 가 configured 에 크게 못 미치면 §42 가 말한 「configured ≠ resident」를 이 Host 에서 확인한 것이 되어 concept:guest-memory-address-translation 의 확인 사례로 흡수한다. huge page backing 이 보이면 question:vm-ram-backed-by-hugetlb 로 넘긴다.",
|
|
"relations": [
|
|
"concept:guest-memory-address-translation",
|
|
"question:vm-configured-vs-current-memory",
|
|
"question:vm-ram-backed-by-hugetlb",
|
|
"reference:record-the-conditions-with-every-memory-experiment"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-qemu-resident-memory-distribution.md",
|
|
"status": "초안",
|
|
"studioId": "fa5b3782-9fcf-4113-8f51-a55c8023b2db",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "이 호스트의 THP 정책과 huge page 상태는 무엇인가",
|
|
"kind": "question",
|
|
"slug": "host-thp-policy",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-4",
|
|
"final/document.md#53-thp-transparent-huge-pages",
|
|
"final/document.md#56-thp와-hugetlb-비교"
|
|
],
|
|
"known": "§53 은 THP 정책을 `cat /sys/kernel/mm/transparent_hugepage/enabled` 로 읽고 출력이 `always [madvise] never` 같은 꼴이라고 적으면서, 현재 정책은 kernel·distribution·Host 설정에 따라 다르므로 실제 시스템에서 확인하라고 했다. §56 은 Host 확인 명령으로 `grep -i huge /proc/meminfo` 와 같은 THP 파일을 들고 `AnonHugePages` 와 `HugePages_Total` 이 같은 의미가 아니라고 적었다. §83 OQ-4 는 확인 항목으로 THP policy · AnonHugePages · HugePages_Total · HugePages_Free · Hugepagesize 다섯을 적었다.",
|
|
"unknown": "이 Host 의 THP 정책이 always 인지 madvise 인지 never 인지, 그리고 다섯 항목의 실제 값. HugeTLB pool 이 잡혀 있는지(HugePages_Total 이 0 인지)도 아직 모른다.",
|
|
"next-verification": "Host 에서 `cat /sys/kernel/mm/transparent_hugepage/enabled` 와 `grep -i huge /proc/meminfo` 를 찍어 §83 OQ-4 의 다섯 항목을 그대로 적는다. 같은 명령을 각 Guest 에서도 찍어 Guest 쪽 정책과 구분한다 — §52 가 Guest 단계와 Host backing 을 하나의 설정으로 취급하지 말라고 적었다.",
|
|
"decision-criterion": "다섯 항목의 값이 Host 와 Guest 각각에 대해 적히면 닫는다. HugePages_Total 이 0 이 아니면 그 pool 을 VM 이 쓰고 있는지를 question:vm-ram-backed-by-hugetlb 로 넘긴다. THP 가 always 인데 latency-sensitive workload 를 돌리고 있으면 §54 가 말한 compaction 영향을 측정할지 여부를 Decision 으로 넘긴다 — 이 질문 자체는 정책 값을 확정하는 데서 끝난다.",
|
|
"relations": [
|
|
"concept:huge-pages-in-a-vm",
|
|
"question:vm-ram-backed-by-hugetlb",
|
|
"reference:record-the-conditions-with-every-memory-experiment"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-host-thp-policy.md",
|
|
"status": "초안",
|
|
"studioId": "50730203-df5f-4604-8535-7e93ab6fb724",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "가상 머신 RAM 이 HugeTLB 로 명시적으로 backing 되어 있는가",
|
|
"kind": "question",
|
|
"slug": "vm-ram-backed-by-hugetlb",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-5",
|
|
"final/document.md#55-hugetlb",
|
|
"final/document.md#52-vm에서-huge-page를-볼-때-주의할-점"
|
|
],
|
|
"known": "§55 는 HugeTLB 를 관리자가 미리 준비한 명시적 Huge Page pool 을 VM 이나 application 이 명시적으로 쓰는 방식으로 적고, 사전 확보로 예측 가능성을 높이는 대신 일반 memory allocation 의 유연성이 줄어드는 trade-off 가 있다고 밝혔다. §52 는 VM 에 변환 단계가 둘이라 Guest page-table 단계 · Host backing · EPT 매핑을 구분해야 하고 Guest 와 Host 의 page-size 선택을 하나의 설정으로 취급하면 안 된다고 적었다. §83 OQ-5 는 `virsh dumpxml <VM_NAME>` 로 libvirt memory backing 설정을 확인하고 Host `/proc/meminfo` 및 QEMU `smaps` 계열과 교차 검증하라고 했다.",
|
|
"unknown": "각 VM 의 libvirt 설정에 memory backing 항목이 있는지, 있다면 HugeTLB 를 가리키는지. 그리고 그것이 Host `/proc/meminfo` 의 HugePages 사용량 및 QEMU smaps 의 huge-page 항목과 맞는지.",
|
|
"next-verification": "VM 마다 `virsh dumpxml <VM_NAME>` 를 남기고 memory backing 관련 요소를 그대로 인용한다. 같은 시각에 Host `grep -i huge /proc/meminfo` 와 `cat /proc/<QEMU_PID>/smaps_rollup` 을 찍어 세 자료를 나란히 놓는다 (§83 OQ-5).",
|
|
"decision-criterion": "세 자료가 서로 맞으면 이 환경의 backing 종류를 확정하고 닫는다. 설정에 backing 이 없고 HugePages_Total 도 0 이면 이 VM 들은 명시적 HugeTLB backing 을 쓰지 않는다고 적고 닫는다 — 그 경우 §54 의 THP 쪽 특성만 남는다. 설정과 Host 값이 어긋나면 그 어긋남이 Case 가 된다.",
|
|
"relations": [
|
|
"concept:huge-pages-in-a-vm",
|
|
"question:host-thp-policy",
|
|
"question:qemu-resident-memory-distribution",
|
|
"reference:record-the-conditions-with-every-memory-experiment"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-vm-ram-backed-by-hugetlb.md",
|
|
"status": "초안",
|
|
"studioId": "f460e9a2-35ef-46ff-81cf-b36627a1b231",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "지금 게스트와 호스트에서 swap 이 실제로 오가고 있는가",
|
|
"kind": "question",
|
|
"slug": "swap-activity-in-guest-and-host",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-6",
|
|
"final/document.md#63-swap-used만-보고-장애를-판단하면-안-된다",
|
|
"final/document.md#61-guest-swap과-host-swap"
|
|
],
|
|
"known": "§63 은 Swap Used 값 하나로 memory pressure 가 심하다고 단정할 수 없고 과거에 swap-out 된 cold page 가 남아 있을 수도 있다고 적으면서, 더 중요한 물음으로 지금 swap-in/out 이 지속되는가 · reclaim pressure 가 증가하는가 · major fault 가 증가하는가 · storage latency 가 같이 증가하는가 넷을 들고 Guest 와 Host 를 동시에 확인하라고 했다. 확인 명령은 `free -h` 와 `vmstat 1` 이다. §61 은 Guest Swap 이 Guest kernel 에서 `/dev/vda` 로 내려가는 경로이고 Host Swap 은 QEMU memory backing 이 Host kernel 에 눌리는 경로라 둘이 다르며, Guest 가 여유 있어 보이는데 Host 에서 swap/reclaim 이 심할 수도 있다고 적었다. §83 OQ-6 도 같은 두 명령을 Guest 와 Host 양쪽에 적었다.",
|
|
"unknown": "지금 이 환경에서 swap-in/out 이 실제로 오가고 있는지, 오간다면 Guest 쪽인지 Host 쪽인지 둘 다인지. Swap Used 가 0 이 아니더라도 그것이 남아 있는 cold page 인지 진행 중인 활동인지.",
|
|
"next-verification": "Guest 와 Host 양쪽에서 같은 시각에 `free -h` 를 한 번 찍고 `vmstat 1` 을 일정 시간 돌려 `si`/`so` 열이 계속 0 인지 본다 (§83 OQ-6 · §63). Guest 는 VM 마다 따로 찍는다. 같은 창에서 §63 이 든 나머지 셋 — reclaim pressure · major fault · storage latency — 도 함께 남긴다.",
|
|
"decision-criterion": "관측 구간 내내 Guest 와 Host 모두 `si`/`so` 가 0 이면 지금은 swap 이 오가지 않는다고 적고 닫는다 — Swap Used 값이 0 이 아니어도 그렇게 적는다. 한쪽에서 swap 이 오가면 그 방향(§61 의 두 경로 중 어느 쪽인지)을 적고 그 구간의 storage latency 및 Guest latency 와 함께 놓아 question:host-memory-pressure-vs-guest-latency 로 넘긴다.",
|
|
"relations": [
|
|
"concept:memory-pressure-reclaim-swap-oom",
|
|
"concept:page-fault-layers-in-a-vm",
|
|
"question:host-memory-pressure-vs-guest-latency",
|
|
"question:host-major-fault-vs-storage-latency",
|
|
"reference:memory-symptom-needs-layer-separation"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-swap-activity-in-guest-and-host.md",
|
|
"status": "초안",
|
|
"studioId": "d3e8501f-e1b5-4f8f-aa9a-89f9c89a308e",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "호스트 메모리 압박이 게스트 애플리케이션 지연을 실제로 밀어 올리는가",
|
|
"kind": "question",
|
|
"slug": "host-memory-pressure-vs-guest-latency",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-7",
|
|
"final/document.md#59-host-memory-pressure와-reclaim",
|
|
"final/document.md#60-host-swap이-vm에-미치는-영향",
|
|
"final/document.md#62-memory-pressure와-storage-contention의-연결"
|
|
],
|
|
"known": "§59 는 Host RAM 수요가 available physical memory 에 가까워지면 reclaim 이 돌고, file-backed clean page 는 버렸다 다시 읽으면 되지만 anonymous page 는 swap 같은 backing 이 필요하다고 적었다. §60 은 Guest 가 단순히 RAM 에 접근한다고 생각하는 사이에 Host 쪽에서는 backing page 가 RAM 에 없어 Host Page Fault → swap-in I/O → RAM 복원 → Guest 실행 계속으로 바뀔 수 있다고 적었다. §62 는 그 결과 Guest swap I/O · Host swap I/O · DB I/O · filesystem writeback 이 한 물리 장치로 몰려 storage contention 과 application latency 로 이어질 수 있고, CPU 사용률이 낮다고 memory/storage 문제가 없는 것은 아니라고 밝혔다. §83 OQ-7 은 실험 개념을 baseline → Guest latency 측정 → Host memory pressure 유도 → Host reclaim/swap 관측 → Guest latency 재측정으로 적고 CPU 와 storage 도 동시에 관측하라고 했다.",
|
|
"unknown": "이 Host 에서 memory pressure 를 유도했을 때 reclaim 과 swap 이 실제로 도는지, 돈다면 같은 구간의 Guest application latency 가 baseline 대비 얼마나 달라지는지, 그리고 그 변화가 CPU 쪽이 아니라 memory/storage 쪽 지표와 같은 방향으로 움직이는지.",
|
|
"next-verification": "baseline 구간에서 Guest application latency 와 Host `vmstat 1` · storage latency · CPU 를 같은 시각에 기록한다. 그 다음 Host 에 memory pressure 를 유도하고 같은 네 가지를 다시 기록한 뒤, pressure 를 걷고 세 번째 구간을 찍는다 (§83 OQ-7). 세 구간 모두에 reference:record-the-conditions-with-every-memory-experiment 의 Host · VM · Workload 조건을 남긴다. Guest 쪽 swap 은 question:swap-activity-in-guest-and-host 와 같은 방법으로 함께 찍는다.",
|
|
"decision-criterion": "pressure 구간에서 Host reclaim/swap 이 돌았는데도 Guest latency 가 baseline 과 다르지 않으면, 이 구성에서는 그 정도의 압박이 Guest 까지 오지 않는다고 적고 닫는다. latency 가 오르면 그 세 구간 표가 Case 가 되고 이 질문은 그 Case 가 닫는다. 압박을 어디까지 허용할지 — swappiness 를 바꿀지, VM 배치를 바꿀지, balloon 을 쓸지 — 는 그 Case 가 나온 뒤 Decision 으로 넘긴다.",
|
|
"relations": [
|
|
"concept:memory-pressure-reclaim-swap-oom",
|
|
"concept:page-fault-layers-in-a-vm",
|
|
"question:swap-activity-in-guest-and-host",
|
|
"question:host-major-fault-vs-storage-latency",
|
|
"reference:record-the-conditions-with-every-memory-experiment"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-host-memory-pressure-vs-guest-latency.md",
|
|
"status": "초안",
|
|
"studioId": "a05fe2a6-eb7c-4bcc-a7da-58e729436468",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "이 가상 머신들에 virtio-balloon 이 붙어 있는가",
|
|
"kind": "question",
|
|
"slug": "virtio-balloon-configured",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-8",
|
|
"final/document.md#65-virtio-balloon-구조",
|
|
"final/document.md#64-ballooning이-필요한-이유"
|
|
],
|
|
"known": "§65 는 virtio-balloon 을 Guest kernel 쪽 driver 와 QEMU 쪽 device 가 virtqueue 로 맞물린 구성으로 그리고, Guest RAM 자체를 제공하는 장치가 아니라 이미 있는 backing 을 Host 와 Guest 가 협력해 회수·반환하는 데 쓰는 가상 장치라고 적었다. §64 는 Host 가 Guest 안에서 어느 memory 가 중요한지 완전히 알지 못하므로 무작정 swap-out 하기보다 Guest kernel 과 협력하는 편이 유리할 수 있다는 것을 이 장치의 존재 이유로 들었다. §83 OQ-8 은 `virsh dumpxml <VM_NAME>` 로 확인하고 Guest 에서도 관련 driver/device 상태를 확인하라고 하면서, 환경에 따라 driver 이름과 표시 방식이 달라질 수 있으니 실제 장비에서 검증하라는 단서를 달았다.",
|
|
"unknown": "각 VM 의 libvirt 설정에 balloon device 가 있는지, 있다면 Guest 안에서 해당 driver 가 실제로 올라와 있는지. 이 환경에서 그 driver 가 어떤 이름으로 보이는지도 SSOT 에 없다.",
|
|
"next-verification": "VM 마다 `virsh dumpxml <VM_NAME>` 를 남기고 balloon 관련 요소를 그대로 인용한다. 각 Guest 에서 balloon 관련 driver/device 상태를 확인해 실제로 보이는 이름과 함께 적는다 (§83 OQ-8). `virsh dommemstat <VM_NAME>` 출력도 같이 남긴다 — question:vm-configured-vs-current-memory 가 같은 명령을 찍으므로 한 번에 받는다.",
|
|
"decision-criterion": "설정과 Guest 쪽 상태가 둘 다 적히면 닫는다. 붙어 있지 않으면 이 환경에서는 ballooning 이 동작하지 않는다고 적고 question:balloon-target-vs-guest-available-memory 는 열지 않은 채로 둔다 — 장치가 없으면 그 실험이 성립하지 않는다. 붙어 있으면 그 질문으로 넘긴다.",
|
|
"relations": [
|
|
"concept:virtio-balloon-memory-reclaim",
|
|
"question:balloon-target-vs-guest-available-memory",
|
|
"question:vm-configured-vs-current-memory"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-virtio-balloon-configured.md",
|
|
"status": "초안",
|
|
"studioId": "63aef96b-03ef-4dbd-9ba6-9e61c6e695e1",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "balloon target 을 바꾸면 게스트가 쓸 수 있는 메모리는 어떻게 따라 움직이는가",
|
|
"kind": "question",
|
|
"slug": "balloon-target-vs-guest-available-memory",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-9",
|
|
"final/document.md#66-balloon-inflate",
|
|
"final/document.md#68-balloon-deflate",
|
|
"final/document.md#69-ballooning을-과도하게-하면-guest가-압박을-받는다"
|
|
],
|
|
"known": "§66 은 inflate 하면 Guest 안의 balloon 이 커져 Guest usable memory 가 줄고 Host 가 회수할 수 있는 backing 이 는다고 적었고, §68 은 target 을 줄이면 balloon page 가 반환되어 Guest usable memory 가 는다고 적었다. §69 는 working set 이 큰데 과도하게 inflate 하면 Guest available memory 감소 → Guest memory pressure → reclaim → page cache 회수 → Guest swap → 심하면 Guest OOM 으로 이어질 수 있고, Host RAM 을 확보하려는 조치가 Guest storage I/O 와 application latency 를 올릴 수 있다고 밝혔다. §83 OQ-9 는 Host/libvirt memory setting → Guest `free -h` / `/proc/meminfo` → Guest reclaim/swap 변화 순으로 관측하고 과도한 ballooning 시의 latency/swap/OOM 가능성은 별도 실험으로 보라고 했다.",
|
|
"unknown": "이 환경에서 balloon target 을 바꿨을 때 Guest 의 available memory 가 얼마나 따라 움직이는지, 그 반영이 즉시인지 지연이 있는지, 그리고 어느 지점부터 Guest reclaim/swap 이 시작되는지.",
|
|
"next-verification": "question:virtio-balloon-configured 가 balloon 이 붙어 있다고 확정한 뒤에 연다. baseline 에서 Guest `free -h` 와 `/proc/meminfo` 와 `vmstat 1` 을 찍고, Host 에서 balloon target 을 한 단계 낮춘 뒤 같은 셋을 다시 찍는다. 단계를 몇 번 반복해 target 과 Guest available memory 의 대응을 표로 만든다 (§83 OQ-9). 과도 inflate 구간은 §69 가 경고한 대로 별도 실험으로 분리하고 Guest application latency 를 같이 잰다.",
|
|
"decision-criterion": "target 값과 Guest available memory 가 표로 대응되고 reclaim/swap 이 시작되는 지점이 적히면 닫는다. Guest swap 이나 OOM 까지 갔으면 그 구간이 Case 가 되고, 운영에서 balloon 을 쓸지와 target 하한을 얼마로 둘지는 그 Case 가 나온 뒤 Decision 으로 넘긴다.",
|
|
"relations": [
|
|
"concept:virtio-balloon-memory-reclaim",
|
|
"concept:memory-pressure-reclaim-swap-oom",
|
|
"question:virtio-balloon-configured",
|
|
"reference:record-the-conditions-with-every-memory-experiment"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-balloon-target-vs-guest-available-memory.md",
|
|
"status": "초안",
|
|
"studioId": "6a60abe7-ea2c-46c2-bb9f-8281bfbe9644",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "QEMU 메모리는 vCPU 가 도는 NUMA node 와 같은 node 에 있는가",
|
|
"kind": "question",
|
|
"slug": "qemu-memory-numa-placement",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-11",
|
|
"final/document.md#75-vcpu와-numa의-연결",
|
|
"final/document.md#76-vcpu-pinning만으로는-numa-최적화가-끝나지-않는다",
|
|
"final/document.md#78-numa는-실제-장비-topology부터-확인한다"
|
|
],
|
|
"known": "§75 는 Guest vCPU 가 Host 에서 QEMU vCPU thread 이고, VM 의 vCPU thread 가 Node 0 CPU 에서 실행되는데 그 VM 의 Host physical backing page 가 Node 1 에 있으면 Guest 에서는 단순한 memory load 인 것이 실제 하드웨어에서는 NUMA interconnect 를 건널 수 있다고 적었다. §76 은 vCPU 를 pinning 해도 memory 가 다른 node 에 배치되어 있으면 remote access 가 많아질 수 있으므로 vCPU placement 와 memory placement/binding 을 함께 봐야 한다고 밝혔다. §78 은 `numastat -p <QEMU_PID>` 로 QEMU process 별 memory distribution 을, `virsh vcpupin` 과 `virsh vcpuinfo` 로 vCPU placement 를 본다고 적었다. §83 OQ-11 은 `numastat -p <QEMU_PID>` 결과를 vCPU placement 와 비교해 vCPU → Node 0 / Memory → Node 0 인지 vCPU → Node 0 / Memory → Node 1 인지 확인하라고 했다.",
|
|
"unknown": "이 Host 에서 각 VM 의 QEMU memory 가 어느 NUMA node 에 얼마나 나뉘어 있는지, 그리고 그 분포가 그 VM 의 vCPU thread 가 도는 node 와 맞는지 어긋나는지. Host 가 다중 NUMA node 인지 자체가 아직 확정되지 않았고 그것은 CPU 부의 question:host-numa-topology 가 받는다.",
|
|
"next-verification": "question:host-numa-topology 가 이 Host 를 다중 NUMA node 로 확정한 뒤에 연다. VM 마다 `numastat -p <QEMU_PID>` 로 node 별 memory 분포를 찍고, 같은 시각에 `virsh vcpuinfo <VM_NAME>` 와 `virsh vcpupin <VM_NAME>` 로 vCPU 배치를 적어 나란히 놓는다 (§78 · §83 OQ-11).",
|
|
"decision-criterion": "node 별 memory 분포와 vCPU 배치가 VM 마다 한 표로 적히면 닫는다. 둘이 같은 node 로 모여 있으면 §76 이 말한 어긋남이 이 환경에 없다고 적고 닫는다. 어긋나 있으면 그 표가 Case 가 되고, 그 어긋남이 실제 latency 를 바꾸는지는 question:numa-remote-access-vs-workload-latency 가 받는다. memory binding 을 걸지 여부는 그 뒤 Decision 으로 넘긴다. 단일 NUMA node 로 나오면 이 질문은 이 환경에 적용되지 않는다고 적고 닫는다.",
|
|
"relations": [
|
|
"concept:numa-locality-for-vcpu-and-memory",
|
|
"question:host-numa-topology",
|
|
"question:numa-remote-access-vs-workload-latency",
|
|
"reference:record-the-conditions-with-every-memory-experiment"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-qemu-memory-numa-placement.md",
|
|
"status": "초안",
|
|
"studioId": "1b59e6de-16f1-42b2-81bb-01d6198e34bf",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "NUMA remote access 가 이 작업의 지연을 실제로 바꾸는가",
|
|
"kind": "question",
|
|
"slug": "numa-remote-access-vs-workload-latency",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-12",
|
|
"final/document.md#74-local-memory와-remote-memory",
|
|
"final/document.md#77-guest-numa"
|
|
],
|
|
"known": "§74 는 remote access 가 local access 와 동일한 비용이라고 가정할 수 없으며 추가 latency/bandwidth 비용이 있을 수 있다고 적었다. §77 은 큰 VM 에서는 Guest 에게 NUMA topology 를 노출하고 가능하면 Guest 가 인식하는 topology 와 실제 Host placement 가 합리적으로 대응되도록 구성할 수 있다고 적었다. §83 OQ-12 는 NUMA node 가 2개 이상인 경우에만 우선순위를 높이라고 하면서 실험을 local placement baseline → latency/throughput/memory metrics → remote-heavy placement → 동일 workload 비교로 적고, 단순 topology 만 보고 성능 문제라고 단정하지 말라는 단서를 달았다.",
|
|
"unknown": "이 Host 에서 local placement 와 remote-heavy placement 를 각각 구성했을 때 같은 workload 의 latency 와 throughput 이 실제로 다른지, 다르다면 얼마나 다른지. Guest NUMA 를 노출한 구성인지도 SSOT 에 적혀 있지 않다.",
|
|
"next-verification": "question:host-numa-topology 가 다중 node 로 확정하고 question:qemu-memory-numa-placement 가 현재 배치를 적은 뒤에 연다. local placement 로 맞춘 구성에서 같은 workload 를 돌려 latency · throughput · memory 지표를 기록하고, remote-heavy placement 로 바꿔 같은 workload 를 같은 조건으로 다시 돌린다 (§83 OQ-12). 두 구간 모두에 reference:record-the-conditions-with-every-memory-experiment 의 세 묶음을 남긴다.",
|
|
"decision-criterion": "두 배치의 latency/throughput 이 측정 오차 안에서 다르지 않으면 이 workload 에서는 NUMA locality 가 우선순위가 아니라고 적고 닫는다. 차이가 나면 그 비교표가 Case 가 되고, vCPU pinning 과 memory binding 을 운영 구성으로 채택할지는 그 Case 가 나온 뒤 Decision 으로 넘긴다. 단일 NUMA node 로 나오면 §83 이 적은 대로 우선순위를 낮추고 닫는다.",
|
|
"relations": [
|
|
"concept:numa-locality-for-vcpu-and-memory",
|
|
"question:qemu-memory-numa-placement",
|
|
"question:host-numa-topology",
|
|
"reference:record-the-conditions-with-every-memory-experiment"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-numa-remote-access-vs-workload-latency.md",
|
|
"status": "초안",
|
|
"studioId": "380d4975-3be5-451d-aede-2e24c5a2a09e",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "게스트 page fault 증가는 workload 변화를 따라가는가",
|
|
"kind": "question",
|
|
"slug": "guest-page-fault-vs-workload",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-13",
|
|
"final/document.md#45-guest-page-fault",
|
|
"final/document.md#46-page-fault의-대표적인-원인"
|
|
],
|
|
"known": "§45 는 Guest Page Fault 가 GVA → GPA 단계에서 접근을 완료할 수 없을 때 나고 Guest Kernel 이 page 를 확보해 매핑을 갱신한 뒤 instruction 을 재시도한다고 적으면서, Page Fault 자체가 프로그램 오류를 뜻하지 않는다고 밝혔다. §46 은 대표 원인으로 demand paging · swap-in · permission fault · copy-on-write · invalid access 다섯을 들고 Page Fault 와 Segmentation Fault 가 다르다고 적었다. §83 OQ-13 은 Guest 에서 page-fault 관련 지표를 관측하되 정상 demand paging 인지 · COW 인지 · Guest swap-in 인지 · application working-set 증가인지를 분리하고, Page Fault 증가만으로 오류라고 판단하지 말라고 했다.",
|
|
"unknown": "이 환경의 Guest 에서 workload 가 늘 때 page fault 지표가 실제로 함께 오르는지, 오른다면 minor 인지 major 인지, 그리고 §46 의 다섯 원인 중 어느 쪽으로 설명되는지.",
|
|
"next-verification": "Guest 에서 idle 구간과 workload 구간을 나눠 page-fault 관련 지표를 같은 방법으로 기록한다 — `vmstat 1` 로 시간에 따른 변화를 보고 프로세스 수준은 `/proc/<PID>/stat` 계열로 minor/major 를 가른다. 같은 구간에 Guest 의 swap 활동(question:swap-activity-in-guest-and-host 와 같은 방법)과 application 의 working set 을 함께 남겨 §83 OQ-13 이 든 네 갈래를 분리한다.",
|
|
"decision-criterion": "workload 구간의 fault 증가가 minor 위주이고 swap-in 이 없으면 정상 demand paging 으로 설명된다고 적고 닫는다 — §45 가 말한 대로 그것은 오류가 아니다. major fault 가 함께 오르면 Guest swap-in 을 의심하고 그 구간을 question:swap-activity-in-guest-and-host 로 넘긴다. 어느 쪽으로도 설명되지 않으면 그 관측이 Case 가 된다.",
|
|
"relations": [
|
|
"concept:page-fault-layers-in-a-vm",
|
|
"question:swap-activity-in-guest-and-host",
|
|
"question:host-major-fault-vs-storage-latency",
|
|
"reference:memory-symptom-needs-layer-separation"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-guest-page-fault-vs-workload.md",
|
|
"status": "초안",
|
|
"studioId": "074a1cb4-cea8-4dad-bc4c-899692ff9aa9",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "호스트 major fault 와 스토리지 지연은 같은 시간축에서 함께 움직이는가",
|
|
"kind": "question",
|
|
"slug": "host-major-fault-vs-storage-latency",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-14",
|
|
"final/document.md#49-host-page-fault도-별도로-존재한다",
|
|
"final/document.md#62-memory-pressure와-storage-contention의-연결"
|
|
],
|
|
"known": "§49 는 QEMU 도 Host 의 일반 userspace process 이므로 QEMU memory backing 에 Host virtual-memory 관리가 적용되고 demand allocation·reclaim/swap 때문에 Host 쪽에서도 page fault 가 날 수 있다고 적으면서, VM 메모리 분석에서는 Guest Page Fault · Host Page Fault · EPT 관련 사건을 적어도 셋으로 구분해야 한다고 밝혔다. §62 는 Guest swap I/O · Host swap I/O · DB I/O · filesystem writeback 이 한 물리 장치로 몰릴 수 있고 Host memory pressure → reclaim/swap → storage I/O 증가 → storage contention → DB latency → application latency 로 이어질 수 있다고 적었다. §83 OQ-14 는 Host memory pressure 실험 시 Host fault · swap activity · storage latency · Guest application latency 를 같은 시간축으로 비교하라고 했다.",
|
|
"unknown": "Host 에 memory pressure 가 걸린 구간에서 Host major fault 와 swap activity 와 storage latency 가 실제로 같은 방향으로 움직이는지, 그리고 그 움직임이 Guest application latency 와 시간적으로 겹치는지.",
|
|
"next-verification": "question:host-memory-pressure-vs-guest-latency 의 실험과 같은 실행에서 받는다 — baseline · pressure · 회복 세 구간 각각에 Host 의 major fault 와 swap 활동(`vmstat 1`), storage latency, Guest application latency 를 같은 타임스탬프로 남긴다 (§83 OQ-14). 네 계열을 한 시간축에 놓고 본다.",
|
|
"decision-criterion": "네 계열이 같은 구간에서 함께 오르면 §62 가 그린 사슬이 이 환경에서 이어진다는 것을 확인한 것이 되고 그 시계열이 Case 가 된다. major fault 는 오르는데 storage latency 가 따라 오르지 않으면 이 장치 구성에서는 그 사슬이 여기서 끊긴다고 적고 닫는다. 어느 쪽이든 storage 쪽 대응 — 장치 분리나 swap 위치 변경 — 은 그 뒤 Decision 으로 넘긴다.",
|
|
"relations": [
|
|
"concept:page-fault-layers-in-a-vm",
|
|
"concept:memory-pressure-reclaim-swap-oom",
|
|
"question:host-memory-pressure-vs-guest-latency",
|
|
"question:swap-activity-in-guest-and-host",
|
|
"reference:memory-symptom-needs-layer-separation"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "memory-virtualization/question/question-host-major-fault-vs-storage-latency.md",
|
|
"status": "초안",
|
|
"studioId": "a2cbc828-59ab-4e70-916a-d053e71960ae",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"decision": []
|
|
}
|
|
},
|
|
"network-virtualization": {
|
|
"topic": "network-virtualization",
|
|
"title": "네트워크 가상화 — Guest 의 packet 이 Host Physical NIC 까지 가는 길",
|
|
"readerQuestion": "Guest 애플리케이션이 보낸 packet 은 어느 계층을 거쳐 Host Physical NIC 까지 가고, 요청이 느리거나 닿지 않을 때 그것이 network 가상화의 어느 자리 문제인지 무엇을 보고 가르는가?",
|
|
"kinds": {
|
|
"case": [],
|
|
"concept": [
|
|
{
|
|
"title": "Guest 의 packet 이 Host Physical NIC 에 닿기까지 — virtio-net · virtqueue · vhost-net · TAP · Bridge",
|
|
"kind": "concept",
|
|
"slug": "guest-packet-path-to-physical-nic",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#94-전체-네트워크-계층",
|
|
"final/document.md#121-이-ssot에서-파생될-concept",
|
|
"final/document.md#125-최종-기준-구조",
|
|
"final/document.md#89-문서-목적",
|
|
"final/document.md#90-virsh-libvirt-virtio-구분",
|
|
"final/document.md#91-virtio-net은-정확히-어디에-있는가",
|
|
"final/document.md#92-frontend와-backend",
|
|
"final/document.md#93-guest-os는-왜-qemu가-아니라-virtio-net을-사용하는가",
|
|
"final/document.md#95-physical-nic의-역할",
|
|
"final/document.md#96-linux-bridge의-역할",
|
|
"final/document.md#97-routing의-역할",
|
|
"final/document.md#98-nat의-역할",
|
|
"final/document.md#99-tap의-역할",
|
|
"final/document.md#100-virtqueue의-역할",
|
|
"final/document.md#101-guest-tcp-ip-stack의-역할",
|
|
"final/document.md#102-packet이-keycloak까지-올라오는-과정",
|
|
"final/document.md#103-qemu-virtio-device-model의-역할",
|
|
"final/document.md#104-왜-tap-→-vhost-net-→-qemu-→-virtqueue-라고-일반화하면-안-되는가",
|
|
"final/document.md#105-control-path와-data-path",
|
|
"final/document.md#106-qemu가-userspace인데-packet이-qemu를-안-거칠-수-있는-이유",
|
|
"final/document.md#107-vhost-net-최적화",
|
|
"final/document.md#108-vhost-net은-qemu를-제거하지-않는다",
|
|
"final/document.md#109-fast-path와-slow-control-path",
|
|
"final/document.md#110-data-copy-최적화",
|
|
"final/document.md#111-interrupt-notification-최적화",
|
|
"final/document.md#112-multi-queue-최적화",
|
|
"final/document.md#113-offload-최적화",
|
|
"final/document.md#114-linux-bridge가-항상-host-tcp-ip-stack을-거치는-것은-아니다",
|
|
"final/document.md#115-host-physical-nic로-나갈-때-virtio를-다시-거치지-않는다",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-4",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-5",
|
|
"final/document.md#124-핵심-claim"
|
|
],
|
|
"basis-version": "Linux KVM/QEMU/libvirt 의 virtio-net frontend + vhost-net kernel backend + TAP + Linux Bridge 구성. §94 가 「가장 기본적인 구조」로 잡은 것이 이 조합이고, 같은 절이 실제 환경은 Bridge · NAT · Routed Network · macvtap · SR-IOV · VFIO passthrough · Open vSwitch · Kubernetes CNI 에 따라 달라질 수 있다고 적었으므로 이 글은 그 조합 안에서만 유효하다. SSOT 가 커널·QEMU·libvirt 버전을 한 번도 적지 않아 특정 버전에 고정하지 않는다 — 여기 적힌 것은 이 문서가 서술한 구성이고, 버전이 바뀌어 서술이 낡았는지는 §122 의 확인을 실제 Host 에서 돌릴 때 드러난다. §110 이 copy 동작을 kernel version · QEMU version · vhost configuration · offload · NIC capability 에 따라 달라진다고 못박은 것이 그 경계다.",
|
|
"classification": "§121 이 virsh · libvirt · QEMU · virtio · virtio-net · Frontend/Backend · virtqueue · QEMU virtio Device Model · vhost-net · TAP · Linux Bridge · Routing · NAT · Physical NIC · Guest TCP/IP Stack · Socket · Data Path/Control Path · Fast Path · Multi-Queue · Offload · Packet tracing 을 한 목록으로 묶고 「이 요소들이 하나의 packet 실행 경로를 설명하므로 하나의 CONCEPT 로 관리한다」고 적었다. 제1부의 §19 가 CPU 실행 경로에 대해 같은 지시를 했고 그 부도 개념 하나로 내려앉았다. 경로 중간을 잘라 내면 인과가 끊긴다 — virtqueue 만 떼면 무엇과 무엇 사이의 queue 인지 말할 수 없고, vhost-net 만 떼면 무엇을 우회하는지 말할 수 없다. 이 글이 확정하는 것은 구조가 어떻게 동작하는가와 그 구조에서 어긋날 수 있는 자리(§117.4·§117.5)까지다. §103 이 나눈 두 역할(장치 관리 · packet 처리)이 이 글의 척추이고, §104 · §108 · §114 · §115 는 그 척추를 잘못 일반화한 그림 넷을 차례로 지운다.",
|
|
"missing-verification": "이 테스트 Host 에서 잰 값이 하나도 없다. 제3부는 개념 서술이고 §122 가 OQ-1~OQ-7 로 아직 돌리지 않은 확인 목록을 적어 두었다. 이 Host 의 network 가 Bridge 인지 NAT 인지(OQ-1), TAP 이 무엇인지(OQ-2), vhost-net 을 실제로 쓰는지(OQ-3), multi-queue 가 켜져 있는지(OQ-5)를 확인하기 전까지 이 글은 「이 구조에서는 이렇게 동작한다」까지만 말하고 「이 서버가 그 구조다」라고는 말하지 않는다. §116 이 이 테스트 환경에 얹어 그린 펼친 경로도 같은 이유로 확인 대상이며, 후보 대장에 SSOT-116-test-environment-packet-path 로 NEEDS_EVIDENCE 를 적어 두었다.",
|
|
"relations": [
|
|
"reference:bisect-the-packet-path-with-capture-points",
|
|
"reference:verify-the-network-path-before-blaming-the-application",
|
|
"question:vm-network-mode-bridge-nat-or-routed",
|
|
"question:tap-interface-to-vm-mapping",
|
|
"question:is-vhost-net-actually-in-use",
|
|
"question:qemu-backend-vs-vhost-net-on-this-host",
|
|
"question:virtio-net-multi-queue-enabled",
|
|
"question:actual-packet-path-nginx-to-keycloak",
|
|
"question:network-virtualization-cpu-cost-under-load",
|
|
"concept:kvm-vcpu-to-physical-cpu"
|
|
],
|
|
"ssot-assets": [
|
|
"packet-control-and-data-paths"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "network-virtualization/concept/concept-guest-packet-path-to-physical-nic.md",
|
|
"status": "초안",
|
|
"studioId": "5b1de31c-0495-4226-8607-73ed7284b314",
|
|
"assets": [
|
|
"packet-control-and-data-paths"
|
|
],
|
|
"assetFiles": [
|
|
"packet-control-and-data-paths"
|
|
],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"reference": [
|
|
{
|
|
"title": "packet 이 어디서 끊겼는지는 계층마다 capture 해서 가른다",
|
|
"kind": "reference",
|
|
"slug": "bisect-the-packet-path-with-capture-points",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#119-실제-packet-path-추적",
|
|
"final/document.md#118-실제-linux에서-확인할-명령어",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-1",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-2",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-3",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-6",
|
|
"final/document.md#113-offload-최적화",
|
|
"final/document.md#114-linux-bridge가-항상-host-tcp-ip-stack을-거치는-것은-아니다",
|
|
"final/document.md#99-tap의-역할"
|
|
],
|
|
"classification": "§119 가 Host 의 physical NIC · bridge · tap/vnet 과 Guest 의 interface 에 각각 `tcpdump -ni` 를 걸고 어디까지 보이는지로 의심 구간을 좁히는 절차를 적었다. 세 가지 판독을 예로 들었다 — Physical NIC O · Bridge O · TAP X 이면 Host Bridge/TAP mapping 을, TAP O · Guest NIC X 이면 virtio/vhost/Guest NIC 계층을, Guest NIC O · Socket X 이면 Guest routing/firewall/listen 상태를 의심한다. §117.1~.3 이 그 절차가 다루는 증상(연결 오류 · routing 오류 · NAT/firewall 오류)을 대고 §118 이 각 지점에서 무엇을 실행할지를 댄다. 원 프로젝트의 이름을 지워도 규칙이 남는다는 점에서 Case 요약이 아니고, OQ-1 · OQ-2 · OQ-6 셋이 모두 이 규칙 위에서 돌아가므로 세 글에 되풀이하지 않고 한 편으로 두고 가리킨다.",
|
|
"scope": "KVM/QEMU/libvirt 로 만든 VM 이 밖과 통신하지 못하거나 특정 구간에서만 실패할 때, 그리고 실제 packet 경로를 처음 확인할 때. capture 지점은 Host 쪽 physical NIC · bridge · tap/vnet 과 Guest 쪽 interface 넷이고, 그 이름은 `ip link` · `bridge link` · `ip tuntap show` · `virsh domiflist <domain>` · `virsh net-list --all` · `virsh net-dumpxml <network>` · `ip route` · `ip rule` 로 먼저 확정한다(§118). 판독은 어느 지점까지 보였는가로만 하고, 보이지 않은 첫 지점의 앞 구간을 의심 구간으로 삼는다.",
|
|
"exceptions": "offload(GSO · GRO · TSO · Checksum offload)가 켜져 있으면 tcpdump 에 보이는 packet size 와 checksum 이 실제 wire 와 다르게 보일 수 있다(§113 · §117.6) — 크기와 checksum 이 이상하다는 이유만으로 결함으로 읽지 않는다. Bridge 가 단순 L2 forwarding 만 하는 구성에서는 frame 이 Host 의 L3 TCP/IP stack 을 거치지 않으므로 Host 쪽 L3 관측에서 안 보이는 것이 정상이다(§114). 네 지점이 모두 보이는데도 느린 경우는 이 규칙이 답하지 않는다 — 그때는 reference:verify-the-network-path-before-blaming-the-application 으로 넘긴다. 이 저장소에는 아직 이 절차를 실제로 돌린 출력이 없어 규칙은 SSOT 가 서술한 것이고 이 Host 의 출력으로 확인된 것이 아니다.",
|
|
"relations": [
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"reference:verify-the-network-path-before-blaming-the-application",
|
|
"question:vm-network-mode-bridge-nat-or-routed",
|
|
"question:tap-interface-to-vm-mapping",
|
|
"question:actual-packet-path-nginx-to-keycloak"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "network-virtualization/reference/reference-bisect-the-packet-path-with-capture-points.md",
|
|
"status": "초안",
|
|
"studioId": "68a848c7-eaff-4421-ad25-de21673ca40c",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "Keycloak 실험 결과를 애플리케이션 원인으로 읽기 전에 network 계층을 따로 검증한다",
|
|
"kind": "reference",
|
|
"slug": "verify-the-network-path-before-blaming-the-application",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#120-keycloak-refresh-token-실험과의-관계",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-7",
|
|
"final/document.md#124-핵심-claim",
|
|
"final/document.md#89-문서-목적",
|
|
"final/document.md#116-현재-keycloak-k3s-테스트-환경과-연결",
|
|
"final/document.md#102-packet이-keycloak까지-올라오는-과정"
|
|
],
|
|
"classification": "§120 은 Refresh Token 경쟁 자체는 virtio-net 문제가 아니지만 Client → Nginx → VM1/VM2 → K3s → Keycloak → PostgreSQL/Redis 경로를 공유하므로 network virtualization 문제가 실험 결과에 영향을 줄 수 있다고 적고, Node1 요청만 지연 · VM2 packet loss · Host bridge misconfiguration · NAT/conntrack issue · Host CPU contention 으로 인한 vhost 처리 지연을 Refresh Token 경쟁이나 DB lock 으로 오해하지 않도록 network path 를 별도로 검증하라고 못박았다. §124 의 Claim 17 이 같은 것을 한 줄로 적었고, §117.7 은 반대 방향까지 덧붙였다 — vhost-net · QEMU thread · softirq 도 Host CPU 를 쓰므로 network 문제처럼 보이는 것이 CPU scheduling 문제일 수도 있다. reference:bisect-the-packet-path-with-capture-points 가 network 안에서 어느 hop 인지를 가른다면 이 규칙은 그것이 network 인지 아닌지를 가르므로 둘은 다른 물음에 답한다.",
|
|
"scope": "여러 노드가 같은 network path 를 공유하는 실험의 결과를 원인에 귀속할 때. 특히 노드별로만 나타나는 지연, 특정 VM 에서만 나는 실패, 부하 구간에서만 커지는 latency 를 애플리케이션 동시성이나 저장소 lock 으로 결론내기 전에 적용한다. 검증 순서는 경로가 무엇인지(question:actual-packet-path-nginx-to-keycloak) · 그 경로가 끊기지 않았는지(reference:bisect-the-packet-path-with-capture-points) · network 처리가 Host CPU 를 얼마나 쓰는지(question:network-virtualization-cpu-cost-under-load) 셋이고, 셋을 애플리케이션 실험과 같은 시간축에 기록한다.",
|
|
"exceptions": "Keycloak 은 virtqueue · vhost-net · TAP · Bridge · Physical NIC 를 직접 알지 못하고 Guest socket 위에서만 동작하므로(§102) 애플리케이션 로그만으로는 이 계층이 원인인지 아닌지를 가를 수 없다 — 이 규칙은 애플리케이션 쪽 관측을 늘려서는 적용되지 않는다. 반대로 network 계층이 깨끗해도 이 규칙이 애플리케이션 결함을 배제해 주지는 않는다. §116 이 그린 이 환경의 경로는 아직 확인된 것이 아니라 기준 구조를 얹은 그림이고, K3s 안의 CNI · Service · Pod network 는 §116 이 별도 계층으로 미뤄 두어 이 규칙의 범위 밖이다. 이 저장소에는 이 규칙으로 원인을 실제로 가른 실험이 아직 없다.",
|
|
"relations": [
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"reference:bisect-the-packet-path-with-capture-points",
|
|
"question:actual-packet-path-nginx-to-keycloak",
|
|
"question:network-virtualization-cpu-cost-under-load",
|
|
"question:virtualization-layer-saturation-during-refresh"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "network-virtualization/reference/reference-verify-the-network-path-before-blaming-the-application.md",
|
|
"status": "초안",
|
|
"studioId": "ca36f0db-9028-4761-91a8-afb7eb30f2b9",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"question": [
|
|
{
|
|
"title": "이 호스트의 가상 머신 네트워크는 Bridge 인가 NAT 인가 Routing 인가",
|
|
"kind": "question",
|
|
"slug": "vm-network-mode-bridge-nat-or-routed",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#122-open-question-oq-1",
|
|
"final/document.md#96-linux-bridge의-역할",
|
|
"final/document.md#97-routing의-역할",
|
|
"final/document.md#98-nat의-역할",
|
|
"final/document.md#114-linux-bridge가-항상-host-tcp-ip-stack을-거치는-것은-아니다",
|
|
"final/document.md#94-전체-네트워크-계층"
|
|
],
|
|
"known": "§96 은 Linux Bridge 를 Host Kernel 안의 L2 software switch 로, Destination MAC 을 보고 port 를 고르며 MAC learning 을 한다고 적었다. §97 은 Routing 을 L3·IP 기반으로 서로 다른 IP network 를 잇는 것으로, §98 은 NAT 을 packet 의 IP/Port 를 바꾸는 것으로 놓고 VM 이 private subnet 을 쓰면 Host 가 NAT gateway 처럼 동작할 수 있다는 예(192.168.122.10 → 203.0.113.10)를 들었다. 그 셋을 두고 §98 은 VM network 를 분석할 때 Bridge 기반인가 · Routing 기반인가 · NAT 기반인가를 먼저 구분하라고 적었다. §114 는 Bridge 가 단순 L2 forwarding 을 하면 frame 이 Host 의 L3 stack 을 반드시 거치는 것은 아니고 Routing · NAT · Host-local termination · Firewall 이 걸릴 때 L3/Netfilter 경로가 개입한다고 밝혔다. §94 는 이 문서가 기준으로 삼은 구조가 virtio-net + vhost-net + TAP + Linux Bridge 이고 실제 환경은 Bridge · NAT · Routed Network · macvtap · SR-IOV · VFIO passthrough · Open vSwitch · Kubernetes CNI 에 따라 달라질 수 있다고 적었다. 이 Host 의 network 구성이 셋 중 무엇인지는 SSOT 어디에도 적혀 있지 않다.",
|
|
"unknown": "이 Host 의 VM network 가 Bridge 기반인지 NAT 기반인지 Routing 기반인지, libvirt virtual network 로 정의되어 있는지 Host 의 bridge 에 직접 붙어 있는지, 그리고 그 구성에서 VM 을 떠난 frame 이 Host 의 L3/Netfilter 경로를 지나는지 지나지 않는지.",
|
|
"next-verification": "§122 OQ-1 이 적은 다섯을 그대로 돌린다 — `virsh net-list --all` 로 정의된 virtual network 를 열거하고, 나온 이름마다 `virsh net-dumpxml <network>` 로 forward mode 와 bridge 이름을 읽고, `ip link` 로 interface 목록을, `bridge link` 로 어떤 interface 가 어느 bridge 에 enslave 되어 있는지를, `ip route` 로 routing table 을 적는다. 출력은 그대로 증거로 남긴다.",
|
|
"decision-criterion": "세 구성 중 무엇인지가 출력으로 확정되면 닫는다. Bridge 로 나오면 §96 과 §114 의 L2 forwarding 서술이 이 Host 에 적용된다고 적고, NAT 으로 나오면 §98 의 주소 변환이 경로에 들어간다고 적고, Routing 이면 §97 의 L3 결정이 들어간다고 적는다. 어느 쪽이든 그 결과가 question:actual-packet-path-nginx-to-keycloak 의 capture 지점을 정하고, §94 가 기준으로 삼은 구조와 다르면 이 부의 서술 가운데 어느 절이 이 Host 에 적용되지 않는지를 함께 적는다.",
|
|
"relations": [
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"reference:bisect-the-packet-path-with-capture-points",
|
|
"question:tap-interface-to-vm-mapping",
|
|
"question:actual-packet-path-nginx-to-keycloak"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "network-virtualization/question/question-vm-network-mode-bridge-nat-or-routed.md",
|
|
"status": "초안",
|
|
"studioId": "38716d7f-1aa1-48d8-ab4c-3fc505332128",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "VM1 과 VM2 의 TAP/vnet interface 는 무엇이고 어디에 붙어 있는가",
|
|
"kind": "question",
|
|
"slug": "tap-interface-to-vm-mapping",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#122-open-question-oq-2",
|
|
"final/document.md#99-tap의-역할",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-1",
|
|
"final/document.md#118-실제-linux에서-확인할-명령어"
|
|
],
|
|
"known": "§99 는 TAP 이 물리 장치가 아니라 Host Linux Kernel 이 제공하는 가상 Ethernet interface(tap0 · vnet0 같은 이름)이고 VM 의 Ethernet frame 과 Host Linux networking 을 잇는 접점이라고 적었다. 수신은 Linux Bridge → TAP → VM, 송신은 VM → TAP → Linux Bridge 다. 확인 명령으로 `ip link` · `ip tuntap show` · `bridge link` · `virsh domiflist <domain>` 을 들었고 §118 이 같은 명령을 계층별 목록으로 다시 적었다. §117.1 은 TAP/Bridge 연결 오류의 증상으로 VM 외부 통신 불가 · Host ↔ VM 통신 불가 · 특정 VM 만 통신 불가 셋을 들었다. 두 VM 의 interface 이름도 어느 bridge 에 붙어 있는지도 SSOT 에는 없다.",
|
|
"unknown": "VM1 과 VM2 의 Host 쪽 interface 이름이 각각 무엇인지, 각 interface 의 MAC 과 model 이 무엇인지, 그리고 그 둘이 같은 bridge 에 붙어 있는지 서로 다른 곳에 붙어 있는지.",
|
|
"next-verification": "`virsh domiflist vm1` 과 `virsh domiflist vm2` 로 각 VM 의 interface · type · source · model · MAC 을 적고, `ip link` 로 Host 에 실재하는 이름과 대조하고, `bridge link` 로 어느 bridge 의 port 인지를 적는다 (§122 OQ-2). 두 VM 의 결과를 한 표로 남긴다.",
|
|
"decision-criterion": "VM 마다 interface 이름 · MAC · 붙어 있는 bridge 를 한 표로 적으면 닫는다. 이 표는 reference:bisect-the-packet-path-with-capture-points 가 요구하는 capture 지점의 이름을 대는 것이라, 표가 없으면 question:actual-packet-path-nginx-to-keycloak 의 tcpdump 를 어느 interface 에 걸어야 하는지 정할 수 없다. 두 VM 이 서로 다른 bridge 에 붙어 있으면 §117.1 의 「특정 VM 만 통신 불가」를 진단할 때 그 사실을 먼저 본다고 적는다.",
|
|
"relations": [
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"reference:bisect-the-packet-path-with-capture-points",
|
|
"question:vm-network-mode-bridge-nat-or-routed",
|
|
"question:actual-packet-path-nginx-to-keycloak"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "network-virtualization/question/question-tap-interface-to-vm-mapping.md",
|
|
"status": "초안",
|
|
"studioId": "397c4789-0272-449b-86d6-2c4a3a122401",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "이 호스트에서 vhost-net 이 실제로 packet datapath 를 맡고 있는가",
|
|
"kind": "question",
|
|
"slug": "is-vhost-net-actually-in-use",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#122-open-question-oq-3",
|
|
"final/document.md#103-qemu-virtio-device-model의-역할",
|
|
"final/document.md#104-왜-tap-→-vhost-net-→-qemu-→-virtqueue-라고-일반화하면-안-되는가",
|
|
"final/document.md#106-qemu가-userspace인데-packet이-qemu를-안-거칠-수-있는-이유",
|
|
"final/document.md#107-vhost-net-최적화",
|
|
"final/document.md#108-vhost-net은-qemu를-제거하지-않는다",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-4"
|
|
],
|
|
"known": "§103 은 QEMU 의 virtio Device Model 이 Host Userspace 의 QEMU process 안에 있고, 장치 생성/설정/관리와 실제 packet datapath 처리라는 두 역할을 나눠 봐야 한다고 적었다. datapath 는 QEMU backend 를 직접 쓰면 TAP → QEMU virtio backend → virtqueue → Guest 이고, vhost-net 을 쓰면 TAP → vhost-net → virtqueue → Guest 다. §104 는 그래서 `TAP → vhost-net → QEMU → virtqueue` 를 일반적인 경로로 그리면 안 된다고 못박았고, §106 은 장치의 생성/관리 주체와 runtime datapath 처리 주체가 다르다는 것을 CPU 가상화(QEMU 가 vCPU 를 만들어도 Guest instruction 은 KVM/VMX 가 실행한다)에 빗대 설명했다. §108 은 vhost-net 을 써도 QEMU 가 lifecycle · device 생성 · feature negotiation · queue configuration · backend 연결 · device reset 을 계속 맡는다고 적었다. §117.4 는 높은 packet rate 에서 QEMU userspace 가 datapath 를 직접 처리하면 CPU overhead 가 커질 수 있다고 밝혔다. 이 Host 가 둘 중 어느 backend 를 쓰는지는 SSOT 에 없다.",
|
|
"unknown": "이 Host 에서 vhost 커널 모듈이 올라와 있는지, 두 VM 의 domain XML 과 QEMU arguments 가 vhost backend 를 쓰도록 되어 있는지, 그래서 지금 흐르는 packet 의 datapath 가 §103 이 그린 둘 가운데 어느 쪽인지.",
|
|
"next-verification": "`lsmod | grep vhost` 로 모듈 적재를 보고(§122 OQ-3 · §118), 그것만으로는 그 VM 이 쓴다는 뜻이 아니므로 `virsh dumpxml vm1` · `virsh dumpxml vm2` 의 interface 절에서 driver name 을 읽고 QEMU 실행 인자를 함께 확인한다. Host 에 vhost 커널 스레드가 떠 있는지도 process 목록에서 확인해 VM 별로 짝지어 적는다.",
|
|
"decision-criterion": "두 VM 각각에 대해 backend 가 vhost-net 인지 QEMU userspace 인지가 설정과 실행 상태로 확정되면 닫는다. vhost-net 이면 §125 의 「Data Path - vhost-net 사용」 그림이 이 Host 의 경로라고 적고, QEMU backend 면 「Data Path - QEMU backend 사용」 쪽이라고 적는다. 어느 쪽이든 그 결과가 question:qemu-backend-vs-vhost-net-on-this-host 의 비교 대상 하나를 고정하고, 후보 SSOT-116-test-environment-packet-path 의 NEEDS_EVIDENCE 를 푸는 세 확인 가운데 하나가 된다.",
|
|
"relations": [
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"question:qemu-backend-vs-vhost-net-on-this-host",
|
|
"question:network-virtualization-cpu-cost-under-load",
|
|
"question:actual-packet-path-nginx-to-keycloak"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "network-virtualization/question/question-is-vhost-net-actually-in-use.md",
|
|
"status": "초안",
|
|
"studioId": "484fd832-c7d3-413e-897a-97892e4e10ac",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "QEMU backend 와 vhost-net 의 차이가 이 호스트에서 실제로 보이는가",
|
|
"kind": "question",
|
|
"slug": "qemu-backend-vs-vhost-net-on-this-host",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#122-open-question-oq-4",
|
|
"final/document.md#107-vhost-net-최적화",
|
|
"final/document.md#110-data-copy-최적화",
|
|
"final/document.md#111-interrupt-notification-최적화",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-4"
|
|
],
|
|
"known": "§107 은 QEMU userspace 가 packet 마다 I/O 를 처리하면 Host Kernel ↔ QEMU Userspace 전환이 쌓이고 packet rate 가 높아질수록 userspace/kernel transition · scheduling · copy · notification 비용이 커질 수 있다고 적었다. 최적화 방향은 packet 마다 QEMU userspace 가 개입하던 것을 kernel backend 로 옮겨 context switch 와 userspace overhead 를 줄이는 것이다. §110 은 그 구조가 buffer descriptor 로 불필요한 copy 를 줄이도록 설계되어 있지만 항상 zero-copy 라고 일반화하면 안 되고 실제 copy 여부가 kernel version · QEMU version · vhost configuration · offload · NIC capability · packet path · GSO/GRO/TSO 에 따라 달라질 수 있다고 밝혔다. §122 OQ-4 는 비교할 축으로 Latency · Throughput · QEMU CPU · Host CPU · Context Switch · Packet rate 여섯을 적었다. 이 Host 에서 두 backend 를 재 본 값은 없다.",
|
|
"unknown": "같은 workload 를 두 backend 로 돌렸을 때 여섯 축이 실제로 얼마나 달라지는지, 그리고 그 차이가 이 실험의 결과를 다르게 읽어야 할 만큼인지 아니면 측정 흔들림 안인지.",
|
|
"next-verification": "question:is-vhost-net-actually-in-use 로 현재 backend 를 먼저 확정한 뒤, 같은 VM 을 다른 backend 로 바꿔 구성하고 동일한 workload 를 양쪽에 건다. 두 구성 각각에서 §122 OQ-4 의 여섯 축을 같은 시각에 기록한다 — Latency 와 Throughput 과 packet rate 는 부하 도구가 재는 값을, QEMU CPU 와 Host CPU 와 context switch 는 Host 에서 process/스레드 단위로 잰다. 부하를 걸기 전 같은 여섯을 한 번 찍어 기준값으로 둔다.",
|
|
"decision-criterion": "두 구성의 여섯 축을 한 표로 나란히 적으면 닫는다. 차이가 기준값 흔들림 안이면 이 Host 의 packet rate 에서는 backend 선택이 결과를 바꾸지 않는다고 적고 닫는다. 차이가 나면 그 표가 Case 가 되고, 어느 backend 로 실험을 고정할지는 그 Case 뒤에 Decision 으로 넘긴다. 어느 쪽이든 §110 이 말한 조건들(kernel/QEMU version · offload · NIC capability)을 함께 적지 않으면 다른 환경에 재사용할 수 없다.",
|
|
"relations": [
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"question:is-vhost-net-actually-in-use",
|
|
"question:network-virtualization-cpu-cost-under-load",
|
|
"reference:verify-the-network-path-before-blaming-the-application"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "network-virtualization/question/question-qemu-backend-vs-vhost-net-on-this-host.md",
|
|
"status": "초안",
|
|
"studioId": "bded6560-dc8d-473d-966c-5b037bb84c4c",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "이 가상 머신들의 virtio-net 에 multi-queue 가 켜져 있는가",
|
|
"kind": "question",
|
|
"slug": "virtio-net-multi-queue-enabled",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#122-open-question-oq-5",
|
|
"final/document.md#112-multi-queue-최적화",
|
|
"final/document.md#100-virtqueue의-역할",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-5"
|
|
],
|
|
"known": "§112 는 queue 하나만 쓰면 특정 vCPU 나 처리 경로에 부하가 몰릴 수 있고 virtio-net 은 multi-queue 를 쓸 수 있다고 적었다. RX Queue 0~3 을 vCPU 0~3 에 대응시킨 예를 들었고 목적을 packet processing 병렬화 · single queue bottleneck 완화 · multi-core 활용으로 놓았으며, 효과가 workload · CPU affinity · IRQ placement · queue configuration 에 따라 달라진다고 밝혔다. §100 은 virtqueue 가 Guest 와 Host backend 가 I/O buffer 를 주고받는 descriptor 기반 shared queue 이고 네트워크에서는 보통 TX/RX 를 쓴다고 적었다. §117.5 는 single queue bottleneck 의 확인 대상으로 virtio multi-queue · IRQ distribution · per-vCPU CPU usage · RSS/RPS/XPS 를 들었다. 이 VM 들의 queue 수는 SSOT 에 없다.",
|
|
"unknown": "두 VM 의 virtio-net 에 설정된 queue 수가 몇인지, Guest 가 실제로 몇 개를 쓰고 있는지, 그리고 그 queue 들의 IRQ 가 vCPU 에 흩어져 있는지 한 vCPU 에 몰려 있는지.",
|
|
"next-verification": "§122 OQ-5 가 적은 넷을 확인한다 — QEMU/libvirt 의 NIC configuration(`virsh dumpxml` 의 interface 절에 적힌 queue 수), Guest 에서 `ethtool` 로 본 channel 수, 실제 queue count, 그리고 Guest 의 IRQ 분포. 두 VM 을 같은 방식으로 적고 vCPU 수와 나란히 남긴다.",
|
|
"decision-criterion": "VM 마다 설정된 queue 수 · Guest 가 쓰는 queue 수 · IRQ 분포를 한 표로 적으면 닫는다. queue 가 하나로 나오면 §117.5 의 single queue bottleneck 이 이 환경에서 구조적으로 가능하다고 적고, 그것이 실제로 병목인지는 부하를 건 뒤 question:network-virtualization-cpu-cost-under-load 의 per-vCPU 사용량이 답한다. 여럿으로 나오고 IRQ 도 흩어져 있으면 이 축은 현재 실험에서 우선순위를 낮추고 닫는다.",
|
|
"relations": [
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"question:network-virtualization-cpu-cost-under-load",
|
|
"question:is-vhost-net-actually-in-use"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "network-virtualization/question/question-virtio-net-multi-queue-enabled.md",
|
|
"status": "초안",
|
|
"studioId": "1a996f8d-b04e-49d2-88ef-4877e21f7c0e",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "Host Nginx 에서 Keycloak 까지 packet 은 실제로 어디를 지나는가",
|
|
"kind": "question",
|
|
"slug": "actual-packet-path-nginx-to-keycloak",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#122-open-question-oq-6",
|
|
"final/document.md#116-현재-keycloak-k3s-테스트-환경과-연결",
|
|
"final/document.md#119-실제-packet-path-추적",
|
|
"final/document.md#94-전체-네트워크-계층",
|
|
"final/document.md#125-최종-기준-구조"
|
|
],
|
|
"known": "§116 은 이 테스트 환경의 경로를 Client → Host Physical NIC → Host Nginx → Host Network → VM1/VM2 → K3s → Keycloak 으로 놓고, VM network 까지 펼치면 Physical NIC → Host Network Stack/Bridge/Route/NAT → TAP(vm1)/TAP(vm2) → vhost-net → virtqueue → virtio-net → Guest Network Stack → K3s networking → Keycloak 이 된다고 적었다. §94 와 §125 가 같은 경로를 수신·송신 양방향으로 그렸다. §119 는 그 경로를 확인하는 방법으로 Host 에서 physical NIC · bridge · tap/vnet 에, Guest 에서 guest interface 에 각각 `tcpdump -ni` 를 걸고 어디까지 보이는지로 의심 구간을 좁히라고 적었다. §116 이 그린 펼친 경로는 이 Host 에서 확인된 것이 아니라 기준 구조를 이 환경에 얹은 그림이다.",
|
|
"unknown": "Host Nginx 를 지난 packet 이 실제로 어느 bridge 와 어느 TAP 을 거쳐 어느 VM 의 interface 로 들어가는지, 그리고 그 경로가 §116 이 그린 그림과 같은지 다른지. K3s 안에서 Keycloak Pod 까지 가는 구간은 §116 이 별도 계층으로 미뤄 둔 부분이라 이 물음이 닫는 범위 밖이다.",
|
|
"next-verification": "question:vm-network-mode-bridge-nat-or-routed 와 question:tap-interface-to-vm-mapping 으로 capture 지점의 이름을 먼저 확정한다. 그다음 Host 에서 `sudo tcpdump -ni <physical-nic>` · `sudo tcpdump -ni <bridge>` · `sudo tcpdump -ni <tap-or-vnet>` 을, Guest 에서 `sudo tcpdump -ni <guest-interface>` 를 동시에 걸고 Host Nginx 를 통해 Keycloak 요청을 한 번 보낸다 (§122 OQ-6 · §119). 네 지점의 O/X 를 그대로 적는다. offload 가 켜져 있으면 §113 대로 packet size 와 checksum 이 wire 와 다르게 보일 수 있으므로 그 상태를 함께 적는다.",
|
|
"decision-criterion": "네 지점의 capture 결과로 실제 경로를 한 줄로 적으면 닫는다. §116 이 그린 그림과 같으면 그 그림을 이 Host 에서 확인된 경로로 승격하고 후보 SSOT-116-test-environment-packet-path 를 다시 판정한다. 다르면 어느 hop 이 다른지를 적고 §119 의 표대로 그 구간을 의심 구간으로 넘긴다. 중간 지점에서 끊기면 그것은 이 물음의 답이 아니라 장애이므로 reference:bisect-the-packet-path-with-capture-points 로 넘긴다.",
|
|
"relations": [
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"reference:bisect-the-packet-path-with-capture-points",
|
|
"question:vm-network-mode-bridge-nat-or-routed",
|
|
"question:tap-interface-to-vm-mapping",
|
|
"reference:verify-the-network-path-before-blaming-the-application"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "network-virtualization/question/question-actual-packet-path-nginx-to-keycloak.md",
|
|
"status": "초안",
|
|
"studioId": "600a2621-f6d8-42df-9629-7db65011545d",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "부하 중 network 가상화가 지연을 바꿀 만큼 호스트 CPU 를 쓰는가",
|
|
"kind": "question",
|
|
"slug": "network-virtualization-cpu-cost-under-load",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#122-open-question-oq-7",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-7",
|
|
"final/document.md#107-vhost-net-최적화",
|
|
"final/document.md#111-interrupt-notification-최적화",
|
|
"final/document.md#120-keycloak-refresh-token-실험과의-관계"
|
|
],
|
|
"known": "§117.7 은 vhost-net · QEMU thread · softirq 도 Host CPU 를 쓰므로 network 문제처럼 보여도 CPU scheduling 문제일 수 있다고 적었다. §107 은 packet rate 가 높아질수록 userspace/kernel transition · scheduling · copy · notification 비용이 커질 수 있다고, §111 은 packet 마다 과도한 interrupt/notification 이 나면 overhead 가 커져 batching 과 interrupt moderation 이 중요하다고 밝혔다. §120 은 Node1 요청만 지연 · VM2 packet loss · Host bridge misconfiguration · NAT/conntrack issue · Host CPU contention 으로 인한 vhost 처리 지연을 Refresh Token 경쟁이나 DB lock 으로 오해하지 않도록 network path 를 따로 검증하라고 적었다. §122 OQ-7 은 관찰 대상으로 QEMU CPU · vhost thread · softirq · Host CPU · Guest CPU · network latency 여섯을 적었다. 이 여섯 가운데 vhost thread 와 softirq 는 제1부·제2부 어디에도 나오지 않아 CPU 가상화 쪽 물음이 재는 지표와 겹치지 않는다.",
|
|
"unknown": "Keycloak load test 구간에서 vhost 커널 스레드와 softirq 가 Host CPU 를 얼마나 쓰는지, 그 사용량이 QEMU vCPU thread 사용량과 어떻게 나뉘는지, 그리고 그것이 network latency 와 같은 시간축에서 함께 움직이는지.",
|
|
"next-verification": "Keycloak load test 를 돌리면서 §122 OQ-7 의 여섯을 같은 시각에 기록한다 — Host 에서 QEMU process 와 vhost 커널 스레드의 CPU 사용량을 스레드 단위로, softirq 시간을, Host 전체 CPU 를 적고, 각 Guest 에서 Guest CPU 를, 부하 도구에서 network latency 를 적는다. 부하를 걸기 전 같은 여섯을 한 번 찍어 기준값으로 둔다. Guest 의 steal time 과 QEMU vCPU thread 쪽은 question:virtualization-layer-saturation-during-refresh 가 이미 재므로 같은 실험 구간에서 두 기록을 함께 남긴다.",
|
|
"decision-criterion": "부하 구간의 vhost thread 와 softirq 사용량이 기준값과 다르지 않고 network latency 도 움직이지 않으면, 이 부하 수준에서는 network 가상화가 Host CPU 를 지연에 영향을 줄 만큼 쓰지 않는다고 적고 닫는다. 셋 중 하나라도 움직이면 그 표가 Case 가 되고, vhost thread 를 어느 CPU 에 둘지나 multi-queue 를 켤지는 그 Case 뒤에 Decision 으로 넘긴다. 어느 쪽이든 이 결과가 없으면 §120 이 경고한 오귀속(network 지연을 Refresh Token 경쟁으로 읽는 것)을 배제했다고 말할 수 없다.",
|
|
"relations": [
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"reference:verify-the-network-path-before-blaming-the-application",
|
|
"question:qemu-backend-vs-vhost-net-on-this-host",
|
|
"question:virtio-net-multi-queue-enabled",
|
|
"question:virtualization-layer-saturation-during-refresh"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "network-virtualization/question/question-network-virtualization-cpu-cost-under-load.md",
|
|
"status": "초안",
|
|
"studioId": "79ac61d6-cbe6-483b-9961-02ca8c02b5c2",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"decision": []
|
|
}
|
|
},
|
|
"storage-virtualization": {
|
|
"topic": "storage-virtualization",
|
|
"title": "스토리지 가상화 — Guest 의 write() 와 fsync() 가 Host 물리 NVMe 까지 가는 길",
|
|
"readerQuestion": "Guest 애플리케이션의 write() 와 fsync() 는 어느 계층을 거쳐 Host 의 물리 NVMe 까지 가고, 느리거나 데이터가 남지 않았을 때 그것이 storage 가상화의 어느 자리 문제인지 무엇을 보고 가르는가?",
|
|
"kinds": {
|
|
"case": [],
|
|
"concept": [
|
|
{
|
|
"title": "Guest 의 write() 가 virtqueue 에 실리기까지 — VFS · Filesystem · Page Cache · Block Layer · virtio-blk",
|
|
"kind": "concept",
|
|
"slug": "guest-block-io-path-to-virtqueue",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#128-전체-구조",
|
|
"final/document.md#129-guest-application-read-write-에서-시작",
|
|
"final/document.md#130-vfs-공통-파일-인터페이스-계층",
|
|
"final/document.md#131-filesystem-ext4-xfs-파일-세계를-block-공간에-배치",
|
|
"final/document.md#132-inode",
|
|
"final/document.md#133-page-cache-write-가-바로-ssd-write는-아니다",
|
|
"final/document.md#134-guest-block-i-o-layer",
|
|
"final/document.md#135-dev-vda-guest가-보는-가상-block-device",
|
|
"final/document.md#136-dev-vda-와-filesystem-관계",
|
|
"final/document.md#137-virtio-blk-guest의-가상-block-device-driver",
|
|
"final/document.md#138-virtio-blk와-virtqueue",
|
|
"final/document.md#139-virtqueue의-실제-의미",
|
|
"final/document.md#166-storage-i-o-completion",
|
|
"final/document.md#127-문서-목적",
|
|
"final/document.md#172-storage-virtualization-canonical-flow",
|
|
"final/document.md#173-network-virtualization과-비교",
|
|
"final/document.md#177-최종-요약"
|
|
],
|
|
"basis-version": "QEMU/KVM 의 virtio-blk frontend + virtqueue 구성. Guest 는 ext4 나 XFS 를 쓰고 buffered I/O 로 `write()` 하는 경우를 기준으로 삼는다 — §130 이 ext4 와 XFS 를 같은 자리에 놓았고 §133 이 「일반적인 buffered I/O」를 전제로 Page Cache 를 설명했다. SSOT 가 커널·QEMU·libvirt 버전을 한 번도 적지 않아 특정 버전에 고정하지 않는다. 이 글이 멈추는 경계도 SSOT 가 그었다 — §127 이 qcow2 내부 L1/L2 table · blk-mq tag allocator · NVMe submission/completion queue 를 별도 문서로 미뤘고, §134 도 `bio` · request · queue · `blk-mq` 가 실제로 존재한다고만 적고 내부로 들어가지 않는다.",
|
|
"classification": "제4부에서 경로가 시작하는 구간이다. §129 부터 §139 까지가 한 줄기로 이어진다 — 애플리케이션은 `write(fd, buffer, size);` 를 부르고, VFS 가 그 fd 를 어느 filesystem 구현으로 보낼지 정하고, ext4/XFS 가 파일 세계를 block 공간에 배치하고, Page Cache 가 dirty page 로 받아 두었다가 writeback 하고, Block I/O Layer 가 그것을 READ/WRITE/FLUSH/DISCARD 요청으로 바꾸고, `/dev/vda` 를 통해 virtio-blk driver 가 Virtio block request 로 구성해 virtqueue 에 게시한다. 중간을 잘라 내면 인과가 끊긴다 — Page Cache 만 떼면 `write()` 성공이 왜 SSD 영속화가 아닌지 말할 수 없고, virtqueue 만 떼면 무엇과 무엇 사이의 queue 인지 말할 수 없다. §128 과 §172 와 §177 이 그린 canonical flow 의 Guest 절반이 이 글이고, 나머지 셋(backend 형태 · 완료의 뜻 · Host block stack)은 그 그림의 어느 구간을 맡는지로 갈린다. §166 이 completion 도 같은 virtqueue 로 돌아온다고 적어 이 글의 마지막 절이 된다.",
|
|
"missing-verification": "이 테스트 Host 에서 잰 값이 하나도 없다. §175 의 OQ-1 이 이 Host 의 `/dev/vda` 가 무엇에 붙어 있는지를 아직 확인하지 않았다고 적어 두었고, Guest 의 filesystem 이 ext4 인지 XFS 인지도 SSOT 어디에도 없다. 이 글은 「이 구조에서는 이렇게 동작한다」까지만 말하고 「이 서버가 그 구조다」라고는 말하지 않는다. 실제 요청 수나 latency 를 재려면 question:guest-fsync-latency-vs-host-storage-latency 가 먼저 돌아야 한다.",
|
|
"relations": [
|
|
"concept:qemu-block-backend-forms",
|
|
"concept:write-completion-is-not-durability",
|
|
"concept:host-block-stack-under-the-vm",
|
|
"reference:confirm-the-disk-backend-on-the-host",
|
|
"question:vm-disk-backend-mapping",
|
|
"question:guest-fsync-latency-vs-host-storage-latency",
|
|
"concept:guest-packet-path-to-physical-nic"
|
|
],
|
|
"ssot-assets": [
|
|
"guest-block-io-to-virtqueue"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/concept/concept-guest-block-io-path-to-virtqueue.md",
|
|
"status": "초안",
|
|
"studioId": "3b05c1f0-13e9-414a-97a6-2078fb4bab26",
|
|
"assets": [
|
|
"guest-block-io-to-virtqueue"
|
|
],
|
|
"assetFiles": [
|
|
"guest-block-io-to-virtqueue"
|
|
],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "/dev/vda 뒤에 있는 것 — qcow2 · RAW · Host block device",
|
|
"kind": "concept",
|
|
"slug": "qemu-block-backend-forms",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#140-vm-boundary를-넘으면-qemu가-등장",
|
|
"final/document.md#141-qemu가-물리-ssd를-직접-제어하는-것은-아니다",
|
|
"final/document.md#142-qcow2-host에서는-파일-guest에서는-디스크",
|
|
"final/document.md#143-qcow2-virtual-size와-실제-host-사용량",
|
|
"final/document.md#144-raw-image",
|
|
"final/document.md#145-host-block-device를-직접-backend로-사용-가능",
|
|
"final/document.md#146-실제-연결-확인",
|
|
"final/document.md#128-전체-구조",
|
|
"final/document.md#172-storage-virtualization-canonical-flow",
|
|
"final/document.md#174-핵심-claim"
|
|
],
|
|
"basis-version": "QEMU block backend 세 형태 — qcow2 파일 · RAW 파일 · Host block device — 를 libvirt 로 정의한 VM 기준. §142 는 `/var/lib/libvirt/images/keycloak-node1.qcow2` 를, §145 는 `/dev/nvme0n1p3` 를 예로 들었다. SSOT 가 QEMU·libvirt 버전을 적지 않아 특정 버전에 고정하지 않고, §127 이 qcow2 내부 L1/L2 table 을 범위 밖으로 두었으므로 이 글은 backend 형태의 구분과 그 구분이 무엇을 바꾸는지까지만 설명한다.",
|
|
"classification": "§128 이 핵심 문장으로 뽑은 것이 이 글의 물음이다 — Guest 는 `/dev/vda` 를 실제 block device 처럼 보지만 Host 에서는 그 disk 가 qcow2 파일일 수도, RAW 파일일 수도, 실제 block device 일 수도 있다. 세 갈래가 각각 무엇을 바꾸는지가 SSOT 에 따로 적혀 있다 — qcow2 는 Copy-on-Write · sparse allocation · snapshot 에 유리하지만 metadata/mapping 처리가 있고(§144), virtual size 와 Host 실제 할당량이 갈리며(§143), 파일 backend 이면 Guest filesystem 아래에 Host filesystem/storage stack 이 한 번 더 놓인다(§141). C1 과 갈라 두는 이유는 이 구간에서만 경로가 셋으로 분기하기 때문이다 — 경로를 그리는 글에 이 분기를 넣으면 세 갈래 각각의 결과가 한 글의 각주가 된다. §144 가 「RAW = 무조건 빠름 / qcow2 = 무조건 느림」으로 일반화하지 말라고 못박은 것이 이 글이 닫는 자리다.",
|
|
"missing-verification": "이 Host 의 backend 가 셋 중 무엇인지 SSOT 에 없다. §175 의 OQ-1 · OQ-2 · OQ-3 · OQ-5 넷이 그 확인을 아직 돌리지 않은 상태로 적혀 있고, §143 이 든 100 GB/3GB 나 1GB→10GB→40GB 는 예시 값이지 이 장비에서 잰 값이 아니다. 성능 비교도 이 저장소에 없다 — §144 가 cache mode · storage backend · workload pattern · queue depth · snapshot chain · underlying filesystem · physical device 에 따라 달라진다고만 적었다.",
|
|
"relations": [
|
|
"concept:guest-block-io-path-to-virtqueue",
|
|
"concept:write-completion-is-not-durability",
|
|
"concept:host-block-stack-under-the-vm",
|
|
"reference:confirm-the-disk-backend-on-the-host",
|
|
"question:vm-disk-backend-mapping",
|
|
"question:disk-image-format-and-actual-host-usage",
|
|
"question:host-block-device-under-the-disk-image"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/concept/concept-qemu-block-backend-forms.md",
|
|
"status": "초안",
|
|
"studioId": "892b6087-8961-486c-b4d9-d3dd46bc2605",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "완료라는 말의 네 가지 뜻 — write · writeback · flush · 전원 장애 생존",
|
|
"kind": "concept",
|
|
"slug": "write-completion-is-not-durability",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#147-vm에서는-page-cache가-두-번-나타날-수-있다",
|
|
"final/document.md#148-write-완료와-영속화는-다르다",
|
|
"final/document.md#149-direct-i-o",
|
|
"final/document.md#150-fsync-가-필요한-이유",
|
|
"final/document.md#151-flush",
|
|
"final/document.md#152-가장-위험한-상황-거짓-완료",
|
|
"final/document.md#153-qemu-cache-mode",
|
|
"final/document.md#154-cache=none",
|
|
"final/document.md#155-cache=writeback",
|
|
"final/document.md#156-writeback-=-위험-이라고-단정하면-안-되는-이유",
|
|
"final/document.md#157-device-side-cache",
|
|
"final/document.md#170-postgresql-예시-wal과-durability",
|
|
"final/document.md#133-page-cache-write-가-바로-ssd-write는-아니다",
|
|
"final/document.md#174-핵심-claim"
|
|
],
|
|
"basis-version": "Linux `O_DIRECT` 와 QEMU/libvirt disk 의 `cache=none` · `cache=writeback` 두 설정 기준. §153 이 「대표적으로 볼 수 있는 설정」으로 이 둘만 들었으므로 다른 cache mode 값은 이 글의 범위가 아니고, SSOT 가 QEMU 버전을 적지 않아 버전별 차이도 다루지 않는다. §151 이 실제 ordering/durability semantics 는 더 복잡하다고 스스로 밝힌 만큼 이 글은 WRITE 와 FLUSH 의 구분까지만 말한다.",
|
|
"classification": "§148 이 네 단계를 한 줄로 세웠다 — `write()` 완료 ≠ writeback 완료 ≠ fsync/flush 완료 ≠ 전원 장애에도 안전한 durability. 그 네 자리마다 VM 에서만 생기는 사정이 붙는다. Page Cache 가 Guest 와 Host 양쪽에 두 번 나타날 수 있고(§147), Direct I/O 는 Page Cache 우회이지 자동 durability 보장이 아니며(§149), `cache=none` 은 이중 caching 을 줄이지만 즉시 durable media 반영을 뜻하지 않고(§154), `cache=writeback` 이 Guest 의 `fsync()`/FLUSH 를 무시한다는 뜻도 아니다(§155). Host Page Cache 를 우회해도 device 쪽 volatile write cache 가 남는다(§157). 이 글이 닫는 것은 §152 다 — Guest 에게 `FLUSH 완료` 라고 응답했는데 실제로는 Host RAM 에만 있는 상태는 성능 문제가 아니라 durability contract 가 깨지는 correctness 문제다. C1·C2·C4 가 경로를 말한다면 이 글은 그 경로 위에서 「완료」라는 응답이 무엇을 보장하는지를 말하므로 물음이 다르다. §170 의 PostgreSQL WAL 예시가 그 보장이 깨질 때 무엇이 어긋나는지를 보여 준다.",
|
|
"missing-verification": "이 Host 의 disk cache mode 가 무엇인지 SSOT 에 없다 — §175 OQ-4 가 그 확인을 적어 두었다. 거짓 완료(§152)가 이 환경에서 실제로 일어나는지도, device 가 power-loss protection 을 갖췄는지도 재지 않았다. §157 이 device flush/FUA semantics 와 power-loss protection 여부가 「중요할 수 있다」고만 적은 자리가 그대로 남아 있다. `fsync()` latency 값은 question:guest-fsync-latency-vs-host-storage-latency 가 잰 뒤에야 이 글에 들어온다.",
|
|
"relations": [
|
|
"concept:guest-block-io-path-to-virtqueue",
|
|
"concept:qemu-block-backend-forms",
|
|
"concept:host-block-stack-under-the-vm",
|
|
"reference:a-speedup-that-removed-durability-is-not-an-optimization",
|
|
"question:qemu-disk-cache-mode",
|
|
"question:guest-fsync-latency-vs-host-storage-latency"
|
|
],
|
|
"ssot-assets": [
|
|
"write-completion-boundaries"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/concept/concept-write-completion-is-not-durability.md",
|
|
"status": "초안",
|
|
"studioId": "8fafaeea-8746-4537-87a4-679a3a91aa4c",
|
|
"assets": [
|
|
"write-completion-boundaries"
|
|
],
|
|
"assetFiles": [
|
|
"write-completion-boundaries"
|
|
],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "VM 아래에 한 번 더 있는 Host block stack — blk-mq · I/O Scheduler · NVMe",
|
|
"kind": "concept",
|
|
"slug": "host-block-stack-under-the-vm",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#158-host-block-layer",
|
|
"final/document.md#159-여러-vm이-하나의-nvme를-공유하면",
|
|
"final/document.md#160-blk-mq-multi-queue-block-layer",
|
|
"final/document.md#161-i-o-scheduler",
|
|
"final/document.md#162-none",
|
|
"final/document.md#163-실제-i-o-scheduler-확인",
|
|
"final/document.md#164-nvme-driver와-physical-device",
|
|
"final/document.md#165-nvme와-ssd-구분",
|
|
"final/document.md#167-storage-contention",
|
|
"final/document.md#141-qemu가-물리-ssd를-직접-제어하는-것은-아니다",
|
|
"final/document.md#172-storage-virtualization-canonical-flow"
|
|
],
|
|
"basis-version": "현대 Linux 의 `blk-mq` 기반 block layer 와 NVMe driver 기준. scheduler 는 §161 이 든 `none` · `mq-deadline` · `bfq` 셋이고 §163 의 확인 예시 출력은 `[none] mq-deadline` 이다. SSOT 가 커널 버전을 적지 않아 고정하지 않고, §127 이 blk-mq tag allocator 와 NVMe submission/completion queue 를 별도 문서로 미뤘으므로 이 글은 queue 가 여럿이라는 구조와 dispatch 정책이 있다는 데까지만 설명한다.",
|
|
"classification": "§158 의 한 문장이 이 글이 존재하는 이유다 — Host Block Layer 는 그 I/O 가 VM 안 PostgreSQL 에서 시작했는지 Host process 에서 시작했는지를 본질적으로 구분해서 처리하는 계층이 아니고, 모두 Host block request 다. 그래서 VM 두 대와 Nginx 와 Host 기타 프로세스의 요청이 한 queue 로 들어오고(§159), `blk-mq` 가 CPU 마다 queue 를 두어 병렬로 처리하고(§160), I/O Scheduler 가 들어온 순서 그대로 dispatch 하지 않을 수 있다(§161). 그 결과가 §167 의 Storage Contention 이고 SSOT 는 그것을 CPU Contention 과 다른 자원 경쟁으로 못박았다 — 하나는 Host logical CPU 실행 시간 경쟁이고 하나는 IOPS/bandwidth/queue/device 처리시간 경쟁이다. C2 가 backend 형태를 말한다면 이 글은 그 backend 아래에서 요청이 어떻게 줄을 서고 누구와 섞이는지를 말한다. §165 의 SSD 와 NVMe 구분(저장장치 종류 대 PCIe 기반 protocol/interface)이 이 계층의 이름을 정리한다.",
|
|
"missing-verification": "이 Host 의 I/O Scheduler 도, VM 들이 같은 물리 장치를 쓰는지도 SSOT 에 없다 — §175 의 OQ-5 · OQ-6 · OQ-7 이 그 확인을 적어 두었다. §159 가 그린 VM1·VM2·Nginx 동시 I/O 도 이 환경에서 실제로 겹치는지 재지 않았고, §167 의 「VM1 에서 대량 I/O 가 발생하면 VM2 의 storage latency 가 증가할 수 있다」는 구조적 가능성이지 관측이 아니다.",
|
|
"relations": [
|
|
"concept:guest-block-io-path-to-virtqueue",
|
|
"concept:qemu-block-backend-forms",
|
|
"concept:write-completion-is-not-durability",
|
|
"reference:read-storage-before-blaming-cpu-for-latency",
|
|
"question:host-block-device-under-the-disk-image",
|
|
"question:host-io-scheduler",
|
|
"question:vm1-storage-load-vs-vm2-latency"
|
|
],
|
|
"ssot-assets": [
|
|
"vms-sharing-one-nvme"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/concept/concept-host-block-stack-under-the-vm.md",
|
|
"status": "초안",
|
|
"studioId": "ecaaa37d-7b33-4cbf-9bc5-4c6caad14ee5",
|
|
"assets": [
|
|
"vms-sharing-one-nvme"
|
|
],
|
|
"assetFiles": [
|
|
"vms-sharing-one-nvme"
|
|
],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"reference": [
|
|
{
|
|
"title": "지연 원인을 CPU 로 읽기 전에 storage 계층을 따로 잰다",
|
|
"kind": "reference",
|
|
"slug": "read-storage-before-blaming-cpu-for-latency",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#168-cpu가-정상이어도-storage-때문에-느릴-수-있다",
|
|
"final/document.md#167-storage-contention",
|
|
"final/document.md#169-storage-관측-명령어",
|
|
"final/document.md#174-핵심-claim",
|
|
"final/document.md#177-최종-요약"
|
|
],
|
|
"classification": "§168 이 규칙을 그대로 적었다 — PostgreSQL 이 storage completion 을 기다리고 있으면 CPU usage 가 높지 않을 수도 있어서 CPU 30% 인데 request latency 2초 가 가능하고, 따라서 CPU 지표만으로 latency 원인을 판단하면 안 된다. §167 이 그 앞에서 CPU Contention 과 Storage Contention 을 다른 자원 경쟁으로 갈라 두었고, §169 가 무엇을 볼지의 도구를 대고, §177 이 CPU usage 만 보지 말고 함께 볼 여덟 가지를 나열한다. §174 Claim 6 이 같은 것을 한 줄로 접었다 — Storage 성능은 Guest 내부만으로 결정되지 않는다. 원 프로젝트의 이름을 지워도 규칙이 남으므로 Case 요약이 아니고, 제2부의 reference:memory-symptom-needs-layer-separation 이 메모리 증상 안에서 다섯 갈래를 가른다면 이 규칙은 CPU 냐 storage 냐를 가르므로 답하는 물음이 다르다.",
|
|
"scope": "요청 latency 나 처리량 저하의 원인을 자원에 귀속하려는 모든 판독. 특히 CPU usage 가 낮은데 latency 가 큰 구간, DB 가 낀 경로, VM 여러 대가 한 물리 장치를 쓰는 환경에 적용한다. 같은 시간축에 남길 것은 §177 이 적은 여덟이다 — Guest I/O latency · Host I/O queue · Host storage latency · QEMU backend · cache mode · I/O Scheduler · NVMe · 다른 VM 의 Storage load. 도구는 §169 가 댄다 — Guest 에서 `lsblk` · `mount` · `df -h` · `cat /proc/mounts` · `iostat -xz 1`, Host 에서 `virsh domblklist <VM_NAME>` · `qemu-img info <disk-image>` · `lsblk` · `cat /sys/block/<device>/queue/scheduler` · `iostat -xz 1` · `iotop`.",
|
|
"exceptions": "이 규칙은 어느 자원인지를 좁힐 뿐 원인을 확정하지 않는다. storage 지표가 깨끗해도 CPU 쪽이 아니라는 뜻은 아니고, 제1부가 적은 vCPU contention 과 steal time 은 이 규칙 밖에서 따로 본다. `iostat -xz 1` 이 보여 주는 device utilization 성격의 지표는 NVMe 처럼 병렬성이 큰 장치에서 포화도로 그대로 읽히지 않는다 — §160 이 NVMe 가 높은 queue depth 와 병렬성을 지원한다고 적은 것이 그 이유다. Host 에서 잰 값은 어느 VM 의 I/O 인지 구분해 주지 않는다(§158) — VM 별로 가르려면 §169 의 `iotop` 이나 VM 쪽 관측을 같은 시각에 함께 찍어야 한다. 이 저장소에는 이 규칙으로 원인을 실제로 가른 측정이 아직 없다.",
|
|
"relations": [
|
|
"concept:host-block-stack-under-the-vm",
|
|
"concept:guest-block-io-path-to-virtqueue",
|
|
"reference:confirm-the-disk-backend-on-the-host",
|
|
"question:vm1-storage-load-vs-vm2-latency",
|
|
"question:guest-fsync-latency-vs-host-storage-latency",
|
|
"reference:memory-symptom-needs-layer-separation"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/reference/reference-read-storage-before-blaming-cpu-for-latency.md",
|
|
"status": "초안",
|
|
"studioId": "b1cda8c4-d31d-4ae4-a4d9-4103a0ab3109",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "빨라진 구성이 durability contract 를 지운 것은 아닌지 먼저 가른다",
|
|
"kind": "reference",
|
|
"slug": "a-speedup-that-removed-durability-is-not-an-optimization",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#171-성능과-durability의-trade-off",
|
|
"final/document.md#152-가장-위험한-상황-거짓-완료",
|
|
"final/document.md#156-writeback-=-위험-이라고-단정하면-안-되는-이유",
|
|
"final/document.md#148-write-완료와-영속화는-다르다",
|
|
"final/document.md#157-device-side-cache",
|
|
"final/document.md#150-fsync-가-필요한-이유",
|
|
"final/document.md#170-postgresql-예시-wal과-durability",
|
|
"final/document.md#174-핵심-claim"
|
|
],
|
|
"classification": "§171 이 규칙 문장을 그대로 적었다 — `fsync()` 를 없애서 빨라졌다면 그것이 최적화가 아니라 durability contract 를 제거한 것일 수 있다. 그 판단이 왜 성능 문제가 아닌지는 §152 가 댄다. Guest 에게 `FLUSH 완료` 라고 응답했는데 실제로는 Host RAM 에만 있으면 PostgreSQL 은 durability 가 확보되었다고 판단하고, 직후 Host 전원이 나가면 RAM 의 data 가 사라진다 — 성능 문제가 아니라 correctness 문제다. §156 이 반대 방향의 오독까지 막는다. writeback 이라는 이름만 보고 위험하다고 단정하는 것이 아니라 Guest 의 flush/fsync semantics 가 전체 backend/storage stack 에서 올바르게 보존되는지를 보는 것이 정확한 표현이다. 규칙이 향하는 대상이 설정 선택과 성능 개선의 평가라 개념 본문의 각주로 두면 다음 프로젝트가 다시 찾지 못한다.",
|
|
"scope": "storage 관련 설정이나 코드를 바꿔 write/commit latency 가 개선됐을 때, 그리고 cache mode · Direct I/O · `fsync()` 호출 · filesystem mount option 을 고르기 전에 적용한다. 확인 순서는 무엇이 빨라졌는지가 아니라 어느 경계가 사라졌는지다 — §148 의 네 단계(`write()` 완료 · writeback 완료 · fsync/flush 완료 · 전원 장애에도 안전한 상태) 중 어디까지 보장되는지를 먼저 적고, §156 이 그린 chain(Guest 가 요구한 durability → Guest Filesystem → Guest Block Layer → virtio → QEMU/backend → Host Storage → Device)에서 그 의미가 끊기는 자리가 있는지를 본다. DB workload 에서는 §170 처럼 애플리케이션이 어떤 durability protocol 을 쓰는지도 함께 적는다.",
|
|
"exceptions": "durability 를 실제로 포기해도 되는 데이터가 있다 — 다시 만들 수 있는 캐시나 파생 파일이 그렇고, 그때는 빨라진 것이 정당한 선택이다. 이 규칙이 요구하는 것은 포기하지 말라는 것이 아니라 포기한 사실을 적으라는 것이다. 반대로 `cache=none` 이나 Direct I/O 를 골랐다는 사실만으로 durability 가 확보되지도 않는다 — Host Page Cache 우회는 즉시 durable media 반영이 아니고(§149 · §154), device 쪽 volatile write cache 가 그 뒤에 또 있다(§157). §151 이 실제 ordering/durability semantics 는 더 복잡하다고 밝혔으므로 이 규칙으로 특정 설정이 안전하다고 결론내지 않는다. 이 저장소에는 durability 를 실제로 재 본 실험이 없다 — 규칙은 SSOT 가 서술한 것이고 이 Host 에서 확인된 것이 아니다.",
|
|
"relations": [
|
|
"concept:write-completion-is-not-durability",
|
|
"concept:qemu-block-backend-forms",
|
|
"question:qemu-disk-cache-mode",
|
|
"question:guest-fsync-latency-vs-host-storage-latency",
|
|
"reference:read-storage-before-blaming-cpu-for-latency"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/reference/reference-a-speedup-that-removed-durability-is-not-an-optimization.md",
|
|
"status": "초안",
|
|
"studioId": "decbab8d-9c98-45c6-b5b8-e6162e407299",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "Guest 안에서 본 disk 로 backend 를 단정하지 않는다",
|
|
"kind": "reference",
|
|
"slug": "confirm-the-disk-backend-on-the-host",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#145-host-block-device를-직접-backend로-사용-가능",
|
|
"final/document.md#146-실제-연결-확인",
|
|
"final/document.md#143-qcow2-virtual-size와-실제-host-사용량",
|
|
"final/document.md#144-raw-image",
|
|
"final/document.md#135-dev-vda-guest가-보는-가상-block-device",
|
|
"final/document.md#142-qcow2-host에서는-파일-guest에서는-디스크",
|
|
"final/document.md#169-storage-관측-명령어"
|
|
],
|
|
"classification": "§145 가 규칙 문장을 그대로 적었다 — 「Guest 에 `/dev/vda` 가 있다」는 정보만으로 backend 구조를 알 수 없다. §135 도 Guest Linux 가 `/dev/vda` 를 하나의 block device 로 인식하지만 그것이 Host 의 실제 SSD 라는 뜻은 아니라고 적었고, §142 는 같은 대상을 Host 관점(파일 하나)과 Guest 관점(디스크)에서 보면 둘 다 맞다고 놓았다. §146 이 그 확정 절차를 댄다 — Guest 에서 `lsblk`, Host 에서 `virsh domblklist <VM_NAME>` 로 Target 과 Source 를 잇는다. 이 규칙이 없으면 §143 의 virtual size 와 §144 의 성능 일반화가 둘 다 잘못된 전제 위에서 읽힌다. 제3부의 reference:bisect-the-packet-path-with-capture-points 가 network 경로를 확정하는 자리에 있는 것과 같은 위치이고, OQ-1 · OQ-2 · OQ-3 · OQ-5 넷이 모두 이 규칙 위에서 돌아가므로 네 글에 되풀이하지 않고 한 편으로 두고 가리킨다.",
|
|
"scope": "VM 의 disk 성능·용량·durability 를 판단하기 전에, 그리고 Guest 안에서 잰 storage 수치를 해석하기 전에 적용한다. 확정할 것은 셋이다 — `/dev/vda` 가 붙은 Source 가 무엇인지(`virsh domblklist <VM_NAME>`), 그것이 qcow2 인지 RAW 인지 Host block device 인지(`qemu-img info <image>`), 그리고 파일이라면 어느 Host filesystem 과 어느 block device 위에 있는지(§169 의 `lsblk`). 용량을 볼 때는 `qemu-img info` 의 virtual size 와 실제 allocation 을 구분해서 본다(§143).",
|
|
"exceptions": "backend 가 Host block device 라면(§145) qcow2 쪽 확인 — virtual size 와 실제 할당량의 차이, mapping/metadata 처리 — 은 적용되지 않는다. 반대로 backend 를 확정했다고 성능이 예측되지도 않는다. §144 가 「RAW = 무조건 빠름 / qcow2 = 무조건 느림」으로 일반화하지 말라고 못박고 cache mode · storage backend · workload pattern · queue depth · snapshot chain · underlying filesystem · physical device 를 함께 들었으므로, 이 규칙은 무엇 위에서 재고 있는지를 확정할 뿐 결과를 대신 말하지 않는다. 이 저장소에는 아직 이 절차를 실제로 돌린 출력이 없다.",
|
|
"relations": [
|
|
"concept:qemu-block-backend-forms",
|
|
"concept:guest-block-io-path-to-virtqueue",
|
|
"question:vm-disk-backend-mapping",
|
|
"question:disk-image-format-and-actual-host-usage",
|
|
"question:host-block-device-under-the-disk-image",
|
|
"reference:read-storage-before-blaming-cpu-for-latency"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/reference/reference-confirm-the-disk-backend-on-the-host.md",
|
|
"status": "초안",
|
|
"studioId": "274c5636-33f6-4c19-a54b-d2a96af31289",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"question": [
|
|
{
|
|
"title": "이 호스트의 /dev/vda 는 어떤 backend 에 붙어 있는가",
|
|
"kind": "question",
|
|
"slug": "vm-disk-backend-mapping",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-1",
|
|
"final/document.md#135-dev-vda-guest가-보는-가상-block-device",
|
|
"final/document.md#145-host-block-device를-직접-backend로-사용-가능",
|
|
"final/document.md#146-실제-연결-확인",
|
|
"final/document.md#140-vm-boundary를-넘으면-qemu가-등장"
|
|
],
|
|
"known": "§135 는 virtio-blk 를 쓰는 VM 에서 `/dev/vda` · `/dev/vdb` 같은 이름이 보이고 Guest Linux 가 그것을 하나의 block device 로 인식하지만 그것이 Host 의 실제 SSD 라는 뜻은 아니라고 적었다. §145 는 backend 가 반드시 파일일 필요도 없어서 `/dev/vda` 아래에 qcow2 file · RAW file · Host block device 셋이 올 수 있고, 그래서 「Guest 에 `/dev/vda` 가 있다」는 정보만으로 backend 구조를 알 수 없다고 못박았다. §140 은 VM 경계를 넘으면 QEMU 의 virtio-blk Device Model 과 Block Backend 가 그 연결을 맡는다고 적었고, §146 은 확인 방법으로 Guest 의 `lsblk` 와 Host 의 `virsh domblklist <VM_NAME>` 를 들고 Target/Source 표 예시(`vda` → `/var/lib/libvirt/images/vm1.qcow2`)를 보였다. 이 Host 의 VM 이 어떤 Source 에 붙어 있는지는 SSOT 어디에도 없다.",
|
|
"unknown": "이 Host 의 각 VM 에 어떤 disk 가 몇 개 붙어 있는지, 각 Target(`vda` 등)의 Source 가 무엇인지, 그리고 그 Source 가 파일인지 Host block device 인지.",
|
|
"next-verification": "§175 OQ-1 이 적은 둘을 그대로 돌린다 — 각 Guest 에서 `lsblk` 로 보이는 block device 와 partition 을 적고, Host 에서 `virsh domblklist <VM_NAME>` 로 Target 과 Source 를 적는다. VM 마다 한 행으로 남기고 출력은 그대로 증거로 둔다.",
|
|
"decision-criterion": "VM 마다 Target ↔ Source 대응이 출력으로 확정되면 닫는다. Source 가 파일이면 question:disk-image-format-and-actual-host-usage 와 question:host-block-device-under-the-disk-image 로 이어지고, Host block device 면 §145 의 갈래가 이 환경이라고 적고 qcow2 쪽 후속 물음 둘은 이 Host 에 적용되지 않는다고 적는다. 어느 쪽이든 그 결과가 reference:confirm-the-disk-backend-on-the-host 가 요구하는 첫 항목을 채운다.",
|
|
"relations": [
|
|
"concept:qemu-block-backend-forms",
|
|
"concept:guest-block-io-path-to-virtqueue",
|
|
"reference:confirm-the-disk-backend-on-the-host",
|
|
"question:disk-image-format-and-actual-host-usage",
|
|
"question:host-block-device-under-the-disk-image"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/question/question-vm-disk-backend-mapping.md",
|
|
"status": "초안",
|
|
"studioId": "1f76d54b-bb17-4174-8128-0c2d71d4256f",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "이 disk image 는 qcow2 인가 RAW 인가, 그리고 Guest 가 보는 크기와 Host 가 실제로 쓰는 크기는 얼마나 다른가",
|
|
"kind": "question",
|
|
"slug": "disk-image-format-and-actual-host-usage",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-3",
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-2",
|
|
"final/document.md#143-qcow2-virtual-size와-실제-host-사용량",
|
|
"final/document.md#144-raw-image",
|
|
"final/document.md#142-qcow2-host에서는-파일-guest에서는-디스크"
|
|
],
|
|
"known": "§142 는 같은 대상이 Host 에서는 파일 하나이고 Guest 에서는 `/dev/vda` 라는 디스크이며 둘 다 맞다고 놓았다. §143 은 qcow2 가 가상 disk size 와 실제 Host 할당량이 다를 수 있다고 적고, Guest 가 보는 공간 100 GB 에 Host 실제 할당 3GB 인 그림과 Guest 가 데이터를 기록하면서 Actual 이 1GB → 10GB → 40GB 로 늘 수 있다는 예를 들었다. 확인 명령으로 `qemu-img info vm1.qcow2` 를 들고 `virtual size` 와 실제 allocation 을 구분해서 봐야 한다고 했다. §144 는 RAW 가 qcow2 보다 구조가 단순하고 qcow2 는 Copy-on-Write · sparse allocation · snapshot 등에 유리하지만 metadata/mapping 처리가 있다고 적으면서, 「RAW = 무조건 빠름 / qcow2 = 무조건 느림」으로 일반화하면 안 된다고 못박았다. §175 OQ-2 는 `qemu-img info /path/to/disk-image` 를, OQ-3 은 `qemu-img info <image>` · `du -h <image>` · `ls -lh <image>` 셋을 들고 세 명령이 보여주는 의미가 서로 다를 수 있으므로 비교하라고 했다. 이 Host 의 image 형식도 크기도 SSOT 에 없다.",
|
|
"unknown": "각 VM 의 disk image 가 qcow2 인지 RAW 인지, 그리고 `qemu-img info` 의 virtual size 와 `du -h` 가 보여주는 실제 점유량과 `ls -lh` 가 보여주는 파일 크기 셋이 이 Host 에서 얼마나 벌어져 있는지.",
|
|
"next-verification": "question:vm-disk-backend-mapping 이 확정한 Source 경로마다 `qemu-img info <image>` 로 file format 과 virtual size 와 disk size 를 적고, 같은 경로에 `du -h <image>` 와 `ls -lh <image>` 를 돌려 세 값을 한 행에 나란히 남긴다 (§175 OQ-2 · OQ-3). VM 이 여럿이면 한 표로 만든다.",
|
|
"decision-criterion": "image 마다 형식과 세 크기가 한 표로 적히면 닫는다. qcow2 이고 세 값이 벌어져 있으면 §143 이 말한 차이가 이 Host 에서 얼마인지를 확정한 것이고, 그 차이가 시간에 따라 어떻게 자라는지는 별도 측정으로 넘긴다. RAW 이면 §144 의 갈래가 이 환경이라고 적고 sparse 여부만 세 값의 차이로 판정한다. 어느 쪽이든 성능 결론은 여기서 내지 않는다 — §144 가 형식만으로 일반화하지 말라고 적었고, 성능은 question:vm1-storage-load-vs-vm2-latency 와 question:guest-fsync-latency-vs-host-storage-latency 가 잰다.",
|
|
"relations": [
|
|
"concept:qemu-block-backend-forms",
|
|
"reference:confirm-the-disk-backend-on-the-host",
|
|
"question:vm-disk-backend-mapping",
|
|
"question:host-block-device-under-the-disk-image"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/question/question-disk-image-format-and-actual-host-usage.md",
|
|
"status": "초안",
|
|
"studioId": "ae69e2e1-1a3b-4ae3-ae2a-572af07b95d7",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "이 VM 들의 QEMU disk cache mode 는 무엇인가",
|
|
"kind": "question",
|
|
"slug": "qemu-disk-cache-mode",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-4",
|
|
"final/document.md#153-qemu-cache-mode",
|
|
"final/document.md#154-cache=none",
|
|
"final/document.md#155-cache=writeback",
|
|
"final/document.md#156-writeback-=-위험-이라고-단정하면-안-되는-이유",
|
|
"final/document.md#147-vm에서는-page-cache가-두-번-나타날-수-있다"
|
|
],
|
|
"known": "§153 은 QEMU/libvirt disk 에서 대표적으로 볼 수 있는 설정으로 `cache=none` 과 `cache=writeback` 을 들고, 이름만 보고 「none = cache 자체가 없음 / writeback = 무조건 위험」으로 해석하면 부정확하며 핵심은 QEMU 가 Host Page Cache 와 write completion/flush semantics 를 어떤 방식으로 사용할 것인가라고 적었다. §154 는 `cache=none` 을 Host Page Cache 를 우회하는 방향의 I/O 구성으로 놓아 이중 caching 을 줄일 수 있지만 Host Page Cache 우회가 즉시 durable media 반영을 뜻하지는 않는다고 했다. §155 는 `cache=writeback` 이 Host Page Cache 를 사용할 수 있는 구성이고 일반 write 는 Host RAM 에서 빠르게 completion 될 수 있지만 그것이 Guest 의 `fsync()`/FLUSH 를 무시한다는 뜻은 아니라고 적었다. §147 은 Guest buffered I/O + Host file-backed disk + Host Page Cache 를 함께 쓰면 같은 데이터가 Guest RAM 과 Host RAM 양쪽에 cache 될 수 있다고 밝혔다. §175 OQ-4 는 `virsh dumpxml <VM_NAME>` 로 disk driver 설정의 cache 관련 값을 확인하라고 했다. 이 Host 의 설정 값은 SSOT 에 없다.",
|
|
"unknown": "각 VM 의 disk driver 에 cache 값이 명시되어 있는지, 명시되어 있다면 무엇인지, 명시가 없다면 그 자리에서 실제로 무엇이 적용되는지.",
|
|
"next-verification": "VM 마다 `virsh dumpxml <VM_NAME>` 을 돌려 disk 요소의 driver 설정을 그대로 적는다 (§175 OQ-4). 출력의 cache 관련 값과 함께 같은 disk 의 Source 와 형식(question:vm-disk-backend-mapping · question:disk-image-format-and-actual-host-usage 의 결과)을 한 행에 둔다.",
|
|
"decision-criterion": "VM 마다 cache 값이 출력으로 확정되면 닫는다. `cache=none` 이면 §154 의 서술이, `cache=writeback` 이면 §155 의 서술이 이 Host 에 적용된다고 적고, 어느 쪽이든 §156 이 요구한 것 — Guest 의 flush/fsync semantics 가 전체 chain 에서 보존되는가 — 은 이 값만으로 답하지 않는다고 함께 적는다. 값이 명시되어 있지 않으면 그 사실을 적고 실제 적용 값을 확인할 방법을 다음 검증으로 남긴다. 이 값을 어떻게 둘 것인지는 후보 대장의 SSOT-153-157-cache-mode-choice-not-made 가 NEEDS_DECISION 으로 들고 있다.",
|
|
"relations": [
|
|
"concept:write-completion-is-not-durability",
|
|
"reference:a-speedup-that-removed-durability-is-not-an-optimization",
|
|
"question:guest-fsync-latency-vs-host-storage-latency",
|
|
"question:vm-disk-backend-mapping"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/question/question-qemu-disk-cache-mode.md",
|
|
"status": "초안",
|
|
"studioId": "480f86ac-86fb-4290-9bf5-584763aa1363",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "disk image 는 최종적으로 어느 Host block device 위에 있는가",
|
|
"kind": "question",
|
|
"slug": "host-block-device-under-the-disk-image",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-5",
|
|
"final/document.md#158-host-block-layer",
|
|
"final/document.md#159-여러-vm이-하나의-nvme를-공유하면",
|
|
"final/document.md#141-qemu가-물리-ssd를-직접-제어하는-것은-아니다",
|
|
"final/document.md#163-실제-i-o-scheduler-확인"
|
|
],
|
|
"known": "§141 은 backend 가 qcow2 파일이면 QEMU 가 결국 Host Linux 에 파일 I/O 를 요청하고 그것이 Host Filesystem → Host Block Layer → NVMe Driver → Physical NVMe 로 내려가므로 Guest storage stack 아래에 Host storage stack 이 한 번 더 존재할 수 있다고 적었다. §158 은 그 경로를 QEMU → `vm1.qcow2` → Host ext4/XFS → Host Block Layer → `/dev/nvme0n1` 로 그리고, Host Block Layer 는 그 I/O 가 VM 에서 시작했는지 Host process 에서 시작했는지를 본질적으로 구분해서 처리하는 계층이 아니라고 밝혔다. §159 는 여러 VM 의 QEMU 와 Nginx 와 Host 기타 프로세스가 같은 Host Block Layer 를 지나 하나의 NVMe 로 간다고 그렸다. §175 OQ-5 는 `lsblk` 와 `findmnt` 로 확인하라고 했다. 이 Host 의 filesystem 배치도 물리 장치도 SSOT 에 없다.",
|
|
"unknown": "disk image 가 놓인 디렉터리가 어느 mount point 에 속하는지, 그 mount 가 어느 partition 과 어느 block device 위에 있는지, 그리고 VM 들의 image 가 같은 device 를 공유하는지 서로 다른 device 에 있는지.",
|
|
"next-verification": "question:vm-disk-backend-mapping 이 확정한 Source 경로마다 `findmnt` 로 그 경로가 속한 mount 와 source device 를 적고, `lsblk` 로 그 device 의 partition·상위 device 관계를 적는다 (§175 OQ-5). VM 이 여럿이면 image 경로 → mount → partition → device 한 행씩으로 남긴다.",
|
|
"decision-criterion": "image 마다 최종 block device 이름이 확정되면 닫는다. VM 들이 같은 device 를 공유하면 §159 가 그린 상황이 이 환경이라고 적고 question:vm1-storage-load-vs-vm2-latency 가 그것을 실제로 재는 물음이 된다. 서로 다른 device 면 그 사실을 적고 그 질문의 전제가 이 환경에 없다고 함께 적는다. 여기서 확정한 device 이름이 question:host-io-scheduler 의 `<device>` 자리를 채운다.",
|
|
"relations": [
|
|
"concept:host-block-stack-under-the-vm",
|
|
"concept:qemu-block-backend-forms",
|
|
"question:vm-disk-backend-mapping",
|
|
"question:host-io-scheduler",
|
|
"question:vm1-storage-load-vs-vm2-latency",
|
|
"reference:confirm-the-disk-backend-on-the-host"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/question/question-host-block-device-under-the-disk-image.md",
|
|
"status": "초안",
|
|
"studioId": "eead2379-94fe-463a-9e5e-06a01bf2105d",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "이 호스트의 I/O Scheduler 는 무엇인가",
|
|
"kind": "question",
|
|
"slug": "host-io-scheduler",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-6",
|
|
"final/document.md#161-i-o-scheduler",
|
|
"final/document.md#162-none",
|
|
"final/document.md#163-실제-i-o-scheduler-확인",
|
|
"final/document.md#160-blk-mq-multi-queue-block-layer"
|
|
],
|
|
"known": "§161 은 여러 I/O request 가 있다고 해서 항상 들어온 순서 그대로 device 에 전달되는 것은 아니고 I/O Scheduler 가 dispatch 정책을 갖는다고 적으며 대표적으로 볼 수 있는 것으로 `none` · `mq-deadline` · `bfq` 를 들었다. §162 는 `none` 이 복잡한 scheduling 정책을 최소화해 비교적 직접 device 쪽으로 dispatch 하는 방향이고 NVMe 처럼 device 자체가 강한 병렬성과 queueing 기능을 가진 경우 적합할 수 있지만, `none` 이 block layer 가 아무 일도 하지 않는다는 뜻은 아니라고 적었다. §160 은 `blk-mq` 가 CPU 마다 queue 를 두어 여러 CPU 가 병렬로 block I/O 를 처리하는 구조를 그렸다. §163 은 확인 명령으로 `cat /sys/block/nvme0n1/queue/scheduler` 를 들고 예시 출력 `[none] mq-deadline` 에서 대괄호 안이 현재 선택된 scheduler 라고 했으며, SATA/SCSI device 라면 `cat /sys/block/sda/queue/scheduler` 처럼 확인한다고 적었다. 이 Host 의 값은 SSOT 에 없다.",
|
|
"unknown": "이 Host 에서 VM disk image 를 담고 있는 block device 의 현재 scheduler 가 무엇이고 선택 가능한 목록이 무엇인지, 그리고 장치가 여럿이면 장치마다 값이 다른지.",
|
|
"next-verification": "question:host-block-device-under-the-disk-image 가 확정한 device 이름을 넣어 `cat /sys/block/<device>/queue/scheduler` 를 돌리고 출력을 그대로 적는다 (§175 OQ-6). 대괄호가 붙은 값이 현재 선택된 것이다. VM image 가 놓인 장치가 여럿이면 장치마다 적는다.",
|
|
"decision-criterion": "device 마다 현재 scheduler 와 선택지가 출력으로 적히면 닫는다. `none` 이면 §162 의 서술이 이 Host 에 적용된다고 적고, `mq-deadline` 이나 `bfq` 면 dispatch 정책이 개입한다는 사실을 question:vm1-storage-load-vs-vm2-latency 의 해석 조건으로 넘긴다. scheduler 를 바꿀지 말지는 이 질문이 정하지 않는다 — 바꾼 전후를 비교한 측정이 나온 뒤 Decision 으로 넘긴다.",
|
|
"relations": [
|
|
"concept:host-block-stack-under-the-vm",
|
|
"question:host-block-device-under-the-disk-image",
|
|
"question:vm1-storage-load-vs-vm2-latency",
|
|
"reference:read-storage-before-blaming-cpu-for-latency"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/question/question-host-io-scheduler.md",
|
|
"status": "초안",
|
|
"studioId": "77aa0188-5c2b-4f47-bda7-7c66b084968d",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "VM1 의 storage 부하가 VM2 의 지연을 실제로 밀어 올리는가",
|
|
"kind": "question",
|
|
"slug": "vm1-storage-load-vs-vm2-latency",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-7",
|
|
"final/document.md#167-storage-contention",
|
|
"final/document.md#159-여러-vm이-하나의-nvme를-공유하면",
|
|
"final/document.md#168-cpu가-정상이어도-storage-때문에-느릴-수-있다"
|
|
],
|
|
"known": "§159 는 여러 VM 의 QEMU 와 Host 프로세스가 같은 Host Block Layer queue 로 들어와 device 로 dispatch 된다고 그렸다. §167 은 여러 VM 이 동일한 Physical NVMe 를 사용하면 storage resource 경쟁이 발생할 수 있고 VM1 에서 대량 I/O 가 발생하면 VM2 의 storage latency 가 증가할 수 있다고 적으면서, CPU Contention 이 Host logical CPU 실행 시간 경쟁이고 Storage Contention 은 IOPS/bandwidth/queue/device 처리시간 경쟁이라 둘은 다른 자원 경쟁이라고 못박았다. §168 은 storage completion 을 기다리는 동안 CPU usage 가 높지 않을 수 있어 CPU 30% 인데 request latency 2초 가 가능하다고 적었다. §175 OQ-7 은 VM1 에서 별도의 테스트 파일/디스크로 controlled I/O load 를 발생시키고 VM2 의 application latency 와 Host storage 지표를 동시에 보라고 했다. 이 Host 에서 그렇게 재 본 결과는 없다.",
|
|
"unknown": "VM1 에 controlled I/O load 를 걸었을 때 VM2 의 application latency 가 baseline 대비 실제로 달라지는지, 달라진다면 같은 구간의 Host storage 지표가 같은 방향으로 움직이는지, 그리고 그 변화가 CPU 쪽 지표와는 무관한지.",
|
|
"next-verification": "먼저 question:host-block-device-under-the-disk-image 로 두 VM 이 같은 device 를 쓰는지 확정한다. 그 다음 baseline 구간에서 VM2 의 application latency 와 Host `iostat -xz 1` 과 CPU 를 같은 시각에 기록하고, VM1 에서 별도의 테스트 파일/디스크로 controlled I/O load 를 발생시킨 뒤 같은 셋을 다시 기록하고, 부하를 걷은 회복 구간을 세 번째로 찍는다 (§175 OQ-7). 어느 VM 이 I/O 를 내는지는 §169 의 `iotop` 으로 함께 남긴다.",
|
|
"decision-criterion": "세 구간 표에서 부하 구간에만 VM2 latency 와 Host storage 지표가 함께 오르면 §167 이 그린 경쟁이 이 환경에서 이어진다는 것을 확인한 것이고 그 시계열이 Case 가 된다. VM1 부하 중에도 VM2 latency 가 baseline 과 다르지 않으면 이 구성과 이 부하 수준에서는 경쟁이 관찰되지 않는다고 적고 닫는다. 장치를 분리할지 · scheduler 를 바꿀지 · I/O 를 제한할지는 그 Case 가 나온 뒤 Decision 으로 넘긴다.",
|
|
"relations": [
|
|
"concept:host-block-stack-under-the-vm",
|
|
"reference:read-storage-before-blaming-cpu-for-latency",
|
|
"question:host-block-device-under-the-disk-image",
|
|
"question:host-io-scheduler",
|
|
"question:guest-fsync-latency-vs-host-storage-latency",
|
|
"question:vcpu-contention-under-two-vm-load"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/question/question-vm1-storage-load-vs-vm2-latency.md",
|
|
"status": "초안",
|
|
"studioId": "ed64d4ff-aa01-48cc-ae68-6a683b025cde",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "Guest 의 fsync() 지연과 Host storage 지연은 같이 오르는가",
|
|
"kind": "question",
|
|
"slug": "guest-fsync-latency-vs-host-storage-latency",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-8",
|
|
"final/document.md#150-fsync-가-필요한-이유",
|
|
"final/document.md#169-storage-관측-명령어",
|
|
"final/document.md#170-postgresql-예시-wal과-durability",
|
|
"final/document.md#171-성능과-durability의-trade-off",
|
|
"final/document.md#148-write-완료와-영속화는-다르다"
|
|
],
|
|
"known": "§150 은 `fsync(fd);` 가 변경 내용을 필요한 영속성 경계까지 반영하도록 요청하는 것이고 VM 에서는 그 의미가 Guest Filesystem → Guest Block Layer → virtio-blk → QEMU/Backend → Host Storage Stack → Physical Storage 전체 stack 으로 전달되어야 한다고 적었다. §148 은 `write()` 완료와 writeback 완료와 fsync/flush 완료와 전원 장애에도 안전한 durability 가 서로 다르다고 못박았다. §170 은 PostgreSQL 이 WAL 등의 durability protocol 을 쓰며 필요한 시점에 storage synchronization 을 수행하고, VM storage layer 가 flush/fsync semantics 를 제대로 보존하지 않으면 PostgreSQL 의 durability assumption 과 실제 storage behavior 가 어긋날 수 있다고 적었다. §171 은 특히 DB workload 에서 `fsync()` latency 가 transaction latency 와 연결될 수 있다고 했다. §169 는 Host 관측으로 `iostat -xz 1` 과 `iotop` 을 들었다. §175 OQ-8 은 Guest application/DB latency 와 Host `iostat` 를 시간축으로 함께 관찰하라고 했다. 이 Host 에서 잰 값은 없다.",
|
|
"unknown": "이 Host 에서 Guest 의 `fsync()` 나 DB transaction latency 가 커지는 구간과 Host 쪽 storage latency 가 커지는 구간이 시간축에서 겹치는지, 겹친다면 두 값이 같은 크기로 움직이는지 Guest 쪽이 더 크게 벌어지는지.",
|
|
"next-verification": "Guest 에서 application/DB latency 를 기록하면서 같은 시각에 Host 에서 `iostat -xz 1` 을 돌려 두 계열을 같은 타임스탬프로 남긴다 (§175 OQ-8). 부하가 없는 구간과 있는 구간을 모두 찍고, 어느 프로세스가 I/O 를 내는지는 §169 의 `iotop` 으로 함께 적는다. 이때의 cache mode 는 question:qemu-disk-cache-mode 의 결과를 조건으로 함께 기록한다.",
|
|
"decision-criterion": "두 계열이 같은 구간에서 함께 오르면 §150 이 말한 전달 경로가 이 환경에서 이어진다는 것을 확인한 것이고 그 시계열이 Case 가 된다. Guest `fsync()` latency 는 오르는데 Host storage latency 가 따라 오르지 않으면 그 차이가 어느 계층에서 생기는지 — Guest 쪽 대기인지 QEMU/backend 쪽인지 — 를 다음 물음으로 남기고 닫는다. 어느 쪽이든 성능을 위해 durability 를 줄이는 방향은 reference:a-speedup-that-removed-durability-is-not-an-optimization 을 지나야 한다.",
|
|
"relations": [
|
|
"concept:write-completion-is-not-durability",
|
|
"concept:guest-block-io-path-to-virtqueue",
|
|
"reference:a-speedup-that-removed-durability-is-not-an-optimization",
|
|
"reference:read-storage-before-blaming-cpu-for-latency",
|
|
"question:qemu-disk-cache-mode",
|
|
"question:vm1-storage-load-vs-vm2-latency",
|
|
"question:host-major-fault-vs-storage-latency"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "storage-virtualization/question/question-guest-fsync-latency-vs-host-storage-latency.md",
|
|
"status": "초안",
|
|
"studioId": "be3c2c58-1ed4-4eae-876a-3114faa61c94",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"decision": []
|
|
}
|
|
},
|
|
"lab-environment-build": {
|
|
"topic": "lab-environment-build",
|
|
"title": "실험대 환경 구성 — 제1~4부의 구조 위에 게스트 세 대를 실제로 세우고 옮기기",
|
|
"readerQuestion": "제1~4부가 설명한 구조 위에 실험대 한 대를 실제로 세우고 다시 옮기려 할 때, 무엇이 새로 필요해지고 어디서 막히는가?",
|
|
"kinds": {
|
|
"case": [
|
|
{
|
|
"title": "호스트 안에서는 404, 밖에서는 connection refused — 앞 체인의 accept 가 libvirt 의 reject 를 막지 못했다",
|
|
"kind": "case",
|
|
"slug": "nftables-accept-did-not-stop-the-libvirt-reject",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다",
|
|
"final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나",
|
|
"final/document.md#178-이-부의-출처와-범위"
|
|
],
|
|
"classification": "제3부가 그린 게스트 패킷 경로 위에서 이 구축이 가장 오래 막힌 지점이다. **증상**(observed) — 호스트에서 `curl http://192.168.122.10` 을 치면 404 로 돌아오고(엣지 nginx 가 응답한다) 밖에서 `curl http://100.83.212.4` 를 치면 connection refused 가 온다. 타임아웃이 아니라 즉시 거절이라는 것이 단서였다 — 드롭이면 기다리다 죽는다. **원인**(observed) — libvirt 는 자기 테이블 `ip libvirt_network` 의 `guest_input` 체인을 `ct state established,related accept` 다음의 `reject` 로 끝낸다. 그 reject 규칙의 카운터가 4 패킷 240 바이트였고 밖에서 친 curl 횟수와 정확히 일치해 범인이 확정됐다 — 범인 확정에 쓴 것이 이 숫자다. **왜 우리 규칙이 안 먹혔나** — DNAT 파일에 `priority filter - 10` 으로 먼저 도는 `forward` 체인을 두고 `ct state new accept` 를 넣어 두었는데, nftables 는 같은 훅에 붙은 base 체인을 우선순위 순으로 전부 평가한다. 앞 체인의 `accept` 는 「이 체인은 통과」라는 뜻이지 「평가 끝」이 아니고 `drop` 만이 즉시 종결이다. iptables 감각으로 쓰면 정확히 여기서 틀린다. **해결**(observed) — 구멍을 libvirt 체인 맨 앞에 뚫는다. `insert` 가 맨 앞이고 `add` 가 맨 뒤라 §180 의 명령은 `nft insert rule ip libvirt_network guest_input` 로 시작한다. 그리고 이 규칙은 휘발성이다(observed) — libvirt 가 네트워크를 다시 세우면 `guest_input` 을 새로 쓰면서 날아가므로 DNAT 유닛의 `ExecStartPost` 에 넣는다. 증상·원인·수정·수정의 수명이 한 절 안에서 닫혀 독립성 검사를 통과한다. 이 Case 가 생긴 이유는 decision:edge-nginx-moved-into-a-guest-vm 이 감수한 비용 3번이고, 「경로가 OUTPUT 에서 FORWARD 로 바뀌었다」는 그 한 줄이 여기서 실제 비용으로 청구됐다. 그 한 줄 자체는 세 문장이라 따로 Concept 으로 세우지 않고 이 Case 의 배경 절로 넣는다.",
|
|
"missing-verification": "이 호스트 한 대에서만 본 것이다. §180 가 미확인으로 남긴 것은 하나다(unknown) — libvirt 의 `firewall_backend` 가 iptables 일 때도 같은 구멍이 필요한지는 재지 않았고 이 호스트는 nftables 백엔드다. question:guest-input-hole-under-the-iptables-backend 가 그것을 받는다. 그리고 증상·원인·해결이 전부 SSOT 본문에 적힌 관측이고 이 저장소의 `final/evidence/` 에는 그 `curl` 과 규칙 덤프의 출력 원문이 아직 없다 — 카운터 4 패킷 240 바이트는 근거가 본문이지 명령 출력 파일이 아니다. 재현을 다시 돌려 원문을 `final/evidence/raw/` 에 남기면 그때 증거가 붙는다. 수정이 재기동을 견디는지도 `ExecStartPost` 를 넣은 뒤 실제로 libvirt 네트워크를 다시 세워 확인한 기록이 없다.",
|
|
"relations": [
|
|
"decision:edge-nginx-moved-into-a-guest-vm",
|
|
"question:guest-input-hole-under-the-iptables-backend",
|
|
"reference:verify-a-build-guide-in-execution-order",
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"reference:bisect-the-packet-path-with-capture-points"
|
|
],
|
|
"ssot-assets": [
|
|
"nftables-forward-hook-chain-order"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/case/case-nftables-accept-did-not-stop-the-libvirt-reject.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [
|
|
"nftables-forward-hook-chain-order"
|
|
],
|
|
"assetFiles": [
|
|
"nftables-forward-hook-chain-order"
|
|
],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "5120MB 를 줬는데 353MB 를 쓰고 있었다 — 선언한 양과 실제로 드는 양",
|
|
"kind": "case",
|
|
"slug": "declared-memory-and-disk-are-ceilings-not-occupancy",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#198-자원-할당과-실사용은-다르다",
|
|
"final/document.md#199-디스크-오버레이는-얼마나-쓰나",
|
|
"final/document.md#202-철거-실제-출력-전문",
|
|
"final/document.md#197-측정-환경",
|
|
"final/document.md#195-이-부의-출처와-범위",
|
|
"final/document.md#211-이-부의-출처와-범위"
|
|
],
|
|
"classification": "제7부가 이 호스트에서 **자원의 양**을 처음 잰 자리다. 제1~4부에는 이 호스트에서 잰 값이 하나도 없고(문서 머리말이 그렇게 선언한다) 제5·6부는 버전·주소·명령까지만 관측했다. **물음은 하나다** — 선언한 양과 실제로 드는 양이 얼마나 다른가. **관측 넷이 같은 답에 닿는다**(observed, 2026-09-10 · `test-server`). ① 메모리 — k3s 만 떠 있고 Keycloak 은 안 올린 상태에서 `kc-lab-1` 은 할당 5120MB 에 실사용 353MB(7%), `kc-lab-2` 는 할당 3120MB 에 실사용 301MB 다. 합계 8240MB 를 할당했는데 실제 점유는 654MB 다. ② 디스크 — 20GB 를 두 장 선언했는데 실제 파일은 `kc-lab-1.qcow2` 1.4GiB, `kc-lab-2.qcow2` 665MiB 이고 바닥 `base.qcow2` 는 `virtual size` 3GiB 에 `disk size` 335MiB 다. 40GB 를 선언해 2.1GB 를 썼다. ③ 철거 — 게스트 셋을 지우니 `df -h /` 가 11G 에서 **7.9G** 로 내려 3.1GB 가 회수됐다(내역은 1.4GB + 665MB + 시드 ISO 셋). ④ 그래서 11,648MB·226GB 짜리 호스트 한 대에서 게스트 세 대가 무리 없이 돈다. **진단** — `virt-install --memory 4096` 으로 만든 `kc-lab-2` 의 할당이 3120 으로 보이는 것이 이 관측의 매듭이다. `dommemstat` 의 `actual` 은 **현재 할당**이지 선언한 상한이 아니고 상한은 `virsh dominfo` 의 `Max memory` 에 있다. 줄어든 원인은 virtio-balloon 회수로 보인다(inferred). **결론** — 선언은 상한이지 점유가 아니다. `free` 의 `available`(6005MB)이 `free`(2599MB)보다 훨씬 큰 것도 같은 이유이고 **VM 을 얼마나 더 띄울 수 있는지는 `free` 가 아니라 `available` 로 본다.** 같은 문장의 세 번째 함정이 `virsh pool-info` 의 `Allocation 7.84 GiB` 인데 그것은 풀이 얹힌 호스트 루트 파일시스템 전체의 사용량이지 VM 만의 사용량이 아니다. **읽을 때의 전제 둘** — 원본이 표기 규약을 스스로 밝혔다(추정값·예상값이 없고 없는 값은 「미측정」이다). 그리고 SSOT 안에 같은 대상의 스냅샷이 두 벌 있다 — §218 은 2026-09-03 값(`RAM 7.4Gi` · `kc-lab-1` `3584M` · 게스트 2대)이고 이 기록은 2026-09-10 값이다. 그 사이에 호스트 RAM 이 8GB→12GB 로 물리 증설되고 게스트 메모리가 재배분됐다(§332). **두 값이 어긋나 보이면 틀린 것이 아니라 다른 날이다.** 중첩 가상화는 켜져 있지만(`nested` 가 `Y`) 이 실험대는 쓰지 않는다 — 시간을 재는 실험에서 측정값을 왜곡하기 때문이다.",
|
|
"missing-verification": "**이 호스트 한 대의 한 시점이다.** ① 이 값은 k3s 만 떠 있는 상태의 것이고 Keycloak 2 파드 + PostgreSQL + Redis + Prometheus 가 올라간 뒤의 값은 재지 않았다(미측정). 원본이 §198 에서 그렇게 밝힌다. ② `dommemstat` 의 `actual` 과 `dominfo` 의 `Max memory` 를 **나란히 찍어 보지 않았다**(미측정). 그래서 3120 이 balloon 회수의 결과라는 것은 관측이 아니라 추론이다(inferred). ③ `kc-lab-edge` 의 디스크 크기는 재 두지 않았다(미측정) — 3.1GB 회수 합계에서 역산하면 1GB 안팎이다. ④ 디스크 값은 엣지를 만들기 **전**, k3s 2노드만 있던 시점의 것이라 ③과 시점이 다르다. ⑤ QEMU 프로세스 쪽 RSS 는 이 기록이 재지 않았다 — question:qemu-resident-memory-distribution 이 그것을 받는다. **아직 열려 있는 물음과의 관계** — question:vm-configured-vs-current-memory 가 묻는 세 값(configured · Guest 사용량 · Host resident) 가운데 앞의 둘이 여기서 나왔고 셋째가 비어 있다. question:disk-image-format-and-actual-host-usage 가 묻는 `qemu-img info`·`du`·`ls` 세 값 가운데 `qemu-img info` 와 `ls` 가 나왔고 `du` 가 비어 있다. **둘 다 닫히지 않았다** — 이 기록은 그 물음들에 부분으로 답한 것이고 닫는 것은 그 Question 이 적은 절차다. ⑥ 이 저장소의 `final/evidence/` 에는 이 명령들의 출력 원문이 없다 — 근거가 SSOT 본문의 코드 블록이지 `raw/` 의 파일이 아니다.",
|
|
"relations": [
|
|
"question:vm-configured-vs-current-memory",
|
|
"question:disk-image-format-and-actual-host-usage",
|
|
"question:qemu-resident-memory-distribution",
|
|
"concept:inside-a-qcow2-file-the-mapping-table-and-its-clusters",
|
|
"setup:tear-down-the-lab-and-know-what-survives",
|
|
"setup:power-cycle-the-lab-and-reallocate-guest-memory",
|
|
"reference:tool-output-is-not-the-subject-state"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/case/case-declared-memory-and-disk-are-ceilings-not-occupancy.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"concept": [
|
|
{
|
|
"title": "qcow2 파일 한 장이 담는 것 — 매핑표와 데이터 클러스터가 같은 파일 안에 있고, 파일 밖을 가리키는 것은 백킹 파일 경로 하나다",
|
|
"kind": "concept",
|
|
"slug": "what-a-qcow2-file-carries",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#181-qcow2-가-담는-것과-담지-않는-것",
|
|
"final/document.md#178-이-부의-출처와-범위"
|
|
],
|
|
"basis-version": "QEMU 11.1.1 · libvirt 12.7.0 위의 qcow2 다 — §178 가 적은 이 실험대의 버전이고 게스트는 Debian 12 genericcloud 다. 크기 계산의 기준은 §181 가 밝힌 클러스터 64KiB · L2 항목 8B 다. 이 글이 멈추는 경계도 SSOT 가 그었다 — 제4부의 §127 이 qcow2 내부 L1/L2 table 을 별도 문서로 미뤘고 §181 도 그 안으로 들어가지 않는다. 온프렘 → 클라우드 이미지 반입 절차는 §181 가 스스로 (external, 코드 관측 아님) 으로 표시한 부분이라 이 글의 기준 밖이고 SSOT 에만 남긴다.",
|
|
"classification": "제4부의 스토리지 가상화를 **이식** 관점에서 이어 적은 자리다. 네 가지가 이 글의 뼈대다. (1) **매핑표와 데이터 클러스터가 같은 파일 안에 있다.** 표에 적히는 값은 호스트 물리 주소가 아니라 파일 안의 오프셋이라 파일을 통째로 옮겨도 그대로 유효하다. 파일 밖을 가리키는 것은 백킹 파일 경로 하나뿐이고(헤더에 절대경로 문자열) 그래서 이식에서 확인할 외부 참조도 그 하나다. (2) **따라가는 것과 따라가지 않는 것이 갈린다.** 따라가는 쪽은 파일시스템 전체·설치 패키지·설정·DB 파일, 디스크에 쓰인 캐시(컨테이너 이미지, apt 캐시), `machine-id` 와 SSH 호스트키, 내부 스냅샷이다. 따라가지 않는 쪽은 실행 중인 프로세스(PID·FD·소켓·JVM 힙), 페이지 캐시와 안 내려간 dirty page, VM 정의 XML(vCPU·RAM·NIC·machine type·CPU 모델), UEFI NVRAM·백킹 파일·호스트 쪽 구성이다. (3) **희소(sparse) 할당이지 압축이 아니다.** 20GB 이미지가 2GB 인 것은 쓴 블록만 파일에 존재하기 때문이고 1TB 를 채우면 1TB 파일이 된다. 메타데이터 오버헤드는 0.02% 미만이다(1TiB 당 약 160MiB). 게스트에서 지워도 파일은 줄지 않는다 — 클러스터가 이미 할당된 상태라 `fstrim`(디스크에 `discard='unmap'` 필요)이나 `qemu-img convert` 가 필요하다. (4) **실행 상태까지 옮기려면 qcow2 복사로는 안 된다** — `virsh save`→복사→`restore`(VM 이 멈추고 RAM 크기만큼 파일이 더 생긴다)이거나 `virsh migrate --live --copy-storage-all`(두 호스트 libvirt 가 붙고 CPU 모델이 호환돼야 한다)이다. 이 Concept 을 세운 것은 절을 훑어서가 아니라 §183 의 물음 둘을 먼저 고른 뒤 거꾸로 물어서다 — question:virsh-save-ram-dump-size-and-time 은 (4) 의 「RAM 크기만큼 파일이 더 생긴다」를 재고, question:qcow2-transfer-time-over-wifi 는 (3) 이 말한 희소 할당된 실제 파일 크기를 이 호스트의 WiFi 로 옮기는 시간을 잰다. 둘 다 이 네 가지를 모르면 무엇을 재는지 말할 수 없다.",
|
|
"relations": [
|
|
"question:virsh-save-ram-dump-size-and-time",
|
|
"question:qcow2-transfer-time-over-wifi",
|
|
"concept:qemu-block-backend-forms",
|
|
"concept:guest-block-io-path-to-virtqueue",
|
|
"question:disk-image-format-and-actual-host-usage"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/concept/concept-what-a-qcow2-file-carries.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "설치를 안 했는데 왜 뜨는가 — 클라우드 이미지는 설치가 끝난 디스크이고 cloud-init 이 빈칸을 채운다",
|
|
"kind": "concept",
|
|
"slug": "a-cloud-image-is-an-installed-disk-and-cloud-init-fills-the-blanks",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#229-왜-os를-설치하지-않아도-vm이-뜨는가",
|
|
"final/document.md#236-클라우드-이미지와-cloud-init",
|
|
"final/document.md#235-multipass-virt-install-virsh-무엇이-다른가",
|
|
"final/document.md#214-전체-구조-한눈에-보기",
|
|
"final/document.md#215-vm-한-대의-디스크-구성",
|
|
"final/document.md#216-설정-파일이-게스트에-도달하는-경로",
|
|
"final/document.md#217-부팅할-때-일어나는-일"
|
|
],
|
|
"basis-version": "Debian 12 genericcloud(`debian-12-genericcloud-amd64.qcow2`) 위의 cloud-init 22.4.2 이고 NoCloud 데이터소스다. 호스트는 §197 이 적은 QEMU 11.1.1 · libvirt 12.7.0 이다. 시드 ISO 의 크기(370KB)와 게스트가 보는 디스크 구성(`vda` 20G · `vdb` 370K)은 §215 의 2026-09-03 실측이고, `base.qcow2` 의 크기는 그 절이 333M, §199 의 2026-09-10 실측이 335MiB 로 적어 시점이 다르다. cloud-init 의 스키마 검사 동작은 게스트에 실제로 깔린 22.4.2 기준이고 판올림이 바뀌면 달라진다.",
|
|
"classification": "**가장 자주 막히는 자리는 「VM 은 격리된 빈 공간이니 OS 를 설치해야 하는 것 아닌가」이고, 답은 격리는 맞지만 설치는 필수가 아니다** 이다. 넷으로 푼다. **① 설치는 목적이 아니라 수단이다.** 설치 프로그램이 하는 일(파티션 테이블·파일시스템·패키지 배치·부트로더·초기 설정)의 결과는 「부팅 가능한 특정 바이트 배열」이고 그 배열은 결국 **파일 하나의 내용**이다. Debian 과 Ubuntu 는 자기 빌드 서버에서 그 설치를 **한 번** 하고 완성된 디스크를 qcow2 로 공개한다 — 소스에서 컴파일하는 것과 빌드된 바이너리를 받는 것의 차이와 같다. **② 그래서 일부러 비워 둔 것이 있다.** 그대로 복제하면 모든 복사본의 hostname·SSH 호스트키·machine-id·파일시스템 UUID 가 같아지고, 그것은 DHCP 가 두 기계를 하나로 오인하거나 두 서버가 같은 신원을 주장하는 문제가 된다. 클라우드 이미지는 그 값들을 비워 둔 채 배포되고 **cloud-init 이 첫 부팅에 그 빈칸을 채운다.** 전통적 설치가 「설치 + 개인화」를 부팅 전에 대화형으로 하는 것이라면 클라우드는 설치를 배포자가 미리 끝내고 개인화만 첫 부팅에 자동으로 한다. **③ 격리는 실행 시점에 KVM/QEMU 가 만든다.** 「설치를 안 했으니 격리가 약한가」는 오해다 — 게스트는 자기 커널로 부팅하고 자기 메모리 공간에서 돌며, 디스크 내용을 어떻게 얻었는지와는 무관하다. **④ 이 실험대가 cloud-init 을 고른 이유는 재생성 비용이다.** 대안 셋(ISO 정식 설치 · `virt-customize`/`guestfish` 로 이미지 개조 · cloud-init) 가운데 셋째를 쓰는 것은 **게스트를 반복해서 지우고 다시 만드는 것이 실험 그 자체**이기 때문이고, 두 노드가 바이트 단위로 같은 초기 상태여야 실험 결과가 오염되지 않기 때문이다. **이 실험대에서의 모양** — 가장 자주 하는 오해는 **시드 ISO 를 OS 이미지로 착각하는 것**이다. 시드는 OS 가 아니라 설정 데이터만 담은 370KB 짜리 별도 디스크이고, 게스트에게 디스크는 두 장이다 — `vda`(20G · ext4 · 여기서 부팅)와 `vdb`(370K · LABEL=CIDATA · iso9660 · 마운트조차 안 된다). 같은 내용이 세 곳(원본 YAML · 구워진 ISO · 풀에 올라간 볼륨)에 존재해 **원본만 고치면 VM 에 반영되지 않는다.** 부팅은 다섯 단계이고(QEMU 가 `vda` 에서 부팅 → cloud-init 기동 → 블록 장치 스캔 → `LABEL=CIDATA` 발견해 잠깐 마운트 → 설정 적용 후 언마운트) **3번이 실패하면 hostname 이 `localhost` 로 남고 사용자가 생성되지 않는다.** `--os-variant`·`genericcloud` 변종 선택도 여기 걸린다 — `genericcloud` 는 virtio 드라이버만 담아 가볍고 KVM 에 맞지만, 바로 그 이유로 case:cloud-init-failures-all-look-like-ssh-refused 의 첫째 원인이 생긴다. **거꾸로 뽑은 Concept 이다.** setup:create-three-guests-with-cloud-init 은 「base 이미지를 받아 오버레이로 게스트 셋을 만든다」로 시작하고 그 Case 의 원인 넷은 전부 「시드가 안 읽혔다」로 수렴하는데, 둘 다 이 네 문단을 모르면 왜 그 명령을 치는지 읽을 수 없다. 한 문단으로 접히지 않아(§229·§236 이 합쳐 188줄이다) 독립 기록이 된다. **이 글이 멈추는 곳** — 파일이 어떻게 생겼는지는 concept:inside-a-qcow2-file-the-mapping-table-and-its-clusters 가 받고, 시드 세 명령의 옵션별 뜻은 그 Setup 이 받는다.",
|
|
"relations": [
|
|
"setup:create-three-guests-with-cloud-init",
|
|
"case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"concept:inside-a-qcow2-file-the-mapping-table-and-its-clusters",
|
|
"concept:what-a-qcow2-file-carries",
|
|
"question:does-the-guide-rebuild-this-lab"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/concept/concept-a-cloud-image-is-an-installed-disk-and-cloud-init-fills-the-blanks.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "qcow2 파일 안 — 매핑표와 클러스터, 그리고 항목이 0 이면 바닥에 다시 묻는다",
|
|
"kind": "concept",
|
|
"slug": "inside-a-qcow2-file-the-mapping-table-and-its-clusters",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#231-qcow2-파일-내부는-어떻게-생겼나-매핑표가-전부다",
|
|
"final/document.md#230-디스크-이미지를-\"복사한다\"는-것의-실제-원리",
|
|
"final/document.md#228-qcow2와-backing-store-오버레이",
|
|
"final/document.md#232-qemu-img-와-qemu-system-x86_64-는-다른-도구다",
|
|
"final/document.md#233-오버레이는-docker-레이어와-같은-아이디어다",
|
|
"final/document.md#234-그래서-마이그레이션과-스냅샷이-된다"
|
|
],
|
|
"basis-version": "QEMU 11.1.1 의 qcow2 v3 이고 `cluster_size` 65536(기본값)이다. 실측은 Debian 12 genericcloud 배포본 `base.qcow2` 한 장에 대한 것이고 `qemu-img info`·`qemu-img map --output=json` 출력이 근거다. L2 항목 8바이트·하위 16비트가 클러스터 안 위치라는 비트 나누기는 그 `cluster_size` 에서 나온 계산이다. 압축은 zlib 이고, 포맷 자체는 zstd 도 지원하지만 이 이미지에서 관측된 것은 zlib 쪽이다.",
|
|
"classification": "**concept:what-a-qcow2-file-carries 가 자기 기준에서 미뤄 둔 자리다** — 그 기록은 「파일을 다른 호스트로 들고 갔을 때 무엇이 따라가나」를 다루면서 내부 L1/L2 table 은 기준 밖으로 선언했다. 이 글이 그 안이다. **출발점은 raw 다.** 디스크는 섹터가 0번부터 늘어선 1차원 배열이고 파티션 테이블도 파일시스템도 부트로더도 전부 그 배열 안의 바이트다 — **디스크 바깥에 보관되는 정보가 없다.** 그래서 배열을 통째로 파일에 담으면 raw 이고, 되돌린 디스크는 바이트 단위로 같아 똑같이 부팅한다. 20GB 디스크는 20GB 파일이 된다 — 안 쓴 구간까지 0으로 채워 기록하기 때문이다. **qcow2 가 더하는 것은 매핑표 하나다.** 「가상 디스크의 이 위치가 파일 안의 어디에 있는가」를 적어 두고 안 쓴 구간은 아예 기록하지 않는다. **표만 담는 것이 아니다** — 표는 같은 파일 안의 오프셋을 가리키고 가리켜진 데이터 클러스터도 그 파일 안에 함께 있다(`disk size` 335MiB 가 그 증거다. 표만이라면 수십 KB 다). 그리고 표에 적히는 값이 **호스트 물리 주소가 아니라 파일 안의 몇 번째 바이트**라 파일을 다른 기계로 옮겨도 표가 그대로 유효하다. **다섯 가지가 이 글의 뼈대다.** ① **클러스터** — 섹터 하나하나를 매핑하면 표가 너무 커져 64KB 덩어리로 끊는다. 클러스터는 qcow2 **파일 안에서만** 쓰는 논리 단위라 물리 디스크와 무관하고, 「블록 크기」가 다섯 층(물리 섹터 · 호스트 파일시스템 블록 · qcow2 클러스터 · 게스트가 보는 논리 섹터 · 게스트 파일시스템 블록)에 따로 있어 헷갈리기 쉽다. FAT·NTFS 도 할당 단위를 클러스터라 부르는데 **같은 단어, 다른 층**이다. ② **2단계 매핑** — 표 한 장이면 20GB 에 대해 수 MB 가 되므로 L1 → L2 로 나눈다. 클러스터가 2^16 이고 항목이 8바이트라 L2 한 장에 8192(2^13)개가 들어가고, 게스트 오프셋의 하위 16비트가 클러스터 안 위치, 그다음 13비트가 L2 인덱스, 그 위 전부가 L1 인덱스다. 운영체제의 페이지 테이블과 같은 구조이고 **필요한 L2 만 만들면 되므로 안 쓴 영역은 L1 항목이 0 인 채로 끝난다.** ③ **항목이 0 이면 무슨 일이 생기나** — 여기가 오버레이의 핵심이다. 바닥이 없으면 0으로 채운 64KB 를 만들어 돌려주고, 바닥이 있으면 **바닥 파일의 같은 위치를 읽는다.** 그래서 `kc-lab-1.qcow2` 는 자기가 바꾼 클러스터만 들고 있다. **바닥 경로는 헤더에 문자열로 박혀 있어**(`backing_file_offset`) 바닥을 옮기거나 이름을 바꾸면 게스트가 부팅하지 못하고, 그래서 경로는 절대경로로 준다. ④ **refcount** — 클러스터마다 참조 횟수를 따로 관리해서 1 이면 그냥 덮어쓰고 1 보다 크면 쓰기 전에 복사본을 만든다. 이것이 copy-on-write 이고 `snapshot-create-as` 가 데이터를 복사하지 않고 refcount 만 올리기 때문에 스냅샷이 순식간에 찍힌다. ⑤ **압축** — 배포용 이미지는 **클러스터 단위 zlib 압축**이 켜져 있다. `qemu-img map --output=json` 실측에서 1236개 중 **606개가 `compressed: True`** 였고, 3 GiB 가 324 MiB 가 되는 것은 희소(2.01 GiB 가 구멍) · 압축(남은 1010 MiB → 324 MiB) · genericcloud 자체가 작은 것 셋이 겹친 결과다. **압축도 우리가 한 것이 아니라 Debian 이 배포 시점에 한 것**이고, 압축 클러스터는 게스트가 쓰면 **압축하지 않은 형태로 새로 할당**되므로 오버레이에 쌓이는 것은 비압축 클러스터다. **backing chain 은 Docker 레이어와 같은 아이디어이고 쓰임이 다르다** — Docker 는 빌드 시점에 의도적으로 쌓고 층의 정체성이 다이제스트인데 qcow2 는 런타임 파생이고 정체성이 경로 문자열이다. **체인이 깊으면 읽기가 느려져**(L2 항목이 0 일 때마다 한 층 아래로 내려간다) 두세 겹을 넘기지 않고, 굳힐 때는 `qemu-img commit`(아래층 병합 — 다른 오버레이가 있으면 깨진다)이나 `qemu-img convert`(단일 파일로 평탄화 — 옮길 때는 이쪽이 안전하다)를 쓴다. **`qemu-img` 와 `qemu-system-x86_64` 는 다른 도구다** — 앞엣것은 파일만 만지고 VM 이 없어도 돈다. **거꾸로 필요로 하는 것이 둘이다.** question:qcow2-transfer-time-over-wifi 는 「`qemu-img convert` 로 먼저 줄인 뒤 옮기는 편이 변환 시간까지 합쳐도 나은가」를 묻고, question:disk-image-format-and-actual-host-usage 는 `qemu-img info`·`du`·`ls` 세 값이 왜 갈리는지를 묻는다. 둘 다 이 다섯을 모르면 무엇을 재는지 말할 수 없다.",
|
|
"relations": [
|
|
"concept:what-a-qcow2-file-carries",
|
|
"question:qcow2-transfer-time-over-wifi",
|
|
"question:disk-image-format-and-actual-host-usage",
|
|
"concept:a-cloud-image-is-an-installed-disk-and-cloud-init-fills-the-blanks",
|
|
"concept:qemu-block-backend-forms",
|
|
"case:declared-memory-and-disk-are-ceilings-not-occupancy"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/concept/concept-inside-a-qcow2-file-the-mapping-table-and-its-clusters.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"setup": [
|
|
{
|
|
"title": "lab host 에 가상화 패키지를 깔고 virsh 가 sudo 없이 돌게 만든다",
|
|
"kind": "setup",
|
|
"slug": "prepare-the-lab-host-for-virtualization",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비",
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"pinned-versions": [
|
|
"libvirt = 12.7.0",
|
|
"QEMU = 11.1.1"
|
|
],
|
|
"classification": "**단계 00 과 01 을 갈라 두 편으로 만들었다**(2026-09-14 재분해). 처음에는 둘이 같은 셸에서 이어 치는 한 줄기라 묶었는데, SSOT 를 원본 가이드로 다시 채우고 나니 §186 이 344줄 §187 이 604줄이라 한 기록에 담기지 않는다. 묶은 편은 「이 단계가 세우는 것」과 「전제와 되돌리기」와 「막히면」이 단계마다 다른데 그것을 한 벌만 적게 만들었고, 실제로 접어 넣을 때 원본의 「확인」 87건 가운데 절반이 유실됐다. 나눈 기준은 SSOT 가 이미 절로 갈라 둔 곳이다 — 두 절이 각각 여덟 칸(어디서 치는가·이 단계가 세우는 것·전제와 되돌리기·먼저 본다·실행 절차·확인 묶음·막히면·성립 범위)을 따로 갖고 있다. **세우는 것** — Arch 에 `qemu-full`·`libvirt`·`virt-install`·`dnsmasq` 를 깔고 `libvirtd.socket`(`.service` 가 아니다)을 켜고 사용자를 `libvirt` 그룹에 넣은 뒤, `LIBVIRT_DEFAULT_URI=qemu:///system` 을 `~/.bashrc` 에 고정하고 `default` 네트워크가 재부팅 뒤에도 살아 있게 한다. 끝나는 상태는 `virsh list --all` 이 `sudo` 없이 오류 없이 끝나는 것 하나이고 표는 비어 있다. **읽는 사람이 그대로 치는 명령이라 Setup 이다.** 패키지 설치·유닛 활성화·그룹 추가·네트워크 기동이 전부 복사돼야 하는 명령이고, 사람이 내용을 읽고 고쳐야 하는 파일은 `~/.bashrc` 하나뿐이라 거기만 편집기로 연다 — 이 실험대는 `echo >>` 로 넣었고 두 번 따라 하면 같은 줄이 한 번 더 붙는다. **먼저 보는 것 둘**(가상화 확장 플래그와 KVM 모듈)이 이 단계의 앞에 있고, 여기서 막히면 뒤의 어떤 단계도 의미가 없다. **가드레일 셋**(observed) — `.service` 가 아니라 `.socket` 을 켠다, `usermod` 뒤에 로그아웃하고 다시 들어온다, `net-list` 에 `--all` 을 준다(빼면 `inactive` 인 네트워크가 목록에 아예 안 나와 「없음」과 「꺼짐」을 구분할 수 없다). **이 절차가 감당하지 않는 것** — 빈 목록을 대상의 부재로 읽는 문제는 reference:tool-output-is-not-the-subject-state 가 받는다. **유효 범위** — 만드는 명령은 구축할 때 친 것을 옮긴 것이고 재실행으로 검증되지 않았다(unknown). `lsmod | grep kvm` 의 출력은 캡처돼 있지 않고, 호스트 코어 수가 가이드의 「16 코어」와 §178 의 「논리 코어 8」로 갈린 채 남아 있다. 원본 가이드 00 에 되돌리는 절차가 없어 이 편에는 되돌리기가 없다.",
|
|
"relations": [
|
|
"setup:create-three-guests-with-cloud-init",
|
|
"concept:what-a-qcow2-file-carries",
|
|
"reference:tool-output-is-not-the-subject-state",
|
|
"reference:verify-a-build-guide-in-execution-order",
|
|
"question:does-the-guide-rebuild-this-lab"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "lab-environment-build/setup/setup-prepare-the-lab-host-for-virtualization.md",
|
|
"status": "게시 전",
|
|
"studioId": "7c66a553-0008-4294-a27a-687bd1bda0c1",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다",
|
|
"kind": "setup",
|
|
"slug": "create-three-guests-with-cloud-init",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#187-단계-01-게스트-세-대",
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"pinned-versions": [
|
|
"libvirt = 12.7.0",
|
|
"Debian GNU/Linux = 12 (bookworm)",
|
|
"cloud-init = 22.4.2"
|
|
],
|
|
"classification": "**단계 01 을 setup:prepare-the-lab-host-for-virtualization 에서 떼어 낸 편이다**(2026-09-14 재분해). SSOT §187 이 604줄이고 그 안에 확인만 스물 몇 건이라, 앞 단계와 한 기록에 담으면 둘 중 하나가 요약된다. **세우는 것** — Debian 12 genericcloud 이미지 한 장을 받아 그 위의 오버레이로 게스트 셋을 만든다 — `kc-lab-edge`(1 vCPU · 1024MB · 10GB) · `kc-lab-1`(2 vCPU · 5120MB · 20GB) · `kc-lab-2`(2 vCPU · 4096MB · 20GB). 게스트마다 `#cloud-config` 파일과 시드 ISO 를 만들고, MAC 에 주소를 못 박는 DHCP 예약을 넣고, `virt-install` 로 띄운다. **읽는 사람이 그대로 치는 명령이라 Setup 이다.** 절차의 절반이 셸에 붙여 넣는 코드블록이고(`xorrisofs` · `vol-create-as`/`vol-upload` · `virsh net-update` 세 줄 · `virt-install` 세 줄), Case 의 평문 한 칸에 담으면 복사가 안 된다. 사람이 내용을 읽고 이해해야 하는 파일이 둘(`kc-lab-1.yaml` 과 `meta-kc-lab-1`)이라 그 둘만 편집기로 열고, 나머지는 CLI 를 그대로 둔다. **순서가 결과를 바꾸는 곳이 둘**(observed) — DHCP 예약이 VM 생성보다 먼저여야 하고(뒤면 게스트가 동적 대역에서 아무 주소나 잡고 예약을 나중에 넣어도 리스가 유지된다), `vol-create-as` 뒤에 `vol-upload` 가 따라야 한다(빠뜨리면 목록에는 이름이 보이는데 안이 0 이다). **가드레일 셋**(observed) — 시드를 `--cloud-init` 으로 붙이지 않고 `bus=virtio` 로 붙인다(Debian genericcloud 에는 AHCI 드라이버가 없어 데이터소스를 못 찾는다), `sudo` 는 리스트가 아니라 문자열로 적는다(게스트의 cloud-init 22.4.2 스키마 검사기가 리스트를 거부한다), `virsh net-update` 에 `--live --config` 를 둘 다 준다. **끝났다는 판정**은 `virsh list --all` 의 세 줄과 게스트의 `hostname`·`cloud-init status: done` 이고 대기는 약 50초였다(observed). **이 절차가 감당하지 않는 것** — 실패 증상 쪽은 case:cloud-init-failures-all-look-like-ssh-refused 가 받는다. 여기는 세우는 순서와 명령만 적고, 「SSH 가 안 붙는다」로만 보이는 실패 넷을 가르는 방법은 그 Case 를 가리킨다. **유효 범위** — `virt-install` 세 줄은 구축할 때 친 것을 옮긴 것이고 재실행으로 검증되지 않았다(unknown). `kc-lab-1.yaml` 을 템플릿에서 어떻게 만드는지가 원본에도 없고, `kc-lab.yaml.example` 이 반입되지 않아 대조하지 못했다. 원본 가이드 01 에 되돌리는 절차가 없다.",
|
|
"relations": [
|
|
"setup:prepare-the-lab-host-for-virtualization",
|
|
"case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"setup:install-k3s-server-and-agent",
|
|
"concept:what-a-qcow2-file-carries",
|
|
"reference:verify-a-build-guide-in-execution-order",
|
|
"question:vm-configured-vs-current-memory"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "lab-environment-build/setup/setup-create-three-guests-with-cloud-init.md",
|
|
"status": "게시 전",
|
|
"studioId": "f972ca27-7e18-41c1-9494-59cc6f676ae2",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다",
|
|
"kind": "setup",
|
|
"slug": "install-k3s-server-and-agent",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#188-단계-02-k3s-server-와-agent",
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"pinned-versions": [
|
|
"k3s = v1.36.4+k3s1",
|
|
"Debian GNU/Linux = 12 (bookworm)"
|
|
],
|
|
"classification": "**세우는 것** — `kc-lab-1` 에 k3s server, `kc-lab-2` 에 agent 를 깔고 둘 다 `--node-ip` 를 명시한다. lab host 에는 `~/.kube/config` 를 `600` 으로 두되 k3s 가 쓴 `https://127.0.0.1:6443` 을 `https://192.168.122.11:6443` 으로 고친 사본이고, 워크스테이션에서는 같은 파일을 고치지 않고 SSH 터널로 쓴다 — **같은 파일이라도 어느 기계에서 읽느냐에 따라 맞는 주소가 다르다**는 것이 이 단계의 요지다. k3s 가 딸려 오게 하는 다섯(Traefik · servicelb · local-path · flannel · kube-router)도 여기서 함께 선다. **Setup 인 이유** — 설치 한 줄, kubeconfig 세 줄, 토큰을 꺼내는 두 줄, agent 설치 한 줄이 전부 그대로 복사돼야 하는 명령이고, 특히 kubeconfig 세 줄은 「왜 세 줄이 다 필요한가」가 명령마다 다르다(`mkdir` 이 없으면 리다이렉션이 경로를 못 만들고, `sed` 가 없으면 lab host 가 자기 자신을 두드리고, `600` 은 이것이 클러스터 admin 자격증명이기 때문이다). **비밀은 길이만 본다**(§185 의 ②) — `echo \"${#TOKEN} 자\"` 로 확인하고 이 실험대에서는 108자였다(observed). 그 앞에 `[ ${#TOKEN} -ge 50 ] || ...` 가드를 두는 것이 case:an-empty-token-installed-the-agent-anyway 가 기록한 실패를 막는 자리이고, 히스토리에 남기고 싶지 않으면 `--token-file` 로 넘기는 형태가 따로 있다. **끝났다는 판정은 `Ready` 두 줄이 아니다** — `-o wide` 의 INTERNAL-IP 가 `--node-ip` 로 준 값과 같은지까지 본다. 어긋나도 지금은 증상이 없고 단계 03 의 nginx upstream 과 노드 상실 실험에서 뒤늦게 갈린다. `systemctl cat` 으로 유닛 이름이 노드마다 다르다는 것(`k3s` 대 `k3s-agent`)도 여기서 확인해 둔다. **정상인데 실패로 읽히는 것 하나** — agent 노드에서 `kubectl` 이 `localhost:8080` 으로 거절되는 것은 kubeconfig 가 없기 때문이고, 그것을 채우려고 admin kubeconfig 를 복사하지 않는다. **유효 범위** — 설치 명령 두 줄은 재실행으로 검증되지 않았고(unknown), 토큰 108자는 판올림에 따라 달라진다.",
|
|
"relations": [
|
|
"case:an-empty-token-installed-the-agent-anyway",
|
|
"setup:create-three-guests-with-cloud-init",
|
|
"setup:edge-nginx-and-host-dnat",
|
|
"reference:check-the-nearest-layer-first",
|
|
"reference:verify-a-build-guide-in-execution-order",
|
|
"question:does-the-guide-rebuild-this-lab"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "lab-environment-build/setup/setup-install-k3s-server-and-agent.md",
|
|
"status": "게시 전",
|
|
"studioId": "5e629c2f-e653-4dd9-b1c9-1fe6b2bb181d",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "엣지 게스트에 nginx 를 세우고 호스트 DNAT 으로 밖에서 들어오는 길을 연다",
|
|
"kind": "setup",
|
|
"slug": "edge-nginx-and-host-dnat",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat",
|
|
"final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나",
|
|
"final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"pinned-versions": [
|
|
"nginx (엣지 게스트) = 1.22.1",
|
|
"Debian GNU/Linux = 12 (bookworm)",
|
|
"libvirt = 12.7.0"
|
|
],
|
|
"classification": "**세우는 것** — 엣지 게스트에 nginx 를 깔고 `sites-available/keycloak-lab` 에 upstream 둘(`192.168.122.11:80` · `192.168.122.12:80`)과 80 서버 블록을 쓴 뒤 `sites-enabled/default` 를 지운다. lab host 쪽에는 `/etc/nftables.d/lab-edge-dnat.nft` 와 `lab-edge-dnat.service` 둘을 둔다. **이 단계에서 물리 호스트가 실험대를 위해 하는 일이 끝난다** — DNAT 규칙 하나와 단계 01 의 DHCP 예약 세 줄이 전부다. **Setup 인 이유** — 저장소 원본이 있는 설정 파일 넷을 옮겨 놓는 절차이고(`deploy/lab/edge/` 의 `nginx-keycloak-lab.conf` · `lab-edge-dnat.nft` · `lab-edge-dnat.service` · `reload-nginx.sh`), 파일 내용 자체가 본문에 그대로 들어가야 한다. **왜 여기만 호스트에서 치는가** — 규칙 첫 줄이 `iifname \"tailscale0\"` 인데 VM 에는 Tailscale 을 넣지 않기로 했으므로 엣지에는 그 인터페이스가 없다. **이 단계에서는 80 만 세운다** — 인증서가 없는데 `ssl_certificate` 줄을 미리 써 두면 설정 전체가 실패해 80 블록까지 안 뜬다. **가드레일 둘** — SNAT 을 걸지 않는다(masquerade 를 붙이면 엣지가 모든 클라이언트를 `192.168.122.1` 로 보고 이 실험대가 재는 `X-Forwarded-For` 계약이 무의미해진다), `X-Forwarded-Proto` 는 실제로 들어오는 프로토콜과 같게 둔다(80 인데 `https` 라고 적으면 Keycloak 이 리다이렉트를 `https://` 로 만들어 로그인 도중 끊긴다 — 이 값은 단계 04 에서 바뀐다). **끝났다는 판정은 아래에서 위로 네 층**이다 — `curl -I http://192.168.122.11` 이 `404`(Traefik 이 답했다는 신호다), `192.168.122.10` 이 `301`, 밖에서 도메인이 `301`, TLS 이후가 `200`. 한 층씩 끼워 두면 「DNAT 이 문제인가 엣지 안이 문제인가」를 헷갈리지 않는다. **이 절차가 감당하지 않는 것** — libvirt 의 `guest_input` 이 밖에서 오는 요청을 `reject` 로 끊는 문제는 case:nftables-accept-did-not-stop-the-libvirt-reject 가 받는다. 여기서는 유닛의 `ExecStartPost` 가 그 구멍을 맨 앞에 `insert` 한다는 사실만 적는다 — libvirt 가 네트워크를 다시 세우면 날아가기 때문에 유닛에 붙어 있다. **유효 범위** — 이 호스트의 libvirt `firewall_backend` 는 nftables 이고 iptables 일 때 같은 구멍이 필요한지는 재지 않았다(unknown). 저장소 원본과 대조한 것은 `.nft` 와 유닛 파일 둘이다(observed).",
|
|
"relations": [
|
|
"case:nftables-accept-did-not-stop-the-libvirt-reject",
|
|
"decision:edge-nginx-moved-into-a-guest-vm",
|
|
"setup:install-k3s-server-and-agent",
|
|
"setup:wildcard-certificate-with-dns-01-and-a-deploy-hook",
|
|
"reference:check-the-nearest-layer-first",
|
|
"question:guest-input-hole-under-the-iptables-backend"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "lab-environment-build/setup/setup-edge-nginx-and-host-dnat.md",
|
|
"status": "게시 전",
|
|
"studioId": "74a7bacf-e5d8-4129-926a-c8cf5cacb8c9",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "DNS-01 으로 와일드카드 인증서를 받고 갱신이 서빙까지 닿게 한다",
|
|
"kind": "setup",
|
|
"slug": "wildcard-certificate-with-dns-01-and-a-deploy-hook",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"pinned-versions": [
|
|
"nginx (엣지 게스트) = 1.22.1",
|
|
"nginx (물리 호스트) = 1.30.4",
|
|
"Debian GNU/Linux = 12 (bookworm)"
|
|
],
|
|
"classification": "**세우는 것** — 엣지 게스트에 `certbot` 과 `python3-certbot-dns-cloudflare` 를 깔고, `/etc/letsencrypt/cloudflare.ini` 를 `600` 으로 두고, `-d hyeonworks.com -d '*.hyeonworks.com'` 으로 인증서를 받은 뒤, 단계 03 의 nginx 파일을 443 블록이 있는 모양으로 바꾸고, `renewal-hooks/deploy/reload-nginx.sh` 를 `chmod +x` 로 넣는다. 인증서·certbot·타이머·훅이 전부 엣지 게스트에 살고 물리 호스트에는 아무것도 두지 않는다. **Setup 인 이유** — 순서가 그 자체로 보안 조치인 명령이 있다. `install -m 600 /dev/null` 을 **비어 있을 때** 먼저 쳐서 권한을 만들고 그다음에 토큰을 쓴다 — 반대로 하면 그사이가 열려 있다. 확인도 `-rw-------` 과 바이트 수가 0 이 아닌지까지만 보고 값을 찍지 않는다. `--dry-run` 을 먼저 도는 것도 절차의 일부다(Let's Encrypt 의 주당 중복 인증서 5장 한도를 dry-run 이 쓰지 않는다). **왜 DNS-01 인가는 따로 있다** — 그 판단과 감수한 비용은 decision:dns-01-because-the-lab-is-not-on-the-public-internet 이 받고, 여기는 그 결정을 실행하는 명령만 적는다. **이 단계의 알맹이는 갱신이 서빙까지 닿게 하는 것이다** — 배포판 기본 `certbot-renew.service` 에는 `ExecStartPost` 도 `--deploy-hook` 도 없어서(observed) 인증서를 새로 받는 데까지만 책임지고, nginx 는 기동 시점에 읽은 인증서를 메모리에 들고 있으므로 경로가 그대로인 채 내용만 바뀌어도 모른다. 훅에 실행 권한이 없으면 certbot 이 **조용히 건너뛴다**. `post/` 가 아니라 `deploy/` 에 넣는 이유도 절차에 적힌다 — `post/` 는 갱신이 없어도 매번 돌아 하루 두 번 워커를 갈아치운다. **끝났다는 판정은 tailnet 에 붙은 다른 기계에서 친다** — 엣지 안에서 치면 `Connection refused` 가 나는데 그것은 설정 문제가 아니라 친 곳의 문제다. 체인은 `openssl s_client` 로 번호가 3까지 가는지 보고(단계가 1개면 `cert.pem` 을 쓴 것이고 그때도 브라우저는 정상으로 보인다), 갱신은 로그 문구가 아니라 **워커 PID** 로 판정한다. **유효 범위** — 443 을 얹기 전에 nginx 판 번호를 본다. `http2` 를 지시어로 쓰려면 1.25.1 이상이고 엣지는 1.22.1 이라 `listen` 의 파라미터로 쓴다. 이 절차가 서빙까지 닿는 것을 실제로 잰 기록은 case:renewal-succeeded-while-the-old-certificate-kept-serving 에 있고, 거기 적힌 2305초는 훅이 물리 호스트에만 있던 시절 값이라 지금 배치에서 다시 재지 않았다(inferred). 체인 실측도 이름을 따로 받던 시절 것이고 와일드카드로 받은 지금의 `s_client` 출력은 SSOT 에 없다(unknown).",
|
|
"relations": [
|
|
"decision:dns-01-because-the-lab-is-not-on-the-public-internet",
|
|
"case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"setup:edge-nginx-and-host-dnat",
|
|
"reference:tool-output-is-not-the-subject-state",
|
|
"reference:check-the-nearest-layer-first"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "lab-environment-build/setup/setup-wildcard-certificate-with-dns-01-and-a-deploy-hook.md",
|
|
"status": "게시 전",
|
|
"studioId": "975a6d61-4e34-4034-a0d2-01fea3b498a3",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "Keycloak 2노드와 PostgreSQL 을 k3s 에 올리고 클러스터가 묶였는지 확인한다",
|
|
"kind": "setup",
|
|
"slug": "keycloak-two-nodes-and-postgres-on-k3s",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"pinned-versions": [
|
|
"k3s = v1.36.4+k3s1",
|
|
"curlimages/curl = 8.11.1"
|
|
],
|
|
"classification": "**세우는 것** — 저장소 루트에서 `kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml` 한 줄로 네임스페이스 `keycloak-lab` 과 그 안의 전부가 선다. StatefulSet `keycloak`(파드 `keycloak-0`·`keycloak-1`) · Deployment `postgres` · Secret `keycloak-lab-secrets` · `local-path` PVC · Ingress · JGroups 디스커버리 테이블 `jgroups_ping` 과 메시지 포트 7800. **Setup 인 이유는 세우는 명령이 아니라 확인하는 명령 쪽에 있다.** `apply` 는 한 줄이고 그 뒤가 길다 — `kubectl get all` 은 이름과 달리 전부가 아니라서(Secret·ConfigMap·PVC·Ingress 가 안 나온다) 한 번 더 치고, Deployment→ReplicaSet→Pod 사슬 어디서 끊겼는지를 해시로 맞춰 보고, Secret 은 세 층(`describe` 의 바이트 수 · 파드 안의 `${#VAR}` 길이 · 둘이 같은가)으로 보고, Service 뒤에 파드가 있는지는 Endpoints 로 본다. 이 명령들이 본문에 코드블록으로 있어야 읽는 사람이 자기 클러스터에서 그대로 친다. **비밀은 길이만 본다** — `describe` 가 `19 bytes`·`22 bytes` 를 보여 주고 파드 안에서 `길이=19` 가 나오면 이어진 것이다. `-o yaml` 로 보지 않는다(base64 는 암호화가 아니라 인코딩이라 스크롤백과 화면 공유에 값이 그대로 남는다). **끝났다는 판정을 셋으로 가른다**(observed) — 로그의 `ISPN000094` 는 「그때 그렇게 보였다」, `jgroups_ping` 테이블은 「지금 등록되어 있다」, `vendor_cluster_size` 는 「지금 그 노드가 그렇게 안다」다. 테이블에는 둘 다 있는데 로그가 `(1)` 이면 서로를 찾기는 했는데 7800 으로 메시지가 안 가는 것이고, 이 실험대에서 실제로 그 일이 있었다. 각 노드가 자기가 아는 멤버 수를 보고하므로 **한 노드만 보면 분단을 놓친다**. **★ Keycloak 컨테이너에는 `curl` 이 없다**(observed. exit code 127) — 그래서 밖에서 묻고, Prometheus 가 없으면 `curlimages/curl:8.11.1` 임시 파드를 띄운다. **유효 범위** — `keycloak-cluster.yaml` 원문이 `source/` 에 반입되지 않아 파드 자원 한도·프로브·`persistent-user-sessions` 설정값은 SSOT 안에서 대조하지 못했다(unknown). 같은 이유로 Keycloak 과 PostgreSQL 의 판 번호가 SSOT 제6부에 적혀 있지 않아 고정한 버전에서 빠져 있다 — 지어내지 않는다. `local-path` 가 PVC 를 노드에 묶는 비용은 실험 쪽 기록이고 이 부의 범위 밖이다.",
|
|
"relations": [
|
|
"setup:install-k3s-server-and-agent",
|
|
"setup:prometheus-and-grafana-for-the-lab",
|
|
"setup:wildcard-certificate-with-dns-01-and-a-deploy-hook",
|
|
"reference:tool-output-is-not-the-subject-state",
|
|
"reference:check-the-nearest-layer-first"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "lab-environment-build/setup/setup-keycloak-two-nodes-and-postgres-on-k3s.md",
|
|
"status": "게시 전",
|
|
"studioId": "7b113a04-180a-40ec-9270-033531c22221",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "Prometheus 와 Grafana 를 올려 클러스터 안을 밖에서 본다",
|
|
"kind": "setup",
|
|
"slug": "prometheus-and-grafana-for-the-lab",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#192-단계-06-prometheus-와-grafana",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"pinned-versions": [
|
|
"k3s = v1.36.4+k3s1",
|
|
"Debian GNU/Linux = 12 (bookworm)"
|
|
],
|
|
"classification": "**세우는 것** — `kubectl apply -f deploy/lab/k8s/observability.yaml` 로 네임스페이스 `observability` 에 Grafana 1 · Prometheus 1 · node-exporter 2(DaemonSet, 노드마다 하나)가 뜨고, 스크레이프 대상이 `keycloak`·`kubelet`·`node-exporter`·`prometheus` 넷이 된다. Grafana 는 밖에 열지 않고 `port-forward svc/grafana 3000:3000` 으로만 본다 — 이 터널은 명령을 실행한 기계에서만 열리고 Ctrl+C 로 사라지므로 실험대의 노출면이 늘지 않는다. **왜 두는가**(observed) — 판정을 밖에서만 하면 놓친다. 7800 을 끊었는데 외부 응답이 전부 200 이었고, 분단된 노드가 스스로 로드밸런서에서 빠졌기 때문이다. **Setup 인 이유** — 확인 명령이 이 실험대의 도구 사정에 묶여 있다. `jq` 가 없어서 `grep -o '\"job\":\"[^\"]*\"' | sort -u` 와 `tr ',' '\\n'` 으로 필드를 뽑고, 그 이상 가공해야 하면 파서를 짜지 않고 화면의 JSON 을 그대로 읽는다. 이 형태가 본문에 그대로 있어야 읽는 사람이 친다. **끝났다는 판정에서 봐야 할 것은 거기 있는 이름이 아니라 없는 이름이다** — Redis·BFF·PostgreSQL 이 스크레이프 대상에 없고, 그것은 「안 찍은 것」이 아니라 「지표가 없는 것」이다. `node-exporter` 줄이 둘인지 세는 것도 같은 이유다 — 하나면 그 노드의 CPU·메모리·디스크 지표가 통째로 없는 채로 실험을 하게 된다. 값이 안 나올 때의 `\"result\":[]` 는 「0 이다」가 아니라 「그런 지표가 없다」는 뜻이다. **★ `up` 을 믿지 않는다**(observed) — 503 이 나는 동안에도 `up` 은 1 이었다. 프로세스가 살아 있고 `/metrics` 가 응답하기만 하면 1 이라 「살아 있지만 쓸모없는」 상태를 못 본다. 그래서 `vendor_cluster_size` 같은 기능 지표를 함께 보고, `up=1` 인데 밖에서 200 이 아니면 그 조합이 곧 증거다. **유효 범위** — `observability.yaml` 원문이 `source/` 에 없어 스크레이프 주기·보존 기간·Grafana 대시보드 구성은 대조하지 못했다(unknown). 같은 이유로 Prometheus·Grafana·node-exporter 의 판 번호가 SSOT 제6부에 없어 고정한 버전에는 이 스택이 올라탄 k3s 와 게스트 OS 만 적는다. 그리고 node-exporter 가 게스트 안에서 재는 값이라 호스트에서 같은 것을 재면 다른 수가 나올 수 있는데, 이 실험대는 호스트 쪽 지표를 긁지 않는다.",
|
|
"relations": [
|
|
"setup:keycloak-two-nodes-and-postgres-on-k3s",
|
|
"setup:install-k3s-server-and-agent",
|
|
"reference:tool-output-is-not-the-subject-state",
|
|
"question:does-the-guide-rebuild-this-lab"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "lab-environment-build/setup/setup-prometheus-and-grafana-for-the-lab.md",
|
|
"status": "게시 전",
|
|
"studioId": "0cb0f195-b06b-4f8e-b52e-675eb0918805",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "실험대를 철거하고 무엇이 남는지 확인한다",
|
|
"kind": "setup",
|
|
"slug": "tear-down-the-lab-and-know-what-survives",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#202-철거-실제-출력-전문",
|
|
"final/document.md#204-재구축할-때-무엇이-남아-있나",
|
|
"final/document.md#195-이-부의-출처와-범위"
|
|
],
|
|
"pinned-versions": [
|
|
"libvirt = 12.7.0",
|
|
"QEMU = 11.1.1",
|
|
"커널 = 7.2.2-arch1-1",
|
|
"호스트 = Arch Linux · i5-1135G7 · RAM 11,648MiB",
|
|
"게스트 = Debian 12 genericcloud"
|
|
],
|
|
"classification": "**기존 Setup 일곱은 세우는 쪽만 덮는다.** SSOT 가 그것을 직접 적었다 — 「가이드에는 세우는 절차만 있고 철거가 없다」. 이 편은 2026-09-10 에 실제로 돌린 철거의 명령과 출력 전문이다. **세 단계다.** ① 게스트 — `virsh destroy` 로 전원을 뽑고 `virsh undefine --remove-all-storage` 로 정의와 디스크를 함께 지운다. 끝났다는 판정은 `Volume` 줄이 **두 개** 나오는 것이다(`vda` 오버레이와 `vdb` 시드 ISO). ② DHCP 예약 — `virsh net-update default delete ip-dhcp-host` 로 세 줄을 지운다. **삭제할 때도 `mac`·`name`·`ip` 세 속성을 다 준다.** ③ 확인 — 철거 전후를 다섯 줄로 견준다(`virsh list --all` 3대→없음, `vol-list` 7개→`base.qcow2` 1개, 예약 3줄→0줄, `df -h /` 11G→7.9G, `virbr0` UP→DOWN). **읽는 사람이 그대로 치는 명령이라 Setup 이다.** 실험대를 쓰는 사람은 반드시 한 번은 내리고, 내리는 순서를 틀리면 다음 구축이 막힌다 — `--remove-all-storage` 를 빠뜨리면 도메인만 사라지고 디스크 파일이 남아 다음 `virt-install` 이 「이미 있다」로 실패한다. **가드레일 셋**(observed) — ① `virsh destroy` 는 종료 신호가 아니라 **전원을 뽑는 것**이다. 워크로드를 정상으로 내리고 싶으면 setup:power-cycle-the-lab-and-reallocate-guest-memory 의 종료 순서를 먼저 돌고 오고, 여기서는 지우는 것이 목적이라 뽑는다. ② 예약 삭제에서 속성이 하나라도 비면 `XML error: Cannot use host name '' in network 'default'` 로 거부된다. ③ **zsh 에서 루프로 돌리면 그 오류를 만난다** — zsh 는 따옴표 없는 변수를 단어 분리하지 않아 bash 에서 되던 `set -- $entry` 가 `$1` 에 문자열 전체를 넣고 `$2`·`$3` 을 비운다. 세 줄을 값 그대로 쓰는 편이 안전하다. **이 절차가 끝나면 무엇이 남아 있나** — 아홉 행의 표가 그것을 가른다. 남는 쪽은 `base.qcow2`(335MB · 다음 오버레이의 바닥), 패키지, `~/workspace/cloud/kc-lab-{1,2}.yaml`(키와 비밀번호가 들어 있다), `~/.ssh/config` 의 `kc-lab-*` 항목, libvirt `default` 네트워크 정의(예약만 지웠다), `/etc/letsencrypt/`(정책으로 남긴다)다. 사라지는 쪽은 게스트 디스크·시드 ISO·DHCP 예약·k3s 와 모든 워크로드다. **모르면 재구축이 「왜 이건 이미 있지」와 「왜 이건 없지」의 반복이 된다.** **`virbr0` 가 `DOWN` 인 것은 고장이 아니다** — 붙은 tap 인터페이스가 없어서이고 주소는 그대로 남아 VM 을 다시 띄우면 자동으로 `UP` 이 된다. `virsh net-start` 를 찾아 헤매지 않는다. **인증서를 지우지 않는 이유는 한도가 아니다** — 이 실험대의 이름 셋이 tailnet 주소를 가리켜 지금 재발급이 되는지를 모르기 때문이고, 백업은 `tar` 하나에 30초다. 어느 쪽인지는 question:is-this-lab-issuing-certificates-with-http-01-or-dns-01 가 한 줄로 닫는다. **이 절차가 감당하지 않는 것** — 호스트 계층 철거(`deploy/lab/host/teardown-host.sh`)는 그 스크립트가 `source/` 에 반입되지 않아 여기 없다(unknown). 다시 세우는 쪽은 setup:prepare-the-lab-host-for-virtualization 부터의 일곱 편이 받는다. **유효 범위** — 이 명령들은 2026-09-10 에 이 호스트에서 실제로 돌려 받은 출력이고, 다시 돌려 검증하지는 않았다 — 다시 돌리면 실험대가 없어진다.",
|
|
"relations": [
|
|
"setup:create-three-guests-with-cloud-init",
|
|
"setup:power-cycle-the-lab-and-reallocate-guest-memory",
|
|
"case:declared-memory-and-disk-are-ceilings-not-occupancy",
|
|
"question:is-this-lab-issuing-certificates-with-http-01-or-dns-01",
|
|
"question:does-the-guide-rebuild-this-lab",
|
|
"reference:tool-output-is-not-the-subject-state"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "lab-environment-build/setup/setup-tear-down-the-lab-and-know-what-survives.md",
|
|
"status": "게시 전",
|
|
"studioId": "87e0138d-dedc-4734-89b3-4ebe7c2df56d",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "실험대를 껐다 켜고 게스트 메모리를 다시 나눈다",
|
|
"kind": "setup",
|
|
"slug": "power-cycle-the-lab-and-reallocate-guest-memory",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#332-vm-메모리-재배분-게스트를-다시-만들지-않는다",
|
|
"final/document.md#333-안전한-종료-순서",
|
|
"final/document.md#334-복구-순서-종료의-역순",
|
|
"final/document.md#313-k3s-server와-agent-죽였을-때가-다르다"
|
|
],
|
|
"pinned-versions": [
|
|
"libvirt = 12.7.0",
|
|
"QEMU = 11.1.1",
|
|
"k3s = v1.36.4+k3s1",
|
|
"게스트 = Debian 12 genericcloud",
|
|
"기준 배치 = 2026-09-03 · 호스트 RAM 증설 뒤 재배분, 그 뒤 값은 2026-09-10 실측"
|
|
],
|
|
"classification": "**세우는 것도 지우는 것도 아니라 껐다 켜는 절차다.** 기존 Setup 일곱은 00~06 단계를 세우고 setup:tear-down-the-lab-and-know-what-survives 는 지운다. 실험대를 계속 쓰면 실제로 자주 하는 일은 셋째다. **① 게스트를 다시 만들지 않고 메모리를 재배분한다** — `virsh setmaxmem kc-lab-1 5120M --config` 다음에 `virsh setmem kc-lab-1 5120M --config`. `setmaxmem` 이 **상한**(부팅 시 게스트가 보는 총량)이고 `setmem` 이 **현재 할당**이며 현재값을 상한보다 크게 줄 수 없으므로 **순서가 정해져 있다.** `--config` 는 다음 부팅부터, `--live` 는 실행 중인 도메인에 즉시인데 `setmaxmem --live` 는 대개 거부된다 — 게스트가 부팅 시 메모리 맵을 정하기 때문이다. **상한을 바꾸려면 껐다 켠다.** 확인은 `virsh dominfo | grep -i memory` 와 게스트의 `free -m` 둘이다. 이 실험대는 호스트를 8GB→12GB 로 물리 증설한 뒤 이 방법으로 재배분했고 **게스트 재생성이나 디스크 조작은 전혀 필요 없었다.** **② 안전한 종료는 위에서부터다** — Keycloak StatefulSet 을 0으로 내려 클러스터에서 정상 탈퇴시키고(`wait --for=delete`), PostgreSQL 을 마지막에 충분한 시간을 주고 내리고, 게스트를 `virsh shutdown` 으로 ACPI 정상 종료한 뒤 호스트를 끈다. **왜 순서가 중요한가** — `virsh shutdown` 은 게스트 systemd 가 k3s 를 멈추고 k3s 가 컨테이너에 SIGTERM 을 보내는 연쇄인데, 유예 시간이 짧으면 **PostgreSQL 이 강제 종료되어 다음 기동에 crash recovery 가 돈다.** 미리 내려 두면 그 위험이 없다. clean shutdown 판정은 `postmaster.pid` 가 **남아 있지 않은 것**이다. **③ 복구는 역순이고 PostgreSQL 이 먼저다** — Keycloak 이 DB 없이 뜨면 기동에 실패한다. 그리고 **스케일을 0으로 내려 두면 자동으로 복구되지 않는다.** 명시적으로 올려야 한다. **읽는 사람이 그대로 치는 명령이라 Setup 이다.** 글쓴이만 다시 돌릴 재현 순서가 아니다 — 실험대를 물려받은 사람이 노트북을 끄기 전에 반드시 하는 일이고, 순서를 틀렸을 때의 대가(crash recovery)가 자료에 적혀 있다. **이 절차가 감당하지 않는 것** — k3s server 와 agent 를 **죽였을 때가 다르다**는 것(agent 를 죽이면 그 노드의 워크로드만 사라지고 server 를 죽이면 관측과 조작 수단이 함께 사라진다)은 여기서 한 줄로 적고, 왜 그래서 관측 스택을 server 쪽에 두는지는 decision:two-guest-vms-instead-of-installing-k3s-on-the-host 와 setup:prometheus-and-grafana-for-the-lab 이 받는다. 지우는 쪽은 setup:tear-down-the-lab-and-know-what-survives 다. **유효 범위** — 이 절차의 근거는 제9부(`session-lab-concepts.md`)이고 그 문서의 실측 스냅샷은 2026-09-03, 재배분 기록은 2026-09-11 이다. 제7부의 2026-09-10 값과 날짜가 엇갈리므로 게스트 메모리 수치는 그 시점의 것으로 읽는다. 이 순서대로 다시 돌려 검증하지는 않았다(unknown).",
|
|
"relations": [
|
|
"setup:tear-down-the-lab-and-know-what-survives",
|
|
"setup:keycloak-two-nodes-and-postgres-on-k3s",
|
|
"setup:install-k3s-server-and-agent",
|
|
"decision:two-guest-vms-instead-of-installing-k3s-on-the-host",
|
|
"case:declared-memory-and-disk-are-ceilings-not-occupancy",
|
|
"question:vm-configured-vs-current-memory"
|
|
],
|
|
"publication": "게시됨",
|
|
"file": "lab-environment-build/setup/setup-power-cycle-the-lab-and-reallocate-guest-memory.md",
|
|
"status": "게시 전",
|
|
"studioId": "8a9ca3d4-7d6e-4f38-862d-296b2640d288",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"reference": [
|
|
{
|
|
"title": "단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다",
|
|
"kind": "reference",
|
|
"slug": "verify-a-build-guide-in-execution-order",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#182-이-구축에서-드러난-문서-결함의-공통-원인",
|
|
"final/document.md#178-이-부의-출처와-범위"
|
|
],
|
|
"classification": "§182 이 규칙을 그대로 적었다 — 「그래서 이런 문서는 작성 시점이 아니라 실행 순서로 검증해야 한다. 각 단계에서 「이 시점에 이 리소스가 존재하는가」, 「이 셸에서 이 명령이 도는가」를 따로 본다.」 그 앞에 근거가 되는 결함 여섯이 표로 있다(observed) — 03 단계에 nginx 설치 단계가 없어 `/etc/nginx: No such file or directory`, 03 단계의 설정 블록이 `http2 on;` 이라 Debian 12 의 nginx 1.22 에서 `unknown directive`, 04 단계의 인증서 경로가 lineage 이름과 달라 와일드카드는 `live/hyeonworks.com/` 인데 `live/auth.hyeonworks.com/` 이라 적혀 있었고, 00·03·05·06 단계가 저장소를 lab host 에 있다고 가정해 `cp: cannot stat 'deploy/...'`, 05 단계가 그 시점에 없는 리소스를 `-l app=bff` 로 조회하고(BFF 는 한참 뒤에 뜬다), 04 단계의 확인 명령을 칠 위치가 틀렸다(엣지 VM 안에서 tailnet 주소를 치면 `connection refused` — 게스트에는 Tailscale 이 없다). 공통 원인은 하나다(inferred) — 개별 명령은 전부 실제로 돌았던 것이고, 틀린 것은 명령이 아니라 그 명령이 놓인 위치다. 나중 시점의 환경에서 확인한 명령과 출력을 앞 단계에 적으면 각 줄은 참인데 순서대로 따라가면 막힌다. 여섯을 따로 쪼개지 않은 것은 같은 물음(왜 순서대로 따라가면 막히나)에서 나와 같은 결론에 닿기 때문이고, 한 편의 Case 로 두지 않고 Reference 로 둔 것은 원 프로젝트의 이름(hyeonworks·BFF·Tailscale·가이드 번호)을 지워도 규칙이 남기 때문이다. 여섯 결함은 이 Reference 본문의 표가 된다.",
|
|
"scope": "사람이 한 단계씩 따라 실행하도록 쓴 구축·운영 문서 전부에 적용한다 — 이 부의 기반 7단계 가이드처럼 앞 단계의 결과 위에 뒤 단계가 서는 문서다. 검사하는 축이 둘이고 §182 이 그 둘을 직접 적었다 — 시점(이 시점에 이 리소스가 존재하는가)과 셸(이 셸에서 이 명령이 도는가). 실행 방법은 각 단계를 그 단계가 실제로 놓이는 자리에서 한 번 돌려 보고, 명령을 칠 주체가 호스트인지 게스트인지를 문서가 매번 말하게 하는 것이다. 결함이 나온 자리가 그 두 축을 그대로 가리킨다 — 설치·복사·조회는 시점 쪽에서 깨졌고(`/etc/nginx` 부재, `cp: cannot stat`, `-l app=bff`), 확인 명령의 위치는 셸 쪽에서 깨졌다(게스트 안에서 tailnet 주소).",
|
|
"exceptions": "이 규칙이 잡는 것은 순서와 위치이지 명령의 정확성이 아니다. §182 이 적었듯 개별 명령은 전부 실제로 돌았던 것이라, 이 검사를 통과해도 오타·잘못된 플래그·낡은 옵션은 그대로 남는다. 배포판 차이도 이 축에서는 안 잡힌다 — `http2 on;` 이 Debian 12 의 nginx 1.22 에서 막힌 것은 순서 문제가 아니라 지시어가 1.25.1 이상이라는 버전 문제이고, 그것은 대상 배포판에서 실제로 돌려 봐야 나온다(여섯 중 이 한 줄만 성격이 다르다). 한 번 통과한 문서가 계속 통과하지도 않는다 — 단계가 하나 끼어들거나 환경이 바뀌면 같은 검사를 다시 돌려야 한다. 그리고 이 저장소에는 이 규칙으로 가이드를 고친 뒤 처음부터 다시 따라가 본 기록이 아직 없다 — 규칙은 결함 여섯의 공통 원인에서 나온 것이고 규칙 자체가 결함을 막아 냈다는 관측은 없다.",
|
|
"relations": [
|
|
"decision:edge-nginx-moved-into-a-guest-vm",
|
|
"case:nftables-accept-did-not-stop-the-libvirt-reject",
|
|
"question:qcow2-transfer-time-over-wifi"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/reference/reference-verify-a-build-guide-in-execution-order.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "같은 설정 파일이 두 배포판에서 같은 뜻이 아니다 — 옮기기 전에 세 가지를 본다",
|
|
"kind": "reference",
|
|
"slug": "a-config-file-does-not-mean-the-same-thing-on-two-distros",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#203-실측으로-드러난-함정-셋",
|
|
"final/document.md#284-nginx-설정-구조-sites-available-은-nginx-기능이-아니다",
|
|
"final/document.md#288-게스트-배포판-debian이란-무엇이고-ubuntu와-무엇이-다른가",
|
|
"final/document.md#286-패키지명-대응표",
|
|
"final/document.md#287-없어서-오히려-편한-것",
|
|
"final/document.md#285-롤링-릴리스와-부분-업그레이드-금지",
|
|
"final/document.md#212-\"이건-arch라서-하는-건가-\"에-대한-답",
|
|
"final/document.md#209-nginx-keycloak-lab-conf-스티키-스위치와-신뢰-경계"
|
|
],
|
|
"classification": "**규칙** — 설정 파일을 배포판이 다른 기계로 옮길 때 셋을 본다. ① **그 지시어가 대상의 판올림에 있는가.** `http2 on;` 은 nginx 1.25.1 이상이라 Debian 12 의 1.22 에서 `unknown directive \"http2\"` 로 설정 전체가 죽고, `listen 443 ssl http2;` 형태는 1.22 와 1.30 양쪽에서 다 돈다 — **양쪽에서 도는 형태가 무엇인지까지 확인해야 규칙이 답을 낸다.** ② **패키지가 기본으로 켜 둔 것이 충돌하지 않는가.** Debian 계열은 `/etc/nginx/sites-enabled/default` 가 처음부터 붙어 `:80 default_server` 로 선언돼 있어 같은 선언과 충돌한다 — 심볼릭 링크를 걸 때 같이 지운다. ③ **그 배포판이 그 관례를 갖고 있는가.** `sites-available`/`sites-enabled` 는 **nginx 의 기능이 아니라 Debian/Ubuntu 패키지 메인테이너가 만든 관례**다. nginx 가 아는 것은 `include` 뿐이라 Arch 에서는 `nginx.conf` 의 `http { }` 안에 `include /etc/nginx/sites-enabled/*;` 를 직접 넣어야 이 파일이 효력을 갖는다. **②와 ③이 같은 관례의 양면이다** — 한쪽은 있어서 충돌하고 한쪽은 없어서 손으로 넣어야 한다. **근거**(observed) — 이 실험대는 호스트가 Arch(nginx 1.30.4)이고 엣지 게스트가 Debian 12(nginx 1.22.1)라 같은 설정이 두 판올림 사이를 오갔고, §203 이 실측으로 드러난 함정 셋을 적었는데 셋 다 배포판 차이였다. 세 번째 함정과 짝이 되는 사실이 `deploy/lab/edge/nginx-keycloak-lab.conf` 의 주석에 있다 — 파일 자신이 `listen` 의 인자 형태를 고른 이유를 「그 지시어는 nginx ≥ 1.25.1 이 필요하고 엣지 게스트는 Debian 12(nginx 1.22)다. 이 형태는 양쪽에서 다 돌고 이 실험대가 실제로 돌리는 것이다」로 적어 두었다. cloud-init 쪽에도 같은 모양이 하나 있다 — 게스트의 cloud-init 22.4.2 스키마 검사기가 `sudo` 리스트 형태를 거부하는데 **그 형태로도 부팅은 된다.** 판올림이 다르면 검사기의 판정도 달라진다. **적용하면 실제로 달라지는 것** — 이 셋을 보지 않고 옮기면 증상이 「설정 전체가 안 뜬다」나 「왜 이 파일이 무시되지」로 나타나고 둘 다 원인이 파일 안에 없다.",
|
|
"scope": "사람이나 스크립트가 한 기계에서 쓰던 설정 파일을 **다른 배포판·다른 판올림의 기계로 옮기는 모든 자리**에 적용한다 — 이 실험대에서는 호스트(Arch)와 게스트(Debian 12) 사이, 그리고 운영과 실험대 사이다. 특히 자주 걸리는 것이 데몬 설정(nginx·systemd 유닛)과 부트 시점 설정(cloud-init)이다. **적용 범위를 가르는 기준이 하나 더 있다** — §212 가 적었듯 낯선 것의 대부분은 배포판 때문이 아니다. 클라우드가 대신 해 주던 일(KVM·libvirt·cloud-init·DHCP 예약)과 이미 누가 해 두었던 일(nginx upstream·certbot·k3s 설치)이 대부분이고 **진짜 배포판 고유는 얼마 안 된다**(이 실험대에서는 `conf.d` include 부재·롤링 업그레이드·`libvirtd.socket`·패키지명). 그러니 이 규칙은 **막히는 것마다 꺼내 드는 설명이 아니라, 파일을 옮길 때 한 번 도는 검사다.** 같은 구성을 Ubuntu 에서 해도 가상화·네트워크 층은 명령 이름만 조금 바뀐다.",
|
|
"exceptions": "**같은 계열 안에서도 판올림이 다르면 걸린다** — Debian 과 Ubuntu 는 계열이 같지만 패키지 판올림이 달라 ①이 그대로 적용된다. 반대로 **배포판이 같으면 안 걸리는 것도 아니다** — Arch 는 롤링 릴리스이고 **부분 업그레이드를 지원하지 않아** `pacman -Sy 패키지` 로 DB 만 갱신하고 일부만 설치하면 같은 기계에서도 라이브러리 판이 어긋난다. **이 규칙이 잡지 못하는 것** — 순서와 위치 문제는 이 축에서 안 잡힌다. reference:verify-a-build-guide-in-execution-order 가 그쪽을 맡고, 그 기록이 자기 예외 절에서 이쪽을 가리킨다(「배포판 차이도 이 축에서는 안 잡힌다 … 대상 배포판에서 실제로 돌려 봐야 나온다」). 두 규칙이 서로의 사각을 덮는다. **실행으로만 확인되는 것도 있다** — ①은 문서로 판올림을 대조해 예측할 수 있지만 ②·③은 그 배포판에 실제로 깔아 봐야 드러난다(기본으로 붙어 있는 사이트가 무엇인지, 그 배포판이 어떤 include 관례를 갖는지는 패키지 메인테이너가 정한다). **배포판 차이가 아닌 것을 이 규칙으로 설명하지 않는다** — SELinux/AppArmor 가 Arch 에 기본 활성이 아닌 것은 이 규칙이 잡는 종류이지만, RHEL 계열에서 k3s 에 정책 패키지가 필요한 것은 옮긴 설정의 문제가 아니라 그 배포판의 보안 모듈 문제다.",
|
|
"relations": [
|
|
"reference:verify-a-build-guide-in-execution-order",
|
|
"setup:edge-nginx-and-host-dnat",
|
|
"setup:create-three-guests-with-cloud-init",
|
|
"case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reference:check-the-nearest-layer-first"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/reference/reference-a-config-file-does-not-mean-the-same-thing-on-two-distros.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"question": [
|
|
{
|
|
"title": "libvirt 의 firewall_backend 가 iptables 일 때도 guest_input 에 구멍이 필요한가",
|
|
"kind": "question",
|
|
"slug": "guest-input-hole-under-the-iptables-backend",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#183-이-부에서-파생될-open-question-oq-1",
|
|
"final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다"
|
|
],
|
|
"known": "§180 가 이 호스트에서 본 것을 적었다 — libvirt 가 자기 테이블 `ip libvirt_network` 의 `guest_input` 체인을 `reject` 로 끝내고, 우리가 `priority filter - 10` 으로 먼저 돌게 둔 `forward` 체인의 `ct state new accept` 는 그 `reject` 를 막지 못했다. nftables 는 같은 훅에 붙은 base 체인을 우선순위 순으로 전부 평가하고 `accept` 는 「이 체인은 통과」일 뿐 `drop` 만이 즉시 종결이기 때문이다. 그래서 구멍을 libvirt 체인 맨 앞에 `insert` 로 뚫었고, libvirt 가 네트워크를 다시 세우면 날아가므로 DNAT 유닛의 `ExecStartPost` 에 넣었다. §180 는 이 호스트가 nftables 백엔드라고 밝히고 iptables 백엔드는 재지 않았다고 미확인으로 남겼다.",
|
|
"unknown": "libvirt 의 `firewall_backend` 를 iptables 로 둔 호스트에서 밖→게스트 FORWARD 경로를 무엇이 끝내는지, 그리고 그때 우리 `forward` 체인의 `ct state new accept` 가 실제로 먹는지 아니면 거기서도 libvirt 쪽 규칙에 구멍을 따로 뚫어야 하는지.",
|
|
"next-verification": "§183 이 적은 그대로 백엔드를 갈아 재 본다 — libvirt 의 `firewall_backend` 를 iptables 로 두고 네트워크를 다시 세운 뒤 밖에서 엣지로 `curl` 을 치고 결과가 응답인지 connection refused 인지, 그리고 응답까지 걸린 시간을 적는다. 같은 시각에 libvirt 가 만든 규칙을 그대로 덤프해 어느 규칙이 패킷을 끝내는지와 그 카운터가 친 횟수와 맞는지를 본다 — §180 에서 범인을 확정한 것이 그 카운터였다. 출력 원문을 `final/evidence/raw/` 에 남기고 `meta/` 에 명령·cwd·실행 시각·종료 코드를 적는다.",
|
|
"decision-criterion": "백엔드를 iptables 로 둔 상태에서 밖에서 친 요청이 응답을 받으면 그 백엔드에서는 구멍이 필요 없다고 적고 닫는다. 여전히 거절되면 어느 규칙이 끝냈는지와 그때의 구멍 방법을 case:nftables-accept-did-not-stop-the-libvirt-reject 의 해결 절에 행으로 더한 뒤 닫는다. 어느 쪽이든 그 결과가 decision:edge-nginx-moved-into-a-guest-vm 이 감수한 비용 3번의 적용 범위를 정한다 — 지금 그 비용은 nftables 백엔드에서만 확인된 것이다.",
|
|
"relations": [
|
|
"case:nftables-accept-did-not-stop-the-libvirt-reject",
|
|
"decision:edge-nginx-moved-into-a-guest-vm",
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"question:vm-network-mode-bridge-nat-or-routed"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/question/question-guest-input-hole-under-the-iptables-backend.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "virsh save 의 RAM 덤프 크기와 소요 시간은 할당 메모리에 어떻게 비례하는가",
|
|
"kind": "question",
|
|
"slug": "virsh-save-ram-dump-size-and-time",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#183-이-부에서-파생될-open-question-oq-2",
|
|
"final/document.md#181-qcow2-가-담는-것과-담지-않는-것",
|
|
"final/document.md#178-이-부의-출처와-범위"
|
|
],
|
|
"known": "§181 는 qcow2 복사로는 실행 상태가 따라오지 않는다고 적었다 — 실행 중인 프로세스(PID·FD·소켓·JVM 힙)와 페이지 캐시·안 내려간 dirty page 는 파일에 없다. 실행 상태까지 옮기려면 `virsh save`→복사→`restore` 이고 그때 VM 이 멈추고 RAM 크기만큼 파일이 더 생긴다. 다른 길은 `virsh migrate --live --copy-storage-all` 인데 두 호스트 libvirt 가 붙고 CPU 모델이 호환돼야 한다. 대상 환경은 §178 가 적었다 — 호스트 RAM 11,648MiB(약 11.4GiB), QEMU 11.1.1 · libvirt 12.7.0, 게스트는 Debian 12 3대다. 제2부는 게스트에 준 RAM 과 게스트가 실제로 쓰는 양이 갈린다는 것을 ballooning 으로 설명했지만 이 호스트에서 잰 값은 하나도 없다.",
|
|
"unknown": "이 호스트의 게스트 한 대를 `virsh save` 했을 때 생기는 파일이 실제로 몇 바이트인지, 그것이 할당 RAM 과 같은지 게스트가 실제로 쓰던 양에 가까운지, 그리고 save 와 restore 가 각각 몇 초 걸리는지. 할당 RAM 을 바꾸면 그 셋이 어떻게 따라 움직이는지.",
|
|
"next-verification": "§183 이 적은 그대로 잰다 — 게스트 한 대에 `virsh save` 와 `restore` 를 돌리고 생긴 파일의 크기와 각 단계의 소요 시간을 적는다. 같은 게스트의 할당 RAM 을 바꿔 두 번 이상 반복해 비례 관계를 본다. §183 이 덧붙인 대조를 함께 한다 — 같은 시각에 balloon 쪽 실사용값을 찍어 덤프 크기가 할당량 쪽인지 실사용량 쪽인지를 가른다. 그 대조군이 question:balloon-target-vs-guest-available-memory 와 question:vm-configured-vs-current-memory 가 재는 값이다. 출력 원문을 `final/evidence/raw/` 에 남긴다.",
|
|
"decision-criterion": "할당 RAM 두 값 이상에서 덤프 크기와 소요 시간이 나오고 그것이 할당량과 실사용량 중 어느 쪽을 따라가는지 말할 수 있으면 닫는다. 그 값이 나오면 이 실험대를 멈췄다 다시 세우는 데 드는 시간이 정해지고, concept:what-a-qcow2-file-carries 가 「RAM 크기만큼 파일이 더 생긴다」고만 적은 자리에 이 호스트의 실제 수치가 들어간다. 덤프가 실사용량을 따라간다고 나오면 제2부의 balloon 기록이 그 반대 방향의 근거를 하나 얻는다.",
|
|
"relations": [
|
|
"concept:what-a-qcow2-file-carries",
|
|
"question:qcow2-transfer-time-over-wifi",
|
|
"question:balloon-target-vs-guest-available-memory",
|
|
"question:vm-configured-vs-current-memory",
|
|
"concept:virtio-balloon-memory-reclaim"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/question/question-virsh-save-ram-dump-size-and-time.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "WiFi 전용 호스트에서 대용량 qcow2 를 옮기는 데 실제로 몇 시간이 걸리는가",
|
|
"kind": "question",
|
|
"slug": "qcow2-transfer-time-over-wifi",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#183-이-부에서-파생될-open-question-oq-3",
|
|
"final/document.md#181-qcow2-가-담는-것과-담지-않는-것",
|
|
"final/document.md#178-이-부의-출처와-범위"
|
|
],
|
|
"known": "§178 는 이 호스트에 이더넷 없이 WiFi 만 있다고 적었다(observed) — 브리지를 못 쓰고 libvirt NAT(`virbr0`) + 호스트 진입 구조를 택한 것도 같은 제약 때문이다. §181 는 qcow2 가 희소(sparse) 할당이라 20GB 이미지가 2GB 일 수 있고 1TB 를 채우면 1TB 파일이 된다고 적었다. 게스트에서 지워도 파일은 줄지 않으므로(`fstrim` 이나 `qemu-img convert` 가 필요하다) 옮길 바이트 수는 시간이 지날수록 커지는 쪽이다. 이 호스트의 이미지가 지금 몇 바이트인지는 SSOT 어디에도 없다 — 제4부의 question:disk-image-format-and-actual-host-usage 가 아직 묻고 있는 중이다.",
|
|
"unknown": "이 호스트의 qcow2 파일이 실제로 몇 바이트이고 그것을 이 WiFi 링크로 다른 기계에 옮기는 데 몇 분·몇 시간이 걸리는지. 그리고 `qemu-img convert` 로 먼저 줄인 뒤 옮기는 편이 변환 시간까지 합쳐도 전체 시간을 줄이는지.",
|
|
"next-verification": "먼저 파일 크기를 확정한다 — `qemu-img info` 와 `du` 로 가상 크기와 실제 점유를 따로 적는다(question:disk-image-format-and-actual-host-usage 가 같은 출력을 쓴다). 그다음 게스트를 멈춘 상태에서 그 파일 한 장을 다른 기계로 한 번 복사하고 시작·종료 시각과 평균 전송률을 적는다. 이어 `qemu-img convert` 로 줄인 사본을 같은 방법으로 한 번 더 옮겨 변환 시간까지 합친 총 시간을 견준다. 출력 원문을 `final/evidence/raw/` 에 남긴다.",
|
|
"decision-criterion": "파일 크기와 전송 시간이 한 번의 실측으로 나오면 닫는다. 그 시간이 이 실험대를 다른 기계로 옮기거나 백업하는 것이 현실적인 절차인지, 아니면 가이드 7단계를 다시 도는 재구축이 더 빠른지를 가른다. 재구축이 더 빠르다고 나오면 reference:verify-a-build-guide-in-execution-order 가 요구하는 검증은 선택이 아니라 전제가 된다 — 옮길 수 없는 실험대는 문서로만 복원된다.",
|
|
"relations": [
|
|
"concept:what-a-qcow2-file-carries",
|
|
"question:virsh-save-ram-dump-size-and-time",
|
|
"question:disk-image-format-and-actual-host-usage",
|
|
"reference:verify-a-build-guide-in-execution-order",
|
|
"concept:qemu-block-backend-forms"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/question/question-qcow2-transfer-time-over-wifi.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "이 실험대의 certbot 은 HTTP-01 로 받고 있는가 DNS-01 로 받고 있는가",
|
|
"kind": "question",
|
|
"slug": "is-this-lab-issuing-certificates-with-http-01-or-dns-01",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#204-재구축할-때-무엇이-남아-있나",
|
|
"final/document.md#266-dns-01-은-언제-쓰는가-네-가지-경우",
|
|
"final/document.md#265-도메인-검증-http-01-vs-dns-01"
|
|
],
|
|
"known": "**둘 중 하나여야 하는데 문서 둘이 서로 다른 것을 가리킨다.** §266 이 그 불일치를 직접 적었다 — 그 절의 결론과 §190 은 DNS-01 을 가리키는데 원본 가이드 `docs/guides/04-tls/README.md` 는 `certbot certonly --webroot`(HTTP-01)로 적혀 있고 전제도 「공개 DNS 에 이름 셋이 이 호스트를 가리켜야 한다」이다. **그 전제는 지금 성립하지 않는다**(observed) — `dig +short auth.hyeonworks.com` 이 `100.83.212.4` 를 내고 `100.64.0.0/10` 은 CGNAT 용 예약 대역이라 공개 인터넷에서 라우팅 자체가 안 된다. 방화벽을 여는 문제가 아니라 그 주소가 인터넷에 존재하지 않고, Let's Encrypt 를 tailnet 에 초대할 방법도 없다. **플러그인은 깔려 있다**(observed) — `certbot plugins` 에 `dns-cloudflare` 가 보인다. **읽지 못한 이유도 기록돼 있다** — §204 가 「미측정」으로 적었고 호스트의 `sudo` 가 비밀번호를 요구해 비대화식으로 읽지 못했다. 정황은 DNS-01 쪽으로 기울어 있다 — 이 실험대는 `*.hyeonworks.com` 와일드카드를 쓰는데 ACME 명세가 와일드카드를 DNS-01 로만 허용하므로 지금 서빙되는 인증서가 와일드카드라면 발급은 DNS-01 로 이뤄졌을 수밖에 없다. 다만 그 인증서가 실제로 와일드카드인지도 이 저장소에 출력으로 남아 있지 않다.",
|
|
"unknown": "`/etc/letsencrypt/renewal/*.conf` 의 `authenticator` 가 무엇인지. 그리고 그 값이 `webroot`·`standalone` 이면 **지금 갱신이 실제로 돌고 있는지** — HTTP-01 로 설정돼 있는데 검증이 성립하지 않는 주소라면 갱신은 조용히 실패하고 있을 것이고, 그런데도 `certbot-renew.timer` 는 `active` 로 보인다. 반대로 `dns-cloudflare` 라면 §266 이 적은 대가(API 토큰이 서버에 있고 유출되면 도메인 전체의 DNS 를 조작당한다)가 이 호스트에 실재하는 것이므로 그 토큰의 범위가 존 하나 + DNS:Edit 으로 좁혀져 있는지도 함께 봐야 한다. 그 범위 역시 SSOT 에 없다.",
|
|
"next-verification": "§204 와 §266 이 적은 네 줄을 그대로 친다 — `sudo grep -H authenticator /etc/letsencrypt/renewal/*.conf` 로 방식을 읽고, `certbot plugins | grep -E '^\\*'` 로 쓸 수 있는 방식을 적고, `sudo certbot renew --dry-run` 으로 갱신이 실제로 되는지 보고, `dig +short auth.hyeonworks.com` 으로 Let's Encrypt 가 올 수 있는 주소인지를 같은 시각에 함께 남긴다. 호스트의 `sudo` 가 비밀번호를 요구하므로 **비대화식이 아니라 콘솔에서 친다** — 이것이 §204 가 미측정으로 남긴 이유다. 함께 찍을 것이 둘 더 있다 — 지금 서빙되는 인증서가 와일드카드인지(`openssl s_client` 나 `certbot certificates` 의 Domains)와 lineage 이름이 무엇인지다(§209 의 주석이 lineage 가 첫 `-d` 를 따라 `live/hyeonworks.com/` 이 된다고 적는다). 출력 원문을 `final/evidence/raw/` 에 남기고 `meta/` 에 명령·cwd·실행 시각·종료 코드를 적는다. **비밀이 섞이지 않게** — `cloudflare.ini` 의 토큰 값은 찍지 않는다.",
|
|
"decision-criterion": "`authenticator` 한 값과 `--dry-run` 결과가 나오면 닫는다. **`dns-cloudflare` 면** decision:dns-01-because-the-lab-is-not-on-the-public-internet 이 이 실험대의 현재 상태를 적은 것이 확인되고, setup:tear-down-the-lab-and-know-what-survives 의 「인증서를 지우지 않는다」가 정책에서 **선택**으로 바뀐다 — §204 가 적은 대로 그때는 백업이 헛수고이므로 지워도 되고 재구축 절차가 한 단계 짧아진다. **`webroot`·`standalone` 이면** 그 Decision 이 적은 것은 의도이고 실제 설정은 다른 것이므로 SSOT 의 §190 과 가이드 04 중 어느 쪽이 실재인지를 먼저 고친 뒤 그 기록을 다시 판정한다. 그리고 `--dry-run` 이 실패하면 **갱신이 이미 멈춰 있다**는 뜻이라 그 자체가 새 Case 다 — case:renewal-succeeded-while-the-old-certificate-kept-serving 이 「갱신은 되는데 서빙까지 안 갔다」를 다뤘다면 이번 것은 「갱신 자체가 안 된다」이고 증상이 또 조용하다. 어느 쪽이든 question:does-the-guide-rebuild-this-lab 이 재려는 7단계 가운데 04 의 통과 조건이 그때 확정된다.",
|
|
"relations": [
|
|
"decision:dns-01-because-the-lab-is-not-on-the-public-internet",
|
|
"setup:wildcard-certificate-with-dns-01-and-a-deploy-hook",
|
|
"setup:tear-down-the-lab-and-know-what-survives",
|
|
"case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"decision:no-public-tunnel-because-a-third-hop-pollutes-the-measurement",
|
|
"question:does-the-guide-rebuild-this-lab"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/question/question-is-this-lab-issuing-certificates-with-http-01-or-dns-01.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"decision": [
|
|
{
|
|
"title": "엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리",
|
|
"kind": "decision",
|
|
"slug": "edge-nginx-moved-into-a-guest-vm",
|
|
"readiness": "READY",
|
|
"decision-status": "ADOPTED",
|
|
"source": [
|
|
"final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나",
|
|
"final/document.md#178-이-부의-출처와-범위",
|
|
"final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다"
|
|
],
|
|
"decision-evidence": "§178 가 이 실험대의 현재 상태를 observed 로 적었다 — `test-server`(Arch Linux, i5-1135G7 논리 코어 8, RAM 11,648MiB, QEMU 11.1.1 · libvirt 12.7.0) 위에 Debian 12 genericcloud 게스트 3대가 있고 그중 1대가 엣지(nginx·certbot), 나머지 둘이 k3s 노드다. 즉 바뀐 구성이 이미 서 있다. 설정 원본은 §178 가 `../source/deploy/lab/edge/` 로, 구축 순서는 `../source/docs/guides/` 의 기반 7단계 가이드로 가리킨다. §179 은 전/후 경로를 나란히 적었다 — 전은 `tailnet:443 ─▶ [호스트 nginx] ─────────────▶ Traefik(게스트 .11/.12)`, 후는 `tailnet:443 ─▶ [호스트 커널 DNAT] ─▶ [엣지 nginx(.10)] ─▶ Traefik(.11/.12)` 다. 이 저장소에 남은 것은 그 문서와 설정 원본이고, 전환 전후를 같은 부하로 잰 측정은 없다.",
|
|
"grounds": "§179 이 이유를 직접 적었다 — 「바꾼 이유는 성능이 아니라 더러워지는 층의 격리다」. nginx 설정·인증서·certbot·deploy 훅은 자주 갈아엎는 것들인데 호스트에 있으면 초기화가 불가능하고, 엣지 장애 실험이 SSH 까지 위험하게 만든다. 성능을 근거로 삼지 않은 이유도 같은 절이 댄다 — L7 홉 수는 전후 모두 2홉 그대로이고(observed) 늘어난 것은 커널이 하는 L4 전달 한 번뿐이라 `X-Forwarded-*` 계약이 그대로 성립한다. 이 결정은 무엇을 빠르게 하려는 것이 아니라 무엇을 지울 수 있게 하려는 것이다. 앞선 제약은 하드웨어가 걸었다 — §178 가 적었듯 이 호스트에는 이더넷 없이 WiFi 만 있어 브리지를 못 쓰고 libvirt NAT(`virbr0`) + 호스트 진입 구조를 택했다. 브리지였다면 밖에서 게스트로 바로 들어오므로 아래 감수한 비용 가운데 2·3번이 생기지 않는다. SSOT 가 적지 않은 대안을 지어내지 않는다 — 비교된 것은 「호스트에 두기」와 「게스트로 옮기기」 둘이고, 브리지 대 NAT 는 고른 것이 아니라 이더넷이 없어 하나만 남은 것이다.",
|
|
"classification": "감수한 비용은 §179 의 표가 일곱 줄로 적었고 그 절이 스스로 둘로 갈랐다(★, inferred). 구조적으로 생긴 것은 둘이다 — (2) **DNAT**: 전에는 호스트가 직접 `:443` 을 들었으니 넘길 일이 없었는데 지금은 호스트에 리스너가 아예 없다. (3) **libvirt 방화벽에 구멍**: 호스트→게스트는 OUTPUT 경로라 필터를 안 탔지만 밖→게스트는 FORWARD 다. 「호스트가 게스트에 접속한다」와 「밖에서 게스트로 들어온다」는 커널이 보기에 완전히 다른 일이라는 것이 이 이동의 본질이다. 같은 계열의 가드레일이 (4) SNAT 금지 명시다 — L4 를 한 번 더 타면서 masquerade 를 붙이고 싶어지는데 붙이면 엣지가 모든 클라이언트를 `192.168.122.1` 로 본다. 나머지 넷은 배포판이 달라서 생긴 잡무다 — (1) nginx 설치(새 게스트의 cloud-init 은 `curl`·`nftables` 만 깐다), (5) `sites-available` 관례(호스트는 Arch 라 그 디렉터리가 없어 `nginx.conf` 에 include 를 직접 넣었고 게스트는 Debian 이라 기본으로 있다), (6) nginx 버전 차이(Arch 1.30 vs Debian 12 의 1.22, `http2 on;` 지시어가 1.25.1 이상이다), (7) certbot·인증서·갱신 훅이 게스트로(인증서를 읽는 주체가 nginx 이기 때문이다). 이 결정이 실제로 얼마를 물렸는지는 case:nftables-accept-did-not-stop-the-libvirt-reject 가 보여 준다 — 3번을 뚫는 것이 이 구축에서 가장 오래 막힌 지점이었다.",
|
|
"relations": [
|
|
"case:nftables-accept-did-not-stop-the-libvirt-reject",
|
|
"reference:verify-a-build-guide-in-execution-order",
|
|
"question:guest-input-hole-under-the-iptables-backend",
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"question:vm-network-mode-bridge-nat-or-routed"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/decision/decision-edge-nginx-moved-into-a-guest-vm.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "인증서는 DNS-01 로 받는다 — 실험대 주소가 공개 인터넷에 없어 HTTP-01 이 성립하지 않는다",
|
|
"kind": "decision",
|
|
"slug": "dns-01-because-the-lab-is-not-on-the-public-internet",
|
|
"readiness": "READY",
|
|
"decision-status": "PROPOSED",
|
|
"source": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"decision-evidence": "§190 가 제약을 실측으로 적었다(observed) — `dig +short auth.hyeonworks.com` 이 `100.83.212.4` 를 낸다. 이 실험대의 도메인 셋이 전부 tailnet 주소를 가리킨다. **여기까지가 이 결정의 근거이고, 여기는 재어 둔 값이다.** **그 다음이 안 재어져 있다** — 이 실험대가 실제로 어느 방식으로 발급받고 있는지는 SSOT 가 스스로 미측정으로 적었다. §204: 「**미측정** — 이 실험대의 certbot 이 HTTP-01 인지 DNS-01 인지 아직 확인하지 않았다. 호스트의 `sudo` 가 비밀번호를 요구해 비대화식으로 읽지 못했다.」 §266 은 한 발 더 나가 **문서 둘이 어긋나 있다**고 적었다 — 그 절의 결론과 §190 은 DNS-01 을 가리키는데 원본 가이드 `docs/guides/04-tls/README.md` 는 `certbot certonly --webroot`(HTTP-01)로 적혀 있고, 그 전제(「공개 DNS 에 이름 셋이 이 호스트를 가리켜야 한다」)는 지금 성립하지 않는다. **`certbot plugins` 의 세 줄은 이 결정이 적용됐다는 근거가 아니다** — SSOT 는 그 출력을 「쓸 수 있는 검증 방식」으로 적었고(`* dns-cloudflare`·`* standalone`·`* webroot`), §204 도 「플러그인은 이미 깔려 있다」로만 쓴다. 쓸 수 있는 것과 실제로 쓴 것은 다르고, 그 셋 중 둘은 HTTP-01 쪽이다. **서 있는 것으로 확인된 것은 서빙 쪽뿐이다** — 밖에서 친 `openssl s_client` 가 체인 0~3 네 단계와 `Verify return code: 0 (ok)` 를 냈고 `curl` 이 `404 tls=0` 을 냈다. 다만 §190 이 스스로 밝히듯 그 체인 실측은 이름을 따로 받던 시절의 것이고, 와일드카드로 받은 지금의 `s_client` 출력과 `certbot certificates` 의 실제 화면은 SSOT 에 없다(unknown). 나머지 값 — 자격증명이 `/etc/letsencrypt/cloudflare.ini` 에 `600`, 발급 대상이 `-d hyeonworks.com -d '*.hyeonworks.com'` — 은 §190 의 절차 표에 적힌 값이지 이 호스트에서 읽어 낸 출력이 아니다. 설정 원본은 §184 가 `../source/deploy/lab/edge/` 로 가리키고 리비전은 `9465582b5d1630eb4ae7c4e078021486919bf6b6` 다. 없는 것도 분명하다 — HTTP-01 을 실제로 시도해 실패한 기록은 없다. 그 경로가 막혔다는 근거는 시도가 아니라 주소 대역이다.",
|
|
"grounds": "**제약** — `100.64.0.0/10` 은 CGNAT 용으로 예약된 대역이라 공개 인터넷에서 라우팅 자체가 되지 않는다. 방화벽을 여는 문제가 아니라 그 주소가 인터넷에 존재하지 않고, Let's Encrypt 를 tailnet 에 초대할 방법도 없다. **두 방식이 요구하는 것이 반대다** — HTTP-01 은 Let's Encrypt 가 우리 서버로 들어오는 인바운드 검증이라 공개 인터넷에서 보여야 하고, DNS-01 은 certbot 이 DNS 공급자 API 로 나가는 아웃바운드 검증이라 보일 필요가 없다. 그래서 DNS-01 이 남는다. **SSOT 는 이 선택을 우열로 적지 않았다** — 「공개 서버라면 HTTP-01 이 맞고」 토큰도 DNS 연동도 없어 관리할 것이 적다고 대안 쪽을 먼저 적는다. 이 결정은 더 나은 방식을 고른 것이 아니라 하나만 성립하는 자리에서 그것을 쓴 것이다. 딸려 온 이득이 하나 있다 — DNS-01 은 와일드카드를 받을 수 있어 `*.hyeonworks.com` 한 장으로 덮는다.",
|
|
"missing-verification": "**무엇이 안 재어졌나** — `/etc/letsencrypt/renewal/*.conf` 의 `authenticator` 값이다. 그 한 값이 이 결정이 이 실험대에 **실제로 적용돼 있는지**를 가른다. SSOT 가 그것을 스스로 미측정으로 적었고(§204), §266 은 같은 자리에서 문서 둘이 어긋나 있다고 적었다. **무엇을 재면 닫히나** — §266 이 「확인」으로 적어 둔 네 줄을 그대로 친다: `sudo grep -H authenticator /etc/letsencrypt/renewal/*.conf`(webroot/standalone 이면 HTTP-01), `certbot plugins | grep -E '^\\*'`(쓸 수 있는 검증 방식), `sudo certbot renew --dry-run`(갱신이 실제로 되는가), `dig +short auth.hyeonworks.com`(LE 가 올 수 있는 주소인가). 호스트의 `sudo` 가 비밀번호를 요구하므로 **비대화식이 아니라 콘솔에서 친다** — §204 가 미측정으로 남긴 이유가 그것이다. 출력 원문은 `final/evidence/raw/` 에 남기고 명령·cwd·실행 시각·종료 코드를 `meta/` 에 적는다. `cloudflare.ini` 의 토큰 값은 찍지 않는다. **판정이 어떻게 움직이나** — `dns-cloudflare` 면 `decision-status` 를 `ADOPTED` 로 올린다. `webroot`·`standalone` 이면 이 결정문은 의도이고 실제 설정은 다른 것이므로, §190 과 원본 가이드 `docs/guides/04-tls/README.md` 중 어느 쪽이 실재인지를 SSOT 에서 먼저 가른 뒤 다시 판정한다. **이 결정은 받는 쪽이다** — 재는 일과 종료 기준은 question:is-this-lab-issuing-certificates-with-http-01-or-dns-01 가 갖고 있고, 그 Question 이 닫히면 이 노드의 `decision-status` 가 움직인다. **`readiness` 를 `READY` 로 둔 이유** — 이 결정의 근거(`dig` 가 내는 `100.83.212.4`, `100.64.0.0/10` 은 CGNAT 용 예약 대역이라 인바운드 HTTP-01 이 성립하지 않는다)는 재어져 있다. 재어지지 않은 것은 그 결정이 **지금 적용돼 있는가**이고, 그 불확실성은 `decision-status` 가 `PROPOSED` 로 진다. 근거 자체가 미측정이었다면 `NEEDS_EVIDENCE` 였을 것이다.",
|
|
"classification": "**감수한 비용** — Cloudflare API 토큰이 엣지 VM 안 평문 파일(`/etc/letsencrypt/cloudflare.ini`)에 놓인다. 발급 검증이 수십 초 걸리는 것(TXT 가 퍼질 때까지 기다린다)과, 인증서를 받는 일이 DNS 공급자에 묶이는 것도 함께 온다. **가드레일** — 토큰 권한을 `Edit zone DNS` · `Specific zone` · `hyeonworks.com` 으로 좁힌다. `All zones` 로 두면 계정의 모든 도메인에 대한 DNS 수정 권한이 그 파일에 놓이고, Global API Key 는 폐기하면 그 키를 쓰던 다른 것들이 전부 같이 죽는다. 파일은 `install -m 600 /dev/null` 로 **비어 있을 때** 먼저 600 을 만든다 — 토큰을 쓰고 나서 권한을 고치면 그사이가 열려 있다. 확인도 `-rw-------` 과 바이트 수가 0 이 아닌지까지만 보고 값을 찍지 않는다. 토큰이 맞는지는 `/user/tokens/verify` 의 `\"status\":\"active\"` 와 `\"success\":true` 로 보고, `\"code\":6003` 이면 값이 틀렸거나 잘렸고 `\"code\":9109` 면 권한 범위가 모자라다 — 여기서 걸러 두면 뒤에서 실패했을 때 DNS 문제인지 토큰 문제인지를 헷갈리지 않는다. 발급은 `--dry-run` 을 먼저 돌린다. Let's Encrypt 의 주당 중복 인증서 5장 한도를 dry-run 이 쓰지 않기 때문이고, dry-run 은 인증서를 저장하지 않으므로 그 직후 `certbot certificates` 가 `No certificates found` 를 내는 것이 정상이다. **결정이 남긴 이름 규칙** — `live/hyeonworks.com/` 은 certbot 이 이 묶음(lineage)을 관리하려고 **첫 번째 `-d`** 에서 따온 라벨이고 서빙과 무관하다. 브라우저가 보는 유효 호스트명은 `-d` 로 준 이름 전부다. 그래서 `auth.hyeonworks.com` 으로 다시 받을 필요가 없고, nginx 설정에는 디렉터리 경로를 한 글자도 다르지 않게 적어야 한다 — `live/auth.hyeonworks.com/` 이라고 적으면 `cannot load certificate` 로 막힌다(§182 가 이것을 가이드 결함 여섯 중 하나로 셌다). 와일드카드는 한 단계만 덮으므로 `a.b.hyeonworks.com` 도, apex 인 `hyeonworks.com` 자신도 `*.hyeonworks.com` 에 들어가지 않아 `-d` 를 둘 준다. **이 결정이 끝내지 못한 것** — 받은 인증서가 갱신 뒤 실제로 서빙되는지는 이 결정 밖이고 case:renewal-succeeded-while-the-old-certificate-kept-serving 가 받는다.",
|
|
"relations": [
|
|
"question:is-this-lab-issuing-certificates-with-http-01-or-dns-01",
|
|
"case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"decision:edge-nginx-moved-into-a-guest-vm",
|
|
"reference:tool-output-is-not-the-subject-state",
|
|
"reference:check-the-nearest-layer-first",
|
|
"reference:verify-a-build-guide-in-execution-order"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/decision/decision-dns-01-because-the-lab-is-not-on-the-public-internet.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "호스트에 직접 깔지 않고 게스트 VM 두 대로 간다 — 관측자가 실험 대상과 함께 죽으면 안 된다",
|
|
"kind": "decision",
|
|
"slug": "two-guest-vms-instead-of-installing-k3s-on-the-host",
|
|
"readiness": "READY",
|
|
"decision-status": "ADOPTED",
|
|
"source": [
|
|
"final/document.md#213-왜-호스트에-직접-깔지-않고-vm-2대인가",
|
|
"final/document.md#313-k3s-server와-agent-죽였을-때가-다르다",
|
|
"final/document.md#218-실험대-전체-배치-2026-09-03-구축-완료-실측값"
|
|
],
|
|
"decision-evidence": "§213 이 「나중에 반드시 다시 묻게 되는 판단이므로 근거를 남긴다」로 시작해 이유 여섯을 표로 적고, 그 아래에 **정직한 반대편**과 **채택하지 않은 절충안**을 따로 절로 두었다. 실행된 상태도 SSOT 에 있다 — §218 의 2026-09-03 배치가 `kc-lab-1`(k3s server)과 `kc-lab-2`(k3s agent) 두 게스트를 `virbr0` 뒤에 그리고, 호스트 `test-server` 에는 nginx 와 libvirt/KVM 만 남긴다. 나중에 엣지 게스트가 하나 더 붙어 셋이 되는데 그것은 decision:edge-nginx-moved-into-a-guest-vm 이 받는 별개의 결정이다.",
|
|
"grounds": "**제약** — 물리 머신이 한 대다. **이유 여섯**(중요도 순으로 SSOT 가 적은 그대로). ① **독립 커널이 둘 필요하다** — 같은 커널에 k3s server 와 agent 를 올리면 「노드」가 이름뿐이라 노드 간 방화벽·파티션·노드 상실 실험이 **성립하지 않는다.** ② **파괴 실험 후 복원** — VM 은 qcow2 오버레이를 지우면 몇 초 만에 초기 상태이고 호스트는 재설치 말고 되돌릴 방법이 없다. ③ **관측자를 살려 둔다** — 노드를 죽이는 실험인데 그 노드가 호스트면 SSH·libvirt·nginx 가 같이 죽는다. **관측 수단이 실험 대상과 함께 죽으면 안 된다.** ④ **호스트 오염 방지** — k3s 는 nftables 규칙·CNI 인터페이스·커널 모듈·systemd 유닛을 대량으로 심는다. ⑤ **운영 배포판과 일치** — 호스트는 Arch 인데 운영 k3s 가 다른 배포판이면 커널·systemd 차이가 잡음이 된다. 게스트를 운영과 같게 맞추면 그 잡음이 사라진다. ⑥ **netem 격리** — 커널이 분리되어 지연 주입이 게스트 안에 갇힌다. 호스트에서 걸면 SSH 까지 느려진다. **감수한 비용** — SSOT 가 정직한 반대편을 직접 적었다: **계약 검증(2홉 헤더, 쿠키/origin)만 볼 거라면 호스트에 단일 노드 k3s 를 직접 까는 편이 충분하고 그게 더 빠르다.** VM 경로가 필요해지는 것은 클러스터와 장애 실험부터다. 즉 이 결정은 실험 범위를 넓히는 대가로 구축 시간과 게스트 OS 몫의 메모리를 치른 것이다. **채택하지 않은 절충안** — 「호스트를 노드 1, VM 을 노드 2로」. 게스트 OS 하나(약 350MB)와 설치 수고를 아끼지만 ③과 ④를 포기하게 되고, 7.4Gi 예산에서 **그 350MB 보다 관측자 분리가 더 값지다고 판단했다.** **이 결정이 나중에 청구된 자리** — k3s server 와 agent 는 죽였을 때가 다르다(agent 를 죽이면 그 노드의 워크로드만 사라지고 server 를 죽이면 관측과 조작 수단이 함께 사라진다). 그래서 관측 스택을 server 쪽에 두고 agent 쪽을 장애 주입 대상으로 삼는 규칙이 생겼고, `nodeSelector` 로 못박아 실험이 재현 가능해졌다 — ③의 같은 논리가 클러스터 안에서 한 번 더 적용된 것이다.",
|
|
"classification": "이 실험대의 **가장 밑에 있는 결정**이다. 나머지 결정들(엣지를 게스트로 옮긴다 · 주소를 DHCP 예약으로 고정한다 · Docker 를 호스트에 깔지 않는다)은 전부 「게스트 VM 위에 세운다」를 전제로 서 있다. decision:edge-nginx-moved-into-a-guest-vm 과 물음이 다르다 — 저쪽은 이미 있는 게스트 구조에서 엣지가 어디 사는가를 정했고, 이쪽은 게스트 구조 자체를 쓸 것인가를 정했다. 그래서 한 기록에 합쳐지지 않는다. 독립성 검사도 통과한다 — 이 결정을 setup:prepare-the-lab-host-for-virtualization 의 한 절로 접으면 그 Setup 이 「왜 가상화 패키지부터 까는가」에 답할 자리가 없어지고, 여섯 이유 가운데 ①·③·⑥ 은 절차가 아니라 실험 설계의 근거라 절차 안에서 말할 수 없다.",
|
|
"relations": [
|
|
"decision:edge-nginx-moved-into-a-guest-vm",
|
|
"setup:prepare-the-lab-host-for-virtualization",
|
|
"setup:install-k3s-server-and-agent",
|
|
"setup:prometheus-and-grafana-for-the-lab",
|
|
"decision:no-docker-on-the-lab-host",
|
|
"setup:power-cycle-the-lab-and-reallocate-guest-memory"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/decision/decision-two-guest-vms-instead-of-installing-k3s-on-the-host.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "게스트 주소는 libvirt DHCP 예약으로 고정한다 — 바꿀 수 없어서가 아니라 되돌리기가 가장 비싸서",
|
|
"kind": "decision",
|
|
"slug": "fix-guest-addresses-with-a-dhcp-reservation",
|
|
"readiness": "READY",
|
|
"decision-status": "ADOPTED",
|
|
"source": [
|
|
"final/document.md#247-dhcp-예약-ip-dhcp-host-과-mac-52-54-00",
|
|
"final/document.md#201-네트워크-dhcp-예약의-실제-동작",
|
|
"final/document.md#245-libvirt-default-네트워크와-virbr0",
|
|
"final/document.md#246-dnsmasq-libvirt-내장-dhcp-dns",
|
|
"final/document.md#248---live---config"
|
|
],
|
|
"decision-evidence": "§247 이 **인과 순서를 직접 못박았다** — 「upstream 에 IP 를 박으려고 예약을 건다」가 아니라 반대이고, 고정 주소가 필요한 이유가 여럿이고 그것을 충족하는 수단이 DHCP 예약이며 그 결과로 얻은 주소를 upstream 에도 적는 것이다. 같은 절이 이유 넷을 중요도 순으로, 대안 둘을 표로 적는다. 실행된 상태는 §201 의 실측이다(observed, 2026-09-10) — `virsh net-update default add ip-dhcp-host ... --live --config` 이 `Updated network default persistent config and live state` 를 냈고, 예약을 먼저 넣고 `virt-install` 한 게스트가 첫 부팅에서 바로 `192.168.122.10` 을 받았다. MAC 대역은 QEMU/KVM 에 할당된 OUI `52:54:00` 을 쓴다.",
|
|
"grounds": "**제약 — 고정 주소가 필요한 이유 넷**(중요도 순). ① **k3s 가 IP 를 설정 파일과 인증서에 굽는다.** `--node-ip`·`--tls-san`·agent 의 `K3S_URL=https://192.168.122.11:6443`·kubeconfig 의 `server:` 가 전부 IP 를 담는다. server 노드의 IP 가 바뀌면 agent 가 합류하지 못하고 API 서버 인증서의 SAN 도 어긋나 **재발급이나 재설치**가 필요해진다 — **되돌리기가 가장 비싼 항목이다.** ② **nginx 는 upstream 주소를 기동 시점에 한 번만 해석한다.** 오픈소스판은 `upstream` 블록의 이름을 설정 로드 때 해석하고 런타임에 다시 조회하지 않아(재조회하려면 `resolver` + 변수 트릭이나 상용판이 필요하다) 뒤쪽 IP 가 바뀌면 reload 전까지 계속 502 다. ③ **VM 을 반복해서 죽이는 것이 실험 그 자체다.** `virsh destroy` 로 노드 상실을 재현하는데 되살릴 때마다 주소가 달라질 여지가 있으면 실험이 성립하지 않는다. ④ **장애 주입 규칙이 주소 기반이다.** 「kc-lab-2 로 가는 7800 을 막아라」에서 IP 가 어긋나면 **조용히 엉뚱한 것을 막는다** — 실패가 드러나지 않는 종류라 특히 위험하다. **대안과 왜 아닌가** — 게스트 안에서 static IP 를 설정하면 cloud-init 이 복잡해지고 libvirt 는 그 사실을 몰라 **설정이 두 곳으로 흩어진다.** upstream 에 호스트명을 쓰면 libvirt dnsmasq 가 풀어 주긴 하지만 호스트의 리졸버가 `virbr0` 를 바라봐야 하고 **②(기동 시 1회 해석)는 그대로 남는다.** DHCP 예약은 **주소 관리가 libvirt 한 곳에 모이고** 게스트는 평범한 DHCP 클라이언트로 두면 된다 — 그 한 곳이 libvirt 가 네트워크마다 하나씩 띄우는 dnsmasq 이고, 예약은 네트워크 정의 XML 의 `<ip><dhcp><host>` 에 들어간다. **감수한 비용 셋**(observed). ① **순서가 결과를 바꾼다** — 예약을 넣고 나서 `virt-install` 해야 한다. 반대면 게스트가 동적 대역(`.2`~`.254`)에서 아무 주소나 받고 예약이 먹으려면 리스 만료를 기다리거나 재부팅해야 한다. ② **MAC 이 한 글자만 달라도 오류 없이 조용히 무시된다.** 예약의 `mac` 과 `virt-install --network ...,mac=` 이 정확히 같아야 하고 다르면 동적 범위에서 아무 주소나 받는다 — 증상이 「왜 IP 가 다르지?」로만 나타난다. ③ **플래그 둘을 다 줘야 한다.** `--live` 만 주면 재부팅에 사라지고 `--config` 만 주면 지금 반영이 안 된다. 성공 판정은 출력에 `persistent config` 와 `live state` **두 마디가 다 나오는 것**이고 한쪽만 나온 것을 성공으로 읽으면 「재부팅하니 IP 가 바뀐다」가 된다. **그리고 의도와 기록은 다른 자리에 있다** — `net-dumpxml` 의 예약은 **줄 의도**이고 `net-dhcp-leases` 는 **실제로 준 기록**이라 둘이 다를 수 있다. 동적 범위(`.2`~`.254`)가 예약 주소 `.11`·`.12` 를 품고 있지만 dnsmasq 는 정적 예약된 주소를 다른 클라이언트에게 내주지 않아 이대로도 정상 동작한다 — 더 방어적으로 가려면 범위를 `.100`~`.254` 로 좁혀 분리한다.",
|
|
"classification": "setup:create-three-guests-with-cloud-init 이 이 예약을 넣는 명령을 치고 「DHCP 예약이 VM 생성보다 먼저여야 한다」를 순서가 결과를 바꾸는 자리로 적지만, **왜 주소를 고정하는가와 왜 이 방식인가는 그 절차 안에 없다.** 그 Setup 의 한 절로 접으면 이유 넷과 대안 둘이 명령 옆의 주석으로 줄어들고, 특히 ①(되돌리기가 가장 비싸다)과 ④(조용히 엉뚱한 것을 막는다)는 절차가 아니라 **이 실험대가 무엇을 재려 하는가**에 붙은 근거라 절차 안에서 말할 자리가 없다. 그래서 독립 기록이다. 기술이 존재한다는 사실을 근거로 삼지 않았다 — SSOT 가 대안 둘을 표로 견주고 그 각각이 어디서 깨지는지를 적었다.",
|
|
"relations": [
|
|
"setup:create-three-guests-with-cloud-init",
|
|
"setup:install-k3s-server-and-agent",
|
|
"setup:edge-nginx-and-host-dnat",
|
|
"decision:two-guest-vms-instead-of-installing-k3s-on-the-host",
|
|
"reference:tool-output-is-not-the-subject-state",
|
|
"concept:two-l7-hops-and-the-entry-point-recursion"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/decision/decision-fix-guest-addresses-with-a-dhcp-reservation.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "lab host 에는 Docker 를 깔지 않는다 — 이미지 저장소가 갈리고, 그 위에 네트워크 변수가 하나 더 붙는다",
|
|
"kind": "decision",
|
|
"slug": "no-docker-on-the-lab-host",
|
|
"readiness": "READY",
|
|
"decision-status": "ADOPTED",
|
|
"source": [
|
|
"final/document.md#281-docker를-lab-host에-설치하면-안-되는-이유",
|
|
"final/document.md#282-그러면-이미지는-어떻게-넣는가",
|
|
"final/document.md#280-무엇을-어디에-설치하는가"
|
|
],
|
|
"decision-evidence": "§280 의 설치 위치 표가 Docker 를 **워크스테이션에만** 두고 lab host 와 게스트에서 뺀다. §281 이 그 이유를 「결론부터」로 적고 충돌 지점 넷을 표로 든다. §282 가 대신 쓰는 방법과 그 주의 셋을 적고, 실제로 쓰는 명령(`docker save ... | ssh test-server \"ssh kc-lab-1 'sudo k3s ctr images import -'\"`)까지 있다.",
|
|
"grounds": "**제약** — k3s 는 자체 containerd 를 번들한다. Docker 와 무관하게 이미 완결된 스택이고 소켓(`/run/k3s/containerd/containerd.sock` 대 `/run/containerd/containerd.sock`)과 이미지 저장 경로(`/var/lib/rancher/k3s/agent/containerd/` 대 `/var/lib/docker/`)가 다르다. **핵심 근거** — Docker 를 깔면 **containerd 인스턴스가 둘이 되고 둘은 서로의 이미지를 알지 못한다.** 증상은 `docker images` 에는 보이는데 파드는 `ErrImageNeverPull` 이고, **원인이 눈에 보이지 않아 오래 헤맨다.** **충돌은 저장소 말고도 셋 더 있다** — cgroup 드라이버(dockerd 기본 `cgroupfs` 대 k3s `systemd`. 한 노드에서 두 관리자가 cgroup 트리를 다툰다), iptables/nftables(Docker 가 `DOCKER`·`DOCKER-USER` 체인과 MASQUERADE 를 심어 flannel 규칙과 순서가 엉키면 파드 트래픽이 Docker 규칙에 걸린다), 브리지 대역(`docker0` 가 `172.17.0.0/16` 을 점유해 서비스 대역이나 사내망과 겹치면 라우팅이 깨진다). **이 실험대에는 이유가 하나 더 있다** — lab host 에서 libvirt 가 `virbr0` NAT 와 자체 방화벽 규칙을 운영 중이라 Docker 의 iptables 규칙이 얹히면 게스트 네트워크가 예측 불가능해진다. **네트워크 장애를 의도적으로 주입하는 실험대에서 원인 불명의 네트워크 변수를 늘리는 것은 치명적이다** — 실험 결과인지 환경 문제인지 구분할 수 없게 된다. **기각한 대안** — `k3s server --docker` 로 Docker 를 런타임으로 지정하는 방법이 과거에 있었지만 쿠버네티스 1.24 의 dockershim 제거 이후 별도 `cri-dockerd` 를 요구하며 권장되지 않는다. **얻는 것이 없다.** **감수한 비용** — 이미지를 넣는 길이 셋 가운데 둘째(`ctr images import`)로 좁아진다. 공개 이미지(Keycloak·PostgreSQL·Redis)는 아무 준비도 필요 없지만 자체 빌드 이미지(BFF·token-mediator·echo)는 워크스테이션에서 `docker save` 해 lab host 를 경유해 게스트로 흘려 넣어야 하고, 주의가 셋 붙는다 — **노드마다 따로 반입한다**(스케줄러가 어디에 배치할지 모르고 한쪽에만 있으면 반대편에 배치될 때 실패한다), 매니페스트에 `imagePullPolicy: Never` 를 준다(없으면 로컬에 있어도 레지스트리에서 당기려다 실패한다), **`ctr` 이 아니라 `k3s ctr` 을 쓴다**(시스템에 별도 `ctr` 이 있으면 다른 소켓을 보게 되어 「성공했는데 파드는 이미지를 못 찾는」 상태가 된다). ssh 가 두 번 중첩되는 것도 비용이다 — 게스트가 libvirt NAT 뒤에 있어 워크스테이션에서 직접 못 붙고 lab host 의 `~/.ssh/config` 별칭을 거쳐야 한다. 빌드·배포 반복이 잦아지면 셋째 길(클러스터 내 레지스트리)로 옮긴다.",
|
|
"classification": "**무엇을 깔지 않는가**라는 결정이라 setup:install-k3s-server-and-agent 안에 접으면 절차의 한 줄로 사라진다. 그 Setup 은 k3s 를 깔고 `kubectl` 로 보는 것까지이고, 「Docker 를 여기 깔면 왜 안 되는가」와 「그럼 이미지를 어떻게 넣는가」는 그 절차가 끝난 뒤에 처음 부딪히는 물음이다. decision:two-guest-vms-instead-of-installing-k3s-on-the-host 와 같은 축에 있다 — 호스트를 진입점과 하이퍼바이저로만 남긴다는 그 결정의 ④(호스트 오염 방지)가 여기서 컨테이너 런타임 쪽으로 한 번 더 적용됐다. 다만 물음이 다르다 — 저쪽은 실험 대상을 어디에 둘 것인가이고 이쪽은 이미지를 어디서 만들어 어떻게 넣을 것인가다. 기술이 존재한다는 사실을 근거로 삼지 않았고, 대안(`--docker`)을 기각한 이유와 채택한 길의 대가가 자료에 있다.",
|
|
"relations": [
|
|
"decision:two-guest-vms-instead-of-installing-k3s-on-the-host",
|
|
"setup:install-k3s-server-and-agent",
|
|
"setup:keycloak-two-nodes-and-postgres-on-k3s",
|
|
"setup:prometheus-and-grafana-for-the-lab",
|
|
"reference:tool-output-is-not-the-subject-state"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-environment-build/decision/decision-no-docker-on-the-lab-host.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"build-completion-judgment": {
|
|
"topic": "build-completion-judgment",
|
|
"title": "끝났다는 판정 — 성공으로 보이는 실패를 무엇이 가려내나",
|
|
"readerQuestion": "구축의 한 단계가 끝났다는 것을 무엇을 보고 판정하고, 성공으로 보이는 실패는 어디서 가려지는가?",
|
|
"kinds": {
|
|
"case": [
|
|
{
|
|
"title": "빈 토큰이 조용히 흘러갔다 — 설치 출력은 성공이었고 agent 만 5초마다 다시 죽었다",
|
|
"kind": "case",
|
|
"slug": "an-empty-token-installed-the-agent-anyway",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약",
|
|
"final/document.md#188-단계-02-k3s-server-와-agent",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"classification": "**증상**(observed) — 02 의 agent 설치가 끝까지 돌고 출력에 오류가 없는데 `kubectl get nodes` 에 두 번째 노드가 나타나지 않는다. **원인**(observed) — 토큰을 꺼내는 명령을 게스트 안에서 쳤다. 게스트에는 lab host 의 개인키도 `~/.ssh/config` 도 없어서 `ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token'` 이 `Host key verification failed.` 로 끝나는데, 그것을 `TOKEN=$(...)` 로 감싸면 오류는 stderr 로 흘러가고 `TOKEN` 에는 빈 문자열이 담긴다. 셸은 아무 불평도 하지 않는다. **왜 성공으로 읽히나** — 설치 스크립트가 `--token ''` 을 받아 `level=fatal msg=\"Error: --token is required\"` 로 죽는데 그 전까지를 다 성공으로 찍고 끝나므로 설치 출력만 보면 성공이고, 유닛은 `Restart=always` 라 5초마다 조용히 재시도한다. 실패가 화면이 아니라 `journalctl -u k3s-agent` 안에만 있다. **결론** — 게스트에 들어가지 않고 lab host 한 셸에서 `ssh kc-lab-1 '...'` 형태로 친다. 셸이 하나뿐이면 「지금 어디 있더라」가 생기지 않는다. 그리고 값을 쓰기 전에 길이로 가른다 — `[ ${#TOKEN} -ge 50 ] || echo \"TOKEN 이 비었다 — 3번으로 돌아간다\"`. 이 실험대의 토큰은 108자였고 형식이 `K10<해시>::server:<비밀번호>` 라 판올림에 따라 자릿수가 달라지므로 가드가 재는 것은 값이 아니라 `0` 이 아니라는 사실이다. 히스토리에 남기고 싶지 않으면 `--token-file` 로 넘긴다 — 그러면 「토큰을 꺼낸 셸과 같은 셸에서 쳐야 한다」는 제약도 없어진다. 비밀을 값이 아니라 길이로만 확인하는 것은 §185 의 ② 가 먼저 정한 표기 규약이고, 이 Case 는 그 규약이 실제로 무엇을 막는지를 보여 준다.",
|
|
"missing-verification": "설치 출력과 `journalctl` 의 원문이 이 저장소의 `final/evidence/` 에 없다 — `level=fatal msg=\"Error: --token is required\"` 도 `Host key verification failed.` 도 근거가 SSOT 본문이지 명령 출력 파일이 아니다. 108자도 같다. 그리고 이 실패를 일부러 다시 만들어 본 기록이 없어 「빈 토큰으로 설치하면 설치 출력이 성공으로 끝난다」는 한 번의 관측이다. §184 가 밝힌 검증 방식 때문에 재현은 쉽지 않다 — agent 를 다시 깔면 돌고 있는 노드가 없어진다. 재현 없이 남길 수 있는 것은 `journalctl` 쪽이고, 그것을 `final/evidence/raw/` 에 남기면 진단 절에 증거가 붙는다. 가드 한 줄이 실제로 빈 토큰을 잡아 본 기록도 없다.",
|
|
"relations": [
|
|
"reference:tool-output-is-not-the-subject-state",
|
|
"reference:check-the-nearest-layer-first",
|
|
"case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"question:does-the-guide-rebuild-this-lab",
|
|
"reference:verify-a-build-guide-in-execution-order"
|
|
],
|
|
"publication": "초안",
|
|
"file": "build-completion-judgment/case/case-an-empty-token-installed-the-agent-anyway.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "cloud-init 이 안 도는 원인은 넷인데 증상은 「SSH 가 안 붙는다」 하나였다",
|
|
"kind": "case",
|
|
"slug": "cloud-init-failures-all-look-like-ssh-refused",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#187-단계-01-게스트-세-대",
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비"
|
|
],
|
|
"classification": "**증상** — 게스트는 `running` 인데 `ssh donghyeon@192.168.122.11` 이 `Permission denied (publickey)` 로 끝난다. 이 한 증상 뒤에 원인이 넷이고 **넷 다 게스트 밖에 있다.** ① **시드를 `--cloud-init` 으로 붙였다**(observed) — 그 옵션은 시드를 SATA CD-ROM 으로 붙이는데 Debian `genericcloud` 이미지는 크기를 줄이려고 물리 하드웨어 드라이버를 뺐다. AHCI 장치가 보이지 않아 cloud-init 이 데이터소스를 못 찾고 조용히 끝난다. `bus=virtio` 로 디스크로 붙여야 한다. ② **YAML 파싱에 실패했다** — cloud-init 은 파싱에 실패해도 아무 오류를 남기지 않는다. ③ **`vol-upload` 를 빠뜨렸다** — `vol-create-as` 는 빈 볼륨을 만들 뿐이라 목록에는 이름이 보이는데 안이 0 으로 채워져 있고, cloud-init 은 `cidata` 라벨을 못 찾아 조용히 끝난다. ④ **`default` 네트워크의 autostart 가 `no` 다** — 지금은 되고 호스트를 재부팅한 다음 세 게스트의 SSH 가 한꺼번에 실패한다. **판정** — SSH 설정을 고치기 전에 호스트명 한 낱말을 본다. `ssh kc-lab-edge 'hostname'` 이 `kc-lab-edge` 를 내면 시드가 읽힌 것이고, 시드가 읽혔으면 같은 파일에 있던 SSH 키도 들어갔다는 뜻이다. `localhost` 가 나오면 SSH 가 아니라 시드부터 의심한다. 못 들어가면 `virsh screenshot kc-lab-1 /tmp/kc1.ppm` 으로 화면을 떠서 로그인 프롬프트 앞의 호스트명을 읽는다(확장자와 무관하게 PNG 로 저장된다) — `localhost login:` 이면 cloud-init 이 아예 안 돌았고 `kc-lab-1 login:` 이면 돌았고 사용자·키 단계에서 틀린 것이라 콘솔로 들어간다. 콘솔 비밀번호(`plain_text_passwd`)가 키가 안 들어갔을 때의 유일한 탈출구다. **반대 방향도 한 번 있다**(observed) — 게스트의 cloud-init `22.4.2` 스키마 검사기는 `sudo: ['ALL=(ALL) NOPASSWD:ALL']` 리스트 형태를 거부하면서 어느 키가 문제인지 안 알려 주는데(`users.0` 전체를 찍고 「어느 스키마에도 안 맞는다」고만 한다), 그 형태로도 부팅은 된다 — `kc-lab-1`·`kc-lab-2` 가 그 상태로 NOPASSWD sudo 가 돌고 있다. 검사가 통과해도 안 도는 쪽이 넷이고 검사에 걸려도 도는 쪽이 하나라, 어느 방향이든 검사 결과를 상태로 읽으면 틀린다. **예방** — 시드를 만들기 전에 `grep -c '__'` 가 0, `grep -c 'ssh-ed25519\\|ssh-rsa'` 가 2, `python3 -c yaml.safe_load` 가 `YAML OK` 인지를 본다. 셋이 맞아도 cloud-config 로 유효한 것은 아니라(키 이름 오타 `user` 와 `users` 는 그냥 통과한다) 게스트가 한 대라도 떠 있으면 cloud-init 자신의 스키마 검사기를 쓴다. 그리고 `instance-id` 에 타임스탬프를 넣는다 — id 가 같으면 user-data 를 고쳐도 반영되지 않는다. 정상이면 `cloud-init status` 가 `done` 이고 이 실험대에서 `running` 에서 `done` 까지 약 50초 걸렸다.",
|
|
"missing-verification": "넷 가운데 SSOT 가 관측으로 표시한 것은 ①(SATA·AHCI)과 스키마 검사기의 거부 문구뿐이다. ②·③·④ 는 막히면 표의 항목이라 이 실험대에서 실제로 그 증상을 본 것인지 가이드가 예상해 적은 것인지 SSOT 가 가르지 않았다 — 이 글에서 그 셋은 「그렇게 되는 구조」까지이고 「그렇게 됐다」가 아니다. 원문도 없다 — `Permission denied (publickey)` 도 `cloud-init status: done` 도 약 50초도 근거가 SSOT 본문이고 `final/evidence/` 에 명령 출력 파일이 없다. `virsh screenshot` 으로 뜬 화면도 남아 있지 않다. 그리고 §184 가 밝힌 검증 방식 때문에 ①~③ 은 다시 재현하기 어렵다 — 게스트를 다시 만들면 돌고 있는 실험대가 없어진다. ④ 만은 호스트를 재부팅해 확인할 수 있지만 그 기록도 없다.",
|
|
"relations": [
|
|
"case:an-empty-token-installed-the-agent-anyway",
|
|
"reference:tool-output-is-not-the-subject-state",
|
|
"question:does-the-guide-rebuild-this-lab",
|
|
"concept:what-a-qcow2-file-carries",
|
|
"reference:verify-a-build-guide-in-execution-order"
|
|
],
|
|
"publication": "초안",
|
|
"file": "build-completion-judgment/case/case-cloud-init-failures-all-look-like-ssh-refused.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다 — 2305초와 1~2초",
|
|
"kind": "case",
|
|
"slug": "renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신",
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat",
|
|
"final/document.md#194-이-부에서-파생될-open-question"
|
|
],
|
|
"classification": "**문제** — 인증서가 갱신되는 것과 그 인증서가 서빙되는 것은 다른 일인데, 배포판이 주는 것은 앞의 절반뿐이다. **관측**(observed) — `systemctl cat certbot-renew.service` 에 `ExecStartPost` 도 `--deploy-hook` 도 없다. 유닛은 `/usr/bin/certbot -q renew` 한 줄이고 인증서를 새로 받는 데까지만 책임진다. 갱신에서 서빙까지 걸린 시간이 훅 없이 **2305초(38분 25초)**, 훅을 넣으면 **1~2초**였다. 훅도 사람도 없었다면 다음 nginx 재시작까지, 즉 사실상 무기한이다. **진단** — nginx 는 인증서를 기동 시점에 읽어 메모리에 들고 있고 certbot 은 `live/` 심볼릭 링크만 갈아 끼운다. 경로는 그대로이고 내용만 바뀌므로 nginx 는 바뀐 줄 모른다. **88일 동안 이 결함이 보이지 않는다** — 타이머는 정상이고 매번 `SUCCESS` 로 끝나며 만료 30일 전까지는 갱신 자체를 하지 않아 발현할 기회가 없다. 발현하는 날의 증상은 인증서 만료이고, 그날에도 로그에는 `SUCCESS` 라고 적혀 있다. **해결** — `/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh` 에 `nginx -t && nginx -s reload` 두 줄을 두고 `chmod +x` 를 준다. 실행 권한이 없으면 certbot 이 조용히 건너뛴다. `post/` 가 아니라 `deploy/` 에 넣는 것은 `post/` 가 갱신이 없어도 매번 돌아 하루 두 번 워커를 갈아치우기 때문이다. 훅을 저장소에 두는 까닭도 여기 있다 — 호스트에만 두었을 때는 호스트를 초기화하면 아무 오류 없이 사라졌다. **판정은 로그 문구가 아니라 워커 PID 로 한다.** `certbot renew --dry-run` 은 훅이 호출되는지까지만 말해 준다. 강제 갱신 전후로 `ps -eo pid,lstart,args | grep 'nginx: worker'` 를 찍어 PID 가 바뀌었으면 reload 된 것이고, `lstart` 를 같이 뽑는 것은 PID 가 우연히 재사용됐을 때를 가르기 위해서다. 로그를 믿으면 안 되는 이유가 바로 나온다 — certbot 이 `Hook 'deploy-hook' ran with error output` 이라고 찍는데 실패가 아니다. nginx 의 `types_hash` 경고가 stderr 로 나갔을 뿐이고 내용은 `test is successful`·`signal process started` 다. 로그에서 `error` 를 grep 하는 감시를 걸면 성공한 훅을 실패로 오독한다. **훅을 넣어도 안전한가**(observed) — reload 는 무중단이었다. 새 연결 8856건 전부 200, p95 는 205.7ms 대 204.3ms 로 변화 없음. 845KB 를 20k/s 로 받는 중이던 요청이 전송 12초째에 reload 를 맞고도 845361바이트를 온전히 받았다(연결수 1) — 옛 워커가 그 요청을 끝까지 책임진다. **수치를 내기 전에 시계를 쟀다**(observed). test-server 가 NTP 미동기로 106초 빨랐고, 보정하지 않은 첫 계산은 훅이 인증서 발급보다 104초 먼저 실행된 것이 되어 물리적으로 불가능했다. 음수 지연이 나오면 계산이 아니라 시계를 의심한다.",
|
|
"missing-verification": "**2305초는 훅이 물리 호스트에만 있던 시절의 값이다**(inferred) — §194 가 그대로 남겼다. 지금은 certbot·인증서·갱신 훅이 전부 엣지 게스트에 있고, 그 배치에서 다시 재면 같은 수가 나오는지는 재지 않았다(미측정). 그래서 이 글의 2305초는 「훅이 없으면 이만큼 벌어진다」의 한 사례이지 지금 배치의 값이 아니다. 1~2초 쪽도 같은 시기의 값이다. 원문도 없다 — 2305초·8856건·p95 두 값·845361바이트·106초가 전부 SSOT 본문에 적힌 수이고 `final/evidence/` 에 측정 출력이 없다. 워커 PID 전후 비교의 출력도 남아 있지 않다. 그리고 88일 잠복은 구조에서 끌어낸 것이지(만료 30일 전에야 갱신을 시작한다) 실제로 한 주기를 돌려 본 것이 아니다 — 훅을 뺀 채로 실제 만료일까지 가 본 기록은 없고 그런 기록이 있을 이유도 없다.",
|
|
"relations": [
|
|
"decision:dns-01-because-the-lab-is-not-on-the-public-internet",
|
|
"reference:tool-output-is-not-the-subject-state",
|
|
"reference:check-the-nearest-layer-first",
|
|
"reference:verify-a-build-guide-in-execution-order",
|
|
"question:does-the-guide-rebuild-this-lab"
|
|
],
|
|
"publication": "초안",
|
|
"file": "build-completion-judgment/case/case-renewal-succeeded-while-the-old-certificate-kept-serving.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"concept": [],
|
|
"reference": [
|
|
{
|
|
"title": "가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다",
|
|
"kind": "reference",
|
|
"slug": "check-the-nearest-layer-first",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat",
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약",
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql"
|
|
],
|
|
"classification": "§189 이 절차를 그대로 적었다 — 「한 번에 밖에서 치지 않고 가까운 층부터 본다. 어디서 끊겼는지가 바로 나온다.」 엣지 구축의 확인이 네 칸이고 칸마다 건너뛰는 층이 하나씩 는다(observed) — ① nginx 를 건너뛴 `curl -I http://192.168.122.11` 이 `404`, ② DNAT 을 건너뛴 `http://192.168.122.10` 이 `301`, ③ 밖에서 `http://auth.hyeonworks.com` 이 `301 https://auth.hyeonworks.com/`, ④ TLS 이후 `https://auth.hyeonworks.com/realms/master` 가 `200`. **①의 `404` 가 성공 신호다** — 게스트의 80 을 Traefik 이 듣고 있고 매칭되는 Ingress 규칙이 없다고 답한 것이다. `502` 면 Traefik 은 떴는데 뒤에 백엔드가 없는 것이고 연결 거부·타임아웃이면 02 로 돌아간다. ②가 통과하는데 ③이 안 되면 문제는 DNAT 이고 ②에서 막히면 문제는 엣지 안이다 — 이 한 층을 끼워 두면 그 둘을 헷갈리지 않는다. 05 에서도 같은 모양이 반복된다 — 밖에서 `200` 이면 nginx → Traefik → Ingress → Service → 파드가 전부 이어진 것이고, `502`·`503` 이면 Ingress 가 있는지, Service 뒤에 파드가 있는지, 파드가 Ready 인지 순서로 뒤에서부터 되짚는다. **명령의 모양도 이 순서를 따라간다.** §185 의 ① 이 확인 명령을 두 종류로 갈라 적는데, 무엇이 잘못됐는지 모르는 상태에서는 값만 뽑는 `curl -s -o /dev/null -w '%{http_code}\\n'` 를 쓰지 않는다 — 골라 놓은 한 칸 말고는 전부 버리기 때문이다. 그래서 ①이 `-I` 로 시작해 ④에서 `%{http_code}` 로 줄어든다. **층을 좁힌 다음은 그 층의 문구를 읽는다.** 같은 「안 된다」가 층마다 다른 낱말로 나온다 — nginx upstream 의 `connect() failed (113: No route to host)` 는 네트워크, `(111: Connection refused)` 는 프로세스, `no live upstreams` 는 둘 다 죽었다는 판단이다(노드를 잃었을 때 이 세 줄이 1분 안에 순서대로 나왔다). k3s agent 노드의 `dial tcp [::1]:8080: connect: connection refused` 는 네트워크 문제가 아니라 kubeconfig 을 하나도 못 찾아 하드코딩 기본값으로 넘어간 것이다. 파드의 Exit Code 도 그것만으로 말이 된다 — `137` 은 OOM 이나 강제 종료, `1` 은 애플리케이션이 스스로 끝낸 것, `127` 은 명령을 못 찾은 것이다. Keycloak 컨테이너에서 `curl` 이 `127` 로 끝나는 것도 같은 읽기라, 그때는 안에서 묻기를 포기하고 Prometheus 나 `curlimages/curl` 임시 파드로 밖에서 묻는다.",
|
|
"scope": "프록시나 컨트롤러가 겹쳐 있어 밖에서 한 번 쳐서는 어디서 끊겼는지 알 수 없는 스택에 적용한다. 이 실험대의 요청 경로는 호스트 DNAT → 엣지 nginx → Traefik → Ingress → Service → 파드 여섯 층이다. 쓰는 방법은 둘이다 — 가장 가까운 층에서 시작해 한 칸씩 밖으로 나오며 치고, **층마다 무엇이 성공 신호인지를 미리 정해 둔다.** 성공이 `200` 이 아닌 층이 있다는 것이 이 규칙의 알맹이다(①의 `404`, ②·③의 `301`). 그리고 한 층을 좁힌 뒤에는 그 층이 내는 문구·errno·종료 코드가 어느 자원을 가리키는지를 읽는다. 사람이 손으로 치는 선까지만 쓴다 — 그 이상 가공해야 하면 파서를 짜지 않고 화면에 나온 것을 그대로 읽는다.",
|
|
"exceptions": "**치는 위치가 틀리면 층 판정이 통째로 무의미해진다.** 04 의 확인을 엣지 게스트 안에서 치면 `connect to 100.83.212.4 port 443 failed: Connection refused` 인데 이것은 층의 답이 아니다 — 엣지에서 나간 패킷은 호스트의 `virbr0` 으로 들어가고 DNAT 규칙은 `iifname \"tailscale0\"` 만 매칭하므로 안 걸린다. 그래서 이 절차를 쓰기 전에 각 칸을 어느 기계에서 치는지가 먼저 정해져 있어야 하고, 그것을 문서가 매번 말하게 하는 것은 reference:verify-a-build-guide-in-execution-order 가 받는다. 그리고 이 절차는 **어디서 끊겼는지**를 좁힐 뿐 **왜 끊겼는지**를 말하지 않는다 — ①에서 `404` 가 나와도 그 뒤의 값이 틀렸을 수 있고(02 의 INTERNAL-IP 가 그 모양이다), 로그를 읽어 좁히려다 잘린 문구를 붙들 수도 있다(nginx 에러 로그는 2048바이트에서 잘린다). 그쪽은 reference:tool-output-is-not-the-subject-state 가 받는다. 제3부의 reference:bisect-the-packet-path-with-capture-points 와는 재는 것이 다르다 — 그쪽은 네 지점에서 capture 를 떠 패킷이 사라진 구간을 좁히고, 이쪽은 층을 건너뛴 요청의 응답 코드로 좁힌다. 패킷이 아예 안 보이는 상태에서는 이 절차가 답을 못 내므로 그때 그쪽으로 넘어간다.",
|
|
"relations": [
|
|
"case:an-empty-token-installed-the-agent-anyway",
|
|
"case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"reference:tool-output-is-not-the-subject-state",
|
|
"reference:bisect-the-packet-path-with-capture-points",
|
|
"case:nftables-accept-did-not-stop-the-libvirt-reject"
|
|
],
|
|
"publication": "초안",
|
|
"file": "build-completion-judgment/reference/reference-check-the-nearest-layer-first.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
},
|
|
{
|
|
"title": "도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다",
|
|
"kind": "reference",
|
|
"slug": "tool-output-is-not-the-subject-state",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql",
|
|
"final/document.md#192-단계-06-prometheus-와-grafana",
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비"
|
|
],
|
|
"classification": "§191 이 규칙을 직접 적었다 — 클러스터가 형성됐는지를 셋으로 보고 **셋이 다른 것을 본다.** 로그 `ISPN000094` 는 「그때 그렇게 보였다」, 테이블 `jgroups_ping` 은 「지금 등록되어 있다」, 지표 `vendor_cluster_size` 는 「지금 그 노드가 그렇게 안다」이다. 테이블에는 둘 다 있는데 로그가 `(1)` 이면 서로를 찾기는 했는데 7800 으로 메시지가 안 가는 것이고, 이 실험대에서 실제로 그 일이 벌어졌다(observed). 각 노드가 자기가 아는 멤버 수를 보고하므로 한 노드만 보면 분단을 놓친다. §192 가 그 다음 칸을 적었다 — **`up` 을 믿지 않는다**(observed). 503 이 나는 동안에도 `up` 은 1 이었다. 프로세스가 살아 있고 `/metrics` 가 응답하기만 하면 1 이므로 「살아 있지만 쓸모없는」 상태를 보지 못한다. 이 관측대를 따로 세운 이유가 그것이다 — 7800 을 끊었을 때 외부 응답이 전부 200 이었고 분단된 노드가 스스로 로드밸런서에서 빠졌다. 밖에서 본 초록이 안의 분단을 가렸다. 같은 모양이 제6부 전체에 흩어져 있다. **빈 출력이 부재가 아닌 것** — `virsh` 가 기본으로 붙는 `qemu:///session` 과 VM 을 만든 `qemu:///system` 이 어긋나면 VM 이 만들어졌는데 `virsh list` 에 안 나오고, `net-list` 는 `--all` 을 빼면 `inactive` 인 네트워크가 아예 안 나와 「없음」과 「꺼짐」이 구분되지 않는다. `ip-dhcp-host` 로 `grep` 하면 예약이 멀쩡히 들어가 있어도 아무것도 안 나온다 — 그것은 `net-update` 의 섹션 이름이라 XML 안에 그 문자열이 없다. `kubectl get all` 은 이름과 달리 Secret·ConfigMap·PVC·Ingress 를 안 내놓고, `-l app=postgres` 에 Deployment 줄이 없는 것도 라벨을 파드 템플릿에만 달았기 때문이지 없는 것이 아니다. `\"result\":[]` 는 「0 이다」가 아니라 「그런 지표가 없다」이고, 스크레이프 대상 목록에서 **봐야 할 것은 거기 있는 이름이 아니라 없는 이름이다** — 이 실험대는 Redis·BFF·PostgreSQL 을 긁지 않으므로 그 지표가 없는 것은 측정 실패가 아니라 측정된 공백이다. 이벤트도 기본 한 시간만 남아 없는 것이 무사를 뜻하지 않는다. nginx 에러 로그는 2048바이트에서 잘려, 이 실험대에서 502 원인이 잘린 채로 error 로그에 있었고 access 로그에는 3492자로 온전히 남아 있었다. **초록이 정상이 아닌 것** — `nginx -t` 는 `sites-available` 을 `site-available` 로 잘못 친 빈 파일에도 통과한다(아무 에러 없이 아무 일도 안 일어난다). `.nft` 의 포트를 `433` 으로 쳐도 nft 가 군말 없이 받고 80 은 멀쩡히 넘어가므로 03 은 다 통과한 뒤 04 에서 HTTPS 만 안 되는 형태로 뒤늦게 터진다. `kubectl get nodes` 두 줄이 `Ready` 여도 `-o wide` 의 INTERNAL-IP 가 `--node-ip` 로 준 값과 다를 수 있고, 그때는 지금 아무 증상이 없다가 03 의 upstream 과 노드 상실 실험에서 어긋난다 — `get nodes -o wide` 는 k3s 가 보고한 값이고 `systemctl cat` 의 `ExecStart` 는 우리가 준 값이라 둘을 견줘야 한다. 유닛 이름이 노드마다 달라 agent 에서 `systemctl stop k3s` 를 치면 아무 일도 안 일어나고 「주입했는데 증상이 없다」로 읽힌다. Secret 은 `describe` 의 `19 bytes`·`22 bytes` 와 파드 안 `${#VAR}` 의 `길이=19` 가 같아야 주입까지 이어진 것이고, 파드 둘이 `Running` 이어도 Endpoints 가 하나면 이미 한쪽으로만 가고 있던 트래픽을 이중화 실패로 오독하게 된다. `node-exporter` 줄이 하나뿐이면 그 노드의 지표가 통째로 없는 채로 실험을 하게 된다. 인증서도 같다 — `cert.pem` 을 쓰면 체인이 끊기는데 브라우저는 캐시나 AIA 로 보완해서 정상으로 보이고 캐시 없는 클라이언트에서만 깨지므로, 믿을 수 있는 판정은 `openssl s_client` 의 단계 수뿐이다(이 실험대의 실측은 0~3 네 단계, `Verify return code: 0 (ok)`). **침묵과 경고도 상태가 아니다** — `kubectl rollout status` 는 끝날 때까지 아무것도 안 찍고 그 침묵이 정상이며 300초를 다 쓰고 타임아웃으로 끝나면 그것도 답이다(「안 떴다」가 확정된다). `nginx -t` 의 `[warn] could not build optimal types_hash` 는 통과를 막지 않고 실패는 `[emerg]` 줄에 파일과 줄 번호로 나온다 — 이 경고를 실패로 오독하는 일이 04 에서 실제로 벌어졌다.",
|
|
"scope": "상태를 묻는 명령의 출력으로 구축 단계의 통과를 판정할 때 적용한다. 쓰는 방법은 셋이다. ① **그 명령이 무엇을 세는지 먼저 적는다** — 어느 연결에 붙어 있나(`virsh uri`), 꺼진 것도 세나(`net-list --all`), 이름이 약속한 만큼 내놓나(`kubectl get all`), 어디까지 남기나(2048바이트, 이벤트 한 시간). ② **비어 있는 출력을 낼 때 「없다」와 「못 봤다」를 갈라 적는다** — `\"result\":[]` 를 0 으로 읽지 않고, 목록에 없는 이름을 측정 실패가 아니라 측정된 공백으로 기록한다. ③ **한 근거로 판정하지 않고 시제나 층이 다른 것을 함께 본다** — 로그(과거)와 테이블(현재 등록)과 지표(현재 인식), 보고된 값과 준 값, Secret 의 저장과 주입, 클러스터 안과 밖. 값을 안 찍고 길이만으로 판정하는 §185 의 ② 도 이 축에 있다.",
|
|
"exceptions": "이 규칙이 잡는 것은 **출력을 상태로 읽는 것**이지 출력 자체의 정확성이 아니다. 세 근거가 다 초록이어도 그 셋이 다 같은 층에서 나왔으면 여전히 한 근거다 — 밖에서 친 200 이 분단을 가린 것이 그 모양이고, 그래서 관측대를 안쪽에 따로 세웠다. 그 관측대도 `up` 하나로는 같은 실패를 되풀이하므로 기능 지표를 함께 본다. 그리고 근거를 늘리는 데는 비용이 있다 — 명령이 늘고 손으로 치는 선을 넘으면 파서를 짜게 되는데, §192 가 그 선을 `grep -o` 와 `tr ',' '\\n'` 까지로 그었다. 「없다」와 「못 봤다」를 가르는 것도 도구가 대신해 주지 않는다 — 스크레이프 대상 목록에서 없는 이름을 보는 것은 사람이 그 이름을 미리 알고 있을 때만 된다. 마지막으로 이 규칙의 근거 대부분이 **이 실험대 한 대에서 한 번씩 본 것**이라, 다른 판 번호나 다른 배포판에서 같은 명령이 같은 것을 세는지는 재지 않았다.",
|
|
"relations": [
|
|
"case:an-empty-token-installed-the-agent-anyway",
|
|
"case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"reference:check-the-nearest-layer-first",
|
|
"question:does-the-guide-rebuild-this-lab"
|
|
],
|
|
"publication": "초안",
|
|
"file": "build-completion-judgment/reference/reference-tool-output-is-not-the-subject-state.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"question": [
|
|
{
|
|
"title": "가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가",
|
|
"kind": "question",
|
|
"slug": "does-the-guide-rebuild-this-lab",
|
|
"readiness": "OPEN",
|
|
"source": [
|
|
"final/document.md#194-이-부에서-파생될-open-question",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"known": "§184 가 이 부의 검증 방식을 갈라 적었다(observed) — 읽기 전용 확인은 돌아가는 실험대에서 실제로 실행해 출력을 그대로 실었다. **만드는 명령은 다르다.** VM 을 다시 만들거나 k3s 를 다시 깔면 지금 돌고 있는 실험대가 없어지므로, 그 명령들은 구축할 때 쓴 것을 그대로 옮기고 결과 상태를 확인하는 것으로 대신했다. 그래서 §186~§192 의 생성 명령은 「그때 이렇게 쳤다」까지이고 「지금 다시 쳐도 같은 상태가 된다」는 확인되지 않았다(unknown). §194 가 그것을 첫 물음으로 남겼다. 다시 세울 때 필요한 것 가운데 일부는 저장소에 없다 — 매니페스트 둘(`keycloak-cluster.yaml`·`observability.yaml`)과 cloud-init 템플릿 `kc-lab.yaml.example` 이 `source/` 에 반입되지 않았고, 가이드가 화면에 옮겨 적은 만큼만 있다. 세울 대상 자체는 적혀 있다 — 7단계와 단계마다의 통과 조건(`virsh list` 가 돈다 · 세 게스트에 SSH 가 붙는다 · `kubectl get nodes` 에 둘 다 Ready · 밖에서 요청이 파드까지 닿는다 · `https://` 가 열리고 체인이 4단계 · 관리 콘솔 로그인 · `vendor_cluster_size` 가 2). 그리고 §182 가 가이드를 순서대로 따라가며 나온 결함 여섯을 이미 적었는데, reference:verify-a-build-guide-in-execution-order 는 그 규칙으로 가이드를 고친 뒤 **처음부터 다시 따라가 본 기록이 아직 없다**고 스스로 밝혔다.",
|
|
"unknown": "지금의 가이드 7단계를 빈 호스트에서 처음부터 순서대로 쳤을 때 어느 단계에서 멈추는지, 멈춘다면 그것이 §182 가 이미 센 여섯 중 하나인지 그때는 안 보이던 새 결함인지. 그리고 `source/` 에 없는 매니페스트 둘과 cloud-init 템플릿 없이 02·05·06 이 문서만으로 서는지. 다시 선 실험대가 지금과 같은 상태인지를 무엇으로 판정할 것인가도 정해져 있지 않다 — 단계마다의 통과 조건 일곱이 같은 값을 내는 것으로 충분한지, 판 번호(libvirt `12.7.0` · `QEMU emulator version 11.1.1` · `v1.36.4+k3s1` · `nginx/1.22.1`)까지 같아야 하는지.",
|
|
"next-verification": "실험대를 멈추지 않고 재려면 대상이 따로 있어야 한다 — 이 호스트가 아닌 다른 기계, 또는 이 호스트에 게스트 세 대를 새 이름으로 한 벌 더 세우는 것이다. 뒤엣것은 §187 이 적은 배치(호스트 RAM 11,648MiB 에 세 게스트 합 10240MB)에서는 메모리가 모자라므로 게스트 크기를 줄여 돌리고 그 사실을 함께 적는다. 순서는 가이드 그대로 00 부터 06 까지이고, 각 단계의 「이 단계가 끝나면」 명령을 치고 출력을 `final/evidence/raw/` 에 원문으로 남긴다 — `meta/` 에 명령·cwd·실행 시각·종료 코드를 적는다. 막힌 자리마다 무엇이 없어서 막혔는지(리소스인가 셸인가 명령 자체인가)를 §182 의 두 축으로 분류해 적는다. 매니페스트 둘과 cloud-init 템플릿은 먼저 `source/` 로 반입해 `final/` 에 넣고 시작한다 — 없으면 이 검증이 재는 것이 「가이드가 서는가」가 아니라 「빠진 파일을 다시 만들 수 있는가」가 된다.",
|
|
"decision-criterion": "00 부터 06 까지 통과 조건 일곱이 전부 같은 값을 내면 「가이드만으로 이 실험대가 다시 선다」고 적고 닫는다. 그러면 question:qcow2-transfer-time-over-wifi 가 재는 이동 시간과 견줄 대상이 생긴다 — 옮기는 것이 빠른지 다시 세우는 것이 빠른지가 그때 정해지고, 옮기는 것이 비현실적이라면 이 실험대의 유일한 복원 경로가 문서가 된다. 어느 단계에서든 막히면 그 자리를 §182 의 결함 표에 행으로 더하고 reference:verify-a-build-guide-in-execution-order 의 「규칙 자체가 결함을 막아 냈다는 관측은 없다」를 그 결과로 바꾼다 — 막힌 자리가 그 규칙이 잡는 두 축(시점·셸) 안이면 규칙이 통한 것이고, 밖이면 축이 모자란 것이라 규칙을 고친다. 어느 쪽이든 §186~§192 의 생성 명령에 붙은 unknown 이 그때 확인 또는 반증으로 바뀐다.",
|
|
"relations": [
|
|
"reference:verify-a-build-guide-in-execution-order",
|
|
"question:qcow2-transfer-time-over-wifi",
|
|
"case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"case:an-empty-token-installed-the-agent-anyway",
|
|
"reference:tool-output-is-not-the-subject-state"
|
|
],
|
|
"publication": "초안",
|
|
"file": "build-completion-judgment/question/question-does-the-guide-rebuild-this-lab.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"decision": []
|
|
}
|
|
},
|
|
"lab-entry-path-and-measurement-integrity": {
|
|
"topic": "lab-entry-path-and-measurement-integrity",
|
|
"title": "실험대의 진입 경로 — 한 겹을 더하면 무엇이 오염되나",
|
|
"readerQuestion": "브라우저에서 파드까지 이 실험대의 요청이 지나는 길에는 무엇이 서 있고, 거기에 한 겹을 더하거나 헤더를 덧붙이면 이 실험대가 재려는 계약이 왜 성립하지 않게 되는가?",
|
|
"kinds": {
|
|
"case": [],
|
|
"concept": [
|
|
{
|
|
"title": "L7 이 두 겹인 이유와, 진입점 하나를 이중화하려 할 때 끝나지 않는 재귀",
|
|
"kind": "concept",
|
|
"slug": "two-l7-hops-and-the-entry-point-recursion",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#261-진입점-자체가-죽으면-로드밸런서의-재귀-문제",
|
|
"final/document.md#275-호스트-nginx와-traefik은-무엇이-다른가-둘-다-필요한-이유",
|
|
"final/document.md#257-리버스-프록시와-upstream",
|
|
"final/document.md#258-왜-tls를-끊어서-내용을-보는가"
|
|
],
|
|
"basis-version": "이 실험대의 2026-09-03 배치 기준이다 — 호스트 nginx(Arch · 1.30.4)가 TLS 를 끊고 게스트 두 대의 Traefik(k3s v1.36.4 기본 ingress)으로 평문 HTTP 를 넘긴다. 엣지가 게스트로 옮겨진 뒤에도 L7 홉 수는 2 그대로다(§179). ALB/NLB 대조는 AWS 의 두 제품을 기준으로 쓴 것이고 이 실험대에서 관측한 것이 아니다. VRRP 절도 keepalived 일반 동작이고 이 실험대에 구성하지 않았다.",
|
|
"classification": "**호스트 nginx 는 이 실험대의 단일 장애점이다.** 숨길 이유가 없고 물리 머신도 한 대이므로 그것 역시 SPOF 다 — 알려진 한계로 남긴다. 이 글은 그 사실에서 시작해 셋을 푼다. **① ALB 와 NLB 는 계층이 다른 것이 아니다.** 둘 다 클러스터 밖의 로드밸런서이고 같은 자리를 놓고 고르는 두 선택지라 Ingress Controller 와 대응되는 관계가 아니다. **진입점 자리는 하나이고 L7 처리는 어딘가에서 반드시 한 번 일어난다** — 배치의 차이는 진입점과 L7 처리기가 같은 장비인가 다른 장비인가뿐이다. ALB 패턴은 하나가 두 역할을 겸하고, NLB 패턴은 진입점을 L4 로 두고 L7 처리를 클러스터 안으로 옮긴다. **② 이 실험대와 운영은 둘 중 어느 쪽도 아닌 L7 두 겹이다.** 밖의 nginx 가 TLS 를 끊고 `X-Forwarded-*` 를 넣으므로 ALB 에 가까운데 그 뒤에 Traefik 이 또 L7 이다. **두 겹을 쌓는 이유는 역할이 다르기 때문**이다 — nginx 는 **고정** IP:포트를 알고 사람이 파일을 고쳐 reload 하며 「어느 노드로」를 정하고, Traefik 은 API 서버를 감시하며 파드 생성·소멸을 따라가고 「어느 파드로」를 정한다. nginx 는 클러스터의 존재를 모르고 파드 IP 가 바뀌는 것도 모른다. **Traefik 만으로는 부족한 이유**가 여기서 나온다 — servicelb 덕에 두 노드의 80 에 다 바인딩되지만 **브라우저는 어느 노드로 가야 할지 모르고** 그 노드가 죽으면 그 IP 도 죽는다. Traefik 은 노드 안에서 파드로 나눠 주지만 노드들 사이에서는 나눠 주지 못한다. 반대로 nginx 만 쓰면 Ingress 리소스를 못 쓰고 파드 IP 가 바뀔 때마다 수동 수정이다. **그리고 두 경우 모두 운영 구조와 달라진다** — 운영이 `host nginx → k3s(Traefik)` 이므로 실험대도 그 2홉을 복제해야 `X-Forwarded-*` 신뢰 경계 결론이 그대로 이전된다. 이것이 결정적인 이유다. **③ 진입점을 이중화하려 하면 재귀가 끝나지 않는다.** 한 머신 안에서 nginx 를 여럿 띄우는 것은 의미가 없다 — nginx 는 이미 master 1 + worker N 구조로 리스닝 소켓을 공유하고, 같은 머신에 인스턴스를 늘려도 그 머신이 죽으면 전부 죽어 가용성이 늘지 않는다. 진짜 이중화는 머신을 늘리는 것인데 그러면 **「어느 nginx 로 갈지는 누가 정하는가」**가 새로 생기고 앞에 LB 를 또 두면 그것이 SPOF 다. 실무는 이 재귀를 소프트웨어가 아니라 **네트워크 계층의 장치**로 끊는다 — VIP + VRRP(keepalived)는 **선택자가 없고 IP 자체가 이동한다**(MASTER 가 죽으면 BACKUP 이 VIP 를 가져가고 gratuitous ARP 로 스위치의 MAC 테이블을 갱신한다. 같은 IP 인데 트래픽이 다른 장비로 흐른다), DNS 다중 A 레코드는 클라이언트가 고르고, 애니캐스트는 라우터가 고르고, 클라우드 LB 는 **재귀를 AWS 가 대신 풀어 준 것**이지 재귀가 없는 것이 아니다. **이 실험대는 이중화하지 않는다** — 물리 머신이 한 대라 keepalived 를 구성해도 그 머신이 죽으면 끝이고, 검증 대상은 Keycloak 의 세션·토큰이지 LB 가용성이 아니다. 다만 Traefik 은 이미 두 노드에 떠 있으므로 「노드 하나를 죽이고 호스트 nginx 의 upstream 이 어떻게 반응하는지」는 그대로 관찰할 수 있고 그것이 이 실험대가 다루는 범위다. **거꾸로 뽑았다** — decision:edge-nginx-moved-into-a-guest-vm 의 근거가 「L7 홉 수는 전후 모두 2홉 그대로」인데 왜 2홉인지가 그 기록에 없고, reference:overwrite-a-forwarded-header-at-the-trust-boundary-do-not-extend-it 는 「경계」가 그 둘 중 어디인지를 전제로 삼는다. **`keycloak-session-store` 와 겹치지 않는다** — 저쪽은 세션이 어디에 있는가를 재고 이쪽은 요청이 어느 층을 지나는가를 말한다.",
|
|
"relations": [
|
|
"decision:edge-nginx-moved-into-a-guest-vm",
|
|
"reference:overwrite-a-forwarded-header-at-the-trust-boundary-do-not-extend-it",
|
|
"decision:no-public-tunnel-because-a-third-hop-pollutes-the-measurement",
|
|
"setup:edge-nginx-and-host-dnat",
|
|
"concept:guest-packet-path-to-physical-nic",
|
|
"reference:check-the-nearest-layer-first"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-entry-path-and-measurement-integrity/concept/concept-two-l7-hops-and-the-entry-point-recursion.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"setup": [],
|
|
"reference": [
|
|
{
|
|
"title": "신뢰 경계에서는 forwarded 헤더를 덧붙이지 않고 덮어쓴다",
|
|
"kind": "reference",
|
|
"slug": "overwrite-a-forwarded-header-at-the-trust-boundary-do-not-extend-it",
|
|
"readiness": "READY",
|
|
"source": [
|
|
"final/document.md#209-nginx-keycloak-lab-conf-스티키-스위치와-신뢰-경계",
|
|
"final/document.md#259-x-forwarded--와-신뢰-경계",
|
|
"final/document.md#207-lab-edge-dnat-nft-dnat-파일이-자기-안에-적어-둔-네-가지",
|
|
"final/document.md#206-이-부의-출처와-범위"
|
|
],
|
|
"classification": "**규칙** — 맨 바깥 프록시는 클라이언트가 보낸 `X-Forwarded-For` 를 **버리고 자기가 본 주소로 덮어쓴다.** nginx 에서는 `proxy_set_header X-Forwarded-For $remote_addr;` 이고 `$proxy_add_x_forwarded_for` 가 아니다. 설정 정본이 그 줄 위에 이유를 적어 두었다 — 「$remote_addr, not $proxy_add_x_forwarded_for. This is the trust boundary: a client-supplied X-Forwarded-For must be discarded, not extended, or nothing downstream can rely on the value.」 **왜 그런가** — 둘의 차이는 클라이언트가 보낸 값을 사슬 앞에 남기느냐 버리느냐다. 덧붙이면 **위조된 값이 사슬에 남고, 그러면 뒤쪽의 어느 것도 그 헤더를 근거로 쓸 수 없다.** 헤더 자체는 누구나 보낼 수 있는 평범한 HTTP 헤더라 값을 믿게 만드는 것은 헤더가 아니라 **그 값을 누가 썼는가**이고, 그 「누가」를 하나로 만드는 것이 이 규칙이다. **커널 쪽에도 같은 계약의 절반이 있다** — 호스트의 DNAT 파일이 「DNAT only, never SNAT」을 적고 이유를 붙였다. masquerade 를 걸면 출발지가 다시 쓰여 엣지가 **모든 클라이언트를 `192.168.122.1` 로 보게 되고**, 그러면 이 실험대가 재는 `X-Forwarded-For` 계약이 **조용히 무효가 된다.** SNAT 없이도 응답이 돌아오는 것은 게스트의 기본 경로가 호스트라 응답이 그 자리를 다시 지나고 conntrack 이 변환을 알아서 되돌리기 때문이다. 즉 **경계 앞에서는 출발지를 바꾸지 않고, 경계에서는 클라이언트가 준 값을 버린다** — 둘이 한 계약이다. **이 규칙이 이 저장소에 없던 것이다**(observed) — §206 이 설정 원본의 주석 59줄을 대조하며 「둘은 이 저장소 어디에도 없다」고 센 둘 중 하나가 이것이다. §189·§190 은 그 줄을 싣기만 하고 왜 그 형태인지를 적지 않았다.",
|
|
"scope": "**신뢰 경계에 서 있는 맨 바깥 프록시 한 대**에 적용한다 — 이 실험대에서는 엣지 게스트의 nginx 이고, 클라우드라면 ALB 가 그 자리다. 경계가 어디인지는 「그 앞에 우리가 통제하지 않는 것이 있는가」로 가른다. 대상 헤더는 `X-Forwarded-For` 뿐 아니라 `X-Forwarded-Proto`·`X-Forwarded-Host` 를 함께 본다 — 셋 다 관례적 헤더이고 클라이언트가 임의로 보낼 수 있다. **적용 시점** — 설정을 처음 쓸 때가 아니라 **프록시를 한 겹 더 넣거나 옮길 때**가 이 규칙이 실제로 쓰이는 자리다. 이 실험대가 엣지를 호스트에서 게스트로 옮겼을 때 경계가 옮겨 갔고, 공개 터널을 앞에 붙였다면 경계가 Cloudflare 엣지로 한 번 더 옮겨 갔을 것이다(그래서 decision:no-public-tunnel-because-a-third-hop-pollutes-the-measurement 가 그것을 기각했다).",
|
|
"exceptions": "**경계 안쪽의 두 번째 홉은 반대다** — 이 실험대의 Traefik 처럼 신뢰하는 프록시 뒤에 서는 것은 앞이 쓴 값을 **이어받아야** 하고 거기서 덮어쓰면 원래 클라이언트 주소가 사라진다. 그래서 `$proxy_add_x_forwarded_for` 가 틀린 값이 아니라 **자리가 정해져 있는 값**이다 — 경계에서 쓰면 위조를 통과시키고 경계 안에서 안 쓰면 주소를 잃는다. **L4 통과 구성에는 적용되지 않는다** — NLB 처럼 TCP 를 그대로 흘리면 원본 IP 가 보존되어 헤더가 아예 필요 없고, 그때 쓰는 것은 PROXY protocol 이라 이 규칙의 대상이 아니다. **경계 앞에 CDN 이나 터널이 있으면 그 공급자의 헤더가 정본이 된다** — Cloudflare 라면 `CF-Connecting-IP` 이고, 그때는 이 규칙을 그 헤더에 대해 다시 세워야 한다. **이 규칙만으로 신뢰가 완성되지 않는다** — 경계 프록시가 아닌 경로로 뒤쪽에 직접 닿을 수 있으면 헤더를 어떻게 쓰든 소용이 없다. 그 경로를 막는 것은 방화벽과 네트워크 배치의 일이고 이 실험대에서는 게스트가 libvirt NAT 뒤에 있는 것이 그 역할을 한다.",
|
|
"relations": [
|
|
"concept:two-l7-hops-and-the-entry-point-recursion",
|
|
"setup:edge-nginx-and-host-dnat",
|
|
"decision:edge-nginx-moved-into-a-guest-vm",
|
|
"decision:no-public-tunnel-because-a-third-hop-pollutes-the-measurement",
|
|
"case:nftables-accept-did-not-stop-the-libvirt-reject"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-entry-path-and-measurement-integrity/reference/reference-overwrite-a-forwarded-header-at-the-trust-boundary-do-not-extend-it.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
],
|
|
"question": [],
|
|
"decision": [
|
|
{
|
|
"title": "공개 터널을 쓰지 않고 tailnet 직결로 둔다 — 홉이 하나 늘면 재려던 계약이 오염된다",
|
|
"kind": "decision",
|
|
"slug": "no-public-tunnel-because-a-third-hop-pollutes-the-measurement",
|
|
"readiness": "READY",
|
|
"decision-status": "ADOPTED",
|
|
"source": [
|
|
"final/document.md#305-tunnel-채택하지-않은-이유를-남긴-자산",
|
|
"final/document.md#302-왜-적용하지-않는-것을-남겨두는가",
|
|
"final/document.md#300-9층-deploy-무엇이-살아-있고-무엇이-참조인가",
|
|
"final/document.md#301-전체-지도"
|
|
],
|
|
"decision-evidence": "§305 가 `deploy/tunnel/cloudflared-config.yml` 을 두고 「그런데 이 실험대는 채택하지 않았다」로 기각을 명시하고 이유를 전/후 홉 그림으로 적는다. 기각한 파일을 지우지 않고 남긴 방침도 §300~§302 에 따로 있다 — 저장소에 있으나 적용되지 않는 설정이 여럿이고 그것들은 죽은 코드가 아니라 **의도적으로 남겨 둔 참조 자산**이라고 전체 지도와 함께 적는다.",
|
|
"grounds": "**대안의 매력은 인정하고 시작한다** — Cloudflare named tunnel 은 **아웃바운드 연결만 쓰므로 포트포워딩 없이 공개 HTTPS 이름을 얻는다.** 공유기를 건드릴 수 없는 환경에서 매력적인 선택지이고, SSOT 는 그 설정의 함정 둘(`service:` 에 `127.0.0.1` 을 쓰면 cloudflared 컨테이너 자신을 가리킨다 · 마지막 catch-all 이 없으면 오류가 난다)까지 적어 두었다. **기각 근거** — 터널을 쓰면 `브라우저 → Cloudflare 엣지 → nginx → Traefik → Pod` 로 **3홉**이 되고 지금은 `브라우저 → nginx → Traefik → Pod` 로 2홉이다. Cloudflare 엣지가 TLS 를 끊고 다시 맺으면서 홉이 하나 늘고 `CF-Connecting-IP` 같은 자체 헤더가 섞인다. **이 실험대가 측정하려는 것이 정확히 `nginx → Traefik` 2홉의 forwarded 헤더 계약이므로 앞에 한 겹이 더 붙으면 측정이 오염된다.** 근거가 「더 나쁘다」가 아니라 **재려는 것과 충돌한다**는 데 있다. **감수한 비용** — 진입 주소가 tailnet(`100.83.212.4`, `100.64.0.0/10` CGNAT 예약 대역)으로 남아 **공개 인터넷에서 라우팅되지 않는다.** 그 대가가 두 곳에서 청구됐다 — 인증서를 HTTP-01 로 받을 수 없어 DNS-01 로 가야 했고(decision:dns-01-because-the-lab-is-not-on-the-public-internet), 재구축 때 `/etc/letsencrypt/` 를 지워도 되는지가 미확정으로 남았다(question:is-this-lab-issuing-certificates-with-http-01-or-dns-01). **되살아나는 조건이 적혀 있다** — 조건이 바뀌어(예: 다른 회선으로 이전) 공개 접근이 필요해지면 이 파일이 그대로 쓰인다. 그래서 지우지 않는다. 지우지 않는 방침 자체에도 근거가 셋 있다 — ① 이 저장소의 목적이 비교라 **선택지를 나란히 두고 트레이드오프를 기록하는 것 자체가 산출물**이고 하나만 남기면 「왜 이걸 골랐는가」의 근거가 사라진다 ② 죽은 코드가 아니라 **테스트되는 코드**다(`scripts/verify-*.sh` 가 붙어 있어 실행되지 않을 뿐 깨지면 드러난다) ③ 실험대 전용 설정은 `lab/` 아래로 분리해 일반 배포 설정과 섞이지 않게 두었다.",
|
|
"classification": "**진입 경로에 무엇을 더할 것인가**를 정한 결정이고, 그 물음이 이 주제의 독자 질문이다. concept:two-l7-hops-and-the-entry-point-recursion 이 「L7 이 두 겹이고 그 둘의 역할이 다르다」를 설명하면 이 결정은 「거기에 세 번째를 붙이지 않는다」를 정한다. reference:overwrite-a-forwarded-header-at-the-trust-boundary-do-not-extend-it 와도 같은 축이다 — 저쪽은 경계에서 헤더를 어떻게 다루는가이고 이쪽은 경계 앞에 무엇을 세우지 않는가다. decision:dns-01-because-the-lab-is-not-on-the-public-internet 과 물음이 다르다 — 그 결정은 「공개 인터넷에 없는 주소에서 인증서를 어떻게 받나」이고 이 결정은 「왜 공개 인터넷에 두지 않기로 했나」라 시간 순서상 이쪽이 먼저다. 한 기록에 합치면 원인과 결과가 한 칸에 들어간다. **기술이 존재한다는 사실을 근거로 바꾸지 않았다** — 터널이 무엇을 해 주는지가 아니라 그것이 이 실험대의 측정 대상에 무엇을 하는지가 근거다.",
|
|
"relations": [
|
|
"concept:two-l7-hops-and-the-entry-point-recursion",
|
|
"reference:overwrite-a-forwarded-header-at-the-trust-boundary-do-not-extend-it",
|
|
"decision:dns-01-because-the-lab-is-not-on-the-public-internet",
|
|
"decision:edge-nginx-moved-into-a-guest-vm",
|
|
"question:is-this-lab-issuing-certificates-with-http-01-or-dns-01"
|
|
],
|
|
"publication": "초안",
|
|
"file": "lab-entry-path-and-measurement-integrity/decision/decision-no-public-tunnel-because-a-third-hop-pollutes-the-measurement.md",
|
|
"status": "게시 전",
|
|
"studioId": "",
|
|
"assets": [],
|
|
"assetFiles": [],
|
|
"evidenceFiles": []
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"candidates": [
|
|
{
|
|
"id": "SSOT-02-19-execution-path-as-one-concept",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#2-전체-구조",
|
|
"final/document.md#19-이-ssot에서-파생될-concept"
|
|
],
|
|
"summary": "virsh · libvirt · QEMU · /dev/kvm · KVM Core · kvm_intel · VMX 를 거쳐 물리 CPU 에 닿는 실행 경로가 한 줄기로 이어진다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "§19 가 이 경로를 하나의 CONCEPT 로 관리하라고 적었고, 경로 중간을 잘라 내면 인과가 끊겨 독립성 검사를 통과한다. 이 프로젝트에서 유일하게 독립 기록이 되는 후보다"
|
|
},
|
|
{
|
|
"id": "SSOT-03-component-roles",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#3-각-구성요소의-역할"
|
|
],
|
|
"summary": "구성요소 일곱의 역할 — virsh 는 CLI 이고 VM 을 실행하는 것은 QEMU 프로세스다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "경로 위 정거장들의 설명이다. 따로 떼면 무엇으로 가는 길목인지 말할 수 없어 어느 것도 혼자 서지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-04-vcpu-thread-mapping",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#4-vcpu와-vcpu-thread"
|
|
],
|
|
"summary": "Guest 의 vCPU 하나에 Host 의 QEMU vCPU thread 하나가 대응한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "이 대응이 없으면 뒤의 스케줄링·contention·steal time 절이 무엇에 대한 이야기인지 성립하지 않는다. 개념의 한 절이다"
|
|
},
|
|
{
|
|
"id": "SSOT-05-host-scheduler-owns-vcpu-thread",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#5-host-linux-scheduler와-실제-cpu"
|
|
],
|
|
"summary": "vCPU thread 도 다른 Host thread 와 같이 Linux Scheduler 가 실행할 logical CPU 를 정한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "같은 경로의 다음 칸이다. 독립 기록으로 빼면 앞뒤 절을 다시 설명해야 한다"
|
|
},
|
|
{
|
|
"id": "SSOT-06-kvm-run-and-vm-entry",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#6-kvm_run과-guest-실행"
|
|
],
|
|
"summary": "vCPU thread 가 KVM_RUN 을 부르면 VM Entry 로 Guest 코드가 실제 CPU 에서 실행된다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "경로가 커널로 넘어가는 지점이다. VM Exit 절과 짝이라 떼면 왕복이 반쪽만 남는다"
|
|
},
|
|
{
|
|
"id": "SSOT-07-vm-exit-is-not-shutdown",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#7-vm-entry와-vm-exit"
|
|
],
|
|
"summary": "VM Exit 은 VM 종료가 아니라 Guest 실행에서 Hypervisor 로 제어권이 넘어가는 전환이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "오해를 바로잡는 정의 한 절이다. 정의 하나로 독립 기록을 만들면 §8·§9·§24.9 가 갈 곳을 잃는다"
|
|
},
|
|
{
|
|
"id": "SSOT-08-what-causes-vm-exit",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#8-무엇이-실제로-vm-exit을-발생시키는가"
|
|
],
|
|
"summary": "VM Exit 은 root 권한이 아니라 VMCS 의 VM-Execution Control 설정과 동작 종류가 정한다 — HLT · IN/OUT · CPUID · CR · MSR · Exception · External Interrupt",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "일곱 원인이 같은 질문에 같은 결론으로 답한다 — 무엇이 Exit 하는지는 VMX control 이 정한다. 개념 안의 표나 하위 절이지 일곱 편이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-09-exit-handled-in-kvm-or-qemu",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#9-vm-exit-이후-처리"
|
|
],
|
|
"summary": "Exit Reason 에 따라 KVM 안에서 끝나기도 하고 KVM_RUN 이 반환되어 QEMU 까지 올라가기도 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "§8 과 한 왕복을 이루는 절이다. §24.9 의 「Exit 처리 위치가 KVM 인지 QEMU 인지 본다」도 여기에 기댄다"
|
|
},
|
|
{
|
|
"id": "SSOT-10-idle-guest-releases-cpu",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#10-guest가-idle이면-물리-cpu는-어떻게-되는가"
|
|
],
|
|
"summary": "Guest 가 idle 이면 vCPU thread 가 block/sleep 되고 Host 는 그 CPU 시간을 다른 workload 에 쓴다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "§8.1 의 HLT exiting 이 그대로 이어지는 결론이다. 원인 절과 결과 절을 다른 기록으로 나눌 이유가 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-11-four-vcpu-is-not-four-cpus",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#11-vm의-4-vcpu는-정확히-무엇을-의미하는가"
|
|
],
|
|
"summary": "4 vCPU 는 실행 컨텍스트 4개이지 물리 CPU 4개의 영구 예약이 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "§4·§5 에서 이미 나온 사실을 한 문장으로 못박은 자리다. 없애고 개념의 한 절로 넣어도 이해가 달라지지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-12-overcommit-and-contention",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#12-cpu-contention과-overcommit"
|
|
],
|
|
"summary": "vCPU 총합이 logical CPU 를 넘으면 vCPU thread 끼리 Host CPU 시간을 두고 경쟁한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "§24.2·§24.3 이 같은 내용을 문제 목록 형태로 다시 적는다. 한 기록 안에서 한 번 설명하고 표로 잇는 편이 맞다"
|
|
},
|
|
{
|
|
"id": "SSOT-13-steal-time-is-a-clue",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#13-steal-time"
|
|
],
|
|
"summary": "steal time 은 Host contention·overcommit 을 의심할 단서이지 단독으로 원인을 확정하는 지표가 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "지표 하나의 읽는 법이다. §24.4 와 §25 의 표 행이 같은 것을 말하므로 개념 안에서 한 번 설명한다"
|
|
},
|
|
{
|
|
"id": "SSOT-14-observation-commands",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#14-실제-linux에서-확인할-수-있는-것"
|
|
],
|
|
"summary": "이 계층을 눈으로 확인하는 Linux 명령 아홉 — /proc/cpuinfo · lsmod · /dev/kvm · virsh list · ps -T · psr · steal time · perf kvm",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "Reference 가 되려면 적용 조건과 예외를 댈 수 있어야 하는데 이 저장소에는 이 명령들을 실제로 돌린 출력이 아직 없다. 지금은 개념의 「어디를 보면 이 경로가 보이는가」 절이다"
|
|
},
|
|
{
|
|
"id": "SSOT-15-layered-diagnosis-view",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#15-cpu-가상화-관점에서-장애를-보는-방법"
|
|
],
|
|
"summary": "VM 안이 느릴 때 Guest · Host/QEMU · KVM · Hardware 로 계층을 나눠 본다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "이 관점으로 원인을 실제로 가른 사건이 이 저장소에 아직 하나도 없다. 근거 Case 가 없는 규칙은 Case 결론을 선언문으로 바꾼 것도 못 되므로 개념의 한 절로 둔다"
|
|
},
|
|
{
|
|
"id": "SSOT-16-kvm-layer-under-the-keycloak-experiment",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#16-현재-keycloak-k3s-실험과의-관계"
|
|
],
|
|
"summary": "Keycloak 멀티 노드 실험 구조 아래에 KVM 계층이 하나 더 있고, 그래서 원인을 분리해야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "§19 가 「Keycloak/K3s 실험 결과와 Host 자원 문제를 구분하는 기준」을 CONCEPT 범위에 넣었다. 이 개념이 왜 필요한지를 말하는 절이라 개념 안에 있어야 한다"
|
|
},
|
|
{
|
|
"id": "SSOT-18-bare-metal-vs-vm-cpu-path",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#18-bare-metal-k3s와-vm-기반-k3s의-차이"
|
|
],
|
|
"summary": "bare-metal K3s 는 Host scheduler 직행이고 VM 기반 K3s 는 vCPU · QEMU thread · KVM/VMX 계층이 더 붙는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "같은 경로를 계층 유무로 견준 것이다. 경로를 설명한 글 안에서만 대비가 성립한다"
|
|
},
|
|
{
|
|
"id": "SSOT-24-cpu-failure-modes",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#24-cpu-가상화-계층에서-발생할-수-있는-문제"
|
|
],
|
|
"summary": "Guest saturation · overcommit · contention · steal time · scheduling latency · vCPU 과다 할당 · 잘못된 pinning · 과도한 VM Exit · Host saturation — 구조적으로 가능한 문제와 각각의 확인 지표",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "§28 이 CONCEPT 에서 확정할 범위를 「어떤 문제가 발생할 수 있는가 · 어떤 지표로 의심하나 · 어느 계층인가」로 그었고 이 절이 정확히 그것이다. 아홉 모드가 같은 물음에 같은 결론으로 답하므로 개념 안의 하위 절이지 아홉 편이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-24-8-cgroup-throttling-boundary",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#24-cpu-가상화-계층에서-발생할-수-있는-문제"
|
|
],
|
|
"summary": "K3s cgroup CPU limit 으로 인한 throttling 은 KVM 문제가 아니지만 같은 application latency 로 보여 진단 경계에 포함한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "이 개념이 다루는 경계의 바깥을 짚는 절이라 개념이 스스로 안고 있어야 한다. 따로 빼면 「CPU 가상화 문제가 아닌 것」만 담은 기록이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-24-11-numa-locality",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#24-cpu-가상화-계층에서-발생할-수-있는-문제"
|
|
],
|
|
"summary": "NUMA node 사이의 remote memory access 가 성능에 영향을 줄 수 있다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "문서가 스스로 「상세한 memory placement 와 NUMA tuning 은 메모리 가상화 CONCEPT 에서 다룬다」고 범위를 넘겼다. 이 프로젝트가 메모리 가상화를 담는 SSOT 를 가지면 그때 다시 판정한다. 지금 CPU 개념에 넣으면 그 글에서 이어 쓸 수 없는 문단이 하나 남는다"
|
|
},
|
|
{
|
|
"id": "SSOT-25-layer-diagnosis-table",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#25-cpu-문제를-계층별로-구분하는-진단표"
|
|
],
|
|
"summary": "문제 11개를 주된 계층 · 대표 현상 · 우선 확인할 것으로 늘어놓은 진단표",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "표 자체가 §24 를 한 화면으로 접은 것이다. 문서도 「원인을 확정하는 것이 아니라 조사 범위를 줄이는 것」이라고 적었다. 개념 본문의 표로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-26-layers-to-separate-when-reading-the-experiment",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#26-현재-keycloak-실험에서-cpu-문제를-오판하지-않기-위한-기준"
|
|
],
|
|
"summary": "Application/Auth · Storage · K3s · Guest · Virtualization · Host 여섯 경계를 분리해서 읽는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "§15 의 계층 분리를 Keycloak 실험에 맞춰 다시 그린 것이다. 개념이 설명한 경로 위에 실험 요소를 얹은 그림이라 같은 글에 있어야 읽힌다"
|
|
},
|
|
{
|
|
"id": "SSOT-28-what-the-concept-confirms",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#28-concept-->-open-question-->-case-적용-기준"
|
|
],
|
|
"summary": "CONCEPT 은 구조적 가능성과 지표와 계층까지만 확정하고, 현재 서버에서 실제로 발생하는지는 확정하지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "이 개념이 무엇을 사실로 말하고 무엇을 말하지 않는지의 경계다. 그 경계를 다른 기록에 두면 이 글을 읽는 사람이 어디까지 믿어야 하는지 모른다"
|
|
},
|
|
{
|
|
"id": "SSOT-17-26-22-separate-concurrency-and-load-tests",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#17-동시성-테스트와-부하-테스트를-분리해야-한다",
|
|
"final/document.md#26-현재-keycloak-실험에서-cpu-문제를-오판하지-않기-위한-기준",
|
|
"final/document.md#22-현재-단계의-핵심-claim"
|
|
],
|
|
"summary": "refresh 경쟁은 CPU 여유 상태에서 concurrency 실험으로 먼저 보고, 자원 포화는 별도의 load/stress 실험으로 본다",
|
|
"disposition": "NEEDS_DECISION",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "문서는 「분리해서 수행하는 것이 원인 분석에 유리하다」까지다. 프로젝트가 실제로 그렇게 하기로 정한 기록도, 감수한 비용(실험이 두 벌이 되고 두 결과를 잇는 해석이 따로 필요하다)도 적혀 있지 않다. 권고를 Decision 으로 올리면 「기술이 존재한다」를 근거로 바꾸는 것과 같은 실수가 된다. 실험 계획이 정해지면 다시 판정한다"
|
|
},
|
|
{
|
|
"id": "SSOT-22-claims-1-11",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#22-현재-단계의-핵심-claim"
|
|
],
|
|
"summary": "Claim 1~11 — virsh 의 역할부터 idle vCPU 의 CPU 반환까지, 메커니즘 서술을 한 문장씩으로 줄인 것",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "§3~§11 의 서술을 선언문으로 바꾼 것이다. 선언문만 모은 기록은 근거를 가리키지 못한다. 개념 본문이 같은 것을 근거와 함께 이미 담는다"
|
|
},
|
|
{
|
|
"id": "SSOT-22-claims-12-13",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#22-현재-단계의-핵심-claim"
|
|
],
|
|
"summary": "Claim 12·13 — contention/overcommit 이 Guest 성능에 영향을 주며, VM 기반 실험 환경의 CPU contention 이 Keycloak 실험 결과를 왜곡할 수 있다",
|
|
"disposition": "NEEDS_EVIDENCE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "다른 Claim 과 달리 이 둘은 「이 환경에서 실제로 그러한가」를 말한다. 이 테스트 Host 에서 잰 값이 없어 개념 본문에서도 가능성으로만 쓰고, 측정하면 Case 후보로 다시 판정한다"
|
|
},
|
|
{
|
|
"id": "SSOT-20-oq-1-vcpu-contention-under-two-vm-load",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question"
|
|
],
|
|
"summary": "OQ-1. 현재 테스트 Host 에서 VM 두 대에 부하를 주면 vCPU contention 이 실제로 발생하는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:vcpu-contention-under-two-vm-load",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. 종료 기준이 없다던 앞의 판정은 §20 이 적은 확인 대상 다섯을 종료 기준으로 옮겨 적어 해소했다. OQ-7 과 한 글감으로 합칠지는 값을 잰 뒤에야 아는 것이라 지금은 각각 올리고 relations 로 잇는다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-20-oq-2-virtualization-layer-saturation-during-refresh",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question"
|
|
],
|
|
"summary": "OQ-2. Keycloak 동시 refresh 실험 중 CPU 가상화 계층이 결과에 영향을 줄 정도로 포화되는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:virtualization-layer-saturation-during-refresh",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. 아는 것과 모르는 것은 실험 전에도 갈린다 — §16·§26 이 서술한 계층 구조와 §20 이 적은 관찰 대상 다섯이 known 이고, 이 Host 에서의 값이 unknown 이다. 종료 기준은 §20 이 스스로 밝힌 목적(refresh 경쟁과 Host resource contention 의 분리)에서 끌어 적었다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-20-oq-3-idle-vcpu-thread-appearance",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question"
|
|
],
|
|
"summary": "OQ-3. Guest 가 idle 일 때 vCPU thread 는 실제 테스트 환경에서 어떻게 보이는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:idle-vcpu-thread-appearance",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. 결과가 §10 의 서술과 같으면 개념으로 흡수되고 다르면 Case 가 된다던 앞의 판정은 버리는 이유가 아니라 그대로 종료 기준이다 — decision-criterion 에 그 갈림을 적었다. 확인 명령도 §20 이 이미 두 줄로 적어 두었다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-20-oq-4-vm-exit-distribution-by-workload",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question"
|
|
],
|
|
"summary": "OQ-4. 실제 workload 에서 어떤 VM Exit 이 주로 발생하는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:vm-exit-distribution-by-workload",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. 도구가 이 환경에서 되는지부터 확인해야 한다는 앞의 판정은 버리는 이유가 아니라 next-verification 의 첫 단계다 — 안 되는 것으로 확인되는 것도 이 질문을 닫는 결과다. OQ-11 과는 종료 기준이 달라 합치지 않고 relations 로 잇는다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-20-oq-5-vcpu-thread-migration-without-pinning",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question"
|
|
],
|
|
"summary": "OQ-5. pinning 을 하지 않은 상태에서 vCPU thread 는 Host logical CPU 사이를 실제로 이동하는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:vcpu-thread-migration-without-pinning",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. 관찰 하나짜리 물음이지만 답에 따라 OQ-10 의 pinning 실험을 할 이유가 있는지가 갈린다 — 설계가 답에 달려 있다는 조건을 만족한다. 독립 Question 으로 남을지 OQ-10 의 관측이 될지를 재 봐야 안다던 앞의 판정은 decision-criterion 에 그대로 옮겼다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-20-oq-6-is-production-on-a-hypervisor",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#20-이-concept에서-파생되는-open-question"
|
|
],
|
|
"summary": "OQ-6. 현재 운영 서버는 CPU 가상화 계층의 영향을 받는 구조인가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:is-production-on-a-hypervisor",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. 확인 전에는 아는 것과 모르는 것을 나눠 적을 수 없다던 앞의 판정을 뒤집었다 — §18 과 §20 이 그린 두 경로가 known 이고 운영 서버가 어느 쪽인지가 unknown 이다. 답에 따라 §25 의 어느 행을 운영 진단에 쓰는지가 갈리므로 설계가 답에 달린 Question 의 조건을 만족한다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-27-oq-7-steal-time-increase-under-two-cpu-bound-vms",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question"
|
|
],
|
|
"summary": "OQ-7. VM 두 대를 동시에 CPU-bound 로 만들면 Guest steal time 은 실제로 얼마나 증가하는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:steal-time-increase-under-two-cpu-bound-vms",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. OQ-1 의 수치판이라 재고 나면 둘이 한 Question 인지 갈린다던 앞의 판정은 그대로다. 다만 그 판정은 값을 잰 뒤의 일이고, 지금 둘을 미리 한 글감으로 합치면 어느 쪽 종료 기준으로 닫혔는지가 남지 않는다. 각각 올리고 relations 로 잇는다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-27-oq-8-throughput-vs-vcpu-count",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question"
|
|
],
|
|
"summary": "OQ-8. vCPU 수를 늘릴수록 현재 테스트 Host 에서 Keycloak 처리량도 계속 증가하는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:throughput-vs-vcpu-count",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. 어디서 꺾이면 「과다 할당」으로 부를지 기준이 없다던 앞의 판정은 §24.6 에서 기준을 끌어 적어 해소했다 — 처리량이 더 오르지 않는 지점이 있으면 그것이 이 workload 의 상한이고, 세 구성이 같은 방향으로 오르면 상한에 닿지 않은 것이다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-27-oq-9-throttling-vs-contention",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question"
|
|
],
|
|
"summary": "OQ-9. K3s CPU limit 으로 생긴 throttling 과 Host vCPU contention 을 지표로 구분할 수 있는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:throttling-vs-contention",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. 재현하기 전에는 구분이 되는지조차 모른다던 앞의 판정이 곧 이 종류의 전제다. Case 는 재현한 뒤에 쓰고, 그 전까지 무엇을 어떻게 재현해 무엇을 견줄지를 적어 두는 자리가 Question 이다. 견줄 지표는 §25 가 이미 두 행으로 갈라 적어 두었다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-27-oq-10-pinning-before-and-after",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question"
|
|
],
|
|
"summary": "OQ-10. CPU pinning 전후로 Keycloak latency 와 vCPU scheduling 변동이 달라지는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:pinning-before-and-after",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. 측정 전에 Decision 으로 올리면 감수한 비용 없이 선택을 만들게 된다던 앞의 판정은 그대로 유효하다 — 그래서 Decision 이 아니라 Question 으로 올린다. 설계가 답에 달려 있고 다음 검증과 종료 기준이 있는 것이 이 종류의 조건이고 셋 다 갖췄다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-27-oq-11-exit-distribution-for-keycloak-workload",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question"
|
|
],
|
|
"summary": "OQ-11. Keycloak workload 의 VM Exit 분포는 idle · CPU-bound · I/O-bound 와 어떻게 다른가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:exit-distribution-for-keycloak-workload",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. OQ-4 와 같은 측정에 묶이는 것과 한 글감인 것은 다르다. OQ-4 는 workload 다섯의 분포를 견주는 물음이고 이것은 Keycloak 구간의 분포가 latency 와 같이 움직이는지를 묻는다 — 종료 기준이 달라 각각 올리고 relations 로 잇는다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-27-oq-12-host-numa-topology",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#27-문제-영역에서-파생되는-추가-open-question"
|
|
],
|
|
"summary": "OQ-12. 현재 Host 의 NUMA topology 가 VM 성능을 고려해야 할 정도의 구조인가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:host-numa-topology",
|
|
"reason": "NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정했다. Question 은 답이 아직 없는 판단을 적는 종류라 이 Host 에서 잰 값이 하나도 없다는 것이 글감으로 올리지 못할 이유가 아니다 — 무엇을 알고 무엇을 모르는지, 무엇을 재면 닫히는지를 적어 두는 자리가 Question 이고, 값이 나오면 그때 Case 가 된다. 문서가 종료 기준을 스스로 적은 유일한 OQ 라 Question 으로 올리기에 조건이 가장 갖춰진 후보다. Host 를 확인하기 전에는 답이 없다는 것이 앞의 NEEDS_EVIDENCE 사유였는데, 답이 없는 상태를 그대로 적는 것이 Question 이다. 사용자가 §20·§27 의 OQ 열둘을 OQ 하나에 글감 하나로 올리기로 정했다."
|
|
},
|
|
{
|
|
"id": "SSOT-21-record-system-flow",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#21-open-question에서-case가-만들어지는-흐름"
|
|
],
|
|
"summary": "SSOT → CONCEPT → OPEN QUESTION → CASE 로 이어지고 CASE 에서 다시 OPEN QUESTION 이 나온다는 기록 체계",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "CPU 가상화가 아니라 이 저장소가 기록을 잇는 방식을 설명한 절이다. 분석에는 남아야 하고 공개 기록으로는 읽을 사람이 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-23-next-verification-plan",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#23-다음-단계"
|
|
],
|
|
"summary": "VMX 확인부터 VM Exit 관찰까지 테스트 Host 에서 돌릴 검증 순서와, 그 뒤 네트워크 가상화로 넘어간다는 계획",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "다음에 무엇을 잴지의 진행 계획이다. 계획은 실행되면 Case 가 되고 그 전까지는 분석에 남는다"
|
|
},
|
|
{
|
|
"id": "SSOT-29-gva-gpa-hpa-two-translations",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#29-이-문서에서-먼저-고정할-전체-구조",
|
|
"final/document.md#79-전체-memory-virtualization-실행-경로",
|
|
"final/document.md#87-최종-기준-그림"
|
|
],
|
|
"summary": "GVA → GPA → HPA. Guest 가 물리 주소라고 믿는 GPA 와 실제 Host RAM 의 HPA 사이에 변환이 한 번 더 있다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "§29 가 이 부에서 먼저 고정할 구조로 놓고 §87 이 같은 그림을 최종 기준으로 다시 그렸다. 이 부의 나머지 다섯 개념이 전부 이 경로 위의 자리라 이것을 빼면 어느 것도 혼자 서지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-30-32-virtual-memory-basics",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#30-일반-linux의-virtual-memory부터-시작한다",
|
|
"final/document.md#31-page와-physical-frame",
|
|
"final/document.md#32-virtual-address-=-page-+-offset"
|
|
],
|
|
"summary": "프로세스마다 독립된 virtual address space 가 있고, 매핑 단위는 4 KiB page 와 physical frame 이며, virtual address 는 page 와 offset 으로 갈린다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "KVM 고유 기능이 아니라 일반 Linux 의 주소 변환이다. 개념의 첫 절로 들어가면 되고 따로 읽을 이유가 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-33-guest-page-table",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#33-guest-page-table"
|
|
],
|
|
"summary": "Guest Kernel 이 프로세스마다 virtual page → guest physical frame 매핑을 관리하지만 매 접근마다 커널 코드가 table 을 뒤지는 것은 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "변환 경로의 한 단계다. 이 단계만 떼어 읽을 사람이 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-34-mmu-performs-the-translation",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#34-mmu-실제-주소-변환을-수행하는-cpu-하드웨어"
|
|
],
|
|
"summary": "주소 변환을 실제로 수행하는 주체는 CPU 의 MMU 이고 Guest Kernel 은 table 을 구성·관리한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "「누가 만들고 누가 쓰는가」는 개념 본문의 한 절이다"
|
|
},
|
|
{
|
|
"id": "SSOT-35-tlb-caches-translations",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#35-tlb-주소-변환-결과의-cpu-cache"
|
|
],
|
|
"summary": "TLB 는 최근 translation 결과를 CPU 가 캐시해 둔 것이라 hit 이면 page-table walk 를 건너뛴다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "변환 경로의 성능 특성이다. §43 의 nested walk 비용과 §51 의 TLB coverage 가 이것을 전제한다"
|
|
},
|
|
{
|
|
"id": "SSOT-35-tlb-miss-is-not-page-fault",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#35-tlb-주소-변환-결과의-cpu-cache"
|
|
],
|
|
"summary": "TLB Miss 는 table 을 조회해 정상 매핑을 찾는 사건이고 Page Fault 는 현재 접근을 완료할 수 없는 사건이라 둘은 다르다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:page-fault-layers-in-a-vm",
|
|
"reason": "「어느 계층의 사건인가」를 가르는 첫 구분이라 fault 계층 개념의 도입부에 들어간다. §82 의 CLAIM-MEM-07 이 같은 것을 한 문장으로 적었다"
|
|
},
|
|
{
|
|
"id": "SSOT-36-bare-metal-vs-vm-translation",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#36-bare-metal과-vm의-차이"
|
|
],
|
|
"summary": "bare-metal 은 page table 한 번으로 끝나지만 VM 에서는 Guest 가 얻은 physical address 가 Host physical address 가 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "EPT 가 왜 필요한지를 세우는 대조다. 개념 안에서 EPT 절 바로 앞에 온다"
|
|
},
|
|
{
|
|
"id": "SSOT-37-ept-second-stage-translation",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#37-ept-extended-page-tables"
|
|
],
|
|
"summary": "EPT 는 GPA → HPA 를 CPU 하드웨어가 수행하는 second-level address translation 이고 Guest Page Table 과 같은 table 이 아니다. AMD 에는 NPT 계열이 대응한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "주소 변환 경로의 두 번째 단계다. 이 개념의 핵심 절이지 독립 글이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-38-ept-isolates-guest-physical-space",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#38-왜-ept가-필요한가"
|
|
],
|
|
"summary": "VM 둘이 같은 GPA 를 써도 EPT 가 서로 다른 HPA 로 연결해 Guest physical address space 를 격리한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "EPT 절의 근거 예시다. 표 한 줄이나 예 하나로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-39-shadow-page-table-vs-ept",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#39-shadow-page-table과-ept의-의미"
|
|
],
|
|
"summary": "하드웨어 second-level translation 이 없던 방식은 hypervisor 가 shadow mapping 을 유지해야 해 관리 비용이 컸고, EPT/NPT 는 그 두 단계를 하드웨어로 옮겼다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "EPT 가 무엇을 대신하는지를 설명하는 배경이다. 개념 안의 한 절이면 충분하다"
|
|
},
|
|
{
|
|
"id": "SSOT-40-qemu-backs-guest-ram-in-host-userspace",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#40-qemu는-guest-ram을-어떻게-준비하는가"
|
|
],
|
|
"summary": "QEMU 는 Host userspace process 라 Guest RAM 도 자신의 Host Virtual Address Space 에 마련하고, RAM 하드웨어를 직접 제어하는 것이 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "관리 경로의 시작이다. OQ-3 이 재려는 QEMU RSS 가 무엇인지도 이 절이 정한다"
|
|
},
|
|
{
|
|
"id": "SSOT-41-kvm-set-user-memory-region",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#41-kvm_set_user_memory_region",
|
|
"final/document.md#80-전체-memory-virtualization-관리-경로"
|
|
],
|
|
"summary": "QEMU 가 마련한 HVA 범위와 Guest GPA 범위의 대응을 `KVM_SET_USER_MEMORY_REGION` 으로 KVM 에 등록하고, 런타임 변환은 CPU 가 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "관리 경로(§80)와 실행 경로(§79)를 가르는 자리다. 개념이 그 둘을 같이 그린다"
|
|
},
|
|
{
|
|
"id": "SSOT-42-configured-memory-is-not-resident-memory",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#42-configured-memory와-실제-physical-ram-사용량은-같지-않을-수-있다"
|
|
],
|
|
"summary": "configured memory · Guest 가 지금 쓰는 memory · Host 에서 지금 resident 한 physical memory 셋은 같지 않을 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "OQ-2 와 OQ-3 이 이 절을 재려고 만들어졌다. 개념이 셋의 구분을 세우고 두 Question 이 이 Host 의 값을 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-43-nested-page-table-walk-cost",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#43-guest-page-table-자체도-메모리에-있다"
|
|
],
|
|
"summary": "Guest Page Table 자체도 Guest physical memory 에 있는 자료구조라 entry 를 읽는 데도 EPT 변환이 필요하고, 그래서 nested walk 에는 비용이 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "왜 TLB 가 중요한지를 잇는 절이다. 개념 안에서 TLB 절과 붙어 있어야 읽힌다"
|
|
},
|
|
{
|
|
"id": "SSOT-44-normal-memory-access-does-not-vm-exit",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#44-정상-memory-access는-매번-vm-exit하지-않는다",
|
|
"final/document.md#79-전체-memory-virtualization-실행-경로"
|
|
],
|
|
"summary": "정상 매핑이 있으면 CPU MMU 가 직접 변환하므로 Guest RAM 접근마다 VM Exit 이나 QEMU userspace 왕복이 생기지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "CPU 부에서 VM Exit 을 읽은 사람이 가장 쉽게 틀리는 자리라 §44 가 「잘못된 이해」를 먼저 그렸다. 개념의 결론 절이다"
|
|
},
|
|
{
|
|
"id": "SSOT-45-guest-page-fault",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#45-guest-page-fault",
|
|
"final/document.md#48-guest-page-fault와-ept-violation-비교",
|
|
"final/document.md#49-host-page-fault도-별도로-존재한다"
|
|
],
|
|
"summary": "Guest Page Fault 는 GVA → GPA 단계에서, EPT Violation 은 GPA → HPA 단계에서, Host Page Fault 는 QEMU backing 의 Host virtual memory 에서 각각 따로 일어난다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:page-fault-layers-in-a-vm",
|
|
"reason": "§48 의 비교표와 §49 의 세 갈래는 이 부에서 가장 여러 번 다시 쓰이는 구분이다. §86 의 진단 분류가 그 위에 서 있고 OQ-13·OQ-14 가 각각 Guest 쪽과 Host 쪽을 재라고 말한다. 정상 경로를 그리는 글과 갈라 둔 이유는 물음이 다르기 때문이다 — 그쪽은 정상일 때 무엇이 일어나는가를, 이쪽은 완료되지 않을 때 어느 계층이 받는가를 말한다"
|
|
},
|
|
{
|
|
"id": "SSOT-46-page-fault-causes",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#46-page-fault의-대표적인-원인"
|
|
],
|
|
"summary": "demand paging · swap-in · permission fault · copy-on-write · invalid access 다섯이 Page Fault 의 대표 원인이고, Page Fault 는 Segmentation Fault 와 다르다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:page-fault-layers-in-a-vm",
|
|
"reason": "Guest Page Fault 절 아래의 하위 절이다. 다섯을 각각 글로 만들면 표 한 줄이 될 것이 다섯 편이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-47-ept-violation",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#47-ept-violation"
|
|
],
|
|
"summary": "Guest 쪽 변환은 성공했는데 그 GPA 의 second-stage 접근을 현재 EPT 조건으로 완료할 수 없으면 EPT Violation 이고 VM Exit 을 거쳐 KVM 이 받는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:page-fault-layers-in-a-vm",
|
|
"reason": "같은 개념의 두 번째 계층이다. §48 의 비교표가 이 절과 §45 를 나란히 놓는다"
|
|
},
|
|
{
|
|
"id": "SSOT-48-guest-page-fault-vs-ept-violation",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#48-guest-page-fault와-ept-violation-비교"
|
|
],
|
|
"summary": "문제 위치 · 관련 table · 기본 관점 · 주요 처리 계층 · 앱 오류 여부 다섯 항목으로 Guest Page Fault 와 EPT Violation 을 갈라 놓은 비교표",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:page-fault-layers-in-a-vm",
|
|
"reason": "개념 본문의 표다. 표 하나를 기록 하나로 만들지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-49-host-page-fault",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#49-host-page-fault도-별도로-존재한다"
|
|
],
|
|
"summary": "QEMU 도 Host 의 일반 userspace process 라 demand allocation·reclaim·swap 때문에 Host 쪽에서도 page fault 가 난다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:page-fault-layers-in-a-vm",
|
|
"reason": "세 계층 가운데 셋째다. OQ-14 가 이 계층의 major fault 를 재려고 만들어졌다"
|
|
},
|
|
{
|
|
"id": "SSOT-50-51-huge-page-and-tlb-coverage",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#50-huge-page가-필요한-이유",
|
|
"final/document.md#51-huge-page와-tlb-coverage",
|
|
"final/document.md#52-vm에서-huge-page를-볼-때-주의할-점"
|
|
],
|
|
"summary": "page 가 커지면 같은 범위를 더 적은 매핑으로 덮어 TLB pressure 와 page-table walk 부담이 줄 수 있고, VM 에서는 Guest 단계·Host backing·EPT 매핑 셋을 따로 봐야 한다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:huge-pages-in-a-vm",
|
|
"reason": "OQ-4 와 OQ-5 가 이 개념을 필요로 한다. THP 정책 하나를 읽어도 그것이 Guest 쪽인지 Host backing 쪽인지 모르면 값이 뜻을 갖지 않는다. §51 은 예시 숫자를 특정 CPU 의 실제 TLB 용량으로 읽지 말라는 단서까지 달았다"
|
|
},
|
|
{
|
|
"id": "SSOT-52-huge-page-has-two-stages-in-a-vm",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#52-vm에서-huge-page를-볼-때-주의할-점"
|
|
],
|
|
"summary": "VM 에는 변환 단계가 둘이라 「Huge Page 를 쓴다」는 말만으로는 Guest page table 쪽인지 Host backing 쪽인지 EPT 매핑 쪽인지 정해지지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:huge-pages-in-a-vm",
|
|
"reason": "개념의 핵심 절이다. OQ-5 가 이 구분을 그대로 확인 항목으로 쓴다"
|
|
},
|
|
{
|
|
"id": "SSOT-53-thp",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#53-thp-transparent-huge-pages"
|
|
],
|
|
"summary": "THP 는 조건이 맞는 영역에 Linux 가 Huge Page 를 투명하게 활용하려는 기능이고 정책은 `/sys/kernel/mm/transparent_hugepage/enabled` 에서 읽는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:huge-pages-in-a-vm",
|
|
"reason": "개념 안의 한 절이다. 정책 값 자체는 OQ-4 가 이 Host 에서 읽는다"
|
|
},
|
|
{
|
|
"id": "SSOT-54-thp-tradeoff",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#54-thp의-trade-off"
|
|
],
|
|
"summary": "Huge Page 에는 큰 contiguous 영역이 필요해 fragmentation 이 심하면 compaction 이 끼어들 수 있고, 그래서 THP 가 항상 성능을 높인다고 단정할 수 없다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:huge-pages-in-a-vm",
|
|
"reason": "THP 절의 뒷면이다. 이익만 적고 비용을 빼면 개념이 반쪽이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-55-hugetlb",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#55-hugetlb"
|
|
],
|
|
"summary": "HugeTLB 는 관리자가 미리 확보한 명시적 Huge Page pool 을 쓰는 방식이라 예측 가능성이 오르는 대신 일반 allocation 의 유연성이 준다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:huge-pages-in-a-vm",
|
|
"reason": "THP 와 짝을 이루는 절이다. §56 의 비교표가 둘을 나란히 놓는다"
|
|
},
|
|
{
|
|
"id": "SSOT-56-thp-vs-hugetlb",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#56-thp와-hugetlb-비교"
|
|
],
|
|
"summary": "관리 주체 · 애플리케이션 개입 · 유연성 · 사전 예약 · compaction 영향 · VM RAM backing 여섯 항목의 THP↔HugeTLB 비교표와 `AnonHugePages` ≠ `HugePages_Total`",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:huge-pages-in-a-vm",
|
|
"reason": "개념 본문의 표다. OQ-4 가 그 표의 항목을 이 Host 에서 읽는다"
|
|
},
|
|
{
|
|
"id": "SSOT-57-memory-overcommit",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#57-memory-overcommit",
|
|
"final/document.md#58-cpu-overcommit과-memory-overcommit의-차이",
|
|
"final/document.md#59-host-memory-pressure와-reclaim"
|
|
],
|
|
"summary": "configured 총량이 Host RAM 을 넘는 구성이 성립하는 이유는 configured capacity 와 현재 working set 이 같지 않기 때문이고, RAM 이 모자라면 CPU 처럼 시간을 나누는 것이 아니라 reclaim·swap·ballooning·OOM 이 개입한다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:memory-pressure-reclaim-swap-oom",
|
|
"reason": "OQ-6·OQ-7·OQ-14 셋이 이 개념 위에서만 읽힌다. CPU overcommit 과 성격이 다르다는 §58 이 이 부 전체에서 가장 오해되기 쉬운 자리라 앞에서 한 번 세워야 한다"
|
|
},
|
|
{
|
|
"id": "SSOT-58-cpu-overcommit-vs-memory-overcommit",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#58-cpu-overcommit과-memory-overcommit의-차이"
|
|
],
|
|
"summary": "CPU 가 모자라면 scheduler 가 실행 시간을 나누지만 RAM 이 모자라면 「지금 존재해야 하는 page 를 어디에 둘 것인가」가 남는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:memory-pressure-reclaim-swap-oom",
|
|
"reason": "개념의 도입 대조다. 한 절이면 되고 독립 글로 읽을 것이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-59-host-reclaim",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#59-host-memory-pressure와-reclaim"
|
|
],
|
|
"summary": "Host 수요가 available physical memory 에 가까워지면 reclaim 이 돌고, file-backed clean page 는 버렸다 다시 읽으면 되지만 anonymous page 는 swap 같은 backing 이 필요하다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:memory-pressure-reclaim-swap-oom",
|
|
"reason": "압박 경로의 첫 단계다. 개념 본문의 한 절이다"
|
|
},
|
|
{
|
|
"id": "SSOT-60-host-swap-turns-guest-ram-access-into-io",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#60-host-swap이-vm에-미치는-영향"
|
|
],
|
|
"summary": "Guest 는 RAM 에 접근했다고 생각하는데 Host backing page 가 swap-out 되어 있으면 그 접근이 Host page fault 와 swap-in I/O 대기로 바뀐다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:memory-pressure-reclaim-swap-oom",
|
|
"reason": "이 개념이 존재하는 이유가 되는 절이다. OQ-7 이 이것을 실험으로 확인한다"
|
|
},
|
|
{
|
|
"id": "SSOT-61-guest-swap-is-not-host-swap",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#61-guest-swap과-host-swap"
|
|
],
|
|
"summary": "Guest Swap 은 Guest kernel 이 `/dev/vda` 로 내려보내는 것이고 Host Swap 은 QEMU backing 이 Host kernel 에 눌리는 것이라 서로 다른 계층이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:memory-pressure-reclaim-swap-oom",
|
|
"reason": "계층 구분이라 개념 안에 있어야 한다. OQ-6 이 둘을 같은 시각에 재라고 요구하는 근거다"
|
|
},
|
|
{
|
|
"id": "SSOT-62-memory-pressure-reaches-storage",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#62-memory-pressure와-storage-contention의-연결",
|
|
"final/document.md#81-cpu-network-storage-memory-연결"
|
|
],
|
|
"summary": "Guest swap I/O · Host swap I/O · DB I/O · filesystem writeback 이 한 물리 장치로 몰릴 수 있어 memory pressure 가 storage contention 을 거쳐 application latency 까지 온다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:memory-pressure-reclaim-swap-oom",
|
|
"reason": "메모리를 CPU 사용률로 판단하지 않게 하는 절이다. OQ-14 가 이 연결을 같은 시간축에서 확인한다"
|
|
},
|
|
{
|
|
"id": "SSOT-63-swap-used-alone-is-not-a-verdict",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#63-swap-used만-보고-장애를-판단하면-안-된다"
|
|
],
|
|
"summary": "Swap Used 값 하나로 memory pressure 를 단정할 수 없고 swap-in/out 지속 여부 · reclaim pressure · major fault · storage latency 를 Guest 와 Host 에서 함께 봐야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:memory-symptom-needs-layer-separation",
|
|
"reason": "「지표 하나로 계층을 단정하지 않는다」는 규칙의 대표 사례라 그 Reference 의 한 행으로 들어간다. 규칙을 두 벌 쓰면 어느 쪽을 따르는지가 갈린다"
|
|
},
|
|
{
|
|
"id": "SSOT-71-oom-mechanism",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#71-oom"
|
|
],
|
|
"summary": "reclaim 으로도 필요한 memory 를 확보하지 못하면 OOM 이 되고 OOM Killer 가 프로세스를 골라 종료할 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:memory-pressure-reclaim-swap-oom",
|
|
"reason": "압박 경로의 마지막 단계다. 개념 안에서 §72 와 붙어 있어야 한다"
|
|
},
|
|
{
|
|
"id": "SSOT-72-guest-oom-vs-host-oom",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#72-guest-oom과-host-oom"
|
|
],
|
|
"summary": "Guest OOM 은 Guest 프로세스를 죽이지만 Host OOM 에서 QEMU 가 victim 이 되면 VM 전체가 멈추고, cgroup limit 이 있으면 Host RAM 이 남아도 그 경계에서 OOM 이 난다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:memory-pressure-reclaim-swap-oom",
|
|
"reason": "영향 범위가 다르다는 것이 이 개념의 결론이다. OOM 의 발생 계층을 확인하라는 §86 의 갈래도 여기서 나온다"
|
|
},
|
|
{
|
|
"id": "SSOT-64-65-virtio-balloon",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#64-ballooning이-필요한-이유",
|
|
"final/document.md#65-virtio-balloon-구조"
|
|
],
|
|
"summary": "Host 는 Guest 안에서 어느 memory 가 중요한지 모르므로 무작정 swap-out 하는 대신 Guest kernel 과 협력해 회수한다. virtio-balloon 은 그 협력용 가상 장치이고 RAM 자체를 제공하는 장치가 아니다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:virtio-balloon-memory-reclaim",
|
|
"reason": "OQ-8 과 OQ-9 가 이 개념을 필요로 한다. balloon 은 Guest 안의 driver 와 QEMU 쪽 device 가 virtqueue 로 맞물린 별도 메커니즘이라 압박 경로 안의 한 절로 넣으면 inflate/deflate 의 방향조차 설명할 자리가 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-66-balloon-inflate",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#66-balloon-inflate"
|
|
],
|
|
"summary": "Host 가 balloon 을 inflate 하면 Guest 안의 balloon 이 커져 Guest usable memory 가 줄고 Host 가 회수할 수 있는 backing 이 는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:virtio-balloon-memory-reclaim",
|
|
"reason": "개념의 핵심 동작이다. §68 의 deflate 와 한 절 안에서 방향이 갈린다"
|
|
},
|
|
{
|
|
"id": "SSOT-67-what-balloon-page-return-means",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#67-balloon-page-반환의-의미"
|
|
],
|
|
"summary": "balloon driver 는 Guest page 를 확보하고 그 사실을 Host 에 알리는 것이며, Host 쪽 실제 release 동작은 QEMU/KVM 버전과 backing 종류·설정에 따라 달라질 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:virtio-balloon-memory-reclaim",
|
|
"reason": "단정하지 않은 자리를 그대로 옮겨야 하는 절이라 개념 본문에 둔다"
|
|
},
|
|
{
|
|
"id": "SSOT-68-balloon-deflate",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#68-balloon-deflate"
|
|
],
|
|
"summary": "balloon target 을 줄이면 balloon page 가 반환되어 Guest usable memory 가 는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:virtio-balloon-memory-reclaim",
|
|
"reason": "inflate 와 짝인 한 문장이다. 따로 읽을 것이 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-69-over-inflation-pressures-the-guest",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#69-ballooning을-과도하게-하면-guest가-압박을-받는다"
|
|
],
|
|
"summary": "working set 이 큰데 과도하게 inflate 하면 Guest reclaim → page cache 회수 → Guest swap → 심하면 Guest OOM 으로 이어져, Host RAM 을 확보하려던 조치가 Guest latency 를 올릴 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:virtio-balloon-memory-reclaim",
|
|
"reason": "개념의 감수 비용 절이다. OQ-9 가 이 경로를 실험 대상으로 삼는다"
|
|
},
|
|
{
|
|
"id": "SSOT-70-ballooning-is-not-memory-hotplug",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#70-ballooning과-memory-hotplug"
|
|
],
|
|
"summary": "ballooning 은 기존 capacity 안에서 usable memory 를 회수·반환하는 것이고 hotplug 는 capacity 자체를 더하는 것이라 다르다. `virtio-mem` 같은 다른 방식도 있어 동적 memory 관리를 ballooning 하나로 일반화하면 안 된다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:virtio-balloon-memory-reclaim",
|
|
"reason": "개념의 경계 절이다. 잘못 뭉치는 것을 막는 문장이라 본문에 있어야 한다"
|
|
},
|
|
{
|
|
"id": "SSOT-73-74-numa-local-and-remote",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#73-numa",
|
|
"final/document.md#74-local-memory와-remote-memory",
|
|
"final/document.md#75-vcpu와-numa의-연결"
|
|
],
|
|
"summary": "NUMA 에서는 어느 CPU 가 어느 RAM 을 읽느냐로 비용이 갈리고, Guest vCPU 는 Host 의 QEMU vCPU thread 라 그 thread 가 도는 node 와 backing page 가 있는 node 가 어긋나면 remote access 가 된다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:numa-locality-for-vcpu-and-memory",
|
|
"reason": "OQ-11 과 OQ-12 가 이 개념 위에 선다. CPU 부의 question:host-numa-topology 가 「상세한 memory placement 와 NUMA tuning 은 메모리 가상화 CONCEPT 에서 다룬다」고 범위를 넘긴 자리가 여기다"
|
|
},
|
|
{
|
|
"id": "SSOT-75-vcpu-placement-decides-which-node-reads-ram",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#75-vcpu와-numa의-연결"
|
|
],
|
|
"summary": "Guest 안에서는 단순한 memory load 인데 실제 하드웨어에서는 vCPU thread 가 도는 node 에 따라 interconnect 를 건널 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:numa-locality-for-vcpu-and-memory",
|
|
"reason": "개념의 핵심 연결이다. CPU 부의 vCPU thread 서술과 메모리 배치가 여기서 만난다"
|
|
},
|
|
{
|
|
"id": "SSOT-76-pinning-alone-does-not-finish-numa",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#76-vcpu-pinning만으로는-numa-최적화가-끝나지-않는다"
|
|
],
|
|
"summary": "vCPU 를 Node 0 에 pinning 해도 backing page 가 Node 1 에 몰려 있으면 remote access 가 늘 수 있어 vCPU placement 와 memory placement 를 함께 봐야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:numa-locality-for-vcpu-and-memory",
|
|
"reason": "개념의 결론 절이다. OQ-11 이 이 어긋남을 이 Host 에서 확인한다"
|
|
},
|
|
{
|
|
"id": "SSOT-77-guest-numa",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#77-guest-numa"
|
|
],
|
|
"summary": "큰 VM 에서는 Guest 에게 NUMA topology 자체를 노출하고 Guest node 와 Host node 가 합리적으로 대응되도록 구성할 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:numa-locality-for-vcpu-and-memory",
|
|
"reason": "개념 안의 한 절이다. 이 프로젝트의 VM 이 그런 크기인지는 SSOT 에 적혀 있지 않다"
|
|
},
|
|
{
|
|
"id": "SSOT-78-numa-observation-commands",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#78-numa는-실제-장비-topology부터-확인한다"
|
|
],
|
|
"summary": "`lscpu` · `numactl --hardware` · `numastat -p <QEMU_PID>` · `virsh vcpupin` · `virsh vcpuinfo` — NUMA topology 와 배치를 읽는 명령",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:numa-locality-for-vcpu-and-memory",
|
|
"reason": "명령 목록만으로는 규칙이 되지 않는다. 개념 본문에서 무엇을 읽는 명령인지와 함께 놓아야 뜻이 있고, 실제 값은 OQ-11·OQ-12 가 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-78-measure-topology-before-optimizing",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#78-numa는-실제-장비-topology부터-확인한다"
|
|
],
|
|
"summary": "Host 가 단일 NUMA node 면 cross-node 문제가 주요 이슈가 아닐 수 있으므로 topology 를 먼저 재고 NUMA 최적화 필요성을 판단한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:memory-symptom-needs-layer-separation",
|
|
"reason": "「재기 전에 단정하지 않는다」는 같은 규칙의 NUMA 판이라 그 Reference 의 적용 범위 한 줄로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-79-memory-execution-path",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#79-전체-memory-virtualization-실행-경로"
|
|
],
|
|
"summary": "TLB → Guest Page Table(#PF 가능) → GPA → VM Boundary → EPT(Violation 가능) → HPA → NUMA node 로 이어지는 실행 경로 한 장",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "개념이 그리는 경로 그대로다. 그림 한 장을 기록 하나로 만들지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-80-memory-management-path",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#80-전체-memory-virtualization-관리-경로"
|
|
],
|
|
"summary": "virsh → libvirt → QEMU(backing · balloon device · ioctl) → KVM(memory slot · EPT 매핑) → CPU 로 이어지는 관리 경로",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "실행 경로와 짝이라 같은 개념 안에서 나란히 놓아야 한다"
|
|
},
|
|
{
|
|
"id": "SSOT-81-memory-pressure-to-application-latency",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#81-cpu-network-storage-memory-연결"
|
|
],
|
|
"summary": "memory pressure → reclaim/swap → storage I/O → contention → application latency 로 이어지는 사슬과, vCPU pinning + memory placement 가 함께 NUMA locality 를 정한다는 연결",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:memory-pressure-reclaim-swap-oom",
|
|
"reason": "압박 개념의 결론 사슬이다. §62 와 같은 것을 한 번 더 그린 자리라 그 개념 안에 함께 둔다"
|
|
},
|
|
{
|
|
"id": "SSOT-81-four-resource-map",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#81-cpu-network-storage-memory-연결"
|
|
],
|
|
"summary": "CPU · Network · Storage · Memory 넷을 한 그림에 놓고 memory 가 그 전부를 받친다고 적은 지도",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "이 문서 네 부 전체의 지도이지 메모리만의 개념이 아니다. 제3부(네트워크)와 제4부(스토리지)가 같은 그림을 쓰므로 SSOT 에 두고 각 개념이 필요할 때 가리킨다"
|
|
},
|
|
{
|
|
"id": "SSOT-87-final-reference-picture",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#87-최종-기준-그림"
|
|
],
|
|
"summary": "Guest Userspace → Guest Kernel(TLB · Page Table) → VM Boundary → KVM/CPU(EPT) → Host RAM(NUMA) 한 장과, 관리 경로와 자원 압박 경로를 따로 기억하라는 지시",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "§29 와 §79 가 그린 것을 최종 기준으로 다시 그린 것이다. 같은 경로를 세 번째 기록으로 만들지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-82-claims-translation",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#82-핵심-claim-registry"
|
|
],
|
|
"summary": "CLAIM-MEM-01~06 — 두 단계 변환 · Guest Page Table 의 관리 주체 · EPT 의 하드웨어 지원 · 정상 접근이 VM Exit 을 부르지 않는다는 것 · QEMU backing 과 region 등록 · configured ≠ resident",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "개념 본문이 이미 서술한 것을 한 문장씩으로 줄인 목록이다. 개념의 요약 표로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-82-claims-fault-layers",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#82-핵심-claim-registry"
|
|
],
|
|
"summary": "CLAIM-MEM-07~09 — TLB Miss ≠ Page Fault · Guest Page Fault 와 EPT Violation 의 발생 단계가 다르다 · Page Fault 자체는 프로그램 오류가 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:page-fault-layers-in-a-vm",
|
|
"reason": "fault 계층 개념이 서술한 것의 요약이다. 그 글의 표로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-82-claims-huge-page",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#82-핵심-claim-registry"
|
|
],
|
|
"summary": "CLAIM-MEM-10~11 — Huge Page 가 TLB/page-table 효율을 개선할 가능성 · THP 와 HugeTLB 는 같은 방식이 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:huge-pages-in-a-vm",
|
|
"reason": "huge page 개념의 요약 두 줄이다"
|
|
},
|
|
{
|
|
"id": "SSOT-82-claims-pressure",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#82-핵심-claim-registry"
|
|
],
|
|
"summary": "CLAIM-MEM-12~14 와 17 — Memory Overcommit 의 성격 · Guest Swap 과 Host Swap 의 계층 · pressure 가 storage 와 latency 를 악화시킬 수 있다 · Guest OOM 과 Host OOM 의 영향 범위",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:memory-pressure-reclaim-swap-oom",
|
|
"reason": "압박 개념의 요약이다. 개념 안의 표로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-82-claims-balloon",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#82-핵심-claim-registry"
|
|
],
|
|
"summary": "CLAIM-MEM-15~16 — virtio-balloon 은 협력용 장치이지 RAM 제공 장치가 아니다 · 과도한 inflate 는 Guest reclaim/swap/OOM 을 부를 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:virtio-balloon-memory-reclaim",
|
|
"reason": "balloon 개념의 요약 두 줄이다"
|
|
},
|
|
{
|
|
"id": "SSOT-82-claims-numa",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#82-핵심-claim-registry"
|
|
],
|
|
"summary": "CLAIM-MEM-18 — NUMA 시스템에서는 vCPU placement 와 memory placement 를 함께 봐야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:numa-locality-for-vcpu-and-memory",
|
|
"reason": "NUMA 개념의 결론을 한 문장으로 줄인 것이다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-1-host-numa-topology-overlap",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-1"
|
|
],
|
|
"summary": "OQ-1. Host 의 실제 NUMA topology 는 무엇인가 — node 수 · node 별 CPU · node 별 memory · node distance",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:host-numa-topology",
|
|
"reason": "CPU 부의 question:host-numa-topology 가 같은 것을 이미 묻고 있고 그 unknown 이 「단일인지 다중인지」를 그대로 담고 있다. 같은 한 번의 측정으로 닫히는 물음을 두 편으로 만들지 않는다. 그 질문이 「SSOT 는 이 확인에 쓸 명령을 적지 않았다」고 남긴 자리에 §83 OQ-1 의 `lscpu` · `numactl --hardware` 와 node 별 memory · node distance 를 확인 항목으로 더한다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-2-vm-configured-vs-current-memory",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-2"
|
|
],
|
|
"summary": "OQ-2. 각 VM 의 configured/current memory 는 얼마인가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:vm-configured-vs-current-memory",
|
|
"reason": "§42 가 세운 세 값의 차이를 이 Host 에서 재는 물음이다. 답에 따라 overcommit 상태인지가 정해지고 그 뒤 실험 설계가 갈린다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-3-qemu-resident-memory-distribution",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-3"
|
|
],
|
|
"summary": "OQ-3. QEMU process 의 Host resident memory 는 어떻게 분포하는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:qemu-resident-memory-distribution",
|
|
"reason": "configured 와 RSS 의 차이, 그리고 backing 이 anonymous 인지 huge page 인지를 재는 물음이다. §40·§41 이 서술한 backing 구조가 이 Host 에서 어떤 값인지가 답이다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-4-host-thp-policy",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-4"
|
|
],
|
|
"summary": "OQ-4. Host THP 정책은 무엇인가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:host-thp-policy",
|
|
"reason": "§53 이 정책은 kernel/distribution/Host 설정에 따라 다르니 실제 시스템에서 확인하라고 적었다. 값을 읽어야 닫힌다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-5-vm-ram-backed-by-hugetlb",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-5"
|
|
],
|
|
"summary": "OQ-5. VM RAM 이 HugeTLB 로 명시적으로 backing 되어 있는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:vm-ram-backed-by-hugetlb",
|
|
"reason": "§52 가 가른 세 자리 가운데 Host backing 쪽을 이 Host 에서 확정하는 물음이다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-6-swap-activity-in-guest-and-host",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-6"
|
|
],
|
|
"summary": "OQ-6. Guest 와 Host 에서 현재 swap 이 발생하는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:swap-activity-in-guest-and-host",
|
|
"reason": "§63 이 swap-used 값이 아니라 지금 오가는지를 보라고 적었다. Guest 와 Host 를 같은 시각에 재야 §61 의 계층 구분이 값으로 갈린다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-7-host-memory-pressure-vs-guest-latency",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-7"
|
|
],
|
|
"summary": "OQ-7. Host memory pressure 가 Guest latency 에 영향을 주는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:host-memory-pressure-vs-guest-latency",
|
|
"reason": "§60·§62 가 그린 경로가 이 Host 에서 실제로 이어지는지를 baseline → 압박 유도 → 재측정으로 확인하는 물음이다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-8-virtio-balloon-configured",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-8"
|
|
],
|
|
"summary": "OQ-8. virtio-balloon 이 VM 에 구성되어 있는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:virtio-balloon-configured",
|
|
"reason": "구성 여부가 정해지지 않으면 OQ-9 의 실험 자체가 성립하지 않는다. §83 이 driver 이름과 표시 방식이 환경마다 다를 수 있다는 단서를 달았다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-9-balloon-target-vs-guest-available-memory",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-9"
|
|
],
|
|
"summary": "OQ-9. Balloon target 변화가 Guest available memory 에 어떻게 반영되는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:balloon-target-vs-guest-available-memory",
|
|
"reason": "§66·§68 의 방향과 §69 의 과도 inflate 위험을 이 Host 에서 값으로 확인하는 물음이다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-10-host-numa-topology-overlap",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-10"
|
|
],
|
|
"summary": "OQ-10. VM vCPU 는 어느 Host CPU 에 배치되어 있는가",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:host-numa-topology",
|
|
"reason": "§83 OQ-10 이 스스로 「CPU 가상화 SSOT 의 pinning/overcommit 관측과 연결한다」고 적었고, CPU 부의 question:host-numa-topology 가 이미 두 VM 의 vCPU 배치를 unknown 에 담고 있다. 같은 `virsh vcpuinfo`/`vcpupin` 한 번으로 닫히는 것을 따로 세우지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-11-qemu-memory-numa-placement",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-11"
|
|
],
|
|
"summary": "OQ-11. QEMU memory 는 어느 NUMA node 에 배치되어 있는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:qemu-memory-numa-placement",
|
|
"reason": "CPU 부가 넘긴 자리다. vCPU 배치는 그쪽이 재고 memory 배치와 둘의 어긋남은 여기서 잰다 — `numastat -p <QEMU_PID>` 는 CPU 부 어디에도 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-12-numa-remote-access-vs-workload-latency",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-12"
|
|
],
|
|
"summary": "OQ-12. NUMA remote access 가 실제 workload latency 에 의미 있는 영향을 주는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:numa-remote-access-vs-workload-latency",
|
|
"reason": "topology 를 아는 것과 latency 가 달라지는 것은 다른 물음이다. §83 이 단순 topology 만 보고 성능 문제라고 단정하지 말라고 적었다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-13-guest-page-fault-vs-workload",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-13"
|
|
],
|
|
"summary": "OQ-13. Guest Page Fault 가 workload 변화와 함께 증가하는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:guest-page-fault-vs-workload",
|
|
"reason": "§46 의 다섯 원인 가운데 무엇이 늘었는지를 갈라야 답이 된다. Page Fault 증가만으로 오류라고 판단하지 말라는 것이 §83 의 단서다"
|
|
},
|
|
{
|
|
"id": "SSOT-83-oq-14-host-major-fault-vs-storage-latency",
|
|
"kindCandidate": "QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#83-실제-환경에서-확인할-open-question-oq-14"
|
|
],
|
|
"summary": "OQ-14. Host Page Fault/major fault 와 storage latency 가 상관되는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:host-major-fault-vs-storage-latency",
|
|
"reason": "§49 의 Host 계층과 §62 의 storage 연결을 같은 시간축에서 확인하는 물음이다"
|
|
},
|
|
{
|
|
"id": "SSOT-84-recommended-experiment-order",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#84-권장-실험-순서"
|
|
],
|
|
"summary": "Host physical memory/NUMA 확인부터 NUMA locality 실험까지 열두 단계로 적은 권장 검증 순서",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "분석 진행 계획이다. 어느 Question 을 먼저 여느냐의 순서일 뿐 독립 기록으로 읽을 사람이 없다. CPU 부의 §23 도 같은 처분이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-85-conditions-to-record-with-every-experiment",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#85-실험-시-반드시-같이-기록할-것"
|
|
],
|
|
"summary": "Host(CPU model · core/thread · RAM · NUMA topology · swap 설정 · kernel version · THP policy · physical storage) · VM(vCPU · configured/current RAM · memory backing · balloon device · guest swap · guest kernel) · Workload(application · heap 설정 · request concurrency · DB workload · 측정 시간) 세 묶음을 실험마다 함께 남긴다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:record-the-conditions-with-every-memory-experiment",
|
|
"reason": "열두 Question 전부에 같이 걸리는 규칙이라 각 글에 되풀이할 것이 아니라 한 편으로 두고 가리킨다. 원 사건 이름을 지워도 규칙이 남는다 — 메모리 실험 결과를 다른 환경에 재사용하려면 어떤 조건을 같이 남겨야 하는가에 대한 답이다"
|
|
},
|
|
{
|
|
"id": "SSOT-86-classify-the-symptom-before-concluding",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#86-문제를-진단할-때의-분류"
|
|
],
|
|
"summary": "메모리 latency 나 OOM 을 보고 한 번에 「메모리 부족」이라고 결론내지 않고 Guest Virtual Memory · Virtualization Translation · Host Memory · Dynamic Memory · NUMA 다섯 갈래로 먼저 가른다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:memory-symptom-needs-layer-separation",
|
|
"reason": "다음 프로젝트에도 그대로 적용되는 판독 규칙이다. 다섯 갈래는 이 부의 여섯 개념을 가로지르는 라우팅이라 어느 개념 안에 넣어도 나머지 넷을 가리키지 못한다"
|
|
},
|
|
{
|
|
"id": "SSOT-88-do-not-judge-memory-from-one-guest-free",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#88-결론"
|
|
],
|
|
"summary": "Guest 하나의 `free -h` 만 보고 메모리 상태를 판단하지 않고 Guest → QEMU → Host → NUMA → Storage 영향을 같은 시간축에서 관측한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:memory-symptom-needs-layer-separation",
|
|
"reason": "§86 과 같은 규칙의 결론 문장이다. 그 Reference 의 적용 조건 한 줄로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-88-concept-is-fixed-oq-becomes-case",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#88-결론"
|
|
],
|
|
"summary": "이 문서의 개념 부분은 SSOT 로 고정하고 실제 서버에 종속되는 설정과 동작은 OQ-1~OQ-14 를 실험해 CASE 로 전환한다는 경계 선언",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-memory-address-translation",
|
|
"reason": "이 부가 무엇까지 확정하고 무엇을 확정하지 않는지를 그은 문장이라 개념의 missing-verification 이 그대로 받는다. CPU 부의 §28 도 같은 처분이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-89-part-scope-and-experiment-goals",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#89-문서-목적"
|
|
],
|
|
"summary": "제3부가 다루는 범위 — Guest 애플리케이션의 요청이 Guest Kernel · virtio-net · virtqueue · vhost-net · TAP · Bridge/Routing · Physical NIC 를 거쳐 나가고 돌아오는 구조 — 와 그 위에서 검증하려는 Keycloak 멀티 노드 · 동일 Refresh Token 동시 갱신 · 세션 상태 공유 · Host Nginx → VM → K3s → Keycloak 요청 경로 일곱 항목",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "이 부가 무엇을 다루고 무엇을 다루지 않는지의 선언이다. 따로 떼면 아무 것도 설명하지 않는 목차가 되고, 개념의 classification 과 missing-verification 이 그대로 받는다. CPU 부의 §28 과 메모리 부의 §88 도 같은 처분이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-90-virsh-libvirt-virtio-are-different-things",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#90-virsh-libvirt-virtio-구분"
|
|
],
|
|
"summary": "virsh 는 libvirt 에 관리 명령을 넣는 CLI 이고 packet datapath 에 직접 참여하지 않는다 · libvirt 는 VM lifecycle 과 NIC/network configuration 을 관리한다 · virtio 는 명령어도 단일 프로그램도 단일 커널 모듈도 아니라 Guest 와 Host/Hypervisor 사이의 표준 인터페이스다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§121 이 virsh · libvirt · virtio 셋을 CONCEPT 의 포함 범위 첫 줄에 넣었다. 셋의 구분은 경로의 어느 자리가 관리이고 어느 자리가 datapath 인지를 가르는 도입부라 개념 밖에 두면 §105 의 control/data path 절이 시작할 자리를 잃는다"
|
|
},
|
|
{
|
|
"id": "SSOT-91-virtio-net-is-split-across-guest-and-host",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#91-virtio-net은-정확히-어디에-있는가"
|
|
],
|
|
"summary": "virtio-net 은 한 자리에 있는 하나의 프로세스가 아니다. Guest 측에 TCP/IP Stack · virtio-net Frontend Driver · virtqueue 가 있고 Host 측에 QEMU virtio-net Device Model · vhost-net · TAP · Bridge/Routing/NAT · Physical NIC Driver 가 있다. virtio 는 특정 커널 계층이 아니라 frontend 와 backend 사이의 I/O 계약이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "개념이 서 있는 문장이다. 이 한 줄을 떼면 뒤의 §103~§109 가 「누가 무엇을 처리하는가」를 물을 수 없다. 독립 기록으로 만들면 경로의 정거장 배치도만 담은 글이 하나 남는다"
|
|
},
|
|
{
|
|
"id": "SSOT-92-frontend-and-backend",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#92-frontend와-backend"
|
|
],
|
|
"summary": "Frontend 는 Guest Kernel 의 virtio-net driver, Backend 는 Guest 가 넘긴 packet buffer 를 Host 쪽에서 처리하는 구현이고 backend 는 QEMU userspace 이거나 vhost-net kernel backend 다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§91 이 그은 분할에 이름을 붙인 절이다. 두 절을 떼어 두 기록으로 만들면 어느 쪽도 혼자 읽히지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-93-guest-sees-a-nic-not-qemu",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#93-guest-os는-왜-qemu가-아니라-virtio-net을-사용하는가"
|
|
],
|
|
"summary": "Guest 는 「QEMU 를 호출한다」가 아니라 「내 NIC 를 쓴다」로 동작한다. QEMU 가 Virtual PCI Bus 에 virtio NIC 를 노출하면 Guest Linux 가 virtio device 를 발견해 driver 를 bind 하고 ens3/eth0 형태의 interface 가 생긴다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "물리 서버의 경로와 VM 의 경로를 나란히 놓아 frontend 가 왜 Guest 안에 있어야 하는지를 보이는 절이다. 개념의 도입 절로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-94-121-125-packet-path-as-one-concept",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#94-전체-네트워크-계층",
|
|
"final/document.md#121-이-ssot에서-파생될-concept",
|
|
"final/document.md#125-최종-기준-구조"
|
|
],
|
|
"summary": "virtio-net + vhost-net + TAP + Linux Bridge 를 기준으로 한 수신·송신 양방향 전체 계층과, 그 요소들이 하나의 packet 실행 경로를 설명하므로 하나의 CONCEPT 로 관리한다는 §121 의 지시, 그리고 §125 가 다시 그린 Control/Setup 경로와 vhost-net·QEMU backend 두 Data Path",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§121 이 이 요소들을 하나의 CONCEPT 로 관리하라고 적었고 §125 가 같은 경로를 최종 기준 구조로 다시 그렸다. 경로 중간을 잘라 내면 packet 이 어디서 와서 어디로 가는지 말할 수 없어 독립성 검사를 통과한다. 제3부에서 유일하게 독립 개념이 되는 후보다"
|
|
},
|
|
{
|
|
"id": "SSOT-94-baseline-structure-is-one-of-many",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#94-전체-네트워크-계층"
|
|
],
|
|
"summary": "이 문서가 기준으로 삼은 것은 가장 기본적인 virtio-net + vhost-net + TAP + Linux Bridge 구조이고, 실제 환경은 Bridge · NAT · Routed Network · macvtap · SR-IOV · VFIO passthrough · Open vSwitch · Kubernetes CNI 에 따라 달라질 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "개념이 무엇을 기준으로 쓴 글인지를 밝히는 문장이라 basis-version 이 받는다. 따로 떼면 「이 문서가 안 다루는 구성 목록」만 남는다"
|
|
},
|
|
{
|
|
"id": "SSOT-95-physical-nic-hardware-is-not-the-interface-object",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#95-physical-nic의-역할"
|
|
],
|
|
"summary": "Physical NIC 는 링크와 서버를 잇는 하드웨어이고 Host Kernel 의 NIC driver 가 그것을 제어해 Linux 가 network interface 로 노출한다. Physical NIC hardware 와 Linux interface object 는 같지 않다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "경로의 마지막 정거장 설명이다. 하드웨어와 interface object 의 구분 한 줄만으로는 독립 기록이 되지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-96-linux-bridge-is-an-l2-software-switch",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#96-linux-bridge의-역할"
|
|
],
|
|
"summary": "Linux Bridge 는 Host Kernel 안의 L2 software switch 로 Ethernet frame 의 Destination MAC 을 보고 port 를 고른다. L2 forwarding · MAC learning · 여러 virtual/physical port 연결이 역할이고 `bridge link` · `bridge fdb show` · `ip link show type bridge` 로 본다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "경로 위 정거장 하나의 역할이다. §97·§98 과 나란히 놓여야 「이 셋 중 어느 구조인가」라는 물음이 성립하므로 셋을 흩으면 물음이 사라진다"
|
|
},
|
|
{
|
|
"id": "SSOT-97-routing-is-l3-not-l2",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#97-routing의-역할"
|
|
],
|
|
"summary": "Routing 은 Bridge 와 달리 L3 이고 IP 기반이며 서로 다른 IP network 를 잇는다. destination IP 를 보고 interface 나 next-hop 을 고르고 `ip route` 로 본다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§96 과 짝을 이루는 대비 절이다. 개념 본문의 표 한 행이 될 것을 기록으로 만들지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-98-tell-bridge-routing-and-nat-apart",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#98-nat의-역할"
|
|
],
|
|
"summary": "NAT 은 packet 의 IP/Port 를 바꾸고 VM 이 private subnet 을 쓰면 Host 가 NAT gateway 처럼 동작할 수 있다. 따라서 VM network 를 분석할 때 Bridge 기반인가 · Routing 기반인가 · NAT 기반인가를 먼저 구분해야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "구분해야 한다는 요구 자체는 개념이 안고, 이 Host 가 셋 중 어느 것인지는 §122 OQ-1 이 받아 question 글감이 되었다. 규칙과 그 규칙이 낳은 물음을 같은 자리에 두 번 적지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-99-tap-is-the-host-side-attach-point",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#99-tap의-역할"
|
|
],
|
|
"summary": "TAP 은 물리 장치가 아니라 Host Linux Kernel 이 제공하는 가상 Ethernet interface(tap0 · vnet0)이고 VM 의 Ethernet frame 과 Host Linux networking 을 잇는 접점이다. `ip link` · `ip tuntap show` · `bridge link` · `virsh domiflist <domain>` 로 본다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "경로가 Guest 를 떠나 Host 로 넘어오는 자리의 설명이다. 이 interface 가 이 Host 에서 무엇인지는 §122 OQ-2 가 따로 묻는다"
|
|
},
|
|
{
|
|
"id": "SSOT-100-virtqueue-is-a-descriptor-shared-queue",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#100-virtqueue의-역할"
|
|
],
|
|
"summary": "virtqueue 는 NIC 도 Linux network interface 도 아니고 Guest 와 Host backend 가 I/O buffer 를 주고받는 descriptor 기반 shared queue 다. 네트워크에서는 TX(Guest → Host)와 RX(Host → Guest)를 쓰고, packet payload 를 매번 userspace API 호출로 넘기는 대신 Guest memory buffer 와 descriptor 를 공유·참조하도록 설계되어 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "경로의 한 정거장이자 §110·§112 가 말하는 최적화가 걸리는 자리다. 떼어 내면 무엇과 무엇 사이의 queue 인지 말할 수 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-101-guest-tcp-ip-stack-is-real",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#101-guest-tcp-ip-stack의-역할"
|
|
],
|
|
"summary": "Guest TCP/IP Stack 은 Guest Linux Kernel 의 실제 network stack 이다. Socket · TCP · UDP · IP · Routing · Neighbor/ARP · Firewall · Network Driver 가 Guest 안에 그대로 있고 VM 이라고 stack 이 가짜인 것이 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "경로의 Guest 쪽 절반이다. 하위 절 넷(Socket · TCP · IP · Ethernet)은 일반 Linux network stack 설명이라 이 개념 밖에서 독립 기록이 될 자리가 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-102-keycloak-only-sees-a-socket",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#102-packet이-keycloak까지-올라오는-과정"
|
|
],
|
|
"summary": "Ethernet Frame → IP Packet → TCP Segment → Socket → HTTP → Keycloak 순으로 올라오고, Keycloak 은 virtqueue · vhost-net · TAP · Bridge · Physical NIC 를 직접 알 필요 없이 Guest Linux 가 준 TCP socket 위에서 HTTP 를 처리한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "경로의 끝이자 §120 이 「애플리케이션 증상과 network 계층을 갈라야 한다」고 말할 때 근거로 삼는 문장이다. 개념의 마지막 절로 들어가고 Reference 쪽에서는 source 로 가리킨다"
|
|
},
|
|
{
|
|
"id": "SSOT-103-qemu-device-model-has-two-roles",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#103-qemu-virtio-device-model의-역할"
|
|
],
|
|
"summary": "QEMU 의 virtio Device Model 은 Host Userspace 의 QEMU process 안에 있고 역할이 둘이다 — 장치 생성/설정/관리(device 노출 · feature negotiation · virtqueue 설정 · backend 연결)와 실제 packet datapath 처리. datapath 는 QEMU backend 를 직접 쓰는 경우와 vhost-net 을 쓰는 경우로 갈린다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§104·§106·§108 이 되풀이해 교정하는 오해의 근원이 이 두 역할을 하나로 본 것이다. 네 절이 같은 결론(관리 주체와 packet 처리 주체는 다르다)에 닿으므로 개념 안의 한 절이지 네 편이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-104-do-not-generalize-tap-vhost-qemu-virtqueue",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#104-왜-tap-→-vhost-net-→-qemu-→-virtqueue-라고-일반화하면-안-되는가"
|
|
],
|
|
"summary": "`TAP → vhost-net → QEMU → virtqueue` 를 모든 환경의 일반적인 packet 경로로 그리면 안 된다. vhost-net 의 목적 하나가 datapath 에서 QEMU userspace 를 우회하는 것이라 fast path 는 `TAP → vhost-net → virtqueue → Guest` 로 이해한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§124 의 Claim 9 와 같은 문장이고 §103 이 나눈 두 역할의 따름정리다. 개념이 그리는 그림이 어떤 그림이 아닌지를 못박는 절이라 그 그림과 같은 글에 있어야 한다"
|
|
},
|
|
{
|
|
"id": "SSOT-105-control-path-vs-data-path",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#105-control-path와-data-path"
|
|
],
|
|
"summary": "Control/Setup Path 는 virsh → libvirt → QEMU → virtio-net Device Model 로 이어져 feature negotiation · virtqueue setup · vhost-net setup 을 하고, Data Path 는 packet 이 반복해 흐르는 경로다. 여기서 control 은 Kubernetes Control Plane 이 아니라 일반적인 설정/제어 경로를 뜻한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "경로를 둘로 가르는 축이다. 축을 떼면 §90 의 virsh/libvirt 와 §94 의 packet 계층이 한 그림에 놓일 이유가 없어진다"
|
|
},
|
|
{
|
|
"id": "SSOT-106-device-owner-is-not-the-per-packet-processor",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#106-qemu가-userspace인데-packet이-qemu를-안-거칠-수-있는-이유"
|
|
],
|
|
"summary": "장치의 생성/관리 주체라는 것과 모든 packet 의 runtime datapath 를 처리한다는 것은 다르다. QEMU 가 vCPU 를 만든다고 Guest 의 ADD·MOV·SUB 를 전부 QEMU 가 실행하지 않는 것처럼, QEMU 가 virtual NIC 를 만든다고 packet 을 하나씩 처리해야 하는 것은 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "CPU 가상화와의 비유로 §103 의 두 역할을 다시 설명한 절이다. 이 비유는 cpu-virtualization 의 개념을 relations 로 가리키는 다리가 되고, 따로 떼면 비유만 남는다"
|
|
},
|
|
{
|
|
"id": "SSOT-107-vhost-net-moves-the-hot-path-into-the-kernel",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#107-vhost-net-최적화"
|
|
],
|
|
"summary": "QEMU userspace 가 packet 마다 I/O 를 처리하면 kernel ↔ userspace 전환 비용이 쌓이고 packet rate 가 높아질수록 transition · scheduling · copy · notification 비용이 커질 수 있다. vhost-net 은 hot path 를 kernel backend 로 옮겨 그 비용을 줄인다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "개념이 설명하는 두 datapath 가운데 하나의 존재 이유다. 이 Host 에서 그 차이가 실제로 보이는지는 §122 OQ-4 가 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-108-vhost-net-does-not-remove-qemu",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#108-vhost-net은-qemu를-제거하지-않는다"
|
|
],
|
|
"summary": "vhost-net 을 써도 QEMU 는 VM lifecycle · virtual hardware model · virtio device 생성 · feature negotiation · queue configuration · backend 연결 · device reset · control/configuration 처리를 계속 맡는다. vhost-net 은 QEMU 제거가 아니라 반복적인 virtio packet datapath 의 상당 부분을 Host Kernel 로 offload 하는 것이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§124 의 Claim 16 과 같은 문장이고 §103 의 역할 A 가 남아 있음을 확인하는 절이다. §104 와 같은 오해를 반대편에서 막는 것이라 같은 글에 있어야 짝이 맞는다"
|
|
},
|
|
{
|
|
"id": "SSOT-109-fast-path-and-control-slow-path",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#109-fast-path와-slow-control-path"
|
|
],
|
|
"summary": "Fast Path 는 빈번히 반복되는 packet forwarding/data transfer 경로(TAP → vhost-net → virtqueue)이고 Control/Slow Path 는 device 초기화 · feature negotiation · queue setup · configuration change · device reset 처럼 빈도가 낮은 설정/예외 처리다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§105 의 축에 빈도라는 이름을 붙인 절이다. 두 절이 같은 결론에 닿으므로 개념 안의 한 절로 합친다"
|
|
},
|
|
{
|
|
"id": "SSOT-110-not-always-zero-copy",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#110-data-copy-최적화"
|
|
],
|
|
"summary": "virtio/virtqueue/vhost 구조는 buffer descriptor 로 불필요한 copy 와 context switch 를 줄이도록 설계되어 있지만 항상 zero-copy 라고 일반화하면 안 된다. 실제 copy 여부는 kernel version · QEMU version · vhost configuration · offload · NIC capability · packet path · GSO/GRO/TSO 에 따라 달라질 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§121 이 CONCEPT 포함 범위에 넣은 최적화 항목이고, 「항상 그렇다」로 굳히면 안 된다는 단서다. 단서 하나로는 독립 기록이 되지 않고 개념의 한 절로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-111-notification-batching-and-moderation",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#111-interrupt-notification-최적화"
|
|
],
|
|
"summary": "Guest 와 Host 는 queue 에 새 packet/buffer 가 있음을 서로 알려야 하고, packet 마다 과도한 interrupt/notification 이 나면 overhead 가 커질 수 있어 batching · interrupt moderation · queueing 이 중요하다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "virtqueue 절의 이어지는 설명이다. §122 의 어떤 OQ 도 이것만 따로 재지 않으므로 개념 안에 둔다"
|
|
},
|
|
{
|
|
"id": "SSOT-112-multi-queue",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#112-multi-queue-최적화"
|
|
],
|
|
"summary": "queue 하나만 쓰면 특정 vCPU/처리 경로에 부하가 몰릴 수 있어 virtio-net 은 multi-queue 를 쓸 수 있다. RX Queue 를 vCPU 에 나눠 packet processing 을 병렬화하고 single queue bottleneck 을 줄이는데, 효과는 workload · CPU affinity · IRQ placement · queue configuration 에 따라 달라진다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "구조 설명은 개념이 안고, 이 Host 의 virtio-net 에 실제로 켜져 있는지는 §122 OQ-5 가 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-113-offload-optimizations",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#113-offload-최적화"
|
|
],
|
|
"summary": "TSO · GSO · GRO · Checksum Offload 는 작은 packet 을 하나씩 처리하는 CPU overhead 와 segmentation/aggregation 비용을 줄인다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§121 이 포함 범위에 넣은 최적화 항목이라 개념 본문의 목록으로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-113-117-6-offload-distorts-packet-capture",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#113-offload-최적화",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-6"
|
|
],
|
|
"summary": "offload 가 켜져 있으면 tcpdump 에서 보이는 packet size 나 checksum 이 실제 wire 에서 보이는 것과 다르게 보일 수 있다. §117.6 은 packet capture 가 예상과 다르게 보이는 원인 후보로 GSO · GRO · TSO · Checksum offload 를 든다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:bisect-the-packet-path-with-capture-points",
|
|
"reason": "계층마다 capture 해서 끊긴 자리를 가르는 규칙이 스스로 안고 있어야 하는 예외다. 이 단서 없이 capture 결과를 읽으면 정상 동작을 이상으로 읽는다"
|
|
},
|
|
{
|
|
"id": "SSOT-114-bridge-does-not-always-traverse-host-l3",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#114-linux-bridge가-항상-host-tcp-ip-stack을-거치는-것은-아니다"
|
|
],
|
|
"summary": "Bridge 가 단순 L2 forwarding 을 하는 경우 frame 이 반드시 Host 의 일반적인 L3 TCP/IP stack 을 거치는 것은 아니다(VM1 TAP → Bridge → VM2 TAP). Routing · NAT · Host-local termination · Firewall 이 걸리면 L3/Netfilter 경로가 개입하므로 `Physical NIC → Host TCP/IP Stack → Bridge` 를 고정된 packet path 로 보면 안 된다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§104 와 같은 종류의 교정이고 경로가 구성에 따라 달라진다는 개념의 결론이다. 동시에 capture 결과를 읽는 예외이기도 해서 Reference 쪽에서는 source 로 가리킨다"
|
|
},
|
|
{
|
|
"id": "SSOT-115-host-nic-does-not-go-through-virtio-again",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#115-host-physical-nic로-나갈-때-virtio를-다시-거치지-않는다"
|
|
],
|
|
"summary": "Host Physical NIC 로 나갈 때 virtio 를 다시 거치는 것이 아니라 Physical NIC 의 실제 driver 를 쓴다. virtio 는 Guest virtual I/O device 와 Host backend 사이의 인터페이스이지 Host 쪽에도 한 벌 더 있는 계층이 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§124 의 Claim 13 과 같은 문장이고 §91 의 「virtio 는 I/O 계약이다」가 경로 끝에서 다시 확인되는 자리다. 개념의 마지막 교정 절로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-116-test-environment-packet-path",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#116-현재-keycloak-k3s-테스트-환경과-연결"
|
|
],
|
|
"summary": "이 테스트 환경의 경로를 Client → Host Physical NIC → Host Nginx → Host Network → VM1/VM2 → K3s → Keycloak 으로 놓고, VM network 까지 펼치면 Bridge/Route/NAT → TAP(vm1)/TAP(vm2) → vhost-net → virtqueue → virtio-net → Guest Network Stack → K3s networking → Keycloak 이 된다는 서술",
|
|
"disposition": "NEEDS_EVIDENCE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "이 그림의 앞부분(Host Nginx 아래 VM 두 대에 K3s/Keycloak)은 §89 가 밝힌 실험 구성이지만, 펼친 뒷부분은 이 Host 가 Bridge 인지 NAT 인지도(OQ-1) TAP 이 무엇인지도(OQ-2) vhost-net 을 쓰는지도(OQ-3) 확인하기 전의 가정이다. 「이 환경에서 실제로 그러하다」는 주장이라 재기 전에는 Case 도 Concept 도 아니다. OQ-1·2·3·6 이 답하면 다시 판정한다. CPU 부의 SSOT-22-claims-12-13 과 같은 처분이다"
|
|
},
|
|
{
|
|
"id": "SSOT-117-1-3-connectivity-failures",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-1",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-2",
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-3"
|
|
],
|
|
"summary": "TAP/Bridge 연결 오류(VM 외부 통신 불가 · Host ↔ VM 통신 불가 · 특정 VM 만 통신 불가) · Routing 오류(같은 subnet 은 되는데 다른 subnet 은 안 됨 · gateway 까지는 되고 외부는 실패) · NAT/Firewall 오류(VM → Internet 실패 · 외부 → VM 접근 실패 · 특정 port 만 실패)와 각각의 확인 명령",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:bisect-the-packet-path-with-capture-points",
|
|
"reason": "증상 셋 모두가 「어느 hop 에서 끊겼는가」로 환원되고 §119 의 계층별 capture 가 그것을 가른다. 셋을 따로 기록으로 만들면 같은 규칙의 부분 증상이 셋이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-117-4-vhost-net-unused-or-inefficient-datapath",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-4"
|
|
],
|
|
"summary": "높은 packet rate 에서 QEMU userspace 가 datapath 를 직접 처리하면 CPU overhead 가 커질 수 있고, 관찰 대상은 QEMU CPU usage · vhost thread · packet rate · latency · context switch 다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§107 이 말한 비용이 구조에서 어긋나는 자리로 다시 나온 것이다. 개념의 「이 구조에서 어긋날 수 있는 자리」 절로 들어가고, 이 Host 에서 실제로 그런지는 OQ-3·OQ-4·OQ-7 이 받는다. CPU 부의 §24 도 같은 처분이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-117-5-single-queue-bottleneck",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-5"
|
|
],
|
|
"summary": "queue/vCPU 하나에 packet processing 이 몰릴 수 있고 확인 대상은 virtio multi-queue · IRQ distribution · per-vCPU CPU usage · RSS/RPS/XPS 다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "§112 와 같은 결론에 닿는 절이라 개념 안에서 같은 자리에 놓인다. 이 Host 의 상태는 OQ-5 가 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-117-7-host-cpu-contention-looks-like-network-latency",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#117-이-구조에서-발생할-수-있는-문제-117-7"
|
|
],
|
|
"summary": "vhost-net · QEMU thread · softirq 도 Host CPU 를 쓰므로 network 문제처럼 보여도 CPU scheduling 문제일 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-the-network-path-before-blaming-the-application",
|
|
"reason": "증상을 어느 계층에 귀속할지의 규칙이고 §120 과 방향만 반대다(하나는 애플리케이션 증상을 network 로, 하나는 network 증상을 CPU 로 되돌린다). 같은 Reference 의 적용 조건과 예외로 들어가고, 재는 것은 OQ-7 이 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-118-per-layer-observation-commands",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#118-실제-linux에서-확인할-명령어"
|
|
],
|
|
"summary": "Physical NIC · Linux Bridge · TAP/vnet · libvirt VM NIC · libvirt network · Routing · Guest NIC · virtio 장치 · vhost 아홉 자리를 보는 명령 목록 — `ip link` · `ethtool` · `bridge link` · `bridge fdb show` · `ip tuntap show` · `virsh domiflist` · `virsh net-list --all` · `virsh net-info` · `virsh net-dumpxml` · `ip route` · `ip rule` · `ip neigh` · `lspci` · `lsmod | grep virtio` · `lsmod | grep vhost`",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:bisect-the-packet-path-with-capture-points",
|
|
"reason": "명령 목록 자체는 규칙이 아니다. §119 의 규칙이 각 지점에서 무엇을 실행할지를 이 목록이 대므로 그 Reference 의 본문 표가 된다. CPU 부의 §14 는 붙일 Reference 가 없어 개념으로 갔지만 여기는 있다"
|
|
},
|
|
{
|
|
"id": "SSOT-119-bisect-the-path-with-tcpdump",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#119-실제-packet-path-추적"
|
|
],
|
|
"summary": "Host 의 physical NIC · bridge · tap/vnet 과 Guest 의 interface 에서 각각 `tcpdump -ni` 를 걸어 어디까지 보이는지로 의심 구간을 좁힌다 — Physical NIC O · Bridge O · TAP X 이면 Host Bridge/TAP mapping 을, TAP O · Guest NIC X 이면 virtio/vhost/Guest NIC 계층을, Guest NIC O · Socket X 이면 Guest routing/firewall/listen 상태를 의심한다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:bisect-the-packet-path-with-capture-points",
|
|
"reason": "원 사건의 이름을 지워도 규칙이 남는다 — KVM/libvirt 환경이면 어디서든 같은 순서로 구간을 좁힌다. §117.1~.3 의 증상 셋과 §118 의 명령 목록이 이 규칙의 적용 대상과 도구이고, §113·§114 가 예외를 댄다. OQ-1 · OQ-2 · OQ-6 셋이 모두 이 규칙 위에서 돌아가므로 세 글에 되풀이하지 않고 한 편으로 두고 가리킨다. 메모리 부의 §85·§86 이 세운 기준을 따랐다"
|
|
},
|
|
{
|
|
"id": "SSOT-120-verify-the-network-path-separately",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#120-keycloak-refresh-token-실험과의-관계"
|
|
],
|
|
"summary": "Refresh Token 경쟁 자체는 virtio-net 문제가 아니지만 Client → Nginx → VM1/VM2 → K3s → Keycloak → PostgreSQL/Redis 경로를 공유하므로 network virtualization 문제가 실험 결과에 영향을 줄 수 있다. Node1 요청만 지연 · VM2 packet loss · Host bridge misconfiguration · NAT/conntrack issue · Host CPU contention 으로 인한 vhost 처리 지연을 Refresh Token 경쟁이나 DB lock 으로 오해하지 않도록 network path 를 별도로 검증한다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-the-network-path-before-blaming-the-application",
|
|
"reason": "이 부가 존재하는 이유(§89)와 결론(§124 Claim 17)을 잇는 규칙이고, 다음 프로젝트에도 그대로 적용된다 — 경로를 공유하는 실험에서 애플리케이션·저장소 원인으로 결론내기 전에 공유 계층을 따로 검증한다. §119 의 규칙은 network 안에서 어느 hop 인지를 가르고 이 규칙은 그것이 network 인지 아닌지를 가르므로 둘은 다른 물음에 답한다"
|
|
},
|
|
{
|
|
"id": "SSOT-122-oq-1-vm-network-mode-bridge-nat-or-routed",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#122-open-question-oq-1"
|
|
],
|
|
"summary": "OQ-1. 현재 VM network 는 Bridge · NAT · Routing 중 어떤 구조인가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:vm-network-mode-bridge-nat-or-routed",
|
|
"reason": "§122 가 아직 돌리지 않은 확인으로 적어 둔 물음이다. 답이 없고 답에 따라 이 부의 서술이 이 Host 에 적용되는지가 갈리며 §122 가 확인 명령이나 관찰 대상을 함께 적어 다음 검증이 있다. 앞선 두 부가 OPEN QUESTION 을 그대로 question 글감으로 올린 것과 같은 기준이고, 다른 부의 question 이 같은 측정으로 닫는 물음도 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-122-oq-2-tap-interface-to-vm-mapping",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#122-open-question-oq-2"
|
|
],
|
|
"summary": "OQ-2. VM1/VM2 의 TAP/vnet interface 는 무엇인가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:tap-interface-to-vm-mapping",
|
|
"reason": "§122 가 아직 돌리지 않은 확인으로 적어 둔 물음이다. 답이 없고 답에 따라 이 부의 서술이 이 Host 에 적용되는지가 갈리며 §122 가 확인 명령이나 관찰 대상을 함께 적어 다음 검증이 있다. 앞선 두 부가 OPEN QUESTION 을 그대로 question 글감으로 올린 것과 같은 기준이고, 다른 부의 question 이 같은 측정으로 닫는 물음도 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-122-oq-3-is-vhost-net-actually-in-use",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#122-open-question-oq-3"
|
|
],
|
|
"summary": "OQ-3. 현재 환경에서 vhost-net 이 실제 사용되는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:is-vhost-net-actually-in-use",
|
|
"reason": "§122 가 아직 돌리지 않은 확인으로 적어 둔 물음이다. 답이 없고 답에 따라 이 부의 서술이 이 Host 에 적용되는지가 갈리며 §122 가 확인 명령이나 관찰 대상을 함께 적어 다음 검증이 있다. 앞선 두 부가 OPEN QUESTION 을 그대로 question 글감으로 올린 것과 같은 기준이고, 다른 부의 question 이 같은 측정으로 닫는 물음도 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-122-oq-4-qemu-backend-vs-vhost-net-on-this-host",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#122-open-question-oq-4"
|
|
],
|
|
"summary": "OQ-4. QEMU backend 와 vhost-net 의 성능 차이가 현재 Host 에서 관찰 가능한가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:qemu-backend-vs-vhost-net-on-this-host",
|
|
"reason": "§122 가 아직 돌리지 않은 확인으로 적어 둔 물음이다. 답이 없고 답에 따라 이 부의 서술이 이 Host 에 적용되는지가 갈리며 §122 가 확인 명령이나 관찰 대상을 함께 적어 다음 검증이 있다. 앞선 두 부가 OPEN QUESTION 을 그대로 question 글감으로 올린 것과 같은 기준이고, 다른 부의 question 이 같은 측정으로 닫는 물음도 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-122-oq-5-virtio-net-multi-queue-enabled",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#122-open-question-oq-5"
|
|
],
|
|
"summary": "OQ-5. Multi-queue 가 현재 virtio-net 에 활성화되어 있는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:virtio-net-multi-queue-enabled",
|
|
"reason": "§122 가 아직 돌리지 않은 확인으로 적어 둔 물음이다. 답이 없고 답에 따라 이 부의 서술이 이 Host 에 적용되는지가 갈리며 §122 가 확인 명령이나 관찰 대상을 함께 적어 다음 검증이 있다. 앞선 두 부가 OPEN QUESTION 을 그대로 question 글감으로 올린 것과 같은 기준이고, 다른 부의 question 이 같은 측정으로 닫는 물음도 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-122-oq-6-actual-packet-path-nginx-to-keycloak",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#122-open-question-oq-6"
|
|
],
|
|
"summary": "OQ-6. Host Nginx 에서 VM1/VM2 Keycloak 까지 실제 packet path 는 무엇인가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:actual-packet-path-nginx-to-keycloak",
|
|
"reason": "§122 가 아직 돌리지 않은 확인으로 적어 둔 물음이다. 답이 없고 답에 따라 이 부의 서술이 이 Host 에 적용되는지가 갈리며 §122 가 확인 명령이나 관찰 대상을 함께 적어 다음 검증이 있다. 앞선 두 부가 OPEN QUESTION 을 그대로 question 글감으로 올린 것과 같은 기준이고, 다른 부의 question 이 같은 측정으로 닫는 물음도 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-122-oq-7-network-virtualization-cpu-cost-under-load",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#122-open-question-oq-7"
|
|
],
|
|
"summary": "OQ-7. Keycloak load test 시 network virtualization 이 latency 에 영향을 줄 정도로 Host CPU 를 사용하는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:network-virtualization-cpu-cost-under-load",
|
|
"reason": "§122 가 아직 돌리지 않은 확인으로 적어 둔 물음이다. 답이 없고 답에 따라 이 부의 서술이 이 Host 에 적용되는지가 갈리며 §122 가 확인 명령이나 관찰 대상을 함께 적어 다음 검증이 있다. 앞선 두 부가 OPEN QUESTION 을 그대로 question 글감으로 올린 것과 같은 기준이고, 다른 부의 question 이 같은 측정으로 닫는 물음도 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-123-oq-to-case-flow",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#123-open-question-→-case"
|
|
],
|
|
"summary": "SSOT → CONCEPT → OPEN QUESTION → 실제 packet capture/configuration 확인/load test → CASE 로 이어지는 기록 흐름과 vhost-net 예시",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "네트워크 가상화가 아니라 이 저장소가 기록을 잇는 방식을 설명한 절이다. 분석에는 남아야 하고 공개 기록으로는 읽을 사람이 없다. CPU 부의 §21 도 같은 처분이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-124-claims-1-16",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#124-핵심-claim"
|
|
],
|
|
"summary": "Claim 1~16 — virsh 는 datapath 에 참여하지 않는다 · libvirt 는 lifecycle/config 를 관리한다 · virtio 는 단일 프로세스나 모듈이 아닌 표준이다 · frontend 는 Guest Kernel 에 있다 · QEMU Device Model 은 생성/negotiation/lifecycle 을 맡는다 · virtqueue 는 descriptor 기반 shared queue 다 · vhost-net 이 없으면 QEMU userspace 가 backend 다 · vhost-net 은 datapath 상당 부분을 Host Kernel 로 옮긴다 · 그러므로 TAP → vhost-net → QEMU → virtqueue 로 일반화하면 안 된다 · TAP 은 Host-side 접점이다 · Bridge 는 L2, Routing 은 L3 다 · Physical NIC 로 나갈 때 virtio 를 다시 거치지 않는다 · Guest TCP/IP Stack 은 실제 stack 이다 · Keycloak 은 socket 만 안다 · vhost-net 의 목적은 QEMU 제거가 아니라 offload 다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-packet-path-to-physical-nic",
|
|
"reason": "열여섯 전부가 메커니즘 서술이고 §90~§115 가 이미 본문으로 말한 것을 한 화면으로 접은 것이다. 「이 환경에서 실제로 그러하다」고 주장한 것이 없어 NEEDS_EVIDENCE 로 남길 것도 없다. 개념 본문의 요약 표로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-124-claim-17-network-and-token-race-look-alike",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#124-핵심-claim"
|
|
],
|
|
"summary": "Claim 17 — Network virtualization 문제와 Keycloak Refresh Token 경쟁 문제는 별개지만 같은 실험 환경에서 서로 비슷한 증상으로 보일 수 있으므로 계층별 관측이 필요하다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-the-network-path-before-blaming-the-application",
|
|
"reason": "열일곱 중 유일하게 메커니즘이 아니라 판독 규칙이다. §120 이 본문으로 말한 것의 결론 문장이라 그 Reference 의 한 줄로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-126-next-practice-order",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#126-다음-실습-순서"
|
|
],
|
|
"summary": "Physical NIC 확인부터 multi-queue/offload 확인까지 열두 단계의 실습 순서와, 검증되지 않은 항목은 OPEN QUESTION 으로 남기고 결과가 나오면 CASE 로 전환한다는 원칙, 그 다음 K3s/CNI/Service/Pod network 계층을 잇는다는 계획",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "다음에 무엇을 어떤 순서로 잴지의 진행 계획이다. 계획은 실행되면 Case 가 되고 그 전까지는 분석에 남는다. CPU 부의 §23 도 같은 처분이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-128-139-guest-block-io-path",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#128-전체-구조",
|
|
"final/document.md#129-guest-application-read-write-에서-시작",
|
|
"final/document.md#134-guest-block-i-o-layer",
|
|
"final/document.md#137-virtio-blk-guest의-가상-block-device-driver",
|
|
"final/document.md#138-virtio-blk와-virtqueue"
|
|
],
|
|
"summary": "§128 이 그린 canonical flow 의 Guest 절반 — 애플리케이션의 `write()` 가 VFS · ext4/XFS · Guest Page Cache · Guest Block Layer · `/dev/vda` · virtio-blk Frontend 를 거쳐 virtqueue 에 실린다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "경로 중간을 잘라 내면 인과가 끊긴다 — Page Cache 만 떼면 `write()` 성공이 왜 SSD 영속화가 아닌지 말할 수 없고, virtqueue 만 떼면 무엇과 무엇 사이의 queue 인지 말할 수 없다. 독립성 검사를 통과하고, 제4부에서 경로가 시작하는 구간이라 나머지 셋(backend 형태 · 완료의 뜻 · Host block stack)이 이 글을 딛고 갈린다"
|
|
},
|
|
{
|
|
"id": "SSOT-140-142-what-is-behind-dev-vda",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#140-vm-boundary를-넘으면-qemu가-등장",
|
|
"final/document.md#141-qemu가-물리-ssd를-직접-제어하는-것은-아니다",
|
|
"final/document.md#142-qcow2-host에서는-파일-guest에서는-디스크"
|
|
],
|
|
"summary": "VM 경계를 넘으면 QEMU 의 virtio-blk Device Model 과 Block Backend 가 받고, backend 가 파일이면 QEMU 는 Host Linux 에 파일 I/O 를 요청하므로 Guest storage stack 아래에 Host storage stack 이 한 번 더 존재할 수 있다. 같은 대상이 Host 에서는 파일 하나이고 Guest 에서는 디스크다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:qemu-block-backend-forms",
|
|
"reason": "이 구간에서만 경로가 셋(qcow2 · RAW · Host block device)으로 분기하고, 갈래마다 결과가 다르다 — virtual size 와 실제 할당량의 차이 · mapping/metadata 처리 · Host filesystem 이 한 겹 더 있는지. 경로를 그리는 C1 에 넣으면 세 갈래의 결과가 한 글의 각주가 된다. §128 이 핵심 문장으로 뽑은 것이 이 분기다"
|
|
},
|
|
{
|
|
"id": "SSOT-147-157-completion-and-cache-boundaries",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#147-vm에서는-page-cache가-두-번-나타날-수-있다",
|
|
"final/document.md#148-write-완료와-영속화는-다르다",
|
|
"final/document.md#149-direct-i-o",
|
|
"final/document.md#150-fsync-가-필요한-이유",
|
|
"final/document.md#151-flush",
|
|
"final/document.md#152-가장-위험한-상황-거짓-완료"
|
|
],
|
|
"summary": "Page Cache 가 Guest 와 Host 양쪽에 두 번 나타날 수 있고, `write()` 완료 ≠ writeback 완료 ≠ fsync/flush 완료 ≠ 전원 장애에도 안전한 durability 이며, Guest 에게 거짓 완료를 응답하는 것은 성능 문제가 아니라 correctness 문제다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:write-completion-is-not-durability",
|
|
"reason": "C1·C2·C4 가 「데이터가 어디를 지나는가」를 말한다면 이 글은 「완료라는 응답이 무엇을 보장하는가」를 말하므로 물음이 다르다. 없애고 경로 글의 한 절로 넣으면 §148 의 네 경계와 §152 의 correctness 판정이 각주가 되고, cache mode 를 고르는 사람이 다시 찾지 못한다"
|
|
},
|
|
{
|
|
"id": "SSOT-158-167-host-block-stack",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#158-host-block-layer",
|
|
"final/document.md#159-여러-vm이-하나의-nvme를-공유하면",
|
|
"final/document.md#160-blk-mq-multi-queue-block-layer",
|
|
"final/document.md#161-i-o-scheduler",
|
|
"final/document.md#164-nvme-driver와-physical-device",
|
|
"final/document.md#167-storage-contention"
|
|
],
|
|
"summary": "Host Block Layer 는 그 I/O 가 VM 에서 왔는지 Host process 에서 왔는지 본질적으로 구분해 처리하는 계층이 아니라 모두 Host block request 이고, `blk-mq` 와 I/O Scheduler 와 NVMe Driver 를 지나며, 여러 VM 이 한 device 를 쓰면 Storage Contention 이 생긴다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:host-block-stack-under-the-vm",
|
|
"reason": "backend 형태를 말하는 C2 아래에서 요청이 어떻게 줄을 서고 누구와 섞이는지는 다른 물음이다. §158 의 「모두 Host block request 다」가 §167 의 경쟁을 낳고, 그 경쟁은 이 계층을 설명하지 않으면 읽히지 않는다. OQ-5 · OQ-6 · OQ-7 셋이 이 글 위에서 돌아간다"
|
|
},
|
|
{
|
|
"id": "SSOT-168-cpu-is-fine-but-storage-is-slow",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#168-cpu가-정상이어도-storage-때문에-느릴-수-있다"
|
|
],
|
|
"summary": "PostgreSQL 이 storage completion 을 기다리면 CPU usage 가 높지 않을 수 있어 CPU 30% 인데 request latency 2초 가 가능하므로 CPU 지표만으로 latency 원인을 판단하면 안 된다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:read-storage-before-blaming-cpu-for-latency",
|
|
"reason": "원 프로젝트의 이름을 지워도 규칙이 남는다 — DB 가 낀 경로면 어디서든 같은 순서로 자원을 가른다. §167 이 CPU Contention 과 Storage Contention 을 다른 자원 경쟁으로 갈라 적용 조건을 대고 §169 와 §177 이 무엇을 함께 볼지를 댄다. 제2부의 §85·§86 이 세운 기준을 따랐다"
|
|
},
|
|
{
|
|
"id": "SSOT-171-durability-vs-latency-tradeoff",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#171-성능과-durability의-trade-off"
|
|
],
|
|
"summary": "`fsync()` 를 없애서 빨라졌다면 그것이 최적화가 아니라 durability contract 를 제거한 것일 수 있다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:a-speedup-that-removed-durability-is-not-an-optimization",
|
|
"reason": "설정 선택과 성능 개선의 평가에 걸리는 규칙이라 개념 본문의 각주로 두면 다음에 같은 판단을 하는 사람이 찾지 못한다. §152 가 왜 성능 문제가 아닌지를, §156 이 반대 방향의 오독(writeback 이라는 이름만 보고 위험하다고 단정하는 것)을 막아 적용 조건과 예외가 둘 다 SSOT 안에 있다"
|
|
},
|
|
{
|
|
"id": "SSOT-145-backend-cannot-be-inferred-from-the-guest",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#145-host-block-device를-직접-backend로-사용-가능"
|
|
],
|
|
"summary": "「Guest 에 `/dev/vda` 가 있다」는 정보만으로 backend 구조를 알 수 없다 — 그 아래에 qcow2 file · RAW file · Host block device 셋이 올 수 있다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:confirm-the-disk-backend-on-the-host",
|
|
"reason": "OQ-1 · OQ-2 · OQ-3 · OQ-5 넷이 모두 이 규칙 위에서 돌아가므로 네 글에 되풀이하지 않고 한 편으로 두고 가리킨다. §146 이 확정 절차를, §143·§144 가 확정하지 않고 읽었을 때 무엇을 잘못 읽게 되는지를 댄다. 제3부의 reference:bisect-the-packet-path-with-capture-points 와 같은 위치다"
|
|
},
|
|
{
|
|
"id": "SSOT-175-oq-1-vm-disk-backend-mapping",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-1"
|
|
],
|
|
"summary": "OQ-1. VM 의 `/dev/vda` 는 어떤 Host backend 에 연결되어 있는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:vm-disk-backend-mapping",
|
|
"reason": "§175 가 아직 돌리지 않은 확인으로 적어 둔 물음이다. 답이 없고, 답에 따라 §142~§145 중 어느 갈래가 이 Host 에 적용되는지가 갈리며, `lsblk` 와 `virsh domblklist <VM_NAME>` 라는 다음 검증이 있다. 앞선 세 부가 OPEN QUESTION 을 그대로 question 글감으로 올린 것과 같은 기준이고, 다른 부의 question 이 같은 측정으로 닫는 물음도 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-175-oq-2-disk-image-format",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-2"
|
|
],
|
|
"summary": "OQ-2. Backend 는 qcow2 인가 RAW 인가",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:disk-image-format-and-actual-host-usage",
|
|
"reason": "OQ-3 과 같은 한 번의 `qemu-img info` 출력으로 닫힌다 — 형식은 그 출력의 첫 줄이고 OQ-3 이 요구한 세 값 비교의 전제다. 같은 측정으로 닫히는 물음을 두 편으로 만들지 않는다. 제2부가 OQ-1 과 OQ-10 을 한 물음으로 합친 것과 같은 판단이다"
|
|
},
|
|
{
|
|
"id": "SSOT-175-oq-3-disk-image-format-and-actual-host-usage",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-3"
|
|
],
|
|
"summary": "OQ-3. qcow2 Virtual Size 와 실제 Host 사용량은 얼마나 다른가 — `qemu-img info` · `du -h` · `ls -lh` 세 명령이 보여주는 의미가 서로 다를 수 있으므로 비교한다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:disk-image-format-and-actual-host-usage",
|
|
"reason": "§143 이 100 GB 대 3GB 라는 예시로만 말한 차이를 이 Host 에서 확정하는 물음이다. 답이 없고, 답에 따라 저장 공간 계획과 §144 의 성능 일반화 경계가 갈리며, 세 명령이라는 다음 검증이 있다. OQ-2 를 흡수해 한 편으로 둔다"
|
|
},
|
|
{
|
|
"id": "SSOT-175-oq-4-qemu-disk-cache-mode",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-4"
|
|
],
|
|
"summary": "OQ-4. QEMU disk cache mode 는 무엇인가 — `virsh dumpxml <VM_NAME>` 의 disk driver 설정에서 cache 관련 값을 확인한다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:qemu-disk-cache-mode",
|
|
"reason": "§153~§156 이 두 설정의 뜻을 갈라 놓았지만 이 Host 가 무엇을 쓰는지는 적혀 있지 않다. 답에 따라 §147 의 이중 caching 이 이 환경에 있는지가 갈리고 확인 명령이 있다. 값을 어떻게 둘 것인지는 별도로 NEEDS_DECISION 에 남겼다"
|
|
},
|
|
{
|
|
"id": "SSOT-175-oq-5-host-block-device-under-the-disk-image",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-5"
|
|
],
|
|
"summary": "OQ-5. qcow2 가 최종적으로 어느 Host block device 위에 있는가 — `lsblk` 와 `findmnt` 로 확인한다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:host-block-device-under-the-disk-image",
|
|
"reason": "OQ-1 이 image 경로까지 확정한다면 이 물음은 그 경로가 놓인 물리 장치까지 내려간다. 닫는 명령도 결과가 여는 물음도 다르다 — 여기서 나온 device 이름이 OQ-6 의 `<device>` 자리를 채우고 OQ-7 의 전제(두 VM 이 같은 장치를 쓰는가)를 정한다"
|
|
},
|
|
{
|
|
"id": "SSOT-175-oq-6-host-io-scheduler",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-6"
|
|
],
|
|
"summary": "OQ-6. Host I/O Scheduler 는 무엇인가 — `cat /sys/block/<device>/queue/scheduler`",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:host-io-scheduler",
|
|
"reason": "§161·§162 가 정책의 존재와 `none` 의 뜻을 설명했지만 이 Host 의 값은 없다. 답에 따라 OQ-7 의 해석 조건이 달라지고 §163 이 확인 명령과 읽는 법(대괄호 안이 현재 선택된 것)을 함께 적어 다음 검증이 있다"
|
|
},
|
|
{
|
|
"id": "SSOT-175-oq-7-vm1-storage-load-vs-vm2-latency",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-7"
|
|
],
|
|
"summary": "OQ-7. VM1 Storage load 가 VM2 latency 에 영향을 주는가 — VM1 에서 별도의 테스트 파일/디스크로 controlled I/O load 를 발생시키고 VM2 의 application latency 와 Host storage 지표를 동시에 본다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:vm1-storage-load-vs-vm2-latency",
|
|
"reason": "§167 이 구조적 가능성으로만 적은 것을 이 Host 에서 재는 물음이다. 답에 따라 장치 분리나 I/O 제한이 필요한지가 갈리고 실험 설계가 §175 에 적혀 있다. 제1부의 question:vcpu-contention-under-two-vm-load 와 부하 형태가 같지만 재는 자원이 CPU 가 아니라 storage 라 같은 측정으로 닫히지 않는다 — relations 로만 이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-175-oq-8-guest-fsync-latency-vs-host-storage-latency",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#175-실제-테스트-서버에서-확인할-open-questions-oq-8"
|
|
],
|
|
"summary": "OQ-8. Guest `fsync()` latency 와 Host storage latency 가 같이 증가하는가 — Guest application/DB latency 와 Host `iostat` 를 시간축으로 함께 관찰한다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:guest-fsync-latency-vs-host-storage-latency",
|
|
"reason": "§150 이 말한 전달 경로가 이 환경에서 이어지는지를 재는 물음이다. 제2부의 question:host-major-fault-vs-storage-latency 와 가장 가깝지만 그쪽은 Host memory pressure 로 유도한 major fault 가 storage 를 미는지를 보고 이쪽은 애플리케이션의 `fsync()` 가 Host storage 까지 전달되는지를 본다 — 유도 방법도 계열도 달라 같은 실행으로 닫히지 않는다. relations 로만 이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-127-part-scope-and-deferred-internals",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#127-문서-목적"
|
|
],
|
|
"summary": "제4부가 다루는 범위 — Guest 의 `write()`/`fsync()` 가 Host 물리 SSD/NVMe 까지 내려가는 하나의 일관된 경로, 그리고 VFS 부터 Storage contention 까지 열넷 — 과, qcow2 내부 L1/L2 table · blk-mq tag allocator · NVMe submission/completion queue 를 별도 문서로 미룬다는 선언",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "이 부가 무엇을 다루고 무엇을 다루지 않는지의 선언이다. 따로 떼면 아무 것도 설명하지 않는 목차가 되고, 네 개념의 basis-version 과 missing-verification 이 그대로 받는다. 제3부의 §89 와 제2부의 §88 도 같은 처분이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-129-application-does-not-touch-the-device",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#129-guest-application-read-write-에서-시작"
|
|
],
|
|
"summary": "VM 안의 PostgreSQL 이나 Keycloak 은 SSD 나 virtio-blk 를 직접 다루지 않고 `write(fd, buffer, size);` 로 Guest Linux Kernel 에 파일 연산을 요청하며, 이 시점에는 아직 QEMU · qcow2 · Host NVMe 가 등장하지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "경로의 첫 칸이다. 한 문장으로 개념 안에 설명되므로 독립 기록이 되지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-130-vfs-routes-to-the-filesystem",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#130-vfs-공통-파일-인터페이스-계층"
|
|
],
|
|
"summary": "VFS 는 여러 filesystem 을 동일한 API 로 쓰게 하는 공통 계층이고, 이 fd 가 어떤 파일인가 → 어떤 filesystem 에 속하는가 → 해당 구현으로 연산 전달 순으로 동작한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "경로의 두 번째 칸이고 ext4 든 XFS 든 같은 자리를 지난다는 것을 말한다. 개념 본문의 한 절이다"
|
|
},
|
|
{
|
|
"id": "SSOT-131-132-filesystem-and-inode-map-files-onto-blocks",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#131-filesystem-ext4-xfs-파일-세계를-block-공간에-배치",
|
|
"final/document.md#132-inode"
|
|
],
|
|
"summary": "ext4/XFS 가 파일·디렉터리라는 논리 구조를 block 공간에 배치하고, inode 가 파일 metadata 와 저장 위치를 관리하며 파일 이름 자체와 inode 는 같은 것이 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "§132 스스로 「inode 내부 구현까지 파고들 필요는 없지만 filesystem 이 파일과 block 을 연결한다는 점은 알아야 한다」고 범위를 그었다. 그 한 문단이 개념 안에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-133-write-lands-in-page-cache-first",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#133-page-cache-write-가-바로-ssd-write는-아니다"
|
|
],
|
|
"summary": "buffered I/O 에서 `write()` 는 Page Cache 에 먼저 닿아 dirty page 가 되고 나중에 writeback 되므로 `write()` 성공은 Physical SSD 영속화 완료가 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "경로 위의 한 칸이라 C1 에 들어간다. 이 절이 던진 「완료가 무슨 뜻인가」는 §148 이 받아 concept:write-completion-is-not-durability 로 이어지므로 그 글도 이 절을 source 로 가리킨다"
|
|
},
|
|
{
|
|
"id": "SSOT-134-block-layer-turns-file-offsets-into-device-requests",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#134-guest-block-i-o-layer"
|
|
],
|
|
"summary": "Block I/O Layer 가 filesystem 세계의 「offset 8192 에 4KB write」를 block device 세계의 READ/WRITE/FLUSH/DISCARD 요청으로 바꾸고, 내부에는 `bio` · request · queue · `blk-mq` 가 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "두 세계를 잇는 칸이다. 내부 자료구조는 §127 이 범위 밖으로 두었으므로 개념 본문에서도 이름까지만 적는다"
|
|
},
|
|
{
|
|
"id": "SSOT-135-dev-vda-is-not-the-host-ssd",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#135-dev-vda-guest가-보는-가상-block-device"
|
|
],
|
|
"summary": "virtio-blk 를 쓰는 VM 에서는 `/dev/vda` · `/dev/vdb` 처럼 보이고 Guest Linux 는 그것을 하나의 block device 로 인식하지만 그것이 Host 의 실제 SSD 라는 뜻은 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "경로의 칸이면서 reference:confirm-the-disk-backend-on-the-host 가 세우는 규칙의 근거다. 개념에 흡수하고 그 Reference 가 source 로 가리킨다"
|
|
},
|
|
{
|
|
"id": "SSOT-136-block-device-view-and-filesystem-view",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#136-dev-vda-와-filesystem-관계"
|
|
],
|
|
"summary": "`/dev/vda` → partition → filesystem → mount point 로 이어지는 관계이고, `cd /var/lib/postgresql` 은 filesystem 세계를 보는 것이며 `lsblk` 에서 `vda` 를 보는 것은 block device 세계를 보는 것이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "두 관점의 구분이라 개념 안의 그림 한 장으로 들어간다. 독립 기록으로 두면 문단 둘인 글이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-137-driver-and-device-node-are-different-things",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#137-virtio-blk-guest의-가상-block-device-driver"
|
|
],
|
|
"summary": "`/dev/vda` 는 Guest Linux 에 보이는 block device 이고 virtio-blk 는 그 virtual block device 를 제어하는 Guest Kernel driver 다 — Network 의 `ens3` 와 virtio-net 이 같은 관계다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "이름 둘을 가르는 절이다. 개념 본문의 한 절이고, Network 대응은 §173 의 표가 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-138-139-virtqueue-is-descriptors-over-guest-ram",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#138-virtio-blk와-virtqueue",
|
|
"final/document.md#139-virtqueue의-실제-의미"
|
|
],
|
|
"summary": "virtio-blk driver 가 Guest Block Layer 의 요청을 Virtio block request 로 구성해 virtqueue 에 게시하고, virtqueue 는 단순한 데이터 파이프가 아니라 Guest RAM 의 I/O buffer 를 descriptor 가 가리키는 구조이며 처리가 끝나면 backend 가 completion 을 돌려준다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "경로의 마지막 칸이다. 제3부의 concept:guest-packet-path-to-physical-nic 이 같은 구조를 packet 쪽에서 설명하므로 여기서는 storage 요청의 모양만 적고 그 글을 relations 로 가리킨다"
|
|
},
|
|
{
|
|
"id": "SSOT-166-completion-travels-back-through-virtqueue",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#166-storage-i-o-completion"
|
|
],
|
|
"summary": "WRITE 요청은 아래로 내려가고 완료는 NVMe → NVMe Driver → Host Block Layer → QEMU/backend → virtqueue completion → virtio-blk → Guest Block Layer 로 올라오므로 virtqueue 는 request 뿐 아니라 completion 전달 구조까지 포함해 이해해야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "이 절의 결론 문장이 virtqueue 를 가리키므로 C1 의 마지막 절로 들어간다. 왕복 경로 한 장을 기록 하나로 만들지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-172-177-canonical-flow-and-summary",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#172-storage-virtualization-canonical-flow",
|
|
"final/document.md#177-최종-요약"
|
|
],
|
|
"summary": "Guest Userspace 부터 Non-volatile Media 까지의 canonical flow 와 그 역방향 completion, 그리고 write 완료 ≠ writeback 완료 ≠ flush 완료 ≠ 전원 장애에도 살아남는 durability 라는 요약과 CPU usage 만 보지 말고 함께 볼 여덟 가지",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "네 개념이 나눠 설명하는 경로를 한 장으로 그린 지도다. 경로가 시작하는 C1 이 그 그림을 열면서 싣고 나머지 셋은 자기가 그림의 어느 구간인지를 가리킨다. 제2부가 §79 와 §87 을 같은 이유로 첫 개념에 흡수했다"
|
|
},
|
|
{
|
|
"id": "SSOT-173-network-storage-correspondence",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#173-network-virtualization과-비교"
|
|
],
|
|
"summary": "virtio-net ↔ virtio-blk · packet ↔ block I/O request · TX/RX virtqueue ↔ I/O virtqueue · TAP/network backend ↔ QEMU block backend · Linux Bridge/Route ↔ Host filesystem/block stack · Physical NIC ↔ Physical SSD/NVMe · Guest TCP/IP Stack ↔ Guest VFS/Filesystem/Block Layer · send/recv ↔ read/write/fsync 대응표",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:guest-block-io-path-to-virtqueue",
|
|
"reason": "제3부를 읽은 독자를 이 부로 잇는 다리다. SSOT 가 스스로 붙인 단서(학습용 대응 관계이며 각 요소가 1:1 로 같은 종류라는 뜻은 아니다)까지 표와 함께 옮긴다. 표 하나를 독립 기록으로 두면 문단 셋인 글이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-143-virtual-size-is-not-host-allocation",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#143-qcow2-virtual-size와-실제-host-사용량"
|
|
],
|
|
"summary": "qcow2 는 가상 disk size 와 실제 Host 할당량이 다를 수 있고(Guest 가 보는 100 GB 에 Host 실제 3GB), Guest 가 기록하면서 Actual 이 1GB → 10GB → 40GB 로 늘 수 있으므로 `qemu-img info vm1.qcow2` 의 `virtual size` 와 실제 allocation 을 구분해서 봐야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:qemu-block-backend-forms",
|
|
"reason": "backend 가 qcow2 일 때만 생기는 성질이라 그 갈래를 설명하는 개념 안에 들어간다. 이 Host 의 값을 재는 것은 question:disk-image-format-and-actual-host-usage 가 따로 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-144-raw-is-not-automatically-faster",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#144-raw-image"
|
|
],
|
|
"summary": "RAW 는 qcow2 보다 구조가 단순하고 qcow2 는 Copy-on-Write · sparse allocation · snapshot 에 유리하지만 metadata/mapping 처리가 있으며, 「RAW = 무조건 빠름 / qcow2 = 무조건 느림」으로 일반화하면 안 된다 — 실제 성능은 cache mode · storage backend · workload pattern · queue depth · snapshot chain · underlying filesystem · physical device 에 영향을 받는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:qemu-block-backend-forms",
|
|
"reason": "형식 둘을 가르면서 그 구분만으로 성능을 말하지 말라고 닫는 절이다. 개념이 분기를 설명하고 끝나는 자리가 여기라 개념 본문의 마지막 절이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-174-claims-1-3-virtual-disk-and-nested-stacks",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#174-핵심-claim"
|
|
],
|
|
"summary": "Claim 1~3 — Guest 의 `/dev/vda` 는 virtual block device 이고 실제 Host backend 는 qcow2 · RAW · Host block device 등이 될 수 있다 · `virtio-blk + virtqueue` 가 Guest block I/O 를 Host backend 와 연결한다 · qcow2 가 Host filesystem 위의 파일이면 Guest filesystem 아래에 Host filesystem/storage stack 이 한 번 더 존재한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:qemu-block-backend-forms",
|
|
"reason": "셋 다 메커니즘 서술이고 §135·§138·§141·§142·§145 가 이미 본문으로 말한 것을 한 화면으로 접은 것이다. 「이 환경에서 실제로 그러하다」고 주장한 것이 없어 NEEDS_EVIDENCE 로 남길 것도 없다. 개념 본문의 요약 표로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-153-155-qemu-cache-mode",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#153-qemu-cache-mode",
|
|
"final/document.md#154-cache=none",
|
|
"final/document.md#155-cache=writeback"
|
|
],
|
|
"summary": "`cache=none` 과 `cache=writeback` 은 이름 그대로 읽으면 부정확하고 핵심은 QEMU 가 Host Page Cache 와 write completion/flush semantics 를 어떻게 쓰는가다 — `cache=none` 은 우회 방향이라 이중 caching 을 줄이지만 즉시 durable media 반영이 아니고, `cache=writeback` 은 Host Page Cache 를 쓰지만 Guest 의 `fsync()`/FLUSH 를 무시한다는 뜻이 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:write-completion-is-not-durability",
|
|
"reason": "완료의 뜻을 설명하는 개념 안에서 설정 둘이 그 뜻을 어떻게 바꾸는지의 절이 된다. 설정 이름 셋을 각각 기록으로 만들면 같은 물음에 답하는 글이 셋이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-156-writeback-is-not-automatically-unsafe",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#156-writeback-=-위험-이라고-단정하면-안-되는-이유"
|
|
],
|
|
"summary": "정확한 표현은 「writeback caching 에서는 volatile cache 가 존재할 수 있으므로 Guest 의 flush/fsync semantics 가 전체 backend/storage stack 에서 올바르게 보존되는지가 중요하다」이며, Guest 가 요구한 durability 가 Guest Filesystem → Guest Block Layer → virtio → QEMU/backend → Host Storage → Device chain 에서 깨지지 않아야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:write-completion-is-not-durability",
|
|
"reason": "개념이 그리는 chain 그대로다. 이 절이 세운 판독 규칙은 reference:a-speedup-that-removed-durability-is-not-an-optimization 이 예외 항목으로 받아 두 곳에 같은 문장을 두지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-157-device-side-cache",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#157-device-side-cache"
|
|
],
|
|
"summary": "Host Page Cache 를 우회해도 Storage controller/device 가 volatile write cache 를 가질 수 있어 RAM 에서 나갔다 ≠ Device 에 command 가 전달됐다 ≠ 전원이 끊겨도 살아남는 상태가 됐다 이고, device flush/FUA semantics 와 power-loss protection 여부도 중요할 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:write-completion-is-not-durability",
|
|
"reason": "§148 의 네 경계 뒤에 하나가 더 있다는 절이라 같은 개념의 마지막 칸이다. 따로 떼면 앞의 세 경계를 다시 설명해야 읽힌다"
|
|
},
|
|
{
|
|
"id": "SSOT-170-postgresql-wal-durability",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#170-postgresql-예시-wal과-durability"
|
|
],
|
|
"summary": "PostgreSQL 은 WAL 등의 durability protocol 을 쓰며 필요한 시점에 storage synchronization 을 수행하고, VM storage layer 가 flush/fsync semantics 를 제대로 보존하지 않으면 PostgreSQL 의 durability assumption 과 실제 storage behavior 가 어긋날 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:write-completion-is-not-durability",
|
|
"reason": "개념이 말한 보장이 깨질 때 애플리케이션에서 무엇이 어긋나는지를 보이는 예시다. 예시 하나를 독립 기록으로 만들지 않고 개념 본문의 적용 절로 넣는다"
|
|
},
|
|
{
|
|
"id": "SSOT-174-claims-4-5-cache-and-durability",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#174-핵심-claim"
|
|
],
|
|
"summary": "Claim 4~5 — Guest 와 Host 양쪽에 Page Cache 가 존재할 수 있고 Direct I/O 와 QEMU cache mode 는 Host Page Cache 사용 방식과 연결된다 · `write()` 완료와 durability 는 같은 의미가 아니다(write ≠ writeback ≠ fsync/flush 완료 ≠ 전원 장애에도 안전한 상태)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:write-completion-is-not-durability",
|
|
"reason": "둘 다 메커니즘 서술이고 §147~§157 이 본문으로 말한 것을 접은 것이다. 개념 본문의 요약 표로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-159-one-nvme-serves-many-sources",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#159-여러-vm이-하나의-nvme를-공유하면"
|
|
],
|
|
"summary": "VM1 QEMU · VM2 QEMU · Nginx · Host 기타 프로세스의 요청이 같은 Host Block Layer queue 에서 관리되어 NVMe 로 dispatch 된다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:host-block-stack-under-the-vm",
|
|
"reason": "§158 의 「모두 Host block request 다」가 그림으로 이어진 것이다. 개념 본문의 한 절이고, 이 Host 에서 실제로 겹치는지는 question:vm1-storage-load-vs-vm2-latency 가 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-160-blk-mq-parallel-queues",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#160-blk-mq-multi-queue-block-layer"
|
|
],
|
|
"summary": "`blk-mq` 는 CPU 마다 queue 를 두어 여러 CPU 가 병렬로 block I/O 를 처리하게 하며 NVMe 가 높은 병렬성과 queue depth 를 지원하기 때문에 중요하고, Storage 처리 역시 CPU scheduling 과 완전히 독립된 세계는 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:host-block-stack-under-the-vm",
|
|
"reason": "Host block stack 의 한 칸이다. 마지막 문장이 제1부의 CPU 개념과 이어지는 자리라 개념의 relations 로 잇고 본문은 여기 한 절로 둔다"
|
|
},
|
|
{
|
|
"id": "SSOT-161-io-scheduler-reorders-requests",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#161-i-o-scheduler"
|
|
],
|
|
"summary": "여러 I/O request 가 있다고 해서 항상 들어온 순서 그대로 device 에 전달되는 것은 아니고 I/O Scheduler 가 dispatch 정책을 가지며 대표적으로 `none` · `mq-deadline` · `bfq` 를 볼 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:host-block-stack-under-the-vm",
|
|
"reason": "Host block stack 의 한 칸이다. 이 Host 의 값은 question:host-io-scheduler 가 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-162-none-scheduler",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#162-none"
|
|
],
|
|
"summary": "`none` 은 복잡한 scheduling 정책을 최소화해 비교적 직접 device 쪽으로 dispatch 하는 방향이고 NVMe 처럼 device 자체가 강한 병렬성과 queueing 을 가지면 적합할 수 있지만, block layer 가 아무 일도 하지 않는다는 뜻은 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:host-block-stack-under-the-vm",
|
|
"reason": "scheduler 값 하나의 뜻이다. 개념 본문에서 §161 의 목록에 붙는 한 문단이고 독립 기록이 될 분량도 물음도 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-163-read-the-active-io-scheduler",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#163-실제-i-o-scheduler-확인"
|
|
],
|
|
"summary": "`cat /sys/block/nvme0n1/queue/scheduler` 의 출력 `[none] mq-deadline` 에서 대괄호 안이 현재 선택된 scheduler 이고, SATA/SCSI device 라면 `cat /sys/block/sda/queue/scheduler` 처럼 확인한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:host-block-stack-under-the-vm",
|
|
"reason": "명령 하나와 출력 읽는 법이다. 개념 본문에 두고, 이 Host 에서 실제로 돌리는 것은 question:host-io-scheduler 가 next-verification 으로 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-164-nvme-driver-is-a-host-kernel-driver",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#164-nvme-driver와-physical-device"
|
|
],
|
|
"summary": "NVMe Driver 는 Host Linux Kernel 의 device driver 이고 Network 에서 physical NIC driver 가 하드웨어를 제어하는 것과 동일한 계층적 위치다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:host-block-stack-under-the-vm",
|
|
"reason": "Host block stack 의 마지막 칸이다. 한 문단으로 개념 안에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-165-nvme-is-a-protocol-ssd-is-a-device-class",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#165-nvme와-ssd-구분"
|
|
],
|
|
"summary": "SSD 는 저장장치의 넓은 종류이고 NVMe 는 PCIe 기반 non-volatile storage 를 위한 protocol/interface 이며, SSD 아래에 SATA SSD(SATA/AHCI)와 NVMe SSD(PCIe + NVMe)가 갈린다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:host-block-stack-under-the-vm",
|
|
"reason": "이 계층의 이름을 정리하는 절이다. 용어 구분 하나를 독립 기록으로 만들지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-167-storage-contention-is-not-cpu-contention",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#167-storage-contention"
|
|
],
|
|
"summary": "여러 VM 이 동일한 Physical NVMe 를 쓰면 storage resource 경쟁이 생겨 VM1 의 대량 I/O 가 VM2 의 storage latency 를 올릴 수 있고, CPU Contention(Host logical CPU 실행 시간 경쟁)과 Storage Contention(IOPS/bandwidth/queue/device 처리시간 경쟁)은 다른 자원 경쟁이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:host-block-stack-under-the-vm",
|
|
"reason": "§158·§159 가 세운 구조의 결과라 같은 개념 안에서 닫힌다. 여기서 나온 판독 규칙은 reference:read-storage-before-blaming-cpu-for-latency 가 받고, 이 Host 에서 재는 것은 question:vm1-storage-load-vs-vm2-latency 가 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-146-verify-the-guest-to-host-disk-link",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#146-실제-연결-확인"
|
|
],
|
|
"summary": "Guest 의 `lsblk` 와 Host 의 `virsh domblklist <VM_NAME>` 로 Target 과 Source 를 이어 `/dev/vda` → virtio-blk → QEMU → `/var/lib/libvirt/images/vm1.qcow2` 관계를 확인한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:confirm-the-disk-backend-on-the-host",
|
|
"reason": "명령 둘과 판독 절차 자체가 §145 규칙의 실행 방법이다. 그 Reference 의 scope 가 요구하는 첫 확인이라 본문 절로 들어간다. 제3부에서 §118 의 명령 목록을 §119 의 Reference 로 넣은 것과 같다"
|
|
},
|
|
{
|
|
"id": "SSOT-169-storage-observation-commands",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#169-storage-관측-명령어"
|
|
],
|
|
"summary": "device I/O 관측은 `iostat -xz 1` 로 read/write throughput · IOPS · request latency · queue 상태 · device utilization 성격의 지표를 보고, 어떤 process 가 I/O 를 내는지는 `iotop` 으로 본다. Guest 는 `lsblk` · `mount` · `df -h` · `cat /proc/mounts` · `iostat -xz 1`, Host 는 `virsh domblklist <VM_NAME>` · `qemu-img info <disk-image>` · `lsblk` · `cat /sys/block/<device>/queue/scheduler` · `iostat -xz 1` · `iotop`",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:read-storage-before-blaming-cpu-for-latency",
|
|
"reason": "명령 목록 자체는 규칙이 아니다. §168 의 규칙이 무엇을 함께 보라고 요구하고 이 목록이 그 도구를 대므로 그 Reference 의 본문 표가 된다. 제3부의 §118 도 같은 처분이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-174-claim-6-storage-perf-is-not-guest-only",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#174-핵심-claim"
|
|
],
|
|
"summary": "Claim 6 — Storage 성능은 Guest 내부만으로 결정되지 않고 QEMU/backend · Host block queue · I/O scheduler · NVMe · cache · 다른 VM 의 storage load 가 함께 영향을 준다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:read-storage-before-blaming-cpu-for-latency",
|
|
"reason": "여섯 Claim 중 유일하게 메커니즘이 아니라 판독 규칙이다. §168 이 본문으로 말한 것의 결론 문장이라 그 Reference 의 한 줄로 들어간다. 제3부의 Claim 17 과 같은 처분이다"
|
|
},
|
|
{
|
|
"id": "SSOT-176-recommended-practice-order",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#176-권장-실습-흐름"
|
|
],
|
|
"summary": "Guest 에서 `/dev/vda` 확인부터 DB fsync latency 와 Host storage latency 상관관계 확인까지 여덟 단계의 권장 실습 흐름",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "다음에 무엇을 어떤 순서로 잴지의 진행 계획이다. 계획은 실행되면 Case 가 되고 그 전까지는 분석에 남는다. 제1부의 §23 · 제2부의 §84 · 제3부의 §126 도 같은 처분이었다"
|
|
},
|
|
{
|
|
"id": "SSOT-153-157-cache-mode-choice-not-made",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#153-qemu-cache-mode",
|
|
"final/document.md#156-writeback-=-위험-이라고-단정하면-안-되는-이유"
|
|
],
|
|
"summary": "이 환경의 VM disk cache mode 를 `cache=none` 으로 둘 것인가 `cache=writeback` 으로 둘 것인가",
|
|
"disposition": "NEEDS_DECISION",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "SSOT 는 두 설정의 뜻과 각각의 오해를 갈라 놓기만 했고 어느 쪽을 쓰기로 정했다고 적지 않았다. §156 은 오히려 이름만 보고 단정하지 말라고 못박아 방향을 제시하지 않는다. 감수한 비용도 적혀 있지 않다. 게다가 현재 값이 무엇인지조차 확인되지 않았다 — question:qemu-disk-cache-mode 가 그것을 먼저 답해야 한다. 권고 없는 서술을 Decision 으로 올리면 「기술이 존재한다」를 근거로 바꾸는 실수가 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-178-part5-provenance-and-scope",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#178-이-부의-출처와-범위"
|
|
],
|
|
"summary": "제5부가 무엇을 보고 쓴 것인지 — 기반 7단계 가이드·실측 기록·개념 누적·설정 원본·리비전의 자리, 그리고 막히지 않은 단계는 여기 적지 않는다는 범위 선언",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "무엇을 보고 썼고 무엇을 뺐는지는 분석의 범위 기록이다. 읽는 사람이 아니라 다음에 이 SSOT 를 여는 사람을 위한 자리라 독립 기록이 되지 않는다. 제1부의 §1(이 문서의 범위)이 candidateScope 의 excluded 로 빠진 것과 같은 성격이지만, 이 절은 관측된 환경을 함께 담고 있어 범위 안에 두고 그 환경 쪽만 따로 후보로 갈랐다"
|
|
},
|
|
{
|
|
"id": "SSOT-178-lab-host-observed-environment",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#178-이-부의-출처와-범위"
|
|
],
|
|
"summary": "이 실험대의 관측된 환경 — `test-server`(Arch Linux, i5-1135G7 논리 코어 8, RAM 11,648MiB, QEMU 11.1.1 · libvirt 12.7.0), 이더넷 없이 WiFi 만 있어 브리지 대신 libvirt NAT(`virbr0`) + 호스트 진입, 게스트는 Debian 12 genericcloud 3대(엣지 1 · k3s 2)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:edge-nginx-moved-into-a-guest-vm",
|
|
"reason": "환경 서술 자체는 기록이 아니라 전제다. 그런데 이 전제 가운데 「이더넷이 없어 브리지를 못 쓴다」가 그 결정의 제약이자 감수한 비용 2·3번이 생긴 이유라 그 기록의 전제 절로 들어간다. 제5부의 다른 기록은 relations 로 그 절을 가리킨다"
|
|
},
|
|
{
|
|
"id": "SSOT-179-move-edge-nginx-into-a-guest-vm",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나",
|
|
"final/document.md#178-이-부의-출처와-범위"
|
|
],
|
|
"summary": "엣지 nginx 를 물리 호스트에서 게스트 VM(.10) 으로 옮기고 호스트는 커널 DNAT 만 하게 한다 — 이유는 성능이 아니라 자주 갈아엎는 층의 격리",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:edge-nginx-moved-into-a-guest-vm",
|
|
"reason": "이 프로젝트의 첫 Decision 이다. 방향을 실제로 골라 이미 그렇게 서 있고(§178 의 게스트 3대), 근거가 SSOT 에 직접 적혀 있고(더러워지는 층의 격리 — 호스트에서는 초기화가 불가능하고 엣지 장애 실험이 SSH 를 위험하게 만든다), 감수한 비용이 일곱 줄의 표로 적혀 있다. 「기술이 존재한다」를 근거로 바꾼 것이 아니라 SSOT 가 스스로 「바꾼 이유는」이라고 적은 문장을 그대로 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-179-l7-hop-count-unchanged",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나"
|
|
],
|
|
"summary": "L7 홉 수는 전후 모두 2홉 그대로이고 늘어난 것은 커널이 하는 L4 전달 한 번뿐이라 `X-Forwarded-*` 계약이 그대로 성립한다(observed)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:edge-nginx-moved-into-a-guest-vm",
|
|
"reason": "이 관측이 혼자 답하는 물음이 없다. 「성능 때문이 아니다」를 떠받치는 근거라 그 결정의 grounds 안에서만 뜻이 있다. 떼어 내면 무엇과 무엇을 견준 홉 수인지 말할 수 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-179-seven-new-requirements",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나"
|
|
],
|
|
"summary": "이동으로 새로 필요해진 일곱 — nginx 설치 · DNAT · libvirt 방화벽에 구멍 · SNAT 금지 명시 · `sites-available` 관례 · nginx 버전 차이(Arch 1.30 vs Debian 12 의 1.22, `http2 on;` 지시어가 1.25.1 이상) · certbot 과 갱신 훅의 이전",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:edge-nginx-moved-into-a-guest-vm",
|
|
"reason": "목록 그대로는 이 이동의 감수한 비용이지 다음 프로젝트의 규칙이 아니다. 일곱 중 넷은 SSOT 자신이 「배포판이 달라서 생긴 잡무」라고 적었고, 그것을 일반 규칙으로 올리면 Arch→Debian 이라는 한 번의 조합을 규칙으로 승격하는 것이 된다. 그 결정의 감수한 비용 표로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-179-no-snat-on-the-dnat-path",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나"
|
|
],
|
|
"summary": "L4 를 한 번 더 태우면서 masquerade 를 붙이면 엣지가 모든 클라이언트를 `192.168.122.1` 로 보게 되므로 SNAT 를 붙이지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:edge-nginx-moved-into-a-guest-vm",
|
|
"reason": "규칙의 모양을 하고 있지만 SSOT 에 한 줄뿐이라 적용 조건과 예외를 댈 자료가 없다. 실제로 붙여 보고 클라이언트 주소가 뭉개지는 것을 관측한 기록도 없다. 그 결정의 가드레일 한 줄로 들어가고, 다른 구성에서도 성립하는지는 재 본 뒤에 다시 판정한다"
|
|
},
|
|
{
|
|
"id": "SSOT-179-output-to-forward-path-change",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나"
|
|
],
|
|
"summary": "「호스트가 게스트에 접속한다」는 OUTPUT 경로이고 「밖에서 게스트로 들어온다」는 FORWARD 경로라, 커널이 보기에 완전히 다른 일이다(inferred)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:nftables-accept-did-not-stop-the-libvirt-reject",
|
|
"reason": "세 문장으로 끝나는 배경이라 Concept 의 조건인 「처음부터 설명해야 Case 를 이해할 수 있는 구조」에 못 미친다. 한두 문장으로 Case 안에서 설명되는 것은 Concept 이 아니다. 이 사실이 실제 비용으로 청구된 자리가 그 Case 라 거기 배경 절로 넣는다"
|
|
},
|
|
{
|
|
"id": "SSOT-180-outside-only-connection-refused",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다",
|
|
"final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나"
|
|
],
|
|
"summary": "호스트에서는 404 로 응답하는 엣지가 밖에서는 connection refused 였고, 범인은 libvirt `guest_input` 체인 끝의 `reject`(카운터 4 패킷 240 바이트가 밖에서 친 curl 횟수와 일치)였다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:nftables-accept-did-not-stop-the-libvirt-reject",
|
|
"reason": "이 프로젝트의 첫 Case 다. 하나의 문제 · 관측(두 자리에서 친 curl 의 결과가 갈린다) · 진단(카운터가 횟수와 일치해 범인 확정) · 결론(구멍을 맨 앞에 insert 하고 ExecStartPost 로 다시 넣는다)이 한 절 안에서 닫힌다. 다른 부의 Case 가 0 이었던 이유는 이 Host 에서 잰 값이 없어서인데 이 절에는 관측값이 있다"
|
|
},
|
|
{
|
|
"id": "SSOT-180-nftables-evaluates-every-base-chain-on-a-hook",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다"
|
|
],
|
|
"summary": "nftables 는 같은 훅에 붙은 base 체인을 우선순위 순으로 전부 평가하고, 앞 체인의 `accept` 는 「이 체인은 통과」일 뿐 `drop` 만이 즉시 종결이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:nftables-accept-did-not-stop-the-libvirt-reject",
|
|
"reason": "이것은 그 Case 의 진단 자체다. 떼어 내면 Case 에는 증상과 해결만 남고 왜 우리 규칙이 안 먹혔는지가 빠진다. 진단을 별도 Concept 으로 올리면 한 사건을 둘로 쪼개는 것이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-180-guest-input-hole-is-volatile",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다"
|
|
],
|
|
"summary": "libvirt 체인에 넣은 규칙은 libvirt 가 네트워크를 다시 세우면 `guest_input` 을 새로 쓰면서 날아가므로 DNAT 유닛의 `ExecStartPost` 에 넣는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:nftables-accept-did-not-stop-the-libvirt-reject",
|
|
"reason": "그 Case 의 해결 절이다. 수정이 재기동을 견디는 방법까지가 결론이라 떼면 Case 의 결론이 닫히지 않는다. 남의 체인에 넣은 규칙 일반으로 넓히기에는 SSOT 가 libvirt 한 경우만 관측했다"
|
|
},
|
|
{
|
|
"id": "SSOT-181-what-a-qcow2-file-carries",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#181-qcow2-가-담는-것과-담지-않는-것"
|
|
],
|
|
"summary": "qcow2 는 매핑표와 데이터 클러스터가 같은 파일 안에 있고 표의 값이 파일 안 오프셋이라 통째로 옮겨도 유효하며, 파일 밖을 가리키는 것은 백킹 파일 경로 하나뿐이다. 따라가는 것과 따라가지 않는 것이 갈린다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:what-a-qcow2-file-carries",
|
|
"reason": "§183 의 물음 둘(virsh save 덤프 비용 · WiFi 로 qcow2 이동 시간)을 먼저 고른 뒤 거꾸로 물어서 나왔다. 그 둘은 「무엇이 파일에 있고 무엇이 없는가」를 모르면 무엇을 재는지조차 말할 수 없다. 제4부의 개념 넷은 Guest→Host 경로를 설명하고 이 글은 그 위에 이식 경계를 얹으므로 자리가 겹치지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-181-sparse-is-not-compression",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#181-qcow2-가-담는-것과-담지-않는-것"
|
|
],
|
|
"summary": "20GB 이미지가 2GB 인 것은 희소 할당이지 압축이 아니고, 1TB 를 채우면 1TB 파일이 된다. 메타데이터 오버헤드는 0.02% 미만(1TiB 당 약 160MiB)이며 게스트에서 지워도 파일은 줄지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:what-a-qcow2-file-carries",
|
|
"reason": "같은 파일 형식의 성질이라 그 개념의 한 절이다. 따로 두면 「무엇이 담기나」와 「얼마나 담기나」가 갈려 둘 다 반쪽이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-181-moving-running-state-needs-save-or-live-migrate",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#181-qcow2-가-담는-것과-담지-않는-것"
|
|
],
|
|
"summary": "실행 상태까지 옮기려면 qcow2 복사로는 안 되고 `virsh save`→복사→`restore`(VM 이 멈추고 RAM 크기만큼 파일이 더 생긴다) 또는 `virsh migrate --live --copy-storage-all`(두 호스트 libvirt 연결과 CPU 모델 호환 필요)이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:what-a-qcow2-file-carries",
|
|
"reason": "「실행 중인 프로세스는 파일에 없다」의 뒷면이라 같은 개념 안에서 닫힌다. 이 호스트에서 실제로 얼마가 드는지는 question:virsh-save-ram-dump-size-and-time 이 따로 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-181-onprem-to-cloud-image-import",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#181-qcow2-가-담는-것과-담지-않는-것"
|
|
],
|
|
"summary": "온프렘 이미지를 클라우드로 올릴 때의 포맷(AWS raw·VMDK·VHD, Azure 고정 크기 VHD, GCP import 도구)과 실제 작업량이 있는 곳(드라이버·게스트 에이전트·cloud-init datasource·고정 IP→DHCP·fstab/GRUB UUID), 그리고 컷오버는 반드시 재부팅이라는 것",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "SSOT 자신이 (external, 코드 관측 아님) 으로 표시한 부분이다. 이 실험대에서 한 번도 해 보지 않은 절차라 기록으로 올리면 관측한 것과 외부 지식이 한 글 안에서 같은 무게를 갖게 된다. concept:what-a-qcow2-file-carries 의 basis 밖이라고 적고 SSOT 에만 남긴다"
|
|
},
|
|
{
|
|
"id": "SSOT-182-verify-a-build-guide-in-execution-order",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#182-이-구축에서-드러난-문서-결함의-공통-원인"
|
|
],
|
|
"summary": "단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다 — 각 단계에서 「이 시점에 이 리소스가 존재하는가」와 「이 셸에서 이 명령이 도는가」를 따로 본다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-a-build-guide-in-execution-order",
|
|
"reason": "SSOT 가 규칙문으로 직접 적었고 적용 조건(앞 단계 위에 뒤 단계가 서는 문서)과 예외(명령의 정확성과 배포판 차이는 이 축에서 안 잡힌다)를 자료가 댄다. 원 프로젝트의 이름(hyeonworks·BFF·Tailscale·가이드 번호)을 지워도 규칙이 남아 Case 요약을 선언문으로 바꾼 것이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-182-six-guide-defects",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#182-이-구축에서-드러난-문서-결함의-공통-원인"
|
|
],
|
|
"summary": "가이드를 순서대로 따라가며 나온 결함 여섯 — nginx 설치 단계 부재, `http2 on;`, 인증서 lineage 경로, 저장소 위치 가정, 없는 리소스 조회, 확인 명령을 칠 셸",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-a-build-guide-in-execution-order",
|
|
"reason": "여섯이 같은 물음(왜 순서대로 따라가면 막히나)에서 나와 같은 결론(틀린 것은 명령이 아니라 놓인 위치)에 닿으므로 하나다. 표의 행 하나가 될 것을 기록 하나로 만들지 않는다. 그리고 그 하나가 남기는 것이 사건이 아니라 규칙이라 Case 가 아니라 그 Reference 의 근거 표로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-180-183-oq-1-iptables-firewall-backend",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#183-이-부에서-파생될-open-question-oq-1",
|
|
"final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다"
|
|
],
|
|
"summary": "libvirt `firewall_backend` 가 iptables 일 때도 `guest_input` 구멍이 필요한가, 아니면 그때는 우리 `forward` 체인의 `accept` 가 실제로 먹는가",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:guest-input-hole-under-the-iptables-backend",
|
|
"reason": "§180 가 미확인으로 남긴 것을 §183 이 물음으로 다시 적었다. 답이 아직 없고 한 번의 측정으로 닫히며, 답에 따라 그 Case 의 해결이 이 호스트에만 적용되는지 일반인지가 갈린다. 다른 부의 question 가운데 같은 측정으로 닫히는 것이 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-183-oq-2-virsh-save-ram-dump-cost",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#183-이-부에서-파생될-open-question-oq-2",
|
|
"final/document.md#181-qcow2-가-담는-것과-담지-않는-것"
|
|
],
|
|
"summary": "`virsh save`/`restore` 의 RAM 덤프 크기와 소요 시간이 할당 메모리와 어떻게 비례하는가 — 제2부의 balloon 실사용값과 대조하면 대조군이 된다(미측정)",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:virsh-save-ram-dump-size-and-time",
|
|
"reason": "§181 가 「RAM 크기만큼 파일이 더 생긴다」고만 적고 이 호스트의 수치를 대지 않았다. 제2부의 question:balloon-target-vs-guest-available-memory 와 같은 실험 구간에서 재지만 닫는 물음이 다르다 — 저쪽은 balloon target 과 게스트 available 의 차이를, 이쪽은 덤프 크기가 할당량과 실사용량 중 무엇을 따라가는지를 묻는다. §183 이 그 둘을 대조군으로 쓰라고 직접 적었으므로 relations 로 잇고 합치지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-183-oq-3-qcow2-transfer-over-wifi",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#183-이-부에서-파생될-open-question-oq-3",
|
|
"final/document.md#181-qcow2-가-담는-것과-담지-않는-것",
|
|
"final/document.md#178-이-부의-출처와-범위"
|
|
],
|
|
"summary": "WiFi 전용 호스트에서 대용량 qcow2 이동이 현실적으로 몇 시간인가(미측정)",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:qcow2-transfer-time-over-wifi",
|
|
"reason": "답에 따라 설계가 갈린다 — 옮기는 것이 현실적이면 백업·이전이 절차가 되고, 아니면 이 실험대는 문서로만 복원되므로 reference:verify-a-build-guide-in-execution-order 의 검증이 전제가 된다. OQ-2 와 같은 실험 계열이지만 재는 것이 다르다(한쪽은 같은 호스트의 RAM 덤프, 한쪽은 다른 기계로 가는 파일 전송)라 한 번의 측정으로 함께 닫히지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-184-part6-provenance-and-scope",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"summary": "제6부가 무엇을 보고 쓴 것인지 — 기반 7단계 가이드 묶음(`00-lab-host`~`06-observability`, 3,223줄)·설정 원본 네 개·리비전 `9465582b5d1630eb4ae7c4e078021486919bf6b6` 의 자리, 그리고 여기 안 적는 것(실험 26건, 값을 적지 않는 비밀, `source/` 에 반입되지 않은 매니페스트 둘과 cloud-init 템플릿)",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "무엇을 보고 썼고 무엇을 뺐는지는 분석의 범위 기록이다. 제5부의 §178 을 같은 처분으로 둔 것과 같은 성격이라 §184 도 candidateScope 의 excluded 로 빼지 않고 범위 안에 두되 이 출처·범위 쪽은 독립 기록으로 만들지 않는다. 이 절이 함께 담은 검증 방식은 따로 후보로 갈랐다"
|
|
},
|
|
{
|
|
"id": "SSOT-184-verification-asymmetry-read-vs-create",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#184-이-부의-출처와-범위",
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약"
|
|
],
|
|
"summary": "읽기 전용 확인은 돌아가는 실험대에서 실제로 실행해 출력을 그대로 실었고, 만드는 명령은 다시 치면 지금 돌고 있는 실험대가 없어지므로 구축할 때 쓴 것을 옮기고 결과 상태를 확인하는 것으로 대신했다 — 그래서 §186~§192 의 생성 명령은 「그때 이렇게 쳤다」까지이고 「지금 다시 쳐도 같은 상태가 된다」는 확인되지 않았다(unknown)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:does-the-guide-rebuild-this-lab",
|
|
"reason": "이 문단은 §194 의 첫 물음이 존재하는 이유 그 자체다. 떼어 내면 그 질문의 known 에 「왜 아직 확인되지 않았는가」가 빠지고 물음이 「안 해 봤다」로만 남는다. 같은 물음에 답하는 자료라 그 Question 의 known 으로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-185-two-forms-of-a-check-command",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약",
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat"
|
|
],
|
|
"summary": "확인 명령을 두 종류로 갈라 적는다 — 실무자가 치는 짧은 형태(`curl -I`)와 근거를 남기려고 여러 번 재는 긴 형태(`curl -s -o /dev/null -w '%{http_code}\\n'`). 값만 뽑는 뒤엣것은 골라 놓은 한 칸 말고는 전부 버리므로 무엇이 잘못됐는지 모르는 상태에서는 쓸 것이 못 된다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:check-the-nearest-layer-first",
|
|
"reason": "SSOT 가 두 규약을 직접 이었다 — 「03 의 층별 확인이 `-I` 로 시작해 `%{http_code}` 로 줄어드는 순서가 그래서 나온다」. 층을 좁혀 가는 절차의 첫 칸이 이 선택이라 그 Reference 의 적용 절로 들어간다. 따로 두면 왜 `-I` 로 시작하는지가 절차에서 빠진다"
|
|
},
|
|
{
|
|
"id": "SSOT-185-no-placeholders-secrets-by-length",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약",
|
|
"final/document.md#188-단계-02-k3s-server-와-agent",
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql"
|
|
],
|
|
"summary": "자리표시자를 두지 않고 값을 찾는 명령을 함께 적는다. 비밀은 길이나 존재 여부만 확인하고 값을 찍지 않는다 — `echo \"${#TOKEN} 자\"`, Secret 의 `19 bytes`·`22 bytes`",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:an-empty-token-installed-the-agent-anyway",
|
|
"reason": "이 표기 규약이 실제 가드로 바뀐 자리가 그 Case 다 — 값을 찍지 않고 길이만 재는 습관이 `[ ${#TOKEN} -ge 50 ]` 한 줄을 낳았고 그 한 줄이 빈 토큰을 잡는다. 규약만 떼어 내면 무엇을 막았는지 말할 수 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-185-label-every-block-with-its-shell",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약"
|
|
],
|
|
"summary": "모든 코드 블록에 어디서 치는지를 붙인다 — `[워크스테이션]`·`[lab host]`·`[kc-lab-edge]`·`[kc-lab-1]`·`[kc-lab-2]`, 기본은 `[lab host]`. 게스트는 libvirt NAT(`192.168.122.0/24`) 안에 있어 워크스테이션에서 직접 닿지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-a-build-guide-in-execution-order",
|
|
"reason": "제5부의 이 Reference 가 검사하는 축이 둘이고 그중 하나가 「이 셸에서 이 명령이 도는가」다. 이 규약은 그 축을 문서가 스스로 지키게 하는 표기 방법이라 검사 규칙과 표기 규칙으로 짝이 된다. 새 Reference 로 세우면 같은 축을 두 편이 나눠 갖게 된다. 그 기록은 이미 쓰여 있어 이 행은 다음 개정에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-185-run-remotely-from-the-lab-host",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약",
|
|
"final/document.md#188-단계-02-k3s-server-와-agent"
|
|
],
|
|
"summary": "게스트 안에서 `ssh kc-lab-1 '...'` 를 치면 `Host key verification failed.` 로 끝나는데, `TOKEN=$(...)` 로 감싸면 오류는 stderr 로 흘러가고 `TOKEN` 에는 빈 문자열이 담긴다. 02 의 agent 설치가 `--token ''` 을 받아 `level=fatal msg=\"Error: --token is required\"` 로 죽지만 설치 스크립트는 그 전까지를 다 성공으로 찍고 끝나고, 유닛은 `Restart=always` 라 5초마다 조용히 재시도한다(observed)",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:an-empty-token-installed-the-agent-anyway",
|
|
"reason": "하나의 문제 · 관측(설치 출력은 성공인데 `kubectl get nodes` 에 노드가 안 는다) · 진단(빈 변수를 셸이 불평하지 않고, 설치 스크립트가 앞 단계를 성공으로 찍고, 유닛이 조용히 재시도한다) · 결론(게스트에 들어가지 않고 lab host 에서 치고, 길이 가드를 앞에 둔다)이 닫힌다. 제6부에서 「성공으로 보이는 실패」가 가장 또렷하게 관측된 자리다"
|
|
},
|
|
{
|
|
"id": "SSOT-185-exit-criteria-before-each-step",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약"
|
|
],
|
|
"summary": "단계마다 통과 조건이 앞에 있다 — 각 단계 첫머리의 「이 단계가 끝나면」과 그 상태를 확인하는 명령, 그리고 7단계 전체의 통과 조건 표(`virsh list` 가 돈다 · 세 게스트에 SSH · 두 노드 Ready · 밖에서 파드까지 · 체인 4단계 · 관리 콘솔 로그인 · `vendor_cluster_size` 가 2)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-a-build-guide-in-execution-order",
|
|
"reason": "제5부의 Reference 가 「이 시점에 이 리소스가 존재하는가」를 검사 축으로 삼았는데 그 축을 실제로 검사 가능하게 만드는 장치가 이 통과 조건이다. 검사 규칙과 그 규칙이 요구하는 문서 구조라 한 편 안에 있어야 한다. 그 기록은 이미 쓰여 있어 이 행도 다음 개정에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-186-lab-host-virtualization-setup",
|
|
"kindCandidate": "SETUP",
|
|
"sourceRefs": [
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비"
|
|
],
|
|
"summary": "단계 00 이 세우는 것 — 저장소 `~/workspace/keycloak-pattern`, Arch 패키지 넷, libvirt `12.7.0` · `QEMU emulator version 11.1.1`, `libvirtd.socket`(`.service` 가 아니다), 그룹 `donghyeon libvirt wheel`, `LIBVIRT_DEFAULT_URI=qemu:///system`, `default` 네트워크가 만드는 `virbr0` · `192.168.122.0/24`",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:prepare-the-lab-host-for-virtualization",
|
|
"reason": "재판정(2026-09-12). 처음에는 CONCEPT 후보로 보고 KEEP_IN_SSOT 했다 — 「무엇이 세워져 있나」를 설명하는 글로는 독립 기록이 될 만큼 두껍지 않았고, 절차를 담을 종류가 스킬에 없었다. Studio 의 여섯 번째 종류 `SETUP`(환경 구성)을 확인해 스킬에 반영하면서 처분이 바뀐다 — 이 절은 설명이 아니라 남이 그대로 치는 명령이고, 그것을 담는 종류가 생겼다. 재분해(2026-09-14) — 단계 01(§187)과 묶어 한 편으로 올렸던 것을 되돌린다. SSOT 를 원본 가이드로 다시 채우니 §186 이 344줄 §187 이 604줄이고 두 절이 각각 여덟 칸을 따로 갖고 있어, 묶으면 둘 중 하나의 「이 단계가 세우는 것」과 「막히면」이 통째로 요약된다. 실제로 묶은 편에서 원본의 확인 87건 가운데 절반이 유실됐다. §186 쪽이 기존 작업본의 id 를 물려받는다."
|
|
},
|
|
{
|
|
"id": "SSOT-187-three-guests-build-procedure",
|
|
"kindCandidate": "SETUP",
|
|
"sourceRefs": [
|
|
"final/document.md#187-단계-01-게스트-세-대",
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비"
|
|
],
|
|
"summary": "게스트 세 대를 세우는 절차 — base 이미지 내려받기와 `qemu-img info` 세 줄 확인, 게스트마다 cloud-init 파일과 시드 ISO(`xorrisofs` · `vol-create-as` · `vol-upload`), DHCP 예약 세 줄(`--live --config`), `virt-install` 세 줄, 그리고 `virsh list --all` 과 `cloud-init status` 로 끝났음을 판정하는 것까지",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:create-three-guests-with-cloud-init",
|
|
"reason": "대장에 없던 후보다 — 제6부를 분해할 때 §187 에서 뽑은 후보 일곱은 전부 실패 증상이나 개별 관측이었고, 「세우는 절차 자체」는 담을 종류가 없어 후보로 세우지도 않았다. 환경 구성 종류를 확인하고 뒤늦게 적었다. 재판정(2026-09-14) — MERGE_INTO 에서 PROMOTE 로 올린다. 묶은 까닭이었던 「둘이 같은 셸에서 이어 치는 한 줄기」는 여전히 맞지만, 그것은 관계로 이으면 되는 일이고 한 기록에 담을 근거가 되지 않는다. SSOT §187 은 604줄에 확인만 스물 몇 건이고 전제·되돌리기·막히면·성립 범위가 §186 과 전부 다르다. 이 편이 새 작업본이 된다 — 기존 작업본의 id 는 §186 쪽이 물려받는다."
|
|
},
|
|
{
|
|
"id": "SSOT-186-an-empty-virsh-list-is-not-an-empty-host",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비"
|
|
],
|
|
"summary": "`virsh` 가 기본으로 붙는 `qemu:///session` 과 VM 을 만든 `qemu:///system` 이 어긋나면 VM 은 만들어졌는데 `virsh list` 에 안 나온다. `net-list` 도 `--all` 을 빼면 `inactive` 인 네트워크가 목록에 아예 안 나와 「없음」과 「꺼짐」을 구분할 수 없다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "빈 목록을 대상의 부재로 읽는 두 가지 모양이고, 그 Reference 가 답하는 물음과 같다. 둘 다 SSOT 가 한 문단씩만 적어 따로 세울 자료가 없고, 규칙의 행으로 들어갈 때 다른 도구의 같은 모양(`kubectl get all`·`grep`·`\"result\":[]`)과 나란히 놓여야 규칙이 보인다"
|
|
},
|
|
{
|
|
"id": "SSOT-186-autostart-no-breaks-the-next-step-after-reboot",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비",
|
|
"final/document.md#187-단계-01-게스트-세-대"
|
|
],
|
|
"summary": "`default` 네트워크의 autostart 가 `no` 면 지금은 되고 호스트를 재부팅한 다음 01 의 SSH 가 전부 실패하는데, 그때 원인을 게스트에서 찾게 된다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reason": "「SSH 가 안 붙는다」는 한 증상에 원인이 여럿이고 전부 게스트 밖에 있다는 것이 그 Case 의 결론인데, 이것이 그 목록의 네 번째다. 원인이 다른 단계에 있다는 점까지 같아서 그 Case 의 원인 표에 행으로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-186-host-core-count-contradiction",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비",
|
|
"final/document.md#194-이-부에서-파생될-open-question"
|
|
],
|
|
"summary": "가이드의 실측 줄은 「이 실험대의 호스트는 16 코어 전부에서 지원한다」인데 §178 의 대상 환경은 논리 코어 8(i5-1135G7)이다. 두 값이 어긋나고 어느 쪽이 이 호스트의 값인지는 재지 않았다(unknown)",
|
|
"disposition": "BLOCKED",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "같은 SSOT 안에서 두 값이 어긋난 상태라 어느 쪽을 근거로 삼아도 틀릴 수 있다. 한 번 재면 닫히지만 재기 전에는 글감이 아니다. 다만 이 부의 판정은 어느 쪽이어도 바뀌지 않는다 — 세 게스트의 vCPU 합이 5 라 8 에서도 16 에서도 CPU overcommit 이 아니고, §187 이 실제로 판정한 것은 메모리 쪽이다. 원본을 고친 뒤에 다시 판정한다"
|
|
},
|
|
{
|
|
"id": "SSOT-186-no-kvm-means-software-emulation",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비"
|
|
],
|
|
"summary": "KVM 없이도 QEMU 는 돌지만 소프트웨어 에뮬레이션이 되어 수십 배 느리다 — VM 이 「뜨긴 뜨는데 느리다」면 대개 여기다. `lsmod | grep kvm` 의 실제 출력은 캡처해 두지 않았다(unknown)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:prepare-the-lab-host-for-virtualization",
|
|
"reason": "재판정(2026-09-12). KEEP_IN_SSOT 였던 것은 이 사실을 받아 줄 기록이 없었기 때문이다. 이제 이 단계의 절차가 Setup 으로 서므로 그 「막히면」 표의 한 행(`VM 이 극단적으로 느리다 · KVM 미사용 · `lsmod | grep kvm`·BIOS`)으로 들어간다. 재분해(2026-09-14) — 나뉜 둘 가운데 §186 쪽으로 간다. `lsmod | grep kvm` 과 BIOS 확인이 게스트를 만들기 전에 도는 「세우기 전에 먼저 본다」의 두 확인이고, 게스트 편의 「막히면」에는 「VM 이 느리다 · KVM 미사용 · 앞 단계로」 한 행만 남는다."
|
|
},
|
|
{
|
|
"id": "SSOT-187-three-guests-sizing-and-overcommit",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#187-단계-01-게스트-세-대",
|
|
"final/document.md#194-이-부에서-파생될-open-question"
|
|
],
|
|
"summary": "게스트 세 대의 IP·MAC·vCPU·메모리·디스크. 메모리는 처음 만들 때 3584MB 였고 실험을 늘리며 5120/4096 으로 재배분했다 — 호스트 RAM 이 11,648MiB(약 11.4GiB) 이고 세 게스트 합이 10240MB 다. 배정 합이 호스트 RAM 보다 작아(배정률 10240/11648 = 87.9%) 제2부 §57 의 「Guest configured memory 총량이 Host physical RAM보다 크다」에 이 배치는 해당하지 않는다(observed). 엣지의 1024MB·vCPU 1 이 nginx 와 certbot 에 충분한지는 재지 않았다(미측정)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:vm-configured-vs-current-memory",
|
|
"reason": "제2부의 그 질문이 「VM 에 준 RAM 과 지금 실제로 쓰는 양이 얼마나 다른가」를 묻고 이 배치가 그 질문의 대상이자 전제다. 엣지 1024MB 가 충분한가도 같은 한 번의 측정(게스트별 configured 대 current)으로 닫히므로 따로 세우지 않는다. 같은 측정으로 닫히는 물음을 두 편으로 만들지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-187-overlay-on-a-base-image",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#187-단계-01-게스트-세-대"
|
|
],
|
|
"summary": "게스트 디스크는 `/var/lib/libvirt/images/base.qcow2` 위의 오버레이(`backing_store=`)이고 복사가 아니다. `virt-install` 의 `Allocating 'kc-lab-edge.qcow2' | 10 GB 00:00` 이 즉시 끝나는 것이 정상이다 — 제4부 §143 의 희소 할당이 여기서 그대로 보인다. base 는 `qemu-img info` 에 `backing file:` 줄이 없어야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:what-a-qcow2-file-carries",
|
|
"reason": "그 Concept 이 이미 「파일 밖을 가리키는 것은 백킹 파일 경로 하나뿐」과 「희소 할당이지 압축이 아니다」를 뼈대로 삼았고, 이것은 그 둘이 이 실험대에서 실제로 찍힌 모양이다. 그 기록의 근거 행으로 들어갈 때 뜻이 있고 떼면 `00:00` 한 줄만 남는다. 그 기록은 이미 쓰여 있어 다음 개정에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-187-cloud-init-failures-share-one-symptom",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#187-단계-01-게스트-세-대",
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비"
|
|
],
|
|
"summary": "cloud-init 이 안 돈 자리는 여럿인데 증상은 「SSH 가 안 붙는다」 하나로만 나타난다 — 시드를 `--cloud-init` 으로 붙이면 SATA CD-ROM 이 되고 Debian `genericcloud` 에는 AHCI 드라이버가 없어 데이터소스를 못 찾고 조용히 끝나며(observed), YAML 파싱에 실패해도 아무 오류를 남기지 않고, `vol-upload` 를 빠뜨리면 목록에는 이름이 보이는데 안이 0 으로 채워져 있다. 가르는 것은 호스트명 한 낱말이다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reason": "같은 물음(게스트가 떴는데 왜 못 들어가나)에서 나와 같은 결론(증상은 SSH 인데 원인은 전부 시드 쪽이고, 호스트명 한 낱말이 그 둘을 가른다)에 닿는 관측이 넷이라 한 Case 다. 관측·진단·결론이 한 절에서 닫히고, 넷을 따로 쪼개면 표의 행 하나가 될 것이 네 편이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-187-the-schema-checker-rejects-what-boots",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#187-단계-01-게스트-세-대"
|
|
],
|
|
"summary": "게스트의 cloud-init `22.4.2` 스키마 검사기가 `sudo: ['ALL=(ALL) NOPASSWD:ALL']` 리스트 형태를 거부하고 어느 키가 문제인지 안 알려 준다(`users.0` 전체를 찍고 「어느 스키마에도 안 맞는다」고만 한다). 그런데 리스트 형태도 부팅은 된다 — `kc-lab-1`·`kc-lab-2` 가 그 상태로 NOPASSWD sudo 가 돌고 있다(observed)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reason": "같은 절의 같은 주제이고 검사 결과와 실제 상태가 갈리는 방향만 반대다 — 위의 넷은 검사를 통과하고 안 돌았고 이것은 검사에 걸리고 돌았다. 그 Case 의 결론(판정을 시드가 읽혔는지로 한다)을 양쪽에서 떠받치므로 같은 글 안에 있어야 대조가 보인다"
|
|
},
|
|
{
|
|
"id": "SSOT-187-three-checks-that-yaml-passes-but-cloud-config-fails",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#187-단계-01-게스트-세-대"
|
|
],
|
|
"summary": "시드를 만들기 전 세 줄 — `grep -c '__'` 가 0, `grep -c 'ssh-ed25519\\|ssh-rsa'` 가 2, `python3 -c yaml.safe_load` 가 `YAML OK`. 셋이 맞아도 cloud-config 로 유효한 것은 아니라(키 이름 오타 `user` 와 `users` 는 그냥 통과한다) 게스트가 한 대라도 떠 있으면 cloud-init 자신의 스키마 검사기를 쓴다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reason": "그 Case 의 예방 절이다. 「무엇을 보고 시드가 맞다고 판정했나」가 그 Case 의 물음이라 검사 세 줄과 그 셋이 못 잡는 것이 같은 글 안에서 닫힌다. 규칙의 모양이지만 cloud-init 한 도구에만 적용되고 예외를 댈 자료가 이 절뿐이라 Reference 로 올리지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-187-seed-volume-needs-create-then-upload",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#187-단계-01-게스트-세-대"
|
|
],
|
|
"summary": "`vol-create-as` 는 빈 볼륨을 만들 뿐이고 내용은 `vol-upload` 가 채운다. `instance-id` 에 타임스탬프를 넣는 것은 cloud-init 이 인스턴스마다 한 번만 초기화 모듈을 돌리기 때문이고, id 가 같으면 user-data 를 고쳐도 반영되지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reason": "그 Case 의 원인 표 두 행이다. 둘 다 증상이 「SSH 가 안 붙는다」 또는 「고쳤는데 반영이 안 된다」로만 나타나 원인을 시드 밖에서 찾게 만든다는 점에서 같은 결론에 닿는다"
|
|
},
|
|
{
|
|
"id": "SSOT-187-dhcp-reservation-before-vm-creation",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#187-단계-01-게스트-세-대"
|
|
],
|
|
"summary": "DHCP 예약이 VM 생성보다 먼저다 — 순서가 반대면 게스트가 동적 대역에서 아무 주소나 받고 그 뒤에 예약을 넣어도 이미 잡은 리스가 유지된다. 넣을 때는 `--live --config` 를 둘 다 준다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-a-build-guide-in-execution-order",
|
|
"reason": "앞 단계의 결과 위에 뒤 단계가 서는 문서에서 순서를 뒤집으면 각 명령은 참인데 결과가 틀린다는, 제5부 Reference 가 적은 바로 그 모양이다. 그 규칙의 근거 표에 행으로 들어간다. 그 기록은 이미 쓰여 있어 다음 개정에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-187-grep-for-ip-dhcp-host-finds-nothing",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#187-단계-01-게스트-세-대"
|
|
],
|
|
"summary": "예약을 확인할 때 `ip-dhcp-host` 로 `grep` 하면 예약이 멀쩡히 들어가 있어도 아무것도 안 나온다 — 그것은 `net-update` 의 섹션 이름이라 XML 안에 그 문자열이 없다. 확인은 `<host mac=...>` 로 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "찾은 것이 없다는 출력이 대상이 없다는 뜻이 아닌 또 하나의 모양이고, 이번에는 도구가 아니라 찾는 말이 대상에 존재하지 않았다. 그 Reference 의 행으로 다른 모양들과 나란히 놓일 때 규칙이 보인다"
|
|
},
|
|
{
|
|
"id": "SSOT-187-cloud-init-packages-certbot-contradiction",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#187-단계-01-게스트-세-대",
|
|
"final/document.md#194-이-부에서-파생될-open-question"
|
|
],
|
|
"summary": "cloud-init 의 `packages` 에 certbot 이 들어 있었는지가 가이드 안에서 갈린다 — 01 의 예시와 03 의 본문은 `[curl, nftables]` 뿐이라 적고 04 는 `kc-lab.yaml.example` 의 `packages` 에 certbot 이 있다고 적는다. 원본 example 파일이 `source/` 에 없어 대조하지 못했다(unknown)",
|
|
"disposition": "BLOCKED",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "대조할 원본이 저장소에 없어 어느 쪽이 맞는지 판정할 방법이 지금은 없다. 어느 쪽이든 §189 의 「`/etc/nginx` 가 없다」는 관측과는 어긋나지 않지만(nginx 는 어느 목록에도 없다) certbot 은 갈린 채다. 원본을 반입한 뒤에 다시 판정한다"
|
|
},
|
|
{
|
|
"id": "SSOT-188-k3s-two-nodes-and-what-comes-bundled",
|
|
"kindCandidate": "SETUP",
|
|
"sourceRefs": [
|
|
"final/document.md#188-단계-02-k3s-server-와-agent"
|
|
],
|
|
"summary": "단계 02 가 세우는 것 — `kc-lab-1` 은 server(`k3s.service`), `kc-lab-2` 는 agent(`k3s-agent.service`), 둘 다 `v1.36.4+k3s1`. 따로 설치하지 않아도 딸려 오는 것이 다섯이다(Traefik · servicelb · local-path · flannel · kube-router)",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:install-k3s-server-and-agent",
|
|
"reason": "재판정(2026-09-12). CONCEPT 후보로 KEEP_IN_SSOT 했던 이유가 §186 과 같다 — 설치 명령과 kubeconfig 세 줄은 설명이 아니라 치는 것이라 Concept 으로 세우면 코드블록이 갈 데가 없었다. 환경 구성 종류가 그 절차를 담는다. 딸려 오는 다섯(Traefik·servicelb·local-path·flannel·kube-router)은 그 Setup 의 「무엇을 세우나」 표에 남는다."
|
|
},
|
|
{
|
|
"id": "SSOT-188-the-same-kubeconfig-needs-a-different-address-per-machine",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#188-단계-02-k3s-server-와-agent"
|
|
],
|
|
"summary": "lab host 는 k3s 원본의 `https://127.0.0.1:6443` 을 `sed` 로 `192.168.122.11` 로 바꿔 읽고, 워크스테이션은 SSH 터널을 뚫어 원본 그대로 읽는다 — 같은 파일이라도 어느 기계에서 읽느냐에 따라 맞는 주소가 다르다. 둘 다 되는 것은 API 서버 인증서 SAN 에 `IP Address:127.0.0.1` 과 `IP Address:192.168.122.11` 이 다 들어 있기 때문이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-a-build-guide-in-execution-order",
|
|
"reason": "제5부 Reference 의 셸 축(「이 셸에서 이 명령이 도는가」)이 설정 파일 쪽으로 한 칸 넓어진 것이다 — 같은 파일이 어느 기계에서 맞는가. 새 Reference 로 세우면 한 축을 두 편이 나눠 갖는다. 그 기록은 이미 쓰여 있어 다음 개정에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-188-token-is-checked-by-length-not-value",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#188-단계-02-k3s-server-와-agent"
|
|
],
|
|
"summary": "토큰은 값이 아니라 길이로 확인한다 — 이 실험대에서는 108자였고 `K10<해시>::server:<비밀번호>` 형식이라 판올림에 따라 자릿수가 달라지므로 중요한 것은 `0` 이 아니라는 사실이다. agent 설치 앞에 `[ ${#TOKEN} -ge 50 ] || echo \"TOKEN 이 비었다\"` 가드를 둔다. 히스토리에 남기고 싶지 않으면 `--token-file` 로 넘긴다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:an-empty-token-installed-the-agent-anyway",
|
|
"reason": "그 Case 의 해결 절이다. 빈 토큰이 조용히 흘러간다는 것이 문제이고 길이 가드가 그것을 잡는 한 줄이라, 떼면 Case 의 결론이 닫히지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-188-ready-two-lines-is-not-the-right-node-ip",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#188-단계-02-k3s-server-와-agent"
|
|
],
|
|
"summary": "`kubectl get nodes` 두 줄이 `Ready` 인 것과 노드 IP 가 맞는 것은 다르다 — `-o wide` 의 INTERNAL-IP 가 `--node-ip` 로 준 값과 달라도 지금은 아무 증상이 없다가 03 의 nginx upstream 과 노드 상실 실험에서 어긋난다. `get nodes -o wide` 는 k3s 가 보고한 IP 이고 `systemctl cat` 의 `ExecStart` 는 우리가 준 IP 다. 유닛 이름이 노드마다 달라 agent 에서 `systemctl stop k3s` 를 치면 아무 일도 일어나지 않고 「주입했는데 증상이 없다」로 읽힌다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "초록으로 보이는 출력이 통과를 뜻하지 않는 모양이고, 보고한 값과 준 값이 다른 근거라는 것까지 그 Reference 가 답하는 물음 그대로다. 「명령이 통과했는데 일은 안 일어났다」도 같은 규칙의 행이다"
|
|
},
|
|
{
|
|
"id": "SSOT-188-agent-kubectl-falls-back-to-localhost-8080",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#188-단계-02-k3s-server-와-agent"
|
|
],
|
|
"summary": "agent 노드의 `kubectl` 이 `dial tcp [::1]:8080: connect: connection refused` 를 내는 것은 정상이다(observed) — 명령은 심볼릭 링크로 있고 없는 것은 kubeconfig 이며, 넷 다 못 찾으면 kubectl 은 오류 없이 하드코딩 기본값 `http://localhost:8080` 으로 넘어간다. 실패가 「권한 없음(403)」이 아니라 「설정 없음」으로 나타난다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:check-the-nearest-layer-first",
|
|
"reason": "오류 문구가 어느 층의 것인지를 읽는 예다 — `localhost:8080` 이 보이면 네트워크 문제가 아니라 설정을 하나도 못 찾았다는 뜻이고, 그 Reference 의 errno 113 대 111 과 exit code 127 이 같은 읽기다. 층을 좁혀 가는 절차의 판독 절로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-188-do-not-copy-the-admin-kubeconfig-to-the-agent",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#188-단계-02-k3s-server-와-agent"
|
|
],
|
|
"summary": "`k3s.yaml` 을 복사해 넣으면 agent 노드에서도 다 보이지만 복사하지 않는다 — 워커 한 대가 털리면 클러스터 전체가 털리는 구성이 된다. agent 가 원래 가진 신원은 `O = system:nodes, CN = system:node:kc-lab-2` 이고 Node authorizer 와 NodeRestriction admission 이 자기 노드에 배정된 객체만 다루도록 제한한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:install-k3s-server-and-agent",
|
|
"reason": "재판정(2026-09-12). KEEP_IN_SSOT 였다 — 이 실험대 밖으로 옮길 만한 규칙으로 보기에는 근거가 이 한 배치뿐이었다. 다만 이것은 단계 02 절차 안의 가드레일이라(agent 에서 `kubectl` 이 `localhost:8080` 으로 거절되는 것을 kubeconfig 복사로 메우지 않는다) 그 Setup 의 한 절로 들어간다."
|
|
},
|
|
{
|
|
"id": "SSOT-189-check-from-the-nearest-layer-up",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat",
|
|
"final/document.md#185-가이드-묶음이-스스로-정한-규약",
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql"
|
|
],
|
|
"summary": "한 번에 밖에서 치지 않고 가까운 층부터 한 층씩 건너뛰며 확인한다 — ① nginx 를 건너뛴 `curl -I http://192.168.122.11` 이 `404`, ② DNAT 을 건너뛴 `http://192.168.122.10` 이 `301`, ③ 밖에서 `http://auth.hyeonworks.com` 이 `301`, ④ TLS 이후 `https://.../realms/master` 가 `200`. ①의 `404` 가 성공 신호다 — Traefik 이 듣고 있고 매칭되는 Ingress 규칙이 없다는 뜻이며 `502` 면 뒤에 백엔드가 없는 것이고 연결 거부·타임아웃이면 02 로 돌아간다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:check-the-nearest-layer-first",
|
|
"reason": "SSOT 가 절차를 그대로 적었고 층마다 성공 신호가 다르다는 것까지 실측 표로 댄다. 적용 조건(프록시가 겹쳐 있어 밖에서 한 번 쳐서는 어디서 끊겼는지 모르는 스택)과 예외(치는 위치가 틀리면 층 판정이 통째로 무의미하다 — 엣지 안에서 tailnet 주소를 치면 `connection refused` 다)를 자료가 댄다. 원 프로젝트의 도메인과 주소를 지워도 규칙이 남아 Case 요약을 선언문으로 바꾼 것이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-189-upstream-errno-113-vs-111",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat"
|
|
],
|
|
"summary": "upstream 이 둘이라 한쪽이 죽으면 nginx 가 자동으로 뺀다. 로그 세 줄의 대응이 다르다 — `connect() failed (113: No route to host)` 는 네트워크, `(111: Connection refused)` 는 프로세스, `no live upstreams` 는 둘 다 죽었다고 판단한 것이다. 노드를 잃었을 때 이 세 줄이 1분 안에 순서대로 나왔다(observed)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:check-the-nearest-layer-first",
|
|
"reason": "층을 좁힌 뒤 그 층의 오류 문구가 어느 자원을 가리키는지 읽는 절이라 같은 절차의 뒷부분이다. 관측이 있지만 혼자 답하는 물음이 없다 — 「어디서 끊겼나」를 묻는 절차 안에서만 뜻이 있다"
|
|
},
|
|
{
|
|
"id": "SSOT-189-nginx-error-log-truncates-at-2048-bytes",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat"
|
|
],
|
|
"summary": "nginx 에러 로그는 2048바이트에서 잘린다. 이 실험대에서 502 원인이 error 로그에 있었는데 잘려 있었고 access 로그에는 3492자로 온전히 남아 있었다(observed) — 잘린 문자열을 놓고 원인을 추측하면 없는 문제를 좇게 된다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "도구가 낸 출력이 대상의 전부가 아닌 모양이다. 한 문단짜리 관측이라 혼자 서지 않고, 그 Reference 의 행으로 「비었다」·「잘렸다」·「안 나온다」가 나란히 놓일 때 규칙이 보인다"
|
|
},
|
|
{
|
|
"id": "SSOT-189-a-path-typo-passes-every-check",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat"
|
|
],
|
|
"summary": "`sites-available` 을 `site-available` 로 치면 `nano` 가 군말 없이 빈 새 파일을 열고, 저장해도 nginx 는 그 파일을 영원히 안 읽고, `nginx -t` 는 멀쩡히 통과한다 — 아무 에러 없이 아무 일도 안 일어난다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "검사를 통과한 것이 적용됐다는 뜻이 아닌 모양이라 그 Reference 가 답하는 물음과 같다. §189 의 systemd 유닛 경로 오타도 같은 형태(`nano` 가 없는 파일을 말없이 만든다)라 한 행에 같이 놓인다"
|
|
},
|
|
{
|
|
"id": "SSOT-189-warn-is-not-emerg",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat",
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "`nginx -t` 는 `syntax is ok` 와 `test is successful` 두 마디가 다 나와야 통과이고 앞의 `[warn] could not build optimal types_hash` 줄은 통과를 막지 않는다. 실패면 `[emerg]` 줄에 파일과 줄 번호가 찍힌다 — 04 에서 이 경고를 실패로 오독하는 일이 실제로 벌어지므로 03 에서 경고와 오류를 가르는 눈을 들여 둔다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "출력 문구를 상태로 읽는 모양이고 04 에서 실제로 오독이 벌어진 자리가 case:renewal-succeeded-while-the-old-certificate-kept-serving 의 훅 판정이다. 규칙 쪽에 행으로 두고 그 Case 가 relations 로 가리킨다"
|
|
},
|
|
{
|
|
"id": "SSOT-189-port-433-typo-surfaces-one-step-later",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat"
|
|
],
|
|
"summary": "`.nft` 의 포트를 `443` 대신 `433` 으로 치면 `433` 도 유효한 포트라 nft 가 군말 없이 받고 80 은 멀쩡히 넘어가므로 이 단계는 다 통과한 뒤 04 에서 HTTPS 만 안 되는 형태로 뒤늦게 터진다. 이 실험대에서 실제로 나왔던 오타다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "단계의 통과 조건이 그 단계에서 심은 결함을 못 잡는 모양이다 — 03 의 판정은 80 만 보므로 443 의 오타가 초록으로 남는다. 그 Reference 의 「초록이 정상을 뜻하지 않는다」 행에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-189-edge-setup-files-and-revert",
|
|
"kindCandidate": "SETUP",
|
|
"sourceRefs": [
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat"
|
|
],
|
|
"summary": "단계 03 이 세우는 파일들과 그 자리 — 엣지의 `/etc/nginx/sites-available/keycloak-lab` 과 심볼릭 링크, lab host 의 `/etc/nftables.d/lab-edge-dnat.nft` 과 `/etc/systemd/system/lab-edge-dnat.service`, 저장소 원본 `deploy/lab/edge/` 넷. `table ip lab_edge` / `delete table ip lab_edge` 두 줄짜리 관용구로 같은 파일을 몇 번 적용해도 안전하게 만든다. 되돌리기는 네 줄이다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:edge-nginx-and-host-dnat",
|
|
"reason": "재판정(2026-09-12). 설정 파일 넷과 되돌리기 네 줄은 CONCEPT 으로 쓸 수 없어 KEEP_IN_SSOT 했다 — 파일 내용 자체가 본문에 들어가야 하는데 Concept 으로 세우면 「무엇을 설명하는 글인가」가 성립하지 않는다. 환경 구성은 파일과 명령을 그대로 싣는 종류라 여기로 올린다. 이 단계에서 막힌 것(§180)은 이미 case:nftables-accept-did-not-stop-the-libvirt-reject 가 받았고 겹치지 않는다."
|
|
},
|
|
{
|
|
"id": "SSOT-189-reload-is-judged-by-worker-pid",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat",
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "reload 가 반영됐는지는 워커 PID 로 본다 — reload 는 마스터를 그대로 두고 워커만 갈아 끼우므로 전후로 PID 가 바뀌면 새 설정이 적용된 것이다. 04 에서 인증서 갱신이 서빙까지 닿았는지를 똑같은 방법으로 판정한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"reason": "이 판정 방법이 실제로 값을 낸 자리가 그 Case 다 — 훅이 nginx 를 정말 갈아 끼웠는지를 로그 문구가 아니라 이 PID 로 갈랐고 2305초와 1~2초가 그렇게 나왔다. 방법만 떼어 내면 무엇을 판정했는지가 빠진다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-dns-01-because-the-address-is-not-routable",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "이 실험대의 도메인 셋은 tailnet 주소(`100.83.212.4`)로 풀린다. `100.64.0.0/10` 은 CGNAT 용 예약 대역이라 공개 인터넷에서 라우팅 자체가 되지 않아 HTTP-01 이 성립하지 않고, Let's Encrypt 를 tailnet 에 초대할 방법도 없다. 그래서 DNS-01 을 쓴다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:dns-01-because-the-lab-is-not-on-the-public-internet",
|
|
"reason": "제약 → 선택 → 이유 → 대안 → 감수한 비용 → 가드레일이 한 절에 다 있다. SSOT 가 이 선택을 우열로 적지 않고 「공개 서버라면 HTTP-01 이 맞고 토큰도 DNS 연동도 없어 관리할 것이 적다」고 대안 쪽을 먼저 적은 뒤 감수한 비용(Cloudflare API 토큰이 엣지 VM 안 평문 파일에 놓인다)과 가드레일(권한을 `Edit zone DNS`·`Specific zone`·`hyeonworks.com` 으로 좁힌다)을 댄다. 기술이 존재한다는 사실을 이유로 바꾼 것이 아니라 SSOT 가 적은 제약 문장을 그대로 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-narrow-the-cloudflare-token-scope",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "토큰 권한을 `All zones` 로 두면 계정의 모든 도메인에 대한 DNS 수정 권한이 그 평문 파일에 놓이고, Global API Key 는 폐기하면 그 키를 쓰던 다른 것들이 전부 같이 죽는다. 파일은 `install -m 600 /dev/null` 로 비어 있을 때 미리 600 을 만들고, 토큰이 맞는지는 `/user/tokens/verify` 의 `\"status\":\"active\"` 로 본다 — `\"code\":6003` 이면 값이 틀렸거나 잘렸고 `\"code\":9109` 면 권한 범위가 모자라다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:dns-01-because-the-lab-is-not-on-the-public-internet",
|
|
"reason": "그 결정이 감수한 비용을 실제로 줄인 가드레일이라 결정 안에서 닫힌다. 떼어 내면 왜 권한을 좁혔는지가 「일반적으로 좋다」가 되고, 이 실험대가 그것을 고른 제약(토큰이 게스트 안 평문 파일에 놓인다)이 빠진다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-lineage-directory-name-is-not-the-san-list",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "`live/hyeonworks.com/` 은 certbot 이 이 묶음을 관리하려고 첫 번째 `-d` 에서 따온 라벨이고 서빙과 무관하다. 브라우저가 보는 유효 호스트명은 `-d` 로 준 이름 전부다. 그래서 nginx 설정에는 디렉터리 경로를 한 글자도 다르지 않게 적어야 하고, 와일드카드는 한 단계만 덮으므로 apex 인 `hyeonworks.com` 자신도 `-d` 를 따로 준다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:dns-01-because-the-lab-is-not-on-the-public-internet",
|
|
"reason": "DNS-01 을 골라 와일드카드로 받기로 한 결과 생긴 이름 규칙이라 그 결정의 결과 절이다. §182 가 이미 이 경로 오류를 가이드 결함 여섯 중 하나로 세었고 reference:verify-a-build-guide-in-execution-order 가 그 표를 갖고 있으므로, 여기서는 왜 그렇게 정해지는지를 결정 안에 둔다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-fullchain-not-cert",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "`cert.pem` 을 쓰면 중간 인증서가 빠져 체인이 끊기는데 브라우저는 대개 캐시나 AIA 로 보완해서 정상으로 보이고 캐시가 없는 클라이언트에서만 깨진다. 유일하게 믿을 수 있는 판정은 `openssl s_client` 의 단계 수다 — 이 실험대의 실측은 0~3 네 단계였고 각 단계의 `i:` 가 다음 단계의 `s:` 와 이어지며 `Verify return code: 0 (ok)` 였다(observed)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "브라우저에서 정상으로 보이는 것이 대상의 상태가 아닌 모양이고, 그래서 다른 도구로 층을 바꿔 판정한다는 결론까지 그 Reference 와 같다. 인증서 한 경우로 Reference 를 따로 세우면 같은 규칙이 둘로 갈린다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-renewal-never-reached-serving",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신",
|
|
"final/document.md#194-이-부에서-파생될-open-question"
|
|
],
|
|
"summary": "배포판 기본 `certbot-renew.service` 에는 `ExecStartPost` 도 `--deploy-hook` 도 없어 인증서를 새로 받는 데까지만 책임진다. nginx 는 인증서를 기동 시점에 읽어 메모리에 들고 있고 certbot 은 `live/` 심볼릭 링크만 갈아 끼우므로 경로는 그대로이고 내용만 바뀌어 nginx 는 모른다. 갱신에서 서빙까지가 훅 없이 2305초(38분 25초), 훅을 넣으면 1~2초였다(observed). 88일 동안은 이 결함이 보이지 않는다 — 타이머는 매번 `SUCCESS` 로 끝나고 만료 30일 전까지는 갱신 자체를 하지 않는다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"reason": "하나의 문제 · 관측(2305초 대 1~2초) · 진단(유닛에 훅이 없고 nginx 는 기동 시점에 읽으며 링크만 갈린다) · 결론(`deploy/` 훅과 `chmod +x`, 판정은 로그 문구가 아니라 워커 PID)이 닫힌다. 이 부에서 「초록으로 보이는 실패」가 가장 오래 숨어 있는 모양이고 발현하는 날의 증상이 인증서 만료라 값이 크다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-the-hook-log-says-error-and-it-succeeded",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "certbot 이 `Hook 'deploy-hook' ran with error output` 이라고 찍는데 실패가 아니다 — nginx 의 `types_hash` 경고가 stderr 로 나갔을 뿐이고 내용은 `test is successful`·`signal process started` 다. 로그에서 `error` 를 grep 하는 감시를 걸면 성공한 훅을 실패로 오독한다. 실행 권한이 없으면 certbot 이 훅을 조용히 건너뛰고, `post/` 에 넣으면 갱신이 없어도 매번 돌아 하루 두 번 워커를 갈아치운다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"reason": "그 Case 의 판정 절이다 — 훅이 돌았는지를 무엇으로 판정하느냐가 그 Case 의 결론이고 이것이 그 판정을 틀리게 만드는 자리다. 떼면 「워커 PID 로 판정한다」가 왜 필요한지가 빠진다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-clock-skew-106-seconds",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "test-server 가 NTP 미동기로 106초 빨랐고, 보정하지 않은 첫 계산은 훅이 인증서 발급보다 104초 먼저 실행된 것이 되어 물리적으로 불가능했다(observed) — 음수 지연이 나오면 계산이 아니라 시계를 의심한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"reason": "그 Case 의 2305초가 나온 측정을 성립시킨 보정이다. 규칙의 모양이지만 SSOT 에 한 문단과 명령 세 줄뿐이라 적용 조건과 예외를 댈 자료가 없고, 떼면 그 Case 의 수치가 어떻게 나왔는지 말할 수 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-reload-was-not-interruptive",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "reload 가 무중단인지도 쟀다(observed) — 새 연결 8856건 전부 200, p95 는 205.7ms 대 204.3ms 로 변화 없음. 845KB 를 20k/s 로 받는 중이던 요청이 전송 12초째에 reload 를 맞고도 845361바이트를 온전히 받았다(연결수 1). 옛 워커가 그 요청을 끝까지 책임진다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"reason": "훅을 넣어 갱신 때마다 reload 하기로 한 결론이 안전한지를 떠받치는 측정이다. 혼자 답하는 물음이 없다 — 「그래도 되는가」는 그 Case 의 결론 안에서만 물어진다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-http2-directive-needs-nginx-1-25-1",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신",
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat"
|
|
],
|
|
"summary": "`http2 on;` 지시어는 nginx 1.25.1 이상에만 있고 엣지는 Debian 12 의 `nginx/1.22.1`, 물리 호스트는 Arch 의 `nginx/1.30.4` 다. `listen` 의 파라미터로 쓰면 양쪽에서 다 돌고, 지시어 형태로 쓰면 `[emerg] unknown directive \"http2\"` 로 막힌다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-a-build-guide-in-execution-order",
|
|
"reason": "§182 가 이미 이것을 가이드 결함 여섯 중 하나로 세었고 그 Reference 의 예외 절이 「배포판 차이는 이 축에서 안 잡힌다」고 이 한 줄만 성격이 다르다고 적었다. 판 번호 두 개는 그 예외의 근거 값이라 같은 기록에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-check-tls-from-a-machine-on-the-tailnet",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "엣지 안에서 `https://auth.hyeonworks.com` 을 치면 `connect to 100.83.212.4 port 443 failed: Connection refused` 다 — 엣지에서 나간 패킷은 호스트의 `virbr0` 으로 들어가고 DNAT 규칙은 `iifname \"tailscale0\"` 만 매칭하므로 안 걸린다. 설정 문제가 아니라 친 위치 문제다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:check-the-nearest-layer-first",
|
|
"reason": "층을 좁혀 가는 절차가 성립하려면 각 층을 어느 기계에서 치는지가 먼저 정해져야 한다는 것이고, 이것이 그 절차의 예외 절이다. §182 가 같은 것을 가이드 결함으로도 세었지만 거기서는 「문서가 위치를 안 적었다」가 요점이고 여기서는 「판정이 통째로 무의미해진다」가 요점이라 이 Reference 쪽이 맞다"
|
|
},
|
|
{
|
|
"id": "SSOT-190-certificate-issuance-and-renewal-procedure",
|
|
"kindCandidate": "SETUP",
|
|
"sourceRefs": [
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "인증서를 받고 갱신이 서빙까지 닿게 하는 절차 — `certbot`·`python3-certbot-dns-cloudflare` 설치, `install -m 600 /dev/null` 로 비어 있을 때 먼저 권한을 만드는 자격증명 파일, 토큰 검증, `--dry-run` 뒤 발급, 443 블록을 더한 nginx 설정, `renewal-hooks/deploy/reload-nginx.sh` 와 `chmod +x`, 그리고 tailnet 의 다른 기계에서 치는 판정 명령들",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:wildcard-certificate-with-dns-01-and-a-deploy-hook",
|
|
"reason": "대장에 없던 후보다 — §190 에서 뽑은 후보 열은 왜 DNS-01 인가(Decision)와 갱신이 서빙까지 안 닿았다(Case)로 갈렸고, 그 둘을 실행하는 절차는 담을 종류가 없어 후보가 되지 못했다. 환경 구성 종류를 확인하고 적는다. 이미 PROMOTE 된 둘과 겹치지 않는다 — 판단은 Decision 이, 측정은 Case 가 갖고, 여기는 명령과 순서만 갖는다."
|
|
},
|
|
{
|
|
"id": "SSOT-191-keycloak-two-nodes-and-postgres-layout",
|
|
"kindCandidate": "SETUP",
|
|
"sourceRefs": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql"
|
|
],
|
|
"summary": "단계 05 가 세우는 것 — 네임스페이스 `keycloak-lab`, StatefulSet `keycloak`(파드 `keycloak-0`·`keycloak-1`), Deployment `postgres`, Service 뒤 Endpoints `10.42.0.67:8080,10.42.1.155:8080`, Secret `keycloak-lab-secrets`, StorageClass `local-path` 의 PVC, Ingress HOSTS `auth.hyeonworks.com`, JGroups 디스커버리 테이블 `jgroups_ping` 과 메시지 포트 7800",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:keycloak-two-nodes-and-postgres-on-k3s",
|
|
"reason": "재판정(2026-09-12). CONCEPT 후보로 KEEP_IN_SSOT 했다 — 배치를 설명하는 글은 매니페스트 원문이 `source/` 에 없어 근거가 얇았다. 그런데 이 절의 알맹이는 배치 설명이 아니라 `apply` 뒤에 무엇을 어떤 명령으로 확인하는가이고, 그 명령들이 전부 읽는 사람이 자기 클러스터에서 치는 것이다. 매니페스트가 없다는 한계는 그대로 남아 Setup 의 유효 범위에 적힌다."
|
|
},
|
|
{
|
|
"id": "SSOT-191-get-all-is-not-everything",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql"
|
|
],
|
|
"summary": "`kubectl get all` 은 이름과 달리 전부가 아니다 — Secret·ConfigMap·PVC·Ingress 가 안 나온다. 그 넷이 빠진 줄 모르고 「다 만들어졌다」로 판정하는 것이 흔한 오독이라 `get secret,configmap,pvc,ingress` 를 한 번 더 친다. `-l app=postgres` 에 Deployment 줄이 안 나오는 것도 정상이다 — 라벨을 파드 템플릿에만 달았고 ReplicaSet 과 파드는 물려받지만 Deployment 는 아니다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "명령 이름이 약속하는 것과 그 명령이 실제로 세는 것이 다른 모양이고, 그 Reference 가 답하는 물음 그대로다. `virsh list` 의 URI 와 `net-list --all` 과 같은 계열이라 한 표에서 나란히 읽힌다"
|
|
},
|
|
{
|
|
"id": "SSOT-191-a-secret-has-three-layers",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql"
|
|
],
|
|
"summary": "값이 있는 것과 파드가 그 값을 받은 것은 다르다(observed) — `describe secret` 이 `KC_BOOTSTRAP_ADMIN_PASSWORD: 19 bytes`·`POSTGRES_PASSWORD: 22 bytes` 를 내고, 파드 안에서 `${#KC_BOOTSTRAP_ADMIN_PASSWORD}` 가 `길이=19` 면 주입까지 이어진 것이다. `길이=0` 이면 Secret 에는 있는데 이 파드가 안 받았다 — 환경변수로 준 Secret 은 값을 바꿔도 파드를 다시 만들기 전까지 갱신되지 않는다. `-o yaml` 로 보지 않는다(base64 는 암호화가 아니라 인코딩이다)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "한 층의 출력이 다음 층의 상태를 말하지 않는 모양이라 그 Reference 의 행이다. 그리고 값을 안 찍고 길이만으로 판정하는 방법이 §185 ② 의 표기 규약과 같은 것이라 규칙 쪽에 모인다"
|
|
},
|
|
{
|
|
"id": "SSOT-191-endpoints-say-what-is-behind-the-service",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql"
|
|
],
|
|
"summary": "Service 뒤에 파드가 있는지는 Endpoints 로 본다 — 셀렉터가 안 맞으면 Service 는 있는데 뒤가 비고 증상이 「연결은 되는데 응답이 없다」라 원인이 Service 에 있다는 것이 잘 안 보인다. 하나뿐이면 나머지 한 파드가 readiness 를 통과하지 못한 것이고, 그 상태로 이중화 실험을 하면 이미 한쪽으로만 가고 있던 트래픽을 이중화 실패로 오독한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "파드 둘이 `Running` 이라는 출력이 트래픽이 둘로 간다는 뜻이 아닌 모양이다. 「초록이 정상을 뜻하지 않는다」의 행이고, 이 오독이 실험 결과를 통째로 뒤집는다는 점에서 그 Reference 가 왜 필요한지를 가장 잘 보여 준다"
|
|
},
|
|
{
|
|
"id": "SSOT-191-192-a-check-that-passed-is-not-a-system-that-works",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql",
|
|
"final/document.md#192-단계-06-prometheus-와-grafana",
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비"
|
|
],
|
|
"summary": "클러스터가 형성됐는지는 셋으로 보고 셋이 다른 것을 본다 — 로그 `ISPN000094` 는 「그때 그렇게 보였다」, 테이블 `jgroups_ping` 은 「지금 등록되어 있다」, 지표 `vendor_cluster_size` 는 「지금 그 노드가 그렇게 안다」이다. 테이블에는 둘 다 있는데 로그가 `(1)` 이면 서로를 찾기는 했는데 7800 으로 메시지가 안 가는 것이고 이 실험대에서 실제로 그 일이 벌어졌다(observed). 각 노드가 자기가 아는 멤버 수를 보고하므로 한 노드만 보면 분단을 놓친다. 그리고 `up` 을 믿지 않는다(observed) — 503 이 나는 동안에도 `up` 은 1 이었다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "제6부 전체에 흩어져 있던 같은 모양의 관측들이 이 두 절에서 규칙으로 적혔다 — 출력이 비었다고 대상이 없는 것이 아니고(`\"result\":[]` 는 「0 이다」가 아니라 「그런 지표가 없다」), 초록이라고 일이 된 것이 아니며(`up`=1 인데 503), 한 근거로는 시제가 갈린다. 적용 조건(상태를 묻는 명령의 출력으로 단계의 통과를 판정할 때)과 예외(`up` 은 프로세스 생존만 말하므로 기능 지표를 함께 본다, 이벤트는 기본 한 시간만 남아 없음이 무사를 뜻하지 않는다)를 SSOT 가 스스로 댄다. 원 프로젝트의 이름을 지워도 규칙이 남는다"
|
|
},
|
|
{
|
|
"id": "SSOT-191-events-expire-and-exit-codes-speak",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql"
|
|
],
|
|
"summary": "안 뜰 때는 순서가 있다 — 이벤트, `describe pod`, 로그, 안에서 보기. REASON 하나가 다음 행동을 정한다(`FailedScheduling` 은 클러스터를 봐야 하고 `ErrImagePull` 은 로그를 볼 것도 없다). 이벤트는 기본 한 시간만 남으므로 아무것도 없는 것이 「문제가 없다」를 뜻하지 않는다. Exit Code 는 그것만으로 말이 된다 — `137` 은 OOM 이나 강제 종료, `1` 은 애플리케이션이 스스로 끝낸 것, `127` 은 명령을 못 찾은 것이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:check-the-nearest-layer-first",
|
|
"reason": "가까운 층부터 좁혀 가는 절차가 파드 쪽에서 반복된 것이고 오류 문구·종료 코드가 어느 층을 가리키는지 읽는 것까지 같다. 이벤트가 비어 있는 것이 무사를 뜻하지 않는다는 한 줄만 성격이 달라 그쪽은 reference:tool-output-is-not-the-subject-state 의 예외 절이 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-191-the-keycloak-image-has-no-curl",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql"
|
|
],
|
|
"summary": "Keycloak 컨테이너에는 `curl` 이 없다(observed) — 공식 이미지가 최소 구성이라 `wget` 도 `nc` 도 없고 `sh: line 1: curl: command not found` 에 `command terminated with exit code 127` 이 붙는다. 그래서 밖에서 묻는다 — Prometheus 로 묻거나 `curlimages/curl` 임시 파드를 띄운다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:check-the-nearest-layer-first",
|
|
"reason": "층을 좁히려는데 그 층 안에 도구가 없을 때 어느 자리에서 묻는가의 문제라 같은 절차의 한 절이다. exit code 127 이 「명령을 못 찾았다」로 읽히는 것도 그 Reference 의 문구 판독과 같다"
|
|
},
|
|
{
|
|
"id": "SSOT-191-rollout-status-silence-is-normal",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql"
|
|
],
|
|
"summary": "`kubectl rollout status` 는 끝날 때까지 아무것도 안 찍고 멈춰 있고 그 침묵이 정상이다. StatefulSet 은 파드를 하나씩 순서대로 띄우므로 중간에 `0/2` 로 한참 멈춰 있는 것도 정상이고, 300초를 다 쓰고 타임아웃으로 끝나면 그것도 답이다 — 「안 떴다」가 확정된다. Deployment 는 파드를 직접 만들지 않고 ReplicaSet 을 만들며 StatefulSet 은 ReplicaSet 없이 파드에 순번 이름을 붙인다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "출력이 없는 것을 실패로도 성공으로도 읽지 않는 모양이고, 타임아웃이 결론이 되는 자리까지 그 Reference 의 물음이다. 컨트롤러 사슬은 두세 문장으로 그 행 안에서 설명되므로 Concept 으로 세우지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-191-manifests-not-imported",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#191-단계-05-keycloak-2노드와-postgresql",
|
|
"final/document.md#192-단계-06-prometheus-와-grafana",
|
|
"final/document.md#194-이-부에서-파생될-open-question"
|
|
],
|
|
"summary": "매니페스트 둘(`keycloak-cluster.yaml`·`observability.yaml`)과 cloud-init 템플릿 `kc-lab.yaml.example` 이 `source/` 에 반입되지 않았다 — 파드 자원 한도·프로브 설정·`persistent-user-sessions` 값·스크레이프 주기·보존 기간·Grafana 대시보드 구성을 SSOT 안에서 대조할 방법이 지금은 없다(unknown)",
|
|
"disposition": "BLOCKED",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "가이드가 화면에 옮겨 적은 만큼만 있고 원문이 없다. 이 상태에서 05·06 의 구성값을 글감으로 올리면 근거가 가이드의 인용문 하나뿐이 된다. 원본을 반입한 뒤에 다시 판정한다"
|
|
},
|
|
{
|
|
"id": "SSOT-192-observability-exists-because-outside-checks-missed-a-partition",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#192-단계-06-prometheus-와-grafana"
|
|
],
|
|
"summary": "판정을 밖에서만 하면 놓친다(observed) — 7800 을 끊었는데 외부 응답이 전부 200 이었고 분단된 노드가 스스로 로드밸런서에서 빠졌기 때문이다. 클러스터 안을 보는 눈이 따로 있어야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "Decision 의 모양이지만 SSOT 가 감수한 비용을 적지 않았고 대안을 견준 기록도 없어 그 조건에 못 미친다. 대신 이 관측이 「한 근거로 판정하지 않는다」의 가장 강한 근거라 그 Reference 의 근거 절로 들어간다 — 밖에서 본 200 이 안의 분단을 가렸다"
|
|
},
|
|
{
|
|
"id": "SSOT-192-look-for-the-missing-job-name",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#192-단계-06-prometheus-와-grafana"
|
|
],
|
|
"summary": "봐야 할 것은 거기 있는 이름이 아니라 없는 이름이다 — 스크레이프 대상은 `keycloak`·`kubelet`·`node-exporter`·`prometheus` 넷이고 Redis·BFF·PostgreSQL 이 없다. 어떤 실험에서 지표를 못 찾으면 「측정이 실패했다」로 적기 전에 이 목록에 그 job 이 있었는지를 먼저 본다. 가이드는 그것을 「스크린샷 누락」이 아니라 측정된 공백으로 기록했다. `\"result\":[]` 도 「0 이다」가 아니라 「그런 지표가 없다」는 뜻이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "그 Reference 의 「비었다」 쪽 절반을 가장 정확하게 적은 자리다. 「없다」와 「못 봤다」를 값에서 갈라 적는다는 결론까지 같아 규칙과 근거의 관계다"
|
|
},
|
|
{
|
|
"id": "SSOT-192-node-exporter-must-be-two",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#192-단계-06-prometheus-와-grafana"
|
|
],
|
|
"summary": "`node-exporter` 로 시작하는 줄이 둘인지 센다 — 하나뿐이면 노드 하나가 빠진 것이고 그러면 그 노드의 CPU·메모리·디스크 지표가 통째로 없는 채로 실험을 하게 된다. 그때는 관측이 아니라 02 의 노드 상태부터 본다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "파드가 `Running` 이라는 출력이 두 노드를 다 재고 있다는 뜻이 아닌 모양이고, Endpoints 가 하나뿐인 것과 같은 결론(빠진 쪽을 모른 채 실험 결과를 읽게 된다)에 닿는다"
|
|
},
|
|
{
|
|
"id": "SSOT-192-no-jq-read-the-json",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#192-단계-06-prometheus-와-grafana"
|
|
],
|
|
"summary": "이 실험대에는 `jq` 가 없어 `grep -o '\"job\":\"[^\"]*\"' | sort -u` 와 `tr ',' '\\n'` 로 필드만 뽑는다. 여기까지가 사람이 손으로 치는 선이고 그 이상 가공해야 한다면 파서를 짜지 않고 화면에 나온 JSON 을 그대로 읽는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:prometheus-and-grafana-for-the-lab",
|
|
"reason": "재판정(2026-09-12). KEEP_IN_SSOT 였다 — 도구가 없어서 생긴 사정이라 다음 프로젝트에 옮길 규칙이 아니었다. 다만 단계 06 의 확인 명령이 실제로 그 형태(`grep -o` · `tr ',' '\\n'`)로 적혀 있으므로 그 Setup 의 「확인 방법」 절로 들어간다."
|
|
},
|
|
{
|
|
"id": "SSOT-192-port-forward-does-not-widen-exposure",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#192-단계-06-prometheus-와-grafana"
|
|
],
|
|
"summary": "Grafana 는 밖에 열지 않고 `port-forward svc/grafana 3000:3000` 으로 본다. `Forwarding from 127.0.0.1:3000 -> 3000` 한 줄이 찍히고 명령이 멈춰 있는 것이 정상이며, 이 터널은 명령을 실행한 기계에서만 열리고 Ctrl+C 로 사라진다 — 밖에 포트를 여는 것이 아니라 보는 동안만 뚫는 것이라 실험대의 노출면이 늘지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:prometheus-and-grafana-for-the-lab",
|
|
"reason": "재판정(2026-09-12). KEEP_IN_SSOT 였다 — 세 문장짜리 사실이라 Concept 으로 세울 두께가 아니었다. Grafana 를 여는 방법이 곧 이 사실이므로 단계 06 Setup 의 실행 절차 한 절로 들어간다."
|
|
},
|
|
{
|
|
"id": "SSOT-192-observability-stack-build-procedure",
|
|
"kindCandidate": "SETUP",
|
|
"sourceRefs": [
|
|
"final/document.md#192-단계-06-prometheus-와-grafana"
|
|
],
|
|
"summary": "관측 스택을 세우는 절차 — `kubectl apply -f deploy/lab/k8s/observability.yaml`, `rollout status`, 파드 네 줄(node-exporter 가 둘인지 센다), targets 의 job 이름과 `health`·`lastError` 를 `jq` 없이 읽는 형태, `vendor_cluster_size` 질의, Grafana `port-forward`",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:prometheus-and-grafana-for-the-lab",
|
|
"reason": "대장에 없던 후보다 — §192 에서 뽑은 후보 다섯은 왜 두는가(Decision)와 개별 오독 셋, 도구 사정 하나였고 「세우는 절차」는 후보로 세우지 않았다. 담을 종류가 없었다. 환경 구성 종류를 확인하고 적는다."
|
|
},
|
|
{
|
|
"id": "SSOT-193-step-to-section-map",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#193-이-구축이-제1~4부의-어느-구조에-닿나"
|
|
],
|
|
"summary": "7단계가 만지는 것이 제1~4부의 어느 절에 적힌 구조인지를 19줄 표로 잇는다. 04 는 가상화 계층에 거의 닿지 않고(인증서 발급·갱신·훅은 게스트 안 애플리케이션 계층이다) 06 의 줄은 제1~4부 전체에 걸린다(inferred)",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "이 문서 안에서 부와 부를 잇는 색인이다. 독립 기록으로 읽을 사람이 없고 옮기면 어느 문서의 어느 절인지가 사라진다. 제6부 글감들이 제1~4부 개념을 relations 로 가리키는 근거로 SSOT 에 남는다"
|
|
},
|
|
{
|
|
"id": "SSOT-193-node-exporter-measures-from-inside-the-guest",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#193-이-구축이-제1~4부의-어느-구조에-닿나",
|
|
"final/document.md#192-단계-06-prometheus-와-grafana"
|
|
],
|
|
"summary": "node-exporter 는 게스트 커널이 내놓는 값을 읽으므로 제1부 §13 의 steal time, 제2부 §52·§63 의 메모리·스왑, 제4부 §169 의 I/O 지표가 전부 「게스트가 본 것」이다. 호스트에서 같은 것을 재면 다른 수가 나올 수 있는데 이 실험대는 호스트 쪽 지표를 긁지 않는다(inferred)",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "제1·2·4부의 question 열여럿이 이미 「이 Host 에서 재면 무엇이 나오는가」를 묻고 있고 그 unknown 이 이 문제를 담고 있다. 새 물음을 세우면 같은 측정으로 닫히는 것이 한 편 더 생긴다. §194 도 이것을 물음으로 적지 않았다 — 지어내지 않고 SSOT 에 남긴다"
|
|
},
|
|
{
|
|
"id": "SSOT-194-oq-1-does-the-guide-rebuild-this-lab",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#194-이-부에서-파생될-open-question",
|
|
"final/document.md#184-이-부의-출처와-범위"
|
|
],
|
|
"summary": "생성 명령은 재실행으로 검증되지 않았다 — VM 을 다시 만들거나 k3s 를 다시 깔면 돌고 있는 실험대가 없어지기 때문이다. 가이드대로 쳐서 이 상태가 다시 서는지는 확인된 적이 없다(unknown)",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:does-the-guide-rebuild-this-lab",
|
|
"reason": "답이 아직 없고, 답에 따라 설계가 갈리며(재구축이 복원 경로인가 아닌가), 다음 검증과 종료 기준이 명확하다. 제5부의 question:qcow2-transfer-time-over-wifi 가 「옮길 수 없는 실험대는 문서로만 복원된다」고 적었고 reference:verify-a-build-guide-in-execution-order 가 「이 규칙으로 가이드를 고친 뒤 처음부터 다시 따라가 본 기록이 아직 없다」고 적은 자리를 이 물음이 정면으로 받는다. 셋이 relations 로 이어지고 재는 것은 서로 다르다"
|
|
},
|
|
{
|
|
"id": "SSOT-194-oq-4-three-outputs-were-not-captured",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#194-이-부에서-파생될-open-question",
|
|
"final/document.md#186-단계-00-lab-host-가상화-준비",
|
|
"final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat"
|
|
],
|
|
"summary": "`lsmod | grep kvm` 의 실제 출력, 03 의 `curl -I http://192.168.122.11` 출력, 04 의 `curl -v` 협상 출력 — 셋 다 캡처해 두지 않았다(unknown)",
|
|
"disposition": "NEEDS_EVIDENCE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "물음이 아니라 증거 공백이다. 셋 다 이미 판정에 쓴 값이고(①의 404, TLS 협상, KVM 사용 여부) 출력만 남기지 않았으므로 새로 물을 것이 없다. 다시 쳐서 `final/evidence/raw/` 에 원문을 남기면 그때 이 부의 글감들에 증거가 붙는다"
|
|
},
|
|
{
|
|
"id": "SSOT-194-oq-5-token-length-varies-by-release",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#194-이-부에서-파생될-open-question",
|
|
"final/document.md#188-단계-02-k3s-server-와-agent"
|
|
],
|
|
"summary": "k3s 토큰 108자는 판올림에 따라 달라진다. 다른 판에서 몇 자인지는 재지 않았다(unknown)",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": null,
|
|
"reason": "답이 달라져도 바뀌는 것이 없다 — 가이드가 이미 「중요한 것은 값이 아니라 `0` 이 아니라는 사실」이라고 적었고 가드도 `-ge 50` 이라 자릿수에 기대지 않는다. 설계가 답에 걸리지 않으므로 Question 이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-194-oq-6-remeasure-2305s-in-the-guest-layout",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#194-이-부에서-파생될-open-question",
|
|
"final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신"
|
|
],
|
|
"summary": "갱신에서 서빙까지 2305초는 훅이 물리 호스트에만 있던 시절의 값이다(inferred). 엣지 VM 배치에서 다시 재면 같은 수가 나오는지는 재지 않았다(미측정)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"reason": "그 Case 의 관측이 어디까지 유효한지를 긋는 문장이라 missing-verification 절 그대로다. 따로 Question 으로 세우면 같은 Case 를 읽지 않고는 무엇을 재는지 말할 수 없는 물음이 한 편 더 생긴다"
|
|
},
|
|
{
|
|
"id": "SSOT-195-part7-source-and-scope",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#195-이-부의-출처와-범위"
|
|
],
|
|
"summary": "제7부의 출처(`source/docs/lab-virtualization.md` 496줄 전문)·리비전 `9465582b`·측정일 2026-09-10·옮긴 방식(heading 줄만 손대고 나머지는 줄 단위 대조해 차이 0)·원본 절과 이 문서 절을 잇는 표",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "이 부가 어디서 왔고 어디까지가 유효한지를 적은 자리다. §1·§178·§184 와 같은 성격이라 같은 처분을 준다 — 독립 기록으로 읽을 사람이 없고, 분석에는 반드시 남아야 한다"
|
|
},
|
|
{
|
|
"id": "SSOT-195-what-this-part-measured-first",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#195-이-부의-출처와-범위",
|
|
"final/document.md#196-이-문서가-무엇인가"
|
|
],
|
|
"summary": "제1~4부에는 이 호스트에서 잰 값이 하나도 없고 제5·6부는 버전·주소·명령을 관측했다. 이 부가 처음으로 **자원의 양과 시간**을 잰다. 표기 규약도 원본이 스스로 밝혔다 — 추정값·예상값이 없고 없는 값은 「미측정」으로 적는다(코드 블록 출력이 observed, 「미측정」이 unknown)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:declared-memory-and-disk-are-ceilings-not-occupancy",
|
|
"reason": "그 Case 가 인용하는 수가 어느 등급의 근거인지를 정하는 전제다. 떼어 내면 Case 의 숫자가 어디까지 믿을 수 있는지 말할 자리가 없어진다. 「가이드는 무엇을 치는가, 이 문서는 그때 어떤 값이 나왔나」라는 §196 의 역할 분담도 같은 문단에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-197-measurement-environment",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#197-측정-환경"
|
|
],
|
|
"summary": "측정 환경 실측 — 논리 코어 8(4코어×2스레드) · `Mem: 11648` 에 `available 6005` · `/` 226G 중 9.9G · libvirt 12.7.0 · QEMU 11.1.1 · 커널 7.2.2-arch1-1. vCPU 합 `2+2+1=5` 로 여유를 뒀고 오버커밋은 libvirt 가 막지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:declared-memory-and-disk-are-ceilings-not-occupancy",
|
|
"reason": "그 Case 의 환경 절이다. 값이 이 호스트 한 대에서 나온 것이라 호스트 사양 없이는 「8240MB 를 할당했는데 돈다」가 무슨 뜻인지 말할 수 없다. 「VM 을 얼마나 더 띄울 수 있는지는 free 가 아니라 available 로 본다」도 같은 절에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-197-nested-virtualization-is-on-but-unused",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#197-측정-환경"
|
|
],
|
|
"summary": "`kvm_intel.parameters.nested` 가 `Y` 로 켜져 있지만 이 실험대는 쓰지 않는다 — 일회용으로 만들려는 층(게스트)은 이미 일회용이고 비싼 층(호스트의 인증서·DNS)은 중첩으로 해결되지 않으며, 지연 주입처럼 **시간을 재는 실험**에서 중첩은 측정값을 왜곡한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:declared-memory-and-disk-are-ceilings-not-occupancy",
|
|
"reason": "Decision 의 모양이지만 대안을 견준 기록도 감수한 비용도 SSOT 에 없어 그 조건에 못 미친다. 다만 「켜져 있는데 안 쓴다」는 측정 환경을 읽을 때 반드시 알아야 하는 사실이라 그 Case 의 환경 절에 한 줄로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-198-allocation-is-a-ceiling-not-an-occupancy",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#198-자원-할당과-실사용은-다르다",
|
|
"final/document.md#199-디스크-오버레이는-얼마나-쓰나"
|
|
],
|
|
"summary": "k3s 만 떠 있는 상태에서 `kc-lab-1` 은 할당 5120MB 에 실사용 353MB(7%), `kc-lab-2` 는 할당 3120MB 에 실사용 301MB 였다. 디스크도 같다 — 20GB 를 두 장 선언했는데 실제 파일은 1.4GiB 와 665MiB 이고 바닥 `base.qcow2` 는 `virtual size` 3GiB 에 `disk size` 335MiB 다. 그래서 11.6GB·226GB 짜리 호스트 한 대에서 게스트 세 대가 돈다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:declared-memory-and-disk-are-ceilings-not-occupancy",
|
|
"reason": "하나의 물음(선언한 양과 실제로 드는 양이 얼마나 다른가)에서 나와 하나의 결론(선언은 상한이고 점유는 한 자릿수 %다)에 닿는 관측 넷이라 한 Case 다. 메모리·디스크·풀·철거 회수량을 쪼개면 같은 결론의 부분 증상 넷이 된다. 제7부가 처음으로 이 호스트의 **양**을 잰 자리이고, 제1~4부의 개념(§42 configured ≠ resident · §143 virtual size ≠ 실제 할당)이 이 호스트에서 얼마인지를 처음 말한다"
|
|
},
|
|
{
|
|
"id": "SSOT-198-dommemstat-actual-is-not-max-memory",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#198-자원-할당과-실사용은-다르다"
|
|
],
|
|
"summary": "`virt-install --memory 4096` 으로 만든 `kc-lab-2` 의 `dommemstat` `actual` 이 3120 으로 보인다. `actual` 은 **현재 할당**이지 선언한 상한이 아니고 상한은 `virsh dominfo` 의 `Max memory` 에 있다. 줄어든 원인은 virtio-balloon 회수로 보이지만(inferred) 두 값을 나란히 찍어 보지는 않았다(미측정)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:declared-memory-and-disk-are-ceilings-not-occupancy",
|
|
"reason": "그 Case 의 진단 자체다. 「왜 4096 이 3120 으로 보이나」에 답하지 않으면 앞의 표가 오타처럼 읽힌다. 따로 세우면 한 사건을 둘로 쪼개는 것이 되고, 확정되지 않은 부분(balloon 귀속·Max memory 미측정)은 그 Case 의 `missing-verification` 으로 간다"
|
|
},
|
|
{
|
|
"id": "SSOT-199-pool-allocation-is-not-vm-usage",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#199-디스크-오버레이는-얼마나-쓰나"
|
|
],
|
|
"summary": "`virsh pool-info default` 의 `Allocation 7.84 GiB` 는 **풀이 얹힌 호스트 루트 파일시스템 전체**의 사용량이지 VM 만의 사용량이 아니다. VM 이 얼마를 쓰는지는 `ls -l` 로 본다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:declared-memory-and-disk-are-ceilings-not-occupancy",
|
|
"reason": "그 Case 가 인용하는 세 번째 숫자가 다른 것을 세고 있다는 경고다. 두 문장이면 Case 안에서 끝나고, 「도구 출력이 대상 상태가 아니다」의 한 예이기도 해 reference:tool-output-is-not-the-subject-state 가 관계로 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-200-ssh-opens-before-cloud-init-finishes",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#200-부팅-cloud-init-은-얼마나-걸리나"
|
|
],
|
|
"summary": "`package_update: true` 에 패키지 5개를 받는 엣지 게스트의 cloud-init 이 약 50초에 `status: done` 이 됐다. SSH 가 붙는 것과 cloud-init 이 끝난 것은 다르고 — SSH 가 먼저 열리고 패키지 설치가 뒤에 이어진다 — `done` 을 안 기다리고 다음 단계를 치면 「방금 깐 패키지가 없다」가 나온다. `running`·`done`·`error` 셋을 구분한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reason": "그 Case 가 이미 「약 50초」를 인용하고 있고, 여기는 그 수가 어디서 나왔는지와 세 상태의 구분을 준다. 그 Case 의 물음(왜 「SSH 가 안 붙는다」 하나로만 보이나)과 이 관측의 물음(SSH 가 붙었는데 왜 아직 안 끝났나)이 같은 축의 앞뒤라 표의 한 행으로 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-201-dhcp-reservation-observed-behaviour",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#201-네트워크-dhcp-예약의-실제-동작"
|
|
],
|
|
"summary": "`net-update ... --live --config` 이 성공하면 `Updated network default persistent config and live state` **두 마디가 다 나온다**(한쪽만 나온 것을 성공으로 읽으면 「재부팅하니 IP 가 바뀐다」가 된다). 예약을 먼저 넣고 `virt-install` 해 게스트가 첫 부팅에 `.10` 을 받았다. `net-dumpxml` 의 예약은 **줄 의도**이고 `net-dhcp-leases` 는 **실제로 준 기록**이라 둘이 다를 수 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:fix-guest-addresses-with-a-dhcp-reservation",
|
|
"reason": "그 Decision 이 감수한 비용의 관측 근거다 — 순서를 지켜야 하고, 출력 두 마디를 다 봐야 하며, 의도와 기록이 갈릴 수 있다는 것이 예약 방식을 고른 대가다. 관측만 떼어 독립 기록으로 만들면 왜 그 방식을 골랐는지가 빠진다"
|
|
},
|
|
{
|
|
"id": "SSOT-201-virbr0-goes-down-when-no-guest-is-attached",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#201-네트워크-dhcp-예약의-실제-동작"
|
|
],
|
|
"summary": "게스트를 전부 철거하면 `virbr0` 가 `DOWN` 이 되는데 **주소 `192.168.122.1/24` 는 그대로 남아 있다.** 붙은 tap 인터페이스가 하나도 없어서이고 네트워크 정의가 사라진 것이 아니라 VM 을 다시 띄우면 자동으로 `UP` 이 된다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "그 Reference 가 모은 「빈 출력이 부재가 아닌 것」의 반대쪽 예다 — 여기서는 `DOWN` 이라는 값이 고장이 아니다. 같은 규칙(그 명령이 무엇을 세는지부터 가른다)의 사례라 그 기록의 표에 행으로 들어가고, 따로 세우면 한 줄짜리 Case 가 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-202-teardown-with-real-output",
|
|
"kindCandidate": "SETUP",
|
|
"sourceRefs": [
|
|
"final/document.md#202-철거-실제-출력-전문"
|
|
],
|
|
"summary": "2026-09-10 에 실제로 돌린 철거 절차와 그 출력 전문 — 게스트 셋을 `virsh destroy` 후 `undefine --remove-all-storage`(`Volume` 줄이 `vda`·`vdb` 둘 다 나와야 한다), DHCP 예약을 `net-update delete` 로 지우고(삭제할 때도 `mac`·`name`·`ip` 세 속성을 다 준다), 철거 전후를 다섯 줄로 견준다(`df -h /` 11G→7.9G, 3.1GB 회수)",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:tear-down-the-lab-and-know-what-survives",
|
|
"reason": "SSOT 가 직접 적었다 — 「가이드에는 세우는 절차만 있고 철거가 없다」. 기존 Setup 일곱은 00~06 단계를 세우는 쪽만 덮고 내리는 쪽이 비어 있다. 읽는 사람이 자기 기계에서 그대로 치는 명령이고(루프·XML 조각·확인 명령), 글쓴이만 다시 돌릴 재현 순서가 아니다 — 실험대를 쓰는 사람은 반드시 한 번은 내린다"
|
|
},
|
|
{
|
|
"id": "SSOT-202-zsh-does-not-word-split-unquoted-variables",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#202-철거-실제-출력-전문"
|
|
],
|
|
"summary": "예약 삭제를 zsh 에서 루프로 돌리면 `XML error: Cannot use host name '' in network 'default'` 가 난다. zsh 는 따옴표 없는 변수를 단어 분리하지 않아 bash 에서 되던 `set -- $entry` 가 `$1` 에 문자열 전체를 넣고 `$2`·`$3` 을 비운다. 세 줄을 값 그대로 쓰는 편이 안전하다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:tear-down-the-lab-and-know-what-survives",
|
|
"reason": "그 Setup 의 「막히면」 한 행이다. 셸이 달라 명령이 다르게 동작하는 것은 reference:verify-a-build-guide-in-execution-order 가 이미 두 축(시점·셸) 중 하나로 세어 둔 유형이라 새 규칙이 아니고, 절차를 따라 치는 사람이 바로 만나는 자리라 그 Setup 안에 둔다"
|
|
},
|
|
{
|
|
"id": "SSOT-202-destroy-is-pulling-the-plug",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#202-철거-실제-출력-전문"
|
|
],
|
|
"summary": "`virsh destroy` 는 종료 신호가 아니라 **전원을 뽑는 것**이다. 정상 종료를 원하면 `virsh shutdown` 을 쓰고 꺼질 때까지 기다린다. `--remove-all-storage` 를 빠뜨리면 도메인만 사라지고 디스크 파일이 남아 다음 `virt-install` 이 「이미 있다」로 실패한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:tear-down-the-lab-and-know-what-survives",
|
|
"reason": "그 Setup 의 가드레일 둘이다. 한 문장씩이면 절차 안에서 끝나고, 정상 종료 순서 쪽은 setup:power-cycle-the-lab-and-reallocate-guest-memory 가 따로 받는다 — 지우는 것과 껐다 켜는 것은 다른 절차다"
|
|
},
|
|
{
|
|
"id": "SSOT-203-a-config-file-does-not-travel-between-distros",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#203-실측으로-드러난-함정-셋"
|
|
],
|
|
"summary": "실측으로 드러난 함정 셋이 전부 배포판 차이였다 — ① 게스트 cloud-init 22.4.2 의 스키마 검사기가 `sudo` 리스트 형태를 거부하는데 부팅은 된다 ② `http2 on;` 은 nginx 1.25.1 이상이라 Debian 12 의 1.22 에서 `unknown directive` 이고 `listen 443 ssl http2;` 형태는 양쪽에서 다 돈다 ③ Debian 계열은 `sites-enabled/default` 가 처음부터 `:80 default_server` 로 붙어 있어 충돌한다. 「배포판이 바뀌면 함정도 바뀐다」",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:a-config-file-does-not-mean-the-same-thing-on-two-distros",
|
|
"reason": "원 프로젝트의 이름(hyeonworks·kc-lab·Arch·Debian 12)을 지워도 규칙이 남는다 — 설정을 다른 배포판으로 옮길 때 지시어의 판올림·패키지가 기본으로 켜 둔 것·그 배포판이 갖고 있지 않은 관례 셋을 본다. 셋을 쪼개지 않은 것은 같은 물음에서 나와 같은 결론에 닿기 때문이고, Case 로 두지 않은 것은 이 실험대의 사건이 아니라 다음 프로젝트에도 적용되는 판정 기준이기 때문이다"
|
|
},
|
|
{
|
|
"id": "SSOT-203-the-schema-checker-rejects-what-actually-boots",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#203-실측으로-드러난-함정-셋"
|
|
],
|
|
"summary": "`sudo: ['ALL=(ALL) NOPASSWD:ALL']` 리스트 형태를 `cloud-init schema` 가 거부하면서 `users.0` 블록을 통째로 찍고 「어느 스키마에도 안 맞는다」고만 한다. 그런데 그 형태로도 부팅은 되고 `kc-lab-1`·`kc-lab-2` 가 그 상태로 NOPASSWD sudo 가 멀쩡히 돌았다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reason": "그 Case 가 이미 「반대 방향도 한 번 있다」로 이 관측을 담고 있다. 여기 것이 원문이라 그 기록의 근거로 들어가고, 따로 세우면 같은 결함의 반대쪽 증상이 두 편이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-204-what-survives-a-teardown",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#204-재구축할-때-무엇이-남아-있나"
|
|
],
|
|
"summary": "철거해도 남는 것(`base.qcow2` 335MB · 패키지 · cloud-init YAML · `~/.ssh/config` 항목 · libvirt `default` 네트워크 정의 · `/etc/letsencrypt/`)과 사라지는 것(게스트 디스크·시드 ISO · DHCP 예약 · k3s 와 모든 워크로드)을 아홉 행으로 가른다. 모르면 재구축이 「왜 이건 이미 있지」와 「왜 이건 없지」의 반복이 된다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:tear-down-the-lab-and-know-what-survives",
|
|
"reason": "철거 절차의 결과를 적은 것이라 그 Setup 의 「이 절차가 끝나면 무엇이 남아 있나」 절이다. 떼어 내면 절차만 남고 왜 이 순서로 지우는지가 빠진다 — 남길 것을 먼저 알아야 `--remove-all-storage` 를 어디에 주는지가 정해진다"
|
|
},
|
|
{
|
|
"id": "SSOT-204-certbot-authenticator-was-never-confirmed",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#204-재구축할-때-무엇이-남아-있나",
|
|
"final/document.md#266-dns-01-은-언제-쓰는가-네-가지-경우"
|
|
],
|
|
"summary": "`/etc/letsencrypt/` 를 지우지 않는 이유는 한도가 아니라 **지금 재발급이 되는지를 모른다**는 것이다. 이 실험대의 이름 셋은 tailnet 주소 `100.83.212.4`(`100.64.0.0/10`, CGNAT 예약 대역)를 가리켜 공개 인터넷에서 라우팅되지 않는다. 설정이 `dns-cloudflare` 면 다시 받으면 끝이고 `webroot`·`standalone` 이면 검증 방식부터 손봐야 하는데, 호스트의 `sudo` 가 비밀번호를 요구해 비대화식으로 읽지 못했다(미측정)",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "question:is-this-lab-issuing-certificates-with-http-01-or-dns-01",
|
|
"reason": "답이 아직 없고, 한 줄(`sudo grep -H authenticator /etc/letsencrypt/renewal/*.conf`)로 닫히며, 답에 따라 실제로 갈리는 것이 둘이다 — 재구축 때 `/etc/letsencrypt/` 를 지워도 되는지, 그리고 decision:dns-01-because-the-lab-is-not-on-the-public-internet 가 적은 것이 이 실험대의 현재 상태인지다. §266 이 같은 자리에서 문서 둘이 어긋나 있다고 밝혔다 — 가이드 04 는 `--webroot`(HTTP-01)로 적혀 있는데 그 전제(공개 DNS 가 이 호스트를 가리킨다)는 지금 성립하지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-206-part8-source-and-scope",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#206-이-부의-출처와-범위"
|
|
],
|
|
"summary": "제8부의 출처(`source/deploy/lab/edge/` 네 파일 125줄)와 대조 원장 — 실행되는 줄 49 는 §189·§190 에 **전부** 들어 있었고 빠진 것은 주석 59줄이다(파일마다 몇 줄인지 표로 있다). 공백을 정규화해 대조했고 인용은 영어 원문 그대로 두고 옆에 우리말을 붙인다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "무엇이 이미 있었고 무엇이 없었는지를 센 원장이다. 커버리지 원장은 분석에는 반드시 남아야 하고 공개 기록으로 읽을 사람이 없다 — candidate-disposition.md 가 `KEEP_IN_SSOT` 의 예로 그대로 든 종류다"
|
|
},
|
|
{
|
|
"id": "SSOT-207-the-host-carries-exactly-one-traffic-rule",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#207-lab-edge-dnat-nft-dnat-파일이-자기-안에-적어-둔-네-가지"
|
|
],
|
|
"summary": "DNAT 파일이 자기 첫 줄에 「This is the ONLY lab traffic rule the physical host carries」라고 적고, 옮겨 간 것의 목록을 이어 적는다 — nginx 설정·인증서·certbot·deploy 훅. PREROUTING nat 이 라우팅 결정보다 먼저 돌아 호스트에 리스너가 남아 있어도 이 규칙이 이기므로 전환이 원자적이고 되돌리기는 `nft delete table ip lab_edge` 한 줄이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:edge-nginx-moved-into-a-guest-vm",
|
|
"reason": "그 Decision 의 **완료 상태를 파일 자신이 선언한 것**이다. 「더러워지는 층의 격리」가 실제로 어디까지 갔는지가 이 주석에 목록으로 있고, 전환이 원자적이라는 것과 되돌리기 한 줄이 그 결정의 감수한 비용 옆에 들어간다. 떼어 내면 결정이 실행됐다는 증거가 기록 밖에 남는다"
|
|
},
|
|
{
|
|
"id": "SSOT-207-no-forward-chain-on-purpose",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#207-lab-edge-dnat-nft-dnat-파일이-자기-안에-적어-둔-네-가지"
|
|
],
|
|
"summary": "「No forward chain here on purpose」 — libvirt 의 `guest_input` 이 `oif virbr0 ... reject` 로 끝나고 앞 base 체인의 accept 가 뒤 체인의 reject 를 막지 못하므로 구멍은 libvirt 자기 체인 안 맨 앞에 유닛의 `ExecStartPost` 로 뚫는다. **그 `forward` 체인을 지웠다는 사실과 지운 이유를 파일이 자기 자리에 적어 두었다**",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:nftables-accept-did-not-stop-the-libvirt-reject",
|
|
"reason": "**이것이 그 Case 의 결말이다.** §180 은 먼저 도는 `forward` 체인에 `ct state new accept` 를 넣었다가 안 먹힌 이유까지 적고 고친 파일이 어떻게 되었는지는 적지 않았다. 답이 여기 있어 그 Case 의 결론이 비로소 닫힌다 — §189 의 코드 블록에 남아 있던 빈 줄 하나가 그 자리다"
|
|
},
|
|
{
|
|
"id": "SSOT-207-dnat-only-never-snat",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#207-lab-edge-dnat-nft-dnat-파일이-자기-안에-적어-둔-네-가지"
|
|
],
|
|
"summary": "「DNAT only, never SNAT」 — 게스트의 기본 경로가 호스트이므로 응답이 이 자리를 다시 지나고 conntrack 이 변환을 알아서 되돌린다. masquerade 를 붙이면 엣지가 모든 클라이언트를 `192.168.122.1` 로 보게 되어 이 실험대가 재는 `X-Forwarded-For` 계약이 **조용히 무효가 된다**",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:overwrite-a-forwarded-header-at-the-trust-boundary-do-not-extend-it",
|
|
"reason": "그 Reference 와 같은 규칙의 커널 쪽 절반이다 — 경계 앞에서 출발지 주소를 바꾸지 않는 것과 경계에서 클라이언트가 보낸 헤더를 버리는 것이 같은 계약을 지킨다. 두 기록으로 나누면 「무엇이 그 값을 믿을 수 있게 하는가」가 둘로 갈린다"
|
|
},
|
|
{
|
|
"id": "SSOT-208-the-hyphen-before-execstartpost",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#208-lab-edge-dnat-service-execstartpost-앞의---가-무엇을-봐주나"
|
|
],
|
|
"summary": "`ExecStartPost=-` 의 하이픈은 「이 명령이 실패해도 유닛을 실패로 보지 않는다」는 뜻이다. `libvirt_network` 테이블은 가상 네트워크가 떠 있어야 존재해서 부팅 순서에 따라 이 유닛이 먼저 돌 수 있고, `-` 가 없으면 규칙 삽입 실패에 **DNAT 까지 같이 안 실린다.** `-` 를 두면 DNAT 는 실리고 구멍만 빠져 나중에 `systemctl restart` 한 번으로 다시 뚫린다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:edge-nginx-and-host-dnat",
|
|
"reason": "그 Setup 이 유닛 파일을 그대로 싣는데 §189 는 이 하이픈을 설명하지 않았다. 절차를 따라 치는 사람이 유닛을 보고 바로 묻는 자리라 그 기록의 가드레일에 들어간다. 세 문장이면 끝나 따로 Concept 으로 세울 크기가 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-208-an-active-unit-does-not-mean-the-hole-is-open",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#208-lab-edge-dnat-service-execstartpost-앞의---가-무엇을-봐주나"
|
|
],
|
|
"summary": "`-` 때문에 구멍 삽입이 실패해도 유닛은 `active` 이고 아무 오류도 없다. 그러므로 이 유닛이 `active` 라는 것은 **DNAT 가 실렸다는 뜻이지 구멍이 뚫렸다는 뜻이 아니다** — 그 상태는 §180 의 증상과 똑같이 「호스트 안에서는 되는데 밖에서만 안 된다」로 보인다",
|
|
"disposition": "NEEDS_EVIDENCE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "SSOT 가 스스로 `inferred` 로 표시했다 — **이 상태를 실제로 재현해 보지 않았다.** 두 층이 갈린다: 하이픈의 systemd 의미와 `libvirt_network` 가 네트워크에 딸려 존재한다는 것은 관측이지만, 「그래서 유닛이 active 인 채로 밖에서만 막힌 상태가 된다」는 재현되지 않은 결말이다. 그 결말이 이 후보의 전부라 지금 올리면 검증 없는 실패 모드를 기록으로 굳히게 된다. Open Question 으로 열지 않은 이유는 답에 따라 설계가 달라지지 않기 때문이다 — `-` 는 이미 의도대로 붙어 있고 바뀔 것이 없다. 재현하는 방법은 정해져 있다(가상 네트워크를 내린 상태에서 유닛을 기동하고 `systemctl is-active` 와 밖에서의 `curl` 을 함께 찍는다). 그 출력이 `final/evidence/raw/` 에 남으면 case:nftables-accept-did-not-stop-the-libvirt-reject 의 두 번째 재현 조건으로 다시 판정한다"
|
|
},
|
|
{
|
|
"id": "SSOT-209-the-sticky-session-switch-left-off",
|
|
"kindCandidate": "OPEN_QUESTION",
|
|
"sourceRefs": [
|
|
"final/document.md#209-nginx-keycloak-lab-conf-스티키-스위치와-신뢰-경계",
|
|
"final/document.md#260-스티키-세션"
|
|
],
|
|
"summary": "`upstream k3s_traefik` 안에 `# ip_hash;` 가 주석으로 남아 있고, 파일이 그 옆에 **끈 쪽이 관찰할 거리가 있는 상태**라고 적는다 — Infinispan 이 라우팅을 해 주므로 실패하지는 않고 느려질 뿐이다. Keycloak 이 권장하는 것은 `AUTH_SESSION_ID` 쿠키 기반 어피니티이고 `ip_hash` 는 브라우저 한 대짜리 실험대의 값싼 대용품이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:edge-nginx-and-host-dnat",
|
|
"reason": "그 Setup 이 세우는 `nginx-keycloak-lab.conf` 안의 줄이고, 주석 처리된 한 줄이 실험 설계라는 것은 그 파일을 옮겨 놓는 절차가 반드시 말해야 하는 것이다. **끈 쪽이 얼마나 느려지는지를 재는 것은 여기서 열지 않는다** — 그것은 세션이 어디에 있는지를 측정으로 다루는 `keycloak-session-store` 의 물음이고, 이 프로젝트에서 열면 같은 실험이 두 저장소에서 따로 굴러간다"
|
|
},
|
|
{
|
|
"id": "SSOT-209-overwrite-the-forwarded-header-do-not-extend-it",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#209-nginx-keycloak-lab-conf-스티키-스위치와-신뢰-경계",
|
|
"final/document.md#259-x-forwarded--와-신뢰-경계"
|
|
],
|
|
"summary": "「$remote_addr, not $proxy_add_x_forwarded_for. This is the trust boundary: a client-supplied X-Forwarded-For must be discarded, not extended, or nothing downstream can rely on the value.」 §189·§190 은 이 줄을 싣기만 하고 왜 그 형태인지를 적지 않았다 — 덧붙이면 위조된 값이 사슬에 남고 뒤쪽의 어느 것도 그 헤더를 근거로 쓸 수 없다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:overwrite-a-forwarded-header-at-the-trust-boundary-do-not-extend-it",
|
|
"reason": "**이 저장소 어디에도 없던 설명이다** — §206 이 「둘은 이 저장소 어디에도 없다」로 센 둘 중 하나다. 원 프로젝트의 이름을 지워도 규칙이 남고(맨 바깥 프록시는 클라이언트가 보낸 forwarded 헤더를 버린다), 적용 조건과 예외가 자료에서 나온다 — 경계 안쪽의 두 번째 홉은 덧붙이는 쪽이 맞고 §207 의 「SNAT 를 걸지 않는다」가 같은 계약의 커널 쪽 절반이다. Case 결론을 선언문으로 바꾼 것이 아니라 이 실험대가 **재려고 세운 계약** 자체다"
|
|
},
|
|
{
|
|
"id": "SSOT-209-http2-as-a-listen-parameter",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#209-nginx-keycloak-lab-conf-스티키-스위치와-신뢰-경계"
|
|
],
|
|
"summary": "`http2` 를 별도 지시어가 아니라 `listen` 의 인자로 쓴 이유를 파일이 적는다 — `http2 on;` 은 nginx 1.25.1 이상이 필요하고 엣지 게스트는 Debian 12(nginx 1.22)다. **이 형태가 Arch 의 1.30 과 Debian 12 의 1.22 양쪽에서 다 돈다**는 확인이 파일 쪽에만 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:a-config-file-does-not-mean-the-same-thing-on-two-distros",
|
|
"reason": "그 Reference 의 ① 축(지시어가 그 판올림에 있는가)의 근거이고, 「양쪽에서 다 도는 형태가 무엇인가」가 그 규칙이 실제로 내놓는 답이다. §203② 와 같은 관측의 다른 판이라 따로 세우면 한 사실이 두 기록이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-209-the-file-header-still-names-the-old-location",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#209-nginx-keycloak-lab-conf-스티키-스위치와-신뢰-경계"
|
|
],
|
|
"summary": "`nginx-keycloak-lab.conf` 의 머리말이 「lab host 에 배포한다」·「Arch 는 그 관례를 주지 않는다」로 적혀 있는데, **같은 파일 아래쪽 주석은 「엣지 게스트는 Debian 12」라고 적는다.** 파일이 있는 자리도 `deploy/lab/edge/` 이고 §179 대로 엣지는 게스트로 옮겨졌다 — 머리말만 옮기기 전 상태로 남았다(inferred)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:verify-a-build-guide-in-execution-order",
|
|
"reason": "그 Reference 가 이미 센 결함 여섯과 **같은 종류**다 — 각 줄은 어느 시점엔가 참이었고 틀린 것은 명령이 아니라 그 명령이 놓인 위치다. 다른 것은 찾은 자리뿐이라(그쪽은 가이드, 이쪽은 설정 정본) 그 기록의 표에 일곱 번째 행으로 들어가고, 규칙의 적용 범위가 가이드 밖으로 한 칸 넓어진다"
|
|
},
|
|
{
|
|
"id": "SSOT-210-renewed-lineage-is-the-mechanical-criterion",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#210-reload-nginx-sh-deploy-와-post-를-가르는-한-줄"
|
|
],
|
|
"summary": "`deploy/` 와 `post/` 를 가르는 것은 `RENEWED_LINEAGE` 환경 변수다 — certbot 이 그것을 세운 실행에서만 `deploy/` 를 돈다. 파일이 그 옆에 실패를 이름으로 적어 두었다 — 「Without this, D-4 measured the failure exactly: the renewal succeeds, the timer reports SUCCESS, and the old certificate keeps being served for 38m25s」. `38m25s` 와 SSOT 의 `2305초` 는 같은 값이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:renewal-succeeded-while-the-old-certificate-kept-serving",
|
|
"reason": "그 Case 의 진단에 기계적 기준 하나가 빠져 있었다 — 「실제로 갱신됐을 때만」이 무엇으로 판정되는지가 `RENEWED_LINEAGE` 다. 실험 이름 `D-4` 도 그 Case 의 근거 표시가 되지만 원문 `experiment-d4-certificate-renewal.md` 는 `source/` 에 반입되지 않았다(unknown) — 그 사실도 같은 기록의 유효 범위에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-211-part9-source-and-scope",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#211-이-부의-출처와-범위"
|
|
],
|
|
"summary": "제9부의 출처(`source/docs/session-lab-concepts.md` 4,725줄 전문)·리비전 `9465582b`·옮긴 방식(원본의 `###` 를 절로 올리고 `####` 를 `###` 로 내렸다. heading 이 아닌 줄은 한 글자도 바꾸지 않았고 차이는 비밀 자리표시자 한 줄뿐이다)·13개 층과 절 번호를 잇는 표·원본 안의 죽은 링크 목록",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "§1·§178·§184·§195·§206 과 같은 자리다 — 이 부가 어디서 왔고 어디까지가 유효한지를 적었다. 분석에는 반드시 남아야 하고 독립 기록으로 읽을 사람이 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-211-two-snapshots-on-two-different-days",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#211-이-부의-출처와-범위",
|
|
"final/document.md#218-실험대-전체-배치-2026-09-03-구축-완료-실측값"
|
|
],
|
|
"summary": "§218 의 전체 배치는 2026-09-03 값이고 제7부는 2026-09-10 값이다. 그 사이에 호스트 RAM 이 8GB→12GB 로 물리 증설됐고(§332) 게스트 메모리가 재배분됐다(§332) — `RAM 7.4Gi`→`Mem: 11648`, `kc-lab-1` `3584M`→할당 5120MB, 게스트 2대→3대. **두 값이 어긋나 보이면 틀린 것이 아니라 다른 날이다**",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:declared-memory-and-disk-are-ceilings-not-occupancy",
|
|
"reason": "그 Case 가 인용하는 수가 언제 것인지를 정하는 전제다. 같은 대상의 두 스냅샷이 SSOT 안에 나란히 있는데 날짜를 붙이지 않으면 그 Case 의 표가 서로 모순된 것으로 읽힌다. 「어긋나 보이면 다른 날이다」는 한 문단이면 끝나 따로 세울 크기가 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-211-overlap-with-keycloak-session-store",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#211-이-부의-출처와-범위"
|
|
],
|
|
"summary": "이 부는 `keycloak-session-store` 프로젝트와 주제가 겹친다 — 원본 제목이 「세션 저장소 실험대 — 개념 사전」이고 §314~§321(Infinispan·JGroups)과 §322~§330(Prometheus)은 그쪽 SSOT 가 측정으로 더 깊이 다루는 영역이다. 그래도 빼지 않는다 — 이 문서는 `docs/virtualization/source/` 로 반입된 것이라 빼면 `source/` 를 지우는 순간 사라진다. 견줄 때는 **저쪽이 측정이고 이쪽이 정의**라는 것을 먼저 본다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "분해 범위를 가른 판단 자체다. 이 한 줄이 §314~§330 의 처분 근거이고, 분석에는 반드시 남아야 하지만 공개 기록으로 읽을 사람이 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-212-most-of-this-is-not-because-of-arch",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#212-\"이건-arch라서-하는-건가-\"에-대한-답"
|
|
],
|
|
"summary": "낯선 명령이 쏟아지는 이유는 Arch 때문이 아니라 셋 중 하나다 — 클라우드가 대신 해줬거나(KVM·libvirt·virbr0·cloud-init·DHCP 예약), 이미 누가 해뒀거나(nginx upstream·certbot·k3s 설치), 진짜 Arch 특유(`conf.d` include 부재·롤링 업그레이드·`libvirtd.socket`·패키지명)다. 셋째는 6층에 모아 둔 몇 개뿐이고 같은 구성을 Ubuntu 에서 해도 1~5층은 명령 이름만 조금 바뀐다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:a-config-file-does-not-mean-the-same-thing-on-two-distros",
|
|
"reason": "그 Reference 의 **적용 범위 절**이다 — 배포판 차이로 돌릴 것과 돌리면 안 되는 것을 가르는 기준이고, 이것이 없으면 그 규칙이 모든 낯선 명령을 배포판 탓으로 돌리는 데 쓰인다. 규칙과 그 규칙이 적용되지 않는 범위는 한 기록 안에 있어야 한다"
|
|
},
|
|
{
|
|
"id": "SSOT-213-two-guest-vms-instead-of-k3s-on-the-host",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#213-왜-호스트에-직접-깔지-않고-vm-2대인가"
|
|
],
|
|
"summary": "호스트에 직접 깔지 않고 VM 2대로 간 이유 여섯 — ① 물리 머신이 한 대라 독립 커널 둘이 없으면 노드 간 방화벽·파티션·노드 상실이 성립하지 않는다 ② qcow2 오버레이를 지우면 몇 초 만에 초기 상태다 ③ **관측 수단이 실험 대상과 함께 죽으면 안 된다** ④ k3s 가 호스트에 nftables 규칙·CNI·커널 모듈을 대량으로 심는다 ⑤ 게스트를 운영 배포판과 맞추면 커널·systemd 차이가 잡음에서 빠진다 ⑥ 커널이 분리돼 netem 지연 주입이 게스트 안에 갇힌다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:two-guest-vms-instead-of-installing-k3s-on-the-host",
|
|
"reason": "명시적 선택 근거가 SSOT 에 그대로 있고 **정직한 반대편과 채택하지 않은 절충안까지 적혀 있다** — 계약 검증(2홉 헤더·쿠키/origin)만 볼 거라면 호스트에 단일 노드 k3s 가 더 빠르고, 「호스트를 노드 1, VM 을 노드 2로」는 게스트 OS 350MB 와 설치 수고를 아끼지만 ③·④를 포기하게 되어 7.4Gi 예산에서 그 350MB 보다 관측자 분리가 값지다고 판단했다. 이 실험대의 가장 밑에 있는 결정이고, decision:edge-nginx-moved-into-a-guest-vm(엣지가 왜 게스트로 갔나)과는 다른 물음이라 한 기록에 합쳐지지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-214-218-what-the-seed-is-and-is-not",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#214-전체-구조-한눈에-보기",
|
|
"final/document.md#215-vm-한-대의-디스크-구성",
|
|
"final/document.md#216-설정-파일이-게스트에-도달하는-경로",
|
|
"final/document.md#217-부팅할-때-일어나는-일"
|
|
],
|
|
"summary": "가장 자주 오해하는 지점은 **시드 ISO 를 OS 이미지로 착각하는 것**이다. 시드는 OS 가 아니라 설정 데이터만 담은 370KB 짜리 별도 디스크다. VM 한 대의 디스크는 `vda`(20G · ext4 · 여기서 부팅)와 `vdb`(370K · CIDATA · iso9660 · 마운트 안 됨) 둘이고, 같은 내용이 세 곳(원본 YAML · 구워진 ISO · 풀에 올라간 볼륨)에 존재해 **원본만 고치면 VM 에 반영되지 않는다.** 부팅은 다섯 단계이고 3번(LABEL=CIDATA 발견)이 실패하면 hostname 이 `localhost` 로 남는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:a-cloud-image-is-an-installed-disk-and-cloud-init-fills-the-blanks",
|
|
"reason": "그 Concept 이 설명하는 구조의 그림 절이다 — 「설치는 배포자가 미리 끝냈고 개인화만 첫 부팅에 일어난다」가 실제로 어떤 디스크 두 장과 어떤 순서로 벌어지는지가 여기 있다. 떼어 내면 개념만 남고 그 개념이 이 실험대에서 어떤 모양인지가 빠진다"
|
|
},
|
|
{
|
|
"id": "SSOT-218-the-completion-test-is-seven-commands",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#218-실험대-전체-배치-2026-09-03-구축-완료-실측값"
|
|
],
|
|
"summary": "구축 완료 판정 기준 일곱 — `kubectl get nodes` Ready 2개, `dig A` 가 `100.83.212.4`, `curl -sI https://` 가 `HTTP/2 404`, `ssl_verify_result` 가 `0`, `curl -sI http://` 가 `301`, `systemctl is-active nginx certbot-renew.timer` 가 `active active`. **`404` 가 성공 신호다** — TLS 가 정상 종료되고 Traefik 까지 갔는데 매칭되는 Ingress 규칙이 없다는 뜻이고, `502` 나 `connection refused` 면 체인 어딘가가 끊긴 것이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:check-the-nearest-layer-first",
|
|
"reason": "그 Reference 가 이미 「①의 404 가 성공 신호다」를 담고 있고, 여기 것은 같은 판정을 **완료 시점에 한 벌로 묶은 판**이다. 층마다 성공 신호가 다르다는 같은 규칙의 다른 각도라 그 기록의 한 절이 되고, 따로 세우면 같은 규칙이 두 편이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-219-layer-heading-virtualization",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#219-1층-가상화"
|
|
],
|
|
"summary": "1층(가상화)의 층 머리. 이름만 있고 본문이 없다 — 층 이름이 본문 안에서 「6층 참고」처럼 계속 불리기 때문에 지우지 않았다고 §211 이 밝힌다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "문서의 뼈대일 뿐 주장이 없다. 넣을 자리조차 없어 MERGE_INTO 가 아니라 KEEP_IN_SSOT 다"
|
|
},
|
|
{
|
|
"id": "SSOT-220-223-who-does-what-in-the-kvm-stack",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#220-vt-x-amd-v-하드웨어-가상화-확장",
|
|
"final/document.md#221-kvm",
|
|
"final/document.md#222-qemu",
|
|
"final/document.md#223-libvirt-virsh-libvirtd"
|
|
],
|
|
"summary": "VT-x/AMD-V(없으면 못 뜨는 것이 아니라 TCG 로 폴백해 **50배쯤 느려진다**) · KVM(`kvm.ko`+`kvm_intel.ko`. CPU·메모리 가상화만 맡고 장치 에뮬레이션은 안 한다) · QEMU(장치 에뮬레이터. **VM 하나가 호스트에서 도는 QEMU 프로세스 하나**다) · libvirt/virsh/libvirtd(XML 로 정의를 저장하는 관리 계층. 없으면 재부팅에 VM 정의가 전부 사라진다)의 역할 분담",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:kvm-vcpu-to-physical-cpu",
|
|
"reason": "제1부 §3 이 같은 역할 분담을 이미 적었고 그 Concept 이 그것을 담고 있다. 여기 것은 「그래서 패키지를 왜 둘 다 깔아야 하나」와 「VM 메모리 3584M 이 호스트 입장에선 프로세스 RSS 다」라는 실험대 쪽 각도를 더해 그 기록의 한 절이 된다. 넷을 따로 세우면 어느 Case 도 필요로 하지 않는 개념이 넷 쌓인다"
|
|
},
|
|
{
|
|
"id": "SSOT-224-227-what-makes-virsh-answer-at-all",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#224-연결-uri-qemu-system-vs-qemu-session",
|
|
"final/document.md#225-보조-그룹과-재로그인",
|
|
"final/document.md#226-멱등성과-&&-단축-평가",
|
|
"final/document.md#227-systemd-소켓-활성화-libvirtd-socket"
|
|
],
|
|
"summary": "`qemu:///system` 과 `qemu:///session` 은 **완전히 분리된 두 인스턴스**라 어긋나면 만든 VM 이 `virsh list` 에 안 나온다 · `usermod -aG` 뒤 그룹 목록은 로그인 시점에 고정돼 이미 떠 있는 셸에 소급되지 않는다 · libvirt 명령 중에는 멱등하지 않은 것이 있어 `&&` 단축 평가와 엮이면 두 번째 실행이 다르게 끝난다 · `.service` 가 아니라 `.socket` 을 켠다(systemd 가 소켓을 열어 두고 접속이 오면 그때 데몬을 띄운다)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:prepare-the-lab-host-for-virtualization",
|
|
"reason": "**그 Setup 이 이미 가드레일 셋으로 적어 둔 것의 원문이다** — `.socket` 을 켠다, `usermod` 뒤에 다시 들어온다, 목록이 비었다고 부재로 읽지 않는다. 절차를 치는 사람이 왜 그렇게 하는지를 묻는 자리라 그 기록 안에 있어야 하고, 떼어 내면 가드레일에 이유가 없어진다"
|
|
},
|
|
{
|
|
"id": "SSOT-228-230-what-copying-a-disk-image-really-is",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#228-qcow2와-backing-store-오버레이",
|
|
"final/document.md#230-디스크-이미지를-\"복사한다\"는-것의-실제-원리"
|
|
],
|
|
"summary": "디스크는 섹터가 0번부터 늘어선 1차원 배열이고 파티션 테이블도 파일시스템도 부트로더도 전부 그 배열 안의 바이트다 — **디스크 바깥에 보관되는 정보가 없다.** 그래서 배열을 통째로 파일에 담으면 raw 이고 되돌린 디스크는 바이트 단위로 같아 똑같이 부팅한다. qcow2 는 거기에 희소 저장·backing file·스냅샷을 더한 것이고, 그대로 복제하면 machine-id·SSH 호스트키·파일시스템 UUID·hostname 이 같아지는 문제가 남는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:inside-a-qcow2-file-the-mapping-table-and-its-clusters",
|
|
"reason": "그 Concept 의 출발점이다 — 「raw 에 무엇을 더하면 qcow2 가 되는가」를 풀려면 raw 가 무엇인지가 먼저 있어야 하고, §231 이 자기 첫 문장에서 이 절을 그렇게 가리킨다. 복제 시 중복되는 식별자 넷은 concept:a-cloud-image-is-an-installed-disk-and-cloud-init-fills-the-blanks 가 관계로 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-229-why-a-vm-boots-without-an-install",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#229-왜-os를-설치하지-않아도-vm이-뜨는가"
|
|
],
|
|
"summary": "「VM 은 격리된 빈 공간이니 OS 를 설치해야 하는 것 아닌가」 — 격리는 맞지만 설치는 필수가 아니다. 설치는 목적이 아니라 수단이고 목적은 「부팅 가능한 특정 바이트 배열」이라는 **파일 하나의 내용**이다. 배포자가 그 설치를 한 번 해서 qcow2 로 공개한 것이 클라우드 이미지이고, 그대로 복제하면 전부 동일해지므로 hostname·계정·SSH 호스트키·machine-id 를 **일부러 비워 둔 채** 배포한다. **격리는 실행 시점에 KVM/QEMU 가 만드는 것**이지 설치가 만드는 것이 아니다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:a-cloud-image-is-an-installed-disk-and-cloud-init-fills-the-blanks",
|
|
"reason": "**거꾸로 뽑은 Concept 이다.** setup:create-three-guests-with-cloud-init 은 「base 이미지를 받아 오버레이로 게스트 셋을 만든다」로 시작하고 case:cloud-init-failures-all-look-like-ssh-refused 의 원인 넷은 전부 「시드가 안 읽혔다」로 수렴하는데, 둘 다 「설치를 안 했는데 왜 뜨는가」와 「왜 빈칸이 있는가」를 모르면 읽을 수 없다. 그 전제를 Setup 의 명령 옆에 한두 문장으로 끼워 넣을 수 없어(§229·§236 이 합쳐 188줄이다) 독립 기록이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-231-inside-a-qcow2-file",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#231-qcow2-파일-내부는-어떻게-생겼나-매핑표가-전부다"
|
|
],
|
|
"summary": "qcow2 가 raw 에 더하는 것은 **매핑표** 하나다 — 클러스터(기본 64KB)를 최소 단위로 L1→L2 2단계로 찾고, L2 항목이 0 이면 바닥이 없을 때 0 을 만들어 돌려주고 있을 때 바닥의 같은 위치를 읽는다(이것이 오버레이다). refcount 가 1 보다 크면 쓰기 전에 복사하는 것이 copy-on-write 이고 스냅샷이 순식간에 찍히는 이유다. 배포용 이미지는 **클러스터 단위 zlib 압축**이 켜져 있다 — `qemu-img map` 실측으로 1236개 중 606개가 `compressed: True` 였고 3GiB 가 324MiB 가 되는 것은 희소(2.01GiB 가 구멍) · 압축(1010MiB→324MiB) · genericcloud 자체가 작은 것 셋이 겹친 결과다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:inside-a-qcow2-file-the-mapping-table-and-its-clusters",
|
|
"reason": "**concept:what-a-qcow2-file-carries 가 자기 기준 안에서 명시적으로 미뤄 둔 자리다** — 그 기록의 `basis-version` 이 「제4부 §127 이 qcow2 내부 L1/L2 table 을 별도 문서로 미뤘고 §181 도 그 안으로 들어가지 않는다」라고 적었다. 이 절이 그 안이고, 이 호스트에서 실제로 돌린 `qemu-img info`·`qemu-img map` 출력을 갖고 있다. 거꾸로 필요로 하는 것이 둘이다 — question:qcow2-transfer-time-over-wifi 는 「`qemu-img convert` 로 먼저 줄이는 편이 나은가」를 묻는데 압축이 클러스터 단위이고 쓰기가 비압축으로 새로 할당된다는 것을 모르면 무엇을 재는지 말할 수 없고, question:disk-image-format-and-actual-host-usage 는 `qemu-img info`·`du`·`ls` 세 값이 왜 갈리는지를 묻는다"
|
|
},
|
|
{
|
|
"id": "SSOT-232-234-qemu-img-is-not-qemu-system",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#232-qemu-img-와-qemu-system-x86_64-는-다른-도구다",
|
|
"final/document.md#233-오버레이는-docker-레이어와-같은-아이디어다",
|
|
"final/document.md#234-그래서-마이그레이션과-스냅샷이-된다"
|
|
],
|
|
"summary": "`qemu-img` 는 디스크 이미지 파일만 만지고 VM 을 돌리지 않는다(VM 이 꺼져 있어도, 아예 없어도 된다). 오버레이는 Docker 레이어와 같은 copy-on-write 아이디어인데 쓰임이 다르다 — Docker 는 빌드 시점에 의도적으로 쌓고 층의 정체성이 다이제스트인데 qcow2 는 런타임 파생이고 정체성이 **경로 문자열**이다. 디스크가 파일 하나이므로 복사가 곧 이관이고 그래서 마이그레이션과 스냅샷이 된다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:inside-a-qcow2-file-the-mapping-table-and-its-clusters",
|
|
"reason": "그 Concept 의 backing chain 절이 이미 Docker 대조표를 담고 있고, 두 도구의 구분은 그 기록을 읽는 사람이 명령을 칠 때 바로 필요한 한 줄이다. 마이그레이션·스냅샷 쪽은 concept:what-a-qcow2-file-carries 가 이미 담았다 — 셋을 따로 세우면 같은 파일 형식이 다섯 기록으로 흩어진다"
|
|
},
|
|
{
|
|
"id": "SSOT-235-236-cloud-image-and-cloud-init",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#235-multipass-virt-install-virsh-무엇이-다른가",
|
|
"final/document.md#236-클라우드-이미지와-cloud-init"
|
|
],
|
|
"summary": "multipass·virt-install·virsh 는 배포판이 아니라 **계층이 다른 도구**다. 클라우드 이미지는 설치가 끝난 qcow2 이고 빈칸을 첫 부팅에 채우는 것이 cloud-init 이다 — 대안 셋(ISO 정식 설치 · `virt-customize` 로 이미지 개조 · cloud-init) 가운데 셋째를 고른 이유는 **게스트를 반복해서 지우고 다시 만드는 것이 실험 그 자체**이고 두 노드가 바이트 단위로 같은 초기 상태여야 하기 때문이다. `genericcloud` 는 virtio 드라이버만 담아 가볍고 KVM 에는 이것을 쓴다. `user-data` 는 반드시 `#cloud-config` 로 시작해야 하고 아니면 **조용히 무시된다**",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:a-cloud-image-is-an-installed-disk-and-cloud-init-fills-the-blanks",
|
|
"reason": "그 Concept 의 본론이다. §229 가 「왜 설치가 필요 없나」를 풀고 이 절이 「그래서 무엇으로 빈칸을 채우나」를 푼다 — 둘이 한 물음의 앞뒤라 한 기록이고, 쪼개면 전반부만으로는 아무 명령도 설명하지 못한다"
|
|
},
|
|
{
|
|
"id": "SSOT-236-leave-an-emergency-console-password",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#236-클라우드-이미지와-cloud-init"
|
|
],
|
|
"summary": "`ssh_pwauth: false` + 키 인증만 설정한 상태에서 cloud-init 이 실패하면 **그 게스트에 들어갈 방법이 전혀 없다** — 사용자가 생성되지 않았으니 키도 비밀번호도 없고 콘솔에 붙어도 로그인할 수 없어 실패 원인을 적은 `/var/log/cloud-init.log` 를 읽을 수가 없다. 콘솔 로그인은 sshd 가 아니라 로컬 PAM 을 타므로 `plain_text_passwd` 를 넣어 두면 `ssh_pwauth: false` 를 그대로 두고도 이 막다른 골목을 피한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reason": "그 Case 가 이미 「콘솔 비밀번호가 키가 안 들어갔을 때의 유일한 탈출구다」로 담고 있고, 여기 것이 그 이유의 원문이다 — 진단이 불가능해지는 메커니즘(로그를 읽을 수 없다)이 그 기록의 판정 절을 닫는다"
|
|
},
|
|
{
|
|
"id": "SSOT-237-the-seed-was-on-a-bus-the-guest-could-not-see",
|
|
"kindCandidate": "CASE",
|
|
"sourceRefs": [
|
|
"final/document.md#237-확정된-함정---cloud-init-+-debian-genericcloud-조합은-동작하지-않는다"
|
|
],
|
|
"summary": "`virt-install --cloud-init` 은 시드를 **SATA CD-ROM** 으로 붙이는데 Debian `genericcloud` 는 크기를 줄이려고 물리 하드웨어 드라이버를 빼서 AHCI/SATA 장치를 보지 못한다. 게스트에게 시드는 존재하지 않는 장치이고 cloud-init 은 `cidata` 라벨을 못 찾아 데이터소스 없이 조용히 끝난다. 해결은 시드를 `bus=virtio` 디스크로 붙이는 것이고 성공 판정은 `virsh domblklist` 에 `sda` 가 아니라 `vdb` 가 보이는 것이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reason": "그 Case 의 **원인 ① 의 원문**이다. 같은 물음(왜 「SSH 가 안 붙는다」 하나로만 보이나)에서 나와 같은 결론에 닿는 관측이라 그 기록의 표에 이미 행으로 들어가 있고, 따로 세우면 같은 결함의 네 원인 중 하나만 독립 기록이 되어 나머지 셋과 균형이 깨진다"
|
|
},
|
|
{
|
|
"id": "SSOT-238-239-what-the-three-seed-commands-do",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#238-시드-iso-를-굽는-세-명령이-각각-하는-일",
|
|
"final/document.md#239-시드-디렉터리-구조와-파일명-규칙"
|
|
],
|
|
"summary": "`xorrisofs` 로 굽고 `vol-create-as` 로 풀에 자리를 잡고 `vol-upload` 로 내용을 붓는다 — ②와 ③이 나뉜 이유는 libvirt 가 볼륨을 「선언」과 「기록」 두 단계로 다루기 때문이고 ②의 크기 인자가 실제 ISO 와 다르면 ③에서 잘리거나 남는다. `-volid CIDATA` 를 빠뜨리면 cloud-init 이 장치를 못 찾고, NoCloud 는 ISO 루트에서 **정확히 `user-data` 와 `meta-data`** 라는 이름을 찾으므로 `-graft-points` 로 이름을 바꿔 담는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:create-three-guests-with-cloud-init",
|
|
"reason": "그 Setup 이 이미 세 명령을 그대로 싣고 「`vol-create-as` 뒤에 `vol-upload` 가 따라야 한다」를 순서가 결과를 바꾸는 자리로 적어 두었다. 여기 것은 각 옵션이 무엇을 하는지라 그 절차의 주석이 되고, 떼어 내면 붙여 넣는 명령에 이유가 없어진다"
|
|
},
|
|
{
|
|
"id": "SSOT-240-241-two-diagnostic-tools",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#240-진단-도구-virsh-screenshot",
|
|
"final/document.md#241-base-이미지가-무엇인지-확인하는-법"
|
|
],
|
|
"summary": "`virsh screenshot` 은 게스트에 로그인할 수 없을 때 화면을 그대로 PNG 로 떠서 볼 수 있어 **이 문제를 푼 결정적 도구**였다. base 이미지가 무엇인지 의심될 때는 공식 체크섬과 대조한다 — 변종을 잘못 받았거나 받다가 끊겨 HTML 오류 페이지를 저장했으면 `qemu-img info` 가 `file format: raw` 로 읽는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "case:cloud-init-failures-all-look-like-ssh-refused",
|
|
"reason": "그 Case 가 `virsh screenshot` 을 판정 절의 두 번째 수단으로 이미 쓰고 있다(확장자와 무관하게 PNG 로 저장된다는 것까지). 체크섬 대조 쪽은 setup:create-three-guests-with-cloud-init 이 받는다 — 도구 둘을 따로 세우면 어느 Case 도 필요로 하지 않는 개념이 둘 쌓인다"
|
|
},
|
|
{
|
|
"id": "SSOT-242-243-firmware-and-os-variant",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#242-uefi-ovmf-edk2-ovmf",
|
|
"final/document.md#243---os-variant-osinfo"
|
|
],
|
|
"summary": "OVMF(`edk2-ovmf`)는 VM 에 줄 UEFI 펌웨어 구현이고 기본값은 SeaBIOS 다. `--os-variant`/osinfo 는 게스트 OS 종류를 libvirt 에 알려 주는 값으로 libvirt 가 이것으로 virtio 사용 여부·디스크 버스·NIC 모델을 고른다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "이 실험대는 둘 다 기본값을 썼고 두 값을 바꿔 무엇이 달라지는지 관측한 기록이 없다. 어느 Case·Decision·Question 도 이것을 필요로 하지 않아 거꾸로 뽑히지 않았고, 넣을 절도 없어 MERGE_INTO 가 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-244-layer-heading-virtual-network",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#244-2층-가상-네트워크"
|
|
],
|
|
"summary": "2층(가상 네트워크)의 층 머리. 이름만 있고 본문이 없다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "문서의 뼈대일 뿐 주장이 없다. §219 와 같은 처분이다"
|
|
},
|
|
{
|
|
"id": "SSOT-245-246-where-the-reservation-lives",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#245-libvirt-default-네트워크와-virbr0",
|
|
"final/document.md#246-dnsmasq-libvirt-내장-dhcp-dns"
|
|
],
|
|
"summary": "libvirt `default` 네트워크는 소프트웨어 브리지 `virbr0`(호스트가 `.1`)와 거기 붙은 NAT 규칙이고, VM 들은 이 브리지에서 서로 직접 통신하며 밖으로 나갈 때만 호스트 IP 로 마스커레이딩된다. libvirt 는 네트워크마다 dnsmasq 인스턴스를 하나씩 띄워 IP 를 나눠 주고 이름을 해석한다 — 예약이 실제로 사는 곳이 그 dnsmasq 설정이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:fix-guest-addresses-with-a-dhcp-reservation",
|
|
"reason": "그 Decision 이 「주소 관리가 libvirt 한 곳에 모인다」를 근거로 드는데 그 한 곳이 어디인지가 여기 있다. 두 문단이면 끝나고, 떼어 내면 「libvirt 에 모인다」가 어디에 모인다는 말인지 알 수 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-247-fix-guest-addresses-with-a-dhcp-reservation",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#247-dhcp-예약-ip-dhcp-host-과-mac-52-54-00"
|
|
],
|
|
"summary": "고정 주소가 필요한 이유가 넷이고(중요도 순) 그것을 충족하는 수단으로 DHCP 예약을 골랐다 — ① **k3s 가 IP 를 설정 파일과 인증서에 굽는다**(`--node-ip`·`--tls-san`·`K3S_URL`·kubeconfig 의 `server:`. server IP 가 바뀌면 agent 가 합류하지 못하고 API 인증서 SAN 도 어긋나 재발급이나 재설치가 필요해 되돌리기가 가장 비싸다) ② nginx 는 upstream 주소를 **기동 시점에 한 번만** 해석해 뒤쪽 IP 가 바뀌면 reload 전까지 계속 502 다 ③ `virsh destroy` 로 노드 상실을 재현하는 것이 실험 그 자체다 ④ 장애 주입 규칙이 주소 기반이라 어긋나면 **조용히 엉뚱한 것을 막는다**. 대안 둘(게스트 안 static IP · upstream 에 호스트명)은 설정이 두 곳으로 흩어지거나 ②가 그대로 남는다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:fix-guest-addresses-with-a-dhcp-reservation",
|
|
"reason": "프로젝트가 실제로 고른 방향이고 근거와 대안과 감수한 비용이 전부 SSOT 에 있다 — 예약을 `virt-install` 보다 먼저 넣어야 하고, MAC 이 한 글자만 달라도 **오류 없이 조용히 무시되어** 동적 대역에서 아무 주소나 받는다. 「upstream 에 IP 를 박으려고 예약을 건다」가 아니라 그 반대라는 인과 순서를 SSOT 가 직접 못박았다. setup:create-three-guests-with-cloud-init 은 그 명령을 치는 순서만 적고 왜 이 방식인지는 담지 않아 그 기록 안에 접히지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-248-live-and-config",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#248---live---config"
|
|
],
|
|
"summary": "`--live` 는 실행 중인 객체에만, `--config` 는 영구 정의에만 적용한다. 둘 다 줘야 「지금부터, 그리고 재부팅 후에도」가 되고 한쪽만 주면 「왜 적용이 안 되지」로 시간을 잡아먹는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:fix-guest-addresses-with-a-dhcp-reservation",
|
|
"reason": "그 결정을 실행할 때 반드시 붙는 플래그 둘이고 §201 의 「출력 두 마디가 다 나오는가」가 이것을 확인하는 방법이다. 아홉 줄짜리라 독립 기록이 될 크기가 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-249-250-why-nat-and-not-a-bridge",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#249-nat-vs-브리지-vs-macvtap",
|
|
"final/document.md#250-wifi에서-브리지가-안-되는-이유"
|
|
],
|
|
"summary": "NAT(`virbr0`)·브리지·macvtap 셋 가운데 NAT 를 채택했다. 브리지가 안 되는 이유는 **802.11 데이터 프레임이 기본 3-address 모드**라 AP 가 연결된 station 의 MAC 만 알고 있고 그 station 이 자기 것이 아닌 출발지 MAC 을 단 프레임을 보내면 버리기 때문이다 — 브리지된 VM 이 정확히 그런 프레임을 보낸다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:edge-nginx-moved-into-a-guest-vm",
|
|
"reason": "그 Decision 의 `grounds` 가 이미 「이 호스트에는 이더넷 없이 WiFi 만 있어 브리지를 못 쓰고 libvirt NAT + 호스트 진입 구조를 택했다」를 적고 「브리지 대 NAT 는 고른 것이 아니라 이더넷이 없어 하나만 남은 것」이라고 못박았다. 여기 것은 그 제약이 왜 물리 계층에서 성립하는지라 그 기록의 제약 절에 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-251-253-ssh-and-name-resolution-per-hop",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#251-ssh-키는-\"머신\"이-아니라-\"홉\"-단위다",
|
|
"final/document.md#252-~-ssh-config-의-first-match-wins-규칙",
|
|
"final/document.md#253-etc-hosts-와-이름-해석-순서"
|
|
],
|
|
"summary": "SSH 인증은 항상 클라이언트 1대 → 서버 1대라 **필요한 키 수는 머신 수가 아니라 홉 수**로 정해진다 — 이 실험대의 홉은 둘이고 둘째(test-server → kc-lab-1/2)가 새로 생긴 것이다. `~/.ssh/config` 는 first-match-wins 이고 확장자가 없다. 이름 해석은 `/etc/nsswitch.conf` 의 `hosts:` 가 정하며 `files`(=`/etc/hosts`)에서 답을 찾으면 DNS 로 나가지 않는다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:create-three-guests-with-cloud-init",
|
|
"reason": "그 Setup 의 끝나는 조건이 「SSH 가 키로 붙는다」이고, 새 홉이 하나 생긴다는 것과 그래서 키를 어디서 만들어 어디에 넣는지가 그 절차의 전제다. 셋을 따로 세우면 SSH 일반론이 되어 이 실험대의 물음에 답하지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-254-the-original-of-part5-edge-move",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#254-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나"
|
|
],
|
|
"summary": "제5부 §179(엣지를 물리 호스트에서 VM 으로 옮기면 무엇이 새로 필요해지나)의 **원문**이다. §211 이 겹치는 세 자리 중 하나로 표로 적었다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "같은 내용이 SSOT 안에 두 판으로 있고 글감은 §179 쪽에서 이미 났다 — decision:edge-nginx-moved-into-a-guest-vm 이 그것이다. 여기서 다시 후보를 올리면 한 주장이 두 기록이 되고, 원문 쪽이 더 자세한 자리는 그 기록의 근거로 이미 들어간다"
|
|
},
|
|
{
|
|
"id": "SSOT-255-the-original-of-part5-nftables",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#255-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다"
|
|
],
|
|
"summary": "제5부 §180(nftables 는 앞 체인의 `accept` 로 뒤 체인의 `reject` 를 막지 못한다)의 **원문**이다. §211 이 겹치는 세 자리 중 하나로 적었다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "§254 와 같다. 글감은 case:nftables-accept-did-not-stop-the-libvirt-reject 로 이미 났고 그 Case 의 진단이 이 절의 내용이다"
|
|
},
|
|
{
|
|
"id": "SSOT-256-layer-heading-host-entry",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#256-3층-호스트-진입"
|
|
],
|
|
"summary": "3층(호스트 진입)의 층 머리. 이름만 있고 본문이 없다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "§219 와 같은 처분이다"
|
|
},
|
|
{
|
|
"id": "SSOT-257-258-why-tls-is-terminated-here",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#257-리버스-프록시와-upstream",
|
|
"final/document.md#258-왜-tls를-끊어서-내용을-보는가"
|
|
],
|
|
"summary": "리버스 프록시의 `upstream` 블록은 뒤쪽 서버 여럿을 하나의 논리 이름으로 묶는다. **TLS 종료**는 프록시가 암호를 풀어 평문 HTTP 를 읽는 것이고, 「굳이 왜 푸는가」에 대한 답 넷 가운데 첫째가 근본적이다 — 호스트명과 경로로 라우팅하려면 내용을 봐야 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:two-l7-hops-and-the-entry-point-recursion",
|
|
"reason": "그 Concept 이 「L7 이 두 겹인데 왜 그런가」를 푸는데, L7 이 무엇을 하기에 두 겹이 필요한지가 여기 있다. 앞머리 두 절이라 그 기록의 도입이 되고 따로 세우면 어느 것도 이 둘만으로는 물음에 답하지 못한다"
|
|
},
|
|
{
|
|
"id": "SSOT-259-the-trust-boundary-of-forwarded-headers",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#259-x-forwarded--와-신뢰-경계"
|
|
],
|
|
"summary": "`X-Forwarded-Proto`·`X-Forwarded-Host`·`X-Forwarded-For` 는 프록시가 뒤쪽 서버에 「원래 클라이언트는 이랬다」고 알려 주는 관례적 헤더군이고, 그 값을 믿을 수 있느냐는 누가 그것을 썼느냐에 달려 있다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:overwrite-a-forwarded-header-at-the-trust-boundary-do-not-extend-it",
|
|
"reason": "그 Reference 가 정하는 규칙의 대상이 무엇인지를 적은 자리다. 스물네 줄이고 규칙 없이 정의만 남기면 읽을 이유가 없어 그 기록의 앞 절이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-260-sticky-sessions",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#260-스티키-세션"
|
|
],
|
|
"summary": "같은 클라이언트의 요청을 항상 같은 백엔드로 보내는 것. nginx 오픈소스판에서는 `ip_hash` 나 `hash <키> consistent` 로 구현한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:edge-nginx-and-host-dnat",
|
|
"reason": "§209 의 주석 처리된 `# ip_hash;` 가 이것이고, 그 Setup 이 세우는 파일 안의 줄이라 같은 기록에 들어간다. 열일곱 줄짜리 정의라 독립 기록이 될 크기가 아니고, **켜고 끄며 재는 것은 `keycloak-session-store` 의 물음이라 여기서 열지 않는다**"
|
|
},
|
|
{
|
|
"id": "SSOT-261-two-l7-hops-and-the-entry-point-recursion",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#261-진입점-자체가-죽으면-로드밸런서의-재귀-문제",
|
|
"final/document.md#275-호스트-nginx와-traefik은-무엇이-다른가-둘-다-필요한-이유"
|
|
],
|
|
"summary": "**호스트 nginx 는 이 실험대의 단일 장애점이다.** ALB(L7)와 NLB(L4)는 계층이 다른 것이 아니라 같은 자리를 놓고 고르는 두 선택지이고, 이 실험대와 운영은 둘 중 어느 쪽도 아닌 **L7 두 겹**이다 — 밖의 nginx 는 「어느 노드로」를 고정 IP:포트로 정하고 안의 Traefik 은 「어느 파드로」를 API 서버를 감시하며 동적으로 정한다. 한 머신 안에서 nginx 를 여럿 띄우는 것은 의미가 없고(이미 master+worker N 이고 그 머신이 죽으면 전부 죽는다) 앞에 LB 를 또 두면 그 LB 가 SPOF 라 **재귀가 끝나지 않는다.** 실무는 그 재귀를 소프트웨어가 아니라 네트워크 계층으로 끊는다 — VIP+VRRP 는 **선택자가 없고 IP 자체가 이동한다**",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:two-l7-hops-and-the-entry-point-recursion",
|
|
"reason": "거꾸로 뽑았다 — decision:edge-nginx-moved-into-a-guest-vm 의 근거가 「L7 홉 수는 전후 모두 2홉 그대로」인데 왜 2홉인지가 그 기록에 없고, reference:overwrite-a-forwarded-header-at-the-trust-boundary-do-not-extend-it 는 「경계」가 그 둘 중 어디인지를 전제로 삼는다. 둘 다 이 구조를 모르면 읽을 수 없다. 한 문단으로 접히지 않는 이유는 이것이 정의가 아니라 **재귀가 어디서 끊기는가**라는 구조적 설명이고 §261·§275 가 합쳐 229줄이기 때문이다. `keycloak-session-store` 와 겹치지 않는다 — 저쪽은 세션이 어디에 있는가를 재고 이쪽은 요청이 어느 층을 지나는가를 말한다"
|
|
},
|
|
{
|
|
"id": "SSOT-262-nginx-t-checks-syntax-not-intent",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#262-nginx--t"
|
|
],
|
|
"summary": "`nginx -t` 는 설정 파일 문법 검사이고 실제로 적용하지 않고 파싱만 한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:tool-output-is-not-the-subject-state",
|
|
"reason": "그 Reference 가 이미 「`nginx -t` 는 `sites-available` 을 `site-available` 로 잘못 친 빈 파일에도 통과한다」를 담고 있다. 도구가 무엇을 세는지의 예 하나라 그 기록의 표에 들어가고, 아홉 줄짜리 정의는 독립 기록이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-263-layer-heading-tls",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#263-4층-tls"
|
|
],
|
|
"summary": "4층(TLS)의 층 머리. 이름만 있고 본문이 없다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "§219 와 같은 처분이다"
|
|
},
|
|
{
|
|
"id": "SSOT-264-265-acme-and-the-two-challenges",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#264-acme",
|
|
"final/document.md#265-도메인-검증-http-01-vs-dns-01"
|
|
],
|
|
"summary": "ACME 는 인증서 발급을 자동화하는 프로토콜(RFC 8555)이고 Let's Encrypt 가 대표 구현체다. 「이 도메인이 정말 네 것이냐」를 증명하는 방식이 둘 — HTTP-01 은 Let's Encrypt 가 우리 서버로 들어오는 인바운드 검증이고 DNS-01 은 certbot 이 DNS 공급자 API 로 나가는 아웃바운드 검증이다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:dns-01-because-the-lab-is-not-on-the-public-internet",
|
|
"reason": "그 Decision 의 `grounds` 가 이미 「두 방식이 요구하는 것이 반대다」로 이 대비를 담고 있다. 여기 것은 그 정의의 원문이라 같은 기록의 근거로 들어가고, 따로 세우면 그 결정에서 제약과 대안이 떨어져 나간다"
|
|
},
|
|
{
|
|
"id": "SSOT-266-when-dns-01-is-the-only-option",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#266-dns-01-은-언제-쓰는가-네-가지-경우"
|
|
],
|
|
"summary": "**DNS-01 이 HTTP-01 보다 좋은 방식이 아니다** — HTTP-01 이 못 쓰이는 자리에 쓰는 것이다. 네 경우 ① 와일드카드(증명의 급이 달라 ACME 명세가 DNS-01 로만 허용한다) ② 서버가 공개 인터넷에서 안 보일 때(**이 실험대가 여기다**) ③ 80 을 못 쓸 때 ④ 인증서를 쓸 기계와 발급받는 기계가 다를 때. 값으로 치르는 것도 넷 — **API 토큰이 서버에 있어야 하고 유출되면 도메인 전체의 DNS 를 조작당해 인증서 한 장보다 피해가 크다**(그래서 존 하나 + DNS:Edit 으로 좁힌다) · 공급자에 묶인다 · TXT 전파를 기다려 느리다 · 공급자가 API 를 안 주면 못 쓴다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:dns-01-because-the-lab-is-not-on-the-public-internet",
|
|
"reason": "그 Decision 의 원문이고 **감수한 비용이 여기에만 적혀 있다** — 기존 기록의 `grounds` 는 제약과 대안을 적었지만 토큰 유출 시 피해 범위와 그것을 좁히는 방법(존 하나 + DNS:Edit)은 담지 못했다. 그 기록의 감수한 비용 절로 들어간다. 이 절이 함께 밝힌 문서 불일치는 question:is-this-lab-issuing-certificates-with-http-01-or-dns-01 가 따로 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-267-268-cert-files-and-private-ip-in-public-dns",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#267-fullchain-pem-privkey-pem-cert-pem-chain-pem",
|
|
"final/document.md#268-공개-dns에-사설-ip를-넣는-것"
|
|
],
|
|
"summary": "certbot 이 만드는 네 파일(`fullchain.pem`·`privkey.pem`·`cert.pem`·`chain.pem`)의 구분과, 공개 DNS 에 사설 IP 를 넣는 것의 의미",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:wildcard-certificate-with-dns-01-and-a-deploy-hook",
|
|
"reason": "그 Setup 이 `fullchain.pem` 을 쓰고 §209 의 주석이 「fullchain.pem, never cert.pem: 중간 인증서를 빼면 데스크톱 브라우저에서는 통과하고 모바일과 curl 에서 실패한다」를 적는다. 네 파일의 구분은 그 절차를 치는 사람이 경로를 고를 때 필요한 표라 그 기록 안에 있어야 한다"
|
|
},
|
|
{
|
|
"id": "SSOT-269-layer-heading-k3s",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#269-5층-k3s"
|
|
],
|
|
"summary": "5층(k3s)의 층 머리. 이름만 있고 본문이 없다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "§219 와 같은 처분이다"
|
|
},
|
|
{
|
|
"id": "SSOT-270-271-server-agent-and-the-flags-that-bake-in-an-ip",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#270-k3s-server-agent-node-token",
|
|
"final/document.md#271---node-ip---tls-san"
|
|
],
|
|
"summary": "k3s 는 쿠버네티스를 단일 바이너리로 압축한 배포판이고 `server` 가 컨트롤 플레인(API 서버·스케줄러·etcd 대신 SQLite)을, `agent` 가 워크로드만 맡는다. agent 가 합류할 때 쓰는 공유 비밀이 node-token 이다. `--node-ip` 는 노드가 광고할 IP 를 고정하고 `--tls-san` 은 API 서버 인증서의 SAN 목록에 값을 더한다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:install-k3s-server-and-agent",
|
|
"reason": "그 Setup 이 두 게스트에 치는 명령의 인자가 이것이고, §247 이 「k3s 가 IP 를 설정 파일과 인증서에 굽는다」를 예약의 첫째 이유로 드는 근거도 이 두 플래그다. 절차 옆의 주석이라 독립 기록이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-272-273-kubeconfig-is-not-where-you-think",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#272-kubeconfig의-127-0-0-1-문제",
|
|
"final/document.md#273-agent-노드에는-kubeconfig가-없다-localhost-8080-오류"
|
|
],
|
|
"summary": "k3s 가 만드는 `/etc/rancher/k3s/k3s.yaml` 은 서버 주소가 `https://127.0.0.1:6443` 이라 호스트로 복사하면 호스트 자기 6443 을 가리켜 실패한다 — `sed` 로 VM IP 로 바꾼다. 리다이렉션은 셸이 명령보다 먼저 처리하므로 `~/.kube` 가 없으면 `cat` 이 시작되기도 전에 끝나고, `sudo` 는 `cat` 에만 걸리고 `>` 에는 안 걸려 `sudo tee` 를 쓴다. agent 노드에도 `kubectl` **명령은 있지만** kubeconfig 가 없어 하드코딩 기본값 `localhost:8080` 으로 넘어간다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:install-k3s-server-and-agent",
|
|
"reason": "그 Setup 이 「lab host 에서 kubectl 로 본다」로 끝나는데 그 한 줄이 실제로는 복사 + `sed` + 권한 셋이다. `localhost:8080` 오류의 판정 쪽은 reference:check-the-nearest-layer-first 가 이미 담고 있어(「네트워크 문제가 아니라 kubeconfig 을 하나도 못 찾아 하드코딩 기본값으로 넘어간 것」) 절차 쪽만 여기로 온다"
|
|
},
|
|
{
|
|
"id": "SSOT-274-278-what-k3s-brings-with-it",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#274-traefik-k3s-기본-ingress",
|
|
"final/document.md#276-servicelb-klipper-lb",
|
|
"final/document.md#277-flannel-vxlan",
|
|
"final/document.md#278-networkpolicy와-k3s의-내장-컨트롤러"
|
|
],
|
|
"summary": "k3s 가 기본으로 딸려 주는 것 넷 — Traefik(기본 ingress. `--disable=traefik` 으로 끈다) · servicelb/klipper-lb(클라우드 LB 가 없는 환경에서 `type: LoadBalancer` 를 처리하려고 **모든 노드에** hostPort 를 여는 DaemonSet) · flannel VXLAN(노드가 다르면 파드 간 트래픽을 UDP 8472 로 캡슐화) · NetworkPolicy 컨트롤러(kube-router 의 netpol 을 k3s 서버 프로세스 안에 내장해 기본 활성)",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:install-k3s-server-and-agent",
|
|
"reason": "그 Setup 의 제목이 「k3s server 와 agent 를 깔고 ... 본다」이고 딸려 오는 것이 무엇인지는 그 절차의 결과다. 넷을 따로 세우면 k3s 일반 문서가 되어 이 주제의 독자 질문에 답하지 않는다 — 다만 servicelb 가 두 노드 80 을 다 여는 것은 concept:two-l7-hops-and-the-entry-point-recursion 이 「브라우저는 어느 노드로 가야 할지 모른다」를 말할 때 필요해 관계로 잇는다"
|
|
},
|
|
{
|
|
"id": "SSOT-279-how-to-read-a-manifest",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#279-매니페스트-읽는-법-deploy-lab-k8s-echo-yaml-을-예로"
|
|
],
|
|
"summary": "`deploy/lab/k8s/echo.yaml` 을 예로 매니페스트를 읽는 법 — 네임스페이스의 유효 범위, Deployment·Service 의 각 칸이 무엇을 정하는지",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "286줄짜리 쿠버네티스 일반 입문이고 이 실험대가 관측한 것이 아니다. 예로 든 `echo.yaml` 원본은 `source/` 에 반입되지 않아(§211 이 죽은 링크로 센다) 인용할 정본도 없다. 어느 Case·Decision·Question 도 이것을 필요로 하지 않아 거꾸로 뽑히지 않았고, 절차 기록에 넣기에는 그 절차가 쓰는 매니페스트가 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-280-282-no-docker-on-the-lab-host",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#280-무엇을-어디에-설치하는가",
|
|
"final/document.md#281-docker를-lab-host에-설치하면-안-되는-이유",
|
|
"final/document.md#282-그러면-이미지는-어떻게-넣는가"
|
|
],
|
|
"summary": "lab host 에 Docker 를 깔지 않는다 — **이미지 저장소가 둘로 갈려 `docker build` 한 이미지를 k3s 가 보지 못한다.** k3s 는 자체 containerd 를 번들하고 소켓과 저장 경로가 다르므로 증상이 「`docker images` 에는 보이는데 파드는 `ErrImageNeverPull`」이다. 충돌은 저장소 말고도 셋 더 있다(cgroup 드라이버 `cgroupfs` 대 `systemd`, `DOCKER`/`DOCKER-USER` 체인과 MASQUERADE 가 flannel 규칙과 엉킨다, `docker0` 가 `172.17.0.0/16` 을 점유). **이 실험대에는 이유가 하나 더 있다** — libvirt 가 `virbr0` NAT 와 자체 방화벽 규칙을 운영 중이라 Docker 의 규칙이 얹히면 네트워크 장애를 의도적으로 주입하는 실험대에서 **실험 결과인지 환경 문제인지 구분할 수 없게 된다**",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:no-docker-on-the-lab-host",
|
|
"reason": "프로젝트가 실제로 고른 방향이고(§280 의 설치 위치 표가 Docker 를 워크스테이션에만 둔다) 근거·대안·감수한 비용이 전부 있다 — 대신 쓰는 것은 `docker save | ssh ... k3s ctr images import -` 이고 그 대가가 **노드마다 따로 반입해야 하고**(스케줄러가 어디에 배치할지 모른다) 매니페스트에 `imagePullPolicy: Never` 를 줘야 하며 `ctr` 이 아니라 `k3s ctr` 을 써야 한다는 것이다. `k3s server --docker` 는 1.24 의 dockershim 제거 이후 `cri-dockerd` 를 요구하며 얻는 것이 없다고 기각 이유까지 적혀 있다. setup:install-k3s-server-and-agent 안에 접으면 「무엇을 깔지 않는가」라는 결정이 절차의 한 줄로 사라진다"
|
|
},
|
|
{
|
|
"id": "SSOT-283-288-what-is-really-because-of-the-distro",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#283-6층-arch-특이사항",
|
|
"final/document.md#284-nginx-설정-구조-sites-available-은-nginx-기능이-아니다",
|
|
"final/document.md#285-롤링-릴리스와-부분-업그레이드-금지",
|
|
"final/document.md#286-패키지명-대응표",
|
|
"final/document.md#287-없어서-오히려-편한-것",
|
|
"final/document.md#288-게스트-배포판-debian이란-무엇이고-ubuntu와-무엇이-다른가"
|
|
],
|
|
"summary": "여기 있는 것만이 진짜 「배포판이라서」 하는 일이다 — `sites-available`/`sites-enabled` 는 **nginx 의 기능이 아니라 Debian/Ubuntu 패키지 메인테이너의 관례**라 Arch 에서는 `nginx.conf` 에 `include` 를 직접 넣어야 한다 · Arch 는 롤링 릴리스이고 **부분 업그레이드를 지원하지 않는다** · 패키지명 대응표 · Arch 에는 SELinux 도 AppArmor 도 기본 활성이 아니라 RHEL 계열에서 필요한 정책 패키지가 없어도 된다 · 게스트의 Debian 이 무엇이고 Ubuntu 와 무엇이 다른가",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "reference:a-config-file-does-not-mean-the-same-thing-on-two-distros",
|
|
"reason": "그 Reference 의 세 축 가운데 ②(패키지가 기본으로 켜 둔 것)와 ③(그 배포판이 갖고 있지 않은 관례)의 근거가 전부 여기 있다. 특히 §284 는 §203③(Debian 기본 사이트가 `default_server` 를 먹고 있다)의 반대쪽 절반이다 — 한쪽은 관례가 있어서 충돌하고 한쪽은 없어서 include 를 넣어야 한다. 규칙과 그 근거를 두 기록으로 나누면 규칙만 남은 쪽이 선언문이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-289-291-gitignore-anchoring",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#289-7층-git",
|
|
"final/document.md#290-gitignore-패턴-앵커링",
|
|
"final/document.md#291-이미-추적-중인-파일은-무시되지-않는다"
|
|
],
|
|
"summary": "`.gitignore` 패턴은 슬래시가 어디 있느냐로 적용 범위가 달라지고, 이미 인덱스에 들어간 파일은 패턴을 추가해도 계속 추적된다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "이 프로젝트의 어느 주제의 독자 질문에도 답하지 않는다 — 가상화도 실험대 구축도 진입 경로도 아니고 저장소 운영 일반이다. 원본이 자기 저장소를 다루며 쌓아 둔 메모라 분석에는 남기고 독립 기록으로는 만들지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-292-299-how-package-installation-works",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#292-8층-패키지-저장소와-설치-원리",
|
|
"final/document.md#293-저장소-repository-란-무엇인가",
|
|
"final/document.md#294-설치는-다섯-단계로-진행된다",
|
|
"final/document.md#295-apt-debian-ubuntu",
|
|
"final/document.md#296-pacman-arch",
|
|
"final/document.md#297-왜-http로-받아도-안전한가-서명-신뢰-사슬",
|
|
"final/document.md#298-세-배포판-대조표",
|
|
"final/document.md#299-이-실험대에서-어디에-나타나는가"
|
|
],
|
|
"summary": "저장소의 실체는 HTTP 서버에 올린 파일 트리와 인덱스이고 설치는 배포판과 무관하게 다섯 단계로 같다. apt 와 pacman 의 저장소 목록·패키지 포맷·명령 대응. **저장소 주소가 `https` 가 아니어도 안전한 이유는 신뢰가 전송 경로가 아니라 서명에 걸려 있기 때문**이고, 이 실험대에 나타나는 자리는 호스트의 `pacman -S` 한 줄과 게스트 cloud-init 의 `packages:` 한 줄이다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "여덟 절이 전부 리눅스 패키지 관리 일반이고 이 실험대에서 관측된 것은 §299 의 두 줄뿐이다. 그 두 줄은 setup:prepare-the-lab-host-for-virtualization 과 setup:create-three-guests-with-cloud-init 이 이미 명령으로 담고 있어 더할 것이 없다. 개념으로 올리면 어느 기록도 필요로 하지 않는 개념이 여덟 쌓인다"
|
|
},
|
|
{
|
|
"id": "SSOT-300-302-why-unapplied-configs-are-kept",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#300-9층-deploy-무엇이-살아-있고-무엇이-참조인가",
|
|
"final/document.md#301-전체-지도",
|
|
"final/document.md#302-왜-적용하지-않는-것을-남겨두는가"
|
|
],
|
|
"summary": "저장소에 있으나 지금 적용되지 않는 설정이 여럿인데 죽은 코드가 아니라 **의도적으로 남겨 둔 참조 자산**이다. 이유 셋 — ① 이 저장소의 목적이 비교라 배포 형태도 선택지를 나란히 두고 트레이드오프를 기록하는 것 자체가 산출물이고 하나만 남기면 「왜 이걸 골랐는가」의 근거가 사라진다 ② 죽은 코드가 아니라 **테스트되는 코드**다(`scripts/verify-*.sh` 가 붙어 있어 실행되지 않을 뿐 깨지면 드러난다) ③ 배포 형태가 바뀌면 되살아나므로 실험대 전용 설정은 `lab/` 아래로 분리해 두었다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:no-public-tunnel-because-a-third-hop-pollutes-the-measurement",
|
|
"reason": "그 Decision 이 「채택하지 않았는데 왜 지우지 않았나」에 답할 때 쓰는 근거다. 기각한 선택지를 지우지 않고 이유와 함께 남긴다는 것이 이 저장소의 방침이고, 그 방침 없이 결정만 적으면 `tunnel/` 이 왜 아직 거기 있는지 설명되지 않는다"
|
|
},
|
|
{
|
|
"id": "SSOT-303-304-deploy-assets-not-applied-here",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#303-reverse-proxy-1홉-계약의-원본",
|
|
"final/document.md#304-tls-같은-일을-하는-두-구현"
|
|
],
|
|
"summary": "`reverse-proxy/` 는 1홉 계약의 원본(`keycloak.env.example` 의 네 줄)이고 `tls/` 는 같은 일을 하는 두 구현(`nginx.conf` 와 `Caddyfile`)이다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "이 실험대가 적용하지 않은 배포 형태이고 그 파일들의 원본은 `source/` 에 반입되지 않았다. 적용되지 않는 것을 왜 남겨 두는가라는 방침 쪽은 §300~§302 가 이미 후보로 올라갔고, 개별 파일의 내용은 인용할 정본이 이 저장소에 없어 기록으로 쓸 근거가 없다"
|
|
},
|
|
{
|
|
"id": "SSOT-305-no-public-tunnel-because-a-third-hop-pollutes-the-measurement",
|
|
"kindCandidate": "DECISION",
|
|
"sourceRefs": [
|
|
"final/document.md#305-tunnel-채택하지-않은-이유를-남긴-자산"
|
|
],
|
|
"summary": "`cloudflared-config.yml` 은 아웃바운드 연결만 쓰므로 포트포워딩 없이 공개 HTTPS 이름을 얻어 공유기를 건드릴 수 없는 환경에서 매력적인 선택지인데 **채택하지 않았다.** Cloudflare 엣지가 TLS 를 끊고 다시 맺으면서 홉이 2에서 3으로 늘고 `CF-Connecting-IP` 같은 자체 헤더가 섞이는데, 이 실험대가 측정하려는 것이 정확히 `nginx → Traefik` 2홉의 forwarded 헤더 계약이라 **앞에 한 겹이 더 붙으면 측정이 오염된다.** 그래서 tailnet 직결을 택했고, 조건이 바뀌어 공개 접근이 필요해지면 이 파일이 그대로 쓰인다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "decision:no-public-tunnel-because-a-third-hop-pollutes-the-measurement",
|
|
"reason": "프로젝트가 실제로 기각한 선택지이고 근거가 「더 나쁘다」가 아니라 **재려는 것과 충돌한다**여서 다른 결정들과 물음이 다르다. 감수한 비용도 분명하다 — 공개 인터넷에서 닿지 않는 주소로 남게 되고, 그 대가가 decision:dns-01-because-the-lab-is-not-on-the-public-internet 과 question:is-this-lab-issuing-certificates-with-http-01-or-dns-01 로 이어진다. 되살아나는 조건까지 적혀 있어 Decision 의 조건을 다 채운다"
|
|
},
|
|
{
|
|
"id": "SSOT-306-the-example-suffix-convention",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#306-example-접미사-관례"
|
|
],
|
|
"summary": "비밀이 들어갈 자리가 있는 파일은 `.example` 로 커밋하고 실파일은 무시한다는 관례",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "열여섯 줄짜리 저장소 운영 관례이고 이 프로젝트의 어느 독자 질문에도 답하지 않는다. §211 이 비밀 한 줄을 자리표시자로 바꾼 것과 같은 규약이지만 그 사실은 §211 의 출처·범위 후보가 이미 담고 있다"
|
|
},
|
|
{
|
|
"id": "SSOT-307-layer-heading-k8s-resources",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#307-10층-쿠버네티스-리소스-이-실험대에서-실제로-쓴-것들"
|
|
],
|
|
"summary": "10층(쿠버네티스 리소스)의 층 머리. 5층이 k3s 자체라면 여기는 그 위에 올린 리소스들이라는 한 줄뿐이다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "§219 와 같은 처분이다"
|
|
},
|
|
{
|
|
"id": "SSOT-308-312-the-resources-this-lab-actually-put-on",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#308-워크로드-세-종류-무엇을-언제-쓰는가",
|
|
"final/document.md#309-저장소-pvc-pv-storageclass",
|
|
"final/document.md#310-secret-감춰지지-않는다",
|
|
"final/document.md#311-rbac-serviceaccount-clusterrole-binding",
|
|
"final/document.md#312-배치-제어-nodeselector-라벨-taint"
|
|
],
|
|
"summary": "이 실험대가 실제로 쓴 리소스 — 워크로드 세 종류(Deployment·StatefulSet·DaemonSet)가 각각 보장하는 것, PVC/PV/StorageClass 의 요청과 제공, **Secret 은 감춰지지 않는다**(base64 는 인코딩이지 암호화가 아니다), Prometheus 가 API 에 타깃을 물으려면 필요한 ServiceAccount·ClusterRole·Binding, nodeSelector·라벨·taint 로 배치를 못박는 법",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:keycloak-two-nodes-and-postgres-on-k3s",
|
|
"reason": "그 Setup 이 올리는 것이 정확히 이 리소스들이다(Keycloak 이 StatefulSet, PostgreSQL 이 Deployment + PVC, 자격이 Secret). RBAC 과 nodeSelector 쪽은 setup:prometheus-and-grafana-for-the-lab 이 쓰고 관계로 잇는다. 다섯을 따로 세우면 쿠버네티스 입문서가 되어 이 주제의 독자 질문에 답하지 않는다 — 여기서 이 리소스들이 갖는 뜻은 「이 실험대가 무엇을 올렸나」뿐이다"
|
|
},
|
|
{
|
|
"id": "SSOT-313-server-and-agent-differ-when-you-kill-them",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#313-k3s-server와-agent-죽였을-때가-다르다"
|
|
],
|
|
"summary": "k3s server 와 agent 는 **죽였을 때가 다르다** — agent 를 죽이면 그 노드의 워크로드만 사라지고, server 를 죽이면 API 서버가 없어져 관측과 조작 수단이 함께 사라진다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:install-k3s-server-and-agent",
|
|
"reason": "그 Setup 이 세우는 두 역할의 차이가 실험에서 실제로 갈리는 자리다. 그리고 §213 의 셋째 이유(**관측 수단이 실험 대상과 함께 죽으면 안 된다**)와 §330 의 규칙(관측 스택을 server 쪽에 두고 agent 쪽을 죽인다)이 이 사실 위에 서 있어 decision:two-guest-vms-instead-of-installing-k3s-on-the-host 가 관계로 받는다. 스무 줄짜리라 독립 기록이 아니다"
|
|
},
|
|
{
|
|
"id": "SSOT-314-319-infinispan-and-jgroups-structure",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#314-11층-keycloak-클러스터링-내부-infinispan과-jgroups",
|
|
"final/document.md#315-두-층으로-되어-있다",
|
|
"final/document.md#316-디스커버리와-트랜스포트는-다른-경로다",
|
|
"final/document.md#317-코디네이터",
|
|
"final/document.md#318-클러스터-뷰",
|
|
"final/document.md#319-주요-jgroups-프로토콜-지표-이름에-그대로-나온다"
|
|
],
|
|
"summary": "Keycloak 클러스터링은 두 층이다 — Infinispan(분산 캐시)이 JGroups(그룹 통신) 위에서 돌고 그 아래가 TCP 7800 이다. **디스커버리와 트랜스포트는 다른 경로다**(전자는 PostgreSQL `JGROUPS_PING` 테이블, 후자는 7800. 끊기면 「DB 엔 등록되는데 클러스터가 안 붙는다」). `coord = t` 인 노드가 코디네이터이고 뷰는 그 순간의 멤버 명단이다. 주요 JGroups 프로토콜(GMS·FD_SOCK2·MERGE3·NAKACK2·TCP)은 지표 이름에 그대로 나온다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "**`keycloak-session-store` 프로젝트가 측정으로 다루는 영역이다.** §211 이 그 경계를 직접 그었다 — 「두 프로젝트를 견줄 때는 저쪽이 측정이고 이쪽이 정의라는 것을 먼저 본다」. 여기서 기록으로 올리면 같은 주제의 정본이 두 저장소에 생기고, 근거도 이쪽이 얇다 — 이 절들이 인용하는 실험 원문(`experiment-00-session-replication.md`)은 `source/` 에 반입되지 않았다(unknown). 그렇다고 SSOT 에서 빼지는 않는다 — 빼면 `source/` 를 지우는 순간 사라진다"
|
|
},
|
|
{
|
|
"id": "SSOT-320-321-where-the-session-is-and-how-it-is-written",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#320-세션은-어디에-있는가-두-곳이되-역할이-다르다",
|
|
"final/document.md#321-세션-쓰기-트랜잭션의-세-가지-설계-결정"
|
|
],
|
|
"summary": "Keycloak 26 의 기본값 `persistent-user-sessions` 에서 **PostgreSQL 이 진실의 원천이고 노드 간 공유는 거기서만 일어난다.** Infinispan `sessions` 는 자기 노드가 로그인시킨 세션만 담는 룩어사이드 캐시이고 **세션 엔트리는 노드 사이를 건너가지 않는다**(원본이 처음에 반대로 썼다가 실험 0 에서 측정해 고쳤다). 세션 쓰기 트랜잭션에는 설계 결정 셋이 보인다 — 낙관적 락(`VERSION=$5`), `skip locked`, 그리고 `SET LOCAL synchronous_commit TO OFF`(**DB 가 강제 종료되면 직전 수백 밀리초의 세션 갱신이 사라질 수 있는 의도된 트레이드오프**)",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "§314~§319 와 같은 이유다 — 저쪽이 측정이고 이쪽이 정의이며, 근거인 실험 원문과 PostgreSQL 문장 로그가 이 저장소에 없다. 여기서 Case 나 Concept 으로 올리면 측정을 갖고 있는 프로젝트의 기록과 경쟁하는 정본이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-322-329-how-prometheus-is-put-together",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#322-12층-관측성-prometheus의-구조",
|
|
"final/document.md#323-세-부분으로-되어-있다",
|
|
"final/document.md#324-exporter-패턴",
|
|
"final/document.md#325-서비스-디스커버리-타깃을-적어두지-않는다",
|
|
"final/document.md#326-relabel-걸러내고-이름을-붙인다",
|
|
"final/document.md#327-메트릭-타입",
|
|
"final/document.md#328-up-가장-중요한-합성-지표",
|
|
"final/document.md#329-tsdb와-보존-기간"
|
|
],
|
|
"summary": "수집(scrape)·저장(TSDB)·질의(PromQL) 세 부분이고 **pull 방식**이라 대상이 죽으면 긁기가 실패해 `up` 이 0 이 된다 — **죽은 사실 자체가 데이터가 된다.** exporter 패턴, 파드 IP 가 재시작마다 바뀌므로 정적 목록 대신 서비스 디스커버리, 전부 가져온 뒤 relabel 로 걸러내고 이름을 붙이는 것(`pod`·`node` 라벨이 실험에서 결정적이다), 메트릭 타입 넷, `up` 이 장애 실험에서 핵심인 이유(다른 지표는 대상이 죽으면 **사라져서** 「언제부터 죽었나」를 알 수 없다), TSDB 보존 기간과 `emptyDir` 의 위험",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "§314~§321 과 같은 자리다 — §211 이 §322~§330 을 `keycloak-session-store` 가 측정으로 더 깊이 다루는 영역으로 표시했다. 이 프로젝트 쪽에서 `up` 에 대해 관측한 것은 이미 reference:tool-output-is-not-the-subject-state 가 담고 있고(「503 이 나는 동안에도 `up` 은 1 이었다」) 그쪽이 이 실험대의 관측이다. 구조 설명 여덟 절을 따로 올리면 Prometheus 입문서가 되고 정본이 두 저장소에 갈린다"
|
|
},
|
|
{
|
|
"id": "SSOT-330-the-observer-must-not-die-with-the-observed",
|
|
"kindCandidate": "REFERENCE",
|
|
"sourceRefs": [
|
|
"final/document.md#330-관측-시스템의-장애-도메인"
|
|
],
|
|
"summary": "**관측 시스템은 관측 대상과 같이 죽으면 안 된다** — 죽는 순간을 기록해야 하는데 같이 죽으면 기록이 없다. 노드가 둘뿐인 실험대에서는 완전히 피할 수 없으므로 규칙으로 정한다 — `kc-lab-1`(server)에 관측 스택을 두고 죽이지 않으며 `kc-lab-2`(agent)를 장애 주입 대상으로 삼고, `nodeSelector` 로 못박아 실험이 재현 가능하게 만든다",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:prometheus-and-grafana-for-the-lab",
|
|
"reason": "그 Setup 이 관측 스택을 어디에 올리는지를 정하는 규칙이고 `nodeSelector` 한 줄이 그 절차 안에 있다. 규칙 자체는 §213 의 셋째 이유(관측자를 살려 둔다)가 이미 같은 말을 해 decision:two-guest-vms-instead-of-installing-k3s-on-the-host 와 겹친다 — 둘을 각각 Reference 로 올리면 한 규칙이 두 편이 되므로, 절차 쪽에 두고 그 Decision 이 관계로 받는다"
|
|
},
|
|
{
|
|
"id": "SSOT-331-layer-heading-virtualization-operations",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#331-13층-가상화-운영-실행-중-바꾸는-것들"
|
|
],
|
|
"summary": "13층(가상화 운영 — 실행 중 바꾸는 것들)의 층 머리. 이름만 있고 본문이 없다",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "§219 와 같은 처분이다"
|
|
},
|
|
{
|
|
"id": "SSOT-332-334-power-cycle-and-reallocate",
|
|
"kindCandidate": "SETUP",
|
|
"sourceRefs": [
|
|
"final/document.md#332-vm-메모리-재배분-게스트를-다시-만들지-않는다",
|
|
"final/document.md#333-안전한-종료-순서",
|
|
"final/document.md#334-복구-순서-종료의-역순"
|
|
],
|
|
"summary": "게스트를 다시 만들지 않고 메모리를 재배분한다 — `setmaxmem` 이 상한, `setmem` 이 현재 할당이고 현재값을 상한보다 크게 줄 수 없어 **순서가 정해져 있다.** `setmaxmem --live` 는 대개 거부되므로 상한을 바꾸려면 껐다 켠다. 안전한 종료는 **위에서부터** — Keycloak 을 0으로 내려 클러스터에서 정상 탈퇴시키고, PostgreSQL 을 마지막에 충분한 시간을 주고 내리고, 게스트를 ACPI 정상 종료한 뒤 호스트를 끈다. 복구는 역순이고 **PostgreSQL 이 먼저다.** clean shutdown 판정은 `postmaster.pid` 가 남아 있지 않은 것이다",
|
|
"disposition": "PROMOTE",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "setup:power-cycle-the-lab-and-reallocate-guest-memory",
|
|
"reason": "읽는 사람이 자기 기계에서 그대로 치는 명령이고 기존 Setup 일곱(00~06 단계)에 없는 절차다 — 그것들은 세우는 쪽만 덮는다. 글쓴이만 다시 돌릴 재현 순서가 아니라 **실험대를 쓰는 사람이 반복해서 하는 일**이고(호스트를 8GB→12GB 로 증설한 뒤 실제로 이 방법으로 재배분했다), 순서를 틀리면 PostgreSQL 이 강제 종료되어 다음 기동에 crash recovery 가 도는 실제 대가가 있다. setup:tear-down-the-lab-and-know-what-survives 와는 다른 절차다 — 저쪽은 지우고 이쪽은 껐다 켠다"
|
|
},
|
|
{
|
|
"id": "SSOT-335-what-follows-a-qcow2-to-another-host",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#335-qcow2-파일을-다른-물리-서버로-옮기면-무엇이-따라가나"
|
|
],
|
|
"summary": "제5부 §181(qcow2 가 담는 것과 담지 않는 것)의 **원문**이다. §211 이 겹치는 세 자리 중 하나로 적었고 하위 절 넷을 포함한다 — 따라가는 것과 안 가는 것, 희소 할당이지 압축이 아니라는 것, 실행 상태까지 옮기려면 `virsh save`/`migrate` 라는 것",
|
|
"disposition": "MERGE_INTO",
|
|
"dispositionReview": "CONFIRMED",
|
|
"target": "concept:what-a-qcow2-file-carries",
|
|
"reason": "글감은 §181 쪽에서 이미 났고 그 기록의 `source` 가 §181 를 가리킨다. 원문 쪽이 더 자세한 자리(이미지가 커졌을 때의 전송 비용과 회피책 — 디스크 분리·공유 스토리지·증분 백업·앱 레벨 복제)는 그 기록의 근거로 들어가고, question:qcow2-transfer-time-over-wifi 가 재려는 것이 정확히 그 비용이다. 새 후보로 올리면 한 주장이 두 기록이 된다"
|
|
},
|
|
{
|
|
"id": "SSOT-335-enterprise-migration-and-cloud-import",
|
|
"kindCandidate": "CONCEPT",
|
|
"sourceRefs": [
|
|
"final/document.md#335-qcow2-파일을-다른-물리-서버로-옮기면-무엇이-따라가나"
|
|
],
|
|
"summary": "실무 마이그레이션 — 6R 분류, 컷오버 중심의 7단계, 사람이 하는 일, 온프렘→클라우드 이전(포맷 변환·게스트 준비·업로드 경로와 재구축 대안), 그리고 **실무가 이미지를 직접 옮기지 않는 이유**(재현성·비밀 유출·형상관리)와 그럼에도 이미지 이동이 맞는 자리",
|
|
"disposition": "KEEP_IN_SSOT",
|
|
"dispositionReview": "CONFIRMED",
|
|
"reason": "concept:what-a-qcow2-file-carries 의 `basis-version` 이 이미 이 경계를 그었다 — 「온프렘 → 클라우드 이미지 반입 절차는 §181 가 스스로 (external, 코드 관측 아님) 으로 표시한 부분이라 이 글의 기준 밖이고 SSOT 에만 남긴다」. 이 실험대에서 관측한 것이 아니라 밖에서 가져온 지식이고, 그 판정을 뒤집을 새 근거가 §335 원문에도 없다"
|
|
}
|
|
],
|
|
"counts": {
|
|
"topics": 7,
|
|
"nodes": 91,
|
|
"written": 91,
|
|
"unwritten": 0,
|
|
"unlisted": 0,
|
|
"candidates": 390
|
|
},
|
|
"unlisted": [],
|
|
"history": {
|
|
"2026-09-08": "S2 — SSOT(절 28개)에서 후보 39건을 뽑아 처분을 적고 PROMOTE 하나만 글감으로 올렸다. 앵커 형식을 h2 절 제목 슬러그로 정하면서 final/document.md 의 frontmatter 를 떼고 heading 단계를 맞췄다(본문 문장은 그대로).",
|
|
"2026-09-08 · S2 재판정": "§20 의 OQ-1~6 과 §27 의 OQ-7~12 열둘을 NEEDS_EVIDENCE 에서 PROMOTE 로 다시 판정하고 cpu-virtualization 주제에 question 글감 열둘로 올렸다. readiness 는 전부 OPEN 이고 known 에는 SSOT 가 서술한 것만, unknown 에는 이 Host 에서 재야 아는 것만 적었다. 앵커는 h2 절 제목 슬러그에 OQ 번호를 구분자로 붙인 형식이다. 후보 27건과 concept 글감은 그대로 두었다.",
|
|
"2026-09-09 · S2-B 제3부": "SSOT 제3부(네트워크 가상화 §89~§126, 절 38개)에서 후보 48건을 뽑아 처분을 적고 PROMOTE 10건을 network-virtualization 주제의 글감으로 올렸다 — concept 하나 · reference 둘 · question 일곱. candidateScope.sections 에 제3부 절 제목 38개를 덧붙였다. 제1부·제2부의 주제 둘과 후보 116건은 그대로 두었다.",
|
|
"2026-09-09 · S2-C 제4부": "SSOT 제4부(스토리지 가상화 §127~§177, 절 51개)에서 후보 56건을 뽑아 처분을 적고 PROMOTE 14건을 storage-virtualization 주제의 글감으로 올렸다 — concept 넷 · reference 셋 · question 일곱. candidateScope.sections 에 제4부 절 제목 51개를 덧붙이고 최상단 note 의 「주제도 하나이고 글감도 concept 하나」를 지금 상태(주제 넷 · 글감 57)로 고쳤다. 제1~3부의 주제 셋과 후보 164건은 그대로 두었다.",
|
|
"2026-09-11 · S2-D 제5부": "SSOT 제5부(실험대에서 실제로 확인한 것 §178~§183, 절 6개)에서 후보 19건을 뽑아 처분을 적고 PROMOTE 7건을 새 주제 lab-environment-build 의 글감으로 올렸다 — case 하나 · concept 하나 · reference 하나 · question 셋 · decision 하나. 이 프로젝트의 첫 case 와 첫 decision 이다. candidateScope.sections 에 제5부 절 제목 6개를 덧붙여 182개가 됐고 §178 는 excluded 로 빼지 않고 범위 안에 두었다(관측된 환경을 함께 담고 있다). 제1~4부의 주제 넷과 후보 213건은 그대로 두었다. 남겨 둔 것 하나 — §178 가 VM 네트워크를 libvirt NAT 로 관측해 제3부 question:vm-network-mode-bridge-nat-or-routed 의 unknown 일부가 답해졌지만 이번 범위가 아니라 그 노드는 건드리지 않았다.",
|
|
"2026-09-12 · S2-E 제6부": "SSOT 제6부(실험대는 어떻게 세워졌나 §184~§194, 절 11개)에서 후보 65건을 뽑아 처분을 적고 PROMOTE 7건을 글감으로 올렸다 — 새 주제 build-completion-judgment 에 case 셋 · reference 둘 · question 하나, 기존 주제 lab-environment-build 에 decision 하나. candidateScope.sections 에 제6부 절 제목 11개를 덧붙여 193개가 됐고 §184 는 §178 과 같은 기준으로 excluded 에서 빼지 않고 범위 안에 두었다(이 부의 검증 방식을 함께 담고 있어 §186~§192 의 관측이 어디까지 유효한지를 그 절이 정한다). 제1~5부의 주제 다섯과 후보 232건, 글감 64개는 그대로 두었다. 남겨 둔 것 둘 — ① MERGE_INTO 일곱 건의 target 이 이미 쓰여 있는 기록이라 본문에 반영되지 않았다. ② §184 가 반입 원본의 리비전 `9465582b5d1630eb4ae7c4e078021486919bf6b6` 을 처음으로 적었는데 sourceRepository 의 `revision` 은 여전히 null 이고 `verified` 는 「고정할 저장소 리비전이 없다」로 남아 있다. 그 칸을 고치는 것은 프로젝트 출처 기록을 다시 쓰는 일이라 이번 범위 밖으로 두었다.",
|
|
"2026-09-12 · S2-F 환경 구성": "Studio 의 여섯 번째 종류 `SETUP`(화면 이름 「환경 구성」)이 스킬에 빠져 있었다는 것을 확인하고 (계약의 `RecordKind` 는 여섯이다 — tech-log-frontend @ 9e5642c · `studio-api.openapi.yaml:838-840`), 제6부 §186~§192 의 기반 7단계 구축 절차를 그 종류로 올렸다. 기존 주제 lab-environment-build 에 setup 글감 여섯 — 단계 00·01 을 한 편으로 묶고 02~06 을 한 편씩. 묶은 이유는 둘이 같은 셸에서 이어 치는 한 줄기이고 00 이 끝나는 상태(`virsh list` 가 돈다)가 그것만으로는 쓸 데가 없기 때문이다 — §187 의 「막히면」 표 마지막 줄이 00 으로 되돌린다. 후보는 여덟 건을 다시 판정하고(KEEP_IN_SSOT → PROMOTE 넷 · KEEP_IN_SSOT → MERGE_INTO 넷) 대장에 없던 셋을 더했다. 재판정 사유는 전부 같다 — 절차를 담는 종류가 스킬에 없어서 「설명하는 글」로 바꿔 보다가 두께가 안 나오면 SSOT 에 남겼던 것이다. 기존 글감 71개와 제1~5부의 처분은 건드리지 않았다. 「성공으로 보이는 실패」를 다룬 Case 셋은 재현하고 검증한 결론이라 Case 그대로 두고, 이번 여섯은 그 Case 들을 relations 로 가리킨다. 남겨 둔 것 둘 — ① Keycloak·PostgreSQL·Prometheus·Grafana·node-exporter 의 판 번호가 SSOT 제6부에 없어 그 두 글감의 pinned-versions 에서 빠져 있다. 매니페스트 원문이 `source/` 에 반입되지 않은 것이 원인이고 지어내지 않는다. ② sourceRepository 의 `revision` 은 여전히 null 이다 — 제6부의 반입 원본 리비전은 두 번째 항목에 적혀 있다.",
|
|
"2026-09-14 · S2-G 환경 구성 재분해": "SSOT 제6부 §186~§192 를 원본 가이드 7편(`source/docs/guides/`, 3,116줄)으로 다시 채운 뒤 환경 구성 기록을 6편에서 7편으로 나누고 전부 다시 썼다. setup:stand-up-the-lab-host-and-three-guests 하나가 §186(344줄)과 §187(604줄)을 같이 물고 있어 setup:prepare-the-lab-host-for-virtualization 과 setup:create-three-guests-with-cloud-init 둘로 가른다. 후보 대장도 맞췄다 — SSOT-187-three-guests-build-procedure 를 MERGE_INTO 에서 PROMOTE 로 올리고, SSOT-186-lab-host-virtualization-setup 과 SSOT-186-no-kvm-means-software-emulation 의 target 을 §186 쪽 새 slug 로 고쳤다. 일곱 편의 본문 뼈대를 KSS A층과 같은 열 절로 맞췄다 — 읽기 전에·이 단계가 세우는 것·전제와 되돌리기·세우기 전에 먼저 본다·실행 절차·구성 값·끝났는지 판정한다·통과 조건을 한 번에 다시 본다·막히면·무엇이 관측이고 무엇이 아닌가. 가이드의 「이 단계가 끝나면」이 기록에 0건이던 것을 일곱 편 전부에 인용으로 넣었고, 확인은 「무엇을 확인하는가 → 명령 → 어디를 봐야 하는가 → 이 결과가 의미하는 것」 형태로 옮겼다. 되돌리기는 SSOT 가 unknown 으로 적어 둔 여섯 편을 그대로 unknown 으로 두고, 단계 03 한 편만 원문의 네 줄을 실었다. 기존 여섯 편의 frontmatter id 와 studio 주소는 그대로 두고, 나뉜 둘 가운데 §186 쪽이 `7c66a553-0008-4294-a27a-687bd1bda0c1` 을 물려받아 §187 쪽이 새 작업본이 된다. 다른 주제와 후보는 건드리지 않았다.",
|
|
"2026-09-16 · S2-H 제7·8·9부": "SSOT 대조로 `source/` 의 세 문서가 SSOT 에 들어와 11,878행 → 17,512행 · 여섯 부 → 아홉 부 · 194절 → 338절이 됐는데 candidateScope.sections 가 §2~§194 그대로여서 새 144절에서 뽑힌 후보가 하나도 없었다. 그 144절 가운데 140절을 범위에 넣고(§205·§336·§337·§338 은 excluded) 후보 90건을 처분과 함께 올렸다 — PROMOTE 13 · MERGE_INTO 53 · KEEP_IN_SSOT 23 · NEEDS_EVIDENCE 1. 제7부(실측)에서 Case 1 · Setup 1 · Reference 1 · Question 1, 제8부(설정 주석)에서 Reference 1, 제9부(개념 사전)에서 Concept 3 · Decision 4 · Setup 1 이 났다. §208 의 「`ExecStartPost=-` 때문에 구멍이 빠져도 유닛은 active 다」는 SSOT 가 스스로 inferred 로 적고 재현하지 않아 NEEDS_EVIDENCE 로 두었다 — 재현 출력이 `final/evidence/raw/` 에 남으면 case:nftables-accept-did-not-stop-the-libvirt-reject 의 두 번째 재현 조건으로 다시 판정한다. 기존 후보 300건과 글감 78개는 건드리지 않았다."
|
|
}
|
|
}
|