Ceph 集群运维实战:常见命令、故障诊断与性能优化

命令表最大的问题不是太长,而是不告诉你哪一条会后悔。这篇把 Ceph 命令分三类,只留真正改变判断的十几条,并说清哪些动作是不可逆的。

技术文章约 29 分钟7 次阅读

#开篇:先回答"这套系统凭什么不乱"

Ceph 集群运维要解决的核心问题只有一个:当数据被打散存到几十块盘上之后,任何一次 OSD 上下线、扩容或故障替换,你凭什么确信数据不会丢、业务不会断、集群最终还能回到均衡? 答案来自 Ceph 最底层的一个机制:任何文件在进入集群时都会被切成固定大小的对象(object),对象按哈希归入逻辑容器 PG(Placement Group,归置组),PG 再由 CRUSH 算法映射到一组具体的 OSD;Monitor 用共识协议维护这张"集群地图",客户端拿到地图后直接读写,不需要每次询问 Monitor。一切运维命令,本质上都是在操控"对象 → PG → OSD"这套映射关系——读命令看映射的现状,问命令算映射的结果,动命令改映射的前提。

我刚接手 Ceph 时也收集过一份命令清单,一百多条,按"日常 / 管理 / 诊断 / 优化"分了四类。用了半年之后发现一个尴尬的事实:清单里真正在关键时刻救过我的不超过十几条,而其中三条曾经差点把事情弄得更糟。问题不在于清单太长,在于它没有标注哪一条是"问",哪一条是"动"。这篇文章先把底层机制讲清楚,再给出完整可复现的命令序列,让每一条"动"类命令都能从机制上解释它为什么存在、代价是什么。

#原理层:从数据落盘推导出每一条命令

#数据如何切片:对象、PG 与 OSD

Ceph 不保存传统意义上的"文件",它把一切数据都切成对象(object)。对象有固定大小(块设备默认 4 MiB,由 rbd 等上层按需设置),有全局唯一的 ID,落到磁盘上就是一块带数据的文件。一个 1 GiB 的镜像,在 Ceph 里就是几百个对象。

问题随之而来:集群里有几万个对象、几十块盘,如果每次写操作都要查一次"这个对象该放哪",那个查询中心就是性能和可用性的单点。Ceph 的选择是分两层做映射:

  • 第一层,对象 → PG:对对象 ID 做哈希,把对象归入某个 PG。PG 是对象的逻辑聚合,一个 PG 里的对象共享同一份副本策略和放置决定。PG 数量由 pool 的 pg_num 决定,创建池时指定。
  • 第二层,PG → OSD:PG 不绑定机器,而是把 pgid 喂给 CRUSH 算法,由 CRUSH 在 OSD 拓扑里挑出一组 OSD 作为该 PG 的副本位置(3 副本就挑 3 个)。

为什么中间要垫一层 PG,而不是让对象直接映射到 OSD?两个理由:

  1. 元数据量:如果映射粒度是对象,集群地图要记录几十亿条映射;垫一层 PG 之后,记录的是"每个 PG 落在哪些 OSD",而 PG 数通常只有几百到几千,元数据小到可以放进每个客户端的内存。
  2. 迁移粒度:故障、扩容引起的分布变化以 PG 为单位整体搬迁,而不是逐对象查询。一个 PG 迁移就是一组对象一起搬家,迁移任务的数量可控。

顺带解释两个池级参数为什么重要。pg_num 是对象哈希后可能落入的 PG 总数:对象 ID 哈希后对 pg_num 取模(或掩码),所以 pg_num 取 2 的幂能让哈希结果均匀分布,这也是 ceph -s 里 non-power-of-two pg_num 告警的来源。pgp_num 是"实际参与放置计算"的 PG 数,正常情况下与 pg_num 一致;扩容时先把 pg_num 翻倍,新版本 pgp_num 自动跟随,旧版本需要两条命令分别设置。增大 pg_num 时每个 PG 会分裂(split)成更小的 PG——分裂只是重新归类对象、不复制数据,但可能让少量对象在瞬间发生跨 OSD 迁移,所以生产上通常一次翻倍、观察迁移节奏后再动。缩小池时顺序相反:先降 pgp_num 再降 pg_num,否则会出现"PG 数量已减少、部分对象仍停留在旧放置"的中间状态。

#CRUSH:不需要中心化查表的放置算法

CRUSH(Controlled Replication Under Scalable Hashing,可扩展哈希下的受控复制)是 Ceph 放置逻辑的核心。它的输入是:pgid、CRUSH 地图(OSD 的树状拓扑加权重)、放置规则(rule:副本数、故障域)、随机种子;输出是一串有序的 OSD 列表。

