多维度解析etcd,一个比zookeeper更加优秀的键值对存储系统古明地盆

这次我们来聊一聊etcd,不过在此之前先来来说说分布式系统。

分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统,所以它们是一组计算机节点或软件共同对外提供服务。但对于用户而言,就好像在请求一台服务器。因为在分布式系统中,各个节点之间的协作是通过网络进行的,所以分布式系统中的节点在空间分布上几乎没有任何限制,可以分布于不同的机柜、机房,甚至是不同的国家和地区。

分布式系统的设计目标一般包括如下几个方面:

1.可用性:可用性是分布式系统的核心需求,其用于衡量一个分布式系统持续对外提供服务的能力。

2.可扩展性:增加机器后不会改变或极少改变系统行为,并且能获得近似线性的性能提升。

3.容错性:系统发生错误时,具有对错误进行规避以及从错误中恢复的能力。

4.性能:对外服务的响应延时和吞吐率要能满足用户的需求。

但虽然分布式架构可以组建一个强大的集群,但实际工作中遇到的挑战也要比传统单体架构大得多,具体表现如下所示。

1.节点之间的网络通信是不可靠的,存在网络延时和丢包等情况。

2.存在节点处理错误的情况,节点自身随时也有宕机的可能。

3.同步调用使系统变得不具备可扩展性。

提到分布式系统,就不得不提CAP原理,CAP原理在计算机科学领域广为人知,如果说系统架构师将CAP原理视作分布式系统的设计准则一点也不为过。先来看看CAP的完整定义:

C:Consistency(一致性),这里的一致性特指强一致。通俗地说,就是所有节点上的数据时刻保持同步,一致性严谨的表述是原子读写,即所有读写都应该看起来是"原子性"的,或串行的。所有的读写请求都好像是经全局排序过的一样,写后面的读一定能读到前面所写的内容。

P:Tolerancetothepartitionofnetwork(分区容忍性),当发生网络分区时(即节点之间无法通信),在丢失任意多消息的情况下,系统仍然能够正常工作。

相信大家都非常清楚CAP原理的指导意义:在任何分布式系统中,可用性、一致性和分区容忍性这三个方面都是相互矛盾的,三者不可兼得,最多只能取其二。

1)AP满足但C不满足:如果既要求系统高可用又要求分区容错,那么就要放弃一致性了。因为一旦发生网络分区(P),节点之间将无法通信,为了满足高可用(A),每个节点只能用本地数据提供服务,这样就会导致数据的不一致。一些信奉BASE(BasicAvailability,Sostate,EventuallyConsistency)原则的NoSQL数据库(例如,Cassandra、CouchDB等)往往会放宽对一致性的要求(满足最终一致性即可),以此来换取基本的可用性。

3)CA满足但P不满足:指的是如果不存在网络分区,那么强一致性和可用性是可以同时满足的。

CAP原理明确指出了完美满足CAP三种属性的分布式系统是不存在的。了解CAP原理的目的在于,其能够帮助我们更好地理解分布式协议实现过程中的取舍,在后面了解etcd的时候就会明白。

首先我们为什么要使用etcd?

因为开发分布式系统是一件比较困难的事情,其中的困难主要体现在分布式系统的"部分失败"上。"部分失败"是指信息在网络的两个节点之间传送的时候,网络出现了故障,发送者无法知道接收者是否收到了这个信息。而且导致这种故障的原因很复杂,接收者可能在出现网络错误之前就已经收到了信息,也可能没有收到,又或者接收者的进程结束而没能接收。

这就需要我们有一个天生支持分布式的系统,而etcd就是帮我们做这件事的,并且它是一个键值对存储系统。

其实现代的键值对存储系统都是分布式的,zookeeper是其中历史最悠久的项目之一,它起源于Hadoop,具有成熟、健壮以及丰富的特性。既然如此,那么我们为什么不使用zookeeper呢?原因有以下几点:

而现在,我们有了更好的选择etcd,与zookeeper相比它更简单,安装、部署和使用更加容易,并且etcd的某些功能是zookeeper所没有的。因此,在很多场景下,etcd比zookeeper更受用户的青睐,具体表现在如下几个方面:

etcd的官方定义如下:

Ahighly-availablekeyvaluestoreforsharedconfigurationandservicediscovery

很多人看到上述官方定义的第一反应可能是,etcd是一个键值存储仓库,却没有重视官方定义的后半句:用于配置共享和服务发现。

也就是说,etcd是一个Go语言编写的分布式、高可用的一致性键值存储系统,用于提供可靠的分布式键值存储、配置共享和服务发现等功能。etcd可以用于存储关键数据和实现分布式调度,它在现代化的集群运行中能够起到关键性的作用。

etcd以一致和容错的方式存储数据,分布式系统可以使用etcd实现一致性键值存储、配置管理、服务发现和分布式系统的协同等功能。而常见的etcd使用场景包括:服务发现、分布式锁、分布式数据队列、分布式通知和协调、主备选举等等。

etcd基于Raft协议,通过复制日志文件的方式来保证数据的强一致性。当客户端应用写一个key时,首先会存储到etcd的Leader上,然后再通过Raft协议复制到etcd集群的所有成员中,以此维护各成员(节点)状态的一致性与可靠性。虽然etcd是一个强一致性的系统,但也支持从非Leader节点读取数据以提高性能,但是写操作仍然需要Leader,所以当发生网络分区时,写操作仍可能失败。此外etcd实现了一个Go语言版的Raft程序库,并广泛应用于各种项目,除了etcd之外,还包括dockerswarmkit等。

etcd具有一定的容错能力,假设集群中共有n个节点,即便集群中(n-1)/2个节点发生了故障,只要剩下的(n+1)/2个节点达成一致,也能操作成功。因此,它能够有效地应对网络分区和机器故障带来的数据丢失风险。

etcd默认数据一更新就落盘持久化,数据持久化存储使用WAL(writeaheadlog,预写式日志)格式。WAL记录了数据变化的全过程,在etcd中所有数据在提交之前都要先写入WAL中;etcd的Snapshot(快照)文件则存储了某一时刻etcd的所有数据,默认设置为每10000条记录做一次快照,经过快照后WAL文件即可删除。

etcd在设计的时候重点考虑了如下的四个要素。

1.简单

2.安全

3.性能

4.可靠

简单地说,etcd可以扮演两大角色,具体如下:

在分布式系统中,如何管理节点间的状态一直是一个难题,etcd像是专门为集群环境的服务发现和注册而设计的,它提供了数据TTL失效、数据改变监视、多值、目录、分布式锁原子操作等功能,可以方便地跟踪并管理集群节点的状态

etcd(server)大体上可以分为如下几个部分:

通常,一个用户的请求发送过来,会经由HTTPServer转发给Store进行具体的事务处理。如果涉及到节点的修改,则交给Raft模块进行仲裁和日志记录,然后再同步给别的etcd节点以确认数据提交,只有当半数以上的节点确认了该节点状态的修改之后,才会真正进行数据的提交(持久化),然后再次同步。

各个节点在任何时候都有可能变成Leader、Follower、Candidate等角色,同时为了减少创建链接开销,etcd节点在启动之初就会创建并维持与集群其他节点之间的链接。

etcd集群的各个节点之间需要通过网络来传递数据,具体表现为如下几个方面。

那么问题来了,用户会从集群中哪个节点读写数据呢?

为了保证数据的强一致性,etcd集群中所有的数据流向都是一个方向,从Leader(主节点)流向Follower,也就是所有Follower的数据必须与Leader保持一致,如果不一致会被覆盖。

简单点说就是,用户可以对etcd集群中的所有节点进行读写。首先读取非常简单,因为每个节点保存的数据是强一致的。对于写入来说,etcd集群中的节点会选举出Leader节点,如果写入请求来自Leader节点,则可以直接写入,然后Leader节点会把写入分发给所有Follower;如果写入请求来自其他Follower节点,那么写入请求会给转发给Leader节点,由Leader节点写入之后再分发给集群上所有其他节点。

如何选取Leader?

假设集群中有三个节点,集群启动之初节点中并没有被选举出的Leader。

正如上面介绍的那样,etcd的定位是通用的一致性key/value存储,但也有服务发现和共享配置的功能。因此,典型的etcd应用场景包括但不限于分布式数据库、服务注册与发现、分布式锁、分布式消息队列、分布式系统选主等。etcd的定位是通用的一致性key/value存储,同时也面向服务注册与发现的应用场景。下面将对etcd的一些典型应用场景进行简单概括。

1.服务注册与发现

服务发现(ServiceDiscovery)要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。

从本质上说,服务发现就是要了解集群中是否有进程在监听UDP或者TCP端口,并且通过名字就可以进行查找和链接。而要解决服务发现的问题,需要具备如下三个条件:

