从 DNS 协议到 CoreDNS:构建云原生时代的高可用解析服务

DNS,作为互联网的地址簿,是任何分布式系统赖以生存的基石。在 Kubernetes 主导的云原生时代,它的角色从简单的域名解析扩展为核心的服务发现机制。任何 DNS 的抖动或失效都可能导致应用层的大规模雪崩。本文旨在为中高级工程师和架构师提供一个从底层原理到实战演进的完整视角,我们将深入剖服 DNS 协议的内在机制,剖析 CoreDNS 的插件化架构,并最终给出一套在生产环境中构建高可用、高性能 DNS 解析服务的可落地路径。

现象与问题背景

在复杂的生产环境中,DNS 问题往往以诡异且难以复现的形式出现。工程师们经常遇到以下几类典型“悬案”:

  • 偶发性解析超时: 服务在大部分时间运行正常,但高峰期或特定场景下,部分 Pod 内的应用日志会报出 “Unknown Host” 或 “Connection Timeout” 错误,几秒后又自行恢复。
  • 服务启动缓慢: 新部署的应用 Pod 需要很长时间才能进入 `Ready` 状态,排查发现大量时间消耗在依赖服务的域名解析上。
  • DNS 服务器高负载: 在 Kubernetes 集群中,CoreDNS(或其前身 kube-dns)的 Pod CPU 使用率居高不下,甚至频繁被 OOMKilled,成为整个集群的稳定性的瓶颈。
  • 跨节点解析失败: 当发生网络局部抖动时,明明服务实例正常,但位于某些节点上的客户端 Pod 却无法解析到其他节点上的服务,而同节点内的解析则正常。
  • 域名解析风暴: 某个应用(或其依赖的 SDK)存在缺陷,在特定条件下发起海量的无效域名查询,瞬间打垮整个集群的 DNS 服务。一个典型的罪魁祸首是在 `resolv.conf` 中常见的 `ndots:5` 配置。

这些现象的根源,往往不是 DNS 服务本身“挂了”,而是其架构设计、配置参数、以及与底层操作系统和网络设施的交互细节中,隐藏着性能和可用性的魔鬼。要彻底解决这些问题,我们必须跳出“重启 CoreDNS 试试”的运维思维,回到计算机科学的基础原理中去寻找答案。

关键原理拆解

要构建一个稳固的上层建筑,我们必须深刻理解其地基。对于 DNS 服务,其地基就是 DNS 协议本身、操作系统内核的网络栈和缓存机制。

第一性原理:DNS 查询的本质与开销

我们首先以一位计算机科学教授的视角,来审视一次 DNS 查询的完整生命周期。

  • 协议选择:UDP vs. TCP
    DNS 主要使用 UDP 作为其传输层协议(端口 53)。这是基于效率的经典权衡。UDP 是无连接的,开销极小,一次查询-响应通常只需一个 RTT (Round-Trip Time)。这对于需要海量、低延迟解析的场景至关重要。然而,UDP 的“不可靠”和 512 字节的传统大小限制也是其弱点。当响应包超过 512 字节时,服务器会返回一个被截断(TC=1)的响应,客户端需要自动切换到 TCP 重新发起查询。现代 DNS 通过 EDNS0 扩展机制,允许客户端和服务器协商更大的 UDP 包大小,但这并非万无一失。此外,权威服务器之间的区域传送(AXFR/IXFR)等高可靠性场景,则强制使用 TCP。
  • 查询类型:递归 vs. 迭代
    从客户端(Stub Resolver)到本地 DNS 服务器(如 CoreDNS)的查询是递归查询。客户端将解析的全部工作委托给服务器,自己则“坐等”最终的 IP 地址或失败结果。而本地 DNS 服务器为了找到答案,会向根(.)、顶级域(.com)、权威域(example.com)的各级域名服务器发起一系列迭代查询。它像一个侦探,逐级询问,直到找到最终的权威答案。这个过程涉及多次网络往返,是 DNS 延迟的主要来源。
  • 缓存的生命周期:TTL 与一致性
    DNS 系统在性能和数据新鲜度之间取得平衡的关键是缓存TTL (Time-To-Live)。每一条 DNS 记录(RR)都附带一个 TTL 值,指示该记录可以在缓存中保留多长时间。缓存可以存在于多个层面:浏览器、操作系统(如 nscd/systemd-resolved)、本地 DNS 服务器(CoreDNS 的 `cache` plugin)。这是一个典型的分布式系统缓存问题,它遵循的是最终一致性模型。较长的 TTL 可以极大地提升性能,降低权威服务器的负载,但代价是变更生效的延迟。较短的 TTL 则相反。在服务发现场景中,合理的 TTL 设置至关重要。
  • 内核的参与:Socket 与 Conntrack
    一次 DNS 查询并非简单的应用层通信。当 CoreDNS Pod 发出一个 UDP 包时,它首先通过 Socket 系统调用进入内核态。Linux 内核的网络协议栈会为其分配一个端口,封装 IP 头和以太网帧,然后通过网卡发送出去。对于有状态防火墙或 NAT(如 Kubernetes 中常用的 iptables/IPVS),内核需要通过 conntrack 机制来跟踪这个“连接”。尽管 UDP 是无连接的,conntrack 仍会为 `IP:Port -> IP:Port` 的五元组创建一个条目,并设置一个超时时间。当 DNS QPS 极高时,这个 conntrack 表可能会被迅速耗尽,导致新的连接无法建立,表现为随机的解析失败。这是许多 K8s 集群中 DNS 不稳定的核心原因之一。

系统架构总览

理解了底层原理后,我们可以设计一个在 Kubernetes 环境下高可用的 CoreDNS 架构。这个架构的核心思想是:分层缓存、就近解析、故障隔离

我们将架构分为两层:

  1. 集群级 DNS 服务 (Cluster-Level DNS): 这是整个集群的权威解析器,负责处理所有内部服务域名(`.cluster.local`)的解析,并作为外部域名解析的最终出口。
  2. 节点级 DNS 缓存 (Node-Level DNS Cache): 在每个工作节点上部署一个轻量级的 DNS 缓存实例,拦截该节点上所有 Pod 的 DNS 请求。

用文字描述的架构图如下:

  • 一个 Kubernetes 集群,包含多个 Master 节点和 Worker 节点。
  • 在 `kube-system` 命名空间中,运行着一个 CoreDNS Deployment,它管理着 2 个或更多的 CoreDNS Pod 副本。这些副本通过 Pod 反亲和性 规则被调度到不同的 Worker 节点上,避免单点故障。
  • 一个名为 `kube-dns` 的 ClusterIP Service 指向这些 CoreDNS Pod。所有需要进行集群级解析的请求,理论上都应该发往这个虚拟 IP。
  • 在每个 Worker 节点上,都运行着一个由 DaemonSet 管理的 NodeLocal DNSCache Pod。这个 Pod 直接监听节点上的一个特定 IP 地址(通常是一个保留的链路本地地址,如 169.254.20.10)。
  • 每个业务 Pod 内的 `/etc/resolv.conf` 文件,其 `nameserver` 配置不再是 `kube-dns` 的 ClusterIP,而是其所在节点的 NodeLocal DNSCache 的 IP 地址(169.254.20.10)。

数据流解析:

  1. 业务 Pod 内的应用发起一次 DNS 查询,例如 `myservice.default.svc.cluster.local`。
  2. 请求首先被发送到本机的 NodeLocal DNSCache
  3. NodeLocal DNSCache 检查自己的缓存:
    • 缓存命中: 如果缓存中有记录且未过期,立即返回结果。这是最快路径,网络开销仅限于节点内部的 Loopback。
    • 缓存未命中: NodeLocal DNSCache 会根据请求的域名类型决定下一步操作。
      • 对于内部域名(`.cluster.local`),它会将请求转发给集群级的 `kube-dns` Service
      • 对于外部域名(如 `www.google.com`),它会直接转发给预配置的上游 DNS 服务器(如数据中心的 DNS 或公网 DNS)。
  4. 发往 `kube-dns` Service 的请求,通过 kube-proxy 的负载均衡机制(IPVS 或 iptables),被路由到某个健康的 CoreDNS Pod
  5. 集群级的 CoreDNS Pod 使用 `kubernetes` 插件,通过 Watch Kubernetes API Server 获取 Service 和 Endpoint 的信息,从而解析内部域名,并将结果返回给 NodeLocal DNSCache。
  6. NodeLocal DNSCache 收到响应后,一方面返回给业务 Pod,另一方面将其存入自己的缓存。

