You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将Rails应用从Mongoid(MongoDB)迁移至ActiveRecord(Postgres)时如何维持内存稳定?

Rails MongoDB转PostgreSQL迁移内存暴涨问题解决指南

问题背景

我近期在协助一款Rails应用做数据库迁移:原本用MongoDB(搭配Mongoid)存储数据,初创阶段跑得顺风顺水,但随着客户量和复杂统计查询需求上来,我们决定把数据规范化后迁到PostgreSQL(用ActiveRecord做ORM)。

为了保证MongoDB里的非规范化数据符合新库要求,我们在Rails环境里做迁移,这样能让模型的验证、回调和完整性检查都生效。开发环境小数据量迁移没问题,但到staging服务器用真实生产数据跑的时候,部分迁移的内存占用跟着实例数量直线飙升,把16GB内存加16GB交换空间都耗光后直接被系统终止了。

我们现在是逐个迁移实例,想找个能让内存维持在接近恒定水平的办法,目前怀疑的原因有两个:一是ActiveRecord或Mongoid一直握着已导入对象的引用没放;二是迁移在单个数据库事务里执行,PostgreSQL的内存占用一直涨直到迁移完成。

想请教几个问题:

  • 内存线性增长的可能原因到底有哪些?
  • 怎么才能降低内存占用?
  • 有没有办法让Mongoid和/或ActiveRecord释放旧的对象引用?
  • 要不要手动调用Ruby的GC?
  • 把迁移拆成多个数据库事务有用吗?

迁移的大致代码是这样的:

class MigrateSomeThing < ActiveRecord::Migration[5.2]
  def up
    Mongodb::ModelName.all.each do |old_thing|
      # Mongoid's #.all.each works with batches
      create_thing(old_thing, Postgres::ModelName.new)
    end
    raise "Not all rows could be imported" if MongoDB::ModelName.count != Postgres::ModelName.count
  end

  def down
    Postgres::ModelName.delete_all
  end

  def create_thing(old_thing, new_thing)
    attrs = old_thing.attributes
    # ... 调整属性适配PostgreSQL
    new_thing.attributes = attrs
    new_thing.save!
  end
end

内存线性增长的核心原因

咱们先把可能的原因拆解清楚:

  • 对象引用没被释放:虽然Mongoid的all.each是分批取数据,但你每次循环创建的ActiveRecord实例(new_thing),还有Mongoid的文档实例(old_thing),可能被Ruby的对象引用链牢牢攥着——比如迁移类本身、ORM的内部缓存(比如Mongoid的身份映射、ActiveRecord的连接池缓存),导致GC没法回收这些对象,内存自然越积越多。
  • 单事务累积数据:默认情况下,Rails迁移的up方法是在一个大事务里跑的。PostgreSQL在事务中会保留所有修改的日志和数据版本,迁移的行越多,这个事务占用的内存就越大,直到事务提交才会释放。
  • ORM自动缓存拖后腿:ActiveRecord会自动缓存查询结果和实例,save!后的实例会被存在连接池里;Mongoid如果开了身份映射,也会缓存已加载的文档,这些缓存都会持续占用内存。
  • 临时对象没及时清理:迁移中生成的attrs哈希、中间处理的变量,如果没有被标记为可回收,也会慢慢占满内存。

降低内存占用的具体方案

针对这些原因,给你几个实操性强的解决办法:

1. 主动分批处理+清理ORM引用

别光依赖Mongoid的默认分批,主动控制批次大小,每批处理完就手动清理ORM的缓存:

def up
  batch_size = 1000 # 根据服务器内存调整,比如500或2000
  Mongodb::ModelName.find_each(batch_size: batch_size) do |old_thing|
    create_thing(old_thing, Postgres::ModelName.new)
  end

  # 或者用each_slice做更细的批次控制+清理
  # Mongodb::ModelName.all.each_slice(batch_size) do |batch|
  #   batch.each { |old_thing| create_thing(old_thing, Postgres::ModelName.new) }
  #   # 清理Mongoid的文档引用
  #   Mongoid::IdentityMap.clear if Mongoid.identity_map_enabled?
  #   # 清理ActiveRecord的连接池缓存
  #   ActiveRecord::Base.clear_active_connections!
  #   # 手动触发GC
  #   GC.start
  # end

  raise "Not all rows could be imported" if MongoDB::ModelName.count != Postgres::ModelName.count
end
  • find_each比all.each更可控,明确指定批次大小。
  • Mongoid::IdentityMap.clear:如果开了身份映射,这个方法能直接清空Mongoid缓存的所有文档实例。
  • ActiveRecord::Base.clear_active_connections!:强制断开所有ActiveRecord连接,释放缓存的实例。

2. 拆分迁移为多个小事务

把每一批次的操作放在独立事务里,避免单个大事务累积过多数据:

def up
  batch_size = 1000
  Mongodb::ModelName.all.each_slice(batch_size) do |batch|
    ActiveRecord::Base.transaction do
      batch.each { |old_thing| create_thing(old_thing, Postgres::ModelName.new) }
    end
    # 每批事务提交后清理缓存+触发GC
    Mongoid::IdentityMap.clear
    ActiveRecord::Base.clear_active_connections!
    GC.start
  end
  # ... 校验逻辑
end

这个方法对降低PostgreSQL的内存占用特别有效——每批事务提交后,数据库会立即释放该批次的事务日志和版本数据,不会一直累积。唯一要注意的是,如果你的迁移要求完全原子性(要么全成功要么全失败),这个方案就不适用;但如果可以接受分批提交(就算中间失败,后续可以从断点继续),这绝对是首选。

3. 尽量避免不必要的ORM实例

如果你的迁移不需要依赖模型的验证和回调(当然你说需要,但部分场景可以灵活调整),直接用批量插入代替逐个创建实例:

def up
  batch_size = 1000
  Mongodb::ModelName.all.each_slice(batch_size) do |batch|
    attrs_list = batch.map do |old_thing|
      attrs = old_thing.attributes
      # 调整属性适配PostgreSQL
      attrs.except('_id') # 去掉MongoDB的_id字段
    end
    # 批量插入,不创建ActiveRecord实例,直接写库
    Postgres::ModelName.insert_all!(attrs_list)
    # 清理+GC
    Mongoid::IdentityMap.clear
    GC.start
  end
end

insert_all!会跳过ActiveRecord的实例创建、验证和回调,但会触发数据库的约束检查(比如非空、唯一索引)。如果你的迁移必须用验证和回调,那还是得用save!,但可以在每批后及时清理实例。

4. 手动调用GC的正确姿势

手动调用GC.start是有用的,但别在每次循环都调用——那样会拖慢迁移速度。最好是在每批处理完成后调用一次,让Ruby及时释放那些已经没用的对象内存。对于迁移这种一次性任务,GC的短暂性能开销完全可以接受。

5. 关闭ORM的查询缓存

迁移过程中不需要查询缓存,直接关掉它避免额外内存占用:

def up
  ActiveRecord::Base.uncached do
    Mongodb::ModelName.find_each(batch_size: 1000) do |old_thing|
      create_thing(old_thing, Postgres::ModelName.new)
    end
  end
  # ... 校验逻辑
end

你的问题逐一解答

  1. 内存线性增长的可能原因:主要是ORM(Mongoid/ActiveRecord)缓存了大量实例未释放、单个大事务累积数据库资源、临时对象未被GC回收这几个点。
  2. 如何降低内存占用:采用主动分批处理+每批清理ORM缓存、拆分多个小事务、批量插入、手动触发GC这些组合方案。
  3. 释放Mongoid/ActiveRecord旧引用:Mongoid用Mongoid::IdentityMap.clear(需开启身份映射);ActiveRecord用ActiveRecord::Base.clear_active_connections!或ActiveRecord::Base.reset_all。
  4. 是否手动调用Ruby GC:非常建议在每批处理后手动调用,能及时释放内存,避免内存持续暴涨。
  5. 拆分多个事务是否有效:效果非常明显,尤其是解决PostgreSQL的事务内存累积问题,每批提交后数据库会立即释放该批次的资源。

内容的提问来源于stack exchange,提问作者Qqwy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:12:37