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