NodeLocal DNS 实战:本地缓存如何优化集群 DNS 解析性能
1. 为什么要在集群里单独折腾一个本地 DNS很多团队在集群规模还小的时候DNS 解析这块基本不会出问题CoreDNS 两三个副本跑着QPS 也不高谁都不会多看一眼。等到节点数上了几十台、Pod 数量破千各种诡异的解析超时、5 秒延迟、偶发的no such host就开始冒头这时候才会有人回头去翻 CoreDNS 的监控发现延迟曲线在高峰期像心电图一样抖。NodeLocal DNS 就是在这个阶段被搬上台面的东西它不是什么新鲜概念本质上是把 DNS 解析的第一跳从跨节点的 CoreDNS 挪到每个节点本机的一个缓存代理上让绝大多数解析请求在节点内部就消化掉。先把话说清楚NodeLocal DNS 是一个跑在节点上的 DNS 缓存服务通常以 DaemonSet 形式部署监听一个固定的本地地址社区默认用169.254.20.10这个地址属于链路本地网段不会和常规业务网段冲突。它做的事情很朴素——Pod 的 DNS 请求先打到本机的这个缓存命中就直接返回没命中才由它转发给集群里的 CoreDNS拿到结果后缓存下来。就这么一个简单的缓存层解决的是三个实打实的痛点一是跨节点 DNS 查询带来的网络延迟和 conntrack 表压力二是 UDP 协议下 DNS 请求丢包导致的 5 秒超时重试三是 CoreDNS 在流量高峰时的负载集中问题。这篇文章适合谁看如果你正在维护一个中等以上规模的集群被 DNS 解析延迟困扰过或者你正准备给集群做网络层面的优化那这篇内容基本能覆盖你从原理理解到落地部署的全过程。如果你只是跑个小集群玩玩那这东西的收益可能不明显但了解一下它的工作机制也没坏处毕竟排查 DNS 问题时这些知识都用得上。下面我会按“为什么这么设计——核心机制怎么跑——怎么部署——怎么排错”的顺序展开中间穿插我自己踩过的坑和实测数据尽量让你看完就能动手。2. 核心机制拆解本地缓存到底怎么接管解析流量2.1 请求路径的变化与缓存命中逻辑要理解 NodeLocal DNS 的价值得先看清楚请求路径到底变了什么。默认情况下Pod 里的/etc/resolv.conf指向的是集群 DNS 服务的 ClusterIP比如10.96.0.10这个 IP 是虚拟的请求经过 kube-proxy 的 iptables 或 ipvs 规则做 DNAT转发到某个 CoreDNS Pod 所在的节点。也就是说一个 DNS 查询要跨节点走一趟中间还涉及 NAT 转换和 conntrack 记录。节点越多、CoreDNS 副本分布越不均匀这个跨节点跳转的不确定性就越大。NodeLocal DNS 接管之后路径变成Pod 请求先到本机169.254.20.10这个地址由节点上的一个虚拟网卡通常是nodelocaldns这个 dummy 接口承载请求直接被本机的缓存进程接住。命中缓存的话整个解析在节点内部闭环一个网络包都不用出节点。没命中的话缓存进程再以普通 UDP 请求转发给 CoreDNS 的 ClusterIP拿到结果写进缓存并返回给 Pod。这里有个关键设计值得说为什么用169.254.20.10而不是别的地址链路本地地址段169.254.0.0/16在 RFC 3927 里定义专门用于单链路通信不会和路由表里的常规网段打架也不需要额外配置路由。选这个段是社区经过权衡的你如果非要用别的地址也能改但没必要跟着默认走最省心。缓存命中率是这套方案的核心指标。实测下来一个正常运行的业务集群DNS 查询里重复查询的比例相当高——服务发现场景下同一个 Service 名会被反复解析TTL 内完全可以走缓存。我经手过的一个集群接入 NodeLocal DNS 后缓存命中率稳定在 85% 以上CoreDNS 的 QPS 直接降了一个数量级。这个数字不是拍脑袋的你可以自己在缓存进程的监控里看到cache_hits和cache_misses两个计数器算一下比例就清楚了。2.2 为什么它能缓解 UDP 丢包导致的 5 秒超时DNS 默认走 UDPUDP 本身不保证送达丢包后客户端要等超时重传。Linux 下 glibc 的 resolver 默认超时是 5 秒重试 2 次这就是那个臭名昭著的“DNS 解析卡 5 秒”的来源。跨节点查询时丢包概率受网络拥塞、conntrack 表满、网卡队列溢出等因素影响一旦丢了业务侧就是实打实的 5 秒卡顿。NodeLocal DNS 在这块的改善有两层。第一层是路径短了请求不出节点经过的网络设备少丢包概率自然下降。第二层更关键缓存进程和 Pod 之间是本机通信即使偶尔丢包重传的往返时间也是微秒级不会像跨节点那样叠加网络抖动。而且社区版本的实现里对 UDP 的处理做了优化配合 TCP 回退机制进一步降低了超时概率。注意NodeLocal DNS 并不能 100% 消除 5 秒超时它只是把概率压到很低。如果你的集群里还有大量解析超时得回头查 conntrack 参数、网卡队列、CoreDNS 本身的健康度别指望一个缓存层包治百病。2.3 与 CoreDNS 的分工边界很多人会误以为部署了 NodeLocal DNS 就可以把 CoreDNS 撤了这是错的。NodeLocal DNS 是缓存层不是权威解析层缓存未命中时它必须把请求转发给上游这个上游就是 CoreDNS。所以两者的关系是“前置缓存 后端解析”CoreDNS 依然是集群 DNS 的真正入口负责处理 Service 记录、外部域名转发、存根域配置这些逻辑。分工上有个细节要注意NodeLocal DNS 的转发目标配置里集群域比如cluster.local指向 CoreDNS 的 ClusterIP外部域名则可能直接走节点上的上游 DNS 或者也转发给 CoreDNS 再由它转发。具体怎么配取决于你的网络环境如果节点能直接访问外部 DNS让 NodeLocal DNS 直接转发外部查询可以减轻 CoreDNS 负担如果节点出网受限那就统一走 CoreDNS。这个选择在部署时要明确配错了会导致外部域名解析异常。3. 部署前的准备工作别急着 apply先把这些理清楚3.1 环境前提与版本兼容性检查动手之前有几件事必须先确认否则部署到一半卡住很浪费时间。首先是集群的 CNI 和 kube-proxy 模式NodeLocal DNS 依赖 iptables 规则把169.254.20.10的流量劫持到本机如果你的集群用的是纯 ipvs 模式且没有开启相应的本地地址处理需要额外确认规则是否生效。我遇到过 ipvs 模式下本地地址劫持不生效的情况排查了半天才发现是 kube-proxy 的配置问题。其次是内核参数。缓存进程要监听本地地址并处理大量 UDP 请求net.core.rmem_max和net.core.wmem_max这两个 socket 缓冲区参数建议调大默认值在高并发下容易导致 UDP 丢包。我一般会把它们设到26214400这个量级具体值根据节点规格调整内存大的节点可以再往上给。版本兼容性方面NodeLocal DNS 的镜像版本要和集群的 Kubernetes 版本大致匹配太老的镜像可能不支持新的配置项。部署清单里的image字段记得改成你实际能拉到的仓库地址很多团队卡在镜像拉取失败上尤其是内网环境。3.2 地址规划与冲突排查169.254.20.10这个地址虽然默认不冲突但部署前还是建议扫一遍确认节点上没有其他服务占用这个地址。检查方法很简单在节点上执行ip addr show看有没有接口已经绑了这个 IP再用ss -lunp | grep 169.254.20.10看端口占用情况。如果确实冲突了可以在配置里改localDNS地址但改完记得同步修改 Pod 的resolv.conf指向两边必须一致。还有一个容易忽略的点如果你的集群里有服务网格或者自定义的 DNS 拦截规则可能会和 NodeLocal DNS 的劫持规则打架。部署前把现有的 iptables 规则导出来看一眼重点看有没有对53端口或169.254.0.0/16段的特殊处理有的话提前协调好优先级。3.3 部署方式选型DaemonSet 还是手动铺标准做法是用 DaemonSet每个节点跑一个 Pod这样节点扩缩容时自动跟随管理成本最低。DaemonSet 的 Pod 需要hostNetwork: true因为它要监听节点的本地地址走 host 网络是最直接的方式。同时要给它足够的权限NET_ADMIN能力是必须的因为要操作 iptables 规则。有些团队出于安全考虑不想给 Pod 太高权限会考虑手动在节点上装二进制。这种方式我不太推荐节点多了之后版本管理、配置同步、故障替换都是麻烦事DaemonSet 的声明式管理优势太明显了。除非你的环境有硬性限制不允许 hostNetwork否则老老实实用 DaemonSet。4. 完整部署实操从清单编写到验证生效4.1 核心配置清单逐项说明部署清单主要包含几块ServiceAccount 和 RBAC缓存进程需要读取 Service 和 Endpoint 信息来做集群域解析、ConfigMap存 Corefile 配置、DaemonSet跑缓存进程。我挑几个关键配置项展开说。ConfigMap 里的 Corefile 是核心典型配置长这样cluster.local:53 { errors cache { success 9984 30 denial 9984 5 } reload loop bind 169.254.20.10 forward . 10.96.0.10 { force_tcp } prometheus :9253 }这里cache块的success 9984 30意思是成功结果最多缓存 9984 条、TTL 上限 30 秒denial 9984 5是失败结果缓存 5 秒。TTL 上限设 30 秒是个折中设太长会导致 Service 变更后解析更新不及时设太短缓存命中率上不去。30 秒这个值在大多数业务场景下够用如果你的服务变更非常频繁可以适当调低。forward后面的force_tcp是个值得注意的选项它强制转发给上游时用 TCP。为什么因为缓存进程到 CoreDNS 这一段如果还用 UDP丢包问题依然存在用 TCP 可以规避 UDP 丢包代价是稍微多一点开销。实测下来这个选项对降低超时很有帮助建议开着。DaemonSet 部分的关键配置spec: template: spec: hostNetwork: true dnsPolicy: ClusterFirstWithHostNet containers: - name: nodelocaldns image: 你的镜像仓库/nodelocaldns:版本 args: - -localip - 169.254.20.10 - -conf - /etc/Corefile - -upstreamsvc - kube-dns securityContext: privileged: true-upstreamsvc kube-dns这个参数让缓存进程自动发现 CoreDNS 的 Service不用硬编码 ClusterIP集群 DNS 服务改名或重建时不用改配置。privileged: true是为了操作 iptables如果不想给这么高权限可以细化成只给NET_ADMIN但实测有些环境细化后规则设置会失败稳妥起见先用 privileged 跑通再收紧。4.2 让 Pod 用上本地缓存resolv.conf 的改造部署完 DaemonSet 只是第一步Pod 的resolv.conf还指向原来的 ClusterIP得把它改成169.254.20.10。改法有两种一是改 kubelet 的--cluster-dns参数指向本地地址这样新建的 Pod 自动生效二是用 MutatingWebhook 动态注入。第一种简单直接但改 kubelet 参数需要重启 kubelet影响面大第二种灵活可以按命名空间灰度。我一般推荐改 kubelet 参数因为逻辑最简单出问题好排查。改之前先确认 kubelet 的启动参数里--cluster-dns当前值改成169.254.20.10后重启 kubelet然后新建一个测试 Pod进去cat /etc/resolv.conf确认nameserver已经是本地地址。注意已有的 Pod 不会自动更新需要重建才生效这点在滚动发布时要考虑到。提示改 kubelet 参数前先在单个节点上验证确认本地缓存工作正常再批量推。我见过一次性全改导致部分节点缓存进程没起来、Pod 解析全挂的事故灰度这一步不能省。4.3 验证缓存是否真正生效部署完怎么确认它真的在工作三个层面验证。第一层看进程状态kubectl get pods -n kube-system -l k8s-appnode-local-dns确认每个节点都有 Pod 且 Running。第二层看缓存命中缓存进程暴露了 Prometheus 指标默认:9253抓一下nodelocaldns_cache_hits_total和nodelocaldns_cache_misses_total命中率应该随时间上升。第三层做实际解析测试在 Pod 里dig一个 Service 名连续查两次第二次的响应时间应该明显更短微秒级同时观察 CoreDNS 的 QPS 是否下降。我习惯用一个简单的脚本做验证在测试 Pod 里循环解析同一个域名 100 次统计平均耗时和 P99。接入前 P99 可能在几十毫秒接入后应该降到个位数毫秒。这个对比数据是说服团队继续推进的最有力证据。5. 常见问题排查与避坑经验实录5.1 部署后解析反而变慢或失败这是最常见的反馈。先查缓存进程的日志kubectl logs看有没有forward失败、bind失败之类的报错。如果日志显示转发到上游超时检查forward配置里的上游地址是否正确以及节点到 CoreDNS 的网络是否通。有个隐蔽的坑如果集群用了网络策略NetworkPolicy可能把缓存进程到 CoreDNS 的流量拦了需要放行。还有一种情况是 iptables 规则没生效。在节点上iptables -t nat -L -n | grep 169.254.20.10看有没有对应的 DNAT 或 REDIRECT 规则没有的话说明缓存进程启动时设置规则失败通常是权限问题。把 Pod 的 securityContext 调成 privileged 再试。5.2 缓存命中率上不去命中率低说明大量查询是唯一的或者 TTL 太短。先看业务侧是不是每次请求都带随机子域名有些服务发现实现会这样这种没法缓存。再看 TTL 配置如果业务域名的 TTL 本身很短比如 5 秒缓存效果自然有限。可以适当调大cache块的 TTL 上限但要权衡解析实时性。另外检查一下是不是有大量 NXDOMAIN 查询。失败结果默认只缓存 5 秒如果业务在频繁查询不存在的域名比如某些 SDK 的探测逻辑这部分会持续穿透到 CoreDNS。可以在监控里看denial相关的计数确认是不是这个原因。5.3 节点重启后缓存进程异常节点重启后 DaemonSet 的 Pod 会重建但有时候会出现 Pod Running 却解析不通的情况。排查下来多半是 iptables 规则残留或者本地地址没绑上。处理办法是删掉 Pod 让它重建重建时会重新设置规则和绑定地址。如果反复出现检查节点的启动顺序确保网络插件先于缓存进程就绪。下面这张表是我整理的常见问题速查遇到问题先对号入座现象可能原因排查动作解析全部超时缓存进程未启动或规则未生效查 Pod 状态、iptables 规则部分域名解析失败上游转发配置错误或网络策略拦截查日志、测上游连通性解析变慢但能通缓存未命中且上游响应慢看命中率、查 CoreDNS 负载节点重启后异常规则残留或地址未绑定重建 Pod、查接口地址外部域名解析失败外部转发路径配置不当检查 Corefile 的 forward 规则5.4 资源占用与调优缓存进程本身很轻量内存占用主要取决于缓存条目数默认配置下每个节点几十 MB 级别。但如果你的集群 Service 特别多、查询模式复杂缓存条目可能涨上去需要关注内存指标。CPU 占用在高峰期会有波动一般单核的百分之几到十几节点规格够的话不用特别担心。调优上我建议关注两个参数cache的条目上限和 socket 缓冲区。条目上限设太小会导致频繁淘汰、命中率下降设太大占内存根据实际查询基数调整。socket 缓冲区前面提过高并发下必须调大否则 UDP 丢包会让缓存效果大打折扣。6. 上线后的收益评估与扩展思路部署完不能就这么放着得用数据说话。我一般会对比接入前后三个指标CoreDNS 的 QPS 曲线、业务侧 DNS 解析的 P99 延迟、以及节点上 conntrack 表的使用率。前两个直接反映缓存效果第三个反映跨节点 DNS 流量减少带来的连带收益。我经手的一个集群接入后 CoreDNS QPS 从峰值 8000 降到 800 左右业务侧解析 P99 从 45ms 降到 3msconntrack 使用率下降了约 20%这些数字拿去给团队汇报很有说服力。扩展思路上NodeLocal DNS 可以和 CoreDNS 的自动扩缩容配合缓存层挡住大部分流量后CoreDNS 的副本数可以适当减少节省资源。另外如果你的集群跨多个可用区可以考虑让缓存进程优先转发到同可用区的 CoreDNS 副本进一步降低跨区流量不过这需要额外的拓扑感知配置复杂度会上升按需评估。最后分享一个我在实际运维中的体会NodeLocal DNS 这类基础设施组件部署本身不难难的是上线后的持续观测和问题定位。建议从第一天就把缓存命中率、转发延迟、错误计数这几个指标接入监控大盘设好告警阈值。DNS 问题往往表现为业务侧的偶发超时没有监控数据支撑排查起来就是大海捞针。把观测做在前面后面省下的时间远超部署本身的花费。