etcd中watch原始碼解讀

Rick.lz發表於2021-07-21

etcd中watch的原始碼解析

前言

etcd是一個cs網路架構,原始碼分析應該涉及到client端,server端。client主要是提供操作來請求對key監聽,並且接收key變更時的通知。server要能做到接收key監聽請求,並且啟動定時器等方法來對key進行監聽,有變更時通知client。

這裡主要分析了v3版本的實現

client端的程式碼

etcd

Watch

client端的實現相對簡單,我們主要來看下這個Watch的實現

// client/v3/watch.go

type Watcher interface {
	// 在鍵或字首上監聽。將監聽的事件
	// 通過定義的返回的channel進行返回。如果修訂等待通過
	// 監聽被壓縮,然後監聽將被伺服器取消,
	// 客戶端將釋出壓縮的錯誤觀察響應,並且通道將關閉。
	// 如果請求的修訂為 0 或未指定,則返回的通道將
	// 返回伺服器收到監視請求後發生的監視事件。
	// 如果上下文“ctx”被取消或超時,返回的“WatchChan”關閉,
	// 並且來自此關閉通道的“WatchResponse”具有零事件且為零“Err()”。
	// 一旦不再使用觀察者,上下文“ctx”必須被取消,
	// 釋放相關資源。
	//
	// 如果上下文是“context.Background/TODO”,則返回“WatchChan”
	// 不會被關閉和阻塞直到事件被觸發,除非伺服器
	// 返回一個不可恢復的錯誤(例如 ErrCompacted)。
	// 例如,當上下文通過“WithRequireLeader”和
	// 連線的伺服器沒有領導者(例如,由於網路分割槽),
	// 將返回錯誤“etcdserver: no leader”(ErrNoLeader),
	// 然後 "WatchChan" 以非零 "Err()" 關閉。
	// 為了防止觀察流卡在分割槽節點中,
	// 確保使用“WithRequireLeader”包裝上下文。
	//
	// 否則,只要上下文沒有被取消或超時,
	// watch 將永遠重試其他可恢復的錯誤,直到重新連線。
	//
	// TODO:在最後一個“WatchResponse”訊息中顯式設定上下文錯誤並關閉通道?
	// 目前,客戶端上下文被永遠不會關閉的“valCtx”覆蓋。
	// TODO(v3.4): 配置watch重試策略,限制最大重試次數
	//(參見 https://github.com/etcd-io/etcd/issues/8980)
	Watch(ctx context.Context, key string, opts ...OpOption) WatchChan

	// RequestProgress requests a progress notify response be sent in all watch channels.
	RequestProgress(ctx context.Context) error

	// Close closes the watcher and cancels all watch requests.
	Close() error
}

// watcher implements the Watcher interface
type watcher struct {
	remote   pb.WatchClient
	callOpts []grpc.CallOption

	// mu protects the grpc streams map
	mu sync.Mutex

	// streams 儲存所有由 ctx 值鍵控的活動 grpc 流。
	streams map[string]*watchGrpcStream
	lg      *zap.Logger
}

// watchGrpcStream 跟蹤附加到單個 grpc 流的所有watch資源。
type watchGrpcStream struct {
	owner    *watcher
	remote   pb.WatchClient
	callOpts []grpc.CallOption

	// ctx 控制內部的remote.Watch requests
	ctx context.Context
	// ctxKey 用來找流的上下文資訊
	ctxKey string
	cancel context.CancelFunc

	// substreams 持有此 grpc 流上的所有活動的watchers
	substreams map[int64]*watcherStream
	// 恢復儲存此 grpc 流上的所有正在恢復的觀察者
	resuming []*watcherStream

	// reqc 從 Watch() 向主協程傳送觀察請求
	reqc chan watchStreamRequest
	// respc 從 watch 客戶端接收資料
	respc chan *pb.WatchResponse
	// donec 通知廣播進行退出
	donec chan struct{}
	// errc transmits errors from grpc Recv to the watch stream reconnect logic
	errc chan error
	// Closec 獲取關閉觀察者的觀察者流
	closingc chan *watcherStream
	// 當所有子流 goroutine 都退出時,wg 完成
	wg sync.WaitGroup

	// resumec 關閉以表示所有子流都應開始恢復
	resumec chan struct{}
	// closeErr 是關閉監視流的錯誤
	closeErr error

	lg *zap.Logger
}

// watcherStream 代表註冊的觀察者
// watch()時,構造watchgrpcstream時構造的watcherStream,用於封裝一個watch rpc請求,包含訂閱監聽key,通知key變更通道,一些重要標誌。
type watcherStream struct {
	// initReq 是發起這個請求的請求
	initReq watchRequest

	// outc 向訂閱者釋出watch響應
	outc chan WatchResponse
	// recvc buffers watch responses before publishing
	recvc chan *WatchResponse
	// 當 watcherStream goroutine 停止時 donec 關閉
	donec chan struct{}
	// 當應該安排流關閉時,closures 設定為 true。
	closing bool
	// id 是在 grpc 流上註冊的 watch id
	id int64

	// buf 儲存從 etcd 收到但尚未被客戶端消費的所有事件
	buf []*WatchResponse
}

