Ceph OSD CPU 占用高排查:BlueFS Buffered IO 与 NUMA 亲和性深度分析

版本: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_iterdio_bio_submit不会出现 generic_file_buffered_read。出现这个函数名,100% 说明在走 page cache。


二、BlueFS IO 模式原理

2.1 KernelDevice 双 fd 机制

BlueFS 底层的 KernelDeviceopen()同时打开两套文件描述符

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_ioFLAG_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,取决于:

  1. 发起 IO 的线程跑在哪个核
  2. 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 等待时间,绑核收益非常显著,建议在灰度验证后全量推进。

相关推荐
一个行走的民2 小时前
Ceph Monitor 管理职责全景解析
ceph
一个行走的民2 小时前
Ceph Monitor 如何管理文件系统(FS)元数据
ceph
运维栈记3 小时前
Ceph 入门:一文读懂分布式存储的“瑞士军刀”
分布式·ceph
一个行走的民4 小时前
Ceph Monitor 订阅推送机制深度解析
ceph
一个行走的民17 小时前
Ceph OSD 故障恢复机制与 PG Log 深度解析
ceph
bukeyiwanshui1 天前
20260527 Ceph 集群安装过程
ceph
AOwhisky1 天前
Ceph系列第一期:Ceph分布式存储核心概念与架构初识
linux·运维·笔记·分布式·ceph·学习·架构
bukeyiwanshui1 天前
20260527 ceph添加节点
ceph
bukeyiwanshui1 天前
20260527 Ceph 集群组件管理
ceph