etcd原理剖析一

为什么Kubernetes使用etcd?

首先我们来看服务高可用以及数据一致性。单副本存在单点故障,而多副本又引入数据一致性问题。

为了解决数据一致性问题,需要引入一个共识算法。例如Raft等。etcd选择了Raft,它将复杂的一致性问题分解成了Leader选举,日志同步,安全性三个相对独立的子问题,只要集群一半以上的节点存货就可以提供服务,具备良好的可用性。

其次再来看数据模型和API。数据模型参考了ZooKeeper,使用的是基于目录的层次结构。API相比Zookeepre来说,使用了简单,易用的REST API,提供了常用的Get/Set/Delete/Watch等API,实现对Key-Value数据的查询,更新,删除,监听等操作。

Key-value存储引擎上,ZooKeeper采用的是Concurrent HashMap,而etcd使用的是简单内存树,它的节点数据结构精简后如下。这是一个典型的低容量设计,数据全部放在内存,无虚考虑数据分片,只能保存key的最新版本,简单容易实现。

在这里插入图片描述

type node struct {
    Path string // 节点路径
    Parent *node // 关联父亲节点
    Value string // key的value值
    ExpireTime time.Time // 过期时间
    Chilren map[string]*node // 此节点的孩子节点
}

最后来看一下可维护性。Raft算法提供了成员变更算法,可基于此实现成员在线,安全变更,同时协调服务使用Go语言编写,无依赖,部署简单。

在这里插入图片描述

下面我从构建分布式系统的核心要素角度,总结了etcd v2的核心技术点。无论是NoSQL存储还是SQL存储,文档存储,其实大家要解决的问题都是类似的,基本就是图中总结的数据模型复制共识算法API事务一致性成员故障检测等方面。

在这里插入图片描述

但是etcdv2也很有局限性。

首先是功能局限性,etcd v2不支持范围和分页查询,不支持多key事务。

  • etcd v2不支持范围查询和分页。分页对于数据较多的场景是必不可少的。在Kubernetes中,在集群规模增大之后,Pod,Event等资源可能会数千个以上,但是etcd v2不支持分页,不支持范围查询,expensive request会导致严重的性能乃至雪崩问题。
  • etcd v2不支持多key事务。在实际转账等业务场景中,往往我们需要在一个事务中同时更新多个key。

然后是Watch机制可靠性问题。k8s严重依赖etcd Watch机制,然后etcd v2是内存型,不支持保存key历史版本数据库,只在内存中使用滑动窗口保存了最近的1000条变更时间,当etcd server写请求较多,网络波动时等场景,很容易出现事件丢失问题,进而又触发client数据全量拉取,产生大量expensive request,甚至导致etcd雪崩。

其次是性能瓶颈问题,etcd v2早期使用了见到那,易调试的HTTP/1.x API,但是随着Kubernetes支撑的集群规模越来越大,HTTP/1.x协议的瓶颈逐渐暴露了出来。比如集群规模大的时候由于HTTP/1.x没有压缩机制,批量拉取较多Pod的时候容易导致APIServer和etcd出现CPU高负载,OOM(out of memory),丢包等问题。

另一方面,etcd v2 client会通过HTTP长连接轮询Watch事件,当watcher较多的时候,因为HTTP/1.x不支持多路复用,会创建大量的连接,消耗server端过多的socket和内存资源。

什么是HTTP多路复用?

HTTP多路复用和HTTP/2协议中的一个特性,它允许在一个TCP连接上同时传输多个HTTP请求和相应,从而提高了Web应用程序的性能和效率。

传统的HTTP/1.x协议中,每个HTTP请求都需要建立一个TCP连接,这会导致连接的建立和释放开销比较大,同时也会浪费网络带宽和资源。而HTTP/2协议中,请求和响应被分割成很多帧(Frame),每个帧都有一个唯一的标识符,这样多个请求和响应就可以在一个TCP连接上通过多路复用同时进行,而不需要建立多个TCP连接。

