基於 gRPC 的服務註冊與發現和負載均衡的原理與實戰
gRPC是一個現代的、高效能、開源的和語言無關的通用 RPC 框架,基於 HTTP2 協議設計,序列化使用 PB(Protocol Buffer),PB 是一種語言無關的高效能序列化框架,基於 HTTP2+PB 保證了的高效能。go-zero是一個開源的微服務框架,支援 http 和 rpc 協議,其中 rpc 底層依賴 gRPC,本文會結合 gRPC 和 go-zero 原始碼從實戰的角度和大家一起分析下服務註冊與發現和負載均衡的實現原理
基本原理
原理流程圖如下:
從圖中可以看出 go-zero 實現了 gRPC 的 resolver 和 balancer 介面,然後通過 gprc.Register 方法註冊到 gRPC 中,resolver 模組提供了服務註冊的功能,balancer 模組提供了負載均衡的功能。當 client 發起服務呼叫的時候會根據 resolver 註冊進來的服務列表,使用註冊進來的 balancer 選擇一個服務發起請求,如果沒有進行註冊 gRPC 會使用預設的 resolver 和 balancer。服務地址的變更會同步到 etcd 中,go-zero 監聽 etcd 的變化通過 resolver 更新服務列表
Resolver 模組
通過 resolver.Register 方法可以註冊自定義的 Resolver,Register 方法定義如下,其中 Builder 為 interface 型別,因此自定義 resolver 需要實現該介面,Builder 定義如下
// Register 註冊自定義resolver
func Register(b Builder) {
m[b.Scheme()] = b
}
// Builder 定義resolver builder
type Builder interface {
Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)
Scheme() string
}
Build 方法的第一個引數 target 的型別為Target定義如下,建立 ClientConn 呼叫 grpc.DialContext 的第二個引數 target 經過解析後需要符合這個結構定義,target 定義格式為: scheme://authority/endpoint_name
type Target struct {
Scheme string // 表示要使用的名稱系統
Authority string // 表示一些特定於方案的引導資訊
Endpoint string // 指出一個具體的名字
}
Build 方法返回的 Resolver 也是一個介面型別。定義如下
type Resolver interface {
ResolveNow(ResolveNowOptions)
Close()
}
流程圖下圖
因此可以看出自定義 Resolver 需要實現如下步驟:
- 定義 target
- 實現 resolver.Builder
- 實現 resolver.Resolver
- 呼叫 resolver.Register 註冊自定義的 Resolver,其中 name 為 target 中的 scheme
- 實現服務發現邏輯 (etcd、consul、zookeeper)
- 通過 resolver.ClientConn 實現服務地址的更新
go-zero 中 target 的定義如下,預設的名字為discov
// BuildDiscovTarget 構建target
func BuildDiscovTarget(endpoints []string, key string) string {
return fmt.Sprintf("%s://%s/%s", resolver.DiscovScheme,
strings.Join(endpoints, resolver.EndpointSep), key)
}
// RegisterResolver 註冊自定義的Resolver
func RegisterResolver() {
resolver.Register(&dirBuilder)
resolver.Register(&disBuilder)
}
Build 方法的實現如下
func (d *discovBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (
resolver.Resolver, error) {
hosts := strings.FieldsFunc(target.Authority, func(r rune) bool {
return r == EndpointSepChar
})
// 獲取服務列表
sub, err := discov.NewSubscriber(hosts, target.Endpoint)
if err != nil {
return nil, err
}
update := func() {
var addrs []resolver.Address
for _, val := range subset(sub.Values(), subsetSize) {
addrs = append(addrs, resolver.Address{
Addr: val,
})
}
// 呼叫UpdateState方法更新
cc.UpdateState(resolver.State{
Addresses: addrs,
})
}
// 新增監聽,當服務地址發生變化會觸發更新
sub.AddListener(update)
// 更新服務列表
update()
return &nopResolver{cc: cc}, nil
}
那麼註冊進來的 resolver 在哪裡用到的呢?當建立客戶端的時候呼叫 DialContext 方法建立 ClientConn 的時候回進行如下操作
- 攔截器處理
- 各種配置項處理
- 解析 target
- 獲取 resolver
- 建立 ccResolverWrapper
建立 clientConn 的時候回根據 target 解析出 scheme,然後根據 scheme 去找已註冊對應的 resolver,如果沒有找到則使用預設的 resolver
ccResolverWrapper 的流程如下圖,在這裡 resolver 會和 balancer 會進行關聯,balancer 的處理方式和 resolver 類似也是通過 wrapper 進行了一次封裝
緊著著會根據獲取到的地址建立 htt2 的連結
到此 ClientConn 建立過程基本結束,我們再一起梳理一下整個過程,首先獲取 resolver,其中 ccResolverWrapper 實現了 resovler.ClientConn 介面,通過 Resolver 的 UpdateState 方法觸發獲取 Balancer,獲取 Balancer,其中 ccBalancerWrapper 實現了 balancer.ClientConn 介面,通過 Balnacer 的 UpdateClientConnState 方法觸發建立連線 (SubConn),最後建立 HTTP2 Client
Balancer 模組
balancer 模組用來在客戶端發起請求時進行負載均衡,如果沒有註冊自定義的 balancer 的話 gRPC 會採用預設的負載均衡演算法,流程圖如下
在 go-zero 中自定義的 balancer 主要實現瞭如下步驟:
- 實現 PickerBuilder,Build 方法返回 balancer.Picker
- 實現 balancer.Picker,Pick 方法實現負載均衡演算法邏輯
- 呼叫 balancer.Registet 註冊自定義 Balancer
- 使用 baseBuilder 註冊,框架已提供了 baseBuilder 和 baseBalancer 實現了 Builer 和 Balancer
Build 方法的實現如下
func (b *p2cPickerBuilder) Build(readySCs map[resolver.Address]balancer.SubConn) balancer.Picker {
if len(readySCs) == 0 {
return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
}
var conns []*subConn
for addr, conn := range readySCs {
conns = append(conns, &subConn{
addr: addr,
conn: conn,
success: initSuccess,
})
}
return &p2cPicker{
conns: conns,
r: rand.New(rand.NewSource(time.Now().UnixNano())),
stamp: syncx.NewAtomicDuration(),
}
}
go-zero 中預設實現了 p2c 負載均衡演算法,該演算法的優勢是能彈性的處理各個節點的請求,Pick 的實現如下
func (p *p2cPicker) Pick(ctx context.Context, info balancer.PickInfo) (
conn balancer.SubConn, done func(balancer.DoneInfo), err error) {
p.lock.Lock()
defer p.lock.Unlock()
var chosen *subConn
switch len(p.conns) {
case 0:
return nil, nil, balancer.ErrNoSubConnAvailable // 沒有可用連結
case 1:
chosen = p.choose(p.conns[0], nil) // 只有一個連結
case 2:
chosen = p.choose(p.conns[0], p.conns[1])
default: // 選擇一個健康的節點
var node1, node2 *subConn
for i := 0; i < pickTimes; i++ {
a := p.r.Intn(len(p.conns))
b := p.r.Intn(len(p.conns) - 1)
if b >= a {
b++
}
node1 = p.conns[a]
node2 = p.conns[b]
if node1.healthy() && node2.healthy() {
break
}
}
chosen = p.choose(node1, node2)
}
atomic.AddInt64(&chosen.inflight, 1)
atomic.AddInt64(&chosen.requests, 1)
return chosen.conn, p.buildDoneFunc(chosen), nil
}
客戶端發起呼叫的流程如下,會呼叫 pick 方法獲取一個 transport 進行處理
總結
本文主要分析了 gRPC 的 resolver 模組和 balancer 模組,詳細介紹瞭如何自定義 resolver 和 balancer,以及通過分析 go-zero 中對 resolver 和 balancer 的實現瞭解了自定義 resolver 和 balancer 的過程,同時還分析可客戶端建立的流程和呼叫的流程。寫作不易,如果覺得文章對你有幫助的話,有勞 star?
專案地址
https://github.com/tal-tech/go-zero
框架地址
https://github.com/tal-tech/go-zero/tree/master/zrpc
文件地址
https://www.yuque.com/tal-tech/go-zero/rhakzy
微信交流群
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 基於gRPC的註冊發現與負載均衡的原理和實戰RPC負載
- Spring RSocket:基於服務註冊發現的 RSocket 負載均衡Spring負載
- 服務註冊與發現的原理和實現
- 【SpringCloud】3.OpenFeign——服務註冊與負載均衡SpringGCCloud負載
- go基於grpc構建微服務框架-服務註冊與發現GoRPC微服務框架
- Nacos服務註冊與發現的原理
- golang consul-grpc 服務註冊與發現GolangRPC
- Web Api 基於Zookeeper的服務註冊與發現WebAPI
- Nacos服務註冊與發現原理
- 一文搞懂服務註冊發現的原理與實現
- 服務發現與負載均衡機制-Service負載
- Nacos 服務註冊與發現原理分析
- 微服務 - 叢集化 · 服務註冊 · 健康檢測 · 服務發現 · 負載均衡微服務負載
- NodeJs服務註冊與服務發現實現NodeJS
- java版gRPC實戰之七:基於eureka的註冊發現JavaRPC
- GRPC 負載均衡實現RPC負載
- Kubernetes:服務與負載均衡負載
- Eureka實現服務註冊與發現
- 實現etcd服務註冊與發現
- Docker Swarm :gRPC 基於 DNS 的負載均衡DockerSwarmRPCDNS負載
- 基於Docker + Consul + Registrator的服務註冊與發現叢集搭建Docker
- 基於Docker + Consul + Nginx + Consul-template的服務負載均衡實現DockerNginx負載
- 聊聊微服務的服務註冊與發現!微服務
- SpringCloud服務的註冊與發現(Eureka)SpringGCCloud
- SpringColud Eureka的服務註冊與發現SpringGC
- Spring Cloud實戰系列(一) - 服務註冊與發現EurekaSpringCloud
- nacos服務註冊與發現
- Eureka服務註冊與發現
- consul服務註冊與服務發現的巨坑
- 透過Nginx實現gRPC服務的負載均衡 | gRPC雙向資料流的互動NginxRPC負載
- Spring Cloud Eureka 實現服務註冊與發現SpringCloud
- SpringCloudAlibaba - 整合 Nacos 實現服務註冊與發現SpringGCCloud
- gRPC的負載均衡RPC負載
- 分散式中幾種服務註冊與發現元件的原理與比較分散式元件
- SpringCloud(二):服務呼叫與負載均衡SpringGCCloud負載
- SAP 應用服務負載均衡的實現負載
- 微服務(三) Eureka註冊中心和Ribbon負載均衡微服務負載
- 微服務4:服務註冊與發現微服務