// Watch post一個watch請求,通過run()來監聽watch新建立的watch通道,等待watch事件
func (w *watcher) Watch(ctx context.Context, key string, opts ...OpOption) WatchChan {
	ow := opWatch(key, opts...)

	var filters []pb.WatchCreateRequest_FilterType
	if ow.filterPut {
		filters = append(filters, pb.WatchCreateRequest_NOPUT)
	}
	if ow.filterDelete {
		filters = append(filters, pb.WatchCreateRequest_NODELETE)
	}

	wr := &watchRequest{
		ctx:            ctx,
		createdNotify:  ow.createdNotify,
		key:            string(ow.key),
		end:            string(ow.end),
		rev:            ow.rev,
		progressNotify: ow.progressNotify,
		fragment:       ow.fragment,
		filters:        filters,
		prevKV:         ow.prevKV,
		retc:           make(chan chan WatchResponse, 1),
	}

	ok := false
	ctxKey := streamKeyFromCtx(ctx)

	var closeCh chan WatchResponse
	for {
		// 查詢或分配適當的 grpc 監視流
		w.mu.Lock()
		if w.streams == nil {
			// closed
			w.mu.Unlock()
			ch := make(chan WatchResponse)
			close(ch)
			return ch
		}

		// streams是一個map,儲存所有由 ctx 值鍵控的活動 grpc 流
		// 如果該請求對應的流為空,則新建
		wgs := w.streams[ctxKey]
		if wgs == nil {
			// newWatcherGrpcStream new一個watch grpc stream來傳輸watch請求
			// 建立goroutine來處理監聽key的watch各種事件
			wgs = w.newWatcherGrpcStream(ctx)
			w.streams[ctxKey] = wgs
		}
		donec := wgs.donec
		reqc := wgs.reqc
		w.mu.Unlock()

		// couldn't create channel; return closed channel
		if closeCh == nil {
			closeCh = make(chan WatchResponse, 1)
		}

		// 等待接收值
		select {
		// reqc 從 Watch() 向主協程傳送觀察請求
		case reqc <- wr:
			ok = true
		case <-wr.ctx.Done():
			ok = false
		case <-donec:
			ok = false
			if wgs.closeErr != nil {
				closeCh <- WatchResponse{Canceled: true, closeErr: wgs.closeErr}
				break
			}
			// 重試,可能已經從沒有 ctxs 中刪除了流
			continue
		}

		// receive channel
		if ok {
			select {
			case ret := <-wr.retc:
				return ret
			case <-ctx.Done():
			case <-donec:
				if wgs.closeErr != nil {
					closeCh <- WatchResponse{Canceled: true, closeErr: wgs.closeErr}
					break
				}
				// 重試,可能已經從沒有 ctxs 中刪除了流
				continue
			}
		}
		break
	}

	close(closeCh)
	return closeCh
}

總結:

1、判斷key是否滿足watch的條件;

2、過濾監聽事件;

3、構造watch請求;

4、查詢或分配新的grpc watch stream;

5、傳送watch請求到reqc通道;

6、返回WatchResponse 接收chan給客戶端;

newWatcherGrpcStream

new一個watch grpc stream來傳輸watch請求

// newWatcherGrpcStream new一個watch grpc stream來傳輸watch請求
func (w *watcher) newWatcherGrpcStream(inctx context.Context) *watchGrpcStream {
	ctx, cancel := context.WithCancel(&valCtx{inctx})

	//構造watchGrpcStream
	wgs := &watchGrpcStream{
		owner:      w,
		remote:     w.remote,
		callOpts:   w.callOpts,
		ctx:        ctx,
		ctxKey:     streamKeyFromCtx(inctx),
		cancel:     cancel,
		substreams: make(map[int64]*watcherStream),
		respc:      make(chan *pb.WatchResponse),
		reqc:       make(chan watchStreamRequest),
		donec:      make(chan struct{}),
		errc:       make(chan error, 1),
		closingc:   make(chan *watcherStream),
		resumec:    make(chan struct{}),
	}

	// 建立goroutine來處理監聽key的watch各種事件
	go wgs.run()
	return wgs
}

總結:

1、構造watchGrpcStream;

2、建立goroutine也就是run來處理監聽key的watch各種事件;

run

處理監聽key的watch各種事件

// 通過etcd grpc伺服器啟動一個watch stream
// run 管理watch 的事件chan
func (w *watchGrpcStream) run() {
	var wc pb.Watch_WatchClient
	var closeErr error

	closing := make(map[*watcherStream]struct{})

	defer func() {
		w.closeErr = closeErr
		// shutdown substreams and resuming substreams
		for _, ws := range w.substreams {
			if _, ok := closing[ws]; !ok {
				close(ws.recvc)
				closing[ws] = struct{}{}
			}
		}
		for _, ws := range w.resuming {
			if _, ok := closing[ws]; ws != nil && !ok {
				close(ws.recvc)
				closing[ws] = struct{}{}
			}
		}
		w.joinSubstreams()
		for range closing {
			w.closeSubstream(<-w.closingc)
		}
		w.wg.Wait()
		w.owner.closeStream(w)
	}()

	// 使用 etcd grpc 伺服器啟動一個流
	if wc, closeErr = w.newWatchClient(); closeErr != nil {
		return
	}

	cancelSet := make(map[int64]struct{})

	var cur *pb.WatchResponse
	for {
		select {
		// Watch() 請求
		case req := <-w.reqc:
			switch wreq := req.(type) {
			case *watchRequest:
				outc := make(chan WatchResponse, 1)
				// TODO: pass custom watch ID?
				ws := &watcherStream{
					initReq: *wreq,
					id:      -1,
					outc:    outc,
					// unbuffered so resumes won't cause repeat events
					recvc: make(chan *WatchResponse),
				}

				ws.donec = make(chan struct{})
				w.wg.Add(1)
				go w.serveSubstream(ws, w.resumec)

				// queue up for watcher creation/resume
				w.resuming = append(w.resuming, ws)
				if len(w.resuming) == 1 {
					// head of resume queue, can register a new watcher
					if err := wc.Send(ws.initReq.toPB()); err != nil {
						w.lg.Debug("error when sending request", zap.Error(err))
					}
				}
			case *progressRequest:
				if err := wc.Send(wreq.toPB()); err != nil {
					w.lg.Debug("error when sending request", zap.Error(err))
				}
			}

			// 來自watch client的新事件
		case pbresp := <-w.respc:
			if cur == nil || pbresp.Created || pbresp.Canceled {
				cur = pbresp
			} else if cur != nil && cur.WatchId == pbresp.WatchId {
				// merge new events
				// 合併新事件
				cur.Events = append(cur.Events, pbresp.Events...)
				// update "Fragment" field; last response with "Fragment" == false
				cur.Fragment = pbresp.Fragment
			}

			switch {
			// 表示是建立的請求
			case pbresp.Created:
				// response to head of queue creation
				if len(w.resuming) != 0 {
					if ws := w.resuming[0]; ws != nil {
						w.addSubstream(pbresp, ws)
						w.dispatchEvent(pbresp)
						w.resuming[0] = nil
					}
				}

				if ws := w.nextResume(); ws != nil {
					if err := wc.Send(ws.initReq.toPB()); err != nil {
						w.lg.Debug("error when sending request", zap.Error(err))
					}
				}

				// 為下一次迭代重置
				cur = nil
				// 表示取消的請求
			case pbresp.Canceled && pbresp.CompactRevision == 0:
				delete(cancelSet, pbresp.WatchId)
				if ws, ok := w.substreams[pbresp.WatchId]; ok {
					// signal to stream goroutine to update closingc
					close(ws.recvc)
					closing[ws] = struct{}{}
				}

				// reset for next iteration
				cur = nil

				//因為是流的方式傳輸,所以支援分片傳輸,遇到分片事件直接跳過
			case cur.Fragment:
				continue

			default:
				// dispatch to appropriate watch stream
				ok := w.dispatchEvent(cur)

				// reset for next iteration
				cur = nil

				if ok {
					break
				}

				// watch response on unexpected watch id; cancel id
				if _, ok := cancelSet[pbresp.WatchId]; ok {
					break
				}

				cancelSet[pbresp.WatchId] = struct{}{}
				cr := &pb.WatchRequest_CancelRequest{
					CancelRequest: &pb.WatchCancelRequest{
						WatchId: pbresp.WatchId,
					},
				}
				req := &pb.WatchRequest{RequestUnion: cr}
				w.lg.Debug("sending watch cancel request for failed dispatch", zap.Int64("watch-id", pbresp.WatchId))
				if err := wc.Send(req); err != nil {
					w.lg.Debug("failed to send watch cancel request", zap.Int64("watch-id", pbresp.WatchId), zap.Error(err))
				}
			}

		// 檢視client Recv失敗。如果可能,生成另一個,重新嘗試傳送watch請求
		// 證明傳送watch請求失敗,會建立watch client再次嘗試傳送
		case err := <-w.errc:
			if isHaltErr(w.ctx, err) || toErr(w.ctx, err) == v3rpc.ErrNoLeader {
				closeErr = err
				return
			}
			if wc, closeErr = w.newWatchClient(); closeErr != nil {
				return
			}
			if ws := w.nextResume(); ws != nil {
				if err := wc.Send(ws.initReq.toPB()); err != nil {
					w.lg.Debug("error when sending request", zap.Error(err))
				}
			}
			cancelSet = make(map[int64]struct{})

		case <-w.ctx.Done():
			return
			// closurec 獲取關閉觀察者的觀察者流
		case ws := <-w.closingc:
			w.closeSubstream(ws)
			delete(closing, ws)
			// no more watchers on this stream, shutdown, skip cancellation
			if len(w.substreams)+len(w.resuming) == 0 {
				return
			}
			if ws.id != -1 {
				// 客戶端正在關閉一個已建立的監視;在伺服器上主動關閉它而不是等待
				// 在下一條訊息到達時關閉
				cancelSet[ws.id] = struct{}{}
				cr := &pb.WatchRequest_CancelRequest{
					CancelRequest: &pb.WatchCancelRequest{
						WatchId: ws.id,
					},
				}
				req := &pb.WatchRequest{RequestUnion: cr}
				w.lg.Debug("sending watch cancel request for closed watcher", zap.Int64("watch-id", ws.id))
				if err := wc.Send(req); err != nil {
					w.lg.Debug("failed to send watch cancel request", zap.Int64("watch-id", ws.id), zap.Error(err))
				}
			}
		}
	}
}

// dispatchEvent 將 WatchResponse 傳送到適當的觀察者流
func (w *watchGrpcStream) dispatchEvent(pbresp *pb.WatchResponse) bool {
	events := make([]*Event, len(pbresp.Events))
	for i, ev := range pbresp.Events {
		events[i] = (*Event)(ev)
	}
	// TODO: return watch ID?
	wr := &WatchResponse{
		Header:          *pbresp.Header,
		Events:          events,
		CompactRevision: pbresp.CompactRevision,
		Created:         pbresp.Created,
		Canceled:        pbresp.Canceled,
		cancelReason:    pbresp.CancelReason,
	}

	// 如果watch IDs 索引是0, 所以watch resp 的watch ID 分配為 -1 ,並廣播這個watch response
	if wr.IsProgressNotify() && pbresp.WatchId == -1 {
		return w.broadcastResponse(wr)
	}

	return w.unicastResponse(wr, pbresp.WatchId)

}

總結:

1、通過etcd grpc伺服器啟動一個watch stream;

2、select檢測各個chan的事件(reqc、respc、errc、closingc);

3、dispatchEvent 分發事件,處理;

newWatchClient

再來看下newWatchClient,建立一個grpc client連線etcd grpc server

func (w *watchGrpcStream) newWatchClient() (pb.Watch_WatchClient, error) {
	// 將所有訂閱的stream標記為恢復
	close(w.resumec)
	w.resumec = make(chan struct{})
	w.joinSubstreams()
	for _, ws := range w.substreams {
		ws.id = -1
		w.resuming = append(w.resuming, ws)
	}
	// 去掉無用,即為nil的stream
	var resuming []*watcherStream
	for _, ws := range w.resuming {
		if ws != nil {
			resuming = append(resuming, ws)
		}
	}
	w.resuming = resuming
	w.substreams = make(map[int64]*watcherStream)

	// 連線到grpc stream,並且接受watch取消
	stopc := make(chan struct{})
	donec := w.waitCancelSubstreams(stopc)
	wc, err := w.openWatchClient()
	close(stopc)
	<-donec

	// 對於client出錯的stream,可以關閉,並且建立一個goroutine,用於轉發從run()得到的響應給訂閱者
	for _, ws := range w.resuming {
		if ws.closing {
			continue
		}
		ws.donec = make(chan struct{})
		w.wg.Add(1)
		go w.serveSubstream(ws, w.resumec)
	}

	if err != nil {
		return nil, v3rpc.Error(err)
	}

	// 建立goroutine接收來自新grpc流的資料
	go w.serveWatchClient(wc)
	return wc, nil
}

// serveWatchClient 將從grpc stream收到的訊息轉發到run()
func (w *watchGrpcStream) serveWatchClient(wc pb.Watch_WatchClient) {
	for {
		resp, err := wc.Recv()
		if err != nil {
			select {
			case w.errc <- err:
			case <-w.donec:
			}
			return
		}
		select {
		case w.respc <- resp:
		case <-w.donec:
			return
		}
	}
}

總結:

1、將所有訂閱的stream標記為恢復;

2、連線到grpc stream,並且接受watch取消;

3、關閉出錯的client stream,並且建立goroutine,用於轉發從run()得到的響應給訂閱者;

4、建立goroutine接收來自新grpc流的資料。

serveSubstream

