文章目录
-
- [一、 现象:Deep Scrubbing 永远跑不完](#一、 现象:Deep Scrubbing 永远跑不完)
- [二、 番外篇:为什么 Autoscaler "失灵"了?](#二、 番外篇:为什么 Autoscaler “失灵”了?)
-
- [1. 源码揭秘:Autoscaler 的"盲区"](#1. 源码揭秘:Autoscaler 的“盲区”)
- [2. 缺失的拼图:告诉它"我是主力"](#2. 缺失的拼图:告诉它“我是主力”)
- [三、 中场休息:Autoscaler 的"建议"与人工的"克制"](#三、 中场休息:Autoscaler 的“建议”与人工的“克制”)
-
- [1. 开启"只看不动"模式](#1. 开启“只看不动”模式)
- [2. 查看系统建议](#2. 查看系统建议)
- [3. 人工决策:克制的艺术](#3. 人工决策:克制的艺术)
- [四、 决策与机制:从 128 到 512 的漫长扩容](#四、 决策与机制:从 128 到 512 的漫长扩容)
-
- [为什么扩容花了整整两天?(Step-by-Step 机制)](#为什么扩容花了整整两天?(Step-by-Step 机制))
- [五、 瓶颈:令人窒息的 30MB/s](#五、 瓶颈:令人窒息的 30MB/s)
- [六、 破局:解锁带宽,全速愈合](#六、 破局:解锁带宽,全速愈合)
-
- [1. mclock 的"保守"与"神秘爆发"](#1. mclock 的“保守”与“神秘爆发”)
- [2. 人工介入:小步快跑,稳字当头](#2. 人工介入:小步快跑,稳字当头)
- [七、 总结:涉及的 Ceph 技术知识点](#七、 总结:涉及的 Ceph 技术知识点)
#作者:高瑞东
在维护ceph分布式存储系统时,我们常常会从一个不起眼的报警信号出发,最终揭开系统架构层面的隐患。最近,我们在处理一起 Ceph 集群(版本17.2.5) Deep Scrubbing(深度清洗)无法按计划完成 的问题时,就经历了一次典型的"以小见大"的排查过程。
本文将复盘这次故障的解决路径:从发现 Scrubbing 积压,到定位"巨型 PG"根因,再到突破 30MB/s 的恢复速度限制,最后通过修复失效的 Autoscaler 机制,实现集群的彻底治愈。
一、 现象:Deep Scrubbing 永远跑不完
问题起因 :运维监控发现,某核心存储池的 Deep Scrubbing 任务长期处于积压状态,无法在预定的 osd_scrub_begin_hour 和 osd_scrub_end_hour 窗口内完成。
科普:Scrubbing 为什么不能停?
Ceph 的 Scrubbing 机制(类似 RAID 的 Verify)是数据一致性的最后一道防线。
- 检测静默损坏:硬盘会发生位翻转(Bit Rot),只有通过 Deep Scrubbing 逐字节比对副本,才能发现并修复。
- 避免灾难叠加:如果长期不清洗,坏块会悄悄累积。一旦某天发生磁盘故障需要数据恢复,可能发现剩余的副本恰好也是"坏"的,导致数据彻底丢失。
因此,Scrubbing 积压绝非小事,它意味着集群正处于"裸奔"风险中。
初步分析 :
Deep Scrubbing 需要读取对象数据并进行校验,是一个高 I/O 消耗的操作。如果它跑不完,通常意味着:
- 磁盘性能瓶颈?(排除,磁盘负载尚可)
- 单次 Scrubbing 耗时过长?
深入排查 :
检查 PG (Placement Group) 状态后,我们发现了一个惊人的事实:该存储池的 pg_num 仅为 128,但数据量却非常大。这意味着每个 PG 承载了过多的对象(Objects)和数据量。
结论 :这是典型的 "Giant PG"(巨型 PG) 问题。
当 PG 过大时,Scrubbing 锁定 PG 的时间变长,不仅影响业务 I/O,而且导致系统无法在有限的时间窗口内清洗完如此巨大的数据块,最终导致任务积压。
二、 番外篇:为什么 Autoscaler "失灵"了?
这里有一个令人费解的细节:我们明明开启了 Autoscaler (pg_autoscale_mode = on),为什么它没有自动把 PG 加上去?
1. 源码揭秘:Autoscaler 的"盲区"
带着疑问,我们查阅了 Ceph 源码 <src/pybind/mgr/pg_autoscaler/module.py>。Autoscaler 计算理想 PG 数的核心逻辑在 _get_pool_pg_targets 函数中。
它在决定给一个存储池分配多少 PG 时,会计算一个关键指标 capacity_ratio(该池子应占总容量的比例)。
python
# 核心逻辑简化:
# 1. 计算池子实际占用(或预设目标)
pool_raw_used = max(pool_logical_used, target_bytes) * raw_used_rate
# 2. 计算容量占比
capacity_ratio = float(pool_raw_used) / capacity
capacity_ratio = max(capacity_ratio, target_ratio)
这里暴露了问题的根源:
-
场景还原:
- 集群规模 :54 个 OSD,单盘 9.1TB,总容量约 491TB。
- 存储池现状 :该池已使用 54TB 物理容量(含三副本,即逻辑数据量约 18TB)。
- PG 现状 :初始仅 128 个。
-
Autoscaler 的"滞后计算":
- 计算容量占比 :
54TB / 491TB ≈ 0.11(11%)。 - 计算理想 PG :
- 集群目标总 PG =
54 OSD * 100 = 5400。 - 该池分到的 Raw PG =
5400 * 0.11 = 594。 - 该池的逻辑 PG (
pg_num) =594 / 3副本 = 198。 - 向下取整到 2 的幂次方 -> 128。
- 集群目标总 PG =
- 阈值拦截(致命一击) :
- Autoscaler 计算出的理想值是 128(或者接近 256)。
- 当前已经是 128 了。
- Autoscaler 认为:"完美!不需要调整。"
- 计算容量占比 :
这就导致了一个尴尬的局面:
Autoscaler 认为一切正常,但实际上每个 PG 承载了 54TB / 128 ≈ 432GB 的数据(物理容量)。
对于 Deep Scrubbing 来说,要在一个时间窗口内清洗 432GB 的数据,对 HDD 来说简直是天方夜谭。这直接导致了 Scrubbing 任务超时、积压,最终卡死。
结论 :默认的 Autoscaler 逻辑是基于"容量占比"分配 PG 的。对于**"数据量很大(54TB)但占比不高(11%)"的存储池,它分配的 PG 数(128)虽然符合均衡原则,但严重不足以支撑 Deep Scrubbing 的性能需求**。
2. 缺失的拼图:告诉它"我是主力"
为了打破这个滞后僵局,我们需要手动介入,告诉 Autoscaler:"别管现在的比例了,这个池子就是未来的主力,请按最大比例预留 PG!"
我们通过设置 target_size_ratio 来实现这一点。
从上面的代码可以看到,max(capacity_ratio, target_ratio) 会取两者中的最大值。如果我们设置 target_size_ratio = 0.9(预示该池将占用 90% 的集群空间):
- Autoscaler 会忽略当前 33% 的占比。
- 强制按 90% 计算:
理想 PG = (5400 * 0.9) / 3 = 1620。 - 取整后得到 2048(或 1024)。
- 这会立即触发扩容,将 PG 数量拉升到一个能让单 PG 数据量保持在合理范围(如 30-50GB)的水平。
修正命令:
bash
# 告诉 Autoscaler:这个池子是绝对主力,预计占用 90% 的容量
ceph osd pool set <pool_name> target_size_ratio 0.9
一旦设置,代码中的 max(capacity_ratio, target_ratio) 就会选中 0.9。Autoscaler 会立刻计算出:总 PG 数 * 90% = 理想 PG 数(例如 2048)。
但是,直接让 Autoscaler 自动接管一切真的安全吗?
三、 中场休息:Autoscaler 的"建议"与人工的"克制"
在查明原理后,我们面临一个关键抉择:是直接开启自动模式(mode=on)让系统放飞自我,还是采取更稳妥的方式?
考虑到集群正如火如荼地运行核心业务,如果让 Autoscaler 突然发起大规模的 PG 分裂(Split),随之而来的数据迁移(Rebalancing)可能会引发不可控的 I/O 抖动。
于是,我们采取了 "咨询模式" (pg_autoscale_mode = warn)。
1. 开启"只看不动"模式
我们将 Autoscaler 设置为 warn 模式:
bash
ceph osd pool set <pool_name> pg_autoscale_mode warn
在这个模式下,Autoscaler 会在后台利用上述公式默默计算理想的 PG 数(pg_num_final),但绝不会自动执行调整 。它只会通过 ceph health 抛出 POOL_TOO_FEW_PGS 警告,告诉我们:"我觉得现在的 PG 太少了"。
2. 查看系统建议
通过 ceph osd pool autoscale-status 命令,我们可以看到 Autoscaler 的"心理活动":
text
POOL SIZE TARGET SIZE RATE RAW CAPACITY RATIO TARGET RATIO EFFECTIVE RATIO BIAS PG_NUM NEW PG_NUM AUTOSCALE
rbd_data 50T 100T 3.0 500T 0.1000 0.2000 0.2000 1.0 128 1024 warn
可以看到,系统明确建议我们将 PG 数调整为 1024 (NEW PG_NUM)。
3. 人工决策:克制的艺术
虽然系统建议 1024,但我们决定分步走 。
直接从 128 跳到 1024 步子太大。为了保证业务平稳,我们决定先手动扩容到 512。
这正是 warn 模式的价值所在:它提供了基于算法的精准计算辅助,但把最终何时执行、执行多少的"扳机"留给了管理员。
四、 决策与机制:从 128 到 512 的漫长扩容
为了彻底解决问题,我们将 pg_num 扩容到 512。
为什么要扩容?(扩容的好处)
- 化整为零:将巨大的数据块切分为更小的单元,Scrubbing 更加轻快,不再阻塞业务。
- 并发提升:更多的 PG 意味着更多的数据队列和锁粒度,能更好地发挥 HDD 的并发性能。
- 均衡分布:更多的小 PG 能更均匀地散落在 OSD 上,避免出现"热点 OSD"。
我们执行了以下命令:
bash
ceph osd pool set <pool> pg_num 512
ceph osd pool set <pool> pgp_num 512
为什么扩容花了整整两天?(Step-by-Step 机制)
很多人误以为执行完命令后,PG 数量会瞬间翻倍。但在我们的 HDD 集群上,这个过程持续了两天。这是 Ceph 刻意设计的保护机制。
-
步进牵引 (Gap Throttling) :
Ceph Manager 有一个保护参数
mgr_max_pg_num_change(默认 128)。它强制pg_num(拆分)最多只能比pgp_num(迁移)快 128 个。- 当
pg_num达到pgp_num + 128时,系统会强制暂停拆分。 - 必须等待慢速的数据迁移(
pgp_num增加)追上来,缩短差距后,pg_num才会继续增加。
- 当
-
流控保护 (Misplaced Throttling) :
数据迁移的速度由
target_max_misplaced_ratio(默认 5%)控制。- 如果当前因为扩容导致的"位置错误"对象超过 5%,
pgp_num就会停止增加。 - 对于 HDD 集群,数据搬运速度远慢于 SSD,导致这个"等待-追赶"的循环被拉得很长。
- 如果当前因为扩容导致的"位置错误"对象超过 5%,
这种拆分-等待-迁移-再拆分的循环,虽然耗时,但避免了数千个 PG 同时产生导致的 I/O 风暴,保护了脆弱的 HDD 性能。
五、 瓶颈:令人窒息的 30MB/s
在扩容开始后,我们遇到了第二个拦路虎:恢复吞吐量卡在 30MB/s。
专业分析 :
面对这种极度平稳的低速,我们排除了硬件瓶颈,锁定了 mclock scheduler 的 QoS 限制。
对于 HDD OSD 而言,默认的 mClock 参数过于保守,且 high_client_ops 策略极度压制了后台流量。
六、 破局:解锁带宽,全速愈合
找到症结后,我们打出了一套组合拳:
1. mclock 的"保守"与"神秘爆发"
我们尝试调整 mclock 的配置(Ceph Quincy 版本的默认 QoS 调度器),试图为数据迁移开绿灯:
bash
ceph config set osd osd_mclock_profile high_recovery_ops
然而,结果令人沮丧。即使开启了 high_recovery_ops,Recovery 速度依然不温不火,徘徊在 70MB/s 左右。这说明 mclock 的调度逻辑在 HDD 场景下显得过于"保守",或者说它的上限被某些深层机制限制住了。
但转机出现在一个意想不到的时刻:
当我们手动将 pg_num 调整到位(比如 2048),系统开始逐步调整 pgp_num 进行实际的数据重平衡(Rebalance)时,Recovery 速度突然飙升到了 200MB/s!
观察到的现象:
- Split 阶段(仅 pg_num 增加):速度受限,I/O 压力大,mclock 似乎在极力压制。
- Rebalance 阶段(pgp_num 追赶) :一旦
pg_num稳定,开始纯粹的数据移动时,系统仿佛解开了束缚。
技术真相:Split 与 Rebalance 的本质差异
带着疑惑,我们深入阅读了 Ceph BlueStore 的源码,在 src/os/bluestore/BlueStore.cc 中找到了答案。
-
PG Split 是"元数据操作" (Metadata Only) :
我们在
_split_collection函数中发现,PG 分裂在底层存储引擎中并不会移动物理对象。- 原理:BlueStore 中的对象是按 Hash 存储的。PG 只是一个逻辑桶。
- 实现 :分裂时,代码仅需更新集合(Collection)的
bits(掩码),将原本属于父 PG 的一部分 Hash 范围"划归"给子 PG。 - 代价 :这是一系列密集的 RocksDB 事务操作(High IOPS),涉及大量的元数据更新,但几乎没有数据吞吐(Low Throughput)。
- 现象解释:这就是为什么在 Split 阶段,你会感觉"Recovery 速度(MB/s)"上不去------因为根本就没有数据在搬运!此时的 I/O 压力全部集中在 HDD 最不擅长的随机读写(RocksDB)上。
-
Rebalance 是"数据搬运" (Data Movement) :
当 PG 分裂完成,CRUSH Map 发生变化,数据需要从原来的 OSD 迁移到新的 OSD 时,真正的 Backfill 流程才开始。
- 原理:读取完整的对象数据 -> 网络传输 -> 写入新 OSD。
- 代价:这是典型的顺序读写操作(High Throughput)。
- 现象解释:HDD 擅长顺序吞吐。一旦进入这个阶段,不再受限于 RocksDB 的随机性能,带宽瞬间被跑满,于是你看到了从 70MB/s 到 200MB/s 的"神秘爆发"。
结论 :这并不是 mClock 在 Split 阶段"压制"了速度,而是Split 本身就不产生吞吐量。所谓的"慢",其实是 HDD 在艰难地处理元数据分裂的 IOPS 瓶颈;而随后的"快",才是带宽真正的释放。
2. 人工介入:小步快跑,稳字当头
针对 HDD (Rotational Media) 的物理特性,默认的 mClock 参数可能评估不准。HDD 最大的问题是随机 IOPS 低,且对大块顺序写入的成本计算可能过高。
我们进行了以下深度优化(请根据实际压测结果调整):
-
降低 HDD 成本计算 :
默认的 cost 可能过高,导致 HDD 被认为太忙。
bash# 默认 5.2,调低为 0.5,让调度器认为 HDD 处理大块数据其实"不那么累" ceph config set osd osd_mclock_cost_per_byte_usec_hdd 0.5 -
提升 HDD 容量上限 :
默认的 315 IOPS 比较保守。为了抵消分片(Sharding)带来的损耗计算,可以适当调大。
bash# 默认 315,激进提升至 25000(抵消分片损耗,非真实物理IOPS) ceph config set osd osd_mclock_max_capacity_iops_hdd 25000 -
常规并发参数(辅助) :
适当放宽并发限制(参考
osd.yaml.in):osd_max_backfills: 提升至 4。osd_recovery_sleep: 调整为 0。
通过这套操作,我们将原本预计数天的重平衡时间缩短到了小时级别。
七、 总结:涉及的 Ceph 技术知识点
通过这次实战,我们复习并验证了以下关键技术点:
-
Autoscaler 的触发条件:
- 仅仅开启
on是不够的。对于空池或增长型池,必须配置target_size_ratio或target_size_bytes,否则 Autoscaler 就是"瞎子"。
- 仅仅开启
-
PG Sizing 的艺术:
- PG 太少 -> 巨型 PG -> 运维噩梦。
- PG 太多 -> 元数据爆炸。
- 保持每个 OSD 约 100 个 PG 是黄金法则。
-
mclock QoS 调度器:
- 理解
high_client_opsvshigh_recovery_ops的区别,是掌控 Ceph 恢复速度的关键钥匙。
- 理解
结语 :
从忽略 target_size_ratio 导致的 Autoscaler 失效,到手动扩容后的限速陷阱,每一个环节都是对 Ceph 机制的深刻拷问。希望这篇复盘能帮助大家避开这些"坑",让你的 Ceph 集群跑得更快、更稳。