
做容器跨主机通信这件事折腾过的人应该都有同感单机 Docker 网络太好理解了就是docker0网桥加 veth 对一切透明可查。可一旦容器要跑到两台甚至更多宿主机上问题就来了——A 机上的容器 IP 在 B 机上压根看不见bridge 网络默认只在一个宿主机内生效。官方给的标准答案是 Swarm 的 overlay 驱动确实能用但我实际用下来总有一种黑盒感隧道建没建、报文从哪条路径走、为什么偶尔不通都不太好查。于是后来我换了一种做法用 OVSOpen vSwitch自己搭数据面手动建立 VXLAN 隧道把 Docker 容器的网络接到 OVS 桥上实现跨主机的 overlay 通信。这条路走通之后整个网络从交换机的流表到隧道封装都变得完全可控排障也轻松很多。这篇就把完整的思路和踩过的坑写出来。1. 跨主机容器网络的几种方案我为啥绕了一圈选择了 OVS1.1 Docker 自带 overlay 驱动的边界Docker 内置的 overlay 网络驱动使用方式很简单把节点加入 Swarm 集群然后docker network create -d overlay就能用跨主机容器自动互通。它的底层原理是 Linux 内核的 VXLAN默认走 UDP 4789 端口外加一道控制面负责维护节点之间的隧道关系。这套方案在小规模集群里确实省心但有几个让我觉得别扭的地方。首先是隧道状态不可见。容器通信通了你很难直观看到当前节点上建了哪些 VXLAN 隧道、对端是谁、VNI 是多少。每次排障都只能靠docker network inspect和ip -d link去猜。其次是控制粒度太粗。你没法在网络上插入自定义转发规则、没法做基于流的镜像和监控、更没法在隧道层做精细的 QoS。对普通业务可能无所谓但如果你要把容器网络和其他虚拟化平台比如基于 OVS 的私有云环境对接这种黑盒方案就不好使了。1.2 OVS 为什么适合干这件事OVS 本质上就是一台完全开放的虚拟交换机。你可以把每台宿主机上跑的 OVS 桥想象成物理接入交换机VXLAN 隧道端口就是交换机上的trunk 上联口Docker 容器的 veth 网卡就是接到这台交换机上的终端设备。所有转发的路径、MAC 表、流表规则都能用ovs-vsctl和ovs-ofctl看清楚不再是黑盒。更重要的是VXLAN 隧道的建立可以完全手动控制对端 IP、VNI、封装选项全部自己指定不需要依赖 Swarm 管理面。这样在做跨主机 overlay 的时候数据链路是确定性的——隧道就是两台宿主机之间的一条显式 UDP 连接你甚至能用tcpdump直接在物理网卡上抓到完整的 VXLAN 报文。对排障和网络治理来说这个透明性太值钱了。2. 动手之前先搞明白容器跨主机报文到底走了哪些环节2.1 一个 ICMP 包的完整旅程我用一个具体例子讲清楚报文路径因为后面所有配置、排障都是围绕这条路径展开的。假设 Host-A 上的容器 AIP172.30.0.11要 ping Host-B 上的容器 BIP172.30.0.12两者都在同一个 overlay 二层域内。流程是这样的容器 A 首先发 ARP 请求查询172.30.0.12的 MAC 地址。ARP 广播帧从 A 的 veth 口进入 Host-A 的 OVS 桥ovs-br0OVS 查本地 MAC 表发现目标不在本地端口上于是把 ARP 广播复制到包括 VXLAN 隧道口在内的所有端口。隧道口收到后执行 VXLAN 封装把内层以太网帧塞进一个 UDP 报文外层源 IP 是 Host-A 的物理 IP192.168.56.101外层目的 IP 是 Host-B 的物理 IP192.168.56.102UDP 端口 4789VNI 标记为 100然后从物理网卡发到 underlay 网络。Host-B 收到 UDP 4789 报文后OVS 解封装还原出内层 ARP 请求交给自己的ovs-br0最终经由 veth 口进入容器 B。容器 B 回 ARP 应答接着 ICMP echo 请求和响应就沿同一条隧道双向跑。整条链路有一个关键点VXLAN 是纯 L2 over L3 的封装underlay 只需要保证两台宿主机的三层可达不需要担心 VLAN 划分、交换机 trunk 之类的问题。2.2 OVS 的快路径与慢路径OVS 的转发路径分两段。每一条流的第一个报文来到后内核 datapath 拿不准怎么处理会通过 upcall 把它送到用户态的ovs-vswitchd进程由它去查 OpenFlow 流表、做 MAC 学习然后把决策结果下发为一条内核 datapath 规则并缓存起来。后续属于同一条流的报文就直接在内核态按缓存规则转发了不再经过用户态。这个机制决定了 OVS 在大多数场景下性能不至于太差因为流量大头都走快路径。但如果你用ovs-appctl dpif/dump-flows观察会发现新建连接的前几个报文会有slow-path的 upcall 计数增长。理解这点很重要因为后面做性能调优时快路径命中率是个核心观察指标。3. 环境准备两台主机加 OVS先规划好再动手3.1 实验拓扑与网络规划我用的环境是两台 Ubuntu 22.04 的虚拟机物理网卡分别是eth0管理 IP 各为192.168.56.101和192.168.56.102这两台机器通过同一个二层物理网络互通。Docker 版本 24.0.xOVS 直接用 apt 安装的 3.1 版本。规划表格如下项目Host-AHost-B物理管理 IP192.168.56.101192.168.56.102OVS 桥名称ovs-br0ovs-br0容器子网172.30.0.0/24172.30.0.0/24VXLAN 隧道对端192.168.56.102192.168.56.101VNI100100网关内部端口gw0172.30.0.254/24gw0不配IP备用容器固定 IP172.30.0.11172.30.0.12这里有个很重要的决定172.30.0.0/24这个容器网段在两台宿主机上是同一个广播域不要在两台上配置重复的网关 IP否则会出现 ARP 冲突具体见第 5 节。我选择只让 Host-A 上的 OVS 内部端口gw0持有网关地址172.30.0.254/24Host-B 同样建gw0接口但不配 IP留作备用。3.2 安装 OVS 前的几个准备细节安装命令很简单apt-get update apt-get install openvswitch-switch -y systemctl enable --now openvswitch-switch装完先确认服务状态用ovs-vsctl show能看到一个空的 OVS 实例。有一个容易被忽略的点OVS 会在内核创建名为ovs-system的 datapath 设备如果发现系统里装过其他网桥管理工具要确认没有抢占ovs-system的名字。另外建议提前确认物理网卡的 MTU 是 1500。如果你所在的 underlay 网络支持巨型帧比如物理交换机把 MTU 调到了 9000那后面隧道内层 MTU 可以相应调大如果只有标准 1500那隧道封装后容器内的有效 MTU 必须减去 50 字节不然后面会出现大包不通的诡异问题。这个我放到坑位详解。4. 核心操作从 OVS 桥到容器跨主机互通的全过程4.1 创建 OVS 桥并配置 VXLAN 隧道端口在两台宿主机上分别执行下面的命令先把桥建起来ovs-vsctl add-br ovs-br0接着是重头戏创建 VXLAN 隧道端口。Host-A 上这样写ovs-vsctl add-port ovs-br0 vxlan0 -- \ set interface vxlan0 typevxlan \ options:remote_ip192.168.56.102 \ options:key100Host-B 上把remote_ip换成192.168.56.101其余完全一致。解释一下这几个参数typevxlan指定端口类型为 VXLAN 隧道口remote_ip是对端宿主机的物理 IP也就是 underlay 下一跳key100是 VNI相当于给这个二层域打个编号两端必须一致。如果以后要多台主机互联思路就是每台主机上加 N-1 个 vxlan 端口分别指向其他所有宿主机形成一个 full-mesh 的隧道拓扑。建完之后用ovs-vsctl show确认端口状态能看见vxlan0出现在ovs-br0的端口列表里。这里稍微提醒一下OVS 的 tunnel 端口不一定像普通网卡那样在ip link里显示为vxlan0它是作为 datapath 的虚拟端口存在的用ovs-vsctl list interface vxlan0查看信息更准确。4.2 给 OVS 桥配置网关内部端口Docker 的 macvlan 网络需要一个网关地址作为容器默认路由。我在 Host-A 上创建一个类型为internal的 OVS 端口这个端口在系统里表现为一张虚拟网卡可以被 IP 配置ovs-vsctl add-port ovs-br0 gw0 -- set interface gw0 typeinternal ip link set gw0 up ip addr add 172.30.0.254/24 dev gw0Host-B 上同样创建gw0接口并ip link set gw0 up但不要配置172.30.0.254这个地址避免同网段冲突。这样容器访问外部网络时路由会先到网关172.30.0.254由 Host-A 代为处理。4.3 创建 Docker macvlan 网络并运行容器Docker 这边我用 macvlan 驱动把物理网络的父接口直接指定为ovs-br0在两台宿主机上分别执行docker network create -d macvlan \ --subnet172.30.0.0/24 \ --gateway172.30.0.254 \ -o parentovs-br0 \ cross-net然后启动容器指定固定 IP方便验证Host-A 上docker run -itd --name app-a \ --network cross-net \ --ip 172.30.0.11 \ nginxHost-B 上docker run -itd --name app-b \ --network cross-net \ --ip 172.30.0.12 \ nginxmacvlan 驱动的好处是容器网卡直接挂在ovs-br0上不经过 Linux 的docker0网桥也不涉及 Docker 默认的 NAT。OVS 看到的就是一张张真实的 veth 接口完全像交换机对待物理终端一样处理。4.4 验证跨主机通信先在 Host-A 上进入容器 Aping 容器 Bdocker exec -it app-a bash ping 172.30.0.12如果一切正常你会看到跨主机的 ICMP 应答。为了确认报文确实走了 VXLAN 隧道而不是什么意外路径可以到 Host-A 的物理网卡上抓包tcpdump -i eth0 udp port 4789 -n -v能看到类似下面的输出192.168.56.101.44444 192.168.56.102.4789: UDP, length 106外层 IP 是两台宿主机的物理地址目的端口 4789这说明跨主机流量确实被封装成 VXLAN 在 underlay 里传输了。如果还想看内层到底封装的是什么加个-e或使用-XX查看完整报文头。4.5 本机容器之间是否也需要隧道这里补充一个常见的疑问。同一台宿主机上的两个容器互访时报文进入 OVS 桥后直接在本地端口之间二层转发MAC 学习后根本不需要把报文塞进 VXLAN 隧道。这是一个很自然的优化OVS 的 MAC 学习机制自动完成。你可以在容器 A 上再启一个容器 C比如172.30.0.13然后互相 ping再用ovs-appctl fdb/show ovs-br0观察 MAC 表项会看到同一宿主机容器之间的映射走的是本地 veth 端口不涉及 VXLAN。5. 打通之后的几个坑每个都值得单独说5.1 MTU 不一致引发的小包通、大包不通我最早踩的坑就是 MTU。VXLAN 封装会在内层报文上额外增加 50 字节外层以太网头 14 字节 IP 头 20 字节 UDP 头 8 字节 VXLAN 头 8 字节。如果物理网卡 MTU 是 1500那容器里实际可用的 IP MTU 只有 1450。我第一次没改 MTU结果 ping 小包全通一旦ping -s 1472甚至传个稍大点的文件就卡住客户端表现为连接慢、传输超时。解决办法是在创建 OVS 桥之后统一调整 MTUip link set dev ovs-br0 mtu 1450注意ovs-br0的 MTU 改了之后macvlan 子接口和容器 veth 会继承父接口的 MTU。为了保险也可以在启动容器时用--network相关的com.docker.network.mtu选项指定docker network create -d macvlan \ --subnet172.30.0.0/24 \ --gateway172.30.0.254 \ -o parentovs-br0 \ --opt com.docker.network.mtu1450 \ cross-net这是个容易忽略但影响极大的细节建议在网络规划阶段就定好统一 MTU 策略不要等出了问题再改。5.2 网关 IP 配置冲突两台主机都配同一个网关地址的后果前面提到172.30.0.0/24是跨主机的同一个广播域如果我在 Host-A 和 Host-B 上同时配置172.30.0.254/24这个网关地址两台宿主机都会响应对应的 ARP 请求。容器发到网关的报文会被随机应答到其中一台行为完全随机跨主机容器访问外部网络时就会偶发不通。所以裁决如下网关 IP 必须单点持有要么只在 Host-A 配要么只在 Host-B 配。我实际方案是 Host-A 持有网关地址Host-B 的gw0只做 up 状态不配 IP。如果需要高可用正确做法是引入 VRRP/keepalived 之类的虚拟 IP 机制而不是两台硬配同一个地址。5.3 iptables FORWARD 链策略影响隧道解封装后的本地转发在 macvlan OVS 这套组合里Docker 容器之间的二层转发发生在 OVS datapath 内部多数情况下不经过 Linux 的 netfilter。但 VXLAN 隧道解封装后如果流量要从ovs-br0转发到其他本地接口或者容器需要访问本机之外的 underlay 网络就会涉及内核的 FORWARD 链。我遇到的情况是装了一些安全组件后宿主机默认的 FORWARD 策略被设置成了 DROP导致容器可以 ping 同网段其他容器但访问宿主机上其他非 OVS 接口的资源时不通。排查时用iptables -L FORWARD -v看计数器如果 DROP 行疯狂增长基本就是它了。临时放开可以用iptables -P FORWARD ACCEPT更稳的做法是加白名单规则只放行ovs-br0相关接口的转发流量。5.4 OVS 重启后内部端口 IP 丢失OVS 的桥和端口配置本身是持久化到/etc/openvswitch/conf.db的服务重启后桥、隧道口、internal 口都还在。但ip addr手动配置的 IP 地址不会自动恢复——这部分由 Linux 网络栈管理和 OVS 配置库没有关系。我重启了一台机器之后gw0接口存在但 IP 丢了容器网关直接不可达。解决方式就是写一个 systemd unit 或者开机脚本把 IP 配置、MTU 调整这类非 OVS 自身的操作固定下来。示例 unit[Unit] DescriptionRestore OVS gateway IP Afteropenvswitch-switch.service Requiresopenvswitch-switch.service [Service] Typeoneshot RemainAfterExityes ExecStart/bin/sh -c ip link set dev gw0 up ip addr add 172.30.0.254/24 dev gw0 ip link set dev ovs-br0 mtu 1450 [Install] WantedBymulti-user.target这样即使整机重启网关地址和 MTU 也能自动就位。6. 部署完成后的观察与调优方向6.1 快路径命中率与性能观察整套环境跑起来后我用iperf3做了简单的跨主机容器 TCP 吞吐测试。在不调整任何参数的情况下全程走了 VXLAN 封装的 TCP 流实测吞吐大致落在物理带宽的六成到八成之间。这个结果取决于多个因素报文大小、调度器、CPU 主频以及 OVS 快路径的命中情况。想观察快路径命中可以用ovs-appctl dpif/dump-flows ovs-system | head -20你会看到类似recirc_id(0), ..., in_port(1), eth(...), ip(...), packets:12345, bytes:..., used:...packets增长很稳定说明这条流已经被缓存到内核 datapath没有持续 upcall 到用户态。如果发现某条流的packets数目很小但used老变动或者ovs-vswitchd的 CPU 占用很高可以用ovs-appctl dpif-netdev/pmd-stats-show针对 dpdk或者配合top观察ovs-vswitchd的 CPU确认是不是每条流都在慢路径上处理。6.2 流表和 MAC 表的日常检查命令这套方案最让我舒服的就是可视性。日常检查我基本就靠三条命令ovs-vsctl show看端口和隧道状态ovs-ofctl dump-flows ovs-br0看 OpenFlow 流表ovs-appctl fdb/show ovs-br0看 MAC 地址学习表。网络异常时先从 MAC 表看目标地址有没有被学习到再从流表看不匹配的流量去了哪个端口基本能把问题定位到具体环节。如果容器之间的通信出现问题一个高效的排查顺序是先在容器里 ping 同网段其他 IP接着到宿主机上用ovs-appctl fdb/show ovs-br0看 MAC 表有没有对端容器再到物理网卡抓 UDP 4789 的包确认隧道有没有启用最后到对端宿主机确认解封装后的报文有没有正常进入 OVS 桥。按这个顺序走一遍九成问题都能找到答案。6.3 规模变大时的优化思考如果以后节点数量多了full-mesh 的 VXLAN 隧道数量会按 N×(N-1)/2 增长这个方案会变得难以维护。一个思路是引入 BGP EVPN 作为控制面让 OVS 自动学习远端 MAC 和隧道信息而不是手工维护每台节点的隧道对端。另一个方向是关注 underlay 的 MTU 支持——如果你的物理网络全线支持 9000 巨型帧容器内 MTU 可以提到 8950对大流量传输的提升非常明显。还有一个值得做的优化是启用 OVS 的 sFlow 或 NetFlow 监控。在桥上配一个 Flow Collector就能把整张 overlay 网络的实时转发表和流量统计导出来对长期运行后的网络可视化很有价值。配置很简单ovs-vsctl -- --idsflow create sflow agent192.168.56.101 target\192.168.56.200:6343\ header128 sampling64 polling10 -- \ set bridge ovs-br0 sflowsflow最后再分享一个操作层面的小建议如果可能尽量在刚搭好环境时就把 MTU、网关单点、防火墙 FORWARD 策略这三件事一次性确认到位。这三个问题不会立刻暴露但一定会在你压力测试或者跑真实业务的时候找上门。我后来复盘发现凡是小包通大包不通偶尔超时重启后失联这类玄学故障几乎都能归结到这三处。先把底层网络的确定性做出来再往上加业务后面的日子会好过很多。