ETCD 源码学习--Watch(server)

在 ETCD 源码学习过程,不会讲解太多的源码知识,只讲解相关的实现机制,需要关注源码细节的朋友可以自行根据文章中的提示,找到相关源码进行学习。

主要文件

/etcdserver/api/v3rpc/watch.go    watch 服务端实现

/mvcc/watcher.go  主要封装 watchStream 的实现

/mvcc/watchable_store.go  watch 版本的 KV 存储实现

/mvcc/watchable_store_txn.go  主要实现事务提交后 End() 函数的处理

/mvcc/watcher_group.go

主要流程

主要数据结构

//文件:/etcdserver/api/v3rpc/watch.go
type watchServer struct {
	...
	watchable mvcc.WatchableKV // 键值存储
	....
}

type serverWatchStream struct {
	...
	watchable mvcc.WatchableKV //kv 存储
	...
	gRPCStream  pb.Watch_WatchServer   //与客户端进行连接的 Stream
	watchStream mvcc.WatchStream       //key 变动的消息管道
	ctrlStream  chan *pb.WatchResponse //响应客户端请求的消息管道
	...
	progress map[mvcc.WatchID]bool //该类型的 watch,服务端会定时发送类似心跳消息
	prevKV map[mvcc.WatchID]bool //该类型表明,对于/a/b 这样的监听范围, 如果 b 变化了, 前缀/a也需要通知
	fragment map[mvcc.WatchID]bool //该类型表明,传输数据量大于阈值,需要拆分发送
} 
//文件:/mvcc/watcher.go 
//响应结构体
type WatchResponse struct {
	WatchID WatchID
	Revision int64 //当前 watchResponse 实例创建时对应的 revision 值
	CompactRevision int64 //压缩操作对应的 revison
}

type watchStream struct {
	watchable watchable          //用来记录关联的 watchableStore
	ch        chan WatchResponse // event 事件写入通道
	...
	cancels  map[WatchID]cancelFunc
	watchers map[WatchID]*watcher //用来记录唯一标识与 watcher 的实例的关系
}
//文件:/mvcc/watcher_group.go 
type eventBatch struct {
	//其中记录的 Event 实例是按照 revision 排序的
	evs []mvccpb.Event
	//记录当前 eventBatch 中,有多少个来自不同的 main revison,
	revs int
	//当前 eventBatch 记录的 Event 个数达到上限之后,后续 Event 实例无法加入该 eventBatch 中
	//该字段记录了无法加入该 eventBatch 实例的第一个main revision
	moreRev int64
}

type watcherBatch map[*watcher]*eventBatch
type watcherSet map[*watcher]struct{}
type watcherSetByKey map[string]watcherSet

type watcherGroup struct { 
	keyWatchers watcherSetByKey //记录监听单个 Key 的 watch 实例
	ranges adt.IntervalTree //记录进行范围监听的 watcher 实例。
	watchers watcherSet //记录当前 wathcer 的全部实例
}
//文件:/mvcc/watchable_store.go
type watchableStore struct {
	*store
	mu sync.RWMutex
	victims []watcherBatch //当ch被阻塞时,对应 watcherBatch 实例会暂时记录到这个字段
	victimc chan struct{}  //当有新的 watcherBatch 实例添加到 victims 字段时,会向该通道发送消息
	//未同步的 watcher
	unsynced watcherGroup
	//已完成同步的 watcher
	synced watcherGroup
	stopc chan struct{}
	wg sync.WaitGroup
}

type watcher struct {

	key []byte //监听起始值
	end []byte // 监听终止值,  key 和 end 共同组成一个键值范围
	victim bool //是否被阻塞
	compacted bool //是否压缩
	...
	minRev int64 //最小的 revision main
	id     WatchID
	...
	ch chan<- WatchResponse
}

主要函数

1.函数:Watch
 (1)创建 serverWatchStream 实例。
 (2)启动 serverWatchStream.sendLoop 后台 goroutine。
 (3)启动 serverWatchStream.recvLoop 后台 goroutine。
 (4)等待关闭 创建 serverWatchStream。

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 {
			...
		}
	}()
	// ... 等待关闭
	return err
}

2.函数:recvLoop
 (1)接收客户端请求。
 (2)根据请求类型处理。

