负载均衡不只是轮询:Pingora 的 upstream 设计解析

1. 负载均衡的本质问题

负载均衡要解决的核心矛盾是:调度决策需要全局信息,但全局信息的维护本身有成本

  • 轮询:不需要任何全局信息,决策成本 O(1),但完全忽略后端实际负载差异
  • 最少连接:需要维护全局连接计数,决策成本 O(n),能反映负载但有并发写竞争
  • 延迟感知:需要实时延迟统计,信息更丰富,噪声也更大

没有一种算法是所有场景的最优解。Pingora 通过 PeerSelector trait 把算法策略解耦出来,让业务根据场景选择。

1.1 轮询的统计局限

简单轮询假设所有请求消耗相同资源、所有后端处理能力相同。在以下情况下这个假设会严重失效:

  • 请求本身不均匀:AI 推理场景里,100-token prompt 和 4000-token prompt 消耗的 GPU 时间可能差 40 倍
  • 后端硬件不均匀:混合新旧服务器时 CPU 性能差 2--3 倍很常见
  • 后端有热点:特定数据分布导致某些后端频繁被访问

1.2 Pingora 负载均衡设计目标

目标 实现
高可用 健康检查 + 故障转移
低延迟 就近路由 + 连接预热
可扩展 插件化 PeerSelector trait
可观测 实时指标 + 动态调整

2. Upstream 核心架构

LoadBalancer 由三个协作组件构成:

  • PeerSelector(选择器):实现具体的负载均衡算法(轮询、一致性哈希、P2C 等),通过 trait 解耦,可插拔替换
  • HealthChecker(健康检查器) :维护每个 Backend 的健康状态,主动检查和被动感知双轨并行
  • CircuitBreaker(熔断器):在错误率或延迟超阈值时主动停止向某节点发送请求,防止级联故障

每个 Backend 包含地址、权重、当前连接数和健康状态。健康状态用原子变量存储(AtomicBool),选择器在决策时无锁读取,性能开销极低。

ArcSwap<Vec<Backend>> 是后端列表的容器:服务发现更新列表时,通过原子指针交换整个 Vec,正在进行的请求持有旧 Arc 引用直到完成,不会看到中间状态,也不需要加锁等待。

3. 负载均衡算法:原理与选型

3.1 轮询与加权轮询

简单轮询AtomicUsize 递增取模实现,O(1) 决策,适合同构后端。

平滑加权轮询 是 Nginx 使用的算法:每个节点维护一个"当前权重",每轮选择当前权重最高的节点,选中后将其当前权重减去总权重之和。这确保了选择分布的平滑性------权重为 3:1 的两个节点,输出序列是 AAABAAAB... 而非 AAABAAB...,避免流量突刺。

3.2 一致性哈希:缓存场景的正确选择

普通哈希的问题

朴素哈希:node = hash(key) % N。当 N 变化(添加或移除节点)时,几乎所有 key 的映射都会改变。数学上:N 变为 N+1 时,原本映射到节点 i 的 key 只有 i/(N+1) 的概率仍然映射到 i,其余全部迁移。对于依赖节点本地缓存的场景,这意味着缓存被大规模清洗。

一致性哈希环

一致性哈希把所有可能的哈希值排列在逻辑"环"上(0 到 2³²),节点被映射到环上某些位置。一个 key 被映射到环上最近的顺时针节点。新增一个节点,只有该节点"覆盖区间"内的 key 需要迁移,理论迁移比例降至 1/(N+1)

虚拟节点的必要性

朴素一致性哈希有分布不均问题:N 个物理节点在环上可能极不均匀,某节点"负责"远多于平均值的区间。解法是虚拟节点:每个物理节点在环上放置 K 个虚拟点(通常 K=150--200),统计上各节点覆盖区间趋近均匀。

复制代码
物理节点 A 映射到 150 个虚拟位置散布在环上
物理节点 B 映射到 150 个虚拟位置散布在环上
...
总共 N×150 个位置,分布比 N 个位置均匀得多

