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

          Go:負(fù)載均衡原理分析與源碼解讀

          共 20508字,需瀏覽 42分鐘

           ·

          2022-08-26 17:12

          上一篇文章一起學(xué)習(xí)了Resolver的原理和源碼分析,本篇繼續(xù)和大家一起學(xué)習(xí)下和Resolver關(guān)系密切的Balancer的相關(guān)內(nèi)容。這里說的負(fù)載均衡主要指數(shù)據(jù)中心內(nèi)的負(fù)載均衡,即RPC間的負(fù)載均衡。

          傳送門 服務(wù)發(fā)現(xiàn)原理分析與源碼解讀

          基于 go-zero v1.3.5 和 grpc-go v1.47.0

          負(fù)載均衡

          每一個被調(diào)用服務(wù)都會有多個實例,那么服務(wù)的調(diào)用方應(yīng)該將請求,發(fā)向被調(diào)用服務(wù)的哪一個服務(wù)實例,這就是負(fù)載均衡的業(yè)務(wù)場景。

          負(fù)載均衡的第一個關(guān)鍵點是公平性,即負(fù)載均衡需要關(guān)注被調(diào)用服務(wù)實例組之間的公平性,不要出現(xiàn)旱的旱死,澇的澇死的情況。

          負(fù)載均衡的第二個關(guān)鍵點是正確性,即對于有狀態(tài)的服務(wù)來說,負(fù)載均衡需要關(guān)心請求的狀態(tài),將請求調(diào)度到能處理它的后端實例上,不要出現(xiàn)不能處理和錯誤處理的情況。

          無狀態(tài)的負(fù)載均衡

          無狀態(tài)的負(fù)載均衡是我們?nèi)粘9ぷ髦薪佑|比較多的負(fù)載均衡模型,它指的是參與負(fù)載均衡的后端實例是無狀態(tài)的,所有的后端實例都是對等的,一個請求不論發(fā)向哪一個實例,都會得到相同的并且正確的處理結(jié)果,所以無狀態(tài)的負(fù)載均衡策略不需要關(guān)心請求的狀態(tài)。下面介紹兩種無狀態(tài)負(fù)載均衡算法。

          輪詢

          輪詢的負(fù)載均衡策略非常簡單,只需要將請求按順序分配給多個實例,不用再做其他的處理。例如,輪詢策略會將第一個請求分配給第一個實例,然后將下一個請求分配給第二個實例,這樣依次分配下去,分配完一輪之后,再回到開頭分配給第一個實例,再依次分配。輪詢在路由時,不利用請求的狀態(tài)信息,屬于無狀態(tài)的負(fù)載均衡策略,所以它不能用于有狀態(tài)實例的負(fù)載均衡器,否則正確性會出現(xiàn)問題。在公平性方面,因為輪詢策略只是按順序分配請求,所以適用于請求的工作負(fù)載和實例的處理能力差異都較小的情況。

          權(quán)重輪詢

          權(quán)重輪詢的負(fù)載均衡策略是將每一個后端實例分配一個權(quán)重,分配請求的數(shù)量和實例的權(quán)重成正比輪詢。例如有兩個實例 A,B,假設(shè)我們設(shè)置 A 的權(quán)重為 20,B 的權(quán)重為 80,那么負(fù)載均衡會將 20% 的請求數(shù)量分配給 A,80 % 的請求數(shù)量分配給 B。權(quán)重輪詢在路由時,不利用請求的狀態(tài)信息,屬于無狀態(tài)的負(fù)載均衡策略,所以它也不能用于有狀態(tài)實例的負(fù)載均衡器,否則正確性會出現(xiàn)問題。在公平性方面,因為權(quán)重策略會按實例的權(quán)重比例來分配請求數(shù),所以,我們可以利用它解決實例的處理能力差異的問題,認(rèn)為它的公平性比輪詢策略要好。

          有狀態(tài)負(fù)載均衡

          有狀態(tài)負(fù)載均衡是指,在負(fù)載均衡策略中會保存服務(wù)端的一些狀態(tài),然后根據(jù)這些狀態(tài)按照一定的算法選擇出對應(yīng)的實例。

          P2C+EWMA

          在go-zero中默認(rèn)使用的是P2C的負(fù)載均衡算法。該算法的原理比較簡單,即隨機從所有可用節(jié)點中選擇兩個節(jié)點,然后計算這兩個節(jié)點的負(fù)載情況,選擇負(fù)載較低的一個節(jié)點來服務(wù)本次請求。為了避免某些節(jié)點一直得不到選擇導(dǎo)致不平衡,會在超過一定的時間后強制選擇一次。

          在該復(fù)雜均衡算法中,采用了EWMA指數(shù)移動加權(quán)平均的算法,表示是一段時間內(nèi)的均值。該算法相對于算數(shù)平均來說對于突然的網(wǎng)絡(luò)抖動沒有那么敏感,突然的抖動不會體現(xiàn)在請求的lag中,從而可以讓算法更加均衡。

          go-zero/zrpc/internal/balancer/p2c/p2c.go:133

          atomic.StoreUint64(&c.lag, uint64(float64(olag)*w+float64(lag)*(1-w)))

          go-zero/zrpc/internal/balancer/p2c/p2c.go:139

          atomic.StoreUint64(&c.success, uint64(float64(osucc)*w+float64(success)*(1-w)))

          系數(shù)w是一個時間衰減值,即兩次請求的間隔越大,則系數(shù)w就越小。

          go-zero/zrpc/internal/balancer/p2c/p2c.go:124

          w := math.Exp(float64(-td) / float64(decayTime))

          節(jié)點的load值是通過該連接的請求延遲 lag 和當(dāng)前請求數(shù) inflight 的乘積所得,如果請求的延遲越大或者當(dāng)前正在處理的請求數(shù)越多表明該節(jié)點的負(fù)載越高。

          go-zero/zrpc/internal/balancer/p2c/p2c.go:199

          func (c *subConn) load() int64 {
            // plus one to avoid multiply zero
            lag := int64(math.Sqrt(float64(atomic.LoadUint64(&c.lag) + 1)))
            load := lag * (atomic.LoadInt64(&c.inflight) + 1)
            if load == 0 {
              return penalty
            }

            return load
          }

          源碼分析

          如下源碼會涉及go-zero和gRPC,請根據(jù)給出的代碼路徑進行區(qū)分

          在gRPC中,Balancer和Resolver一樣也可以自定義,同樣也是通過Register方法進行注冊

          grpc-go/balancer/balancer.go:53

          func Register(b Builder) {
            m[strings.ToLower(b.Name())] = b
          }

          Register的參數(shù)Builder為接口,在Builder接口中,Build方法的第一個參數(shù)ClientConn也為接口,Build方法的返回值Balancer同樣也是接口,定義如下:

          可以看出,要想實現(xiàn)自定義的Balancer的話,就必須要實現(xiàn)balancer.Builder接口。

          在了解了gRPC提供的Balancer的注冊方式之后,我們看一下go-zero是在什么地方進行Balancer注冊的

          go-zero/zrpc/internal/balancer/p2c/p2c.go:36

          func init() {
            balancer.Register(newBuilder())
          }

          在go-zero中并沒有實現(xiàn) balancer.Builder 接口,而是使用gRPC提供的 base.baseBuilder 進行注冊,base.baseBuilder 實現(xiàn)了balancer.Builder 接口。創(chuàng)建baseBuilder的時候調(diào)用了 base.NewBalancerBuilder 方法,需要傳入 PickerBuilder 參數(shù),PickerBuilder為接口,在go-zero中 p2c.p2cPickerBuilder 實現(xiàn)了該接口。

          PickerBuilder接口Build方法返回值 balancer.Picker 也是一個接口,p2c.p2cPicker 實現(xiàn)了該接口。

          grpc-go/balancer/base/base.go:65

          func NewBalancerBuilder(name string, pb PickerBuilder, config Config) balancer.Builder {
            return &baseBuilder{
              name:          name,
              pickerBuilder: pb,
              config:        config,
            }
          }

          各結(jié)構(gòu)之間的關(guān)系如下圖所示,其中各結(jié)構(gòu)模塊對應(yīng)的包為:

          • balancer:grpc-go/balancer
          • base:grpc-go/balancer/base
          • p2c: go-zero/zrpc/internal/balancer/p2c

          在哪里獲取已注冊的Balancer?

          通過上面的流程步驟,已經(jīng)知道了如何自定義Balancer,以及如何注冊自定義的Blancer。既然注冊了肯定就會獲取,接下來看一下是在哪里獲取已經(jīng)注冊的Balancer的。

          我們知道Resolver是通過解析DialContext的第二個參數(shù)target,從而得到Resolver的name,然后根據(jù)name獲取到對應(yīng)的Resolver的。獲取Balancer同樣也是根據(jù)名稱,Balancer的名稱是在創(chuàng)建gRPC Client的時候通過配置項傳入的,這里的p2c.Name為注冊Balancer時指定的名稱 p2c_ewma ,如下:

          go-zero/zrpc/internal/client.go:50

          func NewClient(target string, opts ...ClientOption) (Client, error) {
            var cli client

            svcCfg := fmt.Sprintf(`{"loadBalancingPolicy":"%s"}`, p2c.Name)
            balancerOpt := WithDialOption(grpc.WithDefaultServiceConfig(svcCfg))
            opts = append([]ClientOption{balancerOpt}, opts...)
            if err := cli.dial(target, opts...); err != nil {
              return nil, err
            }

            return &cli, nil
          }

          在上一篇文章中,我們已經(jīng)知道當(dāng)創(chuàng)建gRPC客戶端的時候,會觸發(fā)調(diào)用自定義Resolver的Build方法,在Build方法內(nèi)部獲取到服務(wù)地址列表后,通過cc.UpdateState方法進行狀態(tài)更新,后面當(dāng)監(jiān)聽到服務(wù)狀態(tài)變化的時候同樣也會調(diào)用cc.UpdateState進行狀態(tài)的更新,而這里的cc指的就是 ccResolverWrapper 對象,這一部分如果忘記的話,可以再去回顧一下講解Resolver的那篇文章,以便能絲滑接入本篇:

          go-zero/zrpc/resolver/internal/kubebuilder.go:51

          if err := cc.UpdateState(resolver.State{
            Addresses: addrs,
          }); err != nil {
            logx.Error(err)
          }

          這里有幾個重要的模塊對象,如下:

          • ClientConn:grpc-go/clientconn.go:464
          • ccResolverWrapper:grpc-go/resolver_conn_wrapper.go:36
          • ccBalancerWrapper:grpc-go/balancer_conn_wrappers.go:48
          • Balancer:grpc-go/internal/balancer/gracefulswitch/gracefulswitch.go:46
          • balancerWrapper:grpc-go/internal/balancer/gracefulswitch/gracefulswitch.go:247

          當(dāng)監(jiān)聽到服務(wù)狀態(tài)的變更后(首次啟動或者通過Watch監(jiān)聽變化)調(diào)用 ccResolverWrapper.UpdateState 觸發(fā)更新狀態(tài)的流程,各模塊間的調(diào)用鏈路如下所示:

          獲取Balancer的動作是在 ccBalancerWrapper.handleSwitchTo 方法中觸發(fā)的,代碼如下所示:

          grpc-go/balancer_conn_wrappers.go:266

          builder := balancer.Get(name)
          if builder == nil {
            channelz.Warningf(logger, ccb.cc.channelzID, "Channel switches to new LB policy %q, since the specified LB policy %q was not registered", PickFirstBalancerName, name)
            builder = newPickfirstBuilder()
          else {
            channelz.Infof(logger, ccb.cc.channelzID, "Channel switches to new LB policy %q", name)
          }

          if err := ccb.balancer.SwitchTo(builder); err != nil {
            channelz.Errorf(logger, ccb.cc.channelzID, "Channel failed to build new LB policy %q: %v", name, err)
            return
          }
          ccb.curBalancerName = builder.Name()

          然后在 Balancer.SwitchTo 方法中,調(diào)用了自定義Balancer的Build方法:

          grpc-go/internal/balancer/gracefulswitch/gracefulswitch.go:121

          newBalancer := builder.Build(bw, gsb.bOpts)

          上文有提到Build方法的第一個參數(shù)為接口 balancer.ClientConn ,而這里傳入的為 balancerWrapper ,所以gracefulswitch.balancerWrapper實現(xiàn)了該接口:

          到這里我們已經(jīng)知道了獲取自定義Balancer是在哪里觸達的,以及在哪里獲取的自定義的Balancer,和balancer.Builder的Build方法在哪里被調(diào)用。

          通過上文可知這里的balancer.Builder為baseBuilder,所以調(diào)用的Build方法為baseBuilder的Build方法,Build方法的定義如下:

          grpc-go/balancer/base/balancer.go:39

          func (bb *baseBuilder) Build(cc balancer.ClientConn, opt balancer.BuildOptions) balancer.Balancer {
            bal := &baseBalancer{
              cc:            cc,
              pickerBuilder: bb.pickerBuilder,

              subConns: resolver.NewAddressMap(),
              scStates: make(map[balancer.SubConn]connectivity.State),
              csEvltr:  &balancer.ConnectivityStateEvaluator{},
              config:   bb.config,
            }
            bal.picker = NewErrPicker(balancer.ErrNoSubConnAvailable)
            return bal
          }

          Build方法返回了baseBalancer,可以知道baseBalancer實現(xiàn)了balancer.Balancer接口:

          再來回顧下這個流程,其實主要做了如下幾件事:

          1. 在自定義的Resolver中監(jiān)聽服務(wù)狀態(tài)的變更
          2. 通過UpdateState來更新狀態(tài)
          3. 獲取自定義的Balancer
          4. 執(zhí)行自定義Balancer的Build方法獲取Balancer

          如何創(chuàng)建連接?

          繼續(xù)回到ClientConn的updateResolverState方法,在方法的最后調(diào)用balancerWrapper.updateClientConnState方法更新客戶端的連接狀態(tài):

          grpc-go/clientconn.go:664

          uccsErr := bw.updateClientConnState(&balancer.ClientConnState{ResolverState: s, BalancerConfig: balCfg})
          if ret == nil {
          ret = uccsErr // prefer ErrBadResolver state since any other error is
          // currently meaningless to the caller.
          }

          后面的調(diào)用鏈路如下圖所示:

          最終會調(diào)用baseBalancer.UpdateClientConnState方法:

          grpc-go/balancer/base/balancer.go:94

          func (b *baseBalancer) UpdateClientConnState(s balancer.ClientConnState) error {
            // .............
            b.resolverErr = nil
            addrsSet := resolver.NewAddressMap()
            for _, a := range s.ResolverState.Addresses {
              addrsSet.Set(a, nil)
              if _, ok := b.subConns.Get(a); !ok {
                sc, err := b.cc.NewSubConn([]resolver.Address{a}, balancer.NewSubConnOptions{HealthCheckEnabled: b.config.HealthCheck})
                if err != nil {
                  logger.Warningf("base.baseBalancer: failed to create new SubConn: %v", err)
                  continue
                }
                b.subConns.Set(a, sc)
                b.scStates[sc] = connectivity.Idle
                b.csEvltr.RecordTransition(connectivity.Shutdown, connectivity.Idle)
                sc.Connect()
              }
            }
            for _, a := range b.subConns.Keys() {
              sci, _ := b.subConns.Get(a)
              sc := sci.(balancer.SubConn)
              if _, ok := addrsSet.Get(a); !ok {
                b.cc.RemoveSubConn(sc)
                b.subConns.Delete(a)
              }
            }

            // ................
          }

          當(dāng)?shù)谝淮斡|發(fā)調(diào)用UpdateClientConnState的時候,如下代碼中 ok 為 false:

          _, ok := b.subConns.Get(a);

          所以會創(chuàng)建新的連接:

          sc, err := b.cc.NewSubConn([]resolver.Address{a}, balancer.NewSubConnOptions{HealthCheckEnabled: b.config.HealthCheck})

          這里的 b.cc 即為 balancerWrapper,忘記的盆友可以往上翻看復(fù)習(xí)一下,也就是會調(diào)用 balancerWrapper.NewSubConn創(chuàng)建連接

          grpc-go/internal/balancer/gracefulswitch/gracefulswitch.go:328

          func (bw *balancerWrapper) NewSubConn(addrs []resolver.Address, opts balancer.NewSubConnOptions) (balancer.SubConn, error) {
            // .............

            sc, err := bw.gsb.cc.NewSubConn(addrs, opts)
            if err != nil {
              return nil, err
            }
            
            // .............
            
            bw.subconns[sc] = true
            
            // .............
          }

          bw.gsb.cc即為ccBalancerWrapper,所以這里會調(diào)用ccBalancerWrapper.NewSubConn創(chuàng)建連接:

          grpc-go/balancer_conn_wrappers.go:299

          func (ccb *ccBalancerWrapper) NewSubConn(addrs []resolver.Address, opts balancer.NewSubConnOptions) (balancer.SubConn, error) {
            if len(addrs) <= 0 {
              return nil, fmt.Errorf("grpc: cannot create SubConn with empty address list")
            }
            ac, err := ccb.cc.newAddrConn(addrs, opts)
            if err != nil {
              channelz.Warningf(logger, ccb.cc.channelzID, "acBalancerWrapper: NewSubConn: failed to newAddrConn: %v", err)
              return nil, err
            }
            acbw := &acBalancerWrapper{ac: ac}
            acbw.ac.mu.Lock()
            ac.acbw = acbw
            acbw.ac.mu.Unlock()
            return acbw, nil
          }

          最終返回的是acBalancerWrapper對象,acBalancerWrapper實現(xiàn)了balancer.SubConn接口:

          調(diào)用流程圖如下所示:

          創(chuàng)建連接的默認(rèn)狀態(tài)為 connectivity.Idle :

          grpc-go/clientconn.go:699

          func (cc *ClientConn) newAddrConn(addrs []resolver.Address, opts balancer.NewSubConnOptions) (*addrConn, error) {
            ac := &addrConn{
              state:        connectivity.Idle,
              cc:           cc,
              addrs:        addrs,
              scopts:       opts,
              dopts:        cc.dopts,
              czData:       new(channelzData),
              resetBackoff: make(chan struct{}),
            }
           
            // ...........
          }

          在gRPC中為連接定義了五種狀態(tài),分別如下:

          const (
            // Idle indicates the ClientConn is idle.
            Idle State = iota
            // Connecting indicates the ClientConn is connecting.
            Connecting
            // Ready indicates the ClientConn is ready for work.
            Ready
            // TransientFailure indicates the ClientConn has seen a failure but expects to recover.
            TransientFailure
            // Shutdown indicates the ClientConn has started shutting down.
            Shutdown
          )

          在 **baseBalancer ** 中通過b.scStates保存創(chuàng)建的連接,初始狀態(tài)也為connectivity.Idle,之后通過sc.Connect()進行連接:

          grpc-go/balancer/base/balancer.go:112

          b.subConns.Set(a, sc)
          b.scStates[sc] = connectivity.Idle
          b.csEvltr.RecordTransition(connectivity.Shutdown, connectivity.Idle)
          sc.Connect()

          這里sc.Connetc調(diào)用的是acBalancerWrapper的Connect方法,可以看到這里創(chuàng)建連接是異步進行的:

          grpc-go/balancer_conn_wrappers.go:406

          func (acbw *acBalancerWrapper) Connect() {
            acbw.mu.Lock()
            defer acbw.mu.Unlock()
            go acbw.ac.connect()
          }

          最后會調(diào)用addrConn.connect方法:

          grpc-go/clientconn.go:786

          func (ac *addrConn) connect() error {
            ac.mu.Lock()
            if ac.state == connectivity.Shutdown {
              ac.mu.Unlock()
              return errConnClosing
            }
            if ac.state != connectivity.Idle {
              ac.mu.Unlock()
              return nil
            }
            ac.updateConnectivityState(connectivity.Connecting, nil)
            ac.mu.Unlock()

            ac.resetTransport()
            return nil
          }

          從connect開始的調(diào)用鏈路如下所示:

          在baseBalancer的UpdateSubConnState方法的最后,更新了Picker為自定義的Picker:

          grpc-go/balancer/base/balancer.go:221

          b.cc.UpdateState(balancer.State{ConnectivityState: b.state, Picker: b.picker})

          在addrConn方法的最后會調(diào)用ac.resetTransport()真正的進行連接的創(chuàng)建:

          當(dāng)連接已經(jīng)創(chuàng)建好,處于Ready狀態(tài),最后調(diào)用baseBalancer.UpdateSubConnState方法,此時s==connectivity.Ready為true,而oldS == connectivity.Ready為false,所以會調(diào)用b.regeneratePicker()方法:

          if (s == connectivity.Ready) != (oldS == connectivity.Ready) ||
              b.state == connectivity.TransientFailure {
              b.regeneratePicker()
          }
          func (b *baseBalancer) regeneratePicker() {
            if b.state == connectivity.TransientFailure {
              b.picker = NewErrPicker(b.mergeErrors())
              return
            }
            readySCs := make(map[balancer.SubConn]SubConnInfo)

            // Filter out all ready SCs from full subConn map.
            for _, addr := range b.subConns.Keys() {
              sci, _ := b.subConns.Get(addr)
              sc := sci.(balancer.SubConn)
              if st, ok := b.scStates[sc]; ok && st == connectivity.Ready {
                readySCs[sc] = SubConnInfo{Address: addr}
              }
            }
            b.picker = b.pickerBuilder.Build(PickerBuildInfo{ReadySCs: readySCs})
          }

          在regeneratePicker中獲取了處于connectivity.Ready狀態(tài)可用的連接,同時更新了picker。還記得b.pickerBuilder嗎?b.b.pickerBuilder為在go-zero中自定義實現(xiàn)的base.PickerBuilder接口。

          go-zero/zrpc/internal/balancer/p2c/p2c.go:42

          func (b *p2cPickerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
            readySCs := info.ReadySCs
            if len(readySCs) == 0 {
              return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
            }

            var conns []*subConn
            for conn, connInfo := range readySCs {
              conns = append(conns, &subConn{
                addr:    connInfo.Address,
                conn:    conn,
                success: initSuccess,
              })
            }

            return &p2cPicker{
              conns: conns,
              r:     rand.New(rand.NewSource(time.Now().UnixNano())),
              stamp: syncx.NewAtomicDuration(),
            }
          }

          最后把自定義的Picker賦值為 ClientConn.blockingpicker.picker屬性。

          grpc-go/balancer_conn_wrappers.go:347

          func (ccb *ccBalancerWrapper) UpdateState(s balancer.State) {
            ccb.cc.blockingpicker.updatePicker(s.Picker)
            ccb.cc.csMgr.updateState(s.ConnectivityState)
          }

          如何選擇已創(chuàng)建的連接?

          現(xiàn)在已經(jīng)知道了如何創(chuàng)建連接,以及連接其實是在 baseBalancer.scStates 中管理,當(dāng)連接的狀態(tài)發(fā)生變化,則會更新 **baseBalancer.scStates ** 。那么接下來我們來看一下gRPC是如何選擇一個連接進行請求的發(fā)送的。

          當(dāng)gRPC客戶端發(fā)起調(diào)用的時候,會調(diào)用ClientConn的Invoke方法,一般不會主動使用該方法進行調(diào)用,該方法的調(diào)用一般是自動生成:

          grpc-go/examples/helloworld/helloworld/helloworld_grpc.pb.go:39

          func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
            out := new(HelloReply)
            err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)
            if err != nil {
              return nil, err
            }
            return out, nil
          }

          如下為發(fā)起請求的調(diào)用鏈路,最終會調(diào)用p2cPicker.Pick方法獲取連接,我們自定義的負(fù)載均衡算法一般都在Pick方法中實現(xiàn),獲取到連接之后,通過sendMsg發(fā)送請求。

          grpc-go/stream.go:945

          func (a *csAttempt) sendMsg(m interface{}, hdr, payld, data []byte) error {
            cs := a.cs
            if a.trInfo != nil {
              a.mu.Lock()
              if a.trInfo.tr != nil {
                a.trInfo.tr.LazyLog(&payload{sent: true, msg: m}, true)
              }
              a.mu.Unlock()
            }
            if err := a.t.Write(a.s, hdr, payld, &transport.Options{Last: !cs.desc.ClientStreams}); err != nil {
              if !cs.desc.ClientStreams {
                return nil
              }
              return io.EOF
            }
            if a.statsHandler != nil {
              a.statsHandler.HandleRPC(a.ctx, outPayload(true, m, data, payld, time.Now()))
            }
            if channelz.IsOn() {
              a.t.IncrMsgSent()
            }
            return nil
          }

          源碼分析到此就結(jié)束了,由于篇幅有限沒法做到面面俱到,所以本文只列出了源碼中的主要路徑。

          結(jié)束語

          Balancer相關(guān)的源碼還是有點復(fù)雜的,筆者也是讀了好幾遍才理清脈絡(luò),所以如果讀了一兩遍感覺沒有頭緒也不用著急,對照文章的脈絡(luò)多讀幾遍就一定能搞懂。

          如果有疑問可以隨時找我討論,在社區(qū)群中可以搜索dawn_zhou找到我。

          希望本篇文章對你有所幫助,你的點贊是作者持續(xù)輸出的最大動力。



          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 45
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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无码久久精品蜜桃动态图 | 成人免费黄色视频网站 |