这个算法最关键的性质是确定性:同样的输入永远得到同样的输出。这意味着任何一个客户端,只要持有最新版的 CRUSH 地图,就能独立算出一个 PG 应该放在哪三个 OSD 上——不需要去问 Monitor"这个 PG 放哪"。这正是 Ceph 数据面高性能的来源:Monitor 只在控制面发布地图,不参与每次 IO 的寻址。

CRUSH 地图用一棵树描述物理拓扑,典型层级是:root → 机架(rack)→ 主机(host)→ OSD。每条边上挂着权重(weight),OSD 的权重默认等于它的容量占集群总容量的比例。算法从树根一路往下走,按权重做加权随机选择,每选出一个 OSD 就"扣除"它下面的权重,保证同一条选择路径不会重复选出同一个故障域里的两块盘。

#Monitor:共识与集群地图

Monitor(mon)是 Ceph 的控制面。它维护五张集群地图:Monitor Map、OSD Map、PG Map、CRUSH Map、MGR Map。任何拓扑变化——新 OSD 加入、权重调整、池创建——都要先写进地图,再通过以下机制向全集群广播:

  • 共识:Monitor 组成多数派(quorum)。所有地图更新必须在 quorum 内部通过 Paxos 变体达成一致——leader 提出新 epoch 的地图,多数 mon 确认后才算生效。这也是为什么集群至少要有奇数个 monitor(生产常见 3 个,可容忍挂 1 个),以及为什么 monitor 之间的时钟偏差(clock skew)是严肃告警:Paxos 的租约与选举对时钟敏感,偏差过大时 mon 会互相把对方踢出 quorum。
  • 版本与订阅:每张地图带递增的 epoch 号。OSD 与客户端订阅地图,收到新 epoch 后自行比对、应用。客户端写数据时如果拿的是过期地图,会收到 OSD 的"地图过期"反馈,刷新后再写。

这解释了运维中的一个常见现象:mon 抖动或 mon 之间网络分区时,即使所有 OSD 都活着,客户端也可能短暂报错——因为数据面依赖控制面发布的最新地图来建立连接和放置决定。控制面不稳,数据面一定受牵连。

#副本数与故障域:数据安全感的真实来源

一个对象最终有几个副本,由 pool 的 size 决定(默认 3)。写入路径是:客户端把数据发给该 PG 的 primary OSD,primary 同时向另外两个副本 OSD 推数据,全部落盘确认后客户端才收到成功返回。所以副本数既是数据冗余度,也是单次 IO 的写放大系数。

但"有三个副本"不等于"三份都安全"。关键在于故障域(failure domain):三个副本如果落在一台机器的三块盘上,机器一垮三份全没。因此 CRUSH rule 必须指定故障域——小集群通常为 host(副本必须跨主机),大集群为 rack(副本跨机架),跨机房部署则为 datacenter。CRUSH 在选择 OSD 时强制每个副本落在不同的故障域单元里。

由此得出两个直接结论:

  1. size 决定能扛几块盘同时坏:3 副本加 host 故障域,同一 PG 最多能容忍两个副本所在的故障域同时失效,第三个副本仍在服务。
  2. min_size 决定"降级写"的底线:pool 的 min_size(size=3 时常见为 2)是 PG 继续服务 IO 的最小副本数。副本数掉到 min_size 以下,PG 会拒绝写入以保护数据——这是"宁可不可用也不丢数据"的闸门。

运维中大量事故的根源,是把 size=3 当成无限保险,忽略了故障域与 min_size 才是真正的安全边界。当一个 PG 只剩 2 个副本且这 2 个副本还在同一机架时,"还能写"与"随时会丢"是同时成立的。

#OSD 状态机:up/down 与 in/out 是两个维度

OSD 有两个互不相同的开关,混用它们是事故高发区:

维度含义控制方法
up / down守护进程是否在运行、能否与集群通信由 mon 通过心跳自动判定
in / out是否参与数据分布(是否被 CRUSH 选为放置目标)ceph osd out/in <id> 手动控制

两两组合出的四种状态:

  • up + in:正常服役。
  • down + in:进程挂了,但它承载的 PG 还"记在它账上"——集群进入 degraded(降级)状态,缺口由其它副本临时顶上服务;进程回来(up)后 OSD 会自动补齐副本。这是最常见的故障形态。
  • up + out:进程活着,但已从放置候选里剔除。CRUSH 不再把 PG 放给它,它上面的 PG 正在迁走。这就是"主动下线 / 退役 / 换盘"时想要的状态。
  • down + out:既不参与放置、进程也不在。数据已经(或应当已经)迁走,可以安全销毁清理。

