<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于 gRPC 的服務注冊與發(fā)現和負載均衡的原理與實戰(zhàn)

          共 5579字,需瀏覽 12分鐘

           ·

          2020-12-07 01:11

          gRPC是一個現代的、高性能、開源的和語言無關的通用 RPC 框架,基于 HTTP2 協議設計,序列化使用 PB(Protocol Buffer),PB 是一種語言無關的高性能序列化框架,基于 HTTP2+PB 保證了的高性能。go-zero是一個開源的微服務框架,支持 http 和 rpc 協議,其中 rpc 底層依賴 gRPC,本文會結合 gRPC 和 go-zero 源碼從實戰(zhàn)的角度和大家一起分析下服務注冊與發(fā)現和負載均衡的實現原理

          基本原理

          原理流程圖如下:



          從圖中可以看出 go-zero 實現了 gRPC 的 resolver 和 balancer 接口,然后通過 gprc.Register 方法注冊到 gRPC 中,resolver 模塊提供了服務注冊的功能,balancer 模塊提供了負載均衡的功能。當 client 發(fā)起服務調用的時候會根據 resolver 注冊進來的服務列表,使用注冊進來的 balancer 選擇一個服務發(fā)起請求,如果沒有進行注冊 gRPC 會使用默認的 resolver 和 balancer。服務地址的變更會同步到 etcd 中,go-zero 監(jiān)聽 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定義如下,創(chuàng)建 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

          • 實現服務發(fā)現邏輯 (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,
          })
          }

          // 添加監(jiān)聽,當服務地址發(fā)生變化會觸發(fā)更新
          sub.AddListener(update)
          // 更新服務列表
          update()

          return &nopResolver{cc: cc}, nil
          }

          那么注冊進來的 resolver 在哪里用到的呢?當創(chuàng)建客戶端的時候調用 DialContext 方法創(chuàng)建 ClientConn 的時候回進行如下操作

          • 攔截器處理

          • 各種配置項處理

          • 解析 target

          • 獲取 resolver

          • 創(chuàng)建 ccResolverWrapper

          創(chuàng)建 clientConn 的時候回根據 target 解析出 scheme,然后根據 scheme 去找已注冊對應的 resolver,如果沒有找到則使用默認的 resolver



          ccResolverWrapper 的流程如下圖,在這里 resolver 會和 balancer 會進行關聯,balancer 的處理方式和 resolver 類似也是通過 wrapper 進行了一次封裝



          緊著著會根據獲取到的地址創(chuàng)建 htt2 的鏈接



          到此 ClientConn 創(chuàng)建過程基本結束,我們再一起梳理一下整個過程,首先獲取 resolver,其中 ccResolverWrapper 實現了 resovler.ClientConn 接口,通過 Resolver 的 UpdateState 方法觸發(fā)獲取 Balancer,獲取 Balancer,其中 ccBalancerWrapper 實現了 balancer.ClientConn 接口,通過 Balnacer 的 UpdateClientConnState 方法觸發(fā)創(chuàng)建連接 (SubConn),最后創(chuàng)建 HTTP2 Client

          Balancer 模塊

          balancer 模塊用來在客戶端發(fā)起請求時進行負載均衡,如果沒有注冊自定義的 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 負載均衡算法,該算法的優(yōu)勢是能彈性的處理各個節(jié)點的請求,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: // 選擇一個健康的節(jié)點
          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
          }

          客戶端發(fā)起調用的流程如下,會調用 pick 方法獲取一個 transport 進行處理



          總結

          本文主要分析了 gRPC 的 resolver 模塊和 balancer 模塊,詳細介紹了如何自定義 resolver 和 balancer,以及通過分析 go-zero 中對 resolver 和 balancer 的實現了解了自定義 resolver 和 balancer 的過程,同時還分析可客戶端創(chuàng)建的流程和調用的流程。寫作不易,如果覺得文章對你有幫助的話,有勞 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

          微信交流群




          瀏覽 58
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  午夜福利av电影 午夜福利电影AV 午夜精品福利在线 | 操操操电影网 | 日本A片在线免费观看 | 亚洲三级在线观看 | 淫色人妻一区二区三区 |