这个架构的好处是显而易见的:大部分请求被节点本地缓存终结,极大降低了对中心化 CoreDNS 服务的压力和网络延迟。同时,由于避免了大量的 DNS 流量通过 `kube-dns` Service 的 ClusterIP,也就绕开了 `conntrack` 表瓶颈,提升了整体的稳定性。

核心模块设计与实现

现在,我们切换到极客工程师的视角,深入探讨这个架构中关键组件的配置与实现细节。魔鬼藏在细节中。

集群级 CoreDNS (`Corefile` 配置)

这是整个系统的“大脑”,它的配置文件 `Corefile` 定义了所有解析逻辑。一个生产环境的 `Corefile` 可能如下:


.:53 {
    # 错误日志,生产环境必备
    errors
    # 健康检查端点,用于 K8s 的 livenessProbe 和 readinessProbe
    health {
       lameduck 5s
    }
    # 暴露 Prometheus 指标,用于监控
    prometheus :9153

    # 核心插件:处理 kubernetes.local 及相关域名
    kubernetes cluster.local in-addr.arpa ip6.arpa {
       pods insecure
       fallthrough in-addr.arpa ip6.arpa
    }

    # 转发上游 DNS 请求
    forward . /etc/resolv.conf {
       policy round_robin
       health_check 5s
    }

    # 缓存插件
    cache 30 {
        # 成功解析结果缓存 900 秒
        success 900
        # NXDOMAIN (不存在的域) 结果缓存 30 秒,防止缓存污染
        denial 30
    }

    # 重新加载配置,当 ConfigMap 变更时自动生效
    reload

    # 日志插件,按需开启
    # log . "{remote}:{port} - {>id} {type} {class} {name} {proto} {size} {>do} {>bufsize} {rcode} {>rflags} {>rtt}"
}

代码解读与坑点分析:

  • health { lameduck 5s }:这是一个优雅停机的关键设置。当 CoreDNS 准备关闭时,它会先让 `/health` 端点返回不健康,但会继续处理现有请求 5 秒钟,然后再彻底关闭。这可以防止在滚动更新过程中出现解析黑洞。
  • kubernetes 插件:这是实现服务发现的核心。pods insecure 选项允许通过 `pod-ip-address.namespace.pod.cluster.local` 的格式解析 Pod IP,对于调试非常有用,但请注意安全风险。fallthrough 允许当插件无法解析时,将请求传递给下一个插件。
  • forward . /etc/resolv.conf:这里的 `.` 表示转发所有未被前面插件处理的请求。使用 `/etc/resolv.conf` 会继承宿主机的 DNS 配置,这在云环境中通常是最佳实践,因为它会自动指向 VPC 内的 DNS 服务。policy round_robin 比 `random` 更能均分负载。health_check 至关重要,它会定期探测上游服务器,并自动剔除故障节点。
  • cache 30:这里的 30 是一个基础 TTL,但下面的 `success 900` 和 `denial 30` 会覆盖它。对 NXDOMAIN 设置一个较短的缓存时间(如 30 秒)是一个很好的实践,可以快速响应错误的域名配置,同时又能抵御短时间的查询风暴。

节点级 NodeLocal DNSCache (`Corefile` 配置)

NodeLocal DNSCache 的配置相对简单,它本质上是一个专注于缓存和转发的 CoreDNS 实例。


# 内部域名解析
cluster.local:53 {
    log
    errors
    # 将所有 .cluster.local 的请求转发给集群级的 kube-dns Service
    forward . 10.96.0.10 {
       force_tcp
    }
}

# 外部域名解析与指标
.:53 {
    log
    errors
    prometheus :9153
    # 转发给上游 DNS,通常是数据中心或云厂商的 DNS
    forward . 8.8.8.8 1.1.1.1

    # 本地缓存
    cache 30 {
        success 900
        denial 30
        prefetch 1 10% 30m
    }
}

