负载均衡不只是轮询: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 在不增加决策延迟的前提下大幅降低最大负载方差。

相关推荐
小范的技术工坊1 小时前
Linux应用开发(文件属性)
linux·运维·服务器
古法老登瞎折腾2 小时前
大数据智能运维实战(一):机器和任务数据都有了,如何用 MCP 搭建运维助手?
大数据·运维·spring boot·spring ai·mcp
海宇数据2 小时前
零信任架构实战:基于海宇入职背调报告构建自动化即时配送与网约车众包运力准入鉴权网关
运维·人工智能·架构·自动化
姜鱼问生2 小时前
每次开机手动检查 6 样东西?我写了个 2 秒自检脚本
运维·网络·nginx·docker·容器
wangqiaowq2 小时前
PostgresSQL server Docker 安装部署
运维·docker·容器
南棋网络安全3 小时前
ssrf跨站请求伪造(保姆级教程)
运维·服务器·网络·网络安全
帷幕落秋3 小时前
进程服务与文件描述符
linux·运维·面试
帷幕落秋3 小时前
信号、Ctrl 组合键与进程状态
linux·运维·面试
开开心心就好4 小时前
文件被占用删不掉?解锁工具强制删除
java·运维·服务器·开发语言·前端·pdf·ocr
小超人诞生日记4 小时前
Linux 进程优先级
linux·运维·服务器