ETCD 出现高碎片率事件解析

集群频繁触发 etcdDatabaseHighFragmentationRatio 告警, PrometheusRule 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- alert: etcdDatabaseHighFragmentationRatio
annotations:
description: 'etcd cluster "{{ $labels.job }}": database size in use on instance
{{ $labels.instance }} is {{ $value | humanizePercentage }} of the actual
allocated disk space, please run defragmentation (e.g. etcdctl defrag) to
retrieve the unused fragmented disk space.'
runbook_url: https://etcd.io/docs/v3.5/op-guide/maintenance/#defragmentation
summary: etcd database size in use is less than 50% of the actual allocated
storage.
expr: (last_over_time(etcd_mvcc_db_total_size_in_use_in_bytes{job=~".*etcd.*"}[5m])
/ last_over_time(etcd_mvcc_db_total_size_in_bytes{job=~".*etcd.*"}[5m])) <
0.5 and etcd_mvcc_db_total_size_in_use_in_bytes{job=~".*etcd.*"} > 104857600
for: 10m
labels:
severity: warning

相关指标:

  1. etcd_server_quota_backend_bytes:当前后端存储配额大小(字节),默认为 2GB
  2. etcd_mvcc_db_total_size_in_bytes:物理分配的底层数据库总大小(字节),包含了数据(如 keyspace)和碎片,即 DB SIZE
  3. etcd_mvcc_db_total_size_in_use_in_bytes:逻辑上正在使用的底层数据库的总大小(以字节为单位),不包含碎片

也就是说 quota-backend-bytes 配置后,etcd_mvcc_db_total_size_in_bytes 的大小并不会根据这个值而变化,会变的是 etcd_server_quota_backend_bytes,etcd_mvcc_db_total_size_in_bytes 的值指的是 DB SIZE,可以通过以下方式获取 DB SIZE

1
2
ls -lrth ${etcd-data-dir}/member/snap
etcdctl endpoint status -w table

为什么会产生碎片?

  1. ETCD 支持多版本并发控制(MVCC),同时会精确记录其 keyspace 的历史
  2. 压缩操作是清除历史记录的唯一方法,通常用 –auto-compaction-mode 和 –auto-compaction-retention 来实现自动压缩
  3. 但压缩操作后的空闲空间并不会真正在文件系统中释放,而是会被 ETCD 标记为可使用的空闲空间,也就是说压缩操作后仍然会占用磁盘空间
  4. 要真正释放,就需要进行碎片整理,即 etcdctl defrag
1
etcdctl defrag

那么针对 etcdDatabaseHighFragmentationRatio 告警的触发,要怎么判断需不需要进行碎片清理?

  1. 通过 etcd_server_quota_backend_bytes 指标查看实际配额
  2. 通过指标 etcd_mvcc_db_total_size_in_bytes 或者命令检查 DB SIZE,看是否真的很大且接近于 quota-backend-bytes,如果不是则无需担心
  3. 如果是,获取每种资源的数量,查看是什么资源导致 DB Size 这么大,然后通过碎片清理尝试释放空间,如果释放后仍接近于 quota-backend-bytes,那么需要考虑增加配额
    1
    2
    etcdctl get /registry --prefix --keys-only | grep -v ^$ | awk -F '/'  '{ h[$3]++ } END {for (k in h) print h[k], k}' | sort -nr
    etcdctl defrag

节点根目录被打满导致的ETCD憨批修复记录

背景

事情发生在 UAT 环境的其中一台 Controller 节点,节点根目录被打满,同时 etcd 数据没有落盘到独立的磁盘中,导致 etcd 憨批,节点出现 notready

etcd-error-log

修复过程

参考了各种网络资料,最终形成如下修复手段:

  1. 移除 statis pod yaml,从而停止坏掉的 etcd pod
  2. 通过 etcdctl member remove 移除坏掉的 etcd 实例
  3. 备份数据目录并移除
  4. 通过 etcdctl member add 添加新实例,记录 etcdctl 输出的配置信息
  5. 通过裸起容器的方式,启动 etcd 容器,启动需要用到的参数,参考 statis pod yaml 和第 4 步输出的配置信息
  6. 启动后会与 leader 进行数据的同步,可以通过 etcdctl endpoint status -w table 查看状态
  7. 如果同步成功则可以停止 etcd 容器,将 statis pod yaml 放回对应的目录中,集群修复

具体的操作命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# stop issue etcd pod
mv /etc/kubernetes/manifests/etcd.yaml .

# init etcdctl command envs
export endpoints="https://10.82.69.10:2379,https://10.82.69.11:2379,https://10.82.69.12:2379,https://10.82.69.19:2379,https://10.66.10.83:2379"
export cacert="/etc/kubernetes/pki/etcd/ca.crt"
export cert="/etc/kubernetes/pki/etcd/peer.crt"
export key="/etc/kubernetes/pki/etcd/peer.key"

