深入理解etcd(二)--- watch 如何实现?

316 阅读15分钟

1. 引言

为了减少客户端的频繁轮询,etcd引入了高效的Watch机制。通过这一机制,客户端可以监视特定键或一系列键的变化,当这些被监视的键发生更新时,etcd会立即通知相应的客户端。这种事件驱动的方式不仅降低了系统的负载,还提高了响应速度和资源利用率。

2. 使用

启动一个空集群,更新两次 key hello 后,使用 Watch 特性获取 key hello 的历史修改记录

$ etcdctl put hello world1
$ etcdctl put hello world2
$ etcdctl watch hello -w=json --rev=1
{
    "Events":[
        {
            "kv":{
                "key":"aGVsbG8=",
                "create_revision":2,
                "mod_revision":2,
                "version":1,
                "value":"d29ybGQx"
            }
        },
        {
            "kv":{
                "key":"aGVsbG8=",
                "create_revision":2,
                "mod_revision":3,
                "version":2,
                "value":"d29ybGQy"
            }
        }
    ],
    "CompactRevision":0,
    "Canceled":false,
    "Created":false
}

3. 事件机制

Etcd v2的Watch机制通过长连接轮询获取更新,资源消耗大。v3采用gRPC双向流和多路复用,减少连接数,仅在数据变化时推送更新。

image.png

4. 整体流程

watch 整体架构图如下图所示,可以分为两个部分:

1)创建 watcher

2)watch 事件推送

image.png 客户端通过clientv3 Watch发送WatchRequestgRPC Watch Server,后者创建serverWatchStream处理请求。serverWatchStream通过recvLoop接收create/cancel watcher请求,并通过sendLoop将事件转发给客户端。事件由WatchableKV模块管理,通过WatchStream子模块通知serverWatchStreamsyncWatchersLoopsyncVictimsLoop确保可靠性,所有历史版本数据存储在boltdb中。

5. 源码

Q1: 事件是如何存储的?会保留多久?

  • Etcd v2:使用固定容量的环形数组存储历史事件,这在高写入量或网络波动时容易导致事件丢失,并可能需要执行全量同步。
  • Etcd v3:采用MVCC(多版本并发控制)机制将键的历史版本持久化存储在BoltDB中。这种设计支持重启后数据恢复,并通过压缩策略管理历史版本数,从而更有效地处理大量事件并减少数据丢失的风险。

Q2: Client 获取事件机制,etcd 是使用轮询模式还是推送模式呢?

  • v2:基于HTTP协议,每个watcher对应一个TCP连接,采用轮询方式获取更新。
  • v3:利用gRPC的流式传输特性,允许多个watcher共享一个连接,采用推送模式向客户端发送变更通知,提高了效率和资源利用率。

Q3: MVCC模块和gRPC的流式传输如何关联起来的?

  • 每个gRPC连接创建唯一的channel,所有属于该连接的watcher通过这个channel与MVCC层进行数据交互。Watcher结构体包含指向该channel的引用。
  • 在MVCC层:
    • 对于监控单个key的情况,使用map(key -> watcher)存储。
    • 对于监控key范围的情况,使用区间树(增删查的时间复杂度为O(log n))。
  • 当某个key发生变更时,根据key查找对应的watcher并通过该watcher将变更信息发送到channel,使得gRPC链接层能够感知到key的变化。

Q4: 当client和server端出现事件堆积时,server端会丢弃事件吗?

当事件堆积发生时,不会直接丢弃事件,而是将其放入一个名为victims watcher的队列中,异步尝试重试。如果重试后进度已达到最新版本,则将watcher移至synced watcherGroup;若未达到最新版本,则先移到unsynced watcherGroup进行进度更新,完成后同样移至synced watcherGroup

Q5: 若你监听的历史版本号server端不存在了,你的代码该如何处理?

如果watcher监听的版本号小于当前etcd server已压缩的版本号,意味着历史变更数据可能已被删除,此时etcd server会返回ErrCompacted错误给客户端。客户端收到此错误后应重新获取最新的版本号,并再次发起Watch请求。

