云存储迁移

Odoo 云存储迁移为什么不是“跑个上传脚本”就完了:checkpoint、预提交标记与失败重试边界讲透

结合 cloud_storage_migration 源码,讲清 Odoo 如何用 checkpoint、批量切片和预提交标记,把本地附件迁移到云存储做成可续跑、少重复上传、尽量不阻塞业务的链路。

Odoo 开发 框架
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 9 阅读

很多人看到“把附件迁到云存储”,第一反应都是:

写个脚本,循环读文件,上传完删本地文件。

但 Odoo 的 cloud_storage_migration 不是这么做的。 它的目标不是“把文件搬过去”这么简单,而是要在不停机、可续跑、尽量少重复上传的前提下,慢慢把历史附件切过去。

这就决定了它必须回答三个问题:

  1. 先迁谁,后迁谁?
  2. 跑到一半挂了,下一次从哪里接着跑?
  3. 业务还在写附件时,怎么避免把刚生成的文件误搬走?

1. 迁移不是“遍历全表”,而是“扫一段、记一个 checkpoint”

核心入口在 addons/cloud_storage_migration/models/ir_attachment.py_cron_migrate_local_to_cloud_storage()

它先从 ir.config_parameter 里读几个关键参数:

  • cloud_storage_migration_min_attachment_id
  • cloud_storage_migration_max_attachment_id
  • cloud_storage_migration_max_file_size
  • cloud_storage_migration_max_batch_file_size
  • cloud_storage_migration_message_models
  • cloud_storage_migration_all_models

你可以把它理解成一个“带边界的游标扫描器”。

不是每次从头扫全库,而是只扫:

  • id 大于上次 checkpoint 的附件
  • id 小于最大上限的附件
  • 只看满足模型白名单的附件
  • 只看 type = 'binary'、有 store_fname 的附件
  • 文件大小在允许区间内
  • 创建时间至少早于 7 天
  • 不属于 documents.document

这些条件不是装饰品,而是在告诉你:

这份迁移任务是“安全优先”的,不是“覆盖式清仓”。


2. 为什么它要先写 checkpoint,再真的上传

这里最值得注意的是 commit_min_attachment_id()

它不是普通的 ORM write(),而是直接 SQL 更新 ir_config_parameter

  • 先把 cloud_storage_migration_min_attachment_id 推进到当前附件 id
  • 再调用 self.env['ir.cron']._commit_progress(1)

然后才去执行真正的上传。

乍看这像“先记账再干活”,其实是故意的。

这么设计的原因是:

  • cron 可能超时
  • 进程可能被回收
  • 上传本身可能卡在网络上
  • 本地文件和云端文件都可能有临时失败

如果不先推进 checkpoint,一旦中途挂掉,下次重跑时就会重复读到同一批附件; 如果一直反复重试,又很容易把一个半失败的任务拖成死循环。

所以这套逻辑更像:

我承认这是一次“至少尝试过”的迁移,而不是“必须百分之百成功后才前进”的事务。

这也是为什么它在失败时会记录 warning、rollback 当前事务,但不会回滚 checkpoint。 设计取舍很明确:进度比完美重试更重要


3. 真正的上传动作,其实是“先换 URL,再流式推文件”

真正把一个附件从本地搬到云端的是 _migrate_local_to_cloud_storage(session)

这个方法先做了几道硬检查:

  • 只能迁 type == 'binary'
  • 必须有 store_fname
  • 取到本地文件路径后,先给附件生成云端 URL
  • 再调用 provider 提供的 _generate_cloud_storage_upload_info()

然后用 requests.Session() 把本地文件流式上传到云端。

注意这里不是“先把文件读进内存,再一次性 POST”,而是直接把文件对象交给 session.request(...)。 这很适合大文件,也更接近真实生产环境。

上传成功后,它会把记录改成:

  • type = 'cloud_storage'
  • raw = False
  • mimetype 保持原值

换句话说,本地二进制内容不再是这条记录的主存储,记录本身开始变成一个“指向云对象的壳”。

cloud_storage 模块里的 ir.attachment 还会在读取时把 type == 'cloud_storage' 的附件改走下载 URL。 这意味着迁移不是一次性搬家,而是读写路径一起切换


4. 为什么它要跳过“太新”的附件

代码里有一个很容易被忽略的条件:

  • create_date < now - 7 days

这条规则很重要。

因为 Odoo 里很多业务会在“刚创建附件之后”的几分钟甚至几小时内继续处理它:

  • 邮件线程还在追加附件
  • 文档业务还在补字段
  • 上传文件可能还要被下游逻辑立即读取

如果太早把附件迁去云端,业务代码可能还假设它是本地 binary,结果就会产生奇怪的边界问题。

所以这里故意留了一个“冷却期”。 它不是在追求极限同步,而是在追求业务稳定性


5. 这套设计的真实代价是什么

它的代价也很明显:

  • 失败不一定自动重试同一条附件
  • 迁移速度不会特别激进
  • 需要依赖配置参数和 cron 调度
  • 迁移过程必须理解 checkpoint 语义

但换来的好处是:

  • 不用停业务
  • 不容易重复上传同一批文件
  • 可以在 cron 线程里慢慢搬
  • 可以用配置把范围缩小到特定模型

对运维来说,这意味着排障时不要只盯“有没有上传成功”,还要同时看:

  1. cloud_storage_provider 有没有配置
  2. cloud_storage_migration_min_attachment_id 走到哪了
  3. 是否卡在某个模型白名单
  4. 是否因为文件大小或创建时间被过滤掉
  5. 当前附件是不是仍然属于 documents.document 这类被排除对象

结论

cloud_storage_migration 不是一个“上传脚本”,而是一个带 checkpoint、带范围控制、带失败边界的迁移引擎

它最重要的设计点不是“怎么上传”,而是:

如何在不打断业务的情况下,让附件状态从本地 binary 平稳过渡到云端对象。

真正理解这一点,才算看懂了这段源码的价值。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。