func (sws *serverWatchStream) recvLoop() error {
	for {
		req, err := sws.gRPCStream.Recv()
		...
		switch uv := req.RequestUnion.(type) {
		case *pb.WatchRequest_CreateRequest: //创建
			if uv.CreateRequest == nil {
				break
			}

			...

			if !sws.isWatchPermitted(creq) { //权限判断
				...
			}

			filters := FiltersFromRequest(creq)
			//获取版本号
			wsrev := sws.watchStream.Rev()
			rev := creq.StartRevision
			if rev == 0 {
				rev = wsrev + 1
			}
			id, err := sws.watchStream.Watch(mvcc.WatchID(creq.WatchId), creq.Key, creq.RangeEnd, rev, filters...)
			//... 根据监听结果做一些出来

		case *pb.WatchRequest_CancelRequest:
			//主要是删除相关数据
		case *pb.WatchRequest_ProgressRequest:
			//发送一个 Progress Response 给客户端
		default:
			continue
		}
	}
}

3.函数:sendLoop
(1)处理 watchStream 的事件(key 变动消息)。
(2)处理 ctrlStream 的消息(客户端请求,返回响应)。
(3)定时发送 RequestProgress 类似心跳包。

func (sws *serverWatchStream) sendLoop() {
	ids := make(map[mvcc.WatchID]struct{})
	pending := make(map[mvcc.WatchID][]*pb.WatchResponse)
	//发送 Progress 的间隔时间
	interval := GetProgressReportInterval()
	progressTicker := time.NewTicker(interval)
	//... defer 处理
	for {
		select {
		/*
			watchStream 监听的 key 有相关修改
			该消息会通过 watchableStore.notify() 发送
			即在每个 key 进行事务提交的时候 默认执行 watchable_store_txn.End()
			End 调用 notify 进行通知
		*/
		case wresp, ok := <-sws.watchStream.Chan(): 
			...
			evs := wresp.Events
			events := make([]*mvccpb.Event, len(evs))
	
			for i := range evs {
				events[i] = &evs[i]
				if needPrevKV { //如果设置了 PrevKv,
					...
				}
			}
			//构建响应
			wr := &pb.WatchResponse{
				...
			}
			...
			var serr error
			//是否需要拆分发送
			if !fragmented && !ok {
				serr = sws.gRPCStream.Send(wr)
			} else {
				serr = sendFragments(wr, sws.maxRequestBytes, sws.gRPCStream.Send)
			}
			...

		case c, ok := <-sws.ctrlStream:
			...

		case <-progressTicker.C:
			...

		case <-sws.closec:
			return
		}
	}
}

4.watchStream.Watch

func (ws *watchStream) Watch(id WatchID, key, end []byte, startRev int64, fcs ...FilterFunc) (WatchID, error) {
	//防止  key>end 的情况
	if len(end) != 0 && bytes.Compare(key, end) != -1 {
		return -1, ErrEmptyWatcherRange
	}

	//生成 id(WatchId)
	w, c := ws.watchable.watch(key, end, startRev, id, ws.ch, fcs...)
	ws.cancels[id] = c
	ws.watchers[id] = w
	return id, nil
}

5.watchableStore.watch

func (s *watchableStore) watch(key, end []byte, startRev int64, id WatchID, ch chan<- WatchResponse, fcs ...FilterFunc) (*watcher, cancelFunc) {
	wa := &watcher{
		...
	}

	//比较 startRev 和 currentRev,决定添加的 watcher 实例是否已经同步
	synced := startRev > s.store.currentRev || startRev == 0
	if synced {
		wa.minRev = s.store.currentRev + 1
		if startRev > wa.minRev { //设置待添加 watcher 的 minRev 字段值
			wa.minRev = startRev
		}
	}
	if synced { //已同步完成
		s.synced.add(wa)
	} else { //未同步完成
		slowWatcherGauge.Inc()
		s.unsynced.add(wa)
	}
	...
}

在实例化 watchableStore 是会开启两个 goroutine, syncWatchersLoop 和 syncVictimsLoop  用于处理 unsynced 消息和 victims。

6.syncWatchersLoop

func (s *watchableStore) syncWatchersLoop() {
	defer s.wg.Done()

	for {
		...
		if lastUnsyncedWatchers > 0 {//如果 unsynced 中存在数据,进行同步
			//存在需要同步的 watcher
			unsyncedWatchers = s.syncWatchers()
		}
		...
		select {
		case <-time.After(waitDuration):
		case <-s.stopc:
			return
		}
	}
}

