一个批量导入接口在 500 行时正常,2000 行时报 413。运维以为是网关限流,查了半天配置——实际是 nginx 的 client_max_body_size 默认 1MB,而 2000 行的 JSON 正好 1.2MB。而前端拿到 413 时显示的是"网络错误",于是用户以为是网络问题,反复重试。
请求体大小是链路上每一层都有自己的上限,而它们的默认值往往很小、报错信息往往很不友好。而这个"形式"决定了排查难度:同样是"请求太大",有的返回 413、有的返回 400、有的直接 502。而它们的日志都指向不同的组件,于是排查往往从错的地方开始。
这类问题的麻烦不在"限制存在",而在"限制发生在哪一层、以什么形式失败"。
1. 链路上的五道限制
| 层 | 典型限制 |
|---|---|
| 浏览器/客户端 | 无硬限(但内存与体验有限) |
| 网关(nginx/ALB) | 默认 1MB 或 8MB |
| 应用框架 | 上传限制、body 缓存 |
| 反序列化 | 深度、元素数 |
| 数据库 | 单条记录/批量语句大小 |
第二项最常见:nginx 的 client_max_body_size 默认 1MB第一项(浏览器)的"无硬限"其实是个假象:它没有协议限制,但内存与用户体验有限。一个 200MB 的表单会让浏览器卡死,而报错是"页面无响应"。
,而它返回的是 413——一个很多人不认识的码。
第四项更隐蔽:有些 JSON 解析器对"元素数量"或"嵌套深度"有上限,超了抛的是解析错误,看起来像"数据格式不对"。
2. 失败形态的四种表现
| 层 | 表现 |
|---|---|
| 网关 | 413(可能被 CDN 改写) |
| 应用 | 400 / 500(取决于异常处理) |
| 反序列化 | 400 但消息是"格式错误" |
| 数据库 | 500 + SQL 错误 |
第三项最误导:用户看到"JSON 格式错误"时的第一反应是"我的数据格式不对",于是去改数据——而实际是体积问题。这类错配让用户的问题描述也变得不可信。
第一项的"被 CDN 改写"很常见:某些 CDN 会把 413 改写成 502 或通用错误页,于是"限体积"看起来像"服务挂了"。
第三项最误导:用户看到"JSON 格式错误",会去检查自己的数据格式,而实际是"太大了"。
3. 三种应对策略
| 策略 | 适用 |
|---|---|
| 提高上限 | 确有大批量需求 |
| 前端预校验 | 用户体验最好 |
| 分片上传 | 体量不可控 |
| 异步任务(返回任务 ID) | 处理时间长 |
第二项最便宜也最有效:在前端算"行数/字节数",超过阈值就提示,而不是等服务器返回 413。
第四项是"大批量"的正解:把"提交"变成"创建任务",用户拿任务 ID 轮询进度。第四项还有一个"重试"上的优势:异步任务可以按批重试,而同步接口失败只能整体重来。对"部分成功"的批量操作,这是决定性的。
这样请求体小、超时问题消失、失败也能重试单批。
// 三种策略的取舍
上限 5MB + 前端预校验 → 覆盖 95% 场景,成本最低
分片上传(1MB/片) → 体量不可控时用,需要后端拼装
异步任务 → 处理超过 30 秒时用,最稳但改动大
4. 上限该定多少
| 依据 | 说明 |
|---|---|
| 业务最大值 | 真实批量大小 |
| 内存预算 | 并发 × 单请求内存 |
| 反序列化成本 | 大 JSON 的 CPU |
| 超时预算 | 大请求的处理时间 |
第二项是关键约束:100 并发 × 5MB 请求体 = 500MB 内存,而这个数往往被忽略。所以上限不能只看"业务能不能用",还要看"并发时会不会 OOM"。
第二项还有一个"放大"因素:请求体在网关、框架、反序列化过程中各有一份副本(或至少一份缓冲),所以"5MB 请求"可能对应 15MB 内存占用。这也是"上限要比业务值略小"的原因。
第一项要先量:统计真实请求体的 P99 与最大值。凭感觉定"5MB"可能比真实需求大十倍,也可能小一半。
// 三层上限的配置(要一致,否则报错难查)
nginx:
client_max_body_size: 8m
spring:
servlet.multipart.max-request-size: 8MB # 若为 multipart
server.max-http-request-header-size: 16KB # 注意这是 header 不是 body
app:
bulk.max_rows: 5000 # 业务层限制(更友好)
bulk.max_bytes: 8_000_000
还有一种"看不见"的上限:请求头也有大小限制(nginx large_client_header_buffers 默认 8KB,某些环境 4KB)。而"把 token 放进 header"的做法会在大权限 JWT 上撞到它,报错是 400 或 431。
// 头的上限与典型触发场景
cookie 超大:会话里塞了太多状态(>4KB)
JWT 超大:权限列表写进 claim(>4KB)
自定义头堆叠:多级代理各加一个(X-Forwarded-*)
// 症状:请求在网关被拒,应用日志里完全看不到这次请求
5. 报错该怎么给
| 情况 | 响应 |
|---|---|
| 超网关上限 | 413 + 提示(可能被 CDN 改写) |
| 超应用上限 | 413/400 + 明确的"最多 N 行/MB" |
| 超数据库上限 | 500 + 内部日志(不对用户暴露细节) |
| 前端预校验 | 本地提示,不发请求 |
第二项的要求是"告诉用户上限是多少、当前是多少"。而"请求过大"这类消息无法指导用户。
第四项的价值在体验:**用户不必等一次往返才知道"太多了"**第二项的"给出上限与当前值"还有个副作用:它让用户能自己算"分几批"。5000 行上限 + 当前 8200 行 → 分两批,一批 4100。比"请分批"更有用。
,而是立刻能改。
// 应用层的体积错误(可指导用户)
{
"code": "PAYLOAD_TOO_LARGE",
"message": "单次最多 5000 行(当前 8200 行),请分批导入",
"traceId": "9f2c1a7e",
"limit": {"rows": 5000, "bytes": 8000000},
"actual": {"rows": 8200, "bytes": 12400000}
}
验证上限是否生效,有一个"三段式"测法:在阈值下、阈值上、以及恰好等于阈值各发一次。第三项最容易被忽略——很多实现的边界是"大于"还是"大于等于"并不一致。
// 三档验证(以 8MB 为例)
head -c 8300000 /dev/zero | base64 | curl -X POST --data-binary @- $URL # 阈值上 → 413
head -c 7900000 /dev/zero | base64 | curl -X POST --data-binary @- $URL # 阈值下 → 200
head -c 8000000 /dev/zero | base64 | curl -X POST --data-binary @- $URL # 恰好 → 看实现
6. 排错:五种"大请求的奇怪失败"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 413 被显示为"网络错误" | 前端未处理 413 | 加错误映射 |
| 413 变成 502 | CDN 改写 | 查 CDN 配置 |
| 报"JSON 格式错误" | 解析器元素上限 | 查解析器限制 |
| 小请求正常、大请求 500 | 反序列化 OOM | 加体积上限 |
| 加了上限后正常但变慢 | 处理时间超时 | 改异步 |
第一项(前端未处理 413)的现象很典型:浏览器控制台里能看到 413,而页面显示"网络错误"。所以前端错误映射应当至少覆盖 400/401/403/404/413/429/5xx 这几类。
第四项最危险:反序列化的内存峰值可能是请求体的 3–5 倍,于是 5MB 的请求能让进程吃掉 20MB 以上。
还有一个"易被忽略的层":应用内的 body 缓存。有些框架会先把请求体完整读进内存再交给业务,而"读完"这个动作本身就有上限(或没有)。于是"网关放行了 8MB,应用在读的时候 OOM 了"——两个限制不在同一层。
// 三层限制的对齐检查(缺一层就会在别处爆)
网关:client_max_body_size 8m
框架:max-request-size 8m(并且要确认是"流式"还是"全缓冲")
业务:max_rows / max_bytes(给出友好错误)
// 若框架是全缓冲,"8m 请求 + 20 并发"就是 160MB 常驻
7. 边界:什么情况不必设上限
| 场景 | 为什么 | 做法 |
|---|---|---|
| 内部 RPC(可信调用方) | 无滥用风险 | 用框架默认 |
| 已有网关统一限制 | 平台负责 | 跟随平台 |
| 单文件上传(专门通道) | 另有配额 | 单独配置 |
| 测试环境 | 无并发 | 不设 |
判断口径一句话:这个接口会不会收到"超出预期的请求体"。最后一条与"配额"有关的边界:上限应当与"租户/客户端"挂钩,而不是全局一个数。内部批处理需要 50MB,而对外接口只能 1MB——用一个全局值,要么卡死内部,要么放行风险。
会(对外开放、批量导入),就必须有上限且要有指导性报错;不会(内部固定调用),框架默认够用。
8. 自测
- 链路上的五道限制分别在哪些层?为什么 nginx 的默认值最常见?
- 四层失败形态里,哪一种最误导用户?为什么用户会去检查数据格式?
- 为什么"提高上限"要同时考虑并发与内存?"100 并发 × 5MB"意味着什么?
- 前端预校验为什么最便宜有效?异步任务解决了哪两个问题?
- 为什么反序列化的内存峰值可能是请求体的 3–5 倍?
- 判断"是否需要设上限"的那个问题是什么?