Ceph monmap消息编解码过程

本文源码基于luminous-12.2.5版本分析

最近在分析H版本librbd client无法兼容L版本Ceph集群的问题,提前说明下最终结论是H版本被之前的同时修改过才导致的不兼容,官方版本应该是兼容的。这篇文章正是问题分析过程的一次整理总结。

需求描述

需求是想要在同一个OpenStack环境中同时使用H、L版本两个Ceph集群做云主机系统盘、云硬盘存储系统,因此计算节点(也即librbd client节点)就可能分为H、L两个版本,众所周知,升级librbd动态库,需要重启云主机才能生效(除非对librbd做过较大的改动),重启所有云主机这个操作用户显然是不能接受的(当然也可以执行热迁移操作,但大批量的热迁移也存在耗时长、风险大的问题,况且还有部分云主机不支持热迁移),因此如果要上线L版本集群就必须做到兼容H版本client,这样才能保证L版本集群的卷能被当前正使用H版本client的云主机挂载、读写。H版本是之前遗留的,不能升级到L版本(主要考虑2个问题,1是风险高,二是代价大。实际上还有一个小问题就是H版本client不兼容L版本server,这个正是我本次需要解决的问题)。

问题描述

有两个问题:
1. 不修改server的crush参数情况下,客户端执行ceph -s、rbd ls等命令报错:
2. 通过ceph osd crush tunables hammer命令修改crush参数之后,执行命令直接抛异常,原因是decode monmap的created字段时发生越界异常,位置是:

本次重点分析第二个问题,bt查看调用栈(调试的是rbd ls命令):

是在接收到服务端monmap消息后decode过程中抛的异常,具体是在decode created字段出错的。

单步调试发现,在此之前last_changed字段decode出来就是空的,时间都是0,因此怀疑是更早之前就发生错误了,调试后发现最初的错误是在decode mon_addr时就发生了。

通过调试服务端ceph-mon encode过程,发现mon_addr和last_changed、created字段都是被添加到monmap消息体内的,并且通过调试L版本client,这几个字段也都能正常decode出来,因此确认是客户端问题或者服务端发送的monmap消息客户端不兼容。但看了下L版本服务端encode代码,当前版本是v5,并且最低兼容v3版本,而H版本decode是v3版本,也就是说社区代码在设计上兼容性应该是没问题的。因此需要继续分析问题出在哪里。

仔细对比了mon_addr的decode和encode流程(主要是H版本和L版本的差异),发现L版本encode过程检查连接的features,并走了不同的encode过程。因此问题聚焦在了客户端和服务端协商features过程上。

也就是features的第59位是怎么设置上的(1<<59),看了下H版本的代码,发现了这个feature:

按道理在CEPH_FEATURE_HAMMER_0_94_4(1<<55)之后应该不会再有其他feature了才对,但这个CEPH_OSD_PARTIAL_RECOVERY为啥被加入了?去看了下官方代码,发现没有这个feature,然后git log看了下我们的代码仓库,果然是我们自己人加上的。至此问题原因已经清晰。

L版本中用CEPH_FEATURE_MSG_ADDR2(1<<59)来标记是否为新版本消息,这一位是0表示是老版本(未设置该bit表示老版本),但是H版本里面正好用到了这一位CEPH_OSD_PARTIAL_RECOVERY(1<<59),导致L版本服务端检测失败,误认为是新版本客户端,使用了新的编码方式对monmap中的monitor地址信息进行编码,导致客户端无法解码。

改动方案:

由于client端升级困难,因此对L版本服务端进行修改,使其兼容H版本client,增加对其他bit位的检查,当发现client是H版本后,就走老的encode流程,修改之后客户端可以正常使用(包括基本命令行和qemu启动云主机)。

 

此次问题分析的难点主要是:

  1. 不熟悉ceph源码,尤其是消息编解码过程
  2. 编译的第一个环境没有去掉编译优化(-O2),增加debug等级(-g),改成(-O0 -g3 -gdwarf-4)修改之后调试起来就很方便了,https://www-zeuthen.desy.de/unix/unixguide/infohtml/gdb/Inline-Functions.html
  3. 确定服务端是否把monitor地址信息编码到消息体过程被wireshark误导了一段时间

使用tcpdump+wireshark解析Ceph网络包

