一次 413 排查实录:改了 nginx 配置为什么没生效?

4338 字
22 分钟
一次 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 每一层可能出现的大小限制#

层级指令 / 配置默认值说明
nginxclient_max_body_size1m可用位置:http、server、location,向下继承;值为 0 表示不检查
Ingress(ingress-nginx)注解 nginx.ingress.kubernetes.io/proxy-body-size1m只对该 Ingress 自己声明的 host + path 生成的 location 生效
APISIXnginx_config.http.client_max_body_size0(不限制)APISIX 默认不像 nginx 那样限制 body
APISIX插件 client-control 的 max_body_size未启用路由级动态调整 body 上限
后端 Springspring.servlet.multipart.max-file-size / max-request-size1MB / 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)重建 PodConfigMap 改完必须重启才生效

2.4 为什么”改了仓库里的配置却不生效”#

这是本次最容易误判的一点,链条如下:

  1. 项目 Dockerfile 把仓库的 nginx.conf 拷到镜像的 /etc/nginx/conf.d/default.conf
  2. 官方 nginx 镜像的 /etc/nginx/nginx.conf 里本来有 include /etc/nginx/conf.d/*.conf;,所以 conf.d 会被加载
  3. 但线上那个 Pod 的 /etc/nginx/nginx.conf 是被整份替换的(来自 ConfigMap 挂载),内容里是这台网关自己的 upstream 列表
  4. 而这份被替换的 nginx.conf 没有 include /etc/nginx/conf.d/*.conf;
  5. 于是 conf.d/default.conf(也就是你改的那份)从头到尾没有被加载——它是一份”死配置”

验证手段:nginx -T 会把它实际加载的每一份配置文件都打印出来,并带 # configuration file <路径>: 头。如果输出里只有 /etc/nginx/nginx.conf,就说明 conf.d 没被 include。

Terminal window
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.zAPISIX 网关

⚠️ 坑点: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: → 请求头里的域名

线索四:容器里的实际配置

Terminal window
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。

Terminal window
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 是 终端)。

Terminal window
# 我一开始把关键字写错了(client_max_size),所以什么都没查到——正确写法是 client_max_body_size
nginx -T 2>&1 | grep -n -E "client_max_body_size|server_name|include|# configuration file"
cat /etc/nginx/nginx.conf | head -60
ls -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:18
drwxr-xr-x conf.d Sep 23 03:18

三个结论当场得出:

  1. 这个 Pod 是 平台网关 nginx(一个 nginx 代理了 admin / console / website / certificate 等前端,以及两个后端网关),hostname 为 compute-maas-nginx-866dddd44d-d8lgw
  2. 它的 nginx.conf 里没有 client_max_body_size → 走默认 1m → 1.8MB 必然 413
  3. 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 → ⋮ → Restart

4. 根因分析#

#事实影响
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. 浏览器重传原文件(1.7M)→ 应成功
  2. 再传一个 10M 左右的文件 → 验证确实到了 20m 的上限
  3. 看该 nginx Pod 日志,确认不再出现新的 client intended to send too large body
  4. 命令行复现(可选):
Terminal window
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

命令清单(可直接复制)

Terminal window
# 全集群找入口路由
kubectl get ingress -A
kubectl 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.conf
cat /proc/mounts | grep -i nginx
ls -l /etc/nginx/ /etc/nginx/conf.d/
hostname
# 改完重启
kubectl -n <ns> rollout restart deploy/compute-maas-nginx

Windows PowerShell 下上面管道后的 grep 要换成 Select-String。


7. 踩坑与注意事项#

  1. 别把 client_max_size 当关键字:正确指令是 client_max_body_size(中间有 body),写错会什么都查不到
  2. Server: APISIX 不代表 APISIX 拦的:APISIX 会改写所有响应的 Server 头;判断要看错误页签名(openresty vs nginx)
  3. APISIX 默认不限请求体大小:nginx_config.http.client_max_body_size 默认值为 0(不检查),除非显式配置或用 client-control 插件
  4. 共享网关要谨慎:compute-maas-nginx 是多个站点共用的入口,改配置会影响所有走它的流量;优先用 location 粒度,改前备份 ConfigMap 原文,并知会运维/平台
  5. ConfigMap 改了不会自动生效:必须重启工作负载(除非有 reloader)
  6. 镜像没换 tag 等于没发版:Deployment 未设置 imagePullPolicy 时,tag 不变不会重新拉取
  7. 前端也有上限:本项目 BasicUpload 默认 maxSize = 2,且判断是 >=;图片上传另有 5M 限制
  8. 后端也要放开:网关放开后,Spring 的 spring.servlet.multipart.max-file-size / max-request-size 需同步确认
  9. 同一个 nginx 里多个 server 块互不影响:改了 A 块,B 块的请求照样按 B 块的配置走(详见案例二)
  10. 用 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 块(虚拟主机),请求进来时的匹配规则是:

  1. 先按 listen 的端口/IP 分组
  2. 再按请求头里的 Host 去匹配各 server 块的 server_name(精确匹配 > 通配符 > 正则)
  3. 都不匹配时,交给该端口的 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 块说明
51server_name _;(第 28 行起的第一个 server 块)兜底 server,cm.wuxi.dev.ljyun.cn 的请求落在这里
118server_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),因为:

  1. nginx 不会自动 reload 配置文件
  2. 该配置文件是 subPath 挂载的 ConfigMap,subPath 挂载不会热更新,只有新 Pod 才会挂到新内容

8.6 本案例的三条教训#

  1. 改共享网关前,先确认”你访问的 Host 会命中哪个 server 块”:把 server_name 列表和实际域名对一遍,不要看到配置里有 20m 就以为生效了
  2. 存在多个 server 块时,改动要成对做:本次配置里两个块有几乎重复的 location,只改一个块等于没改
  3. subPath 挂载的 ConfigMap 改完必须重建 Pod:只改 ConfigMap 不重启,容器里读到的还是老文件

9. 参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
一次 413 排查实录:改了 nginx 配置为什么没生效?
https://eastenwhite.com/posts/413上传限制排查记录/
作者
东方既白
发布于
2026-09-25
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
东方既白
大家好,我是东方既白
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
最新动态
站点统计
文章
14
分类
3
标签
22
总字数
20,428
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Cloudflare Workers
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录