ceph osd out 的语义是"把这块 OSD 从 CRUSH 放置里摘除",不是关进程。摘除后,原本映射到它的 PG 会重算 acting set(3 副本里它那一份被替换成别的 OSD),数据开始往新成员迁移。这就是"out 会触发一次完整回填"的由来。

与 in/out 并列的还有权重:CRUSH weight 决定该 OSD 在放置中被选中的相对概率,默认等于容量占比,扩容缩容的本质是改这个值;ceph osd reweight 调整的是另一层临时权重,用于容量均衡但 PG 数量不均衡时的微调。

#Rebalance 的真实过程:扩容与故障替换时数据如何"自己动"

把上述机制串起来,一次扩容或故障替换的完整因果链是:

  1. 前提变化:新 OSD 加入 CRUSH 地图(weight 从 0 变成正值),或某 OSD 被 out、或 weight 被调整——CRUSH 地图变了。
  2. 映射重算:每个 PG 都会用新地图重新计算放置列表。一部分 PG 的结果变了,它们被标记为 misplaced(位置不对,但数据没少)。
  3. 迁移执行:OSD 对 misplaced PG 发起迁移,分两种:recovery 是目标 OSD 上已有部分数据,只需补齐差异对象;backfill 是目标 OSD 上完全没有该 PG 的数据,需要整 PG 复制过去(新盘加入时大量发生)。
  4. 收敛判定:所有 PG 回到 active+clean,degraded 与 misplaced 计数归零,再分布完成。

因此扩容不是"把新盘填满",而是"把旧盘上一部分 PG 搬家到新盘",让每个 OSD 承载的 PG 数与它的权重成正比。扩容后你会短暂看到 recovery/backfill 计数上升、部分 PG 处于 backfilling 状态,这是正常且必要的。故障替换的流程只是把第 1 步换成"osd out → 等待迁移 → 销毁 → 新盘重新加入",第 2 到 4 步完全一样。

理解了这条因果链,运维命令就不再是背下来的,而是推导出来的:每条"动"类命令都改变 CRUSH 地图或 OSD 状态,从而改变 PG 的映射,从而触发数据迁移。下面操作层的每一条序列,都能对上号。

#操作层:完整可复现的命令序列

#环境与前置条件

本文命令以 Ceph Reef(18.2.x) 为例,部署方式为 cephadm(容器化部署);绝大多数命令在 Nautilus(14)以来的版本行为一致,差异处会单独注明。执行前需要:

  • 管理节点可执行 ceph 命令。cephadm 部署通常用 cephadm shell 进入容器执行,或在装有 ceph-common 的节点上直接执行。
  • 持有管理员权限的 cephx 密钥(默认 client.admin)。
  • 至少 3 个 Monitor、若干 OSD 的集群;触发"动"类操作前先在非关键 pool 上演练。
  • 所有节点 NTP/PTP 对时正常——这不是可选项,见"边界与坑"。

先确认版本与当前状态:

ceph versions
ceph -s

#命令三分法:先分清"读、问、动"

把命令按"它能否改变集群状态"分成三类,是开始任何操作前的第一件事。这张表把"我记得这条命令"和"我知道这条命令的代价"分开,后者才是值钱的:

类别特征例子什么时候用
读只看状态,不动集群ceph -s、ceph osd df tree、ceph osd perf、ceph pg dump_stuck任何时候,越多越好
问触发一次计算或探测,但不改变数据分布ceph osd map <pool> <object>、ceph pg <pgid> query、ceph health detail定位问题时
动改变集群状态,部分不可逆ceph osd out、ceph osd crush remove、ceph osd destroy、ceph pg repair、ceph osd lost说清楚代价之后

三类里最容易建立感觉的是"问"类:它把 CRUSH 的确定性翻译成看得见的输出。例如想知道某个对象实际落在哪三块盘上:

ceph osd map <pool-name> <object-name>
# osdmap e92 pool 'rbd-demo' (3) object 'journal-1e2f' -> pg 3.9a (3.9a) -> up ([7,3,11], p7) acting ([7,3,11], p7)

输出读作:这个对象属于 pool 3 的 pg 3.9a,当前 up set 是 OSD 7/3/11,primary 是 7。"问"类命令不修改任何状态,可以放心反复跑——用多了,你对"PG 会跟着地图变"的感受会比任何文档都准。

#健康检查命令序列

ceph -s 是绝大多数人第一条学会的命令,也是最容易被读得太快的命令。它有两种读法——读"现在是什么",或者读"最近变过什么":