Q6: 如果你创建了上万个watcher监听key变化,当server端收到一个写请求后,etcd是如何根据变化的key快速找到监听它的watcher呢?一个个遍历watcher吗? 为了高效地管理大量watcher,etcd使用map和区间树来加速查找过程:

  • 监控单个key时,使用map(key -> watcher集合);
  • 监控key范围时,使用区间树,确保增删查操作的时间复杂度为O(log n)。

Q7:如果保证推送事件的可靠性?

最新事件推送机制+历史事件推送机制+异常重试机制

5.1. server

etcd v3 使用的是 gRPC-Gateway 以同时提供 RPC 和 HTTP 服务

service Watch {
  // Watch 用于监视发生的或已经发生过的事件
  // 输入和输出都是流形式,输入流用于创建和取消监视器,而输出流则用于发送事件
  // 单个 Watch RPC 可以同时监视多个键范围,并为多个监视操作流式传输事件。
  // 可以从最后一次压缩修订版开始,监视整个事件历史
  rpc Watch(stream WatchRequest) returns (stream WatchResponse) {
    option (google.api.http) = {
      post: "/v3/watch"
      body: "*"
    };
  }
}

Watch 实现如下,和上述流程图一致,创建 serverWatchStream 并分别启动了sendLoop 和 recvLoop

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

    sws.wg.Add(1)
    go func() {
        // 第一个循环
        sws.sendLoop()
        sws.wg.Done()
    }()

    errc := make(chan error, 1)

    go func() {
        // 第二个循环
        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
        }
    }()

    select {
        case err = <-errc:
        if errors.Is(err, context.Canceled) {
            err = rpctypes.ErrGRPCWatchCanceled
        }
        close(sws.ctrlStream)
        case <-stream.Context().Done():
        err = stream.Context().Err()
        if errors.Is(err, context.Canceled) {
            err = rpctypes.ErrGRPCWatchCanceled
        }
    }

    sws.close()
    return err
}

5.2. sendloop

sendLoop 主要负责把从 MVCC 模块接收的 Watch 事件转发给 client

sendLoop 大致流程就是for + select

func (sws *serverWatchStream) sendLoop() {
	// watch ids that are currently active
	ids := make(map[mvcc.WatchID]struct{})
	// watch responses pending on a watch id creation message
	pending := make(map[mvcc.WatchID][]*pb.WatchResponse)

	interval := GetProgressReportInterval()
	progressTicker := time.NewTicker(interval)
    ...

	for {
		select {
		case wresp, ok := <-sws.watchStream.Chan():
            ...
		case c, ok := <-sws.ctrlStream:
			...
		case <-progressTicker.C:
            ...
		case <-sws.closec:
			return
		}
	}
}

第一种清况,收到mvcc 发送的变更事件,

