grpc負載均衡RoundRobin源碼解讀

grpc client端創建連接時可以用WithBalancer來指定負載均衡組件,這裏研究下grpc自帶的RoundRobin(輪詢調度)的實現。源碼在google.golang.org/grpc/balancer.go中。

roundRobin結構體定義如下:

type roundRobin struct {
	r      naming.Resolver
	w      naming.Watcher
	addrs  []*addrInfo // all the addresses the client should potentially connect
	mu     sync.Mutex
	addrCh chan []Address // the channel to notify gRPC internals the list of addresses the client should connect to.
	next   int            // index of the next address to return for Get()
	waitCh chan struct{}  // the channel to block when there is no connected address available
	done   bool           // The Balancer is closed.
}
  • r是命名解析器,可以定義自己的命名解析器,如etcd命名解析器。如果r爲nil,那麼Dial中參數target將直接作爲可請求地址添加到addrs中。
  • w是命名解析器Resolve方法返回的watcher,該watcher可以監聽命名解析器發來的地址信息變化,通知roundRobin對addrs中的地址進行動態的增刪。
  • addrs是從命名解析器獲取地址信息數組,數組中每個地址不僅有地址信息,還有grpc與該地址是否已經創建了ready狀態的連接。
  • addrCh是地址數組的channel,該channel會在每次命名解析器發來地址信息變化後,將所有addrs通知到grpc內部的lbWatcher,lbWatcher是統一管理地址連接狀態的協程,負責新地址的連接與被刪除地址的關閉操作。
  • next是roundRobin的Index,即輪詢調度遍歷到addrs數組中的哪個位置了。
  • waitCh是當addrs中地址爲空時,grpc調用Get()方法希望獲取到一個到target的連接,如果設置了grpc的failfast爲false,那麼Get()方法會阻塞在此channel上,直到有ready的連接。

roundRobin啓動:

func (rr *roundRobin) Start(target string, config BalancerConfig) error {
	rr.mu.Lock()
	defer rr.mu.Unlock()
	if rr.done {
		return ErrClientConnClosing
	}
	if rr.r == nil {
		// 如果沒有解析器,那麼直接將target加入addrs地址數組
		rr.addrs = append(rr.addrs, &addrInfo{addr: Address{Addr: target}})
		return nil
	}
	// Resolve接口會返回一個watcher,watcher可以監聽解析器的地址變化
	w, err := rr.r.Resolve(target)
	if err != nil {
		return err
	}
	rr.w = w
	// 創建一個channel,當watcher監聽到地址變化時,通知grpc內部lbWatcher去連接該地址
	rr.addrCh = make(chan []Address, 1)
	// go 出去監聽watcher,監聽地址變化。
	go func() {
		for {
			if err := rr.watchAddrUpdates(); err != nil {
				return
			}
		}
	}()
	return nil
}

監聽命名解析器的地址變化:

func (rr *roundRobin) watchAddrUpdates() error {
	// watcher的next方法會阻塞,直至有地址變化信息過來,updates即爲變化信息
	updates, err := rr.w.Next()
	if err != nil {
		return err
	}
	// 對於addrs地址數組的操作,顯然是要加鎖的,因爲有多個goroutine在同時操作
	rr.mu.Lock()
	defer rr.mu.Unlock()
	for _, update := range updates {
		addr := Address{
			Addr:     update.Addr,
			Metadata: update.Metadata,
		}
		switch update.Op {
		case naming.Add:
		//對於新增類型的地址,注意這裏不會重複添加。
			var exist bool
			for _, v := range rr.addrs {
				if addr == v.addr {
					exist = true
					break
				}
			}
			if exist {
				continue
			}
			rr.addrs = append(rr.addrs, &addrInfo{addr: addr})
		case naming.Delete:
		//對於刪除的地址,直接在addrs中刪除就行了
			for i, v := range rr.addrs {
				if addr == v.addr {
					copy(rr.addrs[i:], rr.addrs[i+1:])
					rr.addrs = rr.addrs[:len(rr.addrs)-1]
					break
				}
			}
		default:
			grpclog.Errorln("Unknown update.Op ", update.Op)
		}
	}
	// 這裏複製了整個addrs地址數組,然後丟到addrCh channel中通知grpc內部lbWatcher,
	// lbWatcher會關閉刪除的地址,連接新增的地址。
	// 連接ready後會有專門的goroutine調用Up方法修改addrs中地址的狀態。
	open := make([]Address, len(rr.addrs))
	for i, v := range rr.addrs {
		open[i] = v.addr
	}
	if rr.done {
		return ErrClientConnClosing
	}
	select {
	case <-rr.addrCh:
	default:
	}
	rr.addrCh <- open
	return nil
}

