很多人对 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 weight5; server 192.168.1.11 weight3; server 192.168.1.12 weight2; }这里的 weight 就是权重。Nginx 会按照 5:3:2 的比例来分配请求。也就是说十个请求里第一台分到 5 个第二台 3 个第三台 2 个。但这里有个细节很多人不知道Nginx 的权重轮询不是简单的先来五个再去下一台而是用了平滑加权轮询算法。啥意思如果用简单加权轮询分配顺序可能是10、10、10、10、10、11、11、11、12、1210 出现 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 算出来哈希值是 12345671234567 % 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_fails3 fail_timeout30s; server 192.168.1.11 max_fails3 fail_timeout30s; }max_fails3表示连续失败 3 次就标记为不可用。fail_timeout30s表示 30 秒后再次尝试。主动检查需要用到nginx_upstream_check_module这个第三方模块。Nginx 会主动向后端发送特定的请求比如 HTTP 请求或 TCP 连接根据响应判断服务器是否健康。upstream backend { server 192.168.1.10; server 192.168.1.11; check interval3000 rise2 fall5 timeout1000 typehttp; 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_timeout6m upsync_interval500ms upsync_typeconsul strong_dependencyoff; 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 zonereq_limit:10m rate10r/s; server { location /api/ { limit_req zonereq_limit burst20 nodelay; proxy_pass http://backend; } }这段配置的意思是每个 IP 每秒最多 10 个请求。burst20表示可以临时超出 20 个超出的请求会被延迟处理或者直接拒绝。连接数限制limit_conn_zone $binary_remote_addr zoneconn_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 expires1h domain.example.com path/; server 192.168.1.10; server 192.168.1.11; }Nginx 第一次收到客户端请求时会在响应里塞一个srv_id的 Cookie标记后端服务器。下次客户端再来带着这个 CookieNginx 就知道该转给哪台。这种方式比 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 失效我们用域名配置 upstreamupstream 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 怎么实现灰度基于 Cookiesplit_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; }内网测试用户走新版本外部用户走老版本。基于 Headermap $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 buffer32k flush5s;日志写入是磁盘 IO 操作加 buffer 能减少 IO 次数。但日志丢失的风险会增大要权衡。写在最后Nginx 负载均衡这块表面上看是几个配置指令的事但深入下去涉及进程模型、连接管理、算法原理、健康检查、流量控制等方方面面。我见过太多人只会照着网上的模板配出了问题就抓瞎。真正的运维不是配置工程师而是要理解背后的原理知道为什么这么配出了事能快速定位。负载均衡说到底是在多个资源之间合理分配任务以达到最优的系统性能。这个思想在很多领域都通用不仅仅是 Nginx。如果你能把今天讲的这些内容真正消化掉下次面试被问到 Nginx 负载均衡虽然你说不要面试痕迹但多学点没坏处或者生产环境遇到相关问题都能从容应对。