云原生学习路线导航页(持续更新中)
- kubernetes学习系列快捷链接
- Kubernetes架构原则和对象设计(一)
- Kubernetes架构原则和对象设计(二)
- Kubernetes架构原则和对象设计(三)
- kubectl 和 kubeconfig 基本原理
- kubeadm 升级 k8s集群 1.17到1.20
- Kubernetes常见问题解答
- 查看云机器的一些常用配置
本文主要对Kubernetes控制平面组件:etcd进行介绍,深入理解etcd的设计原理,深入分析 Raft 协议如何实现一致性,如何完成leader选举和日志复制等
1.Etcd介绍
1.1.Etcd简介
-
任何一个系统都要有地方存数据,而在Kubernetes中唯一存储数据的就是etcd
-
etcd是CoreOS公司研发的,后来CoreOS被RedHat收购了
-
etcd的字面意思为 etc distribution,在Linux中etc目录一般是用于存储配置文件的,所以etcd可以理解为分布式配置管理中心。又因为etcd中存储的是key-value形式,所以又可以理解为 分布式键值存储数据库
-
本节主要向大家分享etcd的设计理念,遵循的协议是什么,怎么保证数据一致,如何支持我们之前讲的watch机制,进而理解为什么Kubernetes会选它作为唯一的数据存储
-
分布式场景中需要考虑的一些问题
- 保证服务高可用性
- 在没有分布式部署时,单节点部署存在很多问题:数据备份压力(备份周期不好选择、备份数据有时效性、故障时需要人工手动恢复等)、单点故障导致的数据丢失问题。为了保证数据库的高可用,一般要多实例冗余部署。
- 但是 有状态的服务 和 无状态的服务 高可用复杂性完全不可同日而语,无状态可以非常轻松的完成副本的扩缩,死一个无关紧要,但有状态应用,比如etcd数据库多个member组成的一个集群中数据怎么保持一致性 是非常复杂的事情。
- 实现服务注册和发现
- 在微服务架构中,a服务要访问b服务时,需要获取到b服务的地址,此时就需要服务注册和发现,很多时候注册中心使用键值对就可以满足这个需求,而且效率也比较高
- 实现监听变更
- 如果使用key-value存储一些数据,希望在某个key-value发生变化时得到通知,就需要有一个监听-通知机制
- 这是一个典型的消息机制,可以使用消息队列实现
- 保证服务高可用性
-
Etcd介绍
- Etcd是一个 基于raft协议的 分布式键值存储数据库,对上述三个问题的处理比较友好。Etcd有分布式部署保证自身高可用和数据高可用,可以用key-value实现服务注册和发现,自带watch机制实现消息机制
- Etcd中通过API访问数据,且具备可选的SSL客户端认证来保证数据安全性,有一套完备的认证授权体系,包括角色管理、用户管理等。
- Etcd还具备key的过期和续约机制,使用
- 不过Kubernetes没有使用Etcd自带的认证鉴权体系,所以etcd的速度还是不错的
- Etcd 基于Raft协议保证数据一致性,单实例每秒支持1000次的写操作,2000次的读操作,随着Etcd的升级效果可能会有所提升。
1.2.Etcd的主要功能
- Etcd的Key-value存储
- 键值存储是Etcd的核心功能,结构简单,不需要像关系型数据库那样有结构化表格的row和column,key-value的存储保证了查询速度的迅速
- 另外Etcd也支持数据持久化,存储包括内存数据和磁盘数据
- Etcd的监听机制
- Etcd的监听机制,就是利用watch机制把自己打造成一个消息队列,进而避免引入第三方消息组件
- Kubernetes正是利用了Etcd的watch机制将自己变成一个异步组件
- Etcd使用Lease实现过期续约机制
- 比如说你给一个key设个1分钟,在一分钟有效期之内是可以get到这个数据的,超出1分钟这个数据就失效了,就get不到了
- 当然,可以在过期前发起renew,对key进行续约
- Etcd还可以用于实现 Compare And Swap 乐观锁机制
- Compare And Swap 即更改之前先Get一把,看是否被修改过,没有修改过自己再发起修改。
- CAS 可以用于服务发现和服务注册
1.3.Etcd的键值对存储
- 在etcd中,key以B树的形式存储在内存中, value以B+树的形式存储在硬盘中
1.4.Etcd应用于服务注册与发现
有些人可能对服务注册和发现很熟悉,尤其是做过微服务开发的人,但有些人不一定了解。这里我们举一个最简单的例子来说明。
- 在 Spring Cloud 中,它是一个微服务框架。我们先抛开 ETCD,Spring Cloud 本身有一个服务注册中心,叫做 Eureka。
- 服务提供者的注册
- 如果你要写一个应用,作为一个服务提供方的角色,比如提供一个 REST API 给别人用,那么在部署时,通常会进行高可用部署。例如,你部署了 3 个实例。
- 那么,别人如何使用你的服务呢?你部署的 3 个实例,每个实例的 IP 地址和端口你是知道的。你需要通过服务注册机制,将这些 IP 地址和端口注册到服务注册中心。这是第一步:将自己注册上去。
- 保活与健康检查
- 第二个重点是保活。服务提供者需要不断进行健康检查,确保自己的实例是健康的还是异常的。健康检查非常重要,服务提供者会不断刷新注册中心的状态。
- 如果注册中心在一段时间内没有收到某个实例的心跳,就会认为该实例已经失效。这个机制在很多服务注册中心中都是通用的,比如 Kubernetes 中的 Pod 探活机制(通过 Probe 探测 Pod 是否 Ready)。
- 消费者的服务发现
- 通过这种机制,服务提供者始终将健康的实例注册到服务注册中心。
- 当消费者需要访问某个服务时,它会向服务注册中心查询当前可用的实例。
- 例如,如果当前有 3 个实例都健康,服务注册中心会将这 3 个实例的 IP 地址返回给消费者,消费者即可访问这些实例。
- 续约机制
- 服务提供者需要不断续约,向注册中心表明自己仍然存活。ETCD 的续约和过期机制天然满足这种需求。
- 例如,注册一个服务时,服务中包含 3 个实例,每个实例有自己的 IP 和健康状况。服务提供者会定期更新实例的状态,并以固定时间间隔续约。如果某个实例在指定时间内未完成续约,注册中心会认为该实例已失效。
- 当消费者查询时,失效的实例不会被返回。这就是 ETCD 作为服务注册中心的一种用法,其机制与 Consul 等开源产品类似。
1.5.Etcd应用于消息发布和订阅
1.5.1.消息发布与订阅的基本概念
- 消息发布与订阅是消息队列(Message Queue)的核心功能之一。有些同学可能不太理解消息中心是什么,其实消息中心就是这样的:它同样有多种角色,比如生产者和消费者。
1.5.2.生产者与消费者的角色
-
生产者:负责产生消息。
-
消费者:负责订阅消息。
-
那么,生产者这边一旦有消息了,它就会通过消息中心告知消费者说:“哎,有数据了,你可以来处理了。”消费者就可以基于这个事件,去做一些配置管理,或者是做任何你想做的行为。这就是消息发布与订阅的功能。
1.5.3.ETCD 中的消息发布与订阅
那么接下来,我们讲一下 ETCD 中的消息发布与订阅功能。其实,ETCD 的三大核心功能我们已经讲过了,分别是键值存储、服务注册与发现,以及消息发布与订阅。
1.5.3.1.键值存储与 TTL
- 在 ETCD 中,键值存储可以与 TTL(Time to Live)绑定。TTL 就是它的保活周期时长。
- 那么,你一旦在写数据的时候,跟这个 TTL 绑定,它就会去确定说:“哎,多久没有续约了?”如果超过你设置的时间,比如你设置了 1 分钟,那如果 1 分钟它还没续约,你这个 key 就会消失。
1.5.3.2.消息发布与订阅的实现
- 消息发布与订阅其实就是通过 Watch 机制实现的。在 ETCD 中,你可以通过 Watch 监听某个键值对的变化。当键值对发生变化时,ETCD 会通知订阅的客户端。
- 比如,生产者可以将消息写入某个键值对,消费者通过 Watch 监听该键值对的变化。一旦有新消息写入,消费者会收到通知并处理数据。这其实就是我们上次所说的 Watch 机制。
1.5.3.3.是否可以用 ETCD 代替消息队列
- 那么,是否可以用 ETCD 代替消息队列呢?
- 其实,ETCD 的 Watch 机制可以用于实现简单的消息发布与订阅功能,但它的设计初衷并不是作为消息队列使用的。对于高吞吐量或复杂消息处理场景,建议使用专门的消息队列系统,比如 Kafka、RabbitMQ 等。
- ETCD 更适合用于配置管理、服务注册与发现等场景,消息发布与订阅功能可以作为它的附加能力使用。
1.6.Etcd的安装
1.6.1.etcd的安装
- 访问 etcdio 网站 下载 release 版本
- 解压二进制包,将其放在本地目录下
- 也可以参考官网的安装方法:https://etcd.io/docs/v3.5/install/
1.6.2.启动 etcd
- 启动命令示例:
etcd --listen-client-urls=http://localhost:2379 --advertise-client-urls=http://localhost:2379
- 参数说明:
- –listen-client-urls:客户端访问的 URL。
- –advertise-client-urls:集群成员间通信的 URL。
1.6.3.docker安装方式
- 我这里给出使用docker的安装方法
- 这里使用sh脚本部署,
vim docker-etcd.sh
,然后把下面的内容写入文件 - 注意,如果你安装了kubernetes(已经携带了etcd),这种方式跑不起来,端口会有冲突,可以把下面内容的所有 2379–>2479,2380–>2480
- 我下面的就是更改为 2479、2480 的。如果执行后容器是关闭了,可以docker start一下,把容器跑起来
ETCD_VER=v3.5.18 rm -rf /tmp/etcd-data.tmp && mkdir -p /tmp/etcd-data.tmp && \ docker rmi gcr.io/etcd-development/etcd:${ETCD_VER} || true && \ docker run \ -p 2479:2479 \ -p 2480:2480 \ --mount type=bind,source=/tmp/etcd-data.tmp,destination=/etcd-data \ --name etcd-gcr-${ETCD_VER} \ gcr.io/etcd-development/etcd:${ETCD_VER} \ /usr/local/bin/etcd \ --name s1 \ --data-dir /etcd-data \ --listen-client-urls http://0.0.0.0:2479 \ --advertise-client-urls http://0.0.0.0:2479 \ --listen-peer-urls http://0.0.0.0:2480 \ --initial-advertise-peer-urls http://0.0.0.0:2480 \ --initial-cluster s1=http://0.0.0.0:2480 \ --initial-cluster-token tkn \ --initial-cluster-state new \ --log-level info \ --logger zap \ --log-outputs stderr docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcd --version docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcdctl version docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcdutl version docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcdctl endpoint health docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcdctl put foo bar docker exec etcd-gcr-${ETCD_VER} /usr/local/bin/etcdctl get foo
- 这里使用sh脚本部署,
- 不建议在生产环境中使用二进制部署,因为无法确保资源保证,使用容器管理可以更好地进行资源限制和质量隔离
1.7. etcd 的基本使用
- etcdctl是用于与etcd键值存储系统进行交互的命令行工具,会把命令转成rest http请求转发到etcd
- 在使用etcdctl时,指定endpoint是必需的,因为它告诉etcdctl要连接的etcd实例的位置
- Etcd是一个分布式键值存储系统,可以在多个节点上运行。每个节点都有一个唯一的endpoint,用于标识它在网络上的位置。通过指定endpoint,etcdctl知道要连接到哪个etcd实例以执行命令
- 因为我上面安装时将端口从2379改成了2479,所以下面我的endpoints都指定为
--endpoints=127.0.0.1:2479
- 如果不指定endpoint,默认会连接2379,但是我们很多人的机器上都有kubernetes,默认2379给kubernetes的etcd用了,我们连不上的,所以做实验的时候还是都加上
--endpoints
吧
1.7.1.使用 etcdctl 命令行工具
- 设置环境变量:
# etcdctl 存在两个主要的 API 版本:API v2 和 API v3。API v3 是较新的版本,引入了一些新的功能和改进 export ETCDCTL_API=3
- 常用命令:
# 列出etcd集群的所有成员 etcdctl --endpoints=127.0.0.1:2479 member list 42ca3f1db112078d, started, vm-226-235-tencentos, https://xxx.xxx.xxx.xxx:2380, https://xxx.xxx.xxx.xxx:2379, false # 列出etcd集群的所有成员,以表格方式输出 etcdctl --endpoints=127.0.0.1:2479 member list --write-out=table [root@VM-226-235-tencentos ~/zgy/yamls]# docker exec -it bd0feb758a0b etcdctl --endpoints=127.0.0.1:2479 member list --write-out=table +------------------+---------+------+---------------------+---------------------+------------+ | ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER | +------------------+---------+------+---------------------+---------------------+------------+ | 9b0b0f15059fc529 | started | s1 | http://0.0.0.0:2480 | http://0.0.0.0:2479 | false | +------------------+---------+------+---------------------+---------------------+------------+ # 写入键值对: etcdctl --endpoints=127.0.0.1:2479 put key value # 读取键值对: etcdctl --endpoints=127.0.0.1:2479 get key # 监听键值变化: etcdctl --endpoints=127.0.0.1:2479 watch key # 还可以监听某个前缀 etcdctl --endpoints=127.0.0.1:2479 watch --prefix /api/v1/namespaces/default
1.7.2.键值对的规划
-
键值对可以按照目录结构进行规划
# 键值对可以按照目录结构进行规划 sh-5.0# etcdctl --endpoints=127.0.0.1:2479 put /a/b value1 sh-5.0# etcdctl --endpoints=127.0.0.1:2479 put /a/c value2 # 通过前缀获取键值对: sh-5.0# etcdctl --endpoints=127.0.0.1:2479 get --prefix /a /a/b value1 /a/c value2
-
Kubernetes正是利用了 目录结构的键值对规划,保证了资源存储的唯一性和正常存取,比如我们执行一下
kubectl get pods -n default -v9
,可以看下default下pods的key设计:- 设计为:
/api/v1/namespaces/default/pods
- 则apiserver在查询default命名空间下pod的时候,只需要执行
get --prefix /api/v1/namespaces/default/pods
即可找到所有前缀为这个的pod信息
[root@VM-226-235-tencentos ~/zgy/yamls]# kubectl get pods -n default -v9 I0210 14:12:23.230538 766 loader.go:375] Config loaded from file: /root/.kube/config ...... I0210 14:12:23.267173 766 round_trippers.go:424] curl -k -v -XGET -H "Accept: application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json" -H "User-Agent: kubectl/v1.19.16 (linux/amd64) kubernetes/e37e4ab" 'https://9.135.226.235:6443/api/v1/namespaces/default/pods?limit=500' I0210 14:12:23.269413 766 round_trippers.go:444] GET https://9.135.226.235:6443/api/v1/namespaces/default/pods?limit=500 200 OK in 2 milliseconds I0210 14:12:23.269443 766 round_trippers.go:450] Response Headers:
- 设计为:
-
比如查看一个svc在etcd中的存储内容
- kubernetes资源在etcd中存储的都是protobuf的格式,所以直接查看是有些乱码的,不过不影响查看
# 先查看系统中所有的key sh-5.0# etcdctl --endpoints 127.0.0.1:2379 get --prefix / --keys-only # 再获取某个key的值,比如这里获取 kube-system/kube-dns service 的存储内容 sh-5.0# etcdctl --endpoints 127.0.0.1:2379 get /registry/services/specs/kube-system/kube-dns /registry/services/specs/kube-system/kube-dns k8s v1Service� � kube-dns kube-system"*$1aebd608-6aa9-499c-9d3a-9584df9e7cb42����Z k8s-apkube-dnsZ% kubernetes.io/cluster-servicetrueZ kubernetes.io/nameKubeDNSb prometheus.io/port9153b prometheus.io/scrapetruez� dnsUDP55( dns-tcpTCP55( metricsTCP�G�G( k8s-apkube-dns 10.96.0.10" ClusterIP:NoneBRZ`h "
1.7.3.高级操作
- 仅获取键名:
# 获取/前缀的所有key,一般使用--prefix的时候都要加--keys-only,否则对象多时同时把value输出就没法看了 etcdctl --endpoints=127.0.0.1:2479 get --prefix / --keys-only
- debug模式:类似kubectl的-v9,–debug会输出debug信息,包含环境变量、超时时间、endpoint等
etcdctl --endpoints=127.0.0.1:2479 --debug get key # 获取/a的数据,并且启动debug模式,输出细节 sh-5.0# etcdctl --debug get /a ETCDCTL_CACERT=ca.crt ETCDCTL_CERT=server.crt ETCDCTL_COMMAND_TIMEOUT=5s ETCDCTL_DEBUG=true ETCDCTL_DIAL_TIMEOUT=2s ETCDCTL_DISCOVERY_SRV= ETCDCTL_DISCOVERY_SRV_NAME= ETCDCTL_ENDPOINTS=[127.0.0.1:2379] ETCDCTL_HEX=false ETCDCTL_INSECURE_DISCOVERY=true ETCDCTL_INSECURE_SKIP_TLS_VERIFY=false ETCDCTL_INSECURE_TRANSPORT=true ETCDCTL_KEEPALIVE_TIME=2s ETCDCTL_KEEPALIVE_TIMEOUT=6s ETCDCTL_KEY=server.key ETCDCTL_PASSWORD= ETCDCTL_USER= ETCDCTL_WRITE_OUT=simple WARNING: 2025/02/10 02:18:39 Adjusting keepalive ping interval to minimum period of 10s WARNING: 2025/02/10 02:18:39 Adjusting keepalive ping interval to minimum period of 10s INFO: 2025/02/10 02:18:39 parsed scheme: "endpoint" INFO: 2025/02/10 02:18:39 ccResolverWrapper: sending new addresses to cc: [{127.0.0.1:2379 <nil> 0 <nil>}]
1.7.4.etcd的数据存储
- ETCD 的数据默认存储在运行目录下的
etcd
目录中 - 可以通过指定
--data-dir
来更改存储位置,如果不指定,默认会在运行目录下生成一个etcd
目录
1.7.5.etcd的安全认证
- ETCD 使用双向 TLS 认证,不仅客户端验证服务器端,服务器端也验证客户端,客户端需要携带
cert
、key
和cacert
文件 访问服务器端 - 这些文件在启动命令中已经指定,只需知道如何获取这些参数,当然有需要时也可以在命令中手动指定
sh-5.0# etcdctl --endpoints 127.0.0.1:2479 --cacert=ca.crt --cert=server.crt --key=server.key put /a/b value1
1.7.6.如何查看k-v的细节
- 使用-wjson,输出内容包括:集群信息、member信息、版本信息、kv信息等,其中kv是base64加密的
etcdctl --endpoints 127.0.0.1:2479 get a -wjson [root@VM-226-235-tencentos ~/zgy/yamls]# docker exec -it bd0feb758a0b etcdctl --endpoints=127.0.0.1:2479 get /a -wjson {"header":{"cluster_id":703969991527618354,"member_id":11172039883585733929,"revision":2,"raft_term":3},"kvs":[{"key":"L2E=","create_revision":2,"mod_revision":2,"version":1,"value":"MQ=="}],"count":1}
1.8. etcd 的核心机制
1.8.1.过期时间 TTL(Time To Live)
- 为键值对设置过期时间,默认单位是秒:
etcdctl --endpoints 127.0.0.1:2379 put key value --lease=1234
1.8.2.乐观锁 CAS(Compare And Swap)
- 条件写入:
- 在写入键值对之前,先检查某个条件是否满足的操作。如果条件满足,执行成功请求;如果条件不满足,执行失败请求
- etcdctl txn 命令执行条件写入操作,并通过 --interactive 参数启用交互模式
# 进入写入操作 etcdctl txn --interactive # 命令格式 compare: value("key") = "old_value" success requests: put key new_value failure requests: put key fail_value # 使用示例:每输入一行,要进行回车进入下一条命令 sh-5.0# etcdctl txn --interactive compares: value("/a/b") = "value1" success requests (get, put, del): put /a/b value-new failure requests (get, put, del): put /a/b value1 SUCCESS OK # 查看CAS更新结果 sh-5.0# etcdctl get /a/b /a/b value-new
- etcd使用CAS机制目的就是避免锁的引入,不过etcd还是有锁的,它支持类似于数据库的事务,如果你有多条写操作,可以加起一个transaction,最后一起commit
2.Raft 协议
- 学习网站:
- http://thesecretlivesofdata.com/raft
- https://raft.github.io/
2.1 CAP 原则
- 一致性(Consistency)
- 可用性(Availability)
- 分区容错性(Partition Tolerance)
一致性和可用性一定要有个取舍,强一致代表效率一定不会很高,弱一致时一般都会有数据同步等问题
2.2 Raft 确保一致性的核心:quorum多数派机制
- quorum 多数派机制:即当同意当前操作的节点数据 超过半数时,才允许生效,否则不生效
- Raft确保分布式一致性,其实主要做两件事:
- Leader Election:leader选举
- Log Replication:日志复制
- 这两个过程都可以在这个网站看到动态图
- http://thesecretlivesofdata.com/raft
2.3.Raft 协议的 Leader Election(leader选举)
2.3.1.节点的3种身份
- 当使用 Raft 一致性算法时,节点可以扮演三种不同的角色:Leader(领导者)、Follower(跟随者)和Candidate(候选人)。这些角色在 Raft 协议中起着不同的作用和责任。
- Leader(领导者)
- Leader 是 Raft 群集中的一种特殊节点角色
- Leader 负责处理客户端的请求,并将日志条目复制到其他节点(跟随者)。
- Leader 通过发送心跳消息来维持其领导地位,并防止其他节点成为 Leader。
- Follower(跟随者)
- Follower 是 Raft 群集中的普通节点角色
- Follower 跟随 Leader 的指导,并接受 Leader 发送的日志条目
- Follower 可以投票给候选人,以帮助选举新的 Leader
- Candidate(候选人)
- Candidate 是 Raft 群集中的临时节点角色。
- 当一个节点超过一定时间(每个节点都维护了一个选举超时时间Election Timeout)没有收到leader的心跳时,将会成为候选人,开始一次新的选举过程,试图成为新的 Leader。
- 候选人会向其他节点发送选举请求,并等待其他节点的投票。
- 如果候选人获得了大多数节点的投票,它将成为新的 Leader。
- Leader(领导者)
- Raft 协议通过定期的选举过程来维护 Leader 的稳定性。当 Leader 失败或无法与其他节点通信时,会触发新的选举过程,以选择一个新的 Leader。在选举过程中,节点将转换为候选人角色,并尝试获得其他节点的投票
- Leader-Follower-Candidate 的角色切换机制确保了 Raft 群集的一致性和可用性
- Leader 负责处理客户端请求,Follower 跟随 Leader 的指导,而 Candidate 则参与选举过程以确保 Leader 的连续性。
2.3.2.选举过程
-
初始状态:
- 假设我们有一个 Raft 集群,包含 5 个节点:A、B、C、D 和 E。
- 初始状态下,所有节点都是 Follower。
-
选举超时(Election Timeout):
- 每个 Follower 节点都有一个选举超时计时器。
- 当选举超时计时器到期时,节点将转变为候选人(Candidate)角色,并开始新的选举过程。
-
候选人状态:
- 节点 A 的选举超时计时器到期,它成为候选人,给自己投一票,同时向其他节点发送选举请求(Request Vote)。
- 其他节点(B、C、D 和 E)收到选举请求后,会检查自己的状态并决定是否投票给候选人。
-
投票过程:
- 节点 B、C 和 D 向候选人 A 投票,因为它们还没有投票给其他候选人。
- 节点 E 由于已经投票给了另一个候选人,所以不会再投票给候选人 A。
-
选举结果:
- 如果候选人 A 获得了大多数节点的选票(超过半数),它将成为新的 Leader。
- 假设节点 B、C 和 D 都投票给了候选人 A,那么 A 将成为新的 Leader。
-
Leader 宣布:
- 新的 Leader A 会使用心跳机制 宣布自己的地位,并开始处理客户端的请求。
- 其他节点(B、C、D 和 E)将转变为 Follower 角色,并跟随新的 Leader。
- 选举中的一些注意点:
- 只要一个节点没有leader,且有Candidate向他请求投票,它就一定会投 赞成
- 当一个节点投了票之后,就会重置自己的超时时间,保证不会在投票后自行又发起选举
2.3.3.选举问题一:选票冲突(Vote Conflict)
- 当多个候选人同时发起选举时,可能会导致选票冲突。
- 解决方法:
- 集群初始化时,默认每个节点都是Follower,此时可以为每个节点初始化 随机的选举超时时间,比如150-300毫秒之间随机值,保证节点不会同时发起选举
- 集群运行中,如果出现多个Candidate同时选举,Raft 使用递增的任期号(Term)来解决选票冲突。节点会比较候选人的任期号,并投票给任期号更大的候选人。如果发生脑裂也会这样保证老leader会退位让贤
2.3.4.选举问题二:无法达到多数票(Majority Votes)
- 在选举过程中,候选人需要获得多数节点的选票才能成为新的 Leader。
- 解决方法:
- 如果没有候选人获得多数票,选举将失败。此时,节点可以增加选举超时时间,并重新发起选举过程,直到有候选人获得多数票。
- 另外,raft集群一般都要求 奇数个节点,保证投票时不会发生平票
2.3.5.选举问题三:网络分区(Network Partition)导致脑裂
- 当 Raft 群集中的节点由于网络故障或分区而无法通信时,可能会导致选举过程中断或发生脑裂。
- Raft 使用心跳机制来维持 Leader 的地位,发生网络分区时,每个分区都将产生一个leader,称为脑裂。脑裂是一个常见的分布式系统中的挑战
- 解决方法:
- raft集群一般都要求 奇数个节点,比如5个节点ABCDE,其中A为leader,发生分区后产生两个分区:AB、CDE
- 此时CDE会选举出新的leader,比如C,则此时有两个leader A、C 都在接收客户端请求
- 当有请求进入A时,A写入本地log,然后复制到B,B返回ack,但是只有一票成功,无法达到大多数,所以到达A的请求无法commit,也就无法生效
- 当有请求进入C时,C写入本地log,然后复制到DE,DE都返回ack,2票成功,更改commited生效
- 当网络分区恢复后,A认为自己是leader,向C发心跳,C看到自己的Term更大就会无视A。而C向A发心跳,A发现自己的Term小,就会主动成为C的follower,AB回滚自己未commit的更改,重新比较C的log,同步最新数据,从而解决网络分区带来的脑裂
- 但如果一个网络分区,无法达到多数票,就无法选出leader
4. 选举过程过长(Election Process Takes Too Long)
- 在某些情况下,选举过程可能需要较长时间才能完成,导致系统的可用性下降。
- 解决方法:可以调整选举超时时间和心跳间隔,以减少选举过程的时间。另外,使用预投票(Pre-vote)机制可以更快地检测到网络分区并进行选举。
5. 恢复过程中的数据一致性(Data Consistency during Recovery)
- 当 Leader 失败或重新选举时,可能会导致数据一致性问题。
- 解决方法:Raft 使用日志复制机制来确保数据一致性。当新的 Leader 当选后,它会将自己的日志条目复制到其他节点,以恢复数据一致性。
2.4.Raft 协议的 Log Replication(日志复制)
Raft对数据的变更操作,都是通过 日志复制 + 多数派机制 来完成的
- 每个节点都包括:一致性模块、日志模块、状态机
2.4.1.日志复制原理
-
Leader 推送日志:
- Leader 接收客户端的请求,并将请求转化为日志条目(Log Entry)。
- Leader 将日志条目追加到自己的日志中,并并行地将日志条目发送给其他节点(Followers)。
- 发给其他节点时,是通过下一次的心跳携带数据过去的
-
日志复制过程:
- Followers 接收到 Leader 发送的日志条目后,将其追加到自己的日志中。
- Followers 向 Leader 发送确认消息(Append Entries Response)表示已成功复制日志条目。
-
多数派确认:
- Leader 在收到多数派节点的确认消息后,将该日志条目标记为已提交(Committed)。
- Leader 会通知 Followers 将已提交的日志条目应用到状态机,以实现数据一致性。
-
容错性:
- 如果 Leader 失败,新的 Leader 将被选举出来,并继续进行日志复制过程。
- 如果 Followers 失败或无法通信,Leader 会重试发送日志条目,直到多数派节点确认复制成功。
2.4.2.日志复制过程描述
-
假设我们有一个 Raft 集群,包含 Leader(L)和两个 Followers(F1、F2)。
-
Leader 接收客户端请求,生成日志条目:
Log Entry 1
。 -
Leader 将
Log Entry 1
追加到自己的日志中,并并行发送给 Followers。 -
F1 和 F2 接收到
Log Entry 1
,将其追加到各自的日志中,并发送确认消息给 Leader。 -
Leader 收到来自 F1 和 F2 的确认消息,确认
Log Entry 1
已成功复制。 -
Leader 将
Log Entry 1
标记为已提交,并通知 Followers 将其应用到状态机。 -
F1 和 F2 将
Log Entry 1
应用到各自的状态机,实现数据一致性。
-
2.5.Raft协议中的两个超时时间
-
--heartbeat-interval
:心跳间隔,leader发送心跳的周期,xxxx ms 向follower发送一次心跳 -
--election-timeout
:选举超时,follower能容忍多久没收到心跳的时间,超过后follower就会造反转变成候选人身份,发起新的选举 -
可以在etcd的命令行参数中配置
2.6.节点新角色:Learner 解决增加集群节点问题
-
Raft 协议传统角色构成:Leader、Follower、Candidate,此时想要向 Raft 集群中新增一个节点,存在问题:
- 新节点数据为空,Leader 需要大量数据复制
- 数据同步可能会占用大量网络带宽,导致leader发送心跳失败,进而触发选举重新投票
- 而新节点数据太落后,此时触发选举 可能会产生无效投票,进而影响集群的稳定性
-
为解决上述问题,引入了Learner角色,特性:仅接收数据,不参与投票
-
Learner 引入时间
- Learner的概念最早在 ETCD 3.4 版本 中实现(2019 年)
- 后来逐渐被其他 Raft 实现(如 TiKV、Consul 等)采纳,在 Raft 4.2.1引入
- 因此Learner 是 Raft 协议的扩展功能,并非原始 Raft 论文的一部分,是 Raft 协议在实际工程应用中的一种优化和改进
-
引入Learner后,增加新节点的优势:
- Learner不参与投票,所以票数quorum没有变,原健康节点可快速达成共识
- 避免新节点因数据差异导致无效投票
- 保持集群稳定性
- Learner节点支持异步数据同步,同步完成后将自动转为Follower,参与后续投票
2.7.强一致性和弱一致性
- 弱一致性:
- 主节点 commit 后,会立刻返回客户端确认,不会等待从节点也commit完成。
- 缺点:数据安全性较低,主节点故障时,未 commit 的数据可能丢
- 优点:效率高
- 适用于对性能要求较高,且可以容忍少量数据丢失的场景,如日志系统
- 强一致性:
- 必须所有 follower 二次确认 commit 后,主节点才返回客户端确认。
- 缺点:效率较低
- 优点:数据安全性高
- 适用于对数据一致性要求极高的场景,如金融系统
- 配置参数:
- ETCD 启动时可配置强一致性或弱一致性,
--quorum-read
参数,默认false弱一致性 - 默认一般为弱一致性,因为大多数场景下够用了,如果你的场景里对数据安全性要求非常高,可以配置为强一致性。
- ETCD 启动时可配置强一致性或弱一致性,
2.8.Raft的安全性机制详解
Raft 协议通过选举安全性和Leader 完整性等机制,确保集群中所有节点执行相同的操作序列,避免数据不一致。以下是详细解释:
2.8.1. 选举安全性(Election Safety)
-
定义:在同一个任期内(Term),只能选举出一个 Leader。
-
作用:避免多个 Leader 同时存在,导致集群分裂和数据不一致。
-
实现方式:
- 每个节点在同一任期内只能投一次票(给一个 Candidate)。
- 只有获得多数票的 Candidate 才能成为 Leader。
- 如果选举超时,会进入新的任期,重新发起选举。
-
举例:
- 假设集群有 5 个节点,Term 为 1。
- 如果有两个 Candidate 同时发起选举,最多只有一个 Candidate 能获得至少 3 票(多数票),成为 Leader。
2.8.2.Leader 完整性(Leader Completeness)
- 定义:如果一个日志条目在某个任期被提交(Committed),那么后续任期的 Leader 必须包含这个日志条目。
- 作用:确保已经被提交的日志不会被覆盖或丢失。
- 实现方式:
- Raft 通过选举限制来保证 Leader 完整性:
- 只有日志足够新的节点(即包含更多已提交日志的节点)才能成为 Leader。
- 在选举时,Candidate 会携带自己的日志信息(Term 和 Index)。
- 其他节点会比较 Candidate 的日志是否比自己更新:
- 如果 Candidate 的 Term 更大,或者 Term 相同但 Index 更大,则投票给它。
- 否则,拒绝投票。
- Raft 通过选举限制来保证 Leader 完整性:
- 举例:
- 假设 Term 1 的 Leader 提交了一条日志(Log A)。
- 如果 Term 1 的 Leader 崩溃,Term 2 的选举中,只有那些包含 Log A 的节点才有可能成为 Leader。
- 这样,Log A 就不会丢失,新 Leader 会继续复制 Log A 给其他节点。
2.8.3. 为什么需要这些机制?
- 问题场景:
- 假设某个 Follower 在 Leader 提交日志时变得不可用。
- 稍后,这个 Follower 重新加入集群,并被选举为 Leader。
- 如果这个新 Leader 的日志不完整,它可能会用新的日志覆盖已经提交的日志,导致数据不一致。
- 解决方案:
- Raft 通过选举安全性和Leader 完整性,确保只有日志足够新的节点才能成为 Leader。
- 这样,新 Leader 一定包含所有已提交的日志,避免了数据丢失或不一致。
2.8.4. 总结
- 选举安全性:确保同一任期内只有一个 Leader,避免脑裂。
- Leader 完整性:确保新 Leader 包含所有已提交的日志,避免数据丢失。
- 核心思想:通过日志比较和多数投票机制,Raft 保证了集群的一致性和安全性。
2.9.Etcd基于Raft协议实现一致性
2.9.1.Etcd 的 leader选举
2.9.2.Etcd 的 日志复制(默认弱一致性)
2.9.3.安全性
2.9.4.Etcd 的 失效处理
2.9.5.Etcd 的 wal log日志
- Raft任何请求的数据都会先写到本地 wal log日志里,然后同步给follower
- Etcd wal log 的type早期版本主要有两种,现已扩展到五种
- EntryNormal:表示普通的数据变更操作。这是最常见的类型,用于记录键值对的插入、更新和删除等操作。
- EntryConfChange:表示配置变更操作。这种类型的日志记录用于记录集群配置的变更,例如添加或删除节点、更改节点角色等。
- EntryAddNode:表示添加节点操作。这种类型的日志记录用于记录添加新节点到集群的操作。
- EntryRemoveNode:表示移除节点操作。这种类型的日志记录用于记录从集群中移除节点的操作。
- EntryBarrier:表示屏障操作。这种类型的日志记录用于实现线性一致性,确保在屏障操作之前的所有操作都已提交。
- 使用
etcd-prologue
工具查看 wal log 信息,该工具可以将二进制格式的 wal log 转换为文本格式,便于分析和调试。 - wal是日志记录技术,在etcd中日志的格式为vlog
2.9.6.Etcd对存储的具体实现
- Etcd对存储的具体实现,可以分为4部分:
- KV store:
- 包含 KV index,基于 betree 数据结构,在内存中对所有的key做了一个索引,用于加速查询
- 用于数据索引,提高读写性能。
- boltDB:
- 数据最终存储到backend,目前backend其实用的是另外一个数据库 boltDB。
- boltDB 是谷歌开发的一个轻量级的键值存储数据库,适用于小型数据集。在设计里也可以使用其他db,目前用的是boltDB
- watchable store:
- 提供监听机制,监听数据变化。
- 通过监听机制,客户端可以实时获取数据变更通知。
- lease:
- 控制数据的 TTL(生命周期),到期自动删除。
- 通过 TTL 机制,可以实现数据的自动清理和过期管理。
- KV store:
3.etcd常见问题
3.1.etcdctl 执行报错
- 进入kubernetes携带安装的etcd中,执行etcdctl命令,可能会报错:
[root@VM-226-235-tencentos ~]# kubectl exec -it etcd-vm-226-235-tencentos -n kube-system -- /bin/sh sh-5.0# sh-5.0# etcdctl get --prefix / --keys-only {"level":"warn","ts":"2025-02-10T02:05:21.349Z","caller":"clientv3/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"endpoint://client-cd94a73a-3967-450f-83f0-ec2744418038/127.0.0.1:2379","attempt":0,"error":"rpc error: code = DeadlineExceeded desc = latest balancer error: all SubConns are in TransientFailure, latest connection error: connection closed"} Error: context deadline exceeded
- github issues:https://github.com/etcd-io/etcd/issues/12234
- 这种是证书没有读取成功,可以手动指定证书来执行etcdctl命令
# 先ps -ef查看你的etcd启动命令 [root@VM-226-235-tencentos ~]# ps -ef | grep etcd root 24115 24089 2 2024 ? 7-15:58:52 etcd --advertise-client-urls=https://xxx.xxx.xxx.xxx:2379 --cert-file=/etc/kubernetes/pki/etcd/server.crt --client-cert-auth=true --data-dir=/var/lib/etcd --initial-advertise-peer-urls=https://xxx.xxx.xxx.xxx:2380 --initial-cluster=vm-226-235-tencentos=https://xxx.xxx.xxx.xxx:2380 --key-file=/etc/kubernetes/pki/etcd/server.key --listen-client-urls=https://127.0.0.1:2379,https://xxx.xxx.xxx.xxx:2379 --listen-metrics-urls=http://127.0.0.1:2381 --listen-peer-urls=https://xxx.xxx.xxx.xxx:2380 --name=vm-226-235-tencentos --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt --peer-client-cert-auth=true --peer-key-file=/etc/kubernetes/pki/etcd/peer.key --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt --snapshot-count=10000 --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt # 从启动参数中获取到证书位置,写在etcdctl的命令中 [root@VM-226-235-tencentos ~]# etcdctl --endpoints 127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key put /a/b value1 OK
3.2.etcd和consul如何选择
- consul主要应用场景:服务发现和负载均衡。因此如果你有服务发现的需求,还是优先推荐使用consul,它是专业的
- etcd:也可以做服务发现,且抱着kubernetes的大腿,如果再云原生有一些小的场景需求,使用etcd就足够了,不需要再引入其他组件了,还有学习成本
- 比如spring cloud现在支持native的service discovery,其实就是查询ETCD的
3.3.ETCD 和 Zookeeper 的区别
- zookeeper:使用 Paxis 协议,复杂度较高,性能和一致性维护上存在一些问题。
- etcd:使用Raft协议,是对 Paxos 协议的简化和增强
3.4.ETCD 的一些其他知识点
3.4.1.数据持久化
- 数据存储位置:
- ETCD 的数据存储在
/var/lib/etcd
目录下。 - 该目录通过 volume 挂载到 ETCD Pod 中。
- ETCD 的数据存储在
- 数据持久化:
- 即使 Pod 重启或主机重启,只要
/var/lib/etcd
目录未被删除,数据不会丢失。 - 建议将数据存储在主机目录或独立的 SSD 盘上,而不是容器内的 Overlay FS
- 即使 Pod 重启或主机重启,只要
3.4.2.ETCD 部署模式
- 独立部署 vs. Pod 部署:
- 独立部署:ETCD 集群与 Kubernetes 控制平面分离,适用于大规模集群。
- Pod 部署:ETCD 与 Kubernetes 控制平面部署在同一节点,适用于小型集群。
- 读请求优化:
- ETCD 的读请求不一定要经过 leader,每个 member 都可以处理读请求。
- 如果 ETCD 和 API Server 部署在同一节点,读请求的效率会更高。
3.4.3.ETCD 扩容
- 扩容操作:
- 通过
etcdctl
的add-member
命令添加新 member。 - 扩容时需确保集群处于健康状态。
- 通过
3.4.4.ETCD 高可用性
- 3 个 ETCD member 挂掉 1 个:
- 集群仍可正常工作,因为剩余 2 个 member 可以满足多数投票条件。
- 如果挂掉 2 个 member,集群将无法正常工作。
3.4.5.ETCD 的亲和性
- 亲和性:
- ETCD 和 API Server 之间具有亲和性,因为 API Server 是 ETCD 的主要调用方。
- 在调度系统中,亲和性可以提高服务的稳定性和性能。
- 反亲和性:
- 反亲和性是指将服务的多个实例部署到不同节点,以提高容错性。
3.4.6.ETCD 数据恢复
- 数据恢复:
- 只要有集群的备份数据,即使 3 个 master 节点同时宕机,也可以通过备份恢复集群。
- 使用
etcdctl snapshot
命令进行数据备份和恢复。