大致流程是接受mvcc的事件,获取对应kv版本的值,然后封装一下,转发给client

		case wresp, ok := <-sws.watchStream.Chan():
			if !ok {
				return
			}

			// TODO: evs is []mvccpb.Event type
			// either return []*mvccpb.Event from the mvcc package
			// or define protocol buffer with []mvccpb.Event.
			evs := wresp.Events
			events := make([]*mvccpb.Event, len(evs))
			sws.mu.RLock()
			needPrevKV := sws.prevKV[wresp.WatchID]
			sws.mu.RUnlock()
			for i := range evs {
				events[i] = &evs[i]
				if needPrevKV && !IsCreateEvent(evs[i]) {
					opt := mvcc.RangeOptions{Rev: evs[i].Kv.ModRevision - 1}
					// 获取指定版本的kv
					r, err := sws.watchable.Range(context.TODO(), evs[i].Kv.Key, nil, opt)
					if err == nil && len(r.KVs) != 0 {
						events[i].PrevKv = &(r.KVs[0])
					}
				}
			}

			canceled := wresp.CompactRevision != 0
			wr := &pb.WatchResponse{
				Header:          sws.newResponseHeader(wresp.Revision),
				WatchId:         int64(wresp.WatchID),
				Events:          events,
				CompactRevision: wresp.CompactRevision,
				Canceled:        canceled,
			}

			if wresp.WatchID != clientv3.InvalidWatchID {
				// 如果 watcherID 还没注册到 ids 列表中,就先把这个 event 缓存起来
				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
			// gofail: var beforeSendWatchResponse struct{}
			// 事件发送给客户端,如果不是分片的事件,直接发送,否则分片发送
			if !fragmented && !ok {
				serr = sws.gRPCStream.Send(wr)
			} else {
				serr = sendFragments(wr, sws.maxRequestBytes, sws.gRPCStream.Send)
			}

			if serr != nil {
				if isClientCtxErr(sws.gRPCStream.Context().Err(), serr) {
					sws.lg.Debug("failed to send watch response to gRPC stream", zap.Error(serr))
				} else {
					sws.lg.Warn("failed to send watch response to gRPC stream", zap.Error(serr))
					streamFailures.WithLabelValues("send", "watch").Inc()
				}
				return
			}

			sws.mu.Lock()
			if len(evs) > 0 && sws.progress[wresp.WatchID] {
				//  如果有新的 event 产生,就把 progress 改成 fasle 以忽略掉下次进度更新消息的发送
				sws.progress[wresp.WatchID] = false
			}
			sws.mu.Unlock()

第二种情况是控制逻辑,包括了 watcher 的 create 和 cancel

控制逻辑消息由 recvLoop 产生,recvLoop 收到用户发送的 create 或者 cancel 请求后先调用 watchStream 的方法 create 或者 cancel watcher,然后在通过 chan 异步传递到 sendLoop 中,以维护 sendLoop 中的活跃watcherID 列表

		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
			}

			// track id creation
			wid := mvcc.WatchID(c.WatchId)
			// 如果是被取消了,就从ids中移除
			if c.Canceled {
				delete(ids, wid)
				continue
			}
			// 如果创建则把 watcherID 注册到 ids 列表中,然后把缓存的event都发送到 client
			if c.Created {
				// flush buffered events
				ids[wid] = struct{}{}
				for _, v := range pending[wid] {
					mvcc.ReportEventReceived(len(v.Events))
					if err := sws.gRPCStream.Send(v); err != nil {
						if isClientCtxErr(sws.gRPCStream.Context().Err(), err) {
							sws.lg.Debug("failed to send pending watch response to gRPC stream", zap.Error(err))
						} else {
							sws.lg.Warn("failed to send pending watch response to gRPC stream", zap.Error(err))
							streamFailures.WithLabelValues("send", "watch").Inc()
						}
						return
					}
				}
				delete(pending, wid)
			}

第三种情况是定时器,定时发送watch的进度,发送最新的watch的key版本

		case <-progressTicker.C:
			sws.mu.Lock()
			for id, ok := range sws.progress {
				if ok {
                    // 如下所示
					sws.watchStream.RequestProgress(id)
				}
				sws.progress[id] = true
			}
			sws.mu.Unlock()
func (ws *watchStream) RequestProgress(id WatchID) {
	ws.mu.Lock()
	w, ok := ws.watchers[id]
	ws.mu.Unlock()
	if !ok {
		return
	}
	ws.watchable.progress(w)
}

func (s *watchableStore) progress(w *watcher) {
	s.progressIfSync(map[WatchID]*watcher{w.id: w}, w.id)
}

func (s *watchableStore) progressIfSync(watchers map[WatchID]*watcher, responseWatchID WatchID) bool {
	s.mu.RLock()
	defer s.mu.RUnlock()


	for _, w := range watchers {
		if _, ok := s.synced.watchers[w]; !ok {
			return false
		}
	}


	for _, w := range watchers {
        // 发送最新的版本
		w.send(WatchResponse{WatchID: responseWatchID, Revision: s.rev()})
		return true
	}
	return true
}

第四种情况是表明流已关闭

		case <-sws.closec:
			return

5.3. recvLoop

recvLoop 主要负责接收 client 的 create/cancel watcher 请求,并对请求进行封装处理

func (sws *serverWatchStream) recvLoop() error {
	for {
		req, err := sws.gRPCStream.Recv()
        ...
		switch uv := req.RequestUnion.(type) {
		case *pb.WatchRequest_CreateRequest:
            ...
		case *pb.WatchRequest_CancelRequest:
			...
		case *pb.WatchRequest_ProgressRequest:
            ...
		default:
			// we probably should not shutdown the entire stream when
			// receive an invalid command.
			// so just do nothing instead.
			sws.lg.Sugar().Infof("invalid watch request type %T received in gRPC stream", uv)
			continue
		}
	}
}

第一种情况,创建请求

		case *pb.WatchRequest_CreateRequest:
            // 参数封装,权限校验
            ...
            
            // 创建watch,返回id
            // watchStream 每个grpc流请求唯一
			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 {
            // 发送到sendLoop的ctrlStream
			case sws.ctrlStream <- wr:
			case <-sws.closec:
				return nil
			}

watch 建立

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
	}

	if id == clientv3.AutoWatchID {
		for ws.watchers[ws.nextID] != nil {
			ws.nextID++
		}
		id = ws.nextID
		ws.nextID++
	} else if _, ok := ws.watchers[id]; ok {
		return -1, ErrWatcherDuplicateID
	}

    // 跟mvcc 连接起来
	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,
	}

	s.mu.Lock()
	s.revMu.RLock()
	synced := startRev > s.store.currentRev || startRev == 0
	if synced {
		wa.minRev = s.store.currentRev + 1
		if startRev > wa.minRev {
			wa.minRev = startRev
		}
		// 这里添加进mvcc的synced
		s.synced.add(wa)
	} else {
		slowWatcherGauge.Inc()
        // 这里添加进mvcc的usynced
		s.unsynced.add(wa)
	}
	s.revMu.RUnlock()
	s.mu.Unlock()

	watcherGauge.Inc()

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

第二种情况,取消请求

先调用 watchStream.Cancel() 把 watcher cancel 掉,然后从 serverWatchStream 的几个 map 中移除掉对应数据,并发送一个带 Canceled 标记的消息给 sendLoop 以同步watcherID列表

		case *pb.WatchRequest_CancelRequest:
			if uv.CancelRequest != nil {
				id := uv.CancelRequest.WatchId
				err := sws.watchStream.Cancel(mvcc.WatchID(id))
				if err == nil {
                    // 对应第一部分的ctrl stream
					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()
				}
			}
func (ws *watchStream) Cancel(id WatchID) error {
	ws.mu.Lock()
	cancel, ok := ws.cancels[id]
	w := ws.watchers[id]
	ok = ok && !ws.closed
	ws.mu.Unlock()

	if !ok {
		return ErrWatcherNotExist
	}
	cancel()

	ws.mu.Lock()
	if ww := ws.watchers[id]; ww == w {
		delete(ws.cancels, id)
		delete(ws.watchers, id)
	}
	ws.mu.Unlock()

	return nil
}

5.4. watcher

在etcd中,watcher可以分为三类:

  • Synced Watcher:监听的数据已同步完毕,等待新变更。适用于未指定或指定了未来版本号的watcher。
  • Unsynced Watcher:数据落后于最新变更,正在追赶。适用于指定了过去版本号的watcher。
  • Victim Watcher: 用于处理推送失败事件,需异步重试。

image.png 上述的watcher 实现,由下面代码可知,unsynced和synced 都是watcherGroup

type watchableStore struct {

    ...
	// victims are watcher batches that were blocked on the watch channel
	victims []watcherBatch
	victimc chan struct{}

	// contains all unsynced watchers that needs to sync with events that have happened
	unsynced watcherGroup

	// contains all synced watchers that are in sync with the progress of the store.
	// The key of the map is the key that the watcher watches on.
	synced watcherGroup

}

watcherGroup 如下所示:

单key -> watcher 使用map