代码解读与坑点分析:

  • 双 Server Block:我们为内部域名 (`cluster.local`) 和所有其他域名 (`.`) 定义了不同的处理逻辑。这是精细化控制的关键。
  • forward . 10.96.0.10 { force_tcp }:这里的 `10.96.0.10` 是 `kube-dns` Service 的 ClusterIP。我们在这里强制使用 TCP 将请求转发给集群 CoreDNS。这是一个非常重要的优化! 它将节点与集群 CoreDNS 之间的通信从大量短命的 UDP “连接”转换成少量长寿的 TCP 连接。这几乎可以从根本上解决 `conntrack` 表耗尽的问题,因为一个 TCP 连接在 conntrack 表中只占用一条记录。
  • cache ... { prefetch 1 10% 30m }:`prefetch` 是一个性能利器。它会在缓存条目即将过期时,主动去上游刷新。参数表示:当一个条目的 TTL 还剩 10% 时,如果有至少 1 个查询命中它,就提前去刷新,刷新任务的超时为 30 分钟。这可以用少量的后台流量,换取几乎 100% 的缓存命中率,极大地降低了用户感知的 P99 延迟。

性能优化与高可用设计

架构和基础配置只是起点,真正的稳定性来自于对极端情况的考虑和持续优化。

对抗层:Trade-off 分析

  • 缓存 vs. 数据新鲜度:这是永恒的权衡。在服务发现场景,服务的 IP 可能会变化。过长的缓存(如 900 秒)意味着服务 Endpoint 变更后,最长可能有 15 分钟的解析延迟。解决方案是,Kubernetes 在 Service 的 Endpoint 变化时,依赖它的 Pod 会被通知并重新建立连接,这个过程不完全依赖 DNS。但对于非 K8s 感知的应用,需要将 TTL 降低到与服务漂移容忍度相匹配的水平,例如 60 秒。
  • TCP 转发 vs. 延迟:虽然 NodeLocal DNSCache 使用 TCP 转发到上游可以解决 conntrack 问题,但 TCP 握手本身会引入额外的延迟(1-2 RTT)。然而,由于连接是长连接和复用的,这个初始开销会被摊薄。对于绝大多数应用来说,用几十毫秒的首次解析延迟换取整个系统的稳定性是完全值得的。
  • `autopath` 插件与 `ndots:5` 问题:在 Kubernetes 的 `resolv.conf` 中,`ndots:5` 意味着如果一个域名中的点(`.`)少于 5 个,系统会依次尝试拼接 `search` 域(如 `svc.cluster.local`, `cluster.local`)再进行查询。例如,查询 `www.google.com` 会变成:
    1. `www.google.com.svc.cluster.local.` (NXDOMAIN)
    2. `www.google.com.cluster.local.` (NXDOMAIN)
    3. `www.google.com.` (Success)

    这会产生大量无用的查询。CoreDNS 的 `autopath` 插件可以解决这个问题。它通过与 Kubernetes API Server 交互,智能地判断何时应该进行搜索路径拼接,何时应该直接查询,从而将无用查询降到最低。这是一个以少量 CPU/内存开销换取大量网络和上游 DNS 负载降低的典型优化。

高可用策略

  • Pod 反亲和性:确保集群级 CoreDNS 和其他关键组件(如 Ingress Controller)的 Pod 分布在不同的物理节点、机架甚至可用区。这是最基础的物理容灾。
  • 健康的上游管理:`forward` 插件必须配置多个上游服务器,并且一定要开启 `health_check`。当一个上游 DNS 变慢或失效时,CoreDNS 能自动将其隔离。
  • 资源预留与限制:为 CoreDNS 和 NodeLocal DNSCache Pod 设置合理的 CPU/Memory `requests` 和 `limits`。`requests` 保证其在资源紧张时也能获得必要的资源,`limits` 防止其因异常查询(如攻击或 bug)耗尽节点资源。内存的 `request` 和 `limit` 应该相等,以保证 Pod 获得 Guaranteed 的 QoS 等级。
  • 监控与告警:利用 `prometheus` 插件,建立完善的监控仪表盘和告警规则。核心监控指标包括:
    • `coredns_dns_request_duration_seconds_p99`:P99 解析延迟,最能反映用户体验。
    • `coredns_dns_responses_total{rcode=”NXDOMAIN|SERVFAIL”}`:错误码的速率,突然飙升通常意味着有问题。
    • `coredns_cache_hits_total / coredns_cache_misses_total`:缓存命中率,急剧下降可能表示缓存配置不当或遭遇缓存穿透攻击。
    • `coredns_forward_healthcheck_failures_total`:上游 DNS 健康检查失败次数。