图上有三个角色,分别是服务请求方、服务提供方、服务注册方。假设有三台机器:A、B、C,用于提供邮件发送服务,这个时候服务请求方如果想要使用邮件发送服务,那么就会去找服务提供方。但是服务请求方并不是直接寻找服务提供方,因为它不知道提供请求的是谁,所以它会寻找服务注册方,然后服务注册方将服务提供方的信息返回给服务请求方,比如:返回A、B、C,表示这三台机器是用来提供服务的。

但是服务注册方如何才能准确返回服务提供方的信息呢?显然服务提供方是要先进行注册的,服务注册方保存了提供方的信息。并且提供方还要不断地向注册方发送心跳信息,表示自己还活着。假设B机器挂掉了,那么请求方向注册方寻找服务方的时候,注册方就不会再返回B机器的信息了。所以服务请求方首先找的是服务注册方,而我们的etcd充当的就是服务注册方这一角色,当然zookeeper也是类似。

2.消息发布和订阅

在分布式系统中,组件之间的通信机制最为适用的是消息的发布和订阅机制。具体而言就是,设置一个配置共享中心,消息提供者在这个配置中心发布消息,而消息使用者则订阅它们关心的主题,一旦所关心的主题有消息发布,就会实时通知订阅者。通过这种方式,我们可以实现分布式系统配置的集中式管理和实时动态更新。

1.etcd管理应用配置信息更新

这类场景的使用方式通常是,应用在启动的时候主动从etcd获取一次配置信息,同时在etcd节点上注册Watcher并等待。以后每当配置有更新的时候,etcd都会实时通知订阅者,以此达到获取最新配置信息的目的。

2.分布式日志收集系统

这个系统的核心工作是收集分布在不同机器上的日志。

3.负载均衡

在分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署为多份,以此达到对等服务,即使其中的某一个服务失效了,也不会影响使用。

这样的实现虽然会导致一定程度上数据写入性能的下降,但是却能够实现数据访问时的负载均衡。因为每个对等服务节点上都存储有完整的数据,因此所有用户的访问流量都可以分流到不同的机器上。

4.分布式通知与协调

这里讨论的分布式通知与协调,和消息的发布订阅有点相似。两者都使用了etcd的Watcher机制,通过注册与异步通知机制,实现分布式环境下不同系统之间的通知与协调,从而对数据变更进行实时处理。

实现方式是不同的系统都在etcd上对同一个目录进行注册,同时设置Watcher监控该目录的变化(如果子目录的变化也有需求,那么可以设置成递归模式)。若某个系统更新了etcd的目录,那么设置了Watcher的系统就会收到通知,并也做出相应的通知,然后进行相应的处理。

5.分布式锁

因为etcd使用Raft算法保持了数据的强一致性,操作之后存储到集群中的值就必然是全局一致的,所以etcd很容易实现分布式锁。

而锁服务包含两种使用方式:保持独占,以及控制时序。

1.保持独占

即所有试图获取锁的用户最终只有一个可以得到。

etcd为此提供了一套实现分布式锁原子操作CAS(ComparaAndSwap)的API,通过设置prevExist值,可以保证在多个节点上同时创建某个目录时,只有一个节点能够成功,而成功的那个即可获得分布式锁。

2.控制时序

试图获取锁的所有用户都会进入等待队列,获得锁的顺序是全局唯一的,同时还能决定队列的执行顺序。

etcd为此也提供了一套API(自动创建有序键),它会将一个目录的键值指定为POST动作,这样,etcd就会在目录下生成一个当前最大的值作为键,并存储这个新的值(客户端编号)。

同时还可以使用API按顺序列出所有目录下的键值,此时这些键的值就是客户端的时序,而这些键中存储的值则可以是代表客户端的编号。

6.分布式队列

分布式队列的常规用法与分布式锁的控制时序类似,即通过创建一个先进先出的队列来保证顺序。

另一种比较有意思的实现是在保证队列达到某个条件时再统一按顺序执行,要实现这种方法,可以在"/queue"目录中另外建立一个"/queue/condition"节点,如图所示:

1.condition可以表示队列的大小,比如一个大的任务需要在很多小任务都就绪的情况下才能执行,那么每当有一个小任务就绪时,就将这个condition的数值加1,直到到达大任务规定的数字,然后再开始执行队列里的一系列小任务,直至最终执行大任务。

2.condition可以表示某个任务不在队列中,这个任务既可以是所有排序任务的首个执行程序,也可以是拓扑结构中没有依赖的点。通常,必须在执行这些任务之后才能执行队列中的其他任务。

3.condition还可以表示开始执行任务的通知,可以由控制程序来指定,当condition发生变化时,开始执行队列任务。

7.集群监控与Leader竞选

通过etcd来进行监控的功能实现起来非常简单并且实时性较强,主要会用到如下两点特性:

Leader应用的经典场景是在搜索系统中建立全量索引,如果各个机器分别进行索引的建立,那么将很难保证索引的一致性。通过etcd的CAS机制竞选Leader,再由Leader进行索引计算,最后将计算结果分发到其它节点即可。

但etcd也是有缺点的

比如我们之前说etcd有v2和v3两个版本,现在推荐使用v3。但是相比于etcdv2,etcdv3版本的接口是通过gRPC提供RPC接口的,它放弃了v2版本的HTTP接口,虽然这种改变可以明显提升连接效率,但使用便利性不如v2,特别是不便于维护长连接的应用场景。此外,etcd的定位是通用的一致性KV存储,但在面向服务注册与发现的应用场景中,过于广泛的通用性会使得每个应用的服务注册都有自己的元数据格式,不利于互相整合,受限于元数据格式的兼容性问题,也不利于实现更高级的功能。

这里我们主要探讨etcd和zookeeper之间的区别。zookeeper是一个用户维护配置信息、命名、分布式同步以及分组服务的集中式服务框架,它使用Java语言编写,通过Zab协议来保证节点的一致性。因为zookeeper是一个CP型系统,所以在发生网络分区问题时,系统不能注册或查找服务。

zookeeper和etcd可用于解决的问题:分布式系统的协同和元数据存储。然而,etcd却有着zookeeper的设计和实现的后见之明,zookeeper最大的问题就是太复杂了,etcd吸取了zookeeper的教训后具备更好的工程和运维体验。

etcd和zookeeper相比,其改进之处在于如下几个方面。

另外,etcd广泛支持各种各样的语言和框架,但zookeeper只有它自己的客户端协议:JuteRPC协议。Jute是zookeeper独一无二的协议,且只在特定的语言库(Java和C)中绑定。etcd的客户端协议gRPC,它是一个流行的RPC框架,支持的语言非常多。gRPC也能序列化成通过HTTP传输的JSON,所以通用的命令行工具curl也能与它进行交互。这就为分布式系统的构建者提供了丰富的选择,他们能够用操作系统原生的工具来构建而不是非得围绕etcd用指定的技术,也就是无需迎合etcd,而是让etcd配合你。

至于其它的分布式键值存储系统就不对比了,总之etcd绝对是脱颖而出的。

etcd中存在许多概念,或者说术语,来看一下,先有一个印象。

etcd已经进化到3.x版本了,发展到现在总共有3个可以成为里程碑的版本,分别是0.4、2.0、3.0,我们分别介绍一下。

etcd0.4

etcd0.4版本是etcd对外发布的第一个稳定版本,很多特性均在这个版本成型。比如以下几个特性:

etcd2.0

etcd2.0版本是etcd第一个真正意义上的大版本,其引入了如下几个重要特性:

etcd3.0

etcd3.0版本在etcd2.0的基础上引入了多处优化,可以说是万众瞩目,千呼万唤始出来,并且一经发布即引起了巨大的轰动。优化内容具体如下:

以上就是etcd的几个著名的版本,如果想要更加详细地了解etcd的变迁,可以查看etcd发布的各个版本的CHANGELOG。

在对etcd有一个大致的了解之后,我们就来安装并使用etcd了。

下面我们来看一下etcd的安装,首先操作系统是CentOS7,而且我们这里要搭建三个节点的集群。这里采用我阿里云上的三台centOS7,IP和配置分别如下:

此时三个节点都已经安装完毕了,版本是3.3.11。然后我们来看一下etcd的配置文件。首先etcd的配置文件位于/etc/etcd/etcd.conf中,配置文件总共69行,我们直接原封不动地贴出来吧。

下面就来编写配置文件了,这里我们只写需要的,没写的都是被注释掉的。

首先配置kano节点上的etcd:

但我还是要在吐槽一句,etcd的配置文件是真的恶心。

然后启动etcd:systemctlstartetcd,启动之后查看etcd集群:etcdctlmemberlist。可以看到etcd集群是可以正常启动的,并且kano主机是主节点。

