결과: 병합 (2026-10-20 16:05, kubevirt/kubevirt#19499). 2026-10-29 기준으로 이 변경을 담은 릴리스는 아직 없습니다. 사실은 GitHub 의 PR·리뷰·줄 댓글·타임라인·검사 기록, KubeVirt 소스(PR 의 바탕 커밋
580886af), 그리고 기여 원장의 실측 원자료(operator 지표 원값, reconcile 간격)와 초안에서 가져왔습니다. 시각은 한국 시간입니다.
목차
- 한 줄 요약
- 1. 무엇이 문제였나: 59일 동안 843,995번의 404
- 2. 원인: 소스의 어느 줄인가
- 3. 무엇을 바꿨나: diff 요지
- 4. 반응: 줄 댓글 하나, 승인 둘
- 5. 배운 것: 오픈소스 기여 요령
- 링크
한 줄 요약
| PR | kubevirt/kubevirt#19499 virt-operator: stop GETting the disabled ContainerPath webhook on every reconcile |
| 바꾼 것 | 세 파일. 시작 전 동기화 목록에 한 줄 추가, 직접 GET 하던 12줄 삭제, 기존 단위 시험 확장. 10줄 추가, 13줄 삭제 |
| 반응 | 리뷰어가 주석 하나를 빼자는 줄 댓글을 남기고 승인, 그 코드를 처음 넣은 approver 가 /approve |
| 결과 | PR 공개 10-17 23:37 → 리뷰 10-19 16:54 → 승인 22:23 → 병합 10-20 16:05, 약 2일 16시간 27분. 검사 42개 통과 |
1. 무엇이 문제였나: 59일 동안 843,995번의 404
TT Lab 은 수강생 실습 VM 을 KubeVirt 로 띄웁니다. 2026-10-17 에 TT Lab 클러스터의 KubeVirt 구성 요소를 읽기 명령만으로 훑으면서
(기여 원장의 measure/kv), 리더 virt-operator 가 내보내는 kubevirt_rest_client_requests_total 지표를 그대로 받아 두었습니다.
판은 KubeVirt v1.9.0, 쿠버네티스 v1.34.10 이고, 그 operator 는 59일째 떠 있었습니다. 카운터는 프로세스가 뜬 뒤로 쌓인 값입니다.
| 요청 | 59일 누적 |
|---|---|
GET mutatingwebhookconfigurations → 404 |
843,995 |
LIST nodes → 200 (reconcile 마다 한 번, 비교용) |
843,995 |
| operator 의 모든 REST 요청 | 15,509,446 |
| 2xx 가 아닌 operator 요청 | 843,999 |
404 를 받는 GET 의 수가 reconcile 마다 한 번 하는 LIST nodes 와 한 자리까지 같았습니다. reconcile 한 번에 404 가 정확히 한 번
난다는 뜻입니다. 이 404 는 operator 전체 요청의 5.4%(843,995 ÷ 15,509,446)이고, 2xx 가 아닌 응답 843,999개 중 넷을 뺀 전부입니다.
같은 날 리더 operator 로그의 「Handling KubeVirt resource」 237줄을 세어 보니 reconcile 간격은 최소 6.007초, 중앙 6.055초, 최대 6.088초(236구간)였습니다. 59일을 6.055초로 나누면 약 84만 2천 번이라, 카운터와 크게 어긋나지 않습니다(계산). reconcile 이 6초마다 도는 것 자체는 Lease 갱신 때문에 생기는 별도 문제로 이미 이슈(#19342)와 수정 PR(#19417, 2026-10-29 기준 열려 있음)이 있었습니다. 우리가 본 것은 그 위에 얹힌 다른 문제였습니다. reconcile 이 몇 초마다 돌든, 이 GET 은 처음부터 할 필요가 없는 요청이었습니다.
눈에 띄는 피해는 없었습니다. 그래도 PR 본문에 적은 대로, 이 404 들은 VirtOperatorRESTErrorsBurst 경보가 보는 오류 비율 밑에 늘
5% 정도의 바닥을 깔아 둡니다. 경보 문턱(80%)에는 한참 못 미쳐 경보는 울리지 않지만, 대시보드에서 진짜 오류를 가립니다.
2. 원인: 소스의 어느 줄인가
줄 번호는 PR 의 바탕 커밋 580886af 기준입니다.
- reconcile 마다 불리는
createOrDeleteContainerPathVolumesWebhook는 기능 게이트ContainerPathVolumes가 켜져 있으면 웹훅virt-launcher-pod-mutator를 만들고, 꺼져 있으면 있던 웹훅을 지웁니다. - 꺼진 쪽이 문제였습니다. 273–285행 은 먼저 인포머 캐시를 보고, 없으면 「캐시가 아직 안 찼을 수도 있다」는 주석과 함께 API 서버에 직접 GET 을 보냅니다. 404 면 지울 것이 없다며 돌아갑니다.
- 이 게이트는 기본으로 꺼져 있고, 게이트를 켠 적이 없는 클러스터에는 그 웹훅이 처음부터 없습니다. 그래서 캐시는 매번 비어 있고, 매번 GET 이 나가고, 매번 404 가 돌아옵니다.
그러면 「캐시가 안 찼을 수도 있다」는 걱정은 맞는 걱정일까요. controller 는 일을 시작하기 전에 여러 인포머의 동기화를 기다립니다.
kubevirt.go 169–194행
의 hasSynced 목록입니다. 여기에 검증 웹훅 인포머(180행 ValidationWebhook)는 있는데 변형 웹훅 인포머(MutatingWebhook)만
빠져 있었습니다. 그러니 캐시를 믿지 못한 것에는 이유가 있었고, 고칠 곳은 GET 이 아니라 동기화 목록으로 보였습니다.
이 직접 GET 은 웹훅과 함께 #16662(VEP 165, 2026-02-24 병합)에서 들어왔습니다.
3. 무엇을 바꿨나: diff 요지
# pkg/virt-operator/kubevirt.go: 시작 전에 기다리는 인포머 목록
informers.ValidationWebhook.HasSynced() &&
+ informers.MutatingWebhook.HasSynced() &&
# pkg/virt-operator/resource/apply/admissionregistration.go
- // Feature gate is disabled, delete the webhook if it exists
+ // Feature gate is disabled, delete the webhook if it exists.
_, exists, _ := r.stores.MutatingWebhookCache.Get(webhook)
- if !exists {
- // Also check directly in case cache is not populated
- _, err := r.k8sClient.AdmissionregistrationV1().MutatingWebhookConfigurations().Get(
- context.Background(), webhook.Name, metav1.GetOptions{})
- ... (404 면 nil, 그 밖의 오류면 반환, 있으면 exists = true)
- }
캐시가 믿을 만해지면 캐시에 없다는 것은 지울 것이 없다는 뜻이므로 API 요청을 하지 않습니다. 게이트를 켰다가 끈 경우에는 operator 가
만든 웹훅이 늘 인포머가 고르는 managed-by 라벨을 달고 있으므로 캐시에 보이고, 예전처럼 지워집니다. PR 본문에는 이 선택의 대가도 적었습니다.
누가 손으로 그 라벨을 떼면 operator 는 웹훅을 못 보고 지우지 않습니다. operator 가 라벨로 고르는 다른 객체들도 같은 식으로 다룹니다.
「첫 동기화 전까지만 GET 을 남기는」 안도 생각했지만, 상태를 하나 더 들고 다녀야 해서 한 줄짜리 동기화 추가를 골랐습니다.
시험은 기존 단위 시험 「should not create virt-launcher-pod-mutator webhook when disabled」 을 넓혔습니다. 가짜 클라이언트에 이 웹훅 이름으로
get 이 한 번이라도 기록되면 실패하게 했습니다. PR 본문에 적은 결과는 이렇습니다(Go 1.27.2, linux/amd64, GOFLAGS=-mod=vendor, main 6e80bca).
| 시험 | 결과 |
|---|---|
| 시험만 바꾸고 고치지 않은 상태, 「ContainerPathVolumes feature gate」 묶음 | 3개 중 1개 실패 |
| 고친 뒤 같은 묶음 | 통과 |
go test ./pkg/virt-operator/... |
통과 |
한 번은 ./pkg/virt-operator/resource/apply/ 의 TestInstallStrategy 가 실패했는데, 고친 판과 안 고친 판 모두에서 다시 돌리면 재현되지
않았다는 것도 본문에 그대로 적었습니다. 이 출력의 원본 파일은 원장에 따로 남기지 않았습니다. 고친 operator 를 TT Lab 클러스터에 올려 404 가
사라지는 것까지 재 보지는 않았습니다(미실측). TT Lab 클러스터는 아직 v1.9.0 이고, 이 변경을 담은 릴리스가 없기 때문입니다.
AI 사용은 이렇게 밝혔습니다. KubeVirt 의 AI 기여 정책은
AI 도구를 썼으면 커밋 꼬리말(Assisted-by:·Co-authored-by:·Generated-by:)로 밝히기를 권하고, DCO 서명은 그대로 요구합니다.
커밋에는 Assisted-by: Claude 와 Signed-off-by 를 붙였고, PR 템플릿의 「AI Contributions」 칸에 표시한 뒤 본문 끝에 「지표에서 원인을
추적하고 변경과 시험의 초안을 쓰는 데 Claude 의 도움을 받았고, 모든 줄을 검토했으며 위 숫자는 내 클러스터에서 쟀다」는 문단을 넣었습니다.
한 가지는 원칙대로 되지 않았습니다. 리뷰를 반영해 다시 만든 커밋 47a37b0a 는 GitHub 에서 서명되지 않은 커밋(unsigned)으로 보입니다.
원칙 3 의 SSH 서명이 다시 쓴 커밋에서 빠진 것으로 보입니다.
4. 반응: 줄 댓글 하나, 승인 둘
| 시각 | 일 |
|---|---|
| 10-17 23:37 | PR 공개. prow 가 size/S·area/operator·sig/control-plane 라벨, 리뷰어 둘 지정 |
| 10-17 23:38 | CodeRabbit 은 「이 저장소는 자동 리뷰가 꺼져 있다」며 건너뜀. prow 는 「신뢰되지 않은 작성자는 /ok-to-test 로 시험을 못 연다, 협업자가 /test all 을 쳐야 한다」고 안내 |
| 10-19 16:52 | 리뷰어의 줄 댓글. 16:54 리뷰에서 /test all 과 함께 approver 한 명을 /cc |
| 10-19 22:03 | 주석을 고친 커밋으로 다시 push, 22:13 줄 댓글에 답 |
| 10-19 22:23 | 리뷰어 승인, lgtm 라벨 |
| 10-19 23:27 | approver 가 /approve, 봇이 2단계 e2e 8개(sig-network·storage·compute·operator, Windows, vGPU, IPv6)를 돌림 |
| 10-20 16:05 | 병합 |
리뷰의 내용은 줄 댓글 하나였습니다. 처음 올린 판에는 지운 GET 자리에 다섯 줄짜리 주석이 있었습니다. 「controller 는 웹훅 인포머의 동기화를 기다리고, 웹훅은 늘 라벨을 달고 있으므로 캐시에 없으면 지울 것이 없다, 여기서 API 서버에 물으면 게이트를 안 쓰는 모든 클러스터에서 reconcile 마다 404 GET 이 난다」는 설명이었습니다. 리뷰어는 이것이 그렇게 큰 문제였는지 모르겠다고 했습니다. 웹훅이 생기거나 사라지면 이벤트 처리기가 알아채고 다시 큐에 넣을 테니, 주석은 빼도 되겠다는 의견이었습니다.
맞는 말이었습니다. 동기화 전의 짧은 틈에 캐시가 비어 있더라도, 웹훅이 나타나는 순간 인포머가 다시 reconcile 을 부릅니다. 주석이 이 GET 의 쓸모를 과장하고 있었던 셈입니다. 주석 다섯 줄을 지우고 커밋을 고쳐 다시 올린 뒤, 「인포머가 추가·삭제 때 어차피 다시 큐에 넣으니 주석이 부풀려 말했다, 뺐다」고 짧게 답했습니다. 그 결과 PR 의 코드는 한 줄 추가와 GET 블록 삭제만 남았습니다.
approver 는 virt-operator 디렉터리의 OWNERS 에 있는 사람이고, 이 직접 GET 을 넣은 #16662 의 작성자이기도 합니다. 남긴 말은 /approve 와
감사 한 줄이었습니다. 2단계 e2e 를 포함한 검사 42개가 모두 통과했고, 다음 날 오후 tide 가 병합했습니다. 원장에는 이 PR 이 KubeVirt 를
주력으로 정한 뒤 본체 저장소에 들어간 첫 병합으로 적혀 있습니다.
5. 배운 것: 오픈소스 기여 요령
- 「operator 자신의 지표가 가장 좋은 재현입니다.」 클라이언트 요청 카운터는 코드 경로마다 남습니다. 404 수가 reconcile 수와 같다는 한 줄이 「매 reconcile 마다 정확히 한 번」이라는 주장을 증명해 줍니다. 로그를 뒤지거나 디버거를 붙일 필요가 없었습니다.
- 「우회 코드를 지우기 전에 우회가 생긴 이유를 고칩니다.」 직접 GET 은 캐시를 믿을 수 없어서 생겼고, 캐시를 믿을 수 없었던 이유는 동기화
목록에서 빠진 한 줄이었습니다. 그 한 줄을 넣자 GET 을 지우는 일이 안전해졌습니다. 옆에 같은 종류의 인포머(
ValidationWebhook)가 이미 있었다는 것이 가장 짧은 근거가 되었습니다. - 「주석은 리뷰어를 설득하려고 쓰지 않습니다.」 PR 본문에서 할 설명을 코드 주석에 길게 남겼다가 지적을 받았습니다. 코드에 남을 말은 코드가 바뀌지 않는 한 참인 것만 적고, 이번에 왜 바꿨는지는 커밋 메시지와 PR 본문에 둡니다.
- 「신뢰되지 않은 기여자는 시험을 직접 못 돌립니다.」 KubeVirt 저장소는 외부 작성자가
/ok-to-test로 시험을 열 수 없고, 협업자가/test all을 쳐 주어야 합니다. 그래서 PR 본문에 로컬 시험 결과(고치기 전 실패, 고친 뒤 통과)를 숫자로 적어 두는 것이 더 중요했습니다. - 「고친 커밋에도 서명이 붙는지 봅니다.」 리뷰를 반영해 커밋을 다시 만들 때 서명이 빠졌습니다. DCO 는 통과했지만 원칙과 다릅니다. push 전에
git log --show-signature -1한 번이면 확인됩니다.
티티 한마디. 문지기 아저씨가 6초마다 창고 문을 두드리며 「여기 상자 있어요?」 하고 물었대요. 창고에는 처음부터 그 상자가 없었는데 말이죠. 수첩을 보면 되는데, 수첩 목록에 그 창고만 빠져 있어서 못 믿었던 거예요. 수첩에 한 줄을 적어 드렸더니 두 달 동안 84만 번 넘게 두드리던 일이 사라졌어요. 「노크가 왜 필요했는지」 길게 써 붙인 쪽지는 「그건 빼도 돼요」 하셔서 떼었답니다.
링크
모든 자료는 2026-10-29 에 열람했습니다.
- PR: kubevirt/kubevirt#19499 (병합 커밋
6443e168) - 고친 소스(바탕 커밋):
admissionregistration.go266–301행,kubevirt.go169–194행 - 직접 GET 이 들어온 PR: kubevirt/kubevirt#16662
- 6초 reconcile: 이슈 #19342, PR #19417
- KubeVirt AI 기여 정책: https://github.com/kubevirt/community/blob/main/ai-contribution-policy.md
- 같은 날 같은 조사에서 나온 CDI PR: 7편, containerized-data-importer#4319