# sample: e member list -w table
alias e="etcdctl --endpoints $endpoints --cacert $cacert --cert $cert --key $key"

# or use this one
eval $(kubectl get nodes -owide|grep -E "etcd|control-plane" |awk '{printf "https://"$6":2379,"}'|awk '{gsub(",$","");print "export ETCDCTL_ENDPOINTS=\""$1"\""}') && export ETCDCTL_CACERT=/etc/kubernetes/ssl/etcd/ca.crt && export ETCDCTL_CERT=/etc/kubernetes/ssl/etcd/peer.crt && export ETCDCTL_KEY=/etc/kubernetes/ssl/etcd/peer.key

# remove issue etcd member
etcdctl member remove $issue_etcd_id

# delete etcd data
rm -rf /var/lib/etcd/*

# member add
etcdctl member add wcn-gduvm-mwdcm1 --peer-urls=https://10.82.69.10:2380

# start a temporary etcd pod to restore
nerdctl run -d --name restore_etcd \
-v /etc/kubernetes/ssl/etcd:/etc/kubernetes/ssl/etcd \
-v /var/lib/etcd:/var/lib/etcd \
--network=host \
-e ETCD_NAME="wcn-gduvm-mwdcm1" \
-e ETCD_INITIAL_CLUSTER="wcn-gduvm-mwdcm2=https://10.82.69.11:2380,wcn-gduvm-mwdcm1=https://10.82.69.10:2380,wcn-gduvm-mwdcm3=https://10.82.69.12:2380" \
-e ETCD_INITIAL_ADVERTISE_PEER_URLS="https://10.82.69.10:2380" \
-e ETCD_INITIAL_CLUSTER_STATE="existing" \
--entrypoint=etcd 10.82.49.238/quay.io/coreos/etcd:v3.5.6 --advertise-client-urls=https://10.82.69.10:2379 --auto-compaction-retention=8 --cert-file=/etc/kubernetes/ssl/etcd/server.crt --client-cert-auth=true --data-dir=/var/lib/etcd --election-timeout=5000 --experimental-initial-corrupt-check=true --experimental-watch-progress-notify-interval=5s --heartbeat-interval=250 --key-file=/etc/kubernetes/ssl/etcd/server.key --listen-client-urls=https://127.0.0.1:2379,https://10.82.69.10:2379 --listen-metrics-urls=http://127.0.0.1:2381 --listen-peer-urls=https://10.82.69.10:2380 --metrics=basic --peer-cert-file=/etc/kubernetes/ssl/etcd/peer.crt --peer-client-cert-auth=true --peer-key-file=/etc/kubernetes/ssl/etcd/peer.key --peer-trusted-ca-file=/etc/kubernetes/ssl/etcd/ca.crt --snapshot-count=10000 --trusted-ca-file=/etc/kubernetes/ssl/etcd/ca.crt

# wait for the etcd pod running, if use kubectl and etcdctl to see that both node and member are restored, we can stop it
nerdctl stop restore_etcd

# start etcd pod
mv ./etcd.yaml /etc/kubernetes/manifests/etcd.yaml

etcd leader选举

etcd 是基于 raft 算法进行选举,而 raft 是一种管理日志一致性的协议,将系统中的角色分为三个

  1. leader: 接受客户端的请求,并向 follower 发送同步请求日志
  2. follower: 接收 leader 同步的日志
  3. candidate: 候选者角色,在选举过程中发挥作用

leader 选举

  1. raft 是通过心跳机制来触发 leader 的选举,每一个实例(例如 etcd pod)启动后都会初始化为一个 follower,leader 则会周期性的向所有 follower 发送心跳包,在 etcd 的编排中就能看到相关的参数

etcd编排

  1. 如果 follower 如果在选举超时时间内没有收到 leader 的心跳包,就会等待一个随机的时间,然后发起 leader 选举。每个 follower 都有一个时钟,这个时钟是一个随机的值,集群中谁的时钟先跑完,那么就由谁来发起 leader 选举

  2. 该 follower 会将当前的任期(term) + 1 然后转化为 candidate,先给自己投票然后向集群中的其他 follower 发送 RequestVote RPC

那么最终的结果会有三种:

  1. 自己赢得了最多的票数,成为 leader
  2. 收到了 leader 的消息,表示已经有其他服务抢先成为了 leader
  3. 没有服务获得最高票数,即选举失败,会等待选举时间超时后进行下一次选举

在 raft 协议中,所有的日志条目都只会是 leader 往 follower 写入,且 leader 的日志只增不减,所以能被选举成为 leader 的节点,一定包含了所有已经提交的日志条目