监听 key 范围、key 前缀 -> watcher 使用区间树,区间树对范围查找是olog(n)

// watcherGroup is a collection of watchers organized by their ranges
type watcherGroup struct {
	// keyWatchers has the watchers that watch on a single key
	keyWatchers watcherSetByKey
	// ranges has the watchers that watch a range; it is sorted by interval
	ranges adt.IntervalTree
	// watchers is the set of all watchers
	watchers watcherSet
}

type watcherSetByKey map[string]watcherSet

添加watcher

// add puts a watcher in the group.
func (wg *watcherGroup) add(wa *watcher) {
    wg.watchers.add(wa)
    if wa.end == nil {
        wg.keyWatchers.add(wa)
        return
    }

    // interval already registered?
    ivl := adt.NewStringAffineInterval(string(wa.key), string(wa.end))
    if iv := wg.ranges.Find(ivl); iv != nil {
        iv.Val.(watcherSet).add(wa)
        return
    }

    // not registered, put in interval tree
    ws := make(watcherSet)
    ws.add(wa)
    wg.ranges.Insert(ivl, ws)
}

5.5. mvcc kv

下面的代码展示上一张图的两个循环是如何启动的

func NewServer(cfg config.ServerConfig) (srv *EtcdServer, err error) {
    ...
    srv = &EtcdServer{
        ...
	}
    // 将 mvcc 中的 KV 对象赋值给了 srv
    srv.kv = mvcc.New(srv.Logger(), srv.be, srv.lessor, mvccStoreConfig)
}


func New(lg *zap.Logger, b backend.Backend, le lease.Lessor, cfg StoreConfig) WatchableKV {
	return newWatchableStore(lg, b, le, cfg)
}

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)
    // 这里启动了两个 goroutine,分别是 syncWatchersLoop 和 syncVictimsLoop
    // 历史事件推送
	go s.syncWatchersLoop()
    // 异常场景推送
	go s.syncVictimsLoop()
	return s
}

而推送大致可以分为三类:

syncWatchersLoop:历史事件推送

syncVictimsLoop:异常场景重试

notify:最新事件推送

5.6. notify

notify 主要实现最新事件推送

image.png 当你创建完成 watcher 后,执行 put hello 修改操作时,如流程图所示,请求会经过 KVServer、Raft 模块后最终Apply 到状态机,在 MVCC 的 put 事务中,它会将本次修改的后的 mvccpb.KeyValue 保存到一个 changes 数组中。

如下面的代码所示,在 put 事务结束后,它会将 Key,Value 转换成 Event 事件,然后回调 watchableStore.notify 函数(流程 5)

func (tw *storeTxnWrite) put(key, value []byte, leaseID lease.LeaseID) {
	// 省略其他逻辑
	tw.s.kvindex.Put(key, idxRev)
	tw.changes = append(tw.changes, kv) // 将本次修改的后的 mvccpb.KeyValue 保存到一个 changes 数组中。
}

func (tw *watchableStoreTxnWrite) End() {
	changes := tw.Changes()
	if len(changes) == 0 {
		tw.TxnWrite.End()
		return
	}

	rev := tw.Rev() + 1
	evs := make([]mvccpb.Event, len(changes))
    // 将本次事务中的 cahnges 转换成 event
	for i, change := range changes {
		evs[i].Kv = &changes[i]
		if change.CreateRevision == 0 {
			evs[i].Type = mvccpb.DELETE
			evs[i].Kv.ModRevision = rev
		} else {
			evs[i].Type = mvccpb.PUT
		}
	}

	tw.s.mu.Lock()
	tw.s.notify(rev, evs) // 调用 notify 方法,通知 watchStream
	tw.TxnWrite.End()
	tw.s.mu.Unlock()
}

notify 会匹配出监听过此 key 并处于 synced watcherGroup 中的 watcher,同时事件中的版本号要大于等于 watcher 监听的最小版本号,才能将事件发送到此 watcher 的事件 channel 中。

