ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Nginx负载均衡生产级调优:调度算法、健康检查与压测验证

Nginx负载均衡生产级调优:调度算法、健康检查与压测验证 上周半夜接到一个电话朋友的服务从单机扩到三台中间挂了一台 Nginx 做负载均衡本以为能轻松扛住流量高峰结果接口平均响应时间不降反升翻后台日志发现其中一台业务机器几乎没接到请求另一台被打到 CPU 满载。他第一反应是Nginx 负载均衡是不是坏了其实配置就那几行真正让它稳定工作的东西全藏在upstream块的参数、健康检查的设计和部署细节里。Nginx 负载均衡这个题目看起来简单网上随手一搜全是三行配置搞定的教程但生产环境跑起来之后会话丢失、连接数分化、后端半死不活、离线环境装不上、HTTPS 证书链不全这些问题会一个接一个冒出来。这篇我打算把这几年从最小可用配置一路做到生产级调优的整套东西梳理清楚调度算法怎么挑、健康检查怎么设计、会话保持怎么做、SSL 在哪里终止、内网离线机器怎么装、WebSocket 长连接怎么转发以及压测时怎么用数据证明流量真的被分匀了。刚接触反向代理的新手能照着把环境跑起来已经跑了半年生产想再抠一抠性能的老手应该也能从参数细节里捞到点东西。1. 从一台机器扛不住到三台机器不均衡负载均衡真正要解决的三个问题1.1 加机器之后为什么反而更慢很多人对负载均衡的理解停留在把请求分到多台机器上于是扩完容发现慢第一反应是机器不够。真实的因果往往反过来加机器引入了一个新的单点Nginx 本身和一段新的网络跳转如果没有配套的调优性能反而会退。我见过最常见的三种退化场景第一种是 Nginx 到后端默认走短连接每个请求都要重新握手后端机器的TIME_WAIT迅速堆到几万个端口耗尽第二种是健康检查缺失某台后端进程还在但数据库连接池已经炸了Nginx 依然按权重把流量打过去用户侧表现为偶发超时第三种是会话没做保持用户登录态在几台机器之间来回漂移每次切机器都要重新查一次数据库里的 session等于把省下来的算力又还回去了。这三个问题的共同点是它们都不会在配置语法检查时报错nginx -t永远显示 successful只有真实流量打上去才暴露。所以我在设计任何一套负载均衡之前会先把三个问题写在纸上——连接怎么复用、后端怎么判死、状态放在哪——这三个答案定下来配置才有意义否则就只是在抄别人博客里的模板。1.2 反向代理和负载均衡不是一回事这两个词经常被混着用但它们的职责边界其实很清楚。反向代理解决的是客户端不知道也不该知道后端是谁核心动作是转发和改写请求头负载均衡解决的是在一组等价的后端里挑一个核心动作是调度和容错。Nginx 用同一套机制同时实现这两件事proxy_pass负责转发upstream负责挑人所以配置里经常看到proxy_pass http://backend;这种写法——这个backend不是域名是上面定义好的 upstream 块的名字。理解这一点很关键因为它决定了你排错时该看哪一段。请求根本没到后端那是反向代理层的问题proxy_pass地址、resolver、网络连通性请求到了后端但分配不均那是 upstream 层的问题权重、算法、健康状态。我调试时习惯先在 Nginx 上curl一下后端直连地址确认真能通再回头查 upstream这样能省掉一半的猜测时间。1.3 一个能跑的最小配置先把地基搭起来。下面这份配置是能直接跑的最小集合我把它放在/etc/nginx/conf.d/lb.conf里通过主配置的include引入方便后面单独改upstream app_backend { server 10.0.0.11:8080 weight1; server 10.0.0.12:8080 weight1; server 10.0.0.13:8080 weight1; } server { listen 80; server_name app.internal; location / { proxy_pass http://app_backend; 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_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 3s; proxy_read_timeout 30s; } }这里有几个新手容易忽略的点。proxy_connect_timeout默认 60 秒太长了后端如果已经挂了Nginx 会傻等一分钟才切换用户体感就是页面转圈然后报错压到 3 秒能让故障切换快一个数量级。Host头要显式传某些后端框架靠它做虚拟主机路由或生成绝对链接不传会拿到 Nginx 自己的地址。至于三台后端到底分得匀不匀光看配置看不出来得靠后面第 8 节的压测数据来验证。2. upstream 里的六种调度算法选错算法等于白扩三台机器2.1 默认轮询与 weight权重到底按什么算不写任何算法参数时Nginx 用的是加权轮询round-robin。注意它叫加权轮询——即使你没写weight每台机器的默认权重也是 1所以三台机器严格轮流接单。这个默认行为在机器配置完全一致、每个请求耗时也差不多的时候表现最好一旦机器配置差异大比如一台 2 核一台 16 核就会出现弱机被打爆、强机在划水。weight的取值是相对值而不是百分比写weight5和weight1表示前者承担 5/6 的流量。我一般按 CPU 核数或压测出来的 QPS 上限来配权重比如三台机器压测单机上限分别是 800、800、1600 QPS那就写weight1 1 2比拍脑袋写数字靠谱得多。另外要提醒一句权重调大不等于能力变强如果瓶颈在后端的数据库上给应用层加权重只会让数据库更早崩先把瓶颈定位清楚再动手。2.2 ip_hash 与 hash $request_uri会话保持的两条路ip_hash是最省事的会话保持方案原理是把客户端 IP 的前三段IPv4 的 /24做哈希同一个网段的请求会固定落到同一台后端。配置只有一行upstream app_backend { ip_hash; server 10.0.0.11:8080; server 10.0.0.12:8080; }它的优点是不需要后端做任何改造缺点是两个一是同公司、同小区、同运营商出口的用户会被哈希到同一台机器宿舍楼里几百号人可能全压在一台上二是当某台后端被判死之后它的流量会被重新分配到其他机器重新分配后的映射关系会整体位移原本粘住的会话照样丢。所以在公网 To C 场景我基本不用ip_hash只在纯内网、客户端 IP 相对稳定的小系统里图省事。另一种是hash $request_uri或者按 cookie 里的用户标识做哈希把同一类请求或同一个用户钉到固定后端适合缓存命中率敏感的场景比如后端自己做了本地文件缓存的图片服务。代价是需要你自己保证哈希键的稳定性一旦键变了缓存命中率会瞬间归零。2.3 least_conn 与等开销负载均衡的真实含义least_conn会把新请求交给当前活跃连接数最少的那台机器。它的适用场景是请求耗时差异很大——有的接口 20 毫秒返回有的要跑 3 秒这时候轮询会不公平快机器闲着、慢机器排队。least_conn能动态平衡配置同样是加一行。这里要澄清一个经常被说混的概念等开销负载均衡。它不是一个具体的算法名而是一种假设——假设每台后端处理单个请求的消耗相同所以只按连接数或请求数来分配就是合理的。轮询、least_conn全都建立在这个假设之上。如果后端机器配置天差地别或者请求本身有重有轻比如导出报表和查询列表走同一个 upstream这个假设不成立任何单纯数连接数的算法都会失准。此时的正确做法是拆 upstream把重任务和轻任务分开调度而不是指望算法帮你解决。2.4 算法选型对照表算法配置方式适用场景主要风险加权轮询默认或weightn后端同构、请求耗时接近慢请求堆积弱机被打爆least_connleast_conn;请求耗时差异大长连接场景下判断失准ip_haship_hash;内网、需会话保持且不想改代码NAT 出口导致流量倾斜hash $keyhash $request_uri;缓存命中率敏感键变化导致缓存全失效随机random two;后端数量多、希望避免热点分布均匀性略差于轮询一致性哈希hash $key consistent;后端频繁扩缩容需要额外考虑虚拟节点数我的默认选择是能用加权轮询就用有明确的长短请求混合再上least_conn会话保持优先在应用层解决见第 5 节算法本身不应该承担状态管理的职责。3. 藏在 upstream 块里的七个参数max_fails、fail_timeout、backup、keepalive3.1 max_fails 与 fail_timeout 是一对联动开关这两个参数必须一起看。max_fails是在fail_timeout时间窗口内允许的失败次数超过就把这台机器标记为不可用并且在接下来一个fail_timeout周期内不再向它发请求周期结束后会尝试恢复放一个请求进去试探。默认值是max_fails1、fail_timeout10s这个默认值相当激进——一次网络抖动就会把一台健康机器踢下线 10 秒。upstream app_backend { server 10.0.0.11:8080 max_fails3 fail_timeout15s; server 10.0.0.12:8080 max_fails3 fail_timeout15s; }我在生产里通常调成max_fails3 fail_timeout15s到30s。经验数值是这样推的假设单个后端每秒处理 200 个请求10 秒的摘除窗口意味着 2000 个请求被转移如果后端真的只是抖了一下这个代价太大反过来如果把max_fails设成 10真正宕机的机器会多吃 10 个失败请求才被摘掉用户侧就是零星的 502。3 次是一个在两者之间比较舒服的折中。还有个坑要提前说失败计数是按 upstream 整体累加的不是按客户端来源区分而且proxy_next_upstream触发的重试也会计入失败次数。这意味着如果后端返回 502 的频率刚好卡在阈值附近会出现机器反复上线又下线的抖动日志里表现为 upstream 状态不断切换。遇到这种情况我会先把fail_timeout拉长让状态稳定下来再查后端根因。3.2 keepalive 不配端口会被 TIME_WAIT 吃掉这是我最想强调的一条。Nginx 到后端默认使用 HTTP/1.0 的短连接每个转发请求都要走一次三次握手和四次挥手高并发下 Nginx 侧和后端侧的TIME_WAIT会疯狂增长。我见过一台机器netstat出来的TIME_WAIT有六万多条本地端口范围直接被打满新的转发请求拿不到可用端口报Cannot assign requested address。解决方案是在 upstream 里开启长连接池upstream app_backend { server 10.0.0.11:8080; server 10.0.0.12:8080; keepalive 64; keepalive_requests 1000; keepalive_timeout 60s; } location / { proxy_pass http://app_backend; proxy_http_version 1.1; proxy_set_header Connection ; }keepalive 64表示每 worker 进程为这个 upstream 缓存 64 个空闲长连接数值按后端 QPS × 平均耗时估算再留余量一般 32 到 128 之间够用。注意两个必须配套的动作proxy_http_version要显式设为 1.1因为长连接是 1.1 才有的特性Connection头要清空否则客户端传来的Connection: close会被透传给后端长连接池直接失效。这两个点漏掉任何一个keepalive参数就是个摆设配置不报错但毫无效果——这类看起来生效其实没生效的配置是最难排查的。3.3 proxy_next_upstream重试是蜜糖也是毒药默认情况下请求在error和timeout时会自动切到下一台后端重试这正是负载均衡高可用的核心。但默认值不包含 HTTP 状态码也就是说后端返回 502、503 时 Nginx 不会重试直接把错误页面给用户。我一般会加上proxy_next_upstream error timeout http_502 http_503 http_504; proxy_next_upstream_tries 2; proxy_next_upstream_timeout 10s;加上之后故障转移更彻底但同时要意识到风险重试是整个请求重发如果一个 POST 请求已经写了一半数据、后端在返回响应前挂掉重试到另一台机器可能导致重复下单。所以更稳妥的策略是只对幂等请求开放状态码重试非幂等接口保持默认把幂等性保障放到应用层去做比如业务侧的唯一请求 ID。我在金融类项目里的做法就是查询类 location 打开全部重试写入类 location 单独定义只重试error timeout用两个location块隔离别图省事全局打开。3.4 backup 与 down灰度下线的两个标签backup标记的机器平时不接流量只在所有主机器都不可用时顶上适合放一台低配兜底机器——它的存在意义是网站别彻底打不开而不是分担压力。down则是手动把某台机器标记为不接流量专门用于运维操作。这两个标签的组合用法是平滑下线先把目标机器的权重改成 0 或者直接打上downreload 一次此时新的请求不再进来但已经建立的连接还能自然跑完等一个keepalive_timeout加上业务最长处理时间之后再登录那台机器去做重启、升级或者摘除。我见过有人直接kill后端进程导致正在处理的请求全部中断用户侧看到 502这类问题完全可以通过先摘流量再动手避免。这套流程在灰度发布时同样适用一台一台地摘、一台一台地放出问题能立刻回滚。4. 健康检查的盲区被动探测救不了半死不活的后端4.1 被动检查的判断时机与盲区开源版 Nginx 只有被动健康检查也就是靠真实请求的失败来推断后端状态。它的判断依据就是第 3 节讲的max_fails和fail_timeout。被动检查的盲区在于它必须等到有请求失败才知道后端出事了。如果那台机器的权重只有 1/10可能要等好几秒才有请求落到它身上这几秒里用户就在吃错误如果后端是能接收连接但处理极慢的半死状态请求不会立刻报错而是卡在proxy_read_timeout上要等超时才算失败这个滞后期可能长达几十秒。还有一个更隐蔽的情况后端进程还在、端口还 listen 着但依赖的数据库或缓存挂了代码里有没有兜底逻辑直接决定返回 500 还是正常响应。如果返回的是 200 但内容全是错误提示被动检查永远发现不了Nginx 会一如既往地往它身上灌流量。4.2 主动健康检查的三种落地方式想解决这个盲区就得让探活请求主动打过去。开源 Nginx 没有这个能力我实际用过的三种替代方案是第一种是编译第三方模块如nginx_upstream_check_module在 upstream 里加check interval3000 rise2 fall3 timeout1000 typehttp;它会定时向后端发探活请求连续fall次失败就摘除连续rise次成功就恢复。这是最接近商业版体验的方案代价是 Nginx 需要重新编译离线环境里要提前把源码和补丁文件准备好。第二种是使用集成了健康检查的发行版比如 Tengine配置语法类似对不想折腾编译的人更友好。第三种是外部脚本方案写个定时任务用curl探测每个后端的健康接口健康状态变化时改写 upstream 配置文件并nginx -s reload。这个方案最土但最通用任何环境下都能落地缺点是有 reload 延迟、脚本本身要写得足够健壮。我早期项目里就用的这套脚本里加了互斥锁防止并发 reload跑了好几年没出过岔子。后端的探活接口要专门设计别拿首页做探活——首页依赖多、响应慢探测结果失真。我会让后端提供一个/healthz只检查进程存活和关键依赖数据库连接、缓存连接正常返回 200 加一行文本异常返回 503响应控制在 10 毫秒以内。4.3 平滑下线的完整操作步骤把前面的东西串成一套可执行流程这是我发版时的固定动作修改配置把目标后端设为down或者weight0执行nginx -t确认语法正确。执行nginx -s reloadNginx 会启动新的 worker 进程旧 worker 继续处理存量连接直到自然结束。观察该后端机器上的连接数等活跃连接降到 0一般等keepalive_timeout 最慢接口耗时的余量30 到 60 秒足够。登录后端做重启或升级。恢复配置里的down标记reload观察日志确认有请求正常返回。用第 8 节的压测方法快速验证流量分布恢复正常。注意reload不是重启旧 worker 不会被强杀所以正在处理的请求不会中断。但如果你的业务里有超长连接比如文件上传、WebSocket步骤 3 的等待时间要按实际最长连接时长来算必要时用worker_shutdown_timeout兜底强制退出。5. 会话保持失效的排查链路登录态为什么总是丢5.1 NAT 环境下 ip_hash 的集体失效前面提过ip_hash的缺陷这里展开说一个真实案例。某公司内部系统上了ip_hash上线后反馈整个办公区要么都能登录要么都登不上还得重启服务。排查发现办公区通过一个出口 NAT 访问所有员工对外呈现同一个 IPip_hash把它们全都哈希到了同一台后端。这台机器一重启整个办公区就集体掉线。更麻烦的是同一台机器被反复压测另外两台完全空闲。这个案例说明一个原则凡是涉及客户端 IP 的负载决策都要先问一句客户端 IP 在到达 Nginx 之前经过了哪些代理。如果remote_addr拿到的永远是网关地址基于它的哈希就没有意义。此时要么改用 cookie 哈希要么把 session 外置两条路都比硬撑ip_hash强。5.2 cookie 粘滞与共享存储的取舍cookie 粘滞的思路是给首次访问的用户种一个 cookie里面带上后端标识后续请求按这个标识路由。Nginx 开源版没有内置这个能力需要靠hash $cookie_xxx加后端配合或者引入第三方模块。我更推荐的是把状态外置——用 Redis 之类的集中存储放 sessionNginx 就可以放心地用轮询后端任何一台都能处理任何用户的请求。这样扩容、重启、灰度都不影响用户登录态架构上少一层耦合。代价是引入了一个新的依赖Redis 本身要做高可用并且每次请求多一次网络往返。对小系统来说这个代价可能不划算对任何有扩容需求的系统来说这笔账迟早要还。5.3 一次完整的排查记录有人问过我用户明明登录了刷新页面就变游客我按下面的顺序查的这里把链路完整写出来供你复用第一步看会话丢的是不是有规律。如果只在某些操作后丢大概率是 cookie 的Path或Domain配错了如果是随机丢怀疑路由不稳定。第二步在 Nginx 日志里加上游信息。用log_format把$upstream_addr记进去观察同一个用户的连续请求是不是落到了不同后端。这一步能直接确认是不是负载均衡导致的。日志格式我常用的是log_format lb $remote_addr - $http_x_forwarded_for [$time_local] $request $status $body_bytes_sent upstream$upstream_addr time$upstream_response_time;第三步如果确认是路由漂移检查是否所有节点都能读到同一份 session。常见的坑是某一台机器上的缓存没清、或者 session 存的是本地文件其他机器读不到。第四步检查 cookie 的Secure和SameSite属性。在 HTTPS 站点上如果 cookie 带了Secure但用户走的还是 HTTP浏览器会直接丢弃它表现就是登录成功但下次请求没带 cookie。这个坑在从 HTTP 切 HTTPS 的过程中极其常见。这条链路我从上到下走过十几遍基本上第四步之前就能定位到原因。关键是要有观测点——也就是日志里能反映上游选择的那个字段没有它就只能靠猜。6. 在负载均衡层终止 HTTPS证书、协议与真实 IP 转发6.1 自签名证书的交互式生成流程内网系统上 HTTPS证书往往没有正规签发渠道只能用自签。Nginx 官方文档给的是交互式命令执行后按提示一步步输入信息mkdir -p /etc/nginx/ssl cd /etc/nginx/ssl openssl req -x509 -nodes -newkey rsa:2048 -days 3650 \ -keyout server.key -out server.crt回车之后会依次提示输入 Country Name两位国家代码比如 CN、State or Province Name、Locality Name、Organization Name、Organizational Unit Name、Common Name、Email Address。这里只有一个字段值得认真对待就是Common Name它必须是用户浏览器里输入的访问地址。如果你生成时填了localhost但用户用app.internal访问浏览器照样会弹证书不受信任很多人第一次配自签就栽在这个字段上。Organization 那几个字段随便填不影响功能但如果证书要分发给多个系统填成统一的组织名便于日后识别。-days 3650是十年有效期内网系统取这个值比较省事公网证书现在规范已经压到一年以内别照搬。生成完之后要配到 Nginx 里server { listen 443 ssl; server_name app.internal; ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; location / { proxy_pass http://app_backend; } }浏览器告警的根源是自签证书不在客户端的信任库里。真正的消除告警动作不在服务器端而是把这张证书或者签发它的根证书导入每台客户端机器的受信任存储区。这一步没法靠 Nginx 解决做内网项目时要提前和运维确认分发方式。6.2 协议版本与加密套件怎么定默认的 TLS 配置为了兼容性会打开一些老旧协议我在内部系统里会收紧到ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;ssl_session_cache这个参数很实用但容易被忽略它把 TLS 会话缓存在内存里客户端重连时可以跳过完整的握手过程在移动网络弱信号、频繁断连重连的场景下能省下可观的延迟。10m 大约能缓存四万个会话对绝大多数系统够用。证书链的问题要单独提一下。有 CA 签发的证书通常会给你一个证书文件和一个或多个中间证书如果只配了服务器证书而没拼上中间证书部分浏览器会报证书链不完整。正确做法是把服务器证书和中间证书按顺序拼成一个文件cat server.crt intermediate.crt fullchain.crt然后ssl_certificate指向fullchain.crt。这个坑在手机端尤其明显桌面浏览器可能因为缓存了中间证书而正常显示手机上一打开就告警排查起来很容易走弯路。验证方法是openssl s_client -connect 域名:443 -showcerts看输出的证书链是否完整。6.3 X-Forwarded-For 与后端识别真实来源流量经过 Nginx 之后后端看到的连接来源全是 Nginx 的地址所有的访问日志、风控、限流都会失效。标准做法是加转发头proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host;$proxy_add_x_forwarded_for的行为是在客户端传来的X-Forwarded-For后面追加当前remote_addr所以这个头是一个逗号分隔的列表。后端取的时候要明确策略如果 Nginx 前面没有其他代理取列表里第一个就是真实客户端 IP如果前面还有一层网关第一个可能是伪造值应该取倒数第二个或者按已知的代理层数来数。不加验证地直接信任这个头会带来伪造风险内网系统里也建议在后端做一次格式校验。X-Forwarded-Proto是给后端判断用户到底是走 HTTP 还是 HTTPS用的很多框架靠它决定生成的链接是http://还是https://不传的话在 HTTPS 站点上会生成一堆 HTTP 链接触发混合内容告警。7. 生产部署的四个硬骨头离线安装、开机自启、多站点、WebSocket 转发7.1 离线环境安装 Nginx 的完整流程内网机器不能连外网装 Nginx 只能走离线包。两条路可选一是用发行版的包管理器做本地安装二是有源码编译。包管理器的方式需要提前在外网机器上用yumdownloader或apt-get download把 Nginx 及其所有依赖pcre、zlib、openssl的开发包等一次性拉全拷进内网后yum localinstall ./*.rpm批量安装优点是路径规范、systemd 单元文件自动生成缺点是依赖梳理麻烦少一个包就要来回拷。源码编译的流程更可控我的固定步骤是tar -zxvf nginx-1.x.tar.gz cd nginx-1.x ./configure \ --prefix/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_realip_module \ --with-http_stub_status_module \ --with-http_gzip_static_module \ --with-stream \ --with-pcre../pcre-8.45 \ --with-zlib../zlib-1.3 \ --with-openssl../openssl-3.0.x make make install--with-http_stub_status_module我强烈建议带上它提供一个轻量状态页能看到当前活跃连接数、已处理请求数第 8 节做压测验证全靠它。--with-http_realip_module用于从转发头里还原客户端 IP--with-stream是四层代理支持做数据库或消息队列转发时用得上。把pcre、zlib、openssl的源码目录一起带上编译可以避免依赖系统库版本不一致导致的各种诡异问题在国产化环境或者基础镜像比较精简的机器上尤其重要。编译前记得装好gcc、make以及pcre-devel、zlib-devel、openssl-devel这几套开发工具离线环境下这些也得提前备齐。7.2 systemd 开机自启与平滑重载源码编译安装的 Nginx 没有自动生成服务单元需要手写一个/usr/lib/systemd/system/nginx.service[Unit] Descriptionnginx - high performance web server Afternetwork.target [Service] Typeforking PIDFile/usr/local/nginx/logs/nginx.pid ExecStartPre/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf ExecStart/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf ExecReload/bin/kill -s HUP $MAINPID ExecStop/bin/kill -s QUIT $MAINPID PrivateTmptrue [Install] WantedBymulti-user.target写完之后systemctl daemon-reload再systemctl enable nginx就能开机自启。这里有三个细节值得说Typeforking是必须的因为 Nginx 默认是后台守护进程模式ExecStartPre里带-t做语法预检配置写错时服务启动会直接失败而不是留一个半死的进程ExecReload用的是 HUP 信号对应nginx -s reload的平滑重载语义不会中断存量连接。如果机器上已经装了包管理器版本的 Nginx别重复装源码版两者会抢同一个端口和 PID 文件路径表现是启动成功但访问到的是另一个版本。7.3 一台机器跑多个 Web 项目的 server 拆分小团队资源紧张一台 Nginx 上挂好几个项目是常态。做法是按域名或路径拆server块每个项目一套 upstream文件放在conf.d/下用include conf.d/*.conf统一引入这样新增项目只要加文件不用改主配置。按域名区分的写法是server { listen 80; server_name shop.example.com; location / { proxy_pass http://shop_backend; } } server { listen 80; server_name api.example.com; location / { proxy_pass http://api_backend; } }没有多余域名的场景就按路径前缀拆location /shop/和location /api/分别指向不同 upstream。这里有个容易踩的坑proxy_pass带不带结尾斜杠转发路径完全不同。proxy_pass http://backend;会把原始 URI 原样转发proxy_pass http://backend/;会把 location 匹配到的前缀替换掉。前者适合后端已经按完整路径注册路由的情况后者适合你要把前缀剥掉的情况。这个差别我在两个项目之间反复搞混过现在的习惯是先在浏览器里打一个测试请求看后端日志里收到的路径对不对确认之后再往下写业务配置。还有个细节是server_name的匹配顺序精确域名 前缀通配符 后缀通配符 正则 default_server。如果某个请求返回了不该返回的项目页面八成是server_name没匹配上落到了第一个 server 块默认 server。给主站显式加上default_server标记能避免这种意外。7.4 WebSocket 长连接的转发配置实时通信类服务软交换的信令端口、IM、推送、在线协作走的是 WebSocket用普通的proxy_pass会一直提示连接被关闭。原因是 WebSocket 需要 HTTP 的升级握手而 Nginx 默认不处理Upgrade头。标准配置是先定义一个映射map $http_upgrade $connection_upgrade { default upgrade; close; } upstream ws_backend { server 10.0.0.21:5066; server 10.0.0.22:5066; } server { listen 443 ssl; server_name ws.example.com; location /ws/ { proxy_pass http://ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering off; } }三个要点Connection头不能写死成upgrade要用map动态取值否则普通 HTTP 请求也会被当成升级请求超时时间要拉长到小时级WebSocket 是长连接默认的 60 秒会让连接每隔一分钟被掐断重连proxy_buffering off关闭缓冲保证消息实时性否则 Nginx 可能会攒一批数据再发。关于负载均衡策略WebSocket 长连接有个天然特性连接一旦建立就会一直粘在某台后端上所以常规的轮询只影响新连接的初始分配。如果多台后端之间需要共享状态比如同一通话的两端必须落在同一台机器上要么按业务标识做hash要么让后端通过共享存储同步状态。单纯堆后端数量并不能提升单条长连接的吞吐分配的合理性全靠新连接数量是否均匀。8. 压测验证怎么用数据证明流量真的被分匀了8.1 压测方法与被观测指标配置写完不算完必须用数据验证。我的方法是三台后端各自的访问日志分别统计同时用压测工具打一段时间对比每台机器收到的请求数和平均响应时间。压测工具用wrk或ab都可以命令大概长这样wrk -t4 -c200 -d60s --latency http://app.internal/api/list-t4是四个线程-c200是 200 个并发连接-d60s跑一分钟。跑完后三台后端分别执行awk {print $4} access.log | cut -d: -f2 | sort | uniq -c看每分钟的请求数分布。理想情况下三台的比例应该接近配置的权重比例偏差在 5% 以内。如果某台明显偏低先别急着改配置往下看第 8.2 节。除了请求数还要看几个关键指标Nginx 状态页里的Active connections活跃连接数反映实时压力后端日志里的$upstream_response_time上游响应时间反映后端处理能力系统层面的CPU和load反映机器是否真的吃力。只看其中一个指标很容易误判比如连接数均匀但响应时间不均说明算法没问题、后端能力有问题。8.2 三种典型不均衡现象的定位第一种请求数分布严重偏离权重。先检查后端健康状态被标记为down或者处于fail_timeout周期的机器自然收不到流量。用状态页或者错误日志确认有没有后端被摘除。第二种请求数差不多但某台机器 CPU 明显高。这通常说明请求本身重量不一样同一批请求里有长耗时任务和短任务轮询无法感知。解决方案是拆 upstream把重任务单独分一组或者换成least_conn。第三种请求数分布均匀但长连接场景下新连接分配不均。WebSocket 或者开启了keepalive的场景里连接建立之后就固定在那儿了某个时刻的并发连接数取决于历史累计短时间压测看不出问题跑上几小时就会偏移。这种情况要么定期重连要么在后端做连接数均衡。8.3 内核与 Nginx 层的关键参数压测打到高并发时瓶颈往往不在 Nginx 配置而在系统参数。几个我会检查的参数建议值作用net.core.somaxconn65535加大 accept 队列长度net.ipv4.ip_local_port_range10240 65000扩大可用本地端口范围net.ipv4.tcp_max_tw_buckets65535容纳更多 TIME_WAITnet.ipv4.tcp_tw_reuse1允许复用 TIME_WAIT 连接仅出向fs.file-max1000000系统级文件描述符上限worker_rlimit_nofile65535Nginx 进程可用 fd 数worker_connections10240单 worker 最大连接数worker_processes设成auto让它跟随 CPU 核数worker_connections要结合文件描述符上限来定公式是系统 fd 上限 ÷ worker 进程数。我遇到过worker_connections配了 65535 但系统ulimit -n只有 1024 的情况Nginx 启动时不会报错压测一上来就疯狂报too many open files。所以调参数要成对地调改完记得 reload 并用状态页确认连接数真的上去了。另外要避开一个流传很广的错误建议tcp_tw_recycle。这个参数在较新的内核里已经被移除在还在支持它的内核上开启后NAT 环境下的客户端会出现随机连接失败因为它丢掉了 TCP 时间戳的语义。任何让你打开这个参数的教程都可以直接关掉。9. 我踩过的六个坑与对应的修复动作把上面散落的经验收拢成一张表这些是我实际在项目里反复踩过、也反复帮别人排查过的现象根因修复动作后端端口被 TIME_WAIT 占满upstream 未配 keepalive加keepalive n并设proxy_http_version 1.1和清空Connection头偶发 502日志有 upstream 重试某后端半死但被动检查发现不了加主动健康检查探活接口只检查关键依赖登录态随机丢失路由漂移 session 存本地session 外置到集中存储或改 cookie 哈希整个办公区同时掉线NAT 出口导致ip_hash全部命中一台弃用ip_hash改轮询加共享 session手机上提示证书不安全桌面正常证书链缺中间证书cat server.crt intermediate.crt fullchain.crt站点间歇性打不开无规律内核参数与 Nginx 连接数不匹配成对调整 fd 上限和worker_connections这六个坑有个共同规律它们全都不违反 Nginx 的配置语法。nginx -t会说一切正常reload 也不会报错只有真实流量、真实用户、真实时间才能暴露。所以我现在的习惯是任何一次负载均衡相关的改动都要走一遍配置检查 → 灰度摘节点 → 上线 → 压测验证 → 观察半小时日志的流程任何一个环节跳过后面都会用更长的时间还回去。最后分享一个我很依赖的排查小技巧在 Nginx 日志格式里固定加上$upstream_addr和$upstream_response_time这两个字段。它们几乎能回答关于负载均衡的所有问题——请求到底去了哪台、那台花了多久、有没有发生重试重试时会记录多个用逗号分隔的地址。我负责过的每一个线上事故里这两个字段都提供了最直接的线索比任何监控大盘都来得快。
返回列表