参考
本文参考https://zhuanlan.zhihu.com/p/600893553
https://www.topgoer.com/%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C/go%E6%93%8D%E4%BD%9Cetcd/etcd%E4%BB%8B%E7%BB%8D.html
前沿etcd 与 raft
etcd是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现。
类似项目有zookeeper和consul。
etcd具有以下特点:
- 完全复制:集群中的每个节点都可以使用完整的存档
- 高可用性:Etcd可用于避免硬件的单点故障或网络问题
- 一致性:每次读取都会返回跨多主机的最新写入
- 简单:包括一个定义良好、面向用户的API(gRPC)
- 安全:实现了带有可选的客户端证书身份验证的自动化TLS
- 快速:每秒10000次写入的基准速度
- 可靠:使用Raft算法实现了强一致、高可用的服务存储目录
etcd 基于 golang 编写,github 开源地址:https://github.com/etcd-io/etcd . 本文走读的源码版本为 tag: v3.1.10.
etcd 中关于 raft 算法的实现具有很高的还原性,项目中与 raft 有关的代码主要位于:
模块 | 目录 |
---|---|
raft 算法层 | ./raft/* |
raft 应用层示例 | ./contrib/raftexample/* |
raft 数据结构定义 | ./raft/raftpb |
etcd应用场景
服务发现
服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。
配置中心
将一些配置信息放到 etcd 上进行集中管理。
这类场景的使用方式通常是这样:应用在启动的时候主动从 etcd 获取一次配置信息,同时,在 etcd 节点上注册一个 Watcher 并等待,以后每次配置有更新的时候,etcd 都会实时通知订阅者,以此达到获取最新配置信息的目的。
分布式锁
因为 etcd 使用 Raft 算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。
- 保持独占即所有获取锁的用户最终只有一个可以得到。etcd 为此提供了一套实现分布式锁原子操作 CAS(CompareAndSwap)的 API。通过设置prevExist值,可以保证在多个节点同时去创建某个目录时,只有一个成功。而创建成功的用户就可以认为是获得了锁。
- 控制时序,即所有想要获得锁的用户都会被安排执行,但是获得锁的顺序也是全局唯一的,同时决定了执行顺序。etcd 为此也提供了一套 API(自动创建有序键),对一个目录建值时指定为POST动作,这样 etcd 会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)。同时还可以使用 API 按顺序列出所有当前目录下的键值。此时这些键的值就是客户端的时序,而这些键中存储的值可以是代表客户端的编号。
为什么用 etcd 而不用ZooKeeper?
etcd 实现的这些功能,ZooKeeper都能实现。那么为什么要用 etcd 而非直接使用ZooKeeper呢?
为什么不选择ZooKeeper?
- 部署维护复杂,其使用的Paxos强一致性算法复杂难懂。官方只提供了Java和C两种语言的接口。
- 使用Java编写引入大量的依赖。运维人员维护起来比较麻烦。
- 最近几年发展缓慢,不如etcd和consul等后起之秀。
为什么选择etcd?
- 简单。使用 Go 语言编写部署简单;支持HTTP/JSON API,使用简单;使用 Raft 算法保证强一致性让用户易于理解。
- etcd 默认数据一更新就进行持久化。
- etcd 支持 SSL 客户端安全认证。
最后,etcd 作为一个年轻的项目,正在高速迭代和开发中,这既是一个优点,也是一个缺点。优点是它的未来具有无限的可能性,缺点是无法得到大项目长时间使用的检验。然而,目前 CoreOS、Kubernetes和CloudFoundry等知名项目均在生产环境中使用了etcd,所以总的来说,etcd值得你去尝试。
术语表
中文名 | 英文名 | 说明 |
---|---|---|
算法层 | algorithm module | 内聚了 raft 共识机制的核心模块.代码层面以 sdk 的形式被应用层引入,启动时以独立 goroutine 的形式存在,与应用层通过 channel 通信. |
应用层 | application module | 聚合了 etcd 存储、通信能力的模块.启动时是 raft 节点的主 goroutine,既要负责与客户端通信,又要负责和算法层交互,承上启下. |
算法层节点 | Node/node | 是 raft 节点在算法层中的抽象,也是应用层与算法层交互的唯一入口. |
应用层节点 | raftNode | 是 raft 节点在应用层中的抽象,内部持有算法层节点 Node 的引用,同时提供处理客户端请求以及与集群内其他节点通信的能力. |
通信模块 | transport | etcd 中的网络通信模块,为 raft 节点间通信提供服务. |
提议 | proposal | 两阶段提交中的第一阶段:提议. |
提交 | commit | 两阶段提交中的第二阶段:提交. |
应用 | apply | 将已提交的日志应用到数据状态机,使得写请求真正生效. |
数据状态机 | data state machine | raft 节点用于存储数据的介质. 为避免与 raft 节点状态机产生歧义,统一命名为数据状态机. |
节点状态机 | node state machine | etcd 实现中,raft 节点本质是个大的状态机. 任何操作如选举、提交数据等,最后都会封装成一则消息输入节点状态机中,驱动节点状态发生变化. |
宏观架构梳理
算法层
算法层是 raft 算法的核心实现模块,负责根据共识机制进行请求内容的正确性校验,预写日志状态的维护和管理,可提交日志的进度推进.
简单来看,算法层本质上类似于一台“拼图机器”. 我们把输入的日志比喻成一块块“拼图碎片”,提交拼图碎片的应用层则像个捣蛋的小孩,他将拼图顺序打乱并在其中掺入一些错误的碎片,然后依次将“碎片”提交给拼图机器(算法层).
拼图机器(算法层)则依照对图案轮廓的把握和碎片纹理的观察(raft 共识机制),依次纠正碎片的顺序和内容之后进行归还.
在这个比喻中,算法层可以被视为一台高效且精准的“拼图机器”,专门负责处理和组装日志数据,这些数据类似于一系列的拼图碎片。这台机器的核心是基于 raft 共识机制,一个复杂而强大的算法,设计用来确保分布式系统中数据的一致性和可靠性。当应用层(象征着一个调皮的小孩)生成和提交日志时,它可能会不按顺序提交,甚至夹杂一些错误或无效的数据碎片。这种无序和错误的数据代表了分布式系统中的节点可能出现的不一致或故障情况。
算法层的工作就是要从这些混乱和可能错误的输入中恢复出正确和有序的数据。通过对每个“碎片”(即日志条目)的内容进行仔细的审查和比对,算法层根据 raft 协议的规则来决定哪些碎片是有效的,哪些应该被丢弃或修正。它通过投票和日志复制机制确保所有节点上的数据保持一致。每个节点的算法层都会参与这个过程,从而确保整个系统在面对网络分区、节点故障等问题时仍能保持数据的一致性和完整性。
此外,算法层还需要管理数据碎片的正确顺序。在分布式系统中,维持操作的顺序是至关重要的,因为这直接影响到系统状态的正确性。算法层通过维护一个严格的日志序列,确保所有操作都按照一定的顺序执行。这就像是在解决拼图时,需要找到正确的碎片,并按照特定的顺序将它们放到恰当的位置。
总而言之,算法层(拼图机器)在这个系统中扮演了核心角色,负责处理来自应用层(捣蛋小孩)的混乱和不完整的输入,通过 raft 协议的精妙设计,它能够高效地校正和组织这些数据,最终形成一个完整且一致的数据结构,保证整个系统的稳定运行和数据的准确性。
应用层
在 etcd 的实现中,算法层只聚焦于 raft 共识机制部分的内容,其中涉及网络通信、预写日志持久化、数据状态机管理的工作均在应用层进行实现;
应用层是 raft 节点中承上启下的主干模块,既扮演了与外部客户端通信的服务端角色,也是下层算法库的使用者,还要负责与各模块交互,串联 raft 节点运行的整体流程.
由于本文探讨的重点集中 raft 共识机制部分,因此应用层不是我们所研究的重点,更多的笔墨会集中在算法层的内容中.
宏观架构
etcd 在实现 raft 时,将内容依据职责边界拆分为应用层和算法层两个模块,在引用关系上,算法层是一个被应用层引用的静态库,但在节点启动时,最终应用层和算法层的内容会内聚于两个独立的 goroutine 当中,依据几个 channel 进行模块间的异步通信,两个模块的核心职责点展示如上图。