SPDK 对接 Ceph 性能优化
作者:天翼云谭龙
关键词:SPDK、NVMeOF、Ceph、CPU 负载均衡
SPDK 是 intel 公司主导开发的一套存储高性能开发套件,提供了一组工具和库,用于编写高性能、可扩展和用户态存储应用。它通过使用一些关键技术实现了高性能:
1. 将所有必需的驱动程序移到用户空间,以避免系统调用并且支持零拷贝访问
2. IO 的完成通过轮询硬件而不是依赖中断,以降低时延
3. 使用消息传递,以避免 IO 路径中使用锁
SPDK 是一个框架而不是分布式系统,它的基石是用户态(user space)、轮询(polled-mode)、异步(asynchronous)和无锁的 NVMe 驱动,其提供了零拷贝、高并发直接用户态访问 SSD 的特性。SPDK 的最初目的是为了优化块存储落盘性能,但伴随持续的演进,已经被用于优化各种存储协议栈。SPDK 架构分为协议层、服务层和驱动层,协议层包含 NVMeOF Target、vhost-nvme Target、iscsi Target、vhost-scsi Target 以及 vhost-blkTarget 等,服务层包含 LV、Raid、AIO、malloc 以及 Ceph RBD 等,driver 层主要是 NVMeOF initiator、NVMe PCIe、virtio 以及其他用于持久化内存的 driver 等。
SPDK 架构
Ceph 是目前应用比较广泛的一种分布式存储,它提供了块、对象和文件等存储服务,SPDK 很早就支持连接 Ceph RBD 作为块存储服务,我们在使用 SPDK 测试 RBD 做性能测试时发现性能到达一定规格后无法继续提升,影响产品的综合性能,经过多种定位方法并结合现场与代码分析,最终定位问题原因并解决,过程如下。
1. 测试方法:启动 SPDK nvmf_tgt 并绑定 0~7 号核,./build/bin/nvmf_tgt -m 0xff,创建 8 个 rbd bdev,8 个 nvmf subsystem,每个 rbd bdev 作为 namespace attach 到 nvmf subsystem 上,开启监听,initiator 端通过 nvme over rdma 连接每一个 subsystem,生成 8 个 nvme bdev,使用 fio 对 8 个 nvme bdev 同时进行性能测试。
2. 问题:我们搭建了一个 48 OSD 的 Ceph 全闪集群,集群性能大约 40w IOPS,我们发现最高跑到 20w IOPS 就上不去了,无论增加盘数或调节其他参数均不奏效。
3. 分析定位:使用 spdk_top 显示 0 号核相对其他核更加忙碌,继续加压,0 号核忙碌程度增加而其他核则增加不明显。
查看 poller 显示 rbd 只有一个 poller bdev_rbd_group_poll,与 nvmf_tgt_poll_group_0 都运行在 id 为 2 的 thread 上,而 nvmf_tgt_poll_group_0 是运行在 0 号核上的,故 bdev_rbd_group_poll 也运行在 0 号核。
[root@test]# spdk_rpc.py thread_get_pollers
{
"tick_rate": 2300000000,
"threads": [
{
"timed_pollers": [
{
"period_ticks": 23000000,
"run_count": 77622,
"busy_count": 0,
"state": "waiting",
"name": "nvmf_tgt_accept"
},
{
"period_ticks": 9200000,
"run_count": 194034,
"busy_count": 194034,
"state": "waiting",
"name": "rpc_subsystem_poll"
}
],
"active_pollers": [],
"paused_pollers": [],
"id": 1,
"name": "app_thread"
},
{
"timed_pollers": [],
"active_pollers": [
{
"run_count": 5919074761,
"busy_count": 0,
"state": "waiting",
"name": "nvmf_poll_group_poll"
},
{
"run_count": 40969661,
"busy_count": 0,
"state": "waiting",
"name": "bdev_rbd_group_poll"
}
],
"paused_pollers": [],
"id": 2,
"name": "nvmf_tgt_poll_group_0"
},
{
"timed_pollers": [],
"active_pollers": [
{
"run_count": 5937329587,
"busy_count": 0,
"state": "waiting",
"name": "nvmf_poll_group_poll"
}
],
"paused_pollers": [],
"id": 3,
"name": "nvmf_tgt_poll_group_1"
},
{
"timed_pollers": [],
"active_pollers": [
{
"run_count": 5927158562,
"busy_count": 0,
"state": "waiting",
"name": "nvmf_poll_group_poll"
}
],
"paused_pollers": [],
"id": 4,
"name": "nvmf_tgt_poll_group_2"
},
{
"timed_pollers": [],
"active_pollers": [
{
"run_count": 5971529095,
"busy_count": 0,
"state": "waiting",
"name": "nvmf_poll_group_poll"
}
],
"paused_pollers": [],
"id": 5,
"name": "nvmf_tgt_poll_group_3"
},
{
"timed_pollers": [],
"active_pollers": [
{
"run_count": 5923260338,
"busy_count": 0,
"state": "waiting",
"name": "nvmf_poll_group_poll"
}
],
"paused_pollers": [],
"id": 6,
"name": "nvmf_tgt_poll_group_4"
},
{
"timed_pollers": [],
"active_pollers": [
{
"run_count": 5968032945,
"busy_count": 0,
"state": "waiting",
"name": "nvmf_poll_group_poll"
}
],
"paused_pollers": [],
"id": 7,
"name": "nvmf_tgt_poll_group_5"
},
{
"timed_pollers": [],
"active_pollers": [
{
"run_count": 5931553507,
"busy_count": 0,
"state": "waiting",
"name": "nvmf_poll_group_poll"
}
],
"paused_pollers": [],
"id": 8,
"name": "nvmf_tgt_poll_group_6"
},
{
"timed_pollers": [],
"active_pollers": [
{
"run_count": 5058745767,
"busy_count": 0,
"state": "waiting",
"name": "nvmf_poll_group_poll"
}
],
"paused_pollers": [],
"id": 9,
"name": "nvmf_tgt_poll_group_7"
}
]
}
再结合代码分析,rbd 模块加载时会将创建 io_channel 的接口 bdev_rbd_create_cb 向外注册,rbd bdev 在创建 rbd bdev 时默认做 bdev_examine,这个流程会创建一次 io_channel,然后销毁。在将 rbd bdev attach 到 nvmf subsystem 时,会调用创建 io_channel 接口,因为 nvmf_tgt 有 8 个线程,所以会调用 8 次创建 io_channel 接口,但 disk->main_td 总是第一次调用者的线程,即 nvmf_tgt_poll_group_0,而每个 IO 到达 rbd 模块后 bdev_rbd_submit_request 接口会将 IO 上下文调度到 disk->main_td,所以每个 rbd 的线程都运行在 0 号核上。
综合环境现象与代码分析,最终定位该问题的原因是:由于 spdk rbd 模块在创建盘时 bdev_rbd_create_cb 接口会将每个盘的主线程 disk->main_td 分配到 0 号核上,故导致多盘测试时 CPU 负载不均衡,性能无法持续提高。
4. 解决方案:既然问题的原因是 CPU 负载不均衡导致,那么我们的思路就是让 CPU 更加均衡的负责盘的性能,使得每个盘分配一个核且尽可能均衡。具体到代码层面,我们需要给每个盘的 disk->main_td 分配一个线程,且这个线程均匀分布在 0~7 号核上。我们知道 bdev_rbd_create_cb 是在每次需要创建 io_channel 时被调用,第一次 bdev_examine 的调用线程是 spdk 主线程 app_thread,之后的调用均是在调用者线程上执行,比如当 rbd bdev attach 到 nvmf subsystem 时,调用者所在线程为 nvmf_tgt_poll_group_#,因为这些线程本身就是均匀的创建在 0~7 号核上,故我们可以复用这些线程给 rbd 模块继续使用,将这些线程保存到一个 global thread list,使用 round-robin 的方式依次分配给每个盘使用,该方案代码已推送到 SPDK 社区:bdev/rbd: Loadshare IOs for rbd bdevs between CPU cores (I9acf218c) · Gerrit Code Review (spdk.io)。打上该 patch 后,可以观察到 CPU 负载变得均衡,性能突破 20w,达到集群 40w 能力。
5. 思考回溯:该问题是如何出现或引入的?我们分析 rbd 模块的合入记录,发现在bdev/rbd: open image on only one spdk_thread · spdk/spdk@e1e7344 (github.com)和bdev/rbd: Always save the submit_td while submitting the request · spdk/spdk@70e2e5d (github.com)对 rbd 的结构做了较大的变化,主要原因是 rbd image 有一把独占锁 exclusive_lock,通过 rbd info volumes/rbd0 可查看,这把锁的作用是防止多客户端同时对一个 image 写操作时并发操作,当一个客户端持有这把锁后,其他客户端需要等待该锁释放后才可写入,所以多客户端同时写导致抢锁性能非常低,为此这两个 patch 做了两个大的改动:1)对每个 rbd bdev,无论有多少个 io_channel,最后只调用一次 rbd_open,即只有一个 rbd 客户端,参见接口 bdev_rbd_handle 的调用上下文;2)对每个盘而言,IO 全部收敛到一个线程 disk->main_td 发送给 librbd。
因为每个盘的 disk->main_td 均为第一个 io_channel 调用者的线程上下文,所以他们的线程都在同一个核上,导致 IO 从上游到达 rbd 模块后全部汇聚到一个核上,负载不均衡导致性能无法继续提高。
6. 总结:在定位性能问题时,CPU 利用率是一个重要的指标,spdk_top 是一个很好的工具,它可以实时显示每个核的繁忙程度以及被哪些线程占用,通过观察 CPU 使用情况,结合走读代码流程,能够更快定位到问题原因。
版权声明: 本文为 InfoQ 作者【天翼云开发者社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/dc68c47b61bbf4d9603c75136】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论