记得之前在通读docs.ceph.com上的文档时(http://docs.ceph.com/docs/luminous/dev/wireshark/),有提到过wireshark支持对Ceph网络数据包进行解析,于是试着用tcpdump抓了monitor和client的数据包,然后导入到wireshark进行解析,对H版本来说确实好用,但对L版本来说,monmap解析貌似支持不太好,monitor address地址解析不出来,估计也是只支持低版本的编码协议。参考:https://www.wireshark.org/docs/dfref/c/ceph.html

tcpdump命令(在client节点执行数据包比较少,服务端对接的客户端比较多因此数据包也比较多,当然也可以加入更多的过滤条件来精确抓包):tcpdump -i eth0 host 192.168.0.2 and port 6789 -w ceph.cap

之后把抓到的ceph.cap数据导入到windows系统的wireshark软件中,我下载的是2.6.2版本(官网有2.6.4版本,但是下载困难,就在国内找了软件站下载了2.6.2的)。导入之后就可以自动分析出结果了。

L版本抓包结果(monmap包解析不太好,认为包有问题“malformed packet”,monitor地址信息解析失败,跟我遇到的问题一样,因此误导了我2天,我一直认为服务端就是没有把地址编码进来,客户端才解析不出来,最后在服务端和客户端分别单步调试才发现是编码进去了的):

 

H版本数据包解析就比较完美了:

Mon Map:

OSD Map:

 

Mon Map编解码过程分析

关键数据结构都定义在src\include\buffer.h中,主要包括:

  • buffer::ptr
    •  _raw:保存实际的编码消息数据
    • buffer::ptr::iterator :遍历ptr的迭代器类,提供各种函数用来寻址ptr中的数据
  • buffer::list
    • _buffers:保存ptr的list,也即保存多条编码数据
    • append_buffer:ptr类型,4K对齐,保存编码数据,,其append操作实际是append到ptr._raw,之后会把它push_back到_buffers中,push_back之前会通过ptr构造函数填充ptr的_raw、_off、_len等数据,也即把_raw中保存的数据的偏移量和长度也保存起来,解码时使用
    • buffer::list::iterator:遍历_buffers的迭代器,继承自buffer::list::iterator_impl,主要是对外提供接口,对内封装了iterator_impl的相关接口
    • buffer::list::iterator_impl:遍历_buffers的迭代器实际实现类,主要实现有advance、seek、copy等函数,用来从_buffers里取出数据,advance函数用来在copy函数执行过程中进行ptr内或_buffers的多个ptr前后跳转,有两种场景,一中是跳转还在当前ptr内(ptr内取数据),另外一种是_buffers list中一个ptr已经copy完(跨ptr取数据),需要跳转到下一个ptr对象继续copy。seek函数也是利用advance函数完成数据寻址操作。

以L版本代码为例,编解码代码如下:

encode调用栈:

decode调用栈:

 

接下来要说的是具体的encode和decode流程,以我调试的mon_addr为例:

encode

其他几个字段也是类似过程,就不做分析,差别就是编码的数据类型不一样,比如string、struct等,string属于基础数据类型,encode.h有对应的encode函数,struct数据类型则需要在对应的struct结构体定义其自己的encode函数,如entity_addr_t::encode (src\msg\msg_types.h)。

decode过程也是类似,根据decode的数据类型,找到对应的decode函数,按段解码即可。数据类型是预先定义好的,如decode(mon_addr, p),mon_addr的类型是已知的,因此其decode函数也可以找到(一般都是跟encode放在一起),只不过由于encode.h中有很多的宏,不好找到源码而已,配合gdb单步调试应该容易很多(记得在do_cmake.sh里的cmake命令那行加上-O0 -g3 -gdwarf-4这几个CXX_FLAGS编译选项: -DCMAKE_CXX_FLAGS="-O0 -g3 -gdwarf-4" -DCMAKE_BUILD_TYPE=Debug)。一般是先解码出消息体长度,然后再逐条解码。总之一切过程都是预先定义好的,一旦收到的消息内容与预设的解码方案不匹配,就会导致各种错误。这就是编解码协议存在的原因。(话说这也是我第一次接触编解码协议,看完这些流程还有点小激动)。

encode是把数据存入消息体,decode是从消息体取出数据,最底层的编码解码都是按字节完成的,编码时会强转为char *类型,底层解码也不区分数据类型,由上层使用方负责进行转换,对应的结构体都定义在src\include\buffer.h中,相关的函数实现在src\common\buffer.cc中。

decode的核心是ceph::buffer::ptr::copy_out () (/mnt/ceph/src/common/buffer.cc:1035),最终都是由它把数据从消息体里取出来的。相关的调用栈可以参考上面贴出来的decode调用栈。

下面附上我在调试过程中手绘的协议字段表格:

L版本:

H版本:

 

任意整数以内的加减法口算练习题生成web服务源码及搭建过程

先上服务链接:http://aspirer.wang:3389/kousuan/7

链接的最后一个数字是可以修改的,改成几就是生成几以内的加减法练习题(比如上面的链接就是生成7以内的加减法口算题目,每次刷新都是新的题目不会重复)。

儿子上一年级经常有口算练习题,老师发的是一张习题纸,一共100道题,需要家长复印,但是存在三个小问题:一是复印出来的题目完全一样(有一次我发现儿子做题居然在参考前面一张。。。);二是打印不方便,必须得复印,有些家长是没有复印机的(复印还要带上原件,老师发下来的时候家长不一定能及时拿到原件);三是想自己提前给孩子出题练习其他更大数字的加减法不方便。

有了这个web站,后面还可以稍微修改下,支持生成乘法、除法的口算题。

整体部署架构:nginx+uwsgi+bottle,python编写的web后台服务。

部署过程参考资料:

  1. 使用bottle.py体验WSGI服务
  2. Nginx 部署Bottle + uwsgi

使用nginx的原因是我的博客就是用的它,跟博客部署在一起了,只是端口不同。

源码:

共3个文件:bottle.py是bottle wsgi框架,可以pip install安装,也可以直接拷贝源文件过来,非常方便。gen.py是为了方便后台测试用的,python执行它可以直接打印出题目。kousuan.py是给uwsgi用的,算是wsgi配置文件,当然里面也有一些其他代码,主要是生成html模板文件,以及配置wsgi router。

文件都很短,这里直接贴出来,不放github了:

 

html格式很简单,就没用专门的模板渲染框架如jinjia2等。

uwsgi配置文件:

nginx配置文件:

上面两个文件需要在对应的enabled目录下建立软链接,具体参考上面的部署过程参考资料(第二个链接)。

uwsgi和nginx的安装就不说了,apt就行。

部署好之后重启uwsgi和nginx服务就可以了。

 

监控:

为了及时发现web故障,用监控宝给3389 tcp端口和网站都加了监控,出现不可用会发短信和邮件通知。(我的博客用他们免费版用了这么久,也给人家打个广告)。

 

10.22更新:

  1.  修改了题目生成方法,大幅减少了包含0的题目的数量

 

其他:

生成题目时是暴力穷举符合条件的题目,其实可以根据每个题目的类型(加法或减法)以及生成的第一个数字,来限定第二个数字的随机范围,保证一次就可以生成符合条件的题目,可以很大程度减少计算量,不过对于这么小的程序和用户量的场景来说,这一点点计算量也就无所谓了。

Ceph iscsi方案及环境搭建

方案1:Ceph iscsi gateway及tcmu-runner部署流程

本部署流程文档基于Centos 7.5云主机验证。

TCMU原理介绍:Linux LIO 与 TCMU 用户空间透传 – Lixiubo_Liuyuan.pdf

环境准备

  1. Ceph L版本可用集群
  2. 至少两台Centos7.5版本主机(云主机或物理机)作为iscsi gateway节点,可以与Ceph集群public网络互通,或者其他发行版,但内核版本需要4.16以上

 

RHEL/CentOS 7.5; Linux kernel v4.16 or newer; or the Ceph iSCSI client test kernel

If not using a distro kernel that contains the required Ceph iSCSI patches, then Linux kernel v4.16 or newer or the ceph-client ceph-iscsi-test branch must be used.

Warning: ceph-iscsi-test is not for production use. It should only be used for proof of concept setups and testing. The kernel is only updated with Ceph iSCSI patches. General security and bug fixes from upstream are not applied.

 

部署过程

小提示:两台iscsi gateway节点,如果是使用的云主机,可以先只部署一台,部署ok之后做个自定义镜像,再用自定义镜像创建一台,修改主机名和/etc/hosts文件及iscsi gateway配置即可复制出新的gateway节点。

整体步骤:

  1. 配置两台Centos7.5云主机的主机名,修改/etc/hosts,保证两边可通过主机名互通,本次部署主机名分别为tcmu、tcmu2,ip分别为192.168.0.6、192.168.0.7
  2. 在gateway节点安装ceph包(使用ceph-deploy或手工安装),参考:http://docs.ceph.com/docs/master/start/quick-rbd/#install-ceph
  3. 下载tcmu-runner源码并编译安装
  4. 下载并安装ceph-iscsi-config(rbdtargetgw)、cephiscsicli(rbdtargetapi及gwcli命令),以及相关依赖包

安装ceph包

# 在gateway节点添加Ceph公司内部源,或者添加官方源:yum install centos-release-ceph-luminous.noarch -y
$ ceph-deploy install --release luminous tcmu  # 及tcmu2,ceph-deploy节点需要修改/etc/hosts文件
$ ceph-deploy admin tcmu   # 及tcmu2,或者手工copy /etc/ceph目录到gateway节点
# 验证ceph命令是否正常,如ceph -s

安装tcmu-runner

$ git clone https://github.com/open-iscsi/tcmu-runner.git
$ yum -y install epel-release python-pip python-rbd python-devel python-crypto # 安装依赖包
cd tcmu-runner
$ cmake -Dwith-glfs=false -Dwith-qcow=false -DSUPPORT_SYSTEMD=ON -DCMAKE_INSTALL_PREFIX=/usr
make make install
$ systemctl daemon-reload
$ systemctl enable tcmu-runner   # 如报错,则手工copy service:cp tcmu-runner.service /lib/systemd/system,参考:https://github.com/open-iscsi/tcmu-runner#running-tcmu-runner
$ systemctl start tcmu-runner

安装ceph-iscsi-config

$ git clone https://github.com/open-iscsi/targetcli-fb; git clone https://github.com/open-iscsi/configshell-fb; git clone https://github.com/open-iscsi/rtslib-fb  ## 下载依赖包,targetcli可不装,用ceph-iscsi-cli代替
$ python setup.py install  # 在上述3个依赖包目录下执行安装命令,如有提示缺少相关包则手工pip install安装
$ git clone https://github.com/ceph/ceph-iscsi-config.git
$ python setup.py install # 在ceph-iscsi-config目录执行安装命令,如有提示缺少相关包则手工pip install安装
$ systemctl daemon-reload
$ systemctl enable rbd-target-gw  # 如提示错误,则手工copy service文件到/lib/systemd/system
$ systemctl start rbd-target-gw  # 注意需要先创建配置文件,否则配置文件创建完需要重启该服务

在所有gateway节点创建配置文件:

[root@tcmu ~]# cat /etc/ceph/iscsi-gateway.cfg
[config]
# name of the *.conf file. A suitable conf file allowing access to the ceph
# cluster from the gateway node is required.
cluster_name = ceph
# Place a copy of the ceph cluster's admin keyring in the gateway's /etc/ceph
# drectory and reference the filename here
gateway_keyring = ceph.client.admin.keyring
# API settings.
# The api supports a number of options that allow you to tailor it to your
# local environment. If you want to run the api under https, you will need to
# create crt/key files that are compatible for each gateway node (i.e. not
# locked to a specific node). SSL crt and key files *must* be called
# iscsi-gateway.crt and iscsi-gateway.key and placed in /etc/ceph on *each*
# gateway node. With the SSL files in place, you can use api_secure = true
# to switch to https mode.
# To support the api, the bear minimum settings are;
api_secure = false
# Additional API configuration options are as follows (defaults shown);
api_user = admin
api_password = admin
api_port = 5001
trusted_ip_list = 192.168.0.6, 192.168.0.7

安装ceph-iscsi-cli

$ git clone https://github.com/ceph/ceph-iscsi-cli.git
cd ceph-iscsi-cli
$ python setup.py install --install-scripts=/usr/bin # 在ceph-iscsi-cli目录执行安装命令,如有提示缺少相关包则手工pip install安装
cp usr/lib/systemd/system/rbd-target-api.service /lib/systemd/system
$ systemctl daemon-reload
$ systemctl enable rbd-target-api
$ systemctl start rbd-target-api

iscsi target配置

通过gwcli命令即可配置,过程请参考:http://www.zphj1987.com/2018/04/11/ceph-ISCSI-GATEWAY/

iscsi initiator配置

为了简化部署,可使用gateway节点作为initiator节点来测试功能,相关软件的安装及配置,iscsiadm操作过程请参考:http://www.zphj1987.com/2018/04/11/ceph-ISCSI-GATEWAY/

参考:

  1. http://docs.ceph.com/docs/master/rbd/iscsi-requirements/
  2. http://docs.ceph.com/docs/master/rbd/iscsi-target-cli-manual-install/
  3. http://docs.ceph.com/docs/master/rbd/iscsi-target-cli/
  4. http://www.zphj1987.com/2018/04/11/ceph-ISCSI-GATEWAY/

 

方案2:TGT+rbd backing store部署流程

本次测试在两台Centos7.5云主机上完成,分别为tcmu(192.168.0.6)、tcmu2(192.168.0.7)。

前提:一个可以正常创建卷的ceph L版本集群,TGT节点可以访问该集群。

安装TGT服务(target端)

Centos 7.5添加epel源,可以直接安装scsi-target-utils包(yum –enablerepo=epel -y install scsi-target-utils),但是这个包里的TGT不支持rbd backing store,所以还是要手工编译。

Debian 9发行版可能有tgt-rbd包可以用,https://packages.debian.org/stretch/tgt, https://packages.debian.org/stretch/tgt-rbd, 安装这两个包应该就可以了。

 

编译TGT

  1. 下载源码:wget https://github.com/fujita/tgt/archive/v1.0.73.tar.gz
  2. tar xzf v1.0.73.tar.gz; cd tgt-1.0.73; make; make install (如果make提示xsltproc command not found,需要先执行yum install libxslt -y安装依赖包)
  3. 启动tgtd服务,可以手工启动:/usr/sbin/tgtd -f,或者用systemctl start tgtd(实际测试过程中发现tgt源码目录tgt-1.0.73/scripts下的tgtd.service配置文件并不好用,启动会卡住,我这里是先安装了epel源的scsi-target-utils,然后make install替换掉二进制文件,就可以正常启动带rbd backing store的tgtd服务了)
  4. 检查是否支持rbd backing store:tgtadm –lld iscsi –op show –mode system | grep rbd,输出rbd (bsoflags sync:direct)表示支持。

 

部署iscsi initiator(initiator端)

这里为了方便,直接在节点tcmu2上部署initiator软件(与target端共用一个节点)。

参考http://www.zphj1987.com/2018/04/11/ceph-ISCSI-GATEWAY/ Linux的客户端连接部分即可,主要是安装iscsi-initiator-utils客户端软件,以及多路径软件device-mapper-multipath。

 

创建target及lun(target端)

先创建一个rbd卷:

之后在tcmu、tcmu2上执行相同命令:

$ tgtadm --lld iscsi --mode target --op new --tid 1 --targetname iqn.2018-10.com.netease:cephtgt.target0  # 创建target
$ tgtadm --lld iscsi --mode logicalunit --op new --tid 1 --lun 1 --backing-store disk2 --bstype rbd  # 将rbd卷作为lun添加到target,注意lun id要从1开始,0被tgt使用
$ tgtadm --lld iscsi --op bind --mode target --tid 1 -I ALL  # 配置ACL授权,ALL表示所有节点均可访问该target,也可以用CIDR限制某个网段访问

 

initiator连接到target(initiator端)

在tcmu2节点上执行:

$ iscsiadm -m discovery -t st -p tcmu  # 发现target
$ iscsiadm -m discovery -t st -p tcmu2
$ iscsiadm -m node -T iqn.2018-10.com.netease:cephtgt.target0 -l -p tcmu # 登录target
$ iscsiadm -m node -T iqn.2018-10.com.netease:cephtgt.target0 -l -p tcmu2

即可连接到target,查看映射到本机的iscsi卷:

[root@tcmu2 ~]# lsblk
NAME     MAJ:MIN RM  SIZE RO TYPE  MOUNTPOINT
sda        8:0    0    1G  0 disk 
└─mpathc 252:0    0    1G  0 mpath
sdb        8:16   0    1G  0 disk 
└─mpathc 252:0    0    1G  0 mpath
[root@tcmu2 ~]# multipath -ll   # 查看多路径设备信息
mpathc (360000000000000000e00000000020001) dm-0 IET     ,VIRTUAL-DISK   
size=1.0G features='0' hwhandler='0' wp=rw
|-+- policy='service-time 0' prio=0 status=enabled
| - 10:0:0:1 sda 8:0  failed faulty running
-+- policy='service-time 0' prio=1 status=active
  `- 11:0:0:1 sdb 8:16 active ready running

使用/dev/mapper/mpathc设备即可访问rbd卷disk2,并且是多路径方式,tcmu节点上的tgtd服务异常或者网络异常、节点宕机,均可自动切换到tcmu2的tgtd进行正常的IO访问。

 

添加一个新的lun到target(target端)

先创建一个rbd卷:

在tcmu、tcmu2上分别执行:

$ tgtadm --lld iscsi --mode logicalunit --op new --tid 1 --lun 2 --backing-store disk3 --bstype rbd  # 将rbd卷作为lun添加到target

 

initiator端发现新的lun(initiator端)

 

$ iscsiadm -m session -R  # 重新扫描所有已建立的target连接,发现新的lun及lun大小变动等信息更新

查看映射的iscsi卷方法同上。

 

initiator端登出

$ iscsiadm -m node -T iqn.2018-10.com.netease:cephtgt.target0 --logout # 先确保卷未使用
$ iscsiadm -m node -T iqn.2018-10.com.netease:cephtgt.target0 -o delete
$ iscsiadm -m node  # 查看所有保存的target记录(可能未login)

 

target端清理

$ tgtadm --lld iscsi --mode logicalunit --op delete --tid 1 --lun 2  # 删除target id为1中的id为2的lun,先确保initiator端先logout
$ tgtadm --lld iscsi --mode target --op delete --tid 1  # 删除id为1的target
$ tgt-admin --show   # 查看所有target信息

配置持久化

通过配置文件实现target和initiator端重启后自动恢复相关配置和连接。

待补充

参考:

  1. http://www.zphj1987.com/2018/04/11/ceph-ISCSI-GATEWAY/
  2. https://jerry.red/300/%E5%88%9B%E5%BB%BA-iscsi-target-%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%92%8C-iscsi-initiator-%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%BF%9E%E6%8E%A5
  3. http://linux.vbird.org/linux_server/0460iscsi.php#initiator_exam

 

方案3:LIO+krbd/rbd-nbd实现iscsi target方案

LIO是内核态的iscsi target实现,支持多种backing store,但还不支持rbd,只能用krbd或者rbd-nbd方式先把rbd卷map成block device,之后再将映射的设备如/dev/rbdX或/dev/nbdX给LIO作为block backing store使用,并最终作为target导出给initiator使用。

TCMU是LIO的用户态实现,可直接支持rbd后端:Ceph iscsi gateway及tcmu-runner部署流程

LIO原理介绍:Linux LIO 与 TCMU 用户空间透传 – Lixiubo_Liuyuan.pdf

本次测试在两台Centos7.5云主机上完成,分别为tcmu(192.168.0.6)作为target节点、tcmu2(192.168.0.7)作为initiator节点。

前提:一个可以正常创建卷的ceph L版本集群,target节点可以访问该集群。

相关操作流程如下:

内核模块检查

一般内核都是默认加载的,如果没有加载可以手工modprobe加载上:

# lsmod | grep target_core_mod               
target_core_mod       340809  13 target_core_iblock,target_core_pscsi,iscsi_target_mod,target_core_file,target_core_user
crc_t10dif             12912  2 target_core_mod,sd_mod

 

安装targetcli客户端

# 在target节点(tcmu)上执行
$ git clone https://github.com/open-iscsi/targetcli-fb; git clone https://github.com/open-iscsi/configshell-fb; git clone https://github.com/open-iscsi/rtslib-fb  ## 下载包及依赖
$ python setup.py install  # 在上述3个目录下执行安装命令,targetcli-fb依赖后面两个包,如有提示缺少其他依赖包则手工pip install安装,或者从github上(pip命令需要安装python-pip rpm包)

 

进行target配置

首先要有一个rbd卷,这里已经create过一个vol3,1G大小,属于rbd pool。

然后在target(tcmu)节点上map这个rbd卷:

# 在target节点(tcmu)上执行
$ rbd map vol3  # 或者用rbd-nbd方式map也可以
$ rbd showmapped
id pool image snap device   
0  rbd  vol3  -    /dev/rbd0

之后将/dev/rbd0作为block设备给LIO使用。

在target(tcmu)节点执行targetcli命令:

$ targetcli
targetcli shell version 2.1.fb49
Copyright 2011-2013 by Datera, Inc and others.
For help on commands, type 'help'.
 /> cd /backstores/block
/backstores/blockls
o- block ...................................................................................................... [Storage Objects: 0]
/backstores/block> create name=rbd0 dev=/dev/rbd0 # 添加block后端
Created block storage object rbd0 using /dev/rbd0.
/backstores/blockls
o- block ...................................................................................................... [Storage Objects: 1]
 o- rbd0 .............................................................................. [/dev/rbd0 (1.0GiB) write-thru deactivated]
 o- alua ....................................................................................................... [ALUA Groups: 1]
 o- default_tg_pt_gp ........................................................................... [ALUA state: Active/optimized]/>
cd /iscsi/iscsi> create
Created target iqn.2003-01.org.linux-iscsi.tcmu.x8664:sn.546f452bcfe2.
Created TPG 1.
Global pref auto_add_default_portal=true
Created default portal listening on all IPs (0.0.0.0), port 3260.
/iscsils
o- iscsi .............................................................................................................. [Targets: 1]
 o- iqn.2003-01.org.linux-iscsi.tcmu.x8664:sn.546f452bcfe2 .............................................................. [TPGs: 1]
 o- tpg1 ................................................................................................. [no-gen-acls, no-auth]
 o- acls ............................................................................................................ [ACLs: 0]
 o- luns ............................................................................................................ [LUNs: 0]
 o- portals ...................................................................................................... [Portals: 1]
 o- 0.0.0.0:3260 ....................................................................................................... [OK]
/iscsicd iqn.2003-01.org.linux-iscsi.tcmu.x8664:sn.546f452bcfe2/tpg1/luns
/iscsi/iqn.20...fe2/tpg1/luns> create /backstores/block/rbd0
Created LUN 0.
/iscsi/iqn.20...fe2/tpg1/lunsls
o- luns .................................................................................................................. [LUNs: 1]
 o- lun0 .............................................................................. [block/rbd0 (/dev/rbd0) (default_tg_pt_gp)]
/iscsi/iqn.20...fe2/tpg1/lunscd ..
/iscsi/iqn.20...452bcfe2/tpg1set attribute authentication=0 demo_mode_write_protect=0 generate_node_acls=1 cache_dynamic_acls=1
Parameter demo_mode_write_protect is now '0'.
Parameter authentication is now '0'.
Parameter generate_node_acls is now '1'.
Parameter cache_dynamic_acls is now '1'./iscsi/iqn.20...452bcfe2/tpg1cd /
/> saveconfig
Last 10 configs saved in /etc/target/backup/.
Configuration saved to /etc/target/saveconfig.json/> exit

 

initiator操作

与其他target方式相同,都是用iscsiadm工具连接到target,具体参考TGT+rbd backing store部署流程这篇文档中的操作流程。

# 在tcmu2节点执行
$ iscsiadm -m discovery -t st -p tcmu
$ iscsiadm -m node -T iqn.2018-10.com.netease:cephtgt.target0 -l -p tcmu
$ lsblk
NAME   MAJ:MIN RM SIZE RO TYPE  MOUNTPOINT
sda      8:0    0   1G  0 disk

 

多路径

RBD exclusive lock feature对多路径有影响,参考:https://www.sebastien-han.fr/blog/2017/01/05/Ceph-RBD-and-iSCSI/(第5节),只能是主备模式?这部分有待补充。

 

 

 

QEMU中librbd相关线程和回调及IO写流程简要介绍

概念介绍

Image

对应于LVM的Logical Volume,是能被attach/detach到VM的载体。在RBD中,Image的数据有多个Object组成。

Snapshot

Image的某一个特定时刻的状态,只能读不能写但是可以将Image回滚到某一个Snapshot状态。Snapshot必定属于某一个Image。

Clone

为Image的某一个Snapshot的状态复制变成一个Image。如ImageA有一个Snapshot-1,clone是根据ImageA的Snapshot-1克隆得到ImageB。ImageB此时的状态与Snapshot-1完全一致,区别在于ImageB此时可写,并且拥有Image的相应能力。

元数据

striping

  • order:22,The size of objects we stripe over is a power of two, specifically 2^[order] bytes. The default is 22, or 4 MB.
  • stripe_unit:4M,Each [stripe_unit] contiguous bytes are stored adjacently in the same object, before we move on to the next object.
  • stripe_count:1,After we write [stripe_unit] bytes to [stripe_count] objects, we loop back to the initial object and write another stripe, until the object reaches its maximum size (as specified by [order]. At that point, we move on to the next [stripe_count] objects.

root@ceph1 ~ $ rados -p rbd ls

  • rbd_header.1bdfd6b8b4567:保存image元数据(rbd info的信息)
  • rbd_directory:保存所有image的id和名称列表
  • rbd_info:“overwrite validated”,EC pool使用?
  • rbd_id.vol1:保存image的id
  • rbd_data.233546b8b4567.0000000000000025:保存image数据的对象,按需分配,233546b8b4567为image id,0000000000000025为stripe_unit id,从0开始增长

参考:

  1. http://hustcat.github.io/rbd-image-internal-in-ceph/
  2. http://tracker.ceph.com/issues/19081

回调

回调类

3个特征:

  1. 类名称以C_开头
  2. 实现了finish成员函数
  3. Context子类

举例:

还有一种回调适配器类,通过模板类实现通用的回调类,可以把各种类转换成回调类:

之后通过回调生成函数create_xxx_callback(create_context_callback、create_async_context_callback)函数创建出回调类,供后续注册使用。

回调适配函数

通过模板函数将任意函数转换为回调函数。

为啥不直接用原始函数作为回调函数注册进去?

回调生成函数

create_context_callback、create_async_context_callback上面已经介绍过,这里主要介绍create_rados_callback:

这个函数只做了一件事,就是创建一个rados操作需要的AioCompletion回调类(与上面),而回调类里的回调函数,则是用上面提到的回调适配函数转换的,把普通函数转换为回调函数。

回调注册

有如下几种方式:

  1. 直接注册:通常在最外层,对外接口中使用,一般需要在librbd内部二次封装
  2. 通过回调生成函数:librbd内部使用较多
  3. 通过回调适配函数:librbd内部使用较多

回调与Finisher线程的关系

回调类为啥必须继承Context?

这是因为所有的回调都由finisher线程处理(执行体为Finisher::finisher_thread_entry),而该线程会调用回调类的complete成员函数,Context类实现了这个函数,专门用来作为回调公共类。只是为了方便、统一,并不是必须的,你可以可以自己实现回调类的complete成员函数,而不继承Context。

参考下面finisher thread的关联队列finisher_queue、finisher_queue_rval的入队过程,可了解回调入队过程。

回调流

在rbd image打开过程中,需要执行很多流程来获取image的各种元数据信息(流程描述参考OpenRequest的注释,主要包括V2_DETECT_HEADER、V2_GET_ID|NAME、V2_GET_IMMUTABLE_METADATA、V2_GET_STRIPE_UNIT_COUNT、V2_GET_CREATE_TIMESTAMP、V2_GET_DATA_POOL等),当然你也可以在一个方法中一次获取全部元数据,但会导致单次操作耗时太长,各元数据的获取函数耦合也比较重,这是我个人的猜测,也可能其他方面的考虑,目前还没有理解。

librbd中用回调流的方式,来依次调用各个元数据请求函数和响应处理函数,入口是rbd_open,第一个执行的元数据请求函数是send_v2_detect_header(发送检查是否为v2版本image header的请求),qemu的具体调用栈如下:

通过直接调用+设置回调再调用形成回调流,最后进入send_v2_apply_metadata,它会注册最后一个回调handle_v2_apply_metadata。

控制流

  • 请求:由RadosClient、MgrClient及其成员函数处理,一般是普通dispatch流程,最终都交给AsyncMessenger发送出去
  • 响应:AsyncMessenger相关方法

数据流

由Objecter类及其成员函数处理,一般是fast dispatch流程,最终都交给AsyncMessenger发送出去

数据结构及IO数据流转

控制流

Context

所有回调的基类

CephContext

所有操作都需要用到,存储了各种全局信息,每个client一个(librbd算一个client)

ImageCtx

存储image的全局信息,每个image一个

ContextWQ

IO控制流的工作队列类(包含队列和处理方法),op_work_queue对象

librados::IoCtx、IoCtxImpl

与rados交互所需的全局信息,一个对外一个内部使用,一个pool一个

Finisher、Finisher::FinisherThread

回调执行类,专门管理回调队列并在线程中调用各种回调

数据流

AsyncConnection

与ceph服务端连接信息,由AsyncMessenger维护,所有请求都由其发送,AsyncConnection::process

librbdioAioCompletion

用户层发起的异步IO完成后的librbd内部回调,主要用来记录perf counter信息,以及IO请求发起用户传入的外部回调函数

librbd::ThreadPoolSingleton

封装ThreadPool,实现tp_librbd单例线程

ThreadPool

所有线程池的基类

ThreadPool::PointerWQ

IO数据流、控制流工作队列的共同基类

librbdioImageRequestWQ

IO数据流的工作队列类(包含队列和处理方法),io_work_queue对象

librbdioImageRequest

IO请求的基类,image级别,对应用户IO请求

librbdioAbstractImageWriteRequest

IO写请求的抽象类,继承自ImageRequest

librbdioImageWriteRequest

IO写请求类,继承自AbstractImageWriteRequest

Thread

所有线程、线程池的基类,子类通过start函数启动各自的entry函数进入thread执行体完成实际工作。

Objecter

上层单次IO操作对象,对应用户IO请求

Objecter::Op

上层IO操作对象可能包含多个object,需要拆分成多个Op,对应到rados对象

Dispatcher

与服务端交互的分发方法基类,MgrClient、Objecter、RadosClient都继承自Dispatcher类

Striper

IO封装、解封,读写操作过程中从IO到object互相转换

librbdioObjectRequest、librbdioObjectReadRequest、librbdioAbstractObjectWriteRequest、librbdioObjectWriteRequest

用户IO请求拆分后的object级别的IO请求

线程池与队列

tp_librbd(librbd::thread_pool)

tp_thread启动(处理io_work_queue及op_work_queue):ThreadPoolstart–ThreadPoolstart_threads–new WorkThread(this)–Threadcreate–Threadtry_create–pthread_create–Thread::_entry_func–Threadentry_wrapper–ThreadPoolWorkThread::entry–线程启动完毕,worker开始工作

关联队列1:io_work_queue

入队过程:见下面主要代码流程部分,从ImageRequestWQ::aio_write()到入队io_work_queue。

关联队列2:op_work_queue

入队过程:搜索op_work_queue->queue()即可找到,主要是执行各种rbd image控制操作时会用到。

两个队列的关系及出队过程

由tp_librbd(ThreadPool)的work_queues成员保存,work_queues[0] == op_work_queue,work_queues[1] == io_work_queue。在ThreadPool::worker里会死循环处理这两个队列,交替处理。

io_work_queue出队过程:ThreadPoolworker–ThreadPoolPointerWQ_void_dequeue/_void_process/_void_process_finish–ThreadPoolPointerWQ<librbdioImageRequestlibrbd::ImageCtx >_void_process–librbdio::ImageRequestWQlibrbd::ImageCtx::process

op_work_queue出队过程类似,只是最终调用的是ContextWQ::process。

finisher thread

执行体

Finisher::finisher_thread_entry

thread1:fn-radosclient

  • 启动及用途:libradosRadosClientconnect里启动的finisher thread,为rados client服务,用来执行相关回调

thread2:fn_anonymous

  • 启动及用途:MonClient::init里启动的finisher thread,为monitor client服务,用来执行相关回调
  • 与fn-radosclient的区别:anonymous不会通过perfcounter记录队列长度(queue_len),处理延时(complete_latency),而fn-radosclient会记录

thread3:taskfin_librbd

  • 启动及用途:主要用来给ImageWatcher对象执行各种任务(基于SafeTimer定时的或者基于finisher_queue的),ImageWatcher主要是在镜像属性变动的发送通知给关注方。
  • 入队过程与其他两个类似,看queue方法调用位置即可。

关联队列:Finisher::finisher_queue、finisher_queue_rval

二者区别见注释:

  • 入队过程:所有调用Finisher::queue函数的地方(一般都是finisher.queue,如c->io->client->finisher.queue),
  • 出队过程:线程执行体Finisher::finisher_thread_entry里面出队

入队过程示例(fn-radosclient线程):

handle_write_object是write_object函数注册的回调,属于tp_librbd线程,也即处理io的线程。

rados_completion回调最终传递给了ObjecterOponfinish(经过一次封装:C_aio_Complete(c)),实现了从tp_librbd线程转到msgr-worker-*线程,再到fn-radosclient线程(也即Finisher线程)的流转,这也是(几乎)所有回调都由Finisher线程调用的缘由。

msgr-worker-*

  • 暂未深入分析
  • 启动及用途:异步消息收发线程,主要与ms_dispatch、ms_local线程交互
  • 关联的队列:用于处理各种事件
  • 执行体:NetworkStackadd_thread里面return的lambda函数,由PosixNetworkStackspawn_worker启动
  • 数量:由配置项cct->_conf->ms_async_op_threads决定,默认值3,代码里写死上限值24个,配置项超出这个会被强制改为24,看代码逻辑应该不能在线修改

admin_socket

  • 用途:用来创建ceph-client.admin.2840389.94310395876384.asok,socket文件位置由ceph.conf配置文件中的[client]admin_socket = /var/run/ceph/qemu/$cluster-$type.$id.$pid.$cctid.asok决定。创建完之后作为UNIX domain socket的server端接收客户端请求,并给出响应,客户端可以用ceph –admin-daemon ceph-client.admin.2840389.94310395876384.asok命令发送请求,支持配置修改、perf dump等命令,具体命令列表可以用help子命令查看。
  • 初始化及启动:在CephContext构造函数中初始化,在CephContext::start_service_thread中启动。

ms_dispatch、ms_local

ms_dispatch

  • 用途:暂未深入分析,接收ms_local线程转发的普通dispatch消息,然后转发给Messager注册的普通dispatcher处理(dispatcher有MgrClient、Objecter、RadosClient,他们都继承自Dispatcher类)
  • 关联队列:优先级队列PrioritizedQueue<QueueItem, uint64_t> mqueue
  • 入队:通过DispatchQueue::enqueue入队
  • 出队:线程执行体DispatchQueue::entry

ms_local

  • 用途:初步理解是接收librbd client端请求,转发给ms_dispatch线程处理(普通dispatch,入队mqueue),或者fast dispatch(直接通过Messenger的fast dispatcher发送,messenger目前为AsyncMessenger,dispatcher有MgrClient、Objecter、RadosClient,他们都继承自Dispatcher类)
  • 关联队列:list<pair<Message *, int> > local_messages
  • 入队:通过DispatchQueue::local_delivery入队
  • 出队:线程执行体DispatchQueue::run_local_delivery

启动

safe_timer

  • 用途:管理及触发定时任务事件,librbd中主要用来跟monitor保持心跳(MonClient::schedule_tick),以及ImageWatcher的定时事件。
  • 初始化及启动:qemu中一共启动了3个线程,其中一处是在libradosRadosClientRadosClient构造函数中初始化,在libradosRadosClientconnect中调用SafeTimerinit启动。通过SafeTimer类进行管理和对外提供接口,SafeTimer类包含一个SafeTimerThread类型的成员thread,SafeTimerThread继承Thread类,safe_timer线程通过SafeTimerinit函数使用thread成员进行创建及启动,线程执行的实体函数是SafeTimertimer_thread(SafeTimerThreadentry里面调用),用来轮询检查是否有新的定时任务事件需要触发。另一处是在ImageWatcher对象初始化时启动,第三处未分析,在构造函数处加断点调试即可知晓。
  • 与cephtimer_detailtimer的关系:二者都有定时器功能,但cephtimer_detailtimer更轻量(参考该类的注释),IO卡顿预警功能使用的是cephtimer_detailtimer。

关联的队列:SafeTimer::schedule

  • 入队过程:SafeTimeradd_event_after、SafeTimeradd_event_at
  • 出队过程:SafeTimercancel_event、SafeTimercancel_all_events,以及SafeTimer::timer_thread中正常的事件触发。

service

  • 用途:CephContextServiceThread::entry是线程执行体,有3个工作,1是检查是否需要重新打开log文件,2是检查心跳,3是更新perfcounter中的记录值,但如果是默认配置情况下,这个线程2、3两个任务是不做的。
  • 初始化及启动:过程与admin_socket的启动过程相同,都在CephContext::start_service_thread中完成

log

  • 初始化及启动:在CephContext构造函数中初始化和启动。
  • 用途:负责文件日志打印和内存日志的存储和dump(通过admin socket)。

主要代码流程分析

块设备IO到rados对象映射过程(Striper)

object到osd的crush计算过程

遗留问题

  • 整体IO流程图
  • IO到object到op的拆分过程,以及op执行完毕后如何判断用户层单次IO全部执行完毕
  • object到osd的crush计算过程
  • IO请求发送过程及响应处理过程

perf counter机制

每个image一个perf counter,初始化过程:

使用过程:

cephtimer_detailtimer机制

类似SafeTimer,一个线程专门检查定时任务是否需要触发,可以取消定时任务,取消时如果发现任务已经触发了就忽略,没触发就取消任务。

线程未命名,仍然叫qemu-system-x86,在Objecter对象构造的时候启动: