Skip to content

RDMA::PollCq 复查逻辑缺陷:arm recv_cq 后未及时 re-poll,可能漏掉 recv CQE 并导致 RPC 超时 #3415

Description

@houlin2016

背景

brpc 的 RDMA 实现使用 ibv_req_notify_cq() 的 one-shot 语义:当 CQ 处于“未 armed”到“armed”的窗口里,CQE 如果先到达,可能不会再触发一次 completion-channel upcall/epoll 事件。标准做法是:在 re-arm 之后,至少 re-poll 一次对应 CQ,以确保没有 CQE 被“错过”。

在当前实现中,RdmaEndpoint::PollCq() 的状态机为了优化顺序会先 poll recv_cq,发现空后切换到 send_cq。当两个 CQ 都 poll 为空时,会分别 ReqNotifyCq(true) / ReqNotifyCq(false) 进行 re-arm。但在 arm 完成后的下一轮循环里,代码仍保持 send=true / cq=send_cq,没有按注释所述及时 re-poll recv_cq,从而存在漏处理 recv_cq CQE 的可能。

复现方式(高置信的可测方案)

建议用 verbs mock / stub(或在测试框架中 hook ibv_poll_cq / ibv_req_notify_cq)构造竞态窗口:

  1. 让第一次 ibv_poll_cq(recv_cq, ...) 返回 0(recv_cq 当前为空),代码切到 send_cq
  2. 让随后对 send_cqibv_poll_cq 也返回 0(send_cq 也为空)。
  3. 在代码调用 ibv_req_notify_cq(recv_cq, solicited_only=1) 之前、之后之间的窗口内,注入一个“solicited 的 recv CQE”(该 CQE 已进入 recv CQ,但 completion-channel 事件可能不会再触发)。
  4. 确保之后没有新的 solicited recv 触发(例如不再投递带 solicited flag 的 recv completions)。
  5. 断言:本轮及后续不会调用 ProcessNewMessage() 处理该注入的 recv CQE,最终 RPC 侧出现超时/卡住。

期望行为

当两个 CQ 都 poll 为空并完成 re-arm 后,代码应当在“本轮逻辑流”中至少 re-poll 一次 recv_cq(或严格做到 poll-arm-poll 覆盖两个 CQ),确保不会因 one-shot 通知竞态导致 recv_cq CQE 滞留。

实际行为

当前逻辑在 arm 两个 CQ 后并未立即回到 recv_cq 做 re-poll,而是继续以 send_cq 为主;因此如果 recv CQE 到达发生在 “recv poll empty” 与 “recv notify” 的 one-shot 竞态窗口中,recv CQE 可能会滞留,RPC 一直等不到消息处理,从而超时。

代码位置(建议引用)

src/brpc/rdma/rdma_endpoint.cpp

  • RdmaEndpoint::PollCq()ReqNotifyCq(true/false) 之后的状态切换逻辑(send/cq 没有按注释及时复查 recv_cq

建议修复思路(最小改动)

ReqNotifyCq(true) / ReqNotifyCq(false) 都成功返回后、continue 之前,确保:

  • 直接切回 send=false; cq=recv_cq;(或立即 re-poll recv_cq 一次),使得该轮循环满足“re-arm 后的 recv CQE 检查”。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions