有阵子我们线上一个服务发布后,健康检查隔三差五失败一次,重启Pod又自己好了。折腾了两天,最后定位到是Pod的DNS解析超时——CoreDNS实例扛不住突发查询,Pod这边重试又不够快,健康检查就跟着抖。从那以后我就养成了一个习惯:凡是Pod的网络不对劲,先去翻它的dnsPolicy和resolv.conf,而不是一上来就抓包。
Kubernetes里的Pod DNS策略,看起来只是PodSpec里一行配置,实际它决定了容器内的整套域名解析路径:resolv.conf怎么写、搜索域怎么拼、查询先打到谁头上,全由这几个字段控制。今天这篇文章我想把Pod DNS策略这件事从原理到实操整个过一遍,包括四个内置策略的分工、dnsConfig的合并规则、真实场景怎么选型、以及排查DNS问题时我总结的完整思路。适合刚接触K8s的人建立整体认知,也适合已经在生产环境碰到解析故障的人直接对照排查。
1. 四个内置策略的分工,本质是kubelet在替你写resolv.conf
1.1 先记住一个关键事实:策略最终落在容器里的resolv.conf上
很多人把“Pod DNS策略”理解成一个开关,选一个模式就完了。其实所有策略最终都体现在一件事上:kubelet在创建Pod时,怎么往容器里写一份/etc/resolv.conf。你选了dnsPolicy,等于告诉kubelet:这份文件的内容从哪里来,要不要加上集群的nameserver和搜索域,要不要允许Pod自带自定义配置。
容器里跑的程序,不管是C库的getaddrinfo,还是Java、Go之类语言的运行时,最终都要读这同一份resolv.conf。文件里三个核心字段决定了后续的一切:
- nameserver:DNS服务器的IP,查域名时先向它发请求;
- search:搜索域列表,当查询的域名不是完整域名时,会拿原域名加上这些后缀挨个尝试;
- options:一些解析行为参数,比如ndots、timeout、attempts。
Kubelet自己并不解析域名,它只是把这份文件写好。真正解析动作发生在容器进程、glibc resolver和nameserver之间。所以排查DNS问题,第一步永远是看这份文件到底被写成了什么样子。
1.2 四个内置策略逐个拆解
Kubernetes在PodSpec里通过dnsPolicy字段暴露了四种策略:Default、ClusterFirst、ClusterFirstWithHostNet、None。我按出现频率和实用价值逐个说。
Default的意思是:容器里的resolv.conf直接复制Pod所在节点的这份文件,完全不注入集群的nameserver和搜索域。也就是说,节点上系统DNS怎么配,Pod就怎么配。看起来省事,但它有个致命副作用:容器里解析Service名、跨namespace的集群域名几乎总是失败,因为节点resolv.conf里根本没有cluster.local这个搜索域。
ClusterFirst是默认值,也是绝大多数业务Pod应该保持的策略。kubelet会把集群DNS服务的ClusterIP(通常是CoreDNS的Service IP)作为nameserver,search域则附带三组:当前namespace的svc.cluster.local、全局的cluster.local、以及节点原来的搜索域。这样Pod既能解析同namespace的Service短名,也能解析跨namespace的全限定域名。
ClusterFirstWithHostNet是专门给hostNetwork: true的Pod准备的。当Pod使用宿主机网络命名空间时,如果仍然用ClusterFirst,理论上也能解析集群域名,但宿主机上的网络配置多而杂,再加上统一管理dnsPolicy的需求,官方就单独设计了这个策略,让hostNetwork的Pod也能保持ClusterFirst的语义。实际部署DaemonSet类的采集Agent、监控组件时经常用到。
None是彻底的手工模式。选了None之后,kubelet不会再注入任何集群DNS信息,Pod必须同时在dnsConfig字段里提供完整的nameserver、search和options,否则容器里只会拿到一份几乎空的resolv.conf。这个策略适合边界集群、多集群互联、或者你希望Pod完全用自己的DNS解析体系而不走集群CoreDNS的场景。
1.3 一张表看懂四种策略的差异
| 策略 | resolv.conf来源 | 是否注入集群nameserver | 自动添加cluster.local搜索域 | 典型场景 |
|---|---|---|---|---|
| Default | 节点文件原样复制 | 否 | 否 | 基本不推荐;特殊离线环境可考虑 |
| ClusterFirst | kubelet按集群配置生成 | 是 | 是 | 业务Pod默认值 |
| ClusterFirstWithHostNet | kubelet按集群配置生成,面向hostNetwork | 是 | 是 | hostNetwork的Agent/DaemonSet |
| None | 完全由dnsConfig决定 | 取决于dnsConfig | 取决于dnsConfig | 多集群、自主DNS、Mesh场景 |
我见过不少人把dnsPolicy随手设成Default,理由是“让Pod跟宿主机走一样的DNS,省得配集群DNS”。结果就是业务里Service名解析不了,应用从启动到拉起集群内依赖全部报错。所以除非你有非常明确的理由,否则业务Pod一定要坚持ClusterFirst。
2. dnsConfig自定义参数:覆盖与合并的边界要拿捏准
2.1 dnsConfig里能写什么
dnsPolicy只是一个大方向,落到细节还要靠spec.dnsConfig做微调。这个字段可以写三类内容:
- nameservers:自定义DNS服务器,最多三个,可以是IPv4也可以是IPv6;
- searches:自定义搜索域列表,最多六个;
- options:解析行为参数,每个参数是一组键值对,可以写多个。
这三个字段和dnsPolicy不是互相独立的关系,而是有明确的合并规则。规则不复杂,但官方文档写得很零散,很多人就是栽在这上面。
先说nameservers。当dnsPolicy是ClusterFirst或ClusterFirstWithHostNet时,你写的nameservers会直接覆盖默认的集群DNS地址。也就是说,如果你在dnsConfig里写了一个自建DNS服务器,容器的resolv.conf里nameserver就变成它了,不再有CoreDNS的ClusterIP。这时如果还希望集群内Service域名能解析,必须把集群DNS地址也手动放回nameservers列表里,或者让自建DNS具备递归转发cluster.local的能力。
再看searches。它与nameservers不同,是追加而不是覆盖。kubelet会把默认的集群搜索域放在最前面,然后把你自定义的搜索域追加在后面。这个设计其实很人性化——你只想“加一个内部域名后缀”,它不会动你原有的集群解析逻辑。
最后是options。它跟nameservers一样是覆盖语义,你写什么options,resolv.conf里就是什么options,默认的ndots:5会被你顶掉。所以如果你在dnsConfig里写了一个ndots:1,那容器的解析行为就会从“5个点以下先拼搜索域”变成“1个点以下才拼搜索域”,两者差异很大。这也是容易出问题的地方,后面排查部分我会再展开。
2.2 一份我经常拿来用的配置模板
假设你有一个环境,既想保留集群内Service的正常解析,又希望Pod优先访问企业内部DNS服务器,同时还想优化对外查询的等待时间。可以用下面这个模板:
apiVersion: v1 kind: Pod metadata: name: app-with-custom-dns namespace: default spec: containers: - name: app image: busybox:1.36 command: ["sleep", "3600"] dnsPolicy: ClusterFirst dnsConfig: nameservers: - 10.96.0.10 - 10.20.30.40 searches: - corp.internal options: - name: ndots value: "1" - name: single-request这里nameservers我写了两项:第一项10.96.0.10是集群CoreDNS的ClusterIP,第二项10.20.30.40是企业内部DNS。因为nameserver会覆盖默认值,所以我必须把集群DNS也显式写回来,否则namespace里的Service解析就断了。searches我追加了corp.internal,这样Pod里访问短主机名时,除了会尝试svc.cluster.local和cluster.local,还会顺带尝试corp.internal后缀。options我设了ndots:1和single-request,分别减少搜索域尝试次数和规避并发A/AAAA查询的问题。
还需要注意一点:nameserver最多三个,但实际使用中建议只放两个,因为当你有多个nameserver时,glibc resolver对第一个超时不会立刻切换,而是会按一定规则轮询,三个nameserver反而可能让单个查询的最坏等待时间变长。
2.3 options的语义:ndots、timeout、attempts别乱调
options里最常被讨论的是ndots。它控制的是“什么时候算完整域名”。比如ndots:5,意思是域名里点数少于5个时,resolver先把域名挨个拼上search域去查,全部失败后再拿原域名直接查。默认的ndots:5是Kubernetes为了兼容服务短域名场景而设的:像mysvc这样不带点的名字,必须拿到svc.cluster.local后缀去做尝试,才能实现Service短名解析。
但这个默认值对访问公网域名很不友好。假如你的程序要访问api.example.com,它只有2个点,低于5,于是glibc会先尝试api.example.com.default.svc.cluster.local、api.example.com.svc.cluster.local、api.example.com.cluster.local等一系列后缀,每个都可能在CoreDNS那里产生一次DNS查询,等全部试完再回到原域名。在故障场景下,每一轮尝试都可能在等待超时,整体延迟就会被成倍放大。
这就是为什么很多团队会在dnsConfig里把ndots调成1或2。但代价是要小心:ndots:1意味着像mysvc这种不带点的主机名不会自动拼集群搜索域,得靠程序里写全限定名,比如mysvc.default.svc.cluster.local,或者确保查询的名字自带足够的点。我之前在一个项目里就是把ndots改成1,结果某个老应用还在用短名调服务,差点出事故。所以改这个参数前,务必把业务代码里所有内部服务调用方式都盘一遍。
timeout和attempts的语义比较直白:timeout是单次查询的等待时间,attempts是总尝试次数。Kubernetes的默认值不是写在resolv.conf里的,而是glibc自己的默认值:timeout 5秒、attempts 2次。最坏情况下一个查询可能等10秒以上。如果你的Pod里有健康检查、又依赖域名解析,这个时间大概率会打爆探针阈值。合理做法是在dnsConfig的options里显式设置timeout: 1和attempts: 2,缩短单次等待,让失败快速返回。
3. 真实场景下的选型:什么时候动策略,什么时候别动
3.1 集群内解析不够用不等于要换策略
我见过很多人在一个很常见的场景里纠结:集群内部服务要解析,外部还有一些设备或老系统要解析,是不是该把dnsPolicy改成Default?答案通常是不要。
Default会直接丢掉集群搜索域,连带把全部集群内解析能力也丢了。实际上,绝大多数“既要内部又要外部”的需求,用ClusterFirst加dnsConfig的searches就能解决。你只需要在searches里追加外部域的搜索后缀,让resolver在尝试完集群域之后,再尝试外部域即可。不要动不动就换策略。
如果连追加搜索域都不满足,比如要求Pod在解析特定域名时直接走某个内网DNS、解析其他域名时走另一个DNS,这才轮到用None。Kubernetes原生的dnsPolicy并不支持按域名分流,一旦走到None,所有nameserver和search都得自己维护,维护成本会明显上升。所以大多数情况下,ClusterFirst + dnsConfig 是性价比最高的解法。
3.2 hostNetwork的Pod要选ClusterFirstWithHostNet
如果你的Pod是DaemonSet形态的Agent,比如监控、日志采集、CNI相关组件,往往需要设置hostNetwork: true,让Pod与宿主机共享网络栈。这时候dnsPolicy如果还是ClusterFirst,虽然也能工作,但会遇到两个实际困扰。
第一个困扰是,这类Pod通常还要依赖节点的resolv.conf来解析一些节点级域名,比如云厂商metadata域名、内部NTP域名等。ClusterFirst生成的resolv.conf虽然会保留节点原来的搜索域,但nameserver已经被替换成了集群DNS,部分节点级域名的解析链会变长,甚至出现解析权限问题。
第二个困扰是,hostNetwork的Pod数量一多,每个Pod都可能直接用宿主机的53端口与集群DNS通信,端口冲突、连接复用问题都容易冒出来。而ClusterFirstWithHostNet设计上就是为了让hostNetwork Pod继续保持ClusterFirst的解析语义,因此凡是hostNetwork的Pod,我建议一律优先选它。
3.3 多集群和边界场景用None手搓
跨集群通信、边界节点、或者Pod希望完全绕过K8s自带DNS体系的场景,None才是真正的主场。选None之后,resolv.conf完全由dnsConfig决定,你可以把多个nameserver按优先级写进去,也可以任意组合搜索域。
举一个实际例子:某公司有一个边界集群,Pod既要访问另一个集群里的服务,又要访问公司内部的物理机服务。简单方案是让CoreDNS做forward到两个上游,但对这种独立小集群来说,很多人干脆用None直接在Pod里写多个nameserver,省去维护CoreDNS逻辑的精力。不过要提醒你,None模式下K8s不会帮你注入cluster.local的搜索域,凡是需要解析集群内Service的域名,全都得在dnsConfig的searches里手动写全。写的时候顺序也很重要,搜索域排在越前面,被尝试的优先级越高,如果把公司的域放在cluster.local前面,会让一批本应走集群内解析的请求先去公司DNS绕一圈。
3.4 NodeLocal DNSCache不改变策略,而是叠加在ClusterFirst之上
生产环境Pod密度上去之后,CoreDNS很容易成为瓶颈。很多人问:是不是该换成别的dnsPolicy?其实官方推荐的方案是NodeLocal DNSCache,它的原理是每个节点上跑一个本地DNS缓存,监听169.254.20.10这个地址,Pod通过dnsConfig把nameserver指向它,本地缓存再决定是直接返回还是向上游CoreDNS查询。
这里要注意,NodeLocal DNSCache并没有引入新的dnsPolicy,它只是在ClusterFirst的基础上,通过dnsConfig把nameserver地址替换成本地地址。Pod的搜索域、解析语义跟ClusterFirst完全一致。如果你的集群已经装了NodeLocal DNSCache,业务Pod不需要改dnsPolicy,通常只需要在dnsConfig里把nameserver指向169.254.20.10;如果通过webhook默认注入,那就更无感了。
4. 排障全流程:从一份resolv.conf开始
4.1 第一步,确认Pod拿到的真实DNS配置
排查DNS问题,我从来不会一上来就怀疑CoreDNS挂了。第一件事永远是看Pod里实际的resolv.conf:
kubectl exec -it <pod-name> -n <namespace> -- cat /etc/resolv.conf正常ClusterFirst模式下,输出大概是这样的:
nameserver 10.96.0.10 search default.svc.cluster.local svc.cluster.local cluster.local options ndots:5如果你看到的nameserver不是集群DNS的ClusterIP,或者search域缺少svc.cluster.local和cluster.local,那策略本身可能就有问题,或者有某个准入控制器在偷偷改Pod。如果nameserver指向169.254.20.10之类的地址,说明集群启用了NodeLocal DNSCache。
这一步还能顺带确认另一个容易被忽略的问题:Pod是否真的按期望的dnsPolicy被创建了。有些情况下Deployment的模板写的是ClusterFirst,但实际创建的Pod被某个控制器或webhook改成了Default,不看到真实文件是发现不了的。
4.2 第二步,区分策略问题还是上游DNS问题
拿到resolv.conf之后,第二步是用查询工具做分级验证。我习惯在目标Pod里依次执行三组测试。
第一组,查集群内域名:
kubectl exec -it <pod-name> -n <namespace> -- nslookup mysvc.default.svc.cluster.local如果这个查不通,说明pod到CoreDNS的链路或者CoreDNS本身可能有问题。第二组,查外部域名:
kubectl exec -it <pod-name> -n <namespace> -- nslookup www.example-cluster.local如果外部域名能通、内部域名不通,问题几乎锁死在CoreDNS的转发配置或搜索域上。第三组,绕过Pod内的resolver,直接指定集群DNS地址查:
kubectl exec -it <pod-name> -n <namespace> -- nslookup mysvc.default.svc.cluster.local 10.96.0.10这组能直接验证CoreDNS是否在正常监听和响应。如果直接指定IP能查通,但容器内默认查不通,就要重点关注resolv.conf的search域和ndots配置了。
如果需要看CoreDNS日志,可以用:
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=200注意看SERVFAIL、NXDOMAIN、connection refused、i/o timeout这些关键字。SERVFAIL一般意味着CoreDNS上游有问题,NXDOMAIN更多是搜索域拼接不正确,connection refused则经常是CoreDNS副本数太少、实例在崩溃重启。
4.3 第三步,我踩过的几个高频坑
这几个坑在生产环境反复出现,我把现象、原因和处理方式整理成了表格,方便对照:
| 现象 | 根因 | 处理方式 |
|---|---|---|
| 访问公网域名慢,看日志有一堆NXDOMAIN | 默认ndots:5导致每次查询都先拼search域,多轮尝试被放大 | dnsConfig里设ndots:1,程序内部改用FQDN访问 |
| CoreDNS请求量大,实例单副本容易打满 | Pod数量多且QPS高,CoreDNS无缓存或副本太少 | 调大CoreDNS副本数,开启/调大cache插件TTL |
| 容器的A/AAAA并发查询偶尔超时 | 特定内核与glibc组合对并行IPv4/IPv6查询支持不好 | dnsConfig加options: single-request或single-request-reopen |
| 部分业务域名解析失败,但手动nslookup能通 | search域列表被截断或拼错,实际查询走的域名不对 | 检查searches数量,精简搜索域,必要时用FQDN |
| Pod启动后健康检查连续失败 | 启动时依赖域名解析,resolv.conf的timeout默认5秒且attempts=2,最坏等待过长 | dnsConfig加options: timeout 1、attempts 2 |
这里特别提一下ndots的坑。默认ndots:5对集群内短域名是友好的,但对对外通信是灾难。有一个项目曾经因为查询一个带两个点的公网域名,每次请求都先往cluster.local后缀上试好几轮,遇到上游故障时单次解析能拖到十几秒,业务接口跟着几百毫秒的超时全崩。最后就是在dnsConfig里加了ndots:1,再把程序里的内部服务调用全部改成全限定域名,才彻底解决。
还有一个很容易被忽略的点:如果CoreDNS开启了cache插件,第一次查询某个域名慢是正常的,之后走缓存会明显变快。如果你用上面第三组直接指定IP的方式查通了一次,容器内再查还是失败,就要怀疑是不是上游配置不一致,而不是单纯看单次查询结果。
5. 容易被忽略的周边细节:hosts文件、缓存与超时
5.1 /etc/hosts由kubelet统一维护
很多人以为dnsPolicy只影响resolv.conf,其实kubelet还会在创建容器时生成并管理容器的/etc/hosts。这个文件里会写入Pod的hostname、Pod名称和PodIP的映射,而且当PodIP变化时,kubelet会自动更新。
这个机制带来一个很实用的效果:容器内通过hostname访问自己、通过Pod名称访问同副本的Pod,通常都不走DNS,直接查hosts就可以。但也带来了限制:你在容器里手动改/etc/hosts在某些运行时下是无效的,因为kubelet可能在下一次同步时把它覆盖回原状。所以不要指望通过改hosts来做临时解析,正确做法是调整dnsConfig或在业务侧做hosts映射。
如果集群用了Multus之类多网络插件,Pod有多个网卡多个IP,kubelet对hosts的处理会复杂一些,但核心逻辑不变:它管理的主机名映射只与主IP相关。排查时如果发现某个IP在容器里ping不通,先确认它是否在hosts里,或者切到resolv.conf的查询路径去诊断。
5.2 不同程序解析resolv.conf的方式不一样
容器里跑的进程读取resolv.conf的方式并不完全一致。基于glibc的程序会严格遵守search域和ndots的逻辑;基于musl libc的程序(比如Alpine里编译出来的二进制)解析行为更简单直接,对search域的兼容性也差一些;Go程序如果用了纯Go的resolver,在特定版本下对/etc/resolv.conf的处理逻辑也和cgo版本有差异。
这意味着同一个resolv.conf,不同程序解析同一域名可能得到不同结果。排查时不要只盯着一份文件说“配置明明对的”,要试着用跟业务进程一致的方式去验证。比如业务进程是Go写的,那更贴近的验证方式是写一小段Go代码调net.LookupHost,而不是依赖busybox里的nslookup,后者基于C库,可能与业务进程行为不一致。
5.3 超时和重试策略值得业务侧配合
最后聊一个运维视角的问题。很多团队把DNS超时优化全部寄托在dnsConfig的options上,但业务代码同样重要。健康检查、接口调用、数据库连接初始化,这些链路如果依赖域名解析,最好在业务侧设置与应用场景匹配的解析超时,而不是让glibc的默认行为反复重试。
我在实际项目里的习惯是:网络调用类的域名解析,把dnsConfig里的timeout设成1秒、attempts设成2次;连接池初始化这类可以容忍稍慢的场景,把timeout放宽到2秒、attempts保持2次。同时,凡是允许配置的客户端,都尽量把连接超时单独设置,避免被系统级DNS超时连带拖垮。
还有一个经验是:不要在同一批Pod里混用多种dnsPolicy。如果大部分Pod是ClusterFirst,少数Pod为了省事改成Default,一旦CoreDNS出问题,Default的那批反而最稳,ClusterFirst的那批先开始报错。这种“半挂”状态很容易让排障人员误判故障范围,以为是单点问题,实际是策略差异放大了故障。所以策略调整最好跟着变更管理走,别零散改。
如果有条件,我建议每半年花一点时间做一次Pod DNS配置巡检。方法很简单,用kubectl批量导出所有工作负载的dnsPolicy和dnsConfig,筛出非默认配置的Pod,核对一遍是否还对应当前需求。很多历史遗留的Default策略、过期的search域,就是在这样的巡检里被发现的。DNS策略这件事,越早统一规范,后续出问题的可能性越小。