Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 140 additions & 49 deletions docs/blog/golang/architectural/3.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,121 +4,212 @@ hide_title: true
sidebar_position: 3
---

## 1. 背景

# 基于 Go 与 S3 的高效文件上传方案 —— 支持断点续传与秒传
在即时通信系统里,图片、语音、视频、文件、视频封面等内容都属于对象数据。它们和普通文本消息不同,通常体积更大、上传时间更长、受网络波动影响更明显。如果所有文件都经由业务 API 服务中转,服务端会同时承担业务处理和大流量文件搬运,容易影响消息、会话、群组等核心链路的稳定性。

在现代 App 与 Web 应用中,文件上传是常见且关键的功能。为应对传输中可能出现的中断、重复上传等问题,我们设计并实现了一套基于 **Go 服务端 + S3 存储** 的文件上传机制。该方案支持**断点续传** 与**秒传** ,并具备良好的**可扩展性** 和**极简客户端 SDK** ,实现客户端与 S3 直连,**数据流不经 API 服务中转**
OpenIM 的 S3 对象存储能力,就是为了解决这个问题:让业务服务只负责鉴权、签名、元数据和访问控制,让文件数据直接进入对象存储。这样既能提高上传效率,也能降低 API 服务压力

## 2. 核心定位

## ✨ 设计目标
OpenIM 的 S3 方案不是简单地把文件上传到 MinIO,而是一套面向 IM 场景的大对象管理能力。它覆盖上传、下载、访问、续传、去重、清理、迁移等环节。

整体上可以理解为两层:

- ✅ 极简 SDK:客户端无需存储系统适配逻辑
- 服务端负责控制面:鉴权、生成临时凭证、登记对象信息、生成统一访问地址、执行过期清理。
- SDK 负责客户端上传体验:读取文件、计算哈希、分片上传、断点续传、并发控制、进度回调、取消上传。

- ✅ 降低服务器负载:文件流量不经由 API 服务
这种拆分让“业务控制”和“文件流量”分离。服务端不再搬运大文件,客户端拿到授权后直接上传到对象存储。

- ✅ 支持断点续传与秒传,提升用户体验
## 3. 整体上传体验

- ✅ 兼容任意 S3 接口,易于接入多种对象存储
用户发送图片、视频或文件时,SDK 会先在本地分析文件,判断文件大小和类型,并计算文件内容特征。然后 SDK 向服务端申请一次上传授权。服务端校验用户身份和对象归属后,返回临时上传凭证。之后文件内容由客户端直接传到对象存储,不再经过 OpenIM API 服务中转。

- ✅ 可拓展支持非 S3 的 HTTP PUT 分片上传
上传完成后,服务端会登记这份对象和业务对象名之间的关系,并返回一个 OpenIM 层面的访问地址。业务消息里保存的是这个稳定地址,而不是某个存储厂商暴露的原始地址。后续访问时,OpenIM 再根据对象元数据生成临时访问链接并重定向到真实对象。

这个链路的好处是:

- 上传大文件时不会挤占业务 API 带宽。
- 下载地址可以统一由 OpenIM 管理。
- 底层存储后端变化时,业务层地址可以保持稳定。
- 私有桶也可以通过临时授权安全访问。

## 📦 上传流程
## 4. 多存储后端兼容

OpenIM 支持多种 S3 或 S3 兼容对象存储,包括 MinIO、腾讯云 COS、阿里云 OSS、七牛 Kodo、AWS S3 等。业务层不需要感知具体厂商差异,只需要使用 OpenIM 提供的统一对象能力。

上传流程共分为五步
这带来的优势是部署选择更灵活

- 私有化部署可以使用 MinIO。
- 公有云部署可以接入云厂商对象存储。
- 企业后续更换存储厂商时,业务逻辑不需要整体重写。
- 存储访问域名、内外网地址、权限策略可以按部署环境调整。

### 1. 获取分片大小
## 5. 客户端直传

接口:`GET /object/part_limit`
传统文件上传常见做法是客户端先把文件传给业务服务,业务服务再转存到对象存储。这种方式实现简单,但在大文件和高并发场景下成本很高。

客户端首次上传前,请求服务端获取推荐的分片大小(如 5MB),并进行缓存
OpenIM 采用客户端直传:服务端只签发短期授权,文件本体由客户端直接上传到对象存储。这样 API 服务只处理轻量请求,不承担大文件传输

客户端直传的优势包括:

### 2. 计算文件哈希
- 降低 API 服务带宽和连接压力。
- 大文件上传更稳定,不影响消息收发等核心接口。
- 对象存储天然适合承载大对象和高并发下载。
- 文件上传链路更短,整体吞吐能力更好。

客户端将文件按分片大小切分,并计算每个分片的哈希值。然后将所有分片哈希拼接,最终计算出文件整体哈希(`file_hash`),用于秒传判断。
## 6. 分片上传

对于较大的图片、视频和文件,SDK 会使用分片上传。文件被拆成多个小块,每个分片独立上传,最后由对象存储合并成完整对象。

### 3. 初始化上传
分片上传带来的体验提升很明显:

