Tiana随笔

Nginx 反向代理和负载均衡入门

· Nginx

标签:#Nginx #部署

Nginx 反向代理和负载均衡入门

第一次配反代,接口通了但后台死活登不上,session 每次都丢,原因是 Set-Cookie 在代理过程中被改了。这篇从那次开始,讲 upstream、健康检查、负载策略、请求头和几个常调的超时参数。

我给博客上 HTTPS 之前,先做了一步基础反代。当时后端 uvicorn 跑在 127.0.0.1:8000,Nginx 监听 80 端口,把 /api/ 转给 uvicorn。看上去一切正常,文章列表接口 200。

但我后台登录功能死活登不上。密码输入、点击、接口收到、Redis 里存好 session;然后下一次请求,session 不见了。我打开 Chrome Devtools 看 Set-Cookie,发现有这条:

Set-Cookie: tiana_admin_session=...; HttpOnly; SameSite=Lax

没毛病啊。但浏览器下一次请求没带上。问题在后端返回的 Cookie 之前,被 Nginx 转译了什么东西。

我在 nginx.conf 翻来翻去看了半天,原来 proxy_pass http://127.0.0.1:8000; 后面没加斜杠。意味着 location /api/ 被匹配后,原样转给后端的 /api/。我以为转成根目录 /,实际没有。后端收到的是 /api/blog/posts/list,路径对,但 uvicorn 起在 8000 上面,前面还有一层 proxy_set_header 处理过的 Host 跟实际域名不一致,导致某些 cookie 校验逻辑写错了。

行,最后通过改 proxy_set_header Host $host;,由 Nginx 把真实域名传给后端就好了。这事让我从这以后任何反代配置都开始按"最小可工作集"一步步搭,不一次到位配置太多。

反向代理的最简骨架

我现在的反代模板长这样:

server {
    listen 80;
    server_name blog.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name blog.example.com;

    ssl_certificate     /etc/nginx/ssl/blog.fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/blog.key;

    location /api/ {
        proxy_pass http://127.0.0.1:8000;
        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-Forwarded-Proto $scheme;
        proxy_read_timeout 60s;
    }
}

这就是最小可工作集:把 /api/ 透传到后端 + 三个标准的代理请求头。其它的缓存、超时、限流都是后续增量。

proxy_pass 末尾的斜杠非常关键。http://127.0.0.1:8000(无斜杠)会原样把 /api/blog/posts/list 转给后端;http://127.0.0.1:8000/(有斜杠)会把 /api/ 部分替换为空,剩下 /blog/posts/list。

上游多节点:upstream

我这站点初期一台服务器够用,但承接之后我可能会拆前后端或上多台。Nginx 的 upstream 块就是用来做这事的。

upstream backend_cluster {
    server 10.0.0.11:8000 weight=3;
    server 10.0.0.12:8000 weight=2;
    server 10.0.0.13:8000 weight=1;
    keepalive 32;
}

server {
    listen 80;

    location /api/ {
        proxy_pass http://backend_cluster;
        proxy_set_header Host $host;
    }
}

策略说明:

  • weight=3 表示这一台承接 3/6 的请求,权重越高越常被打。机器性能不同就调 weight
  • 默认策略是 round robin(轮询)
  • ip_hash 让同一客户端落到同一后端,避免缓存命中率抖动
  • least_conn 把请求给连接数最少的机器
  • keepalive 32 跟上游保持 32 个空闲连接,避免反复建连

健康检查

默认 upstream 是「nginx 看着端口在就视为可用」,实际服务挂了 Nginx 还是会发给那台。加 health_check:

upstream backend_cluster {
    server 10.0.0.11:8000;
    server 10.0.0.12:8000;
}

server {
    location /api/ {
        proxy_pass http://backend_cluster;
        health_check interval=5s fails=2 passes=2 uri=/healthz;
    }
}

/healthz 这个路径要后端提供一个不依赖鉴权的轻量接口,返回 200 就是健康的。interval=5s 每 5 秒检查一次,连续失败 2 次摘掉,连续 2 次成功加回。商业版 Nginx 才支持 health_check,开源版要用 ngx_http_upstream_check_module,或者自己写脚本。

便宜的替代:让后端应用自己注册主动健康上报,存到一个 Lua 共享字典,Nginx 拿来判断。开源社区方案活跃,不要死磕一条路。

实际负载策略

我用 ip_hash 比较多。原因是博客后端做了 JWT 缓存,同一用户复访时打同一台机器,命中率更高。但 ip_hash 的副作用是 NAT 后面的用户都打到同一台机器,所以大流量场景用 least_conn 更稳。

我博客这种小流量无所谓,选 ip_hash 主要是简化缓存设计。

真实的请求头处理

上面的 proxy_set_header 是最小集。生产环境往往还需要:

proxy_set_header X-Request-ID $request_id;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port $server_port;

X-Request-ID 用 $request_id 是 Nginx 自带的请求唯一 ID,后端日志里接住它,整个调用链就能串起来。我博客的 access_logs 里就拿这个 ID 索引入行查错。

X-Forwarded-* 给依赖原始域名信息的应用用。比如某些后端按 Host 重定向,如果没拿到原始 Host,重定向 URL 就会指向 127.0.0.1:8000。

超时与缓冲的几个常调参数

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

proxy_read_timeout 是「等到后端响应的时长」,我这站详情接口冷启动偶发 5 秒,60 秒足够。proxy_buffering 媒体流/SSE 场景建议关掉。我当时踩过一次流式接口 nginx 把响应一次吐完,排查就是 buffering 在背后偷偷攒。

我现在的反代心智模型

我每次新增一个反代配置,先把骨架完整读三遍:

  • listen:端口对不对、TLS 证书对不对
  • server_name:域名对不对
  • location:路径匹配规则、URI 替换
  • proxy_set_header:原始 Host/IP/Proto 是否传到了
  • proxy_pass:末尾斜杠对路径的影响
  • upstream:多节点时是否漏了 health_check

然后 nginx -t,然后 nginx -s reload。之后 curl -I 看返回头、查 access_log 看请求路径是否符合预期。

反代配置踩的坑都能 curl -v 看出来,永远是几行 grep 的事。难的不是修,而是知道往哪个方向看。

相关文章