同时etcd v2支持为每个key设置TTL过期事件,client为了防止Key的TTL过期后被删除,需要周期性的刷新key的TTL。实际业务中很有可能若干 key 拥有相同的 TTL,可是在 etcd v2 中,即使大量 key TTL 一样,你也需要分别为每个 key 发起续期操作,当 key 较多的时候,这会显著增加集群负载、导致集群性能显著下降。

最后是内存开销问题。etcd v2 在内存维护了一颗树来保存所有节点 key 及 value。在数据量场景略大的场景,如配置项较多、存储了大量 Kubernetes Events, 它会导致较大的内存开销,同时 etcd 需要定时把全量内存树持久化到磁盘。这会消耗大量的 CPU 和磁盘I/O 资源,对系统的稳定性造成一定影响。

而etcd v3对这些全部做出了改善,完美的适配了k8s所有的需求。

  • 在内存开销,Watch事件可靠性,功能局限上,etcd引入了B-tree,boltdb实现了一个MVCC数据库,数据模型从层次性目录结构改成了扁平的key-value,提供稳定可靠的事件通知,实现了事务,支持多key原子更新,同时基于boltdb的持久化存储,显著降低了etcd的内存占用。
  • 性能上,etcd v3使用了gRPC API,使用protobuf定义消息,消息编解码性能相比JSON超过2倍以上,并通过HTTP/2.0多路复用机制,减少了大量watcher等场景下的连接数。
  • 其次使用 Lease 优化 TTL 机制,每个 Lease 具有一个 TTL,相同的 TTL 的 key 关联一个Lease,Lease 过期的时候自动删除相关联的所有 key,不再需要为每个 key 单独续期。

基础架构:etcd一个读请求是如何执行的?

在这里插入图片描述

  • Client层:提供了间接易用的API,同时支持负载均衡,节点间故障自动转移。
  • API网络层:API网络层主要包括client访问server和server节点之间的通信协议。
  • Raft 算法层:实现了 Leader 选举、日志复制、ReadIndex 等核心算法特性,用于保障 etcd 多个节点间的数据一致性、提升服务可用性等,是 etcd 的基石和亮点。
  • 功能逻辑层:etcd核心特性实现层,如典型的KVServer模块,MVCC模块,Auth鉴权模块,Lease租约模块等。
  • 存储层:包含写前日志WAL模块,快照Snapshot,boltdb模块。其中WAL可以保证etcd crash之后数据不丢失,boltdb则保存了集群元数据和用户写入的数据。

etcd 是典型的读多写少存储,在我们实际业务场景中,读一般占据 2/3 以上的请求。为了让你对 etcd 有一个深入的理解,接下来我会分析一个读请求是如何执行的,带你了解etcd 的核心模块,进而由点及线、由线到面地帮助你构建 etcd 的全景知识脉络。

在这里插入图片描述

KVServer

client发送RPC请求到了server之后,就开始进入我们架构图中的流程二,也就是KVServer模块。

etcd 提供了丰富的 metrics、日志、请求行为检查等机制,可记录所有请求的执行耗时及错误码、来源 IP 等,也可控制请求是否允许通过,比如 etcd Learner 节点只允许指定接口和参数的访问,帮助大家定位问题、提高服务可观测性等,而这些特性是怎么非侵入式的实现呢?

答案就是拦截器。使用grpc拦截器实现了这些东西。

串行读与线性度

进入 KVServer 模块后,我们就进入核心的读流程了,对应架构图中的流程三和四。我们知道 etcd 为了保证服务高可用,生产环境一般部署多个节点,那各个节点数据在任意时间点读出来都是一致的吗?什么情况下会读到旧数据呢?

这里为了帮助你更好的理解读流程,我先简单提下写流程。如下图所示,当 client 发起一个更新 hello 为 world 请求后,若 Leader 收到写请求,它会将此请求持久化到 WAL 日志,并广播给各个节点,若一半以上节点持久化成功,则该请求对应的日志条目被标识为已提交,etcdserver 模块异步从 Raft 模块获取已提交的日志条目,应用到状态机 (boltdb等)。

