文件存储服务的架构演进——从本地存储到 OSS 再到分布式文件系统

发布时间:2026/7/24 16:45:25
文件存储服务的架构演进——从本地存储到 OSS 再到分布式文件系统 文件存储服务的架构演进——从本地存储到 OSS 再到分布式文件系统一、文件存储选型的阶段决策不能用一种方案覆盖所有阶段文件存储是后端系统的基础设施但它的架构选择高度依赖业务阶段和规模。很多团队一开始就会纠结要不要上 MinIO或要不要自建分布式文件系统其实更合理的做法是分阶段演进每个阶段选择最匹配当前规模的方案。在线教育行业的产品团队文件存储经历了三个阶段第一阶段用户量 1万使用本地磁盘 NFS 挂载简单可靠运维成本低。第二阶段用户量 1~10万迁移到阿里云 OSS解决了存储扩容、CDN 加速和跨地域访问的问题。第三阶段用户量 10万引入 MinIO 集群作为热数据层OSS 作为冷数据归档层形成两层缓存架构。这个演进路径不是刻意设计的而是被业务压力推出来的。第一阶段的问题不是技术选型错误而是你没到那个规模之前不需要那个复杂度。二、三层架构接入层、处理层与存储层的解耦接入层负责上传鉴权、流量控制和文件大小校验。上传前需要进行几项校验文件类型白名单不是靠扩展名判断而是校验 MIME Type 和文件头魔数、文件大小限制客户端和服务端双重校验、用户上传频率限制防止恶意刷上传。处理层负责文件的异步处理包括图片裁剪压缩、视频转码截图、文档转 PDF 预览。所有处理都是异步的通过消息队列解耦——上传服务只负责接收文件并返回文件 ID处理结果通过回调或 WebSocket 通知客户端。存储层的设计需要区分热数据和冷数据。热数据是用户频繁访问的文件如近期课程视频、头像存放在 MinIO 集群上提供低延迟读取。冷数据是历史文件或归档数据迁移到 OSS降低存储成本。冷热切换通过生命周期策略自动执行例如文件上传 30 天后自动转为冷数据。三、文件上传的分片与断点续传实现大文件上传如几百 MB 的视频是文件存储服务中需要特别处理的场景。直接上传一个完整文件的风险是网络中断导致从头开始用户体验极差。分片上传的实现逻辑是客户端先将文件分片每片 5MB逐片上传服务端记录每片的上传状态。全部上传完成后客户端发起合并请求服务端将所有分片合并成完整文件。Service public class MultipartUploadService { private final FileStorage storage; private final RedisTemplateString, UploadProgress redisTemplate; public InitUploadResult initUpload(String fileName, long totalSize, String userId) { if (totalSize MAX_FILE_SIZE) { throw new FileUploadException(文件大小超过限制最大支持: (MAX_FILE_SIZE / 1024 / 1024) MB); } String uploadId UUID.randomUUID().toString(); UploadProgress progress UploadProgress.init(uploadId, fileName, totalSize, userId); redisTemplate.opsForValue().set(upload: uploadId, progress, Duration.ofHours(24)); return new InitUploadResult(uploadId, CHUNK_SIZE); } public ChunkUploadResult uploadChunk(String uploadId, int chunkIndex, byte[] chunkData, String chunkMd5) { UploadProgress progress redisTemplate.opsForValue() .get(upload: uploadId); if (progress null) { throw new FileUploadException(上传任务不存在或已过期: uploadId); } // 校验分片 MD5防止传输错误 String actualMd5 DigestUtils.md5Hex(chunkData); if (!actualMd5.equals(chunkMd5)) { throw new FileUploadException(分片数据校验失败分片索引: chunkIndex); } try { String chunkKey uploadId /chunk_ chunkIndex; storage.saveChunk(chunkKey, chunkData); progress.markChunkComplete(chunkIndex); redisTemplate.opsForValue().set(upload: uploadId, progress, Duration.ofHours(24)); return new ChunkUploadResult(progress.getCompletedChunks(), progress.getTotalChunks()); } catch (Exception e) { // 分片保存失败标记为可重试 throw new FileUploadException(分片上传失败请重试, chunkIndex chunkIndex, e); } } public FileInfo completeUpload(String uploadId) { UploadProgress progress redisTemplate.opsForValue() .get(upload: uploadId); if (progress null) { throw new FileUploadException(上传任务不存在或已过期: uploadId); } if (!progress.isAllChunksComplete()) { throw new FileUploadException(分片未全部上传完成已完成: progress.getCompletedChunks() / progress.getTotalChunks()); } // 合并分片 String fileKey progress.getFileName() _ uploadId; ListString chunkKeys new ArrayList(); for (int i 0; i progress.getTotalChunks(); i) { chunkKeys.add(uploadId /chunk_ i); } try { storage.mergeChunks(fileKey, chunkKeys); } catch (Exception e) { throw new FileMergeException(文件合并失败, uploadId uploadId, e); } // 清理分片临时文件 storage.cleanupChunks(uploadId); redisTemplate.delete(upload: uploadId); return new FileInfo(fileKey, progress.getTotalSize(), progress.getFileName()); } }四、访问控制与安全防护文件服务的安全防护需要在多个层面落地。首先是访问鉴权所有文件都通过签名 URL 访问签名包含过期时间和操作权限读/写避免裸暴露文件地址。签发签名 URL 时需校验用户身份非法的请求直接拒绝。其次是防盗链。通过 Referer 白名单和签名 URL 双重防护避免文件被其他网站直接引用消耗流量。对于需要内嵌的场景如企业内网使用 IP 白名单作为补充。然后是内容安全。用户上传的文件需要经过内容审核包括敏感内容识别和病毒扫描。通常通过集成第三方安全服务实现异步审核审核不通过的文件标记为违规并限制访问。五、成本优化的两个策略文件存储的成本主要来自两部分存储成本和流量成本。存储成本的优化通过冷热分层实现上文已提到。流量成本的优化有两个方向使用 CDN 加速降低回源流量。热门文件的访问请求由 CDN 节点响应只有回源时才消耗对象存储的外网流量流量成本可降低 60% 以上。对高频访问的小文件如用户头像、缩略图使用 Nginx 本地缓存或 Redis 缓存减少对存储层的直接读取。缓存策略是写入时主动更新Cache-Aside而不是被动等到读时再缓存Read-Through避免缓存击穿问题。