普通轮询无法应对后端实例性能差异,易导致弱节点过载;需基于cpu、内存等动态指标设定权重,并通过weights与current_weights双数组实现加权轮询,配合服务发现实时更新及线程安全机制保障负载均衡有效性。

为什么普通轮询在服务发现里不够用
后端实例性能不均时,平均分配请求会压垮弱节点。比如一个 api-node-01 是 4c8g,api-node-02 是 2c4g,硬轮询会让后者 QPS 翻倍——不是负载均衡,是负载“误伤”。
权重不是拍脑袋定的,得和实际资源挂钩:CPU 核数、内存、历史 RT 或主动上报的健康分。C++ 里没法靠注解或配置自动绑定这些,必须自己建模。
常见错误是把权重当静态配置写死:weight = {1, 2, 1},但节点上下线、扩容缩容后权重没重算,结果流量卡在已下线节点上。
用 std::vector + 当前权重数组实现加权轮询
核心是维护两个数组:weights 存初始权重(只读),current_weights 存动态值(每次选完要更新)。每次选择前,给每个节点的 current_weights[i] 加上 weights[i],然后挑最大的那个,再把它减去总权重和。
std::vector<int> weights = {3, 1, 2}; // 初始权重
std::vector<int> current_weights = {0, 0, 0};
int total_weight = std::accumulate(weights.begin(), weights.end(), 0);
<p>int select() {
for (size_t i = 0; i < weights.size(); ++i) {
current_weights[i] += weights[i];
}
int max_idx = 0;
for (size_t i = 1; i < current_weights.size(); ++i) {
if (current_weights[i] > current_weights[max_idx]) max_idx = i;
}
current_weights[max_idx] -= total_weight;
return max_idx;
}</p>注意点:
- total_weight 必须是所有有效节点的权重和,节点下线时得实时更新它,不能只删 weights 元素
- current_weights 溢出风险小,但若权重设到万级+高频调用(>10k QPS),建议用 int64_t
- 不支持运行时增删节点,插入/删除需重置整个 current_weights 数组
如何对接服务发现客户端(如 etcd / nacos C++ SDK)
服务发现返回的是实例列表,不是预设权重。得从元数据里提取权重字段,常见位置:
立即学习“C++免费学习笔记(深入)”;
- etcd 的 key-value 中 value 是 JSON,含
"weight"字段 - nacos 的 instance 对象有
weight成员变量(SDK v2.2+) - 自研注册中心常把权重塞在
metadatamap 里,键名可能是"w"、"lb_weight"或"qps_cap"
关键动作:
- 每次收到服务变更回调(add/remove/update),立刻重建 weights 和 current_weights
- 避免在回调里做耗时操作,尤其别同步调用 HTTP 或锁全局结构体
- 如果 SDK 不支持权重字段,就 fallback 到按 IP 哈希或直接用 1 均匀分配,别硬塞默认值
线程安全与更新时机怎么处理
select() 函数会被多线程并发调用,但 current_weights 是共享状态。别用 std::mutex 包整个函数——热点路径锁竞争太狠。
更合理的做法:
- 用 std::atomic_int 数组替代 current_weights,但要注意 fetch_add 和比较更新不是原子组合,得用 CAS 循环
- 更简单:每个线程缓存一份 current_weights 拷贝,定期(比如每 100 次 select 后)从主数组同步一次,牺牲一点精确性换性能
- 权重变更本身是低频事件,所以更新 weights 主数组时用读写锁(std::shared_mutex),读多写少场景很合适
真实环境里最容易被忽略的是:权重更新和实例列表更新不同步。比如 etcd 返回了新实例,但它的 weight 字段为空,你填了默认值 1,结果其他节点权重是 10,这个新节点瞬间被打爆——得加校验,空权重直接过滤或拒绝加入。










