负载均衡与健康检查

单机部署时 Nginx 只是一个代理;一旦同一份应用起了两个以上实例,upstream 就从「写个地址」变成「流量调度器」——算法决定请求怎么分、健康检查决定坏实例多久被摘掉、权重决定灰度放多少量。这三件事配错,扩容反而会让可用性下降。本章讲清楚 upstream 的写法、健康检查的真相,以及多实例环境下的平滑发布流程。

要解决的问题

  • 两个实例怎么分流?机器配置不同怎么办?
  • 实例挂了,Nginx 多久能发现并停止转发?
  • 开源版 Nginx 到底有没有「主动健康检查」?
  • 长连接(keepalive)为什么配了不生效?
  • 新版本要放量,怎么按比例灰度而不是全量切换?
  • 发布时怎么做到用户无感(摘一台发一台)?

upstream 基本写法

# /etc/nginx/conf.d/upstream.conf
upstream app_cluster {
    least_conn;                                     # 负载算法,也可换成 ip_hash 或直接用默认轮询
    server 10.0.0.11:8080 weight=3 max_fails=2 fail_timeout=10s;
    server 10.0.0.12:8080 weight=1 max_fails=2 fail_timeout=10s;
    server 10.0.0.13:8080 backup;                   # 备用,前两台全挂才启用
    server 10.0.0.14:8080 down;                     # 手动下线:维护、发版、压测
    keepalive 32;                                   # 每 worker 与上游保持的空闲长连接数
}

upstream 只写一次,放在 http 块里(用 include 引进来),这样多个 server 可以共用,改一处就全站生效。

算法写法适用场景注意
轮询(默认)不写实例同构、请求耗时接近、无状态机器配置不同会造成慢实例堆积
加权轮询weight=3机器配置不同权重按 CPU/内存比例给,别凭感觉
最少连接least_conn请求耗时差异大(导出、报表)weight 可组合
IP 哈希ip_hash会话粘滞,且没有共享 session同一出口 NAT 的整批用户会压到一台
一致性哈希hash $request_uri consistent上游有本地缓存,追求命中率加实例时数据迁移最少
随机二选一random two least_conn实例特别多(几十台以上)避免了轮询的同步开销

选择原则:能用无状态共享 session 就别用 ip_hash。粘滞会带来两个后果——某台机器故障时大量用户会话丢失、扩缩容时负载严重不均。

健康检查:被动与主动

类型实现判定依据能力边界
被动检查开源版内置,max_fails + fail_timeout真实业务请求返回错误/超时只能靠用户请求「试」出故障,有损
主动检查nginx_upstream_check_module(Tengine/三方模块)或 Nginx Plus周期性发探测请求能提前摘除,但需额外编译模块

被动检查的实际行为:fail_timeout=10s 内失败达到 max_fails=2 次,就把这台标记为不可用,并在接下来的 fail_timeout 时间内不再转发;时间到了会放一个请求进去试探,成功则恢复。默认值是 max_fails=1 fail_timeout=10s

# 主动健康检查:需要三方模块(nginx_upstream_check_module),开源版默认没有
upstream app_cluster {
    server 10.0.0.11:8080;
    server 10.0.0.12:8080;
    check interval=3000 rise=2 fall=3 timeout=1000 type=http;
    check_http_send "HEAD /healthz HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
}

没装模块时不要照抄这段,nginx -t 会直接报 unknown directive "check"。多数中小项目的正确做法是:开源版被动检查 + 一个真实检查依赖的 /healthz,配合外部拨测兜底即可。

keepalive 的三个前提

keepalive 32 是 Nginx 与上游之间的连接复用,能显著降低 TIME_WAIT 和握手开销,但必须同时满足三个条件,缺一个都白配:

条件写法缺失后果
用 HTTP/1.1proxy_http_version 1.1;HTTP/1.0 默认短连接
清空 Connectionproxy_set_header Connection "";默认传 Connection: close,连接用完就关
上游支持 keepalive应用服务器开启长连接空闲超时上游先关连接,Nginx 复用时收到 RST