我们可以使用etcd的命令行工具etcdctl和etcd服务端进行交互。默认情况下,etcdctl使用v2的API,如果需要使用v3的API,则可以先设置以下环境变量。具体命令如下所示:

exportETCDCTL_API=3

然后我们来演示一下etcdctl的常用命令,这里我们使用的都是v3的API,v2和v3在API方面差别还是蛮大的,如果你发现后面的命令不存在的话,那么你很可能是忘记指定API版本为v3了。

1.写入一个key

所有存储的key都通过Raft协议被复制到etcd集群的所有节点上,Raft协议保证了数据的一致性和可靠性。向一个key写入一个值,最简单的一条命令如下所示:

etcdctlputkeyvalue

[root@aqua~]#etcdctlputnamekagura_meaOK[root@aqua~]#我们这里是在aqua主机上写入的,它不是主节点,但是会将请求交给主节点(Leader),如果是读请求就自己处理了,因为Raft协议保证所有节点的数据都是一致的。修改一个key的话还是使用put,直接设置新的value即可。另外如果设置的value包含特殊字符,比如空格、横杠等等,需要使用引号包起来。

#申请一个600s的租约[root@aqua~]#etcdctlleasegrant600lease1bdc76805eb02206grantedwithTTL(600s)[root@aqua~]#etcdctlputname1natsuiro_matsuri--lease=1bdc76805eb02206OK[root@aqua~]#十分钟之后再读取这个key,就会返回一个100错误,表示该key不存在。

租约后面会单独说。

2.读取一个key

为了更好观察,我们再写两个key吧。

[root@aqua~]#etcdctlputname2kagura_nanaOK[root@aqua~]#etcdctlputname3shizuku_ruruOK[root@aqua~]#然后我们来读取key的内容,可以使用如下命令:

etcdctlgetkey

#会同时打印key和value,可以指定--print-value-only只打印value[root@aqua~]#etcdctlgetnamenamekagura_mea[root@aqua~]#etcdctlgetname--print-value-onlykagura_mea[root@aqua~]#还可以打印指定范围的key:etcdctlgetleft_keyright_key,注意区间是左闭右开,会按照字典序比较。

[root@aqua~]#etcdctlgetnamename3namekagura_meaname1natsuiro_matsuriname2kagura_nana[root@aqua~]#etcdctlgetnamename3--print-value-onlykagura_meanatsuiro_matsurikagura_nana[root@aqua~]#遍历所有以name为前缀的key,具体命令:etcdctlgetname--prefix

[root@aqua~]#etcdctlgetname--prefixnamekagura_meaname1natsuiro_matsuriname2kagura_nananame3shizuku_ruru[root@aqua~]#etcdctlgetname--prefix--print-value-onlykagura_meanatsuiro_matsurikagura_nanashizuku_ruru#可以通过--limit限制返回数量[root@aqua~]#etcdctlgetname--prefix--print-value-only--limit2kagura_meanatsuiro_matsuri[root@aqua~]#3.读取老版本的key

etcd支持客户端读取老版本的key,原因是有些应用程序将etcd当作一个配置中心来使用,有读取之前版本key的需求。例如,一个应用可以利用这个特性回滚到较早的某个版本的配置,因为对etcd后端存储的每次修改都会增加etcd集群全局的版本号(revision),所以只需要提供指定的版本号就能读取相应版本的key。

首先我们每执行一次put,全局版本号就会加1,当前的集群版本号是8,因为我做了几个测试。

#首先集群版本是8[root@aqua~]#etcdctlgetnamenamekagura_mea#将name给改掉,使用put,版本加1变成了9[root@aqua~]#etcdctlputnamekagura_MEAOK#默认读取最新版的key[root@aqua~]#etcdctlgetnamenamekagura_MEA#指定集群版本,我们看到又输出之前的内容了[root@aqua~]#etcdctlgetname--rev=8namekagura_mea#因为9是最新版本,所以是最新修改的值[root@aqua~]#etcdctlgetname--rev=9namekagura_MEA#10的话还没有这个版本,所以报错了[root@aqua~]#etcdctlgetname--rev=10Error:etcdserver:mvcc:requiredrevisionisafuturerevision[root@aqua~]#4.按key的字典序来读取

当客户端希望读取大于或等于key的字节值时,可使用--from-key参数来实现。我们来添加以下键值对:

[root@aqua~]#etcdctlputa1OK[root@aqua~]#etcdctlputb2OK[root@aqua~]#etcdctlputc3OK[root@aqua~]#etcdctlputd4OK然后读取字典序大于等于c的记录:

[root@aqua~]#etcdctlgetc--from-keyc3d4namekagura_MEAname1natsuiro_matsuriname2kagura_nananame3shizuku_ruru[root@aqua~]#etcdctlgetc--from-key--print-value-only34kagura_MEAnatsuiro_matsurikagura_nanashizuku_ruru[root@aqua~]#5.删除key

用户可以删除etcd集群中的一个key或者一个范围内的key。

#返回删除的个数[root@aqua~]#etcdctldela1#删除不存在的key也不会报错[root@aqua~]#etcdctldela0#删除指定范围的key,按照字典序排序,依旧是左闭右开[root@aqua~]#etcdctldelbd2#d确实没有被删除[root@aqua~]#etcdctldeld1#删除key的同时返回key和value[root@aqua~]#etcdctldelname--prev-kv1namekagura_MEA[root@aqua~]#与get命令类似,del命令也支持--prefix参数,删除以某个字符串为前缀的key;也支持--from-key,删除字典序大于等于指定的key的所有key。

[root@aqua~]#etcdctldelname3--from-key1[root@aqua~]#etcdctldelname--prefix--prev-kv2name1natsuiro_matsuriname2kagura_nana[root@aqua~]#key的历史与watchetcd具有观察(watch)机制,一旦某个key发生变化,客户端就能感知到变化。对应到etcdctl就是watch子命令,除非该子命令捕获到退出信号(例如,按Ctrl+C捷键就能向etcdctl发送强制退出信号量),否则会一直等待而不会退出,举个栗子:

[root@aqua~]#etcdctlwatchname此时就卡在这个地方了,目前操作的主机是aqua,下面我们在主机kano中更新name这个key,注意:之前name这个key是被我们删掉了的。

[root@kano~]#etcdctlputnamehanserOK[root@kano~]#etcdctlputnameyousaOK[root@kano~]#etcdctldelname1[root@kano~]#etcdctldelname0[root@kano~]#etcdctldelname0[root@kano~]#再来观察之前的终端:

除此之外还可以监视指定范围的key:etcdctlwatchleft_keyright_key,凡是字典序位于该范围内的key都会被监视,注意:区间依旧是左闭右开;也可以监视以某个字符串为前缀的key,etcdctlwatchkey--prefixstring。

以上可以自己尝试,相信此时对于你是不难的。

watch子命令还支持交互(interactive)模式,使用-i选项可watch多个key,具体命令如下所示:

#回车之后直接输入即可[root@aqua~]#etcdctlwatch-iwatchawatchb这里使用主机matsuri。

[root@matsuri~]#etcdctlputa1OK[root@matsuri~]#etcdctlputb2OK[root@matsuri~]#回到之前的终端。

[root@aqua~]#etcdctlwatch-iwatchawatchbPUTa1PUTb21.从某个版本号开始观察

watch除了监视一个key之外,还可以监视这个key的所有版本的变化,这个功能非常有用。例如,一个应用可能希望得到某个key所有变化的通知,如果它一直与etcd保持连接则没问题,但如果这个应用挂起了,而某个key又恰巧在这个时候发生了变化,那么这个应用会有很大的可能性没法及时接收到这个key的更新。为了保证key的变化不丢失,etcd支持客户端能够在任意时刻观察该key的所有变化。

#name我们之前就删掉了[root@aqua~]#etcdctldelname0#获取不到[root@aqua~]#etcdctlgetname#但是可以获取之前版本的name[root@aqua~]#etcdctlgetname--rev=3namekagura_mea#从rev=3的版本开始监视,我们看到返回了每一个版本的信息[root@aqua~]#etcdctlwatchname--rev=3PUTnamekagura_meaPUTnamekagura_MEADELETEnamePUTnamehanserPUTnameyousaDELETEnamewatch监视key的时候也可以指定--prev-kv参数,会返回该key修改前最近一个版本的value,可以自己尝试一下。

2.压缩key版本

为了让客户端能够访问key过去任意版本的value,etcd会一直保存key所有历史的版本的value。然而,etcd所占的磁盘空间不能无限膨胀,因此需要为etcd配置压缩key版本号来释放磁盘空间,具体代码如下所示:

#释放版本号为5之前的所以数据[root@aqua~]#etcdctlcompact5compactedrevision5#能够获取[root@aqua~]#etcdctlgetname--rev=5namekagura_mea#版本号为4的获取不到了[root@aqua~]#etcdctlgetname--rev=4Error:etcdserver:mvcc:requiredrevisionhasbeencompacted[root@aqua~]#在压缩key版本之前,用户需要认真权衡,因为压缩后指定版本之前的所有key/value都将不可用。用户可以通过get一个key(不论存在与否均可)来获取当前etcd服务端的版本号,比如:

[root@aqua~]#etcdctlgetkey-w=json{"header":{"cluster_id":788639929760900286,"member_id":2256447048954485724,"revision":28,"raft_term":3}}[root@aqua~]#通过上述代码我们可以看到,最新版本号是28。

租约是etcdv3API的特性,客户端可以为key授予租约(lease)。当一个key绑定一个租约时,它的生命周期便会与该租约的TTL(timetolive)保持一致,每个租约都有一个由用户授予的最小TTL值,而租约的实际TTL值至少等于用户授予的TTL值,但事实上,它很有可能会大于该值,这一切都由etcd来决定。如果某个租约的TTL超时了,那么该租约就会过期而且上面绑定的所有key都会被自动删除。下面演示一下如何为一个租约授予一个TTL,以及如何为该租约绑定一个key:

#申请一个时限为20秒的租约[root@aqua]#etcdctlleasegrant20lease686376805e9bda1egrantedwithTTL(20s)#为该租约绑定一个key[root@aqua]#etcdctlputfoobar--lease=686376805e9bda1eOK#获取值[root@aqua]#etcdctlgetfoofoobar#再次获取,由于租约已经过期,绑定在是上面的key也就被删除了[root@aqua]#etcdctlgetfoo[root@aqua]#etcdctlgetfoo[root@aqua]##并且一旦过期,此租约也就不能再次使用了[root@aqua]#etcdctlputfoobar--lease=686376805e9bda1eError:etcdserver:requestedleasenotfound[root@aqua]#需要注意的是:租约一旦申请,那么计时就已经开始了,举个栗子。

客户端既然能够授予租约,那么也能够撤销租约,下面介绍一下如何撤销租约:

#申请一个时限为3600秒的租约[root@aqua]#etcdctlleasegrant3600lease686376805e9bda2agrantedwithTTL(3600s)#创建一个key绑定在租约上[root@aqua]#etcdctlputage20--lease=686376805e9bda2aOK#正常获取[root@aqua]#etcdctlgetageage20#但是这个租约我们不想用了,可以将其取消,租约取消之后等价于过期,因此上面的key也会被删除[root@aqua]#etcdctlleaserevoke686376805e9bda2alease686376805e9bda2arevoked#再次获取发现没有了[root@aqua]#etcdctlgetage[root@aqua]#但是存在一个问题,如果我要更新一个绑定在租约上的key该怎么做呢?

[root@aqua~]#etcdctlleasegrant30lease1bdc76805eb0225fgrantedwithTTL(30s)#绑定在租约上[root@aqua~]#etcdctlputnamematsuri--lease=1bdc76805eb0225fOK#更新name[root@aqua~]#etcdctlputnameMATSURIOK[root@aqua~]#etcdctlgetnamenameMATSURI#30s后再次获取,发现还在[root@aqua~]#etcdctlgetnamenameMATSURI[root@aqua~]所以这种情况下等于创建了一个新的没有租约的name,如果想更新时还能使得租约生效,那么可以这么做。

2.续租

客户端也能通过刷新TTL的方式为租约续租,使它不过期:

[root@aqua~]#etcdctlleasekeep-alive1bdc76805eb0227flease1bdc76805eb0227fkeepalivedwithTTL(10)lease1bdc76805eb0227fkeepalivedwithTTL(10)lease1bdc76805eb0227fkeepalivedwithTTL(10)lease1bdc76805eb0227fkeepalivedwithTTL(10).......这个命令是阻塞的,每当块过期时就会续租,并且续租的TTL等于最初授予的值,显然这不常用。

3.获取租约信息

下面我们解释一下配置文件里面的每一个变量(命令行参数),之前介绍了一部分,下面再详细地从头介绍一遍。首先配置文件里面分为好几个块,分别是:Member、Clustering等等,我们一块一块说。

仅支持v2版本,暂且不表。

这里面绝大部分参数我们都是用不到的,最重要的参数是我们安装etcd的时候说的。

etcdv3于2016年6月30日正式发布,该版本标志着etcdv3数据模型和API正式稳定。etcdv3存储的数据通过KVAPI对外暴露,并在API的层级支持mini事务。而为了保证向后的兼容性,etcdv3依然保留了etcdv2的协议和API,同时又提供了一套v3的API。也就是说etcdv2和etcdv3本质上是共享同一套Raft协议代码的两个独立应用,它们的区别在于API不同,存储不同,数据互相隔离。如果从etcdv2升级到etcdv3,那么原来v2的数据还是只能用v2的API来访问,通过v3API创建的数据也只能通过v3的接口来访问,这意味着将etcd集群从v2升级到v3对客户端来讲是透明的。

etcdv3吸收了etcdv2的很多经验,同时又根据etcdv2在实际应用中遇到的问题进行了很多重要的改进,尤其是在效率、可靠性,以及性能上进行了各种优化。

etcd原本的定位就是解决分布式系统的协调问题,现在etcd已经广泛应用于分布式网络、服务发现、配置共享、分布式系统调度和负载均衡等领域。etcdv2的大部分设计和决策已在实践中证明是非常正确的:专注于key-value存储而不是一个完整的数据库,通过HTTP+JSON的方式暴露给外部API,观察者(watch)机制提供持续监听某个key变化的功能,以及基于TTL的key自动过期机制等。这些特性和设计很好地满足了etcd的初步需求。

鉴于以上问题和需求,etcdv3充分借鉴了etcdv2的经验,吸收了etcdv2的教训,做出了如下改进和优化。

下面来解释一下这些特性,以及v2和v3的对比。

protobuf的效率很高,远高于JSON。尽管etcdv2的客户端已经对JSON的序列化和反序列化进行了大量的优化,但是etcdv3的gRPC序列化和反序列化的速度依旧是etcdv2的两倍多。

etcdv2的通信协议使用的是HTTP/1.1,而gRPC支持HTTP/2,HTTP/2对HTTP通信进行了多路复用,可以共享一个TCP连接。因此etcdv3大大减少了客户端与服务器端的连接数,一个客户端只需要与服务器端建立一个TCP连接即可。而对于etcdv2来说,一个客户端需要与服务器端建立多个TCP连接,每个HTTP请求都需要建立一个连接。

观察者机制使得客户端可以监控一个key的变化,当key发生变化时,服务器端将通知客户端,而不是让客户端定期向服务器端发送请求去轮询key的变化。这一点不像zookeeper和consul,对于每个watch请求(实现上是HTTPGET请求)只返回一个事件,如果客户端想要继续watch之前的key,就只能再发送一次watch请求。而在两次watch请求之间,如果key发生了变更,那么客户端就会感知不到。etcd从设计之初就想解决这个问题,支持客户端连续不断地接收所监控的key更新事件。

etcdv2通过索引的方式支持连续watch,客户端每次watch都可以带上之前的key的索引,然后服务端会返回比上一次watch更新的数据。然而,etcdv2的服务端对每个客户端的每个watch请求都维持着一个HTTP长连接,如果数千个客户端watch了数千个key,那么etcdv2服务器端的socket和内存等资源很快就会被耗尽。

etcdv3的改进方法是对来自于同一个客户端的watch请求进行了多路复用(multiplexing),这样的话,同一个客户端只需要与服务器端维护一个TCP连接即可,这就大大减轻了服务器端的压力。

多版本键值可以减轻用户设计分布式系统的难度,通过对多版本的控制,用户可以获得一个一致的键值空间的快照。用户可以在无锁的状态下查询快照上的键值,从而帮助做出下一步决定。

客户端在GET一个key的value时,可以指定一个版本号,服务器端会返回紧接着这个版本之后的value。这样的话,有需要的应用就可以知道key的所有历史变更记录。客户端也可以指定版本号进行watch,服务端会连续不断地把该版本号之后的变更都通知给客户端。

etcdv3除了保存key的所有历史变更记录之外,它还在存储的实现上摒弃了etcdv2的目录式层级化设计,采用一个扁平化的设计。这是因为有的应用会针对单个key进行操作,而有的应用则会递归地对一个目录下的所有key进行操作。在实现上,维护一个目录式的层级化存储会带来一些额外的开销,而扁平化的设计也可以支持用户的这些操作,同时还会更加轻量级。etcdv3使用扁平化的设计,用一个线段树来支持范围查询、前缀查询等。对目录的查询操作,在实现上其实是将目录看作是对相同前缀的key的查询操作。

