etcd

Etcd

介绍

Etcd是CoreOS基于Raft开发的分布式key-value存储,可用于服务发现、共享配置以及一致性保障(如数据库选主、分布式锁等)。

在分布式系统中,如何管理节点间的状态一直是一个难题,etcd像是专门为集群环境的服务发现和注册而设计,它提供了数据TTL失效、数据监听、分布式锁原子操作等功能,可以方便的跟踪并管理集群节点的状态。

  • 键值对存储:将数据存储在分层组织的目录中,如同在标准文件系统中
  • 监测变更:监测特定的键或目录以进行更改,并对值的更改做出反应
  • 简单: curl可访问的用户的API(HTTP+JSON)
  • 安全: 可选的SSL客户端证书认证
  • 快速: 单实例每秒1000次写操作,2000+次读操作
  • 可靠: 使用Raft算法保证一致性

主要功能

  • 基本的key-value存储
  • 监听机制
  • key的过期及续约机制,用于监控和服务发现
  • 原子Compare And Swap和Compare And Delete,用于分布式锁和leader选举

etcd 使用的介绍

etcd 的启动参数讲解

1
2
3
4
5
6
7
8
9
10
11
etcd --listen-client-urls 'http://localhost:12379' \
--advertise-client-urls 'http://localhost:12379' \
--listen-peer-urls 'http://localhost:12380' \
--initial-advertise-peer-urls 'http://localhost:12380' \
--initial-cluster 'default=http://localhost:12380'

# listen-client-urls 监听客户端连接的 URL 列表。当用户通过 etcdctl 或者 API 发起请求时,就会使用这些 URL 进行通信。
# advertise-client-urls 告诉客户端自己的地址,以便它们能够正确地连接到该节点。
# listen-peer-urls 监听同伴(peer)节点连接的 URL 列表。
# initial-advertise-peer-urls 告诉其他同伴节点自己的地址,用于新节点加入时进行发现。
# initial-cluster 用于指定初始的同伴节点列表。

ETCD 常用命令

查看集群成员列表

1
2
etcdctl member list --write-out=table
# --endpoints 指定server节点

读写数据

1
2
3
4
5
6
7
8
9
10
11
12
13
# 写入数据
etcdctl --endpoints=localhost:12379 put /a b

# 读取数据
etcdctl --endpoints=localhost:12379 get /a
# --prefix 按照前缀查询
# --keys-only 只显示键值
# -w 指定输出格式 json
# --debug 输出debug 信息

# 监听key
etcdctl --endpoints=localhost:12379 watch /a
# --prefix 按照前缀查询

ETCD V3 存储架构

image-20231002113043979

Etcd v3 将 watch 和 store 拆开实现,我们先分析下 store 的实现。etcd v3 store 分为两部分:

  • 一部分是内存中的索引,kvindex,是基于Google开源的一个Golang的btree实现的
  • 另外一部分是后端存储。按照它的设计,backend可以对接多种存储,当前使用的boltdb。是一个单机的支持事务的kv存储,etcd 的事务是基于boltdb的事务实现的。

etcd 在每次修改 key 时会生成一个全局递增的版本号(revision)。

  • 然后通过数据结构 B-tree 保存用户 key 与版本号之间的关系;
  • 再以版本号作为 boltdb key,以用户的 key-value 等信息作为 boltdb value,保存到 boltdb。也就是说 etcd 会在boltdb中把每个版本都保存下,从而实现了多版本机制。

reversion的结构

reversion主要由两部分组成,第一部分main rev,每次事务进行加一,第二部分sub rev,同一个事务中的每次操作加一。

etcd 提供了命令和设置选项来控制compact来进行版本压缩,同时支持put操作的参数来精确控制某个key的历史版本数。

内存kvindex保存的就是key和reversion之间的映射关系,用来加速查询。

ETCD V3 写入完整流程

写入流程

image-20231002114332028

  1. 配额、限速、鉴权、包的检查
  2. 请求发送到kvserver 核心处理组件
  3. 请求包装成propose,发送到一致性模块,进行暂存,标记raftlog数据为unstable
  4. 将请求发送给所有follower进行数据同步, 并写入到WAL 日志
  5. 收到半数写入成功后,将 mem中的raftlog unstable的数据变为commited, 更新matchIndex(可以理解为commitIndex)
  6. 数据写入到多版本控制模块, 并更新mem中的 raftlog commited数据为apply, 更新applyIndex

为什么Committed Index 和applyindex 不需要存储

这个和日志复制的机制有关系。首先对于选举,PK的条件不是拼这两个索引值的大小,PK的是最后一条日志的任期号和日志的长度。Leader当选后进行第一次日志复制时,会和Follower进行若干次日志的匹配过程,最终可以得到Leader和各自Follower的日志匹配的matchIndex值。处于majority节点列表的matchIndex的最小值就是当前Leader的commitIndex。所以commitIndex值是完全可以动态计算出来的。

如果所有的日志都保留不截断的话,服务器重启时applyIndex应该等于零。然后重放一下所有的已经提交的日志就可以得到当前的状态机。如果日志截断有快照的话,applyIndex应该正好是日志序列的头部位置,这个位置一般是存储在快照元信息里面的,它是持久化在磁盘中的。

ETCD Watch机制

etcd v3 的watch机制支持watch某个固定的key,也支持watch一个范围(可以用于模拟目录的结构的watch),所以 watchGroup 包含两种watcher,一种是 key watchers,数据结构是每个key对应一组watcher,另外一种是 range watchers, 数据结构是一个 IntervalTree,方便通过区间查找到对应的watcher。

同时,每个 WatchableStore 包含两种 watcherGroup,一种是synced,一种是unsynced,前者表示该group的watcher数据都已经同步完毕,在等待新的变更,后者表示该group的watcher数据同步落后于当前最新变更,还在追赶。

当 etcd 收到客户端的watch请求,如果请求携带了revision参数,则比较请求的revision和store当前的revision,如果大于当前revision,则放入synced组中,否则放入unsynced组。同时 etcd 会启动一个后台的goroutine持续同步unsynced的watcher,然后将其迁移到synced组。

也就是这种机制下,etcd v3 支持从任意版本开始watch,没有v2的1000条历史event表限制的问题(当然这是指没有compact的情况下)

etcd 相关参数

成员相关参数

1
2
3
4
5
6
7
8
--name 'default'
# Human-readable name for this member.
--data-dir '${name}.etcd'
# Path to the data directory. 数据存储目录
--listen-peer-urls 'http://localhost:2380'
# List of URLs to listen on for peer traffic. 本节点与其他节点进行数据交换(选举,数据同步)的监听地址
--listen-client-urls 'http://localhost:2379'
# List of URLs to listen on for client traffic. 监听地址,地址写法是 scheme://IP:port

集群相关参数

1
2
3
4
5
6
7
8
9
10
--initial-advertise-peer-urls 'http://localhost:2380'
# List of this member's peer URLs to advertise to the rest of the cluster. 告知其他集群节点访问哪个URL,initial-advertise-peer-urlsl将是istener-peer-urls的子集
--initial-cluster 'default=http://localhost:2380'
# Initial cluster configuration for bootstrapping. 此处default为节点的--name指定的名字;localhost:2380为--initial-advertise-peer-urls指定的值。
--initial-cluster-state 'new'
# Initial cluster state ('new' or 'existing'). 设置new为初始静态或DNS引导期间出现的所有成员。如果将此选项设置为existing,则etcd将尝试加入现有群集。
--initial-cluster-token 'etcd-cluster'
# Initial cluster token for the etcd cluster during bootstrap. 集群唯一标识,相同标识的节点将视为在一个集群内
--advertise-client-urls 'http://localhost:2379'
# List of this member's client URLs to advertise to the public. 用于通知其他ETCD节点,客户端接入本节点的监听地址,一般来说advertise-client-urls是listen-client-urls子集

安全相关参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--cert-file ''
# Path to the client server TLS cert file. 客户端服务器TLS证书文件的路径。
--key-file ''
# Path to the client server TLS key file. 客户端服务器TLS密钥文件的路径
--client-crl-file ''
# Path to the client certificate revocation list file. 客户端证书吊销列表文件的路径
--trusted-ca-file ''
# Path to the client server TLS trusted CA cert file. 客户端服务器的路径TLS可信CA证书文件
--peer-cert-file ''
# Path to the peer server TLS cert file. 对等服务器TLS证书文件的路径。
--peer-key-file ''
# Path to the peer server TLS key file. 对等服务器TLS密钥文件的路径。
--peer-trusted-ca-file ''
# Path to the peer server TLS trusted CA file. 对等服务器TLS可信CA文件的路径

灾备

1
2
3
4
5
6
7
8
9
10
11
12
# 创建Snapshot
etcdctl --endpoints https://127.0.0.1:3379 --cert /tmp/etcd-certs/certs/127.0.0.1.pem --
key /tmp/etcd-certs/certs/127.0.0.1-key.pem --cacert /tmp/etcd-certs/certs/ca.pem
snapshot save snapshot.db
# 恢复数据到数据目录
etcdctl snapshot restore snapshot.db \
--name infra2 \
--data-dir=/tmp/etcd/infra2 \
--initial-cluster
infra0=http://127.0.0.1:3380,infra1=http://127.0.0.1:4380,infra2=http://127.0.0.1:5380 \
--initial-cluster-token etcd-cluster-1 \
--initial-advertise-peer-urls http://127.0.0.1:5380

Alarm & Disarm Alarm

1
2
3
4
5
6
# 查看endpoint状态
$ ETCDCTL_API=3 etcdctl --write-out=table endpoint status
# 查看alarm
$ ETCDCTL_API=3 etcdctl alarm list
# 清理alarm
$ ETCDCTL_API=3 etcdctl alarm disarm

碎片清理

1
2
3
4
5
6
7
8
9
# 方法一 自动清理
# keep one hour of history
$ etcd --auto-compaction-retention=1
# 方法二 手动清理
# compact up to revision 3
$ etcdctl compact 3
# clean up disk fragment 碎片整理过程会将此存储空间释放回文件系统。
$ etcdctl defrag
Finished defragmenting etcd member[127.0.0.1:2379]

etcd 集群高可用实践

最合适的peer个数

  • 通常为5个, 可以允许2个节点挂掉不影响正常运行
  • 还需要考虑是否为多读少写, 因为所有写都经过leader

apiserver和etcd 部署

  • apiserver和etcd 部署在同一节点
  • apiserver和etcd之间的通讯基于gRPC

存储的规划

  • 最佳实践: Local SSD + auto compact(etcd 支持使用–auto-compact自动合并版本)

事件分离

  • 对于大规模集群,大量的事件会对etcd造成压力, 可以将一些特定事件流量转到 extra etcd

  • API server 启动脚本中指定etcd servers集群

    1
    /usr/local/bin/kube-apiserver --etcd_servers=https://localhost:4001 --etcd-cafile=/etc/ssl/kubernetes/ca.crt --storage-backend=etcd3 --etcd-servers-overrides=/events#https://localhost:4002

减少网络延迟

  • 数据中心内的RTT大概是数毫秒,国内的典型RTT约为50ms,两大洲之间的RTT可能慢至400ms。因此建议etcd集群尽量同地域部署。
  • 当客户端到Leader的并发连接数量过多,可能会导致其他Follower节点发往Leader的请求因为网络拥塞而被延迟处理。
  • 可以在节点上通过流量控制工具(Traffic Control)提高etcd成员之间发送数据的优先级来避免。

