Привет! Меня зовут Александр Егоров, я MLOps-инженер в Альфа-Банке, куда попал через проект компании KTS.

За свою карьеру я построил четыре ML-платформы (одна из которых сейчас в Росреестре) и развиваю с командой пятую. Параллельно учусь в ИТМО по направлению «Безопасность искусственного интеллекта».

В этой статье я немного покритикую Airflow и поделюсь нашей историей миграции на связку Argo Workflows и Argo CD. Spoiler alert: технические подробности и результаты в наличии.

Оглавление

Проблемы Airflow

Airflow — инструмент, который любят и ненавидят. Он изначально создавался как оркестратор для ETL-задач, но со временем его стали использовать и для обучения моделей, и для инференса, и как универсальный шедулер. Однако на масштабе сотен ML-моделей он начинает мешать больше, чем помогать.

Я выделяю три ключевых недостатка Airflow: масштабируемость, отсутствие Kubernetes-нативности и слабый GitOps. Обсудим подробнее каждый из них.

Масштабируемость

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

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

Вторая проблема масштабируемости — запуск множества подов. В Airflow для запуска подов используются такие сущности, как Spark-оператор и Pod-оператор. В результате при выполнении задачи создаётся довольно много контейнеров: сам Spark, воркер и дополнительный wait-контейнер, который отслеживает завершение джобы.

Более того, проблема с количеством подов проявляется уже на этапе установки самого Airflow. Даже для того, чтобы просто выполнять задачи по расписанию и запускать скрипты, требуется целый набор подов с несколькими контейнерами внутри: scheduler, webserver (в версии 3+ — apiserver), statsd, triggerer и другие.

Отсутствие Kubernetes-нативности

Чтобы запустить Spark Application или просто под, приходится использовать Python-обёртки (SparkOperator, KubernetesPodOperator). На самом деле мы хотим объявить сущность в YAML и применить kubectl apply. Вместо этого приходится держать «двойное описание»: Python-DAG + Jinja-шаблон манифеста. Это усложняет и разработку, и отладку.

Раньше дата сайентист просто объявлял в конфиге название модели, версию Python и другие параметры. Теперь же, чтобы превратить такой конфиг в под, нужно:

  1. сформировать DAG на Python (что плохо вяжется с Kubernetes, где описания делаются в YAML или JSON);

  2. встроить в DAG логику шаблонизации манифеста;

  3. и только после этого получить корректный Spark Application.

Все это усложняет дебаг и добавление новых фич. Даже для того, чтобы просто указать ресурсы задачи, приходится:

  • обновлять библиотеку, которая обрабатывает конфиги;

  • вносить изменения в DAG и в Jinja-шаблон;

  • и только потом вносить это все в Spark Application.

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

Слабый GitOps

Главная проблема — костыльная загрузка DAG'ов.

В Airflow есть возможность подтягивать DAG'и с гита. В чарте можно даже прописать несколько репозиториев, чтобы они скачивали даги в сам шедулер.

Однако на практике при установке на разных платформах у нас периодически с гитом что-то было не так, и контейнер с загрузкой DAG'ов падал. К тому же, у нас в принципе нет DAG'ов, которые хранятся в самих репозиториях, потому что они формируются на основе единого конфига, чтобы поддерживать общий формат и единый подход.

Сравнение с альтернативами

Когда мы решили отказываться от Airflow, встал вопрос, чего мы ждем от нового инструмента. Нам был нужен Kubernetes-нативный инструмент с декларативным управлением через YAML и GitOps-подходом. Изначально мы рассматривали Dagster, и Kubeflow.

Первый кандидат отсеялся сразу, потому что не подходил ни под один из наших критериев. Да, у Dagster более современный и приятный интерфейс, чем у продуктов Apache, и работает он чуть быстрее, и компонентов поменьше, но архитектурно он отличается от Airflow весьма условно: DAG’и все также на Python, не K8s-native.

Второй кандидат, Kubeflow, уже представляет собой целую платформу и формально подходит под наши требования: полностью K8s-нативен, поддерживает GitOps и позволяет описывать пайплайны и на Python, и на YAML. Но его тяжесть убивает: десятки компонентов, сотня CRD, долгие и хрупкие установки. Это монолит, маскирующийся под микросервисы.

