服务发现与 Name Resolver 原理¶
在动态容器化与微服务环境中,后端服务的 IP 地址随时可能因扩缩容、故障漂移或滚动更新而发生变化。gRPC 提供了插件化的 名称解析器(Name Resolver) 架构,用于将逻辑服务名称解析为动态的实际物理网络地址列表。
1. gRPC Target URI 命名规范¶
调用 grpc.Dial 时传入的连接目标遵循标准 RFC 3986 URI 语法:
scheme:指定使用的 Resolver 插件类型(如dns、passthrough、unix或自定义的etcd、consul)。如果未指定,默认通常为passthrough或环境全局配置。authority:特定于 Resolver 的认证/上游服务实体(可选,默认通常为空,由三个正斜杠///占位)。endpoint:需要解析的服务名称或具体标识。
常见原生 URI 范例¶
dns:///user-service.production:50051:通过标准系统 DNS 解析。passthrough:///127.0.0.1:8080:直连固定 IP 与端口,不进行动态服务发现。unix:///var/run/grpc-socket.sock:基于操作系统本地 Unix Domain Socket 建立 IPC 通道。
2. Resolver 内部工作流程¶
sequenceDiagram
autonumber
participant App as 客户端应用
participant Core as gRPC 通道 (Channel)
participant Reg as Resolver 注册中心
participant Res as 具体 Resolver 实例
participant SD as 注册中心 (如 Consul / etcd)
App->>Core: grpc.Dial("consul:///order-service")
Core->>Reg: 查询 scheme="consul" 对应的 Builder
Reg-->>Core: 返回 ConsulResolverBuilder
Core->>Res: 调用 Build() 创建 Resolver 实例,传入 cc (ClientConn 回调接口)
Res->>SD: 建立 Watch 监听 order-service 实例变动
SD-->>Res: 推送实例列表 [10.0.1.5:50051, 10.0.1.6:50051]
Res->>Core: 调用 cc.UpdateState({Addresses: [...]})
Note over Core: 通道通知 Balancer 更新连接池与子通道 (Subchannels)
3. 自定义 Resolver 开发实战(以 Go 为例)¶
gRPC 提供了极具扩展性的 resolver.Builder 与 resolver.Resolver 接口:
package main
import (
"google.golang.org/grpc/resolver"
)
const myScheme = "custom"
// 1. 实现 resolver.Builder 接口
type customBuilder struct{}
func (b *customBuilder) Build(
target resolver.Target,
cc resolver.ClientConn,
opts resolver.BuildOptions,
) (resolver.Resolver, error) {
r := &customResolver{
target: target,
cc: cc,
}
r.start()
return r, nil
}
func (b *customBuilder) Scheme() string {
return myScheme
}
// 2. 实现 resolver.Resolver 接口
type customResolver struct {
target resolver.Target
cc resolver.ClientConn
}
func (r *customResolver) start() {
// 模拟从注册中心拉取到两个节点地址
addrs := []resolver.Address{
{Addr: "192.168.1.101:50051"},
{Addr: "192.168.1.102:50051"},
}
// 将最新解析到的地址集合推送给 gRPC 核心通道
r.cc.UpdateState(resolver.State{
Addresses: addrs,
})
}
func (r *customResolver) ResolveNow(resolver.ResolveNowOptions) {
// 当连接异常时,gRPC 核心会触发此方法请求立即重新解析
r.start()
}
func (r *customResolver) Close() {
// 释放相关资源,如关闭 etcd/Consul Watcher
}
// 3. 全局注册插件
func init() {
resolver.Register(&customBuilder{})
}
4. Kubernetes 环境下的选型建议¶
在 Kubernetes 环境中,最常见且最稳健的两种方案:
- Headless Service + 原生 DNS Resolver:
- 创建
ClusterIP: None的 Headless Service。 - 客户端配置
dns:///service-name.namespace.svc.cluster.local:50051配合round_robin。 - Kubernetes DNS 会将该服务下所有 Ready 状态的 Pod IP 以 A 记录全部返回,gRPC 客户端在本地完成多 Pod 轮询直连。
- Kubernetes Gateway / Envoy L7 Proxy:
- 如果客户端多语言异构且不愿在 SDK 维护动态 DNS,在集群内统一部署 Envoy Ingress/Gateway 充当七层代理反向分发。