9

我计划使用延迟作业来运行一些后台分析。在我最初的测试中,我看到了巨大的内存使用量,所以我基本上创建了一个非常简单的任务,它每 2 分钟运行一次,只是为了观察正在使用多少内存。

任务很简单,analytics_eligbile?考虑到数据现在的位置,该方法总是返回 false,因此基本上没有调用任何重击代码。我在开发中的示例数据中有大约 200 个帖子。发布 has_one analytics_facet。

不管这里的内部逻辑/业务如何,这个任务唯一要做的就是调用 analytics_eligible? 每 2 分钟重复 200 次。在 4 小时内,我的物理内存使用量为 110MB,虚拟内存为 200MB。就为了做这么简单的事!我什至无法想象如果它使用真实的生产数据对 10,000 个帖子进行真正的分析,这将消耗多少内存!当然,它可能不会每 2 分钟运行一次,更像是每 30 分钟运行一次,但我仍然认为它不会飞。

这是在 Ubuntu 10.x 64 位上运行 ruby​​ 1.9.7、rails 2.3.5。我的笔记本电脑有 4GB 内存,双核 CPU。

rails真的这么糟糕还是我做错了什么?

 Delayed::Worker.logger.info('RAM USAGE Job Start: ' + `pmap #{Process.pid} | tail -1`[10,40].strip)
Post.not_expired.each do |p|
    if p.analytics_eligible?
        #this method is never called
        Post.find_for_analytics_update(p.id).update_analytics
    end
end
Delayed::Worker.logger.info('RAM USAGE Job End: ' + `pmap #{Process.pid} | tail -1`[10,40].strip)

Delayed::Job.enqueue PeriodicAnalyticsJob.new(), 0, 2.minutes.from_now

后模型

def analytics_eligible?
        vf = self.analytics_facet
        if self.total_ratings > 0 && vf.nil?
            return true
        elsif !vf.nil? && vf.last_update_tv > 0
            ratio = self.total_ratings / vf.last_update_tv
            if (ratio - 1) >= Constants::FACET_UPDATE_ELIGIBILITY_DELTA
                return true
            end
        end
        return false
    end
4

4 回答 4

20

ActiveRecord 相当消耗内存——在进行选择时要非常小心,并注意 Ruby 会自动将块中的最后一条语句作为返回值返回,这可能意味着您正在传回作为结果保存的记录数组某处,因此没有资格获得 GC。

此外,当您调用“Post.not_expired.each”时,您会将所有not_expired 帖子加载到 RAM 中。更好的解决方案是 find_in_batches,它专门一次只将 X 条记录加载到 RAM 中。

修复它可能很简单:

def do_analytics
  Post.not_expired.find_in_batches(:batch_size => 100) do |batch|
    batch.each do |post|
      if post.analytics_eligible?
        #this method is never called
        Post.find_for_analytics_update(post.id).update_analytics
      end
    end
  end
  GC.start
end

do_analytics

这里正在发生一些事情。首先,整个事情都被限定在一个函数中,以防止变量冲突保留来自块迭代器的引用。接下来,find_in_batchesbatch_size一次从数据库中检索对象,只要您不构建对它们的引用,就可以在每次迭代运行后进行垃圾回收,这将降低总内存使用量。最后,我们GC.start在方法结束时调用;这会强制 GC 开始扫描(您不想在实时应用程序中执行此操作,但由于这是一项后台作业,因此如果需要额外的 300 毫秒来运行也可以)。如果返回nil,它也有非常明显的好处,这意味着方法的结果是nil,这意味着我们不会意外挂在从查找器返回的 AR 实例上。

使用这样的东西应该可以确保你不会得到泄漏的 AR 对象,并且应该大大提高性能和内存使用。您需要确保不会在应用程序的其他地方泄漏(类变量、全局变量和类引用是最严重的违规者),但我怀疑这会解决您的问题。

综上所述,在我看来,这是一个 cron 问题(周期性重复工作),而不是 DJ 问题。您可以拥有一个一次性分析解析器,它每 X 分钟运行一次分析script/runner,由 cron 调用,它非常巧妙地清除任何潜在的内存泄漏或每次运行的误用(因为整个过程在最后终止)

于 2010-08-27T06:44:15.277 回答
6

正如 Chris Heald 所建议的那样,批量加载数据并积极使用垃圾收集器会给你带来一些非常大的收益,但人们经常忽略的另一个领域是他们正在加载的框架。

加载默认的 Rails 堆栈将为您提供 ActionController、ActionMailer、ActiveRecord 和 ActiveResource。如果您正在构建一个 Web 应用程序,您可能不会使用所有这些,但您可能使用最多。

在构建后台作业时,您可以通过为此创建自定义环境来避免加载不需要的内容:

# config/environments/production_bg.rb

config.frameworks -= [ :action_controller, :active_resource, :action_mailer ]

# (Also include config directives from production.rb that apply)

这些框架中的每一个都将只是坐在那里等待永远不会发送的电子邮件,或者永远不会调用的控制器。加载它们根本没有意义。调整您的database.yml文件,将您的后台作业设置为在production_bg环境中运行,您将有一个更清晰的开始。

您可以做的另一件事是直接使用 ActiveRecord 而不加载 Rails。这可能是您执行此特定操作所需的全部内容。我还发现,如果您主要执行 SQL 调用来重组记录或删除旧数据,那么使用像Sequel这样的轻量级 ORM会使您的后台工作变得非常轻量级。但是,如果您需要访问模型及其方法,则需要使用 ActiveRecord。不过,出于性能和效率的考虑,有时值得在纯 SQL 中重新实现简单的逻辑。

在测量内存使用情况时,唯一需要关注的数字是“真实”内存。虚拟量包含共享库,并且这些成本在使用它们的每个进程中分摊,即使每个进程都计算在内。

最后,如果运行重要的东西需要 100MB 的内存,但你可以通过三周的工作将它降低到 10MB,我不明白你为什么要打扰。托管提供商的 90MB 内存成本最多约为 60 美元/年,这通常比您的时间便宜得多。

Ruby on Rails 的理念是更关注您的生产力和时间,而不是内存使用。如果你想把它修剪回来,让它节食,你可以做到,但需要一些努力。

于 2010-08-28T17:57:16.703 回答
1

如果您遇到内存问题,一种解决方案是使用另一种后台处理技术,例如resque。就是github使用的BG处理。

由于 Resque 的父/子架构,使用过多内存的作业在完成时会释放该内存。没有不必要的增长

如何?

在某些平台上,当 Resque 工作人员保留作业时,它会立即分叉一个子进程。子进程处理作业然后退出。当孩子成功退出时,工人保留另一个工作并重复该过程。

您可以在 README 中找到更多技术细节。

于 2010-08-23T15:43:17.653 回答
0

Ruby 消耗(和泄漏)内存是一个事实。我不知道您是否可以做很多事情,但至少我建议您看看Ruby Enterprise Edition

REE 是一个开源端口,它承诺“减少 33% 的内存”以及所有其他好处。我已经在生产中使用 REE 和Passenger 快两年了,我很高兴。

于 2010-08-18T13:08:08.343 回答