一、三层角色与数据流
scss
NewClient("dns:///user-svc:8080")
│ 解析 target,按 scheme 查注册表
▼
┌─────────────────────────────────────────────────────────────┐
│ ① Resolver 层(服务发现) resolver/resolver.go │
│ Builder.Scheme()="dns" → Build() 启动解析 │
│ 产出: []Endpoint{Addresses: [10.0.0.1:8080,10.0.0.2:8080]}│
│ + ServiceConfig(可选,服务端下发的 JSON 策略) │
└───────────────┬─────────────────────────────────────────────┘
│ UpdateState(地址变了!)
▼
┌─────────────────────────────────────────────────────────────┐
│ ② Balancer 层(负载均衡策略) balancer/balancer.go │
│ 收到地址列表 → 为每个(组)地址创建 SubConn │
│ SubConn 状态变化 → 重新计算 → 产出新的 Picker │
└───────────────┬─────────────────────────────────────────────┘
│ UpdateState(State{ConnectivityState, Picker})
▼
┌─────────────────────────────────────────────────────────────┐
│ ③ SubConn/Transport 层(具体连接) │
│ SubConn.Connect() → addrConn → newHTTP2Client(真实 TCP+H2)│
└─────────────────────────────────────────────────────────────┘
│
▼
pickerWrapper.pick (08篇站④) ------ 每次 RPC 来"取货"
门面与策略层之间靠两个 wrapper 解耦 :resolver_wrapper.go 与 balancer_wrapper.go------ClientConn 不直接认识任何具体 Resolver/Balancer,全部通过接口对话。
二、Resolver:从名字到地址
接口(resolver/resolver.go:301-327)
scss
type Builder interface {
// gRPC 建通道时同步调用;失败 = 建通道失败
Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
Scheme() string // "dns"/"passthrough"/"unix"/"xds"...
}
type Resolver interface {
ResolveNow(ResolveNowOptions) // "再解析一次"提示(可忽略)
Close()
}
注册表机制(resolver/resolver.go:54-61)
scss
resolver.Register(myBuilder) // init() 里登记 scheme → builder
Get(scheme) // NewClient 时按 target 的 scheme 查
target 决定用哪个 resolver(clientconn.go:162-178 的注释就是权威命名文档):
| target 写法 | resolver | 行为 |
|---|---|---|
10.0.0.5:8080 |
默认(dns) | 把 host 当域名解析 |
dns:///user-svc:8080 |
dns | 标准 DNS 解析(A/SRV 记录) |
passthrough:///... |
passthrough | 不做解析,把地址原样交给 balancer |
unix:///path/sock |
unix | Unix domain socket |
xds:///... |
xds | istio/xDS 控制面下发地址 |
写一个自定义 resolver 有多简单?
实现 Builder + Resolver 两个小接口,init() 里 Register,然后 NewClient("myconsul://user-svc") 就能跑。参考 resolver/manual/manual.go(测试用的可编程 resolver,最短样板)与 examples/features/name_resolving。
三、Balancer:从地址列表到 Picker
三个核心接口(balancer/balancer.go)
scss
type Balancer interface { // :344
UpdateClientConnState(ClientConnState) error // 地址列表/配置变了
ResolverError(error) // resolver 报错
UpdateSubConnState(SubConnState) // 某条连接状态变了(已废弃,改用回调)
}
type SubConn interface { // "一条候选连接"的句柄
Connect() // 发起/触发连接
UpdateAddresses([]resolver.Address)
Shutdown()
}
type Picker interface { // :313
Pick(info PickInfo) (PickResult, error)
// 关键约束(注释原文):"Pick should not block"!
// 没准备好就返回 ErrNoSubConnAvailable,gRPC 会等新 Picker 就绪再重试
}
Pick 不许阻塞是接口契约------08 篇看到 pickerWrapper.pick 的阻塞等待,是 wrapper 在等"下一个 Picker 版本",不是 Picker 自己在等。
两个必读实现
pickfirst(默认) ------"选第一个能通的":
css
拿到 [A,B,C] → 只连 A → A 失败才试 B → ...
通道状态 = 这一条连接的状态
适用:单实例、后端本身是 LB(如 k8s Service/云 LB)------把"选实例"交给外部
roundrobin------"轮询所有 READY 的":
css
连 A,B,C 全部 → 全 READY 后通道才 READY
每次 Pick 轮流返回;某条挂了立即从轮询表剔除
切换方式:grpc.WithDefaultServiceConfig({"loadBalancingConfig":{"round_robin":{}}}),或服务端 service config 直接下发(策略由服务端统一下发是微服务治理的常规操作)。
仓库里的策略动物园(balancer/ 目录)
pickfirst、roundrobin、leastrequest、weightedroundrobin、ringhash(一致性哈希,缓存亲和)、rls(路由查找服务)、grpclb、lazy、autosharding......全都是同一个 Balancer 接口的实现------策略层完全开放(12 篇设计思想:"注册表 + 小接口")。
四、三者联动的完整时序(连接自愈的微观过程)
css
t0 NewClient 通道 IDLE,零网络行为
t1 首次 RPC → ExitIdle → resolver.Build → dns 解析 → [A,B]
t2 UpdateState → balancer(round_robin).UpdateClientConnState
→ NewSubConn(A) NewSubConn(B) → 各自 Connect()
t3 A:TCP+H2 成功 → SubConn READY → balancer 收到状态回调
B:连接中 → 产新 Picker v1{A} → 通道 CONNECTING→(A ready 即 READY)
t4 B 也 READY → Picker v2{A,B} → 轮询真正开始
t5 RPC1: pick → A RPC2: pick → B
t6 A 断了! → SubConn(A) TRANSIENT_FAILURE → Picker v3{B}
通道状态短暂 TRANSIENT_FAILURE(若 B 也挂)
→ 指数退避自动重连 A(backoff 包)
t7 DNS 变了(扩容 C) → resolver 推送 [A,B,C] → balancer 建 C 的 SubConn
------ 全程无一次手工重连,业务零感知
对上篇的状态机:通道的 5 态 = 所有 SubConn 状态按策略聚合的结果(round_robin:全挂=TRANSIENT_FAILURE;pickfirst:唯一那条的状态就是通道状态)。
五、Keepalive 与连接层的关系
keepalive PING 由 http2_client.go 的 transport 层收发,判死后的处理是:transport 报错 → SubConn 状态回调 → balancer 换 Picker / 触发重连。所以 05 篇那些"参数不匹配被 GOAWAY"的现象,症状出现在传输层,治理动作发生在 balancer 层。