简介
随着go与kubernetes的大热,etcd作为一个基于go编写的分布式键值存储,逐渐为开发者所熟知,尤其是其还作为kubernetes的数据存储仓库,更是引起广泛专注。
本文我们就来聊一聊etcd到底是什么及其工作机制。
首先,coreos官方是这么定义etcd的:
A highly-available key value store for shared configuration and service discovery(etcd是一个高可用的用于共享配置和服务发现的KV存储)
事实上,etcd是一个受到zookeeper和doozer启发而催生的项目。除了与之拥有类似的功能之外,现专注于如下几点:
- 简单: 基于http+json的api
- 安全: 可选的ssl认证机制
- 快速: 每个实例每秒支持1000次写操作
- 可信: 使用raft算法实现分布式
使用场景
下面是一些etcd较常用的使用场景:
- 服务发现:在微服务的应用场景中,各服务主动向etcd注册自己,并通过etcd获取其依赖的服务的注册信息。
- 配置管理:事实上配置管理使用的就是消息发布与订阅。即构建一个配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅他们关心的主题,一旦主题有消息发布,就会实时通知订阅者。通过这种方式可以做到分布式系统配置的集中式管理与动态更新。
- 应用调度: 事实上应用调度的功能也是依托于服务的注册与发现。一个服务多个副本都注册至etcd,利用etcd维护一个负载均衡节点表。etcd可以监控一个集群中多个节点的状态,当有一个请求发过来后,可以轮询式的把请求转发给存活着的多个状态。
- 分布式通知与协调:与消息发布和订阅有些相似。都用到了etcd中的Watcher机制,通过注册与异步通知机制,实现分布式环境下不同系统之间的通知与协调,从而对数据变更做到实时处理。与消息发布和订阅不同的是,发布和订阅通常用于获取配置以更新服务。而分布式通知与协调,通常是多个不同的服务监听同一个目录,但其中一个服务修改了该目录,则触发其他服务的相应操作,一个典型的应用场景即通过etcd实现应用的健康检查
- 分布式锁:因为etcd使用raft算法保证了数据的强一致性,某次操作存储至etcd的值必须全局唯一,所以比较容易实现分布式锁
- 分布式队列:与分布式锁的场景类似,可创建一个先进先出队列,保证顺序
- 选主:通过etcd为一些分布式应用选举leader节点
组件说明
架构说明
etcd主要分为四个部分。
HTTP Server: 用于处理用户发送的API请求以及其它etcd节点的同步与心跳信息请求。在最新的版本中也已经支持了grpc
Store:Store 是 Etcd 数据存储的核心部分,它保存了所有的键值对。Store 可以看作是一个内存中的键值数据库,所有的读写操作都会在这个内存结构中进行。Store 是为了提供快速的数据访问而设计的,因为大多数读写操作可以直接从内存中完成。
Raft:Raft强一致性算法的具体实现,是etcd的核心。
WAL:Write Ahead Log(预写式日志),是一种用于持久化数据的技术,它是在内存中的 Store 发生更改之前先将更改记录到磁盘的日志。WAL 的主要目的是为了提高数据的持久性和容错能力。即使在节点突然宕机的情况下,WAL 文件也可以用来恢复内存中的 Store。
通常,一个用户的请求发送过来,会经由HTTP Server转发给Store进行数据的读写操作,如果涉及到节点的修改,则交给Raft模块进行状态的变更、日志的记录,然后再同步给别的etcd节点以确认数据提交,最后进行数据的提交,再次同步。
工作原理
选主
Raft协议是用于维护一组服务节点数据一致性的协议。这一组服务节点构成一个集群,并且有一个主节点来对外提供服务。当集群初始化,或者主节点挂掉后,就需要重新选主。集群中每个节点,任意时刻处于Leader, Follower, Candidate这三个角色之一。选举特点如下:
- 当集群初始化时候,每个节点都是Follower角色;
- 集群中存在至多1个有效的主节点,通过心跳与其他节点同步数据;
- 当Follower在一定时间内没有收到来自主节点的心跳,会将自己角色改变为Candidate,并发起一次选主投票;当收到包括自己在内超过半数节点赞成后,选举成功,自己成为主;当收到票数不足半数选举失败,或者选举超时。若本轮未选出主节点,将进行下一轮选举(出现这种情况,是由于多个节点同时选举,所有节点均未获得过半选票)。
- Candidate节点收到来自主节点的信息后,会立即终止选举过程,进入Follower角色。
- 为了避免陷入选主失败循环,每个节点未收到心跳发起选举的时间是一定范围内的随机值,这样能够避免2个节点同时发起选主。
日志复制
所谓日志复制,是指主节点将每次操作形成日志条目,并持久化到本地磁盘,然后通过网络IO发送给其他节点。其他节点根据日志的逻辑时钟(TERM)和日志编号(INDEX)来判断是否将该日志记录持久化到本地。当主节点收到包括自己在内超过半数节点成功返回,那么认为该日志是可提交的(committed),并将日志输入到状态机,将结果返回给客户端。
这里需要注意的是,每次选主都会形成一个唯一的TERM编号,相当于逻辑时钟。每一条日志都有全局唯一的编号。
主节点通过网络IO向其他节点追加日志。若某节点收到日志追加的消息,首先判断该日志的TERM是否过期,以及该日志条目的INDEX是否比当前以及提交的日志的INDEX跟早。若已过期,或者比提交的日志更早,那么就拒绝追加,并返回该节点当前的已提交的日志的编号。否则,将日志追加,并返回成功。
当主节点收到其他节点关于日志追加的回复后,若发现有拒绝,则根据该节点返回的已提交日志编号,发生其编号下一条日志。
主节点像其他节点同步日志,还作了拥塞控制。具体地说,主节点发现日志复制的目标节点拒绝了某次日志追加消息,将进入日志探测阶段,一条一条发送日志,直到目标节点接受日志,然后进入快速复制阶段,可进行批量日志追加。
按照日志复制的逻辑,我们可以看到,集群中慢节点不影响整个集群的性能。另外一个特点是,数据只从主节点复制到Follower节点,这样大大简化了逻辑流程。
安全性
选主以及日志复制都是为了保证节点间的数据一致性。但是仅仅依靠选主和日志复制并不能完全保证节点间数据一致。比如,当某个节点挂掉了,一段时间后再次重启,并当选为主节点。而在其挂掉这段时间内,集群若有超过半数节点存活,集群会正常工作,那么会有日志提交。这些提交的日志无法传递给挂掉的节点。当挂掉的节点再次当选主节点,它将缺失部分已提交的日志。在这样场景下,按Raft协议,它将自己日志复制给其他节点,会将集群已经提交的日志给覆盖掉。
这显然是不可接受的。
其他协议解决这个问题的办法是,新当选的主节点会询问其他节点,和自己数据对比,确定出集群已提交数据,然后将缺失的数据同步过来。这种方案有明显缺陷,增加了集群恢复服务的时间(集群在选举阶段不可服务),并且增加了协议的复杂度。
Raft的解决办法是,在选主逻辑中,对能够成为主的节点加以限制,确保选出的节点一定包含了集群已经提交的所有日志。如果新选出的主节点已经包含了集群所有提交的日志,那就不需要从和其他节点比对数据了。简化了流程,缩短了集群恢复服务的时间。
这里存在一个问题,加以这样限制之后,还能否选出主呢?答案是:只要仍然有超过半数节点存活,这样的主一定能够选出。因为已经提交的日志必然被集群中超过半数节点持久化,显然前一个主节点提交的最后一条日志也被集群中大部分节点持久化。当主节点挂掉后,集群中仍有大部分节点存活,那这存活的节点中一定存在一个节点包含了已经提交的日志了。
相关术语
- 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开始竞选。
- Term:某个节点成为Leader到下一次竞选时间,称为一个Term。
- Index:数据项编号。Raft中通过Term和Index来定位数据。
部署
docker compose 单机部署
首先创建两个文件夹,一个是数据目录,一个是密钥目录
mkdir -p /docker/etcd/data
mkdir /docker/etcd/ca-certificates
进入密钥目录
cd /docker/etcd/ca-certificates
生成 CA 的私钥和证书
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -subj "/CN=etcd-ca" -days 10000 -out ca.crt
生成 etcd 服务器的私钥和证书签名请求
openssl genrsa -out etcd.key 2048
openssl req -new -key etcd.key -subj "/CN=etcd-server" -out etcd.csr
使用 CA 证书和私钥签署 etcd 服务器的证书
openssl x509 -req -in etcd.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out etcd.crt -days 10000
生成 etcd 客户端的私钥和证书签名请求
openssl genrsa -out etcdclient.key 2048
openssl req -new -key etcdclient.key -subj "/CN=etcd-client" -out etcdclient.csr
使用 CA 证书和私钥签署 etcd 客户端的证书
openssl x509 -req -in etcdclient.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out etcdclient.crt -days 10000
创建 docker-compose.yml 在 /docker/etcd/ 目录下即可
vim docker-compose.yml
services:
etcd:
image: bitnami/etcd
container_name: etcd
restart: always
ports:
- 4001:4001
- 2380:2380
- 2379:2379
volumes:
- /docker/etcd/ca-certificates/:/etc/ssl/certs
- /docker/etcd/data:/bitnami/etcd/data
environment:
- TZ=Asia/Shanghai
- ETCD_NAME=etcd0
- ETCD_DATA_DIR=/bitnami/etcd/data
- ETCD_ROOT_PASSWORD=123456
- ETCD_CLIENT_CERT_AUTH=true
- ETCD_PEER_CLIENT_CERT_AUTH=true
- ETCD_ADVERTISE_CLIENT_URLS=http://192.168.142.157:2379,http://192.168.142.157:4001
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379,http://0.0.0.0:4001
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://192.168.142.157:2380
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_INITIAL_CLUSTER_TOKEN=etcd-cluster-1
- ETCD_INITIAL_CLUSTER=etcd0=http://192.168.142.157:2380
- ETCD_INITIAL_CLUSTER_STATE=new
- ETCD_CERT_FILE=/etc/ssl/certs/etcd.crt
- ETCD_KEY_FILE=/etc/ssl/certs/etcd.key
- ETCD_TRUSTED_CA_FILE=/etc/ssl/certs/ca.crt
- ETCD_PEER_CERT_FILE=/etc/ssl/certs/etcd.crt
- ETCD_PEER_KEY_FILE=/etc/ssl/certs/etcd.key
- ETCD_PEER_TRUSTED_CA_FILE=/etc/ssl/certs/ca.crt
- ETCD_CLIENT_CERT_AUTH=true
元神启动
docker compose up -d
查看是否启动
root@master:/docker/etcd# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
55ff0118d4be bitnami/etcd "/opt/bitnami/script…" 22 minutes ago Up 22 minutes 0.0.0.0:2379-2380->2379-2380/tcp, :::2379-2380->2379-2380/tcp, 0.0.0.0:4001->4001/tcp, :::4001->4001/tcp etcd
运行
docker exec -it etcd etcdctl --endpoints=http://192.168.142.157:2379 --user=root:123456 get /
在这个过程中我遇到了这个问题
chmod: changing permissions of '/bitnami/etcd/data': Operation not permitted
我是这么解决的:设置权限(实在不行干脆把 /docker/etcd 设置成 777)
chmod 777 /docker/etcd/data -R
现在权限问题解决了之后我又遇到了一个新的问题
root@master:/docker/etcd# docker exec -it etcd etcdctl --endpoints=http://192.168.142.157:2379 --user=root:123456 get /
{"level":"warn","ts":"2024-10-04T23:15:09.800270+0800","logger":"etcd-client","caller":"v3@v3.5.16/retry_interceptor.go:63","msg":"retrying of unary invoker failed","target":"etcd-endpoints://0xc0003501e0/192.168.142.157:2379","attempt":0,"error":"rpc error: code = FailedPrecondition desc = etcdserver: authentication is not enabled"}
这个错误是etcd的认证没有开启
接下来让我们来开启认证
先查看是否开启了认证
docker exec -it etcd etcdctl auth status
结果
Authentication Status: false
AuthRevision: 1
查看用户列表和角色列表
docker exec -it etcd etcdctl role list
docker exec -it etcd etcdctl user list
结果:什么都没有
添加root角色
docker exec -it etcd etcdctl role add root
添加root用户
docker exec -it etcd etcdctl user add root:123456
再次查看用户列表和角色列表
给root用户赋予root角色
docker exec -it etcd etcdctl user grant-role root root
结果:两个都是 root
好了,开启认证看看
docker exec -it etcd etcdctl auth enable
参考:https://blog.csdn.net/HYZX_9987/article/details/135237119
感谢大佬的博客 ^–^
这个时候我就再也没有遇到报错了,让我们来插入数据试试
docker exec -it etcd etcdctl --endpoints=http://192.168.142.157:2379 --user=root:123456 put /mykey "myvalue"
查看插入的数据
etcdctl --endpoints=http://192.168.142.157:2379 --user=root:123456 get /mykey
结果
/mykey
myvalue
至此,单机部署完成
etcd 的简单使用
增
etcdctl put /key "value"
设置带有过期时间的键
etcdctl put /key --ttl 60 "value"
删
etcdctl del /key
查
etcdctl get /key
列出
etcdctl ls /
监控
监控键 key 的变化,如果有更新,会输出变化后的值。
etcdctl watch /key
执行事务
执行一个包含多个操作的事务
etcdctl txn \
put /txn_key "value1" \
get /txn_key
检查领导选举
使用 etcd 进行领导者选举。
etcdctl elect --ttl 60 --period 5 /myelection mycandidate
获取成员列表
etcdctl member list
获取集群状态
etcdctl endpoint health