Celery под нагрузкой: очередь не спасёт базу сама по себе

public

Автор: Олег Чуркин (TechOps) · Python Channel


Поставить 500 тысяч задач в Celery несложно. Сложно сделать так, чтобы после этого продолжили работать база данных, внешние сервисы и сами воркеры.

Олег Чуркин разбирает Celery не как способ вынести функцию в фон, а как отдельную производственную систему со своими ограничениями. Очередь принимает нагрузку, но не устраняет её: если без контроля породить сотни тысяч задач, перегрузка просто переместится с веб-приложения на брокер, базу или внешний API.

Главный пример доклада — порционная генерация задач. Вместо того чтобы сразу вызвать .delay() для всей пользовательской базы, команда сделала Chunkificator: задача обрабатывает ограниченный диапазон записей, а затем запускает следующую порцию. Так можно контролировать память и скорость поступления работы, не создавая огромный список задач заранее.

Кому смотреть: backend-разработчикам, которые уже используют Celery и готовятся обрабатывать большие рассылки, ETL-процессы или другие массовые фоновые операции.

Из этого можно взять в работу: перед следующим массовым запуском отдельно ограничить скорость появления задач, а не только количество Celery workers.


Доклад охватывает несколько границ, о которых легко забыть после успешного первого запуска Celery. В аргументы задачи не стоит складывать тяжёлые сериализованные объекты: обычно достаточно передать ID и получить актуальные данные уже внутри worker. Для задач нужны ограничения времени выполнения и срок актуальности, иначе очередь постепенно заполняется зависшей или уже бесполезной работой.

Отдельная проблема — память worker-процессов. Даже небольшая утечка становится заметной на длинной серии задач. worker_max_memory_per_child позволяет перезапустить дочерний процесс до того, как весь worker завершит OOM Killer.

При rolling deployment одновременно живут producers и workers разных версий. Несовместимое изменение аргументов задачи способно сломать сообщения, которые уже находятся в очереди. Поэтому сигнатура фоновой задачи требует такой же дисциплины совместимости, как внешний API.

Особенно полезен разбор неудачных попыток ограничить массовую обработку. Генератор и Celery groups не решают проблему автоматически: данные всё равно могут накопиться в памяти, а rate_limit применяется на уровне worker и меняется вместе с количеством workers. В итоге команде понадобился собственный механизм постепенной генерации работы.

Это хороший доклад о том, что очередь задач — не предохранитель от нагрузки. Она лишь даёт место, где этой нагрузкой можно управлять.