一次 413 排查实录:改了 nginx 配置为什么没生效?
记录一次”上传 1.7M 文件被 413 拒绝”的完整排查过程。 从”以为改自己项目的 nginx.conf 就行”,到最终定位到平台共用的网关 nginx。 包含用到的理论知识、实际操作命令、以及每一步背后的原理。
0. 结论速览(TL;DR)
| 项目 | 内容 |
|---|---|
| 现象 | 上传 1.7M 文件,接口返回 413 Request Entity Too Large |
| 第一反应 | 在项目仓库 nginx.conf 里加 client_max_body_size 20m; → 重新发版后无任何效果 |
| 真正原因 | 请求链路里真正生效的那个 nginx 不是项目镜像里的 nginx,而是平台共用的网关 compute-maas-nginx;它的配置里没有 client_max_body_size,走 nginx 默认值 1m |
| 定位关键 | ① 响应体是 nginx 默认错误页;② 容器里 nginx -T 只列出 /etc/nginx/nginx.conf 一个文件;③ 日志字段 server: _、client: 10.244.x.x |
| 修复动作 | 找到该网关 nginx 的配置来源(ConfigMap),加 client_max_body_size 20m; → 重启工作负载 |
本文记录两个环境的两次事故:
- 案例一(第 1–7 节):配置所在的那台 nginx 根本不是链路上生效的 nginx
- 案例二(第 8 节):配置加对了 nginx,却加进了不会被命中的
server块
1. 背景与现象
- 项目:算力调度管理平台前端(Vue + Vite 构建,产物由 nginx 托管)
- 部署方式:GitLab CI 构建镜像 → 推到 SWR → K8s(Rancher 管理)
- 现象:在管理后台的”模型服务”页面编辑时上传文件(1.7M),浏览器控制台报 413
- 接口:
POST /server-gateway/server/common/upload(相对路径,前端baseURL: "/server-gateway/") - 自己的第一反应:前端是 nginx 托管的,那就改自己仓库里的
nginx.conf
项目里原本的配置(nginx.conf,被 Dockerfile 拷到 conf.d/default.conf):
server { listen 8000; listen [::]:8000; server_name _;
# 上传文件大小限制,默认 1m 会导致 413 client_max_body_size 20m;
root /usr/share/nginx/html; index index.html;
location / { try_files $uri $uri/ /index.html; }
location /admin/ { rewrite ^/admin/(.*)$ /$1 break; try_files $uri $uri/ /index.html; }}改完、构建、发版、再传 → 还是 413。这就是本文要回答的”为什么”。
2. 必备理论知识
2.1 一次上传请求在 K8s 里要经过几层
浏览器 │ ①DNS / 域名 ▼APISIX 网关(API 网关,OpenResty/nginx + Lua) │ ②按路由转发 ▼平台网关 nginx(compute-maas-nginx,一个 Pod 代理多个前端 + 多个后端网关) │ ③按 location 前缀转发 ├──► /admin/** → compute-maas-admin-frontend:8000 (前端静态服务 nginx) ├──► /console/** → compute-maas-console-frontend:8000 (另一个前端) ├──► /server-gateway/** → dispatch-server-gateway-svc:8001 (后端网关) └──► /client-gateway/** → dispatch-client-gateway-svc:8002 (后端网关) │ ▼ 后端微服务(Spring)关键认知:链路上有多个 nginx,每一个都有自己的 client_max_body_size。
只要其中任何一层是默认 1m,1.7M 的请求就会在那里被拒绝。
“我改了 nginx 配置”必须回答一个问题:我改的,是不是真正在链路上生效的那个 nginx?
2.2 每一层可能出现的大小限制
| 层级 | 指令 / 配置 | 默认值 | 说明 |
|---|---|---|---|
| nginx | client_max_body_size | 1m | 可用位置:http、server、location,向下继承;值为 0 表示不检查 |
| Ingress(ingress-nginx) | 注解 nginx.ingress.kubernetes.io/proxy-body-size | 1m | 只对该 Ingress 自己声明的 host + path 生成的 location 生效 |
| APISIX | nginx_config.http.client_max_body_size | 0(不限制) | APISIX 默认不像 nginx 那样限制 body |
| APISIX | 插件 client-control 的 max_body_size | 未启用 | 路由级动态调整 body 上限 |
| 后端 Spring | spring.servlet.multipart.max-file-size / max-request-size | 1MB / 10MB | 表单上传场景 |
| 前端组件 | maxSize(本项目 BasicUpload) | 2(MB) | 浏览器侧先拦,判定是 >=,正好 2M 也会被拦 |
client_max_body_size 是 nginx 的内置固定指令(属于 ngx_http_core_module):
- 名称固定,不能改名,也不支持变量(不能做成运行时可变)
- 值可以写字节数,也可以带单位
k/K、m/M、g/G(大小写都认) - 写在
server块里,该 server 下所有location都继承;写在http里全局生效
2.3 涉及的 K8s 概念(对照本次排查)
| 概念 | 白话解释 | 本次出现的地方 |
|---|---|---|
| Namespace | 项目分区 | 项目命名空间 / ingress-nginx 等 |
| Deployment | “用哪个镜像、几副本、开哪个端口”的说明书 | compute-maas-nginx |
| Pod | 说明书跑出来的实例(容器) | compute-maas-nginx-866dddd44d-d8lgw |
| Service | 给 Pod 一个稳定地址 | compute-maas-admin-frontend、dispatch-server-gateway-svc |
| Ingress | 集群大门 + 路由表(域名/路径 → Service),注解就是开关 | 找到的那条 maas-test-3.portal.ljyun.cn |
| ConfigMap | 外挂配置文件,挂进 Pod 当文件用 | 网关 nginx 的 nginx.conf、前端 /config.js |
| 重启(rollout restart) | 重建 Pod | ConfigMap 改完必须重启才生效 |
2.4 为什么”改了仓库里的配置却不生效”
这是本次最容易误判的一点,链条如下:
- 项目
Dockerfile把仓库的nginx.conf拷到镜像的/etc/nginx/conf.d/default.conf - 官方 nginx 镜像的
/etc/nginx/nginx.conf里本来有include /etc/nginx/conf.d/*.conf;,所以 conf.d 会被加载 - 但线上那个 Pod 的
/etc/nginx/nginx.conf是被整份替换的(来自 ConfigMap 挂载),内容里是这台网关自己的 upstream 列表 - 而这份被替换的
nginx.conf没有include /etc/nginx/conf.d/*.conf; - 于是
conf.d/default.conf(也就是你改的那份)从头到尾没有被加载——它是一份”死配置”
验证手段:nginx -T 会把它实际加载的每一份配置文件都打印出来,并带 # configuration file <路径>: 头。如果输出里只有 /etc/nginx/nginx.conf,就说明 conf.d 没被 include。
nginx -T 2>&1 | grep -n -E "client_max_body_size|server_name|include|# configuration file"2.5 怎么判断”413 到底是谁发的”
排查时不要靠猜,有四个可观测的线索:
线索一:响应头 Server
Server 的值 | 含义 |
|---|---|
nginx/1.21.3 等带版本号 | 多为应用镜像里的 nginx(本项目镜像基于 nginx:1.21.3-alpine) |
nginx(无版本) | 配置里有 server_tokens off; |
APISIX / APISIX/x.y.z | APISIX 网关 |
⚠️ 坑点:APISIX 会改写所有响应的 Server 头(包括来自上游的响应),所以看到 Server: APISIX 不等于 413 是 APISIX 发的。
线索二:错误页的签名
- OpenResty(APISIX 的底座)生成的默认错误页签名是
<hr><center>openresty</center> - 普通 nginx 生成的是
<hr><center>nginx</center>
本次页面签名是 nginx → 说明 413 是一个普通 nginx 发出的,APISIX 只是转发并盖了自己的 Server 头。
线索三:nginx 错误日志的字段
2026/09/23 08:33:24 [error] 32#32: *9 client intended to send too large body: 1834823 bytes,client: 10.244.0.1,server: _,request: "POST /client-gateway/client/common/upload HTTP/1.1",host: "cm.wuxi.dev.ljyun.cn"client intended to send too large body: N bytes→ 请求体超过本机client_max_body_size的直接证据client:→ 上一跳的地址。如果是10.244.x.x(集群 Pod 网段),说明这台 nginx 前面还有一跳,它不是最外层入口server:→ 处理该请求的 server 标识。本次是_,与该 nginx 配置里的server_name _;对应(兜底 server)host:→ 请求头里的域名
线索四:容器里的实际配置
nginx -T # 实际加载了哪些文件、值是多少cat /proc/mounts | grep -i nginx # 这些文件是不是挂载进来的ls -l /etc/nginx/ /etc/nginx/conf.d/ # 文件时间戳、是否软链hostname # 当前 Pod 名如果多个文件(如 nginx.conf、config.js、conf.d/)的时间戳完全一致,基本可以判定它们是 ConfigMap 挂载进来的。
3. 排查过程(实际操作时间线)
Step 1:确认失败的是接口请求,不是后端业务报错
F12 → Network → 找到失败请求:
- 状态码
413,响应体是 HTML(nginx 默认错误页),不是业务 JSON - 结论:不是后端 Spring 的 multipart 报错,而是某个 nginx 拦的
Step 2:先找 Ingress,结果”配了也没用”
在 Rancher 里找到入口 Ingress,注解里已经有:
metadata: annotations: nginx.ingress.kubernetes.io/proxy-body-size: 200M nginx.ingress.kubernetes.io/proxy-buffering: 'off' nginx.ingress.kubernetes.io/use-regex: 'true'spec: ingressClassName: nginx rules: - host: maas-test-3.portal.ljyun.cn http: paths: - path: / pathType: Prefix backend: service: name: compute-maas-svc port: number: 80看起来”已经配了 200M”,但上传依旧 413。注意域名:实际访问的域名与这里的 host 不一致时,请求会落到入口的兜底 server,注解根本作用不到。
Step 3:抓 nginx 错误日志,锁定”是哪台 nginx”
在 Rancher:Workloads → Pods →(命名空间)→ 某个 nginx Pod → ⋮ → View Logs,复现一次上传后搜 too large。
kubectl -n <ns> logs <nginx-pod> | grep -i "too large"得到的日志(关键字段见 2.5):
[error] 32#32: *9 client intended to send too large body: 1834823 bytes,client: 10.244.0.1, server: _, request: "POST /client-gateway/client/common/upload HTTP/1.1",host: "cm.wuxi.dev.ljyun.cn"Step 4:进 Pod,看它真正加载了什么配置
Rancher:Pod 详情 → ⋮ → Execute Shell(Kuboard 是 终端)。
# 我一开始把关键字写错了(client_max_size),所以什么都没查到——正确写法是 client_max_body_sizenginx -T 2>&1 | grep -n -E "client_max_body_size|server_name|include|# configuration file"
cat /etc/nginx/nginx.conf | head -60ls -l /etc/nginx/ /etc/nginx/conf.d/输出(节选):
3:# configuration file /etc/nginx/nginx.conf:47: server_name _;
worker_processes 1;user root;
events { worker_connections 1024; }
http { upstream website { server compute-maas-website:3000; } upstream maas-client { server maas-client-frontend:4173; } upstream compute-client { server compute-maas-console-frontend:8000; } upstream maas-agent { server maas-chat-backend:8000; } upstream compute-maas-admin { server compute-maas-admin-frontend:8000; } upstream client-auth-gateway { server dispatch-client-gateway-svc:8002; } upstream server-auth-gateway { server dispatch-server-gateway-svc:8001; } upstream compute-maas-certificate { server compute-maas-certificate-frontend:8000; }
server { listen 80; listen [::]:80; server_name _; server_tokens off; ... }}
/etc/nginx/:-rw-r--r-- nginx.conf 2242 Sep 23 03:18-rw-r--r-- config.js 152 Sep 23 03:18drwxr-xr-x conf.d Sep 23 03:18三个结论当场得出:
- 这个 Pod 是 平台网关 nginx(一个 nginx 代理了 admin / console / website / certificate 等前端,以及两个后端网关),
hostname为compute-maas-nginx-866dddd44d-d8lgw - 它的
nginx.conf里没有client_max_body_size→ 走默认 1m → 1.8MB 必然 413 nginx -T只列出了/etc/nginx/nginx.conf,没有 conf.d → 项目仓库里那份配置是死配置,改它永远没用
同时,server_name _; + server_tokens off; 与日志中的 server: _、错误页里不带版本号的 nginx 完全对应——413 就是它发的。
Step 5:确定配置来源,并修改
nginx.conf、config.js、conf.d/ 时间戳一致 → 判定为挂载(ConfigMap)。
确认来源:
- Rancher:
工作负载→compute-maas-nginx→ Pod 模板里的Volumes→ 看挂了哪个 ConfigMap - 或 Pod 内:
cat /proc/mounts | grep -i nginx - 认领 ConfigMap:在
配置 → ConfigMaps里找内容包含upstream compute-maas-admin的那一份
修改(两种粒度,选其一):
# 方案 A:只放开两个上传接口(影响面最小,推荐)location /server-gateway/ { client_max_body_size 20m; # 新增 proxy_pass http://server-auth-gateway/;}
location /client-gateway/ { client_max_body_size 20m; # 新增 proxy_pass http://client-auth-gateway/;}# 方案 B:整台网关放开(省事,但影响所有走它的站点)http { client_max_body_size 20m; # 新增 ...}改完 必须重启(ConfigMap 改动不会自动 reload nginx):
Rancher:工作负载 → compute-maas-nginx → ⋮ → Restart4. 根因分析
| # | 事实 | 影响 |
|---|---|---|
| 1 | 平台网关 nginx compute-maas-nginx 的配置里没有 client_max_body_size | 走默认 1m,1.7M 上传被 413 |
| 2 | 该 Pod 的 /etc/nginx/nginx.conf 被 ConfigMap 整份替换,且没有 include conf.d/*.conf | 镜像里 conf.d/default.conf 是死配置,改项目仓库无效 |
| 3 | 找到的 Ingress 注解 proxy-body-size: 200M 属于另一个入口/域名 | 对实际链路不起作用 |
| 4 | 前端 BasicUpload 组件默认 maxSize = 2(MB) | 即使 nginx 放开,超过 2M 也会被浏览器侧拦下 |
5. 验证方法
- 浏览器重传原文件(1.7M)→ 应成功
- 再传一个 10M 左右的文件 → 验证确实到了 20m 的上限
- 看该 nginx Pod 日志,确认不再出现新的
client intended to send too large body - 命令行复现(可选):
curl -i -F "file=@/path/to/10m.bin" "https://<域名>/server-gateway/<上传接口路径>"6. 可复用的排查 SOP
遇到”上传/请求体过大被 413”,按顺序做:
① 浏览器 F12 看响应 - 状态码 413 + nginx 默认错误页 → 是 nginx 拦的 - 记录:请求 URL、host、响应头 Server 值、错误页签名(nginx / openresty)
② 抓日志,锁定是哪台 nginx - 复现一次上传,只看"时间戳最新"的那行 - 搜关键字:too large - 看字段:client(上一跳)、server、host
③ 进那个 Pod 看真实配置 - nginx -T 2>&1 | grep -n -E "client_max_body_size|include|# configuration file" - cat /proc/mounts | grep -i nginx # 配置是挂载的? - ls -l /etc/nginx/ /etc/nginx/conf.d/ # 死配置排查 - hostname # 记录 Pod 名
④ 找到配置来源并修改 - 挂载(ConfigMap)→ 改 ConfigMap - 镜像内 → 改代码仓库 + 重新构建发版 - 改完重启工作负载
⑤ 验证 - 重传 + 看日志 + 顺带确认后端 multipart 限制和前端 maxSize命令清单(可直接复制)
# 全集群找入口路由kubectl get ingress -Akubectl get ingress -A -o yaml | grep -n -A20 "server-gateway"
# 找日志kubectl -n <ns> logs <pod> | grep -i "too large"
# 进容器看配置nginx -T 2>&1 | grep -n -E "client_max_body_size|server_name|include|# configuration file"cat /etc/nginx/nginx.confcat /proc/mounts | grep -i nginxls -l /etc/nginx/ /etc/nginx/conf.d/hostname
# 改完重启kubectl -n <ns> rollout restart deploy/compute-maas-nginxWindows PowerShell 下上面管道后的 grep 要换成 Select-String。
7. 踩坑与注意事项
- 别把
client_max_size当关键字:正确指令是client_max_body_size(中间有body),写错会什么都查不到 Server: APISIX不代表 APISIX 拦的:APISIX 会改写所有响应的Server头;判断要看错误页签名(openrestyvsnginx)- APISIX 默认不限请求体大小:
nginx_config.http.client_max_body_size默认值为0(不检查),除非显式配置或用client-control插件 - 共享网关要谨慎:
compute-maas-nginx是多个站点共用的入口,改配置会影响所有走它的流量;优先用location粒度,改前备份 ConfigMap 原文,并知会运维/平台 - ConfigMap 改了不会自动生效:必须重启工作负载(除非有 reloader)
- 镜像没换 tag 等于没发版:Deployment 未设置
imagePullPolicy时,tag 不变不会重新拉取 - 前端也有上限:本项目
BasicUpload默认maxSize = 2,且判断是>=;图片上传另有 5M 限制 - 后端也要放开:网关放开后,Spring 的
spring.servlet.multipart.max-file-size/max-request-size需同步确认 - 同一个 nginx 里多个
server块互不影响:改了 A 块,B 块的请求照样按 B 块的配置走(详见案例二) - 用
subPath挂载的 ConfigMap 不会热更新:改了也不会同步进正在运行的容器,必须重建 Pod
8. 案例二:wuxi-dev 环境 —— 配置加对了 nginx,却加错了 server 块
8.1 现象
换到第二个环境(cm.wuxi.dev.ljyun.cn)后,console 应用上传依旧 413:
2026/09/23 08:33:24 [error] 32#32: *9 client intended to send too large body: 1834823 bytes,client: 10.244.0.1, server: _, request: "POST /client-gateway/client/common/upload HTTP/1.1",host: "cm.wuxi.dev.ljyun.cn"链路骨架与案例一相同(浏览器 → APISIX → 平台网关 nginx → 后端网关),区别是:
这次的网关是 wuxi-dev 环境那一套(所有 upstream 都带 .wuxi-dev 后缀),而且它的配置里已经有 client_max_body_size 20m; 了,却依然 413。
8.2 理论:nginx 如何选择 server 块
一个 nginx 可以有多个 server 块(虚拟主机),请求进来时的匹配规则是:
- 先按
listen的端口/IP 分组 - 再按请求头里的
Host去匹配各server块的server_name(精确匹配 > 通配符 > 正则) - 都不匹配时,交给该端口的 default server —— 即显式标了
default_server的那个,没标的话就是第一个server块
两个推论:
- 各
server块之间的配置互不继承。你在 A 块(server_name token.ljyun.cn)里写client_max_body_size 20m;,对落在 B 块(server_name _)的请求完全无效。 - 指令只在其所在层级及其子层级继承:
http全局 >server该虚拟主机 >location该路径。
同时这也解释了案例一里日志的 server: _ 字段——错误日志中的 server: 反映的是该请求匹配到的 server,此处即配置里的 server_name _;。
8.3 根因:20m 加在了另一个 server 块
该网关的 /etc/nginx/nginx.conf(4383 字节)里有两个 server 块:
| 行号 | server 块 | 说明 |
|---|---|---|
| 51 | server_name _;(第 28 行起的第一个 server 块) | 兜底 server,cm.wuxi.dev.ljyun.cn 的请求落在这里 |
| 118 | server_name token.ljyun.cn; | 只有用 token.ljyun.cn 访问时才命中 |
http { upstream website { server compute-maas-website.wuxi-dev:3000; } upstream compute-client { server compute-maas-console-frontend.wuxi-dev:8000; } upstream compute-maas-admin { server compute-maas-admin-frontend.wuxi-dev:8000; } upstream client-auth-gateway { server dispatch-client-gateway-svc.wuxi-dev:8002; } upstream server-auth-gateway { server dispatch-server-gateway-svc.wuxi-dev:8001; } ...
# ===== 第 1 个 server:兜底,cm.wuxi.dev.ljyun.cn 走这里 ===== server { listen 80; server_name _; server_tokens off;
location /console/ { proxy_pass http://compute-client; } location /admin/ { proxy_pass http://compute-maas-admin; }
location /client-gateway/ { proxy_pass http://client-auth-gateway/; # ← 没有 client_max_body_size }
location /server-gateway/ { proxy_pass http://server-auth-gateway/; # ← 没有 client_max_body_size } }
# ===== 第 2 个 server:只有 token.ljyun.cn 会命中 ===== server { listen 80; server_name token.ljyun.cn;
location /client-gateway/ { proxy_pass http://client-auth-gateway/; client_max_body_size 20m; # ← 第 167 行,加在这里 }
location /server-gateway/ { proxy_pass http://server-auth-gateway/; client_max_body_size 20m; # ← 第 173 行,加在这里 } }}nginx -T | grep -n 的输出直接印证了这一点——两处 client_max_body_size 20m; 分别在 167 行和 173 行,都落在 server_name token.ljyun.cn 那个块(118 行起)里:
3:# configuration file /etc/nginx/nginx.conf:51: server_name _;118: server_name token.ljyun.cn;167: client_max_body_size 20m;173: client_max_body_size 20m;而我们访问的是 cm.wuxi.dev.ljyun.cn → 落到 server_name _ 兜底块 → 那里的两个 location 没有这条指令 → 默认 1m → 1.8MB 被拒。
8.4 如何一眼判断”配置加对了位置没有”
| 判据 | 本次结果 | 含义 |
|---|---|---|
日志里的 server: 字段 | _ | 请求走的是兜底 server 块 |
nginx -T 里 server_name 与 client_max_body_size 的行号区间 | 20m 在 167/173 行,属于 118 行起的 token.ljyun.cn 块 | 指令加在了不会命中的块里 |
| 你的访问域名 | cm.wuxi.dev.ljyun.cn | 与 token.ljyun.cn 不匹配 → 命中 _ |
/proc/mounts | /dev/sda3 /etc/nginx/nginx.conf ext4 ro | 该文件是节点文件系统上的只读 bind 挂载(subPath 挂载的 ConfigMap),不会热更新,改完必须重建 Pod |
8.5 修复
把 client_max_body_size 20m; 加到真正命中的那个块里——即在 server_name _; 的 server 块的两个 location 内补上:
server { listen 80; listen [::]:80; server_name _; server_tokens off;
location /client-gateway/ { proxy_pass http://client-auth-gateway/; client_max_body_size 20m; # 新增 }
location /server-gateway/ { proxy_pass http://server-auth-gateway/; client_max_body_size 20m; # 新增 } }更省事且更保险的写法是直接写在 http { 里一行(两个 server 块一并覆盖,但等于放开该网关上的所有站点):
http { client_max_body_size 20m; # 新增:http 级,所有 server 生效 ...}改完必须重建 Pod(工作负载 → compute-maas-nginx → ⋮ → Restart),因为:
- nginx 不会自动 reload 配置文件
- 该配置文件是
subPath挂载的 ConfigMap,subPath 挂载不会热更新,只有新 Pod 才会挂到新内容
8.6 本案例的三条教训
- 改共享网关前,先确认”你访问的 Host 会命中哪个 server 块”:把
server_name列表和实际域名对一遍,不要看到配置里有 20m 就以为生效了 - 存在多个 server 块时,改动要成对做:本次配置里两个块有几乎重复的 location,只改一个块等于没改
subPath挂载的 ConfigMap 改完必须重建 Pod:只改 ConfigMap 不重启,容器里读到的还是老文件
9. 参考资料
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!














