纲要
- 引言:从服务发现到负载均衡的必然需求
- 理解负载均衡
- 客户端视角:选择哪个服务实例
- 服务端视角:如何均匀分配流量
- 核心目标:根据服务状态,合理分发请求
- 负载均衡的常见方式
- 无状态负载均衡(随机、轮询、哈希、取模)
- 有状态负载均衡(基于服务器负载、响应时间等)
- P2C 算法简介
- gRPC 负载均衡设计
- Balancer 与 Picker 的角色
- 策略模式与装饰模式的应用
- Builder、Balancer、SubConn、Picker 接口协作
- go-zero 中的负载均衡实现
- go-zero 默认的 P2C 负载均衡器
- Picker 的构建与选择流程
- 代码示例:自定义负载均衡器的基本骨架
- 关键数据结构与决策过程
- 负载均衡与连接管理的联动
- 总结
引言
在上一篇文章中,我们深入分析了 go-zero@latest 的服务注册与发现机制,理解了它如何借助 etcd 让客户端动态获取服务实例列表。然而,拿到一串实例地址之后,新的问题迎面而来:应该选择哪一个实例发起调用? 这就是负载均衡(Load Balancing)要解决的核心问题。本文将讲解 go-zero 在微服务治理中的负载均衡原理,并结合 gRPC 的扩展机制,展示它是如何实现智能、高效的请求分发的。
理解负载均衡
负载均衡可以从两个视角来理解:
- 客户端视角:服务消费者通过服务发现拿到多个功能等价的实例地址后,需要一种策略从中选出一个“最佳”的节点,从而避免单一节点过载,同时提高整体可用性。
- 服务端视角:当请求流量巨大时,单个服务实例无法承受,需要将请求均匀分摊到集群中所有节点上,以实现水平扩展。
无论从哪个角度看,负载均衡的核心目的都是 根据服务实例的实时状态(或无状态的均匀性),将请求合理分配到不同的节点上,使系统资源利用率最大化,并保证调用延迟的稳定性。
负载均衡的常见方式
负载均衡算法可以分为无状态和有状态两大类。
| 无状态 | 随机、轮询、一致性哈希、取模 | 实现简单,不需要记录服务器状态,分配相对均匀 | 服务实例性能相近,请求处理耗时稳定 |
| 有状态 | 最少连接数、加权轮询、P2C | 参考服务器当前负载(请求数、CPU、响应时间等)动态选择,更智能但复杂度高 | 实例性能差异大,或请求处理时间波动明显 |
go-zero 采用了有状态的负载均衡算法 —— P2C(Power of Two Choices)。该算法的思想是:随机挑选两个节点,然后选择其中负载较低的一个。这种方式在保证均衡效果的同时,避免了全量节点状态同步的开销,兼具了随机算法的简洁和自适应能力。
gRPC 负载均衡设计
gRPC 原生并不绑定特定的负载均衡策略,而是通过一套接口抽象,允许用户接入自定义实现。这套接口与之前介绍的服务发现 resolver 配合使用,核心角色包括:
- Balancer:负载均衡器入口,负责创建 Picker,并管理子连接(SubConn)。
- Builder:构建 Balancer 实例的工厂,通过 balancer.Register 注册。
- SubConn:代表到某个具体服务实例的连接。
- Picker:每次 RPC 调用时,从可用 SubConn 中选择一个。
框架的设计遵循策略模式(可以替换不同的负载均衡算法)和装饰模式(利用 ClientConn 更新连接状态)。Balancer 监听来自 resolver 的地址更新,并将地址维护成一组 SubConn;当请求到来时,Picker 从这些 SubConn 中决策出一个,完成实际调用。
#mermaid-svg-2q6i5HAgfnLk3QEa{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2q6i5HAgfnLk3QEa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2q6i5HAgfnLk3QEa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2q6i5HAgfnLk3QEa .error-icon{fill:#552222;}#mermaid-svg-2q6i5HAgfnLk3QEa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2q6i5HAgfnLk3QEa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2q6i5HAgfnLk3QEa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2q6i5HAgfnLk3QEa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2q6i5HAgfnLk3QEa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2q6i5HAgfnLk3QEa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2q6i5HAgfnLk3QEa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2q6i5HAgfnLk3QEa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2q6i5HAgfnLk3QEa .marker.cross{stroke:#333333;}#mermaid-svg-2q6i5HAgfnLk3QEa svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2q6i5HAgfnLk3QEa p{margin:0;}#mermaid-svg-2q6i5HAgfnLk3QEa g.classGroup text{fill:#9370DB;stroke:none;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-2q6i5HAgfnLk3QEa g.classGroup text .title{font-weight:bolder;}#mermaid-svg-2q6i5HAgfnLk3QEa .cluster-label text{fill:#333;}#mermaid-svg-2q6i5HAgfnLk3QEa .cluster-label span{color:#333;}#mermaid-svg-2q6i5HAgfnLk3QEa .cluster-label span p{background-color:transparent;}#mermaid-svg-2q6i5HAgfnLk3QEa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2q6i5HAgfnLk3QEa .cluster text{fill:#333;}#mermaid-svg-2q6i5HAgfnLk3QEa .cluster span{color:#333;}#mermaid-svg-2q6i5HAgfnLk3QEa .nodeLabel,#mermaid-svg-2q6i5HAgfnLk3QEa .edgeLabel{color:#131300;}#mermaid-svg-2q6i5HAgfnLk3QEa .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-2q6i5HAgfnLk3QEa .label text{fill:#131300;}#mermaid-svg-2q6i5HAgfnLk3QEa .labelBkg{background:#ECECFF;}#mermaid-svg-2q6i5HAgfnLk3QEa .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-2q6i5HAgfnLk3QEa .classTitle{font-weight:bolder;}#mermaid-svg-2q6i5HAgfnLk3QEa .node rect,#mermaid-svg-2q6i5HAgfnLk3QEa .node circle,#mermaid-svg-2q6i5HAgfnLk3QEa .node ellipse,#mermaid-svg-2q6i5HAgfnLk3QEa .node polygon,#mermaid-svg-2q6i5HAgfnLk3QEa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2q6i5HAgfnLk3QEa .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa g.clickable{cursor:pointer;}#mermaid-svg-2q6i5HAgfnLk3QEa g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-2q6i5HAgfnLk3QEa g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-2q6i5HAgfnLk3QEa .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-2q6i5HAgfnLk3QEa .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-2q6i5HAgfnLk3QEa .dashed-line{stroke-dasharray:3;}#mermaid-svg-2q6i5HAgfnLk3QEa .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-2q6i5HAgfnLk3QEa #compositionStart,#mermaid-svg-2q6i5HAgfnLk3QEa .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa #compositionEnd,#mermaid-svg-2q6i5HAgfnLk3QEa .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa #dependencyStart,#mermaid-svg-2q6i5HAgfnLk3QEa .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa #dependencyStart,#mermaid-svg-2q6i5HAgfnLk3QEa .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa #extensionStart,#mermaid-svg-2q6i5HAgfnLk3QEa .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa #extensionEnd,#mermaid-svg-2q6i5HAgfnLk3QEa .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa #aggregationStart,#mermaid-svg-2q6i5HAgfnLk3QEa .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa #aggregationEnd,#mermaid-svg-2q6i5HAgfnLk3QEa .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa #lollipopStart,#mermaid-svg-2q6i5HAgfnLk3QEa .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa #lollipopEnd,#mermaid-svg-2q6i5HAgfnLk3QEa .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2q6i5HAgfnLk3QEa .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-2q6i5HAgfnLk3QEa .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2q6i5HAgfnLk3QEa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2q6i5HAgfnLk3QEa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2q6i5HAgfnLk3QEa :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
creates
creates
manages
«interface»
Balancer
+UpdateClientConnState(ClientConnState)
+ResolverError(error)
+UpdateSubConnState(SubConn, SubConnState)
+Close()
«interface»
Picker
+Pick(PickInfo) : PickResult
SubConn
+UpdateAddresses([]Address)
+Connect()
«interface»
Builder
+Build(ClientConn, BuildOptions) : Balancer
+Name() : string
每当 resolver 推送新的地址列表,Balancer 会调用 UpdateClientConnState,内部创建对应的 SubConn 并更新给 Picker。Picker 在 Pick 时根据算法选择一个 SubConn 并返回,gRPC 框架就使用该连接发送 RPC。
go-zero 中的负载均衡实现
go-zero 在 zrpc 包内实现了基于 P2C 的 Balancer 和 Picker。当我们在 zrpc.MustNewClient 里传入 Etcd 配置时,框架会自动注册这个 balancer。
核心流程可以概括为:
以下是一个简化版的 P2C Picker 实现骨架,用于说明其工作方式:
type p2cPicker struct {
mu sync.Mutex
conns []*subConn
}
func (p *p2cPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
p.mu.Lock()
defer p.mu.Unlock()
if len(p.conns) == 0 {
return balancer.PickResult{}, balancer.ErrNoSubConnAvailable
}
// 随机抽取两个连接(Power of Two Choices)
a := p.conns[rand.Intn(len(p.conns))]
b := p.conns[rand.Intn(len(p.conns))]
var picked *subConn
if a.load() < b.load() {
picked = a
} else {
picked = b
}
// 标记开始处理请求,用于计算负载
picked.start()
return balancer.PickResult{SubConn: picked.SubConn, Done: func(info balancer.DoneInfo){
picked.done(info.Err)
}}, nil
}
其中 subConn 包装了 gRPC 的 SubConn,并跟踪当前请求数和错误率。load() 函数根据这两个因素计算出一个综合负载值,从而影响选择。
负载均衡与连接管理的联动
负载均衡并非孤立工作,它与 resolver 的地址更新紧密配合。当 etcd 中某个实例下线,resolver 会更新 Balancer 的状态,Balancer 移除对应的 SubConn 并重新构建 Picker,从而保证后续请求不会被发往已失效的节点。这种动态调整机制,使得系统在扩容、缩容或故障时依然能维持稳定的负载分布。
实例2实例1Picker (P2C)BalancerResolver (etcd)实例2实例1Picker (P2C)BalancerResolver (etcd)#mermaid-svg-gifbtGH0NRl0htBh{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gifbtGH0NRl0htBh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gifbtGH0NRl0htBh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gifbtGH0NRl0htBh .error-icon{fill:#552222;}#mermaid-svg-gifbtGH0NRl0htBh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gifbtGH0NRl0htBh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gifbtGH0NRl0htBh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gifbtGH0NRl0htBh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gifbtGH0NRl0htBh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gifbtGH0NRl0htBh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gifbtGH0NRl0htBh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gifbtGH0NRl0htBh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gifbtGH0NRl0htBh .marker.cross{stroke:#333333;}#mermaid-svg-gifbtGH0NRl0htBh svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gifbtGH0NRl0htBh p{margin:0;}#mermaid-svg-gifbtGH0NRl0htBh .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gifbtGH0NRl0htBh text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-gifbtGH0NRl0htBh .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gifbtGH0NRl0htBh .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-gifbtGH0NRl0htBh .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-gifbtGH0NRl0htBh .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-gifbtGH0NRl0htBh #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-gifbtGH0NRl0htBh .sequenceNumber{fill:white;}#mermaid-svg-gifbtGH0NRl0htBh #sequencenumber{fill:#333;}#mermaid-svg-gifbtGH0NRl0htBh #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-gifbtGH0NRl0htBh .messageText{fill:#333;stroke:none;}#mermaid-svg-gifbtGH0NRl0htBh .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gifbtGH0NRl0htBh .labelText,#mermaid-svg-gifbtGH0NRl0htBh .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-gifbtGH0NRl0htBh .loopText,#mermaid-svg-gifbtGH0NRl0htBh .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-gifbtGH0NRl0htBh .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gifbtGH0NRl0htBh .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-gifbtGH0NRl0htBh .noteText,#mermaid-svg-gifbtGH0NRl0htBh .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-gifbtGH0NRl0htBh .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gifbtGH0NRl0htBh .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gifbtGH0NRl0htBh .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gifbtGH0NRl0htBh .actorPopupMenu{position:absolute;}#mermaid-svg-gifbtGH0NRl0htBh .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-gifbtGH0NRl0htBh .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gifbtGH0NRl0htBh .actor-man circle,#mermaid-svg-gifbtGH0NRl0htBh line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-gifbtGH0NRl0htBh :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}随机选择 S1, S2loop[每次 RPC 调用]UpdateClientConnState([S1, S2])创建 SubConn S1, S2更新 SubConn 列表发起请求响应成功更新 S1 负载值S2 实例下线移除 SubConn S2更新列表只剩 S1
总结
- 负载均衡是微服务治理的关键环节,负责从众多实例中选出合适节点。
- go-zero 选用 P2C 算法,在高效性与准确性之间取得平衡,兼具无状态随机算法的简洁和有状态算法的智能。
- gRPC 提供了 Balancer / Picker 抽象,go-zero 以此为基础实现了可插拔的负载均衡策略。
- 结合 etcd 的服务发现,go-zero 能够在实例变化时自动调整负载均衡器,实现透明的、自适应的流量分发。
理解了这些原理后,我们不仅可以在项目中放心使用 go-zero 的默认负载均衡,还能根据特殊需求快速实现自定义的 Picker,将微服务的治理能力扩展到更复杂的业务场景。接下来,我们将继续深入 go-zero 的其他治理模块,如熔断、限流等,逐步构建出一套完整的微服务运行框架。


