背景
线下有一套kubernetes集群,用于各种测试,某次在创建了一堆开源服务后,发现太乱了,于是想通过之前的某个版本的快照进行恢复,在快照恢复之后出现了比较奇怪的现象,同样一个kubectl get $resource的命令后,出现的返回均不一样,比如这样:
根据现象来看,像是kubectl 调用api-server 的时候出现了数据不一致的情况,由于这个环境的服务均是集群化/HA 部署的,所以根据这个现象,猜测可能存在以下两种情况:
- api-server 的informer机制存在问题,导致了两台api-server之间获取的数据不一致。
- etcd的集群出现了脑裂,即数据不一致,导致了读取到的数据不一致。
由于在固有印象里,etcd是基于raft算法进行强一致校验的,理论上不可能存在脑裂,所以先从api-server开始查,但是在将api-server 的高可用节点逐台关闭后,发现在api-server 单点工作的时候,依然会出现上述情况,所以问题只能是,etcd脑裂了。。。
判断故障点
假设我们发现了etcd出现了脑裂,那么该如何定位是哪个节点的数据不一致呢?如果在数据量较大的情况下,可以根据数据大小进行比对(我的环境数据量较小,从DB SIZE这看不出来)
[root@shcxgvm050 ~]# etcdctl endpoint status -w table
+---------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+---------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| https://10.184.4.238:2379 | 87dd5c835d9ec424 | 3.4.10 | 838 MB | false | false | 48 | 2006274 | 2006274 | |
| https://10.184.4.239:2379 | 87155934c1e2784c | 3.4.10 | 838 MB | true | false | 48 | 2006274 | 2006274 | |
| https://10.184.4.240:2379 | f90da8229f4e3c0f | 3.4.10 | 838 MB | false | false | 48 | 2006274 | 2006274 | |
+---------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
假设可能脑裂的数据量较小,从DB SIZE 层面看不出来,那么可以基于数据本身来进行查看,kubernetes访问etcd的时候,所有的读操作访问的都是follower,只有写操作会经过leader,那么可以分别访问每个etcd的endpoint,查询相同的数据,找到follower里哪个和leader的数据不一致。
[root@shcxgvm050 ~]# \etcdctl --endpoints=https://10.184.4.238:2379 --cacert=/etc/kubernetes/ssl/ca.pem --cert=/etc/kubernetes/ssl/etcd.pem --key=/etc/kubernetes/ssl/etcd-key.pem get /registry/leases/kube-node-lease --prefix -w=json | jq .
{
"header": {
"cluster_id": 16582195919756743000,
"member_id": 7227708511869891000,
"revision": 1240542,
"raft_term": 36
},
"kvs": [
{
"key": "L3JlZ2lzdHJ5L2xlYXNlcy9rdWJlLW5vZGUtbGVhc2UvMTAuMTg0LjQuMjM4",
"create_revision": 1240282,
"mod_revision": 1240410,
"version": 2,
"value": "azhzAAofChZjb29yZGluYXRpb24uazhzLmlvL3YxEgVMZWFzZRK6AwqXAwoMMTAuMTg0LjQuMjM4EgAaD2t1YmUtbm9kZS1sZWFzZSIAKiQyMjdhMmRhZi1mY2ViLTRiNTgtYTc3NC1kMTM2OThiYWY0YjkyADgAQggI6KbgoAYQAGo+CgROb2RlGgwxMC4xODQuNC4yMzgiJDIwZmFkYmQ1LTQyNmEtNGI4My1hZDgzLWI5NzExN2JmMGYzYioCdjF6AIoB+gEKB2t1YmVsZXQSBlVwZGF0ZRoWY29vcmRpbmF0aW9uLms4cy5pby92MSIICOim4KAGEAAyCEZpZWxkc1YxOrgBCrUBeyJmOm1ldGFkYXRhIjp7ImY6b3duZXJSZWZlcmVuY2VzIjp7Ii4iOnt9LCJrOntcInVpZFwiOlwiMjBmYWRiZDUtNDI2YS00YjgzLWFkODMtYjk3MTE3YmYwZjNiXCJ9Ijp7fX19LCJmOnNwZWMiOnsiZjpob2xkZXJJZGVudGl0eSI6e30sImY6bGVhc2VEdXJhdGlvblNlY29uZHMiOnt9LCJmOnJlbmV3VGltZSI6e319fUIAEh4KDDEwLjE4NC40LjIzOBAoIgwIvKngoAYQ5tXGggMaACIA"
},
{
"key": "L3JlZ2lzdHJ5L2xlYXNlcy9rdWJlLW5vZGUtbGVhc2UvMTAuMTg0LjQuMjM5",
"create_revision": 170697,
"mod_revision": 1237244,
"version": 65537,
"value": "azhzAAofChZjb29yZGluYXRpb24uazhzLmlvL3YxEgVMZWFzZRK6AwqXAwoMMTAuMTg0LjQuMjM5EgAaD2t1YmUtbm9kZS1sZWFzZSIAKiRjZGYzMDg2Mi05NzY0LTRkZTEtYWQyMy0xZGNkMjk3ODhlMGQyADgAQggIoNCSnwYQAGo+CgROb2RlGgwxMC4xODQuNC4yMzkiJDg3NGJkYWFlLTM0MjMtNGE3Zi1iNTYwLTA4YjFjZDcxMzYyZioCdjF6AIoB+gEKB2t1YmVsZXQSBlVwZGF0ZRoWY29vcmRpbmF0aW9uLms4cy5pby92MSIICKDQkp8GEAAyCEZpZWxkc1YxOrgBCrUBeyJmOm1ldGFkYXRhIjp7ImY6b3duZXJSZWZlcmVuY2VzIjp7Ii4iOnt9LCJrOntcInVpZFwiOlwiODc0YmRhYWUtMzQyMy00YTdmLWI1NjAtMDhiMWNkNzEzNjJmXCJ9Ijp7fX19LCJmOnNwZWMiOnsiZjpob2xkZXJJZGVudGl0eSI6e30sImY6bGVhc2VEdXJhdGlvblNlY29uZHMiOnt9LCJmOnJlbmV3VGltZSI6e319fUIAEh4KDDEwLjE4NC40LjIzORAoIgwIqcG7nwYQop+0xwMaACIA"
},
{
"key": "L3JlZ2lzdHJ5L2xlYXNlcy9rdWJlLW5vZGUtbGVhc2UvMTAuMTg0LjQuMjQw",
"create_revision": 170206,
"mod_revision": 1237247,
"version": 65587,
"value": "azhzAAofChZjb29yZGluYXRpb24uazhzLmlvL3YxEgVMZWFzZRK6AwqXAwoMMTAuMTg0LjQuMjQwEgAaD2t1YmUtbm9kZS1sZWFzZSIAKiQ2MTQxMTdjMC03ZGVlLTQxOGUtYjc1Zi00Y2RlZjkyOWQ5NzAyADgAQggIyM2SnwYQAGo+CgROb2RlGgwxMC4xODQuNC4yNDAiJDQ2Zjk0MTU5LTY2ZjEtNDg2My1hMjI4LWM0OTc4NjM0Zjc0ZSoCdjF6AIoB+gEKB2t1YmVsZXQSBlVwZGF0ZRoWY29vcmRpbmF0aW9uLms4cy5pby92MSIICMjNkp8GEAAyCEZpZWxkc1YxOrgBCrUBeyJmOm1ldGFkYXRhIjp7ImY6b3duZXJSZWZlcmVuY2VzIjp7Ii4iOnt9LCJrOntcInVpZFwiOlwiNDZmOTQxNTktNjZmMS00ODYzLWEyMjgtYzQ5Nzg2MzRmNzRlXCJ9Ijp7fX19LCJmOnNwZWMiOnsiZjpob2xkZXJJZGVudGl0eSI6e30sImY6bGVhc2VEdXJhdGlvblNlY29uZHMiOnt9LCJmOnJlbmV3VGltZSI6e319fUIAEh4KDDEwLjE4NC40LjI0MBAoIgwIscG7nwYQ6PnOkAIaACIA"
}
],
"count": 3
}
我观察的是其中的mod_revision, mod_revision作用域为 key, 等于修改这个 key 时集群的 Revision, 只要这个 key 更新都会自增,也就是说,可以从这个字段判断value是否和其他endpoint一致,所以,从这就能找到,有问题的节点是etcd-01。
恢复方法
方式1:删除故障member, 然后重新添加该节点
算是比较粗暴的修复方法,就是将当前集群的问题member 从集群内剔除,然后删除节点信息,然后将该节点重新加入到集群,依靠etcd 自身的同步机制,将数据同步。但粗暴不代表无效,我是基于这个方法进行修复的。具体操作步骤如下:
查看当前member 清单
- 如果发现问题节点是leader节点,则需要先手动更换leader
etcdctl member list -w table
+------------------+---------+--------+---------------------------+---------------------------+------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
+------------------+---------+--------+---------------------------+---------------------------+------------+
| 87155934c1e2784c | started | etcd02 | https://10.184.4.239:2380 | https://10.184.4.239:2379 | false |
| 87dd5c835d9ec424 | started | etcd01 | https://10.184.4.238:2380 | https://10.184.4.238:2379 | false |
| f90da8229f4e3c0f | started | etcd03 | https://10.184.4.240:2380 | https://10.184.4.240:2379 | false |
+------------------+---------+--------+---------------------------+---------------------------+------------+
# 将当前leader指向member 87155934c1e2784c
etcdctl move-leader 87155934c1e2784c --endpoints 127.0.0.1:2379
删除异常member节点
# 删除member时需要注意,etcd的服务不要停止
etcdctl member remove 87dd5c835d9ec424
- 当前整个etcd 的cluster内只存在了2个member, 此时要注意,2个节点的etcd是存在脑裂风险的,所以在这段时间内,需要尽量避免对etcd进行写操作。
etcdctl member list -w table
+------------------+---------+--------+---------------------------+---------------------------+------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
+------------------+---------+--------+---------------------------+---------------------------+------------+
| 87155934c1e2784c | started | etcd02 | https://10.184.4.239:2380 | https://10.184.4.239:2379 | false |
| f90da8229f4e3c0f | started | etcd03 | https://10.184.4.240:2380 | https://10.184.4.240:2379 | false |
+------------------+---------+--------+---------------------------+---------------------------+------------+
删除故障member的数据
由于剔除的节点是etcd01, 此后需要将etcd01作为新节点重新加入到集群内,所以需要删除etcd01内的数据,如有需要,可以提前进行快照备份。
# 根据配置文件找到存放wal和snapshot的目录,删除里面的member 目录
systemctl stop etcd
rm -rf /opt/etcd/member
重新加入节点
将该节点重新加入到集群内,加入后状态为unstart
etcdctl member add etcd01 --peer-urls="https://10.184.4.238:2380"
etcdctl member list -w table
+------------------+---------+--------+---------------------------+---------------------------+------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
+------------------+---------+--------+---------------------------+---------------------------+------------+
| 87155934c1e2784c | started | etcd02 | https://10.184.4.239:2380 | https://10.184.4.239:2379 | false |
| 87dd5c835d9ec535 | unstart | etcd01 | https://10.184.4.238:2380 | https://10.184.4.238:2379 | false |
| f90da8229f4e3c0f | started | etcd03 | https://10.184.4.240:2380 | https://10.184.4.240:2379 | false |
+------------------+---------+--------+---------------------------+---------------------------+------------+
修改"新节点"的启动配置文件
# 将etcd启动配置文件内的--initial-cluster-state=new修改为--initial-cluster-state=existing
--initial-cluster-state=existing
systemctl start etcd
- 查看服务,发现节点正常加入随后检查之前冲突点,确保数据一致
etcdctl member list -w table
+------------------+---------+--------+---------------------------+---------------------------+------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
+------------------+---------+--------+---------------------------+---------------------------+------------+
| 87155934c1e2784c | started | etcd02 | https://10.184.4.239:2380 | https://10.184.4.239:2379 | false |
| 87dd5c835d9ec535 | started | etcd01 | https://10.184.4.238:2380 | https://10.184.4.238:2379 | false |
| f90da8229f4e3c0f | started | etcd03 | https://10.184.4.240:2380 | https://10.184.4.240:2379 | false |
+------------------+---------+--------+---------------------------+---------------------------+------------+
方式2:数据恢复
方式1的方法需要重新新增节点,如果存在数据量较大,可以采用备份恢复的方式。
从集群的其他节点获取当前快照
# endpoint指向目前正常工作的节点
etcdctl --endpoints=https://10.184.4.240:2380 snapshot save snapshot.db
在故障节点将数据恢复
# 删除故障节点的数据目录,并恢复数据
rm -rf /opt/etcd
etcdctl snapshot restore snapshot.db --name etcd01 --initial-cluster etcd01=https://10.184.4.238:2379,etcd02=https://10.184.4.239:2379,etcd03=https://10.184.4.240:2379 --initial-cluster-token etcd-cluster --initial-advertise-peer-urls https://10.184.4.238:2380
修改配置,并重启etcd
# 将etcd启动配置文件内的--initial-cluster-state=new修改为--initial-cluster-state=existing
--initial-cluster-state=existing
systemctl start etcd
相比方法二来说,方法一的步骤比较简单,但实际操作的时候,可能会存在做完快照的同时有用户数据写入的情况,从而再一次导致Revision不一致,此外,之前提到奇数节点一旦发生故障,会将集群变成偶数节点,从而在短期内产生脑裂的风险,所以在发现故障后,要赶快进行修复。
发散
etcd为啥会数据不一致?
理论上etcd是强一致性的,但其实只能说raft算法是强一致性的,在算法的应用过程中也可能存在一定的bug, 具体bug原理可以参考etcd3Bug,这边简单阐述下可能触发bug的几个操作。
- etcd开启了鉴权
- 修改了etcd的核心配置,如压缩,心跳包等配置后发生了重启等
选举方法?
初始启动时,节点处于 follower 状态并被设定一个 election timeout,如果在这一时间周期内没有收到来自leader 的 heartbeat,节点将发起选举∶将自己切换为candidate 之后,向集群中其它 follower 节点发送请求,询问其是否选举自己成为Leader。
当收到来自集群中过半数节点的接受投票后,节点即成为 leader,开始接收保存client 的数据并向其它的 follower 节点同步日志。
如果没有达成一致,则candidate 随机选择一个等待间隔(150ms~300ms)再次发起投票,得到集群中半数以上follower 接受的 candidate 将成为 leader。
换句话说,就是leader会定时对所有follower发送心跳包来确定自己的leader地址,这也导致了leader就重启之后,是有概率重新做leader的。