Однако в процессе R&D по Kubeflow мы познакомились с его «подкапотной» технологией Argo Workflow. Оказалось, что этот инструмент не просто покрывает наши нужды, но еще и разворачивается максимально просто. В нем нет горы дополнительных компонентов: один контроллер занимается абсолютно всей логикой шедулинга и выдает метрики по отдельным DAG’ам. Есть и второй под — интефейс, однако мы можем им даже не пользоваться.

Фактически Argo Workflows — это тот же движок, который использует Kubeflow. Мы решили взять его напрямую, без громоздкой обёртки.
Фактически Argo Workflows — это тот же движок, который использует Kubeflow. Мы решили взять его напрямую, без громоздкой обёртки.

Как изменился пайплайн

Как я уже сказал, работать с несколькими сотнями репозиториев с моделями через Airflow было довольно грустно:

  1. В каждом репозитории лежал конфиг с названием, версией Python и ресурсами.

  2. Jenkins клонил репозиторий, Python-библиотека на основе конфига генерировала DAG и Kubernetes-манифесты.

  3. Все это отправлялось в Airflow Scheduler, который через какое-то время отображал DAG в интерфейсе.

Проблема в том, что мы фактически дублировали Helm/Kustomize своими велосипедами на Python и Jinja. К тому же пайплайн был медленным: генерация, передача в Scheduler и переваривание DAG’а занимали до 10 минут.

После перехода на Argo CD пайплайн стал проще. Вот так он теперь выглядит для онлайн-моделей:

  1. Jenkins по-прежнему клонит репозиторий.

  2. Собирается окружение (conda + requirements.txt).

  3. Архив кладётся в S3 для переиспользования.

  4. Генерируется Argo CD Application, который объединяет общий Helm-чарт для моделей и индивидуальные values из репозитория модели.

  5. Argo CD синхронизирует кластер с Git: сервис поднимается и поддерживается в актуальном состоянии.

Для batch-моделей добавляется Argo Workflows: Workflow/CronWorkflow описывают задачи, WorkflowTemplate/ClusterWorkflowTemplate позволяют переиспользовать шаги. Каждая задача запускается как отдельный под, логи собираются в интерфейсе и дублируются в S3.

В интерфейсе ArgoCD это выглядит примерно так:

Здесь, в примере с инференсом batch-модели, генерируются две сущности CronWorkflow. Первый — это сам инференс, второй — мониторинг, который периодически следит за работой модели. CronWorkflow по расписанию генерирует DAG’и, которые выполняют некоторые задачи.

Преимущество такого подхода в том, что для модели четко видны отдельные технические таски (в данном случае это alfa-rclone, kinit и dag-extra-params). Все эти таски вместе с инференсом крутятся в одном поде. Из интерфейса сразу понятно, какая модель запущена и какой у нее процесс.

Важно отметить, что Argo позволяет передавать параметры в Workflow. Это позволило нам сделать «режим дебага»: можно поднять контейнер с отладкой и подключиться к нему из VS Code. Это снимает необходимость постоянно коммитить правки и дебажить через print.

Суммарное время нового пайплайна составляет около 4,5 минут, из которых:

  • 20 секунд уходят на шаги Jenkins без скачиваний;

  • около трех минут занимает подготовка окружения (при этом оно кэшируется, и его можно переиспользовать);

  • 7 секунд скачивается архив из S3;

  • примерно одна минута уходит на распаковку.

Итого, новый K8s-native пайплайн без Python-шаблонизаторов ускорился более чем вдвое, и это с учетом подготовки окружения. Git стал единым источником правды, как мы и хотели изначально. Поддержка и дебаг стали проще благодаря единому Helm-чарту, понятным манифестам и интерактивной отладке. В результате количество инцидентов и тикетов от дата-саентистов и инженеров снизилось примерно на 60%.

Однако здесь я хочу отметить, что моя критика в сторону Airflow применима только к тем кейсам, когда работать приходится с большим количеством моделей. Для маленьких команд с десятком DAG’ов этот инструмент все еще остается разумным выбором. Но если у вас Kubernetes, сотни моделей и желание жить по GitOps, то проще, надежнее и логичнее будет сразу строить пайплайны на связке Argo Workflows и Argo CD.

Немного полезных манифестов: CronWorkflow и WorkflowTemplate

Вместо прощания я приведу два примера, приближенных к тому, что используем мы в проде, чтобы было понятнее, как выглядит описание пайплайнов в Argo Workflows. Лишний Helm-синтаксис из примеров был удален.

WorkflowTemplate

