Linux-35 虚拟网络:容器为什么能通
上一篇从应用视角拆解 socket:一个 fd 如何经历 bind、listen、accept、connect 和收发。这一篇把进程放进容器后再追问:容器没有独占一块真实网卡,为什么却有自己的 eth0、IP、路由表和 localhost,还能访问互联网并被宿主机端口转发?
答案不是“Docker 创建了一张虚拟网卡”这么简单。典型容器网络由多个原本独立的 Linux 机制拼成:network namespace 隔离网络视图,veth pair 跨 namespace 搬运 frame,bridge 完成二层交换,路由决定三层下一跳,netfilter/NAT 改写地址,conntrack 记住双向流状态。容器运行时与 CNI 只是配置这些内核对象的控制面。
先看五个问题:
- network namespace 隔离了哪些网络对象?为什么容器内的
127.0.0.1不是宿主机? - veth pair 如何把两个 namespace 接起来?为什么一端收到的包像从另一端“发出”?
- Linux bridge 与物理交换机有什么相似和不同?同宿主机两个容器通信经过哪些对象?
- 容器私有地址如何访问公网?DNAT、SNAT/MASQUERADE 和 conntrack 各自承担什么?
- 容器能 ping 通却访问不了服务时,应怎样区分监听地址、路由、策略、MTU、DNS、端口映射和应用问题?
1. 容器网络不是模拟出来的另一套 TCP/IP
1.1 容器仍使用宿主机内核
虚拟机通常有独立 guest kernel;普通 Linux 容器只是被 namespace、cgroup、capability、mount 等机制约束的进程。容器里的 socket()、TCP 拥塞控制、路由和 netfilter 仍由宿主机同一个内核执行。
host process ----+
container A -----+--> same Linux kernel --> physical NIC
container B -----+
不同的是:每个进程查网络对象时,被导向所属 network namespace
因此容器网络性能通常不需要完整硬件虚拟化,但内核协议栈、softirq、conntrack 表和物理网卡仍是共享资源。一个容器制造大量小包或连接,可能影响同机其他容器。
1.2 控制面先创建对象,数据面再逐包转发
容器运行时启动容器时执行的是控制面工作:
创建 network namespace
创建 veth pair
把一端移入容器 namespace
给接口配置 IP / MTU / up
把宿主机端接入 bridge
配置容器默认路由
配置转发、防火墙、NAT 或 CNI 规则
之后每个包走内核数据面,不需要 Docker daemon 或 kubelet 逐包参与。daemon 重启不必然让已有连接立刻中断,因为真正的接口、路由、socket 和 conntrack 状态在内核中;但后续配置协调可能受影响。
1.3 容器网络模型不只一种
常见模式包括:
- bridge + veth:单机容器最经典模型
- host network:进程直接加入宿主机 netns,少一层虚拟转发
- routed CNI:为 Pod 网段发布路由,不一定做 SNAT
- overlay:VXLAN/Geneve 等把容器包封装跨主机传输
- ipvlan/macvlan:以不同方式复用物理下层接口
- eBPF datapath:用 BPF 程序替代或加速部分 bridge/netfilter 路径
本文先讲 bridge + veth + route + NAT 的基线。理解这些对象后,overlay 和 eBPF 只是改变中间转发实现,不改变“namespace、接口、地址、路由、策略、socket”这些排查维度。
2. Network namespace:复制一份网络世界的视图
2.1 netns 隔离哪些对象
一个 network namespace 拥有相对独立的:
network interfaces
IP addresses
routing tables and policy rules
ARP/neighbor tables
TCP/UDP sockets and local ports
netfilter rules and many network sysctls
loopback device
/proc/net view(随查看进程 namespace)
所以两个不同 netns 中的进程都可以绑定 0.0.0.0:8080,因为它们不在同一个本地 socket 命名空间。容器端口冲突通常发生在映射到同一宿主机端口时,而不是容器内部端口相同。
2.2 localhost 只在当前 namespace 回环
每个 netns 有自己的 lo。容器访问 127.0.0.1:8080,目标是同一容器 netns 内监听 8080 的 socket,不会穿过 veth 去宿主机。
host netns: 127.0.0.1 -> host lo -> host sockets
container netns: 127.0.0.1 -> container lo -> container sockets
这解释了两个常见现象:
- 宿主机上的数据库只监听
127.0.0.1,容器不能通过“localhost”访问它 - 容器应用只监听
127.0.0.1,宿主机端口映射把包送进容器后也找不到对应非回环入口
容器服务通常监听 0.0.0.0、:: 或容器 eth0 地址;是否应向所有接口开放,还要由 namespace、网络策略和认证共同控制。
2.3 如何手工观察 namespace
# 当前可见的命名 netns
ip netns list
# 查看某个 netns 的接口、地址和路由
ip -n demo link
ip -n demo addr
ip -n demo route
# 在 namespace 内运行命令
ip netns exec demo ss -ltnp
ip netns exec demo ping -c 1 10.0.0.1
容器运行时创建的 netns 不一定注册在 /var/run/netns,所以 ip netns list 可能为空。只要知道容器进程 PID,仍可进入:
sudo nsenter -t "$PID" -n ip addr
sudo nsenter -t "$PID" -n ip route
sudo nsenter -t "$PID" -n ss -ltnp
namespace 是内核对象;命名文件通常只是指向它的引用,不是 namespace 本身。
2.4 netns 生命周期依赖引用
创建 netns 后,只要仍有进程、打开 fd 或 bind mount 引用,它就不会被销毁。容器退出却残留 namespace、veth 或规则,往往意味着 runtime 清理失败或仍有引用。反之,最后引用消失时,内核会清理其接口和 socket;veth peer 也会感知对端消失。
3. veth pair:一根只有两端的虚拟网线
3.1 创建与移动
veth 总是成对出现:
ip link add veth-host type veth peer name veth-container
ip link set veth-container netns demo
ip link set veth-host up
ip -n demo link set veth-container name eth0
ip -n demo link set eth0 up
从一端发送的 Ethernet frame 会作为另一端的接收流量进入其网络栈:
container eth0 TX
-> veth peer crossing
-> host veth-host RX
host veth-host TX
-> veth peer crossing
-> container eth0 RX
它不是共享内存中的一个 IP tunnel,也不自动提供路由、地址或 NAT。若只创建 veth 不配置 IP/bridge/route,两端只是有一条二层连接。
3.2 为什么宿主机看见的是 RX
容器从 eth0 发包时,在容器接口统计里算 TX;包穿到 peer 后,在宿主机 veth 端表现为 RX。抓包时方向常让人困惑:
# 宿主机看 veth peer
sudo tcpdump -ni veth-host
# 容器 namespace 看 eth0
sudo nsenter -t "$PID" -n tcpdump -ni eth0
两处可能看到同一 frame 的不同接口视角。GRO、checksum offload 和抓包 hook 位置还可能让 checksum 看似错误或包大小不同;不要仅凭单点 tcpdump 判定线上 wire 数据损坏。
3.3 veth 有队列和 MTU,不是零成本
包穿过 veth 仍要经历 skb、qdisc/队列、namespace 切换、bridge 或路由、netfilter hooks。它比完整虚拟机设备轻量,却不是免费函数调用。
两端 MTU 应一致并与后续路径兼容。容器 veth 配 1500,而 overlay 再添加 VXLAN/UDP/IP 头时,底层物理网络若仍只支持 1500,就需要降低容器 MTU或依赖分片/PMTU。配置错误常表现为小包正常、大包或 TLS 握手卡住。
3.4 查找 peer 关系
ip -d link show veth-host
ip -n demo -d link show eth0
ethtool -S veth-host 2>/dev/null
ip -d link 可显示 link-netnsid、peer index 等线索,但接口名可能被 runtime 重命名。排查应记录 ifindex、namespace inode 和 peer ifindex,而不只依赖 vethabc123 这类临时名字。
4. Linux bridge:软件二层交换机
4.1 bridge 学习 MAC 到端口的映射
把多个宿主机 veth peer 接入 bridge:
ip link add br0 type bridge
ip link set br0 up
ip link set veth-a master br0
ip link set veth-b master br0
bridge 根据源 MAC 学习转发表 FDB(Forwarding Database),再按目的 MAC 决定从哪个端口转发:
container A eth0 -- veth-a --+
+-- br0 -- host / uplink
container B eth0 -- veth-b --+
FDB:
MAC_A -> veth-a
MAC_B -> veth-b
目的 MAC 已知时只转发到相应端口;未知单播、广播和某些组播会 flood 到适用端口。它与物理交换机的学习逻辑相似,只是运行在 Linux 内核中。
4.2 bridge 自己是否需要 IP
二层转发本身不要求 bridge 有 IP。若宿主机要作为容器默认网关、参与该子网三层通信,通常把网关地址配置在 bridge 设备而不是各个 enslaved veth:
ip addr add 172.18.0.1/16 dev br0
容器配置:
eth0: 172.18.0.2/16
default via 172.18.0.1 dev eth0
容器先用 ARP/neighbor discovery 得到网关 MAC,再把非本子网包交给网关。bridge 负责把 frame 送到拥有该 MAC 的本机三层接口,之后宿主机路由继续处理。
4.3 同宿主机容器通信
A 访问同子网 B:
A 判断 172.18.0.3 在本地 /16
-> ARP: who has 172.18.0.3?
-> B 回 MAC_B
-> A 发 frame dst=MAC_B
-> A eth0 -> veth-a -> br0 FDB -> veth-b -> B eth0
-> B IP/TCP/socket
正常情况下不需要经过宿主机三层路由或 SNAT。是否经过 netfilter bridge hooks、网络策略或 eBPF 程序取决于系统配置,不能仅凭拓扑断言“同机容器绝不经过防火墙”。
4.4 bridge 与交换回路
多个 bridge/uplink 错误连接可能形成二层环路,广播被反复复制。Linux bridge 支持 STP 等机制,但容器运行时通常构造可控的星形拓扑。手工接桥时不要随意把多个物理/虚拟端口互连;二层 loop 可以迅速制造广播风暴,属于破坏性操作。
5. 路由:每个 namespace 都要回答“下一跳是谁”
5.1 路由在发送 socket 之后决定出口
容器应用连接 8.8.8.8:443,内核在容器 netns 查路由:
ip -n demo route get 8.8.8.8
典型结果:
8.8.8.8 via 172.18.0.1 dev eth0 src 172.18.0.2
这决定源地址、出口接口和下一跳。包经 veth/bridge 到宿主机后,宿主机又在自己的 netns 做一次路由查找,决定从物理网卡发出。
container route: destination -> eth0 -> gateway 172.18.0.1
host route: destination -> physical NIC -> upstream gateway
5.2 IP forwarding 是路由器开关
宿主机收到一个目的不是自己的 IP 包,是否允许在接口间转发由 forwarding 与策略共同决定:
sysctl net.ipv4.ip_forward
值为 1 只是允许进入转发路径,不代表防火墙一定放行,也不代表有正确回程路由。容器出网需要:
- 容器默认路由正确
- bridge/veth 正常
- 宿主机 forwarding 允许
- FORWARD 策略允许
- 上游知道容器网段回程,或宿主机做 SNAT
- DNS/目标服务本身可用
5.3 最长前缀匹配与策略路由
路由表选择最具体前缀;默认路由只是没有更具体项时的兜底。源地址、fwmark、入接口等还可通过 ip rule 选择不同路由表:
ip rule show
ip route show table all
ip route get 8.8.8.8 from 172.18.0.2 iif br0
VPN、多网卡、Kubernetes CNI、云厂商路由经常使用 policy routing。只看 ip route 主表可能漏掉真正决策。
5.4 回程路由同样重要
正向包能到目的地,不代表响应知道如何回来。若上游路由器不知道 172.18.0.0/16 在本宿主机后面,响应会丢失;SNAT 把源改成宿主机可路由地址,正是常见容器出网方案。
非对称路由还可能触发 reverse path filtering、状态防火墙或云网络策略。抓包应同时观察请求和响应,不要看到 SYN 从物理网卡发出就认定链路完整。
6. NAT:让私有地址借用可路由身份
6.1 容器出网的 SNAT/MASQUERADE
容器包原始四元组:
172.18.0.2:41000 -> 203.0.113.20:443
公网通常不知道如何路由回私有容器网段。宿主机在出接口做 SNAT:
192.0.2.10:53001 -> 203.0.113.20:443
响应回到 192.0.2.10:53001 后,conntrack/NAT 反向还原为 172.18.0.2:41000,再路由回 veth。
MASQUERADE 是适合出口地址可能变化的源 NAT 形式,会动态使用出口接口地址;固定地址场景可用明确 SNAT。它们都不是加密或访问控制,只是地址/端口改写。
6.2 宿主机端口映射的 DNAT
用户访问宿主机 192.0.2.10:8080,规则可改写目的:
before DNAT:
client 198.51.100.8:52000 -> host 192.0.2.10:8080
after DNAT:
client 198.51.100.8:52000 -> container 172.18.0.2:80
路由随后把包送到 bridge/veth。响应方向由 conntrack 自动执行反向转换,让客户端仍认为它在与宿主机 :8080 通信。
不同运行时/版本可能使用 iptables、nftables、IPVS、eBPF 或用户态 proxy 完成映射。排查时应先确认当前实现,不能只 grep 一张假定存在的 iptables -t nat 表。
6.3 PREROUTING 与 OUTPUT 的视角差异
外部进入宿主机的包通常经过 PREROUTING;宿主机本地进程访问宿主机映射地址,包可能从 OUTPUT 路径开始。若规则只覆盖外部入口,本机 curl host-ip:published-port 可能与外部行为不同。
external client -> NIC -> PREROUTING -> DNAT -> FORWARD
host process -> local OUTPUT -----> DNAT -> route/forward
容器访问同一宿主机映射端口还涉及 hairpin NAT。成熟 runtime 会配置相应规则或 proxy,但自建规则常漏掉这些路径。
6.4 NAT 不是代理
内核 NAT 通常只改包头和状态映射,不终止 TCP 再创建另一条应用连接;因此它看不到 HTTP path,也不能像 L7 reverse proxy 那样做证书、header、重试或内容路由。代理会有两条 socket 连接,NAT 仍是一条端到端 TCP 状态经地址翻译呈现。
7. Conntrack:NAT 为什么记得响应该怎么改
7.1 状态表保存双向 tuple
连接跟踪为流记录 original/reply 方向、协议状态、超时和 NAT 信息。概念上:
original:
172.18.0.2:41000 -> 203.0.113.20:443
translated:
192.0.2.10:53001 -> 203.0.113.20:443
reply:
203.0.113.20:443 -> 192.0.2.10:53001
restore:
203.0.113.20:443 -> 172.18.0.2:41000
后续包不必重新任意选择 NAT 映射,从而保持一条连接双向一致。状态防火墙也可依据 NEW/ESTABLISHED/RELATED/INVALID 分类包。
7.2 conntrack 表是共享有限资源
每条被跟踪的连接占内存和哈希表项。大量短连接、UDP flow、扫描或攻击会让表接近上限:
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
表满时新连接可能被丢弃,并出现内核日志。简单调大 max 会增加内存与查找压力;应先找连接来源、超时、重试风暴和是否所有流都必须跟踪。绕过 conntrack 会影响 NAT/状态策略,必须由网络架构验证,不能作为临时万能修复。
7.3 TCP 状态与 conntrack 状态不是同一对象
TCP socket 状态属于端点协议栈;conntrack 是中间网络路径对流的观察。转发宿主机可能没有该连接的 TCP socket,却有 ESTABLISHED conntrack entry。两套 timeout 和状态机也不完全相同。
所以:
ss 看不到连接 != conntrack 没有记录
conntrack ESTABLISHED != 两端应用都健康
排查 NAT 路径应联合 socket 与 conntrack,而不是用其中一个替代另一个。
7.4 查看连接跟踪
# 需要 conntrack 工具及相应权限
sudo conntrack -S
sudo conntrack -L -p tcp 2>/dev/null | head
# 内核参数与当前数量
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
生产表可能很大,完整 conntrack -L 会产生开销和大量输出。应按协议、地址、端口过滤,并优先看计数/事件采样。
8. 一次容器出网的完整旅程
假设:
container: 172.18.0.2/16, gateway 172.18.0.1
host br0: 172.18.0.1/16
host eth0: 192.0.2.10
remote: 203.0.113.20:443
8.1 正向
1. Go Dial 203.0.113.20:443
2. 容器路由选择 default via 172.18.0.1 dev eth0
3. ARP 得到网关 br0 的 MAC
4. frame 从容器 eth0 进入 veth peer
5. bridge 将 frame 交给宿主机网关/三层
6. host FORWARD 路径做策略检查
7. host 路由选择 eth0
8. POSTROUTING 做 MASQUERADE/SNAT
9. physical NIC 发出
8.2 回程
1. remote 响应到 192.0.2.10:映射端口
2. host conntrack 找到已有 flow
3. 反向 NAT 恢复目的 172.18.0.2:原端口
4. host 路由选择 br0/veth
5. bridge 按 MAC/FDB 送到容器 peer
6. 容器 IP/TCP 找到 socket receive queue
7. Go netpoller 唤醒 goroutine
链路任一处都可能丢包。最有效的定位方法是在关键接口同时抓同一四元组,找到“最后看见请求”和“最先缺少响应”的边界。
8.3 不要一上来全接口抓全部流量
# 示例:按目标和端口过滤,先在容器与宿主机出口分别观察
sudo nsenter -t "$PID" -n tcpdump -ni any 'host 203.0.113.20 and tcp port 443'
sudo tcpdump -ni eth0 'host 203.0.113.20 and tcp port 443'
-i any 的链路层呈现、重复包和方向与具体接口抓包不同;高流量生产环境全量抓包会消耗 CPU、磁盘并包含敏感数据。使用窄过滤、短持续时间、snaplen 和授权的数据处理流程。
9. 容器入站与端口发布
9.1 外部访问 published port
client -> host eth0:8080
-> DNAT destination to 172.18.0.2:80
-> route/FORWARD
-> bridge -> veth -> container eth0
-> TCP lookup container socket :80
要成功必须同时满足:
- 宿主机
:8080映射规则存在且命中正确地址族/接口 - forwarding 与策略允许
- 容器地址和路由有效
- 容器应用监听
0.0.0.0:80、[::]:80或eth0地址 - 应用没有只绑定
127.0.0.1:80 - 容器/节点网络策略和安全组放行
9.2 端口发布不改变容器监听端口
-p 8080:80 的含义是宿主机 8080 转到容器 80。容器内 ss -ltn 仍应看见 :80,不需要应用改监听 8080:
host port 8080 -- translation --> container port 80
把两者混淆会出现映射规则正常、容器里却没有目标 socket 的 connection refused。
9.3 来源地址可能保留也可能变化
纯 DNAT 转发通常能让容器看到真实客户端源地址;hairpin、用户态 proxy、service mesh、云负载均衡或额外 SNAT 可能让来源变成节点/代理地址。应用要获得真实客户端身份时,应明确可信代理链和 PROXY protocol / Forwarded header,不能无条件信任客户端自己提交的 X-Forwarded-For。
9.4 host network 的取舍
host network 让容器进程直接使用宿主机 netns:
优点:少 veth/bridge/NAT 层,端口与观测更直接
代价:端口冲突;网络隔离减弱;容器可见宿主机接口和 localhost;策略边界改变
它不是简单“网络更快”开关。是否值得取决于 pps、延迟、隔离、安全模型和编排能力。
10. MTU、分片与“只有大请求失败”
10.1 封装会吃掉 MTU
物理网络 MTU 1500,VXLAN 还要添加外层 Ethernet/IP/UDP/VXLAN header,可留给内层包的空间变小。若容器仍发送 1500-byte IP packet,外层可能超过链路 MTU。
inner packet 1500
+ overlay headers ~50
= outer packet ~1550 > physical MTU 1500
解决通常是把 Pod/veth MTU 设为 1450 左右(精确值依封装/地址族),或确保底层支持更大 MTU并全路径一致。
10.2 PMTUD 为什么会黑洞
Path MTU Discovery 依赖路由器在包过大且不能分片时返回 ICMP Packet Too Big/Fragmentation Needed。若防火墙错误丢弃这些 ICMP,发送端不知道应降低 packet size:
小 SYN/ACK 正常
小 HTTP request 正常
TLS certificate / 大 response 卡住
重传但始终失败
这类 MTU black hole 很像应用超时。不要把“禁所有 ICMP”当安全最佳实践;必要的 ICMP 是 IP 正常工作的控制面。
10.3 如何验证 MTU 线索
ip link show
ip route get 203.0.113.20
tracepath 203.0.113.20
# IPv4 探测:禁止分片并逐步调整 payload;选项依 ping 实现
ping -M do -s 1400 -c 2 203.0.113.20
ICMP 可能被目标限制,因此 ping 失败本身不是定论。结合 tcpdump 看重传、ICMP too-big 与 TCP MSS;也要检查 veth、bridge、tunnel、物理接口每一层 MTU。
10.4 MSS clamping 只是特定补救
在转发入口调整 TCP MSS 可避免 TCP 建连后发送超过路径能力的 segment,但它不修复 UDP,也不能替代一致 MTU 和正确 ICMP。将 MSS 设得过小会增加包数和 CPU。应先确认 black hole,再针对边界设置并记录原因。
11. DNS:能访问 IP,不代表网络已经正常
11.1 容器有自己的 resolv.conf 视图
容器内 /etc/resolv.conf 可能指向:
- 宿主机/局域网 DNS
- runtime 内置 DNS stub
- Kubernetes CoreDNS service IP
- systemd-resolved stub 的特殊转发方案
cat /etc/resolv.conf
getent ahosts example.com
容器内指向 127.0.0.53 只有在同一 netns 确实有 resolver 监听才有效;宿主机 loopback 上的 systemd-resolved 不会自动出现在容器 localhost。
11.2 DNS 同时使用 UDP、TCP 和搜索规则
常见查询先走 UDP,响应截断或特定场景需 TCP;只放行 UDP/53 会产生偶发失败。search 和 ndots 还可能让短名字触发多个候选查询,造成延迟和 CoreDNS 压力。
应用报告 dial tcp: lookup ... 时,连接甚至还没进入目标 TCP connect。应单独记录 DNS duration、结果、resolver 与错误类别。
11.3 Go resolver 路径可能不同
Go 可使用纯 Go resolver 或 cgo/system resolver,选择受构建、平台、nsswitch.conf、环境和 Go 版本影响。容器镜像缺少 CA 是 TLS 问题,缺少 nsswitch.conf 或 DNS 配置则是解析路径问题;两者经常都被误称为“容器没网”。
可用应用相同镜像、相同 netns 和相同用户做测试,避免宿主机 curl 成功就推断容器进程也能解析。
12. 网络策略与安全边界
12.1 可路由不等于被允许
路由回答“下一跳在哪里”,防火墙/网络策略回答“这个包是否允许”。常见 hook 包括本机 INPUT/OUTPUT、转发 FORWARD、bridge netfilter、CNI eBPF hooks 和云安全组。
route exists + neighbor resolves
仍可能在 policy hook 被 DROP/REJECT
DROP 通常表现为超时,REJECT 可能立刻返回 ICMP 或 RST。安全策略选择要考虑信息暴露、客户端体验和故障诊断,不能只以“超时更安全”概括。
12.2 namespace 隔离不是完整安全边界
容器共享内核。network namespace 隔离接口和 socket 视图,却不能替代 capability、seccomp、LSM、用户 namespace、只读文件系统和及时内核更新。拥有 CAP_NET_ADMIN 的进程可在其作用范围内改变路由、接口与规则;不应为了方便排障长期授予特权容器。
12.3 最小权限排障
读取接口、route、socket 统计通常可在目标 netns 内完成;抓包、修改网络或读取 conntrack 可能需要额外 capability。生产中应使用受审计的临时 debug 容器或节点工具,限定 namespace、时间与数据范围。不要通过 --privileged 作为默认排障方法。
13. Go 服务在容器里的常见边界
13.1 监听地址是第一检查项
// 只允许同一 netns 回环访问
net.Listen("tcp", "127.0.0.1:8080")
// 所有适用 IPv4 本地地址
net.Listen("tcp", "0.0.0.0:8080")
// 简写,地址族行为还受 Go/OS 影响
net.Listen("tcp", ":8080")
服务对外暴露不应只靠监听地址控制。监听所有接口后,用网络策略、防火墙、mTLS/认证和端口发布限定可访问范围。
13.2 优雅退出与 endpoint 更新有竞态
编排系统终止 Pod 时,负载均衡端点撤销、preStop、SIGTERM、readiness 变化和现有连接排空不是瞬间原子事务:
Pod 标记不 ready
-> endpoint 更新传播需要时间
-> 部分客户端仍可能发新连接
-> 服务开始 graceful shutdown
-> 超时后强制关闭
Go 服务应先停止接受新业务、保持足够 drain 窗口,并让客户端具备安全重试。只收到 SIGTERM 就立即 os.Exit 会制造 reset;无限等待又阻塞发布。
13.3 连接池必须响应地址变化
服务发现可能把一个名字解析到变化的 Pod IP。长连接池不会因为 DNS TTL 到期自动迁移已有连接;旧 Pod 下线时才报错。客户端需要连接寿命、健康检查、失败剔除和受控重连,而不是每请求重新 DNS + Dial。
13.4 容器 CPU 限额也会成为网络问题
包已经过 veth 进入 socket queue,Go 进程若因 cgroup quota 被 throttle,不能及时 accept/read:
CPU throttling
-> accept queue / receive queue 增长
-> client latency / retransmission 增长
-> readiness probe 也超时
-> orchestrator 进一步重启
网络排查必须把 cgroup CPU、GC、goroutine 与内核队列放在同一时间轴。重启可能暂时清空队列,却不会修复容量不足。
14. 分层排查方法
14.1 先在正确 namespace 提问
PID=$(docker inspect -f '{{.State.Pid}}' my-container)
sudo nsenter -t "$PID" -n ip -br addr
sudo nsenter -t "$PID" -n ip route
sudo nsenter -t "$PID" -n ss -ltnp
sudo nsenter -t "$PID" -n cat /etc/resolv.conf
/etc/resolv.conf 属于 mount namespace 文件视图;仅 nsenter -n 后由 shell读取的路径可能仍是宿主机 mount 视图。严谨检查可同时进入 mount namespace,或直接通过容器 exec。这个细节说明:容器故障常跨多个 namespace,不能把“进入网络 namespace”理解为进入完整容器环境。
14.2 由近到远验证
1. 进程是否监听正确地址/端口
2. 容器 lo/eth0 是否 UP,IP/MTU 是否正确
3. route get 是否选对源地址、网关和接口
4. neighbor/ARP 是否解析
5. veth peer/bridge/FDB 是否正常
6. host forwarding 与 policy 是否放行
7. NAT/conntrack 是否存在且未耗尽
8. physical NIC 是否看到正向与回程
9. DNS、TLS、应用协议是否才是真正失败阶段
每一步都应验证具体事实,不要只运行 ping。ICMP 通不代表 TCP 端口开放;TCP connect 通不代表 TLS/SNI/证书正确;HTTP 200 也不代表所有客户端路径相同。
14.3 常用观测命令
# 容器内
ip -br link
ip -br addr
ip route
ip rule
ip neigh
ss -ltnp
ss -tin
# 宿主机虚拟拓扑
ip -d link
bridge link
bridge fdb show
bridge vlan show
# 转发/NAT(先确认系统使用哪套后端)
sudo nft list ruleset
sudo iptables-save
# 连接跟踪与接口计数
sudo conntrack -S
ip -s link
iptables 前端在一些系统实际操作 nftables backend;同时打印两者可能看到兼容表示而不是两套独立规则。修改前先识别 owner(Docker、CNI、firewalld、kube-proxy),手工插入规则可能被控制器覆盖或破坏集群网络。
14.4 用 tcpdump 找消失边界
# 容器接口
sudo nsenter -t "$PID" -n tcpdump -ni eth0 'tcp port 443'
# 宿主机 veth、bridge、物理出口
sudo tcpdump -ni vethXYZ 'tcp port 443'
sudo tcpdump -ni br0 'tcp port 443'
sudo tcpdump -ni eno1 'tcp port 443'
请求在 A 出现、B 不出现,问题位于 A-B 之间;请求到达目标但无响应,检查 socket/应用;响应在出口出现却没回容器,检查 conntrack、route、policy。比较时用四元组、sequence/flags 和时间戳,注意 NAT 前后地址会改变。
15. 常用命令知识点扩展
| 命令/短参 | 长参或全称 | 作用 |
|---|---|---|
ip -n NAME |
ip --netns NAME |
在指定命名 network namespace 执行 ip 子命令 |
ip -d |
ip --details |
显示接口类型、peer 等详细信息 |
ip -br |
ip --brief |
紧凑显示 link/address 状态 |
ip route get |
route lookup | 让内核模拟一次路由选择 |
ip neigh |
neighbor table | 查看 ARP/NDP 邻居状态 |
bridge fdb |
forwarding database | 查看 bridge MAC 转发表 |
nsenter -t |
nsenter --target |
以目标 PID 选择 namespace |
nsenter -n |
nsenter --net |
进入 network namespace |
tcpdump -n |
numeric | 不做 DNS/服务名解析 |
tcpdump -i |
interface | 选择抓包接口 |
场景配方:
# 某 PID 的 namespace inode
readlink /proc/"$PID"/ns/net
readlink /proc/1/ns/net
# 查询到目标实际走哪条路
ip route get 203.0.113.20
# 查 bridge 的端口与 MAC 学习
bridge link
bridge fdb show br br0
# 查看 conntrack 容量使用率
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
16. 面试题
Q:network namespace 隔离了什么?
它为进程提供独立的接口、地址、路由与 policy rule、neighbor 表、socket/端口空间、loopback、netfilter 规则及许多网络 sysctl 视图。容器仍共享同一 Linux 内核和物理 CPU/NIC;netns 是网络对象隔离,不是另一套 guest kernel,也不是完整安全边界。
Q:为什么容器里的 localhost 不是宿主机?
127.0.0.1 和 ::1 路由到当前 network namespace 自己的 loopback 设备。容器 netns 与 host netns 有不同 lo 和 socket 表,所以容器访问 localhost 只查容器内监听者。宿主服务只绑 host 127.0.0.1,容器不能把自己的 localhost 当作它。
Q:veth pair 的工作方式是什么?
veth 成对创建;一端发送的 Ethernet frame 作为另一端的接收流量进入其协议栈。把两端放进不同 namespace,就像用一根虚拟网线连接两个网络世界。veth 不自动分配 IP、路由或 NAT,并仍有 skb、队列、MTU 与 CPU 成本。
Q:Linux bridge 如何让同机容器通信?
宿主机 veth peer 作为 bridge port。bridge 从 frame 源 MAC 学习 FDB,按目的 MAC 将已知单播转到对应端口,广播/未知单播按规则 flood。相同二层子网的容器先 ARP 获取对方 MAC,然后经 veth—bridge—veth 通信,通常无需 SNAT。
Q:bridge 为什么有时需要配置 IP,有时不需要?
纯二层交换不需要 IP;如果宿主机要作为容器子网默认网关或自己参与该子网通信,就在 bridge 上配置网关 IP,使 frame 可进入宿主机三层路由。IP 应属于三层逻辑接口,不应重复散落在 bridge slave 上。
Q:容器访问公网为什么常需要 SNAT?
容器私有地址通常不被上游网络路由。宿主机把源地址/端口改成自己的可路由地址,conntrack 记录映射;响应到宿主机后再反向还原并路由给容器。若网络已显式发布容器网段路由,则可以不用 SNAT并保留原源地址。
Q:端口映射是如何工作的?
外部访问宿主机 published port 时,DNAT 将目的宿主地址/端口改为容器 IP/port,路由和 FORWARD 把包送入 veth;conntrack 在响应方向执行反向变换。实现可能是 nftables/iptables、IPVS、eBPF 或 proxy,排查前必须确认本机实际数据面。
Q:NAT 与反向代理有什么区别?
NAT 通常在内核改写 L3/L4 地址并保持一条 TCP 流,不终止应用协议;反向代理接受客户端 socket,再创建上游 socket,形成两条连接,可处理 TLS、HTTP header、重试和 L7 路由。NAT 不是 L7 代理,也不提供认证或加密。
Q:conntrack 为什么可能成为瓶颈?
每条被跟踪 flow 占表项、内存、timeout 和哈希处理。大量短 TCP、UDP、扫描或重试风暴可耗尽 nf_conntrack_max,新流被丢弃。调大上限会增加内存;应同时治理连接来源、复用、超时和是否确实需要跟踪,不能随意绕过影响 NAT 的状态表。
Q:为什么小请求成功、大请求或 TLS 卡住常指向 MTU?
overlay 封装增加外层头,内层 1500-byte packet 可能超过物理 MTU。若 ICMP Packet Too Big 被过滤,PMTUD 无法降低大小,SYN 和小数据正常,大 segment 持续重传。应检查各层 MTU、MSS、ICMP 和抓包,而不是只调应用超时。
Q:容器能访问 IP 却解析不了域名,应查什么?
检查容器 mount 视图的 /etc/resolv.conf、nameserver 是否能从该 netns 访问、UDP/TCP 53 策略、search/ndots、CoreDNS/runtime stub 状态,以及 Go 使用纯 Go 还是系统 resolver。宿主机 127.0.0.53 不会自动成为容器 localhost 的 DNS 服务。
Q:容器端口映射存在,为什么仍 connection refused?
DNAT 只改变目的地址。若容器内没有进程监听目标 port,或应用只绑定 127.0.0.1 而包从 eth0 到达,TCP 会拒绝。还应确认宿主端口与容器端口未混淆、地址族一致,并在容器 netns 用 ss -ltnp 验证。
Q:host network 的优缺点是什么?
它让容器共享宿主机 netns,减少 veth/bridge/NAT 路径并简化某些高性能场景;代价是端口空间冲突、网络可见性扩大、隔离和策略边界减弱、部署更依赖节点。必须根据 pps/延迟收益和安全模型选择,而不是默认更快就使用。
Q:如何定位一个包在容器链路哪一层消失?
先确认容器 socket、接口、route/neighbor;再在容器 eth0、宿主 veth、bridge、物理接口用窄 BPF filter 同时观察同一流;对照 NAT 前后四元组和请求/响应方向,并检查 forwarding、policy、conntrack 和应用队列。最后出现与首次缺失的两个观察点界定问题边界。
Q:为什么容器网络故障可能其实是 CPU 问题?
容器进程受 cgroup quota 限制时可能不能及时 accept/read,导致 accept queue、socket receive queue 和 TCP 延迟增长;节点 softirq 也与应用争 CPU。包已经到达并不表示 goroutine能及时运行。应把 CPU throttling、softirq、Go profile 与网络队列放在同一时间轴。
小结
- 容器共享宿主机内核;runtime/CNI 在控制面创建 namespace、veth、bridge、route、policy 和 NAT,之后内核数据面逐包处理
- network namespace 隔离接口、地址、路由、socket、loopback 和网络规则,因此每个容器的 localhost 和端口空间相对独立
- veth pair 把一端 TX 变成 peer RX,像跨 namespace 的虚拟网线;它不自动提供 IP/路由,并有队列、skb、CPU 与 MTU 成本
- Linux bridge 按 MAC 学习 FDB并完成二层交换;bridge 只有在作为宿主机三层端点/网关时才需要 IP
- 容器与宿主机各自做路由决策;forwarding、回程路由和 policy 都正确,包才真正可达
- SNAT 让私有容器地址借用宿主机可路由地址出网,DNAT 实现 published port;conntrack 保存双向映射,也是有限共享资源
- 外部、宿主本地和容器 hairpin 走不同 netfilter 入口,端口映射实现还可能是 nftables、IPVS、eBPF 或 proxy,排查不能只查固定工具
- overlay 头会缩小可用 MTU;ICMP 被错误过滤会形成 PMTU black hole,典型现象是小包正常、大包或 TLS 卡住
- DNS、监听地址、优雅退出、连接池与 CPU quota 都能表现为“容器网络问题”,应按 socket—namespace—veth—bridge—route—policy—NAT—NIC—应用逐层验证
下一篇讲 namespace 与 cgroup:容器到底隔离了什么 —— PID、mount、user、UTS、IPC、network namespace 如何改变进程视图,cgroup v2 如何限制 CPU、内存、IO 与进程数,以及为什么“看不见”和“用不了”是两套完全不同的机制。
xingliuhua