gRPC是一個現代的、高性能、開源的和語言無關的通用RPC框架,基於HTTP2協議設計,序列化使用PB(Protocol Buffer),PB是一種語言無關的高性能序列化框架,基於HTTP2+PB保證了的高性能。go-zero是一個開源的微服務框架,支持http和rpc協議,其中rpc底層依賴gRPC,本文會結合gRPC和go-zero源碼從實戰的角度和你們一塊兒分析下服務註冊與發現和負載均衡的實現原理node
原理流程圖以下:git
從圖中能夠看出go-zero實現了gRPC的resolver和balancer接口,而後經過gprc.Register方法註冊到gRPC中,resolver模塊提供了服務註冊的功能,balancer模塊提供了負載均衡的功能。當client發起服務調用的時候會根據resolver註冊進來的服務列表,使用註冊進來的balancer選擇一個服務發起請求,若是沒有進行註冊gRPC會使用默認的resolver和balancer。服務地址的變動會同步到etcd中,go-zero監聽etcd的變化經過resolver更新服務列表github
經過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_nameapp
type Target struct { Scheme string // 表示要使用的名稱系統 Authority string // 表示一些特定於方案的引導信息 Endpoint string // 指出一個具體的名字 }
Build方法返回的Resolver也是一個接口類型。定義以下負載均衡
type Resolver interface { ResolveNow(ResolveNowOptions) Close() }
流程圖下圖框架
所以能夠看出自定義Resolver須要實現以下步驟:微服務
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方法的實現以下ui
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的時候回進行以下操做
建立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的話gRPC會採用默認的負載均衡算法,流程圖以下
在go-zero中自定義的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的過程,同時還分析可客戶端建立的流程和調用的流程。但願本文能給你們帶來一些幫助
https://github.com/tal-tech/go-zero
若是以爲文章不錯,歡迎 github 點個 star 🤝