版本:Ceph 14.2.18(Nautilus)
背景
现网某 OSD 节点 CPU 占用持续偏高,通过 perf 抓取火焰图,发现 RocksDB GET 路径下存在大量 generic_file_buffered_read 调用,与预期的 Direct IO 路径不符。本文记录完整的排查过程,并深入分析 BlueFS IO 模式、NUMA 亲和性问题以及现网绑核建议。
一、火焰图现象与初步分析
1.1 火焰图调用栈
RocksDBStore::get
rocksdb::DB::Get
rocksdb::DBImpl::Get
rocksdb::DBImpl::GetImpl
rocksdb::Version::Get
rocksdb::BlockBasedTable::Get
rocksdb::BlockBasedTable::GetFilter
rocksdb::BlockFetcher::ReadBlockContents
├── rocksdb::crc32c::crc32c_3way ← CRC 校验
└── [libpthread-2.17.so]
entry_SYSCALL_64_after_hwframe
do_syscall_64
ksys_pread64
vfs_read
new_sync_read
generic_file_buffered_read ← Buffered IO!
copy_page_to_iter
copy_user_enhanced_fast_string ← memcpy 拷贝
1.2 问题点
generic_file_buffered_read 是 Linux 内核 VFS 层的 Buffered IO 读路径 ,数据经过 page cache。而 BlueFS 设计上默认使用 O_DIRECT,不应该出现这条路径。
注意 :
O_DIRECT的 IO 同样经过 VFS 层,但走的是blkdev_read_iter→dio_bio_submit,不会出现generic_file_buffered_read。出现这个函数名,100% 说明在走 page cache。
二、BlueFS IO 模式原理
2.1 KernelDevice 双 fd 机制
BlueFS 底层的 KernelDevice 在 open() 时同时打开两套文件描述符:
cpp
// os/bluestore/KernelDevice.cc
int fd = ::open(path.c_str(), O_RDWR | O_DIRECT | O_CLOEXEC);
fd_directs[i] = fd; // DIO fd,带 O_DIRECT
fd = ::open(path.c_str(), O_RDWR | O_CLOEXEC);
fd_buffereds[i] = fd; // Buffered fd,不带 O_DIRECT
读取时通过 choose_fd(bool buffered) 选择使用哪个 fd:
cpp
int KernelDevice::choose_fd(bool buffered, int write_hint) const {
return buffered ? fd_buffereds[write_hint] : fd_directs[write_hint];
}
2.2 bluefs_buffered_io 配置项
cpp
// os/bluestore/BlueFS.cc
bool buffered = cct->_conf->bluefs_buffered_io; // 全局开关
r = bdev[p->bdev]->read_random(p->offset + x_off, l, out, buffered);
| 配置值 | 效果 |
|---|---|
false(默认) |
使用 fd_directs(O_DIRECT),绕过 page cache |
true |
使用 fd_buffereds(无 O_DIRECT),走 page cache |
这个开关控制 BlueFS 所有读路径 ,没有例外------RocksDB 的所有文件 IO(SST、WAL、Filter、Index)都经过 BlueRocksEnv → BlueFS → KernelDevice,最终受这个开关控制。
2.3 DIO 的对齐处理
O_DIRECT 要求 offset、length、buffer 地址都必须对齐到块大小(通常 4K)。BlueFS 对未对齐请求做了透明处理:
cpp
// KernelDevice::read_random
if (!buffered && ((off % block_size != 0)
|| (len % block_size != 0)
|| (uintptr_t(buf) % CEPH_PAGE_SIZE != 0)))
return direct_read_unaligned(off, len, buf);
direct_read_unaligned 内部:
用户请求:off=100, len=50(未对齐)
↓
扩展到: aligned_off=0, aligned_len=4096(向下/上对齐)
↓
分配页对齐的内部 buffer
↓
pread(fd_directs, buf, 4096, 0) ← 真正的 DIO,对齐参数
↓
memcpy(out, buf+100, 50) ← 截取用户需要的数据
结论 :bluefs_buffered_io=false 时,所有请求(无论大小、对齐与否)都走 DIO,不会有任何请求漏回 page cache。
三、现网问题定位
3.1 环境信息
机器:dataarch-wlf2-h25-101
CPU:Intel Xeon E5-2680 v4,56 核(28 物理核 × 2 超线程,双 NUMA)
内存:95G
OSD 数量:42 个(ceph-1321 ~ ceph-1362)
Ceph 版本:14.2.18(Nautilus)
存储布局:LVM 部署,OSD 目录为 tmpfs,block 指向 LVM 卷,无独立 block.db
3.2 验证步骤
bash
# 1. 确认有 BlueFS(bluefs 文件存在且值为 1)
cat /var/lib/ceph/osd/ceph-1334/bluefs # 输出: 1
# 2. 确认无独立 block.db(RocksDB 文件在主数据盘的 BlueFS 分区上)
ls /var/lib/ceph/osd/ceph-1334/block*
# 只有 block,没有 block.db 和 block.wal
# 3. 确认问题配置
sudo ceph daemon osd.1334 config get bluefs_buffered_io
# 输出: {"bluefs_buffered_io": "true"} ← 找到了!
3.3 根因确认
bluefs_buffered_io = true(显式配置,非默认)
↓
BlueFS 所有读:使用 fd_buffereds(无 O_DIRECT)
↓
pread64 → vfs_read → generic_file_buffered_read → copy_page_to_iter
↓
SST 文件内容同时存于:
· RocksDB block cache(RocksDB 管理)
· page cache(OS 管理)← 重复缓存,浪费内存
↓
42 个 OSD × 每个 OSD 的 page cache 占用 → 内存压力大
↓
Linux 将其他内存换出到 swap → CPU stall 增加 → CPU 使用率升高
历史背景 :
bluefs_buffered_io在 Ceph 早期版本中默认为true。官方在 options.cc 的 long_description 中明确记录了改回false的原因:"在测试中发现,开启此选项会导致 Linux 内核过度使用 swap,运行数小时后产生大的负向性能影响。"
四、修复方案
4.1 动态修改(无需重启 OSD)
bluefs_buffered_io 是 FLAG_RUNTIME 选项,支持动态修改:
bash
# 单个 OSD 验证效果
sudo ceph daemon osd.1334 config set bluefs_buffered_io false
# 全集群永久生效
ceph config set osd bluefs_buffered_io false
4.2 修复后的变化
| 指标 | 修复前(buffered=true) | 修复后(buffered=false) |
|---|---|---|
| IO 路径 | page cache → memcpy → 用户 buffer | 直接 DIO 到用户 buffer |
| 内存占用 | block cache + page cache(双倍) | 只有 block cache |
| CPU(copy_page_to_iter) | 高 | 消失 |
| CPU(generic_file_buffered_read) | 高 | 消失 |
| RocksDB compaction 顺序读预读 | 有(page cache 预读) | 无 |
4.3 注意事项
修复前确认 RocksDB block cache 大小合理,避免去掉 page cache 兜底后 miss 率大幅上升:
bash
sudo ceph daemon osd.1334 config get rocksdb_cache_size
sudo ceph daemon osd.1334 config get bluestore_cache_size
# 查看 block cache 命中率
sudo ceph daemon osd.1334 perf dump | grep -E "rocksdb_block_cache_hit|rocksdb_block_cache_miss"
五、NUMA 亲和性与 page cache
5.1 page cache 的 NUMA 归属
page cache 的内存页是有 NUMA 归属的。内核分配 page cache 页时遵循本地优先原则:
磁盘 IO 完成 → 内核 IRQ 处理
↓
在"处理该 IRQ 的 CPU 所在的 NUMA node"上分配 page
↓
page cache 页的 NUMA 归属 = 触发/处理 IO 时的 CPU 所在 NUMA node
因此,page cache 页落在哪个 NUMA node,取决于:
- 发起 IO 的线程跑在哪个核
- IRQ affinity 设置(磁盘中断绑定到哪个核)
5.2 跨 NUMA 访问 page cache 的场景
场景一(无跨 NUMA):
OSD 线程 → NUMA0 核 → 触发 IO → IRQ 在 NUMA0 处理
→ page 分配在 NUMA0 → 后续读命中 page cache 在本地 NUMA ✓
场景二(跨 NUMA):
OSD 线程 → NUMA0 核 → 触发 IO
→ IRQ 被调度到 NUMA1 核处理(IRQ affinity 不合理)
→ page 分配在 NUMA1 → 后续 NUMA0 的 OSD 线程访问该 page ✗
场景三(跨 NUMA):
不绑核,OSD 线程在 NUMA0 和 NUMA1 之间漂移
→ 线程 A 在 NUMA0 触发 IO,page 在 NUMA0
→ 线程 A 后来被调度到 NUMA1,读同一 page → 跨 NUMA ✗
5.3 跨 NUMA 对 CPU 的影响
本地 NUMA 内存访问延迟:~80ns
跨 NUMA 内存访问延迟:~150ns(约 2 倍)
generic_file_buffered_read 中的 copy_page_to_iter:
每次 memcpy 都在访问 page cache 中的内存
↓
如果 page 在远端 NUMA node:每次内存访问延迟翻倍
↓
CPU 处于 Memory Stall(在等内存数据)
↓
top 显示 %user CPU 高,但实际有效计算很少
5.4 如何判断是否存在跨 NUMA 访问
方法一:numastat -p <pid>
bash
numastat -p $(pgrep -f "ceph-osd.*1334")
输出中各 Node 的内存占用,若 NUMA0 的 OSD 进程在 Node1 上有大量内存,说明存在跨 NUMA。
方法二:查看线程 CPU 分布
bash
# 查看 OSD 线程跑在哪些核上
ps -eLo pid,tid,psr,comm | grep ceph-osd | head -30
结合 NUMA 拓扑(本机偶数核为 NUMA0,奇数核为 NUMA1),若同一 OSD 进程的线程分散在两个 NUMA node,必然存在跨 NUMA。
方法三:perf stat 硬件计数器
bash
# 监控远端 NUMA 内存访问次数
perf stat -e mem_load_retired.local_pmm,mem_load_retired.remote_pmm \
-p $(pgrep -f "ceph-osd.*1334") sleep 10
remote_pmm 远高于 local_pmm 说明大量跨 NUMA 内存访问。
方法四:/proc/pid/numa_maps
bash
cat /proc/$(pgrep -f "ceph-osd.*1334")/numa_maps | grep -v "^$" | head -30
输出中 N0=xxx N1=xxx 显示每段内存在各 NUMA node 的页数量,可直观判断内存分布是否均匀。
六、IO 进程绑核的好处
6.1 绑核(CPU Affinity)的核心收益
6.1.1 消除跨 NUMA 内存访问
不绑核:线程在 NUMA0/NUMA1 之间漂移
→ 线程本地内存(栈、堆、page cache)分散在两个 NUMA node
→ 内存访问延迟不稳定,最差情况翻倍
绑核(NUMA 感知绑定):OSD N 的所有线程固定在同一 NUMA node
→ 所有内存访问都是本地 NUMA
→ 内存延迟稳定在 ~80ns
6.1.2 提高 CPU Cache 命中率
不绑核:
线程每次被调度可能到不同的核
→ L1/L2 cache 中缓存的数据(OSD 的内存数据结构)全部失效
→ 重新从 L3 或内存加载
绑核:
线程始终在同一组核上运行
→ L1/L2 cache 中的数据更可能命中(温 cache)
→ 减少 cache miss,降低内存访问次数
6.1.3 减少上下文切换开销
不绑核(42 OSD × N 线程):
Linux 调度器在 56 核上调度大量线程
→ 频繁的上下文切换(保存/恢复寄存器、TLB flush)
→ 调度开销本身消耗 CPU
绑核:
每个核的线程数减少,调度竞争降低
→ 上下文切换次数降低
→ TLB flush 减少(特别是 PCID 不足时)
6.1.4 IRQ 亲和性配套优化
绑核后,可以将磁盘 IRQ 也绑定到与 OSD 线程相同的 NUMA node:
bash
# 将 sdb 的 IRQ 绑定到 NUMA0 的核
echo "0,2,4,6,8" > /proc/irq/<irq_num>/smp_affinity_list
这样:
- OSD 线程在 NUMA0
- 磁盘 IRQ 也在 NUMA0 处理
- page cache 页分配在 NUMA0
- OSD 读 page cache → 本地 NUMA,延迟最优
6.1.5 可预期的性能(减少抖动)
绑核后每个 OSD 的 CPU 资源相对独立,不同 OSD 之间的 CPU 竞争减少,延迟抖动(p99/p999)会明显改善。对于存储系统,尾延迟优化往往比平均延迟更重要。
6.2 绑核的代价
| 代价 | 说明 |
|---|---|
| 运维复杂度增加 | 需要维护核的分配方案,OSD 上下线时需要更新绑核配置 |
| CPU 利用率可能降低 | 某个 OSD 空闲时,绑定给它的核不能被其他 OSD 使用 |
| 需要 NUMA 感知规划 | 错误的绑核方案可能反而引入跨 NUMA,需要仔细规划 |
七、Ceph OSD 现网是否需要绑核?
7.1 绑核方案建议
方案一:NUMA 感知绑核(推荐)
按 NUMA 节点划分 OSD,而不是精确到核:
bash
# 将 OSD 1321-1341(21 个)绑定到 NUMA0
# NUMA0 有 28 个逻辑核(0,2,4,...,54)
# 将 OSD 1342-1362(21 个)绑定到 NUMA1
# NUMA1 有 28 个逻辑核(1,3,5,...,55)
使用 numactl 启动(修改 systemd service):
bash
# /etc/systemd/system/ceph-osd@1321.service.d/numa.conf
[Service]
ExecStart=
ExecStart=/usr/bin/numactl --cpunodebind=0 --membind=0 /usr/bin/ceph-osd \
-f --cluster ceph --id %i --setuser ceph --setgroup ceph
同时将对应磁盘的 IRQ 也绑到同一 NUMA node。
方案二:纯 NUMA 内存绑定(保守方案)
如果不想限制 CPU 调度,至少做内存绑定:
bash
numactl --membind=0 ceph-osd ... # 只绑内存,不绑 CPU
这样 page cache 和 OSD 进程的堆内存都分配在 NUMA0,减少跨 NUMA 内存访问,同时保留调度灵活性。
7.2 绑核 vs 不绑核的决策矩阵
| 场景 | 建议 |
|---|---|
| OSD 数量多、CPU 偏紧(你们的情况) | 强烈建议绑核(至少按 NUMA node 绑定) |
| OSD 数量少、CPU 充裕 | 可选,绑核收益有限 |
| 混合部署(OSD 与其他服务共存) | 必须绑核,防止 CPU 争抢 |
| 高吞吐场景(顺序写为主) | 绑核收益中等(写路径 CPU 不敏感) |
| 高 IOPS 场景(随机小 IO 为主,如你们的 RocksDB GET 密集) | 强烈建议绑核 |
7.3 与 bluefs_buffered_io 的优先级
两个优化应该都做,但优先级不同:
第一步(立即生效,零风险):
ceph config set osd bluefs_buffered_io false
→ 直接消除 copy_page_to_iter 的 CPU 开销
→ 释放 page cache 内存
第二步(需要规划,重启 OSD 生效):
按 NUMA 节点绑核 + IRQ 亲和性调整
→ 消除跨 NUMA 延迟
→ 降低尾延迟抖动
→ 长期运行更稳定
第三步(可选,需要评估):
适当增大 bluestore_cache_size / rocksdb_cache_size
→ 去掉 page cache 双缓存后,确保 RocksDB block cache 足够大
八、总结
| 问题 | 根因 | 修复方案 |
|---|---|---|
generic_file_buffered_read 大量出现 |
bluefs_buffered_io=true,BlueFS 读走 page cache |
ceph config set osd bluefs_buffered_io false |
copy_page_to_iter CPU 高 |
page cache → 用户 buffer 的 memcpy 量大 | 同上,改 DIO 后消失 |
| 潜在的内存压力 / swap 使用 | SST 文件被 block cache + page cache 双份缓存 | 同上,释放 page cache |
| OSD CPU 竞争 / 尾延迟抖动 | 42 OSD 不绑核,跨 NUMA 内存访问,调度竞争激烈 | 按 NUMA node 绑核 + IRQ 亲和性调整 |
核心结论 :bluefs_buffered_io=true 是本次 CPU 高的直接原因,改为 false 是低风险、高收益的立即优化。在此基础上,按 NUMA 节点绑核可以进一步降低延迟抖动和 CPU 等待时间,绑核收益非常显著,建议在灰度验证后全量推进。