cluster:
  id:     <cluster-uuid>
  health: HEALTH_WARN
          1 pool(s) have non-power-of-two pg_num
services:
  mon: 3 daemons, quorum a,b,c (age 4d)
  mgr: x(active, since 3d), standbys: y
  osd: 12 osds: 12 up (since 2d), 12 in (since 2d)
data:
  pools:   3 pools, 289 pgs
  objects: 1.20M objects, 4.1 TiB
  usage:   5.6 TiB used, 12 TiB / 18 TiB avail
  pgs:     289 active+clean

since 2d / since 3d / age 4d 是最容易被忽略、也最有信息量的部分:它记录的是"最后一次状态变化距今多久",等价于一个免费的变更审计。

  • osd: 12 osds: 12 up (since 2d):如果这个数字比你上次巡检时小,说明两天前有 OSD 掉过又回来了,而你当时可能完全没收到通知。
  • mon: 3 daemons, quorum a,b,c (age 4d):age 突然变小,说明 Monitor 发生过重新选主。
  • pgs: 289 active+clean 全是 active+clean 时没有信息量;一旦不是,这一行就是优先级最高的信息。

值得养成的习惯是把 ceph -s 输出连同时间戳定期落盘,用来看"变过什么",而不是"现在是什么"。能看出沉默的那部分,才算读懂了这条命令。

第二层是 health detail、osd df tree 与 osd perf:

ceph health detail
ceph osd df tree
ceph osd perf
  • ceph health detail 给的是可执行的提示——哪条规则触发、涉及哪个池、哪块 OSD。HEALTH_WARN 有好几十种,靠记没用,看 detail。
  • ceph osd df tree 一次回答三件事:容量分布是否均衡、每块 OSD 的 %USE 是否接近、PG 数是否均衡。注意 VAR 一列——容量不均衡和 PG 数不均衡时,修法完全不同(前者改 crush weight,后者改 pg_num 或交给 autoscaler)。
  • ceph osd perf 给每块 OSD 的 apply/commit 延迟。集群"整体变慢但 -s 正常"时,先看这里再谈调优。

控制面检查:

ceph mon stat
ceph quorum_status
ceph mgr stat

mon_stat 显示各 monitor 的 rank 与地址,quorum_status 显示当前 quorum 成员与 leader。Monitor 若不在 quorum 里,控制面就是"残废"的,数据面迟早出问题。

#OSD 管理命令序列

先建立现状,再操作:

ceph osd tree                     # 所有 OSD 的 up/down、in/out 与 host 归属
ceph osd find <id>                # 某 OSD 所在主机、盘符
ceph osd metadata <id> | grep -i host

下线一块 OSD 并等待数据迁走(可逆操作的标准序列):

ceph osd out <id>                 # 让数据从这块 OSD 迁出
ceph -s                           # 观察 recovery / backfill 计数
# 等待所有 PG 回到 active+clean 后再进行下一步

osd out 是可逆的(ceph osd in <id> 让它回来),但代价是触发一次完整回填、集群负载显著上升。短期维护(比如换网线、重启节点)且不想触发迁移时,用 noout 而不是 out:

ceph osd set noout                # 禁止自动 mark out,维护窗口用
# 维护动作(换件 / 重启节点 / 固件升级等)
ceph osd unset noout              # 维护结束必须解除

noout 是"禁止 OSD 因为心跳失败被自动踢出放置候选"的开关,只适合短窗口。它不停止已经开始的迁移,也不治疗故障本身;长期挂着 noout 会把 degraded 风险藏起来。

永久移除一块 OSD(退役或换盘前,必须等上一步 out 后的回填完成):

ceph osd destroy <id> --yes-i-really-mean-it      # 销毁该 OSD 的元数据 / 身份,不可逆
ceph osd purge  <id> --yes-i-really-mean-it       # destroy + crush remove + auth 清理一步完成,不可逆

destroy 与 purge 在较新版本第一次执行必须带 --yes-i-really-mean-it,第二次可用 --idempotent。它们不会问你确定不确定,只问你打不打那个参数。

调整权重:

ceph osd crush reweight osd.<id> <weight>     # 调整 CRUSH 权重(相对容量)
ceph osd crush reweight-all                   # 把所有 OSD 权重重置为容量占比
ceph osd reweight <id> <weight>               # 临时 reweight,微调 PG 分布

crush reweight 与 osd reweight 是两套机制:前者改 CRUSH 地图里的静态权重(扩容缩容用它),后者是 OSD 层面的临时权重(容量均衡但 PG 倾斜时用它)。用错层级,问题不会消失,只会换个形状。

#Pool 管理命令序列