减少磁盘IO延迟

  • 对于磁盘延迟,典型的旋转磁盘写延迟约为10毫秒。对于SSD(Solid State Drives,固态硬盘),延迟通常低于1毫秒。HDD(Hard Disk Drive,硬盘驱动器)或者网盘在大量数据读写操作的情况下延时会不稳定。因此强烈建议使用SSD。
  • 同时为了降低其他应用程序的I/O操作对etcd的干扰,建议将etcd的数据存放在单独的磁盘内。
  • 如果不可避免地,etcd和其他的业务共享存储磁盘,那么就需要通过下面ionice命令对etcd服务设置更高的磁盘I/O优先级,尽可能避免其他进程的影响。

合理的日志文件大小

  • etcd以日志的形式保存数据,无论是数据创建还是修改,它都将操作追加到日志文件,因此日志文件大小会随着数据修改次数而线性增长。

    当Kubernetes集群规模较大时,其对etcd集群中的数据更改也会很频繁,集群日记文件会迅速增长。

    为了有效降低日志文件大小,etcd会以固定周期创建快照保存系统的当前状态,并移除旧日志文件。另外当修改次数累积到一定的数量(默认是10000,通过参数“–snapshot-count”指定),etcd也会创建快照文件。

    如果etcd的内存使用和磁盘使用过高,可以先分析是否数据写入频度过大导致快照频度过高,确认后可通过调低快照触发的阈值来降低其对内存和磁盘的使用。

合理的存储配额

存储空间的配额用于控制etcd数据空间的大小。合理的存储配额可保证集群操作的可靠性。如果没有存储配额,也就是etcd可以利用整个磁盘空间,etcd的性能会因为存储空间的持续增长而严重下降,甚至有耗完集群磁盘空间导致不可预测集群行为的风险。如果设置的存储配额太小,一旦其中一个节点的后台数据库的存储空间超出了存储配额,etcd就会触发集群范围的告警,并将集群置于只接受读和删除请求的维护模式。只有在释放足够的空间、消除后端数据库的碎片和清除存储配额告警之后,集群才能恢复正常操作。

自动压缩历史版本

  • etcd会为每个键都保存了历史版本。为了避免出现性能问题或存储空间消耗完导致写不进去的问题,这些历史版本需要进行周期性地压缩。压缩历史版本就是丢弃该键给定版本之前的所有信息,节省出来的空间可以用于后续的写操作。etcd支持自动压缩历史版本。在启动参数中指定参数“–auto-compaction”,其值以小时为单位。也就是etcd会自动压缩该值设置的时间窗口之前的历史版本。

定期消除碎片化

压缩历史版本,相当于离散地抹去etcd存储空间某些数据,etcd存储空间中将会出现碎片。这些碎片无法被后台存储使用,却仍占据节点的存储空间。因此定期消除存储碎片,将释放碎片化的存储空间,重新调整整个存储空间。

备份方案

  • etcd备份: 通过 etcdctl snapshot save, 备份的时间间隔需要考虑, 时间过长, 会导致数据丢失过多, 时间间隔太短, 会造成etcd频繁对数据上锁, 会对etcd造成影响

  • 如何确保备份的时效性, 同时防止磁盘爆掉: 通过 snapshot + 备份wal log , 来用snapshot进行恢复, wallog进行回放。

    etcd的默认工作目录下会生成两个子目录:wal和snap。wal是用于存放预写式日志,其最大的作用是记录整个数据变化的全部历程。所有数据的修改在提交前,都要先写入wal中。snap是用于存放快照数据。为防止wal文件过多,etcd会定期(当wal中数据超过10000条记录时,由参数“–snapshot-count”设置)创建快照。当快照生成后,wal中数据就可以被删除了。如果数据遭到破坏或错误修改需要回滚到之前某个状态时,方法就有两个:一是从快照中恢复数据主体,但是未被拍入快照的数据会丢失;而是执行所有WAL中记录的修改操作,从最原始的数据恢复到数据损坏之前的状态,但恢复的时间较长。

数据备份方案实践

