公众号旧文
一次图床故障背后的系统设计思考
刚试图上传一张 6MB 左右的图片,前端提示:
上传失败:服务器返回的内容不是有效 JSON
第一反应不是图片大小问题,而是怀疑:
- 后端接口是不是挂了?
- 返回格式是不是变了?
- Nginx 反代是不是配置错了?
- 程序是不是异常退出了?
但是经过排查,最终发现:
服务没有挂,程序也没有问题,只是图片大小超过了服务器限制。
这个问题背后,刚好体现了 Web 系统中非常重要的一个概念:
同一个请求,在不同层被拦截,会产生完全不同的错误。
一次上传请求,实际上经过很多层
很多人认为上传图片就是:
浏览器 → 后端
实际上生产环境通常是:
浏览器
|
↓
Nginx
|
↓
应用服务
|
↓
文件存储
每一层都可能限制请求大小。
例如 Nginx:
client_max_body_size 5M;
Go 后端:
http.MaxBytesReader(w, r.Body, MaxUploadSize)
这两个限制看起来都是限制上传大小,但作用完全不同。
第一次失败:为什么不是提示文件太大?
当时 Nginx 设置:
client_max_body_size = 5M
但是上传的是 6MB 图片
请求流程:
浏览器
|
| 上传6MB图片
↓
Nginx
|
| 检查大小超过5MB
↓
直接拒绝
请求没有到后端程序。
Nginx 返回:
413 Request Entity Too Large
但是问题来了,Nginx 默认返回的是 HTML:
<html>
<body>
413 Request Entity Too Large</body>
</html>
而前端代码期待的是:
{
"success": true,
"url": "/uploads/a.png"
}
于是前端尝试解析:
JSON.parse(html)
结果失败。
所以用户看到:服务器返回的内容不是有效 JSON
看起来像 JSON 问题。
实际上真正的问题是图片大小超过限制。
只是错误经过了一层包装。
修改 Nginx 后,为什么错误又变了?
后来把 Nginx 改成:
client_max_body_size 10M;
再次上传。
这次请求通过 Nginx:
浏览器
|
↓
Nginx 10M
|
↓
Go 后端
但是 Go 程序里面还有限制:
MaxUploadSize = 5 << 20
也就是:
5 × 1024 × 1024
= 5MB
于是 Nginx 通过 Go 拒绝,这次返回:
http: request body too large
因为请求已经进入应用层。
错误来源已经变了。
两层限制为什么都需要?
有人可能会想:既然 Go 有限制,那为什么还需要 Nginx?
或者:既然 Nginx 有限制,那为什么应用还要限制?
答案是:两个都需要。
Nginx 限制:保护应用
Nginx 是入口。
它负责:
- 拦截超大请求
- 节省后端资源
- 防止大量垃圾请求进入应用
例如有人上传:
5GB 文件
如果没有 Nginx 限制,请求可能进入:
Go程序
|
读取请求
|
占用内存
|
消耗CPU
|
影响正常用户
应用限制:保护自己
应用不能完全相信前面的代理。
原因是未来可能出现:
- 绕过 Nginx
- 内网访问
- 新增接口
- 配置错误
所以应用自己也必须限制。
例如:
http.MaxBytesReader(...)
就是最后一道保险。
但是,还有一个问题:限制有了,用户体验呢?
这次故障暴露了另一个问题:
很多系统只做了:拒绝错误请求
但是没有做好:告诉用户为什么错误
例如现在:
上传失败:服务器返回内容不是有效 JSON
用户完全不知道发生了什么。
更好的设计应该是:
上传失败:图片大小超过10MB限制
一个成熟的上传系统应该这样设计
第一层:前端提前检查
上传前:
if(file.size > 10 * 1024 * 1024){
alert("图片不能超过10MB");
return;
}
优点:
- 不浪费流量
- 用户立即知道原因
第二层:Nginx 返回统一格式
不要直接返回 HTML。
例如:
{
"error":"文件超过10MB限制"
}
第三层:后端统一错误结构
不要直接返回:
http: request body too large
而应该:
{
"success":false,
"message":"文件大小超过限制"
}
最终用户体验应该是:
用户上传20MB图片
↓
前端检查
↓
提示:
文件不能超过10MB
如果绕过前端:
↓
Nginx拦截
↓
JSON错误
↓
前端展示
如果绕过Nginx:
↓
Go后端拦截
↓
JSON错误
↓
前端展示
这次故障最大的收获
这个问题看似只是图片太大上传失败
实际上涉及:
- HTTP 状态码
- 反向代理
- 应用层保护
- 错误格式设计
- 前后端协议约定
很多线上故障都是类似的问题:
用户看到的是:失败
但是工程师需要继续向下追:
浏览器?
↓
代理?
↓
网关?
↓
应用?
↓
数据库?
↓
存储?
真正优秀的系统,不只是能够拒绝错误请求。
更重要的是当系统拒绝请求时,要告诉用户和开发者,到底为什么拒绝。
这也是生产环境和个人项目之间最大的区别之一。