前段时间帮一个客户环境做巡检,刚登上 k3s 集群就发现不对劲:新部署的服务全部处于 CrashLoopBackOff,Pod 反复重启,外部的 LoadBalancer 访问也时通时断。刚开始谁都会先怀疑镜像或者依赖配置,我也一样,翻日志、看事件、调资源限制,折腾了大半天。最后定位到的根因,说出来你可能觉得离谱:是防火墙出站规则把三类 ICMP 报文给禁了,分别是 echo-reply(type 0)、time-exceeded(type 11)和 destination-unreachable(type 3)。这次 docker + k3s + 防火墙规则冲突导致服务无法正常启动的排查过程,我觉得很有代表性,所以完整记录下来,从现象、定位、复现到解决思路,给同样被这种“网络静默故障”坑过的人一个参考。
1. 故障现象记录:k3s 服务反复重启,防火墙规则成最大嫌疑
1.1 第一现场:Pod 状态和负载均衡都不对劲
先说现象,方便大家对照自己遇到的情况。这套 k3s 集群跑在三个节点上,容器运行时用的是 docker 模式(k3s 启动时加了 --docker 参数,所以集群里的容器运行时是 Docker 而不是默认的 containerd,这也是后面排查时要多留意一层的点)。故障期间,kubectl get pods -n production 看到的状态是这样的:
bash复制NAME READY STATUS RESTARTS AGE
my-service-7d8b9f6b6c-abc12 0/1 CrashLoopBackOff 6 (3m12s ago) 18m
my-service-7d8b9f6b6c-def34 0/1 CrashLoopBackOff 4 (2m58s ago) 18m
Pod 起来之后几十秒就挂,然后被 kubelet 重新拉起,反复循环。查看 kubectl logs 发现进程本身其实启动过,日志最后停在监听端口初始化的地方,等了两分钟又退出了,说明不是代码一启动就 panic,而是在等待某个外部条件。kubectl describe pod 里有明显的线索:
txt复制Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 18m default-scheduler Successfully assigned ...
Normal Pulled 18m kubelet Container image already present on machine
Normal Created 18m kubelet Created container my-service
Normal Started 18m kubelet Started container my-service
Warning Unhealthy 18m kubelet Readiness probe failed: HTTP probe failed with statuscode: 500
Warning BackOff 17m kubelet Back-off restarting failed container
Readiness 探针失败,服务被 Endpoints Controller 剔除,所以 Service 的 kubectl get endpoints 一直为空。到这里,问题范围缩小到:服务进程能跑起来,但探针检查失败。
1.2 环境背景:这套 k3s 集群是怎么搭的
这套环境不是标准的托管 K8s,而是 k3s 单命令搭出来的轻量集群。三个节点,一个 server,两个 agent。网络方面,k3s 默认使用 flannel,后端是 VXLAN。很多朋友容易忽略的是,k3s 通过 --flannel-backend=vxlan 的方式自动创建 flannel.1 接口,容器网段默认是 10.42.0.0/16,每个节点分配一个 /24 子网。
因为容器运行时是 docker 模式,所以节点上同时存在 Docker 的 iptables 链(DOCKER、DOCKER-USER)和 k3s 的 iptables 链(KUBE-SVC、KUBE-SEP、KUBE-FORWARD 等),这两套规则叠加在一起,任何一个链上的插件规则都可能导致流量异常。
集群里这些服务大多数都是内部微服务,通过 Ingress 对外暴露,另外有一批 LoadBalancer 类型的服务,用的是 k3s 自带的 ServiceLB(klipper-lb),也就是每个节点上的 svclb Pod 通过 hostNetwork 监听端口,再转发到后端的业务 Pod。
1.3 从“网络应该没问题”到开始怀疑防火墙
一开始没人往防火墙方向想,因为集群内部 Pod 之间互 ping 是通的,SSH 节点也正常,看 iptables 的默认策略也没有明显问题。真正让我们转向防火墙规则的,是几个服务同时出现相同症状,且重启也没有用。如果是单个服务代码问题,不应该四个模块一起挂。
后来跟安全团队确认,他们近期按新基线做了一轮加固,在防火墙出站规则里禁用了 echo-reply(type 0)、time-exceeded(type 11)、destination-unreachable(type 3)。理由是防止主机被 ping 探测、防止 traceroute 测绘网络路径、防止 ICMP 错误报文泄露内部网络结构。听起来很合理,但恰恰就是这三条规则,让 k3s 的服务网络陷入了瘫痪。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 排查过程:从 kubelet 日志一路查到防火墙规则
2.1 第一步:先看 Pod 事件,别一上来就怀疑代码
遇到服务起不来,我一般有个固定的排查顺序:先 kubectl describe pod 看事件,再 kubectl logs 看应用日志,最后才翻 kubelet 日志。这个顺序能最快缩小范围,避免被应用层的表象迷惑。
这次 describe pod 里 Readiness probe failed 的信息已经很明显了,于是我把探针配置调出来看了一眼,是标准的 HTTP /healthz 探测,端口 8080。探针失败通常有三种原因:一是容器内进程没有监听端口,二是返回码不是 2xx/3xx,三是网络路径上被什么东西拦截了。既然容器日志显示进程起来了,前两种可能性较低,所以重点转向网络路径。
于是我在业务 Pod 所在节点上用 tcpdump 抓探针包:
bash复制tcpdump -i any -nn port 8080
抓了一会儿发现,kubelet 发出的探针请求,在节点上能看到 SYN 包,但业务 Pod 的响应包在节点上却看不到完整往返。更奇怪的是,部分 TCP 连接会卡在握手阶段,看起来像是中间路径上有人静默丢包。
2.2 第二步:用 kubectl 和 tcpdump 锁死网络链路
为了把问题限定在某一段链路,我手动起了一个临时 pod,直接在集群里做三层连通性测试:
bash复制kubectl run nettest --image=busybox --rm -it -- /bin/sh
在 Pod 里 ping 业务 Pod 的 IP,通的。再尝试用 wget 拉业务 Pod 的健康检查接口:
bash复制wget http://10.42.1.35:8080/healthz
有意思的现象出现了:小包请求有时候成功,有时候超时。一旦涉及多分片的 HTTP 响应,成功率明显下降。这已经比较接近 MTU 或 ICMP 错误消息被丢弃的症状了。ping 通说明三层可达,TCP 握手能完成但响应大包卡死,说明传输层出现了“分片黑洞”。
顺手用 ip route get 10.42.1.35 和 ip link show flannel.1 确认了路径要经过 flannel.1 接口,并且 flannel.1 的 MTU 是 1450(VXLAN 封装层 50 字节开销)。如果物理链路 MTU 是 1500,封装后正好需要依赖 PMTUD 来协商,而 PMTUD 又依赖 ICMP type 3 code 4 的错误回报。
2.3 第三步:对比防火墙放行前后,问题瞬间复现
为了验证防火墙规则就是元凶,我们在一台节点上临时把出站规则里被禁的三类 ICMP 全部放行,然后重新创建一个测试 Deployment。结果立竿见影:Pod 启动后 Readiness 探针第一次就通过了,外部访问也恢复正常。等到规则重新禁掉,问题再次复现。
这里要特别强调一个容易踩的坑:很多人查防火墙只查 INPUT 链,觉得出站流量不影响自己访问别人,但实际上 k3s 集群里很多数据面路径,比如 ServiceLB 转发、Flannel 隧道、健康检查,都会经过 OUTPUT 或者 FORWARD 链。出站规则把 ICMP 错误报文一丢,等于把整个集群的网络故障反馈机制关掉了,系统只能靠超时慢慢恢复,表现就是服务起不来、访问卡顿、探针失败。
这个对比试验做好之后,我们基本确定,故障根因就是出站防火墙规则误杀了三类 ICMP 报文。
3. 根因分析:三类 ICMP 类型为什么能卡住 k3s 服务
3.1 被禁掉的三个 ICMP 类型到底都在干什么
很多人对 ICMP 的认知停留在“ping 用的协议”,实际上 ICMP 是 IP 协议的配套控制协议,承担着很多关键的错误反馈和路径发现功能。这次涉及的三个类型,分工完全不同:
| ICMP 类型 | 名称 | 常见用途 | 禁用后的影响 |
|---|---|---|---|
| type 0 | echo-reply | ping 的响应包 | ping 不通,监控误判主机离线 |
| type 3 | destination-unreachable | 目标不可达,其中 code 4 用于 PMTUD | 路径 MTU 发现失效,大包传输卡死 |
| type 11 | time-exceeded | TTL 超时,traceroute 依赖它 | 路径探测失效,隧道链路问题无法定位 |
type 3 是整个事件里最要命的一个,尤其是 code 4 “fragmentation needed but DF set”,当网络设备发现包超过出接口 MTU 且 IP 头带了 DF(Don't Fragment)位时,它会发一个 type 3 code 4 的 ICMP 给源端,告诉源端“你需要把包拆小一点”。如果这个错误报文被防火墙丢弃,源端就会一直以为 1500 字节的包能发出去,结果包在路径中某个设备上被静默丢弃,TCP 会一直重传,最终表现为连接超时、服务假死。
3.2 连锁反应:VXLAN 隧道和 MTU 黑洞
k3s 默认的 flannel 后端是 VXLAN,每个节点上的 flannel.1 接口会封装 UDP 包,VXLAN 头加 UDP 头加 IP 头,总共增加 50 字节的开销。物理网卡 MTU 是 1500,flannel.1 的 MTU 就是 1450。如果容器内发送一个 1472 字节的 UDP 包,加上外层 VXLAN 封装后正好是 1522 字节,超出物理链路 MTU,路径上的设备会尝试回 ICMP type 3 code 4。
问题来了:如果防火墙把出站的 type 3 给丢弃了,发送端永远不知道自己的包太大了,只能是反复重传,直到 TCP 超时。表现就是小包通信正常、大包全部卡死。服务的健康检查虽然只是 GET /healthz,但响应如果超过了单个分片大小,就可能中招。而且一旦 Readiness 探针连续失败,kubelet 就会把容器标记为 Unhealthy,然后不断重启,形成 CrashLoopBackOff。这就是整个故障最核心的因果链路。
3.3 健康检查和 ServiceLB 为什么也跟着“起不来”
kubelet 的 HTTP 探针本质上就是发起一个 TCP 连接然后发 HTTP 请求,它本身不依赖 ICMP,但 TCP 连接建立后如果数据段因为 PMTUD 失效而无法传输,探针就会一直等到超时。另外,探针请求的目标地址是 Pod IP,这中间要经过本机路由表、flannel.1 接口、VXLAN 隧道、对端 flannel.1、最终到容器。任何一个节点上如果出站 ICMP 被禁,整条隧道的大包传输质量都会下降。
ServiceLB(svclb Pod)也是一样,它用 hostNetwork 模式监听宿主机端口,再把流量转发给后端业务 Pod。转发过程如果涉及跨节点,同样要走 flannel 隧道,同样会被 MTU 黑洞影响。所以最终现象就是:Service 的 Endpoints 因为 Prepare 探针失败而被清空,LoadBalancer 的 EXTERNAL-IP 虽然存在,但后面没有可用后端,外部访问自然失败。
另外,time-exceeded 类型被禁之后,像 traceroute、mtr 这类排障工具会完全失效,我们想确认路径中间是否有其他设备修改了 MTU 都变得很困难。这给排查增加了不少时间成本。
3.4 最小复现实验:一条命令复现你的故障
如果你也想验证自己的环境是否有类似问题,可以在测试节点上用 iptables 模拟这个故障,但千万不要在线上环境随意操作。复现步骤如下:
bash复制# 在 k3s 工作节点上执行,模拟出站 ICMP 三类报文被禁
iptables -A OUTPUT -p icmp --icmp-type 0 -j DROP
iptables -A OUTPUT -p icmp --icmp-type 3 -j DROP
iptables -A OUTPUT -p icmp --icmp-type 11 -j DROP
然后在任意 Pod 里测试大包传输:
bash复制# 进入测试 Pod
kubectl exec -it nettest -- sh
# 用 ping 测试路径 MTU,超过 1472 字节会触发分片
ping -M do -s 1500 10.42.1.35
正常环境里,这个命令会打印 Frag needed and DF set 之类的 ICMP 错误提示,然后很快失败;如果碰上了防火墙静默丢弃 ICMP,命令会一直卡住不动,直到超时。这就是典型的 MTU 黑洞症状。用这种方式可以快速确认问题是否和 ICMP 错误报文被丢弃有关。
4. 解决方案:防火墙规则怎么改才既安全又不影响业务
4.1 临时恢复:先把服务拉起来
定位到根因之后,第一件事是恢复业务,而不是急着争论安全策略。我们在节点上临时放行出口 ICMP 错误报文,然后滚动重启了出问题的 Deployment:
bash复制# 在 k3s 所有节点上执行
iptables -D OUTPUT -p icmp --icmp-type 0 -j DROP
iptables -D OUTPUT -p icmp --icmp-type 3 -j DROP
iptables -D OUTPUT -p icmp --icmp-type 11 -j DROP
# 让出问题的服务重新调度
kubectl rollout restart deployment -n production
重启之后,Pod 的 Readiness 探针很快通过,Endpoints 恢复正常,外部访问也回来了。整个恢复过程大约五分钟,说明问题确实在网络反馈机制而不在应用本身。如果你的环境用的是 nftables,把 iptables 命令换成 nft 语法即可。
4.2 长期方案:按需放行而不是一刀切
既然问题出在“一刀切禁用 ICMP”,长期方案就要做精细化的放行规则。安全基线要求防扫描,不等于所有 ICMP 都不能放行。推荐的做法是:对出站方向的 ICMP,按用途区分处理。
bash复制# 放行 ICMP 错误汇报报文,保证 PMTUD 和网络故障反馈正常
iptables -A OUTPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type time-exceeded -j ACCEPT
# 如果需要对外提供 ping,则放行 echo-request 和 echo-reply
iptables -A OUTPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT
更严格的网段级控制,可以把 ICMP 的放行范围限制在集群内部网段之间,比如只允许 10.42.0.0/16、10.43.0.0/16 之间的 ICMP,对外部网段继续保持收敛。这样既保证了集群内部的故障反馈机制,又不至于把所有 ICMP 都敞开放到公网。
4.3 安全与稳定的权衡:别为了“看起来安全”坑了业务
这次故障让我特别想多说一句:网络安全加固的时候,很多人会照搬文档里的拒绝项,觉得 ICMP 禁用得越多越安全。但 ICMP 本身就是 IP 协议栈的“诊断回路”,和 TCP、UDP 一样是网络运行的基础设施。合理的做法是理解每个 ICMP 类型的作用,和运维团队确认哪些是业务依赖的,而不是一刀切全禁。
如果安全团队实在不放心 echo-request/echo-reply,可以至少保留 destination-unreachable 和 time-exceeded 这两类错误报文。这两类报文是网络路径上的“故障通知”,丢掉了它们,网络发生 MTU 问题、路由黑洞、TTL 超时时,发送端什么都不知道,只能靠超时来兜底,这种故障比明着报错难排查得多。
5. k3s/容器环境下防火墙规则避坑清单
5.1 容器运行时与 iptables/nftables 的关系
在 k3s 集群里,Docker 和 k3s 都会往 iptables/nftables 里写入规则。Docker 的规则主要在 DOCKER-USER 和 DOCKER 链,k3s 的规则主要在 KUBE-SERVICE、KUBE-FORWARD 等链。如果你在宿主机上做防火墙加固,一定要先看清楚默认策略是对哪个链生效的。
尤其注意 FORWARD 链。容器流量在宿主机上走的是 FORWARD 链路,不是 INPUT/OUTPUT。很多加固脚本会把 FORWARD 默认策略改成 DROP,结果容器之间、Pod 之间的流量全部被拦。这个坑比 ICMP 更常见,有的人查了半天防火墙,发现 INPUT 链开放得很宽松,但 FORWARD 链却把包全丢了。查看 FORWARD 链可以用:
bash复制iptables -L FORWARD -n -v
关注 DROP 的计数是否在持续增长,如果计数在涨,说明确实有流量被 FORWARD 链丢掉了。
5.2 多节点集群要关注的 4 个流量路径
k3s 集群最少三个节点,流量路径比单机 Docker 复杂很多。做防火墙规划时至少要覆盖以下四条路径:
- 节点到节点:flannel 隧道走的是 UDP 8472 端口,如果节点防火墙禁了 UDP 8472,整个 VXLAN 隧道就断了,Pod 之间全不通。
- kubelet 到 apiserver:走 TCP 6443,这个是最基础的,断掉后节点会反复 NotReady。
- kubelet 到 Pod 探针:走随机高位端口,属于 FORWARD 链路,容易被 FORWARD 默认 DROP 误伤。
- ServiceLB 宿主机端口到后端 Pod:svclb Pod 是 hostNetwork,走 iptables DNAT 规则,规则链上的 ACCEPT 不能少。
每次上线防火墙规则前,至少把这几条路径的连通性测一遍,不然很容易出现“服务看着都正常,但外部就是访问不了”的隐蔽故障。
5.3 常用排查命令速查
这次排障过程中用到的命令,整理成一个清单,下次遇到类似问题可以快速套用:
bash复制# 查看 Pod 事件,快速发现探针失败
kubectl describe pod <pod-name> -n <namespace>
# 查看 Service Endpoints
kubectl get endpoints -n <namespace>
# 在节点上抓包,验证网络路径是否正常
tcpdump -i any -nn port 8080
# 查看本机路由走向
ip route get 10.42.1.35
# 查看 flannel 接口 MTU
ip link show flannel.1
# 测试路径 MTU 是否正常
ping -M do -s 1472 <target-ip>
6. 常见问题速查表:现象 / 原因 / 解法
6.1 问题排查速查表
| 现象 | 可能原因 | 排查命令 | 解决方向 |
|---|---|---|---|
| Pod 反复 CrashLoopBackOff,探针失败 | ICMP 错误报文被禁导致 PMTUD 失效 | kubectl describe pod、tcpdump | 放行 ICMP type 3 code 4 |
| Service Endpoints 为空 | Pod 未就绪,被控制器剔除 | kubectl get endpoints | 先解决 Pod 状态问题 |
| NodePort 外部访问不通 | FORWARD 链默认 DROP | iptables -L FORWARD -n -v | 放行容器流量转发链 |
| DNS 解析超时 | UDP 响应丢失,port unreachable 被丢弃 | nslookup、dig | 放行 ICMP type 3 |
| 大文件传输卡死,小包正常 | MTU 黑洞 | ping -M do -s 1472 | 调整 MTU 或放行 ICMP 错误反馈 |
| ping 不通但 TCP 正常 | echo-request/reply 被禁 | ping 测试 | 按需放行 echo 类型 |
6.2 独家避坑建议
这次踩坑之后,我养成了一个习惯:凡是动防火墙规则,先拿业务流量做一轮真实压测,而不是只跑一遍 ping。压测时重点看大包和小包的差异,比如 iperf3 跑一下 TCP 吞吐,或者直接 curl 一个 10MB 的文件,如果出现“小包秒回、大包卡死”,基本就是 MTU 黑洞或者 ICMP 被丢。
另一个建议是,防火墙规则变更要有统一的回滚预案。安全加固的人可能只知道“我加了几条 DROP 规则”,但不知道这些规则会影响哪些业务链路。运维侧最好在每台节点上把当前的 iptables 规则导出备份,变更前对比一下 diff,出问题能秒级回滚。这次我们就是因为规则是分批下发的,且没有保存完整快照,排查时花了不少时间核对哪条规则在哪个节点上生效。
最后再分享一个小技巧:k3s 集群里,如果怀疑是 MTU 问题,可以临时把 flannel.1 的 MTU 降到 1400 试试,能快速验证是不是 VXLAN 封装导致的大包黑洞。但注意这只是应急手段,长期还是要从防火墙规则和整网 MTU 规划上解决。
