Celery в нагруженных проектах: немного практики

    В преддверии нашей Moscow Python Conf++ мы кратко поговорили с Олегом Чуркиным, техлидом финтех-стартапа, о его обширном опыте работы с Celery: полмиллионе фоновых задачах, багах и тестировании.



    — Расскажи немного деталей о проекте, над которым ты сейчас работаешь?

    В данный момент я занимаюсь финтех-стартапом Statusmoney, который анализирует пользовательские финансовые данные и позволяет клиентам сравнивать свои доходы и расходы с другими группами людей, выставлять лимиты по тратам, наблюдать, как растет или падает благосостояние на графиках. Пока проект ориентирован только на североамериканский рынок.

    Для анализа финансовой информации мы скачиваем и храним все транзакции пользователей и интегрируемся с кредитными бюро, чтобы получить дополнительные данные по кредитной истории.

    Сейчас у нас около 200 тыс пользователей и 1,5 терабайта различных финансовых данных от наших поставщиков. Одних транзакций около 100 млн.

    — А каков технологический стек?

    Стек текущего проекта — это Python 3.6, Django/Celery и Amazon Web Services. Мы активно используем RDS и Aurora для хранения реляционных данных, ElasticCache для кэша и для очередей сообщений, CloudWatch, Prometheus и Grafana – для алертинга и мониторинга. Ну и, конечно, S3 для хранения файлов.

    Мы также очень активно используем Celery для различных бизнес-задач: рассылки уведомлений и массовых рассылок писем, массового обновления различных данных из внешних сервисов, асинхронного API и тому подобного.

    На фронтенде у нас React, Redux и TypeScript.

    — Какой основной характер нагрузок в вашем проекте и как вы с ними
    справляетесь?


    Основная нагрузка в проекте ложится на фоновые задачи, которые исполняет Celery. Ежедневно мы запускаем около полумиллиона различных задач, например, обновление и процессинг (ETL) финансовых данных пользователей из различных банков, кредитных бюро и инвестиционных институтов. Помимо этого отсылаем много уведомлений и рассчитываем множество параметров для каждого пользователя.

    Еще у нас реализован асинхронный API, который «пулит» результаты из внешних источников и также генерирует множество задач.

    В данный момент, после тюнинга инфраструктуры и Celery, справляемся без проблем, но раньше бывало всякое, обязательно расскажу об этом в своем докладе.

    — Как вы это всё масштабируете и обеспечиваете отказоустойчивость?

    Для масштабирования мы используем Auto Scaling Groups – инструментарий, который предоставляет наша облачная платформа AWS. Django и Celery хорошо масштабируются горизонтально, мы только немного настроили лимиты на максимальное количество памяти используемой воркерами uWSGI/Celery.

    — А мониторите чем?

    Для мониторинга cpu/memory usage и доступности самих систем используем Cloud Watch в AWS, различные метрики из приложения и из Celery-воркеров агрегируем с помощью Prometheus, а строим графики и отправляем алерты в Grafana. Для некоторых данных в Grafana мы используем ELK как источник.

    — Ты упоминал асинхронный API. Расскажи чуть подробнее, как он у вас
    устроен.


    У наших пользователей есть возможность «прилинковать» свой банковский (или любой другой финансовый) аккаунт и предоставить нам доступ ко всем своим транзакциям. Процесс «линковки» и обработки транзакций мы отображаем динамически на сайте, для этого используется обычный пулинг текущих результатов с бекенда, а бекенд забирает данные, запуская ETL pipeline из нескольких повторяющихся задач.

    — Celery — продукт с противоречивой репутацией. Как вам с ним живется?

    По моим ощущениям, наши отношения с Celery сейчас находятся на стадии «Принятие» – мы разобрались как фреймворк работает внутри, подобрали для себя настройки, разобрались с деплоем, «обложились» мониторингом и написали несколько библиотек для автоматизации рутинных задач. Некоторой функциональности нам не хватило «из коробки», и мы дописали ее самостоятельно. К сожалению, на момент выбора стека технологий для проекта, у Celery было не так много конкурентов, и если бы мы использовали более простые решения, то нам пришлось бы дописать намного больше.

    С багами именно в четвертой версии Celery мы ни разу не сталкивались. Большинство проблем было связано либо с нашим непониманием того, как это все работает, либо со сторонними факторами.

    О некоторых написанных внутри нашего проекта библиотеках я расскажу в своем выступлении.

    — Мой любимый вопрос. Как вы всю эту музыку тестируете?

    Задачи Celery хорошо тестируются функциональными тестами. Интеграцию тестируем с помощью автотестов и ручного тестирования на QA-стендах и стейджинге. На данный момент мы еще не решили пару вопросов с тестированием периодических задач: как позволять тестировщикам их запускать и как проверять, что расписание у этих задач корректное (соответствует требованиям)?

    — А тесты на фронтенд и вёрстку? Какое вообще соотношение ручного и
    автоматизированного тестирования?


    На фронте мы используем Jest и пишем только юнит-тесты на бизнес-логику. 55% бизнес-критикал кейсов у нас сейчас покрыты автотестами на Selenium, на данный момент у нас около 600 тестов в TestRail и 3000 тестов на бекенде.



    — О чем будет твой доклад на Moscow Python Conf ++ ?

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

    Также затрону тему реализации ETL-пайплайнов на Celery и отвечу, как описать их красиво, какую использовать retry policy, как гранулярно ограничивать количество выполняемых задач в условиях ограниченных ресурсов. Плюс опишу, какие инструменты мы используем для реализации пакетной обработки задач, которая экономно расходует доступную память.

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

    Конференции Олега Бунина (Онтико)

    450,00

    Конференции Олега Бунина

    Поделиться публикацией

    Похожие публикации

    Комментарии 22
      +1
      Интересно, а какие альтернативы celery на питоне есть сейчас? А то большая часть проектов умеет только в redis, что явно не prouduction-ready решение.
        0
          +1
          rq — только redis
          tasktiger — только redis
          Huey — только redis
          WorQ — только redis
          dramatiq — вроде неплохо, но не хватает ряда полезных фич, например, событий.
          django-carrot — странная штука, просто обертка над rabbitmq

          Ну гуглить я тоже умею, интересно все-таки немного отзывов.
            0
            Тада ждем, я пайтон больше для автоматизации использую, с очередями и шинами почти не работал.
              0
              apscheduler — вот такое еще есть. Не только redis, правда rabbitmq нет.
            0

            Что значит "явно не prouduction-ready решение" применительно к редису для очередей?

              0
              Подскажите, что вы будете делать, когда у вас очередь перестанет влазить в память? Это ждет практически любой проект, который использует большое количество фоновых задач.

              Ну и редис не скалируется по человечески.

              К самому редису претензий нет, но использовать его для очереди задач очень странно, на мой взгляд.
                –1
                Понятно. Используем редис. Пока в память влазят миллионы задач, ну а больше нам и не надо, ибо задачи должны быстро обрабатываться, а не копиться, и ненулевое количество задач в очереди вообще нужно для сглаживания пиков нагрузки.
                  0
                  И сколько он у вас памяти ест? Мне вот 450 мб не хватало для 5-10 тысяч задач.
                    0
                    Точно не могу сказать. Он на машине с 32 гигами, выделенными под редис, но в нём не только очереди, но и другие NoSQL данные (в других БД редиса). Ну допустим на максимум 32гб можно ориентироваться. Обычно он жрёт чуть меньше 10Гб.
                      0
                      И какой у вас поток задач в секунду? Ну, у нас пока не доходит до десятка в секунду. У вас под сотню или где-то так же?

                      Просто на мой взгляд, если где-то меньше десятка, то такой поток вытягивает и кролик на 1 ГБ и со слабым диском.
                        0
                        Этот вопрос я так понимаю имеет отношение к скорости редиса. Точных цифр бенчмарков на прод не помню, но сразу могу сказать что он медленнее RabbitMQ, и собственно его скорость зависит от той собственно системы очередей, что используется, у нас своя система, а она сильно влияет на бенчмарки.
                        Из цифр могу сказать что эпизодически ставятся сотни тысяч задач в очередь и этого никто не замечает, если по ошибке поставить 10млн, то да, память кончится. На этот случай есть мониторинг.
                          0
                          Это довольно странно, так как Redis должен быть быстрее RabbitMQ, в силу того, что кролик далеко не всегда хранит свои очереди в памяти. Если редис еще и медленее кролика смысла использовать его никакого нет, на мой взгляд.

                          Просто меня довольно давно удивляет, что все берут для очередей редис, учитывая, что есть rabbitmq, который и более надежный, и более гибкий, и еще позволяет делать довольно просто отказоустойчивый сетап. Мне казалось, что единственное преимущество редиса — это скорость. Или вы взяли его по другой причине?
                            0
                            Так, по скорости, счас посмотрел у нас явно бывает больше 350 в секунду.

                            Редис будет быстрее рэббита если написать на нём свою очередь, состоящую из пары команд PUSH/BLPOP, в которой ничего больше нет.

                            Если сделать так чтобы задачи не терялись в случае любых ситуаций (воркер взял задачу и упал), а так же чтобы не было race condition при различных кейзах, а так же возврат результата работы, + статистика, мониторинг, то будет медленнее.

                            Проблем с надёжностью редиса не вижу. Гибкость как раз больше у редиса (если самому писать под него систему), т.к. это фактически бэкэнд для системы очередей, а не готовая система очередей.

                            Сложно сравнивать голый рэббит и некую систему очередей X, которая использует Redis.
                            По самому редису нет никаких нареканий к надёжности, и гибкости. А скорость обычно пострадает при написании системы очередей под него.
                              0
                              проблема rabbitmq кластера — split brain, у нас уже было такое несколько раз
                                0

                                Нет кластера — нет проблем?) split brain можно словить в любом кластере, в master-slave немного сложнее, но тоже вполне можно попробовать.
                                Или у rabbitmq какая-то серьезная проблема именно со split brain? Вам не подошел autoheal или он не работает в вашем случае?

                    +2
                    redis для очередей вполне production-ready решение, проверено уже на многих проектах.
                    При 50000 задач ежесекундно (при условии что воркеры успевают их разгребать) redis в нашем проекте потреляет 80 Мб памяти.

                    Если очередь перестает помещаться в память, то скорее всего происходит что-то не то: либо задачи накапливаются – что не очень хорошо, либо вы передаете в сообщениях мегабайты данных, чего тоже делать не стоит.
                      0
                      При 50000 задач ежесекундно (при условии что воркеры успевают их разгребать) redis в нашем проекте потреляет 80 Мб памяти.

                      Хм… это самописное решение или таки celery?


                      Если очередь перестает помещаться в память, то скорее всего происходит что-то не то: либо задачи накапливаются – что не очень хорошо, либо вы передаете в сообщениях мегабайты данных, чего тоже делать не стоит.

                      А как насчет таких случаев:


                      1. В сообщениях приходится передавать большие объемы информации. Например, sentry цеплять в сообщение всю инфу по событию, которое приняло по api. Или вы предложили сначала записать это все добро в базу сырым и потом удалить оттуда?
                      2. Задачи выполняются очень долго и все время, пока выполняются висят в очереди (кстати, а у вас так или вы как-то по другому удостоверяетесь в том, что задача не пропала?). К сожалению, те же задачи по интеграции с внешними сервисами могут работать внушительно долго.
                      3. Задачи идут волнами. Например, есть задача "импорт с внешнего источника" и она создает большое количество задач волнами, что как бы приводит к их накоплению.

                      Ну и да, возвращаясь к редису — вы его как-то кластеризируете или он у вас просто так стоит как единая точка отказа?

                        0
                        Промазал, ответил вам ниже.
                +1
                Хм… это самописное решение или таки celery?


                celery, мы в таски передаем только id-шники, сообщение в таком случае и не должно занять больше 2Кб.

                В сообщениях приходится передавать большие объемы информации


                Все еще не совсем понятен юзекейс, зачем вы передаете в задачу всю информацию по событию? Событие в sentry все равно посылается асинхронно без задач, а в других случаях разве не нужно сохранять это событие в промежуточный сторадж (база, кэш)? Чтобы потом прочитать его в задаче по ключу? Мы у себя редко передает в параметры задач что-то больше 2Кб. Вся информация, которая нужна в задаче «достается» из каких-либо стораджей.

                Задачи выполняются очень долго и все время, пока выполняются висят в очереди (кстати, а у вас так или вы как-то по другому удостоверяетесь в том, что задача не пропала?)


                Мы ставим максимальный софт таймаут для некоторых задач на 5 минут, если задача работает больше – значит это ненормально и что-то пошло не так, если внешний сервис лежит, то в задачах у нах предусмотрен exponential backoff и подходящая retry policy. А что значит «не пропала»? Вы имеете ввиду гарантируем ли мы как-то что все задачи действительно выполнены? Нет не гарантируем, да и сам redis такого не гарантирует, но мы знаем об этой особенности, для нашего проекта она пока не критична.

                Задачи идут волнами. Например, есть задача «импорт с внешнего источника» и она создает большое количество задач волнами, что как бы приводит к их накоплению.


                Буду об этом рассказывать в своем докладе. Мы тоже несколько раз получали task flooding, поэтому поигрались с настройками и написали свой чанкификатор задач, который запускает их определенными порциями и таким образом обеспечивает равномерное накопление задач в очередях и не перегружает внутренние (например базу) и внешние сервисы.

                Ну и да, возвращаясь к редису — вы его как-то кластеризируете или он у вас просто так стоит как единая точка отказа?


                Пока мы кластеризуем его хуже чем нам хотелось бы, AWS обеспечивает автоматический фейловер в случае если с мастер нодой что-то случилось, но в те несколько секунд пока происходит фейловер – постановка задач фейлится, может быть какие-то окажутся потеряны. Эту проблему мы у себя пока не решили, но можно посмотреть в сторону github.com/dealertrack/celery-redis-sentinel – там есть retry в случае connection error'a.

                  0

                  У вас довольно интересная схема, хотя мне и кажется, что она немного читерская, потому что, исходя из того, что я понял, всю инфу по таске вы храните во внешнем источнике, получается цепочка redis -> storage -> task execution, вместо rabbitmq -> task execution. Мне сложно понять выгоду такой схемы, но выглядит интересно)


                  А так большое спасибо за такой развернутый ответ, довольно познавательно :)


                  Касательно кластеризации, немного отвратительный опыт с redis sentinel из-за того, что там нельзя разделить bind/advertise адреса. Из-за этого sentinel совсем у нас не работал.

                    +1
                    Самая что ни на есть оптимальная схема)
                    Зачем тянуть в таск то что можно найти уже в воркере?
                    Понятно что это не панацея и от всех случаев не спасет, но сама идея весьма продуктивна и работает в продакшене как часы.
                    А выгода быть может тогда когда данные для успешного выполнения таска могли поменяться.
                    Если мы передадим в таск сразу готовый набор данных то он окажется устаревшим (Страшно подумать, но например отложенная оплата с баланса в системе или другой кошмар на выбор). И ситуация когда мы в таск передаем только тип операции, id юзера ну и id странзакции..(или id таска в БД) Воркер сам все подтянет, проверит возможность и выполнит или задеклайнит таск…

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое