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 32Connection "",每次请求后端都要经历 TCP 握手 -> 请求 -> 关闭。在 1 万 QPS 下,TCP 短连接的开销会让后端服务的 CPU 飙升 30% 以上。

深度配置:Upstream模块详解与加权轮询、IP_Hash算法的高并发压测对比

在网关层,负载均衡算法选不对,比没有负载均衡还糟糕。我曾在一次大促中,因为算法选错,导致后端 3 台服务器,有 2 台闲得发慌,1 台却直接被打挂。

加权轮询(Weighted Round Robin)的陷阱

我们当时的订单服务有三台机器,配置分别是 8 核、4 核、4 核。我一开始想当然地配了 weight=2weight=1weight=1。理论上流量应该是 2:1:1。

但在实际压测中(使用 wrk 模拟 5000 并发),我发现 8 核那台机器的 CPU 瞬间跑满,另外两台却只用了 40%。为什么? 因为 Nginx 的加权轮询是“请求级”的,而不是“负载级”的。如果某个请求处理特别慢(比如某个复杂的报表查询),它就会卡在那台高权重的机器上,导致后续请求继续堆积。

后来我调整了策略,对于计算密集型服务,我不再盲目加权重,而是配合 max_failsfail_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 进行了压测。

我的取舍: 如果后端是无状态的 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_versionproxy_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。

排查过程

被动健康检查的应用

当时我们的 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_timeoutkeepalive_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_failsfail_timeout。我那次 502 事件后,专门配置了被动健康检查,只要某台机器连续出错就直接摘掉,不用等人工干预。

给读者的真心话

配置 Nginx 最忌讳照抄网上的“完美模板”。每台机器的内存、CPU 和业务模型都不一样。 哪怕是一个 proxy_buffer_size,别人设 4k 可能刚好,你设 4k 可能就爆了。

建议大家在测试环境多压测几次,别像我当年那样,为了图快把隐患带到线上。技术是用来解决问题的,不是用来炫技的。