另外 keepalive 的值是「每个 worker 保留的空闲连接数」,不是总连接数上限。32 是常见起点,实例多、QPS 高时可以按「峰值 QPS ÷ worker 数 ÷ 单连接承载」估算。

平滑发布:摘一台,发一台

# 前提:upstream 单独放在 /etc/nginx/conf.d/upstream.conf 里便于脚本改
# 1) 把 11 号机标记为 down,reload 后新请求不再进它
sudo sed -i 's/server 10.0.0.11:8080 /server 10.0.0.11:8080 down /' /etc/nginx/conf.d/upstream.conf
sudo nginx -t && sudo systemctl reload nginx
# 2) 在 11 号机上发布新版本,并本机自测
ssh 10.0.0.11 'cd /srv/app && git fetch --tags && git checkout v1.4.0 && sudo systemctl restart myapp'
ssh 10.0.0.11 'curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/healthz'
# 3) 观察这台机器的错误日志与指标,确认没问题再放回流量
sudo sed -i 's/server 10.0.0.11:8080 down /server 10.0.0.11:8080 /' /etc/nginx/conf.d/upstream.conf
sudo nginx -t && sudo systemctl reload nginx
# 4) 对 12 号机重复同样流程

摘流量要在 reload 后稍等几秒再发版,让已建立的连接自然结束;直接 restart 应用而不摘流量,正在处理请求的用户就会收到 502。

灰度按权重放量

upstream app_cluster {
    server 10.0.0.11:8080 weight=95;   # 稳定版
    server 10.0.0.21:8080 weight=5;    # 新版本,先吃 5% 流量
    keepalive 32;
}
阶段新版本权重观察时长观察指标
首放5(约 5%)15~30 分钟5xx 比例、P95 延迟、日志异常数
加量2530 分钟同上 + 业务核心指标(下单/登录成功率)
半量5030~60 分钟同上
全量100(下线旧实例)留观 24 小时回滚包保留到确认稳定

权重灰度要求「新旧版本接口兼容」:老版本实例还在处理请求,数据库结构、缓存结构、接口字段都不能有破坏性变更,具体做法见第 26 章的三步法。

验证方法

# 看 upstream 生效配置
nginx -T | grep -A10 'upstream app_cluster'
# 请求里带上上游地址,才能看出流量分布(把 $upstream_addr 加进 log_format)
awk '{print $NF}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
# 慢速验证:连续发 30 个请求,观察状态码与耗时
for i in $(seq 1 30); do curl -s -o /dev/null -w '%{http_code} %{time_total}\n' https://api.example.com/healthz; done
# 模拟一台挂掉:停掉 11 号机,确认请求全部成功且日志里不再出现该地址
ssh 10.0.0.11 'sudo systemctl stop myapp'
grep -c '10.0.0.11:8080' /var/log/nginx/error.log

常见坑

现象做法
配了 keepalive 没清空 Connection长连接不生效,TIME_WAIT 依旧很多proxy_http_version 1.1proxy_set_header Connection ""
fail_timeout 设得过短偶发超时就摘机,实例反复抖动按错误率而非单次失败判断,10s 起步
/healthz 只返回 200 不查依赖库挂了仍被认为健康,流量照进健康检查里真连一次数据库/缓存
ip_hash 当默认选项负载严重不均,一台挂掉会话全丢优先共享 session,非必要不用
upstream 写域名不写 IP换 IP 后 Nginx 仍打旧地址用 IP,或配 resolver 并配合变量动态解析
不摘流量直接 restart发布期间用户看到 502先标 down 并 reload,再发版
backup 当常态分流备用机长期空转备用就是备用,常态分流用权重

小结:upstream 的价值不只是分流,而是「能安全地替换机器」;算法按实例是否同构、请求是否耗时均匀来选择,能用共享 session 就别用 ip_hash;开源版只有被动健康检查,靠 max_fails + fail_timeout 加一个真实探测依赖的 /healthz 就够用;keepalive 必须与 HTTP/1.1 和 Connection "" 一起配;发布的标准动作是「摘一台、发一台、验一台、放回去」,灰度则用权重一档一档放量。

笔记加载中…