
brpc 负载均衡与健康检查机制深度解析【免费下载链接】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/brpcbrpc 作为一个工业级 RPC 框架其 Client 端通过命名服务发现下游节点、通过负载均衡算法分配流量、通过健康检查隔离并恢复故障节点。本文将围绕 docs/cn/load_balancing.md 中描述的三大核心机制——命名服务NamingService、负载均衡LoadBalancer与健康检查Health Checking结合仓库源码实现细节展开全面深入的分析。brpc 的负载均衡设计具有三个显著特点一是将命名服务的控制权反转让用户通过回调接口驱动框架更新节点列表从而支持事件驱动型命名服务二是所有负载均衡算法都基于无锁的 DoublyBufferedData 实现保证多线程下零竞争三是健康检查采用每连接一个 bthread的轻量方案替代传统集中式健康检查线程。读者通过本文可以掌握 brpc 的负载均衡全链路原理、命名服务/负载均衡器的扩展方法以及健康检查的完整生命周期管理。上图分别展示了 src/brpc/global.cpp 中命名服务和负载均衡器的注册代码。一、整体架构从命名服务到流量分配brpc 的 Client 端处理链路遵循命名服务 → 负载均衡 → 健康检查的闭环流程上游Client通过命名服务发现所有下游节点Server命名服务把服务名映射为一个可修改的机器列表当列表发生变化时框架通过 NamingServiceActions 通知观察者将最新的 ServerNode 列表同步到负载均衡器中每次 RPC 请求时负载均衡器根据配置的算法如 rr、wrr、random、la、p2c 等从节点列表中选择一台服务器当下游节点出现问题连接失败、超时等时对应的 Socket 会被 SetFailed 标记从负载均衡的候选集中剔除进入健康检查流程被隔离的节点由健康检查线程定期探测成功后通过 Socket::Revive 复活重新加入正常节点集合。这段链路的核心抽象定义在 src/brpc/naming_service.h、src/brpc/load_balancer.h 和 src/brpc/details/health_check.cpp 中。下面分别深入解析。二、命名服务NamingService控制权反转的设计2.1 设计思想为什么反转控制权命名服务的职责是获得服务名对应的所有节点。一个直观的做法是定期调用一个函数以获取最新的节点列表但这会带来两个问题固定的轮询延时定期调用的周期一般在若干秒左右节点变化无法被及时感知无法利用事件通知当命名服务本身提供事件通知时例如 ZooKeeper 的 watch 机制轮询方式白白浪费了这种能力。因此 brpc 反转了控制权不是框架去调用用户的函数而是用户在获得列表后调用框架的接口。对应的两个核心接口定义在 src/brpc/naming_service.h 中// 框架提供给用户NamingService 实现者的回调入口 class NamingServiceActions { public: virtual ~NamingServiceActions() {} virtual void AddServers(const std::vectorServerNode servers) 0; virtual void RemoveServers(const std::vectorServerNode servers) 0; virtual void ResetServers(const std::vectorServerNode servers) 0; }; // 用户需要实现的命名服务基类 class NamingService : public Describable, public Destroyable { public: // 在独立的 bthread 中运行无需线程安全 virtual int RunNamingService(const char* service_name, NamingServiceActions* actions) 0; virtual bool RunNamingServiceReturnsQuickly() { return false; } virtual NamingService* New() const 0; };RunNamingService在框架启动的一个独立 bthread 中执行即 NamingServiceThread实现者在这个线程里持续获取节点列表并通过actions的三个方法把变化告诉框架ResetServers全量重置服务器列表框架会去重并与之前的列表比较然后通知观察者 NamingServiceWatcherAddServers/RemoveServers增量增减服务器。从 src/brpc/naming_service.h 的注释可以看到这个方法的运行环境是dedicated bthread实现时不需要考虑线程安全问题。一个 NamingServiceThread 可以被多个 Channel 共享通过 intrusive_ptr 管理其 ownership。此外RunNamingServiceReturnsQuickly()返回 true 的简单实现如 list 命名服务可以不创建独立 bthread节省线程创建开销——但大多数实现会一直运行因此默认仍需要线程。2.2 三种内建命名服务实现文档以三个实现为例说明这套机制它们在 src/brpc/policy/ 目录下均有对应源码命名服务protocol获取方式关键源码bnsbns://node-name无事件通知定期拉取默认间隔 5 秒由-ns_access_intervalgflag 控制src/brpc/policy/baidu_naming_service.cppfilefile://file-path通过 FileWatcher 监听文件修改时间文件变更后重新读取src/brpc/policy/file_naming_service.cpplistlist://addr1,addr2,...列表内嵌在服务名中读取一次后即退出src/brpc/policy/list_naming_service.cppPeriodicNamingService 抽象对于 bns 这类只能轮询的命名服务brpc 提供了 PeriodicNamingService 基类实现者只需要实现单次获取逻辑GetServers()class PeriodicNamingService : public NamingService { protected: virtual int GetServers(const char *service_name, std::vectorServerNode* servers) 0; virtual int GetNamingServiceAccessIntervalMs() const; int RunNamingService(const char* service_name, NamingServiceActions* actions) override; };file 命名服务的源码级细节在 src/brpc/policy/file_naming_service.cpp 中可以看到它的完整循环逻辑先初始化 FileWatcher 关注文件然后进入for(;;)循环——每次先GetServers读取并ResetServers随后调用fw.check_and_consume()检查文件是否变化如果文件被删除会打印错误日志bthread_usleep(100000L)即每 100ms 轮询一次文件状态只有检测到文件变化change 0才跳出内层循环重新加载。文件解析还支持地址 tag和#注释见SplitIntoServerAndTagsrc/brpc/policy/file_naming_service.cpp并使用std::setServerNode去重且保持文件顺序。list 命名服务列表在服务名里以逗号分隔读取完一次并调用ResetServers后就退出——因为列表不会再变化了。类似的还有dlist动态列表可以从一个地址拉取远端列表和remotefile。2.3 以字符串描述命名服务工厂模式内建为了让用户免于写工厂代码brpc 把字符串 → 对象的工厂逻辑内建到了框架里。用户只需要在Channel.Init()时传入如下格式的naming_service_urlprotocol://service-name e.g. bns://node-name # baidu naming service file://file-path # load addresses from the file list://addr1,addr2,... # use the addresses separated by comma http://url # Domain Naming Service, aka DNS.这套机制基于 brpc 的 Extension 扩展点实现在 src/brpc/global.cpp 中统一注册// Naming Services NamingServiceExtension()-RegisterOrDie(file, g_ext-fns); NamingServiceExtension()-RegisterOrDie(list, g_ext-lns); NamingServiceExtension()-RegisterOrDie(dlist, g_ext-dlns); NamingServiceExtension()-RegisterOrDie(http, g_ext-dns); NamingServiceExtension()-RegisterOrDie(https, g_ext-dns_with_ssl); NamingServiceExtension()-RegisterOrDie(redis, g_ext-dns); NamingServiceExtension()-RegisterOrDie(remotefile, g_ext-rfns); NamingServiceExtension()-RegisterOrDie(consul, g_ext-cns); NamingServiceExtension()-RegisterOrDie(discovery, g_ext-dcns); NamingServiceExtension()-RegisterOrDie(nacos, g_ext-nns); #ifdef BAIDU_INTERNAL NamingServiceExtension()-RegisterOrDie(bns, g_ext-bns); #endif从这份注册表可以看到除了文档重点讲解的 bns/file/list 之外仓库还提供了 httpDNS 域名服务、https自动开启 SSL、redis复用 DNS 解析、remotefile、consul、discovery、nacos 等多种命名服务。它们共同遵循protocol://service-name的统一字符串格式用户可以在新建 Channel 时传入这类描述并直接写在各类配置文件中例如brpc::Channel channel; brpc::ChannelOptions options; // 从文件中加载服务器列表 channel.Init(file://conf/machine_list, rr, options);如何扩展自定义命名服务实现了新的 NamingService 后在 src/brpc/global.cpp 中依葫芦画瓢注册即可。完整的扩展步骤为继承brpc::NamingService或PeriodicNamingService实现RunNamingService/GetServers与New提供一个全局实例如g_ext中的成员在全局初始化处调用NamingServiceExtension()-RegisterOrDie(protocol, instance)之后便可在任何Channel.Init(yourprotocol://service-name, ...)中使用该协议。三、负载均衡LoadBalancer多算法与无锁选择3.1 LoadBalancer 抽象接口brpc 中LoadBalancersrc/brpc/load_balancer.h的职责是从多个服务节点中选择一个节点。其核心接口如下class LoadBalancer : public NonConstDescribable, public Destroyable { public: struct SelectIn { int64_t begin_time_us; bool changable_weights; // 节点权重可能变化 bool has_request_code; // 一致性哈希等算法需要 request_code uint64_t request_code; const ExcludedServers* excluded; // 重试时排除已尝试过的 server }; struct SelectOut { SocketUniquePtr* ptr; bool need_feedback; // 为 true 时 RPC 结束后回调 Feedback() }; struct CallInfo { int64_t begin_time_us; SocketId server_id; int error_code; const Controller* controller; }; virtual bool AddServer(const ServerId server) 0; virtual bool RemoveServer(const ServerId server) 0; virtual size_t AddServersInBatch(const std::vectorServerId servers) 0; virtual size_t RemoveServersInBatch(const std::vectorServerId servers) 0; virtual int SelectServer(const SelectIn in, SelectOut* out) 0; virtual void Feedback(const CallInfo /*info*/) { } virtual LoadBalancer* New(const butil::StringPiece params) const 0; };注意 src/brpc/load_balancer.h 中的关键约束所有方法必须线程安全。注释明确指引读者参考policy/round_robin_load_balancer.cpp学习如何用DoublyBufferedData让SelectServer保持低竞争。这正是文档中强调的核心点Load balancer 最重要的是如何让不同线程中的负载均衡不互斥。SharedLoadBalancer同一文件中对 LoadBalancer 做了 intrusive 共享封装它维护一个_weight_sum原子计数AddServer时加一、RemoveServer时减一并提供Weight()查询当前节点数同时可通过-show_lb_in_vars开关在 bvar 中暴露负载均衡状态。3.2 负载均衡算法注册表与选择指南与 NamingService 类似brpc 用字符串指代一个负载均衡器在 src/brpc/global.cpp 中注册LoadBalancerExtension()-RegisterOrDie(rr, g_ext-rr_lb); LoadBalancerExtension()-RegisterOrDie(wrr, g_ext-wrr_lb); LoadBalancerExtension()-RegisterOrDie(random, g_ext-randomized_lb); LoadBalancerExtension()-RegisterOrDie(wr, g_ext-wr_lb); LoadBalancerExtension()-RegisterOrDie(la, g_ext-la_lb); LoadBalancerExtension()-RegisterOrDie(p2c, g_ext-p2c_ewma_lb); LoadBalancerExtension()-RegisterOrDie(c_murmurhash, g_ext-ch_mh_lb); LoadBalancerExtension()-RegisterOrDie(c_md5, g_ext-ch_md5_lb); LoadBalancerExtension()-RegisterOrDie(c_ketama, g_ext-ch_ketama_lb); LoadBalancerExtension()-RegisterOrDie(c_murmurhash_bl, g_ext-ch_mh_bl_lb); LoadBalancerExtension()-RegisterOrDie(_dynpart, g_ext-dynpart_lb);各算法实现位于 src/brpc/policy/ 目录与算法名一一对应。load_balancer_name还可以携带参数例如random:min_working_instances6 hold_seconds10SharedLoadBalancer::ParseParameterssrc/brpc/load_balancer.h负责解析lb_name与参数。下面给出完整的算法选型表依据 docs/cn/client.md 与各算法源码算法名全称适用场景与特性参数源码文件rrround robin轮询选择下一台服务器要求各节点配置/网络/负载类似无src/brpc/policy/round_robin_load_balancer.cppwrrweighted round robin按权重轮询选到机会正比于权重且结果能较均衡散开实例 tag 须为 int32 权重数字如tag50src/brpc/policy/weighted_round_robin_load_balancer.cpprandomrandomized随机选择前提同 rr无src/brpc/policy/randomized_load_balancer.cppwrweighted random按权重随机选择实例 tag 同 wrrsrc/brpc/policy/weighted_randomized_load_balancer.cpplalocality-aware优先选择延时低的下游实现原理见 docs/cn/lalb.md无src/brpc/policy/locality_aware_load_balancer.cppp2cpower-of-two-choices peak-EWMA随机采样两台服务器把请求发给延时 * (inflight 1) / 权重得分较低者延时尖峰敏感、恢复按 tau 衰减开销 O(1) 与集群规模无关p2c:choices4每次比较采样台数、p2c:tau_ms5000衰减时间src/brpc/policy/p2c_ewma_load_balancer.cppc_murmurhash/c_md5/c_ketamaconsistent hashing一致性哈希增删机器不会使分桶剧烈变化适合 cache 类服务需设置 request_codec_murmurhash:replicas300虚拟节点数默认 100由-chash_num_replicas控制src/brpc/policy/consistent_hashing_load_balancer.cppc_murmurhash_blconsistent hashing with bounded loads带负载上限的一致性哈希Mirrokni 等CACM 2017命中 server 已达容量上限ceil(load_factor * 平均在途请求数)时沿环顺时针溢出到下一个有余量节点热点 key 不再压垮单台机器且溢出总是落到固定后继节点c_murmurhash_bl:load_factor1.5默认 1.25必须大于 1来自-chash_bounded_load_factor、replicas同 c_murmurhashsrc/brpc/policy/consistent_hashing_load_balancer.cpp_dynpartdynamic partition动态分区算法—src/brpc/policy/dynpart_load_balancer.cpp一致性哈希使用要点摘自 docs/cn/client.md发起 RPC 前必须调用Controller.set_request_code()否则 RPC 失败request_code 一般是请求中主键部分的 32 位哈希值request_code 所用的哈希算法不需要与负载均衡算法一致如用c_murmurhash也可以用 MD5 算哈希值常用哈希函数集中在 src/brpc/policy/hasher.h例如controller.set_request_code(brpc::policy::MurmurHash32(key.data(), key.size()))注意甄别主键与属性把整个请求算哈希会导致属性变化时目的地剧烈变化同时注意结构体 padding 问题——应序列化或紧密排列后再计算其他负载均衡算法不需要也不会使用request_code即使设置了也会被忽略。集群宕机恢复时的客户端限流docs/cn/client.md当集群中所有 server 不可用时集群进入恢复状态。设min_working_instances为恰好能服务所有请求的 server 数量q为当前可用 server 数量则恢复期间 client 以概率q/min_working_instances接受请求否则丢弃错误码brpc::ERJECT被拒绝的请求不会被框架重试若在hold_seconds内q保持不变则把流量重新发送到全部可用 server 并离开恢复状态。该机制要求下游 server 能力类似目前仅对rr和random生效开启方式channel.Init(http://..., random:min_working_instances6 hold_seconds10, options);3.3 无锁实现DoublyBufferedData文档指出负载均衡最核心的工程问题是如何让不同线程中的负载均衡不互斥其解决方案是 DoublyBufferedData详见 docs/cn/lalb.md。这是一个双缓冲数据结构一份正在被读的数据 一份正在被写的数据读者永远无锁访问当前活跃副本写者修改另一副本后原子切换从而让SelectServer这种高频读路径完全无锁、零竞争。round_robin_load_balancer.cpp就是这一模式的标准范例。四、健康检查Health Checkingbthread 化的轻量探测4.1 核心模型对于那些无法连接却仍在命名服务中的节点brpc 会定期连接它们成功后对应 Socket 被复活并可能被 LoadBalancer 重新选上——这个过程就是健康检查。文档强调了两个关键事实集合关系被健康检查或在 LoadBalancer 中的节点一定在 NamingService 中状态二分只要一个节点不从命名服务删除它要么是正常的会被 LoadBalancer 选上要么在做健康检查。传统做法是使用一个线程做所有连接的健康检查集中式brpc 则简化了这个过程为需要的连接动态创建一个 bthread 专门做健康检查Socket::StartHealthCheck这个线程的生命周期被对应连接Socket管理。4.2 健康检查的三个阶段根据 docs/cn/load_balancing.md 与 src/brpc/details/health_check.cpp 的实现当 Socket 被SetFailed后如果SocketOptions.health_check_interval为正数健康检查线程就可能启动流程分三个阶段阶段一关闭旧连接。健康检查线程先确保没有其他人在使用该 Socket 后关闭连接。目前是通过对 Socket 的引用计数判断的。这个方案之所以有效在于 Socket 被SetFailed后就不能再被Address了所以引用计数只减不增。源码中对应WaitAndReset(2/*note*/)src/brpc/details/health_check.cpp注释里详细解释了为什么选择等待引用计数降到期望值这种简单方案而非原地复活 Socket后者会改变 SocketId大量代码需要监听并更新不现实。阶段二定期连接直至成功。健康检查线程定期连接远端机器直到连上为止在这个过程中如果 Socket 析构了该线程也就随之退出。源码中每次检查失败会ptr-_hc_count并设置*next_abstime butil::seconds_from_now(ptr-_health_check_interval_s)src/brpc/details/health_check.cpp实现按间隔调度。阶段三复活 Socket。连接成功后调用Socket::Revivesrc/brpc/details/health_check.cpp使 Socket 重新可被其他地方包括 LoadBalancer通过Socket::Address访问到。如果该节点还配置了应用层健康检查路径则继续通过HealthCheckManager::StartCheck启动周期性的 HTTP 健康检查见下文 4.3。4.3 网络层检查与应用层检查brpc 的健康检查分为两个层次详细参数见 docs/cn/client.md网络层检查默认一旦 server 被连接上它即恢复为可用状态。相关 gflagNameValueDescriptionDefined Athealth_check_interval R3两次连续健康检查之间的间隔秒src/brpc/socket_map.cpphealth_check_timeout_ms500健康检查超时毫秒—表中 R 表示该 flag 支持运行时动态修改。应用层检查可选框架会发送一个 HTTP GET 请求到该 server只有返回 200 时它才恢复。这种机制下-health_check_path默认为空为空即关闭应用层检查与-health_check_timeout_ms默认 500ms设置全局的请求路径与超时也可以通过ChannelOptions.hc_option对不同的 channel 设置不同的请求路径和超时且ChannelOptions 的优先级高于 gflag如果在隔离过程中 server 从命名服务中删除了brpc 也会停止连接尝试。源码层面应用层健康检查在 src/brpc/details/health_check.cpp 中通过HealthCheckManager实现StartCheck会创建一个以PROTOCOL_HTTP、max_retry0、超时取min(health_check_timeout_ms, interval*1000)的HealthCheckChannelAppCheck发起带health_check_path的 GET 请求src/brpc/details/health_check.cpp回调OnAppHealthCheckDone::Run中根据 HTTP 状态码决定是否Revive。网络层检查则走ptr-CheckHealth()TCP 连接探测。4.4 生命周期与引用计数从 src/brpc/socket.cpp 可以看到Socket::HoldHCRelatedRef/ReleaseHCRelatedReference两个方法当_health_check_interval_s 0时Socket 会持有一个与健康检查相关的引用计数确保健康检查线程运行期间 Socket 不会被提前回收健康检查结束时释放该引用。这印证了文档所述健康检查线程的生命周期被对应连接管理的实现细节。五、与 Client 端配置的衔接负载均衡和健康检查的配置最终都汇聚到Channel.Initdocs/cn/client.mdint Init(const char* naming_service_url, const char* load_balancer_name, const ChannelOptions* options);当load_balancer_name为 nullptr 或空时此 Init 等同于连接单台 server 的 Initnaming_service_url应为 ip:port 或 域名:port可通过这种方式统一 Channel 的初始化方式例如把naming_service_url和load_balancer_name放在配置文件中连接单台 server 时把load_balancer_name置空连接服务集群时设置有效的算法名称不要在每次请求前动态创建此类 Channel——创建时需访问一次命名服务成本较高且 Channel 可被所有线程共用一般没有动态创建的必要。命名服务的节点过滤可通过ChannelOptions.ns_filter设置自定义NamingServiceFilterdocs/cn/client.mdclass MyNamingServiceFilter : public brpc::NamingServiceFilter { public: bool Accept(const brpc::ServerNode server) const { return server.tag main; } }; brpc::ChannelOptions options; options.ns_filter my_filter; // 默认为 nullptr即不过滤ServerNode由butil::EndPoint addr与std::string tag组成src/brpc/naming_service.h 引用的 server_node.htag 可用于 wrr/wr 的权重声明、VIP 多连接区分等场景。六、总结brpc 负载均衡架构的三板斧brpc 的负载均衡体系可以用三句话概括命名服务控制权反转框架不轮询用户代码而是由命名服务实现者在获得列表后回调NamingServiceActions既支持 bns/file 这类轮询式命名服务也天然支持 zk 这类事件通知式命名服务一切通过protocol://service-name字符串即可描述扩展只需在 src/brpc/global.cpp 中注册负载均衡无锁化借助 DoublyBufferedData 双缓冲技术SelectServer这类高频读路径在不同线程间零互斥同时通过 Extension 机制注册了 rr/wrr/random/wr/la/p2c/一致性哈希族等十余种算法覆盖从简单轮询到带负载上限一致性哈希的全场景健康检查去集中化每个失败连接由专属 bthread 负责关闭旧连接 → 定期重连 → 成功后 Revive的完整生命周期配合引用计数管理线程存活辅以可选的 HTTP 应用层健康检查实现了高效、精准的故障隔离与恢复。进一步阅读负载均衡算法细节docs/cn/client.md含全部算法参数与 gflag 说明DoublyBufferedData 与 Locality-aware 原理docs/cn/lalb.md一致性哈希原理docs/cn/consistent_hashing.md组合 ChannelParallelChannel/SelectiveChanneldocs/cn/combo_channel.md源码入口src/brpc/naming_service.h、src/brpc/load_balancer.h、src/brpc/details/health_check.cpp、src/brpc/global.cpp【免费下载链接】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),仅供参考