Resolver 与 Balancer —— 连接管理三层联动

一、三层角色与数据流

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 层。

相关推荐
huaweichenai1 小时前
spring boot实现base64上传
java·spring boot·后端
点纭1 小时前
spring ai1.0实战:ChatClient(2)
java·后端·spring
yunwei371 小时前
eBPF 实例教程:BPF Arena 零拷贝共享内存
linux·后端·性能优化
UrbanJazzerati1 小时前
AutoHotkey v2 零基础快速入门 Part 1
后端
geovindu2 小时前
rust: pool Pattern
开发语言·后端·设计模式·rust·对象池简写模式
Rain的Java大神之路2 小时前
Java 工程师做 AI,核心机会在「落地」而非「训练」
java·后端·ai编程
柠檬味拥抱2 小时前
基于 YOLO11 的六类生活垃圾视觉分类系统 | 用于社区投放点智能督导、分拣线品类分流、投错率统计分析 | 附完整源码与效果演示
后端
程序员小新7002 小时前
RAG 实战与踩坑:交通法规智能问答助手
人工智能·后端
拖孩2 小时前
AI 时代,个人开发者到底能不能靠一个小程序赚到钱
前端·后端·微信小程序