尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

brpc bthread_id 深度解析:RPC 全流程同步、超时/取消处理与 O(1) 上下文定位的底层机制

brpc bthread_id 深度解析:RPC 全流程同步、超时/取消处理与 O(1) 上下文定位的底层机制 brpc bthread_id 深度解析RPC 全流程同步、超时/取消处理与 O(1) 上下文定位的底层机制【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. brpc means better RPC.项目地址: https://gitcode.com/GitHub_Trending/brpc/brpcbthread_id 是 brpc 为 RPC 调用生命周期设计的一把总钥匙它既能像互斥锁一样串行化一次 RPC 中发送、接收、超时、重试等不同环节又能以 O(1) 代价从网络报文里的correlation_id直接找回对应的 RPC 上下文Controller。本文以 docs/en/bthread_id.md 为主线结合 id.cpp、id.h、types.h 的源码实现与 test/bthread_id_unittest.cpp 的测试用例完整讲解 bthread_id 的设计动机、API 语义、典型使用流程、内部状态机以及在 brpc Channel 中的真实应用帮助你理解为什么 brpc 能优雅地闭合那些其他 RPC 框架广泛存在的并发竞争问题。一、为什么需要 bthread_id一个 RPC 生命周期里的经典竞争问题一个普通的同步 RPC 调用从发起到结束会经历发请求 → 收响应 → 处理结果/超时/重试等多个阶段而这些阶段往往由不同执行流驱动发送方 bthread、网络收包线程、定时器线程、重试逻辑等。如果不加任何同步手段会出现一系列经典竞争response 先于发送完成返回发送代码尚未执行完例如还没注册好回调、还没设置好内部状态响应处理代码就开始运行两者竞争同一份 Controller 状态timer 设置后立刻触发超时定时器注册的瞬间就可能被触发超时处理代码与发送代码竞争重试产生的多个 response 同时到达多次重试的响应可能在同一个时刻返回彼此之间以及和主流程之间互相竞争由correlation_id找 RPC 上下文网络协议里通常只携带一个整数形式的correlation_id若通过correlation_id → 上下文的全局哈希表查找多线程高并发下既慢又难维护取消 RPC需要一种安全、可竞争的方式把取消这个信号传递给正在运行的 RPC。以上这些问题在其他 RPC 框架中广泛存在。brpc 给出的答案就是bthread_id它把一次 RPC 的所有环节用一把可携带数据、可附加错误、可 O(1) 寻址的 ID 锁串行起来。注意本文讨论的是bthread_id_tRPC 生命周期 ID不是bthread_tbthread 的线程 tid。这两个名字确实容易混淆这是历史命名问题使用时务必区分bthread_t由bthread_start_*返回表示一个 M:N 调度下的用户态线程而bthread_id_t是同步/寻址结构与线程无关。二、数据结构64 位 ID 的用户视图与隐藏的 Id 结构体一个bthread_id由两部分组成用户可见的 64 位 id即bthread_id_t定义在 types.htypedef struct { uint64_t value; } bthread_id_t; // bthread_id returned by bthread_id_create* can never be this value. // NOTE: dont confuse with INVALID_BTHREAD! static const bthread_id_t INVALID_BTHREAD_ID {0};隐藏的bthread::Id结构体内部元数据用户接口只操作 id通过 id 定位结构体。id 的编码方式与 brpc 中其他结构一致64 位被切分为两段见 id.cpp位段含义作用高 32 位内存池ResourcePool偏移slotO(1) 时间定位到Id元数据低 32 位version版本号防止 ABA 问题id 被销毁并复用后旧版本号会失效对应源码inline bthread_id_t make_id(uint32_t version, IdResourceId slot) { const bthread_id_t tmp { (((uint64_t)slot.value) 32) | (uint64_t)version }; return tmp; } inline IdResourceId get_slot(bthread_id_t id) { const IdResourceId tmp { (id.value 32) }; return tmp; }也就是说bthread_id_t的value永远不会为 0INVALID_BTHREAD_ID保留高 32 位负责在哪低 32 位负责是不是我。内部的Id结构体id.cpp则携带了锁状态、用户数据和错误回调struct BAIDU_CACHELINE_ALIGNMENT Id { // first_ver ~ locked_ver - 1: unlocked versions // locked_ver: locked // unlockable_ver: locked and about to be destroyed // contended_ver: locked and contended uint32_t first_ver; uint32_t locked_ver; FastPthreadMutex mutex; void* data; // create 时绑定的用户数据 int (*on_error)(bthread_id_t, void*, int); int (*on_error2)(bthread_id_t, void*, int, const std::string); const char *lock_location; uint32_t* butex; // 锁状态所在的 futex 单元 uint32_t* join_butex; // join 等待所在的 futex 单元 SmallQueuePendingError, 2 pending_q; // 见减少等待一节 };其中butex是 brpc 自研的 futex 类原语bthread/butex用于在 bthread 上实现可挂起的锁等待这正是 bthread_id 能够不阻塞 worker 线程地等待锁的原因。三、API 全景六个核心接口与它们的语义bthread_id的接口不算少这是为了覆盖不同的使用流程。核心 API 如下完整声明见 id.hAPI语义bthread_id_create创建 id绑定data与on_error回调写入*idbthread_id_lock加锁若已被他人锁住则等待成功时通过pdata返回绑定的databthread_id_unlock解锁若 pending 队列中有积压的错误则取出执行其on_errorbthread_id_unlock_and_destroy解锁并销毁 id唤醒所有join/lock等待者bthread_id_join同步等待该 id 被销毁bthread_id_error向 id 注入错误未锁则直接加锁执行on_error已锁则入 pending 队列立即返回围绕这六个核心接口还有一组配套 API 覆盖不同场景均可在 id.h 中找到声明bthread_id_create_ranged/bthread_id_create2/bthread_id_create2_ranged_ranged变体让*id, *id1, ..., *idrange-1映射到同一个内部实体方便把版本号编码进 id_2变体C 专用额外携带error_text字符串错误信息更完整。range限制在[1, 1024]id.cpp 中ID_MAX_RANGE 1024。bthread_id_trylock非阻塞加锁成功返回 0已被锁返回EBUSY。bthread_id_lock_and_reset_range加锁的同时重置 range新的 range 必须大于当前 range 才生效见 id.cppbrpc 的CallMethod正是用它按max_retry动态分配版本区间。bthread_id_cancel销毁一个已创建但从未被使用的 id。bthread_id_about_to_destroy宣告即将销毁让正在等待的bthread_id_lock立即以EPERM失败详见下文。bthread_id_error2/bthread_id_error2_verbose带错误描述文本的错误注入。bthread_id_list_*系列管理一批 id 的列表init/destroy/add/swap/resetreset会向列表中所有有效 id 注入同一个错误码线程安全版reset_pthreadsafe/reset_bthreadsafe通过先 swap 出列表、锁外再 reset缩小临界区id.cpp。重要约定on_error回调必须在内部调用bthread_id_unlock()或bthread_id_unlock_and_destroy()否则 id 会一直处于锁定状态。如果bthread_id_create时传入的on_error为nullptr会使用默认回调default_bthread_id_on_error其行为正是bthread_id_unlock_and_destroyid.cpp——这也解释了为什么不传 on_error 调用 bthread_id_error等价于直接销毁测试default_error_is_destroy验证了这一行为。四、典型使用流程发送 / 接收 / 错误处理 / 同步等待这是 bthread_id 设计的灵魂不同的流程阶段通过锁串行化互不竞争。1. 发送 request 的流程bthread_id_create → bthread_id_lock → ... 注册 timer 并发送 RPC ... → bthread_id_unlock发送方先创建并锁住 id在其保护下完成注册超时定时器 把请求写进 socket等准备工作然后解锁。这样即便 timer 立即触发、或响应立刻回来它们也必须先拿到这把锁不可能与发送代码并发执行。2. 接收 response 的流程bthread_id_lock → ... 处理 response ... → bthread_id_unlock_and_destroy收包线程拿到锁后独占处理响应完成后直接销毁销毁会让所有等待者join、后续的lock立即醒来RPC 生命周期宣告结束。3. 异常处理流程timeout / socket fail → bthread_id_error → 执行 on_error 回调回调内部持有锁分两种情况 - 可以重试 / 发 backup request重新注册 timer 并发送 RPC → bthread_id_unlock - 无法重试最终失败bthread_id_unlock_and_destroy错误不是直接改状态而是通过bthread_id_error注入由on_error回调在持锁状态下决策是继续重试解锁后进入下一轮生命周期还是彻底结束解锁并销毁。4. 同步等待 RPC 结束bthread_id_join调用方如同步式Channel::CallMethod的调用线程调用bthread_id_join挂起直到该 id 被unlock_and_destroy唤醒从而把异步完成的 RPC转换为同步等待。五、减少等待的两大优化机制为了让上述流程在竞争下依然高效bthread_id 内置了两个关键机制1. Pending 队列错误先入队解锁后统一执行当bthread_id_error到达时如果 id已经被锁住错误不会被丢弃也不会阻塞调用者而是被压入pending_q内部是SmallQueuePendingError, 2容量 2 以内用内嵌数组、超出后升级为std::deque见 id.cppbthread_id_error立即返回。真正执行发生在bthread_id_unlock时解锁函数先从 pending 队列pop出一条错误转而在持锁状态下调用对应的on_errorid.cpp。若on_error选择继续重试并再次bthread_id_unlock则会接着取出队列中的下一条错误继续处理若选择unlock_and_destroy则清空整个 pending 队列id.cpp未处理的错误被丢弃。测试 bthread_id_unittest.cppmany_error精确验证了这一行为id 未锁时注入 N 个错误立即逐个执行id 锁住时再注入 N 个错误on_error一个都不跑直到bthread_id_unlock才把积压的 N 个错误全部按序取出执行。2. about_to_destroy先让等待者失败再执行不可控的用户回调当 RPC 结束且存在用户回调done时直接执行回调会让bthread_id_lock的等待者干等一段不可预测的时间。brpc 的解法是三步走先调用bthread_id_about_to_destroy把butex置为unlockable_ver并唤醒所有在等待的bthread_id_lock调用者让它们立即以EPERM失败id.cpp再执行用户回调可能耗时较长、不可控最后bthread_id_unlock_and_destroy真正销毁。这样既保证了 id 在即将销毁但还 joinable的状态下没有无谓的等待又避免了回调期间锁被长期占用。测试about_to_destroy_before_locking/about_to_destroy_during_locking/about_to_destroy_cancelledbthread_id_unittest.cpp分别验证了销毁前、锁等待中、以及 about_to_destroy 后被 unlock 取消三种场景等待者都以EPERM立即退出如果之后只是bthread_id_unlock而没有销毁about_to_destroy的效果会被取消后续lock恢复正常。六、源码级原理Id 内部的版本状态机理解 bthread_id 的锁与销毁语义关键是Id中butex所代表的版本状态机id.cppfirst_ver ~ locked_ver - 1 : 未锁定可获取的版本区间 locked_ver : 已锁定 unlockable_ver locked2 : 已锁定且 about_to_destroy contended_ver locked1 : 已锁定且有等待者创建butex即当前版本跳 0 防溢出后设为 1first_ver butexlocked_ver butex rangeid.cpp。普通create等价于range 1加锁trylock直接把butex置为locked_verlock若发现已被锁先把butex置为contended_ver再butex_wait挂起解锁方通过butex_wake唤醒id.cpp 与 id.cpp销毁unlock_and_destroy把butex与join_butex都推进到end_ver locked_ver 3first_ver/locked_ver同步推进然后唤醒所有等待者、归还资源id.cpp。由此可以从源码与测试推断出版本号的推进规律create后当前版本为 V普通 createrange1后unlock_and_destroy版本推进V 4测试error_is_destroy、join_after_destroy中get_version(id) 4rangedrange2后销毁版本推进V 5测试error_is_destroy_ranged中get_version(id1) 5trylock成功后版本为V 1即locked_ver。版本不断递增配合高 32 位的资源槽位从机制上杜绝了 ABA旧 id 在结构体被复用后会因版本不匹配而在has_version()检查id.cpp处被判定为无效EINVAL这正是测试doubly_destroy中断言对已销毁 id 再次bthread_id_error返回EINVAL的原因。七、在 brpc RPC 中的真实应用correlation_id 与 Channelbrpc 把bthread_id_t直接用作 RPC 的调用 ID——CallIdcontroller.htypedef bthread_id_t CallId;Controller持有_correlation_id网络协议报文里传输的correlation_id就是这个 64 位 id服务端/客户端因此可以在O(1) 时间内定位到 RPC 上下文而无需correlation_id → 上下文的全局哈希表。在 channel.cpp 的Channel::CallMethod中可以看到完整应用const CallId correlation_id cntl-call_id(); const int rc bthread_id_lock_and_reset_range( correlation_id, nullptr, 2 cntl-max_retry());发送前用bthread_id_lock_and_reset_range加锁并按max_retry动态重置版本区间2 max_retry对应初次请求 重试 backup所需的版本槽位负的max_retry会导致未定义行为代码会强制归零。之后在这个锁的保护下完成设置超时、选择目标、发送等步骤最后解锁。错误注入则完全走bthread_id_error通道例如 channel.cppstatic void HandleTimeout(void* arg) { bthread_id_t correlation_id { (uint64_t)arg }; bthread_id_error(correlation_id, ERPCTIMEDOUT); } static void HandleBackupRequest(void* arg) { bthread_id_t correlation_id { (uint64_t)arg }; bthread_id_error(correlation_id, EBACKUPREQUEST); }超时定时器触发 →bthread_id_error(id, ERPCTIMEDOUT)需要发 backup request当慢请求超时比例超过阈值时提前发起第二次请求→bthread_id_error(id, EBACKUPREQUEST)socket 失败、取消Controller::RunOnCancel等也都通过on_error回调汇聚到同一个Controller处理入口。这正是原文档所述取消 RPC与超时/重试竞争问题的落地点所有异步事件都变成对同一把锁的错误注入由持锁的on_error统一裁决——继续重试、发 backup、还是彻底失败从而保证任何时刻只有一个执行流在修改 RPC 状态。八、测试验证行为即契约test/bthread_id_unittest.cpp 是对上述语义最直接的契约化验证建议按以下顺序阅读join_after_destroy/join_before_destroy/join_after_destroy_before_unlock验证join在 id 销毁前/后/销毁前解锁前三种时序下的行为以及 ranged id 的id1、id11共享同一实体error_is_destroy/error_is_destroy_ranged/default_error_is_destroy/doubly_destroy验证错误注入即销毁、默认 on_error 行为、重复销毁返回EINVALmany_error验证 pending 队列的积压与解锁后按序执行id_lock/id_lock_and_destroy验证多线程下 lock 的互斥与销毁唤醒about_to_destroy_*三个用例验证about_to_destroy让等待者EPERM失败、且可被unlock取消list_signal验证bthread_id_list_reset向列表中所有 id 批量注入错误error_with_descriptions验证create/create2与error/error2的混用都能正确路由到对应的on_error/on_error2status通过id_status打印出 id 的锁定状态、pending 队列与 on_error 信息是调试 bthread_id 的利器。九、使用注意事项与适用边界on_error内必须解锁bthread_id_error触发的on_error回调运行于持锁状态回调内必须调用bthread_id_unlock或bthread_id_unlock_and_destroy否则 id 永久锁定id.h 有醒目注释测试error_without_unlock也演示了这种错误用法它不是通用互斥锁id.h 头部注释明确指出bthread_id 面向对某个对象的非高竞争动作序列的管理比 mutex 慢不适合做一般性同步——通用同步请使用bthread_mutex/butex等原语ranged id 的 range 上限为 1024且lock_and_reset_range只能扩大不能缩小已有 rangeid 复用与 ABA得益于 version 机制旧 id 的操作会被安全拒绝EINVAL但这也意味着版本号会随生命周期单调递增超长运行的服务应留意 32 位版本号的回绕源码在create时对溢出做了保护处理需要批量管理 id例如一次取消一组 RPC时优先使用bthread_id_list_*系列接口并遵循锁内 swap、锁外 reset的范式以缩小临界区。十、小结bthread_id是 brpc 的 RPC 生命周期管理基石用一把携带数据、可注入错误、可 O(1) 寻址的版本化 ID 锁把发送、接收、超时、重试、取消、同步等待全部串行化从根上消除了 RPC 各环节之间的竞争同时用 pending 队列与about_to_destroy把等待降到最低。理解了 bthread_id就理解了 brpc 的correlation_id为何能如此轻量地完成上下文寻址与错误汇聚——这也是 brpc 在高并发场景下保持正确性与性能的关键一环。进一步阅读可参考中英对照文档 docs/cn/bthread_id.md以及同系列的内存管理机制 docs/en/memory_management.md。【免费下载链接】brpcbrpc is an Industrial-grade RPC framework using C Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, Recommendation etc. brpc means better RPC.项目地址: https://gitcode.com/GitHub_Trending/brpc/brpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表