Up方法:
up方法是grpc內部負載均衡watcher調用的,該watcher會讀全局的連接狀態改變隊列,如果是ready狀態的連接,會調用up方法來改變addrs地址數組中該地址的狀態爲已連接

func (rr *roundRobin) Up(addr Address) func(error) {
	rr.mu.Lock()
	defer rr.mu.Unlock()
	var cnt int
	//將地址數組中的addr置爲已連接狀態,這樣這個地址就可以被client使用了。
	for _, a := range rr.addrs {
		if a.addr == addr {
			if a.connected {
				return nil
			}
			a.connected = true
		}
		if a.connected {
			cnt++
		}
	}
	// 當有一個可用地址時,之前可能是0個,可能要很多client阻塞在獲取連接地址上,這裏通知所有的client有可用連接啦。
	// 爲什麼只等於1時通知?因爲可用地址數量>1時,client是不會阻塞的。
	if cnt == 1 && rr.waitCh != nil {
		close(rr.waitCh)
		rr.waitCh = nil
	}
	//返回禁用該地址的方法
	return func(err error) {
		rr.down(addr, err)
	}
}

down方法:
down方法就簡單了, 直接找到addr置爲不可用就行了。

//如果addr1已經被連接上了,但是resolver通知刪除了,grpc內部如何處理關閉的邏輯?
func (rr *roundRobin) down(addr Address, err error) {
	rr.mu.Lock()
	defer rr.mu.Unlock()
	for _, a := range rr.addrs {
		if addr == a.addr {
			a.connected = false
			break
		}
	}
}

Get()方法:
client 需要獲取一個可用的地址,如果addrs爲空,或者addrs不爲空,但是地址都不可用(沒連接),Get()方法會返回錯誤。但是如果設置了failfast = false,Get()方法會阻塞在waitCh channel上,直至Up方法給到通知,然後輪詢調度可用的地址。

func (rr *roundRobin) Get(ctx context.Context, opts BalancerGetOptions) (addr Address, put func(), err error) {
	var ch chan struct{}
	rr.mu.Lock()
	if rr.done {
		rr.mu.Unlock()
		err = ErrClientConnClosing
		return
	}

	if len(rr.addrs) > 0 {
		// addrs的長度可能變化,如果next值超出了,就置爲0,從頭開始調度。
		if rr.next >= len(rr.addrs) {
			rr.next = 0
		}
		next := rr.next
		//遍歷整個addrs數組,直到選出一個可用的地址
		for {
			a := rr.addrs[next]
			// next值加一,當然是循環的,到len(addrs)後,變爲0
			next = (next + 1) % len(rr.addrs)
			if a.connected {
				addr = a.addr
				rr.next = next
				rr.mu.Unlock()
				return
			}
			if next == rr.next {
				// 遍歷完一圈了,還沒找到,走下面邏輯
				break
			}
		}
	}
	if !opts.BlockingWait { //如果是非阻塞模式,如果沒有可用地址,那麼報錯
		if len(rr.addrs) == 0 {
			rr.mu.Unlock()
			err = status.Errorf(codes.Unavailable, "there is no address available")
			return
		}
		// Returns the next addr on rr.addrs for failfast RPCs.
		addr = rr.addrs[rr.next].addr
		rr.next++
		rr.mu.Unlock()
		return
	}
	// Wait on rr.waitCh for non-failfast RPCs.
	// 如果是阻塞模式,那麼需要阻塞在waitCh上,直到Up方法給通知
	if rr.waitCh == nil {
		ch = make(chan struct{})
		rr.waitCh = ch
	} else {
		ch = rr.waitCh
	}
	rr.mu.Unlock()
	for {
		select {
		case <-ctx.Done():
			err = ctx.Err()
			return
		case <-ch:
			rr.mu.Lock()
			if rr.done {
				rr.mu.Unlock()
				err = ErrClientConnClosing
				return
			}

			if len(rr.addrs) > 0 {
				if rr.next >= len(rr.addrs) {
					rr.next = 0
				}
				next := rr.next
				for {
					a := rr.addrs[next]
					next = (next + 1) % len(rr.addrs)
					if a.connected {
						addr = a.addr
						rr.next = next
						rr.mu.Unlock()
						return
					}
					if next == rr.next {
						// 遍歷完一圈了,還沒找到,可能剛Up的地址被down掉了,重新等待。
						break
					}
				}
			}
			// The newly added addr got removed by Down() again.
			if rr.waitCh == nil {
				ch = make(chan struct{})
				rr.waitCh = ch
			} else {
				ch = rr.waitCh
			}
			rr.mu.Unlock()
		}
	}
}