在这里插入图片描述

此时若 client 发起一个读取 hello 的请求,假设此请求直接从状态机中读取, 如果连接到的是 C 节点,若 C 节点磁盘 I/O 出现波动,可能导致它应用已提交的日志条目很慢,则会出现更新 hello 为 world 的写命令,在 client 读 hello 的时候还未被提交到状态机,因此就可能读取到旧数据,如上图查询 hello 流程所示。

从以上介绍我们可以看出,在多节点 etcd 集群中,各个节点的状态机数据一致性存在差异。而我们不同业务场景的读请求对数据是否最新的容忍度是不一样的,有的场景它可以容忍数据落后几秒甚至几分钟,有的场景要求必须读到反映集群共识的最新数据。

我们首先来看一个对数据敏感度比较低的场景

假如老板让你做一个旁路数据统计服务,希望你每分钟统计下 etcd 里的服务、配置信息等,这种场景其实对数据时效性要求并不高,读请求可直接从节点的状态机获取数据。即便数据落后一点,也不影响业务,毕竟这是一个定时统计的旁路服务而已。

这种直接读状态机数据返回,无需通过Raft协议与集群进行交互的模式,在etcd里面叫做串行读,它具有低延时,高吞吐量的特点,适合对数据一致性要求不高的场景。

我们再来看一个对数据敏感度要求高的场景

当你发布服务,更新服务的镜像的时候,提交的时候显示更新成功,结果你一刷新页面,发现显示的镜像的还是旧的,再刷新又是新的,这就会导致混乱。再比如说一个转账场景,Alice 给 Bob 转账成功,钱被正常扣出,一刷新页面发现钱又回来了,这也是令人不可接受的。

以上的业务场景对数据准确性的要求非常高,在etcd里面,提供了一种线性度的模式来解决对数据一致性要求高的场景。

什么是线性读呢?

你可以理解一旦一个值更新成功,随后任何通过线性读的 client 都能及时访问到。虽然集群中有多个节点,但 client 通过线性读就如访问一个节点一样。etcd 默认读模式是线性读,因为它需要经过 Raft 协议模块,反应的是集群共识,因此在延时和吞吐量上相比串行读略差一点,适用于对数据一致性要求高的场景。

如果你的 etcd 读请求显示指定了是串行读,就不会经过架构图流程中的流程三、四。默认是线性读,因此接下来我们看看读请求进入线性读模块,它是如何工作的。

线性读之ReadIndex

前面我们聊到串行读时提到,它之所以能读到旧数据,主要原因是 Follower 节点收到Leader 节点同步的写请求后,应用日志条目到状态机是个异步过程,那么我们能否有一种机制在读取的时候,确保最新的数据已经应用到状态机中?

在这里插入图片描述

  • 当收到一个线性读请求的时候,它首先会从Leader获取集群最新的已提交的日志索引(committed index)。
  • Leader收到ReadIndex请求的时候,为了防止脑裂等异常场景,会向Follower节点发送心跳确认,一半以上节点确认Leader身份后才能将已提交的索引返回给节点C。
  • C节点则会等待,直到状态机已应用索引(applied index)大于等于Leader的已提交索引(committed Index)(上图的流程四),然后去通知读请求,数据已赶上Leader,你可以去状态机中访问数据了(上图的流程5).

以上就是ReadIndex机制保证数据一致性原理。

总体而言,KVServer 模块收到线性读请求后,通过架构图中流程三向 Raft 模块发起ReadIndex 请求,Raft 模块将 Leader 最新的已提交日志索引封装在流程四的 ReadState结构体,通过 channel 层层返回给线性读模块,线性读模块等待本节点状态机追赶上Leader 进度,追赶完成后,就通知 KVServer 模块,进行架构图中流程五,与状态机中的MVCC 模块进行进行交互了。

MVCC

流程五的多版本并发控制模块是为了解决etcd v2不支持保存key的历史版本,不支持多key事务等问题而产生的。