Pool 是副本策略、配额与逻辑隔离的边界。创建与查看:

ceph osd pool create <pool-name> <pg-num> [<pgp-num>] [replicated] [<crush-rule>]
ceph osd pool ls
ceph osd pool ls detail
ceph osd pool get <pool> size
ceph osd pool get <pool> min_size
ceph osd pool get <pool> pg_num
ceph osd pool autoscale-status

创建示例(3 副本、256 个 PG、使用默认 replicated rule;pgp_num 通常与 pg_num 一致,新版本自动跟随):

ceph osd pool create rbd-demo 256 replicated
ceph osd pool application enable rbd-demo rbd     # 声明用途:rbd / cephfs / rgw

设置副本数与最小副本数:

ceph osd pool set <pool> size 3
ceph osd pool set <pool> min_size 2

副本数改变会立刻触发数据迁移:size 调大,缺失副本开始补齐(recovery);size 调小,多余副本被丢弃。先确认当前写入模式能承受迁移再改。min_size 的设置原则是"小于 size",给降级留空间,同时确保 min_size < size 时 PG 在副本不足时仍能按你的意愿选择继续服务或拒写。

调整 PG 数量(扩容后 PG 不足通常由 autoscaler 处理,手动调整的序列):

ceph osd pool set <pool> pg_num 512    # 先增大 pg_num(触发 PG split)
ceph osd pool set <pool> pgp_num 512   # pgp_num 同步(旧版本需要手动,新版本自动)

PG 数量应当接近 2 的幂,这是 Ceph 官方建议,也是 ceph -s 里 non-power-of-two pg_num 告警的来源。缩小 pg_num 需要先把 pgp_num 降下来、再降 pg_num,且只能合并到较小值,不应作为常规操作。

PG 数量的日常调控可以交给 autoscaler:从 Octopus(15)开始 pg_autoscaler 模块默认启用,ceph osd pool autoscale-status 会给出每个池的当前 pg_num、目标 pg_num 与建议。每个池有三种模式:off(完全手动)、warn(只提示不自动改)、on(按目标自动调整)。新池建议先保持默认,确认自动调整的节奏不会吓到业务再切 on:

ceph osd pool set <pool> pg_autoscale_mode on

配额与重命名:

ceph osd pool set-quota <pool> max_bytes <bytes>
ceph osd pool set-quota <pool> max_objects <count>
ceph osd pool rename <old> <new>

删除 pool 是最高级别的不可逆操作,语法要求"重复两次池名 + 强制参数"才肯执行:

ceph osd pool rm <pool> <pool> --yes-i-really-really-mean-it

删除前确认:该 pool 是否有快照、是否有客户端还在挂载、备份是否已做。命令可以设计成防手滑,但防不了手快。

#扩容命令序列

以 cephadm 为例,加一台存储节点并让它承载 OSD:

ceph orch host add <hostname> <ip-address>          # 把新节点纳入编排
ceph orch host ls                                    # 确认新节点已注册
ceph orch apply osd --all-available-devices          # 把新节点所有可用盘加为 OSD

生产环境更推荐用设备规格文件(device spec),精确控制哪些盘可用、哪些盘保留:

cat > osd-spec.yaml <<'EOF'
service_type: osd
service_id: osd-all
placement:
  hosts:
    - <hostname>
data_devices:
  all: true
  limit: 8
EOF
ceph orch apply osd -i osd-spec.yaml

新 OSD 一上线,CRUSH 地图变化,rebalance 自动开始。这一步不需要任何额外触发命令:

ceph -s                       # 观察 pgs 行:recovery / backfill 计数
ceph osd df tree              # 观察新 OSD 的 %USE 与 PGS 逐步上升

扩容后检查均衡:新 OSD 的 %USE 与 PGS 应该逐步向老 OSD 收敛。如果长时间不均衡,先检查 ceph osd crush reweight-all 是否被跳过、新 OSD 权重是否为 0、pool 的 autoscaler 是否被关。

#故障处理与 OSD 替换命令序列

故障处理的第一原则:按层走,不要跳。Ceph 的怪处在于,几乎所有的"存储问题"最后都能追到非存储的地方——网络、时钟、内存、甚至 BIOS 的电源策略。诊断路径先分层:

# 1) 先分层:是数据面还是控制面?
ceph -s
ceph health detail

# 2) 如果是 OSD 相关,先看单块盘
ceph osd tree                      # 有没有 down / out
ceph osd perf                      # 是不是某几块特别慢
ceph osd metadata <id> | grep -i host   # 慢的几块是否在同一台机器

# 3) 同一台机器上多块盘同时慢 → 往上走一层
#    (网卡错包、CPU 抢占、内存不足、NUMA 绑核、RAID 卡缓存策略)

