很多人对 Nginx 的认知停留在"反向代理服务器"这五个字上,但这么说其实不全面。
Nginx 本质上是一个高性能的 HTTP 和反向代理服务器,由俄罗斯人 Igor Sysoev 在 2002 年搞出来的,最初就是为解决 C10K 问题而生的——啥意思?就是单机同时处理一万个并发连接。
它能干啥?
- Web 服务器(静态资源托管)
- 反向代理(隐藏后端,转发请求)
- 负载均衡(流量分发)
- 邮件代理
- API 网关(现在很多公司这么用)
但咱们今天重点聊负载均衡这块。
要理解 Nginx 的负载均衡原理,得先弄明白一个核心概念:反向代理。
所谓反向代理,就是客户端发请求的时候,根本不知道后面有多少台服务器在干活。客户端只知道一个地址,就是 Nginx。Nginx 收到请求之后,根据内部规则,把请求转发给后端的某台服务器。
正向代理和反向代理的区别,我举个栗子。
你去饭店吃饭,正向代理就是你跟服务员说"我要吃隔壁那家的红烧肉",服务员帮你去隔壁买。反向代理就是你去一个火锅店,菜单上写啥你点啥,后厨有张三、李四、王五三个厨师在炒菜,但你根本不知道是谁给你炒的。
Nginx 就是那个火锅店的"前厅服务员"。
底层核心:事件驱动 + 异步非阻塞
讲负载均衡之前,必须先说清楚 Nginx 的底层架构,否则后面那些算法你只知道怎么配,不知道为啥这么高效。
Nginx 用了master-worker 进程模型。
一个 master 进程管全局,负责读取配置、管理 worker 进程、接收信号。多个 worker 进程负责实际处理请求。
这玩意儿厉害在哪?传统的 Apache 一个请求一个线程,扛不住高并发。Nginx 一个 worker 能同时处理成千上万个请求,靠的就是事件驱动 + 异步非阻塞。
啥意思?
传统模型就像银行的柜台,每个客户来了开一个窗口,客户走了关窗口。人一多,窗口不够用,就排队。Nginx 的模型就像快递驿站,只有一个窗口,但窗口后面的人不停地在多个包裹之间切换——这个包裹扫一下条码,那个包裹签个字,多个任务并发处理,谁先准备好就先处理谁。
具体到代码层面,Nginx 在 Linux 上用的是 epoll,在 BSD 上用 kqueue,这些都是操作系统提供的高效 I/O 多路复用机制。简单说就是:让一个线程同时监控多个连接,哪个连接有数据来了就处理哪个,没数据的就跳过。
这就是为什么 Nginx 能扛高并发的根本原因。
理解了这一点,再来看负载均衡就顺畅了。Nginx 处理请求的时候,根据你配置的 upstream 列表,按照某种算法挑一台后端服务器,然后转发过去。这个过程对客户端完全透明。
四种基础负载均衡算法:没那么简单
回到正题,负载均衡的核心问题是:Nginx 收到请求了,该转给哪台后端?
答案就是负载均衡算法。Nginx 默认提供了几种,每种都有自己的适用场景。
轮询(Round Robin)
这是最简单也最常用的一个。
顾名思义,就是按顺序来。第一个请求给第一台服务器,第二个给第二台……最后一台完了再回到第一台,周而复始。
配置起来特别简单:
upstream backend { server 192.168.1.10; server 192.168.1.11; server 192.168.1.12; } server { listen 80; location / { proxy_pass http://backend; } }就这么三台机器轮流来,平均分配。
但轮询有个致命问题:它不知道每台服务器的实际负载。如果三台机器配置不一样,性能差距很大,轮询就会把性能差的那台累死,性能好的那台闲死。
而且,轮询不考虑请求的复杂度。有的请求处理快,有的慢,轮询还是按顺序来,可能导致快的机器在等慢的机器。
所以纯轮询在生产环境用得不多,除非后端服务器配置完全一样,且请求类型也比较均匀。
权重轮询(Weighted Round Robin)
轮询的升级版。
既然服务器配置不一样,那就给每台机器分配不同的权重。性能好的多分点,性能差的少分点。
upstream backend { server 192.168.1.10 weight=5; server 192.168.1.11 weight=3; server 192.168.1.12 weight=2; }这里的 weight 就是权重。Nginx 会按照 5:3:2 的比例来分配请求。也就是说,十个请求里,第一台分到 5 个,第二台 3 个,第三台 2 个。
但这里有个细节很多人不知道:Nginx 的权重轮询不是简单的"先来五个再去下一台",而是用了平滑加权轮询算法。
啥意思?
如果用简单加权轮询,分配顺序可能是:10、10、10、10、10、11、11、11、12、12(10 出现 5 次连着,11 出现 3 次连着……)。这样会导致某台机器在短时间内集中接收大量请求,造成压力尖峰。
平滑加权轮询的分配会更均匀,像 10、11、10、12、10、11、10、12、10、11 这样,避免连续分配。
Nginx 1.x 之后的版本都默认是平滑的,老版本可能不是。
最少连接(Least Connections)
这个就智能多了。
轮询的思路是"我不管你们谁忙谁闲,我按顺序来"。最少连接不一样,它会实时统计每台后端服务器当前的活跃连接数,把新请求分给连接数最少的那台。
upstream backend { least_conn; server 192.168.1.10; server 192.168.1.11; server 192.168.1.12; }加个least_conn指令就行。
这个算法的好处是:能根据后端的实时负载动态调整分配策略。如果某台机器的请求处理得慢,连接堆积,Nginx 就自动把新请求分给其他机器。
但它也有缺点:只看连接数,不看连接的实际负载。如果某个连接是长连接、慢请求,占着茅坑不拉屎,但连接数还是 1,这时候算法就不会把这台机器的请求分走。所以最少连接适合请求处理时间相对均匀的场景。
IP 哈希(IP Hash)
这个算法比较特殊。
前面的算法都只考虑后端服务器的状态,IP 哈希考虑的是客户端。
upstream backend { ip_hash; server 192.168.1.10; server 192.168.1.11; server 192.168.1.12; }它的原理是:拿客户端的 IP 地址做哈希运算,映射到后端服务器列表中的某一个。这样一来,同一个 IP 的请求永远会落到同一台后端服务器上。
这个特性非常重要,适合需要会话保持的场景。
举个栗子,电商网站,用户登录之后,session 存在某一台后端服务器上。如果用户的下一个请求被 Nginx 分到了另一台机器,那边没有 session 数据,用户就得重新登录。这种体验简直灾难。
IP 哈希就能避免这个问题。同一个用户的请求永远到同一台机器,session 就保住了。
但它也有坑。
如果你们公司出口 IP 是固定的(比如 NAT 后面的一批用户),那这些用户就全部分到同一台后端,负载均衡就形同虚设了。而且,如果后端服务器数量变了,哈希映射关系就会被打乱,session 又丢了。
这些算法背后的数学原理,你知道吗?
说到这,可能有人会问,IP 哈希具体怎么算的?
Nginx 用的是crc32算法,把客户端 IP 转换成一个 32 位的整数,然后对后端服务器的数量取模,得到一个 0 到 N-1 之间的数字,对应到具体的服务器。
举个例子,三台后端服务器,某个客户端 IP 算出来哈希值是 1234567,1234567 % 3 = 1,那就分配到第二台(索引 1)上。
简单粗暴,但很有效。
不过,这种简单的取模方式有个问题:后端服务器数量变化时,几乎所有映射关系都会变。
比如你原来有 3 台,加到 4 台,原来分到索引 1 的请求,现在hash % 4的结果可能变成别的了,session 全乱。
要解决这个问题,得用一致性哈希。但 Nginx 原生的 IP 哈希不是一致性哈希,所以扩容的时候要小心。
真要做一致性哈希,可以考虑 OpenResty 的lua-resty-balancer模块,或者用 HAProxy,人家支持。
高级特性:让负载均衡更智能
上面讲的是基础算法,但在生产环境中,光有这些还不够。Nginx 还提供了一些高级特性来应对复杂的场景。
健康检查
这个必须有。
后端服务器有时候会挂掉——进程崩了、磁盘满了、网络断了……各种原因。如果 Nginx 不知道某台机器挂了,还在往那边转请求,用户就会看到 502 错误。
Nginx 默认有两种健康检查方式:
被动检查:也叫"连接失败检测"。Nginx 在转发请求的时候,如果发现连接被拒绝或者超时,就自动把这台后端标记为"不可用",在接下来的fail_timeout时间内不再往那边转。默认配置:
upstream backend { server 192.168.1.10 max_fails=3 fail_timeout=30s; server 192.168.1.11 max_fails=3 fail_timeout=30s; }max_fails=3表示连续失败 3 次就标记为不可用。fail_timeout=30s表示 30 秒后再次尝试。
主动检查:需要用到nginx_upstream_check_module这个第三方模块。Nginx 会主动向后端发送特定的请求(比如 HTTP 请求或 TCP 连接),根据响应判断服务器是否健康。
upstream backend { server 192.168.1.10; server 192.168.1.11; check interval=3000 rise=2 fall=5 timeout=1000 type=http; check_http_send "GET /health HTTP/1.0\r\n\r\n"; check_http_expect_alive http_2xx http_3xx; }这段配置的意思是:每 3 秒检查一次,连续成功 2 次认为恢复,连续失败 5 次认为挂了。检查方式是发一个 HTTP GET 请求,期待返回 2xx 或 3xx 状态码。
主动检查的好处是能在问题恶化之前发现隐患,但会消耗一些资源。
动态负载均衡
传统的 Nginx 配置是写死在nginx.conf里的,改了之后得nginx -s reload才能生效。在大型互联网公司,这显然不够灵活。
动态负载均衡的意思是:后端服务器列表可以实时变化,不用重启 Nginx。
实现方式有几种:
第一种,用 Nginx Plus(官方商业版),自带动态配置功能。
第二种,用 Consul + nginx-upsync-module。通过 Consul 做服务发现,nginx-upsync 模块定时拉取最新的服务器列表,自动更新 upstream 配置。
upstream backend { upsync 127.0.0.1:8500/v1/kv/upstreams/backend upsync_timeout=6m upsync_interval=500ms upsync_type=consul strong_dependency=off; upsync_dump_path /usr/local/nginx/conf/servers.conf; include /usr/local/nginx/conf/servers.conf; }这套方案在很多公司都用,适合微服务架构。
第三种,用 OpenResty + Lua,自己写脚本动态调整。这种方式最灵活,但开发成本也最高。
流量控制
线上流量就像洪水,有时候会突然暴涨。
Nginx 提供了limit_req和limit_conn模块来做流量控制。
请求频率限制:
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s; server { location /api/ { limit_req zone=req_limit burst=20 nodelay; proxy_pass http://backend; } }这段配置的意思是:每个 IP 每秒最多 10 个请求。burst=20表示可以临时超出 20 个,超出的请求会被延迟处理或者直接拒绝。
连接数限制:
limit_conn_zone $binary_remote_addr zone=conn_limit:10m; server { location /api/ { limit_conn conn_limit 10; proxy_pass http://backend; } }每个 IP 最多保持 10 个连接。
流量控制是保护后端服务的重要手段,配合监控告警,能在第一时间把异常流量挡在外面。
会话保持(Sticky Session)
前面说了 IP 哈希可以做会话保持,但它不够灵活。如果客户端 IP 经常变(比如移动网络),IP 哈希就不好使了。
Nginx 的第三方模块nginx-sticky-module提供了基于 Cookie 的会话保持:
upstream backend { sticky cookie srv_id expires=1h domain=.example.com path=/; server 192.168.1.10; server 192.168.1.11; }Nginx 第一次收到客户端请求时,会在响应里塞一个srv_id的 Cookie,标记后端服务器。下次客户端再来,带着这个 Cookie,Nginx 就知道该转给哪台。
这种方式比 IP 哈希更精准,但也更复杂,Cookie 处理本身就有点重。
现在的互联网公司更倾向于无状态服务 + Redis 共享 Session的方案,把 Session 信息统一存到 Redis 里,后端服务器都可以访问。这样负载均衡算法就完全不用考虑会话保持了,简单粗暴。
实战中的坑,我踩过的那些
理论说再多,不如真实案例来得深刻。
坑一:上游服务器 keepalive 设置不当
有一段时间,我们线上服务响应慢得离谱,但 CPU、内存、磁盘都很正常。
排查了很久,最后发现是 Nginx 上游 keepalive 连接数不够。
upstream backend { server 192.168.1.10; keepalive 32; }这个keepalive 32意思是每个 worker 进程与后端保持 32 个长连接。我们当时后端有 100 台机器,Nginx 有 8 个 worker,总连接数就达到 25600 个。后端服务的文件描述符没调优,很快就耗尽了。
解决办法:根据实际情况调整keepalive参数,同时配合后端的ulimit优化。
坑二:proxy_buffer 设置不合理
Nginx 在转发响应给客户端的时候,会先把后端的响应缓存到内存里,然后发给客户端。这个缓存区的大小如果不合适,就会出问题。
proxy_buffer_size 4k; proxy_buffers 8 16k; proxy_busy_buffers_size 32k;proxy_buffers 8 16k表示分配 8 个 16K 的缓冲区。如果后端响应特别大(比如大文件下载),缓冲区不够用,数据会写到磁盘临时文件,性能就崩了。
我们曾经因为没设置proxy_temp_path,临时文件被写到了系统盘的/tmp,结果磁盘 IO 飙高,整个系统卡顿。
解决办法:把临时文件路径改到大容量磁盘,并合理调整缓冲区大小。
坑三:健康检查触发误判
有一次做促销活动,流量暴涨,后端某些接口响应时间从 50ms 涨到了 200ms。Nginx 的健康检查把响应时间超过阈值的服务器判定为"不可用",大量服务器被踢出集群,雪上加霜。
解决办法:调高健康检查的超时时间,配合业务特性设置合理的阈值。健康检查要稳定,不能太敏感。
坑四:DNS 解析缓存导致 upstream 失效
我们用域名配置 upstream:
upstream backend { server backend1.example.com; server backend2.example.com; }Nginx 在启动时解析一次域名,之后就缓存起来。如果后端 IP 变了(云服务器重启、迁移),Nginx 不知道,还在往老 IP 发请求。
解决办法:用resolver指令配置 DNS 服务器,并设置valid参数让 Nginx 定期重新解析。
Nginx 负载均衡的连接管理细节
讲到这里,必须再说说 Nginx 是怎么管理连接池的,这块很多人没注意过,但非常关键。
Nginx 作为反向代理,每收到一个客户端请求,就需要建立一个到后端的连接。频繁创建和销毁连接开销很大,所以 Nginx 会维护一个连接池。
当请求处理完,连接不会被立即关闭,而是放回连接池等待复用。keepalive参数控制的就是这个连接池的大小。
但这里有个细节:keepalive 连接是在 worker 进程内的,不是跨 worker 共享的。
什么意思?假设 Nginx 有 4 个 worker,每个 worker 都独立维护自己的连接池。如果客户端连接被分配到 worker 1,但 worker 1 的连接池满了,Nginx 不会去借 worker 2 的连接,而是建立新连接。
这就是为什么keepalive数值要合理,太小会导致频繁建立新连接,太大会浪费资源。
另外,keepalive_requests参数控制的是单个长连接最多处理多少个请求,达到上限后连接会被关闭。默认是 1000,可以根据实际情况调整。
七层 vs 四层:什么时候该用什么
Nginx 的负载均衡工作在应用层(OSI 七层模型),能解析 HTTP/HTTPS 协议,根据 URL、Header、Cookie 等信息做转发决策。
但有些场景下,四层负载均衡更合适。
四层负载均衡工作在传输层(TCP/UDP),只根据 IP 和端口做转发,不解析应用层数据。LVS、HAProxy 都是典型的四层负载均衡器。
什么时候用四层?
- 需要处理非 HTTP 协议(MySQL、Redis、游戏服务器)
- 对性能要求极高,希望减少协议解析开销
- 后端服务规模特别大,七层代理成为瓶颈
什么时候用七层?
- 需要根据 URL、Header 做智能路由
- 需要 SSL 卸载
- 需要做灰度发布、A/B 测试
实际生产中,经常是四层 + 七层组合使用。LVS 做最外层的流量分发,把请求转发给多个 Nginx 集群;Nginx 做七层处理,根据业务规则转发给具体的后端服务。
我们公司现在的架构就是这种模式,LVS 在最外层抗大流量,Nginx 在中间做业务路由,后面才是应用服务。
灰度发布:负载均衡的进阶玩法
说一个很实用的场景:灰度发布。
新版本上线,不可能一次性全量切换,万一有 bug 就完蛋了。常规做法是先让一小部分用户用新版本,观察没问题再逐步放量。
Nginx 怎么实现灰度?
基于 Cookie:
split_clients $cookie_version $backend { 10% "backend_v2"; 90% "backend_v1"; } server { location / { proxy_pass http://$backend; } }split_clients模块根据 Cookie 的值做概率分配,10% 的请求到新版本,90% 到老版本。
基于 IP 段:
geo $backend { default backend_v1; 192.168.1.0/24 backend_v2; 10.0.0.0/8 backend_v2; }内网测试用户走新版本,外部用户走老版本。
基于 Header:
map $http_x_green $backend { default backend_v1; "true" backend_v2; }通过自定义 Header 标记,比如前端在测试时加上X-Green: true,请求就走到新版本。
灰度发布是个大学问,Nginx 的这几个指令只是最基础的实现。真正的灰度还要配合监控、告警、回滚机制,才能在生产环境安全使用。
性能调优:让 Nginx 飞起来
最后聊聊性能调优,这块实战性很强。
worker 进程数设置:
worker_processes auto; # 自动设置为 CPU 核心数或者手动指定为核心数。
每个 worker 的连接数:
events { worker_connections 10240; }理论最大值是worker_processes * worker_connections,但实际上还要考虑文件描述符、内存等限制。
文件描述符限制:
worker_rlimit_nofile 65535;Linux 默认一个进程最多打开 1024 个文件描述符,太少了。生产环境必须调大。
Gzip 压缩:
gzip on; gzip_min_length 1k; gzip_comp_level 6; gzip_types text/plain text/css application/json application/javascript;压缩响应数据,减少网络传输量。但压缩本身消耗 CPU,所以gzip_comp_level不要调太高,6 是一个平衡点。
缓存静态资源:
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { expires 7d; access_log off; }给静态资源设置缓存时间,减少重复请求。
日志优化:
access_log /var/log/nginx/access.log main buffer=32k flush=5s;日志写入是磁盘 IO 操作,加 buffer 能减少 IO 次数。但日志丢失的风险会增大,要权衡。
写在最后
Nginx 负载均衡这块,表面上看是几个配置指令的事,但深入下去,涉及进程模型、连接管理、算法原理、健康检查、流量控制等方方面面。
我见过太多人只会照着网上的模板配,出了问题就抓瞎。真正的运维不是配置工程师,而是要理解背后的原理,知道为什么这么配,出了事能快速定位。
负载均衡说到底是在多个资源之间合理分配任务,以达到最优的系统性能。这个思想在很多领域都通用,不仅仅是 Nginx。
如果你能把今天讲的这些内容真正消化掉,下次面试被问到 Nginx 负载均衡(虽然你说不要面试痕迹,但多学点没坏处),或者生产环境遇到相关问题,都能从容应对。