bigywater.tech
微服务 · 51

DNS 服务发现:SRV 记录与 TTL 缓存的坑

用 DNS 做服务发现,TTL 设多少?客户端缓存、启动时只解析一次、不缓存各会怎样?SRV 的优先级和权重怎么起作用?服务晚上线会被负缓存拖多久?应答太大怎么办?这篇用自己写的权威 DNS 服务和 dnspython 逐项量出来。

83 到 105 个
TTL 10 s 的缓存在摘掉后端后打到已停后端的请求数(共 700 个;TTL 2 s 只有 0 到 19 个)
18.1 s
SOA minimum 20 s 时,服务晚于客户端第一次查询上线,遵守负缓存的客户端在上线后过多久才发现它
0 / 24.8% / 75.2%
SRV 里低优先级目标 C、同优先级 A 与 B(权重 1 : 3)分到的流量

TTL 与摘除

实测 · 本机执行输出
  解析方式                  | 错误请求数(每次共 700 个;遵守 TTL 的做了 3 次,删除时刻 4.0 / 4.7 / 5.4 s) | 错误发生的时间范围(秒)                   | DNS 收到的查询数
  ----------------------+--------------------------------------------------------+--------------------------------+-----------
  (a) 遵守 TTL · TTL 2 s  | 0 / 19 / 13                                            | - | 4.7~6.0 | 5.4~6.0          | 7 / 7 / 7 
  (a) 遵守 TTL · TTL 5 s  | 19 / 4 / 83                                            | 4.0~5.0 | 4.7~5.0 | 5.5~10.0   | 3 / 3 / 3 
  (a) 遵守 TTL · TTL 10 s | 105 / 90 / 83                                          | 4.0~10.0 | 4.7~10.0 | 5.5~10.0 | 2 / 2 / 2 
  (b) 启动时解析一次           | 168                                                    | 4.0~13.9                       | 1         
  (c) 不缓存               | 0                                                      | -                              | 700       
  遵守 TTL 的缓存:删掉记录之后,缓存最多还能再用一个 TTL(具体多久取决于删除时刻与缓存过期的相位,所以做了 3 次),这期间约三分之一的请求打到已经停掉的后端;TTL 越短错误越少,但 DNS 查询越多;启动时只解析一次的客户端永远不会发现变化,错误一直持续到结束;不缓存的客户端在删除之后立刻不再出错,代价是每个请求一次 DNS 查询(50 个/秒)

缓存的 TTL 决定摘掉实例后还会有多少请求发过去;不缓存最准但每个请求一次 DNS 查询;只解析一次永远发现不了变化。

SRV 的优先级与权重

实测 · 本机执行输出
  情形       | A 被选 | B 被选 | C 被选  | 占比                  
  ---------+------+------+-------+---------------------
  A、B、C 都在 | 2478 | 7522 | 0     | 24.8% / 75.2% / 0.0%
  A、B 都被删掉 | 0    | 0    | 10000 | 100.0% 给 C          
  优先级是“备用”的语义:只要还有优先级更高(数字更小)的目标,低优先级的目标一点流量都得不到;同优先级里按权重 1 : 3 分(约 25% / 75%);高优先级的都被删掉之后,备用的 C 才接管全部流量。注意:SRV 本身不包含健康状态,“都挂了”要靠有人把它们从区域里删掉

优先级是备用语义;同优先级内按权重分;SRV 不含健康状态。

负缓存

实测 · 本机执行输出
  客户端        | 服务上线时刻 | 客户端第一次解析成功 | 上线之后又过了多久才发现 | DNS 收到的查询数
  -----------+--------+------------+--------------+-----------
  遵守缓存(含负缓存) | 2.0 s  | 20.2 s     | 18.1 s       | 2         
  不缓存        | 2.0 s  | 2.0 s      | 0.0 s        | 3         
  “不存在”也是会被缓存的答案:服务晚于客户端的第一次查询才登记,遵守负缓存的客户端要等到负缓存 TTL(SOA minimum)过期才会再去问;不缓存的客户端在上线后下一次查询就成功

“不存在”也被缓存,新服务要等负缓存 TTL。

应答大小

实测 · 本机执行输出
  目标数 N | UDP 应答里的记录数 | TC 标志 | UDP 应答大小(字节) | TCP 应答里的记录数 | TCP 应答大小(字节)
  ------+-------------+-------+--------------+-------------+-------------
  5     | 5           | False | 241          | 5           | 241         
  20    | 0           | True  | 31           | 20          | 871         
  40    | 0           | True  | 31           | 40          | 1711        
  100   | 0           | True  | 31           | 100         | 4231        
  应答超过 512 字节时,这个服务端把答案清空并置 TC 位,告诉客户端“去用 TCP 重查”(这是我这个服务端的做法;有的服务端会保留放得下的那部分记录再置 TC);客户端库一般会自动回退到 TCP,多一次连接;实例数多的服务不适合只靠一个 SRV 记录集合,要么用 EDNS 提高 UDP 上限,要么拆分

应答超过 512 字节会被截断,要回退 TCP。

分步演示

DNS 服务发现
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。DNS 服务是我用 dnspython 2.9.0 写的权威服务(msl.MiniDNS:UDP 和 TCP,区域可改,放不下时清空答案并置 TC);客户端的缓存是 dnspython 的 Resolver.cache,遵守记录 TTL 与 SOA minimum 的负缓存;“启动时解析一次”和“不缓存”是我写的两种客户端行为;后端是本进程里的 Python 服务;第一节遵守 TTL 的做了 3 个删除时刻(4.0 / 4.7 / 5.4 s)以错开缓存过期的相位,其余每种只测一次
    • 没有验证:操作系统或 JVM 自带的 DNS 缓存(例如 JVM 的 networkaddress.cache.ttl)与它们对 TTL 的处理;Kubernetes CoreDNS 与 ndots(见 k8s 系列);Consul DNS 接口(见后面的篇目);DNS 轮询(多条 A 记录)在不同客户端上的排序;DNSSEC / DoH
    京ICP备2026067502号-1京公网安备11010502063487号