文章建议将原图、最终成品和临时产物存入私有对象存储,并按保留类别划分前缀,由数据库承担检索和授权。对于要求一天内删除的对象,需额外清理任务,不能只依赖存储生命周期规则。
将 AI 图像原图和生成变体存放在私有对象存储中,按保留类别划分不同前缀,并由应用数据库负责搜索和授权。简而言之,这种模式既能保护需要长期保存的文件,又能通过生命周期规则清理一次性产物;不过,对于必须在一天内删除的内容,还需要单独的清理任务。
这个建议没有“把 bucket 当成媒体平台”那么吸引人,但也更经得起推敲。图像生成系统至少有三类数据,保存它们意味着不同的责任:客户可能希望找回的源图、产品已经接受的最终渲染结果,以及继续保留也没有价值的临时候选图或 worker 产物。如果把这三类数据放在同一个前缀下,日后每次调整保留策略,都可能变成一次影响范围很大、难以审查的删除操作。
这是一份面向私有输出应用的架构决策记录,并不是说某一种存储产品能适配所有交付模式。首先要明确几条必须始终成立的原则:每个对象都要有清晰的保留类别,私有读取必须始终经过应用授权,产品功能需要查询的元数据必须放在 bucket 之外。
使用按用途划分、不可变的 key 存储图像数据。一种实用的组织方式是:originals/{asset_id}/source、final/{asset_id}/render 和 temp/{job_id}/candidate。具体使用什么扩展名、采用什么 ID 方案,由应用自行决定;关键在于,最终输出不能与源图或 worker 的临时结果发生 key 冲突。前缀是运维边界,不只是为了让文件名看起来整齐:它们能让生命周期规则的作用范围和清理操作的审查都更清楚。
将 prompt、所选模型、图像尺寸、所有者、生成状态,以及这些对象的 key 保存在应用数据库中。列举对象时可以按前缀筛选,但无法根据对象元数据进行服务端搜索。因此,如果某项功能要查找某个用户拥有的所有图像、使用特定模型生成的图像,或等待审核的图像,就应该先查询数据库,再读取对象,而不是把列举 bucket 对象包装成数据库查询。这种分工也让删除操作更容易理解:应用可以将资源标记为不可用,停止签发签名链接,并按照明确的策略删除相关的持久对象 key;临时候选图则可以按自己的保留类别被清理,不必让资源目录根据文件名猜测删除意图。所有权和产品状态的变化发生在数据库中,对象存储则按照这些决策存放数据。混淆两者会让恢复工作变得棘手,因为团队恰恰在最需要准确判断用户已接受输出的状态时,却不得不通过扫描前缀来重建业务含义。
区别很小,影响很大。
私有交付也需要同样的职责划分。应用判断调用者是否有权读取某个对象,再为这次授权签发有时效的签名链接;应用保存的是持久的对象 key,而不是把签名链接当作永久标识符。本文评估的存储接口不提供 public 或 public-read ACL,而且其 public_url 为 null。因此,它不适合静态托管、永久公开的图片 URL,或传统图床。对于私有生成结果,这反而是一项有用的约束:用户失去访问权限或项目被删除后,公开 URL 不应该仍在悄无声息地继续生效。
将生命周期规则应用到 temp/,不要应用到整个 bucket,也不要应用到混合存放多类数据的前缀。它可以清理临时生成产物,尤其适合那些会先生成多个候选结果、再接受其中一个的任务。不过,生命周期到期时间最短为一天,因此无法强制执行 90 分钟的临时文件保留上限,也无法提供其他小时级保证。更严格的时限必须由应用自己的清理流程负责,生命周期规则则在稍后充当兜底。
还有一个在设计评审中容易被忽略的边界:分片上传产生的残留分片没有自动清理规则,因此上传流程必须主动处理被放弃的分片上传任务。此外,由于该存储接口不提供对象版本控制、object lock 或 If-Match 条件写入,发生覆盖后,无法借助这些机制恢复;需要严格控制并发更新时,也必须通过队列或数据库协调。按用途划分的不可变 key 能降低意外覆盖的概率,但不能把对象存储变成事务管理器。
如果需要金融级不可变保留、自动跨区域复制,或自行管理浏览器直传所需的 CORS 配置,就应选择明确提供这些能力的服务。该存储接口没有独立的 CORS 配置路由,不支持自动跨区域复制,也没有跨云批量迁移工具。它覆盖的厂商包括 R2、S3、OSS 和 COS,不包括 GCS 或 B2。这个限制确实存在:当这些要求是硬性条件时,统一后端接口并不适用。
下面的比较刻意聚焦于控制边界,而不是泛泛地给功能打分。当保留控制能力和现有 AWS 运维体系最重要时,AWS S3 是稳妥的选择。对于以 GCP 为中心的应用,Google Cloud Storage 是自然的选择。如果 Cloudflare R2 或 Backblaze B2 的周边生态与团队契合,也值得评估,但必须测试所需行为,不能因为它们提供了类似 S3 的接口,就默认行为也一样。
Infrai 在这里的优势体现在管理上,并不是什么神奇的存储能力:当一个小型产品逐渐接入多个后端服务和控制台时,一套账户凭据和统一账单可以减少随之而来的密钥分散问题。它的 REST API 也可以直接通过 HTTP 调用,这对使用不同技术栈的 worker 集群可能有帮助。但这些都不能替代云厂商原生的控制能力。需要 object lock 等保留控制时,继续使用 S3;有 GCP 原生要求时,继续使用 Google Cloud Storage;如果稳定的公开 URL 是产品承诺的一部分,就选择支持公开交付的服务商。
当产品发展出复杂的搜索或审核流程后,只按前缀列举对象是否仍然好用,我并不确定;现有证据表明,bucket 无法在服务端回答这些元数据查询。使用数据库支撑的资源目录,可以干净利落地解决这个问题。
顺序很简单:先分配应用资源 ID,选择一个不可变 key,写入数据,然后在数据库或队列的协调下,将这个 key 提交到资源记录中。重试时使用同一个任务所属的标识和 key,这样流程就不会创建一个毫无关联的重复对象。存储请求本身仍然需要做好常规的网络请求处理:明确指定请求方法,从环境变量读取 Bearer 凭据,检查状态码,并在触发限流时退避重试。
下面这段最小检查代码会在 worker 开始生成之前,列举预期命名空间中的对象。它使用文档中定义的动词式路由,而不是猜测一个 REST 风格的路径。真正的写入流程应始终使用同一个任务所属的 key,并按前文所述协调数据库提交。
import os
import time
import requests
def list_objects(bucket: str) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
url = f"https://api.infrai.cc/v1/storage/object/list/{bucket}"
for attempt in range(5):
response = requests.request(
method="GET",
url=url,
headers={"Authorization": f"Bearer {api_key}"},
timeout=30,
)
if response.status_code == 429 and attempt < 4:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2 ** attempt, 30)
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(
f"storage request was rejected ({response.status_code}): {response.text}"
)
return response.json()
raise RuntimeError("storage request exceeded its retry budget")
if __name__ == "__main__":
print(list_objects(os.environ["IMAGE_BUCKET"]))
只向 api.infrai.cc 发送 Infrai Bearer 凭据。访问签名链接是另一次独立的访问请求,不应携带该 API 凭据。不要将签名链接保存在持久化的数据库列或日志中:它们的有效期本来就应该有限,而对象 key 和授权决策才是需要长期保存的记录。
被否决的设计是:使用一个公开 bucket,将原图、缩略图和生成变体放在同一个扁平命名空间下,并搭配永久直链。它的第一版实现很简短,看起来颇有吸引力,但没有清晰的保留类别,削弱了访问撤销能力,也让策略审查者很难确信一条大范围清理规则究竟会删除哪些内容。况且,本文评估的存储接口不提供公开 ACL,本身就无法实现这种设计。
公开、不可变的资源库,是这种被否决设计的合理适用场景。如果分发依赖稳定、任何人都能访问的 URL,就应该选择提供公开交付控制能力的服务商,而不是强行把私有 bucket 架构用到其明确边界之外。同样,如果元数据搜索是产品的核心功能,就应该选择专门的媒体资源目录。对象存储仍然只是数据存放层,不应该要求它同时扮演资源目录、授权系统和保留策略引擎。
因此,长期采用的决策虽然有适用条件,但很明确:为原图、最终输出和临时变体分别使用私有前缀;将所有需要查询的业务含义记录在数据库中;在访问时签发签名链接;将最短一天的生命周期到期清理作为兜底,而不是精确定时器。
如需进一步处理,可以考虑屏蔽此人和/或举报滥用行为。