news 2026/10/14 19:00:26

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从一行 ip route 到容器跨主机通信

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

我使用 docker、k8s 已经有一些时间了,业务也几乎都使用容器部署,但对容器网络一直知之甚少,网络本身也是个较为复杂的内容。

从云计算转 Java 开发近三年,近日发现现在的实践和之前的记忆已经有些偏差了,如 Endpoint-> EndpointSlice、ingress-nginx 停止维护等等等等,于是新搭建了个 k8s 环境顺便学习下容器网络的知识。

ip rule

ip rule按照优先级决定包去哪个 table 里找路由信息

root@DESKTOP-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表(一般为空,保底用)

操作系统决定一个包发往的下一个地址的流程大致如下:

  1. 按 rule 优先级从小到大遍历ip rule。
  2. 对每条 rule,先看选择器是否匹配,比如from all、from 10.0.0.0/8、fwmark 1等。
  3. 如果匹配,就执行动作。最常见动作是lookup <table>。
  4. 去指定 table 里做路由查找:
    • 先最长前缀匹配;
    • 同前缀再比 metric 等。
  5. 如果在这张表里找到了路由,就用它,停止继续查后面的 rule。
  6. 如果在这张表里没找到路由,就继续下一条 rule。
  7. 如果动作是blackhole、unreachable、prohibit,则直接返回对应结果,不再继续。
  8. 所有 rule 都走完还没找到,就返回网络不可达。

默认情况下 linux 上只有这 3 个规则,与有多少个网卡无关。自定义规则通常会落在 0~32766 之间,从而覆盖默认行为。

ip route

ip rule的返回结果是按照优先级去哪个表里查, 如动作里的lookup main代表去查询 main 表:

root@DESKTOP-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.60

main 表是 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 信息:

root@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip addr1: lo:<LOOPBACK,UP,LOWER_UP>mtu65536qdisc 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_UP>mtu1500qdisc 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_UP>mtu1500qdisc 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>指令测试:

root@DESKTOP-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 root@DESKTOP-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 namespace

Linux 中有 NameSpace (命名空间)的技术,在许多的框架里也有体现:k8s、nacos、rabbitmq,ns 之间的内容相互隔离,那么 Network NameSpace 的作用也就显而易见了,在这个 ns 下的网络配置(网卡、路由表、ARP 表、iptables 规则)是独立的。

一个小实践:

# 添加一个 netnsroot@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns add test# 查看 netns 列表(/var//run/netns)root@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netnstest# 进入 test 的 netns 执行 ip addrroot@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test ip addr1: lo:<LOOPBACK>mtu65536qdisc noop state DOWN group default qlen1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00# 进入 test 的 netns 执行 ip routeroot@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test ip route# 删除创建的 netns(清理)root@DESKTOP-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查看信息变化):

# [主机] 新增一个 netnsroot@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns add test_veth# [主机] 新增一个 veth-test 的设备, 类型是 veth, 对端名字叫 veth-test2root@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip link add veth-test type veth peer name veth-test2# [netns] 把对端名 veth-test2 放到 test_veth 的 netns 里root@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip link set veth-test2 netns test_veth# [主机] 启动 veth-testroot@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip link set veth-test up# [netns] 启动 veth-test2root@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test_veth ip link set veth-test2 up# [主机] 给 veth-test 分配 IProot@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip addr add 10.11.13.3/24 dev veth-test# [netns] 给 veth-test2 分配 IProot@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test_veth ip addr add 10.11.14.4/24 dev veth-test2# [主机] 添加路由规则root@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip route add 10.11.14.0/24 dev veth-test# [netns] 添加路由规则root@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip netns exec test_veth ip route add 10.11.13.0/24 dev veth-test2# [主机] ping netnsroot@DESKTOP-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_seq=1ttl=64time=1.68ms64bytes from10.11.14.4:icmp_seq=2ttl=64time=0.104ms64bytes from10.11.14.4:icmp_seq=3ttl=64time=0.068ms ^C ---10.11.14.4pingstatistics ---3packets transmitted,3received,0% packet loss,time2031ms rtt min/avg/max/mdev=0.068/0.615/1.675/0.749 ms# [netns] ping 主机root@DESKTOP-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_seq=1ttl=64time=0.189ms64bytes from10.11.13.3:icmp_seq=2ttl=64time=0.096ms64bytes from10.11.13.3:icmp_seq=3ttl=64time=0.075ms ^C ---10.11.13.3pingstatistics ---3packets transmitted,3received,0% packet loss,time2043ms rtt min/avg/max/mdev=0.075/0.120/0.189/0.049 ms

可以发现,两边都能 ping 通,也就意味着打通了主机与 netns 里进程的互相通信。

# 清理root@DESKTOP-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 里。

container

container(容器)的本质其实是进程。

进程的定义非常多种多样,一个简单的定义是:进程是程序的动态执行。程序是代码或其产物(如二进制文件),静静的躺在磁盘上。而进程是程序被执行起来的样子:内存中的数据、寄存器里的值、堆栈和被打开的文件等这些数据和状态的总和就是进程。

容器通过约束和修改进程的动态表现,为其创造出一个"边界"。其核心原理一个是前面介绍过的 NameSpace 技术,除了 network ns,linux 还提供了 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 是不够的。

解决这个问题的方案分两步:

  1. 给 bridge 本身赋一个 IP,让它”接入协议栈”:能查本机路由表、接收”目的 IP 是自己”的包、能从自己的视角看邻居表。
  2. 打开/proc/sys/net/ipv4/ip_forward=1,允许内核把”目的 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这个虚拟交换机上。

root@DESKTOP-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

查看默认出口网卡:

root@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip route show defaultdefault via192.168.176.1 dev eth0 proto kernel

我这边是 eth0:

# 开启 IP 转发sysctl-wnet.ipv4.ip_forward=1# 对 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

之后就可以测试了:

root@DESKTOP-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_seq=1ttl=64time=0.051ms64bytes from10.0.0.3:icmp_seq=2ttl=64time=0.126ms64bytes from10.0.0.3:icmp_seq=3ttl=64time=0.117ms ^C ---10.0.0.3pingstatistics ---3packets transmitted,3received,0% packet loss,time2055ms rtt min/avg/max/mdev=0.051/0.098/0.126/0.033 ms
# 110.242.70.57 是我这里解析出的 www.baidu.com 对应的 IProot@DESKTOP-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_seq=1ttl=50time=15.1ms64bytes from110.242.70.57:icmp_seq=2ttl=50time=15.0ms64bytes from110.242.70.57:icmp_seq=3ttl=50time=15.2ms ^C ---110.242.70.57pingstatistics ---3packets transmitted,3received,0% packet loss,time2003ms rtt min/avg/max/mdev=14.976/15.102/15.246/0.110 ms

注:如果还是不行,可以执行iptables -I FORWARD 1 -i br0 -o br0 -j ACCEPT试试。理论上,bridge 是二层设备,转发帧应该只看 MAC,不经过 IP 层的 netfilter。但 Linux 有一个模块叫br_netfilter,Docker 会加载它并设置”同一个 bridge 内部端口到端口”的转发为 Drop,导致包被丢弃。

网络拓扑如图:

把上面所有零件拼起来,就是一次ping的完整路径:

docker0

理解了上面的内容,那么学习 docker 网络也比较简单了。

在装了 docker 的主机上执行ip addr:

root@DESKTOP-A8PBREO:/mnt/c/Users/27184/Documents# ip addr3: docker0:<NO-CARRIER,BROADCAST,MULTICAST,UP>mtu1500qdisc 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_PASSWORD=root-p3306:3306-dmysql:5.7

启动完执行ip addr会发现多了一个设备,其实这是 veth 的一端,另一端就在容器里。

如果创建一个网络:

[root@tssxserver ~]# docker network create test-veth67b844951b1eb34589a2851afe1ee3635de6d453b3c3fabf9f69167be2279ebb[root@tssxserver ~]# ip addr | grep 67b844932: br-67b844951b1e:<NO-CARRIER,BROADCAST,MULTICAST,UP>mtu1500qdisc noqueue state DOWN group default inet172.21.0.1/16 brd172.21.255.255 scope global br-67b844951b1e