// serveSubstream 將 watch 響應從 run() 轉發給訂閱者
func (w *watchGrpcStream) serveSubstream(ws *watcherStream, resumec chan struct{}) {
	if ws.closing {
		panic("created substream goroutine but substream is closing")
	}

	// nextRev is the minimum expected next revision
	nextRev := ws.initReq.rev
	resuming := false
	defer func() {
		if !resuming {
			ws.closing = true
		}
		close(ws.donec)
		if !resuming {
			w.closingc <- ws
		}
		w.wg.Done()
	}()

	emptyWr := &WatchResponse{}
	for {
		curWr := emptyWr
		outc := ws.outc

		if len(ws.buf) > 0 {
			curWr = ws.buf[0]
		} else {
			outc = nil
		}
		select {
		case outc <- *curWr:
			if ws.buf[0].Err() != nil {
				return
			}
			ws.buf[0] = nil
			ws.buf = ws.buf[1:]

			// 一旦觀察者建立,retc 就會收到一個 chan WatchResponse
			// 讀取recvc裡面的值
		case wr, ok := <-ws.recvc:
			if !ok {
				// shutdown from closeSubstream
				return
			}
			// 建立
			if wr.Created {
				if ws.initReq.retc != nil {
					ws.initReq.retc <- ws.outc
					// 防止下一次寫入佔用緩衝通道中的插槽併發布重複的建立事件
					ws.initReq.retc = nil
					// 僅在請求時傳送第一個建立事件
					if ws.initReq.createdNotify {
						ws.outc <- *wr
					}
					// once the watch channel is returned, a current revision
					// watch must resume at the store revision. This is necessary
					// 只要watch channel返回,當前revision的watch一定會在store revision是恢復
					// 對於以下情況按預期工作:
					//	wch := m1.Watch("a")
					//	m2.Put("a", "b")
					//	<-wch
					// 如果修訂只繫結在第一個觀察到的事件上,
					// 如果在發出 Put 之前 wch 斷開連線,則重新連線
					// 提交後,它將錯過 Put。
					if ws.initReq.rev == 0 {
						nextRev = wr.Header.Revision
					}
				}
			} else {
				// current progress of watch; <= store revision
				nextRev = wr.Header.Revision
			}

			if len(wr.Events) > 0 {
				nextRev = wr.Events[len(wr.Events)-1].Kv.ModRevision + 1
			}
			ws.initReq.rev = nextRev

			// 上面已經傳送了建立的事件,
			// 觀察者不應釋出重複的事件
			if wr.Created {
				continue
			}

			// TODO pause channel if buffer gets too large
			ws.buf = append(ws.buf, wr)
		case <-w.ctx.Done():
			return
		case <-ws.initReq.ctx.Done():
			return
		case <-resumec:
			resuming = true
			return
		}
	}

	// 如果缺少 id 的事件,則延遲傳送取消訊息
}

總結:

1、etcd v3 API採用了gRPC ,而 gRPC 又利用了HTTP/2 TCP 連結多路複用( multiple stream per tcp connection ),這樣同一個Client的不同watch可以共享同一個TCP連線。

2、watch支援指定單個 key,也可以指定一個 key 的字首;

3、Watch觀察將要發生或者已經發生的事件,輸入和輸出都是流,輸入流用於建立和取消觀察,輸出流傳送事件;

4、WatcherGrpcStream會啟動一個協程專門用於通過 gRPC client stream 接收Server端的 watch response,然後將watch response send 到WatcherGrpcStream的watch response channel。

5、 WatcherGrpcStream 也有一個專門的 協程專門用於從watch response channel 讀資料,拿到watch response之後,會根據response裡面的watchId 從WatcherGrpcStream的map[watchID] WatcherStream 中拿到對應的WatcherStream,並send到WatcherStream裡面的WatchReponse channel。

6、這裡的watchId其實是Server端返回給client端的,當client Send Watch request給Server端時候,response會帶上watchId, 這個watchId是與watch key是一一對應關係,然後client會建立WatchId與WatcherStream的對映關係。

7、WatcherStream是具體的 watch response的處理結構,對於每個watch key,WatcherGrpcStream 也會啟動一個專門的協程處理WatcherStream裡面的watch response channel。

server端的程式碼實現

來看下總體的架構

1、etcd服務端建立newWatchableStore開啟group監聽;

2、呼叫mvcc中syncWatchers將所有未通知的事件通知給所有的監聽者;

3、對watcher通道阻塞時存入victim中資料,開啟syncVictimsLoop;

4、watchServer響應客戶端請求,發起watchStream及watcher例項新建,並將其新增至unsynced或synced中;

5、client端通過grpc proxy向watcherServer傳送watcher請求;

6、grpc proxy提供對同一個key的多次watch合併減少etcd server中重複watcher建立,以提高etcd server穩定性。

etcd

watchableStore

先來看下watchableStore

// 檔案 /mvcc/watchable_store.go
type watchableStore struct {
	*store
	mu sync.RWMutex
	// 當ch被阻塞時,對應 watcherBatch 例項會暫時記錄到這個欄位
	victims []watcherBatch
	// 當有新的 watcherBatch 例項新增到 victims 欄位時,會向該通道傳送訊息
	victimc chan struct{}
	// 未同步的 watcher
	unsynced watcherGroup
	// 已完成同步的 watcher
	synced watcherGroup
	stopc chan struct{}
	wg sync.WaitGroup
}

type watcher struct {
	// 監聽起始值
	key []byte
	// 監聽終止值,  key 和 end 共同組成一個鍵值範圍
	end []byte
	// 是否被阻塞
	victim bool
	// 是否壓縮
	compacted bool
	...
	// 最小的 revision main
	minRev int64
	id     WatchID
	...
	ch chan<- WatchResponse
}

// server/mvcc/watchable_store.go
func newWatchableStore(lg *zap.Logger, b backend.Backend, le lease.Lessor, cfg StoreConfig) *watchableStore {
	if lg == nil {
		lg = zap.NewNop()
	}
	s := &watchableStore{
		store:    NewStore(lg, b, le, cfg),
		victimc:  make(chan struct{}, 1),
		unsynced: newWatcherGroup(),
		synced:   newWatcherGroup(),
		stopc:    make(chan struct{}),
	}
	s.store.ReadView = &readView{s}
	s.store.WriteView = &writeView{s}
	if s.le != nil {
		// use this store as the deleter so revokes trigger watch events
		s.le.SetRangeDeleter(func() lease.TxnDelete { return s.Write(traceutil.TODO()) })
	}
	s.wg.Add(2)
	// 開2個協程
    // syncWatchersLoop 每 100 毫秒同步一次未同步對映中的觀察者。
	go s.syncWatchersLoop()
    // syncVictimsLoop 同步預先傳送未成功的watchers
	go s.syncVictimsLoop()
	return s
}