notify 中如果发送失败后会将 watcher 添加到 victimGroup 中。这里的发送失败主要是由于 ch 阻塞,导致没发出去,也就是图中的事件堆积

// server/storage/mvcc/watchable_store_txn.go 434 行
func (s *watchableStore) notify(rev int64, evs []mvccpb.Event) {
	victim := make(watcherBatch)
    // event 事件进行批量化处理,根据watcher进行分类
    // 然后遍历,一个一个推送
    // 找到key 对应的watch
	for w, eb := range newWatcherBatch(&s.synced, evs) {
		if eb.revs != 1 {
			s.store.lg.Panic(
				"unexpected multiple revisions in watch notification",
				zap.Int("number-of-revisions", eb.revs),
			)
		}
		if w.send(WatchResponse{WatchID: w.id, Events: eb.evs, Revision: rev}) {
			pendingEventsGauge.Add(float64(len(eb.evs)))
		} else {
            // 如果推送失败了就加入到 victim 列表中。
			// move slow watcher to victims
			w.minRev = rev + 1
			w.victim = true
			victim[w] = eb
			s.synced.delete(w)
			slowWatcherGauge.Inc()
		}
	}
	s.addVictim(victim)
}




// 批量找到key对应的watcer
func newWatcherBatch(wg *watcherGroup, evs []mvccpb.Event) watcherBatch {
	if len(wg.watchers) == 0 {
		return nil
	}

	wb := make(watcherBatch)
	for _, ev := range evs {
        // 通过key 查找watcher
		for w := range wg.watcherSetByKey(string(ev.Kv.Key)) {
			if ev.Kv.ModRevision >= w.minRev {
				// don't double notify
				wb.add(w, ev)
			}
		}
	}
	return wb
}
func (w *watcher) send(wr WatchResponse) bool {
	progressEvent := len(wr.Events) == 0
	// 首先是根据 filter 方法,过滤掉不关心的 even
	if len(w.fcs) != 0 {
		ne := make([]mvccpb.Event, 0, len(wr.Events))
		for i := range wr.Events {
			filtered := false
			for _, filter := range w.fcs {
				if filter(wr.Events[i]) {
					filtered = true
					break
				}
			}
			if !filtered {
				ne = append(ne, wr.Events[i])
			}
		}
		wr.Events = ne
	}

	// if all events are filtered out, we should send nothing.
	if !progressEvent && len(wr.Events) == 0 {
		return true
	}
	select {
	//然后发送出去
	case w.ch <- wr:
		return true
	default:
		return false
	}
}

实际上这里的 watcher 就是前面 recvLoop 中收到 create 请求时创建的 watcher,具体如下:

func (ws *watchStream) Watch(id WatchID, key, end []byte, startRev int64, fcs ...FilterFunc) (WatchID, error) {
    // 上文中的 watcher 就是这里创建的,而 watcher.ch 实际就是 watchStream.ch
	w, c := ws.watchable.watch(key, end, startRev, id, ws.ch, fcs...)
}

因此实际上所有的 event 被发送到了 watchStream.ch

然后 sendLoop 不断从 watchStream.ch 中取出 event 并发送给 client

5.7. syncWatchersLoop

syncWatchersLoop 主要负责历史事件推送

notify 只会处理在 synced watcherGroup 中的 watcher ,如果不在则无法立即接收到 event

notify 只用于推送最新事件,如果 watcher 还有旧 event 没有推送,而直接推送最新 event 势必无法保证 event 的先后顺序,因此 notify 中只处理了 synced watcherGroup 中的 watcher

为了保证效率,notify 也没有从 blotdb 中把对应 watcher 的所有 event 都查询出来再进行推送

而 syncWatchersLoop 就是负责处理 unsynced 中的 watcher,将这些 watcher 的历史 event 全部推送给 sendLoop,然后将其移动到 synced watcherGroup,以便下次 notify 时就能直接处理

// server/storage/mvcc/watchable_store.go 211行
func (s *watchableStore) syncWatchersLoop() {
	defer s.wg.Done()

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

		unsyncedWatchers := 0
		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
		}
	}
}