# 4) 控制面:Monitor 的时钟与网络
ceph mon stat
ceph quorum_status
# 容器化部署下查看单 mon 状态需进入对应容器:
ceph daemon mon.<name> mon_status | head -20

经验上最高频的三个根因:跨机架 / 跨机房带宽被打满、单机上的中断集中在少数 CPU 核、以及"以为对时是自动的"。Monitor 对时间敏感,clock skew 相关告警很多人第一反应是加硬件,其实先核 NTP。

一块盘确认物理损坏后的替换序列(以 cephadm + 新盘复用同主机为例):

# 0) 进入维护窗口,防止迁移期间其它盘意外 down 引发连锁
ceph osd set noout

# 1) 让故障 OSD 的数据迁走(如果它还没 out)
ceph osd out <id>
ceph -s                                   # 等待 recovery / backfill 归零,所有 PG active+clean

# 2) 停进程并销毁身份
ceph orch daemon stop osd.<id>            # cephadm 环境停 daemon
ceph osd destroy <id> --yes-i-really-mean-it
ceph osd purge  <id> --yes-i-really-mean-it

# 3) 清掉旧盘上的 LVM 元数据(盘还在本机时)
ceph-volume lvm zap /dev/sdX --destroy

# 4) 拔出旧盘 / 插入新盘,让编排重新发现并部署 OSD
ceph orch daemon add osd <hostname>:/dev/sdY
# 或重新 apply 设备规格,让新的可用盘被自动接管

# 5) 解除维护标志,观察自动 backfill
ceph osd unset noout
ceph -s

关键纪律:不要跳步。out 之后必须等回填完成,再 destroy/purge。顺序错一步的代价通常是数据,不是时间。purge 一步完成 out+crush remove+auth 清理,但它不等回填——如果你在数据没迁完时就 purge,那些 PG 会直接进入 degraded 并从其它副本重建,等于人为制造一次数据丢失窗口。

把"顺序"固化成脚本,并在每次执行前显式确认:

osd_remove() {
  local id="$1"
  echo "即将执行:out -> 等待回填 -> crush remove -> destroy"
  echo "当前该 OSD 上的 PG 数:$(ceph pg ls-by-osd osd.$id 2>/dev/null | wc -l)"
  read -r -p "确认继续?(yes) " ok
  [ "$ok" = yes ] || return 1
  ceph osd out "$id"
  while ceph -s | grep -q "recovery"; do sleep 30; done   # 等迁完,不要跳步
  ceph osd crush remove "osd.$id"
}

那个 while 循环看起来笨,但它是把"我以为迁完了"换成"命令告诉我迁完了"。更稳妥的收敛判定是等待全部 PG 进入 active+clean:

while :; do
  s=$(ceph pg stat)
  echo "$s"
  echo "$s" | grep -q "active+clean" && ! echo "$s" | grep -qE "degraded|misplaced|backfill|recovery" && break
  sleep 30
done

#数据恢复监控命令序列

迁移一旦开始,监控点集中在三处:PG 状态、卡住的 PG、单 OSD 上的 PG 分布。

ceph -s                        # pgs 行的 degraded / misplaced / recovery 计数
ceph pg stat                   # 全部 PG 的状态汇总,如 "289 pgs: 289 active+clean"
ceph pg dump_stuck [inactive|unclean|stale|undersized|degraded|peered]
ceph pg ls                     # 列出所有 PG 及其状态
ceph pg ls-by-osd osd.<id>     # 某 OSD 承载的 PG
ceph pg <pool-id>.<pg-num> query   # 单个 PG 的详细状态,如 ceph pg 3.10 query
ceph osd df tree               # 每 OSD 的 %USE 与 PGS 是否均衡

ceph pg dump_stuck 输出的是"卡住超过阈值的 PG",是恢复期间最该盯的命令:inactive 表示 PG 无法服务 IO,unclean 表示副本数不足,stale 表示 primary 长时间没向 mon 上报——stale 往往指向网络或 mon 侧问题,而不是 OSD 本身。

恢复相关的调参命令(读):

ceph config get osd osd_max_backfills
ceph config get osd osd_recovery_max_active
ceph config get osd osd_recovery_sleep
ceph config get osd osd_client_op_priority
ceph config get osd osd_recovery_op_priority

需要调整时用 ceph config set osd <key> <value>,改完观察 ceph -s 的恢复节奏与客户端延迟偏离开销。见"边界与坑"——这些参数是在选立场,不是在调优。

#验证层:怎么确认我做对了

