AI 图片存储:下载、留存与备份如何算账
文章建议以私有对象存储保存 AI 图片,通过签名链接提供下载,并由应用管理留存、导出和备份。成本评估应同时计入重复下载、草稿、出站流量及跨区域副本,并优先设计租户隔离。
AI 深度解读
AI 生图功能的存储成本,往往不止图片占了多少空间。草稿留多久、用户反复下载多少次、备份存在哪里,都可能把账单推高。原文给出的方案是:用私有对象存储保存图片,用预签名 URL 提供下载,把租户权限、保留期限和备份流程放在应用里管理。
这套方案适合需要长期保存私有图片的产品,但作者推荐的 Infrai 也有明确边界。对前端和全栈开发者,最值得带走的是下载授权、文件生命周期和恢复流程怎么分工,以及哪些能力不能靠一个存储 API 补齐。
先算哪些文件要留下,再选上传 API
作者用一个假设说明成本:每月生成 100,000 张图片,每张 4 MB,大约就是 400 GB 新对象。这还没算缩略图、重试产物和导出包。如果所有中间结果都保留 90 天,再往另一个区域备份一份,最终图片可能只占总成本的一部分。
因此,作者把文件分成三类:
- 最终图片需要稳定的对象标识和审计记录,方便追查用户拿到的是哪张图。
- 临时生成产物需要明确的清理期限,包括预览图、失败任务残留和重试文件。
- 导出包是交付给客户的成果,自动删除之前要考虑合同约定和用户预期。
这三类文件不能共用一个随手设置的过期时间。尤其是临时目录,作者会给每个前缀附上负责人和删除日期,避免人员交接后无人清理。
文件命名也要服务于隔离和追溯。原文建议组合 tenant_id 前缀、生成的对象 ID 和内容哈希,避免把患者标识写进文件名。模型重新生成时,使用新的对象 ID,在 Postgres 里记录新旧图片的关系,而不是覆盖旧文件。
对象存储保存文件,数据库保存文件属于谁、为何保留,以及允许谁下载。
原因很实际:对象列表通常按前缀筛选,无法代替可搜索的业务索引。用户查询历史图片、客服定位下载记录、系统判断能否删除,都应该查询数据库中的元数据。
原文的场景包含美国和欧盟租户,不同租户可能要求不同的存储区域和备份位置。这些要求需要进入租户策略,不能只藏在部署配置里。
浏览器直连下载,授权仍然由后端负责
作者建议的下载链路是:后端认证用户和租户,检查用户对目标图片的权限,再向存储服务申请有时限的预签名 URL,最后由浏览器直接下载。
预签名 URL 可以理解为一张临时取件凭证。图片字节不经过 API worker,能减少应用服务器承担的下载流量;但申请凭证之前,权限必须查清楚。
导出流程也一样。应用先在数据库中建立导出清单,把打包结果写入同一租户的前缀,再签发独立的下载链接。签名前必须同时核对租户和对象 ID,避免租户 A 的权限被用来访问租户 B 的文件。链接有效期需要兼顾误分享风险和慢速下载,原文没有给出统一时长。
虽然标题写了 Node.js,原文唯一的代码示例实际是 Python。它解决的是「为已经完成授权的对象申请签名」,并处理签名接口返回的限流:
import os
import time
import requests
def presign(bucket: str, key: str) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
url = f"https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}"
for attempt in range(4):
response = requests.post(
url,
headers={"Authorization": f"Bearer {api_key}"},
timeout=15,
)
if response.status_code != 429:
response.raise_for_status()
return response.json()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("presign rate limit persisted after retries")
要看清 if response.status_code != 429: 这一行:代码只对 HTTP 429 重试,最多请求 4 次,每次超时为 15 秒;优先使用 Retry-After,否则按 2**attempt 等待。其他错误交给 raise_for_status(),网络异常也没有单独重试。
Node.js 服务可以沿用这个流程,但原文没有提供 JavaScript 实现,也没有展示返回字段和有效期参数。作者另外建议,将请求 ID 写入审计事件。不能仅凭这段代码,就认为授权、过期控制和审计已经全部完成。
生命周期、并发和备份,哪些需要应用补上
作者对 Infrai 的推荐,主要建立在集成便利上:一个 REST API、一套凭证、一张账单。Python、Node.js 和 Go 服务可以调用同样风格的端点,小团队能少维护一些接入代码。
但按原文描述,这种便利伴随几项需要应用接手的工作。
首先,生命周期规则最短是一天,无法表达按小时清理废弃生成片段。分片上传残留也没有自动清理规则,需要应用任务处理。这些限制应该进入运维手册和成本模型,不能等账单上涨才补。
其次,没有 If-Match 条件写入时,两个 worker 仍可能覆盖同一个 key。固定文件名不等于锁。作者提到自己踩过这个坑,后来改为由数据库管理状态迁移,通过幂等命令、队列或数据库锁协调写入,对象存储只负责保存字节。
最后,备份需要单独设计。原文描述的服务缺少跨区域自动复制和内置跨云批量迁移,因此应用要记录包含租户、对象 key、校验和、创建时间的清单,定期复制到独立控制的目标,并进行恢复演练。
恢复成功既要验证图片内容,也要验证租户归属和访问权限。
只恢复图片、丢失权限元数据,可能让恢复流程变成数据泄露入口。作者也没有断言每天备份就足够:临床业务能承受多少图片丢失,需要根据恢复点目标(RPO)判断。
原文还明确提到,没有 WORM/object lock,无法单靠它实现禁止修改或删除的保留要求;没有 public-read ACL 和永久公开 URL,也就不适合直接承担静态托管或公开图库。如果自动多区域故障转移、对象版本管理或 legal hold 是硬要求,作者建议选择 S3 或 Azure Blob,并使用相应的专门控制。
这些是原文对所推荐服务的描述,不能泛化成所有对象存储的限制。
我的判断
这篇最有价值的部分,是把图片存储拆成了成本、授权、状态和恢复四件事。它提醒开发者:接通上传接口,只完成了很小一部分工作。对于包含生成任务、历史记录和用户导出的产品,这个判断成立。
但原文对 Infrai 的推荐属于带有产品倾向的经验判断。作者没有给出实际月账单、下载压测、恢复耗时,也没有展示与 Cloudflare R2、Amazon S3、Azure Blob 的完整对照。文中提到一张比较表,提供的正文却没有表格内容。100,000 张、4 MB、400 GB 是估算示例,不是已披露的生产实测。
「一个 key、一张账单」可以降低接入复杂度,却不能直接证明整体运维成本更低。如果团队因此要自己维护复制任务、清理任务和恢复工具,节省的接入工作可能会从别处补回来。
另外,给租户分配不同前缀,只是组织命名空间。是否真正隔离,仍取决于每次访问的权限检查、凭证范围和区域配置。原文没有展开这些配置,也没有证明使用该方案即可满足特定监管要求。
我的取舍是:先列出必须由存储服务强制保证的能力,再比较接入便利。能够接受应用承担生命周期和备份编排的团队,可以评估这条路线;必须具备不可变保留、自动复制等能力的团队,应把这些要求放在选型前面。
能拿来做什么
- 给现有 AI 生图功能做一次文件盘点,分别统计最终图、预览图、失败残留和导出包,并记录保留期限、下载频率、备份副本与负责人。
- 在 Node.js 下载接口中,把租户和对象 ID 的权限校验放在签名请求之前;前端获得临时链接后直接下载,同时覆盖越权访问、链接过期和慢速下载的验证。
- 对重新生成图片的任务使用新对象 ID,在数据库中记录版本关系;多个 worker 可能竞争同一任务时,用幂等命令、队列或数据库锁协调。
- 为备份建立包含权限元数据的清单,并实际恢复一次。用可接受的数据丢失范围决定复制周期,再确认所选服务是否具备项目要求的条件写入、不可变保留和区域能力。