每过 100ms 就会对 unsynced watcher 进行一次同步。

具体同步逻辑在s.syncWatchers()方法中:

1)从 unsynced watcher group 中选取一组 watcher

2)遍历这组 watcher 得到 minimum revision,并移除掉已经被压缩的 watcher

3)根据第二步中查询到的 minimum revision 查询 键值对并发送这些事件给 watchers

4)最后对这组 watcher 进行判断,若同步完成了就将其从 unsynced watcher group 中移动到 synced watcher group 中

// server/storage/mvcc/watchable_store.go 326行
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)

	tx := s.store.b.ReadTx()
	tx.RLock()
	revs, vs := tx.UnsafeRange(schema.Key, minBytes, maxBytes, 0)
	evs := kvsToEvents(s.store.lg, wg, revs, vs)

	tx.RUnlock()

	victims := make(watcherBatch)
	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
		}

		if w.send(WatchResponse{WatchID: w.id, Events: eb.evs, Revision: curRev}) {
			pendingEventsGauge.Add(float64(len(eb.evs)))
		} else {
			w.victim = true
		}

		if w.victim {
			victims[w] = eb
		} else {
			if eb.moreRev != 0 {
				// stay unsynced; more to read
				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()
}

5.8. syncVictimsLoop

syncVictimsLoop 主要负责异常场景重试。

每过 10ms 或者收到通知信息就进行一次循环,尝试处理因消息堆积发送异常而加入到 victimc 列表中的 watcher。 尝试再次把这些 watcher 相关 event 推送出去以清空 victimc 列表

// server/storage/mvcc/watchable_store.go 243行
func (s *watchableStore) syncVictimsLoop() {
	defer s.wg.Done()

	for {
        // 10ms 调用一次
		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 逻辑比较简单,就是一个遍历尝试发送,然后把发送失败的再添加 victims。

推送成功后根据 revision 进行判断,该将 watcher 添加到哪个 group:

  • 如果追上了最新 revision 就添加到 syncedGroup
  • 没追上就添加到 unsyncedGroup
// server/storage/mvcc/watchable_store.go 269行
func (s *watchableStore) moveVictims() (moved int) {
	s.mu.Lock()
    // 这里先把原victims用临时变量存一下
	victims := s.victims
    // 然后清空原victims
	s.victims = nil
	s.mu.Unlock()

	var newVictim watcherBatch
    // 直接就是一个遍历发送
	for _, wb := range victims {
		// try to send responses again
		for w, eb := range wb {
			// watcher has observed the store up to, but not including, w.minRev
			rev := w.minRev - 1
			if w.send(WatchResponse{WatchID: w.id, Events: eb.evs, Revision: rev}) {
				pendingEventsGauge.Add(float64(len(eb.evs)))
			} else {
                // 还是发送失败就临时加入到 newVictim 列表
				if newVictim == nil {
					newVictim = make(watcherBatch)
				}
				newVictim[w] = eb
				continue
			}
			moved++
		}

		// assign completed victim watchers to unsync/sync
		s.mu.Lock()
		s.store.revMu.RLock()
		curRev := s.store.currentRev
		for w, eb := range wb {
			if newVictim != nil && newVictim[w] != nil {
				// couldn't send watch response; stays victim
				continue
			}
			w.victim = false
			if eb.moreRev != 0 {
				w.minRev = eb.moreRev
			}
			// 根据 reversion 进行判断,如果追上了就添加到 syncedGroup 
            // 没追上就添加到 unsyncedGroup
			if w.minRev <= curRev {
				s.unsynced.add(w)
			} else {
				slowWatcherGauge.Dec()
				s.synced.add(w)
			}
		}
		s.store.revMu.RUnlock()
		s.mu.Unlock()
	}
    // 最后再把本次发送失败的追加到 s.victims 列表中
	if len(newVictim) > 0 {
		s.mu.Lock()
		s.victims = append(s.victims, newVictim)
		s.mu.Unlock()
	}

	return moved
}