3. 负载均衡策略:DNS负载均衡、Nginx/HAProxy反向代理、客户端负载均衡(Ribbon)
负载均衡这东西,说白了就是把流量分摊到多台机器上。但怎么分、在哪分、分完怎么保证不出事,这里头的门道可不少。我这些年踩过的坑,有一半都跟负载策略没选对有关系。
今天咱们就把三种主流方案掰开揉碎了讲。你想想看,一个在线特征服务,每天要扛几十亿次请求,如果负载均衡没做好,那真是「一挂挂一片」。嗯,咱们从最上层的 DNS 开始聊。
3.1 DNS负载均衡:最上层的第一道关卡
DNS 负载均衡,其实就是域名解析的时候给你返回多个 IP。客户端拿到哪个 IP,就去访问哪个机房。这是最粗粒度的负载策略,也是成本最低的。
核心思路:同一个域名配置多条 A 记录,DNS 服务器轮询返回。用户请求自然就被分散到不同入口。
我记得有一次做跨国业务,国内用户访问延迟高得离谱。查了半天,发现 DNS 解析总是把国内用户指向了美国机房。后来我们按地域做了智能 DNS 解析,问题才解决。
3.1.1 常见的DNS策略
| 策略 | 原理 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| 轮询 | 按顺序返回不同IP | 各机房能力对等 | 某机房挂了,DNS还在返回它的IP |
| 地域解析 | 根据来源IP返回最近机房 | 跨国/跨省部署 | IP库不准,用户被导到错误机房 |
| 权重解析 | 按比例返回IP | 机房容量不同 | 权重配错,小机房直接被冲垮 |
| 主备解析 | 主IP正常时只返回主IP | 主备容灾 | 切换不及时,业务中断好几分钟 |
注意:DNS 有缓存。TTL 设得太短,DNS 服务器压力大;设得太长,故障切换慢。我个人习惯把 TTL 设在 60~120 秒,既保证切换速度,又不至于把 DNS 服务器打爆。
3.1.2 健康检查与自动摘除
DNS 本身不做健康检查。它只管解析,不管后端死活。所以你得配合其他手段:
- 自研 DNS 管理平台:定期探测各机房入口,发现异常自动摘除 A 记录
- 结合 GSLB:全局负载均衡设备,能感知后端状态
- 短 TTL + 重试:客户端发现连不上,立即重试另一个 IP
我曾经遇到过一个案例:某机房网络抖动,DNS 没摘除,结果 30% 的请求超时。从那以后,我们给 DNS 管理平台加了「连续 3 次探测失败就自动摘除」的逻辑。
3.2 Nginx/HAProxy反向代理:七层与四层的选择
到了反向代理这一层,咱们就有更多控制权了。Nginx 和 HAProxy 是业界最常用的两个方案。它们俩各有侧重,我一般这么选:
- Nginx:七层(HTTP/HTTPS)处理能力强,适合做协议解析、路由转发
- HAProxy:四层(TCP)性能极致,适合做纯流量转发
我的经验:在线特征服务大部分走 HTTP/gRPC,所以我倾向用 Nginx 做七层负载。但如果对延迟极其敏感(比如毫秒级),我会在前面再加一层 HAProxy 做四层接入。
3.2.1 Nginx的负载均衡配置
Nginx 的 upstream 模块提供了多种负载算法。我挑几个最常用的说说:
upstream feature_service {
# 默认是轮询
server 10.0.1.1:8080 weight=5;
server 10.0.1.2:8080 weight=3;
server 10.0.1.3:8080 backup; # 备用节点
# 最少连接数
least_conn;
# IP哈希,保持会话
ip_hash;
# 一致性哈希(需要第三方模块)
hash $request_uri consistent;
}
server {
listen 80;
location /api/feature {
proxy_pass http://feature_service;
proxy_next_upstream error timeout http_500;
proxy_connect_timeout 3s;
proxy_read_timeout 5s;
}
}
这里有个细节:proxy_next_upstream 一定要配。默认 Nginx 只对 error 和 timeout 重试,但 http_500 也得加上。我遇到过后端返回 500,Nginx 直接把这个响应返回给客户端,导致大量报错。加上 http_500 后,Nginx 会自动换一台重试。
3.2.2 HAProxy的四层转发
HAProxy 的配置更简洁,性能也更好。它支持 active-backup 和 roundrobin 两种主流模式:
frontend fe_feature
bind *:80
default_backend be_feature
backend be_feature
balance roundrobin
server s1 10.0.1.1:8080 check inter 3s fall 2 rise 2
server s2 10.0.1.2:8080 check inter 3s fall 2 rise 2
server s3 10.0.1.3:8080 check inter 3s fall 2 rise 2 backup
注意那个 check inter 3s fall 2 rise 2。这是健康检查的配置:每 3 秒探测一次,连续失败 2 次就摘除,连续成功 2 次才恢复。这个参数我调过很多次,太频繁会加重后端压力,太稀疏又发现不了故障。
关键参数对比:
| 参数 | Nginx | HAProxy |
|---|---|---|
| 最大连接数 | 约 5 万(单进程) | 约 50 万+ |
| 健康检查 | 被动(依赖 proxy_next_upstream) | 主动(check 指令) |
| SSL 卸载 | 原生支持,性能好 | 支持,但不如 Nginx 灵活 |
| 配置热加载 | reload 不丢连接 | reload 会断开旧连接 |
3.3 客户端负载均衡:Ribbon 与更轻量的选择
前面两种都是「服务端负载均衡」,请求先到中间代理,再转发到后端。但客户端负载均衡不一样——客户端自己维护一份服务列表,直接选一台发起调用。
Spring Cloud 时代的 Ribbon 是典型代表。虽然现在官方进入维护期,但它的设计思想仍然值得学习。
3.3.1 Ribbon的工作原理
- 服务发现:从注册中心(如 Eureka)拉取服务列表
- 本地缓存:客户端维护一份服务列表快照
- 负载算法:按规则选一台(轮询、权重、最少并发等)
- 故障处理:调用失败后重试其他节点
// Ribbon 客户端配置示例
@Configuration
public class RibbonConfig {
@Bean
public IRule ribbonRule() {
// 使用加权响应时间策略
return new WeightedResponseTimeRule();
}
@Bean
public IPing ribbonPing() {
// 每 10 秒 ping 一次后端
return new PingUrl(false, "/health");
}
}
避坑指南:我曾经把 Ribbon 的 MaxAutoRetries 设成 3,结果一次超时触发了 3 次重试,3 次重试又各自超时,最终请求耗时超过 15 秒。后来我改成 MaxAutoRetries=1,配合 ReadTimeout=2s,效果好了很多。
3.3.2 客户端负载的优缺点
| 维度 | 优势 | 劣势 |
|---|---|---|
| 延迟 | 少一跳网络,延迟更低 | 客户端需要维护服务列表 |
| 扩展性 | 无中心瓶颈,随客户端数线性扩展 | 每个客户端都要实现负载逻辑 |
| 故障隔离 | 客户端可独立熔断 | 故障感知依赖客户端心跳 |
| 运维 | 不需要维护代理集群 | 升级负载策略需要更新所有客户端 |
现在很多团队转向了 Spring Cloud LoadBalancer 或 gRPC 内置负载均衡。我个人更推荐 gRPC 的 pick_first 和 round_robin 策略,它们原生支持健康检查和重连,省去了很多维护成本。
3.4 三种策略的协同与选型
实际生产环境里,这三种策略不是互斥的,而是层层递进的关系。我画了一张图,帮你理清它们的分工:
你看,这三层各司其职。DNS 管「去哪栋楼」,反向代理管「去哪个楼层」,客户端管「敲哪扇门」。少了任何一层,高可用都会打折扣。
3.4.1 我的选型建议
- 中小规模(日活百万级):DNS + Nginx 就够了。客户端用简单的轮询或随机。
- 大规模(日活千万级):DNS + HAProxy(四层入口)+ Nginx(七层路由)+ 客户端负载均衡。
- 超大规模(日活亿级):自研 DNS 管理平台 + LVS(四层)+ Nginx(七层)+ 客户端自适配。
最后提醒一句:负载均衡不是配完就完事了。你得持续监控各层的流量分布、延迟、错误率。我见过太多团队配了 Nginx 就不管了,结果某台机器 CPU 飙到 90% 都没发现。定期做压测,验证负载策略是否按预期工作——这是 SRE 的基本功。