總結

1、初始化一個watchableStore;

2、啟動了兩個協程

  • syncWatchersLoop:每 100 毫秒同步一次未同步對映中的觀察者;

  • syncVictimsLoop:同步預先傳送未成功的watchers;

syncWatchersLoop

syncWatchersLoop會呼叫syncWatchers來進行watcher的同步操作

// syncWatchersLoop 每 100 毫秒同步一次未同步對映中的觀察者。
func (s *watchableStore) syncWatchersLoop() {
	defer s.wg.Done()

	for {
		s.mu.RLock()
		st := time.Now()
		lastUnsyncedWatchers := s.unsynced.size()
		s.mu.RUnlock()

		unsyncedWatchers := 0
		//如果 unsynced 中存在資料,進行同步
		if lastUnsyncedWatchers > 0 {
			unsyncedWatchers = s.syncWatchers()
		}
		syncDuration := time.Since(st)

		waitDuration := 100 * time.Millisecond
		// more work pending?
		if unsyncedWatchers != 0 && lastUnsyncedWatchers > unsyncedWatchers {
			// be fair to other store operations by yielding time taken
			waitDuration = syncDuration
		}

		select {
		case <-time.After(waitDuration):
		case <-s.stopc:
			return
		}
	}
}

總結:

1、如果unsynced中存在資料,進行同步;

2、100 * time.Millisecond迴圈呼叫一次。

syncWatchers

再來看下syncWatchers

// syncWatchers 通過以下方式同步未同步的觀察者:
// 1. 從未同步的觀察者組中選擇一組觀察者
// 2. 迭代集合以獲得最小修訂並移除壓縮的觀察者
// 3. 使用最小修訂來獲取所有鍵值對並將這些事件傳送給觀察者
// 4. 從未同步組中移除集合中的同步觀察者並移至同步組
func (s *watchableStore) syncWatchers() int {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.unsynced.size() == 0 {
		return 0
	}

	s.store.revMu.RLock()
	defer s.store.revMu.RUnlock()

	// 為了從未同步的觀察者中找到鍵值對,我們需要
	// 找到最小修訂索引,這些修訂可用於
	// 查詢鍵值對的後端儲存
	curRev := s.store.currentRev
	compactionRev := s.store.compactMainRev

	wg, minRev := s.unsynced.choose(maxWatchersPerSync, curRev, compactionRev)
	minBytes, maxBytes := newRevBytes(), newRevBytes()
	revToBytes(revision{main: minRev}, minBytes)
	revToBytes(revision{main: curRev + 1}, maxBytes)

	// UnsafeRange 返回鍵和值。在 boltdb 中,鍵是revisions。
	// 值是後端的實際鍵值對。
	tx := s.store.b.ReadTx()
	tx.RLock()
	revs, vs := tx.UnsafeRange(buckets.Key, minBytes, maxBytes, 0)
	tx.RUnlock()
	evs := kvsToEvents(s.store.lg, wg, revs, vs)

	var victims watcherBatch
	// newWatcherBatch 將觀察者對映到它們匹配的事件。也就是一個map中,可以使觀察者快速找到匹配的事件
	wb := newWatcherBatch(wg, evs)
	for w := range wg.watchers {
		w.minRev = curRev + 1

		eb, ok := wb[w]
		if !ok {
			// 同步未同步的觀察者
			s.synced.add(w)
			s.unsynced.delete(w)
			continue
		}

		if eb.moreRev != 0 {
			w.minRev = eb.moreRev
		}

		// 將前面建立的 Event 事件封裝成 WatchResponse,然後寫入 watcher.ch 通道中
		if w.send(WatchResponse{WatchID: w.id, Events: eb.evs, Revision: curRev}) {
			pendingEventsGauge.Add(float64(len(eb.evs)))
		} else {
			// 如果阻塞,操作放回到victims中
			if victims == nil {
				victims = make(watcherBatch)
			}
			w.victim = true
		}

		if w.victim {
			victims[w] = eb
		} else {
			// 表示後面還有更多的事件
			if eb.moreRev != 0 {
				// 保持未同步,繼續
				continue
			}
			// 標註已經同步
			s.synced.add(w)
		}
		// 從未同步中移除
		s.unsynced.delete(w)
	}
	// 新增阻塞
	s.addVictim(victims)

	vsz := 0
	for _, v := range s.victims {
		vsz += len(v)
	}
	slowWatcherGauge.Set(float64(s.unsynced.size() + vsz))

	return s.unsynced.size()
}

總結:

1、syncWatchers中的主要作用是同步未同步的觀察者;

2、同時也會將前面建立的Event事件封裝成WatchResponse,然後寫入watcher.ch通道中,sendLoop監聽channel就能,及時通知客戶端key的變更。

syncVictimsLoop

再來看下syncVictimsLoop

// syncVictimsLoop tries to write precomputed watcher responses to
// watchers that had a blocked watcher channel
func (s *watchableStore) syncVictimsLoop() {
	defer s.wg.Done()

	for {
		// 將 victims 中的資料嘗試傳送出去
		for s.moveVictims() != 0 {
			// try to update all victim watchers
		}
		s.mu.RLock()
		isEmpty := len(s.victims) == 0
		s.mu.RUnlock()

		var tickc <-chan time.Time
		if !isEmpty {
			tickc = time.After(10 * time.Millisecond)
		}

		select {
		case <-tickc:
		case <-s.victimc:
		case <-s.stopc:
			return
		}
	}
}

主要是呼叫了moveVictims,接下來看下moveVictims的實現

moveVictims

