Nginx 负载均衡与健康检查实战:upstream 加权调度与故障转移

单机瓶颈与负载均衡

个人站长的网站发展到一定阶段,单台服务器的性能就会见顶:一台机器既要跑 Nginx 又要跑 PHP-FPM 和 MySQL,高峰期 CPU 打满,数据库连接池被占光,页面开始卡顿甚至 502。负载均衡的思路是把请求分散到多台后端服务器上,Nginx 本身就是一个非常优秀的七层负载均衡器,不需要额外引入 HAProxy 之类的组件。这篇文章讲透 Nginx upstream 的配置、调度算法、健康检查与故障转移,让站长用最少的学习成本搭建一套可靠的负载均衡。

一、upstream 基础配置

upstream 块定义一组后端服务器,location 里通过 proxy_pass 引用它。最简单的配置如下:

upstream backend {
    server 192.168.1.11;
    server 192.168.1.12;
    server 192.168.1.13;
}

server {
    listen 80;
    server_name www.example.com;
    location / {
        proxy_pass http://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-Real-IP 和 X-Forwarded-For 拿到访客真实 IP,否则日志里全是负载均衡器的内网地址。

二、加权负载均衡

后端机器配置不一致时,用 weight 控制流量比例。新机器性能好就多分一些流量,老机器少分一些:

upstream backend {
    server 192.168.1.11 weight=3;
    server 192.168.1.12 weight=2;
    server 192.168.1.13 weight=1;
}

weight 默认是 1,按权重比例分配:上面配置下,三台机器大约各承担 50%、33%、17% 的请求。加权轮询适合后端硬件差异明显的场景,是生产环境最常用的配置。

三、调度算法选择

除了默认的轮询,upstream 还支持几种常用算法。ip_hash 按访客 IP 做哈希,同一个 IP 的请求固定落到同一台后端,天然解决会话保持问题,适合没有共享 Session 的旧应用;least_conn 把请求分给当前连接数最少的后端,适合请求处理时间差异大的场景;hash 支持按自定义键(如 URL、userId)做一致性哈希,配合缓存后端可以显著提高缓存命中率。

upstream backend {
    ip_hash;
    server 192.168.1.11;
    server 192.168.1.12;
}
upstream cache_backend {
    hash $request_uri consistent;
    server 192.168.1.21;
    server 192.168.1.22;
}

consistent 参数启用一致性哈希,后端增减节点时只有少量缓存键需要重新映射,对缓存类服务非常友好。

四、被动健康检查:max_fails 与 fail_timeout

Nginx 默认就带被动健康检查:连续 max_fails 次请求失败,就把该后端标记为不可用,冷却 fail_timeout 秒后再重新试探。这是零额外成本的基础保障:

upstream backend {
    server 192.168.1.11 max_fails=3 fail_timeout=30s;
    server 192.168.1.12 max_fails=3 fail_timeout=30s;
}

含义是 30 秒内失败 3 次就摘除 30 秒。注意 max_fails 统计的是与后端建立连接或收发响应失败的次数,超时也算失败。被动检查的缺点是:只有流量打到故障节点时才会被发现,没人访问就发现不了故障,所以更适合配合主动检查一起用。

五、故障转移与 proxy_next_upstream 的坑

后端被摘除后,Nginx 会把请求转发给下一个健康的后端,这就是故障转移。但默认情况下,如果请求已经发送给后端才出错,Nginx 不会自动重试下一个节点,需要显式配置 proxy_next_upstream:

proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;

这里有个经典坑:不要把 http_500 加进去,因为 500 说明后端已经处理了请求,重试可能导致重复写入;更危险的是 POST 请求,如果请求体已经发出,Nginx 默认不会对非幂等请求重试。如果你确实需要重试 POST,要配合 proxy_next_upstream_non_idempotent 使用,但业务上必须先保证接口幂等,否则用户可能被重复扣款、重复下单。

六、主动健康检查方案

主动健康检查需要 Nginx Plus 商业版,或者用开源的 OpenResty 加 lua-resty-healthcheck。个人站长不想付费的话,推荐一个轻量方案:用 ngx_healthcheck_module 编译第三方模块,或者干脆写一个定时脚本,用 curl 定期探测各后端的健康 URL,发现异常就通过 Nginx 的 API 或直接修改 upstream 配置剔除节点。脚本方案虽然简陋,但对个人站完全够用:

#!/bin/bash
# /usr/local/bin/backend_health.sh
for ip in 192.168.1.11 192.168.1.12; do
  if ! curl -s -o /dev/null --connect-timeout 3 "http://$ip/healthz"; then
    echo "$(date) $ip DOWN" >> /var/log/backend_health.log
  fi
done

后端应用侧要提供一个轻量的 /healthz 接口,只检查应用和数据库连接是否正常,不要做重逻辑,否则健康检查本身会成为后端的负担。

七、超时与慢后端处理

负载均衡场景下,慢请求会拖垮整体体验:一台后端卡住,用户的请求挂在 Nginx 上占着 worker,连接数耗尽后整个站点假死。必须给代理设置合理的超时:

proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 8 8k;

proxy_read_timeout 是两次读操作之间的间隔超时,不是总超时,长轮询或上传大文件的接口要单独调大。proxy_buffering 开启后,Nginx 会先把后端响应收进缓冲区再发给客户端,避免慢客户端反过来拖住后端 worker,这是高并发下非常重要的保护措施。

八、会话保持的取舍

应用没有共享 Session 时,需要会话保持。ip_hash 最简单,但同一个 NAT 出口的所有用户会被分到同一台机器,可能造成负载不均;更精细的方案是用 sticky cookie 模块(nginx-sticky-module 或 OpenResty),首次请求时下发一个 cookie,之后按 cookie 值哈希到同一后端。如果应用已经把 Session 放进 Redis 或 MySQL,就不需要会话保持,直接轮询即可,这也是最推荐的架构方向。

九、实战:双后端 PHP 应用

把以上知识点拼起来,一个生产可用的双后端配置:

upstream php_backend {
    least_conn;
    server 192.168.1.11:9000 max_fails=3 fail_timeout=30s;
    server 192.168.1.12:9000 max_fails=3 fail_timeout=30s;
}

server {
    listen 80;
    server_name www.example.com;
    root /var/www/html;
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    location ~ \.php$ {
        fastcgi_pass php_backend;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    location ~* \.(css|js|png|jpg|woff2)$ {
        expires 30d;
        access_log off;
    }
}

两台机器各跑一个 PHP-FPM,Nginx 通过 9000 端口做负载均衡,静态资源直接由 Nginx 本地返回不落后端,动态请求按连接数最少原则分发。数据库仍然只跑在其中一台或独立机器上,避免两个后端同时写库的冲突。

十、验证与监控

配置完成后,nginx -t 检查语法,reload 生效。验证方法:连续 curl 多次,观察各后端的 access log 确认请求均匀分布;故意停掉一台后端,再 curl 观察请求是否自动转移到另一台,以及恢复后是否自动回归。监控方面,可以在 access log 里加一个 $upstream_addr 字段,用 GoAccess 按天统计各后端承接的请求量,任何一台长期没有流量,多半是健康检查把它误摘了。

十一、keepalive 与网络规划

负载均衡场景下,Nginx 与后端之间的连接复用能显著降低开销。默认 Nginx 与后端是短连接,每次请求都重新建立 TCP 连接,高并发下握手开销不可忽视。开启 upstream keepalive 让连接池复用连接:

upstream php_backend {
    least_conn;
    server 192.168.1.11:9000 max_fails=3 fail_timeout=30s;
    server 192.168.1.12:9000 max_fails=3 fail_timeout=30s;
    keepalive 32;
}
location ~ \.php$ {
    fastcgi_pass php_backend;
    fastcgi_keep_conn on;
    include fastcgi_params;
}

fastcgi_keep_conn on 配合 keepalive 32,Nginx 会复用与 PHP-FPM 之间的连接。网络规划上,同一机房的多个后端尽量走内网 IP 通信,既省公网流量费又降低延迟;跨机房负载均衡则要接受更高的网络延迟,超时参数要相应放宽,否则一个跨机房请求慢一点就被判超时摘除。后端机器之间还要保证时钟同步(chrony),不然健康检查日志和请求时间线对不上,排查问题会非常痛苦。

十二、灰度发布与热备

负载均衡配置稍加利用,就是一套简易的灰度发布工具。新版本上线时,先把一台后端加入 upstream 并给很小的权重(weight=1),其余后端维持原权重,这样只有一小部分流量打到新版本;观察一段时间日志和错误率没有异常,再逐步提高新节点权重直到全量切换;发现问题则直接把它 weight=0 或从 upstream 摘除,流量立即回退,整个过程用户无感知,也不需要停服。

upstream backend {
    server 192.168.1.11 weight=9;
    server 192.168.1.12 weight=1;  # 新版本灰度节点
    server 192.168.1.13 backup;    # 热备节点
}

backup 参数定义热备节点:平时不承接流量,只有其他所有节点都不可用时才顶上。对于预算有限的个人站长,可以用一台性能较差的旧机器做 backup,主节点挂掉时保证网站仍可访问,虽然慢一点,但比彻底打不开强得多。这就是负载均衡在可用性上最大的价值:让发布和故障都变成可控制的事件,而不是事故。

结语

负载均衡的价值不只是扛住更大的流量,更重要的是可用性:单台后端宕机、重启、发布新版本时,流量自动绕行,用户无感知。个人站长从加权轮询加被动健康检查起步,够用了再上主动检查、会话保持这些进阶能力,循序渐进,避免一上来就把架构搞复杂。记住一个原则:任何一台后端都可能随时挂掉,负载均衡配置的目标就是让单点故障不再等于网站故障。

Last modification:August 31st, 2026 at 08:13 am

Leave a Comment