기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만 있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문 종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다. SSOT 결함 둘을 고쳤다. - kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이 「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽 `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다. - virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데 원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다. 기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은 둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더 든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다). 계약을 셋 고쳤다. - kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/** 28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는 것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온 것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 커밋도 28개가 어긋났다). - virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고 재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다. - kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고 넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다. style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져 (실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고, engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고 있었다(Question 기록에서 11.94 → 3.86). verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.4 KiB
id, kind, slug, title, topic, topicName, project, status, studio, sourceRevision, source
| id | kind | slug | title | topic | topicName | project | status | studio | sourceRevision | source | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ca36f0db-9028-4761-91a8-afb7eb30f2b9 | REFERENCE | verify-the-network-path-before-blaming-the-application | Keycloak 실험 결과를 애플리케이션 원인으로 읽기 전에 network 계층을 따로 검증한다 | network-virtualization | 네트워크 가상화 | virtualization | 초안 | https://hyeonworks.com/studio/documents/ca36f0db-9028-4761-91a8-afb7eb30f2b9/edit | no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다 |
|
Keycloak 실험 결과를 애플리케이션 원인으로 읽기 전에 network 계층을 따로 검증한다
Refresh Token 경쟁 자체는 virtio-net 문제가 아니다. 다만 그 실험에서는 클라이언트에서 Nginx 와 가상 머신, K3s, Keycloak 을 거쳐 PostgreSQL 또는 Redis 까지 가는 경로를 여러 노드가 함께 쓴다. 그 경로 위의 네트워크 가상화 문제는 애플리케이션 동시성 문제와 비슷한 증상으로 나타난다.
관계
- Guest 의 packet 이 Host Physical NIC 에 닿기까지 — virtio-net · virtqueue · vhost-net · TAP · Bridge 이 기준이 「따로 검증한다」고 말하는 계층을 세운 글이다.
- packet 이 어디서 끊겼는지는 계층마다 capture 해서 가른다 이 기준이 문제가 네트워크인지 아닌지를 가른다면, 그 절차는 네트워크 안에서 어느 구간인지를 가른다.
- Host Nginx 에서 Keycloak 까지 packet 은 실제로 어디를 지나는가 검증 순서의 첫 항목이다. 경로를 모르면 무엇을 관측할지도 정해지지 않는다.
- 부하 중 network 가상화가 지연을 바꿀 만큼 호스트 CPU 를 쓰는가 검증 순서의 마지막 항목이고, 네트워크 처리와 CPU 경쟁을 가르는 관측이다.
- Keycloak 동시 refresh 실험 중 CPU 가상화 계층이 결과를 흔들 만큼 포화되는가 같은 실험을 CPU 쪽에서 본 물음이다. 두 계층이 같은 지연으로 보이므로 함께 기록한다.
목적
이 기준은 실험 결과를 잘못된 원인에 붙이는 일을 막는다.
Keycloak 멀티 노드 실험에서 관측되는 지연과 실패에는 네트워크 가상화 쪽 원인이 섞일 수 있다. 노드 하나만 지연되는 경우, 가상 머신 하나에서만 패킷이 유실되는 경우, 호스트 브리지 구성이 잘못된 경우, NAT(Network Address Translation) 나 conntrack 문제, 호스트 CPU 경쟁 때문에 vhost 처리가 밀리는 경우가 여기 들어간다.
이 원인들을 확인하지 않은 채 결과를 Refresh Token 경쟁이나 데이터베이스 락으로 적으면, 고칠 곳이 아닌 곳을 고치게 된다.
이 분리는 실험 결과를 보고 나서 세운 규칙이 아니다. 원문 문서는 이 실험 기반으로 검증하려는 항목을 일곱 개 적어 두었고, 그 마지막이 「네트워크 계층 문제와 애플리케이션/저장소 문제의 분리」였다.
규칙
1. 실험이 지나는 경로를 먼저 적고 시작한다
요청은 클라이언트에서 Nginx 로 가고, 거기서 가상 머신 두 대 중 하나로, 그 안의 K3s 를 지나 Keycloak 으로, 다시 PostgreSQL 또는 Redis 로 간다. 이 경로를 노드들이 공유한다.
경로를 적지 않으면 어느 관측이 어느 구간을 덮는지 정해지지 않는다.
이 실험대에서 그렇게 어긋난 확인이 한 번 있었다. 04 단계의 확인 명령을 엣지 가상 머신 안에서 tailnet 주소로 쳤더니 connection refused 가 돌아왔는데, 엣지에서 나간 패킷은 호스트의 virbr0 으로 들어가고, 들어온 패킷의 도착지 주소를 바꿔 넘기는 호스트의 DNAT(Destination NAT, 도착지 주소 변환) 규칙은 tailscale0 으로 들어온 것만 매칭해서 안 걸렸기 때문이다(§190 · §182). 「설정 문제가 아니라 친 위치 문제다」가 그 단계가 남긴 한 줄이다.
2. 노드 하나에서만 나는 증상을 애플리케이션 동시성으로 먼저 읽지 않는다
노드 하나만 지연되거나 가상 머신 하나에서만 실패하는 증상은 그 노드로 가는 경로 쪽에서도 나올 수 있다. 호스트 브리지 구성 오류, NAT 와 conntrack 문제, 그 가상 머신 쪽 패킷 유실이 같은 모양으로 관측된다.
증상이 노드별로 갈리면 그 노드까지 가는 경로를 먼저 확인한다.
3. 애플리케이션 로그만으로 네트워크 계층을 배제하지 않는다
Keycloak 은 virtqueue 와 vhost-net, TAP, Bridge, 물리 NIC(Network Interface Card) 를 직접 알지 못하고, 게스트가 주는 소켓 위에서만 동작한다. 아래 계층이 어긋나도 애플리케이션 로그에는 느렸다는 것과 실패했다는 것만 남는다.
그래서 애플리케이션 쪽 관측을 아무리 늘려도 이 계층이 원인인지 아닌지는 갈리지 않는다.
4. 네트워크 처리가 쓰는 호스트 CPU 를 실험과 같은 시간축에 기록한다
vhost-net 과 QEMU 스레드, softirq 도 호스트 CPU 를 쓴다. 그래서 네트워크 문제처럼 보이는 지연이 CPU 스케줄링 문제일 수도 있다. 앞의 세 규칙이 애플리케이션 증상을 네트워크 쪽으로 되돌린다면 이 규칙은 네트워크 증상을 CPU 쪽으로 되돌린다. 방향이 반대라서 둘을 한 기준에 둔다.
부하 실험을 돌릴 때 QEMU 와 vhost 의 CPU 사용량, 호스트와 게스트의 CPU, 네트워크 지연을 같은 시간축에 남긴다. 나중에 따로 재면 그때의 부하를 다시 만들어야 한다.
5. 세 가지 검증을 마친 뒤에 원인을 적는다
경로가 무엇인지, 그 경로가 끊기지 않았는지, 네트워크 처리가 호스트 CPU 를 얼마나 쓰는지 셋을 확인한다.
첫째는 호스트 Nginx 에서 Keycloak 까지의 실제 패킷 경로를 추적해서, 둘째는 계층마다 캡처해서, 셋째는 부하 중 CPU 사용량을 재서 답한다.
적용 조건
여러 노드가 같은 네트워크 경로를 공유하는 실험의 결과를 원인에 귀속할 때.
노드별로만 나타나는 지연을 볼 때.
특정 가상 머신에서만 나는 실패를 볼 때.
부하 구간에서만 커지는 지연을 애플리케이션 동시성이나 저장소 lock 으로 결론내려 할 때.
예외
애플리케이션 쪽 관측을 늘리는 것으로는 이 기준을 적용할 수 없다. Keycloak 이 게스트 소켓 위에서만 동작하므로 애플리케이션 로그로는 이 계층이 원인인지 가릴 수 없다.
네트워크 계층이 깨끗하다고 해서 이 기준이 애플리케이션 결함을 배제해 주지는 않는다. 여기서 나오는 것은 네트워크가 원인이 아니라는 것까지다.
가상 머신 위 K3s 안쪽은 이 기준의 범위 밖이다. CNI(Container Network Interface) 와 Service, Pod 네트워크는 별도 계층으로 두고 따로 분석한다.
원문 문서가 이 테스트 환경에 얹어 그린 경로는 확인된 것이 아니라 기준 구조를 그대로 옮겨 놓은 그림이다. 이 저장소에는 이 기준으로 원인을 실제로 가른 실험이 아직 없다.
가까운 것은 실험대를 세울 때 한 번 있었다. 밖에서 온 요청만 엣지에 닿지 않았는데 원인은 게스트 안이 아니라 호스트의 libvirt 방화벽 규칙이었고, 그때 엣지 nginx 는 호스트에서 친 요청에 404 로 응답하고 있었다(§180).
예시
- Keycloak 노드 하나만 응답이 느림 : 그 노드가 있는 가상 머신까지의 경로부터 확인한다
- 가상 머신 하나에서만 요청이 실패 : 호스트 브리지 구성과 그 가상 머신의 tap 연결을 본다
- 부하를 올릴수록 지연이 커짐 : QEMU 와 vhost 의 CPU 사용량을 같은 시간축에서 함께 본다
- 동시 갱신 실험에서 두 번째 요청이 거부됨 : 네트워크 검증 셋을 마친 뒤에 Refresh Token 경쟁으로 적는다
- 네트워크 검증이 전부 깨끗함 : 네트워크가 원인이 아니라는 것까지만 적고 애플리케이션 쪽을 계속 본다