背景
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)构造竞态窗口:
- 让第一次
ibv_poll_cq(recv_cq, ...) 返回 0(recv_cq 当前为空),代码切到 send_cq。
- 让随后对
send_cq 的 ibv_poll_cq 也返回 0(send_cq 也为空)。
- 在代码调用
ibv_req_notify_cq(recv_cq, solicited_only=1) 之前、之后之间的窗口内,注入一个“solicited 的 recv CQE”(该 CQE 已进入 recv CQ,但 completion-channel 事件可能不会再触发)。
- 确保之后没有新的 solicited recv 触发(例如不再投递带 solicited flag 的 recv completions)。
- 断言:本轮及后续不会调用
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 检查”。
背景
brpc 的 RDMA 实现使用
ibv_req_notify_cq()的 one-shot 语义:当 CQ 处于“未 armed”到“armed”的窗口里,CQE 如果先到达,可能不会再触发一次 completion-channel upcall/epoll 事件。标准做法是:在 re-arm 之后,至少 re-poll 一次对应 CQ,以确保没有 CQE 被“错过”。在当前实现中,
RdmaEndpoint::PollCq()的状态机为了优化顺序会先 pollrecv_cq,发现空后切换到send_cq。当两个 CQ 都 poll 为空时,会分别ReqNotifyCq(true)/ReqNotifyCq(false)进行 re-arm。但在 arm 完成后的下一轮循环里,代码仍保持send=true/cq=send_cq,没有按注释所述及时 re-pollrecv_cq,从而存在漏处理recv_cqCQE 的可能。复现方式(高置信的可测方案)
建议用 verbs mock / stub(或在测试框架中 hook
ibv_poll_cq/ibv_req_notify_cq)构造竞态窗口:ibv_poll_cq(recv_cq, ...)返回0(recv_cq 当前为空),代码切到send_cq。send_cq的ibv_poll_cq也返回0(send_cq 也为空)。ibv_req_notify_cq(recv_cq, solicited_only=1)之前、之后之间的窗口内,注入一个“solicited 的 recv CQE”(该 CQE 已进入 recv CQ,但 completion-channel 事件可能不会再触发)。ProcessNewMessage()处理该注入的 recv CQE,最终 RPC 侧出现超时/卡住。期望行为
当两个 CQ 都 poll 为空并完成 re-arm 后,代码应当在“本轮逻辑流”中至少 re-poll 一次
recv_cq(或严格做到 poll-arm-poll 覆盖两个 CQ),确保不会因 one-shot 通知竞态导致recv_cqCQE 滞留。实际行为
当前逻辑在 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 检查”。