权重支持:节点权重越高,分配的虚拟节点越多,分配到的请求比例越大。Pingora 的 pingora-ketama 基于 libketama 算法实现了这一机制。

一致性哈希适合:会话亲和性(同一用户路由到同一节点)、上游节点有本地缓存(命中率取决于路由稳定性)。

一致性哈希不适合:无状态 API 代理(不感知实时负载)、后端节点负载差异大的场景。

3.3 Power of Two Choices(P2C):简单却强大

轮询的最坏情况

N 个节点随机分配任务(等价于轮询),最终负载最重的节点期望接收量约为均值的 O(log N / log log N) 倍。N=1000 时,最忙节点比平均值多约 50%。

P2C 的数学优势

操作极其简单:随机选两个节点,选其中负载较低的那个 。就这一个改动,让最大负载期望降到均值的 O(log log N) 倍。N=1000 时,差距约为 log log 1000 ≈ 3,而不是 50%。

这个结论由 Mitzenmacher 等人在 1998 年理论证明,被称为"两次选择的力量"(The Power of Two Choices),是负载均衡领域的经典结果。

为什么有效:随机一次完全看运气;随机两次并选负载较低的,相当于在局部引入了"避开热点"的偏好。即便只是两个采样点,也能大幅降低最大负载的方差。

P2C 的实践变体

在 Pingora 的延迟感知选择器中,P2C 的两次采样不是完全随机的------可以先随机选 2 个,再用 EWMA(指数加权移动平均)延迟作为评分,选延迟更低的那个。EWMA 比简单平均更快响应最近的延迟变化,平滑因子通常设为 0.7(旧值): 0.3(新样本),在稳定性和响应速度之间取得平衡。

3.4 算法选型对比

算法 决策复杂度 最大负载期望 适用场景
轮询 O(1) O(log N / log log N) 同构后端,短请求
加权轮询 O(n) 取决于权重 异构后端
一致性哈希 O(log N) 取决于 key 分布 缓存场景,会话保持
最少连接 O(n) 近似最优 小集群,长连接
P2C + 延迟感知 O(1) 采样 O(log log N) 大集群,请求耗时差异大

4. 健康检查:主动探测 vs 被动感知

4.1 两种机制的本质差异

主动健康检查:定期向后端发送探测请求(HTTP GET /health、TCP connect),独立于真实流量。

  • 优点:能在节点失效后立刻(下一个检查周期)发现问题,不依赖真实请求失败触发
  • 缺点:对"慢节点"(响应变慢但不失败)不够敏感;有额外的探测流量开销

被动健康检查:观察真实请求的成功/失败,用滑动窗口统计错误率,超阈值则摘除节点。

  • 优点:直接反映真实服务质量,能感知慢节点;零额外开销
  • 缺点:需要积累一定数量的失败样本才能触发,期间真实流量受影响

两者组合是生产标准:主动检查保障基本可用性,被动检查感知性能退化。

4.2 阈值设计原则

成功/失败阈值的设置是稳定性和敏感度的权衡:

阈值设置 问题
连续 1 次失败就摘除 网络抖动导致频繁摘除/恢复,系统震荡
连续 10 次失败才摘除 节点故障期间大量请求失败
恢复阈值与摘除阈值相同 节点恢复时流量过快打回去,可能再次过载

通常的生产经验:失败摘除阈值 3--5 次,恢复阈值 2--3 次,且恢复时配合指数退避的探测间隔(先等 30s,再等 60s,逐步增加)。

5. 熔断器:防止级联故障

5.1 为什么需要熔断器

健康检查解决"节点彻底不可用"的问题。但还有一类更危险的场景:节点变慢但不报错

例如上游数据库慢查询导致响应从 10ms 变为 5000ms,HTTP 状态码仍然是 200。健康检查无法摘除,代理会把大量请求排在这个节点后面等待,连接数急剧上升,最终拖垮代理自身------这就是级联故障

熔断器的作用:当检测到某节点错误率或延迟超过阈值时,主动停止向它发送请求,不等节点自己恢复。

5.2 三状态机

熔断器是一个三状态机,状态间的转换条件是设计关键:

复制代码
        失败率超阈值
Closed ──────────────→ Open
  ↑                      │
  │  连续成功 ≥ 阈值       │ 等待超时后
  │                      ↓
  └─────── HalfOpen ←────┘
              │
              │ 再次失败
              ↓
            Open(重新熔断)
  • Closed(关闭):正常转发请求,统计错误率
  • Open(开启):拒绝所有请求,直接返回错误,给下游喘息时间
  • HalfOpen(半开):允许少量探测请求通过,若成功则恢复 Closed,若失败则重回 Open

HalfOpen 是防止"反复震荡"的关键:如果 Open 直接跳回 Closed,但节点其实还没恢复,会立刻再次触发熔断。HalfOpen 用少量探测流量验证恢复情况,避免大流量直接打过去。

5.3 熔断器 vs 健康检查的配合

机制 检测的问题 响应速度
主动健康检查 节点完全不可达 1 个检查周期(秒级)
被动健康检查 节点错误率上升 实时
熔断器 延迟/错误率超阈值 实时

三者应同时存在,解决不同维度的问题。

6. 动态服务发现

生产环境中,后端列表随扩缩容、发布、故障替换而变化。Pingora 通过 ServiceDiscovery trait 解耦具体实现:

方式 优点 缺点 适用场景
静态配置 简单,无依赖 无法自动更新 开发/调试
DNS 无额外组件 TTL 导致更新延迟,无法携带权重 简单部署
Consul/Etcd 健康状态过滤、标签路由、Watch 推送 需要维护注册中心 生产级

Watch API 的关键价值 :Consul 等注册中心支持阻塞查询(blocking query),变更发生时立刻推送,不需要轮询。节点列表变更时,通过 ArcSwap 原子替换,已建立的连接不受影响,新请求从下一次调度开始使用新列表。

7. 总结:算法选型决策树

复制代码
你的场景是?
│
├── 需要会话保持 / 上游有本地缓存
│   └── 一致性哈希(配置足够的虚拟节点数,通常 150+)
│
├── 大规模集群(> 20 节点),请求耗时差异大
│   └── P2C + EWMA 延迟感知
│
├── 小集群,长连接(数据库代理等)
│   └── 最少连接
│
└── 同构后端,短请求,追求简单
    └── 加权轮询

在任何算法上叠加:
  主动健康检查(基础可用性)
  + 被动健康检查(感知慢节点)
  + 熔断器(防止级联故障)
  + 服务发现(动态列表)

Pingora 的负载均衡设计亮点:通过 PeerSelector trait 把算法策略与健康检查、熔断器解耦,各层独立演化;ArcSwap 保障后端列表更新的无锁原子性;EWMA + P2C 在不增加决策延迟的前提下大幅降低最大负载方差。

相关推荐
skywalk81632 小时前
配置 trusted publishing 什么意思?pypi发布可以配置Trusted Publishing
运维·pypi
万粉变现经纪人2 小时前
如何解决 pip install bitsandbytes 报错 仅支持 Linux+glibc(macOS/Windows 失败)问题
linux·运维·windows·python·scrapy·macos·pip
·云扬·2 小时前
从0到1理解分库分表:我踩过的坑与实战经验
运维·数据库·mysql
Pocker_Spades_A2 小时前
自动化工作流引擎部署与实战:让可视化编排真正落地
运维·自动化
计算机安禾2 小时前
【Linux从入门到精通】第25篇:循环结构——重复造轮子的终结者
linux·运维·chrome
zzzyyy5382 小时前
基础IO(1)
linux·运维·数据库
neo33012 小时前
debian MEDIATEK Corp. Device 7925 无线网卡驱动安装
运维·服务器·debian
其实防守也摸鱼2 小时前
网络安全与数据库运维核心知识点总结(附习题)
运维·网络·数据库·笔记·安全·web安全
面向对象World3 小时前
养虾从入门到放弃(Windows&Ubuntu)
linux·运维·ubuntu