7.syncWatchers
 (1)从 unsynced 中选择一批 watcher 进行实例,并获取这批实例中最小的版本号 minRev。
 (2)从 KV 中查找 minRev - currRev 之间所有的数据。
 (3)将这些数据转换为 events。
 (4)将这些 events 广播给所有 watcher。

func (s *watchableStore) syncWatchers() int {
	...
	curRev := s.store.currentRev // 当前最大值
	compactionRev := s.store.compactMainRev
	//选择一批 watcher 实例,进行同步,并找到这批 watch 最小的 rev
	wg, minRev := s.unsynced.choose(maxWatchersPerSync, curRev, compactionRev)
	minBytes, maxBytes := newRevBytes(), newRevBytes()
	
	//... 查找minRev -  currentRev 所有的键值
	revs, vs := tx.UnsafeRange(keyBucketName, minBytes, maxBytes, 0)
	...
	var evs []mvccpb.Event
	//将 revs 转换为 Event 事件
	evs = kvsToEvents(s.store.lg, wg, revs, vs)

	var victims watcherBatch
	wb := newWatcherBatch(wg, evs)
	for w := range wg.watchers { //广播 watcher
		w.minRev = curRev + 1 //更新 watcher 中的 minRev 字段,这样就不会被同一个修改操作触发两次
		//该 watcher 实例监听的键值没有更新操作(minRev -- currentRev 之间)
		//所以没有被触发,故同步完毕,
		eb, ok := wb[w]
		if !ok {
			// bring un-notified watcher to synced
			s.synced.add(w)
			s.unsynced.delete(w)
			continue
		}
		//更新操作太多,无法全部放入 eventbatch 中,这里 minRev 设置成 moreRev,
		//下次处理 unsynced 时,会将其后的更新操作查询出来继续处理
		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 { //如果堵塞
			if victims == nil {
				victims = make(watcherBatch)
			}
			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)
	}
	//将 victims 变量中记录 watcherBatch 添加到 watchable.victims 中,等待处理
	s.addVictim(victims)
	...
}

9.syncVictimsLoop

func (s *watchableStore) syncVictimsLoop() {
	defer s.wg.Done()

	for {
		for s.moveVictims() != 0 { //循环处理 victims 中缓存的 watcherBatch 中
			// try to update all victim watchers
		}
		...
		var tickc <-chan time.Time
		select {
		case <-tickc:
		case <-s.victimc:
		case <-s.stopc:
			return
		}
	}
}

10.moveVictims
 (1)将 victims 中的数据尝试发送出去。
 (2)如果发送仍然阻塞,需要重新放回 victims。
 (3)判断这些发送完成的版本号是否小于当前版本号,如果是说明者个过程中有数据更新,还没有同步完成,需要添加到 unsynced 中,等待下次同步。如果不是,说明已经同步完成。

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
			if w.send(WatchResponse{WatchID: w.id, Events: eb.evs, Revision: rev}) {
				pendingEventsGauge.Add(float64(len(eb.evs)))
			} else { //如果依然阻塞,重新放回去
				if newVictim == nil {
					newVictim = make(watcherBatch)
				}
				newVictim[w] = eb
				continue
			}
		}
		// assign completed victim watchers to unsync/sync
		s.mu.Lock()
		s.store.revMu.RLock()
		curRev := s.store.currentRev
		for w, eb := range wb { //变量 wb
			...
			if w.minRev <= curRev {
				s.unsynced.add(w) //还有未同步的事件
			} else { //同步完成
				slowWatcherGauge.Dec()
				s.synced.add(w)
			}
		}
		...
	}
	....
	return moved
}

总结

1.当 Watcher 与 服务器建立之后,首先需要进行版本的同步,主要通过 watchableStore 中的 syncVictimsLoop 和 syncWatchersLoop 这两个后台g oroutine 实现。
2.WatchableStoreTxnWrite 重新实现 KV 存储的事务提交回调函数 End(), End 中会调用 watchableStore .notify() 通知 key 的变化。Notify 最终将消息推送的 watchStream.ch 中等待处理。
3.serverWatchStream 会开启两个后台gorotine,recvLoop 和 sendLoop 分别用于接收请求和发送服务消息给客户端。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值