由于etcdv3实现了MVCC,保存了每个key-valuepair的历史版本,数据量大了很多,不能将整个数据库都放在内存里了。因此etcdv3摒弃了内存数据库,转为磁盘数据库,整个数据库都存储在磁盘上,底层的存储引擎使用的是BoltDB。

etcdv3除了提供读写API以外,还提供组合API,即事务API。

很多情况下,客户端需要同时去读或者写一个key或者很多个key。提供同步原语来防止数据竞争是非常重要的,出于这个目的,etcdv2提供了条件更新操作,即CAS(CompareAnd-Swap)操作。客户端在对一个key进行写操作的时候需要提供该key的版本号或当前值,服务器端会对其进行比较,如果服务器端的key值或者版本号已经更新了,那么CAS操作就会失败。但CAS操作只是针对单个key提供了简单的信号量和有限的原子操作,因此远远不能满足更加复杂的使用场景,尤其是当涉及多个key的变更操作时,比如分布式锁和事务处理。故而etcdv3引入了迷你事务(minitransaction)的概念。每个迷你事务都可以包含一系列的条件语句,只有在还有条件满足时事务才会执行成功。

迷你事务支持原子地比较多个键值并且操作多个键值,之前的CAS实际上是一个特殊的针对单个key的迷你事务。这里列举一个简单的例子:Tx(compare:A=1&&B=2,success:C=3,D=3,fail:C=0,D=0,当etcd收到这条事务请求时,etcd会原子地判断A和B当前的值和期望的值。如果判断成功,则C和D的值都会被设置为3。

etcdv2与其他类似的开源一致性系统一样,最多只能有数十万级别的key。主要原因是一致性系统都采用了基于log的复制,而log不能无限增长,所以在某时刻系统需要做个完整的快照,并且将快照存储到磁盘中,在存储快照之后才能将之前的log丢弃。每次存储完整的快照是件非常没有效率的事情,而且对于一致性系统来说,设计增量快照以及传输同步大量数据都是非常烦琐的。etcd通过对Raft和存储系统的重构,能够很好地支持增量快照和传输相对较大的快照,目前etcdv3可以存储百万到千万级别的key。

etcdv2中的每个Watcher都会占用一个TCP资源和一个goroutine资源,大概要消耗30~40kb。etcdv3通过减小每个Watcher带来的资源消耗来支持大规模的watch。一方面,etcd利用了HTTP/2的TCP连接多路复用,这样同一个客户端的不同Watch就可以共享同一个TCP连接了;另一方面,同一个用户的不同Watcher只消耗一个goroutine,这样就再一次减轻了etcd服务器的资源消耗。

然后我们来看看如何使用Python来操作etcd,Python操作etcd使用的是一个叫etcd3的模块,直接pipinstalletcd3即可。

我们随便举个栗子吧。

importetcd3#连接到主节点,当然连接到其它节点也是可以的client=etcd3.Etcd3Client(host="47.94.174.89")#注意:value需要传入字节print(client.put("name",bytes("神乐七奈",encoding="utf-8")))"""header{cluster_id:788639929760900286member_id:12186326826460735587revision:57raft_term:3}"""在Python中,直接调用put方法写入即可,并且我们看到这个方法还有返回值。会返回:集群id、在哪个member上写的(这个肯定是Leader)、集群版本、raft成员数量(说白了就是集群成员数量)

client=etcd3.Etcd3Client(host="47.94.174.89")#返回一个二元组,但并不是我们想像的key-valueresult=client.get("name")#第一个元素是value,不过是字节形式,需要转成字符串print(str(result[0],encoding="utf-8"))#神乐七奈#第二个元素是,具有如下属性print(result[1].create_revision)#57print(result[1].key)#b'name'print(result[1].lease_id)#0print(result[1].mod_revision)#57读取指定范围的key

client=etcd3.Etcd3Client(host="47.94.174.89")#再写几个client.put("name1",bytes("神乐mea",encoding="utf-8"))client.put("name2",bytes("夏色祭",encoding="utf-8"))client.put("name3",bytes("亚绮罗森",encoding="utf-8"))#读取指定范围的key,返回一个生成器result=client.get_range("name","name2")foriteminresult:print(str(item[0],encoding="utf-8"),str(item[1].key,encoding="utf-8"))"""神乐七奈name神乐meaname1"""读取以某个字符串为前缀的key

client=etcd3.Etcd3Client(host="47.94.174.89")#同样返回一个生成器result=client.get_prefix("name")foriteminresult:print(str(item[0],encoding="utf-8"),str(item[1].key,encoding="utf-8"))"""神乐七奈name神乐meaname1夏色祭name2亚绮罗森name33.租约

client=etcd3.Etcd3Client(host="47.94.174.89")#创建一个租约lease=client.lease(60)#创建key绑定在租约上client.put("age",b"28",lease=lease)#获取租约信息print(client.get_lease_info(lease.id))"""header{cluster_id:788639929760900286member_id:12186326826460735587revision:65raft_term:3}ID:7521986096354024026TTL:59grantedTTL:60keys:"age""""总体来说还是比较简单的,至于其它实现可以自己查看。

用户权限功能是在etcd2.1版本中新增加的功能,在2.1版本之前,etcd是一个完全开放的系统,任何用户都可以通过RESTAPI修改etcd存储的数据。etcd2.1版本中增加了用户(User)和角色(Role)的概念,引入了用户认证的功能,为了保持向后的兼容性和可升级性,etcd的用户权限功能默认是关闭的。

用户和角色,很好理解,用户扮演的角色不同,那么相应的权限也不同。

无论数据信道是否经过加密(SSL/TLS,后面会讨论),etcd都支持安全认证以及权限管理。etcd的权限管理借鉴了操作系统的权限管理思想,存在用户和角色两种权限管理方法。在操作系统中,默认存在一个超级管理员root(需要你自己创建),拥有最高权限,其余所有的用户权限都派生自root。

etcd认证体系分为User和Role,Role被授予给User,代表User拥有某种权利,至于权利有多大,则取决于Role到底是什么角色,是超级管理员、普通用户,还是其它的什么。而etcd的认证体系中有一个特殊的用户和角色,那就是root。

root用户用于对etcd访问的全部权限,并且必须在启动认证之前预先创建。而设置root用户的初衷是为了方便管理:管理角色和普通用户,root用户必须是root角色。

root角色可以授予任何用户,一旦某个用户被授予了root角色,它就拥有了全局的读写权限以及修改集群认证配置的权限。一般情况下,root角色所赋予的特权用于集群维护,例如修改集群member关系,存储碎片整理,做数据快照等。

而etcd包含三种类型的资源,具体如下:

1.User

User(用户)是一个被授予权限的身份,每一个用户都可以拥有多个角色(Role),用户操作资源的权限(例如读资源或写资源)是根据用户所具有的角色来确定的,而用户分为root用户和非root用户。

root用户是etcd提供的一个特殊用户,在安全功能被激活之前必须创建root用户,否则会无法启动身份认证功能。root用户具有root角色功能并允许对etcd内部进行任何操作。root用户的主要目的是为了进行恢复:会生成一个密码并存储在某个地方,并且被授予root角色来承担系统管理员的功能。root用户在我们对etcd集群进行故障排除和恢复时非常有用。

2.Role

Role(角色)用来关联权限,etcd中每个角色都具有相应的权限列表,这个权限列表定义了角色对键值资源的访问权限。

root角色具有对所有键值资源的完整权限,而且只有root角色具有管理用户资源和配置资源的权限(例如:修改etcd集群的成员信息)。root角色是内置的,不需要被创建而且不能被修改,但是可以授予任何用户相同的权限。也就是说root既是一个用户也是一个角色,其它的用户也可以具有root角色,那么一旦具备root角色的话和root用户就是差不多等价的了。

3.租约

etcd提供了两种类型的权限(permission):读和写,对权限的所有管理和设置都需要通过具有root角色的用户来实现。权限列表是一个许可的特定权限的列表,后面会说。

键值资源是指存储在etcd中的键值对信息,给定一个用于匹配的模式(pattern)列表,当用户请求的key值匹配到模式列表中的某项时,相应的权限便会被授予。

配置资源存放着整个集群的特定配置信息,包括添加/删除的集群成员、启动/禁用认证功能、替换证书和其它由管理员(root角色持有者)维护的动态配置信息等。

1.获取所有的User

[root@kano~]#etcdctluserlist[root@kano~]#当前没有任何的User。

2.创建一个User

#创建成功[root@kano~]#etcdctluseraddmeaPasswordofmea:Typepasswordofmeaagainforconfirmation:Usermeacreated[root@kano~]#[root@kano~]#etcdctluserlistmea[root@kano~]#3.授予用户对应的Role和撤销用户所拥有的Role(允许部分撤销)

#给mea添加角色,但是super显然不存在,这里只是演示命令[root@kano~]#etcdctlusergrant-rolemeasuperError:etcdserver:rolenamenotfound[root@kano~]##显然是失败的,因为super不是一个角色,也没有授予用户mea[root@kano~]#etcdctluserrevoke-rolemeasuperError:etcdserver:roleisnotgrantedtotheuser[root@kano~]#4.一个用户的详细信息可以通过下面的命令进行获取

#角色为空[root@kano~]#etcdctlusergetmeaUser:meaRoles:[root@kano~]#5.修改密码

[root@kano~]#etcdctluserpasswdmeaPasswordofmea:Typepasswordofmeaagainforconfirmation:Passwordupdated[root@kano~]#6.删除用户

1.列出所有的Role

[root@kano~]#etcdctlrolelist[root@kano~]#2.创建一个Role

[root@kano~]#etcdctlroleaddcommonRolecommoncreated[root@kano~]#一个角色没有密码,它定义了一组访问权限,etcd里的角色被授予访问一个或一个范围内的key。这个范围可以由一个区间[start-key,end-key],其中起始值start-key的字典序要小于结束值end-key。

访问权限可以是读、写或者可读可写,Role角色能够指定键空间下不同部分的访问权限,不过一次只能设置一个path或一组path(使用前缀+*来表示,相当于以某个字符串为开头)的访问权限。

3.授予对某个key只读权限

#授予name的只读权限[root@kano~]#etcdctlrolegrant-permissioncommonreadnameRolecommonupdated[root@kano~]#4.授予对一个范围的key只写权限

#授予a开头的key的只写权限[root@kano~]#etcdctlrolegrant-permissioncommonwriteabRolecommonupdated[root@kano~]#5.授予对一组key只写权限

#授予c开头的key的可读可写权限,需要加上--prefix[root@kano~]#etcdctlrolegrant-permissioncommonreadwritec--prefixRolecommonupdated[root@kano~]#6.查看一个角色具有的权限

[root@kano~]#etcdctlrolegetcommonRolecommonKVRead: c* nameKVWrite: [a,b)(prefixa) c*[root@kano~]#7.收回一个角色的某个权限

#收回对c*进行操作的权限,这里不需要指定读或写,显然是读写都收回[root@kano~]#etcdctlrolerevoke-permissioncommonc*Permissionofkeyc*isrevokedfromrolecommon#对c*进行操作的权限已经没了[root@kano~]#etcdctlrolegetcommonRolecommonKVRead: nameKVWrite: [a,b)(prefixa)[root@kano~]##收回一个本来就没有权限操作的key会报错[root@kano~]#etcdctlrolerevoke-permissioncommond*Error:etcdserver:permissionisnotgrantedtotherole[root@kano~]##只有读或写一种权限也可以,只要有权限,在收回的时候就不会报错[root@kano~]#etcdctlrolerevoke-permissioncommonnamePermissionofkeynameisrevokedfromrolecommon[root@kano~]##可以看到只剩下对[a,b)的写权限了[root@kano~]#etcdctlrolegetcommonRolecommonKVRead:KVWrite: [a,b)(prefixa)[root@kano~]#8.移除某个角色

1.确认root用户已经创建

[root@kano~]#etcdctluserlist[root@kano~]#etcdctluseraddrootPasswordofroot:Typepasswordofrootagainforconfirmation:Userrootcreated[root@kano~]#2.启用权限认证功能

#此时认证就开启了[root@kano~]#etcdctlauthenableAuthenticationEnabled[root@kano~]##也就不能随随便便地写了[root@kano~]#etcdctlputnamenanaError:etcdserver:usernameisempty[root@kano~]#etcdctlgetnameError:etcdserver:usernameisempty#如果想写的话,需要指定用户,然后会提示输入密码[root@kano~]#etcdctlputnamenana--user="root"Password:OK#也可以直接指定,通过user:password方式[root@kano~]#etcdctlputage16--user="root:123456"OK#读也是同理[root@kano~]#etcdctlgetname--user="root"Password:namenana#直接指定密码[root@kano~]#etcdctlgetage--user="root:123456"age16[root@kano~]#3.关闭权限认证功能

[root@kano~]#etcdctlauthdisableError:etcdserver:usernamenotfound#即便是关闭权限,也依旧需要指定一个用户[root@kano~]#etcdctlauthdisable--user="root:123456"AuthenticationDisabled[root@kano~]#这个时候可能有人好奇了,要是没有用户怎么办?答案是如果没有用户,etcd是不会允许你开启认证的,我们举个栗子。

[root@kano~]#etcdctluserdeleterootUserrootdeleted[root@kano~]#etcdctlauthenableError:etcdserver:rootuserdoesnotexist注意:我们说角色会被授予用户,而当我们开启认证的时候,会自动创建root角色并授予root用户。

#此时用户和角色都没有[root@kano~]#etcdctluserlist[root@kano~]#etcdctlrolelist#创建一个root,否则无法开启认证[root@kano~]#etcdctluseraddrootPasswordofroot:Typepasswordofrootagainforconfirmation:Userrootcreated[root@kano~]#etcdctluserlistroot#用户多了root,但是角色还不存在[root@kano~]#etcdctlrolelist#开启认证[root@kano~]#etcdctlauthenableAuthenticationEnabled#发现root角色自动被创建了[root@kano~]#etcdctlrolelist--user="root:123456"root而我们说root用户和root角色都是可以被删除的,但那是在没有开启认证的情况下,如果开启了认证呢?

etcd的传输层安全模型使用了常见的非对称加密模型,其由公开密钥、私钥和证书三部分组成。通信的基础是公私钥以及证书系统,因此,我们将首先介绍生成公/私钥以及证书的过程,然后利用生成好的公私钥和证书配置etcd传输层的安全。

关于etcd的传输安全,这里就不说了,有兴趣可以自己去了解,直接简单回顾一下TLS/SSL的工作原理吧。

最新版本的TLS(TransportLayerSecurity,传输层安全协议)是IETF(InternetEngi-neeringTaskForce,Internet工程任务组)制定的一种新协议,TLS建立在SSL3.0协议规范之上,是SSL3.0的后续版本。TLS与SSL3.0之间的差异主要是它们所支持的加密算法不同,但其基本原理相同。因此,下面将以SSL为例进行介绍。

SSL是一个安全协议,它为基于TCP/IP的通信应用程序提供了隐私与完整性。HTTPS便是使用SSL来实现安全通信的,在客户端与服务器之间传输的数据是通过对称算法(如DES、RC4)进行加密的。公用密钥算法(通常为RSA)是用来获得加密密钥交换和数字签名,此算法使用服务器的SSL数字证书中的公用密钥。有了服务器的SSL数字证书,客户端便可以验证服务器的身份了。

SSL连接总是由客户端启动的,在SSL会话开始时会先进行SSL握手。客户端和服务器端的SSL握手流程如下图所示:

1)客户端向服务器发送消息"你好"(以客户端首选项顺序排序),消息中包含SSL的版本、客户端支持的密码对(加密套件)和客户端支持的数据压缩方法(哈希函数)等。此外,还包含28字节的随机数。

