从内核到云原生:构建高可用 DNS 服务的首席架构师指南

DNS,作为互联网的“电话簿”,是分布式系统中最基础也最容易被忽视的组件。在以 Kubernetes 为代表的云原生时代,服务间发现与通信的频次呈指数级增长,DNS 不再仅仅是简单的域名到 IP 的转换,它已成为系统稳定性和性能的关键路径。当应用日志中充斥着 “UnknownHostException” 或 “Temporary failure in name resolution” 时,往往意味着一场生产事故的开始。本文将以一位首席架构师的视角,从操作系统内核、网络协议栈到 CoreDNS 的具体实践,系统性地剖析如何构建一个可观测、高可用、高性能的 DNS 解析服务。

现象与问题背景:当 DNS 成为瓶颈

在传统的单体应用或面向物理机的架构中,DNS 查询通常频率较低,且结果多为长期不变的静态 IP。然而,在 Kubernetes 这类动态、弹性的环境中,DNS 的负载特性发生了根本性变化,由此引发了一系列新的工程挑战:

  • 查询风暴 (Query Storm): 微服务架构下,服务实例(Pod)生命周期短暂且数量庞大。服务间的每一次调用都可能触发一次或多次 DNS 查询(例如,Java 应用默认不会缓存 DNS 解析结果)。这导致集群内部 DNS 查询的 QPS (Queries Per Second) 远高于传统环境,极易触及 DNS 服务的性能上限。
  • 内核瓶颈:conntrack 表耗尽: DNS 查询主要使用 UDP 协议。在 Linux 内核中,每一个 UDP “连接”(即使是无连接的)也会在 netfilter 的 conntrack 表中创建一条记录,并设置一个超时时间。在高 QPS 场景下,大量的 DNS 查询会迅速填满 conntrack 表,导致新连接被丢弃(drop),现象就是随机的网络延迟或连接失败,而这并不仅仅影响 DNS,会影响节点上的所有网络通信。这是一个极其隐蔽且破坏性巨大的问题。
  • 搜索域(Search Domain)导致的查询放大: Kubernetes Pod 的 /etc/resolv.conf 文件通常会配置多个搜索域,如 svc.cluster.localcluster.local。当应用尝试解析一个外部域名(如 api.google.com)时,系统会依次尝试拼接搜索域进行查询:api.google.com.svc.cluster.localapi.google.com.cluster.local,最后才查询 api.google.com。这种行为被称为查询放大,一个外部域名解析请求可能最终向上游 DNS 服务器发送了 3-5 次无效查询,造成不必要的延迟和负载。
  • 跨可用区(AZ)的延迟与成本: 在公有云环境中,如果 CoreDNS Pod 与发起查询的应用 Pod 不在同一个可用区,一次 DNS 解析就会产生跨区流量,不仅增加了网络延迟,还可能产生额外的流量费用。在高 QPS 场景下,这笔开销不容忽视。

这些问题表明,简单地将 DNS 视为一个“黑盒”基础设施,在云原生环境下是极度危险的。我们需要深入其工作原理,才能从根源上构建稳定可靠的系统。

关键原理拆解:从 RFC 1035 到分布式系统

(教授口吻)要解决工程问题,我们必须回归到计算机科学的基础原理。DNS 的核心设计,本质上是一个大规模、去中心化、最终一致性的分布式数据库。理解其设计哲学是解决上层问题的关键。

  • 层次化与委托 (Hierarchy and Delegation): DNS 的域名空间是一个树状结构,根节点是 .。从根开始,每个节点可以将其子树的管理权“委托”给下一级的权威名称服务器(Authoritative Name Server)。例如,.com 的名称服务器知道 google.com 的名称服务器地址,但它并不知道 www.google.com 的 IP。这种委托机制使得 DNS 能够无限扩展,避免了单点瓶颈,是分布式系统设计中的经典分治策略。
  • 解析器与服务器的角色分离: DNS 系统明确区分了两种角色。递归解析器 (Recursive Resolver),如我们配置的 8.8.8.8 或 CoreDNS,负责为客户端完整地解答一个查询,它会代替客户端执行所有必要的迭代查询。权威服务器 (Authoritative Server) 则只对自己管辖的“区域”(Zone)数据负责。这种分离使得系统关注点分离,递归解析器可以专注于性能、缓存和客户端优化,而权威服务器则专注于数据的准确性和可用性。
  • 缓存与 TTL (Time-To-Live): DNS 响应中最重要的元数据之一就是 TTL。它告知解析器这条记录可以在缓存中存放多久。这是一种典型的时间换一致性的策略。较长的 TTL 可以极大降低权威服务器的负载和网络延迟,但代价是域名变更生效缓慢。较短的 TTL 则相反。在 Kubernetes 中,Service IP 的 TTL 通常非常短(默认 5 秒),因为 Service 对应的后端 Endpoints (Pods) 变化非常频繁,这正是导致缓存命中率低、查询量大的根本原因之一。
  • 协议选择:UDP 与 TCP: RFC 规范定义 DNS 主要使用 UDP 端口 53。这是基于性能的考量:UDP 是无连接的,开销远小于 TCP 的三次握手。一个请求和一个响应只需要两个网络包。然而,UDP 也有其局限性。最初的 DNS 协议限制 UDP 报文大小不超过 512 字节。当响应数据(例如,一个拥有大量 A 记录的域名)超过这个大小时,服务器会返回一个截断(Truncated)标志,客户端需要重新通过 TCP 发起查询。现代 DNS 通过 EDNS0 扩展增大了 UDP 报文的尺寸限制,但 TCP 仍然是处理大数据量响应和区域传送(Zone Transfer, AXFR)的必要后备手段。

