本文主要分析了 etcd v3 为什么选择了 MVCC,以及 etcd v3 中的 MVCC 大致实现原理。

原文作者: 意琦行

原文链接: etcd教程(六)—etcd多版本并发控制 | 指月小筑|意琦行的个人博客

1. 为什么选择MVCC

etcd v2 是一个内存数据库,整个数据库拥有一个Stop-the-World的大锁,通过锁机制来解决并发带来的数据竞争。

但是存在并发性能问题

  • 锁的粒度不好控制,每次都会锁整个数据库

  • 写锁和读锁相互阻塞。

  • 前面的事务会阻塞后面的事务,对并发性能影响很大。

同时在高并发环境下还存在另一个严重的问题:

  • watch 机制可靠性问题:etcd 中的 watch 机制会依赖旧数据,v2 版本基于滑动窗口实现的 watch 机制,只能保留最近的 1000 条历史事件版本,当 etcd server 写请求较多、网络波动时等场景,很容易出现事件丢失问题,进而又触发 client 数据全量拉取,产生大量 expensive request,甚至导致 etcd 雪崩。

熟悉 Kubernetes 的朋友肯定知道,Kubernetes 使用 etcd 做存储,因此 etcd 的问题对 Kubernetes 有很直观的影响,具体如下:

  • etcd 并发性能问题导致 Kubernetes 集群规模受限。
  • watch 机制可靠性问题直接影响到 Kubernetes controller 的正常运行。

在 Kubernetes 中,各种各样的控制器实现了 Deployment、StatefulSet、Job 等功能强大的 Workload。控制器的核心思想是监听、比较资源实际状态与期望状态是否一致,若不一致则进行协调工作,使其最终一致。而这些特性的实现都严重依赖 etcd 的 watch 机制。

而 etcd 背后的公司 CoreOS 也是 Kubernetes 容器生态圈的核心成员之一,此时的 Kubernetes 和 Docker 公司还处于一个激烈的对抗之中,因此,此时的 etcd 迫切的需要解决以上的两个问题。

那么 etcd v3 为什么要选择 MVCC 呢?

解决并发问题的方法有很多,而MVCC 在解决并发问题的同时,还能通多存储多版本数据来解决watch 机制可靠性问题

因此 etcd v3 版本果断选择了基于 MVCC 来实现多版本并发控制。

于是v3则采用了MVCC,以一种优雅的方式解决了锁带来的问题。

  • 执行写操作或删除操作时不会再原数据上修改而是创建一个新版本。

  • 这样并发的读取操作仍然可以读取老版本的数据,写操作也可以同时进行。

这个模式的好处在于读操作不再阻塞,事实上根本就不需要锁。

客户端读key的时候指定一个版本号,服务端保证返回比这个版本号更新的数据,但不保证返回最新的数据。

MVCC能最大化地实现高效地读写并发,尤其是高效地读,非常适合读多写少的场景。

2. MVCC 初体验

如下面的命令所示,第一次 key hello 更新完后,我们通过 get 命令获取下它的 key-value 详细信息。正如你所看到的,除了 key、value 信息,还有各类版本号。

这里我们重点关注 mod_revision,它表示 key 最后一次修改时的 etcd 版本号。

当我们再次更新 key hello 为 world2 后,然后通过查询时指定 key 第一次更新后的版本号,你会发现我们查询到了第一次更新的值,甚至我们执行删除 key hello 后,依然可以获得到这个值。那么 etcd 是如何实现的呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 指定使用 v3 版本API
$ export ETCDCTL_API=3
# 更新key hello为world1
$ etcdctl put hello world1
OK
# 通过指定输出模式为json,查看key hello更新后的详细信息
$ etcdctl get hello -w=json
{
    "kvs":[
        {
            "key":"aGVsbG8=",
            "create_revision":2,
            "mod_revision":2,
            "version":1,
            "value":"d29ybGQx"
        }
    ],
    "count":1
}
# 再次修改key hello为world2
$ etcdctl put hello world2
OK
# 确认修改成功,最新值为wolrd2
$ etcdctl get hello
hello
world2
# 指定查询版本号,获得了hello上一次修改的值
$ etcdctl get hello --rev=2
hello
world1
# 删除key hello
$ etcdctl del  hello
1
# 删除后指定查询版本号3,获得了hello删除前的值
$ etcdctl get hello --rev=3
hello
world2