它的核心由内存树形索引模块(treeIndex)和嵌入式的KV持久化存储卡boltdb组成。

首先我们简单了解一下boltdb,它是一个基于B+ tree实现的key-value键值库,支持事务,提供Get/Put等简易API给etcd操作。

那么etcd是如何基于boltdb保存一个key的多个历史版本呢?

比如我们现在有以下方案:方案1是一个key保存多个历史版本的值;方案2是每次修改操作,生成一个版本号,以版本号为key,value为用户key-value等信息组成的结构体。

很显然方案 1 会导致 value 较大,存在明显读写放大、并发冲突等问题,而方案 2 正是etcd 所采用的。boltdb 的 key 是全局递增的版本号 (revision),value 是用户 key、value 等字段组合成的结构体,然后通过 treeIndex 模块来保存用户 key 和版本号的映射关系。

treeIndex 与 boltdb 关系如下面的读事务流程图所示,从 treeIndex 中获取 key hello 的版本号,再以版本号作为 boltdb 的 key,从 boltdb 中获取其 value 信息。

在这里插入图片描述

treeIndex

treeIndex 模块是基于 Google 开源的内存版 btree 库实现的,为什么 etcd 选择上图中的B-tree 数据结构保存用户 key 与版本号之间的映射关系,而不是哈希表、二叉树呢?在后面的课程中我会再和你介绍。

treeIndex 模块只会保存用户的 key 和相关版本号信息,用户 key 的 value 数据存储在boltdb 里面,相比 ZooKeeper 和 etcd v2 全内存存储,etcd v3 对内存要求更低。

简单介绍了 etcd 如何保存 key 的历史版本后,架构图中流程六也就非常容易理解了, 它需要从 treeIndex 模块中获取 hello 这个 key 对应的版本号信息。treeIndex 模块基于 B tree 快速查找此 key,返回此 key 对应的索引项 keyIndex 即可。索引项中包含版本号等信息。

buffer

在获取到版本号信息后,就可从 boltdb 模块中获取用户的 key-value 数据了。不过有一点你要注意,并不是所有请求都一定要从 boltdb 获取数据。

etcd 出于数据一致性、性能等考虑,在访问 boltdb 前,首先会从一个内存读事务 buffer中,二分查找你要访问 key 是否在 buffer 里面,若命中则直接返回。

boltdb

如果buffer没有命中,此时就需要向boltdb模块查询数据了。进入了流程7。

我们知道 MySQL 通过 table 实现不同数据逻辑隔离,那么在 boltdb 是如何隔离集群元数据与用户数据的呢?答案是 bucket。boltdb 里每个 bucket 类似对应 MySQL 一个表,用户的 key 数据存放的 bucket 名字的是 key,etcd MVCC 元数据存放的 bucket 是meta。

因 boltdb 使用 B+ tree 来组织用户的 key-value 数据,获取 bucket key 对象后,通过boltdb 的游标 Cursor 可快速在 B+ tree 找到 key hello 对应的 value 数据,返回给client。

到这里,一个读请求之路执行完成。

基础架构:etcd一个写请求是如何执行的?

etcd一个写请求的执行流程是怎么样子?在执行写请求的过程中,如何进程crash了,如何保证数据不丢失,命令不重复执行呢?

我们先来一个整体架构:

在这里插入图片描述

  • client端通过负载均衡算法选择一个etcd节点,发起gRPC调用。然后etcd节点收到请求后经过gRPC拦截器,Quota模块后,进入KVServer模块。
  • KVServer模块向Raft模块提交一个提案,提案内容为“大家好,请使用put方法执行一个key为hello,value为world的命令”。
  • 此提案通过RaftHTTP网络模块转发,经过集群多数节点持久化后,状态会变成已提交,etcdserver从Raft模块获取已提交的日志条目,传递给Apply模块,Apply模块通过MVCC模块执行提案内存,更新状态机。
  • etcdserver从Raft模块获取已提交的日志条目,传递给Apply模块,Apply模块通过MVCC模块执行提案内容,更新状态机。

