【etcd】2.mvcc与etcd watch

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
}
(2)事务的保证:
  • 事务 Txn 语句:If()Then()Else()
  • 在 If 语句中,将 createRevision/modRevision/version/value 值作为条件
(3)关于 watch 时用哪个版本号
  • 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 源码流程梳理

(1)创建watcher
(2)回调
(3)watchableStore存储层的定时数据刷新
参考:
  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值