Redis Queue + python-rq: правильный шаблон для предотвращения использования высокой памяти?

В настоящее время мы используем Redis для Go с нашим приложением Python, размещенным на Heroku.

Мы используем Redis с python-rq только как очередь задач, чтобы обеспечить отсроченное выполнение некоторых напряженных задач. Задача - получить некоторые данные из базы данных PostgreSQL и вернуть к ней результаты - таким образом, никакие ценные данные вообще не сохраняются в экземпляре Redis. Мы замечаем, что в зависимости от количества выполненных заданий Redis потребляет все больше и больше памяти (рост @~ 10 МБ/час). Команда FLUSHDB в CLI исправляет это (переносит ее до ~ 700 КБ используемой ОЗУ) до тех пор, пока ОЗУ не будет полностью заполнено.

В соответствии с нашими (неизменными стандартными) настройками результат работы сохраняется в течение 500 секунд. Со временем некоторые задания, конечно, терпят неудачу, и они перемещаются в неудачную очередь.

  • Что нам нужно делать по-другому, чтобы наши задачи выполнялись со стабильным объемом оперативной памяти?
  • Откуда идет потребление ОЗУ?
  • Можно ли вообще отключить постоянство?
  • Из документов, которые я знаю, что 500-секундный TTL означает, что ключ затем "истек", но не действительно удален. В этот момент ключ все еще потребляет память? Могу ли я каким-то образом изменить это поведение?
  • Имеет ли он какое-то отношение к неудавшейся очереди (которая, по-видимому, не имеет TTL, прикрепленного к заданиям, что означает (я думаю), что они сохраняются навсегда)?
  • Просто любопытно: при использовании RQ чисто в качестве очереди, что сохраняется в Redis DB? Это фактический исполняемый код или просто ссылка на то, где можно найти функцию, которая будет выполнена?

Извините за довольно noobish вопросы, но я новичок в теме работы в очереди и после исследования в течение 2+ дней я достиг точки, где я не знаю, что делать дальше. Благодаря, KH

Ответ 1

После двух дней игры я нашел проблему. Я хотел бы поделиться этим с вами вместе с инструментами, которые были полезны:

Основная проблема

Фактическая проблема заключалась в том, что мы упустили возможность передать объект в строку перед сохранением в базе данных PostgreSQL. Без этого приведения строковое представление попало в БД (из-за функции __str__() соответствующего объекта, возвращающего именно желаемое представление); однако, для Редиса, весь объект был передан. После передачи его в Redis связанная задача разбилась на исключение UnpickleError. Это потребляло 5 МБ ОЗУ, которые не были освобождены после аварии.

Дополнительные действия

Чтобы уменьшить объем памяти, мы внедрили следующие дополнительные действия (помните, что мы сохраняем все в отдельной БД, поэтому результаты, которые сохраняет Redis, вообще не используются в нашем приложении):

  • Мы устанавливаем TTL результата задачи в 0 с помощью вызова enqueue_call([...] result_ttl=0)
  • Мы определили пользовательский обработчик Exception - black_hole - чтобы взять все исключения и вернуть False. Это позволяет Redis перемещать задачу в неудачную очередь, где она все равно будет использовать бит памяти. Исключения заранее отправляются по электронной почте нам, чтобы отслеживать их.

Полезные инструменты:

Мы просто работали с redis-cli.

  • redis-cli info | grep used_memory_human → показывает текущее использование памяти. идеально подходит для сравнения объема памяти до и после выполнения задачи.
  • redis-cli keys '*' → показывает все существующие ключи. Этот обзор привел меня к пониманию того, что некоторые задачи не удаляются, хотя они должны были быть (как написано выше, они разбились с помощью UnpickleError и из-за этого не были удалены).
  • redis-cli monitor → показывает в реальном времени обзор того, что происходит в Redis. Это помогло мне узнать, что объекты, которые были перемещены назад и вперед, были слишком массивными.
  • redis-cli debug object <key> → показывает сброс значения ключа.
  • redis-cli hgetall <key> → показывает более читаемый дамп ключевого значения (особенно полезно для конкретного случая использования Redis в качестве очереди задач, так как кажется, что задачи создаются python-rq в этом формате.

Кроме того, я могу ответить на некоторые из вопросов, которые я написал выше:

Из документов, которые я знаю, что 500-секундный TTL означает, что ключ затем "истек", но не действительно удален. В этот момент ключ все еще потребляет память? Могу ли я каким-то образом изменить это поведение?

Фактически, они удаляются, как и подразумевают документы.

Имеет ли он какое-то отношение к неудавшейся очереди (у которой, по-видимому, нет TTL, связанного с заданиями, что означает (я думаю), что они сохраняются навсегда)?

Удивительно, но рабочие места, для которых произошел крах Redis, не были перемещены в неудачную очередь, они были просто "оставлены", что означает, что значения остались, но RQ не заботился об этом, как обычно, при неудачных заданиях.

Соответствующая документация

Ответ 2

Если вы используете обработчик исключений "Черная дыра" из http://python-rq.org/docs/exceptions/, вы также должны добавить job.cancel():

def black_hole(job, *exc_info):
    # Delete the job hash on redis, otherwise it will stay on the queue forever
    job.cancel()
    return False

Ответ 3

Мне не сразу стало ясно, что задание RQ имеет свойства "описание" и "данные". Если не указано, описание задается как строковое представление данных, которое в моем случае было излишне подробным. Явное указание описания на краткое резюме спасло меня от этого.

enqueue(func, longdata, description='short job summary')