有时为了保证可用性,需要同时访问两路服务,哪个先返回就取哪个。在brpc中,这有多种做法: # 当后端server可以挂在一个命名服务内时 Channel开启backup request。这个Channel会先向其中一个server发送请求,如果在ChannelOptions.backup_request_ms后还没回来,再向另一个server发送。之后哪个先回来就取哪个。在设置了合理的backup_request_ms后,大部分时候只会发一个请求,对后端服务只有一倍压力。 示例代码见[example/backup_request_c++](https://github.com/apache/brpc/blob/master/example/backup_request_c++)。这个例子中,client设定了在2ms后发送backup request,server在碰到偶数位的请求后会故意睡眠20ms以触发backup request。 运行后,client端和server端的日志分别如下,"index"是请求的编号。可以看到server端在收到第一个请求后会故意sleep 20ms,client端之后发送另一个同样index的请求,最终的延时并没有受到故意sleep的影响。 ![img](../images/backup_request_1.png) ![img](../images/backup_request_2.png) /rpcz也显示client在2ms后触发了backup超时并发出了第二个请求。 ![img](../images/backup_request_3.png) ## 选择合理的backup_request_ms 可以观察brpc默认提供的latency_cdf图,或自行添加。cdf图的y轴是延时(默认微秒),x轴是小于y轴延时的请求的比例。在下图中,选择backup_request_ms=2ms可以大约覆盖95.5%的请求,选择backup_request_ms=10ms则可以覆盖99.99%的请求。 ![img](../images/backup_request_4.png) 自行添加的方法: ```c++ #include #include ... bvar::LatencyRecorder my_func_latency("my_func"); ... butil::Timer tm; tm.start(); my_func(); tm.stop(); my_func_latency << tm.u_elapsed(); // u代表微秒,还有s_elapsed(), m_elapsed(), n_elapsed()分别对应秒,毫秒,纳秒。 // 好了,在/vars中会显示my_func_qps, my_func_latency, my_func_latency_cdf等很多计数器。 ``` ## Backup Request 限流 如需限制 backup request 的发送比例,可使用内置工厂函数创建限流策略,也可自行实现 `BackupRequestPolicy` 接口。 优先级顺序:`backup_request_policy` > `backup_request_ms`。 ### 使用内置限流策略 调用 `CreateRateLimitedBackupPolicy` 创建限流策略,并将其设置到 `ChannelOptions.backup_request_policy`: ```c++ #include "brpc/backup_request_policy.h" #include brpc::RateLimitedBackupPolicyOptions opts; opts.backup_request_ms = 10; // 超过10ms未返回时发送backup请求 opts.max_backup_ratio = 0.3; // backup请求比例上限30% opts.window_size_seconds = 10; // 滑动窗口宽度(秒) opts.update_interval_seconds = 5; // 缓存比例的刷新间隔(秒) // CreateRateLimitedBackupPolicy返回的指针由调用方负责释放。 // policy的生命周期必须长于channel——先销毁channel,再销毁policy。 std::unique_ptr policy( brpc::CreateRateLimitedBackupPolicy(opts)); brpc::ChannelOptions options; options.backup_request_policy = policy.get(); // Channel不拥有该对象 channel.Init(..., &options); // channel必须在policy析构之前销毁。 ``` 参数说明(`RateLimitedBackupPolicyOptions`): | 字段 | 默认值 | 说明 | |------|--------|------| | `backup_request_ms` | -1 | 超时阈值(毫秒)。-1 表示继承 `ChannelOptions.backup_request_ms`(仅在通过 `ChannelOptions.backup_request_policy` 设置策略时有效;通过 Controller 注入时没有 channel 级的回退值,应显式指定 >= 0 的值)。必须 >= -1。 | | `max_backup_ratio` | 0.1 | backup比例上限,取值范围 (0, 1] | | `window_size_seconds` | 10 | 滑动窗口宽度(秒),取值范围 [1, 3600] | | `update_interval_seconds` | 5 | 缓存刷新间隔(秒),必须 >= 1 | 参数不合法时 `CreateRateLimitedBackupPolicy` 返回 `NULL`。 ### 使用自定义 BackupRequestPolicy 如需完全控制,可实现 `BackupRequestPolicy` 接口并设置到 `ChannelOptions.backup_request_policy`: ```c++ #include "brpc/backup_request_policy.h" class MyBackupPolicy : public brpc::BackupRequestPolicy { public: int32_t GetBackupRequestMs(const brpc::Controller*) const override { return 10; // 10ms后发送backup } bool DoBackup(const brpc::Controller*) const override { return should_allow_backup(); // 自定义逻辑 } void OnRPCEnd(const brpc::Controller*) override { // 每次RPC结束时调用,可在此更新统计 } }; MyBackupPolicy my_policy; brpc::ChannelOptions options; options.backup_request_policy = &my_policy; // Channel不拥有该对象,需保证其生命周期长于Channel channel.Init(..., &options); ``` ### 实现说明 - 比例通过bvar计数器在滑动时间窗口内统计。缓存值通过无锁CAS选举最多每 `update_interval_seconds` 刷新一次,因此每次RPC的开销极低(公共路径仅有两次原子读)。 - Backup决策在做出时立即计数(RPC完成前),以便在延迟抖动期间更快地反馈。总RPC数在完成时统计。这意味着比例在抖动期间可能短暂滞后,这是设计有意为之——限流器的目标是近似的尽力而为的节流,而非精确执行。 - 每个使用限流的Channel会维护两个 `bvar::Window` 采样任务,在Channel数量极多的部署中请留意此开销。 # 当后端server不能挂在一个命名服务内时 【推荐】建立一个开启backup request的SelectiveChannel,其中包含两个sub channel。访问这个SelectiveChannel和上面的情况类似,会先访问一个sub channel,如果在ChannelOptions.backup_request_ms后没返回,再访问另一个sub channel。如果一个sub channel对应一个集群,这个方法就是在两个集群间做互备。SelectiveChannel的例子见[example/selective_echo_c++](https://github.com/apache/brpc/tree/master/example/selective_echo_c++),具体做法请参考上面的过程。 【不推荐】发起两个异步RPC后Join它们,它们的done内是相互取消的逻辑。示例代码见[example/cancel_c++](https://github.com/apache/brpc/tree/master/example/cancel_c++)。这种方法的问题是总会发两个请求,对后端服务有两倍压力,这个方法怎么算都是不经济的,你应该尽量避免用这个方法。