官方推荐etcd集群的备份方式是定期创建快照。和etcd内部定期创建快照的目的不同,该备份方式依赖外部程序定期创建快照,并将快照上传到网络存储设备以实现etcd数据的冗余备份。上传到网络设备的数据,都应进行了加密。即使当所有etcd实例都丢失了数据,也能允许etcd集群从一个已知的良好状态的时间点在任一地方进行恢复。根据集群对etcd备份粒度的要求,可适当调节备份的周期。在生产环境中实测,拍摄快照通常会影响集群当时的性能,因此不建议频繁创建快照。但是备份周期太长,就可能导致大量数据的丢失。

这里可以使用增量备份的方式。如图3-8所示,备份程序每30分钟触发一次快照的拍摄。紧接着它从快照结束的版本(Revision)开始,监听etcd集群的事件,并每10秒钟将事件保存到文件中,并将快照和事件文件上传到网络存储设备中。30分钟的快照周期对集群性能影响甚微。当大灾难来临时,也至多丢失10秒的数据。至于数据修复,首先把数据从网络存储设备中下载下来,然后从快照中恢复大块数据,并在此基础上依次应用存储的所有事件。这样就可以将集群数据恢复到灾难发生前。

image-20231004094024778

心跳周期和选举超时时间的优化

当网络延迟和磁盘延迟固定的情况下,可以优化etcd运行参数来提升集群的工作效率。

etcd基于Raft协议进行Leader选举,当Leader选定以后才能开始数据读写操作,因此频繁的Leader选举会导致数据读写性能显著降低。可以通过调整心跳周期(Heatbeat Interval)和选举超时时间(Election Timeout),来降低Leader选举的可能性。

心跳周期是控制Leader以何种频度向Follower发起心跳通知。心跳通知除表明Leader活跃状态之外,还带有待写入数据信息,Follower依据心跳信息进行数据写入,默认心跳周期是100ms。选举超时时间定义了当Follower多久没有收到Leader心跳,则重新发起选举,该参数的默认设置是1000ms。

如果etcd集群的不同实例部署在延迟较低的相同数据中心,通常使用默认配置即可。如果不同实例部署在多数据中心或者网络延迟较高的集群环境,则需要对心跳周期和选举超时时间进行调整。建议心跳周期参数推荐设置为接近etcd多个成员之间平均数据往返周期的最大值,一般是平均RTT的0.55-1.5倍。如果心跳周期设置得过低,etcd会发送很多不必要的心跳信息,从而增加CPU和网络的负担。如果设置得过高,则会导致选举频繁超时。选举超时时间也需要根据etcd成员之间的平均RTT时间来设置。选举超时时间最少设置为etcd成员之间RTT时间的10倍,以便对网络波动。

心跳间隔和选举超时时间的值必须对同一个etcd集群的所有节点都生效,如果各个节点配置不同,就会导致集群成员之间协商结果不可预知而不稳定。

etcd 高可用解决方案

etcd-operator: coreos开源的,基于kubernetes CRD完成etcd集群配置。Archived

https://github.com/coreos/etcd-operator

Etcd statefulset Helm chart: Bitnami(powered by vmware)

https://bitnami.com/stack/etcd/helm

https://github.com/bitnami/charts/blob/master/bitnami/etcd

kubenetes 如何使用etcd

api server 操作etcd 的参数

1
2
3
4
5
6
7
8
9
10
11
# API server 启动脚本中指定etcd servers集群
spec:
containers:
- command:
- kube-apiserver
- --advertise-address=192.168.34.2
- --enable-bootstrap-token-auth=true
- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
- --etcd-servers=https://127.0.0.1:2379

Kubernets对象在etcd中的存储路径

1
2
3
4
5
6
ectl get --prefix --keys-only /
/registry/namespaces/calico-apiserver
/registry/networkpolicies/calico-apiserver/allow-apiserver
/registry/operator.tigera.io/tigerastatuses/apiserver
/registry/pods/calico-apiserver/calico-apiserver-77dffffcdf-g2tcx
/registry/pods/default/toolbox-68f79dd5f8-4664n

etcd在集群中所处的位置

image-20231004081814873

刘小恺(Kyle) wechat
如有疑问可联系博主