ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

同宿主机容器互访:命名空间、veth与bridge网络深入解析

同宿主机容器互访:命名空间、veth与bridge网络深入解析 有一次我在宿主机上部署一套内部服务同一个网段里起了三个容器A、B、C。A 能 ping 通 B也能 curl 通 B 的接口但到 C 就是超时。三个容器明明都在同一台机器上逻辑上都是邻居为什么表现完全不一样后来顺着容器的命名空间、veth 和 bridge 网络一步步查下去才发现问题出在它们挂在不同的 bridge 网络上。这台机器上的经历让我把同宿主机下不同命名空间的容器间网络通信彻底理了一遍本文就是这次排查过程中沉淀下来的案例和原理。这篇文章适合所有在单台宿主机上跑多容器、却搞不清“为什么有的能通、有的不能通”的人。我会先从 namespace 和 veth 的原理讲起再拆四个真实场景同一 bridge 下互通、跨 bridge 互通、host 网络与 bridge 网络互通以及不用 Docker 纯手工模拟 netns 互联。最后附一份我自己的排错清单。1. 先理清一件事容器“命名空间”到底隔离了什么1.1 容器里的网络世界是一套独立视图容器隔离的核心不是进程本身而是 namespace。Linux 里一个进程从创建开始就活在若干 namespace 中包括 PID、Mount、UTS、IPC、User 和 Network 这几类。对网络通信来说真正起作用的是 network namespace简称 netns。netns 隔离的资源比很多人想的多它不光是隔离 IP 地址还隔离网卡设备、路由表、ARP 表、/proc/net 下的连接状态以及 netfilter 规则也就是 iptables 规则。换句话说同一个宿主机上的两个容器它们看到的网络世界是各自独立的。容器 A 里执行ip addr只能看到自己 netns 里的网卡看不到容器 B 的容器 A 里加一条路由也完全不影响容器 B 的路由表。这就是为什么经常有人说容器不算是“虚拟机”但又比普通进程更隔离——进程只是共享宿主机的网络栈而容器拥有自己的一套网络栈。理解这个后面所有案例就都有基础了。我见过不少新手在宿主机上执行ip addr看到一堆 veth 开头的网卡以为容器网卡丢了或者系统被污染了。其实那些 veth 网卡只是 veth pair 在宿主机这一端的“线头”真正的“线尾”已经插进对应容器的 netns 里了。宿主机看到的这些只是连接宿主侧网络设备的一根根线缆。1.2 怎么进入另一个命名空间看真相排查容器网络问题时不要满足于docker exec。docker exec确实能进容器但它默认只进入了容器的 Mount、PID 等命名空间网络视图其实也是容器自己的所以没问题。但如果容器里没有ip命令很多精简镜像没有你就需要换一种方式。我常用的方法是用nsenter直接进入容器的 netns# 先拿到容器的 PID docker inspect -f {{.State.Pid}} container-name # 再进入它的 netns 查网络信息 nsenter -t PID -n ip addr nsenter -t PID -n ip route nsenter -t PID -n iptables -L这个操作特别有用因为一些基础镜像里的/proc/net信息是残缺的而 nsenter 使用的是宿主机上的工具只是把网络视角切到了目标容器的 netns。我在一次排查中发现容器里的路由表多了条奇怪的黑洞路由容器内ip route和ip netns list都看不到就是用 nsenter 进入后才发现的。注意一点普通的生产容器默认不会出现在ip netns list里因为 Docker 没有把容器的 netns 挂载到/var/run/netns。如果想让某个容器出现在ip netns list里需要手动创建软链ln -s /proc/PID/ns/net /var/run/netns/name不过排查时直接用 nsenter 就够了。1.3 veth pair连接两个网络命名空间的“虚拟网线”容器里的 eth0 不是真的物理网卡它是一个虚拟以太网设备。这种设备总是成对出现一端在容器 netns 里叫 eth0另一端在宿主机或 bridge 上名字形如 vethxxxxxx。这对设备就是 veth pair可以把它想象成一根虚拟网线——从 eth0 发出的数据包会原封不动地从 vethxxxxxx 这端出来反过来也一样。如果用更生活化的类比veth pair 就像一根网线的两个水晶头一个插在容器的“网口”上另一个插在宿主机桥接设备的“网口”上。数据包在两头之间传输不需要经过物理线路也不产生真实流量。理解了 veth pair再回头看 Docker 的默认 bridge 网络一切就通顺了。Docker 会创建一个 Linux bridge默认叫 docker0自定义网络则是一个br-xxxxxxxx设备然后把每个容器的 veth 宿主机端插到这个 bridge 上。于是所有插在同一个 bridge 上的容器虽然 netns 彼此独立却共享一个二层广播域通信链路是完整的。2. 案例一同一 bridge 下两个不同命名空间的容器为什么直接能互通2.1 实验准备两个容器不映射端口也能互访很多人对容器互访有个固有印象A 访问 B 必须做端口映射。其实这完全错误。同一个 bridge 网络下的容器直接用容器 IP 就能互通端口映射只是给宿主机外部或其它网络访问用的。先做实验。我建了一个自定义网络启动两个容器docker network create -d bridge demo-net docker run -itd --name app-a --network demo-net nginx:alpine docker run -itd --name app-b --network demo-net alpine sleep 3600查一下 app-a 的 IPdocker inspect -f {{.NetworkSettings.Networks.demo-net.IPAddress}} app-a假设得到 172.20.0.2然后从 app-b 里 ping app-adocker exec app-b ping -c 2 172.20.0.2结果直接就是通的。注意 app-b 并没有-p端口映射app-a 也没有。它们能互通靠的是共处同一个 bridge 网络而不是端口映射。2.2 数据链路拆解报文到底是怎么走的从 app-b ping app-a数据包的完整路径是这样的app-b 的 netns 里ping 进程生成 ICMP 请求包源地址为 app-b 的容器 IP目标地址为 app-a 的容器 IP。包从 app-b 的 eth0 发出顺着 veth pair 到达宿主机端那个 vethxxxxxx 网卡。宿主机端 veth 网卡插在 demo-net 对应的 Linux bridge 上包进入 bridge。bridge 查自己的 MAC 地址表发现 app-a 的 MAC 地址在另一个 veth 端口上于是把包从那个端口发出去。包再从另一端的 veth pair 进入 app-a 的 eth0最终被 app-a 的网络栈收下。整个过程跟两台物理机插在同一个交换机上通信几乎一样。这里最容易被忽略的一点是两个容器虽然 netns 独立但它们通过一个共同的 bridge 设备共享了二层网络。在二层眼里它们就是同一网段上的两台主机。你可以用 tcpdump 在宿主机端验证。找到 app-a 或 app-b 对应的 veth 网卡名后在宿主机上抓包tcpdump -i any -n icmp执行 ping 的同时抓包能看到 ICMP 请求和回应都会经过宿主机上的 veth 设备这能直观验证上面的链路。2.3 常见的三个误区第一以为容器之间必须用映射端口访问。实际上同一个 bridge 下容器间用容器 IP 直连比走宿主机映射端口更高效少一次 DNAT 转换。这也解释了为什么很多 Docker Compose 项目里服务间连接都写http://service-name:port而不是http://localhost:映射端口。第二以为容器 IP 是固定的。Docker 默认按网络顺序分配 IP容器重启后可能变。所以容器间互访在生产环境最好用 Docker 内置 DNS自定义 bridge 网络下容器名就是主机名可以被解析而不是写死 IP。第三以为 bridge 模式下容器和宿主机互通理所当然。确实默认可以互通而且要经过宿主机 IP 栈转发。但如果你在宿主机上看到 FORWARD 链默认策略是 DROP那容器之间、容器到宿主机之间的流量就会出问题。Docker 安装时会自动把 FORWARD 链改成 ACCEPT 或插入放行规则但有其他软件比如某些防火墙管理工具重置过 iptables 后这个规则会丢失容器间就突然不通了。这类问题我遇到不止一次后面排错清单里会细说。3. 案例二跨 bridge 网络的不同命名空间容器默认不通怎么办3.1 场景还原两个容器分别挂在 net1 和 net2这是文章开头那个 A、B、C 场景的升级版。假设服务 A 在 net1服务 B 在 net2docker network create net1 docker network create net2 docker run -itd --name app-a --network net1 nginx:alpine docker run -itd --name app-b --network net2 alpine sleep 3600从 app-b ping app-adocker exec app-b ping -c 2 app-a-IP默认结果就是不通。这里 Docker 没有报错包发出去就石沉大海。3.2 定位问题被隔离规则挡在哪一层搞清这个问题要看两层。第一层是二层隔离。net1 和 net2 各自对应一个 Linux bridge它们是两个不同的二层广播域。app-b 发出的 ARP 请求只在 net2 的 bridge 上广播根本到不了 net1 那边的 veth 端口所以 app-b 连 app-a 的 MAC 地址都拿不到。第二层是路由和 iptables。即使宿主机开着 IP 转发能从路由表里找到去 net1 网段的路径Docker 还会在 FORWARD 链里插入 DOCKER-ISOLATION 链专门阻断不同 bridge 网络之间的转发流量。这条隔离规则在 Docker 里的优先级很高是为了防止用户无意间把两个不相关的网络通过宿主机路由“打通”。在宿主机上执行iptables -L FORWARD -n -v iptables -L DOCKER-ISOLATION -n -v就能看到相关规则。这个设计初衷是好的但确实坑了不少人有人说“我在宿主机上加了路由为什么还是不互通”多半就是忘了这条 FORWARD 链上的隔离规则。3.3 方案一用 docker network connect 把容器接入另一个网络最推荐的打通方式不是改 iptables而是让一个容器同时接入两个 bridge 网络docker network connect net2 app-a执行后app-a 的 netns 里会多出一块网卡 eth1IP 属于 net2 网段。这时 app-b 就能直接访问 app-a 在 net2 上的 IP 了。这个方案的本质是给同一台“主机”容器又插了一根网线到另一个交换机上两个交换机之间的通信不需要经过任何转发因为通信双方已经处在了同一个二三层可达范围内。还有一种连法是把 app-b 也 connect 到 net1效果一样。我一般看哪个容器的网络配置更灵活尽量少动核心服务。这个方案最大的好处是不需要改宿主机任何路由或 iptables 规则Docker 完全托管重启容器后依然有效。3.4 方案二手动配路由和 iptables理解原理但别用于生产理论上可以手动让两个 bridge 互相路由。前提是内核开启 IPv4 转发sysctl -w net.ipv4.ip_forward1。在宿主机路由表里添加去往对方网络的路由nexthop 指向对应 bridge 的 IP。在 FORWARD 链里放行两个网段之间的流量包括去掉或绕过 DOCKER-ISOLATION 那条 DROP。但你很快会发现Docker 自己的 iptables 管理逻辑会跟你“打架”Docker 服务重启、网络重建、容器重启都可能重置规则。我试过手工加规则后来 docker network 一重建规则全没了。所以这个方案我只建议用来做实验验证原理生产环境绝对别这么干。3.5 方案三通过宿主机端口访问作为临时过渡如果两个服务只是临时需要互相访问且不想改动网络拓扑可以在启动容器时做端口映射。比如 app-a 启动时加-p 8080:80app-b 里访问宿主IP:8080。这个方案在跨主机场景下很常见但在同宿主机内部用性能上多了一次 DNAT 转换而且容器 IP 变化不影响只要宿主机 IP 和端口不变就行。三个方案的取舍我整理成了下表方案优点缺点适用场景docker network connect约定俗成、容易维护、重启保留容器多一块网卡地址规划要留意生产环境优先选择手动路由 iptables不增加容器网卡原理直观Docker 规则冲突、重启失效实验验证、临时调试宿主机端口映射最通用跨宿主机也适用多一次 NAT、端口可能冲突临时互访、外部访问3.6 实验验证我实际做这个实验时一开始只用了 route 和 iptablesping 通了但 HTTP 请求偶尔被 DROP最后定位到 FORWARD 链上 Docker 规则顺序的问题花了不少时间。后来改回docker network connect一分钟解决。所以我现在遇到跨网络通信需求第一反应永远是 connect而不是去碰宿主机防火墙。4. 案例三bridge 容器与 host 网络容器的通信路径有何不同4.1 host 网络容器没有独立 netns有些容器启动时会用--network host比如docker run -itd --name app-host --network host nginx:alpine这种模式下容器不创建新的 netns直接复用宿主机网络栈。它没有自己的 eth0也没有自己的容器 IP你在容器里看到的 IP 就是宿主机的 IP。可以理解为容器进程直接跑在宿主机网络上就像普通进程一样监听端口。4.2 bridge 容器访问 host 容器其实是在访问宿主机从 bridge 容器访问 host 容器的服务本质上就是访问宿主机 IP 的某个端口。比如 app-host 里的 nginx 监听 0.0.0.0:80那么从一个 bridge 容器里curl http://宿主机IP:80就能访问到。因为 host 容器与宿主机共享 IP 地址和端口空间。这里有个最常见的坑如果服务监听的是 127.0.0.1比如nginx配置里写listen 127.0.0.1:80那只有宿主机自己和 host 网络模式的容器能访问到bridge 容器从自己的 netns 发起请求数据包到达宿主机回环接口时目标地址是 127.0.0.1宿主机接受没问题但 bridge 容器发出去的时候目标 IP 必须是宿主机的物理 IP而不是 127.0.0.1。为什么不能在那个 bridge 容器里直接访问 127.0.0.1因为 127.0.0.1 在每个 netns 里都指向自己。bridge 容器里的 127.0.0.1 是它自己不是宿主机更不是 app-host。这个知识点很重要排查“容器里访问 localhost 失败”这类问题时非常有用。4.3 host 容器访问 bridge 容器两条路反向访问就更有意思了。app-host 本身拥有宿主机的完整路由表所以它访问一个 bridge 容器 IP 是直接可达的。比如 app-b 在 demo-net 上的 IP 是 172.20.0.3那 app-host 里直接curl http://172.20.0.3:80就能访问走的是宿主机路由加 bridge 转发完全没问题。如果 bridge 容器做了端口映射比如-p 8081:80那 app-host 也可以访问127.0.0.1:8081。两条路都能通区别在于直连容器 IP 少一次 NAT更干净走映射端口则受 Docker 的 DNAT 规则管理。我在实际环境中遇到过一种组合问题一个 host 网络的服务想调用另一个 bridge 网络里的服务一开始走映射端口后来又加了docker network connect把 host 容器也连进 bridge 网络结果发现 host 网络模式根本不能和普通容器一样再被docker network connect因为 host 模式没有自己的网络设备可插入。所以如果服务注定要同时跑在 host 模式和 bridge 模式跨网络通信就别用 connect 方案老老实实用宿主机 IP 加映射端口或者干脆把 host 模式改掉。5. 手搓 netns不用 Docker从内核层面复现容器间通信5.1 两个 netns 用 veth 直连先让一个点对点互访前面讲了那么多 Docker 场景但如果你想彻底吃透“同宿主机下不同命名空间通信”我建议自己动手在纯 Linux 环境里造两个 netns不依赖 Docker。这会让 Docker 在你眼里变成一个“调用内核网络设施的壳”而不是黑盒。先做最基础的点对点连通。创建 ns1 和 ns2用一根 veth 虚拟网线直连它们# 创建两个网络命名空间 ip netns add ns1 ip netns add ns2 # 创建 veth pair两端分别叫 veth-a 和 veth-b ip link add veth-a type veth peer name veth-b # 把两端分别放进 ns1 和 ns2 ip link set veth-a netns ns1 ip link set veth-b netns ns2 # 在 ns1 里配置 IP 并启用网卡 ip netns exec ns1 ip addr add 10.200.0.1/24 dev veth-a ip netns exec ns1 ip link set veth-a up ip netns exec ns1 ip link set lo up # 在 ns2 里同样操作 ip netns exec ns2 ip addr add 10.200.0.2/24 dev veth-b ip netns exec ns2 ip link set veth-b up ip netns exec ns2 ip link set lo up # 互ping验证 ip netns exec ns1 ping 10.200.0.2这个实验能让你感受到 veth pair 的力量两个完全隔离的网络栈只靠一根“虚拟网线”连起来就能三层互通。没有 bridge没有交换机因为 veth pair 本身就是一条点对点链路。数据包从 ns1 的 veth-a 发出直接出现在 ns2 的 veth-b 上。这里要注意ip netns exec 后面跟的命令是在指定 netns 里执行的。如果你在宿主机上直接执行ip addr是看不到 ns1 里的 veth-a 的。只能通过ip netns exec ns1 ip addr查看。5.2 用 Linux bridge 让多个 netns 互通点对点太简单了稍微复杂一点就加 bridge。模拟多个容器插到同一台虚拟交换机# 创建 bridge ip link add br-test type bridge ip link set br-test up # 创建两对 veth ip link add veth1 type veth peer name veth1-p ip link add veth2 type veth peer name veth2-p # 一端进 netns一端挂 bridge ip link set veth1 netns ns1 ip link set veth2 netns ns2 ip link set veth1-p master br-test ip link set veth2-p master br-test # 启用宿主机侧 veth 端口 ip link set veth1-p up ip link set veth2-p up # 在 ns1 和 ns2 配置同网段 IP ip netns exec ns1 ip addr add 10.200.1.1/24 dev veth1 ip netns exec ns1 ip link set veth1 up ip netns exec ns2 ip addr add 10.200.1.2/24 dev veth2 ip netns exec ns2 ip link set veth2 up # 验证互通 ip netns exec ns1 ping 10.200.1.2这一次两个 netns 之间不再是点对点而是经过一个二层 bridge 互通。这和 Docker bridge 网络的行为完全一致。现在你再回头看 DockerDocker 创建一个 bridgedocker0 或 br-xxxxxxxx给容器创建 veth pair一端放进容器 netns另一端挂 bridge然后通过内置的 IPAM 给容器分配 IP。它做的事情和上面这些命令几乎一模一样只是外面包了一层 API 和数据库。5.3 手工实验如何帮你排 Docker 的错做过这个手工实验后你再遇到 Docker 网络问题思路会清晰很多。比如有人问为什么同一个 bridge 下的两个容器能互通你会知道它们其实是插在同一台“虚拟交换机”上的两台设备为什么跨 bridge 就不通因为两台设备插在不同的交换机上且交换机之间没有级联。为什么 host 模式容器既没有 eth0 也没有容器 IP因为它根本没有创建新的 netns物理网卡就是它的“eth0”。还有一个很实用的小技巧在宿主机上看到一堆 veth 网卡想搞清楚某个 veth 对应哪个容器可以看 veth 的 peer 接口索引ethtool -S vethxxxxxx | grep peer_ifindex ip link | grep -A1 peer_ifindex把 peer_ifindex 和容器里的 eth0 索引对应上就能精确配对。这个技巧在抓包排查时特别有用能帮你快速锁定“这根线”插在哪个容器上。6. 同宿主机容器互通的排错清单我每次都会按这个顺序查6.1 先搞清楚拓扑再动手查包遇到容器间网络不通我第一步不是看防火墙而是先回答这几个问题两个容器是不是同一个 bridge 网络有没有容器用了 host 网络模式目标容器的 IP 是多少服务监听的是 0.0.0.0 还是 127.0.0.1容器里有没有默认路由默认网关是不是 bridge 的 IP这些问题一回答一半以上的问题已经定位了。剩下不确定的再按下面的顺序查在源容器里 ping 目标容器 IP确认三层通不通。ping 不通时在宿主机上抓包tcpdump -i any -n icmp看请求有没有到宿主机的 bridge。如果请求到了 bridge 但没回应查目标容器自身状态比如容器内防火墙、服务监听地址。如果请求根本没到 bridge查源容器的路由表、网络配置以及它和目标容器是否同 bridge。如果三层通了但服务访问失败查目标服务监听地址和端口TCP 和 UDP 要区别对待。6.2 排查命令速查表下面是我整理的高频命令按使用频率排序命令用途说明docker inspect -f {{.NetworkSettings.Networks}} 容器名查看容器网络信息和 IP最常用nsenter -t PID -n ip addr进入容器 netns 查网卡容器内没有 ip 命令时docker exec 容器名 ip route查看容器默认路由确认网关是否正常tcpdump -i any -n host 目标IP宿主机全接口抓包快速定位丢包位置iptables -L FORWARD -n -v查看转发链规则排查 Docker 隔离规则brctl show或bridge link查看 bridge 端口对应关系确认 veth 挂在哪个 bridge 上ip netns exec ns ...手动操作非 Docker 的 netns纯手工实验时必备6.3 几条我踩过坑之后才总结出的经验第一ICMP 不通不代表 TCP 也不通。很多网络环境会屏蔽 ICMP 报文比如云厂商安全组、内部防火墙策略甚至容器镜像内配置了 sysctl 忽略 ping。我遇到过几次ping 全丢但 curl 完全正常。所以判断连通性时最好同时测 TCP 端口别只盯着 ping 的结果。第二容器里的默认网关通常是 bridge 的 IP而不是宿主机的物理网卡 IP。举个例子docker0 的 IP 是 172.17.0.1那挂在 docker0 下的容器默认网关就是 172.17.0.1。有人进容器看到网关 172.17.0.1拼命在宿主机上找这块网卡找不到因为 docker0 本身就是一个虚拟网桥设备不是物理网卡。第三自定义 bridge 的网段不要和宿主机所在局域网网段重叠。一次我图省事把容器网段配成了 192.168.50.0/24结果宿主机局域网正好也是 192.168.50.0/24容器访问局域网内其他机器时路由全乱了因为容器默认网关把目标流量当成了同网段广播。后来我把容器网段换到 172.20.0.0/24 这类私有网段问题立刻消失。如果你希望 docker0 的默认网段固定下来可以在/etc/docker/daemon.json里设置bip{ bip: 172.26.0.1/24 }第四Docker 服务重启后手工加在 iptables 里的规则可能会被重置。这不是 Docker 有意清空而是它启动时会重新生成自己的规则链。所以任何手工网络配置要想持久化要么用docker network connect走 Docker 托管要么写成开机自启脚本并放在 Docker 服务启动之后执行。第五容器内服务监听地址决定谁能访问。无论你怎么配置网络只要服务在容器里只监听 127.0.0.1那从另一个命名空间发起的请求就永远进不来。排查“其他容器访问不到我”时第一步就该确认服务监听的是 0.0.0.0 还是具体 IP。这套整理下来后我最大的体会是遇到容器网络问题别急着加规则先想清楚数据包从哪个 netns 出发、经过哪些设备、到哪个 netns 结束把链路路径画出来问题一半就解决了。最后再分享一个我常用的土办法在容器里 ping 另外一个容器同时在宿主机上用tcpdump -i any -n icmp看包如果能看到请求但看不到回应基本锁定是目标容器侧的问题如果连请求都看不到多半是二层隔离或路由没配对。这套方法我后来用在几十次排障里几乎都是几分钟内定位到问题。
返回列表