「写卡在哪」是 Ceph 排障的第一问。卡在 MON 是 epoch 问题;卡在通信层是 Messenger 连接问题;卡在 PG 是 peering 或 BlueStore 问题。本篇聚焦「卡在通信层」的那一段:msgr2 如何建立连接、Op 如何从 TCP 帧进入 OSD 内部调度队列。
通信层的故障通常有两种表现:一是连接建立失败(ceph 报
connection refused 或
connection reset),通常是防火墙或 OSD
进程未启动;二是连接建立成功但消息处理延迟高,通常是
ShardedOpWQ 的 shard 积压——某个 PG 上的大量 Op
堆积在同一 shard 的队列里,即使 CPU 空闲,其他 shard 上的 Op
也无法帮忙排空它。理解 Messenger 和 ShardedOpWQ
的边界,是判断「是 I/O 路径慢还是排队慢」的前提。
本文是「Ceph RADOS 存储内核」系列第 3 篇(共 18 篇)。→ 系列目录
篇目 核心内容 第 1 篇 · RADOS 全景 缺口、五轴坐标系、18 篇路线 第 2 篇 · MON/MGR 与 Map quorum、OSDMap/PGMap、epoch 传播 第 3 篇 · OSD 与 Messenger daemon、msgr2、Op 进入 OSD 第 4 篇 · CRUSH 加深 rule/bucket/weight 机制钉 第 5 篇 · PG 与 Peering PrimaryLogPG、PG log、状态机 第 6 篇 · 客户端写路径 librados → 副本应答、stale read 边界
版本锚定:Ceph Squid v19.2.5。源码路径以
ceph/cephtagv19.2.5为准。
一、OSD daemon 的角色与内部结构
ceph-osd 是 RADOS 数据面的执行单元,每个 OSD
进程负责:
- 维护本地 ObjectStore(Squid 默认 BlueStore)上一组 PG 的数据和元数据。
- 处理来自客户端和其他 OSD 的
MOSDOp(写请求)、MOSDRepOp(副本写)与MOSDOpReply(副本回复)。 - 参与 peering、recovery、backfill、scrub 等后台流程。
- 向 MON 汇报心跳(
MOSDPing)和 PG 状态。
OSD 类(src/osd/OSD.h @
v19.2.5)是 daemon 的顶层封装。它聚合以下主要子组件:
| 子组件 | 类型 / 文件 | 职责 |
|---|---|---|
client_messenger |
AsyncMessenger * |
接受来自 librados 客户端的连接 |
cluster_messenger |
AsyncMessenger * |
接受来自其他 OSD 的集群内部消息 |
objecter |
Objecter * |
OSD 作为客户端(scrub、backfill 时)向其他 OSD 发请求 |
store |
ObjectStore * |
BlueStore / MemStore / FileStore |
op_shardedwq |
ShardedOpWQ |
主工作队列,分 shard 调度 Op 到 PG |
service |
OSDService |
跨 PG 共享的服务(map 缓存、心跳、session 管理) |
OSD 启动时(OSD::init())先初始化
store,再启动 messenger,然后向 MON
注册(MMonGetVersion →
MOSDBoot)。收到 MON
确认后进入正常服务循环。
二、msgr2(Protocol V2):Frame 格式与握手阶段
为什么需要 msgr2
msgr1(Protocol V1)是 Ceph
的第一代消息协议,使用固定大小 header + 可变
payload,缺乏连接层加密与标准化的认证握手。Nautilus(v14)引入
msgr2(Protocol V2,src/msg/async/ProtocolV2.h
@ v19.2.5)解决:
- 连接层加密:在握手完成后可升级为
AES-GCM 加密帧(
cephx模式下协商 session key)。 - 多路复用框架:一条 TCP 连接上的多个逻辑 stream(目前 Squid 中尚未全面启用多路复用,但协议层为此预留了接口)。
- 消息格式标准化:Frame header 与 payload 分离,便于调试与版本兼容。
msgr1 在 Squid
中仍受支持(ms_bind_msgr1 = true
时开启),但默认只开 msgr2。
Frame 格式
msgr2 的 Frame(src/msg/async/frames_v2.h @
v19.2.5)结构如下:
[ Preamble ] [ Segment 0 ] [ Segment 1 ] ... [ Epilogue ]
- Preamble:固定 8 字节头,含 Tag(帧类型)、Frame 版本、Segment 数量与各 Segment 长度。
- Segment:实际 payload 分段,可有 1 到 4 个 segment;0 号 segment 通常是消息 header,其余是 payload 分片。
- Epilogue:可选的 CRC 或 AEAD tag(在加密模式下为 GCM tag)。
握手阶段
连接建立分四个阶段(ProtocolV2::BANNER_CONNECTING…
等状态):
sequenceDiagram
participant C as Client
participant S as OSD (Server)
C->>S: Banner + Hello frame (client nonce, supported features)
S-->>C: Banner + Hello frame (server nonce, supported features)
Note over C,S: AUTH phase
C->>S: AuthRequest (cephx entity, nonce)
S-->>C: AuthBadMethod / AuthReply (session key)
Note over C,S: SESSION phase
C->>S: ClientIdent (addr, features, cookie)
S-->>C: ServerIdent (addr, features, cookie)
Note over C,S: Connected — ready for Messages
AUTH 阶段协商 cephx session key,SESSION 阶段确认连接
cookie 和 feature bits。连接重用时客户端发
ReconnectRequest,跳过 AUTH 但重新协商
cookie。
三、AsyncMessenger:worker 线程与连接管理
AsyncMessenger(src/msg/async/AsyncMessenger.h
@ v19.2.5)是 msgr2 的 I/O 实现,基于
epoll(Linux)。其内部结构:
- Worker
线程池:
ms_async_op_threads(默认 3)个 worker,每个 worker 持有一个 epoll fd 和一个EventCenter。连接建立时通过 hash 分配给某个 worker,之后该连接的所有 I/O 事件都在同一 worker 上处理,避免跨线程竞争。 - AsyncConnection:每条 TCP 连接对应一个
AsyncConnection对象(src/msg/async/AsyncConnection.h),持有ProtocolV2状态机实例,负责 frame 的收发与 reassembly。 - 读完成回调:读到完整 frame
后,
AsyncConnection调用Messenger::dispatch_queue()把消息入队,最终触发注册的Dispatcher(OSD 就是一个Dispatcher)的ms_dispatch()回调。
Worker 线程不处理业务逻辑,只负责 I/O 和 frame
解析。业务逻辑的实际执行在 OSD 的 ShardedOpWQ
中。
Worker 线程数(ms_async_op_threads)是
Messenger 层的 I/O
并发度上限,与业务逻辑的并发度(osd_op_num_shards * osd_op_num_threads_per_shard)是两个独立的参数。在高并发小
I/O 场景下(如 RBD 上运行虚拟机),首先应确认 worker 线程的
CPU 利用率是否已满,再考虑是否 shard
线程是瓶颈;两者不能互相代替,因为它们在调用链上处于不同位置。
四、Op 进入 OSD:从 socket 到 ShardedOpWQ
分发路径
Op 从网络到 PG 处理的路径(简化,以 MOSDOp
为例):
flowchart TD
A["epoll event: data available"] --> B["AsyncConnection::process()"]
B --> C["ProtocolV2: read frame, parse header"]
C --> D["Messenger::ms_deliver_dispatch()"]
D --> E["OSD::ms_dispatch2()"]
E --> F{"Message type?"}
F -->|MOSDOp| G["OSD::handle_op()"]
F -->|MOSDMap| H["OSD::handle_osd_map()"]
G --> I["OSD::enqueue_op()"]
I --> J["ShardedOpWQ::_enqueue()"]
J --> K["PG::queue_op() in worker thread"]
关键函数链(src/osd/OSD.cc @ v19.2.5):
OSD::ms_dispatch2():MessageDispatcher 入口,按消息类型分发。OSD::handle_op():检查MOSDOp的基本合法性(epoch 检查、pool 存在性),构建OpRequestRef。OSD::enqueue_op():根据目标 PG hash 选择 shard,调用op_shardedwq.queue()。
ShardedOpWQ
ShardedOpWQ(src/osd/OSD.h @
v19.2.5)是 OSD 的核心工作队列,以 shard
为单位隔离并行度:
- 默认 shard 数 =
osd_op_num_shards(默认 5)。 - 每个 shard 由固定数量的 worker
线程(
osd_op_num_threads_per_shard,默认 2)消费。 - Op 按
pg->get_pgid().hash_to_shard(num_shards)分配到固定 shard,同一 PG 的 Op 总在同一 shard 中按序处理,保证 PG 内顺序。
Shard
内部使用优先级队列(OpScheduler)区分客户端
Op、恢复 Op 和 scrub Op 的优先级。Squid v19.2.5 中 mClock
调度器(osd_op_queue = mclock_scheduler)已成为默认,通过
QoS 权重控制前台 Op 与后台任务的资源分配。
mClock 调度器实现来自 Gulati、Merchant、Varman 的
mClock(OSDI
2010,文中同时给出面向多节点的 dmClock
变体):用 reservation / weight / limit 三类 tag
在共享队列上做 QoS。Ceph Classic OSD 在单 OSD(及
shard)上使用该模型的适配,把客户端 Op、recovery、scrub
等分成不同服务类;Squid 默认
osd_op_queue = mclock_scheduler,并通过
profile(如 high_client_ops)隐藏底层三参数(见
docs.ceph.com/en/squid/rados/configuration/mclock-config-ref/)。相对
wpq,它显式保证前台最低份额,同时给后台上限,避免「恢复打满盘」。注意:生产路径主要是单节点
mClock 语义,并非把论文里的全分布式 dmClock
全局状态原样搬进集群。
五、OSD 心跳与 MON 汇报
OSD 的可用性检测不依赖 MON
的主动探测,而是通过OSD 之间的 peer
心跳(MOSDPing)实现。每个 OSD
维护一份「需要与之互相心跳」的 peer 列表,这些 peer 由
OSDMap 决定(通常是同一 CRUSH 叶子下的 OSD 和随机选取的
remote peer)。若 OSD A 在
osd_heartbeat_grace(默认 20 秒)内没有收到 OSD
B 的心跳,A 会通知 MON「我认为 B 已 down」;MON 等到足够多的
OSD 报告同一 peer 失联后,才将其标记为 down
并递增 OSDMap epoch。
这个「peer 心跳 + MON
仲裁」的设计避免了网络分区下的误判:如果只有 A 报告 B
失联,可能是 A–B 之间的网络问题;如果大多数 OSD 都报告 B
失联,才说明 B
本身出了问题。osd_heartbeat_min_peers(默认
3)控制最少要有多少 peer 上报失联才触发 down 判定。
OSD 启动后通过 MOSDBoot 消息向 MON 注册,MON
在 OSDMap 中将其标记为 up 和
in(若之前是 out 则只先标记
up,等 osd_recovery_delay_start
超时后再 in)。这个延迟保证了刚重启的 OSD
在参与数据分布之前有足够时间完成本地 BlueStore 初始化和
peering。
心跳机制还有一个容易被忽视的工程细节:心跳消息使用独立的
cluster_messenger(不同于处理客户端 Op 的
client_messenger),即使
client_messenger 因 ShardedOpWQ
积压而响应迟缓,心跳也不受影响,保证了 MON 对 OSD
存活判断的独立性。这个设计防止了「OSD
忙但未宕机」被误判为「OSD 宕机」——在高 I/O 压力下,客户端 Op
可能排队几秒,但只要心跳正常,OSD 就不会被 down 掉。
六、Op 在 ShardedOpWQ 中的处理
Worker 线程从 shard 队列取出 Op 后:
- 找到对应的
PGRef(OSD::lookup_lock_pg())。 - 检查 PG
状态是否允许服务(
PG::is_active())。 - 调用
PrimaryLogPG::do_request()(对客户端 Op)或相应的 recovery/backfill 处理函数。
如果 PG 正在 peering(非 Active 状态),Op 会被暂存在
PG::waiting_for_active 队列,等 PG 完成 peering
后再重放。这也是客户端写在 peering
期间「阻塞而不报错」的原因——Op 没有被丢弃,只是在等待。
需要注意的是,waiting_for_active
队列并不是无限深的:osd_max_waiting_ops(默认
512)限制了单个 PG 上可以积压的等待 Op
数。超过此上限后,新的 Op 会被拒绝并返回 EAGAIN
给客户端,客户端在刷新 map 后重试(通常 PG 这时也已经完成
peering)。理解这个队列深度上限,是解释「peering
期间客户端看到零散 EAGAIN 而非完全阻塞」现象的关键。
七、小结
- OSD daemon
的分层结构:
AsyncMessenger负责 I/O 多路复用(不做业务逻辑),ShardedOpWQ负责按 PG hash 分片调度,PrimaryLogPG负责一致性语义。这三层的边界是定位「Op 卡在哪」的坐标点。 - msgr2 的核心改进:相对 msgr1 增加了连接层 AES-GCM 加密和标准化握手,Frame 结构(Preamble + Segments + Epilogue)使得调试和版本兼容性更可控;多路复用能力预留但尚未全面启用。
- Worker
线程的不变量:
AsyncMessenger的 worker 线程只做 I/O 和 frame 解析,不能在 worker 上阻塞;业务逻辑全部在ShardedOpWQ的 worker 线程上执行。 - Shard 分配保证 PG 内顺序:同一 PG 的 Op 总在同一 shard 的同一 worker 线程上按序处理,这是 RADOS 写语义(对同一对象的写有序)的实现基础。
- 心跳设计的容错意图:OSD
之间互相心跳、MON
仲裁失联报告,避免网络分区下的单点误判;
osd_heartbeat_grace和osd_recovery_delay_start共同控制故障发现与数据重新分配的速度权衡。 - waiting_for_active 的积压上限:peering
期间 Op 暂存于 PG
本地队列而非被拒绝,但队列深度有上限(
osd_max_waiting_ops);理解此上限避免把「零散 EAGAIN」误诊为网络问题。
八、谱系、争论与开放问题
谱系
Ceph 的消息系统最早是 Sage Weil
博士论文中的简单自研协议(msgr1),重点在于保证 reliable
delivery 语义(重传、序号)而非安全性。msgr2
的设计文档(docs.ceph.com/en/squid/dev/msgr2/)记录了协议设计决策:选择
AES-GCM(而非
TLS)的原因是减少握手往返次数,并在需要时可绕过 TCP 层(RDMA
路径)。
AsyncMessenger 取代早期
SimpleMessenger(单连接一线程模型)发生在
Jewel(v10)前后,动机是:SimpleMessenger
在高并发下线程数线性增长,而 AsyncMessenger 用
I/O 多路复用固定 worker 数量。
争论:epoll 多路复用 vs io_uring
Ceph 的 AsyncMessenger 目前基于
epoll(Linux)。io_uring
在提交与完成路径上可以减少系统调用次数,理论上对高 IOPS OSD
有益。Crimson OSD(基于 Seastar)已经在使用 epoll +
高性能网络,但 Crimson 的网络栈不是 io_uring。社区讨论过把
Classic OSD 的 AsyncMessenger 接到
io_uring,但至 Squid v19.2.5 尚未合并;相关 issue 可在 Ceph
tracker 上查询(无 PR 编号可核实则不引用)。
与 SPDK NVMe-oF 的对比:SPDK 用轮询完成模型,Ceph 用中断/epoll 完成模型;两者都实现了 NVMe-oF target,但 Ceph 的重点是集群语义,SPDK 的重点是单机极致延迟。
开放问题
msgr2 多路复用的实际落地:Protocol V2 协议层预留了多路复用(multiple logical streams over one TCP connection)能力,但 Squid v19.2.5 的生产路径仍以单 TCP 连接单消息流为主。多路复用能减少大集群下的 TCP 连接数(尤其当 OSD 数量乘以副本数时连接数极大)。何时进入生产、对 head-of-line blocking 的影响,目前仍是开放工程问题。
排障坐标系里的本篇落点:若客户端超时而
ceph -s 仍 HEALTH_OK,优先核对「该 PG 所在
shard 是否被恢复类 Op 占满」与「client_messenger 与
cluster_messenger 是否出现不对称延迟」——前者指向 mClock /
恢复限速(第 10 篇),后者指向本篇的双 messenger
与心跳独立性。不要一上来就改
osd_op_num_threads,除非已证明队列深度与 CPU
绑定关系。
九、参考资料
规范 / 官方文档(A)
- Ceph Squid v19.2.5 msgr2 Protocol —
docs.ceph.com/en/squid/dev/msgr2/。 - Ceph Squid v19.2.5 OSD Config Reference —
docs.ceph.com/en/squid/rados/configuration/osd-config-ref/。 - Ceph Squid v19.2.5 mClock Scheduler —
docs.ceph.com/en/squid/rados/configuration/mclock-config-ref/。
源码(A)
src/osd/OSD.h、src/osd/OSD.cc@ v19.2.5:OSD 主类、handle_op()、enqueue_op()。src/msg/async/AsyncMessenger.h@ v19.2.5:worker 线程池与连接管理。src/msg/async/ProtocolV2.h、src/msg/async/ProtocolV2.cc@ v19.2.5:握手状态机。src/msg/async/frames_v2.h@ v19.2.5:Frame 结构定义。src/osd/scheduler/mClockScheduler.h@ v19.2.5:mClock Op 调度。
论文 / 设计(A)
- Ajay Gulati, Arif Merchant, Peter J. Varman. “mClock: Handling Throughput Variability for Hypervisor IO Scheduling.” OSDI 2010(含 dmClock 变体;Ceph mClock 调度器谱系)。
- Ceph msgr2 —
docs.ceph.com/en/squid/dev/msgr2/。
站内对照
- MON/MGR 与 Map(第 2 篇)
- PG 与 Peering(第 5 篇)
- SPDK NVMe-oF(对照:轮询 vs epoll 完成模型)
实验台账
- 本篇无命令输出;msgr2 握手与 Op 入队可通过真实集群
debug_ms观察,不伪造日志。
← 上一篇:MON/MGR 与 Map · 下一篇:CRUSH 加深
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【Ceph RADOS】RADOS 内核全景:五轴坐标系与 18 篇路线
补齐站内 distributed/38 之上的 RADOS 内核层缺口;定义 Map 传播、通信、一致性、本地存储、接入代价五条坐标系,给出 18 篇阅读路线与系列边界。
【Ceph RADOS】MON/MGR 与 Map:quorum、epoch 传播与客户端可见性
拆解 Ceph MON Paxos quorum、OSDMap/PGMap 结构与 epoch 传播机制;说明 MGR 主备模型与模块框架;分析客户端 epoch 缓存与可见性窗口的边界条件。
【Ceph RADOS】CRUSH 加深:rule/bucket/weight 机制与失败域语义
相对 distributed/38 的 CRUSH 算法概览,深入 bucket 类型的算法复杂度差异、rule step 语义、weight 体系与 primary_affinity,分析 chooseleaf 与失败域的精确定义及 CRUSH 与一致性哈希的本质区别。
【Ceph RADOS】PG 与 Peering:PrimaryLogPG、PG log 与状态机
拆解 PG 命名与生命周期、PrimaryLogPG 的日志式复制设计、pg_log_entry_t 格式、last_epoch_started 与 last_epoch_clean 的语义差异,以及 peering 状态机从 Reset 到 Active 的完整路径。