运维动作做完不算完,要用可核查的判定标准确认"做对了"。

#ceph -s 与 ceph health 的判定标准

理想状态:

health: HEALTH_OK
osd:  12 osds: 12 up, 12 in
pgs:  289 active+clean

判定标准逐条核对:

  • HEALTH_OK:没有任何 WARN/ERR。有 WARN 时必须 ceph health detail 看具体原因,不能以"反正能写"带过。
  • OSD 一行的 up 与 in 两个数字:up 数等于 in 数,且都等于期望的总数。任何 down 都是第一优先级;任何 out 若不是你主动做的,都是事故。
  • pgs 全部 active+clean:这是集群"健康"的最终定义。只要还有 recovery、backfill、degraded、misplaced、inactive、peered、stale 中的任何一种,说明数据仍在迁移或副本不足,动作还没有收敛。

#ceph df 与 ceph osd df 的判定标准

ceph df
ceph osd df tree
  • ceph df 看总量:USED、AVAIL、%USED 反映集群容量水位。超过 70% 到 80% 后,rebalance 与 recovery 的余地急剧变小,扩容的决策点应当在此之前。
  • ceph osd df tree 看分布:每块 OSD 的 %USE 应当彼此接近,PGS 一列应当均匀。VAR 越大说明越不均衡——容量不均衡改 crush weight,PG 数不均衡则要查 pg_num 与 autoscaler。判定标准不是一个固定阈值,而是"与上次巡检相比是否在恶化、是否明显偏离均值"。均衡是常态,短暂不均衡(扩容后迁移期)是过程。

#PG 状态的判定标准

  • active+clean:目标态。
  • degraded:存在副本缺口(OSD down 或数据缺失)。数据未丢,但冗余度下降,窗口期风险升高。
  • misplaced:副本都在,但位置不符合当前 CRUSH 地图(扩容、换盘后常见)。迁移会清掉它。
  • peered / undersized:副本数不够组成完整 acting set,PG 还活着但很脆弱;inactive 则是完全不能服务 IO。
  • stale:mon 与 primary 失联超过阈值,问题通常在网络或 mon 侧。

任何"非 clean"状态持续过久,就用 ceph pg dump_stuck 把卡住的 PG 抓出来,再用 ceph pg <pgid> query 看它到底卡在哪一步:

ceph pg dump_stuck unclean
# pg 3.9a is active+remapped+backfill_wait, acting [7,5,3]
# pg 3.b1 is down+peered, acting [7]

第一行表示 pg 3.9a 因为新成员加入处在回填等待中,acting set 已经换成 7/5/3——这是扩容后的正常中间态;第二行表示 pg 3.b1 的 acting set 只剩一个 OSD 7,且该 OSD 状态是 down——这是真故障,需要立刻查 OSD 7 的进程与网络。同样的输出,能不能分辨"正常迁移"与"真的卡住",是恢复期排查的及格线。

#三个必须真跑的演练

预案这种东西,读起来都懂,执行起来都会卡。至少把下面三个动作在非关键 pool 上真跑一遍:

  1. 一次 osd out 到回填完成的完整流程——量出"多久迁完"。这个数字直接决定你的恢复窗口预算:集群坏一块盘后要多久回到安全态,业务方需要容忍多长的降级期。
  2. 一次 Monitor 重启——确认重启后 quorum_status 恢复、客户端不断连太久。
  3. 一次"盘坏了"的模拟——osd destroy 一块可丢弃的 OSD,再把它重新加回来,走一遍 ceph-volume / ceph orch daemon add osd 的完整路径。

这三个动作的价值不在于"到时候会做",而在于你会发现一些文档里没写、但一定会卡住你的东西:认证残留、CRUSH 权重没重置、systemd / daemon 名字对不上,诸如此类。演练时把实际耗时与踩的坑记进运维文档,那才是预案真正生效的部分。

#边界与坑

#不可逆命令清单与正确顺序

把"动"类命令的可逆性标清楚,动手前以自己的版本文档复核:

命令做了什么可逆吗代价
ceph osd out <id>让数据从这块 OSD 迁出可逆(osd in)触发一次完整回填,集群负载显著上升
ceph osd crush remove osd.<id>从拓扑里摘掉可重建,但要重算若在 out 未完成时执行,会连同未迁完的数据一起失去
ceph osd destroy <id>销毁该 OSD 的元数据 / 身份不可逆该 OSD 上的数据被视为不存在
ceph osd purge <id>一步做完 out+crush remove+auth 清理不可逆不等回填,顺序错了就没有回头路
ceph pg repair <pgid>以 primary 为准修复副本不可逆地覆盖如果 primary 本身是坏的那一份,会把坏数据复制到所有副本
ceph osd lost <id> --yes-i-really-mean-it声明某 OSD 的数据永久丢失不可逆之后 PG 会把缺失副本当作"没有"来重建

