結果: マージ (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. 反応: インラインコメント1つ、承認2つ
- 5. 学んだこと: オープンソースコントリビューションのコツ
- リンク
一言まとめ
| PR | kubevirt/kubevirt#19499 virt-operator: stop GETting the disabled ContainerPath webhook on every reconcile |
| 変更点 | 3つのファイル。起動前の同期リストに1行追加、直接GETしていた12行を削除、既存の単体テストを拡張。10行追加、13行削除 |
| 反応 | レビュアーがコメントを1つ外すようインラインコメントを残して承認、そのコードを最初に入れた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、Kubernetes v1.34.10であり、そのoperatorは59日目から稼働していました。カウンターはプロセスが起動した後に蓄積された値です。
| リクエスト | 59日間の累積 |
|---|---|
GET mutatingwebhookconfigurations → 404 |
843,995 |
LIST nodes → 200 (reconcileごとに1回、比較用) |
843,995 |
| operatorのすべてのRESTリクエスト | 15,509,446 |
| 2xxではないoperatorのリクエスト | 843,999 |
404を受け取るGETの数が、reconcileごとに1回行う LIST nodes と一桁まで一致していました。reconcile 1回につき404が正確に1回発生していることを意味します。この404はoperator全体のリクエストの5.4%(843,995 ÷ 15,509,446)であり、2xxではない応答843,999個のうち4個を除いたすべてです。
同日、リーダーoperatorのログの「Handling KubeVirt resource」237行を集計したところ、reconcileの間隔は最小6.007秒、中央値6.055秒、最大6.088秒(236区間)でした。59日を6.055秒で割ると約84万2千回となり、カウンターと大きくずれはありません(計算)。reconcileが6秒ごとに回ること自体はLease更新に起因する別の問題としてすでにIssue(#19342)と#19417修正PR(
, 2026-10-29時点でオープン)が存在していました。私たちが確認したのはその上に重なった別の問題でした。reconcileが何秒ごとに回ろうと、このGETは最初から行う必要のないリクエストでした。 VirtOperatorRESTErrorsBurst 目立った被害はありませんでした。しかしPRの本文に書いた通り、これらの404は
アラートが監視するエラー率の下に常に5%程度のベースを敷いてしまいます。アラートの閾値(80%)には遠く及ばないためアラートは鳴りませんが、ダッシュボード上の本物のエラーを隠してしまいます。
2. 原因: ソースのどの行か 580886af 行番号はPRのベースコミット
- 基準です。
createOrDeleteContainerPathVolumesWebhookreconcileごとに呼び出されるContainerPathVolumesは機能ゲートvirt-launcher-pod-mutatorが有効な場合はWebhook - を作成し、無効な場合は既存のWebhookを削除します。 無効な側が問題でした。 273–285行目 はまずインフォーマーキャッシュを確認し、なければ「キャッシュがまだ一杯になっていないかもしれない」というコメントとともにAPIサーバに直接GETを送信します。404であれば削除するものはないとして戻ります。
- このゲートはデフォルトで無効であり、ゲートを有効にしたことがないクラスタにはそのWebhookが最初から存在しません。そのためキャッシュは毎回空であり、毎回GETが飛び、毎回404が返されます。
それでは、「キャッシュが一杯になっていないかもしれない」という懸念は正しい懸念でしょうか。コントローラーは作業を開始する前に複数のインフォーマーの同期を待ちます。
kubevirt.go 169–194行
の hasSynced のリストです。ここに検証Webhookインフォーマー(180行目 ValidationWebhook)はあるのに変異Webhookインフォーマー(MutatingWebhook)だけが抜けていました。そのため、キャッシュを信頼できなかったことには理由があり、修正すべき箇所はGETではなく同期リストのようでした。
この直接的なGETはWebhookとともに #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はWebhookを確認できず削除しません。operatorがラベルで選択する他のオブジェクトも同様に扱われます。「最初の同期前までのみGETを残す」という案も考えましたが、状態を余分に持たなければならないため、1行の同期追加を選択しました。
テストは、既存の単体テスト「should not create virt-launcher-pod-mutator webhook when disabled」を拡張しました。ダミーのクライアントに対してこのWebhook名で
get が1回でも記録されたら失敗するようにしました。PRの本文に記載した結果は次のとおりです(Go 1.27.2、linux/amd64、 GOFLAGS=-mod=vendor、main 6e80bca).
| テスト | 結果 |
|---|---|
| テストのみ変更して修正していない状態、「ContainerPathVolumes feature gate」のグループ | 3つ中1つ失敗 |
| 修正後の同じグループ | 合格 |
go test ./pkg/virt-operator/... |
合格 |
1回は ./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の助けを借り、すべての行を確認し、上記の数値は自分のクラスターで測定した」という段落を追加しました。1点、原則通りにいかなかったことがあります。レビューを反映して再作成したコミット 47a37b0a は、GitHub上で署名なしコミット(unsigned)として表示されます。原則3のSSH署名が、書き直されたコミットから抜けてしまったようです。
4. 反応:インラインコメント1件、承認2件
| 時刻 | 日 |
|---|---|
| 10-17 23:37 | PR公開。prowが size/S·area/operator·sig/control-plane ラベル、レビュワー2名を指定 |
| 10-17 23:38 | CodeRabbitは「このリポジトリは自動レビューが無効になっている」としてスキップ。prowは「信頼されていない作成者は /ok-to-test でテストを開始できない、コラボレーターが /test all を実行する必要がある」と案内 |
| 10-19 16:52 | レビュワーのインラインコメント。16:54のレビューで /test all とともにアプルーバー1名を /cc |
| 10-19 22:03 | コメントを修正したコミットで再プッシュ、22:13にインラインコメントへ返信 |
| 10-19 22:23 | レビュワーの承認、 lgtm ラベル |
| 10-19 23:27 | アプルーバーが /approve、ボットが2段階e2e 8個(sig-network·storage·compute·operator、Windows、vGPU、IPv6)を実行 |
| 10-20 16:05 | マージ |
レビューの内容はインラインコメントが1件でした。最初にアップロードしたバージョンには、削除したGETの位置に5行のコメントがありました。「コントローラーはWebhookインフォーマーの同期を待ち、Webhookは常にラベルを持っているため、キャッシュに存在しない場合は削除するものがない。ここでAPIサーバーに問い合わせると、ゲートを使用していないすべてのクライアントでreconcileごとに404 GETが発生する」という説明でした。レビュワーは、これがそれほど大きな問題だったのか分からないと述べました。Webhookが作成または削除されたらイベントハンドラーが検知して再度キューイングするはずなので、コメントは削除してもよいのではないかという意見でした。
その通りでした。同期前の短い隙間にキャッシュが空であったとしても、Webhookが出現した瞬間にインフォーマーが再度reconcileを呼び出します。コメントはこのGETの有用性を誇張していたことになります。5行のコメントを削除してコミットを修正し再アップロードした上で、「インフォーマーが追加・削除時にどうせ再びキューに入れるため、コメントが大げさに言っていたので削除した」と簡潔に返信しました。その結果、PRのコードは1行の追加とGETブロックの削除のみとなりました。
アプルーバーは virt-operator ディレクトリのOWNERSに記載されている人物であり、この直接的なGETを追加した#16662の作成者でもあります。残された言葉は /approve と感謝の1行でした。2段階e2eを含む42個の検査がすべて合格し、翌日の午後にtideによってマージされました。記録には、このPRがKubeVirtを主軸とすることを決定した後に本体リポジトリにマージされた最初のPRとして記載されています。
5. 学んだこと:オープンソース貢献のコツ
- 「operator自身のメトリクスが最良の再現手段です。」クライアントリクエストカウンターはコードパスごとに残ります。404の数がreconcileの数と一致するという1行が、「reconcileごとに正確に1回」という主張を証明してくれます。ログを漁ったりデバッガーをアタッチしたりする必要はありませんでした。
- 「回避策コードを削除する前に、回避策が生じた理由を修正します。」 直接GETはキャッシュを信頼できなかったために生まれたものであり、キャッシュを信頼できなかった理由は、同期化リストから抜け落ちていた1行でした。その1行を入れたことで、GETを削除することが安全になりました。すぐ隣に同じ種類のインフォーマー(
ValidationWebhook)がすでに存在していたということが、最も簡潔な根拠となりました。 - 「コメントは、レビュアーを説得するために書くものではありません。」 PR本文で説明すべき内容をコードコメントに長く残してしまい、指摘を受けました。コードに残してよいのは、コードが変わらない限り真であることだけに限り、今回なぜ変更したのかはコミットメッセージとPR本に記載します。
- 「信頼されていない貢献者は、テストを自分で実行できません。」 KubeVirtリポジトリでは外部の作成者が
/ok-to-testでテストを開始することができず、協力者が/test allを入力してあげる必要があります。そのため、PR本文にローカルでのテスト結果(修正前の失敗、修正後の成功)を数値で記載しておくことがより重要でした。 - 「修正したコミットにも署名が付いているか確認します。」 レビューを反映してコミットを作り直す際、署名が抜けていました。DCOは通過しましたが、原則とは異なります。pushする前に
git log --show-signature -1を1回実行すれば確認できます。
Titiの一言。 門番のおじさんが6秒ごとに倉庫の扉を叩いて「ここに箱はありますか?」と尋ねたそうです。倉庫には最初からその箱などなかったのですが。メモを見ればわかるはずなのに、メモのリストにその倉庫だけが抜けていたため、信用できなかったのです。メモに1行書き加えてあげたところ、2ヶ月間に84万回以上も叩いていた作業がなくなりました。「なぜノックが必要だったのか」を長々と書き留めていたメモは、「それは外してもいいですよ」と言われたので剥がしたそうです。
リンク
すべての資料は2026年10月29日に閲覧しました。
- PR: kubevirt/kubevirt#19499 (マージコミット
6443e168) - 修正されたソース(ベースコミット):
admissionregistration.go266–301行目,kubevirt.go169–194行目 - 直接GETが追加されたPR: kubevirt/kubevirt#16662
- 6秒のreconcile: Issue #19342、PR #19417
- KubeVirt AI貢献ポリシー: https://github.com/kubevirt/community/blob/main/ai-contribution-policy.md
- 同日、同じ調査から出てきたCDI PR: 第7部、containerized-data-importer#4319