2)服务器端以消息"你好"回应客户端,此消息包含密码方法(密码对)和由服务器选择的数据压缩方法,以及会话标识和另一个随机数。

客户端和服务器至少必须支持一个公共密码对,否则握手会失败,服务器一般选择最大的公共密码对。

3)服务器端向客户端发送其SSL数字证书(服务器使用带有SSL的X.509V3数字证书。如果服务器端需要通过数字证书与客户端进行认证,则客户端会发出"数字证书请求"的消息。在"数字证书请求"消息中,服务器端发出支持的客户端数字证书类型的列表和可接受的CA的名称。

4)服务器端发出"您好完成"的消息并等待客户端响应。

5)一接收到服务器的"您好完成"消息,客户端(Web浏览器)就会验证服务器的SSL数字证书的有效性,并检查服务器的"你好"消息参数是否可以接受。如果服务器请求客户端数字证书,那么客户端将发送其数字证书;如果没有合适的数字证书是可用的,那么客户端将发送"没有数字证书"的警告。此警告仅仅是警告而己,但是如果客户端数字证书认证是强制性的话,那么服务器应用程序将会使会话失败。

6)客户端发送"客户端密钥交换"消息,此消息包含pre-mastersecret(一个用于对称加密密钥生成中的46字节的随机数字)和消息认证代码(MAC)密钥(用服务器的公用密钥加密)。如果客户端向服务器发送了数字证书,客户端将发出签有客户端的专用密钥的"数字证书验证"消息,通过验证此消息的签名,服务器可以显示验证客户端数字证书的所有权。