与读流程不一样的是写路程还涉及Quato,WAL,Apply三个模块。crash-safe以及幂等性也正是基于WAL和Apply流程的consistent index等实现的。

接下来跟着写请求执行流程图,从0到1分析一个key-value是如何安全,幂等的持久化到磁盘的。

Quota模块

检测配额。一方面默认 db 配额仅为 2G,当你的业务数据、写入 QPS、Kubernetes 集群规模增大后,你的 etcd db 大小就可能会超过 2G。

另一方面我们知道 etcd v3 是个 MVCC 数据库,保存了 key 的历史版本,当你未配置压缩策略的时候,随着数据不断写入,db 大小会不断增大,导致超限。

最后你要特别注意的是,如果你使用的是 etcd 3.2.10 之前的旧版本,请注意备份可能会触发 boltdb 的一个 Bug,它会导致 db 大小不断上涨,最终达到配额限制。

KVServer模块

通过流程二的配额检查后,请求就从API层转发到了流程三的KVServer模块的put方法,我们直到etcd是基于Raft算法实现节点间数据复制的,因此它需要将put写请求内容打包成一个提案消息,提交给Raft模块。不过KVServer模块在提交提案前,还有如下的一系列检查限速。

Preflight Check

为了保证集群稳定性,避免雪崩,任何提交到 Raft 模块的请求,都会做一些简单的限速判断。如下面的流程图所示,首先,如果 Raft 模块已提交的日志索引(committed index)比已应用到状态机的日志索引(applied index)超过了 5000,那么它就返回一个"etcdserver: too many requests"错误给 client。
在这里插入图片描述

然后它会尝试去获取请求中的鉴权信息,若使用了密码鉴权,请求中携带了token,如果token无效则返回"auth: invalid auth token"错误给client。

其次它会检查你写入的包大小是否超过默认的1.5MB,如果超过了会返回"etcdserver: request is too large"错误给client。

Propose

最后通过一系列的检查之后,会生成一个唯一的ID,将此请求关联到一个对应的消息通知channel,然后向Raft模块发起(Propose)一个提案(Proposal),提案内容为“大家好,请使用put方法执行一个key为hello,value为world的命令”,也就是整体架构图里面的流程四。

向Raft模块发起提案后,KVServer模块会等待此put请求,等待写入结果通过消息通知channel返回或则和超时。etcd默认超时时间是7秒(5秒磁盘IO + 2 * 1秒竞选超时时间),如果一个请求超时未返回结果,则可能出现你们熟悉的“etcdserver:request timed out”错误。

WAL模块

Raft模块收到提案后,如果当前节点是Follower,它会转发给Leader,只有Leader才能处理写请求。Leader收到提案之后,通过Raft模块输出待转发给Follower节点的消息和待诗酒花的日志条目,日志条目则封装了我们上面所说的put hello提案内容。

etcdserver 从 Raft 模块获取到以上消息和日志条目后,作为 Leader,它会将 put 提案消息广播给集群各个节点,同时需要把集群 Leader 任期号、投票信息、已提交索引、提案内容持久化到一个 WAL(Write Ahead Log)日志文件中,用于保证集群的一致性、可恢复性,也就是我们图中的流程五模块。

那么WAL日志结构是怎样的呢?

在这里插入图片描述

上图是 WAL 结构,它由多种类型的 WAL 记录顺序追加写入组成,每个记录由类型、数据、循环冗余校验码组成。不同类型的记录通过 Type 字段区分,Data 为对应记录内容,CRC 为循环校验码信息。

