В преддверии нашей 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, как гранулярно ограничивать количество выполняемых задач в условиях ограниченных ресурсов. Плюс опишу, какие инструменты мы используем для реализации пакетной обработки задач, которая экономно расходует доступную память.

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

Комментарии (22)


  1. SirEdvin
    09.10.2018 11:39
    +1

    Интересно, а какие альтернативы celery на питоне есть сейчас? А то большая часть проектов умеет только в redis, что явно не prouduction-ready решение.


    1. eyeofhell Автор
      09.10.2018 11:40

      1. SirEdvin
        09.10.2018 11:47
        +1

        rq — только redis
        tasktiger — только redis
        Huey — только redis
        WorQ — только redis
        dramatiq — вроде неплохо, но не хватает ряда полезных фич, например, событий.
        django-carrot — странная штука, просто обертка над rabbitmq

        Ну гуглить я тоже умею, интересно все-таки немного отзывов.


        1. eyeofhell Автор
          09.10.2018 12:07

          Тада ждем, я пайтон больше для автоматизации использую, с очередями и шинами почти не работал.


        1. Nerevar_soul
          09.10.2018 14:49

          apscheduler — вот такое еще есть. Не только redis, правда rabbitmq нет.


    1. vsespb
      09.10.2018 13:24

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


      1. SirEdvin
        09.10.2018 13:25

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

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

        К самому редису претензий нет, но использовать его для очереди задач очень странно, на мой взгляд.


        1. vsespb
          09.10.2018 13:33

          Понятно. Используем редис. Пока в память влазят миллионы задач, ну а больше нам и не надо, ибо задачи должны быстро обрабатываться, а не копиться, и ненулевое количество задач в очереди вообще нужно для сглаживания пиков нагрузки.


          1. SirEdvin
            09.10.2018 13:35

            И сколько он у вас памяти ест? Мне вот 450 мб не хватало для 5-10 тысяч задач.


            1. vsespb
              09.10.2018 13:38

              Точно не могу сказать. Он на машине с 32 гигами, выделенными под редис, но в нём не только очереди, но и другие NoSQL данные (в других БД редиса). Ну допустим на максимум 32гб можно ориентироваться. Обычно он жрёт чуть меньше 10Гб.


              1. SirEdvin
                09.10.2018 13:42

                И какой у вас поток задач в секунду? Ну, у нас пока не доходит до десятка в секунду. У вас под сотню или где-то так же?

                Просто на мой взгляд, если где-то меньше десятка, то такой поток вытягивает и кролик на 1 ГБ и со слабым диском.


                1. vsespb
                  09.10.2018 13:47

                  Этот вопрос я так понимаю имеет отношение к скорости редиса. Точных цифр бенчмарков на прод не помню, но сразу могу сказать что он медленнее RabbitMQ, и собственно его скорость зависит от той собственно системы очередей, что используется, у нас своя система, а она сильно влияет на бенчмарки.
                  Из цифр могу сказать что эпизодически ставятся сотни тысяч задач в очередь и этого никто не замечает, если по ошибке поставить 10млн, то да, память кончится. На этот случай есть мониторинг.


                  1. SirEdvin
                    09.10.2018 13:51

                    Это довольно странно, так как Redis должен быть быстрее RabbitMQ, в силу того, что кролик далеко не всегда хранит свои очереди в памяти. Если редис еще и медленее кролика смысла использовать его никакого нет, на мой взгляд.

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


                    1. vsespb
                      09.10.2018 14:05

                      Так, по скорости, счас посмотрел у нас явно бывает больше 350 в секунду.

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

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

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

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


                    1. ArsenAbakarov
                      09.10.2018 17:43

                      проблема rabbitmq кластера — split brain, у нас уже было такое несколько раз


                      1. SirEdvin
                        09.10.2018 20:22

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


        1. Bahusss
          09.10.2018 17:20
          +1

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

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


          1. SirEdvin
            09.10.2018 19:01

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

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


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

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


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

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


            1. Bahusss
              09.10.2018 20:00

              Промазал, ответил вам ниже.


  1. Bahusss
    09.10.2018 19:59

    Хм… это самописное решение или таки 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.


    1. SirEdvin
      09.10.2018 20:17

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


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


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


      1. Imrahil
        10.10.2018 18:09
        +1

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