// moveVictims 嘗試watches,如果有pending的event
func (s *watchableStore) moveVictims() (moved int) {
	s.mu.Lock()
	victims := s.victims
	s.victims = nil
	s.mu.Unlock()

	var newVictim watcherBatch
	for _, wb := range victims {
		// 嘗試再次傳送
		for w, eb := range wb {
			// watcher has observed the store up to, but not including, w.minRev
			rev := w.minRev - 1
			// 將前面建立的 Event 事件封裝成 WatchResponse,然後寫入 watcher.ch 通道中
			if w.send(WatchResponse{WatchID: w.id, Events: eb.evs, Revision: rev}) {
				pendingEventsGauge.Add(float64(len(eb.evs)))
			} else {
				// 如果阻塞繼續放回victims
				if newVictim == nil {
					newVictim = make(watcherBatch)
				}
				newVictim[w] = eb
				continue
			}
			moved++
		}

		// 將victim分配到 unsync/sync中
		s.mu.Lock()
		s.store.revMu.RLock()
		curRev := s.store.currentRev
		for w, eb := range wb {
			if newVictim != nil && newVictim[w] != nil {
				// 無法傳送繼續放回到victim中
				continue
			}
			w.victim = false
			if eb.moreRev != 0 {
				w.minRev = eb.moreRev
			}
			// currentRev 是最後完成的事務的revision。
			// minRev 是觀察者將接受的最小revision的更新
			// 說明這一部分還沒有同步到
			if w.minRev <= curRev {
				// 如果未同步,放到unsynced中
				s.unsynced.add(w)
			} else {
				// 同步了直接放入到synced中
				slowWatcherGauge.Dec()
				s.synced.add(w)
			}
		}
		s.store.revMu.RUnlock()
		s.mu.Unlock()
	}

	if len(newVictim) > 0 {
		s.mu.Lock()
		s.victims = append(s.victims, newVictim)
		s.mu.Unlock()
	}

	return moved
}

總結:

1、將 victims 中的資料嘗試傳送出去;

2、如果傳送仍然阻塞,需要重新放回 victims;

3、判斷這些傳送完成的版本號是否小於當前版本號,如果是說明者個過程中有資料更新,還沒有同步完成,需要新增到 unsynced 中,等待下次同步。如果不是,說明已經同步完成。

watchServer

// 檔案:/etcdserver/api/v3rpc/watch.go
type watchServer struct {
	...
	watchable mvcc.WatchableKV // 鍵值儲存
	....
}

type serverWatchStream struct {
	...
	watchable mvcc.WatchableKV //kv 儲存
	...
	// 與客戶端進行連線的 Stream
	gRPCStream  pb.Watch_WatchServer
	// key 變動的訊息管道
	watchStream mvcc.WatchStream
	// 響應客戶端請求的訊息管道
	ctrlStream  chan *pb.WatchResponse
	...
	// 該型別的 watch,服務端會定時傳送類似心跳訊息
	progress map[mvcc.WatchID]bool
	// 該型別表明,對於/a/b 這樣的監聽範圍, 如果 b 變化了, 字首/a也需要通知
	prevKV map[mvcc.WatchID]bool
	// 該型別表明,傳輸資料量大於閾值,需要拆分傳送
	fragment map[mvcc.WatchID]bool
}

func (ws *watchServer) Watch(stream pb.Watch_WatchServer) (err error) {
	sws := serverWatchStream{
		lg: ws.lg,

		clusterID: ws.clusterID,
		memberID:  ws.memberID,

		maxRequestBytes: ws.maxRequestBytes,

		sg:        ws.sg,
		watchable: ws.watchable,
		ag:        ws.ag,

		gRPCStream:  stream,
		watchStream: ws.watchable.NewWatchStream(),
		// chan for sending control response like watcher created and canceled.
		ctrlStream: make(chan *pb.WatchResponse, ctrlStreamBufLen),

		progress: make(map[mvcc.WatchID]bool),
		prevKV:   make(map[mvcc.WatchID]bool),
		fragment: make(map[mvcc.WatchID]bool),

		closec: make(chan struct{}),
	}

	sws.wg.Add(1)
	go func() {
		// 啟動sendLoop
		sws.sendLoop()
		sws.wg.Done()
	}()

	errc := make(chan error, 1)
	// 理想情況下,recvLoop 也會使用 sws.wg 來表示它的完成
	// 但是當 stream.Context().Done() 關閉時,流的 recv
	// 可能會繼續阻塞,因為它使用不同的上下文,導致
	// 呼叫 sws.close() 時死鎖。
	go func() {
		// 啟動recvLoop
		if rerr := sws.recvLoop(); rerr != nil {
			if isClientCtxErr(stream.Context().Err(), rerr) {
				sws.lg.Debug("failed to receive watch request from gRPC stream", zap.Error(rerr))
			} else {
				sws.lg.Warn("failed to receive watch request from gRPC stream", zap.Error(rerr))
				streamFailures.WithLabelValues("receive", "watch").Inc()
			}
			errc <- rerr
		}
	}()

	// 如果 recv goroutine 在 send goroutine 之前完成,則底層錯誤(例如 gRPC 流錯誤)可能會通過 errc 返回和處理。
	// 當 recv goroutine 獲勝時,流錯誤被保留。當 recv 失去競爭時,底層錯誤就會丟失(除非根錯誤通過 Context.Err() 傳播,但情況並非總是如此(因為呼叫者必須決定實現自定義上下文才能這樣做)
	// stdlib 上下文包內建可能不足以攜帶語義上有用的錯誤,應該被重新審視。
	select {
	case err = <-errc:
		if err == context.Canceled {
			err = rpctypes.ErrGRPCWatchCanceled
		}
		close(sws.ctrlStream)
	case <-stream.Context().Done():
		err = stream.Context().Err()
		if err == context.Canceled {
			err = rpctypes.ErrGRPCWatchCanceled
		}
	}

	sws.close()
	return err
}

watchServer在上面啟動了recvLoop和sendLoop,分別來處理和接收客戶度的請求

recvLoop

recvLoop接收客戶端請求

