首页 / 资讯中心 / 文章详情

141、RDMA编程案例:基于RDMA的HPC检查点恢复

141、RDMA编程案例:基于RDMA的HPC检查点恢复 ★ FEATURED ARTICLE
一、一个让我熬夜三天的bug去年做某超算中心的检查点恢复系统遇到一个诡异现象MPI进程在恢复检查点时RDMA写操作返回成功但目标节点的内存数据却是错的。更离谱的是同样的代码在测试集群跑三天不出问题一上生产环境就随机复现。当时我盯着ibv_wc.status IBV_WC_SUCCESS看了两个小时差点怀疑人生。最后用ibv_wc.vendor_err字段抓到了元凶——原来是检查点恢复时远端内存注册的MRMemory Region已经被销毁了但发送端还在用旧的lkey/rkey。这个坑让我意识到RDMA的检查点恢复本质上是在和硬件状态机玩“时间旅行”。你不仅要保存应用数据还得把RDMA连接、内存注册、完成队列这些硬件资源的状态一并“冻结”和“解冻”。二、检查点恢复的RDMA特殊性传统HPC检查点恢复比如BLCR、DMTCP主要关注进程状态和文件描述符。但RDMA引入了一个新维度硬件状态的一致性。当你对一个RDMA连接做检查点时需要处理三个层面的状态QP状态机从INIT到RTS每个状态下的WQE工作请求处理规则不同。恢复时如果状态不对硬件直接报错。内存注册MR的lkey/rkey是硬件翻译虚拟地址到物理地址的钥匙。进程重启后虚拟地址可能变但rkey是固定的——这就导致旧rkey指向了错误的内存。完成队列未完成的WQE和已完成的CQE恢复时要么重放要么丢弃选错了就是数据损坏。三、实战设计一个可恢复的RDMA检查点系统3.1 架构设计原则我的方案核心思路把RDMA连接当作“有状态的外设”来管理。每个RDMA连接都关联一个状态描述符检查点时序列化这个描述符恢复时重建硬件资源。// 检查点状态结构体别这样写struct checkpoint_state // 这里踩过坑一开始把QP的qpn直接存了恢复时发现qpn被其他进程占用了 struct rdma_ckpt_state { uint32_t qp_num; // 恢复时重新分配这个只做校验 uint32_t max_send_wr; // QP属性重建时需要 uint32_t max_recv_wr; enum ibv_qp_state qp_state; // 保存时的状态 uint64_t local_psn; // 包序列号恢复时从这开始 uint32_t rkey; // 远端MR的rkey注意这个会变 uint64_t remote_vaddr; // 远端虚拟地址 // ... 其他属性 };3.2 检查点保存冻结硬件状态保存检查点的关键步骤Step 1暂停所有RDMA操作先发一个自定义的“冻结”控制消息给所有对端让它们停止发送新的RDMA请求。这一步用ibv_modify_qp把QP切换到SQDSuspended状态。// 切换到SQD状态等待所有未完成WQE完成structibv_qp_attrattr;attr.qp_stateIBV_QPS_SQD;// 这里踩过坑必须设置sq_draining1否则硬件可能还在处理WQEattr.sq_draining1;retibv_modify_qp(qp,attr,IBV_QP_STATE|IBV_QP_SQ_DRAINING);if(ret){// 别这样写直接打印错误退出// 应该先尝试软重置再硬重置handle_qp_error(qp,ret);}Step 2清空完成队列遍历CQ把所有已完成的WQE取出来。注意有些WQE可能还在硬件流水线里需要轮询直到CQ为空。// 轮询CQ直到空设置超时防止死锁intpoll_count0;while(poll_countMAX_POLL_RETRIES){intneibv_poll_cq(cq,1,wc);if(ne0)break;// CQ空了// 处理完成事件记录未完成的WQEprocess_wc(wc);poll_count;}// 如果超时还没空说明有WQE卡住了需要硬重置QPif(poll_countMAX_POLL_RETRIES){// 别这样写直接abort// 应该尝试ibv_destroy_qp后重建force_reset_qp(qp);}Step 3保存内存注册信息MR的lkey/rkey是硬件分配的进程重启后必须重新注册。但rkey是给远端用的恢复时远端可能还在用旧的rkey发数据——这就是我开头遇到的bug根源。解决方案检查点时让所有对端也保存一份rkey映射表。恢复时每个节点广播自己的新rkey对端更新映射。// 保存MR信息注意不保存lkey/rkey本身structmr_ckpt_info{void*addr;// 虚拟地址恢复时可能变size_tlength;// 内存长度intaccess_flags;// 访问权限// 别这样写uint32_t lkey; // 恢复时重新分配};3.3 检查点恢复重建硬件状态恢复比保存复杂得多因为进程重启后虚拟地址空间变了硬件资源也要重新分配。Step 1重建内存注册先分配和检查点相同大小的内存然后用ibv_reg_mr重新注册。注意新分配的虚拟地址可能和原来不同需要更新所有对端的地址映射。// 重新注册MRstructibv_mr*new_mribv_reg_mr(pd,new_addr,length,access_flags);if(!new_mr){// 这里踩过坑忘记检查errno// 常见错误EINVAL表示access_flags不对ENOMEM表示内存不够perror(ibv_reg_mr failed);// 别这样写直接exit// 应该尝试释放其他MR后再试retry_reg_mr(pd,new_addr,length,access_flags);}Step 2重建QP用保存的QP属性创建新的QP然后通过ibv_modify_qp把状态机恢复到保存时的状态。// 创建QPstructibv_qp_init_attrinit_attr{.send_cqcq,.recv_cqcq,.cap{.max_send_wrsaved_state.max_send_wr,.max_recv_wrsaved_state.max_recv_wr,},.qp_typeIBV_QPT_RC,// 可靠连接};structibv_qp*new_qpibv_create_qp(pd,init_attr);if(!new_qp){// 别这样写直接返回NULL// 应该检查是否资源耗尽尝试释放其他QPhandle_qp_creation_error(pd,init_attr);}// 状态机恢复INIT - RTR - RTS// 注意PSN必须从保存的值开始否则对端会丢包structibv_qp_attrattr;attr.qp_stateIBV_QPS_INIT;// ... 设置其他属性ibv_modify_qp(new_qp,attr,IBV_QP_STATE|...);attr.qp_stateIBV_QPS_RTR;attr.dest_qp_numsaved_state.remote_qpn;// 对端的QP号attr.ah_attr.is_global1;// ... 设置路径属性ibv_modify_qp(new_qp,attr,IBV_QP_STATE|IBV_QP_AV|...);attr.qp_stateIBV_QPS_RTS;attr.sq_psnsaved_state.local_psn;// 关键恢复PSNibv_modify_qp(new_qp,attr,IBV_QP_STATE|IBV_QP_SQ_PSN|...);Step 3同步对端rkey这是最容易被忽略的一步。恢复后新MR的rkey和原来不同必须通知所有对端更新。// 发送新的rkey给所有对端// 这里踩过坑用RDMA写来发送rkey但此时QP还没完全恢复// 应该用传统的TCP/IP或者共享内存来交换rkeysend_new_rkey_via_tcp(peer_list,new_mr-rkey,new_mr-addr);// 等待所有对端确认收到wait_for_ack(peer_list);3.4 数据一致性重放还是丢弃恢复后那些在检查点之前发出的、但还没完成的RDMA操作怎么处理我的经验是对于可靠连接RC全部重放。因为RC保证按序交付只要PSN连续硬件会自动处理重复包。但有一个例外原子操作fetch_and_add, compare_and_swap。这些操作有副作用重放会导致数据错误。解决方案检查点时记录原子操作的计数器恢复时跳过已完成的原子操作。// 重放未完成的RDMA写操作for(inti0;ipending_wqe_count;i){structpending_wqe*wqepending_wqes[i];// 别这样写直接post_send// 应该先检查对端是否已经收到了这个数据if(wqe-opcodeIBV_WR_RDMA_WRITE){// 用新的rkey和地址重新postwqe-wr.wr.rdma.rkeynew_rkey;wqe-wr.wr.rdma.remote_addrnew_remote_addr;ibv_post_send(new_qp,wqe-wr,bad_wr);}}四、性能优化减少检查点开销检查点恢复的代价很高尤其是RDMA连接重建。我试过几种优化方案方案1预分配QP池在程序启动时创建一批QP检查点时只保存状态恢复时从池里取一个重建。这样避免了ibv_create_qp的系统调用开销。// 预分配QP池#defineQP_POOL_SIZE64structibv_qp*qp_pool[QP_POOL_SIZE];intqp_pool_index0;// 恢复时从池里取structibv_qp*reuse_qpqp_pool[qp_pool_index];// 重置QP状态ibv_modify_qp(reuse_qp,reset_attr,IBV_QP_STATE);// 然后重新初始化方案2惰性恢复不是所有QP都需要立即恢复。可以只恢复那些有未完成操作的QP其他的等到真正使用时再恢复。这需要修改应用层的RDMA调用接口。方案3共享内存加速rkey同步用mmap创建共享内存区域所有进程把rkey映射表放在共享内存里。恢复时直接读共享内存省去TCP通信开销。五、调试技巧那些年我踩过的坑PSN不连续导致丢包恢复时PSN必须从保存的值开始否则对端会认为丢包而重传。但重传的包可能和恢复后的新数据冲突。解决方案恢复后先发一个空包0字节RDMA写来同步PSN。MR生命周期管理进程重启后旧MR自动失效。但远端可能还在用旧的rkey。我写了一个rkey版本号机制每次恢复时递增版本号对端收到新版本号后丢弃旧rkey。CQ溢出检查点期间如果CQ积压太多完成事件恢复后轮询CQ会非常慢。解决方案检查点时把CQ深度调大或者用多个CQ分担。QP状态机死锁在SQD状态时如果还有WQE没完成ibv_modify_qp到RESET会失败。必须先清空SQ。我写了一个强制清空函数// 强制清空SQ别这样写直接ibv_destroy_qpvoidforce_drain_qp(structibv_qp*qp){// 先尝试正常drainstructibv_qp_attrattr;attr.qp_stateIBV_QPS_SQD;attr.sq_draining1;if(ibv_modify_qp(qp,attr,IBV_QP_STATE|IBV_QP_SQ_DRAINING)0){// 等待drain完成usleep(1000);}// 如果还不行直接resetattr.qp_stateIBV_QPS_RESET;ibv_modify_qp(qp,attr,IBV_QP_STATE);}六、个人经验建议别相信ibv_wc.status IBV_WC_SUCCESS这个状态只表示硬件层面操作成功不代表数据正确。一定要在应用层做端到端校验比如CRC。检查点频率不是越高越好RDMA连接重建开销很大我测试过每秒做一次检查点性能下降50%以上。建议根据应用的计算粒度来定比如每1000次迭代做一次。优先保证恢复的正确性再优化性能我见过太多人为了追求低延迟在恢复时偷工减料比如不重建MR直接复用旧rkey结果数据损坏。RDMA的硬件状态机非常脆弱任何不一致都会导致难以调试的bug。日志要详细每次检查点恢复都记录QP状态、PSN、rkey等信息。出问题时这些日志是唯一的线索。我习惯用ibv_query_qp把QP的所有属性dump出来。测试要覆盖边界情况比如在RDMA写进行到一半时做检查点恢复后重放会不会导致数据重复我写了一个fuzzer随机在任意时刻触发检查点跑了三天才把所有bug抓完。最后说一句RDMA检查点恢复不是银弹。如果你的应用对延迟极度敏感比如微秒级交易系统那检查点恢复的开销可能无法接受。但对于HPC科学计算这种分钟级甚至小时级的任务花几秒钟做检查点恢复是完全值得的。关键词核心关键词RDMA、QP、MR、CQ、WQE、PSN、HPC、SQD、SQ、INIT精准长尾关键词RDMA编程案例、基于RDMA的HPC检查点恢复、一、一个让我熬夜三天的bug、二、检查点恢复的RDMA特殊性、1 架构设计原则、2 检查点保存冻结硬件状态、RDMA编程案例详解、RDMA编程案例实战、RDMA编程实战、RDMA性能优化、RDMA协议详解、高速网络编程
阅读完成 · 觉得有帮助?
咨询建站