会发现多了一个 br 开头的设备名。

小结

容器网络并不是黑盒,整个文章其实就讲了 4 件事:

  1. 路由决策(ip rule + ip route):内核拿到一个 IP 包,先按优先级选路由表,再在表内做最长前缀匹配,得到"下一跳 + 出接口"。
  2. Network Namespace:把网卡、路由表、iptables 隔离开,让进程拥有自己独立的网络栈;veth pair 就像一根"虚拟网线", 把两个 ns 串起来。
  3. Linux Bridge:虚拟的 L2 交换机,把"网线"的一端统一收口在一台设备上,实现 N 个 ns 之间两两通信。
  4. 三层转发 + NAT:bridge 被赋 IP + ip_forward=1 就升级成"带路由功能的小交换机";iptables MASQUERADE 把容器源 IP 翻译成 eth0 的出网 IP,容器就真正"能上网"。

Kubernetes 里的 CNI、那么多的网络实现 flannel、calico 等其实也是这些事情的不同拼装。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 18:59:51

GDAL 1.11.5 在 MinGW32 上编译实战指南

简介&#xff1a;本资源为Windows平台下预编译完成的GDAL 1.11.5库&#xff0c;专为使用MinGW工具链&#xff08;特别是Qt Creator搭配MinGW版本&#xff09;进行地理信息开发的C程序员设计&#xff0c;可显著规避源码编译耗时长、依赖复杂、环境配置易出错等痛点。压缩包共149…

作者头像 李华
网站建设 2026/10/14 18:58:08

算法 --字符串

字符串算法题&#xff0c;本质上就是“数组操作”加上“字符编码转换”以及“人类手工计算过程的代码化”。 例题剖析 题目一&#xff1a;最长公共前缀 解法一&#xff1a; class Solution { public:string longestCommonPrefix(vector<string>& strs) {if (strs.…

作者头像 李华
网站建设 2026/10/14 18:56:51

Android内存压测实战:memtester 4.3.0交叉编译与避坑指南

简介&#xff1a;memtester-4.3.0 for Android 是一份面向 Android 应用开发与系统性能调试人员的轻量级内存压力测试工具源码包&#xff0c;适合需要排查内存泄漏、数据损坏或竞争条件等问题的中高级开发者。资源共 23 个文件&#xff0c;以 sh 脚本、h 头文件与 c 源文件为主…

作者头像 李华
网站建设 2026/10/14 18:56:21

把几十篇文献读成一个体系,科迅捷AI帮你写好博士文献综述

博士论文里的文献综述&#xff0c;常常被写成一堆文献的“汇报”&#xff1a;张三研究了什么、李四研究了什么、王五研究了什么&#xff0c;一条条列下来&#xff0c;写到最后也没有自己的观点。这种写法&#xff0c;问题不在文献读得不够多&#xff0c;而在没有建立起“读文献…

作者头像 李华
网站建设 2026/10/14 18:55:47

EPLAN电气设计资源库:提升工业自动化设计效率3-5倍

1. 项目概述&#xff1a;EPLAN电气设计资源整合作为一名在自动化行业摸爬滚打12年的电气工程师&#xff0c;我完整经历了从手工绘制电路图到数字化设计的转型过程。今天要分享的是我多年积累的EPLAN设计资源库&#xff0c;包含自主开发的设备图纸模板、经过实战检验的部件库、标…

作者头像 李华
网站建设 2026/10/14 18:55:29

GMSSL 2.5.4 Windows包使用指南:SM2/SM3/SM4命令行与C接口集成

简介&#xff1a;这份资源是面向Windows平台C/C开发者的国密算法支撑库GMSSL 2.5.4编译成品&#xff0c;适合需要在项目中集成SM2、SM3、SM4等国密算法、又不想自行折腾编译环境的工程师直接取用。压缩包共110个文件&#xff0c;约5.5MB&#xff0c;其中95个.h头文件覆盖ssl、e…

作者头像 李华