刚部署完一套 k8s 集群,还没高兴几分钟,kubectl get pods就看到 kube-system 命名空间里的几个 coredns 处于CrashLoopBackOff。那会儿我第一反应是网络插件没装好,第二反应是 kube-proxy 出了问题,结果排查了半天,最后在一行 CoreDNS 日志里找到了答案。类似的事情后来在几个不同环境里又遇到过,每次崩溃的原因都不一样,但一套完整的排查思路用下来,处理时间基本能控制在十分钟以内。
这篇文章就把这些原因和对应解法完整整理出来,适合正在被CrashLoopBackOff折磨的人,也适合想提前给集群打个预防针的人。放心,问题不复杂,但前提是你要知道去看哪里、怎么读日志、怎么分辨那种"看起来是网络问题、其实是 DNS 问题"的假象。
1. CoreDNS 一崩,影响面远比"无法解析域名"大
先花点时间理解为什么 CoreDNS 崩溃的动静这么大。很多人在CrashLoopBackOff出现后,第一反应是查网络插件、查 kube-proxy、查防火墙,唯独忽略了这个状态背后的 DNS 链路。等绕了一圈回来,才发现集群根子上的 DNS 服务已经反复重启十分钟了。
1.1 一个 DNS 失效的集群,表面上会伪装成各种问题
CoreDNS 在集群里的角色相当于总机:通过 ClusterIP 暴露 DNS 服务,所有 Pod 的/etc/resolv.conf都会被 kubelet 写入nameserver <CoreDNS Service 的 ClusterIP>。也就是说,任何一个 Pod 要解析其他 Service 域名、外部域名,第一步都会先找到 CoreDNS。
如果没有它,你会看到非常多样化的故障现象:
- Service 之间互相调用报
unknown host、getaddrinfo EAI_AGAIN; - 应用日志里出现大量间歇性超时,因为 Pod 内的 DNS 缓存过期后,新查询全部失败;
- 新建 Pod 启动时依赖域名初始化,结果启动失败,显示
CrashLoopBackOff; - 有些应用表现为"连接被拒绝"或"连接超时",根本不直接提 DNS。
为什么容易误判?因为 DNS 故障的表现太像网络故障了。你看到服务 A 连不上服务 B,自然先去查 Service、Endpoint、CNI、kube-proxy,几乎没人第一时间想到 CoreDNS。所以排查这类问题之前,先确认 CoreDNS 本身是不是健康,是一个必须养成的肌肉记忆。
1.2 CrashLoopBackOff 的"重启算法":为什么看起来时好时坏
CrashLoopBackOff是 Kubelet 对反复启动失败的容器的状态描述。Kubelet 每次启动容器,容器进程退出后,会根据退出码判定失败,然后按指数退避的方式等待下一次重启:10 秒、20 秒、40 秒、80 秒、160 秒,最长封顶 300 秒。只要容器每次都在启动后短期内退出,这个循环就会一直持续下去。
所以你会看到 Pod 状态在CrashLoopBackOff和Running之间反复横跳,或者在事件流里看到Back-off restarting failed container。但注意,CrashLoopBackOff只是结果,不是原因。它本身不告诉我们容器为什么失败,你必须要往下挖:
- 容器上次终止时的
Last State是什么? - 退出码是多少?
- 退出前最后一段日志说了什么?
这三样东西,才是破案的关键。别盯着状态发愁,直接去看证据。
2. 先用这五步拿证据:Pod 状态、Last State、退出码与崩溃前日志
排错最忌讳上来就猜。我先给你一套固定的取证序列,按顺序执行,绝大多数情况下能直接锁定问题方向。
2.1 第一步:看 Pod 状态和副本分布
先确认是不是所有副本都崩了:
kubectl -n kube-system get pods -o wide | grep coredns如果只有一个副本处于CrashLoopBackOff,另一个是Running,集群 DNS 不会完全中断,但容量已经下降一半,高峰期可能出现解析超时;如果所有副本都崩了,那影响面就是全集群。这一步还顺便告诉你崩溃 Pod 所在节点,后面登录节点查 resolv.conf 会用到。
2.2 第二步:describe 里的 Last State 和退出码
拿到 Pod 后,立刻 describe:
kubectl -n kube-system describe pod <coredns-pod-name>重点看容器状态部分,尤其是Last State这一行。这是容器上一次退出时的快照,比当前状态更有价值。
退出码本身就是线索。我给一个快速对照表:
| 现象 | 优先怀疑方向 | 下一步怎么做 |
|---|---|---|
| Last State: Terminated, Reason: Error, Exit Code: 1 | CoreDNS 主动退出,多半是配置或插件检测问题 | 翻崩溃前日志,查 loop / parse error |
| Last State: Terminated, Reason: OOMKilled, Exit Code: 137 | 内存超限被杀 | 调 resources,查上游 DNS 超时 |
| Last State: Terminated, Reason: Error, Exit Code: 2 | Corefile 解析失败 | 检查 ConfigMap 里的 Corefile 语法 |
事件里有exec format error | 镜像架构不匹配 | 换多架构镜像 |
事件里有Failed to pull image | 镜像拉取失败 | 这不是 CrashLoopBackOff,是镜像源问题 |
另外要留意 Events 区域。如果事件里既有Back-off restarting failed container,又有别的异常,把事件按时间顺序串起来看,往往能还原崩溃链条。
2.3 第三步:logs --previous 拿到崩溃前的日志
这一步非常关键,但经常被忽略。容器重启后,当前日志往往是空的,因为你看到的是新进程的输出;真正有价值的崩溃现场在上一份日志里:
kubectl -n kube-system logs <coredns-pod-name> --previous如果日志里有Loop ... detected,问题基本就是 DNS 环路;如果有parse error,就是 Corefile 语法问题;如果日志完全空白,那大概率是进程被杀,尤其是 OOM,因为内存被杀不会留下应用级别的日志。
2.4 第四步和第五步:从节点视角看环境
四、五两步经常要交叉进行:
- 如果日志指向 loop 问题,登到 Pod 所在节点,看
/etc/resolv.conf以及本地 DNS 缓存服务(比如 dnsmasq、systemd-resolved)的配置; - 如果日志指向权限问题,看节点的 SELinux 状态、容器 securityContext 以及挂载的只读文件系统;
- 如果日志为空且退出码 137,去节点上执行
dmesg | grep -i oom确认内核 OOM 记录。
这套顺序跑完后,大部分问题已经能归类了。下面逐一展开最常见的几大类根因。
3. loop 插件自检退出:DNS 环路是怎么让 CoreDNS 反复"自杀"的
loop插件的日志可能是最吓人的一行,也是新手最容易理解错的地方。
3.1 loop 插件的原理:CoreDNS 为什么要主动"自杀"
先看一段典型的崩溃日志:
plugin/loop: Loop (127.0.0.1:55923 -> :53) detected for zone "."看到这行,说明 CoreDNS 的 loop 插件在启动阶段主动终止了进程。很多人的第一反应是"这个插件有问题,把它删掉",但恰恰相反——loop 插件是保护机制,不是故障源。
它的工作逻辑是:CoreDNS 启动时,在每个配置了 loop 插件的 zone 内,构造一个随机域名查询发送给自己。如果这个查询经过上游转发链路后又回到了 CoreDNS 自己,loop 插件就判断存在 DNS 解析环路,直接 Fatal 退出。
这么做的意义在于:如果上游链路真的形成了环路,CoreDNS 继续运行下去会导致所有经过它的查询无限循环,最终变成超时风暴。与其让整个系统的解析全部卡死,不如启动时迅速退出,逼你立刻修链路。你可以把它理解成一个接线员发现自己的电话线和隔壁房间的出线口又接了回来,于是直接把总机断电,免得整个楼里的电话都打不出去。
3.2 最常见的环路构造:节点 resolv.conf、本地缓存与 ClusterIP 的三方合唱
要理解环路怎么来的,先看默认配置。标准 Kubernetes 部署里,CoreDNS 的 Corefile 通常长这样:
.:53 { errors health ready kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa } prometheus :9153 forward . /etc/resolv.conf cache 30 loop reload loadbalance }注意forward . /etc/resolv.conf:CoreDNS 会把非集群内部域名的查询转发到容器内/etc/resolv.conf里写的上游 DNS。而 CoreDNS 容器内的/etc/resolv.conf是从所在节点继承下来的,所以它的上游链路天然绑定在节点 DNS 配置上。
一旦节点上的 DNS 配置被改造过,环路就可能出现。我见过最典型的链路是这样:
Pod 查询 -> CoreDNS(ClusterIP 10.96.0.10:53) -> forward 到容器内 /etc/resolv.conf -> 节点配置 nameserver 127.0.0.1 -> 本机 dnsmasq / systemd-resolved -> 上游又指回 10.96.0.10:53 -> 又回到 CoreDNS 自己促成这种环路的常见原因有三个:
- 节点
/etc/resolv.conf被写成nameserver 127.0.0.1,本机恰好有 dnsmasq 在 53 端口监听; - 节点使用 systemd-resolved,
nameserver指向127.0.0.53,而 systemd-resolved 的代理配置里又指向了 CoreDNS 的 ClusterIP; - 集群里装了 NodeLocal DNSCache,但配置指回 CoreDNS ClusterIP,CoreDNS 又指回本机缓存,两边互相指,形成一个闭环。
3.3 处置方案:给 forward 一个"干净的上游"
遇到 loop 日志,不要删 loop 插件,而是拆环路。最快的解法是改 CoreDNS 的 forward,直接指向一个逻辑上不会绕回 CoreDNS 的可靠上游。
kubectl -n kube-system edit configmap coredns把 forward 那行改成明确的上游地址,例如内网环境里网络管理员提供的可用 DNS 服务器:
forward . 10.0.0.2 10.0.0.3保存后重启 CoreDNS:
kubectl -n kube-system rollout restart deployment/coredns随后确认 Pod 进入 Running 状态,日志里不再出现 Loop 字样。
如果环境硬性要求 CoreDNS 必须走本机 DNS 缓存,那你得保证缓存的转发目标不能又指回 CoreDNS。具体来说,本机缓存可以负责把cluster.local这类内部域名转发给 CoreDNS,把外部域名转发给真实上游;而 CoreDNS 的 forward 指向真实上游,两者不要交叉回指。但说实话,这种双向转发配置非常容易踩坑,基础集群里我不推荐一开始就这么搞,先把一个干净的上游路径跑通再说。
这里还想强调一点:loop插件保留着比删掉好得多。删掉它之后,环路不会消失,只会变成查询超时,故障反而更难发现。保留 loop 插件,等于让事故在启动阶段就主动暴露,这是好事。
4. OOMKilled:这种无声死亡怎么定位,又该怎么调内存
第二大类高频原因,是内存问题。
4.1 OOMKilled 和 exit code 137 的取证
如果你在 describe 里看到:
Last State: Terminated Reason: OOMKilled Exit Code: 137那就是 CoreDNS 进程被 cgroup OOM killer 杀掉了。137 是 128 + 9 的结果,也就是收到了 SIGKILL。
这时候去节点上跑一下:
dmesg | grep -i oom大概率能看到内核记录里出现了 coredns 进程名,以及被杀时进程占用的内存页数。这个证据很硬,比任何日志都可靠。
有个容易误导的点:CoreDNS 的崩溃日志可能是空的。因为 OOM 是内核直接杀进程,进程根本来不及写错误日志。所以如果你发现logs --previous什么都没有,但退出码是 137,基本可以断定是内存问题。
4.2 为什么 CoreDNS 会吃内存:缓存、并发与上游超时堆积
CoreDNS 本身不算重内存应用,正常负载下单个副本的内存占用也就几十 MB。但有几个条件叠加,内存就很容易失控:
- cache 插件:默认会缓存 TTL 内的解析记录,缓存条目数有上限,正常不会爆;
- 上游 DNS 超时:转发的外部解析请求如果迟迟得不到响应,CoreDNS 会为每个查询维持等待中的 goroutine,上游越慢,堆积越多,内存涨得越快;
- 业务 QPS 突增:尤其是部署了大量业务 Pod 后,集群内部域名解析频率大幅上升,而默认的 limit 设置可能偏低。
很多发行版的默认 Deployment 里,CoreDNS 的内存 limit 就设在 128Mi 或 256Mi 附近。对于一个小集群够用,但一旦业务规模上来,或者上游链路有延迟,这个值就可能成为瓶颈。
4.3 内存调整与更合理的资源规划
遇到 OOM,最直接的办法是调大内存 limit。但无脑堆内存也不是好习惯,我的建议是分两步走:
第一步,先适度调大 limit,比如从 128Mi 提到 256Mi 或 512Mi,观察 Pod 是否稳定。同时在 Prometheus 里采集 CoreDNS 暴露的指标,重点看process_resident_memory_bytes,以及coredns_dns_request_duration_seconds这类延迟指标,搞清楚是纯粹资源给得太小,还是上游 DNS 太慢导致请求堆积。
第二步,如果发现是上游超时导致的堆积,就去修 forward 链路,不要光靠加内存硬扛。否则今天加到 512Mi,明天业务翻倍又会 OOM。
调整资源时,requests 和 limits 可以拉开一点差距。比如 requests 设 64Mi 或 128Mi,limits 设 256Mi 或 512Mi。requests 过高会导致节点调度困难,limits 过小又容易触发 OOM;拉得太开也不是好事,因为 Kubernetes 的 QoS 分类会被影响。一般 CoreDNS 这种基础组件,我习惯给一个稳妥的中间值,再用监控逐步校准。
另一个常见操作是加副本。把 CoreDNS 副本数从 2 提到 3,并且尽量让副本分布在不同节点,既分散了单点压力,也降低了节点故障时的 DNS 中断风险。如果集群已经装了 Prometheus,记得给 CoreDNS 的可用副本数和内存使用量配上告警,免得下次 OOM 又是半夜才被发现。
5. Corefile 配置错误:一个括号或一次缩进引发的 CrashLoopBackOff
第三大类是配置错误。这类问题定位起来非常快,只要你瞄一眼崩溃日志。如果日志里有parse error、unknown plugin、Caddyfile之类的字样,基本就是 Corefile 出了问题。
5.1 配置解析失败的特征
CoreDNS 脱胎于 Caddy,它读取的 Corefile 是一种带缩进和括号的 DSL。一个典型的解析错误日志长这样:
[FATAL] plugin/errors: read: unexpected end of Caddyfile看到这行,你的第一反应应该是:Corefile 里的某个区块没有正确闭合,或者缩进层级不对。另一种情况是拼写错误,比如插件名写错、插件参数格式过时,导致 CoreDNS 完全不认识这段配置。
5.2 常见 Corefile 语法坑
我整理几个实际环境中经常见到的坑:
- 括号不闭合:Corefile 的 zone 区块用一对大括号包住,少写一个右括号,整个配置解析就会报错。
- Tab 和空格混用:配置文件最好全用空格缩进,不要混入 Tab。Corefile 对缩进敏感,Tab 在某些版本里会导致解析异常。
- ConfigMap 的 YAML 缩进灾难:Corefile 是 ConfigMap 里
data.Corefile字段的值。在 YAML 中它通常是一个块标量:
apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: Corefile: | .:53 { errors health ready kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa } forward . /etc/resolv.conf cache 30 loop reload loadbalance }注意块标量|后面的内容必须保持 Corefile 原有的缩进层级。如果在kubectl edit时不小心把前面的空格也改了,Corefile 的 zone 名可能变成.:53或者解析层级整个错乱。
- 插件版本不匹配:升级 CoreDNS 镜像版本后,某些插件参数的可选值会有变化。比如旧版本里
forward的某些写法到了新版本可能不再被容忍。所以不熟悉某个插件时,不要随意在 Corefile 里追加参数。
5.3 验证配置的两种安全姿势
改 Corefile 前,强烈建议先验证语法。最简单的方式是本地用容器跑一遍:
docker run --rm -v $PWD/Corefile:/etc/coredns/Corefile coredns/coredns:1.11.1 -conf /etc/coredns/Corefile如果它能正常起一个监听 53 端口的进程并且不退出,说明语法没问题。如果报错,正好可以在本地迭代修正,而不是把坏配置直接打到集群里。
没有本地容器环境时,也可以在集群中用一个一次性 Pod 挂载同一个 Corefile 试运行。注意 CoreDNS 的启动参数里明确指定了配置文件路径-conf /etc/coredns/Corefile,如果修改 Deployment 时把 volumeMounts 改坏了,CoreDNS 会直接找不到配置文件,表现同样是 CrashLoopBackOff。
还有一个实操细节:修改 ConfigMap 后,CoreDNS 的 reload 插件可能自动感知并重新加载配置。但如果新配置有错,reload 失败后 Pod 进入 CrashLoop,回滚前还得等退避周期。所以我的习惯是:改之前先把当前 ConfigMap 备份一份,改完再手动 rollout restart,这样在任何时候都能快速回滚。
6. 节点环境在背后使绊子:权限、SELinux、架构不匹配
有时候 Corefile 没问题、内存也没爆,崩溃现场却出现在环境层面。这类问题占比不高,但一出现就容易让人摸不着头脑。
6.1 别忽略 Permission denied:SELinux 与只读根文件系统
CoreDNS 容器启动时需要读取 ConfigMap 挂载进来的 Corefile。如果节点上的 SELinux 处于 enforcing 模式,而策略没有正确允许容器访问这个挂载文件,容器进程就会报权限类错误,比如:
open /etc/coredns/Corefile: permission denied这种时候可以先临时把节点 SELinux 切到 Permissive 模式做个对照测试。如果问题消失,说明确实是 SELinux 策略的问题,测试完再把模式恢复 enforcing。然后根据日志里的 AVC 审计记录调整文件标签或者放行策略。
另一个类似的坑是容器 securityContext 里设置了readOnlyRootFilesystem: true,而 CoreDNS 的某些插件需要写临时文件或访问可写目录。这种情况可以通过单独挂载一个 emptydir 目录来满足写需求,或者区分清楚哪个目录必须可写,不要把整个根文件系统设成只读。官方默认的 CoreDNS Deployment 一般没有这个限制,倒是在一些加固过的集群里会遇到。
6.2 镜像架构不对也能伪装成 CrashLoopBackOff
如果你在一个混合架构的集群里部署 CoreDNS,比如一部分节点是 arm64、一部分是 amd64,拉取镜像时如果没有通过镜像仓库的 manifest 列表自动选择正确架构,容器启动就会出现exec format error。
故障表现是:Events 里能看到standard_init_linux.go:... exec format error,容器很快退出,Pod 同样进入 CrashLoopBackOff。这跟配置完全无关,纯粹是镜像与节点 CPU 架构不匹配。
解决办法是使用支持多架构的镜像,或者按节点架构指定对应 tag。排查时如果发现日志里什么都没有、退出码也很奇怪,记得去 Events 里看一眼是不是架构问题。
6.3 什么时候不是 CoreDNS 的错
还有一种情况,很容易被误拿来和 CrashLoopBackOff 混为一谈:CoreDNS 状态是Running,但集群里的域名解析还是失败。这个时候就别再盯着 CoreDNS 崩溃原因了,问题大概率在链路上:
- 看 CoreDNS Service 的 Endpoints 是否正常;
- 看 kubelet 的
--cluster-dns是否和 CoreDNS Service 的 ClusterIP 一致; - 看节点上的 kube-proxy 是否正常工作,iptables/ipvs 规则有没有到 CoreDNS Pod 的通路;
- 看网络插件是否健康,Pod 与 Service 之间的连通性是否正常。
这类问题表现为"解析超时"而不是"容器崩溃",和 CrashLoopBackOff 属于完全不同的分叉路。先确认方向,再动手,能省掉大量无效排查。
7. 恢复确认与防复发:验证清单和运维纪律
问题修完之后,不能只看 Pod 变成 Running 就收工。CrashLoop 恢复后的验证,以及后续的防复发措施,才是整个排障流程的收尾。
7.1 解除 CrashLoop 后的完整验证清单
建议按顺序跑一遍:
kubectl -n kube-system rollout status deployment/coredns kubectl -n kube-system get pods -o wide | grep coredns确认 Deployment 达到期望副本数后,再实际解析几个代表性域名。
可以用一个临时 Pod 做协议级验证:
kubectl run dns-check --image=busybox:1.28 --restart=Never --rm -it -- sh进去之后:
nslookup kubernetes.default.svc.cluster.local如果你的 busybox 版本没有 nslookup,也可以改用:
getent hosts kubernetes.default.svc.cluster.local同时测试一个外部域名的解析,前提是集群网络能访问该域名对应的上游 DNS。内部和外部两类查询都通过,才算真正把链路验证完整。
如果要检查 CoreDNS 健康端点,可以直接从节点访问 Pod IP 的 8080 健康端口,或者在 CoreDNS Pod 里用 wget 请求/health。注意默认健康端口是 8080,readiness 探针走的是 8181 的/ready,这两个别搞混。
7.2 防复发:把这条路固化进日常运维
恢复只是第一步,我更想说的是后续纪律。
第一,节点 DNS 配置要有统一规范。不要让/etc/resolv.conf随意出现127.0.0.1或回指集群 DNS 的配置,尤其是自动化脚本改完节点 DNS 后,最好自动检查一遍是否成环。
第二,Corefile 变更必须有验证流程。最简单的做法是把"本地容器验证配置"变成一个固定步骤,哪怕只是手动跑一下,也能拦住九成语法问题。
第三,监控要补齐。CoreDNS 的 metrics 被暴露在 9153 端口,通过 Prometheus 采集后,盯住三个关键指标:
- 可用副本数是否低于预期;
- 进程实际内存 RSS 是否接近 limit;
- P99 解析延迟是否异常上升。
一旦这三个指标出现任何一项异常,都值得尽早介入。
第四,副本数至少两个,最好反亲和到不同节点。大集群里如果外部域名查询比例特别高,可以考虑 NodeLocal DNSCache,但要记住配置纪律:让本机缓存的内部域名回指 CoreDNS,外部域名走真实上游,不要搞成双向回环。
我自己排查这类问题的固定顺序是:先logs --previous看有没有 Loop,再看 describe 里的 Last State 是不是 OOMKilled,然后检查 Corefile 语法,最后才往权限和架构方向看。这个顺序基本能覆盖百分之九十以上的 CrashLoopBackOff 场景。如果这次遇到的问题不在上面任何一种,大概率就属于网络链路层面的旁路问题了。把这套排查路径放进自己的运维手册里,下次再见到CrashLoopBackOff,心态会稳很多。