文件上传需通过哈希路由确保一致性,优先用文件名+元数据哈希(如sha256)取模定位节点;分块上传支持断点续传;禁用粘性会话与实时同步;健康检查须穿透至真实存储io。

上传前先做分片哈希路由
文件上传到负载均衡后,不能随机扔给后端存储节点,否则同一个文件多次上传可能落到不同机器,导致读取不一致或重复存储。核心是让相同文件名 + 相同内容的文件,始终映射到同一台后端服务器。
推荐用 MD5 或 SHA256 对文件路径 + 文件名(不含时间戳等动态部分)做哈希,再对节点数取模:
var hash = BitConverter.ToString(SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes($"{fileName}"))).Replace("-", "");
var nodeIndex = Math.Abs(hash.GetHashCode()) % backendNodes.Length;
注意:别用 DateTime.Now、Guid.NewGuid() 作为路由依据——它们每次都不一样,彻底破坏一致性。
- 如果文件名含用户ID,可直接用
userId.GetHashCode() % nodeCount,更快且足够均匀 - 避免用文件内容全文哈希(大文件耗时),优先用元数据哈希 + 内容指纹(如前4KB + 后4KB)组合
- 哈希结果必须用
Math.Abs(...)包裹,否则GetHashCode()可能返回负数,取模后索引越界
上传过程必须支持断点续传和重试
负载均衡下网络更不可靠,单次上传失败率升高。直接 HttpClient.PostAsync() 丢整个文件流,失败就得重头来,浪费带宽又拖慢体验。
实际做法是把文件切块(如 4MB/块),每块单独 POST,并带上 Content-Range 和唯一 uploadId:
POST /api/upload/chunk?uploadId=abc123&chunkIndex=2&totalChunks=5
后端按 uploadId 聚合所有块,最后合并。这样某块失败只重传那一块。
- 前端需记录已成功上传的
chunkIndex,页面刷新后也能续传 -
uploadId建议用文件哈希生成(如Convert.ToBase64String(MD5.HashData(...))),避免服务重启后丢失上下文 - 别在负载均衡层开启「粘性会话(sticky session)」——它解决不了跨请求的块聚合问题,还掩盖了真正的并发缺陷
后端存储节点间不做实时同步
有人想用 Redis 或数据库记“文件在哪台”,再让其他节点去拉取。这引入额外延迟和单点依赖,违背负载均衡初衷。
正确思路是:上传时决定归属节点,后续所有读请求(包括 CDN 回源)都按同样哈希规则路由过去。存储节点只管自己那部分,互不通信。
- CDN 配置回源 Host 时,不能写死某台 IP,得用 DNS 轮询或基于哈希的智能路由中间件(如 Nginx 的
hash $request_uri consistent;) - 删除操作也必须走同样哈希逻辑,否则删错机器
- 扩容节点时,哈希取模会变,必须用一致性哈希(如
Ketama算法)或做数据迁移,不能直接改% nodeCount
健康检查必须穿透到存储接口
负载均衡器默认只 ping 服务器端口或 HTTP 状态码,但后端存储服务可能进程活着、磁盘已满、或数据库连接池耗尽——这时它还在收上传请求,却注定失败。
健康检查 URL 必须真实调用一次存储写入(如写入并删除一个临时小文件),返回 200 才认为节点可用:
GET /health/storage?testWrite=true
否则,流量持续打向一个「假活」节点,错误日志里全是 IOException: No space left on device 或 TimeoutException,但负载均衡器一直显示「正常」。
- 检查频率建议 5–10 秒,超时设为 2 秒以内,避免拖慢整体探测
- 别把健康检查响应写死成
return Ok();,它骗不过真实 IO 压力 - 若使用 Kubernetes,
LivenessProbe和ReadinessProbe必须指向该真实存储健康端点,而非通用 HTTP 端口
哈希路由的种子值、分块大小、健康检查的 IO 深度——这三个地方稍一松动,整套分发逻辑就从「可控」滑向「玄学」。尤其上线前没压测过磁盘 IO 瓶颈的节点,最容易在凌晨三点开始 500 错误连发。










