gRPC DNS 名称解析实现解析:从 Resolver 接口到 c-ares/EventEngine/原生三种实现
【免费下载链接】grpcC++ based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc
导读
在 gRPC 中,客户端通道(ClientChannel)通过"名称解析器(Resolver)"把用户传入的dns:///host:port形式的 URI 转换成一组真实可连接的后端端点地址。本文以仓库中 src/core/resolver/dns/AGENTS.md 为核心骨架,结合 dns_resolver.cc、polling_resolver.cc、ares_resolver.cc 等源码,系统讲解 DNS 解析在 gRPC 中的定位、三种可选解析实现(c-ares / EventEngine / 原生)、核心配置参数、SRV/TXT 扩展查询机制以及如何切换解析器。读完本文,你将掌握 gRPC DNS 解析的完整调用链、关键 Channel Arg 与GRPC_DNS_RESOLVER环境变量的作用,并能在自己的应用中做出正确的解析器选型。
gRPC 名称解析:DNS 模块在整个链路中的位置
gRPC 采用"URI 名称 + Resolver 插件"的架构:用户在创建通道时传入形如dns:///example.com:443的 URI,核心层通过 resolver_registry.cc 中注册的工厂(ResolverFactory)按 URI scheme 找到对应实现。DNS 模块正是dnsscheme 的落地实现,其职责在 src/core/resolver/dns/AGENTS.md 中被明确为:提供Resolver接口的一个具体实现,用 DNS 完成名称解析。
该模块位于 src/core/resolver/dns/,核心文件包括:
- dns_resolver.h:定义
ClientChannelDNSResolverFactory(scheme()返回"dns")与集中式注册入口RegisterDnsResolver; - dns_resolver.cc:
ClientChannelDNSResolver的主体实现,继承自PollingResolver; - service_config_helper.cc:解析 TXT 记录中
grpc_config=服务配置 JSON 的选择逻辑。
从 dns_resolver.cc 可以看到,DNS resolver 通过CoreConfiguration::Builder注册进解析器注册表,成为 gRPC 默认的名称解析机制:
void RegisterDnsResolver(CoreConfiguration::Builder* builder) { VLOG(2) << "Using EventEngine dns resolver"; builder->resolver_registry()->RegisterResolverFactory( std::make_unique<ClientChannelDNSResolverFactory>()); }事实依据:AGENTS.md 明确说明"这是 gRPC 的默认名称解析机制,经过多年生产环境验证"。
三种 DNS 解析实现:c-ares / EventEngine / 原生
AGENTS.md 用三个子目录概括了解析实现的三种方案,结合当前仓库源码结构,三者对应关系如下:
| 实现 | 说明 | 当前仓库中的落点 |
|---|---|---|
| c-ares | 基于 c-ares 异步库,不阻塞线程,可配置查询超时 | ares_resolver.cc、ares_resolver.h(依赖 third_party/cares 的 c-ares 头文件) |
| EventEngine | 使用 gRPC 事件引擎(EventEngine)内置的 DNS 解析能力,与平台 IO 引擎解耦 | dns_resolver.cc 中EventEngineDNSRequestWrapper通过EventEngine::DNSResolver发起查询 |
| native(原生) | 直接使用操作系统的原生 DNS 解析器(如getaddrinfo风格) | 由各平台 EventEngine 实现提供,如 posix_engine.cc、windows_engine.cc 中的 resolver 实现 |
需要说明的是:在当前仓库版本中,DNS 模块源码树只有 dns_resolver.cc 一个统一实现(ClientChannelDNSResolver),c-ares 与原生两种能力被收敛到 EventEngine 抽象之下——StartRequest()调用event_engine_->GetDNSResolver(...)拿到解析器,而EventEngine::DNSResolver的具体后端(c-ares 或平台原生)由 EventEngine 实现决定。AGENTS.md 中的c_ares/、event_engine/、native/三个子目录反映的是该模块历史演进中的目录组织形态,今天的代码已统一经由 EventEngine 接口调度。
从源码结构可以推断:"选哪个解析器"实际上分两层——外层是GRPC_DNS_RESOLVER环境变量决定 EventEngine 使用 c-ares 后端还是原生后端,内层是dnsscheme 统一注册ClientChannelDNSResolverFactory。
核心实现:ClientChannelDNSResolver 的完整解析流程
ClientChannelDNSResolver继承自 polling_resolver.h 中的PollingResolver,具备周期性轮询能力(min_time_between_resolutions控制两次解析的最小间隔,默认 30 秒,见 dns_resolver.cc)。每次解析由StartRequest()触发,创建EventEngineDNSRequestWrapper并并行发起三类 DNS 查询(见 dns_resolver.cc):
- A/AAAA 主机名查询:
LookupHostname解析目标主机名,默认端口为https(443); - SRV 记录查询:若开启
GRPC_ARG_DNS_ENABLE_SRV_QUERIES,查询_grpclb._tcp.<name>,用于 gRPC-LB(grpclb)负载均衡器的发现; - TXT 记录查询:若未禁用服务配置解析,查询
_grpc_config.<name>,用于从 DNS 直接下发 service config。
查询全部完成后,OnResolvedLocked()汇总结果(dns_resolver.cc):
- 若主机名地址与 balancer 地址均为空,返回
UNAVAILABLE错误(如"No results from DNS queries"); - 若至少有一类地址,则依次填充普通端点地址、TXT 解析出的服务配置、SRV 解析出的 balancer 地址;
- 部分失败仅写入
resolution_note,不阻断整体解析。
// 关键配置读取(dns_resolver.cc 构造函数) request_service_config_ = !channel_args() .GetBool(GRPC_ARG_SERVICE_CONFIG_DISABLE_RESOLUTION).value_or(true); enable_srv_queries_ = channel_args() .GetBool(GRPC_ARG_DNS_ENABLE_SRV_QUERIES).value_or(false); query_timeout_ms_ = std::chrono::milliseconds(std::max(0, channel_args() .GetInt(GRPC_ARG_DNS_ARES_QUERY_TIMEOUT_MS) .value_or(GRPC_DNS_DEFAULT_QUERY_TIMEOUT_MS))); // 默认 120000ms超时与重试由 backoff.h 的BackOff策略支持,DNS 模块的默认参数定义在 dns_resolver.cc:
| 参数 | 默认值 |
|---|---|
初始连接退避GRPC_DNS_INITIAL_CONNECT_BACKOFF_SECONDS | 1 秒 |
重连退避倍数GRPC_DNS_RECONNECT_BACKOFF_MULTIPLIER | 1.6 |
最大退避GRPC_DNS_RECONNECT_MAX_BACKOFF_SECONDS | 120 秒 |
抖动GRPC_DNS_RECONNECT_JITTER | 0.2 |
查询超时GRPC_DNS_DEFAULT_QUERY_TIMEOUT_MS | 120000 毫秒 |
DNS 相关的 Channel Arg 与环境变量
Channel Arg(通道参数)
在 include/grpc/impl/channel_arg_names.h 中定义了 DNS 模块的全部通道参数,可在创建通道时通过channel_args设置:
| Channel Arg 名称 | 说明 | 默认值 |
|---|---|---|
grpc.dns_enable_srv_queries(GRPC_ARG_DNS_ENABLE_SRV_QUERIES) | 是否查询_grpclb._tcp.SRV 记录以发现 grpclb 负载均衡器 | false |
grpc.dns_ares_query_timeout(GRPC_ARG_DNS_ARES_QUERY_TIMEOUT_MS) | 单次 DNS 查询超时(毫秒),0表示不超时 | 120000 |
grpc.dns_min_time_between_resolutions_ms(GRPC_ARG_DNS_MIN_TIME_BETWEEN_RESOLUTIONS_MS) | 两次主动解析之间的最小间隔 | 30000毫秒 |
GRPC_ARG_SERVICE_CONFIG_DISABLE_RESOLUTION | 置为true时禁用通过 DNS TXT 记录解析 service config | false(即默认启用 TXT 服务配置解析) |
注意:
GRPC_ARG_DNS_ARES_QUERY_TIMEOUT_MS的命名保留历史语义(源自 c-ares 时代),但当前实现将其复用于 EventEngine 后端,见 dns_resolver.cc 中的 TODO 注释。
环境变量GRPC_DNS_RESOLVER
在 src/core/config/config_vars.yaml 与 config_vars.cc 中定义了环境变量GRPC_DNS_RESOLVER:
"Declares which DNS resolver to use. The default is ares if gRPC is built with c-ares support. Otherwise, the value of this environment variable is ignored."
即:若 gRPC 编译时带上了 c-ares 支持,默认使用 c-ares 解析器;否则该环境变量的取值被忽略(回退到 EventEngine 原生实现)。这一机制说明解析器选型同时受"构建时特性"与"运行时环境变量"双重控制。
SRV 记录:grpclb 负载均衡器发现
当grpc.dns_enable_srv_queries开启后,解析器查询_grpclb._tcp.<target>的 SRV 记录(dns_resolver.cc)。SRV 返回的每个条目都包含主机与端口,随后为每个 balancer 主机再发起一次 A/AAAA 查询(OnSRVResolved→OnBalancerHostnamesResolved,见 dns_resolver.cc)。
值得注意的实现细节:
- balancer 地址会附带
GRPC_ARG_DEFAULT_AUTHORITY通道参数(取 SRV 记录中的主机名),保证后续 RPC 的 authority 正确(dns_resolver.cc); - 若 SRV 查询超时(
timeout_handle_已被清空),后续 balancer 主机名查询不再发起,并记录错误"timed out - not initiating subsequent balancer hostname requests"; - balancer 地址通过
SetGrpcLbBalancerAddresses注入到解析结果,供 grpclb 负载均衡策略使用(dns_resolver.cc)。
TXT 记录:通过 DNS 下发 Service Config
若未禁用,解析器会查询_grpc_config.<target>的 TXT 记录,并寻找以grpc_config=为前缀的条目(dns_resolver.cc)。找到后,JSON 内容交由 service_config_helper.cc 的ChooseServiceConfig处理:
- 解析为 JSON 数组,每项为一个"服务配置选择"(
ServiceConfigChoice),字段包括clientLanguage、percentage、clientHostname、serviceConfig; - 依次匹配:语言必须是
c++(若指定)、当前主机名必须在clientHostname列表内(若指定)、随机百分比rand() % 100必须落在percentage内(若指定); - 命中第一个匹配项即返回其
serviceConfigJSON,否则返回空串(表示无服务配置)。
// service_config_helper.cc 中的选择判定 if (!choice.client_language.empty() && !vector_contains(choice.client_language, "c++")) continue; if (!choice.client_hostname.empty() && !vector_contains(choice.client_hostname, grpc_gethostname())) continue; if (choice.percentage != -1) { int random_pct = rand() % 100; if (random_pct > choice.percentage || choice.percentage == 0) continue; }这种机制使运维人员可以在不修改客户端代码的前提下,通过 DNS TXT 记录灰度下发负载均衡策略、重试配置等 service config。TXT 解析失败时,只要主机名地址解析成功,错误仅记录在resolution_note中,不会阻断连接(见MaybePopulateServiceConfigLocked的注释,dns_resolver.cc)。
解析器选型对性能的影响与建议
AGENTS.md 在 Notes 中特别强调:"选择哪个 DNS 解析器会对性能产生显著影响,为你的应用选对解析器很重要。"结合上文可以给出如下选型参考:
- c-ares 后端:异步、非阻塞,适合高并发、大量 channel 的服务器场景;依赖第三方库 third_party/cares,需在构建时启用,且支持
grpc.dns_ares_query_timeout超时控制; - EventEngine 原生后端:直接利用操作系统解析器,部署简单、无额外依赖,适合对解析吞吐要求不高的客户端场景;
- 两者都通过
EventEngine::DNSResolver统一接口接入,业务代码无需区分。
从代码层面看,ClientChannelDNSResolver::StartRequest()通过event_engine_->GetDNSResolver({/*dns_server=*/authority()})获取解析器(dns_resolver.cc),解析器创建失败时会把错误同时写入addresses与service_config并立即完成请求。此外,IsValidUri会拒绝没有 server name 的dns:URI(dns_resolver.cc),保证dns:///后必须携带目标主机。
进一步阅读
- 解析器抽象与注册机制:resolver.h、resolver_registry.cc
- 轮询式解析器基类:polling_resolver.cc
- EventEngine DNS 接口与 c-ares 后端:ares_resolver.h、ares_resolver.cc
- 通道参数定义:channel_arg_names.h
- 环境变量定义:config_vars.yaml
- 相关测试可参考 test/core/resolver 目录下的解析器测试用例
【免费下载链接】grpcC++ based gRPC (C++, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考