告警 (1)
告警:ceph -s 出现 mons <a>,<b>,... are using a lot of disk space。
触发条件:mon 数据目录大小超过 mon_data_size_warn (Nautilus 默认 15GiB,不同ceph版本可能不同)。
说明:
- mon 是 Paxos 多副本,每个副本各有一份独立的 RocksDB store;
- 内容经 Paxos 提交复制,每台 mon 的 db 大小通常一致;
- PGMap 全量统计由 mgr 维护,不在 mon db 里。
Mon db 空间分类 (2)
Ceph Monitor 分为3层(从下到上):
- 存储层:MonitorDBStore (RocksDB);按 prefix 组织的 KV
- 共识层:Paxos;不理解语义,只搬运/复制/持久化“state-machine 指令”
- 服务层:理解语义(知道 “osd out” 意味着什么)
- OSDMonitor
- MDSMonitor
- LogMonitor
- AuthMonitor
- ConfigMonitor
- ConfigKeyService (
ceph config-key get/set/rm) - …
按层级从下到上(注:非按“导致告警的可能性”排序),占 mon db 的地方有:
- 存储层:RocksDB 未回收;可通过
ceph tell 'mon.*' compact解决; - 共识层:paxos prefix
- 可以理解成 “paxos log” 或 “raft log”;
- 每一轮共识(一个 paxos version)提交的聚合事务——这一轮里各服务(osdmap/auth/mdsmap…)各改了什么,打包成一个 blob 存下来;blob 本身就是完整
MonitorDBStore::Transaction的编码,里面包含本轮对各服务 prefix 的全部写操作——提交时一条原子事务同时写“日志副本”(paxos prefix)和“状态”(服务 prefix)。这正是“两层各有独立裁剪参数”的原因(paxos_trim_min/maxvspaxos_service_trim_min/max——前者裁共识流水,后者裁各服务状态)。 - 另外,还会维护
first_committed/last_committed标记(相当于 paxos log 的范围,raft也会维护)。 - 注意:各服务的正式数据不在这个 prefix 里——osdmap 存 osdmap prefix、auth 存 auth prefix …… paxos prefix 只是“每轮提交了哪些变更”的流水;是consensus service 的范畴;consensus service “不理解” 每个聚合事务里是什么;对它来说,就是修改 state-machine 的指令。
- 保留窗口:
paxos_min(默认 500) ; - 裁剪:
paxos_trim_min=250意思是超出“保留窗口”250个以上才动手裁剪,例如日志窗口长700,则跳过裁剪;长800则裁剪到paxos_min=500;paxos_trim_max=500意思是每次裁剪最多前进 500 个;若由于某种原因,窗口累积到1200,不会一下子裁剪到paxos_min=500,而是第一次先裁剪到700(裁剪paxos_trim_max个),第二次跳过,因为700 < 750(paxos_min + paxos_trim_min);过一段时间,堆积到 750 以上,再裁剪到paxos_min=500。
- 服务层1:OSDMonitor 维护的 osdmap,这是最有可能导致告警的地方;下一节细讲;
- 服务层2:LogMonitor 维护的 cluster 级别的事件log,例如:“osd.123 marked down”、“pg 1.2 entered state active+clean”、health 告警、管理命令审计…… 就是
ceph -w滚动刷屏的那份日志的持久副本;只保留最近 500 个 osdmap epoch 窗内的日志(mon_max_log_epochs=500),有天花板,撑不大。 - 服务层3:其它服务,例如 AuthMonitor/ConfigMonitor/ConfigKeyService 等,占用空间一般比较小,导致告警的可能性不大;
OSDMap(最常见)详解 (3)
存储结构:每个 epoch 存两份 (3.1)
每次 osdmap 提交(leader 的 OSDMonitor::encode_pending),store 里写入:
| 存储项 | 写入点 | 单个体积 |
|---|---|---|
| full map | put_version_full(t, epoch, fullbl)(无条件、每 epoch 一份) |
与集群规模相关,千盘级集群约 2 MB+ |
| incremental | put_version(t, epoch, inc_bl) |
通常几 KB,crush 变更时可达几百 KB |
体积大头永远是 full。例如一次实际案例:2,000+ OSD 集群,单个 full = 2,226,238 B ≈ 2.12 MB。
trim 机制:决定“留多久” (3.2)
Mon leader 周期性尝试 trim,由Monitor::tick()触发;tick周期默认是 5 秒(见配置项mon_tick_interval);trim 时,OSDMonitor::get_trim_to() 计算 trim 终点(floor),trim 机制分批(paxos_service_trim_max)删除 first_committed 到 floor 之间的历史;删除区间[first_committed, floor),floor 保留,成为新的first_committed。
1 | // floor 是 保留的起点,也就是 trim 的终点; |
规则拆解:
min_last_epoch_clean:各 pool “最后一次全部 PG clean” 的 epoch 的最小值- Nautilus 版本按 pool 跟踪,
ceph report的osdmap_clean_epochs.last_epoch_clean.per_pool可查; - 只要集群有降级/恢复中的 PG,或存在长期 down 的 OSD,该值就冻结;
- trim 一路裁到它为止,之后一步都裁不动;
- Nautilus 版本按 pool 跟踪,
- out OSD 的次级钉扎:已
osd out但未osd rm的 OSD,其上报的最老 epoch 同样拉低 floor,直到被osd rm才解除; - 保底区间:
mon_min_osdmap_epochs,和前文共识层的“paxos_min”完全对应,默认值也是500(只是巧合;它俩是不同层里同一个东西,默认值相同不是必须的)。看前文共识层的保底区间paxos_min即可。唯一不同:某些情况下,floor 被拉低,保留区间会超过mon_min_osdmap_epochs,甚至超出很多(而共识层不会:其区间受容忍带上限约束,paxos_min + paxos_trim_min,约 750)。 - 裁剪:和前文共识层对应;配置
paxos_service_trim_min/max和共识层的paxos_trim_min/max也对应,floor − first_committed < paxos_service_trim_min则跳过裁剪;每批最多裁剪paxos_service_trim_max个。 - trim 时机是持续的:每个周期(如前所说,默认 5 秒)都尝试裁到 floor,与 mon_osdmap_full_prune_min 这个数无关(那是 prune 的,见下文);
- 例外:
creating_pgs非空时 trim 完全暂停; mon_osd_force_trim_to可显式指定 floor(会丢弃更早历史,谨慎使用)。
prune 机制:决定“留的东西怎么存” (3.3)
背景:trim 保留区间内每个 epoch 都带一份 2.12 MB 的 full,区间一长(如上万 epoch)体积爆炸。prune 对保留区间内部做压缩,不动区间边界。
激活条件
激活条件是should_prune(),即同时满足如下2个条件(另隐含:总历史须 > mon_min_osdmap_epochs 且 first_committed 尚未达 B):
- 条件1:
(last_committed - mon_min_osdmap_epochs) - first_committed >= mon_osdmap_full_prune_min;即 “可 prune 区间 >= mon_osdmap_full_prune_min”:
1 | 此区间不许动 |
其实 first_committed 就是 floor,因为,假如 floor > first_committed,则会被 trim 机制裁剪到 floor 处。通常 mon_osdmap_full_prune_min 比较大(默认是 10,000),所以,满足此条件,意味着 floor 被某种原因(见上一节)拉的很低(即值很小)。也就是说,prune 机制只是在 trim 机制失效时才起作用;假如 floor 能及时更新,trim 机制会保持“可 prune 区间”很短(甚至为0,即 first_committed 被裁剪到 B),不会触发大于mon_osdmap_full_prune_min的条件。
- 条件 2:最后一个 pinned full 之后还凑得满一个
mon_osdmap_full_prune_interval(默认 10)——不足一个间隔的零头本轮不压(逐轮推进的进度闸门)。
压缩方式
每 mon_osdmap_full_prune_interval(默认 10)个 epoch 保留 1 个 full(pinned)osdmap ,删除中间冗余的 full;增量全部保留;注:最近 500 个 epoch 原样保留(“此区间不许动”);
所以,prune 只是压缩(删除冗余的 full osdmap),旧 osdmap 仍可精确取回:请求落在 pinned 区间的任意 epoch,由 get_full_from_pinned_map 取最近的 pinned full,然后逐个 apply 保留的增量(≤10 个)就精确得到请求的 osdmap。即现场重构——以少量 CPU 时间换 10 倍空间;
所以说,prune 机制是安全的,它没有删除“信息”,只是“压缩”信息。任何需要旧 osdmap 的场景,例如落后很久的 OSD,需要某个 osdmap 时,先拿到最近的 pinned full 再 apply 增量,就会精确得到所需。
注意:prune 是 leader 执行、paxos 全集群复制的动作,无法单机灰度;
故障发生 (3.4)
正常情况下,trim 会把保留区间稳在大约 500 个,prune 永远不会被激活;告警是因为两种机制同时没起作用:
- trim 失效:floor 被
min_last_epoch_clean或者 out OSD 钉死,导致保留区间拉长到数千 epoch; - prune 还未激活:保留区间被拉长到数千,但仍小于
mon_osdmap_full_prune_min(默认是 10,000)- 每个 epoch 仍存一份 full osdmap;
- 占空间 $=$ epoch数 $\times$ full-osdmap-size,线性持续增长;
一个案例:
- full osdmap 大小 2.12MB
- 每天新增约166个epoch
| 集群状态 | 哪个机制工作 | db 体积走势 |
|---|---|---|
| 正常(持续 clean) | 只有 trim,区间约 500 | 稳定在约 1 GiB(500 × 2.12 MB ≈ 1.04 GiB) |
| 轻度异常(floor 钉住 < ~60 天) | trim 停住,区间线性增长,prune 未激活 | 线性上涨(约 0.34 GiB/天),可能撞告警线 15GiB |
| 重度异常(floor 钉住 > ~60 天) | prune 接力,每 10 留 1 | 增速降为约 1/10 |
经常遇到的场景是:floor 被钉住较久,足以撑爆告警线,但又钉得不够久、未触发 prune 机制。
排查步骤(按序执行)(3.5)
- Mon db 体积 与 RocksDB 健康:选择一个 非leader mon 实例(因为所有 mon 实例通常一致)
1 | du --max-depth=1 -h /var/lib/ceph/mon |
- 查看 osdmap 区间,per-pool floors 等信息:在 mon leader 上执行
1 | ceph report > /tmp/report.json |
- 查看 full osdmap 大小
1 | ceph osd getmap -o /tmp/osdmap.current && ls -l /tmp/osdmap.current |
- 估算体积
机制 prune 未激活时:
(osdmap_last_committed - osdmap_first_committed + 1) * full_osdmap_size
若大约等于 mon db size (第一步 du 获取),则确认根因!
处理方案 (3.6)
- 激活 prune 机制(安全,简单):
- 虽然安全,但也建议先备份 mon db;备份一台就够,但注意备份时要停 mon 进程,因为运行中直接拷 RocksDB 可能拿到不一致快照;
- 步骤:a.
ceph tell 'mon.*' injectargs --mon_osdmap_full_prune_min=8000(注意:只要能激活 prune 机制,设置 8000 还是 2000 并无区别,不会影响 prune 后 mon db 的体积;影响体积的是mon_osdmap_full_prune_interval,它是压缩密度);b. 用ceph report观察osdmap_manifest推进(first_pinned/last_pinned),等待压缩完成(完成标志是last_pinned逼近last_committed - 500;prune 每 tick 5 秒推进 1 个间隔,8,000 区间需约 1 小时);c.ceph tell 'mon.*' compact; - 注:如前所述,保留区间没变,信息没有任何丢失,只是压缩了存储,删除冗余 full osdmap,可由最近 pinned full osdmap 和增量原样重构(时间换空间)。
- 找出 floor 被钉住的根因(彻底):找出垫底项(per-pool floor / 长期 down OSD / 未 rm 的 out OSD):
- 降级 pg:尽快恢复clean
- down OSD: 尽快 out,迁数据后
osd rm
- 强制裁剪(风险):设置
mon_osd_force_trim_to;更早的 osdmap 永久不可取,落后 OSD 只能全量同步,非紧急不用! - 调高告警线(消音):设置
mon_data_size_warn到更大值,例如 30GiB;
小结 (4)
- 存储层:
ceph tell mon.{id} compact只回收“删了没缩”的部分(RocksDB内部机制),对真实存量数据无效; - 共识层:通过
paxos_min,paxos_trim_min/max控制 paxos log 的保留区间长度,占很多空间的可能性不大; - 服务层:最可能导致 mon db 体积增大的是 osdmap:相关机制有两套,trim 和 prune;
- trim 缩短保留区间:类似于共识层的 paxos log 的保留区间裁剪,删除 osdmap
first_committed到 floor 之间的 epoch,然后 floor 成为新的first_committed(由此缩短保留区间); - 当 floor 被钉死时,区间持续增长,导致 mon db 体积超限;而 floor 被钉死的原因通常是集群不健康:floor = min(所有 pool 的 last_clean, down/out osd 的依赖);
- 所以,治本的办法是尽快让集群恢复到健康状态:所有 pool 的 pg 都恢复到 clean;down/out osd 迁移完后 remove;这些都依赖数据迁移;
- 线上通常要控制数据迁移速度,所以,有的时候不能尽快“治本”;此时,可以通过 prune 机制降低存储密度;