func (sws *serverWatchStream) recvLoop() error {
	for {
		req, err := sws.gRPCStream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}

		switch uv := req.RequestUnion.(type) {
		// 處理CreateRequest的請求
		case *pb.WatchRequest_CreateRequest:
			if uv.CreateRequest == nil {
				break
			}

			creq := uv.CreateRequest
			...
			if !sws.isWatchPermitted(creq) {
				// 封裝WatchResponse強求
				wr := &pb.WatchResponse{
					Header:       sws.newResponseHeader(sws.watchStream.Rev()),
					WatchId:      creq.WatchId,
					Canceled:     true,
					Created:      true,
					CancelReason: rpctypes.ErrGRPCPermissionDenied.Error(),
				}

				select {
				// ctrlStream響應客戶端請求的訊息管道
				// 傳遞WatchResponse請求
				case sws.ctrlStream <- wr:
					continue
				case <-sws.closec:
					return nil
				}
			}

			filters := FiltersFromRequest(creq)

			wsrev := sws.watchStream.Rev()
			rev := creq.StartRevision
			if rev == 0 {
				rev = wsrev + 1
			}
			// Watch 在流中建立一個新的 watcher 並返回它的 WatchID。
			id, err := sws.watchStream.Watch(mvcc.WatchID(creq.WatchId), creq.Key, creq.RangeEnd, rev, filters...)
			...
			wr := &pb.WatchResponse{
				Header:   sws.newResponseHeader(wsrev),
				WatchId:  int64(id),
				Created:  true,
				Canceled: err != nil,
			}
			if err != nil {
				wr.CancelReason = err.Error()
			}
			select {
			// ctrlStream響應客戶端請求的訊息管道
			// 傳遞WatchResponse請求
			case sws.ctrlStream <- wr:
			case <-sws.closec:
				return nil
			}
			// 處理CancelRequest的請求
		case *pb.WatchRequest_CancelRequest:
			if uv.CancelRequest != nil {
				id := uv.CancelRequest.WatchId
				err := sws.watchStream.Cancel(mvcc.WatchID(id))
				if err == nil {
					sws.ctrlStream <- &pb.WatchResponse{
						Header:   sws.newResponseHeader(sws.watchStream.Rev()),
						WatchId:  id,
						Canceled: true,
					}
					sws.mu.Lock()
					delete(sws.progress, mvcc.WatchID(id))
					delete(sws.prevKV, mvcc.WatchID(id))
					delete(sws.fragment, mvcc.WatchID(id))
					sws.mu.Unlock()
				}
			}
			// 處理ProgressRequest的請求
		case *pb.WatchRequest_ProgressRequest:
			if uv.ProgressRequest != nil {
				sws.ctrlStream <- &pb.WatchResponse{
					Header:  sws.newResponseHeader(sws.watchStream.Rev()),
					WatchId: -1, // response is not associated with any WatchId and will be broadcast to all watch channels
				}
			}
		default:
			// 我們可能不應該在以下情況下關閉整個流
			// 接收有效命令。
			// 什麼都不做。
			continue
		}
	}
}

// server/mvcc/watcher.go
type watchStream struct {
	// 用來記錄關聯的 watchableStore
	watchable watchable
	// event 事件寫入通道
	ch        chan WatchResponse
	...
	cancels  map[WatchID]cancelFunc
	// 用來記錄唯一標識與 watcher 的例項的關係
	watchers map[WatchID]*watcher
}

// Watch 在流中建立一個新的 watcher 並返回它的 WatchID。
func (ws *watchStream) Watch(id WatchID, key, end []byte, startRev int64, fcs ...FilterFunc) (WatchID, error) {
	// prevent wrong range where key >= end lexicographically
	// watch request with 'WithFromKey' has empty-byte range end
	if len(end) != 0 && bytes.Compare(key, end) != -1 {
		return -1, ErrEmptyWatcherRange
	}

	ws.mu.Lock()
	defer ws.mu.Unlock()
	if ws.closed {
		return -1, ErrEmptyWatcherRange
	}

	// watch ID在不等於AutoWatchID的時候被使用,否則將會返回一個自增的id
	if id == AutoWatchID {
		for ws.watchers[ws.nextID] != nil {
			ws.nextID++
		}
		id = ws.nextID
		ws.nextID++
	} else if _, ok := ws.watchers[id]; ok {
		return -1, ErrWatcherDuplicateID
	}

	w, c := ws.watchable.watch(key, end, startRev, id, ws.ch, fcs...)

	ws.cancels[id] = c
	ws.watchers[id] = w
	return id, nil
}

func (s *watchableStore) watch(key, end []byte, startRev int64, id WatchID, ch chan<- WatchResponse, fcs ...FilterFunc) (*watcher, cancelFunc) {
	wa := &watcher{
		key:    key,
		end:    end,
		minRev: startRev,
		id:     id,
		ch:     ch,
		fcs:    fcs,
	}
    // 先上一把大的互斥鎖
    // 多個watch操作,通過這個互斥鎖,保證資料的順序
	s.mu.Lock()
    // 裡面上一把小的讀鎖
    // 讀操作優先,保護讀操作
	s.revMu.RLock()
	// 比較 startRev 和 currentRev,決定新增的 watcher 例項是否已經同步
	synced := startRev > s.store.currentRev || startRev == 0
	if synced {
		wa.minRev = s.store.currentRev + 1
		if startRev > wa.minRev {
			wa.minRev = startRev
		}
        // 新增到已同步的 watcher中
		s.synced.add(wa)
	} else {
		slowWatcherGauge.Inc()
        // 新增到未同步的 watcher中
		s.unsynced.add(wa)
	}
	s.revMu.RUnlock()
	s.mu.Unlock()

	watcherGauge.Inc()

	return wa, func() { s.cancelWatcher(wa) }
}

總結

1、接受客戶端的請求;

2、根據不同的請求資料型別進行處理;

3、主要是通過watchStream來關聯watcher,來處理每一個請求。

sendLoop

響應客戶端的請求

