NGINX 负载均衡算法深度解析
这是一个关于技术创业者土豆和他的亲密伙伴 NGINX 的真实故事。通过土豆电商平台从零到千万用户的技术演进,我们将深入探索负载均衡算法的奥秘。
第一章:创业伊始 - 单服务器的美好与局限
1.1 技术选型的初心
2018 年,土豆辞去大厂工作,开始了”土豆优选”的创业之旅。技术选型时,他选择了 NGINX + Spring Boot + MySQL 的经典架构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| server { listen 80; server_name potoiorshop.com;
location /static/ { root /var/www/html; expires 30d; }
location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
|
系统架构详解:
1 2 3
| 用户请求 → 云服务商负载均衡 → 单台NGINX服务器 → 单台应用服务器 ↓ 单台MySQL数据库
|
1.2 单服务器架构的优势
- 部署简单:所有组件都在一台服务器,运维成本低
- 调试方便:日志集中,问题排查快速
- 成本可控:云服务器月费仅 800 元
1.3 暗藏的风险
土豆在技术笔记中写道:
1 2 3 4 5
| 系统瓶颈分析: 1. 应用服务器:最大支持500并发用户 2. 数据库:最大1000连接,磁盘IO 100MB/s 3. 网络带宽:最大100Mbps 4. 单点故障:任何组件宕机都会导致服务不可用
|
这种架构平稳运行了 9 个月,直到那个改变命运的促销日…
第二章:黑色星期五的灾难与启示
2.1 灾难降临
2019 年双十一,土豆投入 5 万元营销费用,期待带来首批大规模用户。
08:00:用户开始涌入,系统响应时间从 200ms 升至 800ms
09:30:并发用户突破 800,CPU 使用率达到 95%
10:15:数据库连接池耗尽,开始出现 504 错误
10:45:服务器内存溢出,Java 进程被系统杀死
11:00:系统完全不可用,营销费用打了水漂
2.2 根本原因分析
事后复盘,土豆画出了系统瓶颈图:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 用户请求(2000并发) ↓ NGINX(处理正常) ↓ 应用服务器(瓶颈!) ├── 线程池:200线程已满 ├── 堆内存:8GB已耗尽 ├── CPU:4核心100%占用 ↓ 数据库(瓶颈!) ├── 连接数:100/100 ├── 磁盘IO:98MB/s └── CPU:90%占用
|
2.3 技术觉醒
土豆在技术博客中反思:
“单服务器架构就像独木桥,平时可以承载行人通过,但遇到节日人流,桥就会坍塌。我需要建立多车道的现代化大桥。”
第三章:第一次扩展 - 轮询算法的实践
3.1 架构重构
土豆购买了 3 台相同配置的服务器,并重新设计架构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| upstream backend_servers { server 192.168.1.101:8080; server 192.168.1.102:8080; server 192.168.1.103:8080; }
server { listen 80; server_name potoiorshop.com;
location / { proxy_pass http://backend_servers; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 3s; proxy_read_timeout 10s; } }
|
3.2 轮询算法的工作原理
土豆深入研究了轮询算法的实现机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class RoundRobinBalancer: def __init__(self, servers): self.servers = servers self.current_index = 0 self.total_servers = len(servers)
def get_next_server(self): """获取下一个服务器""" server = self.servers[self.current_index] self.current_index = (self.current_index + 1) % self.total_servers return server
servers = ['Server_A', 'Server_B', 'Server_C'] balancer = RoundRobinBalancer(servers)
for i in range(10): print(f"请求{i+1} → {balancer.get_next_server()}")
|
3.3 实际效果与监控数据
部署后,土豆的监控系统显示:
请求分布:
- 服务器 A:33.3%的请求
- 服务器 B:33.3%的请求
- 服务器 C:33.3%的请求
性能提升:
- 最大并发用户:从 500 提升到 1500
- 平均响应时间:从 800ms 降至 300ms
- 系统可用性:从 99%提升到 99.9%
3.4 暴露的新问题
但土豆很快发现了问题:
- 会话丢失:用户登录状态在不同服务器间丢失
- 资源浪费:上传的文件在不同服务器间不共享
- 缓存穿透:每台服务器的本地缓存都需要单独预热
第四章:性能差异的困扰 - 加权轮询的引入
4.1 硬件升级的契机
2020 年,业务快速增长,土豆购入了性能更强的服务器:
| 服务器 |
配置 |
预估性能指数 |
价格 |
| 服务器 D |
32 核/64GB/SSD |
4.0 |
¥ 8,000/月 |
| 服务器 E |
16 核/32GB/SSD |
2.0 |
¥ 4,000/月 |
| 服务器 F |
8 核/16GB/HDD |
1.0 |
¥ 2,000/月 |
4.2 加权轮询配置
1 2 3 4 5
| upstream backend_servers { server 192.168.1.104:8080 weight=4; server 192.168.1.105:8080 weight=2; server 192.168.1.106:8080 weight=1; }
|
4.3 传统加权轮询的问题
土豆预期的请求分布:
1 2 3
| 周期1: D, D, D, D, E, E, F 周期2: D, D, D, D, E, E, F 周期3: D, D, D, D, E, E, F
|
但实际监控显示:
1 2 3 4
| 时间 08:00:00 - 08:00:07: D, D, D, D (服务器D压力飙升) 时间 08:00:08 - 08:00:09: E, E (服务器E压力适中) 时间 08:00:10: F (服务器F压力很小) 时间 08:00:11 - 08:00:14: D, D, D, D (服务器D再次压力飙升)
|
4.4 突发流量分析
使用 APM 工具,土豆发现了关键问题:
服务器 D 的监控数据:
1 2 3 4 5
| 08:00:00 - 08:00:03: CPU使用率 85% → 95% 08:00:04 - 08:00:07: CPU使用率 95% → 100% (触发限流) 08:00:08 - 08:00:09: CPU使用率 100% → 80% (其他服务器处理请求) 08:00:10: CPU使用率 80% → 75% 08:00:11 - 08:00:14: CPU使用率 75% → 100% (再次触发限流)
|
用户在这期间访问网站,体验就像坐过山车。
第五章:平滑的智慧 - NGINX 默认算法的深度解析
5.1 发现平滑加权轮询
在研究 NGINX 文档时,土豆发现了这个被忽略的细节:
“NGINX 默认使用加权轮询算法,但它是平滑的加权轮询,能够避免传统加权轮询的突发流量问题。”
5.2 算法原理深入理解
土豆在白板上推导了整个算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class SmoothWeightedRoundRobin: def __init__(self, servers): self.servers = servers self.current_weights = [0] * len(servers) self.total_weight = sum(server['weight'] for server in servers)
def get_next_server(self): for i in range(len(self.servers)): self.current_weights[i] += self.servers[i]['weight']
selected_idx = self.current_weights.index(max(self.current_weights)) selected_server = self.servers[selected_idx]
self.current_weights[selected_idx] -= self.total_weight
return selected_server
servers = [ {'name': 'Server_D', 'weight': 4}, {'name': 'Server_E', 'weight': 2}, {'name': 'Server_F', 'weight': 1} ]
balancer = SmoothWeightedRoundRobin(servers) sequence = [balancer.get_next_server()['name'] for _ in range(21)] print("请求序列:", ' → '.join(sequence))
|
输出结果:
1
| 请求序列: D → E → D → F → D → E → D → D → E → D → F → D → E → D → D → E → D → F → D → E → D
|
5.3 数学证明:为什么这样设计?
土豆在技术博客中详细证明了算法:
定理 1:权重比例正确性
设服务器 i 的权重为$$w_i$$,总权重$$W = Σw_i$$
在一个完整周期(W 次请求)内,服务器 i 被选中的次数为$$w_i$$
证明:
每次选中服务器 i 时,其 current_weight 减少 W
每个请求,所有服务器的 current_weight 总和增加 W
经过 W 次请求,系统回到初始状态
∴ 服务器 i 被选中次数 = $$(W × w_i) / W = w_i$$
定理 2:最大连续选中次数限制
权重为$$w_i$$的服务器,最大连续被选中次数不超过 $$ceil(w_i / (W - w_i)) + 1$$
5.4 实际部署效果
启用平滑加权轮询后:
请求分布变化:
1 2
| 之前: DDDD E E F (突发模式) 现在: D E D F D E D (平滑模式)
|
性能指标改善:
- 服务器 D 的 CPU 峰值:100% → 85%
- 平均响应时间:350ms → 220ms
- 错误率:2.1% → 0.3%
第六章:长连接的挑战 - 最少连接算法的实战
6.1 直播功能的技术挑战
2021 年,土豆上线了直播带货功能,立即遇到了新问题:
1 2 3 4 5
| 报警时间: 2021-06-15 20:30:00 服务器D: 450个活跃连接 (负载85%) 服务器E: 120个活跃连接 (负载35%) 服务器F: 80个活跃连接 (负载25%)
|
6.2 最少连接算法配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| upstream live_backend { least_conn;
server 192.168.1.104:8080 weight=4; server 192.168.1.105:8080 weight=2; server 192.168.1.106:8080 weight=1;
keepalive 100; keepalive_timeout 30s; keepalive_requests 1000; }
server { location /live/ { proxy_pass http://live_backend;
proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
proxy_connect_timeout 7d; proxy_send_timeout 7d; proxy_read_timeout 7d; } }
|
6.3 算法实现原理
土豆研究了最少连接算法的内部机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class LeastConnectionsBalancer: def __init__(self, servers): self.servers = servers self.connections = {server['name']: 0 for server in servers}
def on_connect(self, server_name): """连接建立时调用""" self.connections[server_name] += 1
def on_disconnect(self, server_name): """连接断开时调用""" self.connections[server_name] -= 1
def get_best_server(self): """获取最佳服务器 - 考虑权重的最小连接""" best_server = None best_score = float('inf')
for server in self.servers: score = self.connections[server['name']] / server['weight']
if score < best_score: best_score = score best_server = server
return best_server
|
6.4 实际效果验证
部署最少连接算法后:
连接分布变化:
1 2 3
| 时间 20:30: 服务器D: 450连接 → 20:45: 280连接 时间 20:30: 服务器E: 120连接 → 20:45: 210连接 时间 20:30: 服务器F: 80连接 → 20:45: 160连接
|
性能改善:
- 直播卡顿率:15% → 3%
- 平均连接建立时间:800ms → 200ms
- 服务器负载均衡度:0.6 → 0.9(越接近 1 越均衡)
第七章:购物车的困境 - IP 哈希算法的完美解决方案
7.1 会话保持的业务需求
2021 年双十一前夕,用户投诉激增:
- “添加到购物车的商品不见了”
- “登录状态总是丢失”
- “结算时提示重新登录”
7.2 问题根因分析
土豆通过日志分析发现了问题模式:
1 2 3 4 5
| 用户 113.204.xx.xx 的请求序列: 10:15:02 请求1 → 服务器D (登录成功,Session存储在D) 10:15:05 请求2 → 服务器E (没有Session,要求重新登录) 10:15:08 请求3 → 服务器F (没有Session,要求重新登录) 10:15:12 请求4 → 服务器D (找到Session,登录成功)
|
7.3 IP 哈希算法配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| upstream cart_backend { ip_hash;
server 192.168.1.104:8080; server 192.168.1.105:8080; server 192.168.1.106:8080;
sticky cookie srv_id expires=1h domain=.potoiorshop.com path=/; }
server { location /cart/ { proxy_pass http://cart_backend;
proxy_connect_timeout 5s; proxy_read_timeout 30s;
proxy_set_header Cookie $http_cookie; proxy_set_header X-Session-Id $cookie_srv_id; } }
|
7.4 哈希算法原理
土豆深入研究了 IP 哈希的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import hashlib
class IPHashBalancer: def __init__(self, servers): self.servers = servers
def get_server_by_ip(self, client_ip): """根据客户端IP选择服务器""" ip_hash = hashlib.md5(client_ip.encode()).hexdigest() ip_hash_int = int(ip_hash, 16)
server_index = ip_hash_int % len(self.servers) return self.servers[server_index]
test_ips = ['113.204.32.101', '220.180.45.203', '183.162.77.88'] balancer = IPHashBalancer(['Server_D', 'Server_E', 'Server_F'])
for ip in test_ips: server = balancer.get_server_by_ip(ip) print(f"IP {ip} → {server}")
|
7.5 会话保持的替代方案
土豆也评估了其他方案:
方案 1:集中式 Session 存储
1 2 3 4 5 6
| upstream backend { server 192.168.1.104:8080; server 192.168.1.105:8080; server 192.168.1.106:8080; }
|
方案 2:基于 Cookie 的会话保持
1 2 3 4 5 6
| upstream backend { sticky cookie srv_id expires=1h; server 192.168.1.104:8080; server 192.168.1.105:8080; server 192.168.1.106:8080; }
|
最终选择 IP 哈希的原因:
- 实现简单,无需额外基础设施
- 性能损耗小
- 适合中小规模应用
第八章:生产环境的高级配置实战
8.1 完整的生产配置
经过 3 年演进,土豆的 NGINX 配置已经相当完善:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| user nginx; worker_processes auto; worker_rlimit_nofile 100000;
events { worker_connections 4096; use epoll; multi_accept on; }
http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048;
upstream product_backend { server 192.168.1.107:8080 weight=3 max_fails=2 fail_timeout=30s; server 192.168.1.108:8080 weight=2 max_fails=2 fail_timeout=30s; server 192.168.1.109:8080 weight=1 max_fails=2 fail_timeout=30s;
server 192.168.1.110:8080 backup; }
upstream user_backend { ip_hash; server 192.168.1.111:8080 max_fails=3 fail_timeout=30s; server 192.168.1.112:8080 max_fails=3 fail_timeout=30s;
hash $remote_addr consistent; }
upstream live_backend { least_conn; server 192.168.1.113:8080 weight=2; server 192.168.1.114:8080 weight=2; server 192.168.1.115:8080 weight=1;
keepalive 32; }
upstream cart_backend { ip_hash; server 192.168.1.116:8080; server 192.168.1.117:8080; }
server { listen 80; server_name potoiorshop.com;
location /nginx_status { stub_status on; access_log off; allow 192.168.1.0/24; deny all; }
location /api/products/ { proxy_pass http://product_backend; proxy_next_upstream error timeout http_500 http_502 http_503; proxy_connect_timeout 2s; proxy_read_timeout 5s; }
location /api/users/ { proxy_pass http://user_backend; proxy_next_upstream off; proxy_connect_timeout 3s; proxy_read_timeout 10s; }
location /api/live/ { proxy_pass http://live_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; }
location /api/cart/ { proxy_pass http://cart_backend; proxy_next_upstream off; proxy_connect_timeout 3s; proxy_read_timeout 10s; }
location / { proxy_pass http://product_backend; proxy_connect_timeout 3s; proxy_read_timeout 10s; } } }
|
8.2 监控与告警配置
土豆建立了完整的监控体系:
1 2 3 4 5 6 7 8 9 10 11 12
| nginx_http_requests_total{server="product",status="200"} 124532 nginx_http_requests_total{server="product",status="500"} 23 nginx_upstream_requests_total{backend="product",server="107"} 45632 nginx_upstream_response_time{backend="product",server="107"} 0.234
- alert: NginxHighErrorRate expr: rate(nginx_http_requests_total{status=~"5.."}[5m]) > 0.05
- alert: NginxUpstreamUnavailable expr: sum(nginx_upstream_servers{state="up"}) < 2
|
8.3 性能调优经验
土豆总结的性能优化清单:
NGINX 层面
- worker_processes = CPU 核心数
- worker_connections = 1024 × CPU 核心数
- 开启 sendfile, tcp_nopush 优化
负载均衡层面
- 合理设置 max_fails 和 fail_timeout
- 根据业务特点选择算法
- 配置合适的备份策略
应用层面
第九章:经验总结与算法选择深度指南
9.1 算法选择决策框架
土豆创建了详细的决策流程图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 开始算法选择 ↓ 问:是否需要会话保持? ├── 是 → 选择IP哈希算法 │ ├── 优点:简单可靠,会话一致 │ └── 缺点:服务器增减影响大,负载可能不均衡 │ └── 否 → 继续判断 ↓ 问:请求处理时间差异大或有长连接? ├── 是 → 选择最少连接算法 │ ├── 优点:动态负载均衡,适合长连接 │ └── 缺点:需要维护连接状态,复杂度高 │ └── 否 → 选择平滑加权轮询算法(默认) ├── 优点:平滑分布,性能优异,配置简单 └── 缺点:不考虑实时负载状态
|
9.2 各算法适用场景深度分析
平滑加权轮询(默认)
最佳场景:
- 大多数 Web API 服务
- 短连接为主的 HTTP 服务
- 服务器性能差异明显的环境
配置要点:
1 2 3 4 5 6
| upstream backend { server backend1 weight=5; server backend2 weight=3; server backend3 weight=1; }
|
最少连接
最佳场景:
- 长连接服务(WebSocket、直播)
- 请求处理时间差异大的服务
- 实时负载敏感的应用
配置要点:
1 2 3 4 5 6
| upstream backend { least_conn; server backend1 weight=2; server backend2 weight=2; }
|
IP 哈希
最佳场景:
- 有状态应用(购物车、用户会话)
- 需要本地缓存的场景
- 文件上传处理
配置要点:
1 2 3 4 5 6
| upstream backend { ip_hash; server backend1; server backend2; }
|
9.3 混合使用策略
土豆发现,在实际生产中,往往需要混合使用不同算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| upstream stateless_api { server 192.168.1.101:8080 weight=3; server 192.168.1.102:8080 weight=2; }
upstream stateful_api { ip_hash; server 192.168.1.103:8080; server 192.168.1.104:8080; }
upstream websocket_api { least_conn; server 192.168.1.105:8080; server 192.168.1.106:8080; }
|
9.4 性能测试数据
土豆通过压测获得了各算法的性能数据:
| 算法 |
平均响应时间 |
吞吐量 |
负载均衡度 |
会话一致性 |
| 轮询 |
156ms |
1250 req/s |
0.95 |
0% |
| 加权轮询 |
142ms |
1380 req/s |
0.92 |
0% |
| 平滑加权轮询 |
128ms |
1520 req/s |
0.98 |
0% |
| 最少连接 |
121ms |
1450 req/s |
0.99 |
0% |
| IP 哈希 |
135ms |
1480 req/s |
0.85 |
100% |
第十章:新的开始 - 面向未来的架构
10.1 当前架构全景
2023 年,”土豆优选”已成为日活百万的电商平台,技术架构也演进为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 全球用户请求 ↓ CDN (阿里云/腾讯云) ↓ DNS负载均衡 ↓ NGINX入口集群 (4台) ↓ 业务网关 (Spring Cloud Gateway) ↓ 各业务上游组: ├── 商品服务 (8台) - 平滑加权轮询 ├── 用户服务 (6台) - IP哈希 + Redis会话 ├── 订单服务 (6台) - 平滑加权轮询 ├── 支付服务 (4台) - IP哈希 ├── 直播服务 (4台) - 最少连接 └── 推荐服务 (3台) - 平滑加权轮询
|
10.2 下一代架构规划
土豆正在规划基于 Service Mesh 的下一代架构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: product-service spec: host: product-service trafficPolicy: loadBalancer: simple: LEAST_CONN subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2
|
10.3 技术心得总结
回顾 5 年技术演进,土豆在团队内部分享:
“负载均衡不仅是技术选型,更是业务理解的体现。每个算法背后都有其哲学:
- 轮询告诉我们:绝对的公平有时不是最优解
- 加权轮询提醒我们:要承认并尊重差异
- 平滑加权轮询展示:在约束中寻找优雅的平衡
- 最少连接启示:关注当下状态比预设规则更重要
- IP 哈希证明:一致性在某些场景下至关重要
技术之路没有终点,只有不断适应变化的智慧。”
10.4 给其他开发者的建议
土豆总结了给初创团队的建议:
- 起步阶段:单服务器 + 简单轮询
- 成长阶段:多服务器 + 平滑加权轮询
- 扩展阶段:按业务拆分 + 混合算法策略
- 成熟阶段:服务网格 + 智能负载均衡
最重要的是:不要过度设计,让架构随着业务一起成长。
本故事基于真实的技术演进案例,人物为化名。技术之路充满挑战,但也充满成长的喜悦。愿每个技术人都能在自己的道路上坚定前行。