如果服务器没有属于数字证书的专用秘钥,它将无法解密pre-master密码,也无法创建对称加密算法的正确密钥,而且握手也将失败。

7)客户端使用一系列的加密运算将pre-mastersecret转化为mastersecret,其中将派生出所有用于加密和消息认证的密钥。然后,客户端将发出"更改密码规范"消息将服务器转换为新协商的密码对。客户端发出的下一个消息("未完成"的消息)为使用此密码方法和密钥加密的第一条消息。

8)服务器以自己的"更改密码规范"和"已完成"消息进行响应。

9)SSL握手结束,并且可以发送加密的应用程序数据。

在数据库领域,并发控制是一个很具有挑战性的问题,常见的并发控制方式包括悲观并发控制、乐观并发控制和多版本并发控制。

在关系数据库管理系统中,悲观并发控制(又名"悲观锁",PessimisticConcurrencyControl,PCC)是一种并发控制的方法,它能以阻止其它事务的方式来修改数据。如果事务执行的操作对某行数据应用了锁,那么只有在这个事务将锁释放之后,其他事务才能够执行与该锁冲突的操作。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本。

乐观并发控制(又名"乐观锁")也是一种并发控制的方法,它假设多用户并发的事务在处理时彼此之间不会互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务都会先检查在该事务读取数据之后,有没有其它事务又修改了该数据,如果其它事务有更新的话,那么正在提交的事务会进行回滚。

乐观并发控制多用于数据争用不大、冲突较少的环境,在这种环境中,偶尔回滚事务的成本会低于读取数据时锁定数据的成本,因此这种情况下乐观并发控制可以获得比其它并发控制方法更高的吞吐量。

对一个系统进行各种优化时,相应的思路其实并不是凭空产生的,而是有方法论的,首先我们应该分析etcd的使用场景,然后才能进行针对性的优化。首先我们知道etcd的定位是一个分布式的、一致的key-value存储,主要用途是共享配置和服务发现,它不是一个类似于ceph那样存储海量数据的存储系统,也不是类似于MySQL这样的SQL数据库。它存储的其实是一些非常重要的元数据,当然,元数据的写操作其实是比较少的,但是会有很多的客户端同时watch这些元数据的变更。也就是说etcd的使用场景是一种"读多写少"的场景,etcd里的一个key,其实并不会发生频繁的变更,但是一旦发生变更,etcd就需要通知监控这个key的所有客户端。

多版本并发控制(Multi-VersionConcurrencyControl,MVCC)则以一种优雅的方式解决了锁带来的问题。在MVCC中,每当想要更改或者删除某个数据对象时,DBMS不会在原地删除或修改这个已有的数据对象本身,而是针对该数据对象创建一个新的版本,这样一来,并发的读取操作仍然可以读取老版本的数据,而写操作就可以同时进行。这个模式的好处在于,可以让读取操作不再阻塞,事实上根本就不需要锁。这是种非常诱人的特性,以至于很多主流的数据库中都采用了MVCC的实现,比如MySQL、PostgreSQL、Oracle、MicrosoftSQLServer等等。

可能有人会有疑问,既然整个数据库使用一把Stop-the-World大锁会导致并发上不去,那么如果换成每个key一把锁是不是就可以了呢?MVCC方案与这种一个key一把锁的方案相比又有什么优势呢?其实即使每个key一把锁,写锁也是会阻塞读锁的(写的时候不能读),而MVCC在写的时候也是可以并发读的,因为写是在最新的版本上进行写的,读却可以读老的版本(客户端读key的时候可以指定一个版本号,服务端保证能返回基于此版本号的新数据,而不是保证返回最新的数据)。

总而言之,MVCC能最大化地实现高效的读写并发,尤其是高效的读,因此其非常适合etcd这种"读多写少"的场景。

我们先来简单回顾一下etcdv2的存储和持久化机制,etcdv2是一个纯内存数据库,写操作先通过Raft复制日志文件,复制成功后将数据写入内存,整个数据库在内存中是一个简单的树结构。etcdv2并未实时地将数据写入磁盘,持久化是靠快照来实现的,具体实现就是将整个内存中的数据复制一份出来,然后序列化成JSON,写入磁盘中,成为一个快照。做快照的时候使用的是复制出来的数据库,客户端的读写请求依旧落在原始的数据库上,这样的话,做快照的操作才不会阻塞客户端的读写请求。

etcdv3存储的逻辑视图是一个扁平的二进制键空间,该键空间对key有一个词法排序索引,因此范围查询的成本很低。

etcd的键空间可维护多个revision,每个原子的修改操作(例如,一个事务操作可能包含多个操作)都会在键空间上创建一个新的revision,之前revision的所有数据均保持不变。旧版本(version)的key仍然可以通过之前的revision进行访问,同样,revision也是被索引的,因此Watcher可以实现高效的范围watch。revision在etcd中可以起到逻辑时钟的作用,revision在群集的生命周期内是单调递增的,如果因为要节省空间而压缩键空间,那么在此revision前的所有revision都将被删除,只保留该revision之后的。

我们将key的创建和删除过程称为一个生命周期,在etcd中,每个key都可能有多个生命周期,也就是说被创建、删除多次。创建一个新key时,如果在当前revision中该key不存在(即之前也没有创建过),那么它的version就会被设置成1;删除key会生成一个key的墓碑,可通过将其version重置为0来结束key的当前生命周期。对key的每一次修改都会增加其version,因此,key的version在key的一次生命周期中是单调递增的。下面让我们来看revision和version在etcdv3中是如何实现的吧。

etcdv3的请求响应的header数据结构具体如下所示:

而etcdv3的最核心的键值对数据结构的定义具体如下所示:

revison是集群存储状态的版本号,存储状态的每一次更新(例如写、删除、事务等)都会让revison的值加1。ResponseHeader.Revision代表该请求成功执行之后etcd的revision,KeyValue.CreateRevision代表etcd的某个key最后一次创建时etcd的revison,KeyValue.ModRevision则代表etcd的某个key最后一次更新时etcd的revison。verison特指etcd键空间某个key从创建开始被修改的次数,即KeyValue.Version,etcdv3支持的Get(…,WithRev(rev))操作会获取etcd处于rev这个revision时的数据,就好像etcdrevision还是rev的时候一样。

etcd将物理数据存储为一棵持久B+树中的键值对,为了高效,每个revision的存储状态都只包含相对于之前revision的增量,一个revision可能对应于树中的多个key。

B+树中键值对的key即revision,revision是一个二元组(main,sub),其中main是该revision的主版本号,sub是同一revision的副版本号,其用于区分同一个revision的不同key。B+树中键值对的value包含了相对于之前revision的修改,即相对于之前revision的一个增量。

B+树按key的字典字节序进行排序,这样etcdv3对revision增量的范围查询(rangequery,即从某个revision到另一个revision)会很快。因为我们已经记录了从一个特定revision到其它revision的修改量,而且etcdv3的压缩操作会删除过时的键值对。

etcdv3还在内存中维护了一个基于B树的二级索引来加快对key的范围查询,该B树索引的key是向用户暴露的etcdv3存储的key,而该B树索引的value则是一个指向上文讨论的持久化B+树的增量的指针。而且etcdv3的压缩操作会删除指向B树索引的无效指针。

etcdv2的每个key只保留一个value,所以数据库并不大,可以直接放在内存中。但是etcdv3实现了MVCC以后,每个key的value都需要保存多个历史版本,这就极大地增加了存储的数据量,因此内存中就会存储不下这么多数据。对此,一个自然的解决方案就是将数据存储在磁盘里,etcdv3当前使用BoltDB将数据存储在磁盘中。

BoltDB是根据HowardChu的LMDB项目开发的一个纯粹的Go语言版的key/value存储,它的目标是为项目提供一个简单、高效可靠的嵌入式的、可序列化的键/值数据库,而不是要求一个像MySQL那样完整的数据库服务器。BoltDB还是一个支持事务的键值存储,etcd的事务就是基于BoltDB的事务实现的。

用作者的话说,BoltDB只提供简单的key/value存储,没有其他的特性,以后也不会有。因此BoltDB可以做到代码精简(小于3KB),质量高,非常适合以BoltDB为基础在其之上构建更加复杂的数据库功能。由于BoltDB的设计适合"读多写少"的场景,因此其也非常适合于etcd。

etcd在BoltDB中存储的key是reversion,value是etcd自己的key-value组合,也就是说etcd会在BoltDB中保存每个版本,从而实现多版本机制。

底层的存储引擎一般包含如下三大类的选择:

其中SQLLite支持ACID事务,但是作为一个关系型数据库,SQLLite主要定位于提供高效灵活的SQL查询语句支持,可以支持复杂的联表查询等。而etcd只是一个简单的KV数据库,并不需要复杂的SQL支持。