要特别说 pg repair:它在"某个副本明显损坏、且你确信 primary 是好的"时是正确工具;在你不知道哪一份是对的的时候用它,等于随机选一份覆盖其余所有副本。osd lost 同理——它的一次性参数 --yes-i-really-mean-it 已经把话说到这个份上,除非你确认坏盘数据必须被放弃(例如盘已物理损毁且无其它副本完整),否则不要碰。这类命令的共同特征是:它们不会问你确定不确定,只问你打不打那个参数。

一个可操作的习惯:把上面这些命令包一层再放进脚本,让执行前必须显式确认——见操作层里的 osd_remove() 函数。关键的纪律只有一个:out 之后等回填,等完再 destroy;没有迁完的数据,任何一步都不能跳过。

#恢复参数是在选立场,不是调优

osd_max_backfills、osd_recovery_max_active、osd_recovery_sleep 控制恢复的并行度与节奏;osd_client_op_priority 与 osd_recovery_op_priority 控制客户端 IO 与恢复 IO 的相对优先级。把恢复调快,前台业务的尾延迟就变差;调慢,集群处于冗余不足状态的时间就变长。这不是"调优",是分配:你决定把这段窗口的不适感分给用户,还是分给风险。

这件事没有统一答案,但有统一的做法——把当前取值和你为什么这么取写进运维文档。半年后的你(或者接手的同事)需要知道这个数字是"某次故障时临时调的",而不是"一直这样"。否则下一次故障,面对一个被调成极限的值,你既不敢还原,也不敢继续加。

#扩容的三个固定套路问题

新 OSD 加入后长时间不参与分布,先查它的 weight 是否为 0(ceph osd tree 里 WEIGHT 列);某个池的 PG 数长期只有个位数,扩容收益会被哈希噪声吃掉大半,应该先按上文序列把 pg_num 翻倍、让分布粒度变细;autoscaler 开着却仍不均衡,多半是池的 pg_autoscale_mode 被设成了 off。记住:扩容的对象是"PG 的重新分布",不是"盘的容量填满"——衡量成功的标准是 ceph osd df tree 里 VAR 的收敛,而不是新盘 %USE 单独降低。

#三个高频失败模式

  1. 把 out 当"摘盘"用,直接 purge:purge 不等回填,数据在迁走前就被宣布不存在,PG 被迫从其它副本重建——人为制造数据丢失窗口。正确顺序永远是 out → 等 active+clean → 再销毁。
  2. 长期挂 noout:noout 适合短维护窗口。挂着它,OSD 死了也不会被移出放置,degraded 状态悄悄延长,等于把风险从"看得见"变成"看不见"。维护结束必须 unset。
  3. 忽略时钟与网络:clock skew 告警、mon 反复选主、stale PG,先查 NTP 与网络再谈其它。Monitor 的共识协议对时钟和网络分区敏感,这是控制面的地基。

#什么时候不该照搬本文

  • 生产环境触发"动"类命令前,本文给的是骨架序列,完整语义必须对照你当前版本的手册复核——命令参数在版本间有差异(例如 destroy / purge 的确认参数、pgp_num 是否自动跟随)。
  • 容量规划与硬件选型是另一套前提(盘型、网络、副本策略),脱离它们给数字是误导。这里只谈操作与判定,不替你做选型。
  • 监控系统怎么搭没有展开。Prometheus + ceph_exporter 是可选项之一,但"看什么指标"比"用什么采集"重要得多——核心指标就是本文验证层那几条:health、osd up/in、PG 状态计数、osd df 的均衡度、osd perf 的延迟。
  • 本文没有给参数推荐值:没有负载画像的推荐值,是把试错成本转嫁给读者。先量出你自己的恢复窗口预算,再决定参数。

#收尾:能带走的一句话

如果只能从这篇带走一句话:把"我知道这条命令"升级成"我知道这条命令不可逆"。Ceph 的绝大多数事故不是不懂原理,而是在不可逆的那一步没有停两秒。原理层回答了你"凭什么相信这套系统",操作层给了你"每一步具体做什么",验证层给了你"怎么确认做对了"——剩下的事情,是带着对映射关系与迁移因果链的理解,把那十几条真正值钱的命令,一条一条在演练场上跑熟。

相关阅读

Related

留言讨论

0 条

还没有留言,来说点什么吧。

Ceph 集群运维实战:常见命令、故障诊断与性能优化 · LXH·BLOG