基于 Ceph 14.2.18源码(
mds/MDSMap.h,mds/MDSMap.cc,mds/FSMap.h,mds/FSMap.cc,mon/MDSMonitor.cc)分析
一、MDS 状态定义
所有 MDS 状态定义在 mds/MDSMap.h 的枚举 DaemonState 中(L62-L100):
cpp
// mds/MDSMap.h: L62-L100
typedef enum {
// ---- 不持有 rank 的 daemon 状态 ----
STATE_NULL // 空值
STATE_BOOT // 进程已启动,向 Mon 宣告自己存在
STATE_STANDBY // 空闲,等待 Monitor 分配
STATE_STANDBY_REPLAY // 正在 replay 某个 active rank,随时可接管
// ---- MDS rank 自身的状态(可能没有 daemon 持有)----
STATE_STOPPED // rank 曾存在,已正常停止,日志为空
STATE_CREATING // daemon 正在为新 rank 创建日志/元数据
STATE_STARTING // daemon 正在启动一个 stopped rank
STATE_REPLAY // 恢复场景:daemon 正在扫描之前失败实例的日志
STATE_RESOLVE // 恢复场景:消歧义分布式操作(import/rename等)
STATE_RECONNECT // 恢复场景:重新连接客户端
STATE_REJOIN // 恢复场景:重新 join 分布式缓存
STATE_CLIENTREPLAY // 重放客户端请求
STATE_ACTIVE // 正常工作状态
STATE_STOPPING // 正在导出元数据,准备退出
STATE_DNE // rank 不存在
// ---- daemon 向 Mon 上报的特殊状态 ----
STATE_DAMAGED // 磁盘/日志损坏,需要离线修复
} DaemonState;
重要区分:rank 的隐式状态
MDSMap.h 注释(L91-L99)明确说明:rank 还有三个隐式状态 ,不通过 DaemonState 枚举表示,而是通过 MDSMap 中的集合来判断:
| 隐式状态 | 判断方法 |
|---|---|
FAILED |
rank ∈ mds_map.failed 且没有 daemon 持有该 rank |
STOPPED |
rank ∈ mds_map.stopped 且没有 daemon 持有该 rank |
DNE |
rank 既不在 failed 也不在 stopped 中 |
MDSMap 的核心数据结构(MDSMap.h L196-L201):
cpp
std::set<mds_rank_t> in; // 集群中已定义的 rank 集合
std::set<mds_rank_t> failed; // 失败的 rank(等待新 standby 接管)
std::set<mds_rank_t> stopped, damaged;
std::map<mds_rank_t, mds_gid_t> up; // rank → 持有该 rank 的 daemon gid
std::map<mds_gid_t, mds_info_t> mds_info; // gid → daemon 详细信息
ceph fs status 显示 failed,本质上是显示 mds_map.failed 集合中的 rank,而非某个 daemon 的 DaemonState。
二、Monitor 中状态流转的完整路径
Monitor 通过两种途径修改 MDS 状态:
- 被动响应 :处理 MDS daemon 主动上报的 beacon 消息(
prepare_beacon()) - 主动检测 :定时 tick 检测 beacon 超时(
tick()→maybe_replace_gid())
2.1 MDS 主动驱动的状态变更:prepare_beacon()
mon/MDSMonitor.cc L536-L796,prepare_beacon() 是 Monitor 接收 MDS 心跳并处理状态变更请求的入口。
2.1.1 BOOT 流程(进程启动 → STANDBY)
cpp
// MDSMonitor.cc L587-L634
if (state == MDSMap::STATE_BOOT) {
// 1. 如果同名 MDS 已存在,先 fail 掉旧实例
if (g_conf()->mds_enforce_unique_name) {
while (mds_gid_t existing = pending.find_mds_gid_by_name(m->get_name())) {
fail_mds_gid(pending, existing); // 清除旧 gid
}
}
// 2. 创建新的 mds_info,初始状态为 STATE_STANDBY
MDSMap::mds_info_t new_info;
new_info.state = MDSMap::STATE_STANDBY;
pending.insert(new_info);
// 3. 初始化该 gid 的 beacon 时间戳
last_beacon[gid].stamp = mono_clock::now();
}
STATE_BOOT 是一个临时过渡状态,只出现在 MDS 向 Mon 发送的消息中,Monitor 处理后立即将其注册为 STATE_STANDBY,不会持久化 BOOT 状态。
2.1.2 MDS 上报状态变更(STANDBY → REPLAY → ... → ACTIVE)
cpp
// MDSMonitor.cc L756-L778
} else if (info.state != MDSMap::STATE_STANDBY && state != info.state &&
!MDSMap::state_transition_valid(info.state, state)) {
// 非法状态转换,拒绝
derr << "daemon reported invalid state transition" << dendl;
return true;
} else {
// 合法转换,直接记录 daemon 上报的新状态
pending.modify_daemon(gid, [state, seq](auto& info) {
info.state = state;
info.state_seq = seq;
});
}
关键约束 :状态转换合法性由 MDSMap::state_transition_valid() 验证(mds/MDSMap.cc L885-L909):
cpp
bool MDSMap::state_transition_valid(DaemonState prev, DaemonState next)
{
if (prev == MDSMap::STATE_REPLAY) {
// REPLAY 只能转到 RESOLVE 或 RECONNECT
if (next != MDSMap::STATE_RESOLVE && next != MDSMap::STATE_RECONNECT)
state_valid = false;
} else if (prev == MDSMap::STATE_REJOIN) {
// REJOIN 只能转到 ACTIVE/CLIENTREPLAY/STOPPED
if (next != STATE_ACTIVE && next != STATE_CLIENTREPLAY && next != STATE_STOPPED)
state_valid = false;
} else if (prev >= MDSMap::STATE_RESOLVE && prev < MDSMap::STATE_ACTIVE) {
// RESOLVE/RECONNECT/REJOIN/CLIENTREPLAY:只能按序前进一步
if (next != prev + 1)
state_valid = false;
}
return state_valid;
}
这意味着:RESOLVE → RECONNECT → REJOIN → CLIENTREPLAY → ACTIVE 必须严格按顺序,每步只能前进一个状态。Monitor 不会主动驱动 MDS 从 REPLAY 走到 RESOLVE 等后续状态,这些状态变更完全由 MDS daemon 自己通过发送 beacon 上报触发。
2.1.3 特殊状态处理
| 上报状态 | Monitor 处理逻辑 | 来源 |
|---|---|---|
STATE_STOPPED |
调用 fsmap.stop(gid):从 up 移除,加入 stopped,清理 standby-replay |
L686-L704 |
STATE_DAMAGED |
调用 fsmap.damaged(gid, epoch):加入 damaged,blacklist IP |
L707-L732 |
STATE_DNE |
调用 fail_mds_gid():走 erase() 逻辑,加入 failed |
L733-L749 |
2.1.4 laggy 标记清除
cpp
// MDSMonitor.cc L673-L680
if (info.laggy()) {
dout(1) << "prepare_beacon clearing laggy flag on " << addrs << dendl;
pending.modify_daemon(info.global_id, [](auto& info) {
info.clear_laggy();
});
}
只要 MDS daemon 还能发来 beacon,无论状态如何,Monitor 都会清除 laggy 标记。
2.2 Monitor 主动检测:tick() 与 maybe_replace_gid()
tick() 是 Monitor 的定时任务(MDSMonitor.cc L2001-L2089),每个 tick_interval 调用一次。
2.2.1 tick() 的执行流程
cpp
void MDSMonitor::tick()
{
// 1. 仅 active leader 执行
if (!is_active() || !is_leader()) return;
// 2. 检测 Mon 自身的延迟(选举等),重置 beacon 时间戳
if (since_last > (mds_beacon_grace - mds_beacon_interval)) {
for (auto &p : last_beacon) {
p.second.stamp = now; // 防止 Mon 自身慢导致误判
}
}
last_tick = now;
// 3. 确保 last_beacon 对所有已知 gid 都有初始值(首次 tick 时用当前时间初始化)
for (auto &p : pending.mds_roles) {
last_beacon.emplace(gid, {mono_clock::now(), 0});
}
// 4. 遍历所有 gid,检测 beacon 超时
for (auto it = last_beacon.begin(); it != last_beacon.end(); ) {
auto since_last = now - beacon_info.stamp;
if (!pending.gid_exists(gid)) {
it = last_beacon.erase(it); // gid 不存在则清理
continue;
}
if (since_last >= mds_beacon_grace) {
// 超过 grace 时间没收到 beacon → 触发处理
if (osdmap_writeable) {
maybe_replace_gid(pending, gid, info, ...);
}
}
++it;
}
// 5. 尝试用 standby 补充 failed 的 rank
for (auto &p : pending.filesystems) {
maybe_promote_standby(pending, *p.second);
}
}
注意第 3 步 :last_beacon.emplace 使用的是默认参数,只有当 last_beacon 中没有该 gid 的记录 时才插入(emplace 语义)。因此只有新出现的 gid 才会被以当前时间初始化,已有记录不受影响。
2.2.2 maybe_replace_gid() 详细逻辑
这是整个状态流转的核心函数(MDSMonitor.cc L1870-L1946):
cpp
void MDSMonitor::maybe_replace_gid(FSMap &fsmap, mds_gid_t gid,
const MDSMap::mds_info_t& info, bool *mds_propose, bool *osd_propose)
{
// ── 步骤1:计算 may_replace 守卫 ──────────────────────────────────
// 找到 last_beacon 中所有 MDS 里最新的一条 beacon 时间
mono_time latest_beacon = mono_clock::zero();
for (const auto &p : last_beacon) {
latest_beacon = std::max(p.second.stamp, latest_beacon);
}
chrono::duration<double> since = now - latest_beacon;
// may_replace = true 的条件:
// "集群中至少有某个 MDS 最近发来过 beacon"
// (距离最新一次任意 beacon 的时间 < max(beacon_interval, beacon_grace * 0.5))
const bool may_replace = since.count() <
std::max(g_conf()->mds_beacon_interval, g_conf()->mds_beacon_grace * 0.5);
const bool frozen = info.is_frozen();
// ── 步骤2:三路分支 ──────────────────────────────────────────────
// 分支A:有 rank 的 daemon(REPLAY/RESOLVE/RECONNECT/REJOIN/ACTIVE/STOPPING 等)
// 且 may_replace=true 且有可用 standby
if (info.rank >= 0 &&
info.state != STATE_STANDBY &&
info.state != STATE_STANDBY_REPLAY &&
may_replace &&
!frozen &&
!fsmap.test_flag(CEPH_MDSMAP_NOT_JOINABLE) &&
(sgid = fsmap.find_replacement_for(...)) != MDS_GID_NONE)
{
// ✅ 触发 fail:移除旧 daemon,提升 standby
fail_mds_gid(fsmap, gid);
fsmap.promote(sgid, *fs, info.rank);
}
// 分支B:STANDBY 或 STANDBY_REPLAY 且 may_replace=true
else if ((STATE_STANDBY_REPLAY || STATE_STANDBY) && may_replace && !frozen) {
// ✅ 触发 fail:直接移除,不需要替代者
fail_mds_gid(fsmap, gid);
}
// 分支C:以上均不满足(包括 may_replace=false 的情况)
else if (!info.laggy()) {
// ⚠️ 仅标记 laggy,不做任何 fail 操作
fsmap.modify_daemon(info.global_id, [](auto& info) {
info.laggy_since = ceph_clock_now();
});
}
// 如果已经是 laggy,则什么都不做(静默)
}
2.2.3 fail_mds_gid() → FSMap::erase() → failed.insert()
只有调用了 fail_mds_gid() 才能将 rank 放入 failed 集合(MDSMonitor.cc L1140-L1162):
cpp
bool MDSMonitor::fail_mds_gid(FSMap &fsmap, mds_gid_t gid)
{
// 1. 对持有 rank 的非 STANDBY_REPLAY daemon,blacklist 其 IP
if (info.rank >= 0 && info.state != STATE_STANDBY_REPLAY) {
blacklist_epoch = mon->osdmon()->blacklist(info.addrs, until);
}
// 2. 调用 FSMap::erase()
fsmap.erase(gid, blacklist_epoch);
last_beacon.erase(gid);
...
}
FSMap::erase() 中的关键逻辑(mds/FSMap.cc L879-L907):
cpp
void FSMap::erase(mds_gid_t who, epoch_t blacklist_epoch)
{
auto &fs = filesystems.at(mds_roles.at(who));
const auto &info = fs->mds_map.mds_info.at(who);
if (info.state != MDSMap::STATE_STANDBY_REPLAY) {
if (info.state == MDSMap::STATE_CREATING) {
// CREATING 阶段失败:从 in 中移除(rank 从未真正存在过)
fs->mds_map.in.erase(info.rank);
} else {
// 其他所有有 rank 的状态(包括 REPLAY/RESOLVE/RECONNECT/ACTIVE 等):
// 将 rank 加入 failed 集合 ← 这就是 ceph fs status 显示 "failed" 的来源
fs->mds_map.failed.insert(info.rank);
}
fs->mds_map.up.erase(info.rank); // rank 不再被 daemon 持有
}
fs->mds_map.mds_info.erase(who); // 删除 gid 记录
fs->mds_map.last_failure_osd_epoch = blacklist_epoch;
mds_roles.erase(who);
}
结论:STATE_RESOLVE 的 daemon 如果被 erase(),会走 else 分支,rank 进入 failed 集合,ceph fs status 显示 failed。但前提是 fail_mds_gid() 被调用。
2.2.4 maybe_promote_standby():处理 failed 集合
cpp
// MDSMonitor.cc L1948-L1999
bool MDSMonitor::maybe_promote_standby(FSMap &fsmap, Filesystem& fs)
{
// 遍历所有 failed rank,尝试找到 standby 接管
set<mds_rank_t> failed;
fs.mds_map.get_failed_mds_set(failed);
for (const auto& rank : failed) {
auto&& sgid = fsmap.find_replacement_for({fs.fscid, rank}, {});
if (sgid) {
// 找到 standby,promote → state = STATE_REPLAY,rank 从 failed 移出
fsmap.promote(sgid, fs, rank);
}
}
}
三、完整状态流转图
MDS 进程启动
│
▼ (发送 BOOT beacon)
[STATE_BOOT] ──────────────────────────────────────────────────────────────
│ Monitor: prepare_beacon() 将 BOOT 处理为 STANDBY,插入 standby_daemons
▼
[STATE_STANDBY] ←─── 每次 tick last_beacon 刷新
│ Monitor: maybe_resize_cluster() 或 maybe_promote_standby() 调用 promote()
▼ (rank 已有 failed 状态时)
[STATE_REPLAY] ──── daemon 自己通过 beacon 上报推进 ────────────────────────
│ MDS daemon 完成日志扫描后,自行上报 STATE_RESOLVE
▼
[STATE_RESOLVE] ──── daemon 自己通过 beacon 上报推进 ───────────────────────
│ MDS daemon 完成分布式操作消歧后,自行上报 STATE_RECONNECT
▼
[STATE_RECONNECT]
│ MDS daemon 完成客户端重连后,自行上报 STATE_REJOIN
▼
[STATE_REJOIN]
│ MDS daemon 完成缓存 rejoin 后,自行上报 STATE_ACTIVE/CLIENTREPLAY
▼
[STATE_ACTIVE] ←────────────────────────────────────────────────────────────
│ Monitor: maybe_resize_cluster() 收缩时设置 STATE_STOPPING
▼
[STATE_STOPPING]
│ MDS daemon 导出完元数据后,自行上报 STATE_STOPPED
▼
rank 进入 stopped 集合(no daemon holds it)
────────────── 故障路径 ───────────────────────────────────────────────────
[任意有 rank 的状态] + beacon 超时 + may_replace=true + 有 standby
│ Monitor: maybe_replace_gid() → fail_mds_gid() → erase()
▼
rank 进入 [failed] 集合(ceph fs status 显示 "failed")
│ Monitor: maybe_promote_standby() 找到 standby
▼
rank 重新进入 [STATE_REPLAY](由新 standby daemon 持有)
四、MDS 心跳(Beacon)对状态变化的影响
4.1 心跳的记录机制
cpp
// MDSMonitor.cc L308-L317
void MDSMonitor::_note_beacon(MMDSBeacon *m)
{
auto &beacon = last_beacon[gid];
beacon.stamp = mono_clock::now(); // 记录收到 beacon 的时间
beacon.seq = seq;
}
last_beacon 是一个 map<mds_gid_t, beacon_info_t> 的内存数据结构,记录每个 gid 最后一次收到 beacon 的时间。这个数据结构是 Monitor leader 独有的内存状态,不持久化到 Paxos。
_note_beacon() 在以下情况被调用:
preprocess_beacon()中:beacon 需要更新 map(有状态变更/健康变更/laggy 清除)时preprocess_beacon()的reply:标签处:无变更但需要回复时
4.2 心跳超时对状态的影响(核心)
maybe_replace_gid() 中的 may_replace 守卫是心跳影响状态流转的关键:
cpp
// MDSMonitor.cc L1881-L1889
mono_time latest_beacon = mono_clock::zero();
for (const auto &p : last_beacon) {
latest_beacon = std::max(p.second.stamp, latest_beacon);
// ↑ 取所有 MDS 中最新一次 beacon 的时间(不只是当前 gid)
}
mono_time now = mono_clock::now();
chrono::duration<double> since = now - latest_beacon;
const bool may_replace = since.count() <
std::max(g_conf()->mds_beacon_interval, g_conf()->mds_beacon_grace * 0.5);
这段逻辑的含义是:may_replace 反映的不是"当前 gid 的心跳是否正常",而是"集群中是否至少有一个 MDS 最近发来过心跳"。
设计意图(代码注释 L1878-L1880):
"We will only take decisive action (replacing/removing a daemon) if we have some indication that some other daemon(s) are successfully getting beacons through recently."
如果所有 MDS 的 beacon 都超时了,可能是 Monitor 自身的网络/性能问题,不应草率地 fail 掉所有 MDS。
4.3 心跳对各状态的具体影响矩阵
| MDS 当前状态 | 心跳正常 | 心跳超时 + may_replace=true | 心跳超时 + may_replace=false |
|---|---|---|---|
| STATE_STANDBY | 正常维护在 standby_daemons | fail_mds_gid() → 从 standby 移除 |
仅标记 laggy |
| STATE_STANDBY_REPLAY | 正常维护 | fail_mds_gid() → 从 mds_info 移除 |
仅标记 laggy |
| STATE_REPLAY | 正常,等 daemon 推进状态 | fail_mds_gid() + 提升 standby → rank 进入 failed 再立即由新 standby 接管 |
仅标记 laggy |
| STATE_RESOLVE | 同上 | 同上 | ⚠️ 仅标记 laggy,rank 停留在 RESOLVE(laggy or crashed) |
| STATE_RECONNECT | 同上 | 同上 | ⚠️ 同上 |
| STATE_ACTIVE | 同上 | 同上 | ⚠️ 同上 |
| STATE_STOPPING | 正常,等 daemon 推进到 STOPPED | 同上 | ⚠️ 同上 |
4.4 laggy 标记的作用与局限
当 maybe_replace_gid() 进入分支 C 时,只是设置 laggy_since:
cpp
fsmap.modify_daemon(info.global_id, [](auto& info) {
info.laggy_since = ceph_clock_now();
});
laggy 标记的影响:
ceph fs status输出变化 :状态显示从resolve变为resolve(laggy or crashed)(FSMap.ccL186-L188)find_replacement_for()中跳过 laggy standby :get_available_standby()会跳过laggy()为 true 的 standby(FSMap.ccL719)- 不影响 rank 的归属 :rank 仍然在
up中,failed集合中没有它
关键局限 :laggy 标记一旦设置,且 may_replace 持续为 false,则:
- 下次 tick 检测时,因为
info.laggy()已为 true,进入分支 C 的else if (!info.laggy())判断为 false - 整个
maybe_replace_gid()什么都不做,彻底静默
cpp
// 后续 tick 中,如果 laggy 已设置且 may_replace 仍为 false:
// 分支A: may_replace=false → 不进入
// 分支B: 不是 STANDBY/STANDBY_REPLAY → 不进入
// 分支C: info.laggy() == true → !info.laggy() = false → 不进入
// 结果:完全静默,状态永远不变
五、针对现网问题的根因分析
问题场景还原
1. 主 MDS 机器挂掉
2. 备 MDS 顶上,调用 FSMap::promote(),state = STATE_REPLAY
3. daemon 通过 beacon 推进:REPLAY → RESOLVE
Monitor 收到 beacon,记录 state = STATE_RESOLVE
4. 此时 systemctl 重启失败,19/28号 MDS 进程已停止
5. MDS 停止发送 beacon
6. tick() 检测到 beacon 超时,调用 maybe_replace_gid()
关键问题:may_replace 的计算
cpp
mono_time latest_beacon = mono_clock::zero();
for (const auto &p : last_beacon) {
latest_beacon = std::max(p.second.stamp, latest_beacon);
}
此时遍历的是 last_beacon map 中所有 gid 的记录。如果整个集群的 MDS 都处于异常状态(其他 MDS 也可能故障),没有任何 MDS 在正常发 beacon,则:
latest_beacon= 很久之前的时间since.count()= 很大的值may_replace = false
进入分支 C:else if (!info.laggy()) → 只标记 laggy_since。
之后的每次 tick:三个分支都不满足,完全静默。
为什么不显示 failed
因为 fail_mds_gid() 从未被调用,所以 FSMap::erase() 从未执行,所以 rank 从未进入 mds_map.failed 集合。ceph fs status 读取的正是 failed 集合,所以显示的不是 failed。
实际显示的是:resolve(laggy or crashed) ← state=RESOLVE + laggy=true
完整问题路径图
MDS 19/28 进程停止
│
▼ tick() beacon 超时检测
maybe_replace_gid()
│
├── 计算 may_replace
│ 集群中所有 MDS 都不健康,latest_beacon 很旧
│ → may_replace = FALSE
│
├── 分支A:may_replace=false → ✗ 不进入
├── 分支B:state=RESOLVE,非 STANDBY → ✗ 不进入
└── 分支C:!info.laggy() → ✓ 首次进入,设置 laggy_since
后续 tick:info.laggy()=true → ✗ 不进入
结果:fail_mds_gid() 从未调用
→ FSMap::erase() 从未调用
→ failed.insert() 从未调用
→ rank 不在 failed 集合中
→ ceph fs status 显示 resolve(laggy or crashed)
→ 永远不会自动恢复
这是一个真实的 BUG
may_replace 的保守策略在单个或少量 MDS 异常 时是合理的保护,但在整集群 MDS 都处于异常状态 时,会导致任何有 rank 的 daemon 都无法被 fail 掉。
尤其对于已经处于 RESOLVE 等恢复中间状态的 MDS,进程停止后:
- 不能自动流转到
failed(maybe_replace_gid被may_replace=false阻止) - 不会被新的 standby 接管(
maybe_promote_standby只处理failed集合中的 rank) ceph fs status永远显示resolve(laggy or crashed),直到人工干预
根本修复方向 :对于 laggy_since 已超过某个阈值(例如 mds_beacon_grace 的若干倍)的 daemon,即使 may_replace=false,也应强制执行 fail_mds_gid()。或者区分"没有 standby 可替换"(需要保守)和"集群整体异常"(需要激进清理)两种场景。
六、相关配置参数
| 参数 | 默认值 | 说明 |
|---|---|---|
mds_beacon_interval |
4s | MDS 发送 beacon 的周期 |
mds_beacon_grace |
15s | beacon 超时阈值,超过后触发 maybe_replace_gid() |
mon_mds_blacklist_interval |
1天 | fail 后 blacklist 持续时间 |
may_replace 的阈值 = max(mds_beacon_interval, mds_beacon_grace * 0.5) = max(4, 7.5) = 7.5 秒 。即:如果 7.5 秒内集群中没有任何 MDS 发来 beacon,may_replace 就为 false。