1. MVCC
MVCC(Multi-Version Concurrency Control)多版本并发控制:一种乐观锁机制,用于实现分布式系统中的并发控制。
(1)几种版本号
-
集群的版本号
-
Revision:逻辑时间戳,全局单调递增,任何 key 的增删改都会使其自增
-
-
key的版本号
-
CreateRevision:创建 key 时集群的 Revision, 直到删除前都不变
-
ModRevision:修改 key 时集群的 Revision, key 更新时会自增
-
Version:初始为1,更新时自增
-
查看revision:
etcdctl get "" --prefix -w=json
{
"header": {
"cluster_id": 14841639068965178418,
"member_id": 10276657743932975437,
"revision": 3,
"raft_term": 2
},
"kvs": [
"key": "YQ==",
"create_revision": 2,
"mod_revision": 3,
"version": 2,
"value": "MQ=="
}],
"count": 1
}
-
事务 Txn 语句:If()Then()Else()
-
在 If 语句中,将 createRevision/modRevision/version/value 值作为条件
-
CreateRevision:watch 某key时,想要从历史记录开始watch
-
ModRevision:watch 某key时,想要从最新一条开始就watch
-
Revision:watch 某个前缀必须使用Revision。如果要watch当前前缀后续的变化,则应该从当前集群的 Revision+1 版本开始watch
(4)历史数据压缩机制:( 压缩:如何回收旧版本数据?)
对历史数据进行管理(不会影响最新数据),如保留数据的天数或历史版本的数量,是可配的。
压缩方式:
- 自动压缩
- 周期性压缩:根据时间
- 版本号压缩:根据revision
- 手动压缩:compact命令
2. etcd watch用法
func Test_client(t *testing.T) {
// 1 connect
cli := connect(t)
defer cli.Close()
// 2 确定watch的版本
rev := getCurrentRevision(t, cli)
t.Log("current rev", rev)
// 3 watch
go func() {
wchan := cli.Watch(
clientv3.WithRequireLeader(context.Background()),
"a",
clientv3.WithRev(rev),
)
t.Log("start watching from", rev)
for wresp := range wchan {
for _, ev := range wresp.Events {
t.Logf("Watch: %s, %q: %q", ev.Type, ev.Kv.Key, ev.Kv.Value)
}
}
}()
// 4 write
time.Sleep(time.Second)
_, _ = cli.Put(context.Background(), "a", fmt.Sprint(rand.Intn(10000)))
time.Sleep(time.Second)
_, _ = cli.Delete(context.Background(), "a")
time.Sleep(time.Second)
}
func getCurrentRevision(t *testing.T, cli *clientv3.Client) int64 {
resp, err := cli.Get(context.Background(), "test")
if err != nil {
t.Fatal(err)
}
return resp.Header.Revision
}
func connect(t *testing.T) *clientv3.Client {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{ADDR},
Username: USER,
Password: PASSWORD,
DialTimeout: time.Second * 2,
})
if err != nil {
t.Fatal(err)
}
return cli
}
3. 关于数据更新停滞问题:WithRequireLeader
问题:etcd集群可能由于故障导致少数几个节点和多数派的网络分区,那么这部分少数派的状态数据可能长时间得不到更新,此时client若请求到少数派节点上,将会取到落后的数据。
解决:WithRequireLeader方法,指定client与etcd server建立watch长连接的节点必须处在和leader保持通信的多数派当中,这样就可以规避因网络分区导致的数据更新停滞问题。
实际是在ctx中添加了 hasleader=true 的kv, 如果建立连接的节点和header失去通信,会报错ErrNoLeader。
// 用法
wchan := cli.Watch(clientv3.WithRequireLeader(context.Background()), "a", clientv3.WithPrefix())
// etcd源码
func WithRequireLeader(ctx context.Context) context.Context {
md, ok := metadata.FromOutgoingContext(ctx)
if !ok { // no outgoing metadata ctx key, create one
md = metadata.Pairs(rpctypes.MetadataRequireLeaderKey, rpctypes.MetadataHasLeader)
return metadata.NewOutgoingContext(ctx, md)
}
copied := md.Copy() // avoid racey updates
// overwrite/add 'hasleader' key/value
copied.Set(rpctypes.MetadataRequireLeaderKey, rpctypes.MetadataHasLeader)
return metadata.NewOutgoingContext(ctx, copied)
}
4. 读源码
4.1 watch 模块
应用层与存储层两部分。
(1)serverWatchStream 应用层
-
启动一个异步协程recvLoop() 用于接收客户端的读操作
-
启动一个异步协程sendLoop() 用于将相应返回给客户端
-
与存储层 watchableStore通过channel通信传递数据
type serverWatchStream struct {
// 存储,其实现是watchableStore
watchable mvcc.WatchableKV
// grpc长连接入口
gRPCStream pb.Watch_WatchServer
// 和底层watchableStore通信的stream
watchStream mvcc.WatchStream
// 读协程recvLoop通过这个channel将同步请求的响应结果推送给写协程sendLoop,并完成对etcd客户端的响应
ctrlStream chan *pb.WatchResponse
}
(2)watchableStore 存储层
是基于内存存储的。数据存在 synced group、unsynced group、victims中。
type watchableStore struct {
// etcd 存储模块
*store
// 当 watchableStore 通过 watcherStream 的 channel 向上层 serverWatchStream 发送回调事件而发现 channel 空间不足时,会将这部分 watcher 添加到 victims 当中
victims []watcherBatch
// 假如某些 watcher 监听数据还有一些历史版本的变更事件需要同步时,会将其存放在此处
unsynced watcherGroup
// 当 watcher 监听数据的历史变更事件已经回调完成,下次发生变更时可以直接进行回调的话,会将其存放在此处
synced watcherGroup
}
synced group:
存储无需回溯历史变更记录的watcher监听器,当有新数据变更事件发生,立即发起watch回调。
unsynced group:
存储需要回溯历史变更记录的watcher监听器,当有新数据变更事件发生,无法立即发起回调的,而是由异步任务syncWatchersLoop处理回调。如果创建的 watcher 指定版本号小于 etcd server 当前最新版本号,就会保存到 unsynced watcherGroup 中。
victims:
用于暂存一部分正在pending的watch回调事件,当在通过watchStream发往上层途中channel 容量不足,为避免notify协程陷入阻塞,会将这部分变更事件追加到victims列表中,列表容量无上限。
4.2 异常场景重试机制
当channel buffer满,会将此 watcher 从 synced 中删除,保存到 victims 中,然后通过异步机制重试,处理victims。
由 WatchableStore 模块的 goroutine syncVictimsLoop,负责 victims 堆积的事件推送:
-
遍历 victims,尝试将堆积的事件再次 send 到 channel 中
-
若推送失败,则再次加入到 victims 中等待下次重试
-
若推送成功
-
若 watcher 监听的最小版本号 (minRev) 小于等于 server 当前版本号 (currentRev),说明可能还有历史事件未推送,加入到 unsynced group 中
-
若 watcher 的最小版本号大于 server 当前版本号,则加入到 synced group 中,等待新的事件
4.3 历史事件推送机制
当watch指定了一个比当前etcd revision更小的版本时,若只监听新的变化,是无法watch到历史数据的。
因此 WatchableKV 模块的 goroutine syncWatchersLoop 异步任务,负责 unsynced 中的历史事件推送:
-
遍历处于 unsynced 的每个 watcher,为优化性能,会选择一批 unsynced 批量同步,找出这一批 unsynced 中监听的最小版本号
-
匹配出需要发送的 watcher,将事件 send 给对应的 watcher 事件接收的 channel
-
发送完成后,将 watcher 从 unsynced 中移除,添加到 synced 中,等待新的事件
-
若 watcher 监听的版本号已经小于当前 etcd 压缩的版本号,历史变更数据可能已丢失,会返回 ErrCompacted
4.4 源码流程梳理
![](https://i-blog.csdnimg.cn/blog_migrate/00612417601cac48dbf0ac20008cb692.png)
![](https://i-blog.csdnimg.cn/blog_migrate/70baba4e55aff9e51ff0254b52f067fb.png)
![](https://i-blog.csdnimg.cn/blog_migrate/bdc0f617febc8ceb5bf481bf5e9c12d0.png)