接口:`POST /object/initiate_multipart_upload`
- 大文件不需要一次性完整上传。
- 某个分片失败时,只需要重传失败部分。
- 可以通过受控并发提升上传速度。
- 上传过程更容易展示细粒度进度。

客户端提交文件整体信息(如 hash、文件名、大小):
SDK 会根据文件大小和服务端限制自动选择合适的分片大小,上层业务不需要手动处理这些细节。

## 7. 秒传

- 若服务器发现该文件已存在(通过 `file_hash` 判断),则返回秒传成功的下载地址;
OpenIM 支持基于文件内容的秒传。SDK 会计算文件内容特征,服务端可以判断相同内容是否已经存在。如果对象存储里已经有这份文件,就不再重复上传文件内容,而是直接登记一次新的业务引用并返回访问地址。

- 若不存在,则返回每个分片的上传签名信息,包括
秒传的价值主要体现在重复发送场景

- 同一用户重复发送相同文件时,可以瞬间完成。
- 不同消息引用同一份真实对象,减少重复存储。
- 降低网络传输成本和对象存储写入成本。
- 弱网环境下减少不必要的重传。

- 分片上传 URL
秒传不是按文件名判断,而是按内容特征判断,因此更适合 IM 文件场景。

- 必需的 Header(如 `Content-Type`, `Authorization`)
## 8. 断点续传

- `PartNumber` 等标识
SDK 会在本地记录上传进度。上传过程中,如果网络中断、应用退出、进程重启,已经成功上传的分片不会丢失。用户再次上传同一个文件时,SDK 可以从本地恢复状态,只补传尚未完成的分片。

断点续传能显著改善弱网和移动端体验:

### 4. 上传分片(直传 S3)
- 大文件失败后不用从头开始。
- 切后台、断网、重启后仍能继续。
- 每个已成功的分片都能沉淀为有效进度。
- 用户重试成本更低,失败体验更可控。

这也是 OpenIM S3 方案相比普通表单上传的重要优势之一。

客户端使用返回的签名信息,将每个分片通过 HTTP PUT 直接上传至 S3,对应:
## 9. 并发与内存控制

SDK 支持分片并发上传,但不会无节制地并发。上传时会根据分片大小和内存预算动态调整并发数量,避免为了追求速度导致客户端内存过高。

- 上传过程中记录每一分片的 ETag 与 PartNumber
当分片较小时,SDK 可以并发上传多个分片;当分片较大时,SDK 会降低并发;如果分片过大,还会退回更稳妥的流式上传方式。

- SDK 持久化这些信息,实现断点续传
这种策略在移动端、桌面端、Web/WASM 等环境中都更稳。它兼顾了上传速度、内存占用和设备稳定性。

## 10. 进度反馈与取消上传

SDK 会把上传过程拆成多个可感知阶段,例如打开文件、计算哈希、获得上传会话、上传分片、完成合并等。上层 UI 可以据此展示更准确的进度,而不是只有一个模糊的百分比。

> ⚠️ 文件内容始终不经由服务端,仅签名和元数据走 API
对于大文件来说,这一点很重要。用户可以清楚知道当前是在本地计算、网络上传,还是等待服务端完成合并

SDK 也支持取消上传。业务侧可以为上传任务绑定取消标识,用户点击取消后,SDK 会中止对应上传任务,避免继续消耗网络和设备资源。

### 5. 完成上传
## 11. 统一访问地址

接口:`POST /object/complete_multipart_upload`
OpenIM 上传完成后返回的是 OpenIM 统一访问地址,而不是直接暴露对象存储原始地址。访问文件时,OpenIM 会根据对象元数据生成临时访问链接,再跳转到真实存储地址。

客户端上传所有分片成功后,调用此接口提交分片信息列表。
这样设计有几个好处:

- 消息里保存的文件地址更稳定。
- 底层存储迁移不会直接影响历史消息。
- 私有对象也可以通过临时授权安全访问。
- 可以统一处理文件名、内容类型、图片缩略图等访问策略。

- 服务端校验每个分片哈希及整体 hash
对于图片类对象,访问层还可以支持缩略图、格式转换、宽高裁剪等派生能力,从而提升聊天图片加载效率。

- 校验通过后,发起 S3 的合并操作
## 12. 权限与隔离

- 最终返回文件的访问地址(可跳转到签名 S3 URL)
普通用户上传对象时,对象会归入当前用户自己的命名空间。这样可以避免用户伪造或覆盖其他用户的文件对象。服务端在生成上传授权前会进行身份校验和对象归属校验,确保上传行为受控。

对象存储的长期密钥不会下发给客户端。客户端拿到的只是短期上传或访问凭证,即使链接泄露,风险也会被限制在较短时间和特定对象范围内。

## ⬇️ 下载流程
这种方式兼顾了直传效率和安全边界。

## 13. 元数据治理

客户端下载时,从服务端获取临时签名下载地址(S3 Pre-signed URL),可设置权限与有效期控制。该地址可通过 API 统一跳转实现权限校验
对象存储只负责保存文件内容,但 IM 系统还需要知道这份文件属于哪个用户、哪个业务类型、使用哪个存储后端、什么时候创建、是否可以清理等信息

