【问题标题】:Django Celery task on Heroku causes high memory usageHeroku 上的 Django Celery 任务导致高内存使用
【发布时间】:2017-05-15 20:30:09
【问题描述】:

我在 Heroku 上有一个 celery 任务,它连接到外部 API 并检索一些数据,存储在数据库中并重复数百次。很快(大约 10 次循环后)Heroku 开始警告高内存使用情况。有什么想法吗?

tasks.py

@app.task
def retrieve_details():
    for p in PObj.objects.filter(some_condition=True):
        p.fetch()

models.py

def fetch(self):
    v_data = self.service.getV(**dict(
        Number=self.v.number
    ))
    response = self.map_response(v_data)

    for key in ["some_key","some_other_key",]:
        setattr(self.v, key, response.get(key))

    self.v.save()

英雄日志

2017-01-01 10:26:25.634
132 <45>1 2017-01-01T10:26:25.457411+00:00 heroku run.5891 - - Error R14 (Memory quota exceeded)

Go to the log: https://api.heroku.com/myapps/xxx@heroku.com/addons/logentries

You are receiving this email because your Logentries alarm "Memory quota exceeded"
has been triggered.

In context:
2017-01-01 10:26:25.568 131 <45>1 2017-01-01T10:26:25.457354+00:00 heroku run.5891 - - Process running mem=595M(116.2%)
2017-01-01 10:26:25.634 132 <45>1 2017-01-01T10:26:25.457411+00:00 heroku run.5891 - - Error R14 (Memory quota exceeded)

【问题讨论】:

  • 能否请您发布从fetch 调用的方法的方法主体?
  • @Avi 你还记得你是如何解决这个问题的吗?你使用了用户 2ps 的建议吗?

标签: python django heroku django-celery


【解决方案1】:

您基本上是将一堆数据加载到内存中的 Python 字典中。这将导致大量内存开销,尤其是当您从本地数据库中获取大量对象时。

您真的需要将所有这些对象存储在字典中吗?

大多数人为这样的事情做的是:

  • 从数据库中一次检索一个对象。
  • 处理该项目(执行您需要的任何逻辑)。
  • 重复。

这样,您最终只会在任何给定时间将单个对象存储在内存中,从而大大减少了内存占用。

如果我是你,我会想办法将我的逻辑转移到数据库查询中,或者简单地单独处理每个项目。

【讨论】:

    【解决方案2】:

    为了扩展名副其实的 rdegges 思想,这里有两种我过去在使用 celery/python 时使用的策略来帮助减少内存占用:(1) 启动每个只处理一个对象的子任务和/或 ( 2) 使用生成器。

    1. 启动每个只处理一个对象的子任务:

      @app.task
      def retrieve_details():
          qs = PObj.objects.filter(some_condition=True)
          for p in qs.values_list('id', flat=True):
              do_fetch.delay(p)
      
      @app.task
      def do_fetch(n_id):
          p = PObj.objects.get(id=n_id)
          p.fetch()
      

      现在您可以使用 --max-tasks-per-child 调整 celery,使其在处理 N 个 PObj(任务)后终止进程,以保持较低的内存占用。

    2. 使用生成器:您也可以尝试使用生成器,这样您就可以(理论上)在调用 fetch 后丢弃 PObj

      def ps_of_interest(chunk=10):
          n = chunk
          start = 0
          while n == chunk:
              some_ps = list(PObj.objects.filter(some_condition=True)[start:start + n])
              n = len(some_ps)
              start += chunk
              for p in some_ps:
                  yield p
      
      @app.task
      def retrieve_details():
          for p in ps_of_interest():
              p.fetch()
      

    为了我的钱,我会选择选项 #1。

    【讨论】:

    • 1b) 每个子任务只处理一个对象 - 请注意,有时初始化和关闭任务的成本可能是整个处理任务成本的重要组成部分。因此,当主要任务通过 ids 传递给子任务少数对象时,请考虑进行批处理
    猜你喜欢
    • 2011-06-19
    • 2015-08-08
    • 2016-12-28
    • 2020-05-11
    • 2014-06-13
    • 1970-01-01
    • 2012-10-08
    • 2016-12-13
    • 2018-08-13
    相关资源
    最近更新 更多