lbWatcher:
lbWatcher會監聽地址變化信息,roundroubin每次有地址變化時,會將所有的地址通知給lbWatcher,lbWatcher本身維護了地址連接的map表,會找出新添加的地址和需要刪除的地址,然後做連接、關閉操作,再調用roundRobin的Up/Down方法通知連接的狀態。

func (bw *balancerWrapper) lbWatcher() {
	notifyCh := bw.balancer.Notify()
	if notifyCh == nil {
		// 沒有定義解析器,直接連接這個地址。
		a := resolver.Address{
			Addr: bw.targetAddr,
			Type: resolver.Backend,
		}
		sc, err := bw.cc.NewSubConn([]resolver.Address{a}, balancer.NewSubConnOptions{})
		if err != nil {
			grpclog.Warningf("Error creating connection to %v. Err: %v", a, err)
		} else {
			bw.mu.Lock()
			bw.conns[a] = sc
			bw.connSt[sc] = &scState{
				addr: Address{Addr: bw.targetAddr},
				s:    connectivity.Idle,
			}
			bw.mu.Unlock()
			sc.Connect()
		}
		return
	}

	for addrs := range notifyCh {
			var newAddrs []resolver.Address
			for _, a := range addrs {
				newAddr := resolver.Address{
					Addr:       a.Addr,
					Type:       resolver.Backend, // All addresses from balancer are all backends.
					ServerName: "",
					Metadata:   a.Metadata,
				}
				newAddrs = append(newAddrs, newAddr)
			}
			var (
				add []resolver.Address // Addresses need to setup connections.
				del []balancer.SubConn // Connections need to tear down.
			)
			resAddrs := make(map[resolver.Address]Address)
			for _, a := range addrs {
				resAddrs[resolver.Address{
					Addr:       a.Addr,
					Type:       resolver.Backend, // All addresses from balancer are all backends.
					ServerName: "",
					Metadata:   a.Metadata,
				}] = a
			}
			bw.mu.Lock()
			// 新添加的地址,需要去新建連接
			for a := range resAddrs {
				if _, ok := bw.conns[a]; !ok {
					add = append(add, a)
				}
			}
			// 要被刪除的地址,需要去關閉連接
			for a, c := range bw.conns {
				if _, ok := resAddrs[a]; !ok {
					del = append(del, c)
					delete(bw.conns, a)
					// Keep the state of this sc in bw.connSt until its state becomes Shutdown.
				}
			}
			bw.mu.Unlock()
			for _, a := range add {
				sc, err := bw.cc.NewSubConn([]resolver.Address{a}, balancer.NewSubConnOptions{})
				if err != nil {
					grpclog.Warningf("Error creating connection to %v. Err: %v", a, err)
				} else {
					bw.mu.Lock()
					bw.conns[a] = sc
					bw.connSt[sc] = &scState{
						addr: resAddrs[a],
						s:    connectivity.Idle,
					}
					bw.mu.Unlock()
					sc.Connect()//  這一步真正做了連接的操作。
				}
			}
			for _, c := range del {
				bw.cc.RemoveSubConn(c)
			}
		}
	}
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章