午前2時、Linuxブリッジが限界を迎えた夜
火曜日の深夜2時14分、PagerDutyのアラートが鳴り響きました。10GbEリンク上の6つのDockerコンテナ全体で毎秒約12万パケットを処理していた決済ゲートウェイで、インバウンドトランザクションの約8%がドロップしていたのです。ホストのメトリクスは深刻でした。CPUコア0と1はksoftirqdによって完全に飽和し、使用率100%に張り付いていました。レイテンシのパーセンタイル値は、安定していた1.8msから450ms超へと跳ね上がっていたのです。
ホストのdmesgログを確認すると、原因はすぐに判明しました。
# dmesg -T | tail -n 2
[Tue Oct 28 02:14:11 2025] nf_conntrack: table full, dropping packet
[Tue Oct 28 02:14:12 2025] nf_conntrack: table full, dropping packet
# top -b -n 1 | head -n 12
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
14 root 20 0 0 0 0 R 98.7 0.0 12:43.11 ksoftirqd/0
21 root 20 0 0 0 0 R 97.4 0.0 11:15.82 ksoftirqd/1
標準のDockerブリッジ(docker0)が、conntrackの過剰な負荷によって破綻していたのです。デフォルトのブリッジモードでは、すべてのパケットがvethペアを通過し、ソフトウェアブリッジのフックを経由し、NetfilterのNATテーブルをヒットし、ステートテーブルエントリを割り当てます。nf_conntrack_maxの値を65,536から262,144へ引き上げたところで、メモリが枯渇してsoftirqによるストールが再発するまでの時間をわずか20分稼げるに過ぎません。
毎秒2,000リクエストを処理する標準的なCRUD APIであれば、ブリッジのオーバーヘッドはほとんど気になりません。しかし、Kafkaブローカー、Redisクラスター、ストリーミング取り込みノードなどのワイヤレートに迫る環境では、ブリッジは大きな足かせとなります。その解決策とは何でしょうか? 仮想ブリッジと接続追跡(conntrack)を完全にバイパスし、MACVLANまたはIPVLANを使用してコンテナの名前空間(namespace)を物理NICに直接アタッチすることです。
アーキテクチャ:MACVLANとIPVLANの比較
どちらのドライバも中間のホップを排除しますが、レイヤ2およびレイヤ3ヘッダーの扱い方は根本的に異なります。
従来のブリッジ経路
標準のDockerネットワーキングでは、パケットは混雑したソフトウェアパイプラインを経由してルーティングされます。
物理NIC (eth0) <--> iptables (NAT / MASQUERADE) <--> Linuxブリッジ (docker0) <--> vethペア <--> コンテナ eth0
フレームごとにパケットバッファのコピー、チェックサムの検証ステップ、カーネル名前空間間のCPUコンテキストスイッチが発生します。MACVLANとIPVLANは、物理ドライバのキューに直接接続することで、このパイプラインを排除します。
MACVLAN:単一インターフェース上で個別の一意なMACアドレスを使用
MACVLANは、eth0などの物理親インターフェースにバインドされたサブインターフェースに対して、個別の仮想レイヤ2 MACアドレスを割り当てます。各コンテナは物理サブネットから直接IPアドレスを取得します。
上位スイッチ <--> 物理NIC (eth0)
|-- eth0.1 (MAC: 02:42:0a:00:00:02, IP: 192.168.10.10) -> コンテナ A
|-- eth0.2 (MAC: 02:42:0a:00:00:03, IP: 192.168.10.11) -> コンテナ B
トップオブラック(ToR)スイッチからは、単一の物理ポートに複数の異なるMACアドレスが存在するように見えます。MACVLANは4つの動作モードをサポートしています。
- Bridgeモード(デフォルト):同一の親インターフェースを持つコンテナ同士は、カーネルメモリ内でローカルに通信します。フレームがホストのNICの外に出ることはありません。
- VEPA(Virtual Ethernet Port Aggregator):同一ホスト内のコンテナ間トラフィックは物理スイッチに送出され、ヘアピンルーティング(折り返し)されます。これにより、ハードウェアファイアウォール上でアクセスポリシーを直接適用できます。
- Privateモード:同一親インターフェース上のすべてのコンテナ間通信を破棄します。上位スイッチがパケットをヘアピン転送しようとした場合でも遮断されます。
- Passthruモード:物理インターフェース全体を単一のコンテナ名前空間に占有させ、仮想サブインターフェースをバイパスします。
注意点:MACVLANを使用するには、プロミスキャスモードを許可するか、大規模なCAMテーブル割り当てに対応した物理スイッチポートが必要です。AWS VPC、Google Cloud Platform、Microsoft Azureなどのパブリッククラウド上でMACVLANを展開すると即座に失敗します。クラウドのハイパーバイザが、許可されていないMACアドレスを持つパケットを破棄するためです。
IPVLAN:単一のMACアドレスと複数のIPアドレス
IPVLANは、クラウドのMACフィルタリングの制限を回避します。新しいレイヤ2アドレスを生成する代わりに、すべてのIPVLANサブインターフェースが親NIC(eth0)のMACアドレスを複製(クローン)します。変更されるのはレイヤ3のIPアドレスのみです。
上位スイッチ <--> 物理NIC (eth0) [MAC: 52:54:00:ab:cd:ef]
|-- ipv0 (MAC: 52:54:00:ab:cd:ef, IP: 192.168.10.10) -> コンテナ A
|-- ipv1 (MAC: 52:54:00:ab:cd:ef, IP: 192.168.10.11) -> コンテナ B
逆多重化(デマルチプレキシング)はネットワーク層で行われます。カーネルは受信フレーム内の宛先IPを検査し、一致するコンテナへとルーティングします。IPVLANには主に2つのモードが用意されています。
- L2モード:MACVLANのBridgeモードと同様に動作しますが、単一のMACアドレスを維持します。ブロードキャストやARPクエリは引き続き機能し、デフォルトの物理ゲートウェイが外部トラフィックをルーティングします。802.1Xポートセキュリティを導入しているエンタープライズネットワークでもスムーズに動作します。
- L3モード:ホストを内部ルータとして動作させます。ARPおよびブロードキャストドメインは完全に排除されます。Linuxカーネルは純粋に宛先IPのルーティングテーブルに基づいて受信パケットをスイッチングし、高密度のマイクロサービスに最大の高スループットを提供します。
ステップ別の実装手順
ドライバの切り替えに必要なセットアップ時間はわずか5分で、既存のブリッジネットワークを停止させる必要もありません。
1. ネットワークパラメータの確認
親インターフェース名、サブネット、およびデフォルトゲートウェイを確認します。
# ip -br addr show dev eth0
eth0 UP 192.168.10.50/24 fe80::5054:ff:fe12:3456/64
# ip route show default
default via 192.168.10.1 dev eth0 proto dhcp metric 100
ここでは、親インターフェースがeth0、ゲートウェイが192.168.10.1、物理CIDRが192.168.10.0/24となります。
2. DockerでMACVLANを設定する
物理サブネット内に専用のIP範囲を切り出します。--ip-rangeを指定することで、静的サーバーやDHCPプールですでに使用されているIPアドレスをDockerが誤って割り当てるのを防ぎます。
docker network create -d macvlan \
--subnet=192.168.10.0/24 \
--gateway=192.168.10.1 \
--ip-range=192.168.10.192/27 \
-o parent=eth0 \
prod-macvlan-net
これにより、32個のアドレス(192.168.10.192~192.168.10.223)がコンテナワークロード専用として確保されます。インスタンスを起動してみましょう。
docker run -d --name payment-processor \
--net=prod-macvlan-net \
--ip=192.168.10.195 \
nginx:alpine
コンテナのインターフェースを確認します。
docker exec payment-processor ip addr show eth0
コンテナは一意のMACアドレスと静的IP 192.168.10.195を保持しています。物理ネットワーク上の外部ホストは、このIPに直接アクセスできます。-p 80:80のようなポート公開も、iptablesのNATルールも、conntrackエントリの消費も一切不要です。
3. ホスト・コンテナ間の通信不能(ブラックホール)問題を解消する
Dockerホストから192.168.10.195へpingを実行してみてください。パケットは100%ドロップします。Linuxカーネルはブロードキャストループを防止するため、物理親インターフェースとそのMACVLAN子エンドポイント間の直接のパケットフローを遮断する仕様になっているためです。
双方向のトラフィックを確立するには、ホスト上に専用のMACVLANリンクを設定します。
# 1. ホストに仮想macvlanインターフェースを追加
sudo ip link add macvlan-shim link eth0 type macvlan mode bridge
# 2. 物理サブネットからホスト用の予約済みIPを割り当て
sudo ip addr add 192.168.10.220/32 dev macvlan-shim
sudo ip link set macvlan-shim up
# 3. shim経由でコンテナトラフィックをルーティング
sudo ip route add 192.168.10.192/27 dev macvlan-shim
ホストからの疎通を確認します。
# ping -c 3 192.168.10.195
PING 192.168.10.195 (192.168.10.195) 56(84) bytes of data.
64 bytes from 192.168.10.195: icmp_seq=1 ttl=64 time=0.048 ms
64 bytes from 192.168.10.195: icmp_seq=2 ttl=64 time=0.039 ms
64 bytes from 192.168.10.195: icmp_seq=3 ttl=64 time=0.041 ms
レイテンシは1.8msから41マイクロ秒へと激減しました。ホストからコンテナへの通信がshim経由でスムーズにルーティングされるようになります。
4. IPVLAN L2モードの導入
クラウドVMや、ポートセキュリティ(スティッキーMAC)が適用されているスイッチ環境では、IPVLAN L2を使用します。
docker network create -d ipvlan \
--subnet=192.168.10.0/24 \
--gateway=192.168.10.1 \
--ip-range=192.168.10.224/27 \
-o parent=eth0 \
-o ipvlan_mode=l2 \
prod-ipvlan-l2
新しいネットワーク上でコンテナを起動します。
docker run -d --name metrics-collector \
--net=prod-ipvlan-l2 \
--ip=192.168.10.225 \
alpine sleep 3600
ホストとゲスト(コンテナ)のハードウェアアドレスを比較します。
# ホストの物理MAC
cat /sys/class/net/eth0/address
# コンテナのMAC
docker exec metrics-collector cat /sys/class/net/eth0/address
どちらも同一のMAC文字列(例:52:54:00:12:34:56)を返します。物理スイッチから見えるハードウェアアドレスは1つだけですが、フィルタリングによる破棄を発生させることなく、異なるIPへのトラフィックが正常に転送されます。
ドライバ選定の指針
ワークロードがブリッジのキャパシティを超えた場合、conntrackテーブルの場当たり的なチューニングに頼るのは避けるべきです。物理インフラの特性に合った適切なドライバを選択しましょう。
- MACVLANを採用すべきケース:ベアメタルサーバー環境や、プライベートなVMware/Proxmoxクラスター環境。スイッチポートごとに複数のMACアドレスが許可されており、サービスが生のレイヤ2ブロードキャスト探索を必要とする場合に最適です。
- IPVLAN L2を採用すべきケース:パブリッククラウド(AWS EC2、GCP Compute Engineなど)、無線インターフェース、または厳格なMACポートセキュリティポリシーが設定された社内ネットワーク環境に適しています。
- IPVLAN L3を採用すべきケース:高密度なバックエンドマイクロサービス環境。上位ルータとBGPルートをピアリングできる場合に、ブロードキャストノイズやARPテーブルのオーバーヘッドを完全に排除できます。
ホスト側のshim設定は必ず構成管理プレイブックに記録しておき、CIDRは計画的に分割し、ip -s link show eth0でパケットドロップを定期的に監視しましょう。