Мы вынесли в WorkflowTemplate общие шаги, которые встречаются в 90% моделей: загрузка архива окружения из S3, загрузка кода и запуск скрипта.

Пример
```yaml
apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: model-train-template
  namespace: ml-pipelines
spec:
  entrypoint: train-model
  arguments:
    parameters:
      - name: model-name
        value: "default-model"
      - name: train-script
        value: "train.py"
    artifacts:
      - name: model-code
        path: /workspace/model
        git:
          repo: "https://github.com/your-org/your-model.git"
          revision: "version_1"
      - name: env-archive
        path: /workspace/env  # ← ВАЖНО: это будет РАСПАКОВАННАЯ директория!
        archive:
          none: {}
        s3:
          endpoint: s3.amazonaws.com
          bucket: ml-artifacts
          key: envs/default-env.zip  
          accessKeySecret:
            name: s3-credentials
            key: accessKey
          secretKeySecret:
            name: s3-credentials
            key: secretKey
          region: us-east-1

  templates:
    - name: train-model
      inputs:
        parameters:
          - name: model-name
          - name: train-script
        artifacts:
          - name: model-code
            path: /workspace/model
          - name: env-archive
            path: /workspace/env  # ← Здесь уже распакованная директория окружения!
      container:
        image: python:3.9-slim
        command: ["/bin/bash", "-c"]
        args:
          - |
            set -e

            # Активируем окружение
            conda activate /workspace/env/ 

            # Запускаем обучение
            python /workspace/model/{{inputs.parameters.train-script}}

        env:
          - name: MODEL_NAME
            value: "{{inputs.parameters.model-name}}"

```

В данном случае мы используем функции Argo Workflow, которые позволяют нам скачать код модели нужной версии с Git напрямую, а окружение, созданное на предыдущем этапе, будет загружаться уже с S3 хранилища (как и Git-репозиторий).

Фишка Argo Workflow в том, что он умеет паковать директории с нужным уровнем компрессии и сохранять их в S3. Затем он также умеет их распаковывать. Делается это нативно и за буквально секунды. Нам не требуется использовать какие-либо PVC для хранения — все это можно реализовать прямо в оперативной памяти пода.

CronWorkflow

Теперь любой Workflow или CronWorkflow может ссылаться на этот шаблон через refTemplate, не дублируя код:

Пример
```yaml
apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
  name: daily-fraud-retrain
  namespace: ml-pipelines
spec:
  schedule: "0 2 * * *"  # каждый день в 02:00 UTC
  concurrencyPolicy: "Forbid"  # не запускать, если предыдущий ещё не завершён
  startingDeadlineSeconds: 3600  # запустить в течение часа, если пропустили
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3

  workflowSpec:
    entrypoint: run-model-via-template

    # Переопределяем аргументы шаблона: параметры + артефакты
    arguments:
      parameters:
        - name: model-name
          value: "fraud-detection-daily"
        - name: train-script
          value: "retrain_daily.py --window=7d"
      artifacts:
        - name: model-code
          path: /workspace/model
          git:
            repo: "https://github.com/your-org/fraud-detection.git"
            revision: "main"  # или тег, например: "v4.2.1"
        - name: env-archive
          path: /workspace/env  # ← Argo автоматически распакует ZIP в эту директорию!
          s3:
            endpoint: s3.amazonaws.com
            bucket: ml-artifacts
            key: envs/fraud-env-v4.zip  # ← ZIP-архив с Python-окружением
            accessKeySecret:
              name: s3-credentials
              key: accessKey
            secretKeySecret:
              name: s3-credentials
              key: secretKey
            region: eu-central-1

    templates:
      - name: run-model-via-template
        templateRef:
          name: model-train-template  # ← ссылка на WorkflowTemplate
          template: train-model       # ← имя шаблона внутри
```

Важно: CronWorkflow — это CRD от Argo, не путать с Kubernetes CronJob. Он работает поверх Workflow, а значит, поддерживает сложные DAG’и, параллельные ветки, артефакты, retry-логику и UI.

Такой подход позволил нам:

  • убрать дублирование кода: шаблоны переиспользуются десятками моделей;

  • упростить обновление: поменяли шаблон → все зависящие пайплайны получили фикс;

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

Это и есть настоящий GitOps: декларативные манифесты, хранящиеся в Git, с контролем версий, review и автоматическим применением через Argo CD.

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

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