Kubernetes 集群内 NodeLocal DNSCache(nodelocaldns)Addon 深度解析:CoreDNS 每节点缓存的设计、清单模板与配置注入
本文以 Kubernetes 仓库中的 nodelocaldns 附加组件为对象,系统讲解 NodeLocal DNSCache 的架构思路、nodelocaldns.yaml 清单中每个对象的职责、__PILLAR__ 模板变量的注入机制,以及它在 NetworkPolicy 环境下的连通性配置与负缓存(negative caching)调优要点。读完本文,你将能读懂该插件的完整部署清单,理解它如何把集群 DNS 查询收敛到本机缓存,并掌握直接基于 nodelocaldns.yaml 定制部署的每一个开关与参数。
一、NodeLocal DNSCache 解决什么问题
在未启用 NodeLocal DNSCache 的集群中,Pod 内发起的 DNS 查询经由 kubelet 下发的 --cluster-dns(即 kube-dns 或 CoreDNS 的 ClusterIP)离开节点,再由 kube-proxy 的 iptables/ipvs 规则做 DNAT 转发到 DNS 后端 Pod。这条路径存在两个已知痛点:
- conntrack 竞争丢包:在高并发下,来自本机 Pod 的大量 DNS 查询经 DNAT 回路时,可能触发 conntrack 的冲突丢包,表现为间歇性 DNS 解析失败;
- 跨节点转发开销与延迟:每次查询都要在宿主机网络栈走一圈转发,且 kube-dns 后端 Pod 的扩缩容、驱逐会直接影响解析可用性。
NodeLocal DNSCache 的解法是:在每个节点上把一份 DNS 缓存放到离 Pod 最近的地方。本仓库的 cluster/addons/dns/nodelocaldns/ 目录正是为此提供的官方 addon 实现。按照仓库 README.md 的说明:它会在集群所有节点上运行一个 node-local-dns Pod,该 Pod 以 CoreDNS 作为 DNS 缓存,以 hostNetwork: true 方式运行,并创建一个承载 链路本地地址(默认 169.254.20.10/32)的专用 dummy 接口来接收 DNS 查询;缓存实例在缓存未命中时才回源连接到集群 DNS(clusterDNS)。
从功能演进看,该特性在 release 1.15 进入 Beta,在 1.18 正式 GA——README 中明确记录了这一过程,本文基于仓库当前版本中的清单文件展开,其中的镜像与配置均以仓库实际内容为准。
二、addon 的构成与整体工作流
目录内只有两个文件,结构非常精简:
| 文件 | 作用 |
|---|---|
| cluster/addons/dns/nodelocaldns/nodelocaldns.yaml | 描述该 addon 全部资源的 YAML 模板,内含待替换的 __PILLAR__ 占位变量 |
| cluster/addons/dns/nodelocaldns/README.md | 使用与设计说明文档 |
清单被设计为"模板 + 变量替换"的形态:当 YAML 被复制到 master 节点时,配置脚本会完成变量代换(详见本文第五节)。最终落地的资源相互配合形成如下链路:
Pod(应用) --查询--> node-local-dns(:53, 169.254.20.10 本机)
├── 命中缓存 → 直接返回
└── 未命中 → kube-dns-upstream Service → 集群内 CoreDNS/kube-dns
节点上缓存层的存在使得绝大多数查询在本机完成,只有 miss 才回到集群 DNS 后端,这正是该插件名称中 "Node-Local" 的含义。
三、逐对象拆解 nodelocaldns.yaml
清单 nodelocaldns.yaml 共声明 5 个资源(以 --- 分隔),下面逐个解读。
3.1 ServiceAccount:node-local-dns
清单第 16–23 行创建了名为 node-local-dns 的 ServiceAccount,并打上 kubernetes.io/cluster-service: "true" 与 addonmanager.kubernetes.io/mode: Reconcile 标签。其中 Reconcile 模式是 addon-manager 的声明式管理语义——它保证该清单以"调和"而非"仅创建一次"的方式被管理,任何被手工篡改的字段都会被 addon-manager 纠正回模板定义的状态。
3.2 Service:kube-dns-upstream(回源入口)
第 25–47 行定义了一个名为 kube-dns-upstream 的 Service,selector 指向 k8s-app: kube-dns 的 Pod,同时暴露 UDP 53 与 TCP 53 两个端口。它的作用是为缓存实例提供稳定的回源端点:即使集群 DNS 后端滚动更新或 Pod IP 变化,缓存也始终通过该 Service 的 ClusterIP 访问后端,无需感知后端变动。
3.3 ConfigMap:node-local-dns(CoreDNS Corefile)
第 48–102 行是插件的核心——一段内置在 ConfigMap 中的 CoreDNS Corefile 配置,定义了四个服务器块(zone),对应节点缓存需要处理的四类查询:
① 集群域名 zone(__PILLAR__DNS__DOMAIN__:53,默认即 cluster.local):
__PILLAR__DNS__DOMAIN__:53 {
errors
cache {
success 9984 30
denial 9984 5
}
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
health __PILLAR__LOCAL__DNS__:8080
}
要点解读:
cache对成功应答(success)缓存 9984 条、TTL 30 秒;对否定应答(denial,即 NXDOMAIN 等)缓存 9984 条、TTL 仅 5 秒——这就是 README 强调的"负缓存 TTL 被压到最低 5 秒",细节见第七节;bind同时绑定在本地链路地址与集群 DNS Service IP 上,保证两类来源的查询都能命中;forward . __PILLAR__CLUSTER__DNS__并追加force_tcp,即集群内查询回源时强制走 TCP(规避大规模 UDP 查询在 conntrack 层面的丢包问题,也正是本项目要缓解的核心故障之一);health在本地链路地址的 8080 端口暴露健康检查端点,与 DaemonSet 的 livenessProbe 呼应。
② 反向解析 zone(in-addr.arpa:53 与 ip6.arpa:53,第 72–93 行):分别承接 IPv4、IPv6 的 PTR 反向查询,同样 cache 30 并强制 TCP 回源到集群 DNS。
③ 兜底外部 zone(. :53,第 94–102 行):处理所有其他域名查询,forward 目标是 __PILLAR__UPSTREAM__SERVERS__——即从 kube-dns ConfigMap 推导出的上游服务器集合。
3.4 DaemonSet:node-local-dns(缓存实例本体)
第 104–191 行定义核心 DaemonSet,关键字段如下表:
| 字段/配置 | 值 | 设计意图 |
|---|---|---|
updateStrategy.rollingUpdate.maxUnavailable |
10% | 节点滚动升级时最多同时不可用 10%,保障升级期间 DNS 基本可用 |
priorityClassName |
system-node-critical |
系统关键 Pod,优先级高于普通业务,避免被驱逐 |
hostNetwork |
true |
直接占用宿主机网络,监听链路本地地址 |
dnsPolicy |
Default(注释:Don't use cluster DNS) |
避免自身依赖集群 DNS 形成回环 |
| tolerations | CriticalAddonsOnly、NoExecute、NoSchedule |
容忍关键插件污点与节点驱逐,确保在每个节点都常驻 |
| 容器镜像 | registry.k8s.io/dns/k8s-dns-node-cache:1.26.8 |
node-cache 镜像(仓库当前版本) |
| 资源 requests | cpu 25m、memory 5Mi |
极低开销的缓存代理 |
| capabilities | NET_ADMIN |
用于管理 dummy 接口与 iptables 规则 |
| 暴露端口 | 53/UDP、53/TCP、9253/TCP | DNS 服务与 Prometheus 指标 |
容器启动参数(第 146 行)揭示了它的三个关键输入:
args: [ "-localip", "__PILLAR__LOCAL__DNS__,__PILLAR__DNS__SERVER__",
"-conf", "/etc/Corefile",
"-upstreamsvc", "kube-dns-upstream" ]
-localip:同时监听链路本地 IP 与集群 DNS IP,二者都作为"本地监听面";-conf:指向挂在的 Corefile;-upstreamsvc:声明使用前面定义的kube-dns-upstreamService 作为回源服务。
健康检查(第 161–167 行)通过 HTTP GET 访问 http://__PILLAR__LOCAL__DNS__:8080/health,initialDelaySeconds: 60 给 CoreDNS 留足启动时间。挂载方面有三处:/run/xtables.lock(hostPath 类型 FileOrCreate,供并发修改 iptables 时互斥)、/etc/coredns(config-volume,取自 node-local-dns ConfigMap,将 Corefile 键映射为 Corefile.base)、/etc/kube-dns(挂载集群内 kube-dns ConfigMap,optional: true,用于推导自定义上游配置)。
3.5 Headless Service:node-local-dns(指标暴露)
第 192–211 行的 node-local-dns Service 是一个 clusterIP: None 的无头 Service,selector 匹配 k8s-app: node-local-dns。YAML 注释写得很直白:"a headless service ... instead of load-balancing it will return the IPs of our associated Pods",并带有 prometheus.io/port: "9253"、prometheus.io/scrape: "true" 注解——它专门把每个节点上缓存实例的 9253 指标端口暴露给 Prometheus 抓取,同时避免负载均衡语义。
四、为什么必须"每节点一个缓存实例"与端口占用
NodeLocal DNSCache 的价值建立在本机性上:只有当 Pod 的查询在本机回环链路内被终结,才能彻底绕开宿主机 iptables DNAT 与 conntrack 竞争。因此 DaemonSet 是唯一正确的部署形态——"在集群所有节点上运行"。同时要注意 hostNetwork: true 意味着该 Pod 与宿主机共享网络命名空间,因此 53 端口在各节点上是被缓存实例独占的,这正是设计上把缓存监听地址放在 169.254.20.10 这类链路本地地址上的原因之一,避免与节点自身可能存在的其他 DNS 服务冲突。
从使用机制上推断,要让 Pod 的查询真正落到本机缓存,通常需要把节点的 kubelet DNS 配置指向该链路本地地址(即把 --cluster-dns 设为 169.254.20.10);本 addon 是整体部署流程(如 configure-helper.sh 所驱动的 kube-up 场景)中的一个组成部分,其模板变量亦由该脚本注入。
五、PILLAR 变量体系:模板如何被注入真实值
README 明确列出了 YAML 中的变量及其取值来源,汇总如下:
| 模板变量 | 含义 | 注入方 |
|---|---|---|
__PILLAR__DNS__SERVER__ |
kube-dns Service 的 ClusterIP | 部署脚本(sed 代换) |
__PILLAR__LOCAL__DNS__ |
链路本地监听地址(默认 169.254.20.10) | 部署脚本(sed 代换) |
__PILLAR__DNS__DOMAIN__ |
集群域名(默认 cluster.local) | 部署脚本(sed 代换) |
__PILLAR__CLUSTER__DNS__ |
集群内查询的回源服务器 | node-cache 镜像(≥1.15.6)启动时读取 kube-dns ConfigMap 推导 |
__PILLAR__UPSTREAM__SERVERS__ |
外部查询的上游服务器 | 同上,由自定义上游配置推导 |
前三者由集群配置脚本在部署期代换。仓库中 cluster/gce/gci/configure-helper.sh 的 setup-nodelocaldns-manifest 函数给出了最直接的实现证据:
function setup-nodelocaldns-manifest {
setup-addon-manifests "addons" "0-dns/nodelocaldns"
local -r localdns_file="${dst_dir}/0-dns/nodelocaldns/nodelocaldns.yaml"
setup-addon-custom-yaml "addons" "0-dns/nodelocaldns" "nodelocaldns.yaml" "${CUSTOM_NODELOCAL_DNS_YAML:-}"
# eventually all the __PILLAR__ stuff will be gone, but theyre still in nodelocaldns for backward compat.
sed -i -e "s/__PILLAR__DNS__DOMAIN__/${DNS_DOMAIN}/g" "${localdns_file}"
sed -i -e "s/__PILLAR__DNS__SERVER__/${DNS_SERVER_IP}/g" "${localdns_file}"
sed -i -e "s/__PILLAR__LOCAL__DNS__/${LOCAL_DNS_IP}/g" "${localdns_file}"
}
可以看到:
- 文件先被复制到
/etc/kubernetes/addons/0-dns/nodelocaldns/nodelocaldns.yaml,目录名冠以0-以保证 DNS 相关资源最先被 addon-manager 创建,防止其他插件抢占 DNS ClusterIP(见同脚本start-kube-addons中的注释); - 如果设置了环境变量
CUSTOM_NODELOCAL_DNS_YAML,则用用户自定义清单整体替换模板; - 三个
sed分别完成域名、DNS Service IP、链路本地地址的代换; - 该函数仅在
ENABLE_CLUSTER_DNS=true且ENABLE_NODELOCAL_DNS=true时被调用(第 2859–2861 行),即 NodeLocal DNSCache 是集群 DNS 之上的可选增强层。
对应的默认值集中在集群配置文件中,例如 cluster/gce/config-default.sh:
export ENABLE_NODELOCAL_DNS="${KUBE_ENABLE_NODELOCAL_DNS:-false}"
export LOCAL_DNS_IP="${KUBE_LOCAL_DNS_IP:-169.254.20.10}"
以及 cluster/gce/config-default.sh 中的 DNS_SERVER_IP="${KUBE_DNS_SERVER_IP:-10.0.0.10}"(测试集群配置在 config-test.sh 中有同款默认值)。也就是说:GCE 类集群默认不启用该插件,需显式开启 ENABLE_NODELOCAL_DNS;链路本地地址默认取 169.254.20.10。
后两个变量(__PILLAR__CLUSTER__DNS__、__PILLAR__UPSTREAM__SERVERS__)则交给容器镜像侧处理:README 说明它们由 registry.k8s.io/k8s-dns-node-cache:1.15.6 或更新版本的镜像在启动时读取 kube-dns ConfigMap(对应 DaemonSet 中挂载 /etc/kube-dns 的 optional: true 卷)来推导自定义上游服务器配置,从而使模板无需为每一类自定义上游而重新代换。换句话说,模板变量被刻意分成了"部署期由脚本填写的静态地址"与"运行期由镜像推导的动态上游"两组,各司其职。
六、链路本地地址的选择建议
README 对监听地址给出了明确约束与建议,值得原样保留并强调:
NodeLocal DNSCache 的本地监听 IP 可以是任何能保证不与集群既有 IP 冲突的地址。官方推荐使用本地作用域地址,例如 IPv4 的链路本地段 169.254.0.0/16,或 IPv6 的 Unique Local Address 段 fd00::/8。
这条建议的工程含义是:169.254.0.0/16 是 RFC 3927 保留的链路本地段,常规集群内不会为业务 Pod/Service 分配该段地址,因此用其中任意地址(仓库默认取 169.254.20.10)承载缓存监听几乎零冲突风险。若你的集群确有地址规划上的特例(例如网络插件已占用该段),则应像上文 LOCAL_DNS_IP 变量一样显式改用一个可保证不冲突的地址,并让 kubelet 侧 DNS 配置指向同一地址。
七、NetworkPolicy 环境下的 DNS 连通性
这是一个容易踩坑的点,README 单独用一节说明:
由于 node-local-dns Pod 运行在 hostNetwork: true 下,基于 namespace selector 的 DNS 出口(egress)放行规则可能不够用——hostNetwork Pod 的流量不从普通 Pod 网络命名空间发出,Calico 文档中常见的"按命名空间选择器放行 DNS"的做法覆盖不到这类查询。
推荐的替代方案是针对回源目标 ClusterIP 的 ipBlock 规则,README 给出的可复制示例为:
spec:
egress:
- ports:
- port: 53
protocol: TCP
- port: 53
protocol: UDP
to:
- ipBlock:
cidr: <well-known clusterIP for DNS>/32
podSelector: {}
policyTypes:
- Ingress
- Egress
即:对任意来源 Pod(podSelector: {})放行前往"集群 DNS 的知名 ClusterIP/32"的 53 端口 TCP/UDP 出口流量,同时显式声明 policyTypes 包含 Ingress 与 Egress。注意 NetworkPolicy 的 egress 是白名单语义,若策略只允许该 CIDR,还需保证缓存实例本地应答(链路本地地址、无跨节点转发)不受出口策略干扰——这正是把监听地址收敛到本机的好处。
八、Negative caching(否定应答缓存)的 5 秒权衡
README 记录了仓库对缓存配置的一次刻意收敛:denial(否定应答)缓存 TTL 被压缩到最低 5 秒。对应证据就在 nodelocaldns.yaml 的集群域名 zone 中:
cache {
success 9984 30
denial 9984 5
}
设计动机与权衡如下:
- 为什么压到 5 秒:若否定应答(NXDOMAIN 等)被长期缓存,依赖 DNS 轮询做编排协调的业务会持续拿到过期的"不存在"结果,导致新创建的 Service、StatefulSet 域名迟迟无法被发现。缩短 denial TTL 能显著降低这类编排失败窗口。
- 代价与调优方向:极端情况下(如针对不存在域名的恶意/高频查询)5 秒 TTL 可能放大回源压力。若确认性能受影响,可调高该 TTL 缓解,但 README 明确警告:依赖 DNS 轮询进行编排的操作可能因此失败,例如针对 StatefulSet 的 Operator。因此该值属于"默认保守、按业务场景谨慎上调"的敏感参数,不建议轻易改动。
对照反向解析 zone 中 cache 30 的配置可以看出:正向集群域名的否定应答使用了更短的独立 TTL,而反向 zone 未区分成功/否定应答,统一为 30 秒。
九、落地部署与验证清单
结合上文分析,在实际集群启用该插件的完整思路可归纳为:
- 开启开关:在集群引导配置中设置
ENABLE_NODELOCAL_DNS=true(GCE/kube-up 路径由 configure-helper.sh 消费;其他部署方式需将模板中的__PILLAR__变量替换为实际值后交给集群 DNS 管理流程); - 核对默认值:
LOCAL_DNS_IP=169.254.20.10、DNS_SERVER_IP=10.0.0.10(kube-dns Service IP)、DNS_DOMAIN=cluster.local,如与实际不符,通过KUBE_LOCAL_DNS_IP等环境变量或自定义清单CUSTOM_NODELOCAL_DNS_YAML覆盖; - 确认 kubelet DNS 指向链路本地地址,使节点上的 Pod 查询流入本机缓存面;
- 验证资源就绪:
node-local-dnsDaemonSet 在每个节点达到预期副本数,kube-dns-upstreamService 正常选中后端; - 功能自检:在任意节点用
dig @169.254.20.10 <service>.<namespace>.svc.<domain>验证集群域名解析,用dig @169.254.20.10 <外部域名>验证外部查询回源链路,并观察kube-dns后端请求量是否显著下降; - 指标巡检:通过无头 Service 暴露的 9253 端口抓取 CoreDNS 指标,检查缓存命中率与错误率;
- 策略核对:若集群启用 NetworkPolicy,按第七节示例为 DNS 出口补上针对集群 DNS ClusterIP 的
ipBlock放行。
综上,NodeLocal DNSCache 是 Kubernetes 把"高可用、低延迟的 DNS"下沉到每个节点的标准实践:从本仓库的 addon 清单可见,它用一段精确的 CoreDNS 配置 + 一个 hostNetwork DaemonSet + 一组模板变量,就完成了缓存面、回源面、指标面与健康面的完整闭环。对集群管理员而言,理解 nodelocaldns.yaml 中每一个对象与 README.md 中的调优告诫,是安全启用与维护该插件的前提。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00