系统架构总览:CoreDNS 的插件化哲学

CoreDNS 作为 CNCF 的毕业项目,已经取代了 kube-dns 成为 Kubernetes 默认的 DNS 服务器。其成功的关键在于其优雅且高度可扩展的插件化架构。你可以将 CoreDNS 服务器想象成一个中间件处理链(Middleware Chain)。

当一个 DNS 查询请求到达 CoreDNS 时,它会根据 Corefile 配置文件中定义的服务器块(Server Block)和插件顺序,依次流经这个链条。每个插件都有机会对请求进行处理:

  • 直接响应: 如果插件可以权威地回答该请求(例如,kubernetes 插件找到了对应的 Service IP),它会生成响应并终止处理链。
  • 传递给下一个插件: 如果插件无法处理该请求,或者它的功能是修改请求(如 rewrite),它会将请求(可能已被修改)传递给链中的下一个插件。
  • 丢弃或拒绝: 某些插件(如用于安全的 block 插件)可能会直接拒绝请求。

这种设计带来了极大的灵活性。你需要缓存?加上 cache 插件。你需要对接 Kubernetes?启用 kubernetes 插件。你需要将无法解析的请求转发给上游 DNS?使用 forward 插件。你需要自定义域名?用 hostsetcd 插件。整个 DNS 服务器的行为完全由你选择和组织的插件链来定义,这使得 CoreDNS 成为了一个通用的 DNS “瑞士军刀”,而不仅仅是为 Kubernetes 服务的工具。

核心模块设计与实现:解剖 Corefile

(极客工程师口吻)理论讲完了,我们来点实在的。CoreDNS 的一切都由一个名为 Corefile 的配置文件驱动。这玩意儿语法简单,但每个插件的参数都藏着魔鬼。下面是一个典型的生产环境配置,我们逐一拆解。


.:53 {
    errors
    health {
        lameduck 5s
    }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
        ttl 30
    }
    prometheus :9153
    forward . /etc/resolv.conf {
        max_concurrent 1000
    }
    cache 30
    loop
    reload
    loadbalance
}
  • kubernetes 插件: 这是在 K8s 中运行的核心。cluster.local 指定了它负责的 DNS 区域。它通过 Watch K8s API Server 的 Service 和 Endpoint 资源,在内存中构建了一个 DNS 记录的查找表。当一个对 my-service.my-namespace.svc.cluster.local 的查询进来时,它能迅速返回 ClusterIP。fallthrough 参数是关键:如果一个查询在 kubernetes 插件中没有匹配到(比如是外部域名),它会把请求“掉落”给下一个插件处理,而不是直接返回 NXDOMAIN。
  • forward 插件: 这是“接盘侠”。所有 kubernetes 插件处理不了的请求都会到这里。. /etc/resolv.conf 表示将所有查询(.)转发给宿主机 /etc/resolv.conf 文件中定义的上游 DNS 服务器。在生产环境中,这里通常会配置为公司内部的 DNS 解析器或高可用的公共 DNS(如 8.8.8.8, 1.1.1.1)。max_concurrent 参数非常重要,它限制了向上游转发的并发请求数,防止 CoreDNS 自身被打垮或打垮上游。
  • cache 插件: 性能优化的第一道防线。cache 30 表示为所有应答缓存 30 秒。这个插件能够显著降低对上游服务器的请求压力,特别是对于外部域名的重复查询。但要注意,它会覆盖掉上游返回的 TTL,所以要根据你对数据新鲜度的要求来权衡这个值。你可以更精细地控制缓存,例如为成功的响应和失败的响应(NXDOMAIN)设置不同的缓存策略,防止“缓存穿透”攻击。
  • loadbalance 插件: 当 DNS 查询返回多个 A 记录时(比如一个 Headless Service),这个插件会自动对记录的顺序进行轮转(round-robin)。这提供了一种简单的、客户端无感的 DNS 负载均衡,虽然粗糙,但在很多场景下足够有效。
  • reloadhealth: reload 插件允许你在不重启 CoreDNS 进程的情况下,通过修改 ConfigMap 优雅地热加载配置。health 插件则暴露了一个 HTTP 健康检查端点,这对于 Kubernetes 的 livenessProbe 和 readinessProbe 至关重要。lameduck 参数的作用是,在 Pod 准备终止时,让健康检查端点先失败一段时间(5秒),等负载均衡器把它从可用列表中移除后,再真正停止进程,从而实现优雅停机。

