想必很多人都知道 ZooKeeper, 通常用作配置共享和服务发现。和它类似, etcd 算是一个非常优秀的后起之秀了。本文重点不在描述他们之间的不同点。首先, 看看其官网关于 etcd 的描述:
A distributed, reliable key-value store for the most critical data of a distributed system.
在云计算大行其道的今天, etcd 有很多典型的使用场景。常言道, 熟悉一个系统先从部署开始。本文接下来将描述, 如何部署 etcd 集群。
安装官网说明文档, 提供了 3 种集群启动方式, 实际上按照其实现原理分为 2 类:
在部署集群之前, 我们需要考虑集群需要配置多少个节点。这是一个重要的考量, 不得忽略。
etcd 使用 RAFT 协议保证各个节点之间的状态一致。根据 RAFT 算法原理, 节点数目越多, 会降低集群的写性能。这是因为每一次写操作, 需要集群中大多数节点将日志落盘成功后, Leader 节点才能将修改内部状态机, 并返回将结果返回给客户端。
也就是说在等同配置下, 节点数越少, 集群性能越好。显然, 只部署 1 个节点是没什么意义的。通常, 按照需求将集群节点部署为 3, 5, 7, 9 个节点。
这里能选择偶数个节点吗? 最好不要这样。原因有二:
当网络分割后, etcd 集群如何处理的呢?
当网络分割恢复后, 少数派的节点会接受集群 Leader 的日志, 直到和其他节点状态一致。
这里只列举一些重要的参数, 以及其用途。
--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
集群中所有节点。按照官网中的文档, 即可完成集群启动。这里略。
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。
在生产环境中, 不可避免遇到机器硬件故障。当遇到硬件故障发生的时候, 我们需要快速恢复节点。etcd 集群可以做到在不丢失数据的, 并且不改变节点 ID 的情况下, 迁移节点。
具体办法是:
在理清 etcd 的各个模块的实现细节后, 方便线上运维, 理解各种参数组合的意义。本文先从网络层入手, 后续文章会依次介绍各个模块的实现。
本文将着重介绍 etcd 服务的网络层实现细节。在目前的实现中, etcd 通过 HTTP 协议对外提供服务, 同样通过 HTTP 协议实现集群节点间数据交互。
网络层的主要功能是实现了服务器与客户端 (能发出 HTTP 请求的各种程序) 消息交互, 以及集群内部各节点之间的消息交互。
etcd-server 大体上可以分为网络层, Raft 模块, 复制状态机, 存储模块, 架构图如图 1 所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3vbo8wHS-1669260930402)(https://hjs-1251193177.cos.ap-shanghai.myqcloud.com/cloud/etcd/etcd-network-layer-implement-1.png)]
图 1 etcd-server 架构图
etcd 集群的各个节点之间需要通过 HTTP 协议来传递数据, 表现在:
各个节点在任何时候都有可能变成 Leader, Follower, Candidate 等角色, 同时为了减少创建链接开销, etcd 节点在启动之初就创建了和集群其他节点之间的链接。
因此, etcd 集群节点之间的网络拓扑是一个任意 2 个节点之间均有长链接相互连接的网状结构。如图 2 所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ocpwmNWm-1669260930403)(https://hjs-1251193177.cos.ap-shanghai.myqcloud.com/cloud/etcd/etcd-network-layer-implement-2.png)]
图 2 etcd 集群节点网络拓扑图
需要注意的是, 每一个节点都会创建到其他各个节点之间的长链接。每个节点会向其他节点宣告自己监听的端口, 该端口只接受来自其他节点创建链接的请求。
在 etcd 实现中, 根据不同用途, 定义了各种不同的消息类型。各种不同的消息, 最终都通过 google protocol buffer 协议进行封装。这些消息携带的数据大小可能不尽相同。例如 传输 SNAPSHOT 数据的消息数据量就比较大, 甚至超过 1GB, 而 leader 到 follower 节点之间的心跳消息可能只有几十个字节。
因此, 网络层必须能够高效地处理不同数据量的消息。etcd 在实现中, 对这些消息采取了分类处理, 抽象出了 2 种类型消息传输通道: Stream 类型通道和 Pipeline 类型通道。这两种消息传输通道都使用 HTTP 协议传输数据。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cg3SYUy4-1669260930403)(https://hjs-1251193177.cos.ap-shanghai.myqcloud.com/cloud/etcd/etcd-network-layer-implement-3.png)]
图 3 节点之间建立消息传输通道
集群启动之初, 就创建了这两种传输通道, 各自特点:
如果非要做做一个类别的话, Stream 就向点与点之间维护了双向传输带, 消息打包后, 放到传输带上, 传到对方, 对方将回复消息打包放到反向传输带上; 而 Pipeline 就像拥有 N 辆汽车, 大消息打包放到汽车上, 开到对端, 然后在回来, 最多可以同时发送 N 个消息。
Stream 类型通道处理数据量少的消息, 例如心跳, 日志追加消息。点到点之间只维护 1 个 HTTP 长链接, 交替向链接中写入数据, 读取数据。
Stream 类型通道是节点启动后主动与其他每一个节点建立。Stream 类型通道通过 Channel 与 Raft 模块传递消息。每一个 Stream 类型通道关联 2 个 Goroutines, 其中一个用于建立 HTTP 链接, 并从链接上读取数据, decode 成 message, 通过 Channel 传给 Raft 模块中, 另外一个通过 Channel 从 Raft 模块中收取消息, 然后写入通道。
具体点, etcd 使用 golang 的 http 包实现 Stream 类型通道:
Pipeline 类型通道处理数量大消息, 例如 SNAPSHOT 消息。这种类型消息需要和心跳等消息分开处理, 否则会阻塞心跳。
Pipeline 类型通道也可以传输小数据量的消息, 当且仅当 Stream 类型链接不可用时。
Pipeline 类型通道可用并行发出多个消息, 维护一组 Goroutines, 每一个 Goroutines 都可向对端发出 POST 请求 (携带数据), 收到回复后, 链接关闭。
具体地, etcd 使用 golang 的 http 包实现的:
在 etcd 中, Raft 协议被抽象为 Raft 模块。按照 Raft 协议, 节点之间需要交互数据。在 etcd 中, 通过 Raft 模块中抽象的 RaftNode 拥有一个 message box, RaftNode 将各种类型消息放入到 messagebox 中, 有专门 Goroutine 将 box 里的消息写入管道, 而管道的另外一端就链接在网络层的不同类型的传输通道上, 有专门的 Goroutine 在等待 (select)。
而网络层收到的消息, 也通过管道传给 RaftNode。RaftNode 中有专门的 Goroutine 在等待消息。
也就是说, 网络层与 Raft 模块之间通过 Golang Channel 完成数据通信。这个比较容易理解。
在 etcd-server 启动之初, 会监听服务端口, 当服务端口收到请求后, 解析出 message 后, 通过管道传入给 Raft 模块, 当 Raft 模块按照 Raft 协议完成操作后, 回复该请求 (或者请求超时关闭了)。
网络层抽象为 Transport 类, 该类完成网络数据收发。对 Raft 模块提供 Send/SendSnapshot 接口, 提供数据读写的 Channel, 对外监听指定端口。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ImDTZGbF-1669260930403)(https://hjs-1251193177.cos.ap-shanghai.myqcloud.com/cloud/etcd/etcd-network-layer-implement-4.png)]
etcd 是用于共享配置和服务发现的分布式, 一致性的 KV 存储系统。etcd 是 CoreOS 公司发起的一个开源项目, 授权协议为 Apache。
提供配置共享和服务发现的系统比较多, 其中最为大家熟知的是 Zookeeper(后文简称 ZK), 而 etcd 可以算得上是后起之秀了。在项目实现, 一致性协议易理解性, 运维, 安全等多个维度上, etcd 相比 Zookeeper 都占据优势。
本文选取 ZK 作为典型代表与 etcd 进行比较, 而不考虑 Consul 项目作为比较对象, 原因为 Consul 的可靠性和稳定性还需要时间来验证 (项目发起方自身服务并未使用 Consul, 自己都不用)。
Service Mesh Made Easy
Consul is a distributed service mesh to connect, secure, and configure services across any runtime platform and public or private cloud
etcd 提供 HTTP 协议, 在最新版本中支持 Google gRPC 方式访问。具体支持接口情况如下:
etcd 使用 Raft 协议来维护集群内各个节点状态的一致性。简单说, etcd 集群是一个分布式系统, 由多个节点相互通信构成整体对外服务, 每个节点都存储了完整的数据, 并且通过 Raft 协议保证每个节点维护的数据是一致的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OyPAeXfU-1669260930404)(https://hjs-1251193177.cos.ap-shanghai.myqcloud.com/cloud/etcd/etcd-principle.png)]
如图所示, 每个 etcd 节点都维护了一个状态机, 并且, 任意时刻至多存在一个有效的主节点。主节点处理所有来自客户端写操作, 通过 Raft 协议保证写操作对状态机的改动会可靠的同步到其他节点。
etcd 工作原理核心部分在于 Raft 协议。本节接下来将简要介绍 Raft 协议, 具体细节请参考其论文。Raft 协议正如论文所述, 确实方便理解。主要分为三个部分: 选主, 日志复制, 安全性。
Raft 协议是用于维护一组服务节点数据一致性的协议。这一组服务节点构成一个集群, 并且有一个主节点来对外提供服务。当集群初始化, 或者主节点挂掉后, 面临一个选主问题。集群中每个节点, 任意时刻处于 Leader, Follower, Candidate 这三个角色之一。选举特点如下:
为了避免陷入选主失败循环, 每个节点未收到心跳发起选举的时间是一定范围内的随机值, 这样能够避免 2 个节点同时发起选主。
所谓日志复制, 是指主节点将每次操作形成日志条目, 并持久化到本地磁盘, 然后通过网络 IO 发送给其他节点。其他节点根据日志的逻辑时钟 (TERM) 和日志编号 (INDEX) 来判断是否将该日志记录持久化到本地。当主节点收到包括自己在内超过半数节点成功返回, 那么认为该日志是可提交的 (committed), 并将日志输入到状态机, 将结果返回给客户端。
这里需要注意的是, 每次选主都会形成一个唯一的 TERM 编号, 相当于逻辑时钟。每一条日志都有全局唯一的编号。
主节点通过网络 IO 向其他节点追加日志。若某节点收到日志追加的消息, 首先判断该日志的 TERM 是否过期, 以及该日志条目的 INDEX 是否比当前以及提交的日志的 INDEX 跟早。若已过期, 或者比提交的日志更早, 那么就拒绝追加, 并返回该节点当前的已提交的日志的编号。否则, 将日志追加, 并返回成功。
当主节点收到其他节点关于日志追加的回复后, 若发现有拒绝, 则根据该节点返回的已提交日志编号, 发生其编号下一条日志。
主节点像其他节点同步日志, 还作了拥塞控制。具体地说, 主节点发现日志复制的目标节点拒绝了某次日志追加消息, 将进入日志探测阶段, 一条一条发送日志, 直到目标节点接受日志, 然后进入快速复制阶段, 可进行批量日志追加。
按照日志复制的逻辑, 我们可以看到, 集群中慢节点不影响整个集群的性能。另外一个特点是, 数据只从主节点复制到 Follower 节点, 这样大大简化了逻辑流程。
截止此刻, 选主以及日志复制并不能保证节点间数据一致。试想, 当一个某个节点挂掉了, 一段时间后再次重启, 并当选为主节点。而在其挂掉这段时间内, 集群若有超过半数节点存活, 集群会正常工作, 那么会有日志提交。这些提交的日志无法传递给挂掉的节点。当挂掉的节点再次当选主节点, 它将缺失部分已提交的日志。在这样场景下, 按 Raft 协议, 它将自己日志复制给其他节点, 会将集群已经提交的日志给覆盖掉。
这显然是不可接受的。
其他协议解决这个问题的办法是, 新当选的主节点会询问其他节点, 和自己数据对比, 确定出集群已提交数据, 然后将缺失的数据同步过来。这个方案有明显缺陷, 增加了集群恢复服务的时间 (集群在选举阶段不可服务), 并且增加了协议的复杂度。
Raft 解决的办法是, 在选主逻辑中, 对能够成为主的节点加以限制, 确保选出的节点已定包含了集群已经提交的所有日志。如果新选出的主节点已经包含了集群所有提交的日志, 那就不需要从和其他节点比对数据了。简化了流程, 缩短了集群恢复服务的时间。
这里存在一个问题, 加以这样限制之后, 还能否选出主呢? 答案是: 只要仍然有超过半数节点存活, 这样的主一定能够选出。因为已经提交的日志必然被集群中超过半数节点持久化, 显然前一个主节点提交的最后一条日志也被集群中大部分节点持久化。当主节点挂掉后, 集群中仍有大部分节点存活, 那这存活的节点中一定存在一个节点包含了已经提交的日志了。
至此, 关于 Raft 协议的简介就全部结束了。
https://github.com/etcd-io/etcd/releases
https://github.com/coreos/etcd
docker pull quay.io/coreos/etcd
docker run -it --rm -p 2379:2379 -p 2380:2380 --name etcd quay.io/coreos/etcd
docker exec -it etcd etcdctl member list // 查询
etcd API 有两种, 一种是 3, 一种是 2, 默认为 2, 我们主要用 3:
API 2:
$ etcdCTL_API=2 etcdctl set /local/dd d
d
$ etcdCTL_API=2 etcdctl get /local/dd
d
API 3:
$ etcdCTL_API=3 etcdctl put mykey "this is awesome"
OK
$ etcdCTL_API=3 etcdctl get mykey
mykey
this is awesome
API 2:
$ etcdCTL_API=2 ./etcdctl
NAME:
etcdctl - A simple command line client for etcd.
USAGE:
etcdctl.exe [global options] command [command options] [arguments...]
VERSION:
3.3.10
COMMANDS:
backup backup an etcd directory
cluster-health check the health of the etcd cluster
mk make a new key with a given value
mkdir make a new directory
rm remove a key or a directory
rmdir removes the key if it is an empty directory or a key-value pair
get retrieve the value of a key
ls retrieve a directory
set set the value of a key
setdir create a new directory or update an existing directory TTL
update update an existing key with a given value
updatedir update an existing directory
watch watch a key for changes
exec-watch watch a key for changes and exec an executable
member member add, remove and list subcommands
user user add, grant and revoke subcommands
role role add, grant and revoke subcommands
auth overall auth controls
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--debug output cURL commands which can be used to reproduce the request
--no-sync don’t synchronize cluster information before sending request
--output simple, -o simple output response in the given format (simple, ‘extended’ or ‘json’) (default: "simple")
--discovery-srv value, -D value domain name to query for SRV records describing cluster endpoints
--insecure-discovery accept insecure SRV records describing cluster endpoints
--peers value, -C value DEPRECATED - "--endpoints" should be used instead
--endpoint value DEPRECATED - "--endpoints" should be used instead
--endpoints value a comma-delimited list of machine addresses in the cluster (default: "http://127.0.0.1:2379,http://127.0.0.1:4001")
--cert-file value identify HTTPS client using this SSL certificate file
--key-file value identify HTTPS client using this SSL key file
--ca-file value verify certificates of HTTPS-enabled servers using this CA bundle
--username value, -u value provide username[:password] and prompt if password is not supplied.
--timeout value connection timeout per request (default: 2s)
--total-timeout value timeout for the command execution (except watch) (default: 5s)
--help, -h show help
--version, -v print the version
API 3:
$ etcdCTL_API=3 ./etcdctl
NAME:
etcdctl - A simple command line client for etcd3.
USAGE:
etcdctl
VERSION:
3.3.10
API VERSION:
3.3
COMMANDS:
get Gets the key or a range of keys
put Puts the given key into the store
del Removes the specified key or range of keys [key, range_end)
txn Txn processes all the requests in one transaction
compaction Compacts the event history in etcd
alarm disarm Disarms all alarms
alarm list Lists all alarms
defrag Defragments the storage of the etcd members with given endpoints
endpoint health Checks the healthiness of endpoints specified in "--endpoints" flag
endpoint status Prints out the status of endpoints specified in "--endpoints" flag
endpoint hashkv Prints the KV history hash for each endpoint in --endpoints
move-leader Transfers leadership to another etcd cluster member.
watch Watches events stream on keys or prefixes
version Prints the version of etcdctl
lease grant Creates leases
lease revoke Revokes leases
lease timetolive Get lease information
lease list List all active leases
lease keep-alive Keeps leases alive (renew)
member add Adds a member into the cluster
member remove Removes a member from the cluster
member update Updates a member in the cluster
member list Lists all members in the cluster
snapshot save Stores an etcd node backend snapshot to a given file
snapshot restore Restores an etcd member snapshot to an etcd directory
snapshot status Gets backend snapshot status of a given file
make-mirror Makes a mirror at the destination etcd cluster
migrate Migrates keys in a v2 store to a mvcc store
lock Acquires a named lock
elect Observes and participates in leader election
auth enable Enables authentication
auth disable Disables authentication
user add Adds a new user
user delete Deletes a user
user get Gets detailed information of a user
user list Lists all users
user passwd Changes password of user
user grant-role Grants a role to a user
user revoke-role Revokes a role from a user
role add Adds a new role
role delete Deletes a role
role get Gets detailed information of a role
role list Lists all roles
role grant-permission Grants a key to a role
role revoke-permission Revokes a key from a role
check perf Check the performance of the etcd cluster
help Help about any command
OPTIONS:
--cacert="" verify certificates of TLS-enabled secure servers using this CA bundle
--cert="" identify secure client using this TLS certificate file
--command-timeout=5s timeout for short running command (excluding dial timeout)
--debug[=false] enable client-side debug logging
--dial-timeout=2s dial timeout for client connections
-d, --discovery-srv="" domain name to query for SRV records describing cluster endpoints
--endpoints=[127.0.0.1:2379] gRPC endpoints
-h, --help[=false] help for etcdctl
--hex[=false] print byte strings as hex encoded strings
--insecure-discovery[=true] accept insecure SRV records describing cluster endpoints
--insecure-skip-tls-verify[=false] skip server certificate verification
--insecure-transport[=true] disable transport security for client connections
--keepalive-time=2s keepalive time for client connections
--keepalive-timeout=6s keepalive timeout for client connections
--key="" identify secure client using this TLS key file
--user="" username[:password] for authentication (prompt if password is not supplied)
-w, --write-out="simple" set the output format (fields, json, protobuf, simple, table)