Administrator
发布于 2022-12-26 / 5583 阅读
112

Nginx 在 Java 架构中的常见用法与调优

我们为什么离不开 Nginx

后端是 Spring Boot 起的 8080 服务,但直接把 8080 暴露在公网既不安全也难扩展。一来没有 TLS 卸载,二来单实例挂了没容灾,三来限流得在每个应用里写。Nginx 在我们架构里身兼数职:反向代理、负载均衡、限流、长连接优化。它配一次能管一片服务,是 Java 后端前面那道刚需的闸门。

反向代理与负载均衡

两个实例 upstream,默认轮询:

upstream order_backend {
    server 10.0.1.11:8080 weight=2;
    server 10.0.1.12:8080;
}
server {
    location /api/ {
        proxy_pass http://order_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;
    }
}

weight=2 让性能好的那台多扛一倍流量。我们最早没配 X-Real-IP,后端拿到的全是 Nginx 的 IP,日志里没法定位真实用户,排障吃了亏才补上。

策略选择

  • 轮询:默认,适合实例同构;
  • ip_hash:按客户端 IP 粘会话,但扩容时会重新分布,已登录用户可能被踢;
  • least_conn:挑连接数最少的,对长请求更友好,我们订单域最终用它;
  • hash $request_uri:按 URL 粘,适合有本地缓存的场景。

限流配置

用 limit_req 挡刷子流量,保护后端。令牌桶算法:

limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
location /api/ {
    limit_req zone=api burst=40 nodelay;
    proxy_pass http://order_backend;
}

rate=20r/s 是稳态速率,burst=40 允许突发排队,nodelay 表示突发不延迟直接放。我们曾遇到爬虫把下单接口打满,加上这层后后端 CPU 从 95% 掉回 40%,正常用户不再被挤。limit_conn 还能限制单 IP 并发连接数,防慢连接耗尽 worker。

长连接调优

反向代理默认每次都新建到后端的连接,高并发下 TIME_WAIT 爆炸,端口耗尽会导致"偶发连接失败"。开启 keepalive:

upstream order_backend {
    server 10.0.1.11:8080;
    keepalive 32;
}
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 3s;
proxy_read_timeout 30s;

调完后到后端的连接数从峰值 1500 降到稳定 60,后端 CPU 掉了一截,TIME_WAIT 从几千降到两位数。keepalive 32 意思是每个 worker 最多复用 32 个到后端的空闲连接。

静态资源与 gzip

前端构建产物我们直接放 Nginx,不走后端,再开 gzip 省带宽:

gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1024;

首页 JS 从 380KB 压到 110KB,首屏快了不少。这部分交给 Nginx 比交给 Spring Boot 省事,后端专注业务。

健康检查与优雅下线

K8s 下线 Pod 时如果直接砍,在途请求会 502。我们在 Nginx 侧配合:K8s 先摘流量(endpoint 从负载均衡移除),N8s 发 SIGTERM 给应用,应用处理完在途请求再退出。Nginx 这边用被动健康检查,发现后端连不上就摘掉,避免把请求转给已下线的实例:

proxy_next_upstream error timeout http_502;
upstream order_backend {
    server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
}

max_fails=3 表示 30 秒内连挂 3 次就摘 30 秒,给应用重启留出时间。这层健康检查让我们滚动发布时用户几乎无感。

灰度发布怎么用 Nginx 做

新版本上线我们常先放 10% 流量灰度。Nginx 用 weight 做简单灰度:

upstream order_backend {
    server 10.0.1.11:8080 weight=9;  # 旧
    server 10.0.1.12:8080 weight=1;  # 新
}

但 weight 是按连接比例,不是按用户,同一用户可能新旧反复跳。要按用户粘灰度得用 hash(如按 userId 取模)或上更细的网关。Nginx 这层只适合"无状态、可随机"的粗灰度,精细灰度交给上层网关。我们把它定位成"兜底分流",不指望它做复杂路由。

常见配置错误

列几个我们踩过的 Nginx 配置错:一是 proxy_pass 后面带不带斜杠,带斜杠会把 location 匹配的路径段去掉再转发,path 错乱;二是忘了 proxy_set_header Host,后端拿到的是 upstream 名而非真实域名,生成绝对链接时出错;三是 keepalive 配了但没设 proxy_http_version 1.1 和 Connection "",keepalive 不生效,连接照样每次新建。这几个错单独看小,叠加起来就是"为什么加了 Nginx 反而慢"。我们把它写成 Nginx 配置评审清单,上线前逐项核对。

nginx 不是银弹

最后提醒:Nginx 解决的是接入层问题,业务限流、鉴权该在应用层做的还是得做,别全推给 Nginx。我们见过有人把复杂业务限流规则写进 Nginx if,又难维护又易错。Nginx 管"流量分发、TLS、基础限流、静态资源"四件它擅长的事,业务逻辑、细粒度权限留给 Spring Boot。职责边界清晰,两边都不累。

写在后面

现在回头看,《Nginx 在 Java 架构中的常见用法与调优》本身不算多难,难的是线上真出问题那十分钟里的判断。经验都是这么来的。

参考