作者:天翼云 谭龙
关键词: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-blk Target 等,服务层蕴含 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 应用状况,联合走读代码流程,可能更快定位到问题起因。