ステータス: ドラフト(2026-10-17)、一部実測。 基準バージョン metallb v0.16.1(コミット 791dfcc63ced)。小さなクラスターがまさにこのバージョン(controller・speaker、FRR モード用の frr-k8s を含む)を稼働させているため、実測は2026-10-17にそのクラスターから 読み取りのみ を行って得ました(CR・ノードラベル・サービス・イベントの読み取り、speaker のログの読み取り)。ARP パケットを直接キャプチャしてはいません。執筆ルール: 実測データを基本とし、計測できなかったものは「未計測」、確認できなかったものは「未確認」と表記します。
目次
- 1. 1行要約: 番号札を配る窓口と、名前を呼ばれたら手を挙げる人
- 2. 構成図
- 3. コードマップ
- 4. コアパスの追跡
- 5. 執筆者の貢献とつながる場所
- 6. 実測: 小さなクラスターでの読み取り結果
- 7. どこから読むべきか
- 8. 最初にコントリビュートしやすい場所
- 9. 制限事項
- 参考
1. 1行要約: 番号札を配る窓口と、名前を呼ばれたら手を挙げる人
クラウドでは type: LoadBalancer サービスを作成すると、クラウド側が外部 IP を1つ割り当ててくれます。ベアメタルにはその「クラウド」がないため、サービスが永遠に <pending> のまま残ります。MetalLB がその穴埋めをします。
仕事は2つあります。 番号札の窓口(controller)はサービスごとにプールから IP を1つ決定し、サービスの status に書き込みます。 手を挙げる人 (speaker)はノードごとに1つずつ存在し、ネットワークから「この IP は誰のものか?」と問われると、決まった1つのノードだけが「自分だ」と答えます(L2 モード)。または、ルーターに「この IP は自分宛てに送るように」と経路を通知します(BGP モード)。
2つの仕事を分けた理由は、必要な権限と配置場所が異なるためです。IP を選ぶ作業はクラスター全体を見渡して 1か所で 行うことで競合がなくなり、特別な権限も必要ありません。通知する作業は各ノードのネットワークカードで ARP パケットを読み書きする必要があるため、 ノードごとに hostNetwork と
NET_RAW 権限で動作させる必要があります。権限が大きい側(speaker)は IP を選ばず、IP を選ぶ側(controller)はパケットに触れません。
| コンポーネント | 役割 | 実行場所 |
|---|---|---|
| controller | サービスごとのプールからの IP 割り当て、共有 IP ルールの検証、status の記録、Webhook | Deployment 1つ |
| speaker | memberlist による互いの生死確認、サービスごとの「自分が広告する順番か」の判定、ARP・NDP 応答 | DaemonSet(ノードごと、hostNetwork) |
| frr-k8s(FRR モード) | speaker が書き込んだ FRRConfiguration を FRR 設定に変換して BGP セッションを運用 | DaemonSet(ノードごと) |
| CRD | IPAddressPool, L2Advertisement, BGPPeer, BGPAdvertisement, ServiceL2Status … | API サーバー |
2. 構成図
図の読み方:
- 上部の左側にある controller は、API サーバーの Service と IPAddressPool のみを確認して IP を決定し、
status.loadBalancer.ingressに書き込みます。ネットワークには何も送信しません。 - 中央の speaker は status に書き込まれた IP を読み取り、memberlist(ポート 7946)でお互いが生きているかを確認します。1つのサービスに対してすべての speaker が同じ計算をそれぞれ行い、計算結果の1位である1つのノードのみが広告を行います。代表を別途選出する合意(リーダー選挙)のプロセスはありません。
- 色は小さなクラスターの現在の状態を示しています。ワーカー A が広告中、ワーカー B が待機、コントロールプレーンのセットは除外ラベルにより候補から外れ、GPU ノードは L2Advertisement の nodeSelectors の範囲外です(セクション 6)。
- 下部右側の BGP は点線です。小さなクラスターは FRR モードでインストールされていますが、BGPPeer が存在しないため BGP 広告は行っていません。
3. コードマップ
| パス | 内容 | 最初に読むファイル |
|---|---|---|
controller/ |
controller バイナリ: サービスの収束、IP 割り当ての呼び出し | main.go, service.go |
speaker/ |
speaker バイナリ: プロトコルごとのハンドラー(L2・BGP)と共通のフロー | main.go, layer2_controller.go |
internal/allocator/ |
プールからの IP 選択、共有 IP(sharing key)のルール | allocator.go |
internal/layer2/ |
ARP・NDP 応答機、gratuitous ARP、インターフェースのスキャン | announcer.go, arp.go |
internal/speakerlist/ |
memberlist のラッパー: 参加、生死イベント、利用可能な speaker のリスト | speakerlist.go |
internal/config/ |
CR を内部設定に変換(nodeSelectors → ノードのセットなど) | config.go |
internal/k8s/ |
コントローラーランタイム上の監視・再同期、ノードの条件・ラベルヘルパー | k8s.go, nodes/nodes.go |
internal/bgp/ |
BGP セッションの実装セット(native、frr、frr-k8s) | frrk8s/frrk8s.go |
api/ |
CRD Go 型 | v1beta1/l2advertisement_types.go |
website/content/ |
ドキュメント(概念、設定、トラブルシューティング) | concepts/layer2.md |
4. コアパスの追跡
4.1 controller: 1つのサービスに1つの IP
controller の SetBalancer, controller/main.go:52 はサービスが変更されるたびに呼び出されます。サービスをコピーして
convergeBalancer, controller/service.go:49 により「あるべき状態」に合わせ、変更点があれば status のみを更新します。
convergeBalancer の思考順序は「現在持っている IP を維持できるか → できなければ新しく取得する」です。サービスがすでに IP を持っている場合、その IP をアロケーターに再度登録し(設定と一致していれば何もなし)、プールが変更されてもはや許可されていなければ削除して新しく取得します。新しく取得する場所は
allocateIPs, controller/service.go:252 です。ユーザーが IP を指定している場合(metallb.io/loadBalancerIPs アノテーションなど)はその IP を、プールを指定している場合はそのプールから、何も指定されていない場合はすべてのプールを巡回して
(Allocate, internal/allocator/allocator.go:363)
空きアドレスを探します。
1つの IP を複数のサービスで共有することも可能です。条件は同じ sharing key(metallb.io/allow-shared-ip アノテーションの値)、重複しないポート、そして同じ backend key です(checkSharing, allocator.go:728)。controller が1つのサービスを削除すると「すべてのサービスの再処理」(SyncStateReprocessAll)を実行します。空きを待っていたサービスにその空いた枠を割り当てるためです。
controller はリーダー選出なしで(LeaderElection: false)Deployment 1つで動作します。割り当て状態はメモリ上に存在しますが、再起動時には各サービスの status に記載された IP をアロケーターに再登録し(convergeBalancer の最初のステップ)、状態を復元します。真実のソース(Single Source of Truth)が API サーバーの status であるために可能な設計です。
4.2 speaker: すべてのサービスに対する「自分の番か」
speaker の SetBalancer, speaker/main.go:385 もサービスごとに呼び出されます。LoadBalancer でない場合、IP がまだない場合、またはその IP がどのプールにも属していない場合は広告を取り下げます。それ以外の場合は有効なプロトコルごとに
handleService, :446 を呼び出し、そこでプロトコルハンドラーの
ShouldAnnounce が空文字列を返せば広告し、理由となる文字列を返せばその理由で広告を取り下げます
(deleteBalancerProtocol, :495)。広告する場合は info ログ
serviceAnnounced とサービスイベント nodeAssigned(「announcing from node …」)を残し、取り下げる場合は serviceWithdrawn と理由
(notOwner, noMatchingAdvertisement …)を残します。セクション 6 の数値はこれらの行をカウントしたものです。
4.3 L2 選挙: 絞り込み、ハッシュによるソート
L2 ハンドラーの ShouldAnnounce, speaker/layer2_controller.go:83
が選挙のすべてです。順番に読み解くと以下のようになります。
- サービスに準備されたエンドポイント(endpoint)が1つもない場合、
notOwnerとなります。受け取る Pod が存在しない IP は誰も広告しません。 - サービスに該当する L2Advertisement のうち、 自ノードを選択したものがなければ
noMatchingAdvertisement(adsMatchNodeL2,:213)となります。nodeSelectors は設定の読み込み時にノード名のセットにあらかじめ変換されています (l2AdvertisementFromCR,internal/config/config.go:743). - 候補ノードのリストを作成します(
speakersForAds,:253).
for s := range eligibleNodes {
if k8snodes.IsNetworkUnavailable(nodes[s]) {
level.Debug(l).Log("event", "skipping should announce l2", "service", name, "reason", "speaker's node has NodeNetworkUnavailable condition")
continue
}
if !c.ignoreExcludeLB && k8snodes.IsNodeExcludedFromBalancers(nodes[s]) {
level.Debug(l).Log("event", "skipping should announce l2", "service", name, "reason", "speaker's node has labeled 'node.kubernetes.io/exclude-from-external-load-balancers'")
continue
}
if adsMatchNodeL2(ads, s) {
res[s] = true
}
}
出所: metallb v0.16.1 speaker/layer2_controller.go、Apache-2.0
eligibleNodes は memberlist が現在生きていると見なしている speaker です(セクション 4.4)。ここで NetworkUnavailable 条件のノード、 除外ラベルの付いたノード、nodeSelectors 外のノードが除外されます。除外ラベルのチェックは
IsNodeExcludedFromBalancers, internal/k8s/nodes/nodes.go:30
ですが、ラベルの 値は確認せず、キーが存在するかどうかのみ を確認します。kubeadm はこのラベルを空の値で付与するため、値が空であっても除外されます。このチェックを無効化するのが speaker の --ignore-exclude-lb フラグです(ズspeaker/main.go:99)。4. サービスが externalTrafficPolicy: Local である場合、そのサービスのエンドポイントが存在するノードのみを残します。ノードを跨いで転送しない方針であるため、エンドポイントのないノードが IP を保持してはなりません。5. 残ったノードをソートします。
// Using the first IP should work for both single and dual stack.
ipString := toAnnounce[0].String()
// Sort the slice by the hash of node + load balancer ips. This
// produces an ordering of ready nodes that is unique to all the services
// with the same ip.
sort.Slice(availableNodes, func(i, j int) bool {
hi := sha256.Sum256([]byte(availableNodes[i] + "#" + ipString))
hj := sha256.Sum256([]byte(availableNodes[j] + "#" + ipString))
return bytes.Compare(hi[:], hj[:]) < 0
})
// Are we first in the list? If so, we win and should announce.
if len(availableNodes) > 0 && availableNodes[0] == c.myNode {
return ""
出所: metallb v0.16.1 speaker/layer2_controller.go、Apache-2.0
このソートが設計の核心です。すべての speaker が 同じ入力(生きているノードのリスト、同じ設定、同じ IP)を用いて 同じ関数を計算するため、お互いに問い合せなくても1位が1人に決まります。投票もロックも存在しません。ノード名に IP を付与してハッシュ化するため、IP ごとに1位が異なり、サービスが複数あれば広告が各ノードに分散します(セクション 6 で 4個と 6個に分かれていた理由です)。ハッシュの材料がサービスではなく IP であるため、IP を共有するサービスは候補リストが同じであれば同じノードが広告します。1つの IP に対して MAC アドレスが複数存在してはならないためです。
代償もこのソートから生じます。入力である「生きているノードのリスト」が変更されると、1位が変わる可能性があります。あるノードの speaker がクラッシュと復旧を繰り返すと、そのノードが1位であった IP はその都度所有者が変わります。 「外部アクセスが途絶えた日」に記載した「広告ノードが1日に 1,275 回変更された」という出来事は、このコードの自然な結果です。そのため、nodeSelectors によって候補を 揺らがないノードに絞り込むことが処方箋となります。
そして3段階と2段階が交わる場所に罠があります。nodeSelectorsが除外ラベルの付いたノードだけを選ぶと、それらのノードは2段階目を通過しますが、
3段階目で脱落し、他のノードは2段階目で noMatchingAdvertisement に落ちます。候補が0になって すべてのspeakerが広告を引っ込めます。
残る痕跡はdebugレベルのログが1行だけです。これが第5節のPRがドキュメントに記した内容です。
4.4 memberlist: 「生きている」の出どころ
選挙の入力である「生きているspeaker」はHashiCorp memberlistが提供します。
speakerlist.New, internal/speakerlist/speakerlist.go:57
はノード名をmemberlistの名前として使い(エンドポイントのnodeNameと合わせるため)、ポッドIPと7946ポートにバインドし、秘密鍵があればそのハッシュで
トラフィックを暗号化します。バインドアドレスやラベルの設定がなければmemberlistをオフにし、その場合は「speakerがすべてのノードにいる」と仮定して
APIサーバーのノードリストをそのまま候補として使います。
UsableSpeakers, :303 はmemberlistの現在の
構成員をそのまま返します。構成員が変わると(参加・離脱) memberlistWatchEvents, :339
が「node event - forcing sync」を残し、 すべてのサービスを再計算させます。
なぜKubernetesのノードのReady条件だけを使わないのでしょうか。ノードコントローラーがノードをNotReadyに変更するまでにはデフォルトの設定で数十秒かかり、 その間、死んだノードがIPを握ったまま残ります。memberlistはノード間で直接pingをやり取りし、数秒以内に疑い(Suspect)として失敗を示します。 代わりにspeakerが頻繁に死に生き返るノードがあれば、その分頻繁に「再計算」が発生します。第6節でその痕跡を見ます。
4.5 ARPへの応答: 応答機とgratuitous ARP
選挙に勝つとL2ハンドラーの SetBalancer, layer2_controller.go:131
が Announce.SetBalancer, internal/layer2/announcer.go:258
にIPを登録します。パケット処理は別で稼働しています。
- どのカードで応答するか。
updateInterfaces,announcer.go:61がノードのネットワークカードを定期的にスキャンし、有効で、ブリッジ配下ではなく、NOARPではないカードごとにARP応答機を、IPv6リンクローカルアドレスが あるカードごとにNDP応答機を作成します。除外リスト(正規表現)はConfigMapmetallb-excludel2から読み込みます (parseAnnouncedInterfacesToExclude,speaker/main.go:572)。 L2Advertisementにinterfacesを記載すると、そのカードでのみ応答します。 - 誰が尋ねたら。
processRequest,internal/layer2/arp.go:73はARPリクエストであり、ブロードキャストであるか自分宛てのものであるとき、そのIPが登録されておりカードも一致していれば (shouldAnnounce,announcer.go:237) 自分のMACで応答します。カーネルではなくspeakerが生ソケットで応答するため、ノードのIP設定は変更されません。これがNET_RAW権限が必要な理由です。 - 主が変わったら。 新しく広告を開始したIPは
spamLoop,announcer.go:157が5秒間、1.1秒間隔でgratuitous ARPをばらまきます。古いMACを記憶しているスイッチやルーターのテーブルを早く修正させるためです。間隔1100ms はissue #172での経験値であるとコメントに記されています。
4.6 BGPとFRRモード(簡単に)
BGPハンドラーの ShouldAnnounce, speaker/bgp_controller.go:155
には選挙がありません。条件を満たすすべてのノードが各自ルーターに経路を通知し、負荷分散はルーターのECMPが行います。それでも除外ラベルの
チェックはここにもあります(:178).
BGPセッションを実際に開く実装は3つのうち1つを環境変数 METALLB_BGP_TYPE で選択します
(speaker/main.go:109,
newBGP, bgp_controller.go:492). frr-k8s の場合
speakerはBGPを直接話さず、ノード別の FRRConfiguration CRを作成しておきます
(updateConfig, internal/bgp/frrk8s/frrk8s.go:193)。
そのCRをfrr-k8sデーモンセットがFRR設定に変更します。同じノードのFRRをMetalLB以外のもので(例:ノード独自の経路広告)も併用できるようにするための
分離です。
5. 執筆者の貢献と続く場所
metallb/metallb#3156(2026-10-17時点オープン)はドキュメントのPRです。修正箇所は2つです。
- L2高度な設定ドキュメントの
nodeSelectorsの説明に「除外ラベルの付いたノードはnodeSelectorsに一致しても広告しない、kubeadmはコントロール プレーンにこのラベルを付与する、それらのノードだけを選ぶと誰も広告しない、回避策は--ignore-exclude-lb(Helmspeaker.ignoreExcludeLB)」 という案内を追加しました。 - トラブルシューティングドキュメントの「speakerがサービスを広告しない場合」のリストにこのラベルを追加しました。
コードとの結びつきは4.3節の通りです。ラベルのチェックは
nodes.go:30、L2でそれを使用している箇所は
layer2_controller.go:268、残るログは同じファイル
269行目のdebugログ1行です(このログはissue #2505をきっかけに#2538で追加されたとPR本文に記載されています)。ドキュメントがコードに追いついていなかった
箇所は「nodeSelectorsはハードフィルター」という説明でした。ハードフィルターなのは確かですが、その手前に もう一つのハードフィルター(除外ラベル)があることが
抜けていました。経緯は 「グローバルアクセスが途絶えた日」にあります。
6. 実測: 小規模クラスターでの観測
2026-10-17、すべてのコマンドは読み取り(kubectl get·logs)です。ノード名は役割に、アドレスは伏せ、元データは別途保管しました。
構成。
| 項目 | 値 |
|---|---|
| controller | Deployment 1個、 controller:v0.16.1、引数 --webhook-mode=enabled |
| speaker | DaemonSet 8/8、 speaker:v0.16.1、hostNetwork、権限は NET_RAW 1つ(残りはすべてdrop)、読み取り専用ルートファイルシステム |
| speakerポート | 9120(メトリクス)、7946 TCP・UDP(memberlist) |
| speaker環境変数 | METALLB_BGP_TYPE=frr-k8s, METALLB_ML_BIND_ADDR=ポッドIP、 METALLB_ML_BIND_PORT=7946 |
| frr-k8s | DaemonSet 8/8(コンテナ5個)、状態クリーンアップ用Deployment 1個。FRRNodeState 8個、FRRConfiguration 0個、BGPPeer 0個 |
| CRD | metallb.io 9種、 frrk8s.metallb.io 4種 |
| IPAddressPool | 1個、アドレス16個、 autoAssign: true。statusに割り当て11・残り5 |
| L2Advertisement | 1個、 nodeSelectors = ホスト名 In [ワーカー A, ワーカー B]、 interfaces なし |
| LoadBalancerサービス | 14個がIP 11個を使用(4つのサービスが同じsharing keyでIP 1つを共有) |
metallb-excludel2 |
基本リスト14個(^docker.*, ^veth.*, ^lxc.*, ^cali.* …) |
FRRモードでインストールされていてもBGPPeerがないためFRRConfigurationも作成されていません。4.6節のfrr-k8sのパスは小規模クラスターの現在の構成では 休止しています。
除外ラベルは -L では見えない。 kubectl get nodes -L node.kubernetes.io/exclude-from-external-load-balancers は8つのノードすべてで
空欄でした。ラベルの値が空文字であるため、「存在しない」のと全く同じに見えます。キーが存在するかどうかで選ぶ -l node.kubernetes.io/exclude-from-external-load-balancers
で改めて確認すると コントロールプレーンセットが浮かび上がりました。4.3節のチェックが値ではなくキーを見る仕様と噛み合い、人間の目には「ラベルなし」、コードの
目には「除外」となる組み合わせです。NetworkUnavailable条件は8つのノードすべてでFalseでした。
選挙結果。 speakerが広告中のサービスごとに残る ServiceL2Status が10個ありました。
| 広告ノード | サービス数 |
|---|---|
| ワーカー A | 4 |
| ワーカー B | 6 |
| その他 6台 | 0 |
候補2台(図2の第4段階)がsha256順にIPを分け合った結果です。IPを共有するサービスのネットにはL2の状態がありませんでしたが、いずれのネットも
準備されたエンドポイントが0個でした。4.3節の第1段階(notOwner)の通り誰も広告していないということです。サービスイベントには
announcing from node "<워커 A>" with protocol "layer2" がサービスごとに8回ずつ残っていました(イベント保存期間内の値)。
memberlistと応答機。 ワーカーAのspeaker(現在のコンテナは10-14開始)の開始ログです。
| ログ | 回数・内容 |
|---|---|
node event - forcing sync(NodeJoin) |
8(自分を含め8ノード) |
memberlist join successfully |
1, number of other nodes: 7 |
created ARP responder for interface |
3: 有線カード、無線カード、 cilium_vxlan |
created NDP responder for interface |
2: 有線カード、 cilium_vxlan |
有線のみを使用するノードなのに 無線カードとCiliumのVXLANデバイスにも ARP応答機が紐づいていました。デフォルトの除外リストに cilium_* がなく、
L2Advertisementに interfaces を記載しなかったためです(4.5節)。現在は問題が顕在化していませんが、広告を特定のカードに限定したい場合は
interfaces を記載するのが適切です。
揺らいだ日の痕跡。 ワーカーBのspeaker(現在のコンテナ、約22日分のログ)を集計しました。
| ログ | 回数 |
|---|---|
service has IP, announcing |
1,948 |
withdrawing service announcement |
920(理由: notOwner 918, noMatchingAdvertisement 2) |
node event - forcing sync |
434 |
memberlist: Suspect … has failed, no acks received |
166(大部分はGPUノードC) |
memberlist: Marking … as failed, suspect timeout reached |
106 |
日付別に分けると、memberlistの不審メッセージは10-04に138件、10-05に39件、10-07に10件、10-13に1件であり、同じ日の広告行は10-04に
1,343件、10-05に322件、その後は1日あたり10件前後でした。同時期のGPUノードCのspeaker再起動回数は360(最後の終了10-08、終了コード1)
です。1つのspeakerが死に生き返るたびにmemberlistの構成員が変わり(4.4節)、すべてのspeakerが再計算を行い、そのノードが1位だったIPは主が
変わります(4.3節)。 noMatchingAdvertisement 2件(10-14)はnodeSelectorsをコントロールプレーンに変更した瞬間にワーカーBが候補から外れて残した行のようです。
speakerの再起動回数。 コントロールプレーンセット 0、ワーカーA 2、ワーカーB 10、GPUノードA 8、GPUノードB 0(ポッドの稼働日数2日)、GPUノードC 360。原因はこの記事では 追っていません(未確認)。
バージョン文字列。 speakerとcontrollerの開始ログがいずれも MetalLB … starting version 0.16.1 (commit dev, branch dev) でした。
公式イメージであるにもかかわらずコミットが dev として記録されます。第8節の最初の項目がその理由です。
7. どこから読むべきか
website/content/concepts/layer2.md。概念をまず 掴みます(ドキュメントは5分で読めます)。speaker/layer2_controller.go:83。選挙の1つの関数を 最後まで読みます。4.3節の5段階がこの中にすべて含まれています。internal/speakerlist/speakerlist.go:57。 選挙の入力がどこから来るのかを確認します。internal/layer2/announcer.go:61とarp.go:73。勝った後に実際にパケットに応答する箇所です。controller/service.go:49。反対側、IPを決定する側です。speaker/main.go:385。2つのプロトコルが1つのフローにどのように組み込まれるのかを 最後に見ます。
8. 初めての貢献におすすめの場所
ソースを読みながら実際に目についた箇所です。まだどこにも投稿していません。
- リリースイメージのコミットが
dev. イメージを作成するワークフローがbuild-argsを키: 값の形式で渡しているため (.github/workflows/publish.yaml:109).
build-args: |
GIT_BRANCH: ${{ github.ref_name }}
GIT_COMMIT: ${{ github.sha }}
出所: metallb v0.16.1 .github/workflows/publish.yaml、Apache-2.0
docker/build-push-action の build-args は行ごとに 키=값 を期待するため、Dockerfileのデフォルト値 ARG GIT_COMMIT=dev がそのまま残るように
見えます。第6節で公式v0.16.1イメージの2つが commit dev, branch dev を記録していることを確認しました。 = に変更する1行の修正候補です。
- memberlist設定ファイルのコメントと動作の不一致。
memberlistFileConfigコメントには「ファイルが存在する場合、すべてのフィールドが必須であり、欠けているとエラー」とありますが (speakerlist.go:357)、実際にエラーになるのは 時間文字列のフィールドだけであり、整数・ブール値のフィールドは欠けていても静かに0・falseになります(例:IndirectChecks0)。また、ファイルを読み込むと--ml-wan-configに関わらずLANのデフォルト値から始まります(:397).internal/speakerlistにはテストファイルが1つもなく、テストを追加することが最初の良い貢献になります。 - エラーメッセージ。 backend keyが異なる場合に「does not match existing sharing key」と表示されます
(
allocator.go:623)。除外ラベル関数のコメントの 「labeld」と対応のない引用符(nodes.go:29)も同じ系統です。 - 静かな「広告なし」。 4.3節の罠は debug ログを1行残すだけです。候補が除外ラベルのために 0 になった場合、サービスイベントを1つ残すことはドキュメント PR(#3156) の次の段階として検討に値します。動作を変更する作業であるため、まずは Issue で尋ねる方が無難です。
- デフォルトの除外リストと Cilium。 6節で
cilium_vxlanに ARP 応答側がアタッチされるのを見ました。デフォルトリスト (config/native/exclude-l2-config.yaml)には CalicoやFlannelのデバイス名はあるものの、Cilium のものはlxcのみです。悪影響がないかどうかの確認が必要です(未確認)。
貢献手順は CONTRIBUTING が DCO 署名(Signed-off-by)を求め、詳細な案内はウェブサイトのコミュニティページに委ねています。PR テンプレートでは、小さくない変更であれば事前に Issue を開くよう求めています。2026-10-17 時点で good first issue のラベルで開かれた Issue は5件以上ありました(テスト実行のドキュメント化、コンテナ単位のテストなど)。
9. 制限事項
- BGP モードは概要のみ扱いました。セッション管理、BFD、コミュニティ、frr-k8s の内部構造は確認しておらず、小さなクラスターには BGP ピアが存在しないため測定も行っていません(未測定)。
- IPv6・デュアルスタックの割り当て、
avoidBuggyIPs、プールセレクター(serviceAllocation)、Webhook の検証については扱っていません。 - 広告ノードが切り替わる際に実際何秒間通信が途切れるのか、Gratuitous ARP がルーターのルーティングテーブルをいつ修正するのかについては、パケットをキャプチャして測定していません(未測定)。
- 6節のログの数値は各 speaker の 現在のコンテナ のログのみをカウントしたものであり、それ以前の再起動時の履歴は含まれていません。
- 障害自体の経緯と診断の順序は 「グローバル接続が途切れた日」に譲ります。
Titi の一言。 クラスメートの8人がそれぞれ同じ数式で「このボールは誰の番?」を計算すれば、会議をしなくてもたった1人だけが手を挙げます。 ところが、名札の裏に「私はパスする」というステッカーが貼られた友達だけを選り分けておくと、ボールは床に転がったままになってしまうのです!
参考
すべての資料は 2026-10-17 に閲覧しました。
- metallb v0.16.1 ソース: https://github.com/metallb/metallb/tree/v0.16.1
- L2 のコンセプトドキュメント: https://github.com/metallb/metallb/blob/v0.16.1/website/content/concepts/layer2.md
- L2 の高度な設定ドキュメント: https://github.com/metallb/metallb/blob/v0.16.1/website/content/configuration/_advanced_l2_configuration.md
- トラブルシューティングドキュメント: https://github.com/metallb/metallb/blob/v0.16.1/website/content/troubleshooting/_index.md
- コントりビューションガイド: https://github.com/metallb/metallb/blob/v0.16.1/CONTRIBUTING.md
- HashiCorp memberlist: https://github.com/hashicorp/memberlist
- Kubernetes 公式ラベル一覧(除外ラベル): https://kubernetes.io/docs/reference/labels-annotations-taints/#node-kubernetes-io-exclude-from-external-load-balancers
- 筆者のドキュメント PR: https://github.com/metallb/metallb/pull/3156
- 同じシリーズ: CloudNativePG を読む. 別シリーズ: グローバル接続が途切れた日
引用したコードと設定は metallb のものであり、Apache License 2.0 に準拠しています。