WAL 记录类型目前支持 5 种,分别是文件元数据记录、日志条目记录、状态信息记录、CRC 记录、快照记录:

  • 文件元数据记录包含节点 ID、集群 ID 信息,它在 WAL 文件创建的时候写入;
  • 日志条目记录包含 Raft 日志信息,如 put 提案内容;
  • 状态信息记录,包含集群的任期号、节点投票信息等,一个日志文件中会有多条,以最后的记录为准;
  • CRC 记录包含上一个 WAL 文件的最后的 CRC(循环冗余校验码)信息, 在创建、切割 WAL 文件时,作为第一条记录写入到新的 WAL 文件, 用于校验数据文件的完整性、准确性等;
  • 快照记录包含快照的任期号、日志索引信息,用于检查快照文件的准确性。

WAL 模块又是如何持久化一个 put 提案的日志条目类型记录呢?

首先我们来看看 put 写请求如何封装在 Raft 日志条目里面。下面是 Raft 日志条目的数据结构信息,它由以下字段组成:

type Entry struct {
    Term uint64
    Index uint64
    Type EntryType
    Data []byte
}
  • Term 是 Leader 任期号,随着 Leader 选举增加;Index 是日志条目的索引,单调递增增加;
  • Type 是日志类型,比如是普通的命令日志(EntryNormal)还是集群配置变更日志(EntryConfChange);
  • Data 保存我们上面描述的 put 提案内容

了解完 Raft 日志条目数据结构后,我们再看 WAL 模块如何持久化 Raft 日志条目。它首先先将 Raft 日志条目内容(含任期号、索引、提案内容)序列化后保存到 WAL 记录的Data 字段, 然后计算 Data 的 CRC 值,设置 Type 为 Entry Type, 以上信息就组成了一个完整的 WAL 记录。

最后计算 WAL 记录的长度,顺序先写入 WAL 长度(Len Field),然后写入记录内容,调用 fsync 持久化到磁盘,完成将日志条目保存到持久化存储中。

当一半以上节点持久化此日志条目后, Raft 模块就会通过 channel 告知 etcdserver 模块,put 提案已经被集群多数节点确认,提案状态为已提交,你可以执行此提案内容了。

于是进入流程六,etcdserver 模块从 channel 取出提案内容,添加到先进先出(FIFO)调度队列,随后通过 Apply 模块按入队顺序,异步、依次执行提案内容。

Apply模块

执行 put 提案内容对应我们架构图中的流程七,其细节图如下。那么 Apply 模块是如何执行 put 请求的呢?若 put 请求提案在执行流程七的时候 etcd 突然 crash 了, 重启恢复的时候,etcd 是如何找回异常提案,再次执行的呢?

在这里插入图片描述

核心就是我们上面介绍的 WAL 日志,因为提交给 Apply 模块执行的提案已获得多数节点确认、持久化,etcd 重启时,会从 WAL 中解析出 Raft 日志条目内容,追加到 Raft 日志的存储中,并重放已提交的日志提案给 Apply 模块执行。

然而这就引发了另外一个问题,如何确保幂等性,防止提案重复执行导致数据混乱呢?

etcd是一个MVCC数据库,每次更新都会生成新的版本号。如果没有幂等性保护,同样的命令,一部分节点执行一次,一部分节点遭遇异常故障后执行多次,则系统的各节点一致性状态无法得到保证,导致数据混乱,这是严重故障。

因此Apply模块在执行提案内容前,首先会判断当前提案是否已经执行过了,如果执行了则直接返回,若没有执行同时无db配额满告警,则进入到MVCC模块开与持久化存储模块打交道。

MVCC

Apply模块判断此提案未执行后,就会调用MVCC模块来执行提案内容。MVCC主要由两部分组成,一个是内存索引模块treeIndex,保存key的历史版本号信息,另一个是boltdb模块,用来持久化存储key-value数据。那么MVCC模块执行put hello为world命令的时候,它是如何构建内存索引和保存哪些数据到db呢?

treeIndex

首先我们来看 MVCC 的索引模块 treeIndex,当收到更新 key hello 为 world 的时候,此key 的索引版本号信息是怎么生成的呢?需要维护、持久化存储一个全局版本号吗?

版本号(revision)在 etcd 里面发挥着重大作用,它是 etcd 的逻辑时钟。etcd 启动的时候默认版本号是 1,随着你对 key 的增、删、改操作而全局单调递增。

