bigywater.tech
gRPC · 15

名字解析:dns 与 passthrough 解析器

grpc-go 解析 dns:/// 目标时,到底查了什么、什么时候重新查、TTL 起不起作用?域名不存在时调用会怎样?这篇在本机起了一个小 DNS 服务器,把每个查询都记下来。

30.0 秒
默认设置下,后端 6 秒时停掉后,DNS 重新解析真正发出的时刻(记录 2 秒时就改了)
[9 10 11] vs [30 0 0]
DNS TXT 里写的 round_robin 在默认环境生效;关掉环境变量后退回 pick_first
443
写 dns:///svc.test 不带端口时 dialer 收到的端口

两个常用的解析器

实测 · 本机执行输出
  passthrough:///svc.test:7000         → DNS 查询:0 个查询;dialer 收到的地址:svc.test:7000;失败 code=Unavailable message="connection error: desc = \"transport: Error while dialing: 没有这个假地址的后端:svc.test:7000\""
  dns://<本机DNS>/svc.test:7000          → DNS 查询:A 1、AAAA 1、TXT 2;dialer 收到的地址:10.9.0.1:7000;成功,后端 0
  dns://<本机DNS>/svc.test               → DNS 查询:A 1、AAAA 1、TXT 2;dialer 收到的地址:10.9.0.1:443;成功,后端 0
  dns:///10.9.0.1:7000                 → DNS 查询:0 个查询;dialer 收到的地址:10.9.0.1:7000;成功,后端 0
  svc.test:7000                        → DNS 查询:0 个查询;dialer 收到的地址:;失败 code=Unavailable message="name resolver error: produced zero addresses"

passthrough 不解析,把名字原样交给 dialer;dns 解析成 IP,一个域名下的所有 A 记录都成为地址。没有 scheme 的 target,NewClient 当作 dns。

查了什么

实测 · 本机执行输出
  90 次调用各后端收到 [30 30 30];dialer 收到的地址:10.9.0.1:7000 10.9.0.3:7000 10.9.0.2:7000
  DNS 服务器收到的查询:A 1、AAAA 1、TXT 2
  查询的名字:TypeAAAA svc.test.;TypeA svc.test.;TypeTXT _grpc_config.svc.test.;TypeTXT _grpc_config.svc.test.lan.

启动时对域名发 A、AAAA 查询,另外还会查 _grpc_config.<域名> 的 TXT(服务配置)。不写端口时默认 443。

TXT 里的服务配置

实测 · 本机执行输出
  默认环境                                   30 次调用各后端收到 [9 10 11];DNS 查询:A 1、AAAA 1、TXT 1
  GRPC_ENABLE_TXT_SERVICE_CONFIG=false   30 次调用各后端收到 [30 0 0];DNS 查询:A 1、AAAA 1

在这个版本里默认读取并应用 DNS TXT 里的服务配置(如负载均衡策略)。不想让 DNS 改变客户端行为,用 GRPC_ENABLE_TXT_SERVICE_CONFIG=false 关掉。

不看 TTL、不轮询

实测 · 本机执行输出
  DNS 的 A 查询时刻(秒,相对 t0):[30.0]
  0–2 秒(记录 [1 2 3]):后端集合 [1 2 3],失败 0 次
  2–6 秒(记录已换成 [3 4 5],后端 1 还在):后端集合 [1 2 3],失败 0 次
  6 秒起(后端 1 已停掉),到最后一次 A 查询之前:[6, 30.0) 后端集合 [2 3],失败 0 次
  最后一次 A 查询之后:后端集合 [3 4 5],失败 0 次

解析成功后,解析器只等连接管理发来的“重新解析”请求,并且两次解析至少隔 30 秒(可用 dns.SetMinResolutionInterval 调整)。DNS 记录改了,客户端不会马上知道。

改小最小间隔

实测 · 本机执行输出
  DNS 的 A 查询时刻(秒,相对 t0):[6.0 7.0]
  0–2 秒(记录 [1 2 3]):后端集合 [1 2 3],失败 0 次
  2–6 秒(记录已换成 [3 4 5],后端 1 还在):后端集合 [1 2 3],失败 0 次
  6 秒起(后端 1 已停掉),到最后一次 A 查询之前:[6, 7.0) 后端集合 [2 3 4 5],失败 0 次
  最后一次 A 查询之后:后端集合 [3 4 5],失败 0 次

改成 1 秒后,连接出问题的当下就会重新解析并用上新地址。但没有连接问题时仍然不会主动查询。

解析失败

实测 · 本机执行输出
  fail-fast 调用:2.3 ms 后返回 code=Unavailable message="name resolver error: produced zero addresses"
  3 秒时 DNS 加上记录;这时 A 查询时刻(相对 t0)[0.0 0.0 1.7 1.7]
  WaitForReady 调用:4.0 秒后返回 code=OK
  整个过程 A 查询时刻(秒):[0.0 0.0 1.7 1.7 4.0]
  A 查询的名字依次是:svc.test. svc.test.lan. svc.test. svc.test.lan. svc.test.

域名不存在时,调用立即以 Unavailable 失败;解析器按指数退避重试。WaitForReady 的调用会等到某次重试成功。

空答案

实测 · 本机执行输出
  fail-fast 调用:0.7 ms 后返回 code=Unavailable message="name resolver error: produced zero addresses"

域名存在但没有 A 记录,与域名不存在的错误信息相同(produced zero addresses)。

分步演示

名字解析
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1 / grpc-go v1.84.0 / protobuf-go v1.36.12 / golang.org/x/net v0.57.0 / protoc 36.2(Homebrew);服务端和客户端都是本机起的 Go 进程(127.0.0.1,端口 9800 以上,不访问任何外部服务);需要看线上字节时用我自己写的 TCP 抓包代理加 golang.org/x/net/http2 的 Framer 解析 HTTP/2 帧;计时用 time.Now 或 perf_counter,数字每次运行会有小幅抖动。DNS 服务器是我用 golang.org/x/net/dns/dnsmessage 写的、只监听本机 UDP 的小程序,每个查询都被记录;A 记录给的是假 IP 10.9.0.x,由自定义 dialer 映射到本机真实后端,所以验证的是解析与地址选择,不涉及真实网络;TXT 实验用子进程设置环境变量;读源码确认:watcher 在解析成功后等待 ResolveNow 且至少间隔 MinResolutionInterval(internal/resolver/dns/dns_resolver.go)
    • 没有验证:系统解析器(没有 scheme 的 target 实际去问了谁,以及它的错误是什么);SRV/grpclb 的查询(默认关闭);IPv6 的 AAAA 记录有内容时的行为;大量 A 记录(超过 UDP 报文大小、需要 TCP 回落)时的行为;真实 DNS 缓存/负载均衡 DNS 的行为;重试退避的具体参数(只观察到 0.0、1.7、4.0 这一次运行的时刻)
    京ICP备2026067502号-1京公网安备11010502063487号