性能优化与高可用设计:从内核到网络

默认配置的 CoreDNS 在大规模集群下是脆弱的。要让它坚如磐石,需要一套组合拳,从应用层一直打到内核层。

1. 水平扩展与部署策略

首先,永远不要只运行一个 CoreDNS Pod。至少部署两个副本,并通过 Pod 反亲和性(podAntiAffinity) 规则,确保它们被调度到不同的物理节点上。这可以防止单节点故障导致整个 DNS 服务中断。


apiVersion: apps/v1
kind: Deployment
# ...
spec:
  replicas: 2
  template:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: k8s-app
                operator: In
                values:
                - kube-dns
            topologyKey: "kubernetes.io/hostname"

同时,配置一个 PodDisruptionBudget (PDB),确保在集群维护、节点缩容等“自愿性中断”事件中,至少有一个 CoreDNS Pod 保持可用。

2. 部署 NodeLocal DNSCache

这是解决前面提到的 conntrack 瓶颈和跨区延迟问题的“银弹”。NodeLocal DNSCache 是一个通过 DaemonSet 部署在每个集群节点上的 DNS 缓存代理。它的工作模式是:

  • 应用 Pod 的 DNS 查询不再直接发送给 CoreDNS 的 Service IP,而是被 iptables 规则拦截,重定向到本地的 NodeLocal DNSCache 进程。
  • 如果请求的域名在本地缓存中命中(例如,其他 Pod 刚刚查询过),它会立刻返回结果,网络路径极短,延迟通常在亚毫秒级。
  • 如果缓存未命中,NodeLocal DNSCache 会代表应用 Pod 去查询上游的 CoreDNS 服务。因为它是在宿主机网络命名空间运行,它与 CoreDNS 的通信会建立一个长久的 TCP 连接,而不是大量的短命 UDP 连接,从而从根本上避免了 conntrack 表的爆炸问题

部署 NodeLocal DNSCache 后,你会观察到集群 DNS 解析的平均延迟和长尾延迟(P99 延迟)都有显著下降,并且系统稳定性大幅提升。

3. 监控与可观测性

你无法优化一个你不能测量的东西。CoreDNS 的 prometheus 插件是你的眼睛。务必将其接入你的监控系统,并设置关键指标的告警。需要死盯的几个核心指标:

  • coredns_dns_request_count_total: 按区域(zone)和协议(protocol)划分的请求总量。可以快速发现异常流量。
  • coredns_dns_request_duration_seconds_bucket: 请求延迟的直方图。P95/P99 延迟的飙升是服务降级的重要信号。
  • coredns_cache_hits_totalcoredns_cache_misses_total: 缓存命中和未命中的计数。缓存命中率的骤降可能意味着配置问题或某种形式的攻击。
  • coredns_forward_request_count_total: 转发到上游 DNS 的请求数。如果这个数字异常高,说明内部解析或缓存可能出了问题。

架构演进与落地路径:从单集群到多云联邦

构建高可用 DNS 服务不是一蹴而就的,它应该是一个分阶段演进的过程。

  1. 阶段一:基线高可用(Baseline HA)。对于任何生产集群,这是起点。将 CoreDNS 部署为至少 2 个副本,配置 Pod 反亲和性与 PDB。建立基础的 Prometheus 监控和告警。这个阶段的目标是保证在单点故障时,DNS 服务不会中断。
  2. 阶段二:节点级性能优化。当集群规模扩大(例如超过 50 个节点或 QPS 超过 1000),或者开始观察到随机网络抖动时,立即部署 NodeLocal DNSCache。这是从“能用”到“好用”的关键一步,能解决绝大多数性能和内核相关的问题。
  3. 阶段三:统一服务发现平面。当你的系统横跨 Kubernetes 和传统的虚拟机/物理机环境时,DNS 可以成为连接它们的桥梁。使用 CoreDNS 的 etcd 插件,将外部服务的域名和 IP 存储在 etcd 中。CoreDNS 从 etcd 读取这些记录,从而让 K8s 内部的应用可以像解析内部服务一样,无缝地解析外部服务的域名,实现统一的服务发现。
  4. 阶段四:多集群/混合云联邦。在多集群或混合云场景下,DNS 是实现服务互通的基石。你可以设计一个分层的 DNS 解析架构:
    • 每个集群的 CoreDNS 负责解析本集群的 cluster.local 域名。
    • 配置 forward 规则,将对其他集群的查询(如 *.cluster2.local)转发到目标集群的 CoreDNS 服务暴露的 LoadBalancer IP 上。
    • 或者,建立一个全局的、高可用的 etcd 集群,所有环境的 CoreDNS 都使用 etcd 插件从这个共享数据源提供服务发现。这是一种更高级的模式,能实现真正的位置无关服务网格。

最终,DNS 不再仅仅是一个被动的解析工具,而是你手中主动构建的、跨越数据中心和云边界的、动态的流量路由与服务发现基础设施的核心。掌握它,才能在复杂的分布式世界中游刃有余。

延伸阅读与相关资源

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