资讯动态

从一行 ip route 到容器跨主机通信

发布时间:2026/10/14 19:00:30 来源:尧图企业网站定制
从一行 ip route 到容器跨主机通信我使用 docker、k8s 已经有一些时间了业务也几乎都使用容器部署但对容器网络一直知之甚少网络本身也是个较为复杂的内容。从云计算转 Java 开发近三年近日发现现在的实践和之前的记忆已经有些偏差了如 Endpoint- EndpointSlice、ingress-nginx 停止维护等等等等于是新搭建了个 k8s 环境顺便学习下容器网络的知识。ip ruleip rule按照优先级决定包去哪个 table 里找路由信息rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip rule0: from all lookuplocal32766: from all lookup main32767: from all lookup default格式为[优先级]: [匹配条件] [动作]字段含义第一列数字优先级数字越小越优先(0 最先看)from all匹配条件所有包lookup local动作查local表(本机 IP、广播地址专用)lookup main动作查main表(平时ip route看到的就是它)lookup default动作查default表(一般为空,保底用)操作系统决定一个包发往的下一个地址的流程大致如下按 rule 优先级从小到大遍历ip rule。对每条 rule先看选择器是否匹配比如from all、from 10.0.0.0/8、fwmark 1等。如果匹配就执行动作。最常见动作是lookup table。去指定 table 里做路由查找先最长前缀匹配同前缀再比 metric 等。如果在这张表里找到了路由就用它停止继续查后面的 rule。如果在这张表里没找到路由就继续下一条 rule。如果动作是blackhole、unreachable、prohibit则直接返回对应结果不再继续。所有 rule 都走完还没找到就返回网络不可达。默认情况下 linux 上只有这 3 个规则与有多少个网卡无关。自定义规则通常会落在 032766 之间从而覆盖默认行为。ip routeip rule的返回结果是按照优先级去哪个表里查, 如动作里的lookup main代表去查询 main 表rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip route show table maindefault via192.168.176.1 dev eth0 proto kernel172.17.0.0/16 dev docker0 proto kernel scopelinksrc172.17.0.1192.168.176.0/20 dev eth0 proto kernel scopelinksrc192.168.187.60main 表是 ip route 指令默认的 table, 上述指令等价于ip route字段含义default via 192.168.176.1如果没有更具体的匹配,就发给192.168.176.1这台设备172.17.0.0/16 dev docker0发往172.17.0.0/16这个网段的包,从docker0网卡出去dev docker0Docker 安装时创建的虚拟网卡,172.17.0.0/16是它的网段proto kernel这条路由是内核根据本机网卡 IP 自动生成的也可能出现proto dhcp表示 DHCP 协议自动分配来的scope link目的地跟本机是同一链路(同一网段)src 172.17.0.1出去时用172.17.0.1作为源 IP通常我们还会执行ip addr看本机的 ip 信息:rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip addr1: lo:LOOPBACK,UP,LOWER_UPmtu65536qdisc noqueue state UNKNOWN group default qlen1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet127.0.0.1/8 scopehostlo valid_lft forever preferred_lft forever inet6 ::1/128 scopehostvalid_lft forever preferred_lft forever2: eth0:BROADCAST,MULTICAST,UP,LOWER_UPmtu1500qdisc mq state UP group default qlen1000link/ether 00:15:5d:e0:2a:b7 brd ff:ff:ff:ff:ff:ff inet192.168.187.60/20 brd192.168.191.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::215:5dff:fee0:2ab7/64 scopelinkvalid_lft forever preferred_lft forever3: docker0:BROADCAST,MULTICAST,UP,LOWER_UPmtu1500qdisc noqueue state UP group default link/ether46:61:fc:1c:93:8b brd ff:ff:ff:ff:ff:ff inet172.17.0.1/16 brd172.17.255.255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::4461:fcff:fe1c:938b/64 scopelinkvalid_lft forever preferred_lft forever可以看到返回值有eth0和docker0这也是上面ip route返回值表示的网卡名。到这里我们就知道发往某个 ip 的包该走哪个路由了可以用ip route get ip指令测试rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip route get 8.8.8.88.8.8.8 via192.168.176.1 dev eth0 src192.168.187.60 uid0cache rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip route get 172.17.0.3172.17.0.3 dev docker0 src172.17.0.1 uid0cache由于 local 表一般转发一些本机127.0…、广播地址所以测试的这些 IP 会走 main 表根据路由规则8.8.8.8 不满足docker0 172.17.0.0/16和eth0 192.168.176.0/20的规则不在本机任何直连网段下一跳会走 default 规则从eth0发给网关192.168.176.1出去时用192.168.187.60作为源 IP。172.17.0.3 满足docker0 172.17.0.0/16的规则在本机直连网络中直接从docker0发出172.17.0.1作为源 IP。注意下面比上面少了via 192.168.176.1的部分原因就是172.17.0.3在系统直连网络内主机不经过任何路由器直接通过 ARP 拿到目标 MAC把帧从对应接口发出去。有via则是目标不在本地链路把包交给网关让网关继续转发。network namespaceLinux 中有 NameSpace 命名空间的技术在许多的框架里也有体现k8s、nacos、rabbitmqns 之间的内容相互隔离那么 Network NameSpace 的作用也就显而易见了在这个 ns 下的网络配置网卡、路由表、ARP 表、iptables 规则是独立的。一个小实践# 添加一个 netnsrootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns add test# 查看 netns 列表(/var//run/netns)rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netnstest# 进入 test 的 netns 执行 ip addrrootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test ip addr1: lo:LOOPBACKmtu65536qdisc noop state DOWN group default qlen1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00# 进入 test 的 netns 执行 ip routerootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test ip route# 删除创建的 netns(清理)rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns del test通过exec test netns的输出可知netns 里看不到主机的网卡、路由规则。本机执行相关指令也看不到 netns 内的网络设备两边的世界是隔离的。那双方怎么通信呢veth pair解决方案是veth pair(虚拟以太网对)其特点如下有两个端点数据从一端进入一定从另一端出来两端可以被各自赋 IP、参与路由那么主机与 netns 内进程通信的解决方案就出来了为 netns 创建一个 veth pair一头在 netns 里一头在主机上这样 netns 就可以跟外界通信了。netns 创建时lo设备默认是 DOWN 状态可以回看上文的ip netns exec test ip addr输出。如果要在 netns 内跑需要本机回环才能正常工作的服务比如很多 health 检测脚本会curl 127.0.0.1记得手动ip netns exec ns ip link set lo up。veth pair 实践实践过程中可以间断执行ip addr查看信息变化# [主机] 新增一个 netnsrootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns add test_veth# [主机] 新增一个 veth-test 的设备, 类型是 veth, 对端名字叫 veth-test2rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip link add veth-test type veth peer name veth-test2# [netns] 把对端名 veth-test2 放到 test_veth 的 netns 里rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip link set veth-test2 netns test_veth# [主机] 启动 veth-testrootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip link set veth-test up# [netns] 启动 veth-test2rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test_veth ip link set veth-test2 up# [主机] 给 veth-test 分配 IProotDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip addr add 10.11.13.3/24 dev veth-test# [netns] 给 veth-test2 分配 IProotDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test_veth ip addr add 10.11.14.4/24 dev veth-test2# [主机] 添加路由规则rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip route add 10.11.14.0/24 dev veth-test# [netns] 添加路由规则rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test_veth ip route add 10.11.13.0/24 dev veth-test2# [主机] ping netnsrootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ping 10.11.14.4PING10.11.14.4(10.11.14.4)56(84)bytes of data.64bytes from10.11.14.4:icmp_seq1ttl64time1.68ms64bytes from10.11.14.4:icmp_seq2ttl64time0.104ms64bytes from10.11.14.4:icmp_seq3ttl64time0.068ms ^C ---10.11.14.4pingstatistics ---3packets transmitted,3received,0% packet loss,time2031ms rtt min/avg/max/mdev0.068/0.615/1.675/0.749 ms# [netns] ping 主机rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test_veth ping 10.11.13.3PING10.11.13.3(10.11.13.3)56(84)bytes of data.64bytes from10.11.13.3:icmp_seq1ttl64time0.189ms64bytes from10.11.13.3:icmp_seq2ttl64time0.096ms64bytes from10.11.13.3:icmp_seq3ttl64time0.075ms ^C ---10.11.13.3pingstatistics ---3packets transmitted,3received,0% packet loss,time2043ms rtt min/avg/max/mdev0.075/0.120/0.189/0.049 ms可以发现两边都能 ping 通也就意味着打通了主机与 netns 里进程的互相通信。# 清理rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns del test_veth网络拓扑如图注ip link add veth-test type veth peer name veth-test2这个指令执行完veth 的两端都在主机上在后续给某一端 set veth-name netns ns 时才把 veth-name 这个端点移动到 ns 里。containercontainer容器的本质其实是进程。进程的定义非常多种多样一个简单的定义是进程是程序的动态执行。程序是代码或其产物如二进制文件静静的躺在磁盘上。而进程是程序被执行起来的样子内存中的数据、寄存器里的值、堆栈和被打开的文件等这些数据和状态的总和就是进程。容器通过约束和修改进程的动态表现为其创造出一个边界。其核心原理一个是前面介绍过的 NameSpace 技术除了 network nslinux 还提供了 pid、mount、uts、user 多种 ns这是 linux 创建新进程的一个可选参数另一个核心原理则是 Cgroups既然容器是一个进程自然会与系统的任何其他进程一起争抢资源Cgroups 提供了限制进程能使用的资源cpu、mem、blkio上限的能力。所以一个进程运行在一个 netns 里前面的内容实现了 netns 与主机的通信也就是只要为每一个容器创建一个 veth 设备容器与主机就可以通信了。那么容器与容器之间如何通信呢一个简单的方案是如果容器A需要与容器B通信那么容器A和容器B间也添加一个 veth 设备仔细想一下其实问题很多如果互相通信的容器有 N个那么需要 N(N-1)/2 个 veth 设备根本不可行。现实生活中只要我们的设备连同一个路由器互相之间就能通信这里也是同理。bridge首先介绍一下交换机和路由器交换机是二层设备工作在数据链路层会根据收到包的 MAC 地址转发给对应的主机路由器是三层设备工作在网络层能根据收到包的 IP 地址决定从哪个网卡转发出去。bridge 就是让所有 veth 都接到一个虚拟交换机上这样互相之间就可以通信了N 个容器只需要 N 个 veth。大名鼎鼎的 docker0 就是一个 bridge。严格地说是一台二层交换机——它只会按 MAC 学习、转发同网段内的帧并不会在不同 IP 网段之间路由包。所以光有 bridge 是不够的。解决这个问题的方案分两步给 bridge 本身赋一个 IP让它”接入协议栈”能查本机路由表、接收”目的 IP 是自己”的包、能从自己的视角看邻居表。打开/proc/sys/net/ipv4/ip_forward1允许内核把”目的 IP 不是自己”的包继续向外转发。只有这两步都做了bridge 才同时是一台 L2 交换机和一台 L3 路由器——容器之间同网段通信走 L2容器对外网通信走 L3 转发。接下来是实践创建 red、blue 两个 netns每个 netns 对应一个 veth最终效果red 和 blue 可以互相 ping 通并能访问外网ping www.baidu.com# 创建两个 netnsipnetnsaddredipnetnsaddblue# 创建 bridge并给它分配网关 IPiplinkaddbr0typebridgeiplinksetbr0 upipaddradd10.0.0.1/24 dev br0# 为 red 创建 veth pairiplinkaddveth-redtypeveth peer name veth-red-briplinksetveth-red netns rediplinksetveth-red-br master br0iplinksetveth-red-br up# 为 blue 创建 veth pairiplinkaddveth-bluetypeveth peer name veth-blue-briplinksetveth-blue netns blueiplinksetveth-blue-br master br0iplinksetveth-blue-br up此时veth-red-br和veth-blue-br都插在br0这个虚拟交换机上。rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# brctl showbridge name bridgeidSTP enabled interfaces br08000.9ee0a443c089 no veth-blue-br veth-red-br配置 netns 内的接口和路由:# 启动 red 里的 veth并分配 IPipnetnsexecrediplinksetveth-red upipnetnsexecredipaddradd10.0.0.2/24 dev veth-redipnetnsexecrediprouteadddefault via10.0.0.1# 启动 blue 里的 veth并分配 IPipnetnsexecblueiplinksetveth-blue upipnetnsexecblueipaddradd10.0.0.3/24 dev veth-blueipnetnsexecblueiprouteadddefault via10.0.0.1查看默认出口网卡rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip route show defaultdefault via192.168.176.1 dev eth0 proto kernel我这边是 eth0# 开启 IP 转发sysctl-wnet.ipv4.ip_forward1# 对 10.0.0.0/24 做 MASQUERADE从 eth0 出去iptables-tnat-APOSTROUTING-s10.0.0.0/24-oeth0-jMASQUERADE# 如果 FORWARD 链默认是 DROP放行转发流量iptables-IFORWARD1-ibr0-oeth0-s10.0.0.0/24-jACCEPT iptables-IFORWARD1-ieth0-obr0-d10.0.0.0/24-mconntrack--ctstateRELATED,ESTABLISHED-jACCEPT之后就可以测试了rootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec red ping 10.0.0.3PING10.0.0.3(10.0.0.3)56(84)bytes of data.64bytes from10.0.0.3:icmp_seq1ttl64time0.051ms64bytes from10.0.0.3:icmp_seq2ttl64time0.126ms64bytes from10.0.0.3:icmp_seq3ttl64time0.117ms ^C ---10.0.0.3pingstatistics ---3packets transmitted,3received,0% packet loss,time2055ms rtt min/avg/max/mdev0.051/0.098/0.126/0.033 ms# 110.242.70.57 是我这里解析出的 www.baidu.com 对应的 IProotDESKTOP-A8PBREO:/mnt/c/Users/14184/Documents# ip netns exec red ping 110.242.70.57PING110.242.70.57(110.242.70.57)56(84)bytes of data.64bytes from110.242.70.57:icmp_seq1ttl50time15.1ms64bytes from110.242.70.57:icmp_seq2ttl50time15.0ms64bytes from110.242.70.57:icmp_seq3ttl50time15.2ms ^C ---110.242.70.57pingstatistics ---3packets transmitted,3received,0% packet loss,time2003ms rtt min/avg/max/mdev14.976/15.102/15.246/0.110 ms注如果还是不行可以执行iptables -I FORWARD 1 -i br0 -o br0 -j ACCEPT试试。理论上bridge 是二层设备转发帧应该只看 MAC不经过 IP 层的 netfilter。但 Linux 有一个模块叫br_netfilterDocker 会加载它并设置”同一个 bridge 内部端口到端口”的转发为 Drop导致包被丢弃。网络拓扑如图把上面所有零件拼起来就是一次ping的完整路径docker0理解了上面的内容那么学习 docker 网络也比较简单了。在装了 docker 的主机上执行ip addrrootDESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip addr3: docker0:NO-CARRIER,BROADCAST,MULTICAST,UPmtu1500qdisc noqueue state DOWN group default link/ether46:61:fc:1c:93:8b brd ff:ff:ff:ff:ff:ff inet172.17.0.1/16 brd172.17.255.255 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::4461:fcff:fe1c:938b/64 scopelinkvalid_lft forever preferred_lft forever会找到这个叫docker0的网桥它就是实现容器间通信、容器对外通信的核心。启动一个容器dockerrun--namemysql-eMYSQL_ROOT_PASSWORDroot-p3306:3306-dmysql:5.7启动完执行ip addr会发现多了一个设备其实这是 veth 的一端另一端就在容器里。如果创建一个网络:[roottssxserver ~]# docker network create test-veth67b844951b1eb34589a2851afe1ee3635de6d453b3c3fabf9f69167be2279ebb[roottssxserver ~]# ip addr | grep 67b844932: br-67b844951b1e:NO-CARRIER,BROADCAST,MULTICAST,UPmtu1500qdisc noqueue state DOWN group default inet172.21.0.1/16 brd172.21.255.255 scope global br-67b844951b1e会发现多了一个 br 开头的设备名。小结容器网络并不是黑盒整个文章其实就讲了 4 件事:路由决策(ip rule ip route)内核拿到一个 IP 包,先按优先级选路由表,再在表内做最长前缀匹配,得到下一跳 出接口。Network Namespace把网卡、路由表、iptables 隔离开让进程拥有自己独立的网络栈veth pair 就像一根虚拟网线, 把两个 ns 串起来。Linux Bridge虚拟的 L2 交换机把网线的一端统一收口在一台设备上实现 N 个 ns 之间两两通信。三层转发 NATbridge 被赋 IP ip_forward1 就升级成带路由功能的小交换机iptables MASQUERADE 把容器源 IP 翻译成 eth0 的出网 IP容器就真正能上网。Kubernetes 里的 CNI、那么多的网络实现 flannel、calico 等其实也是这些事情的不同拼装。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