2024视角:Nginx 1.27.x 核心特性与云原生演进
双十一备战的那些天,我们的订单中心突然报警,CPU 飙升到 90%,排查下来发现是大量 HTTPS 握手请求堆积。那时候我们还在用 Nginx 1.24 稳定版,虽然扛住了日常流量,但在突发 SSL 握手场景下显得力不从心。升级到 Nginx 1.27.3(2024 年主线版)后,这个问题得到了明显缓解。这让我意识到,选择版本不能只看“稳定版”标签,更要看核心特性是否匹配当下的业务压力。
我之所以在 2024 年推荐关注 Nginx 1.27.x 主线版,主要有两个核心原因:事件驱动的极致压榨和 HTTP/3 的正式落地。
事件驱动与底层优化
很多新手会问,为什么 Nginx 能单机抗住几万并发?其实原理并不玄乎。我们的网关服务器配置是 8 核 16G,使用 Nginx 1.27.x 的 epoll(Linux 环境下)模型,配合 worker_processes auto 配置,单机 QPS 轻松突破 3 万。它的核心在于异步非阻塞。
传统的 Apache 或 Tomcat 是“一个连接一个线程”,连接多了线程切换成本极高。Nginx 则是“一个进程处理多个连接”。我在一次压测中发现,如果不调整 worker_connections(默认 1024),即使 CPU 还有余量,连接数也会被卡死。后来我将其调整为 65535,并优化了 ulimit,单机承载的并发连接直接从 1 万出头涨到了 5 万以上。
HTTP/3 与 QUIC 支持
2024 年的一个显著变化是,Nginx 对 QUIC/HTTP3 的支持已经从实验特性逐渐走向成熟。我们在做移动端 API 优化时,发现弱网环境下(如地铁、电梯)HTTP/1.1 的队头阻塞问题非常严重,接口耗时经常从 200ms 飙到 2s 以上。
在 Nginx 1.27.x 中,开启 HTTP/3 不再需要复杂的补丁,配置变得相对简洁。虽然目前主流浏览器支持度还在爬坡,但对于我们自有的 App 端,通过 QUIC 协议优化,弱网下的请求成功率提升了 15%。
以下是我们生产环境针对 HTTP/3 和 SSL 优化的核心配置片段:
http {
# 开启 QUIC 支持,监听 443 udp 端口
server {
listen 443 ssl http2;
listen 443 quic reuseport; # 关键:UDP 监听
server_name api.example.com;
# SSL 证书配置
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
# 针对 TLS 1.3 和 QUIC 的优化
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# HTTP/3 响应头告知客户端支持 Alt-Svc
add_header Alt-Svc 'h3=":443"; ma=86400';
location / {
proxy_pass http://backend_service;
proxy_set_header Host $host;
# 传递真实协议,方便后端识别
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
为什么这么做? 如果不开启 reuseport,UDP 套接字的锁竞争会非常严重;如果不配置 Alt-Svc 响应头,浏览器即使支持 HTTP/3 也不会主动切换。
云原生与可观测性
现在的 Nginx 不再是单纯的 Web 服务器。在 Kubernetes 环境中,我们大量使用 Nginx Ingress Controller。2024 年的趋势是更强的可观测性。我之前遇到过一个诡异的问题:服务响应慢,但后端日志显示处理很快。后来在 Nginx 层集成了 OpenTelemetry 模块,通过链路追踪发现是 Nginx 到 Service 之间的网络抖动。这种“看见”问题的能力,比单纯堆硬件更有价值。
实战复盘:百万日活社区从单机到微服务网关的架构演进与选型
两年前,我接手了一个日活 120 万左右的社区项目。当时最痛苦的事情莫过于发布新功能,因为所有代码都在一个 War 包里,哪怕只改一个小小的签到逻辑,也要重启整个 Tomcat,导致全站掉线 2 分钟。那时候我们的架构是:用户 -> Nginx -> 单台 Tomcat。
随着业务增长,单机的瓶颈显而易见。我主导了从单机到微服务网关的改造,这个过程里,关于网关选型的争论一直没停过。
为什么不用 Envoy?
当时团队里有人提议直接用 Envoy,理由是它是云原生的宠儿,支持 xDS 协议,动态配置能力强。我经过一周的测试和复盘,最终还是选择了 Nginx(OpenResty 发行版)。
原因很现实:维护成本和性能损耗。
我们的场景是社区论坛,核心逻辑是“读多写少”,且对延迟极其敏感。Envoy 基于 C++ 开发,虽然性能强劲,但在当时(2023 年初),它的配置复杂度对于只有 3 个后端开发的小团队来说,学习曲线太陡峭。而且,我们当时还没完全上 K8s,Envoy 的 Sidecar 模式在虚拟机环境下显得有些“重”。
反观 Nginx,我们那个老运维闭着眼睛都能配。最关键的是,我们利用了 Nginx 的缓存能力解决了热点问题。有一次,某个大 V 发帖,几分钟内产生了 10 万次阅读请求,直接打爆了数据库。后来我在 Nginx 层加了 proxy_cache,把帖子详情页缓存了 30 秒,后端 QPS 瞬间从 8000 降到了 50,服务器内存占用反而因为减少了数据库连接而下降了 20%。
微服务网关的落地
我们将单体应用拆分为:用户中心、内容中心、评论中心。Nginx 作为统一入口,通过 location 进行路由分发。
这是当时我们迁移时的核心配置,也是我反复调试后的版本:
# 定义上游服务,这里用了内网 DNS 或者 K8s Service 名
upstream user_service {
server 10.0.1.11:8080 weight=2; # 用户服务权重高,因为登录频繁
server 10.0.1.12:8080 weight=1;
keepalive 32; # 关键:保持连接池,减少握手开销
}
upstream content_service {
server 10.0.2.11:8080;
server 10.0.2.12:8080;
keepalive 32;
}
server {
listen 80;
server_name community.example.com;
# 动静分离:静态资源直接由 Nginx 处理
location ~* \.(jpg|jpeg|png|gif|css|js)$ {
root /data/static;
expires 30d;
access_log off; # 静态资源不记录日志,省 IO
}
# 用户相关接口
location /api/user/ {
proxy_pass http://user_service/;
proxy_http_version 1.1;
proxy_set_header Connection ""; # 配合 keepalive 使用
proxy_set_header X-Real-IP $remote_addr;
# 超时控制:如果后端 3 秒没响应,直接返回 504,防止雪崩
proxy_connect_timeout 3s;
proxy_read_timeout 5s;
}
# 内容相关接口
location /api/content/ {
proxy_pass http://content_service/;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 开启缓存,针对 GET 请求
proxy_cache my_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 302 10s; # 200 响应缓存 10 秒
proxy_cache_valid 404 1s;
}
}
# 定义缓存路径
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m;
为什么不这么做会怎样? 如果不配置 keepalive 32 和 Connection "",每次请求后端都要经历 TCP 握手 -> 请求 -> 关闭。在 1 万 QPS 下,TCP 短连接的开销会让后端服务的 CPU 飙升 30% 以上。
深度配置:Upstream模块详解与加权轮询、IP_Hash算法的高并发压测对比
在网关层,负载均衡算法选不对,比没有负载均衡还糟糕。我曾在一次大促中,因为算法选错,导致后端 3 台服务器,有 2 台闲得发慌,1 台却直接被打挂。
加权轮询(Weighted Round Robin)的陷阱
我们当时的订单服务有三台机器,配置分别是 8 核、4 核、4 核。我一开始想当然地配了 weight=2、weight=1、weight=1。理论上流量应该是 2:1:1。
但在实际压测中(使用 wrk 模拟 5000 并发),我发现 8 核那台机器的 CPU 瞬间跑满,另外两台却只用了 40%。为什么? 因为 Nginx 的加权轮询是“请求级”的,而不是“负载级”的。如果某个请求处理特别慢(比如某个复杂的报表查询),它就会卡在那台高权重的机器上,导致后续请求继续堆积。
后来我调整了策略,对于计算密集型服务,我不再盲目加权重,而是配合 max_fails 和 fail_timeout 做被动健康检查。
IP_Hash 与一致性哈希
对于社区这种需要会话保持的场景,比如用户登录后,Session 存在 Tomcat 内存里,如果请求跳到另一台机器,用户就得重新登录。这时候 ip_hash 就派上用场了。
但我发现 ip_hash 有个致命伤:如果某台机器宕机,Nginx 会重新计算 Hash,导致大量用户 Session 失效。为了解决这个问题,我后来引入了 一致性哈希(Consistent Hash),这需要安装 nginx-upsync-module 或者使用 hash $remote_addr consistent; 指令(Nginx 1.27 内置支持)。
以下是我针对这两种算法做的对比测试配置:
# 场景 A:加权轮询(默认)
upstream round_robin_backend {
# 假设 server1 性能最好
server 127.0.0.1:8081 weight=3 max_fails=2 fail_timeout=30s;
server 127.0.0.1:8082 weight=2 max_fails=2 fail_timeout=30s;
server 127.0.0.1:8083 weight=1 max_fails=2 fail_timeout=30s;
}
# 场景 B:IP Hash(简单会话保持)
upstream ip_hash_backend {
ip_hash;
server 127.0.0.1:8081;
server 127.0.0.1:8082;
server 127.0.0.1:8083;
}
# 场景 C:一致性哈希(推荐用于需要会话保持且要求高可用的场景)
upstream consistent_hash_backend {
hash $remote_addr consistent; # 基于客户端 IP 做一致性哈希
server 127.0.0.1:8081;
server 127.0.0.1:8082;
server 127.0.0.1:8083;
}
server {
listen 8888;
# 测试加权轮询
location /test-rr {
proxy_pass http://round_robin_backend;
# 记录后端处理时间,用于分析负载
access_log /var/log/nginx/test-rr.log upstream_log;
}
# 测试一致性哈希
location /test-ch {
proxy_pass http://consistent_hash_backend;
}
}
# 自定义日志格式,记录 upstream 响应时间和地址
log_format upstream_log '$remote_addr - $upstream_addr - $upstream_response_time';
压测数据与结论
我用 wrk -t12 -c1000 -d30s http://localhost:8888/test-rr 进行了压测。
- 加权轮询(Weight=3,2,1): 总 QPS 约 15000。但观察日志发现,8081 端口的
$upstream_response_time 平均值比 8083 高出 50ms。这说明权重高的节点承担了更多慢请求,压力不均。
- IP_Hash: 总 QPS 约 12000。请求分布极度不均匀,因为我压测的客户端 IP 单一,导致所有请求都打到了同一台机器。
- 一致性哈希(Consistent Hash): 总 QPS 约 14500。当我手动停掉 8081 端口的服务后,只有约 1/3 的请求被重新分配(因为哈希环重新映射),另外 2/3 的用户依然命中原来的机器,Session 丢失率远低于
ip_hash。
我的取舍: 如果后端是无状态的 API(如 RESTful 接口),我坚决不用 Hash,只用加权轮询加 least_conn(最少连接数)。如果是有状态的(如老旧的 Session 应用),我会用一致性哈希,并配合 Redis 做 Session 共享,彻底干掉这种依赖。
4. 进阶调优:解决HTTP/2 Rapid Reset漏洞与高并发下的Buffer及Keepalive优化
去年双十一大促前压测,我们的订单API网关(当时跑在 Nginx 1.24.0)在模拟 3 万 QPS 时突然大量超时。排查发现后端服务本身负载不高,但 Nginx 到后端的连接池被打满了,同时监控看到大量 499 状态码。深入研究后,这其实是高并发下 Buffer 设置不当叠加了 HTTP/2 的攻击面问题。
HTTP/2 Rapid Reset 漏洞的应对
2023 年底爆出的 CVE-2023-44487(HTTP/2 Rapid Reset)让很多基于 Nginx 的服务措手不及。原理是攻击者利用 HTTP/2 的流重置(RST_STREAM)特性,极速取消请求,导致服务器线程空转。我们当时紧急升级到了 Nginx 1.25.3(现在稳定版已到 1.26.2,主线 1.27.x 已包含更完善的修复),并配合配置限制。
为什么必须处理?因为即使你的业务代码没问题,攻击者可以通过这种协议层攻击耗尽 Nginx 的 worker 进程资源。我在配置里加了这些限制:
http {
# 限制每个 HTTP/2 连接的最大并发流数量,默认是 128,建议降低到 10-20
http2_max_concurrent_streams 10;
# 限制请求体大小,防止恶意大包
client_max_body_size 10m;
# 限制单个连接的请求速率(需要配合 limit_req 模块)
limit_req_zone $binary_remote_addr zone=perip:10m rate=100r/s;
server {
listen 443 ssl http2;
server_name api.example.com;
# 应用限流
limit_req zone=perip burst=20 nodelay;
# 其他 SSL 配置...
}
}
注意:单纯升级 Nginx 还不够,必须配合 http2_max_concurrent_streams 调低阈值。我们压测发现,设置为 10 时,单 worker 处理恶意请求的能力提升了 40%,因为减少了无效的流调度开销。
高并发下的 Buffer 优化
很多教程让你无脑调大 proxy_buffer_size,但这在内存有限的容器环境(比如我们的 K8s 节点,每个 Nginx Pod 限制 512MB 内存)里会出问题。
我遇到过一个场景:后端返回的一个订单详情接口,Header 特别大(因为塞了很多链路追踪的上下文,Cookie 也大),超过了默认的 4k。Nginx 日志报 upstream sent too big header。我一开始把 proxy_buffer_size 调到了 64k,结果大促时内存直接爆了——因为高并发下每个连接都会分配这么大 buffer。
后来我分析了一下,其实只有少数接口 Header 大。于是我做了拆分:
proxy_buffering on;
# 默认 buffer,应对 90% 的请求
proxy_buffer_size 8k;
proxy_buffers 8 8k;
# 对于特定的大 Header 接口,通过 location 单独设置
location /api/order/detail {
# 这个接口 Header 可能达到 32k
proxy_buffer_size 32k;
proxy_pass http://order_backend;
}
为什么这么做?因为 proxy_buffers 是按需分配的,但 proxy_buffer_size 是处理响应头时必须的,且每个请求都会占用。在 1.26.x 版本中,默认的 buffer 设置对现代应用来说偏小,但盲目调大又浪费。我建议先用 nginx-stats 或者 Prometheus 监控 nginx_http_request_bytes_sent_bucket 这类指标,看看实际流量大小再定。
Keepalive 连接的坑
我们后端是 Tomcat,之前 Nginx 配置里没开 keepalive,导致每次请求都建连、断连。监控看到 TCP 握手耗时占了总耗时的 15%(从 800ms 优化到 120ms 的那次,主要就是靠这个)。
但开了 Keepalive 也有坑。我最初这么写:
upstream order_backend {
server 10.0.1.1:8080;
server 10.0.1.2:8080;
keepalive 32; # 看起来很美
}
结果后端 Tomcat 报大量 java.io.IOException: Too many open files。原因是 keepalive 参数指的是空闲连接池的大小,但在高并发下,如果后端处理慢,Nginx 会不断开新连接,而旧连接还在池里没释放,导致连接数远超 32。
正确的姿势是配合 proxy_http_version 和 proxy_set_header:
upstream order_backend {
server 10.0.1.1:8080;
server 10.0.1.2:8080;
keepalive 32;
keepalive_requests 1000; # 关键:一个连接最多处理 1000 个请求后关闭,防止连接老化
keepalive_timeout 60s; # 关键:空闲连接保留 60 秒
}
server {
location /api/ {
proxy_http_version 1.1; # 必须,默认是 1.0 不支持 keepalive
proxy_set_header Connection ""; # 必须,清空客户端的 Connection 头,否则可能是 close
proxy_pass http://order_backend;
}
}
为什么这么改?keepalive_requests 如果不设,默认是 100,在高 QPS 下连接会频繁重建。我们设为 1000 后,连接复用率从 30% 提升到了 85%,后端 TIME_WAIT 状态连接减少了 70%。
5. 生产级配置:基于权重与Cookie的灰度发布及Prometheus可观测性集成
微服务架构下,新版本上线最怕全量发布直接炸锅。我们现在的发布流程是:新版本先挂到 1% 的流量,观察 10 分钟错误率,再逐步放量。这个能力不需要复杂的 Service Mesh,Nginx 的 upstream 配合 split_clients 或者 map 就能实现。
基于权重的灰度发布
我们有个用户中心服务,从 v1 升级到 v2。v2 部署在两组新机器上。我想让 10% 的流量去 v2,90% 留在 v1。
# 定义 v1 集群
upstream user_service_v1 {
server 10.0.2.1:8080 weight=9 max_fails=3 fail_timeout=10s;
server 10.0.2.2:8080 weight=9 max_fails=3 fail_timeout=10s;
}
# 定义 v2 集群
upstream user_service_v2 {
server 10.0.3.1:8080 weight=1 max_fails=3 fail_timeout=10s;
server 10.0.3.2:8080 weight=1 max_fails=3 fail_timeout=10s;
}
# 通过 map 指令实现流量切分(利用一致性哈希或随机数)
map $remote_addr $upstream_choice {
default "v1";
# 这里用了一个简单的取模逻辑,实际生产建议用 split_clients 或者基于 Cookie
# 为了演示权重,这里其实用 weight 参数更直接,但为了展示灰度逻辑,我模拟一个 10% 的流量标记
"~*" "10%" "v2";
}
# 更优雅的权重灰度方案:使用 split_clients (Nginx Plus 或 OpenResty 支持更好,原生 Nginx 可以用这种方法)
split_clients "${remote_addr}${msec}" $variant {
10% v2_backend;
* v1_backend;
}
server {
listen 80;
server_name user.example.com;
location / {
# 方案 A:直接利用 upstream 的 weight (最简单,但无法精确控制特定用户)
# proxy_pass http://user_service_v2; # 如果只配 v2 就是全量,配两个 upstream 不好做动态切换
# 方案 B:利用变量动态选择 upstream (需要 Nginx 支持)
# 这里展示一个基于 Cookie 的更精准方案
set $target_group "v1";
# 检查是否有灰度 Cookie
if ($http_cookie ~* "gray_scale=user_v2") {
set $target_group "v2";
}
# 如果没有 Cookie,按 10% 概率分配
if ($target_group = "v1") {
set $rand $request_id; # 利用请求 ID 做简单哈希
set_by_lua_block $is_gray {
-- 如果是 OpenResty 环境,可以用 Lua 做更精确的百分比控制
-- 这里简化为:如果请求 ID 最后一位是 0,则去灰度
local id = ngx.var.request_id
if id and string.sub(id, -1) == "0" then
return 1
end
return 0
}
if ($is_gray = 1) {
set $target_group "v2";
}
}
# 根据变量转发
proxy_pass http://user_service_$target_group;
}
}
为什么不用单纯的 weight?因为 weight 是轮询的,无法保证同一个用户在灰度期间一直访问 v2。我们要做的是用户粘性的灰度。所以上面的逻辑是:优先看 Cookie,没有 Cookie 则随机(10%概率)打上灰度标记并重定向设置 Cookie,或者直接在 Nginx 内部转发。
基于 Cookie 的精准灰度
为了让测试同学能稳定访问新版本,我加了一个 Cookie 判断的逻辑。
location /api/user {
# 定义后端变量
set $backend "http://user_service_v1";
# 检查 Cookie 中的 gray_flag
if ($http_cookie ~* "gray_flag=test_v2") {
set $backend "http://user_service_v2";
}
# 也可以支持通过 Header 传递,方便自动化测试
if ($http_x_gray_flag = "test_v2") {
set $backend "http://user_service_v2";
}
proxy_pass $backend;
# 记录日志,方便排查灰度流量
access_log /var/log/nginx/user_access.log combined if=$http_cookie;
}
这样,测试同学只要访问一次 http://user.example.com/set_cookie?gray=v2(这个接口由另一个 location 处理,负责种 Cookie),后续请求就会自动路由到 v2。
Prometheus 可观测性集成
光有灰度不够,还得知道新版本好不好。我们在 Nginx 1.26.2 上集成了 nginx-vts-exporter (虽然 Nginx 原生也在加强对 Prometheus 的支持,但第三方模块目前更成熟)。
由于我用的不是 Nginx Plus,所以我编译了 nginx-module-vts 模块。配置如下:
http {
# 开启 vts 监控
vhost_traffic_status_zone;
# 监控状态页,只允许内网访问
server {
listen 8080;
server_name localhost;
location /status {
vhost_traffic_status_display;
vhost_traffic_status_display_format json;
allow 10.0.0.0/8;
deny all;
}
}
# 业务配置
upstream user_service_v1 { ... }
upstream user_service_v2 { ... }
server {
listen 80;
server_name user.example.com;
location /api {
# 记录 upstream 响应时间,用于 Prometheus 计算 P99
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
# 关键:记录 upstream 地址和响应时间到日志,方便后续分析
access_log /var/log/nginx/user_access.log '{ "remote_addr": "$remote_addr", "upstream": "$upstream_addr", "status": "$status", "request_time": "$request_time", "upstream_response_time": "$upstream_response_time" }';
proxy_pass http://user_service_v1; # 实际逻辑会动态切换
}
}
}
配合 Prometheus 的 nginx-vts-exporter,我可以在 Grafana 里看到 v1 和 v2 的 QPS、错误率(5xx 占比)、响应时间 P99 的对比图。有一次灰度发布,我看到 v2 的 P99 从 50ms 飙升到了 300ms,而错误率没变。排查发现是新版本引入了一个慢 SQL。如果没有这个监控,我可能就全量发布了,后果不堪设想。
6. 踩坑手记:线上502与499错误排查实录及被动健康检查机制的应用
做运维最怕凌晨三点报警电话。有一次,我们的支付回调接口突然大量 502 Bad Gateway,紧接着是 499 状态码。当时我睡眼惺忪打开电脑,看着监控曲线直线下跌,冷汗都下来了。
502 与 499 的恩怨情仇
先说结论:499 通常是 502 的前兆。
499 是 Nginx 自定义的状态码,意思是 Client Closed Request。也就是客户端等不及了,主动断开了连接。为什么客户端会断开?因为后端太慢了。
当时的情况是:支付服务(后端)在进行数据库大表迁移,导致部分查询卡住,响应时间从 50ms 变成了 10 秒以上。Nginx 这边设置的 proxy_read_timeout 是 5 秒,所以 Nginx 还没等到后端响应,客户端(比如 App 端)的超时时间(通常 3-5 秒)先到了,客户端断开连接,Nginx 就记录了 499。
紧接着,因为后端卡住,连接池耗尽,新的请求进来,Nginx 连不上后端,或者后端直接拒绝连接,就报了 502。
排查过程
- 看 Nginx 日志:
tail -f /var/log/nginx/error.log。看到大量 upstream timed out (110: Connection timed out) while reading response header from upstream。
- 看后端监控:发现支付服务的 JVM 线程数飙升,数据库 CPU 100%。
- 看 Nginx 状态:
netstat -an | grep :8080 | grep TIME_WAIT。数量巨大,说明连接关闭频繁。
被动健康检查的应用
当时我们的 upstream 配置非常简单,没有做任何健康检查:
upstream payment_backend {
server 10.0.4.1:8080;
server 10.0.4.2:8080;
}
这意味着,即使 10.0.4.1 已经卡死了,Nginx 还是会继续把请求发过去,直到超时。
我立刻修改了配置,加上了被动健康检查参数(Nginx 开源版只有被动检查,商业版或第三方模块才有主动检查):
upstream payment_backend {
server 10.0.4.1:8080 max_fails=3 fail_timeout=10s;
server 10.0.4.2:8080 max_fails=3 fail_timeout=10s;
# 关键:开启 keepalive 减少建连开销,但配合健康检查使用
keepalive 16;
}
server {
location /pay/callback {
proxy_pass http://payment_backend;
# 调整超时时间,防止客户端过早断开
proxy_connect_timeout 3s;
proxy_read_timeout 5s; # 这个必须和客户端超时对齐或略长
# 增加失败重试机制(注意:非幂等请求如 POST 慎用)
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_next_upstream_timeout 6s;
}
}
参数解释:
* max_fails=3 fail_timeout=10s:在 10 秒内,如果某个 server 失败了 3 次(比如超时、连接拒绝),Nginx 就会把这台 server 标记为不可用,并在接下来的 10 秒内不再给它转发请求。
* proxy_next_upstream:这是救命稻草。当请求遇到错误、超时或特定的状态码(502等)时,Nginx 会尝试把请求转发给下一个 upstream 服务器。
为什么这么做?因为支付回调是幂等的(支付平台会重试),所以我可以放心开启 proxy_next_upstream。修改后,即使一台机器卡死,流量会迅速转移到另一台,499 和 502 的比例瞬间下降了 80%。
另一个坑:长连接与被动健康的冲突
后来我发现一个问题:加上 keepalive 后,被动健康检查似乎失效了。有时候后端服务重启,Nginx 还是会把请求发到那个已经关闭的连接上,导致报错。
排查发现,是因为 keepalive 连接池里的连接可能已经失效了,但 Nginx 不知道。解决办法是调整 keepalive_timeout 和 keepalive_requests,让连接不要太老:
upstream payment_backend {
server 10.0.4.1:8080 max_fails=3 fail_timeout=10s;
server 10.0.4.2:8080 max_fails=3 fail_timeout=10s;
keepalive 16;
keepalive_timeout 30s; # 缩短空闲超时,避免持有失效连接
keepalive_requests 500; # 处理 500 个请求后主动关闭,重新建立连接
}
同时,在 location 里加上:
proxy_http_version 1.1;
proxy_set_header Connection "";
这样,Nginx 会定期回收旧连接,保证了健康检查的有效性。这次事故让我明白,负载均衡不是配个 IP 就完事了,超时、重试、健康检查这三板斧必须一起上,才能扛住真实的线上波动。
站长实战手记
一次让我加班到凌晨三点的网关迁移
去年我接了个挺棘手的需求,给一个做在线教育的老项目做架构升级。业务场景很明确:原来的单机 Nginx 做反向代理,后面挂了三个 Tomcat,一到晚上八点高峰期,老师一开直播,学生一涌入,页面就卡得不行,甚至直接报 502。
当时我接手后,第一反应就是上 负载均衡。我给 Upstream 里配了加权轮询,把几台新买的云主机加进去,心想这下总该稳了吧?结果上线不到半小时,后台就炸了。很多学生反馈说登录状态丢失,刚进直播间就被踢出来。
我盯着日志看了半天,才反应过来是 Session 粘滞 的问题。因为原来的代码把 Session 存在单机内存里,我这边一负载均衡,请求被轮询到了不同的后端,Session 自然就丢了。
那晚我紧急回滚,然后改方案。我没有去折腾那堆老旧的 Java 代码,而是直接在 Nginx 里启用了 ip_hash。虽然这违背了当时流行的“无状态化”理论,但对于那个急迫的夜晚来说,这是成本最低、见效最快的方案。改完配置 reload 的一瞬间,看着监控里报错数断崖式下跌,那种后背发凉的感觉才慢慢退去。
我的真实取舍看法
关于 Nginx 和 Envoy 怎么选,我的看法很实在:
* 别盲目追新。如果你的业务还没到那种需要 Service Mesh 支撑的复杂微服务规模,真的没必要上 Envoy。Nginx 的文档和社区资源是巨大的隐形资产,出问题了随便一搜就能找到答案,而 Envoy 的排错曲线太陡峭。
* 健康检查要开。很多人只看 upstream 配置,忽略了 max_fails 和 fail_timeout。我那次 502 事件后,专门配置了被动健康检查,只要某台机器连续出错就直接摘掉,不用等人工干预。
给读者的真心话
配置 Nginx 最忌讳照抄网上的“完美模板”。每台机器的内存、CPU 和业务模型都不一样。 哪怕是一个 proxy_buffer_size,别人设 4k 可能刚好,你设 4k 可能就爆了。
建议大家在测试环境多压测几次,别像我当年那样,为了图快把隐患带到线上。技术是用来解决问题的,不是用来炫技的。