架构演进与落地路径

一口气吃成胖子是不现实的。对于一个已有的、正在运行的 Kubernetes 集群,引入这样一套架构需要一个清晰、分阶段的演进路径。

第一阶段:基线优化 (Baseline Hardening)

  1. 评估现状:首先,通过 Prometheus 监控现有的 CoreDNS 性能,确定瓶颈在哪里。是 CPU、内存、还是解析延迟?
  2. 加固集群级 CoreDNS
    • 将 CoreDNS 的 Deployment 副本数增加到至少 3 个。
    • 配置严格的 Pod 反亲和性规则。
    • 优化 `Corefile`,加入 `health`、`prometheus`、`reload` 插件,并对 `cache` 和 `forward` 插件进行初步的参数调优。
    • 为 CoreDNS Pod 设置合理的资源 `requests` 和 `limits`。

这个阶段的改动对现有业务无影响,但能显著提升中心 DNS 服务的健壮性,为下一步打下基础。

第二阶段:部署 NodeLocal DNSCache (Phased Rollout)

  1. 部署 DaemonSet:将 NodeLocal DNSCache 的 DaemonSet 部署到集群中,但此时它还不会被任何 Pod 使用。
  2. 灰度启用:选择一个或几个不那么重要的节点,通过修改 kubelet 的配置 (`–cluster-dns` 参数指向 NodeLocal DNSCache 的 IP),然后重启 kubelet(或排空节点后重建),使新调度到这些节点上的 Pod 自动使用 NodeLocal DNSCache。
  3. 观察与验证:密切监控灰度节点的 DNS 解析行为和业务应用的健康状况。验证延迟是否降低,集群 CoreDNS 的负载是否下降。
  4. 全量推广:在验证成功后,分批次(例如,按机架、按可用区)将此变更推广到所有节点。对于不能中断的节点,这通常需要一个维护窗口来滚动更新。

这是一个重大的架构变更,必须谨慎进行。灰度发布是控制风险的关键。

第三阶段:高级优化与混合云集成 (Advanced Tuning & Hybrid Cloud)

  1. 启用 `autopath`:在 NodeLocal DNSCache 的 `Corefile` 中启用 `autopath` 插件,解决 `ndots` 问题,进一步降低不必要的查询。
  2. 精细化缓存策略:根据业务特性,为不同的域名配置不同的缓存策略。例如,使用多个 Server Block,为频繁变更的内部服务设置较短的 TTL,为稳定的外部依赖设置较长的 TTL。
  3. 混合云/多集群 DNS:如果你的环境横跨多个 VPC 或数据中心,可以利用 CoreDNS 的 `etcd` 或 `redis` 插件,创建一个统一的 DNS 视图,实现跨集群的服务发现。或者使用 `forward` 插件的条件转发功能,将对特定域名的查询(如 `.company.internal`)转发到专线对端的 DNS 服务器上。

通过这三个阶段的演进,你可以将集群的 DNS 服务从一个潜在的“定时炸弹”改造为一个可观测、高弹性、高性能的坚实基础设施,为上层业务的稳定运行提供强有力的保障。

延伸阅读与相关资源

  • 想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
    交易系统整体解决方案
  • 如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
    产品与服务
    中关于交易系统搭建与定制开发的介绍。
  • 需要针对现有架构做评估、重构或从零规划,可以通过
    联系我们
    和架构顾问沟通细节,获取定制化的技术方案建议。
滚动至顶部