精华内容
下载资源
问答
  • 简单了解ETCD的作用

    千次阅读 2019-05-08 18:54:19
    ETCD是一个键值(key-value)存储仓库,相当于分布式存储数据库,用于共享配置和服务发现。 二.主要特点 1.简单:基于HTTP+JSON的API让你用curl就可以轻松使用。 2.安全:可选SSL客户认证机制。 3.快速:每个实例每...

    一.定义
    ETCD是一个键值(key-value)存储仓库,相当于分布式存储数据库,用于共享配置和服务发现。

    二.主要特点
    1.简单:基于HTTP+JSON的API让你用curl就可以轻松使用。
    2.安全:可选SSL客户认证机制。
    3.快速:每个实例每秒支持一千次写操作。
    4.可信:使用Raft算法充分实现了分布式。
    默认使用的场景:
    分布式系统中的数据分为控制数据和应用数据。使用etcd的场景默认处理的数据都是控制数据,对于应用数据,只推荐数据量很小,但是更新访问频繁的情况。

    三.在部分场景的实际运用
    1.服务发现
    本质上来说,服务发现就是想要了解集群中是否有进程在监听udp或tcp端口,并且通过名字就可以查找和连接。要解决服务发现的问题,需要有下面三大支柱,缺一不可。
    (1)一个强一致性、高可用的服务存储目录。基于Raft算法的etcd天生就是这样一个强一致性高可用的服务存储目录。
    (2)一种注册服务和监控服务健康状态的机制。用户可以在etcd中注册服务,并且对注册的服务设置key TTL,定时保持服务的心跳以达到监控健康状态的效果。
    (3)一种查找和连接服务的机制。通过在etcd指定的主题下注册的服务也能在对应的主题下查找到。为了确保连接,我们可以在每个服务机器上都部署一个Proxy模式的etcd,这样就可以确保能访问etcd集群的服务都能互相连接。
    2.消息发布与订阅
    在分布式系统中,最适用的一种组件间通信方式就是消息发布与订阅。即构建一个配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅他们关心的主题,一旦主题有消息发布,就会实时通知订阅者。通过这种方式可以做到分布式系统配置的集中式管理与动态更新。
    (1)**应用中用到的一些配置信息放到etcd上进行集中管理。**这类场景的使用方式通常是这样:应用在启动的时候主动从etcd获取一次配置信息,同时,在etcd节点上注册一个Watcher并等待,以后每次配置有更新的时候,etcd都会实时通知订阅者,以此达到获取最新配置信息的目的。
    (2)**分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在etcd中,供各个客户端订阅使用。**使用etcd的key TTL功能可以确保机器状态是实时更新的。
    分布式日志收集系统。这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用(或主题)来分配收集任务单元,因此可以在etcd上创建一个以应用(主题)命名的目录P,并将这个应用(主题相关)的所有机器ip,以子目录的形式存储到目录P上,然后设置一个etcd递归的Watcher,递归式的监控应用(主题)目录下所有信息的变动。这样就实现了机器IP(消息)变动的时候,能够实时通知到收集器调整任务分配。
    (3)**系统中信息需要动态自动获取与人工干预修改信息请求内容的情况。**通常是暴露出接口,例如JMX接口,来获取一些运行时的信息。引入etcd之后,就不用自己实现一套方案了,只要将这些信息存放到指定的etcd目录中即可,etcd的这些目录就可以通过HTTP的接口在外部访问。
    3.负载均衡
    在场景一中也提到了负载均衡,本文所指的负载均衡均为软负载均衡。分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署多份,以此达到对等服务,即使其中的某一个服务失效了,也不影响使用。由此带来的坏处是数据写入性能下降,而好处则是数据访问时的负载均衡。因为每个对等服务节点上都存有完整的数据,所以用户的访问流量就可以分流到不同的机器上。
    (1)etcd本身分布式架构存储的信息访问支持负载均衡。etcd集群化以后,每个etcd的核心节点都可以处理用户的请求。所以,把数据量小但是访问频繁的消息数据直接存储到etcd中也是个不错的选择,如业务系统中常用的二级代码表(在表中存储代码,在etcd中存储代码所代表的具体含义,业务系统调用查表的过程,就需要查找表中代码的含义)。
    (2)利用etcd维护一个负载均衡节点表。etcd可以监控一个集群中多个节点的状态,当有一个请求发过来后,可以轮询式的把请求转发给存活着的多个状态。

    四.小结
    ETCD是个键值存储数据库,主要功能和特点总结如下:
    (1)可对在ETCD上注册的服务进行监控健康状态,并及时反馈
    (2)确保访问ETCD的集群都能正常连接和访问
    (3)应用中用到的一些配置信息放到etcd上进行集中管理
    (4)分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在etcd中,供各个客户端订阅使用。
    (5)etcd的核心节点都可以处理用户的请求。
    (6)ETCD监控多个节点,有请求发过来后,根据节点的情况进行分配派发。

    展开全文
  • ETCD 简介 + 使用

    万次阅读 2016-12-05 14:19:57
    随着CoreOS和Kubernetes等项目在开源社区日益火热,它们项目中都用到的etcd组件作为一个高可用、强一致性的服务发现存储仓库,渐渐为开发人员所关注。在云计算时代,如何让服务快速透明地接入到计算集群中,如何让...

    随着CoreOS和Kubernetes等项目在开源社区日益火热,它们项目中都用到的etcd组件作为一个高可用、强一致性的服务发现存储仓库,渐渐为开发人员所关注。在云计算时代,如何让服务快速透明地接入到计算集群中,如何让共享配置信息快速被集群中的所有机器发现,更为重要的是,如何构建这样一套高可用、安全、易于部署以及响应快速的服务集群,已经成为了迫切需要解决的问题。etcd为解决这类问题带来了福音,本章将从etcd的应用场景开始,深入解读etcd的实现方式,以供开发者们更为充分地享用etcd所带来的便利。

    1 etcd经典应用场景

    etcd是什么?很多人对这个问题的第一反应可能是,它是一个键值存储仓库,却没有重视官方定义的后半句,用于配置共享和服务发现。

    A highly-available key value store for shared configuration and service discovery.

    实际上,etcd作为一个受到Zookeeper与doozer启发而催生的项目,除了拥有与之类似的功能外,更具有以下4个特点{![引自Docker官方文档]}。

    • 简单:基于HTTP+JSON的API让你用curl命令就可以轻松使用。
    • 安全:可选SSL客户认证机制。
    • 快速:每个实例每秒支持一千次写操作。
    • 可信:使用Raft算法充分实现了分布式。

    随着云计算的不断发展,分布式系统中涉及的问题越来越受到人们重视。受阿里中间件团队对ZooKeeper典型应用场景一览一文的启发{![部分案例引自此文。]},我根据自己的理解也总结了一些etcd的经典使用场景。值得注意的是,分布式系统中的数据分为控制数据和应用数据。使用etcd的场景处理的数据默认为控制数据,对于应用数据,只推荐处理数据量很小,但是更新访问频繁的情况。

    1.1 场景一:服务发现

    服务发现(Service Discovery)要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。从本质上说,服务发现就是想要了解集群中是否有进程在监听udp或tcp端口,并且通过名字就可以进行查找和连接。要解决服务发现的问题,需要有下面三大支柱,缺一不可。

    • 一个强一致性、高可用的服务存储目录。基于Raft算法的etcd天生就是这样一个强一致性高可用的服务存储目录。
    • 一种注册服务和监控服务健康状态的机制。用户可以在etcd中注册服务,并且对注册的服务设置key TTL,定时保持服务的心跳以达到监控健康状态的效果。
    • 一种查找和连接服务的机制。通过在etcd指定的主题下注册的服务也能在对应的主题下查找到。为了确保连接,我们可以在每个服务机器上都部署一个proxy模式的etcd,这样就可以确保能访问etcd集群的服务都能互相连接。

    图1所示为服务发现示意图。 enter image description here

    图1 服务发现示意图

    下面我们来看一下服务发现对应的具体应用场景。

    • 微服务协同工作架构中,服务动态添加。随着Docker容器的流行,多种微服务共同协作,构成一个功能相对强大的架构的案例越来越多。透明化的动态添加这些服务的需求也日益强烈。通过服务发现机制,在etcd中注册某个服务名字的目录,在该目录下存储可用的服务节点的IP。在使用服务的过程中,只要从服务目录下查找可用的服务节点进行使用即可。 微服务协同工作如图2所示。 enter image description here

    图2 微服务协同工作

    • PaaS平台中应用多实例与实例故障重启透明化。PaaS平台中的应用一般都有多个实例,通过域名,不仅可以透明地对多个实例进行访问,而且还可以实现负载均衡。但是应用的某个实例随时都有可能故障重启,这时就需要动态地配置域名解析(路由)中的信息。通过etcd的服务发现功能就可以轻松解决这个动态配置的问题,如图33所示。

    enter image description here

    图3 云平台多实例透明化

    1.2 场景二:消息发布与订阅

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

    • 应用中用到的一些配置信息存放在etcd上进行集中管理。这类场景的使用方式通常是这样的:应用在启动的时候主动从etcd获取一次配置信息,同时,在etcd节点上注册一个Watcher并等待,以后每次配置有更新的时候,etcd都会实时通知订阅者,以此达到获取最新配置信息的目的。
    • 分布式搜索服务中,索引的元信息和服务器集群机器的节点状态信息存放在etcd中,供各个客户端订阅使用。使用etcd的key TTL功能可以确保机器状态是实时更新的。
    • 分布式日志收集系统。这个系统的核心工作是收集分布在不同机器上的日志。收集器通常按照应用(或主题)来分配收集任务单元,因此可以在etcd上创建一个以应用(或主题)命名的目录P,并将这个应用(或主题)相关的所有机器ip,以子目录的形式存储在目录P下,然后设置一个递归的etcd Watcher,递归式地监控应用(或主题)目录下所有信息的变动。这样就实现了在机器IP(消息)发生变动时,能够实时通知收集器调整任务分配。
    • 系统中信息需要动态自动获取与人工干预修改信息请求内容的情况。通常的解决方案是对外暴露接口,例如JMX接口,来获取一些运行时的信息或提交修改的请求。而引入etcd之后,只需要将这些信息存放到指定的etcd目录中,即可通过HTTP接口直接被外部访问。

    enter image description here

    图4 消息发布与订阅

    1.3 场景三:负载均衡

    场景一中也提到了负载均衡,本文提及的负载均衡均指软负载均衡。在分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署多份,以此达到对等服务,即使其中的某一个服务失效了,也不影响使用。这样的实现虽然会导致一定程度上数据写入性能的下降,但是却能实现数据访问时的负载均衡。因为每个对等服务节点上都存有完整的数据,所以用户的访问流量就可以分流到不同的机器上。

    • etcd本身分布式架构存储的信息访问支持负载均衡。etcd集群化以后,每个etcd的核心节点都可以处理用户的请求。所以,把数据量小但是访问频繁的消息数据直接存储到etcd中也是个不错的选择,如业务系统中常用的二级代码表。二级代码表的工作过程一般是这样,在表中存储代码,在etcd中存储代码所代表的具体含义,业务系统调用查表的过程,就需要查找表中代码的含义。所以如果把二级代码表中的小量数据存储到etcd中,不仅方便修改,也易于大量访问。
    • 利用etcd维护一个负载均衡节点表。etcd可以监控一个集群中多个节点的状态,当有一个请求发过来后,可以轮询式地把请求转发给存活着的多个节点。类似KafkaMQ,通过Zookeeper来维护生产者和消费者的负载均衡。同样也可以用etcd来做Zookeeper的工作。

    fuz

    图5 负载均衡

    1.4 场景四:分布式通知与协调

    这里讨论的分布式通知与协调,与消息发布和订阅有些相似。两者都使用了etcd中的Watcher机制,通过注册与异步通知机制,实现分布式环境下不同系统之间的通知与协调,从而对数据变更进行实时处理。实现方式通常为:不同系统都在etcd上对同一个目录进行注册,同时设置Watcher监控该目录的变化(如果对子目录的变化也有需要,可以设置成递归模式),当某个系统更新了etcd的目录,那么设置了Watcher的系统就会收到通知,并作出相应处理。

    • 通过etcd进行低耦合的心跳检测。检测系统和被检测系统通过etcd上某个目录关联而非直接关联起来,这样可以大大减少系统的耦合性。
    • 通过etcd完成系统调度。某系统有控制台和推送系统两部分组成,控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台做的一些操作,实际上只需要修改etcd上某些目录节点的状态,而etcd就会自动把这些变化通知给注册了Watcher的推送系统客户端,推送系统再做出相应的推送任务。
    • 通过etcd完成工作汇报。大部分类似的任务分发系统,子任务启动后,到etcd来注册一个临时工作目录,并且定时将自己的进度进行汇报(将进度写入到这个临时目录),这样任务管理者就能够实时知道任务进度。

    enter image description here

    图6 分布式协同工作

    1.5 场景五:分布式锁

    因为etcd使用Raft算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。

    • 保持独占,即所有试图获取锁的用户最终只有一个可以得到。etcd为此提供了一套实现分布式锁原子操作CAS(CompareAndSwap)的API。通过设置prevExist值,可以保证在多个节点同时创建某个目录时,只有一个成功,而该用户即可认为是获得了锁。
    • 控制时序,即所有试图获取锁的用户都会进入等待队列,获得锁的顺序是全局唯一的,同时决定了队列执行顺序。etcd为此也提供了一套API(自动创建有序键),对一个目录建值时指定为POST动作,这样etcd会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)。同时还可以使用API按顺序列出所有当前目录下的键值。此时这些键的值就是客户端的时序,而这些键中存储的值可以是代表客户端的编号。

    enter image description here

    图7 分布式锁

    1.6 场景六:分布式队列

    分布式队列的常规用法与场景五中所描述的分布式锁的控制时序用法类似,即创建一个先进先出的队列,保证顺序。

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

    • condition可以表示队列大小。比如一个大的任务需要很多小任务就绪的情况下才能执行,每次有一个小任务就绪,就给这个condition数字加1,直到达到大任务规定的数字,再开始执行队列里的一系列小任务,最终执行大任务。
    • condition可以表示某个任务在不在队列。这个任务可以是所有排序任务的首个执行程序,也可以是拓扑结构中没有依赖的点。通常,必须执行这些任务后才能执行队列中的其他任务。
    • condition还可以表示其它的一类开始执行任务的通知。可以由控制程序指定,当condition出现变化时,开始执行队列任务。

    enter image description here 图8 分布式队列

    1.7 场景七:集群监控与LEADER竞选

    通过etcd来进行监控实现起来非常简单并且实时性强,用到了以下两点特性。

    1. 前面几个场景已经提到Watcher机制,当某个节点消失或有变动时,Watcher会第一时间发现并告知用户。
    2. 节点可以设置TTL key,比如每隔30s向etcd发送一次心跳使代表该节点仍然存活,否则说明节点消失。

    这样就可以第一时间检测到各节点的健康状态,以完成集群的监控要求。

    另外,使用分布式锁,可以完成Leader竞选。对于一些长时间CPU计算或者使用IO操作,只需要由竞选出的Leader计算或处理一次,再把结果复制给其他Follower即可,从而避免重复劳动,节省计算资源。

    Leader应用的经典场景是在搜索系统中建立全量索引。如果每个机器分别进行索引的建立,不但耗时,而且不能保证索引的一致性。通过在etcd的CAS机制竞选Leader,由Leader进行索引计算,再将计算结果分发到其它节点。

    enter image description here 图9 Leader竞选

    1.8 场景八:为什么用ETCD而不用ZOOKEEPER?

    阅读了“ZooKeeper典型应用场景一览”一文的读者可能会发现,etcd实现的这些功能,Zookeeper都能实现。那么为什么要用etcd而非直接使用Zookeeper呢?4

    相较之下,Zookeeper有如下缺点。

    1. 复杂。Zookeeper的部署维护复杂,管理员需要掌握一系列的知识和技能;而Paxos强一致性算法也是素来以复杂难懂而闻名于世;另外,Zookeeper的使用也比较复杂,需要安装客户端,官方只提供了java和C两种语言的接口。
    2. Java编写。这里不是对Java有偏见,而是Java本身就偏向于重型应用,它会引入大量的依赖。而运维人员则普遍希望机器集群尽可能简单,维护起来也不易出错。
    3. 发展缓慢。Apache基金会项目特有的“Apache Way”在开源界饱受争议,其中一大原因就是由于基金会庞大的结构以及松散的管理导致项目发展缓慢。

    而etcd作为一个后起之秀,其优点也很明显。

    1. 简单。使用Go语言编写部署简单;使用HTTP作为接口使用简单;使用Raft算法保证强一致性让用户易于理解
    2. 数据持久化。etcd默认数据一更新就进行持久化。
    3. 安全。etcd支持SSL客户端安全认证。

    最后,etcd作为一个年轻的项目,正在高速迭代和开发中,这既是一个优点,也是一个缺点。优点在于它的未来具有无限的可能性,缺点是版本的迭代导致其使用的可靠性无法保证,无法得到大项目长时间使用的检验。然而,目前CoreOS、Kubernetes和Cloudfoundry等知名项目均在生产环境中使用了etcd,所以总的来说,etcd值得你去尝试。

    1:https://github.com/coreos/etcd

    2:http://jm-blog.aliapp.com/?p=1232

    3:http://progrium.com/blog/2014/07/29/understanding-modern-service-discovery-with-docker/

    4:http://devo.ps/blog/zookeeper-vs-doozer-vs-etcd

    2 深度解析etcd

    上一节中,我们概括了许多etcd的经典场景,这一节,我们将从etcd的架构开始,深入到源码中解析etcd。

    2.1 ETCD架构

    enter image description here 图10 etcd架构图

    从etcd的架构图中我们可以看到,etcd主要分为四个部分。

    • HTTP Server: 用于处理用户发送的API请求以及其它etcd节点的同步与心跳信息请求。
    • Store:用于处理etcd支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是etcd对用户提供的大多数API功能的具体实现。
    • Raft:Raft强一致性算法的具体实现,是etcd的核心。
    • WAL:Write Ahead Log(预写式日志),是etcd的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd还通过WAL进行持久化存储。WAL中,所有的数据提交前都会事先记录日志。Snapshot是为了防止数据过多而进行的状态快照;Entry则表示存储的具体日志内容。

    通常,一个用户的请求发送过来,会经由HTTP Server转发给Store进行具体的事务处理,如果涉及到节点的修改,则交给Raft模块进行状态的变更、日志的记录,然后再同步给别的etcd节点以确认数据提交,最后进行数据的提交,再次同步。

    2.2 ETCD2.0.0区别于0.4.6的重要变更列表

    • 获得了IANA认证的端口,2379用于客户端通信,2380用于节点通信,与原先的(4001 peers/7001 clients)共用。
    • 每个节点可监听多个广播地址。监听的地址由原来的一个扩展到多个,用户可以根据需求实现更加复杂的集群环境,如一个是公网IP,一个是虚拟机(容器)之类的私有IP。
    • etcd任意节点均可代理访问Leader节点的请求,所以如果你可以访问任何一个etcd节点,那么就可以对整个集群进行读写操作,而不需要刻意关注网络的拓扑结构。
    • etcd集群和集群中的节点都有了自己独特的ID。这样就防止出现配置混淆,不是本集群的其他etcd节点发来的请求将被屏蔽。
    • etcd配置信息在集群启动时就完全固定,这样有助于用户确认集群大小,正确地配置和启动集群。
    • 运行时节点变更 (Runtime Reconfiguration)。用户不需要重启 etcd 服务即可实现对 etcd 集群结构进行变更,可以动态操作。
    • 重新设计和实现了Raft算法,使得运行速度更快,更容易理解,包含更多测试代码。
    • Raft日志现在是严格的只能向后追加、预写式日志系统,并且在每条记录中都加入了CRC校验码。
    • 启动时使用的_etcd/* 关键字不再暴露给用户
    • 废弃使用集群自动调整功能的standby模式,standby模式会使得用户维护集群更困难。
    • 新增Proxy模式,不加入到etcd一致性集群中,纯粹进行代理转发。
    • ETCD_NAME(-name)参数目前是可选的,不再用于唯一标识一个节点。
    • 摒弃通过配置文件配置 etcd 属性的方式,你可以通过设置环境变量的方式代替。
    • 通过自发现方式启动集群时要求用户提供集群大小,这样有助于确定集群实际启动的节点数量。

    2.3 ETCD概念词汇表

    • Raft:etcd所采用的保证分布式系统强一致性的算法。
    • Node:一个Raft状态机实例。
    • Member: 一个etcd实例。它管理着一个Node,并且可以为客户端请求提供服务。
    • Cluster:由多个Member构成可以协同工作的etcd集群。
    • Peer:对同一个etcd集群中另外一个Member的称呼。
    • Client: 向etcd集群发送HTTP请求的客户端。
    • WAL:预写式日志,etcd用于持久化存储的日志格式。
    • snapshot:etcd防止WAL文件过多而设置的快照,存储etcd数据状态。
    • Proxy:etcd的一种模式,为etcd集群提供反向代理服务。
    • Leader:Raft算法中通过竞选而产生的处理所有数据提交的节点。
    • Follower:竞选失败的节点作为Raft中的从属节点,为算法提供强一致性保证。
    • Candidate:当Follower超过一定时间接收不到Leader的心跳时转变为Candidate开始Leader竞选。
    • Term:某个节点成为Leader到下一次竞选开始的时间周期,称为一个Term。
    • Index:数据项编号。Raft中通过Term和Index来定位数据。

    2.4 集群化应用实践

    etcd作为一个高可用键值存储系统,天生就是为集群化而设计的。由于Raft算法在做决策时需要多数节点的投票,所以etcd一般部署集群推荐奇数个节点,推荐的数量为3、5或者7个节点构成一个集群。

    2.4.1 集群启动

    etcd有三种集群化启动的配置方案,分别为静态配置启动、etcd自身服务发现、通过DNS进行服务发现。

    根据启动环境,你可以选择不同的配置方式。值得一提的是,这也是新版etcd区别于旧版的一大特性,它摒弃了使用配置文件进行参数配置的做法,转而使用命令行参数或者环境变量来配置参数。

    (1). 静态配置

    这种方式比较适用于离线环境。在启动整个集群之前,你如果已经预先清楚所要配置的集群大小,以及集群上各节点的地址和端口信息,那么启动时,你就可以通过配置initial-cluster参数进行etcd集群的启动。

    在每个etcd机器启动时,配置环境变量或者添加启动参数的方式如下。

    参数方法:

    值得注意的是-initial-cluster参数中配置的url地址必须与各个节点启动时设置的initial-advertise-peer-urls参数相同。(initial-advertise-peer-urls参数表示节点监听其他节点同步信号的地址)

    如果你所在的网络环境配置了多个etcd集群,为了避免意外发生,最好使用-initial-cluster-token参数为每个集群单独配置一个token认证。这样就可以确保每个集群和集群的成员都拥有独特的ID。

    综上所述,如果你要配置包含3个etcd节点的集群,那么你在三个机器上的启动命令分别如下所示。

    在初始化完成后,etcd还提供动态增、删、改etcd集群节点的功能,这个需要用到etcdctl命令进行操作。

    (2). etcd自发现模式

    通过自发现的方式启动etcd集群需要事先准备一个etcd集群。如果你已经有一个etcd集群,首先你可以执行如下命令设定集群的大小,假设为3.

    然后你要把这个url地址https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83作为-discovery参数来启动etcd。节点会自动使用https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83目录进行etcd的注册和发现服务。

    所以最终你在某个机器上启动etcd的命令如下。

    如果你在本地没有可用的etcd集群,etcd官网提供了一个可以用公网访问的etcd存储地址。 你可以通过如下命令得到etcd服务的目录,并把它作为-discovery参数使用。

    同样的,当你完成了集群的初始化后,这些信息就失去了作用。当你需要增加节点时,需要使用etcdctl来进行操作。

    为了安全,在每次启动新的etcd集群时,请务必使用新的discovery token进行注册。 另外,如果你初始化时启动的节点超过了指定的数量,多余的节点会自动转化为proxy模式的etcd。

    (3). DNS自发现模式

    etcd还支持使用DNS SRV记录进行启动。关于DNS SRV记录如何进行服务发现,可以参阅RFC2782,所以,你要在DNS服务器上进行相应的配置。

    • 开启DNS服务器上SRV记录查询,并添加相应的域名记录,使得查询到的结果类似如下。

      $ dig +noall +answer SRV _etcd-server._tcp.example.com _etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra0.example.com. _etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra1.example.com. _etcd-server._tcp.example.com. 300 IN SRV 0 0 2380 infra2.example.com.

    • 分别为各个域名配置相关的A记录指向etcd核心节点对应的机器IP,使得查询结果类似如下。

      $ dig +noall +answer infra0.example.com infra1.example.com infra2.example.com infra0.example.com. 300 IN A 10.0.1.10 infra1.example.com. 300 IN A 10.0.1.11 infra2.example.com. 300 IN A 10.0.1.12

    做好了上述两步DNS的配置,就可以使用DNS启动etcd集群了。配置DNS解析的url参数为-discovery-srv,其中某一个节点地启动命令如下。

    当然,你也可以直接把节点的域名改成IP来启动。

    2.4.2 关键部分源码解析

    etcd的启动是从主目录下的main.go开始的,然后进入etcdmain/etcd.go,载入配置参数。如果被配置为proxy模式,则进入startProxy函数,否则进入startEtcd,开启etcd服务模块和http请求处理模块。

    在启动http监听时,为了与集群其他etcd机器(peers)保持连接,均采用了transport.NewTimeoutListener启动方式,在超过指定时间没有获得响应时就会出现超时错误。而在监听client请求时,采用的是transport.NewKeepAliveListener,有助于连接的稳定。

    etcdmain/etcd.go中的setupCluster函数可以看到,对于不同的etcd参数,启动集群的方法略有不同,但是最终需要的就是一个IP与端口构成的字符串。

    在静态配置的启动方式中,集群的所有信息都已经给出,所以直接解析用逗号隔开的集群url信息就好了。

    DNS发现的方式与静态配置启动类似,会预先发送一个tcp的SRV请求,先查看etcd-server-ssl._tcp.example.com下是否有集群的域名信息,如果没有找到,则去查看etcd-server._tcp.example.com。根据找到的域名,解析出对应的IP和端口,即集群的url信息。

    较为复杂是etcd式的自发现启动。首先用自身单个的url构成一个集群,然后在启动的过程中根据参数进入discovery/discovery.go源码的JoinCluster函数。在启动时使用的etcd的token地址中,包含了集群大小(size)信息,所以集群的启动本质上是一个不断监测与等待的过程。启动的第一步就是在这个借用的etcd的token目录下注册自身的信息,然后再监测token目录下所有节点的数量,如果数量没有达到指定值,则循环等待。否则结束等待,进入后续启动过程。

    配置etcd过程中通常要用到两种url地址容易混淆,一种用于etcd集群同步信息并保持连接,通常称为peer-urls;另外一种用于接收用户端发来的HTTP请求,通常称为client-urls。

    • peer-urls:通常监听的端口为2380(老版本使用的端口为7001),包括所有已经在集群中正常工作的所有节点的地址。
    • client-urls:通常监听的端口为2379(老版本使用的端口为4001),为适应复杂的网络环境,新版etcd监听客户端请求的url从原来的1个变为现在可配置的多个。这样etcd就可以配合多块网卡同时监听不同网络下的请求。

    2.4.3 运行时节点变更

    etcd集群启动完毕后,可以在运行的过程中对集群进行重构,包括核心节点的增加、删除、迁移、替换等。运行时重构使得etcd集群无须重启即可改变集群的配置,这也是新版etcd区别于旧版包含的新特性。

    只有当集群中多数节点正常的情况下,你才可以进行运行时的配置管理。因为配置更改的信息也会被etcd当成一个信息存储和同步,如果集群多数节点损坏,集群就失去了写入数据的能力。所以在配置etcd集群数量时,强烈推荐至少配置3个核心节点,配置数目越多,可用性越强。

    (1). 节点迁移、替换

    当你节点所在的机器出现硬件故障,或者节点出现如数据目录损坏等问题,导致节点永久性的不可恢复时,就需要对节点进行迁移或者替换。当一个节点失效以后,必须尽快修复,因为etcd集群正常运行的必要条件是集群中多数节点都正常工作。

    迁移一个节点需要进行四步操作:

    • 暂停正在运行着的节点程序进程
    • 把数据目录从现有机器拷贝到新机器
    • 使用api更新etcd中对应节点指向机器的url记录更新为新机器的ip
    • 使用同样的配置项和数据目录,在新的机器上启动etcd。
    (2). 节点增加

    增加节点可以让etcd的高可用性更强。举例来说,如果你有3个节点,那么最多允许1个节点失效;当你有5个节点时,就可以允许有2个节点失效。同时,增加节点还可以让etcd集群具有更好的读性能。因为etcd的节点都是实时同步的,每个节点上都存储了所有的信息,所以增加节点可以从整体上提升读的吞吐量。

    增加一个节点需要进行两步操作:

    • 在集群中添加这个节点的url记录,同时获得集群的信息。
    • 使用获得的集群信息启动新etcd节点。
    (3). 节点移除

    有时你不得不在提高etcd的写性能和增加集群高可用性上进行权衡。Leader节点在提交一个写记录时,会把这个消息同步到每个节点上,当得到多数节点的同意反馈后,才会真正写入数据。所以节点越多,写入性能越差。在节点过多时,你可能需要移除其中的一个或多个。 移除节点非常简单,只需要一步操作,就是把集群中这个节点的记录删除,则对应机器上的该节点就会自动停止。

    (4). 强制性重启集群

    当集群超过半数的节点都失效时,就需要通过手动的方式,强制性让某个节点以自己为Leader,利用原有数据启动一个新集群。

    此时你需要进行两步操作。

    • 备份原有数据到新机器。
    • 使用-force-new-cluster和备份的数据重新启动节点

    注意:强制性重启是一个迫不得已的选择,它会破坏一致性协议保证的安全性(如果操作时集群中尚有其它节点在正常工作,就会出错),所以在操作前请务必要保存好数据。

    2.5 PROXY模式

    Proxy模式也是新版etcd的一个重要变更,etcd作为一个反向代理把客户的请求转发给可用的etcd集群。这样,你就可以在每一台机器都部署一个Proxy模式的etcd作为本地服务,如果这些etcd Proxy都能正常运行,那么你的服务发现必然是稳定可靠的。

    enter image description here

    图11 Proxy模式示意图

    所以Proxy并不是直接加入到符合强一致性的etcd集群中,也同样的,Proxy并没有增加集群的可靠性,当然也没有降低集群的写入性能。

    那么,为什么要有Proxy模式而不是直接增加etcd核心节点呢?实际上,etcd每增加一个核心节点(peer),都会给Leader节点增加一定程度的负担(包括网络、CPU和磁盘负载),因为每次信息的变化都需要进行同步备份。增加etcd的核心节点固然可以让整个集群具有更高的可靠性,但是当其数量达到一定程度以后,增强可靠性带来的好处就变得不那么明显,反倒是降低了集群写入同步的性能。因此,增加一个轻量级的Proxy模式etcd节点是对直接增加etcd核心节点的一个有效代替。

    熟悉0.4.6这个旧版本etcd的用户会发现,Proxy模式实际上取代了原先的Standby模式。Standby模式具备转发代理的功能。此外,在核心节点因为故障导致数量不足时,还会从Standby模式转为核心节点。而当故障节点恢复时,若etcd的核心节点数量已经达到预设值,则前述节点会再次转为Standby模式。

    但是在新版etcd中,只在最初启动etcd集群的过程中,若核心节点的数量已经满足要求,自动启用Proxy模式,反之则并未实现。主要原因如下。

    • etcd是用来保证高可用的组件,因此它所需要的系统资源(包括内存、硬盘和CPU等)都应该得到充分保障。任由集群的自动变换随意地改变核心节点,无法让机器保证性能。所以etcd官方鼓励大家在大型集群中为运行etcd准备专有机器集群。
    • 因为etcd集群是支持高可用的,部分机器故障并不会导致功能失效。所以在机器发生故障时,管理员有充分的时间对机器进行检查和修复。
    • 自动转换使得etcd集群变得更为复杂,尤其是在如今etcd支持多种网络环境的监听和交互的情况下。在不同网络间进行转换,更容易发生错误,导致集群不稳定。

    基于上述原因,目前Proxy模式有转发代理功能,而不会进行角色转换。

    关键部分源码解析

    从代码中可以看到,Proxy模式的本质就是起一个http代理服务器,把客户发到这个服务器的请求转发给别的etcd节点。

    etcd目前支持读写皆可和只读两种模式。默认情况下是读写皆可,就是把读、写两种请求都进行转发。而只读模式只转发读的请求,对所有其他请求返回501错误。

    值得注意的是,在etcd集群化启动时,除了因为设置proxy参数作为Proxy模式启动之外,如果节点注册自身信息的时候监测到集群的实际节点数量已经符合要求,那么也会退化为Proxy模式。

    2.6 数据存储

    etcd的存储分为内存存储和持久化(硬盘)存储两部分,内存中的存储除了顺序化地记录下所有用户对节点数据变更的记录外,还会对用户数据进行索引、建堆等方便查询的操作。而持久化则使用预写式日志(WAL:Write Ahead Log)进行记录存储。

    在WAL的体系中,所有的数据在提交之前都会进行日志记录。在etcd的持久化存储目录中,有两个子目录。一个是WAL,存储着所有事务的变化记录;另一个则是snapshot,用于存储某一个时刻etcd所有目录的数据。通过WAL和snapshot相结合的方式,etcd可以有效地进行数据存储和节点故障恢复等操作。

    也许你会有这样的疑问,既然已经在WAL实时存储了所有的变更,为什么还需要snapshot呢?原因是这样的,随着使用量的增加,WAL存储的数据会急剧增加,为了防止磁盘空间不足,etcd默认每10000条记录做一次snapshot,经过snapshot以后的WAL文件就可以删除。通过API可以查询的历史etcd操作默认为1000条。

    首次启动时,etcd会把启动的配置信息存储到data-dir参数指定的数据目录中。配置信息包括本地节点ID、集群ID和初始时集群信息。用户需要避免etcd从一个过期的数据目录中重新启动,因为使用过期的数据目录启动的节点会与集群中的其他节点产生不一致(如:之前已经记录并同意Leader节点存储某个信息,重启后又向Leader节点申请这个信息)。所以,为了最大化集群的安全性,一旦有任何数据损坏或丢失的可能性,你就应该把这个节点从集群中移除,然后加入一个不带数据目录的新节点。

    (1)预写式日志(WAL)

    WAL最大的作用是记录了整个数据变化的全部历程。在etcd中,所有数据的修改在提交前,都要先写入到WAL中。使用WAL进行数据的存储使得etcd拥有两个重要功能。

    • 故障快速恢复: 当你的数据遭到破坏时,就可以通过执行所有WAL中记录的修改操作,快速从最原始的数据恢复到数据损坏前的状态。
    • 数据回滚(undo)/重做(redo):因为所有的修改操作都被记录在WAL中,在需要回滚或重做时,只需要反向或正向执行日志中的操作即可。
    WAL与snapshot在etcd中的命名规则

    在etcd的数据目录中,WAL文件以$seq-$index.wal的格式存储。最初始的WAL文件是0000000000000000-0000000000000000.wal,表示是所有WAL文件中的第0个,初始的Raft状态编号为0。运行一段时间后可能需要进行日志切分,把新的条目放到一个新的WAL文件中。

    假设,当集群运行到Raft状态为20,需要进行WAL文件的切分时,则下一份WAL文件就会变为0000000000000001-0000000000000021.wal。如果在10次操作后又进行了一次日志切分,那么后一次的WAL文件名会变为0000000000000002-0000000000000031.wal。可以看到-符号前面的数字是每次切分后自增1,而-符号后面的数字则是根据实际存储的Raft起始状态来定。

    snapshot的存储命名则比较容易理解,以$term-$index.wal格式进行命名存储。term和index就表示存储snapshot时数据所在的Raft节点状态,当前的任期编号以及数据项位置信息。

    (2) 关键部分源码解析

    从代码逻辑中可以看到,WAL有两种模式,读(read)模式和数据添加(append)模式,两者是互斥的。一个新创建的WAL文件处于append模式,并且不会进入到read模式。一个本来存在的WAL文件被打开的时候必然是read模式,只有在所有记录都被读完的时候,才能进入append模式,进入append模式后也不会再进入read模式。这样做有助于保证数据的完整与准确。

    集群在进入到etcdserver/server.goNewServer函数准备启动一个etcd节点时,会检测是否存在以前的遗留WAL数据。

    etcd从v0.4.6升级到v2.0.0,它数据格式存储的格式也变化了。检测的第一步是查看snapshot文件夹下是否有符合规范的文件,若检测到snapshot格式是v0.4的,则调用函数升级到v0.5。从snapshot中获得集群的配置信息,包括token、其他节点的信息等等,然后载入WAL目录的内容,从小到大进行排序。根据snapshot中得到的term和index,找到WAL紧接着snapshot下一条的记录,然后向后更新,直到所有WAL包的entry都已经遍历完毕,Entry记录到ents变量中存储在内存里。此时WAL就进入append模式,为数据项添加进行准备。

    当WAL文件中数据项内容过大达到设定值(默认为10000)时,会进行WAL的切分,同时进行snapshot操作。这个过程可以在etcdserver/server.gosnapshot函数中看到。所以,实际上数据目录中有用的snapshot和WAL文件各只有一个,默认情况下etcd会各保留5个历史文件。

    2.7 RAFT

    新版Etcd中,Raft包就是对Raft一致性算法的具体实现。关于Raft算法的讲解,网上已经有很多文章,有兴趣的读者可以去阅读一下Raft算法论文,非常精彩。本文不再对Raft算法进行详细描述,而是结合etcd,针对算法中一些关键内容以问答的形式进行讲解。Raft算法的相关术语参见概念词汇表一节。

    (1) Raft常见问答一览

    • Raft中一个Term(任期)是什么意思? 在Raft算法中,从时间上讲,一个任期即从某一次竞选开始到下一次竞选开始。从功能上讲,如果Follower接收不到Leader节点的心跳信息,就会结束当前任期,变为Candidate发起竞选,有助于Leader节点故障时集群的恢复。 发起竞选投票时,任期值小的节点不会竞选成功。如果集群不出现故障,那么一个任期将无限延续下去。而投票出现冲突则有可能直接进入下一任再次竞选。

    enter image description here 图12 Term示意图

    • Raft状态机是怎样切换的? Raft刚开始运行时,节点默认进入Follower状态,等待Leader发来心跳信息。若等待超时,则状态由Follower切换到Candidate进入下一轮Term发起竞选,等到收到集群多数节点的投票时,该节点转变为Leader。Leader节点有可能出现网络等故障,导致别的节点发起投票成为新Term的Leader,此时原先的老Leader节点会切换为Follower。Candidate在等待其它节点投票的过程中如果发现别的节点已经竞选成功成为Leader了,也会切换为Follower节点。

    enter image description here

    图13 Raft状态机

    • 如何保证最短时间内竞选出Leader,防止竞选冲突? 在Raft状态机一图中可以看到,在Candidate状态下, 有一个times out,这里的times out时间是个随机值,也就是说,每个机器成为Candidate以后,超时发起新一轮竞选的时间是各不相同的,这就会出现一个时间差。在时间差内,如果Candidate1收到的竞选信息比自己发起的竞选信息Term值大(即对方为新一轮Term),并且新一轮想要成为Leader的Candidate2包含了所有提交的数据,那么Candidate1就会投票给Candidate2。这样就保证了只有很小的概率会出现竞选冲突。

    • 如何防止别的Candidate在遗漏部分数据的情况下发起投票成为Leader? Raft竞选的机制中,使用随机值决定超时时间,第一个超时的节点就会提升Term编号发起新一轮投票,一般情况下别的节点收到竞选通知就会投票。但是,如果发起竞选的节点在上一个Term中保存的已提交数据不完整,节点就会拒绝投票给它。通过这种机制就可以防止遗漏数据的节点成为Leader。

    • Raft某个节点宕机后会如何? 通常情况下,如果是Follower节点宕机,且剩余可用节点数量超过总节点数的一半,集群可以几乎不受影响地正常工作。如果是Leader节点宕机,那么Follower节点会因为收不到心跳而超时,发起竞选获得投票,成为新一轮Term的Leader,继续为集群提供服务。需要注意的是;etcd目前没有任何机制会自动去变化整个集群的总节点数量,即如果没有人为地调用API,etcd宕机后的节点仍然被计算在总节点数中,任何请求被确认需要获得的投票数都是这个总数的一半以上。

    enter image description here 图14 节点宕机

    • 为什么Raft算法在确定可用节点数量时不需要考虑拜占庭将军问题? 拜占庭将军问题中提出,允许n个节点宕机还能提供正常服务的分布式架构,需要的总节点数量为3n+1,而Raft只需要2n+1就可以了。其主要原因在于,拜占庭将军问题中存在数据欺骗的现象,而etcd中假设所有的节点都是诚实的。etcd在竞选前需要告诉别的节点自身的Term编号以及前一轮Term最终结束时的index值,这些数据都是准确的,其他节点可以根据这些值决定是否投票。另外,etcd严格限制Leader到Follower这样的数据流向保证数据一致不会出错。

    • 用户从集群中哪个节点读写数据? Raft为了保证数据的强一致性,所有的数据流向都是一个方向,从Leader流向Follower,即所有Follower的数据必须与Leader保持一致,如果不一致则会被覆盖。也就是说,所有用户更新数据的请求都最先由Leader获得并保存下来,然后通知其他节点将其保存,等到大多数节点反馈时再把数据提交。一个已提交的数据项才是Raft真正稳定存储下来的数据项,不再被修改,最后再把提交的数据同步给其他Follower。因为每个节点都有Raft已提交数据准确的备份(最坏的情况也只是已提交数据还未完全同步),所以任何一个节点都可以处理读请求。

    • etcd实现的Raft算法性能如何? 单实例节点支持每秒1000次数据写入。随着节点数目的增加,数据同步会因为网络延迟越来越慢;而读性能则会随之提升,因为每个节点都能处理用户的读请求。

    (2) 关键部分源码解析

    在etcd代码中,Node作为Raft状态机的具体实现,是整个算法的关键,也是了解算法的入口。

    在etcd中,对Raft算法的调用如下,你可以在etcdserver/raft.go中的startNode找到:

    通过这段代码可以了解到,Raft在运行过程记录数据和状态都是保存在内存中,而代码中raft.StartNode启动的Node就是Raft状态机Node。启动了一个Node节点后,Raft会做如下事项。

    首先,你需要把从集群的其他机器上收到的信息推送到Node节点,你可以在etcdserver/server.go中的Process函数看到。

    检测发来请求的机器是否是集群中的节点,自身节点是否是Follower,把发来请求的机器作为Leader,具体对Node节点信息的推送和处理则通过node.Step()函数实现。

    其次,你需要把日志项存储起来,在你的应用中执行提交的日志项,然后把完成信号发送给集群中的其它节点,再通过node.Ready()监听等待下一次任务执行。有一点非常重要,你必须确保在你发送完成消息给其他节点之前,你的日志项内容已经确切稳定地存储下来了。

    最后,你需要保持一个心跳信号Tick()。Raft有两个很重要的地方用到超时机制:心跳保持和Leader竞选。需要用户在其Raft的Node节点上周期性地调用Tick()函数,以便为超时机制服务。

    综上所述,整个Raft节点的状态机循环类似如下所示:

    而这个状态机真实存在的代码位置为etcdserver/server.go中的run函数。

    对状态机进行状态变更(如用户数据更新等)时将调用n.Propose(ctx, data)函数,在存储数据时,会先进行序列化操作。获得大多数其他节点的确认后,数据会被提交,保存为已提交状态。

    之前提到etcd集群的启动如果使用自发现方式,需要借助别的etcd集群或者DNS,而启动完毕后这些外力就不需要了。etcd会把自身集群的信息作为状态存储起来。所以要变更自身集群节点数量实际上也需要像用户数据变更那样添加数据条目到Raft状态机中。上述功能由n.ProposeConfChange(ctx, cc)实现。当集群配置信息变更的请求同样得到大多数节点的确认反馈后,再进行配置变更的正式操作,代码如下。

    注意:为了避免不同etcd集群消息混乱,ID需要确保唯一性,不能重复使用旧的token数据作为ID。

    2.8 STORE

    顾名思义,Store这个模块就像一个商店一样把etcd已经准备好的各项底层支持加工起来,为用户提供五花八门的API支持,处理用户的各项请求。要理解Store,就要从etcd的API入手。打开etcd的API列表,我们可以看到如下API,均系对etcd存储的键值进行的操作,亦即Store提供的内容。API中提到的目录(Directory)和键(Key),上文中也可能称为etcd节点(Node)。

    • 为etcd存储的键赋值

      反馈的内容含义如下:

      • action: 刚刚进行的动作名称。
      • node.key: 请求的HTTP路径。etcd使用一个类似文件系统的方式来反映键值存储的内容。
      • node.value: 刚刚请求的键所存储的内容。
      • node.createdIndex: etcd节点每次发生变化时,该值会自动增加。除了用户请求外,etcd内部运行(如启动、集群信息变化等)也可能会因为对节点有变动而引起该值的变化。
      • node.modifiedIndex: 类似node.createdIndex,能引起modifiedIndex变化的操作包括set, delete, update, create, compareAndSwap and compareAndDelete。
    • 查询etcd某个键存储的值

    • 修改键值:与创建新值几乎相同,但是反馈时会有一个prevNode值反应了修改前存储的内容。

    • 删除一个值

    • 对一个键进行定时删除:etcd中对键进行定时删除,设定一个ttl值,当这个值到期时键就会被删除。反馈的内容会给出expiration项告知超时时间,ttl项告知设定的时长。

    • 取消定时删除任务

    • 对键值修改进行监控:etcd提供的这个API让用户可以监控一个值或者递归式地监控一个目录及其子目录的值,当目录或值发生变化时,etcd会主动通知。

    • 对过去的键值操作进行查询:类似上面提到的监控,在其基础上指定过去某次修改的索引编号,就可以查询历史操作。默认可查询的历史记录为1000条。

    • 自动在目录下创建有序键。在对创建的目录使用POST参数,会自动在该目录下创建一个以createdIndex值为键的值,这样就相当于根据创建时间的先后进行了严格排序。该API对分布式队列这类场景非常有用。

    • 按顺序列出所有创建的有序键。

    • 创建定时删除的目录:就跟定时删除某个键类似。如果目录因为超时被删除了,其下的所有内容也自动超时删除。

    • 刷新超时时间。

    • 自动化CAS(Compare-and-Swap)操作:etcd强一致性最直观的表现就是这个API,通过设定条件,阻止节点二次创建或修改。即用户的指令被执行当且仅当CAS的条件成立。条件有以下几个。

      • prevValue 先前节点的值,如果值与提供的值相同才允许操作。
      • prevIndex 先前节点的编号,编号与提供的校验编号相同才允许操作。
      • prevExist 先前节点是否存在。如果存在则不允许操作。这个常常被用于分布式锁的唯一获取。

      假设先进行了如下操作:设定了foo的值。 curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=one然后再进行操作:curl http://127.0.0.1:2379/v2/keys/foo?prevExist=false -XPUT -d value=three就会返回创建失败的错误。

    • 条件删除(Compare-and-Delete):与CAS类似,条件成立后才能删除。

    • 创建目录

    • 列出目录下所有的节点信息,最后以/结尾。还可以通过recursive参数递归列出所有子目录信息。

    • 删除目录:默认情况下只允许删除空目录,如果要删除有内容的目录需要加上recursive=true参数。

    • 创建一个隐藏节点:命名时名字以下划线_开头默认就是隐藏键。

      相信看完这么多API,相信读者已经对Store的工作内容有了基本的了解。它对etcd下存储的数据进行加工,创建出如文件系统般的树状结构供用户快速查询。它有一个Watcher用于节点变更的实时反馈,还需要维护一个WatcherHub对所有Watcher订阅者进行通知的推送。同时,它还维护了一个由定时键构成的小顶堆,快速返回下一个要超时的键。最后,所有这些API的请求都以事件的形式存储在事件队列中等待处理。

      3 总结

      通过从应用场景到源码分析的一系列回顾,我们了解到etcd并不是一个简单的分布式键值存储系统。它解决了分布式场景中最为常见的一致性问题,为服务发现提供了一个稳定高可用的消息注册仓库,为以微服务协同工作的架构提供了无限的可能。相信在不久的将来,通过etcd构建起来的大型系统会越来越多。

      4 参考文献

      • https://github.com/coreos/etcd
      • https://groups.google.com/forum/#!topic/etcd-dev/wmndjzBNdZo
      • http://jm-blog.aliapp.com/?p=1232
      • http://progrium.com/blog/2014/07/29/understanding-modern-service-discovery-with-docker/
      • http://devo.ps/blog/zookeeper-vs-doozer-vs-etcd/
      • http://jasonwilder.com/blog/2014/02/04/service-discovery-in-the-cloud/
      • http://www.infoworld.com/article/2612082/open-source-software/has-apache-lost-its-way-.html
      • http://en.wikipedia.org/wiki/WAL
      • http://www.infoq.com/cn/articles/coreos-analyse-etcd
      • http://www.activestate.com/blog/2014/05/service-discovery-solutions
      • https://ramcloud.stanford.edu/raft.pdf
      展开全文
    • etcd-3.3.12

      2019-02-28 15:28:09
      etcd用途很多,可以做服务发现、配置中心等,是一个重要的组件,在devops平台的建设过程中建议选型,etcd-3.3.12版本源码,安装方法参考其他文档。
    • ETCD介绍—etcd概念及原理方面分析

      千次阅读 2021-09-01 16:50:24
      etcd作为一个受到ZooKeeper与doozer启发而催生的项目,除了拥有与之类似的功能外,更专注于以下四点。 简单:基于HTTP+JSON的API让你用curl就可以轻松使用。 安全:可选SSL客户认证机制。 快速:每个实例每秒支持...

      etcd作为一个受到ZooKeeper与doozer启发而催生的项目,除了拥有与之类似的功能外,更专注于以下四点。

      1. 简单:基于HTTP+JSON的API让你用curl就可以轻松使用。
      2. 安全:可选SSL客户认证机制。
      3. 快速:每个实例每秒支持一千次写操作。
      4. 可信:使用Raft算法充分实现了分布式。

      分布式系统中的数据分为控制数据和应用数据。etcd的使用场景默认处理的数据都是控制数据,对于应用数据,只推荐数据量很小,但是更新访问频繁的情况。应用场景有如下几类:场景一:服务发现(Service Discovery)场景二:消息发布与订阅场景三:负载均衡场景四:分布式通知与协调场景五:分布式锁、分布式队列场景六:集群监控与Leader竞选举个最简单的例子,如果你需要一个分布式存储仓库来存储配置信息,并且希望这个仓库读写速度快、支持高可用、部署简单、支持http接口,那么就可以使用etcd。目前,cloudfoundry使用etcd作为hm9000的应用状态信息存储,kubernetes用etcd来存储docker集群的配置信息等。

      1. ETCD是什么

      这里有一个ETCD的相关视频讲解:分布式注册服务中心etcd

      ETCD是用于共享配置和服务发现的分布式,一致性的KV存储系统。该项目目前最新稳定版本为2.3.0. 具体信息请参考[项目首页]和[Github]。ETCD是CoreOS公司发起的一个开源项目,授权协议为Apache。

      提供配置共享和服务发现的系统比较多,其中最为大家熟知的是[Zookeeper](后文简称ZK),而ETCD可以算得上是后起之秀了。在项目实现,一致性协议易理解性,运维,安全等多个维度上,ETCD相比Zookeeper都占据优势。

      2. ETCD vs ZK

      本文选取ZK作为典型代表与ETCD进行比较,而不考虑[Consul]项目作为比较对象,原因为Consul的可靠性和稳定性还需要时间来验证(项目发起方自身服务并未使用Consul, 自己都不用)。

      • 一致性协议: ETCD使用[Raft]协议, ZK使用ZAB(类PAXOS协议),前者容易理解,方便工程实现;
      • 运维方面:ETCD方便运维,ZK难以运维;
      • 项目活跃度:ETCD社区与开发活跃,ZK已经快死了;
      • API:ETCD提供HTTP+JSON, gRPC接口,跨平台跨语言,ZK需要使用其客户端;
      • 访问安全方面:ETCD支持HTTPS访问,ZK在这方面缺失;

      3. ETCD的使用场景

      和ZK类似,ETCD有很多使用场景,包括:

      • 配置管理
      • 服务注册于发现
      • 选主
      • 应用调度
      • 分布式队列
      • 分布式锁

      4. ETCD读写性能

      按照官网给出的[Benchmark], 在2CPU,1.8G内存,SSD磁盘这样的配置下,单节点的写性能可以达到16K QPS, 而先写后读也能达到12K QPS。这个性能还是相当可观的。

      5. ETCD工作原理

      ETCD使用Raft协议来维护集群内各个节点状态的一致性。简单说,ETCD集群是一个分布式系统,由多个节点相互通信构成整体对外服务,每个节点都存储了完整的数据,并且通过Raft协议保证每个节点维护的数据是一致的。

      如图所示,每个ETCD节点都维护了一个状态机,并且,任意时刻至多存在一个有效的主节点。主节点处理所有来自客户端写操作,通过Raft协议保证写操作对状态机的改动会可靠的同步到其他节点。

      ETCD工作原理核心部分在于Raft协议。本节接下来将简要介绍Raft协议,具体细节请参考其[论文]。
      Raft协议正如论文所述,确实方便理解。主要分为三个部分:选主,日志复制,安全性。

      5.1 选主

      Raft协议是用于维护一组服务节点数据一致性的协议。这一组服务节点构成一个集群,并且有一个主节点来对外提供服务。当集群初始化,或者主节点挂掉后,面临一个选主问题。集群中每个节点,任意时刻处于Leader, Follower, Candidate这三个角色之一。选举特点如下:

      • 当集群初始化时候,每个节点都是Follower角色;
      • 集群中存在至多1个有效的主节点,通过心跳与其他节点同步数据;
      • 当Follower在一定时间内没有收到来自主节点的心跳,会将自己角色改变为Candidate,并发起一次选主投票;当收到包括自己在内超过半数节点赞成后,选举成功;当收到票数不足半数选举失败,或者选举超时。若本轮未选出主节点,将进行下一轮选举(出现这种情况,是由于多个节点同时选举,所有节点均为获得过半选票)。
      • Candidate节点收到来自主节点的信息后,会立即终止选举过程,进入Follower角色。为了避免陷入选主失败循环,每个节点未收到心跳发起选举的时间是一定范围内的随机值,这样能够避免2个节点同时发起选主。

      C/C++Linux后台服务器开发高级架构师学习视频 点击 linux服务器学习资料 获取,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。免费学习地址:C/C++Linux服务器开发高级架构师/Linux后台架构师

      5.2 日志复制

      所谓日志复制,是指主节点将每次操作形成日志条目,并持久化到本地磁盘,然后通过网络IO发送给其他节点。其他节点根据日志的逻辑时钟(TERM)和日志编号(INDEX)来判断是否将该日志记录持久化到本地。当主节点收到包括自己在内超过半数节点成功返回,那么认为该日志是可提交的(committed),并将日志输入到状态机,将结果返回给客户端。

      这里需要注意的是,每次选主都会形成一个唯一的TERM编号,相当于逻辑时钟。每一条日志都有全局唯一的编号。

      主节点通过网络IO向其他节点追加日志。若某节点收到日志追加的消息,首先判断该日志的TERM是否过期,以及该日志条目的INDEX是否比当前以及提交的日志的INDEX跟早。若已过期,或者比提交的日志更早,那么就拒绝追加,并返回该节点当前的已提交的日志的编号。否则,将日志追加,并返回成功。

      当主节点收到其他节点关于日志追加的回复后,若发现有拒绝,则根据该节点返回的已提交日志编号,发生其编号下一条日志。

      主节点像其他节点同步日志,还作了拥塞控制。具体地说,主节点发现日志复制的目标节点拒绝了某次日志追加消息,将进入日志探测阶段,一条一条发送日志,直到目标节点接受日志,然后进入快速复制阶段,可进行批量日志追加。

      按照日志复制的逻辑,我们可以看到,集群中慢节点不影响整个集群的性能。另外一个特点是,数据只从主节点复制到Follower节点,这样大大简化了逻辑流程。

      5.3 安全性

      截止此刻,选主以及日志复制并不能保证节点间数据一致。试想,当一个某个节点挂掉了,一段时间后再次重启,并当选为主节点。而在其挂掉这段时间内,集群若有超过半数节点存活,集群会正常工作,那么会有日志提交。这些提交的日志无法传递给挂掉的节点。当挂掉的节点再次当选主节点,它将缺失部分已提交的日志。在这样场景下,按Raft协议,它将自己日志复制给其他节点,会将集群已经提交的日志给覆盖掉。

      这显然是不可接受的。

      其他协议解决这个问题的办法是,新当选的主节点会询问其他节点,和自己数据对比,确定出集群已提交数据,然后将缺失的数据同步过来。这个方案有明显缺陷,增加了集群恢复服务的时间(集群在选举阶段不可服务),并且增加了协议的复杂度。

      Raft解决的办法是,在选主逻辑中,对能够成为主的节点加以限制,确保选出的节点已定包含了集群已经提交的所有日志。如果新选出的主节点已经包含了集群所有提交的日志,那就不需要从和其他节点比对数据了。简化了流程,缩短了集群恢复服务的时间。

      这里存在一个问题,加以这样限制之后,还能否选出主呢?答案是:只要仍然有超过半数节点存活,这样的主一定能够选出。因为已经提交的日志必然被集群中超过半数节点持久化,显然前一个主节点提交的最后一条日志也被集群中大部分节点持久化。当主节点挂掉后,集群中仍有大部分节点存活,那这存活的节点中一定存在一个节点包含了已经提交的日志了。

      至此,关于Raft协议的简介就全部结束了。

      6. ETCD使用案例

      据公开资料显示,至少有CoreOS, Google Kubernetes, Cloud Foundry, 以及在Github上超过500个项目在使用ETCD。

      7. ETCD接口

      ETCD提供HTTP协议,在最新版本中支持Google gRPC方式访问。具体支持接口情况如下:

      • ETCD是一个高可靠的KV存储系统,支持PUT/GET/DELETE接口;
      • 为了支持服务注册与发现,支持WATCH接口(通过http long poll实现);
      • 支持KEY持有TTL属性;
      • CAS(compare and swap)操作;
      • 支持多key的事务操作;
      • 支持目录操作

      ETCD系列之二:部署集群

      1. 概述

      想必很多人都知道ZooKeeper,通常用作配置共享和服务发现。和它类似,ETCD算是一个非常优秀的后起之秀了。本文重点不在描述他们之间的不同点。首先,看看其官网关于ETCD的描述1:

      A distributed, reliable key-value store for the most critical data of a distributed system.

      在云计算大行其道的今天,ETCD有很多典型的使用场景。常言道,熟悉一个系统先从部署开始。本文接下来将描述,如何部署ETCD集群。

      安装官网说明文档,提供了3种集群启动方式,实际上按照其实现原理分为2类:

      • 通过静态配置方式启动
      • 通过服务发现方式启动

      在部署集群之前,我们需要考虑集群需要配置多少个节点。这是一个重要的考量,不得忽略。

      2. 集群节点数量与网络分割

      ETCD使用RAFT协议保证各个节点之间的状态一致。根据RAFT算法原理,节点数目越多,会降低集群的写性能。这是因为每一次写操作,需要集群中大多数节点将日志落盘成功后,Leader节点才能将修改内部状态机,并返回将结果返回给客户端。

      也就是说在等同配置下,节点数越少,集群性能越好。显然,只部署1个节点是没什么意义的。通常,按照需求将集群节点部署为3,5,7,9个节点。

      这里能选择偶数个节点吗? 最好不要这样。原因有二:

      • 偶数个节点集群不可用风险更高,表现在选主过程中,有较大概率或等额选票,从而触发下一轮选举。
      • 偶数个节点集群在某些网络分割的场景下无法正常工作。试想,当网络分割发生后,将集群节点对半分割开。此时集群将无法工作。按照RAFT协议,此时集群写操作无法使得大多数节点同意,从而导致写失败,集群无法正常工作。

      当网络分割后,ETCD集群如何处理的呢?

      • 当集群的Leader在多数节点这一侧时,集群仍可以正常工作。少数节点那一侧无法收到Leader心跳,也无法完成选举。
      • 当集群的Leader在少数节点这一侧时,集群仍可以正常工作,多数派的节点能够选出新的Leader, 集群服务正常进行。

      当网络分割恢复后,少数派的节点会接受集群Leader的日志,直到和其他节点状态一致。

      3. ETCD参数说明

      这里只列举一些重要的参数,以及其用途。

      • —data-dir 指定节点的数据存储目录,这些数据包括节点ID,集群ID,集群初始化配置,Snapshot文件,若未指定—wal-dir,还会存储WAL文件;
      • —wal-dir 指定节点的was文件的存储目录,若指定了该参数,wal文件会和其他数据文件分开存储。
      • —name 节点名称
      • —initial-advertise-peer-urls 告知集群其他节点url.
      • — listen-peer-urls 监听URL,用于与其他节点通讯
      • — advertise-client-urls 告知客户端url, 也就是服务的url
      • — initial-cluster-token 集群的ID
      • — initial-cluster 集群中所有节点

      4. 通过静态配置方式启动ETCD集群

      按照官网中的文档,即可完成集群启动。这里略。

      5. 通过服务发现方式启动ETCD集群

      ETCD还提供了另外一种启动方式,即通过服务发现的方式启动。这种启动方式,依赖另外一个ETCD集群,在该集群中创建一个目录,并在该目录中创建一个_config的子目录,并且在该子目录中增加一个size节点,指定集群的节点数目。

      在这种情况下,将该目录在ETCD中的URL作为节点的启动参数,即可完成集群启动。使用
      --discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83 配置项取代静态配置方式中的--initial-cluster 和inital-cluster-state参数。其中https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83是在依赖etcd中创建好的目录url。

      6. 节点迁移

      在生产环境中,不可避免遇到机器硬件故障。当遇到硬件故障发生的时候,我们需要快速恢复节点。ETCD集群可以做到在不丢失数据的,并且不改变节点ID的情况下,迁移节点。
      具体办法是:

      • 1)停止待迁移节点上的etc进程;
      • 2)将数据目录打包复制到新的节点;
      • 3)更新该节点对应集群中peer url,让其指向新的节点;
      • 4)使用相同的配置,在新的节点上启动etcd进程;

      5. 结束

      本文记录了ETCD集群启动的一些注意事项,希望对你有帮助。

      https://yq.aliyun.com/articles/29897?spm=5176.100239.blogcont11035.15.7bihps

      ETCD系列之三:网络层实现

      1. 概述

      在理清ETCD的各个模块的实现细节后,方便线上运维,理解各种参数组合的意义。本文先从网络层入手,后续文章会依次介绍各个模块的实现。

      本文将着重介绍ETCD服务的网络层实现细节。在目前的实现中,ETCD通过HTTP协议对外提供服务,同样通过HTTP协议实现集群节点间数据交互。

      网络层的主要功能是实现了服务器与客户端(能发出HTTP请求的各种程序)消息交互,以及集群内部各节点之间的消息交互。

      2. ETCD-SERVER整体架构

      ETCD-SERVER 大体上可以分为网络层,Raft模块,复制状态机,存储模块,架构图如图1所示。

      图1 ETCD-SERVER架构图

      • 网络层:提供网络数据读写功能,监听服务端口,完成集群节点之间数据通信,收发客户端数据;
      • Raft模块:完整实现了Raft协议;
      • 存储模块:KV存储,WAL文件,SNAPSHOT管理
      • 复制状态机:这个是一个抽象的模块,状态机的数据维护在内存中,定期持久化到磁盘,每次写请求会持久化到WAL文件,并根据写请求的内容修改状态机数据。 ## 3. 节点之间网络拓扑结构 ETCD集群的各个节点之间需要通过HTTP协议来传递数据,表现在:
      • Leader 向Follower发送心跳包, Follower向Leader回复消息;
      • Leader向Follower发送日志追加信息;
      • Leader向Follower发送Snapshot数据;
      • Candidate节点发起选举,向其他节点发起投票请求;
      • Follower将收的写操作转发给Leader;

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

      因此,ETCD集群节点之间的网络拓扑是一个任意2个节点之间均有长链接相互连接的网状结构。如图2所示。

      图2 ETCD集群节点网络拓扑图

      需要注意的是,每一个节点都会创建到其他各个节点之间的长链接。每个节点会向其他节点宣告自己监听的端口,该端口只接受来自其他节点创建链接的请求。

      4. 节点之间消息交互

      在ETCD实现中,根据不同用途,定义了各种不同的消息类型。各种不同的消息,最终都通过google protocol buffer协议进行封装。这些消息携带的数据大小可能不尽相同。例如 传输SNAPSHOT数据的消息数据量就比较大,甚至超过1GB, 而leader到follower节点之间的心跳消息可能只有几十个字节。

      因此,网络层必须能够高效地处理不同数据量的消息。ETCD在实现中,对这些消息采取了分类处理,抽象出了2种类型消息传输通道:Stream类型通道和Pipeline类型通道。这两种消息传输通道都使用HTTP协议传输数据。

      图3 节点之间建立消息传输通道

      集群启动之初,就创建了这两种传输通道,各自特点:

      • Stream类型通道:点到点之间维护HTTP长链接,主要用于传输数据量较小的消息,例如追加日志,心跳等;
      • Pipeline类型通道:点到点之间不维护HTTP长链接,短链接传输数据,用完即关闭。用于传输数据量大的消息,例如snapshot数据。

      如果非要做做一个类别的话,Stream就向点与点之间维护了双向传输带,消息打包后,放到传输带上,传到对方,对方将回复消息打包放到反向传输带上;而Pipeline就像拥有N辆汽车,大消息打包放到汽车上,开到对端,然后在回来,最多可以同时发送N个消息。

      Stream类型通道
      Stream类型通道处理数据量少的消息,例如心跳,日志追加消息。点到点之间只维护1个HTTP长链接,交替向链接中写入数据,读取数据。

      Stream 类型通道是节点启动后主动与其他每一个节点建立。Stream类型通道通过Channel 与Raft模块传递消息。每一个Stream类型通道关联2个Goroutines, 其中一个用于建立HTTP链接,并从链接上读取数据, decode成message, 通过Channel传给Raft模块中,另外一个通过Channel 从Raft模块中收取消息,然后写入通道。

      具体点,ETCD使用golang的http包实现Stream类型通道:

      • 1)被动发起方监听端口, 并在对应的url上挂载相应的handler(当前请求来领时,handler的ServeHTTP方法会被调用)
      • 2)主动发起方发送HTTP GET请求;
      • 3)监听方的Handler的ServeHTTP访问被调用(框架层传入http.ResponseWriter和http.Request对象),其中http.ResponseWriter对象作为参数传入Writter-Goroutine(就这么称呼吧),该Goroutine的主循环就是将Raft模块传出的message写入到这个responseWriter对象里;http.Request的成员变量Body传入到Reader-Gorouting(就这么称呼吧),该Gorutine的主循环就是不断读取Body上的数据,decode成message 通过Channel传给Raft模块。

      Pipeline类型通道
      Pipeline类型通道处理数量大消息,例如SNAPSHOT消息。这种类型消息需要和心跳等消息分开处理,否则会阻塞心跳。

      Pipeline类型通道也可以传输小数据量的消息,当且仅当Stream类型链接不可用时。

      Pipeline类型通道可用并行发出多个消息,维护一组Goroutines, 每一个Goroutines都可向对端发出POST请求(携带数据),收到回复后,链接关闭。

      具体地,ETCD使用golang的http包实现的:

      • 1)根据参数配置,启动N个Goroutines;
      • 2)每一个Goroutines的主循环阻塞在消息Channel上,当收到消息后,通过POST请求发出数据,并等待回复。

      5. 网络层与Raft模块之间的交互

      在ETCD中,Raft协议被抽象为Raft模块。按照Raft协议,节点之间需要交互数据。在ETCD中,通过Raft模块中抽象的RaftNode拥有一个message box, RaftNode将各种类型消息放入到messagebox中,有专门Goroutine将box里的消息写入管道,而管道的另外一端就链接在网络层的不同类型的传输通道上,有专门的Goroutine在等待(select)。

      而网络层收到的消息,也通过管道传给RaftNode。RaftNode中有专门的Goroutine在等待消息。

      也就是说,网络层与Raft模块之间通过Golang Channel完成数据通信。这个比较容易理解。

      6 ETCD-SERVER处理请求(与客户端的信息交互)

      在ETCD-SERVER启动之初,会监听服务端口,当服务端口收到请求后,解析出message后,通过管道传入给Raft模块,当Raft模块按照Raft协议完成操作后,回复该请求(或者请求超时关闭了)。

      7 主要数据结构

      网络层抽象为Transport类,该类完成网络数据收发。对Raft模块提供Send/SendSnapshot接口,提供数据读写的Channel,对外监听指定端口。

      8. 结束

      本文整理了ETCD节点网络层的实现,为分析其他模块打下基础。

      展开全文
    • etcd 实战 极客时间 整体架构 基础模块介绍 client 层: 包含 client v2 和 v3 两个大版本 API 客户端 API 网络层: 主要包含 clent 访问 server 和 server 节点之间的通信协议 clent 访问 server 分为两个版本...

      文章目录

      参考

      整体架构

      image-20211005183939463

      基础模块介绍

      • client 层: 包含 client v2 和 v3 两个大版本 API 客户端
      • API 网络层:
        • 主要包含 clent 访问 server 和 server 节点之间的通信协议
        • clent 访问 server 分为两个版本:v2 API 采用 HTTP/1.x 协议,v3 API 采用 gRPC 协议
        • server 之间的通信:是指节点间通过 Raft 算法实现数据复制和 Leader 选举等功能时使用的 HTTP 协议
      • Raft 算法层:
        • 实现了 Leader 选举、日志复制、ReadIndex 等核心算法特性
        • 用于保障 etcd 多节点间的数据一致性、提升服务可用性等,是 etcd 的基石和亮点
      • 功能逻辑层:
        • etcd 核心特性实现层
        • 如典型的 KVServer 模块、MVCC 模块、Auth 鉴权模块、Lease 租约模块、Compactor 压缩模块等
        • 其中 MVCC 模块主要有 treeIndex 模块和 boltdb 模块组成
      • 存储层:
        • 包含预写日志 WAL 模块、快照 Snapshot 模块、 boltdb 模块
        • 其中 WAL 可保障 etcd crash 后数据不丢失,boltdb 则保存了集群元数据和用户写入的数据

      写流程 简单了解

      image-20211005182608351
      • 简单了解一下写流程==

        1. client 发起一个更新 hello 为 world 请求后

        2. 若 Leader 收到写请求,它会将此请求持久化到 WAL 日志,并广播给各个节点

          a. 若一半以上节点持久化成功,则该请求对应的日志条目被标识为已提交

          b. 之后,etcdserver 模块异步从 Raft 模块获取已提交的日志条目,应用到状态机(boltdb等)

      读流程 详细了解

      image-20211005183904906

      串行读(数据敏感度低,适用计数等)

      image-20211002184614991
      • W——WAL 新数据;S——状态机 旧数据
      • 串行读
        • 直接读状态机数据返回、无需通过 Raft 协议与集群进行交互
        • 具有低延迟、高吞吐量的特点
      • 为什么串行读,会读到旧数据?
        • 因为 Follower 节点收到 Leader 节点的同步些请求后,应用日志条目到状态机是个异步过程
        • 因此产生一种需求,能否确保最新的数据已经应用到状态机中?—— 产生了线性读 ReadIndex 机制

      线性读(数据敏感度高,要求一致性)

      image-20211002185120699
      • 线性读 ReadIndex,原理图如上所示,以下简要文字描述
        1. Follower C 收到一个线性读请求时,首先会从 Leader 获取集群最新的已提交的日志索引(committed index),如上图流程2(发起请求)
        2. Leader 收到 ReadIndex 请求时,为了防止脑裂等异常场景,会向 Follower 节点发送心跳确认
          • 一半以上节点确认 Leader 身份后,才能将已提交的索引(committed index)返回给节点 C (上图流程3)
        3. C 节点会等待,直到状态机已应用索引(applied index)大于等于 Leader 的已提交索引时(committed index)(见上图流程4)
          • 然后去通知读请求,数据已赶上 Leader,可以去状态机中访问数据了(上图流程5)

      简而言之,就是当前节点向 Leader 问”你进行到哪了“,Leader 回复一个当前号码”committed index“,当前节点用自己的状态机号码"applied index"对比一下,若当前节点落后,则等一等,等到号码赶上

      (这中间还会涉及到,Leader 是否是真正的 Leader,因此会采用心跳机制向周围节点确认,防止该 Leader 是个假的,也就是防止脑裂)

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

      写流程 详细了解

      image-20211005182608351
      • 简单了解一下写流程

        1. client 发起一个更新 hello 为 world 请求后

        2. 若 Leader 收到写请求,它会将此请求持久化到 WAL 日志,并广播给各个节点

          a. 若一半以上节点持久化成功,则该请求对应的日志条目被标识为已提交

          b. 之后,etcdserver 模块异步从 Raft 模块获取已提交的日志条目,应用到状态机(boltdb等)

      回顾一下写入过程(详细版)

      client 发送 put请求时,首先到达 KVServer 模块(此部分还有认证、鉴权、限速模块),之后到达 Raft 模块,将日志条目写入到 Wal 模块(持久化),之后将日志条目写入到 Raft 稳定日志(内存中),此时标记为已提交状态,之后到达 Apply 模块,此模块利用 consistent index 字段记录已执行的日志(这个也是保证了 crash-safe 和幂等性),之后达到 MVCC 模块,实现真正的存储,MVCC 模块包含两部分 treeIndex 和 boltdb,treeIndex 是在内存中,维护 版本号 和用户key的映射关系,真正的数据是存储在 boltdb 中,boltdb 中,key 为版本号,value 是个结构体(包含用户的 key-value信息、关联的 LeaseID 等),boltdb 也是存在内存中,因此需要将数据磁盘化进行持久化,但不可能一条数据一次提交,开销太昂贵,所以采用批量提交方式,因此提交一条数据的话,虽然标记已提交了,但是并未在存储状态机中,因此直接访问状态机,不会获得最新数据,为了解决此问题,etcd 引入 buffer 机制,一个作用是为了缓存,另一个也是为了存储还未存到状态机的新数据(从而可以对外服务),那么这一部分数据在哪呢?其实在 boltdb 的 buffer bucket 桶中。

      etcd 重启时

      Raft 日志 从 WAL 模块恢复

      treeIndex 从 boltdb 模块恢复

      与读流程不一样的模块

      • 相比于读流程,写流程还涉及到 Quota、WAL、Apply 三个模块
      • crash-safe 及幂等性也正是基于 WAL 和 Apply 流程的 consistant index 实现的
        • crash-safe 就是保证发生故障时,仍可以正常回复
        • 幂等性就是 一个操作多次提交,但结果和 一次提交执行的结果相同,不会产生多次重复操作

      Quota 模块

      • 检查是否还有存储空间,去存新增数据
      • 默认 db 配额是 2G
      • 工作原理
        • 当 etcd server 收到 put/txn 等写请求时,会首先检查当前 etcd db 大小加上你请求的 key-value 大小之和是否超过了配额(quota-backend-bytes)
        • 若超过了配额,会产生告警 Alarm 请求,告警类型是 NO SPACE
          • 并通过 Raft 日志同步给其他节点,告知 db 无空间了,并将告警持久化存储到 db 中
        • 当把配额调大后,为什么集群依然拒绝写入?
          • 因为 Apply 模块在执行每个命令的时候,都会检查当前是否存在 NO SPACE 告警
            • 如果有,则拒绝写入
            • 所以,需要你额外发送一个取消告警(etcdctl alarm disarm)的命令,以消除所有告警
      • etcd 社区建议配额不要超过 8 G
      • 另外如何预防这种超配额问题呢?
        • 在压缩模块设置合理的压缩策略,用来回收旧版本

      KVServer 模块(读流程有此模块)

      • 经过上面的配额检查后,到达此
      • 此模块,需要将 put 写请求内容打包成一个提案消息,提交给 Raft 模块
      • 不过载 KVServer 模块在提交提案前,还要做一些检查和限速
      Preflight Check 检查
      • 作用:为了保证集群稳定,避免雪崩,任何提交到 Raft 模块的请求,都会做一些简单的限速判断

      • 什么时候限速呢?

        • 例如,Raft模块已提交的日志索引(committed index)比已应用到状态机的日志索引(applied index)多了5000
        • 就是 Raft 模块记录的日志多,状态机处理不过来了
      • 同时还会检查 token的有效性,及包的大小

      • image-20211005190535263

      WAL 模块

      • Raft 模块收到提案后
        • 如果当前节点是 Follower 节点,则会转发给 Leader 节点
        • 因为只有 Leader 节点才能处理写请求
      • 之后 Leader 节点上的 etcdserver 从 Raft 模块获取以上的消息和日志条目后
        • 会将 put 提案消息广播给集群各个节点
        • 同时需要把集群 Leader 任期号、投票信息、已提交索引、提案内容持久化到一个 WAL(Write Ahead Log)日志文件中(用于保证集群的一致性、可恢复性),这个也就是上面的流程 5
      WAL 记录类型(5种)
      1. 文件元数据记录
      • 包含节点 ID、集群 ID 信息,它在 WAL 文件创建的时候写入
      1. 日志条目记录
        • 包含 Raft 日志信息,如 put 提案内容
      2. 状态信息记录
        • 包含集群的任期号、节点投票信息等,一个日志文件中会有多条,以最后的记录为准
      3. CRC 记录
        • 包含上一个 WAL 文件的最后的 CRC(循环冗余校验码)信息
        • 在创建、切割 WAL 文件时,作为第一条记录写入到新的 WAL 文件,用于校验数据文件的完整性、准确性等
      4. 快照记录
        • 包含快照的任期号、日志索引信息,用于检查快照文件的准确性
      WAL 日志的结构
      image-20211005194845282
      WAL 日志构成举例

      以 日志条目记录 类型 为例,其数据结构,包含了如下字段

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

      再看 WAL 模块如何持久化 Raft 日志条目

      1. 首先将 Raft 日志条目内容(含任期号、索引、提案内容)序列化后保存到 WAL 记录的 Data 字段
      2. 然后计算 Data 的 CRC 值,设置 Type 为 Entry Type
      3. 以上信息就组成了一个完整的 WAL 记录
      4. 最后计算 WAL 记录的长度,顺序写入 WAL 长度(Len Field)
      5. 然后写入记录内容,调用 fsync 持久化到磁盘,完成日志条目保存到持久化存储中

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

      于是进入了流程 6

      etcdserver 模块从 channel 取出提案内容,添加到先进先出(FIFO)调度队列

      随后通过 Apply 模块按入队顺序,异步、依次执行提案内容

      Apply 模块

      image-20211005201851241
      crash-safe 如何实现

      核心就是 WAL 日志

      • 若 put 请求提案在执行流程七的时候 etcd 突然 crash 了, 重启恢复的 时候,etcd 是如何找回异常提案,再次执行的呢?
      • 因为 Raft 模块再将提案交给 Apply 模块前,已在 WAL 模块中进行持久化(就是 WAL 中的内容是获得多数节点确认的)
      • 因此 etcd 重启时,会从 WAL 中解析出 Raft 日志条目内容,追加到 Raft 日志存储中,并重放已提交的日志提案给 Apply 模块执行
      幂等性如何实现?

      该考虑的问题 —— 如何确保幂等性,防止提案重复执行导致数据混乱?

      引入 consistent index 字段,存储已执行过的日志条目索引

      • 提案的唯一标识 —— 日志条目索引
        • 日志条目索引是 全局单调递增的,每个日志条目索引对应一个提案
      • 考虑利用日志条目索引,解决幂等性问题
        • 考虑在db中,利用日志条目索引,记录其对应的执行状态
          • 这样可以,但是不够安全
          • 比如 命令的请求执行成功了,但在更新状态时发生失败了
          • 因此需要将这两个操作(执行,状态更新)作为原子事务提交
          • 所以 etcd 引入了一个 consistant index 字段,存储系统当前已经执行过的日志条目索引,实现幂等性
      MVCC 模块

      Apply 模块 判断此提案未执行后,就会调用 MVCC 模块来执行提案内容

      MVCC 模块主要由两部分组成

      1. 内存索引模块 treeIndex,保存 key 的历史版本号信息
      2. boltdb 模块,用来持久化存储 key-value 数据
      image-20211005211548994
      treeIndex
      • 维护 key 与 版本号revision 的映射关系

      • 版本号(revision)在 etcd 里发挥着重大作用,是 etcd 的逻辑时钟

        • etcd 启动时的默认版本号是 1,随着对 key 的增删改操作,而全局单调递增
      • etcd 不需要持久化一个全局版本号

        • 因为启动时,从最小值 1 开始枚举到最大值,未读到数据的时候结束,最后读出来的版本号,即为当前 etcd 的最大版本号 currentRevision
      • 采用数据结构 B-tree

        • 简单描述 B-tree 与 B+tree 的区别
        • B-tree 中间节点存储数据
        • B+tree 中间节点不存储数据,只存储索引,叶子节点存储所有节点的数据,利用链表连接起来,适合区间搜索
      boltdb
      • 基于 B+tree 实现的 key-value 嵌入式 db
        • key 为 版本号revision,value包含的内容很丰富(要存储的key,value信息等)
        • 重启时,treeIndex 也是根据 boltdb 重建的
        • value 值就是以下信息的结构体序列化成的二进制数据
          • 下面的 key 指的是用户的 key ,不是 版本号,value 指的是用户设置的 值
          • key 名称
          • key 创建时的版本号(create_revision)
          • 最后一次修改时的版本号(mod_revison)
          • key 自身修改的次数(version)
          • value 值
          • 租约信息(Lease)
      • 通过提供桶(bucket)机制实现类似 MySQL 表的逻辑隔离
        • 上面提到为了保证日志的幂等性,保存了一个名为 consistent index 的变量在 db 里面,实际上就存储在 元数据 meta 桶里面

      通过 boltdb 提供的 put 接口,etcd 快速完成了将你的数据写入 boltdb,对应上面流程 2

      但需要注意的是,以上流程中,etcd 并未提交事务(commit)

      因此数据只更新在 boltdb 所管理的内存数据结构中,并未持久化到 db 文件中

      一次一提交,开销昂贵,大大损失性能,因此 etcd 采用批量提交

      未提交时,通过 buffer 读取,etcd 引入 bucket buffer 来保存暂未提交的事务数据

      在更新 boltdb 时,etcd 也会同步数据到 bucket buffer

      • 事务提交带来的性能损失
        • 事务的提交过程,包含 B+tree 的平衡、分裂,将 boltdb 的脏数据(dirty page)、元数据信息刷新到磁盘上,因此事务提交的开销是昂贵的
        • 若每次更新都提交事务,etcd 写性能就会较差
        • 解决方法 —— 合并再合并(大白话,批量提交)
          • 因为 boltdb key 是版本号,put/delete 操作是,版本号都是递增的,属于顺序写入
          • 因此考虑调整 boltdb 的 bucket.FillPercent 参数,使每个 page 填充更多数据,减少 page 填充更多数据,减少 page 的分裂次数并降低 db 空间
          • 此外,etcd 通过合并多个写事务请求,异步机制定时(默认每100ms)将批量事务一次性提交,大大提高吞吐量
          • 不过也带来个问题,因为事务未提交,因此读请求可能无法从 boltdb 获取到最新数据
            • 为解决此问题,etcd 引入一个 bucket buffer 来保存暂未提交的事务数据
            • 在更新 boltdb 时,etcd 也会同步数据到 bucket buffer

      Raft 实现高可用,数据一致性

      多种多副本复制方法利弊分析

      • 多副本常用的技术方案主要有主从复制和去中心化复制。主从复制,又分为全同步复制、 异步复制、半同步复制,比如 MySQL/Redis 单机主备版就基于主从复制实现的。
      • 全同步复制是指主收到一个写请求后,必须等待全部从节点确认返回后,才能返回给客户 端成功。因此如果一个从节点故障,整个系统就会不可用。这种方案为了保证多副本的一 致性,而牺牲了可用性,一般使用不多
      • 异步复制是指主收到一个写请求后,可及时返回给 client,异步将请求转发给各个副本, 若还未将请求转发到副本前就故障了,则可能导致数据丢失,但是可用性是最高的。
      • 半同步复制介于全同步复制、异步复制之间,它是指主收到一个写请求后,至少有一个副 本接收数据后,就可以返回给客户端成功,在数据一致性、可用性上实现了平衡和取舍。
      • 跟主从复制相反的就是去中心化复制,它是指在一个 n 副本节点集群中,任意节点都可接 受写请求,但一个成功的写入需要 w 个节点确认,读取也必须查询至少 r 个节点。

      共识算法——解决上述方法困境

      image-20211005214942593
      • 共识算法,基于复制状态机背景提出的
      • 上图是复制状态机的结构,由共识模块、日志模块、状态机组成
        • 共识模块保证各个节点日志的一致性
        • 然后各个节点基于同样的日志、顺序执行指令,最终各个复制状态机的结果实现一致

      Raft 算法

      • 共识算法的祖师爷是 Paxos,但是过于复杂,难于理解
      • 因此为了可理解性、易于实现,Raft 算法诞生了,将复杂的共识问题拆分成三个子问题
        1. Leader 选举,Leader 故障后集群能快速选出新 Leader
        2. 日志复制,集群只有 Leader 能写入日志,Leader 负责复制日志到 Follower 节点,并强制 Follower 节点与自己保持相同
        3. 安全性,一个任期内集群只能产生一个 Leader、已提交的日志条目在发生 Leader 选举时,一定会存在更高任期的新 Leader 日志中、各个节点的状态机应用的任意位置的日志条目内容应一样等
      • Raft 原理动画 (推荐看看):http://thesecretlivesofdata.com/raft/

      Leader 选举

      三种角色

      • Follower
        • 跟随者,同步从 Leader 收到的日志,etcd 启动的时候默认为此状态
      • Candidate
        • 竞选者,可以发起 Leader 选举
      • Leader
        • 集群领导者,唯一性,拥有同步日志的特权,续订是广播心跳给 Follower 节点,以维持领导者身份

      选举过程 简述

      • 当 Follower 节点接收 Leader 节点心跳消息超时后,会转变为 Candidate 节点,并可发起竞选投票,若获得集群多数节点的支持后,它就可转变为 Leader 节点
      • Leader 都会有个任期号,通过任期号,可以比较各个节点的数据新旧、识别过期的 Leader 等,任期号在 Raft 算法中充当逻辑时钟,发挥着重要作用
      • 进入 Candidate 状态的节点,会发起选举流程,自增任期号,投票给自己,冰箱其他节点发送竞选 Leader 投票消息
        • 正常来说,任期号弱于对方,且没投票,当然投给对方
        • 但是为了防止任期号相同,因此引入随机数,使每个节点等待发起选举的时间点不一致

      日志复制

      image-20211006191415117
      1. 首先 Leader 收到 client 的请求后,etcdserver 的 KVServer 模块会向 Raft 模块提交一个(如 put hello 为 world的)提案消息,消息类型是 MsgProp
      2. Leader 的 Raft 模块获取到 MsgProp 提案消息后,为此提案生成一个日志条目,追加到为持久化、不稳定的 Raft 日志中
      3. 随后会遍历集群 Follower 列表和进度信息,为每个 Follower 生成追加(MsgApp)类型的 RPC 消息,此消息中包含待复制给 Follower 的日志条目

      这里就出现两个疑问了❓

      1. Leader 如何知道哪个索引位置发送日志条目给 Follower,以及 Follower 已复制的日志最大索引是多少呢?
      2. 日志条目什么时候才会追加到稳定的 Raft 日志中呢?Raft 模块负责持久化吗?

      Raft 日志

      image-20211006192544716

      以上是 Raft 日志复制过程中的日志细节图

      • 最上方是日志条目序号/索引
      • 每个日志条目内容保存了 Leader 任期号和提案内容

      解答疑问 1

      Leader 如何知道哪个索引位置发送日志条目给 Follower,以及 Follower 已复制的日志最大索引是多少呢?

      • Leader 会维护两个核心字段来追踪各个 Follower 的进度信息
        • NetIndex 字段,表示 Leader 发送给 Follower 节点的下一个日志条目索引
        • MatchIndex 字段,表示 Follower 节点复制的最大日志条目的索引(上图 C 节点最大日志条目索引是 5,A节点是4)

      解答疑问 2

      日志条目什么时候才会追加到稳定的 Raft 日志中呢?Raft 模块负责持久化吗?

      • etcd Raft 模块设置及实现上抽象了网络、存储、日志等模块,本身不会进行网络、存储相关操作
        • 上册应用需结合自己业务场景选择内置的模块或自定义实现网络、存储、日志等模块
      • (见上图2,3流程)上层应用通过 Raft 模块的输出接口(如 Ready 结构),获取到待持久化的日志条目和待发送给 Peer 节点的消息后(如上面的 MsgApp 日志消息)
        • 之后需要 持久化 日志条目到自定义的 WAL 模块(图中流程4)
        • 并通过自定的网络模块将消息发送给 Peer 节点(图中流程4)
      • 日志条目持久化到稳定存储中后,此时就可以将日志条目追加到稳定的 Raft 日志中(图中流程5)
        • 即便 Raft 这个日志是内存存储,节点重启后也不会丢失任何日志条目
        • 因为 WAL 模块已持久此日志条目,可通过它重建 Raft 日志
      • etcd Raft 模块提供了一个内置的内存存储(MemoryStorge)模块实现,etcd 使用就是这个,Raft 日志条目保存在内存中
      • 网络模块并未提供内置的实现,etcd 基于 HTTP 协议实现了 peer 节点间的网络通信
        • 并根据消息类型,支持选择 pipeline 、stream 等模式发送,显著提高了网络吞吐量、降低了延时

      日志流图梳理

      • Raft 模块输入的是 Msg 消息,输出是一个 Ready 结构

        • Ready 结构包含待持久化的日志条目、发送给 peer 节点的消息、已提交的日志条目内容、线性查询结果等 Raft 输出核心信息
      • etcdserver 模块通过 channel 从 Raft 模块获取到 Ready 结构后(图中流程3)

        • 因为其是 Leader 节点,所以它首先会通过基于 HTTP 协议的网络模块将追加日志条目消息(MsgApp)广播给 Follower(图中流程4)
        • 同时将待持久化的日志条目持久化到 WAL 文件中(图中流程4)
        • 最后将日志追加到稳定的 Raft 日志存储中(图中流程5)
      • 各个 Follower 收到追加日志条目(MsgApp)消息

        • 通过安全检查后,会持久化消息到 WAL 日志中,并将消息追加到 Raft 日志存储
        • 随后想 Leader 回复一个应答追加日志条目(MsgAppResp)的消息,告知 Leader 当前已复制的日志最大索引(图中流程6)
      • Leader 收到应答追加日志条目(MsgAppResp)消息后,会将 Follower 回复的已复制日志最大索引更新到跟踪 Follower 进展的 Match Index 字段

      • 最后 Leader 根据 Follower 的 MatchIndex 信息,计算出一个位置

        • 若该位置已经被一半以上节点持久化,那么这个位置之前的日志条目都可以被标记为已提交
      • Leader 在发送心跳消息给 Follower 节点时,会告知目前已经提交的日志索引位置

      • 最后各个节点的 etcdserver 模块,可通过 channel 从 Raft 模块获取到已提交的日志条目(图中流程7)

        • 之后应用日志条目内容到存储状态机(图中流程8),返回结果给 client
        • 存储状态机是什么? 我理解是 boltdb 部分,将用户提交的操作,进行真正的处理及存储,此部分只是写到了 WAL 日志和本地内存的 Raft 稳定日志中

      安全性

      选举规则

      • 当节点收到选举投票的时候
        • 需检查候选者的最后一条日志中的任期号
        • 若小于自己则拒绝投票
        • 若任期号相同,日志却比自己段,也拒绝为其投票

      日志复制规则

      Leader 节点若频繁发生 crash 后,可能会导致 Follower 节点与 Leader 节点日志条目冲突,如何保证各个节点的同 Raft 日志位置含有同样的日志条目?

      此异常的安全性保障依靠 Raft 算法的安全机制:

      1. Leader 完全特性
      2. 只附加原则
      3. 日志匹配等
      Leader 完全特性
      • 是指若某个日志条目在某个任期号中已经被提交,那么这个条目必然出现在更大任期号的所有 Leader 中
      只附加原则
      • Leader 只能追加日志条目,不能删除已持久化的日志条目
      日志匹配特性
      • 为了保证各个节点日志一直性,Raft 算法在追加日志的时候,引入了一致性检查
        • Leader 在发送追加日志 RPC 消息是,会把新的日志条目紧接着之前的条目的索引位置和任期号包含在里面
        • Follower 节点会检查相同索引位置的任期号是否与 Leader 一致,一致才能追加

      鉴权

      整体架构

      控制面

      image-20211006205615830
      • 可以通过客户端工具 etcdctl 和鉴权 API 动态调整认证、鉴权规则,AuthServer 收到请求后,为了确保各节点加农机安全元数据一致性,会通过 Raft 模块进行数据同步
      • 执行流程为
        • 首先客户端设置规则,形成 Raft 日志
        • 当对应的 Raft 日志条目被集群半数以上节点确认后,Apply 模块通过鉴权存储(AuthStore)模块,执行日志条目的内容
        • 最后设置的规则将会存储到 boltdb 的一系列”鉴权表“里

      数据面

      image-20211006210358405
      • 数据面鉴权流程,由认证和授权流程组成

      • 认证的目的是检查 client 的身份是否合法、防止匿名用户访问等

      • 目前 etcd 实现了两种认证机制

        • 密码认证和证书认证
      • 认证通过后,为了提高密码认证性能,会分配一个 Token 给 client

        • client 后续其他请求携带此 Token,server 就可快速完成 client 的身份校验工作
      • 实现分配 Token 的服务也有很多中,这是 TokenProvider 所负责的,目前支持 SimpleToken 和 JWT 两种

        • JWT 是无状态的。JWT Token 自带用户名、版本号、过期时间 等描述信息,etcd server 不需要保存它
      • 通过认证后,在访问 MVCC 模块之前,还需要通过授权流程

        • 授权的目的是检查 client 是否有权限操作你请求的数据路径
        • etcd 实现了 RBAC 机制,支持为每个用户分配一个角色,为每个角色授予最小化的权限

      如何安全存储密码

      • etcd 的用户密码存储正是融合了高安全性 hash 函数(Blowfish encryption algorithm)、随机的加盐 salt、可自定义的 hash 值计算迭代次数 cost。

      • etcd 创建用户流程

        • 鉴权模块收到此命令后,它会使用 bcrpt 库的 blowfish 算法,基于明文密码、随机分配的 salt、自定义的 cost、迭代多次计算得到一个 hash 值,并将加密算法版本、salt 值、 cost、hash 值组成一个字符串,作为加密后的密码。
        • 最后,鉴权模块将用户名作为 key,用户名、加密后的密码作为 value,存储到 boltdb 的 authUsers bucket 里面,完成一个账号创建。
        • 当你使用用户账号访问 etcd 的时候,你需要先调用鉴权模块的 Authenticate 接口,它 会验证你的身份合法性。
      • etcd 验证密码流程

        • 鉴权模块首先会根据你请求的用户名,从 boltdb 获取加密后的密码,因此 hash 值 包含了算法版本、salt、cost 等信息,因此可以根据你请求中的明文密码,计算出最终的 hash 值,若计算结果与存储一致,那么身份校验通过。

      租约 Lease(检测客户端是否存活)

      • 首先了解 Leader 选举机制的本质是什么呢?
        • 是为了保证 Leader 的唯一性
      • 之后若主节点故障了,备节点如何快速感应到呢?(也就是活性探测)这里有两种活性探测方案
        1. 被动型检测,通过探测节点定时拨测 Leader 节点,看其是否健康,比如 Redis Sentinel
        2. 主动上报,Leader 节点定期向协调服务发送”特殊心跳“汇报状态
          • 若为正常发送心跳,且超过和协调服务约定的最大存活时间后,就会被协调服务移除 Leader 身份标识
          • 同时其他节点通过协调服务,快速告知到 Leader 故障了,进而发起新的选举

      今天的主题,Lease,正是基于主动型上报模式,提供的一种活性探测机制

      Lease,就是 client 和 etcd server 之间存在一个约定,内容是 etcd server 保证在约定的有效期内(TTL),不会删除你关联到此 Lease 的 key-value

      整体架构

      image-20211007194551005
      • etcd 在启动的时候,创建 Lessor 模块时,会启动两个常驻 goroutine

        • RevokeExpiredLease 任务,定期检查是否有过期 Lease,发起撤销过期的 Lease 操作
          • 定期从最小堆取出已过期的 Lease,执行删除 Lease 和其关联的 key列表
          • etcd Lessor 主循环每隔 500ms 执行一次撤销 Lease 检查(RevokeExpiredLease),每次轮询堆顶
            • 若过期,则加入待淘汰列表,直到堆顶的 Lease 过期时间大于当前,结束本轮轮询
          • Lessor 模块获取到过期的 LeaseID,保存在一个名为 expiredC 的 channel 中
            • etcd server 的主循环会定期从 channel 中获取 LeaseID,发起 revoke 请求,通过 Raft Log 传递给 Follower 节点
          • 各节点收到 revoke Lease 请求后,获取关联到此 Lease 上的 key 列表
            • 从 boltdb 中删除 key,从 Lessor 的 Lease map 内存中删除此 Lease 对象
            • 最后还需要从 boltdb 的 Lease bucket 中删除这个 Lease
          • 简而言之,Lessor 定期扫描过期的,记录到 channel中,Leader 定期查看 channel,发现有过期的了,发起 revoke(删除)操作,就是通知整个集群删除该 Lease 和其关联的数据

        检查 Lease 是否过期、维护最小堆、针对过期的 Lease 发起 revoke 操作,都是 Leader 节点负责的,类似于 Lease 的仲裁者

        通过以上清晰的权责划分,降低了 Lease 特性的实现复杂度

        因此也会面临个问题

        当 Leader 因重启、crash、磁盘 IO 等异常不可用时,选出了新 Leader,那么新 Leader 要完成以上职责,必须重建 Lease 过期最小堆等管理数据结构

        重建会触发什么问题呢?

        etcd 早期版本为了优化,并未持久化存储 Lease 剩余 TTL 信息,因此重建时就会自动给所有 Lease 自动续期了

        然而若频繁出现 Leader 切换,切换时间小于 Lease 的 TTL,这会导致 Lease 永远无法删除,大量 key 堆积,db 大小超过配额等异常

        为了解决此问题,etcd 引入了检查点机制,就是下面的 CheckpointScheduledLease 任务

        • CheckpointScheduledLease,定时触发更新 Lease 的剩余到期时间的操作
          • etcd 启动时,Leader 节点后台会运行此异步任务
            • 定期批量将 Lease 剩余的 TTL 基于 Raft Log 同步给 Follower 节点
            • Follower 节点收到 Checkpoint 请求后,更新内存数据结构 LeaseMap 的剩余 TTL 信息
          • 当 Leader 节点收到 KeepAlive 请求时
            • 也会通过 checkpoint 机制把此 Lease 的剩余 TTL 重置,并同步给 Follower 节点,尽量确保续期后集群各个节点的 Lease 剩余 TTL 一致性
          • 最后注意,此特性属于试验特性,可通过 experimental -enable-lease-checkpoint 参数开启
      • Lessor 模块提供的其他接口

        • Grant 表示创建一个 TTL 为指定秒数的 Lease,Lessor 会将 Lease 信息持久化存储在 boltdb 中
        • Revoke 表示撤销 Lease 并删除其关联的数据
        • LeaseTimeToLive 表示获取一个 Lease 的有效期、剩余时间
        • LeaseKeepAlive 表示为 Lease 续期
      # 创建一个TTL为600秒的lease,etcd server返回LeaseID 
      $ etcdctl lease grant 600 
      lease 326975935f48f814 granted with TTL(600s)
      
      # 查看lease的TTL、剩余时间 
      $ etcdctl lease timetolive 326975935f48f814 
      lease 326975935f48f814 granted with TTL(600s), remaining(590s)
      
      image-20211007195437042
      • 上图为将用户 key 与指定 Lease 关联(命令见上图 流程见下图)
        • 通过 put 命令,指定参数 --lease
        • 内部原理,MVCC 模块会通过 Lessor 的 Attach 方法,将 key 关联到 Lease 在 key 内存集合 ItemSet
          • 问题:Lease 的 key 集合是在内存中,那么 etcd 重启时,如何知道每个 Lease 上关联了哪些 key 呢?
          • 答:etcd 的 MVCC 模块在持久化存储 key-value 的时候,保存到 boltdb 的 value 是个结构体(mvccpb.KeyValue)
            • 该结构体不仅 包含了用户的 key-value 数据,还包含了 关联的 LeaseID 等信息

      回顾一下写入过程,client 发送 put请求时,首先到达 KVServer 模块,之后到达 Raft 模块,将日志条目写入到 Wal 模块(持久化),之后将日志条目写入到 Raft 稳定日志(内存中),此时标记为已提交状态,之后到达 Apply 模块,此模块利用 consistent index 字段记录已执行的日志,之后达到 MVCC 模块,实现真正的存储,MVCC 模块包含两部分 treeIndex 和 boltdb,treeIndex 是在内存中,维护 版本号 和用户key的映射关系,真正的数据是存储在 boltdb 中,boltdb 中,key 为版本号,value 是个结构体(包含用户的 key-value信息、关联的 LeaseID 等),boltdb 也是存在内存中,因此需要将数据磁盘化进行持久化,但不可能一条数据一次提交,开销太昂贵,所以采用批量提交方式,因此提交一条数据的话,虽然标记已提交了,但是并未在存储状态机中,因此直接访问状态机,不会获得最新数据,为了解决此问题,etcd 引入 buffer 机制,一个作用是为了缓存,另一个也是为了存储还未存到状态机的新数据(从而可以对外服务),那么这一部分数据在哪呢?其实在 boltdb 的 buffer bucket 桶中。

      image-20211007195856727

      MVCC

      • 核心思想:保存一个 key-value 数据的多个历史版本
        • etcd 基于它实现了可靠的 Watch 机制,避免 client 频繁发起 List Pod 等昂贵操作,保障etcd 集群稳定性
        • 同时 MVCC 还能以较低的并发控制开销,实现各类隔离级别的事务,保障事务的安全性,是事务特性的基础
      • MVCC 机制是基于多版本技术实现的一种乐观锁机制
        • 乐观地认为数据不会发生冲突,但当事务提交时,具备检测数据是否冲突的能力
      • 在 MVCC 数据库中,更新一个 key-value 数据时,并不会直接覆盖原数据,而是新增一个版本来存储新数据,每个数据都有一个版本号
        • 当指定版本读取数据时,实际上访问的是版本号生成那个时间点的快照数据
        • 当删除数据时,实际上也是新增一条代删除标识的数据记录

      整体架构

      image-20211007212646053
      • Apply 模块通过 MVCC 模块来执行 put 请求,持久化 key-value 数据
      • MVCC 模块将请求划分为两类别
        • 读事务(ReadTxn),负责处理 range 请求
        • 写事务(WriteTxn),负责 put/delete 操作
      • 读写事务基于 treeIndex、Backend/boltdb 提供的能力,实现对 key-value 的增删改查功能
        • treeIndex 基于内存版的 B-tree 实现了 key 索引管理,保存了用户 key 和版本号(revision)的映射关系等信息
        • Backend 模块负责 etcd 的 key-value 持久化存储,主要由 ReadTx、BatchTx、Buffer 组成
          • ReadTx 定义了抽象的读事务接口
          • BatchTx 在 ReadTx 之上定义了抽象的写事务接口
          • Buffer 是数据缓存区
          • etcd 支持多种 Backend 实现,目前采用 boltdb,其是基于 B+tree 实现的、支持事务的 key-value 嵌入式数据库
      image-20211007213332678

      treeIndex 原理

      • 实现 key 的历史版本保存
      • 为什么选用 B-tree,而不是哈希表、平衡二叉树等?
        • 因为 B-tree 支持范围查询,同时每个节点可容纳多个数据,树的高度更低,更扁平,查找次数更少,性能优越
      image-20211007213736831
      • 在 treeIndex 中,每个节点的 key 是一个 keyIndex 结构,etcd 就是通过此保存 用户key 与 版本号 的映射关系
        • image-20211007213915621 - 用户的 key - 最后一次修改 key 时的版本号 - key 的若干代(generation)版本号信息
        • image-20211007213939834 - ver key 的修改次数 - created 是 generation 创建时的版本号 - revs 是对此 key 的修改版本号记录列表**(注意 revison 是个结构体)**
        • image-20211007214054800image-20211007214106498
          • main 是全局递增的版本号,是 etcd 逻辑时钟,随着 put/txn/delete 等事务递增
          • sub 是一个事务内的子版本号,从 0 开始随事务内的 put/delete 操作递增
          • 比如启动一个空集群,全局版本号默认为 1,执行下面的 txn 事务,它包含两次 put、一 次 get 操作,那么按照我们上面介绍的原理,全局版本号随读写事务自增,因此是 main 为 2,sub 随事务内的 put/delete 操作递增,因此 key hello 的 revison 为{2,0},key world 的 revision 为{2,1}。

      MVCC 更新 key 原理

      image-20211007214643584

      先获取,然后填充结构体,写入到 boltdb 缓存,写入到 buffer,再写入到 treeIndex,最后通过异步 goroutine 进行批量提交

      • 因为是第一次创建 hello key,此时 keyIndex 索引为空
      • 其次 etcd 会根据当前的全局版本号(空集群启动时默认为 1)自增,生成 put hello 操作 对应的版本号 revision{2,0},这就是 boltdb 的 key
        • boltdb 的 value 是 mvccpb.KeyValue 结构体,它是由用户 key、value、 create_revision、mod_revision、version、lease 组成。它们的含义分别如下:
          • create_revision 表示此 key 创建时的版本号。在我们的案例中,key hello 是第一次创 建,那么值就是 2。当你再次修改 key hello 的时候,写事务会从 treeIndex 模块查询 hello 第一次创建的版本号,也就是 keyIndex.generations[i].created 字段,赋值给 create_revision 字段
          • mod_revision 表示 key 最后一次修改时的版本号,即 put 操作发生时的全局版本号加 1
          • version 表示此 key 的修改次数。每次修改的时候,写事务会从 treeIndex 模块查询 hello 已经历过的修改次数,也就是 keyIndex.generations[i].ver 字段,将 ver 字段值 加 1 后,赋值给 version 字段。
      • 填充好 boltdb 的 KeyValue 结构体后,这时就可以通过 Backend 的写事务 batchTx 接口 将 key{2,0},value 为 mvccpb.KeyValue 保存到 boltdb 的缓存中,并同步更新 buffer, 如上图中的流程 2 所示。(缓存到 boltdb,保存到 buffer)
      • 此时存储到 boltdb 中的 key、value 数据如下:
        • image-20211007214949038
      • 然后 put 事务需将本次修改的版本号与用户 key 的映射关系保存到 treeIndex 模块中,也 就是上图中的流程 3(保存到 treeIndex)
        • 因为 key hello 是首次创建,treeIndex 模块它会生成 key hello 对应的 keyIndex 对象, 并填充相关数据结构
      • 但是此时数据还并未持久化,为了提升 etcd 的写吞吐量、性能,一般情况下(默认堆积的 写事务数大于 1 万才在写事务结束时同步持久化),数据持久化由 Backend 的异步 goroutine 完成,它通过事务批量提交
        • 定时将 boltdb 页缓存中的脏数据提交到持久化存 储磁盘中,也就是下图中的黑色虚线框住的流程 4

      MVCC 查询 key 原理

      image-20211007220019274
      • 并发读特性的核心原理是创建读事务对象时,它会全量拷贝当前写事务未提交的 buffer 数 据,并发的读写事务不再阻塞在一个 buffer 资源锁上,实现了全并发读
      • 未带版本号读,默认是读取最新的数据

      MVCC 删除 key 原理

      • etcd 实现的是延期删除模式,原理与 key 更新类似

      • 与更新 key 不一样之处在于

        • 生成的 boltdb key 版本号{4,0,t}追加了删除标识 (tombstone, 简写 t)
        • boltdb value 变成只含用户 key 的 KeyValue 结构体
        • treeIndex 模块也会给此 key hello 对应的 keyIndex 对象,追加一个空的 generation 对象,表示此索引对应的 key 被删除了
      • boltdb 此时会插入一个新的 key revision{4,0,t},此时存储到 boltdb 中的 key-value 数 据如下

        • image-20211007220534799
      • key 打上删除标记后有哪些用途呢

        • 一方面删除 key 时会生成 events,Watch 模块根据 key 的删除标识,会生成对应的 Delete 事件
        • 另一方面,当你重启 etcd,遍历 boltdb 中的 key 构建 treeIndex 内存树时,你需要知道 哪些 key 是已经被删除的,并为对应的 key 索引生成 tombstone 标识
          • 真正删除 treeIndex 中的索引对象、boltdb 中的 key 是通过压缩 (compactor) 组件异步完成
          • 正因为 etcd 的删除 key 操作是基于以上延期删除原理实现的,因此只要压缩组件未回收 历史版本,我们就能从 etcd 中找回误删的数据

      Watch

      带着问题去思考(4大核心问题)

      1. client 获取事件的机制,etcd 是使用轮询模式还是推送模式呢?两者各有什么优缺 点?
      2. 事件是如何存储的? 会保留多久?watch 命令中的版本号具有什么作用?
      3. 当 client 和 server 端出现短暂网络波动等异常因素后,导致事件堆积时,server 端会丢弃事件吗?若你监听的历史版本号 server 端不存在了,你的代码该如何处理?
      4. 如果你创建了上万个 watcher 监听 key 变化,当 server 端收到一个写请求后, etcd 是如何根据变化的 key 快速找到监听它的 watcher 呢?

      轮询 vs 流式传输

      问题1: client 获取事件的机制,etcd 是使用轮询模式还是推送模式呢?两者各有什么优缺点?

      两种机制 etcd 都使用过

      etcd v2 Watch 机制实现中,client 通过 HTTP/1.1 协议长连接定时轮询 server,获取最新的数据变化事件,然而当你的 watcher 成千上万的时,即使集群空负载,大量轮询也会产生一定的 QPS, server 端会消耗大量的 socket、内存等资源,导致 etcd 的扩展性、稳定性无法满足 Kubernetes 等业务场景诉求

      etcd v3 中,为了解决 etcd v2 的以上缺陷,使用的是基于 HTTP/2 的 gRPC 协议,双向流的 Watch API 设计,实现了连接多路复用

      在 HTTP/2 协议中,HTTP 消息被分解独立的帧(Frame),交错发送,帧是最小的数据 单位。每个帧会标识属于哪个流(Stream),流由多个数据帧组成,每个流拥有一个唯一 的 ID,一个数据流对应一个请求或响应包

      image-20211008072005785
      • 如图所示,client 正在向 server 发送数据流 5 的帧,同时 server 也正在向 client 发送 数据流 1 和数据流 3 的一系列帧。一个连接上有并行的三个数据流,HTTP/2 可基于帧的 流 ID 将并行、交错发送的帧重新组装成完整的消息。
      • etcd 基于以上介绍的 HTTP/2 协议的多路复用等机制,实现了一个 client/TCP 连接支持多 gRPC Stream, 一个 gRPC Stream 又支持多个 watcher,如下如所示
        • 同时事件通知模式也从 client 轮询优化成 server 流式推送,极大降低了 server 端 socket、内存等资源
        • 在 clientv3 库中,Watch 特性被抽象成 Watch、Close、RequestProgress 三个简单 API 提供给开发者使用,屏蔽了 client 与 gRPC WatchServer 交互的复杂细节,实现了一 个 client 支持多个 gRPC Stream,一个 gRPC Stream 支持多个 watcher,显著降低了开发复杂度。
        • 同时当 watch 连接的节点故障,clientv3 库支持自动重连到健康节点,并使用之前已接收的最大版本号创建新的 watcher,避免旧事件回放
      image-20211008072226113

      滑动窗口 vs MVCC

      问题 2:事件是如何存储的? 会保留多久?watch 命令中的版本号具有什么作用?

      该问题本质是:历史版本存储

      etcd 经历了从滑动窗口到 MVCC 机制的演变,滑动窗口是仅保存有限的最近历史版本到内存中,而 MVCC 机制则将历史版本保存在磁盘中, 避免了历史版本的丢失,极大的提升了 Watch 机制的可靠性

      1. etcd v2 采用简单的环形数组来存储历史事件版本,当 key 被修改后,相关事件就 会被添加到数组中来。若超过 eventQueue 的容量,则淘汰最旧的事件
        • 缺陷显而易见的,固定的事件窗口只能保存有限的历史事件版本,是不可靠的
        • 当写请求较多的时候、client 与 server 网络出现波动等异常时,很容易导致事件丢失, client 不得不触发大量的 expensive 查询操作,以获取最新的数据及版本号,才能持续监听数据
        • 特别是对于重度依赖 Watch 机制的 Kubernetes 来说,显然是无法接受的。因为这会导致控制器等组件频繁的发起 expensive List Pod 等资源操作,导致 APIServer/etcd 出现高负载、OOM 等,对稳定性造成极大的伤害
      2. etcd v3 的 MVCC 机制,就是为解决 etcd v2 Watch 机制不可靠而诞生,etcd v3 则是将一个 key 的历史修改版本保存在 boltdb 里面
        • boltdb 是一个基于磁盘文件的持久化存储,因此它重启后历史事件不像 etcd v2 一样会丢失
        • 同时你可通过配置压缩策略,来控制保存的历史版本数

      最后 watch 命令中的版本号具有什么作用呢?

      • 版本号是 etcd 逻辑时钟,当 client 因网络等异常出现连接闪断后,通过版本号,它就可从 server 端的 boltdb 中获取错过的历史事件, 而无需全量同步,它是 etcd Watch 机制数据增量同步的核心

      可靠的时间推送机制

      问题 3:当 client 和 server 端出现短暂网络波动等异常因素后,导致事件堆积时,server 端会丢弃事件吗?若你监听的历史版本号 server 端不存在了,你的代码该如何处理?

      此问题的本质是可靠事件推送机制,要搞懂它,我们就得弄懂 etcd Watch 特性的整体架构、核心流程

      整体架构

      image-20211008073152501

      简要介绍一下 watch 的整体流程

      1. 当你通过 etcdctl 或 API 发起一个 watch key 请求的时候,etcd 的 gRPCWatchServer 收到 watch 请求后,会创建一个 serverWatchStream
        • serverWatchStream 负责接收 client 的 gRPC Stream 的 create/cancel watcher 请求 (recvLoop goroutine),并将从 MVCC 模块接收 的 Watch 事件转发给 client(sendLoop goroutine)
      2. 当 serverWatchStream 收到 create watcher 请求后,serverWatchStream 会调用 MVCC 模块的 WatchStream 子模块分配一个 watcher id,并将 watcher 注册到 MVCC 的 WatchableKV 模块
      3. 在 etcd 启动的时候,WatchableKV 模块会运行 syncWatchersLoop 和 syncVictimsLoop goroutine
        • 分别负责不同场景下的事件推送,它们也是 Watch 特性可靠性的核心之一
      4. 从架构图中你可以看到 Watch 特性的核心实现是 WatchableKV 模块

      核心解决方案(复杂度管理、问题拆分)

      • etcd 根据不同场景,对问题进行了分解,将 watcher 按场景分类,实现了轻重分离、低耦合

      synced watcher

      • 表示此类 watcher 监听的数据都已经同步完毕,在等待新的变更
      • 如果你创建的 watcher 未指定版本号 (默认 0)、或指定的版本号大于 etcd sever 当前最新的版本号 (currentRev),那么它就会保存到 synced watcherGroup 中
        • watcherGroup 负责管理多个 watcher,能够根据 key 快速找到监听该 key 的一个或多个 watcher

      unsynced watcher

      • 表示此类 watcher 监听的数据还未同步完成,落后于当前最新数据变更,正在努力追赶
        • 如果你创建的 watcher 指定版本号小于 etcd server 当前最新版本号,那么它就会保存到 unsynced watcherGroup 中
        • 比如我们的这个案例中 watch 带指定版本号 1 监听时,版本号 1 和 etcd server 当前版本之间的数据并未同步给你,因此它就属于此类

      最新事件推送机制

      image-20211008104950194

      当 etcd 收到一个写请求,key-value 发生变化的时候,处于 syncedGroup 中的 watcher,是如何获取到最新变化事件并推送给 client 的呢?

      当你创建完成 watcher 后,此时你执行 put hello 修改操作时,如上图所示,请求经过 KVServer、Raft 模块后 Apply 到状态机时,在 MVCC 的 put 事务中,它会将本次修改的后的 mvccpb.KeyValue 保存到一个 changes 数组中

      在 put 事务结束时,它会将 KeyValue 转换成 Event 事件,然后 回调 watchableStore.notify 函数(流程 5)。

      • notify 会匹配出监听过此 key 并处于 synced watcherGroup 中的 watcher,同时事件中的版本号要大于等于 watcher 监听的 最小版本号,才能将事件发送到此 watcher 的事件 channel 中。

      serverWatchStream 的 sendLoop goroutine 监听到 channel 消息后,读出消息立即推 送给 client(流程 6 和 7),至此,完成一个最新修改事件推送。

      异常场景重试机制

      新增了一个watcherBatch 结构名为 victim ,保障 Watch 时间的高可用性

      当堆积的事件推送到 watcher 接收 channel 失败后,会加入到 victim watcherBatch 数据结构中,等待下次重试

      • 若出现 channel buffer 满了,etcd 为了保证 Watch 事件的高可靠性,并不会丢弃它

        • 而 是将此 watcher 从 synced watcherGroup 中删除
        • 然后将此 watcher 和事件列表保存到 一个名为受害者 victim 的 watcherBatch 结构中,通过异步机制重试保证事件的可靠性
      • 还有一个点你需要注意的是,notify 操作它是在修改事务结束时同步调用的,必须是轻量级、高性能、无阻塞的,否则会严重影响集群写性能

      • 我们知道 WatchableKV 模块会启动两个异步 goroutine

        • 其中一个是 syncVictimsLoop,正是它负责 slower watcher 的堆积的事件推送
          • 基本工作原理是,遍历 victim watcherBatch 数据结构,尝试将堆积的事件再次推送到 watcher 的接收 channel 中。
          • 若推送失败,则再次加入到 victim watcherBatch 数据 结构中等待下次重试
          • 若推送成功,watcher 监听的最小版本号 (minRev) 小于等于 server 当前版本号 (currentRev)说明可能还有历史事件未推送,需加入到 unsynced watcherGroup
          • 若 watcher 的最小版本号大于 server 当前版本号,则加入到 synced watcher 集合
      • 下面一幅图总结各类 watcher 状态转换关系

      image-20211008110755841

      历史事件推送机制

      • WatchableKV 模块的另一个 goroutine,syncWatchersLoop,正是负责 unsynced watcherGroup 中的 watcher 历史事件推送

      • 在历史事件推送机制中,如果你监听老的版本号已经被 etcd 压缩了,client 该如何处理?

        • 要了解这个问题,我们就得搞清楚 syncWatchersLoop 如何工作
          • 核心支撑是 boltdb 中存储了 key-value 的历史版本。
        • syncWatchersLoop,它会遍历处于 unsynced watcherGroup 中的每个 watcher
          • 为了 优化性能,它会选择一批 unsynced watcher 批量同步,找出这一批 unsynced watcher 中监听的最小版本号。
        • 通过指定查询的 key 范围的最小版本号作为 开始区间,当前 server 最大版本号作为结束区间,遍历 boltdb 获得所有历史数据
        • 然后将 KeyValue 结构转换成事件,匹配出监听过事件中 key 的 watcher 后,将事件发送 给对应的 watcher 事件接收 channel 即可
        • 发送完成后,watcher 从 unsynced watcherGroup 中移除、添加到 synced watcherGroup 中,如下面的 watcher 状态转换 图黑色虚线框所示。
      • 若 watcher 监听的版本号已经小于当前 etcd server 压缩的版本号,历史变更数据就可能已丢失,因此 etcd server 会返回 ErrCompacted 错误给 client

        • client 收到此错误后,需重新获取数据最新版本号后,再次 Watch。
        • 你在业务开发过程中,使用 Watch API 最 常见的一个错误之一就是未处理此错误。
      image-20211008112700279

      高效的事件匹配

      问题 4:如果你创建了上万个 watcher 监听 key 变化,当 server 端收到一个写请求后, etcd 是如何根据变化的 key 快速找到监听它的 watcher 呢?

      答:

      etcd 的确使用 map 记录了监听单个 key 的 watcher,

      但是你要注意的是 Watch 特性不仅仅可以监听单 key,它还可以指定监听 key 范围、key 前缀,因此 etcd 还使用了如下的区间树。

      • 当收到创建 watcher 请求的时候,它会把 watcher 监听的 key 范围插入到上面的区间树 中,区间的值保存了监听同样 key 范围的 watcher 集合 /watcherSet。
      • 当产生一个事件时,etcd 首先需要从 map 查找是否有 watcher 监听了单 key,其次它还 需要从区间树找出与此 key 相交的所有区间,然后从区间的值获取监听的 watcher 集合。
      • 区间树支持快速查找一个 key 是否在某个区间内,时间复杂度 O(LogN),因此 etcd 基于 map 和区间树实现了 watcher 与事件快速匹配,具备良好的扩展性。
      image-20211008113038120

      事务(安全实现多 key 操作)

      • 事务,它就是为了简化应用层的编程模型而诞生的

      • etcd v3 为了解决多 key 的原子操作问题,提供了全新迷你事务 API,同时基于 MVCC 版 本号,它可以实现各种隔离级别的事务。它的基本结构如下

        • image-20211008113438914
        • 可以看到,事务 API 由 If 语句、Then 语句、Else 语句组成,这与我们平 时常见的 MySQL 事务完全不一样

        • If 语句支持哪些检查项呢?

          • key 的最近一次修改版本号 mod_revision,简称 mod。你可以通过它检查 key 最近一次被修改时的版本号是否符合你的预期
          • key 的创建版本号 create_revision,简称 create。你可以通过它检查 key 是否已存在
          • key 的修改次数 version。你可以通过它检查 key 的修改次数是否符合预期
          • key 的 value 值。你可以通过检查 key 的 value 值是否符合预期

      整体流程

      image-20211008113711316
      • 上图是 etcd 事务的执行流程,当你通过 client 发起一个 txn 转账事务操作时
        • 通过 gRPC KV Server、Raft 模块处理后,
        • 在 Apply 模块执行此事务的时候,
        • 它首先对你的事务的 If 语句进行检查,也就是 ApplyCompares 操作,
          • 如果通过此操作,则执行 ApplyTxn/Then 语句,
          • 否则执行 ApplyTxn/Else 语句。
        • 在执行以上操作过程中,它会根据事务是否只读、可写,通过 MVCC 层的读写事务对象, 执行事务中的 get/put/delete 各操作,也就是我们上一节课介绍的 MVCC 对 key 的读写 原理。

      事务 ACID 特性

      • ACID 是衡量事务的四个特性,由原子性(Atomicity)、一致性(Consistency)、隔离 性(Isolation)、持久性(Durability)组成
        • 原子性(Atomicity)是指在一个事务中,所有请求要么同时成功,要么同时失败
        • 持久性(Durability)是指事务一旦提交,其所做的修改会永久保存在数据库
        • 一致性(Consistency)的表述,其实在不同场景下,它的含义是 不一样的
          • CAP 原理中的一致性是指可线性化。核心原理是虽然整个系统是由多副本组成,但 是通过线性化能力支持,对 client 而言就如一个副本,应用程序无需关心系统有多少个副本
          • 一致性哈希,它是一种分布式系统中的数据分片算法,具备良好的分散性、平衡性
          • 事务中的一致性,它是指事务变更前后,数据库必须满足若干恒等条件的状态约束,一致性往往是由数据库和业务程序两方面来保障的。
        • 隔离性,它是指事务在执行过程中的可见性。 常见的事务隔离级别有以下四种
          • 未提交读(Read UnCommitted),也就是一个 client 能读取到未提交的事务。比 如转账事务过程中,Alice 账号资金扣除后,Bob 账号上资金还未增加,这时如果其他 client 读取到这种中间状态,它会发现系统总金额钱减少了,破坏了事务一致性的约束
          • 已提交读(Read Committed),指的是只能读取到已经提交的事务数据,但是存 在不可重复读的问题。比如事务开始时,你读取了 Alice 和 Bob 资金,这时其他事务修改 Alice 和 Bob 账号上的资金,你在事务中再次读取时会读取到最新资金,导致两次读取结 果不一样
          • 可重复读(Repeated Read),它是指在一个事务中,同一个读操作 get Alice/Bob 在事务的任意时刻都能得到同样的结果,其他修改事务提交后也不会影响你本 事务所看到的结果
          • 串行化(Serializable),它是最高的事务隔离级别,读写相互阻塞,通过牺牲并发 能力、串行化来解决事务并发更新过程中的隔离问题。对于串行化我要和你特别补充一 点,很多人认为它都是通过读写锁,来实现事务一个个串行提交的,其实这只是在基于锁 的并发控制数据库系统实现而已。
            • 为了优化性能,在基于 MVCC 机制实现的各个数据库系 统中,提供了一个名为“可串行化的快照隔离”级别,相比悲观锁而言,它是一种乐观并 发控制,通过快照技术实现的类似串行化的效果,事务提交时能检查是否冲突。

      boltdb 持久化存储你的 key-value 数据

      boltdb 磁盘布局

      image-20211008144425166
      • boltdb 文件指的是你 etcd 数据目录下的 member/snap/db 的文件, etcd 的 keyvalue、lease、meta、member、cluster、auth 等所有数据存储在其中。
      • etcd 启动的时候,会通过 mmap 机制将 db 文件映射到内存,后续可从内存中快速读取文件中的数据
      • 写请求通过 fwrite 和 fdatasync 来写入、持久化数据到磁盘
      • 文件的内容由若干个 page 组成,一般情况下 page size 为 4KB
      • page 按照功能可分为元数据页 (meta page)、B+ tree 索引节点页 (branch page)、B+ tree 叶子节点页 (leaf page)、空闲页管理页 (freelist page)、空闲页 (free page)
        • 文件最开头的两个 page 是固定的 db 元数据 meta page
        • 空闲页管理页记录了 db 中哪 些页是空闲、可使用的
        • 索引节点页保存了 B+ tree 的内部节点,如图中的右边部分所示,它们记录了 key 值
        • 叶子节点页记录了 B+ tree 中的 key-value 和 bucket 数据
      • boltdb 逻辑上通过 B+ tree 来管理 branch/leaf page, 实现快速查找、写入 key-value 数据

      boltdb API

      • boltdb 提供了非常简单的 API 给上层业务使用,当我们执行一个 put hello 为 world 命令时
        • boltdb 实际写入的 key 是版本号,value 为 mvccpb.KeyValue 结构体
      • 这里我们简化下,假设往 key bucket 写入一个 key 为 r94,value 为 world 的字符串, 其核心代码如下:
        • 通过 boltdb 的 Open API,我们获取到 boltdb 的核心对象 db 实例后
        • 然后通过 db 的 Begin API 开启写事务,获得写事务对象 tx
        • 通过写事务对象 tx, 你可以创建 bucket
        • 这里我们创建了一个名为 key 的 bucket(如果不存在),并使用 bucket API 往其中更新了一个 key 为 r94,value 为 world 的数据
        • 最后我们使用写事务的 Commit 接口提交整个事务,完成 bucket 创建和 key-value 数据 写入
      image-20211008144752594

      核心数据结构介绍

      • boltdb 整个文件由一个个 page 组成。最开头 的两个 page 描述 db 元数据信息,而它正是在 client 调用 boltdb Open API 时被填充 的
      image-20211008145003479

      page 磁盘页结构

      • 由页 ID(id)、页类型 (flags)、数量 (count)、溢出页数量 (overflow)、页面数据起始位置 (ptr) 字段组成
      • 页类型目前有如下四种:0x01 表示 branch page,0x02 表示 leaf page,0x04 表示 meta page,0x10 表示 freelist page
      • 数量字段仅在页类型为 leaf 和 branch 时生效
      • 溢出页数量是指当前页面数据存放不下, 需要向后再申请 overflow 个连续页面使用
      • 页面数据起始位置指向 page 的载体数据,比如 meta page、branch/leaf 等 page 的内容

      meta page 数据结构

      • 第 0、1 页我们知道它是固定存储 db 元数据的页 (meta page)
      • 它由 boltdb 的文件标识 (magic)、版 本号 (version)、页大小 (pagesize)、boltdb 的根 bucket 信息 (root bucket)、freelist 页面 ID(freelist)、总的页面数量 (pgid)、上一次写事务 ID(txid)、校验码 (checksum) 组 成。

      meta page 十六进制分析

      image-20211008145332025
      • 了解完 db 元数据页面原理后,那么 boltdb 是如何根据元数据页面信息快速找到你的 bucket 和 key-value 数据呢?

        • 这就涉及到了元数据页面中的 root bucket,它是个至关重要的数据结构。下面我们看看 它是如何管理一系列 bucket、帮助我们查找、写入 key-value 数据到 boltdb 中。

      bucket 数据结构

      image-20211008145530510
      • 之前介绍过的 auth/lease/meta 等熟悉的 bucket,它们都 是 etcd 默认创建的。那么 boltdb 是如何存储、管理 bucket 的呢?
        • meta page 中的,有一个名为 root、类型 bucket 的重要数据结构
        • bucket 由 root 和 sequence 两个字段组成,root 表示该 bucket 根节点的 page id
        • image-20211008145745210
        • meta page 中的 bucket.root 字段,存储的是 db 的 root bucket 页面信息
        • 你所看到的 key/lease/auth 等 bucket 都是 root bucket 的子 bucket
        • image-20211008145931126
        • 当 bucket 比较少时,我们子 bucket 数据可直接从 meta page 里 指向的 leaf page 中找到

      leaf page

      • meta page 的 root bucket 直接指向的是 page id 为 4 的 leaf page, page flag 为 0x02
        • leaf page 它的磁盘布局如下图所示,前半部分是 leafPageElement 数组,后半 部分是 key-value 数组
          • leafPageElement 包含 leaf page 的类型 flags, 通过它可以区分存储的是 bucket 名称 还是 key-value 数据
          • 当 flag 为 bucketLeafFlag(0x01) 时,表示存储的是 bucket 数据
          • 否则存储的是 keyvalue 数据
          • 当存储的是 bucket 数据的时候,key 是 bucket 名称,value 则是 bucket 结构信息
          • bucket 结构信息含有 root page 信息,通过 root page(基于 B+ tree 查找算法),可以快速找到你存储在这个 bucket 下面的 key-value 数据所在页面
        • image-20211008150111123

      branch page

      • branch page 保存 B+ tree 的非叶子节点 key 数据

      • boltdb 使用了 B+ tree 来高效管理所有子 bucket 和 key-value 数据,因此它可以支持大 量的 bucket 和 key-value

      • 只不过 B+ tree 的根节点不再直接指向 leaf page,而是 branch page 索引节点页。branch page flags 为 0x01

      • 磁盘布局如下图所示,前半 部分是 branchPageElement 数组,后半部分是 key 数组

        • branchPageElement 包含 key 的读取偏移量、key 大小、子节点的 page id
        • 根据偏移量和 key 大小,我们就可以方便地从 branch page 中解析出所有 key,然后二分搜索匹配 key,获取其子节点 page id,递归搜索,直至从 bucketLeafFlag 类型的 leaf page 中找 到目的 bucket name
        • 注意,boltdb 在内存中使用了一个名为 node 的数据结构,来保存 page 反序列化的结 果
      • image-20211008151809909

      从上面分析过程中你会发现,boltdb 存储 bucket 和 key-value 原理是类似的,将 page 划分成 branch page、leaf page,通过 B+ tree 来管理实现

      boltdb 为了区分 leaf page 存储的数据类型是 bucket 还是 key-value,增加了标识字段 (leafPageElement.flags)

      因此 key-value 的数据存储过程我就不再重复分析了

      freelist

      • 再看 meta page 中的另外一个核心字段 freelist
        • 我们知道 boltdb 将 db 划分成若干个 page,那么它是如何知道哪些 page 在使用中,哪 些 page 未使用呢
        • 答案是 boltdb 通过 meta page 中的 freelist 来管理页面的分配,freelist page 中记录了 哪些页是空闲的
        • 当你在 boltdb 中删除大量数据的时候,其对应的 page 就会被释放,页 ID 存储到 freelist 所指向的空闲页中
        • 当你写入数据的时候,就可直接从空闲页中申请页面使用
        • 下图是 freelist page 存储结构,pageflags 为 0x10,表示 freelist 类型的页,ptr 指向空闲页 id 数组
          • 注意在 boltdb 中支持通过多种数据结构(数组和 hashmap)来管理 free page,这里我介绍的是数组
      image-20211008152156195

      Open 原理

      • 首先它会打开 db 文件并对其增加文件锁目的是防止其他进程也以读写模式打开它后, 操作 meta 和 free page,导致 db 文件损坏
      • 其次 boltdb 通过 mmap 机制将 db 文件映射到内存中,并读取两个 meta page 到 db 对象实例中
      • 然后校验 meta page 的 magic、version、checksum 是否有效,若两个 meta page 都无效,那么 db 文件就出现了严重损坏,导致异常退出

      Put 原理

      • 通过 bucket API 创建一个 bucket、发起一个 Put 请求更 新数据时,boltdb 是如何工作的呢
      • 首先是根据 meta page 中记录 root bucket 的 root page,按照 B+ tree 的查找算法,从 root page 递归搜索到对应的叶子节点 page 面,返回 key 名称、leaf 类型
        • 如果 leaf 类型为 bucketLeafFlag,且 key 相等,那么说明已经创建过,不允许 bucket 重复创建,结束请求。
        • 否则往 B+ tree 中添加一个 flag 为 bucketLeafFlag 的 key,key 名称为 bucket name,value 为 bucket 的结构
      • 创建完 bucket 后,你就可以通过 bucket 的 Put API 发起一个 Put 请求更新数据
        • 它的 核心原理跟 bucket 类似,根据子 bucket 的 root page,从 root page 递归搜索此 key 到 leaf page
        • 如果没有找到,则在返回的位置处插入新 key 和 value
      image-20211008152812578

      事务提交原理

      image-20211008152849773
      • 首先从上面 put 案例中我们可以看到,插入了一个新的元素在 B+ tree 的叶子节点,它可 能已不满足 B+ tree 的特性,因此事务提交时,第一步首先要调整 B+ tree,进行重平 衡、分裂操作,使其满足 B+ tree 树的特性

      • 在重平衡、分裂过程中可能会申请、释放 free page,freelist 所管理的 free page 也发生 了变化。因此事务提交的第二步,就是持久化 freelist

      • 事务提交的第三步就是将 client 更新操作产生的 dirty page 通过 fdatasync 系统调用,持 久化存储到磁盘中

      • 最后,在执行写事务过程中,meta page 的 txid、freelist 等字段会发生变化,因此事务的最后一步就是持久化 meta page

      • 通过以上四大步骤,我们就完成了事务提交的工作,成功将数据持久化到了磁盘文件中, 安全地完成了一个 put 操作

      • 真正持久化数据到磁盘是通过事务提交执行的

      压缩 回收旧版本数据

      • etcd 是通过什么机制来回收历史版本数据,控制索引内存占用和 db 大小的呢?
        • 目前 etcd 支持两种压 缩模式,分别是时间周期性压缩和版本号压缩
          • 周期性压缩,希望 etcd 只保留最近一段时间写入的历史版本时,你就可以选择配置 etcd 的压缩模 式为 periodic,保留时间为你自定义的 1h 等
          • 版本号压缩,当你写请求比较多,可能产生比较多的历史版本导致 db 增长时,或者不确定配置 periodic 周期为多少才是最佳的时候
            • 可以通过设置压缩模式为 revision,指定保留的 历史版本号数。比如你希望 etcd 尽量只保存 1 万个历史版本,那么你可以指定 compaction-mode 为 revision,auto-compaction-retention 为 10000
        • 压缩的本质是回收历史版本,目标对象仅是历史版本,不包括一个 key-value 数据的最新版本,因此你可以放心执行压缩命令,不会删除你的最新版本数据

      整体架构

      image-20211008154045990

      压缩原理

      • Compact 请求经过 Raft 日志同步给多数节点后,etcd 会从 Raft 日志取出 Compact 请求,应用此请求到状态机执行

      • 执行流程如下图所示

        • MVCC 模块的 Compact 接口首先会检查 Compact 请求的版本号 rev 是否已被压缩过,若是则返回 ErrCompacted 错误给 client
        • 其次会检查 rev 是否大 于当前 etcd server 的最大版本号,若是则返回 ErrFutureRev 给 client
        • 通过检查后,Compact 接口会通过 boltdb 的 API 在 meta bucket 中更新当前已调度的压缩版本号 (scheduledCompactedRev) 号,然后将压缩任务追加到 FIFO Scheduled 中,异步调度执行
        • image-20211008154521805
      • 为什么 Compact 接口需要持久化存储当前已调度的压缩版本号到 boltdb 中呢?

        • 试想下如果不保存这个版本号,etcd 在异步执行的 Compact 任务过程中 crash 了,那么 异常节点重启后,各个节点数据就会不一致。
        • 因此 etcd 通过持久化存储 scheduledCompactedRev,节点 crash 重启后,会重新向 FIFO Scheduled 中添加压缩任务,已保证各个节点间的数据一致性。
      • 异步的执行压缩任务会做哪些工作呢?

        • 首先回顾 treeIndex 索引模块,它是 etcd 支持保存历史版本的核心 模块,每个 key 在 treeIndex 模块中都有一个 keyIndex 数据结构,记录其历史版本号信息
        • 异步压缩任务的第一项工作,就是压缩 treeIndex 模块中的各 key 的历 史版本、已删除的版本
          • image-20211008155158407
          • 为了避免压缩工作影响读写性能,首先会克隆一个 B-tree,然后通过克隆后的 B-tree 遍历每一个 keyIndex 对象,压缩历史版本号、清理已删除的版本
          • 假设当前压缩的版本号是 CompactedRev, 它会保留 keyIndex 中最大的版本号,移除小于等于 CompactedRev 的版本号,并通过一个 map 记录 treeIndex 中有效的版本号返回 给 boltdb 模块使用
          • 为什么要保留最大版本号呢?
            • 因为最大版本号是这个 key 的最新版本,移除了会导致 key 丢失。而 Compact 的目的是 回收旧版本
          • Compact 任务执行完索引压缩后,它通过遍历 B-tree、keyIndex 中的所有 generation 获得当前内存索引模块中有效的版本号,这些信息将帮助 etcd 清理 boltdb 中的废弃历史 版本
        • 压缩任务的第二项工作就是删除 boltdb 中废弃的历史版本数据。如上图所示,它通过 etcd 一个名为 scheduleCompaction 任务来完成
          • image-20211008155241117
          • scheduleCompaction 任务会根据 key 区间,从 0 到 CompactedRev 遍历 boltdb 中的 所有 key,通过 treeIndex 模块返回的有效索引信息,判断这个 key 是否有效,无效则调 用 boltdb 的 delete 接口将 key-value 数据删除
          • 在这过程中,scheduleCompaction 任务还会更新当前 etcd 已经完成的压缩版本号 (finishedCompactRev),将其保存到 boltdb 的 meta bucket 中
          • scheduleCompaction 任务遍历、删除 key 的过程可能会对 boltdb 造成压力,为了不影响正常读写请求,它在执行过程中会通过参数控制每次遍历、删除的 key 数(默认为 100,每批间隔 10ms),分批完成 boltdb key 的删除操作

      为什么压缩后 db 大小不减少呢?

      • 上节课我们介绍 boltdb 实现时,提到过 boltdb 将 db 文件划分成若干个 page 页,page 页又有四种类型,分别是 meta page、branch page、leaf page 以及 freelist page
      • branch page 保存 B+ tree 的非叶子节点 key 数据leaf page 保存 bucket 和 key value 数据,freelist 会记录哪些页是空闲的
      • 当我们通过 boltdb 删除大量的 key,在事务提交后 B+ tree 经过分裂、平衡,会释放出 若干 branch/leaf page 页面,然而 boltdb 并不会将其释放给磁盘,调整 db 大小操作是 昂贵的,会对性能有较大的损害
      • boltdb 是通过 freelist page 记录这些空闲页的分布位置,当收到新的写请求时,优先从 空闲页数组中申请若干连续页使用,实现高性能的读写(而不是直接扩大 db 大小)。当 连续空闲页申请无法得到满足的时候, boltdb 才会通过增大 db 大小来补充空闲页
      • 一般情况下,压缩操作释放的空闲页就能满足后续新增写请求的空闲页需求,db 大小会趋 于整体稳定。
      展开全文
    • etcd TLS集群

      2021-10-11 13:44:00
      概述 etcd集群默认情况下使用明文互相通讯,在某些场景下不安全,etcd走的都是HTTP协议,...证书的X509v3 extensions信息部分,X509v3 Extended Key Usage表示证书的用途。 server证书"server auth",client证书"cl
    • ETCD

      2021-04-16 23:09:16
      1、ETCD的使用场景 配置管理 服务注册于发现 选主 应用调度 分布式队列 分布式锁 2、ETCD实现配置管理 2.1、ETCD实现配置管理
    • etcd 服务入门指南

      2021-05-09 00:11:26
      点击下方公众号「关注」和「星标」回复“1024”获取独家整理的学习资料!Etcd 是一个使用一致性哈希算法(Raft)在分布式环境下的 key/value 存储服务。利用 Etcd 的特性...
    • kubernetes部署etcd集群

      2021-01-20 12:34:30
      用途 192.168.122.134/k8s-master1 etcd、kube-apiserver、kube-controller-manager、kube-scheduler 192.168.122.135/k8s-node1 etcd、kubelet、docker、kube_proxy 192.168.122.136/k8s-node2 etcd、...
    • ETCD基本操作

      千次阅读 2019-06-09 17:20:40
      etcd简介 etcd主要功能介绍 ... 用于储存key-value键值对,同时它不仅仅是存储,它主要用途是提供共享配置及服务发现,(后面这两个特征非常有特 点,主要用于container中),对于leader的选举非常优秀,他...
    • etcd的多版本并发控制

      2020-08-29 00:03:56
      深入浅出之etcd 深入浅出之etcd(二) etcd版本之v3 etcd之安全性阐述 在数据库领域,并发控制是一个具有挑战性的领驭。常见的并发控制方式包括悲观并发控制,乐观并发控制和多版本并发控制 悲观并发控制 在关系型...
    • https://github.com/coreos/etcd/blob/master/Documentation/api.mdAtomic Compare-and-Swapetcd can be used as a centralized coordination service in a cluster, andCompareAndSwap(CAS) is the most basic op.....
    • etcd实现原理

      千次阅读 2018-06-23 13:33:16
      网上关于 Etcd 的使用介绍的文章不少,但分析具体架构实现的文章不多,同时 Etcd v3的文档也非常稀缺。本文通过分析 Etcd 的架构与实现,了解其优缺点以及瓶颈点,一方面可以学习分布式系统的架构,另外一方面也可以...
    • Etcd 架构与实现解析

      万次阅读 多人点赞 2018-03-20 16:18:33
      前一段时间的项目里用到了 Etcd, 所以研究了一下它的源码以及实现。网上关于 Etcd 的使用介绍的文章不少,但分析具体架构实现的文章不多,同时 Etcd v3的文档也非常稀缺。本文通过分析 Etcd 的架构与实现,了解其优...
    • ETCD-常用命令介绍

      千次阅读 2019-12-17 17:43:16
      官方文档说ETCD的主要用途是“配置共享和服务发现”,关于这两个主要用途,在后面项目中如果有接触到再慢慢探究其具体的应用。现在主要还是将ETCD用于数据存储(这个行为有种将它当做Redis使用的...
    • Etcd 解析

      2018-07-12 11:39:59
      Etcd是一个分布式的,一致的 key-value 存储,主要用途是共享配置和服务发现。 主要提供以下能力 提供存储以及获取数据的接口,它通过协议保证 Etcd 集群中的多个节点数据的强一致性。用于存储元信息以及共享配置。...
    • ETCD结构、特点及和zookeeper对比

      千次阅读 2021-11-09 22:09:57
      文章目录etcd 是什么etcd结构简介etcd 数据通道etcd通信架构应用场景和ZK对比 etcd 是什么 etcd 这个名字由两部分组成: etc 和 d ,即 UNIX/Linux操作系统的“/etc” 目录和分布式( distributed )首字母的“d ”...
    • etcd-raft学习

      2022-01-14 15:48:54
      raft 算法其实由好几个协议组成,etcd-raft 将其统一定义在了 Message 结构体之中,以下总结了该结构体的成员用途: type Message struct { Type MessageType `protobuf:"varint,1,opt,name=type,enum=raftpb....
    • etcd Raft库解析

      万次阅读 2019-05-17 10:19:12
      序言 今年初开始学习了解Raft协议,论文读下来之后...本篇文章解析etcd的Raft库实现,基于etcd 3.1.10版本。etcd的Raft库,位于其代码目录的Raft中。 我自己也单独将3.1.10的代码拉出了一个专门添加了我阅读代码注...
    • etcd是一个分布式的,一致的 key-value 存储,主要用途是共享配置和服务发现。Etcd 已经在很多分布式系统中得到广泛的使用。 etcd实现服务注册与发现功能,主要使用key-value存储,租约(lease)以及watc.
    • 目录 1、软件版本和环境介绍 2、服务器信息介绍(以下称主机名) 3、etcd安装部署 3.1、cfssl安装 3.2、创建etcd证书 ...3.3、etcd ca配置 ...3.4、etcd ca证书 ...3.9、etcd安装(三台机器都的执行此步...
    • 深入浅出之etcd

      2020-07-30 22:56:58
      向其他节点发起投票请求 Follwer将收到的写操作转发给Leader 因此,etcd集群节点的网络拓扑是一个任意两个节点之间均有长链接互相链接的网络结构,拓扑图大致如下: etcd数据通道 在etcd中根据不同的用途定义了不同...
    • go使用etcd3构建微服务发现和配置管理Aug 16, 2016 · 4 min read[golangetcd分布式微服务]今天Go 1.7正式版发布了!etcd是一个高可用的键值存储系统,主要用于共享配置和服务发现。etcd是由CoreOS开发并维护的,...
    • etcd 介绍(笔记)

      2020-04-08 15:33:29
      参考: https://etcd.io http://jolestar.com/etcd-architecture/ git 地址:https://github.com/etcd-io/etcd ...Etcd is a distributed, consistentkey-valuestore forshared configurationandservice di...
    • ETCD初学指南

      2019-08-01 17:19:26
      疑问2:有什么用处 疑问3:怎么使用 一、etcd是什么 经过网络的搜索,资料的查阅。在学习ETCD之前,这里先介绍一个技术:CoreOS。 CoreOS 是一个基于 Docker 的轻量级容器化 Linux 发行版,...
    • etcd 基础

      2020-11-26 16:26:24
      etcd特性 简单接口 版本: V2和V3互不兼容(kubernetes1.1版本后启用V2),V2基于内存而V3持久化到数据库 http协议:提供简单的调用接口,get、put、post、delete 数据交换格式:html、json、xml html:http协议...
    • ETCD相关

      2019-02-12 11:44:29
      etcd作为一个受到ZooKeeper与doozer启发而催生的项目,除了拥有与之类似的功能外,更专注于以下四点。 简单:基于HTTP+JSON的API让你用curl就可以轻松使用。 安全:可选SSL客户认证机制。 快速:每个实例每秒支持...
    • 这种存储的主要用途是至少向可能需要与其通信的所有相关方提供服务的IP和端口。 该数据通常与其他类型的信息一起扩展。 发现工具倾向于提供某种API,服务可以使用该API进行自身注册,其他人也可以使用该API查找有关...

    空空如也

    空空如也

    1 2 3 4 5 ... 20
    收藏数 1,607
    精华内容 642
    关键字:

    etcd用处