因为 boltdb 中的 key 就包含此信息,所以 etcd 并不需要再去持久化一个全局版本号。我们只需要在启动的时候,从最小值 1 开始枚举到最大值,未读到数据的时候则结束,最后读出来的版本号即是当前 etcd 的最大版本号 currentRevision。

MVCC 写事务在执行 put hello 为 world 的请求时,会基于 currentRevision 自增生成新的 revision 如{2,0},然后从 treeIndex 模块中查询 key 的创建版本号、修改次数信息。这些信息将填充到 boltdb 的 value 中,同时将用户的 hello key 和 revision 等信息存储到B-tree,也就是下面简易写事务图的流程一,整体架构图中的流程八。

在这里插入图片描述

boltdb

MVCC 写事务自增全局版本号后生成的 revision{2,0},它就是 boltdb 的 key,通过它就可以往 boltdb 写数据了,进入了整体架构图中的流程九。

boltdb 上一篇我们提过它是一个基于 B+tree 实现的 key-value 嵌入式 db,它通过提供桶(bucket)机制实现类似 MySQL 表的逻辑隔离。

在 etcd 里面你通过 put/txn 等 KV API 操作的数据,全部保存在一个名为 key 的桶里面,这个 key 桶在启动 etcd 的时候会自动创建。

除了保存用户 KV 数据的 key 桶,etcd 本身及其它功能需要持久化存储的话,都会创建对应的桶。比如上面我们提到的 etcd 为了保证日志的幂等性,保存了一个名为 consistent index 的变量在 db 里面,它实际上就存储在元数据(meta)桶里面。

那么写入 boltdb 的 value 含有哪些信息呢?

写入boltdb的value,并不是简单的world,如果只存一个用户value,索引又是保存在易失的内存上,那重启etcd只会,我们就丢失了用户的key名,无关构建treeIndex模块了。

因此为了构建索引和支持Lease等特性,etcd会持久化以下信息:

  • key名称
  • Key创建时的版本号,最后一次修改时的版本号,key自身修改的次数
  • value值
  • 租约信息

boltdb value 的值就是将含以上信息的结构体序列化成的二进制数据,然后通过 boltdb提供的 put 接口,etcd 就快速完成了将你的数据写入 boltdb,对应上面简易写事务图的流程二。

但是 put 调用成功,就能够代表数据已经持久化到 db 文件了吗?

这里需要注意的是,在以上流程中,etcd 并未提交事务(commit),因此数据只更新在boltdb 所管理的内存数据结构中。

事务提交的过程,包含 B+tree 的平衡、分裂,将 boltdb 的脏数据(dirty page)、元数据信息刷新到磁盘,因此事务提交的开销是昂贵的。如果我们每次更新都提交事务,etcd写性能就会较差。

那么解决的办法是什么呢?etcd 的解决方案是合并再合并

首先 boltdb key 是版本号,put/delete 操作时,都会基于当前版本号递增生成新的版本号,因此属于顺序写入,可以调整 boltdb 的 bucket.FillPercent 参数,使每个 page 填充更多数据,减少 page 的分裂次数并降低 db 空间。

其次 etcd 通过合并多个写事务请求,通常情况下,是异步机制定时(默认每隔 100ms)将批量事务一次性提交(pending 事务过多才会触发同步提交), 从而大大提高吞吐量,对应上面简易写事务图的流程三。

但是这优化又引发了另外的一个问题, 因为事务未提交,读请求可能无法从 boltdb 获取到最新数据。

为了解决这个问题,etcd 引入了一个 bucket buffer 来保存暂未提交的事务数据。在更新boltdb 的时候,etcd 也会同步数据到 bucket buffer。因此 etcd 处理读请求的时候会优先从 bucket buffer 里面读取,其次再从 boltdb 读,通过 bucket buffer 实现读写性能提升,同时保证数据一致性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胡桃姓胡,蝴蝶也姓胡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值