func (sws *serverWatchStream) sendLoop() {
	// watch ids that are currently active
	ids := make(map[mvcc.WatchID]struct{})
	// watch 響應等待 watch id 建立訊息
	pending := make(map[mvcc.WatchID][]*pb.WatchResponse)
	...

	for {
		select {
		// 監聽key 變動的訊息管道
		case wresp, ok := <-sws.watchStream.Chan():
			if !ok {
				return
			}
			...
			canceled := wresp.CompactRevision != 0
			wr := &pb.WatchResponse{
				Header:          sws.newResponseHeader(wresp.Revision),
				WatchId:         int64(wresp.WatchID),
				Events:          events,
				CompactRevision: wresp.CompactRevision,
				Canceled:        canceled,
			}

			if _, okID := ids[wresp.WatchID]; !okID {
				// buffer if id not yet announced
				wrs := append(pending[wresp.WatchID], wr)
				pending[wresp.WatchID] = wrs
				continue
			}

			mvcc.ReportEventReceived(len(evs))

			sws.mu.RLock()
			fragmented, ok := sws.fragment[wresp.WatchID]
			sws.mu.RUnlock()

			var serr error
			if !fragmented && !ok {
				// 通過rpc傳送響應給客戶端
				serr = sws.gRPCStream.Send(wr)
			} else {
				serr = sendFragments(wr, sws.maxRequestBytes, sws.gRPCStream.Send)
			}

			...
			// 監聽響應客戶端請求的訊息管道
		case c, ok := <-sws.ctrlStream:
			if !ok {
				return
			}

			if err := sws.gRPCStream.Send(c); err != nil {
				if isClientCtxErr(sws.gRPCStream.Context().Err(), err) {
					sws.lg.Debug("failed to send watch control response to gRPC stream", zap.Error(err))
				} else {
					sws.lg.Warn("failed to send watch control response to gRPC stream", zap.Error(err))
					streamFailures.WithLabelValues("send", "watch").Inc()
				}
				return
			}
			....
		case <-progressTicker.C:
			sws.mu.Lock()
			for id, ok := range sws.progress {
				if ok {
					// WatchStream
					// 定時傳送 RequestProgress,類似心跳包
					sws.watchStream.RequestProgress(id)
				}
				sws.progress[id] = true
			}
			sws.mu.Unlock()

		case <-sws.closec:
			return
		}
	}
}

type WatchStream interface {
	// Watch 建立了一個觀察者. 觀察者監聽發生在給定的鍵或範圍[key, end]上的事件的變化。
	//
	// 整個事件歷史可以被觀察,除非壓縮。
	// 如果"startRev" <=0, watch觀察當前之後的事件。

	// 將返回watcher的id,它顯示為WatchID
	// 通過流通道傳送給建立的監視器的事件。
	// watch ID在不等於AutoWatchID的時候被使用,否則將會返回一個自增的id
	Watch(id WatchID, key, end []byte, startRev int64, fcs ...FilterFunc) (WatchID, error)

	// Chan返回一個Chan。所有的觀察響應將被髮送到返回的chan。
	Chan() <-chan WatchResponse

	// RequestProgress請求給定ID的觀察者的進度。響應只在觀察者當前同步時傳送。
	// 響應將通過附加的WatchRespone Chan傳送,使用這個流來確保正確的排序。
	// 相應不包含事件。響應中的修訂是進度的觀察者,因為觀察者當前已同步。
	RequestProgress(id WatchID)

	// Cancel 通過給出它的 ID 來取消一個觀察者。如果 watcher 不存在,則會報錯
	Cancel(id WatchID) error

	// Close closes Chan and release all related resources.
	Close()

	// Rev 返回流監視的 KV 的當前版本。
	Rev() int64
}

總結:

1、通過watchStream.Chan監聽key值的變更;

2、處理 ctrlStream 的訊息(客戶端請求,返回響應);

3、定時傳送 RequestProgress 類似心跳包。

連線複用

上面我們提到了連線複用,我們來看看如何實現複用的

// 其中Watch()函式傳送watch請求,第一次傳送後遞迴呼叫Watch實現持續監聽
func (w *watcher) Watch(ctx context.Context, key string, opts ...OpOption) WatchChan {
	ow := opWatch(key, opts...)

	var filters []pb.WatchCreateRequest_FilterType
	if ow.filterPut {
		filters = append(filters, pb.WatchCreateRequest_NOPUT)
	}
	if ow.filterDelete {
		filters = append(filters, pb.WatchCreateRequest_NODELETE)
	}

	wr := &watchRequest{
		ctx:            ctx,
		createdNotify:  ow.createdNotify,
		key:            string(ow.key),
		end:            string(ow.end),
		rev:            ow.rev,
		progressNotify: ow.progressNotify,
		fragment:       ow.fragment,
		filters:        filters,
		prevKV:         ow.prevKV,
		retc:           make(chan chan WatchResponse, 1),
	}

	ok := false
	ctxKey := streamKeyFromCtx(ctx)

	var closeCh chan WatchResponse
	for {
		// 查詢或分配適當的 grpc 監視流
		w.mu.Lock()
		if w.streams == nil {
			// closed
			w.mu.Unlock()
			ch := make(chan WatchResponse)
			close(ch)
			return ch
		}

		// streams是一個map,儲存所有由 ctx 值鍵控的活動 grpc 流
		// 如果該請求對應的流為空,則新建
		wgs := w.streams[ctxKey]
		if wgs == nil {
			// newWatcherGrpcStream new一個watch grpc stream來傳輸watch請求
			// 建立goroutine來處理監聽key的watch各種事件
			wgs = w.newWatcherGrpcStream(ctx)
			w.streams[ctxKey] = wgs
		}
		donec := wgs.donec
		reqc := wgs.reqc
		w.mu.Unlock()

		// couldn't create channel; return closed channel
		if closeCh == nil {
			closeCh = make(chan WatchResponse, 1)
		}

		// 等待接收值
		select {
		// reqc 從 Watch() 向主協程傳送觀察請求
		case reqc <- wr:
			ok = true
		case <-wr.ctx.Done():
			ok = false
		case <-donec:
			ok = false
			if wgs.closeErr != nil {
				closeCh <- WatchResponse{Canceled: true, closeErr: wgs.closeErr}
				break
			}
			// 重試,可能已經從沒有 ctxs 中刪除了流
			continue
		}

		// receive channel
		if ok {
			select {
			case ret := <-wr.retc:
				return ret
			case <-ctx.Done():
			case <-donec:
				if wgs.closeErr != nil {
					closeCh <- WatchResponse{Canceled: true, closeErr: wgs.closeErr}
					break
				}
				// 重試,可能已經從沒有 ctxs 中刪除了流
				continue
			}
		}
		break
	}

	close(closeCh)
	return closeCh
}

例如這個client的watch

1、newWatcherGrpcStream new一個watchGrpcStream來傳輸watch請求;

2、監聽watchGrpcStream的reqc.c,來傳送請求;

這倆實現了連線複用,只要沒有關閉,就能一直監聽傳送請求資訊。

總結

上面主要總結了etcd中watch機制,client端比較簡答,server端的實現比較複雜;

client主要是提供操作來請求對key監聽,並且接收key變更時的通知。server要能做到接收key監聽請求,並且啟動定時器等方法來對key進行監聽,有變更時通知client。

v3版本中watch依賴gRPC介面,實現連線複用。

相關文章