OpenIM 会为每个业务对象登记元数据。元数据把“业务对象名”和“真实存储对象”分离开来。多个业务对象可以引用同一个真实对象,这也是秒传和去重能够成立的基础。

元数据治理带来的能力包括:

## ✅ 特性一览
- 统一生成访问地址。
- 支持内容去重和秒传。
- 支持按业务类型做生命周期清理。
- 支持统计真实对象是否仍被引用。
- 支持未来迁移底层存储。

| 特性 | 说明 |
| --- | --- |
| 秒传支持 | 基于文件 hash 实现上传秒跳过 |
| 断点续传 | 支持断线、异常退出后继续上传 |
| 极简 SDK | 客户端无需签名逻辑,仅执行 PUT |
| 存储兼容性强 | 支持任意兼容 HTTP PUT 的对象存储 |
| 高可拓展性 | 可快速接入其他上传协议和存储系统 |
## 14. 生命周期清理

IM 系统里的文件对象并不都需要永久保存。比如图片、语音、视频、视频封面等消息对象,可以根据业务策略设置保留时间。

OpenIM 支持按业务分类清理过期对象。清理时不会简单粗暴地删除真实文件,而是先删除过期业务引用,再判断真实对象是否仍被其他业务对象引用。只有没有任何引用时,才会删除对象存储里的真实文件。

## ⚙️ 可扩展性设计
这种引用计数式清理可以避免误删。它和秒传能力天然配套:同一份真实文件可能被多个消息引用,只有所有引用都过期后,真实对象才应该被删除。

## 15. FormData 小程序兼容通道

该方案以“服务端统一生成签名 + 客户端通用 PUT 上传”为核心架构,只要目标存储系统支持分片上传并接受带签名的 PUT 请求,即可无缝接入:
除了完整的 SDK 分片上传链路,OpenIM 还保留了 FormData 表单直传能力。这条链路主要用于小程序 JSSDK,用来适配小程序平台的文件上传能力和调用限制。

需要特别说明的是,Go 语言提供的 SDK 不会调用 FormData。Go SDK 使用的是完整的分片上传链路。

- 支持 Amazon S3、MinIO、阿里云 OSS、腾讯云 COS 等
FormData 是兼容通道,不具备完整上传链路的高级能力。它不支持:

- 也可扩展支持其它上传服务
- 分片上传。
- 秒传。
- 本地断点续传。
- 分片并发。
- 分片级校验和分片级进度回调。

因此,普通 App、桌面端、Go SDK、Web/WASM 端应优先使用完整的 SDK 上传能力;小程序 JSSDK 在平台限制下可以使用 FormData 作为简化直传方案。

客户端代码不变,服务端扩展签名生成逻辑即可。
## 16. 存储迁移能力

由于 OpenIM 在元数据中记录了对象所属的存储后端,系统具备从一个对象存储迁移到另一个对象存储的基础能力。例如从自建 MinIO 迁移到云厂商对象存储,或者在不同云厂商之间切换。

迁移时,系统可以读取旧存储中的真实对象,写入新存储,并更新元数据中的存储后端信息。业务消息里保存的是 OpenIM 统一访问地址,因此迁移后历史消息仍然可以通过原有方式访问。

这降低了长期运维风险,也减少了被单一存储厂商或部署形态绑定的成本。

## 17. 相比传统上传方式的优势

OpenIM S3 方案的优势可以归纳为五类。

性能方面,文件流量直达对象存储,API 服务只处理控制请求,大文件上传不会挤占核心业务接口。

稳定性方面,分片上传、断点续传、分片校验、取消上传和受控并发共同提升了弱网环境下的成功率。

成本方面,秒传和去重减少重复上传与重复存储,生命周期清理减少无效对象长期占用空间。

安全方面,客户端只拿短期授权,不接触对象存储长期密钥;文件访问也可以通过临时链接进行控制。

演进方面,统一对象地址和元数据治理让底层存储可以迁移或替换,业务层不需要感知具体厂商变化。

## 18. 适用场景

OpenIM S3 对象存储适合以下场景:

- 聊天图片、原图、缩略图。
- 语音消息。
- 视频消息和视频封面。
- 普通文件消息。
- ASR 等需要临时对象上传的能力。
- 私有化部署中的文件存储。
- 大文件、弱网、移动端上传场景。

如果业务只需要小程序端简单表单上传,可以使用 FormData 兼容通道;如果需要秒传、分片、断点续传和更完整的上传体验,应使用 SDK 的完整上传能力。

## 19. 总结

OpenIM 的 S3 对象存储能力,是一套面向 IM 大对象场景的文件基础设施。它通过客户端直传降低服务端压力,通过分片和断点续传提升弱网体验,通过秒传和生命周期清理降低成本,通过统一访问地址和元数据治理提升长期可维护性。

它的核心价值,是把文件上传从“业务 API 的沉重负担”变成“可授权、可续传、可去重、可治理、可迁移的对象服务能力”。对于企业级 IM,这种设计能同时提升性能、稳定性、安全性、成本控制和后续演进能力。
Loading
Loading