土法炼钢 · 系统与基础设施

【Ceph RADOS】OSD 与 Messenger:daemon 结构、msgr2 协议与 Op 入队路径

文章导航

分类入口
storagedistributed
标签入口
#ceph#osd#messenger#msgr2#asyncmessenger#shardedopwq#squid#v19.2.5

目录

「写卡在哪」是 Ceph 排障的第一问。卡在 MON 是 epoch 问题;卡在通信层是 Messenger 连接问题;卡在 PG 是 peering 或 BlueStore 问题。本篇聚焦「卡在通信层」的那一段:msgr2 如何建立连接、Op 如何从 TCP 帧进入 OSD 内部调度队列。

通信层的故障通常有两种表现:一是连接建立失败(ceph 报 connection refusedconnection 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/ceph tag v19.2.5 为准。


一、OSD daemon 的角色与内部结构

ceph-osd 是 RADOS 数据面的执行单元,每个 OSD 进程负责:

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 注册(MMonGetVersionMOSDBoot)。收到 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)解决:

  1. 连接层加密:在握手完成后可升级为 AES-GCM 加密帧(cephx 模式下协商 session key)。
  2. 多路复用框架:一条 TCP 连接上的多个逻辑 stream(目前 Squid 中尚未全面启用多路复用,但协议层为此预留了接口)。
  3. 消息格式标准化: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 ]

握手阶段

连接建立分四个阶段(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 线程与连接管理

AsyncMessengersrc/msg/async/AsyncMessenger.h @ v19.2.5)是 msgr2 的 I/O 实现,基于 epoll(Linux)。其内部结构:

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):

  1. OSD::ms_dispatch2():MessageDispatcher 入口,按消息类型分发。
  2. OSD::handle_op():检查 MOSDOp 的基本合法性(epoch 检查、pool 存在性),构建 OpRequestRef
  3. OSD::enqueue_op():根据目标 PG hash 选择 shard,调用 op_shardedwq.queue()

ShardedOpWQ

ShardedOpWQsrc/osd/OSD.h @ v19.2.5)是 OSD 的核心工作队列,以 shard 为单位隔离并行度:

Shard 内部使用优先级队列(OpScheduler)区分客户端 Op、恢复 Op 和 scrub Op 的优先级。Squid v19.2.5 中 mClock 调度器(osd_op_queue = mclock_scheduler)已成为默认,通过 QoS 权重控制前台 Op 与后台任务的资源分配。

mClock 调度器实现来自 Gulati、Merchant、Varman 的 mClockOSDI 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 中将其标记为 upin(若之前是 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 后:

  1. 找到对应的 PGRefOSD::lookup_lock_pg())。
  2. 检查 PG 状态是否允许服务(PG::is_active())。
  3. 调用 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 而非完全阻塞」现象的关键。


七、小结

  1. OSD daemon 的分层结构AsyncMessenger 负责 I/O 多路复用(不做业务逻辑),ShardedOpWQ 负责按 PG hash 分片调度,PrimaryLogPG 负责一致性语义。这三层的边界是定位「Op 卡在哪」的坐标点。
  2. msgr2 的核心改进:相对 msgr1 增加了连接层 AES-GCM 加密和标准化握手,Frame 结构(Preamble + Segments + Epilogue)使得调试和版本兼容性更可控;多路复用能力预留但尚未全面启用。
  3. Worker 线程的不变量AsyncMessenger 的 worker 线程只做 I/O 和 frame 解析,不能在 worker 上阻塞;业务逻辑全部在 ShardedOpWQ 的 worker 线程上执行。
  4. Shard 分配保证 PG 内顺序:同一 PG 的 Op 总在同一 shard 的同一 worker 线程上按序处理,这是 RADOS 写语义(对同一对象的写有序)的实现基础。
  5. 心跳设计的容错意图:OSD 之间互相心跳、MON 仲裁失联报告,避免网络分区下的单点误判;osd_heartbeat_graceosd_recovery_delay_start 共同控制故障发现与数据重新分配的速度权衡。
  6. 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)

源码(A)

论文 / 设计(A)

站内对照

实验台账


上一篇:MON/MGR 与 Map · 下一篇:CRUSH 加深

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .