最近在 Ceph 集群升级时发现一个奇怪的现象,服务端升级正常完成之后,所有的内核态 rbd 客户端的 IO 都卡住了,简单分析原因如下。
测试环境
CentOS 7.4
Luminous 12.2.12 ceph 服务端
3.10.0-693 内核态 ceph 客户端
复现手段
OSD 节点升级方式如下:
- 给待升级节点 OSD 设置
noout
标签(注意不是通过 ceph osd set noout 这种全局标签手段); - 重启待升级节点所有 OSD;
- 清除
noout
标签;
使用如下的命令行进行模拟,最后一个 OSD 放在最后启动是为了模拟 OSD up 稍晚于 rm-noout 操作的情况:
1 | # ceph osd add-noout 0 1 2; sleep 3; for i in 0 1 2; do systemctl stop ceph-osd@$i; done; sleep 3; for i in 0 1; do systemctl start ceph-osd@$i; done; sleep 3; ceph osd rm-noout 0 1 2; sleep 3; for i in 2; do systemctl start ceph-osd@$i; done; |
信息采集
1 | [Mar17 08:57] libceph: osd0 down |
从内核 dmesg 打印来看,最后一次的 OSD 全部 down 非常值得怀疑,从升级过程来看,并不存在 OSD 重复 stop 的情况。
1 | # cat /sys/kernel/debug/ceph/<fsid>/osdmap |
升级之后,内核客户端拿到的 osdmap 中 OSD.0 和 OSD.1 是 down 的,显然与服务端真实状态对不上,不过和 dmesg 的日志信息完全吻合。
故障分析
由于该问题属于必现故障,因此可以排除机器、网络等外部因素,只需要从代码着手即可。
在 L 版本中,osdmap 中 OSD 的状态位从 8 bit 切换成了 32 bit,但是 3.10.0-693 内核态客户端由于年代久远,仍然只能处理 8 bit 状态位,显然在 OSD 状态的处理上,客户端与 L 版本服务端的配合非常值得怀疑。
通过分析服务端代码,发现 add/rm-noout 操作都会设置 OSD 状态,而且是设置在非低 8 位,与老版本客户端(不支持 osdmap OSD 状态位 32 bit)交互时,会截取低 8 位的状态发送给客户端,显然,这种情况下,客户端会收到全 0 的 8 位状态位。此外,需要注意的是,OSD 状态位的设置或取消是异或操作,而不是简单的赋值操作。
下面结合内核态客户端代码与服务端的升级操作,来分析一下客户端的行为。
1 | // linux-3.10.0-693/net/ceph/osdmap.c |
- 初始时刻,服务端和客户端都处于稳态,所有 OSD 都是 up 状态;
- 服务端升级开始,通过 ceph osd add-noout 命令为该节点 OSD 设置 nouout 标签,生成新 osdmap,并发送给客户端仅截取了低 8 bit 的版本;
- 客户端收到新 osdmap,8 bit OSD 状态位全 0;
- 客户端多此一举将全 0 解释成
CEPH_OSD_UP
,并与原来的状态进行异或操作,在客户端侧 OSD 被置成 down,即错误的置成了 down; - 服务端重启 OSD 进程,OSD down,生成新 osdmap,且低 8 位
CEPH_OSD_UP
置位; - 客户端收到新 osdmap,由于在第 4 步中,客户端 OSD 状态已置为 down,异或操作导致 OSD 置成 up,即错误的置成了 up;
- 服务端继续步骤 5 的操作,OSD 进程启动,OSD up,生成新 osdmap;
- 客户端收到新 osdmap,需要特别注意的是新 up 的 OSD 并不是通过上面描述的异或操作置上的 up 状态,而是走的上面代码中的后一个
while
循环,也就是说在客户端看来,OSD 状态仍然保持 up; - 服务端升级结束,通过 ceph osd rm-noout 命令为该节点 OSD 取消 nouout 标签,生成新 osdmap 并发送给客户端仅截取了低 8 bit 的版本;
- 客户端收到新 osdmap,8 bit OSD 状态位全 0;
- 客户端多此一举将全 0 解释成
CEPH_OSD_UP
,并与原来的状态进行异或操作,OSD 状态从第 6 步的 up 状态被置成 down,即错误的置成了 down; - 最终,客户端得到的 OSD 状态就是
(exists)
而不是(exists, up)
。
当然,这里忽略了 rm-noout
操作早于某些 OSD 进程启动完成这种情况,即我们从客户端 osdmap 看到的部分 OSD up,部分 OSD down 的情况,但分析过程类似,结果都是可解释的。
解决方案
本质上,这是代码的 bug,规避也很容易,一种就是服务端升级完成之后再将全部 OSD 进行重启,还一种就是服务端升级时不进行 add/rm-noout 操作。
从代码上来说,服务端和客户端都可以进行修改,一是服务端不编码低 8 位为 0 的 OSD 状态,二是客户端停止将低 8 为 0 的 OSD 状态解析成 CEPH_OSD_UP
,当然,服务端的修改会更简单一些。
参考资料
libceph: osd_state is 32 bits wide in luminous
https://github.com/torvalds/linux/commit/0bb05da2ec57163b7a25efef001ed8f52b18b070
osd/OSDMap: make osd_state 32 bits wide
https://github.com/ceph/ceph/pull/15390
mon/OSDMonitor: remove zeroed new_state updates
https://github.com/ceph/ceph/pull/16518
osd/OSDMap: stop encoding osd_state with >8 bits wide states only for old client