LevelDB和RocksDB分别是Google和Facebook开发的存储引擎,RocksDB是在LevelDB的基础上针对Flash设备做了优化,其底层实现原理都是log-structuredmerge-tree(LSMtree)。就是将有序的key/value存储在不同的文件中,并通过"层级"将它们分开,并且周期性地将小的文件合并为更大的文件,这样做就能把随机写转化为顺序写,从而提高随机写的性能,因此特别适合"写多读少"和"随机写多"的场景。同时需要注意的是,LevelDB和RocksDB都不支持完整的ACID事务。

而LMDB和BoltDB则是基于B树和mmap的数据库,基本原理是用mmap将磁盘的page映射到内存的page,而操作系统则是通过COW(copy-on-write)技术进行page管理,通过cow技术,系统可实现无锁的读写并发,但是无法实现无锁的写写并发,这就注定了这类数据库读性能超高,但写性能一般,因此非常适合于"读多写少"的场景。同时BoltDB支持完全可序列化的ACID事务,因此最适合作为etcd的底层存储引擎。

THE END
1.三体联想程心.圣母心.同理心“如果我竞选,你们认为有可能成功?”程心问。从智子那里回来后,这个问题一直萦绕在她的脑际,几乎使她彻夜未眠。 “如果你那么做,几乎肯定能成功。”伊万·安东诺夫说,这个英俊的俄罗斯人是候选人中除曹彬外最年轻的,四十三岁,却资历非凡。他曾是俄罗斯最年轻的海军中将,官至波罗的海舰队副司令,因绝症而冬眠。 https://www.jianshu.com/p/1c05a8e28401
2.学生会主席竞选个人演讲稿第三, 假如竞选成功,我将怎么做? 长期以来,院学生会与各系的沟通交流是过少的。假如我竞选成功我将加强院与各系学生会的交流,克服相互间脱节的情况。其次,协调好学生会内部成员之间的关系,特别是与师兄师姐们的关系,各成员是社会人而不是经济人,他们需要更多的是社会与心理上的关怀,应该扩大关怀维度,让他们有更...https://m.zuowendang.com/fanwen/1731933540.html
1.2024年竞聘稿范文联创号最后我想说的是:如果我竞聘成功了,我将以自己勤劳的双手不遗余力地做好本职工作,用最好的成绩来回报大家的厚爱;如果我失败了,也许是我不够优秀,尽管是如此钟情于她,我依然会潇洒地挥挥手。不管以后在什么岗位上,我都会一如既往的秉承财务人求真、务实、严谨的工作态度,默默奉献自己的微薄力量。 https://www.lian-bj.com/lc/564295.html
2.岗位竞聘演讲稿(精选23篇)因为我当时还没有毕业,也没有什么经验,所以学校让我带的是比较容易的大班。但现在,我认为自己不仅可以带大班,也能够带好小班。不论学校分配我到哪个年级,我都能够接受,并把工作做好。当然,我也还有很多需要学习的地方,所以若是我成功进入了幼儿园,希望有经验的老师能够多多帮助我,我也会努力学习,早日独当一面。https://www.oh100.com/a/202212/5740028.html
3.(通用)竞聘校长演讲稿19篇⑺建立日常工作规章制度,调动处室人员的工作积极性,团结处室人员做好日常工作,保证教育教学工作的正常开展。 以上,是我参加公开竞聘演讲的主要内容,如果组织上给予信任,我将按照我的指导思想和工作思路,以我的热情,使我校总务处工作更上一层楼,如果没有聘我,我也将一如既往地努力工作,为祖国的教育事业和长安小学的...https://www.yjbys.com/yanjianggao/fayangao/4380149.html
4.竞聘领班演讲稿范文(精选18篇)如果竞聘成功,我将认真履行领班的职责,切实承担好自己所肩负的使命,不辜负领导的信任和期待。在过去两年半的销售与管理工作中,我深得老板同事顾客的好评与认可,我相信在我的坚持下我也一定得到重百各位领导的赏识,因为我认真努力,喜欢把别人的事业当成自己的事业来做!我非常以我是重百人,我是丸美柜员为荣,所以我工...https://www.jy135.com/fayangao/1485078.html
5.中班运动课程:足球宝贝(如东县栟茶镇栟茶幼儿园王陆宇)队长竞选日今天是足球队长竞选日,孩子们会做什么样的竞选演说?又用什么样的方式选出队长?请跟我一起走进竞争激烈的竞选现场。选手们个个主意多多、伶牙俐齿、能言善辩! 制作球衣新年到了,孩子们的愿望不是新年礼物,不是玩游戏,而是要举办一场足球比赛。好,接下来让我们一起做准备吧。什么材料易于孩子们自己制作...https://fzzx.rdedu.net/Item/3762.aspx
6.竞选成功感言(通用12篇)在现实生活或工作学习中,我们心中时常会积累了不少感想和见解,这时就可以写一篇感言,用以表达所思所想。应该怎么写感言才合适呢?下面是小编整理的竞选成功感言(通用12篇),希望对大家有所帮助。 竞选成功感言 篇1 各位同学,各位老师: 大家好!我是六(12)班的叶洋悦同学。这次我能当上大队委,离不开老师、同学们...https://mip.ruiwen.com/shiyongwen/ganyan/2379128.html
7.小学一年级竞选班长演讲稿(精选10篇)想法2:请班上学习和纪律好的同学为大家讲一讲,在学习和纪律上他们平时是怎样想、怎样做的,他们的成功秘诀是什么。 想法3:我还想请同学们的爸爸妈妈来我们101班,一起夸一夸自己的孩子,好让大家都来互相学习。 最后我会尽我最大的力量,来做好班长的工作,我有信心,我一定能当个好班长。如果这次竞选不成功,我...https://www.gdyjs.com/shiyongwen/yanjianggao/69288.html
8.一年级竞选演讲稿15篇在社区里,我是一名社区活动积极分子,经常参加居委会组织 的各项社区活动,为社区出一份力,使我们居住的环境更加美好。 如果我竞选成功,我一定会为班级出谋划策,尽心尽力的.工作,努力做好老师的得力小助手,还会全心全意地为同学们服务。并且改正自身存在的种种缺点,做好同学们的领头雁。 https://www.cnfla.com/yanjianggao/2980228.html
9.豆瓣我又买鲜花啦 126005 个成员 人间情侣观察 203787 个成员 电影票房·资料库 49689 个成员 豆瓣奶茶小组 129581 个成员 王者荣耀 165267 个成员 女子推理社 5955 个成员 如果我们可以不通过消费获得快乐 220434 个成员 今天跟团团团了吗 39971 个成员 https://www.douban.com/
10.竞选班级劳动委员演讲稿(精选10篇)我的优点说完了,希望大家投我一票。我一定会做好这个劳动委员的。 竞选班级劳动委员演讲稿 篇2 敬爱的老师,亲爱的同学们: 大家好! 我竞选劳动卫生委员。 我这个人热爱劳动,劳动没有轻活、重活、脏活、累活之分。有这样一句话——只有丢人的窝囊废,没有丢人的职业!打扫教室、打扫卫生区、打扫走廊等等等等,很多...https://www.yuwenmi.com/fanwen/yanjianggao/519775.html
11.有关竞聘演讲稿(精选19篇)很荣幸能站在这里参加这次竞选,首先我做下自我介绍:我是来自12班的xxx,今天我竞选的职务是编辑部部长。竞选编辑部部长对我来说是一次不小的'挑战,大家精彩的演讲也给了我不少的压力。然而面对今天的挑战,这些压力,我只能尽力去争取,无论成功或失败,我要做到的是无愧于己,无愧于心。 https://www.fwsir.com/yanjiang/html/yanjiang_20210207202646_695469.html
12.特朗普:中国人的抱负之高超乎你们想像特朗普:我看过很多关于我的报道,包括你们《华盛顿邮报》,很多都是错的,里面充满了仇恨,其实我不是个坏人,我只是在做我份内的事,我在竞选美国总统,这并不容易,当然截至目前我的人生是很圆满的。有人说“你要是特别成功就不会出来竞选了”,我理解他话里的意思。你干了一百件好事,就犯了一两个错误,而人们就会...https://www.guancha.cn/DonaldTrump/2016_03_26_355094_s.shtml
13.五年级竞选班长发言稿7篇我之所以竞选班长职务,是因为我的各科学习成绩都很优秀,是同学们的好朋友,老师的得力小助手。我的领导能力也很好,可以带动全班同学一起学习。我热爱这个班集体,所以我的责任心强,对老师给我布置的工作都会认真负责、一丝不苟地去完成,从不拒绝。 如果竞选成功,我会努力做好以下三点: ...https://www.liuxue86.com/a/4942024.html