Data Engineering: от Python и SQL до продакшн-пайплайнов

Практический курс для перехода от ML/DL к профессии Data Engineer. Вы прокачаете Python и SQL под задачи инженера данных, научитесь проектировать ETL/ELT-пайплайны, оркестрировать их в Airflow и контейнеризировать всю инфраструктуру в Docker. Каждая глава — конкретный навык с чек-листами и лучшими практиками, минимум теории, максимум применимого кода.

1. Основы Python и SQL для инженера данных

Основы Python и SQL для инженера данных

Почему ML-разработчик, который пишет нейросети на PyTorch, может провалить собеседование на позицию джуниор-инженера данных? Потому что профессия Data Engineer требует не умения строить модели, а умения перемещать, трансформировать и хранить данные — а для этого нужны глубокие знания Python и SQL, которые отличаются от тех, что применяются в машинном обучении.

Вы знакомы с Python через призму pandas и sklearn. Но инженер данных пишет на Python совсем другое: парсеры API, ETL-скрипты, валидаторы схем, коннекторы к базам. А SQL для него — это не «достать датасет для модели», а инструмент оптимизации запросов на миллиарды строк. Разберёмся, что именно нужно прокачать.

Python: что важно инженеру данных

Ваш опыт построения ML/DL моделей — сильная база. Но есть зоны, где инженер данных действует иначе.

Работа с файлами и потоками данных. Инженер данных постоянно читает и пишет файлы: CSV, JSON, Parquet, Avro. Не через pd.read_csv один раз, а потоково, чанками, с обработкой ошибок на каждой итерации.

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

Работа с API и источниками данных. В отличие от ML, где данные обычно лежат в файле, инженер данных часто тянет их из REST API, очередей сообщений или баз. Библиотека requests — ваш лучший друг, но важно уметь обрабатывать rate limits, retry-логику и пагинацию.

Обработка ошибок и логирование. В ML вы можете позволить себе упасть с traceback — перезапустите ноутбук. Инженер данных не имеет такой роскоши: пайплайн работает ночью, без присмотра. Поэтому try/except с конкретными исключениями и структурированное логирование через модуль logging — обязательны.

Работа с базами данных из Python. Библиотека SQLAlchemy позволяет абстрагироваться от конкретной СУБД и писать код, который работает с PostgreSQL, MySQL или SQLite. Но для инженера данных критически важно понимать разницу между постоянным соединением и пулом соединений — в продакшне десятки пайплайнов стучатся в одну базу одновременно.

SQL: от запросов к инженерному мышлению

Вы знаете основы SQL. Но инженер данных должен уметь не просто доставать данные, а проектировать схемы, оптимизировать запросы и понимать, как СУБД выполняет запрос внутри.

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

Здесь SUM(...) OVER(...) считает нарастающий итог по каждому клиенту, а ROW_NUMBER() нумерует заказы клиента по убыванию суммы. Без оконных функций это потребовало бы самоджойна таблицы — дорого и громоздко.

CTE (Common Table Expressions) делают сложные запросы читаемыми. Вместо вложенных подзапросов на три уровня вы пишете поименованные блоки, которые можно переиспользовать.

EXPLAIN ANALYZE — инструмент, который должен стать привычкой. Перед тем как запустить запрос на продакшн-данных, проверьте его план выполнения. Вы увидите, какие индексы используются, где происходит сканирование всей таблицы (Seq Scan), и где можно добавить индекс или переписать запрос.

Если в выводе видно Seq Scan на таблице с миллионами строк — это сигнал добавить составной индекс:

Типы JOIN «под капотом». Важно понимать, что JOIN — это не просто «объединить таблицы». СУБД выбирает стратегию: Nested Loop (для маленьких таблиц), Hash Join (для средних, без индекса), Merge Join (для отсортированных данных). Знание этих механизмов помогает писать запросы, которые оптимизатор выполнит эффективно.

Где Python и SQL пересекаются на практике

На практике инженер данных постоянно комбинирует оба инструмента. Типичный сценарий: Python тянет данные из API, валидирует и очищает их, а затем загружает в базу через SQL. Или наоборот — SQL формирует агрегаты, а Python забирает результат и отправляет в другую систему.

Ключевой паттерн — не загружать всё в память Python, если то же самое можно сделать средствами SQL. Трансформация внутри базы данных почти всегда быстрее, чем выгрузка в Python и обратная загрузка. Python нужен там, где SQL бессилен: работа с внешними API, сложная бизнес-логика с условиями, интеграция с очередями сообщений.

> Если вы можете сделать трансформацию в SQL — делайте в SQL. Python оставляйте для оркестрации, интеграции и тех трансформаций, которые невозможно выразить декларативно.

2. Проектирование ETL/ELT пайплайнов

Проектирование ETL/ELT пайплайнов

Почему пайплайн, который отлично работал на тестовых данных, ломается в продакшене на второй неделе? Потому что между «скрипт, который перекладывает данные» и «надёжный пайплайн» — пропасть. Инженер данных строит не разовые скрипты, а системы, которые работают без присмотра, переживают сбои, принимают данные с изменённой схемой и дают предсказуемый результат при повторном запуске. Именно этому учит проектирование ETL/ELT пайплайнов.

В предыдущей статье вы прокачали Python и SQL. Теперь соберём их в архитектуру.

ETL против ELT: не синтаксис, а архитектурное решение

ETL (Extract → Transform → Load) — данные сначала извлекаются из источника, трансформируются в промежуточном слое и только потом загружаются в целевое хранилище. ELT (Extract → Load → Transform) — данные сначала попадают в хранилище «как есть», а трансформируются уже внутри него.

Разница не в порядке букв. Разница в том, где живёт бизнес-логика и когда вы платите за вычисления.

| Критерий | ETL | ELT | |---|---|---| | Где трансформация | Вне хранилища (Spark, Python) | Внутри хранилища (SQL) | | Когда использовать | Жёсткие схемы, регулируемые данные | Облачные DWH, часто меняющиеся модели | | Стоимость | Платите за compute до загрузки | Платите за compute при запросе | | Гибкость | Низкая — изменение логики = перепроцессинг | Высокая — сырые данные уже на месте |

В 2026 году большинство команд выбирают ELT, потому что облачные хранилища (BigQuery, Snowflake, Redshift) делают вычисления внутри SQL дешёвыми и масштабируемыми. Но ETL остаётся обязательным для финансовых данных, где валидация должна происходить до попадания в хранилище.

Шесть слоёв пайплайна

Любой пайплайн, независимо от инструментов, состоит из шести слоёв. Пропуск любого из них — технический долг, который вернётся ночным алертом.

1. Ingestion (извлечение). Данные покидают источник. Здесь критична идемпотентность — повторный запуск ingestion не должен создать дубликаты. Способы: чтение из конкретной партиции по дате, использование watermark (отметка «последний обработанный ID»), или CDC (Change Data Capture), который передаёт только изменения.

2. Staging (буферная зона). Сырые данные ложатся в промежуточное хранилище без изменений. Это ваша страховка: если трансформация сломается, вы сможете переиграть её отсюда, а не тянуть данные из источника заново. Формат — тот же, что у источника (JSON, CSV, Avro). Никакой бизнес-логики на этом слое быть не должно.

3. Validation (валидация). Проверка схемы, типов, обязательных полей, диапазонов значений. Невалидные записи не удаляются — они попадают в карантин (quarantine) с метаданными: какая проверка не пройдена, когда, какая запись.

4. Transformation (трансформация). Бизнес-логика: джойны, агрегаты, enrichment, приведение типов. Здесь работаете SQL или PySpark. Главный принцип — каждая трансформация идемпотентна и детерминированна. Один и тот же вход даёт один и тот же выход, сколько бы раз вы ни запускали задачу.

5. Loading (загрузка). Данные попадают в целевое хранилище. Используйте MERGE (upsert) вместо INSERT — это ключ к идемпотентности. Партиционируйте по дате, чтобы можно было перезаписать конкретный день, не трогая остальные.

6. Serving (предоставление). Данные доступны потребителям: дашборды, ML-модели, аналитики. На этом слое создаются агрегаты, витрины, materialized views.

Паттерн idempotency: почему это не опция, а требование

Идемпотентность — свойство, при котором повторный запуск пайплайна даёт тот же результат, что и первый. Без неё вы получаете дубли, расхождения в метриках и невозможность безопасного retry.

Три практических приёма:

  • Partition overwrite. Перезаписывайте данные по ключу партиции (например, date=2024-01-15). При повторном запуске та же партиция перезаписывается целиком.
  • MERGE вместо INSERT. Как показано выше — обновляет существующие записи, вставляет новые.
  • Watermark tracking. Храните в отдельной таблице метку «последняя обработанная запись». При запуске читайте только данные после этой метки.
  • Обработка ошибок: три стратегии

    Не все ошибки равны. Инженер данных должен различать три типа и выбирать правильную стратегию для каждого.

    Транзиентные ошибки (сеть, rate limit, таймаут) — retry с экспоненциальной задержкой. Три-пять попыток, затем алерт.

    Ошибка данных (невалидная запись, пропущенное поле) — карантин. Запись сохраняется с описанием ошибки, пайплайн продолжает работу.

    Ошибка логики (баг в коде трансформации) — остановка пайплайна и алерт. Нельзя продолжать обработку, если бизнес-логика работает неправильно.

    Схема-эволюция: как не сломать потребителей

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

    Принципы:

  • Только аддитивные изменения. Добавляйте новые колонки, никогда не удаляйте и не переименовывайте без периода деприкации.
  • Валидация на границе. На входе ingestion-слоя проверяйте, что схема соответствует ожиданиям. Несовпадение — не падение, а карантин + алерт.
  • Версируйте breaking changes. Если схема меняется несовместимо — создайте v2, позвольте потребителям мигрировать в своём темпе.
  • Практический пример: пайплайн из API в DWH

    Соберём всё вместе. Допустим, вы тянете заказы из REST API и загружаете в PostgreSQL.

    Обратите внимание: каждый слой — отдельная функция. Это не просто «чистый код», это требование оркестрации — в следующей статье Airflow будет запускать каждый слой как отдельную задачу с независимым retry.

    Ловушки, о которых не пишут в туториалах

    Поздно приходящие данные. Заказ, созданный в 23:59, может попасть в логи только в 00:05 следующего дня. Если ваш пайплайн партиционирует по календарной дате, эта запись пропадёт. Решение — watermark с запасом (lookback window): при обработке партиции за 15 января читайте данные начиная с 14 января.

    Скрытые дубли. API может вернуть одну и ту же запись дважды. Idempotent loading через MERGE / ON CONFLICT решает это, но только если у вас есть надёжный первичный ключ.

    Рост карантина. Если карантин растёт каждый день — это не проблема данных, это проблема источника. Настройте алерт на процент невалидных записей. Если он превышает 5% — пора разговаривать с командой-владельцем источника.

    ---

    Спроектировать пайплайн — значит принять десятки архитектурных решений до написания первой строки кода. ETL или ELT? Партиционирование по дате или по ID? Карантин или падение? Каждое решение определяет, будет ли ваша система надёжной или хрупкой. В следующей статье мы научим эти пайплайны работать по расписанию и восстанавливаться после сбоев с помощью Airflow.

    3. Оркестрация процессов с Apache Airflow

    Оркестрация процессов с Apache Airflow

    Представьте: вы написали ETL-скрипт, он работает, данные текут. Но кто запускает его в 3 ночи? Кто перезапустит, если упадёт? Кто не даст ему запуститься дважды и надублировать данные? Кто свяжет пять таких скриптов в цепочку, где каждый следующий ждёт завершения предыдущего? Ответ — оркестратор. И в мире Data Engineering стандарт де-факто — Apache Airflow.

    Airflow — это не планировщик задач типа cron. Это платформа для определения, расписывания и мониторинга рабочих процессов (workflows) в виде направленных ациклических графов (DAG). Каждый DAG — это описание того, что должно произойти, в каком порядке и при каких условиях.

    DAG: единица организации в Airflow

    DAG (Directed Acyclic Graph) — это набор задач с определёнными зависимостями. «Направленный» — потому что у каждой задачи есть направление: от предшественника к последователю. «Ациклический» — потому что циклов быть не может: задача A не может зависеть от B, которая зависит от A.

    Каждый DAG — это Python-файл. Airflow сканирует папку dags/, находит в ней определения DAG и регистрирует их. Вот минимальный рабочий DAG:

    Оператор >> задаёт зависимости: extract завершается → запускается transform → затем load. Если extract упадёт, остальные не запустятся.

    Операторы: строительные блоки DAG

    Оператор — это описание одной единицы работы. Airflow поставляет десятки встроенных операторов, но три используются чаще всего.

    PythonOperator — выполняет Python-функцию. Универсальный инструмент для всего, что нельзя выразить готовым оператором.

    SQLExecuteQueryOperator — выполняет SQL-запрос. Удобен для трансформаций внутри базы данных.

    Обратите внимание на {{ ds }} — это Jinja-шаблон, который Airflow подставляет автоматически: дата выполнения DAG в формате YYYY-MM-DD. Это ключ к идемпотентности — каждый запуск обрабатывает конкретную дату.

    BashOperator — выполняет shell-команду. Используется для запуска скриптов, CLI-утилит, Docker-контейнеров.

    Передача данных между задачами: XCom

    Задачи в Airflow выполняются изолированно — часто на разных воркерах. Как передать результат одной задачи другой?

    XCom (Cross-communication) — механизм для обмена небольшими данными между задачами. По умолчанию функция, помеченная @task, автоматически пушит свой возвращаемый значение в XCom.

    > XCom предназначен для метаданных (пути к файлам, даты партиций, счётчики строк), а не для самих данных. Передавать через XCom DataFrame на миллион строк — плохая практика. Для больших объёмов пишите в S3/HDFS, а в XCom передавайте путь к файлу.

    Расписание и catchup

    Параметр schedule определяет, когда DAG запускается. Варианты:

  • Cron-выражение: "0 3 *" — каждый день в 3:00
  • Предустановки: "@daily", "@hourly", "@weekly"
  • None — только ручной запуск
  • Dataset-based (Airflow 3) — запуск при обновлении данных
  • Параметр catchup контролирует поведение при первом деплое DAG. Если catchup=True и start_date — год назад, Airflow создаст 365 запусков — по одному на каждый пропущенный день. В продакшене почти всегда ставьте catchup=False, чтобы не запустить лавину исторических пересчётов.

    Сенсоры: ждём данных, а не время

    Типичная ошибка — запускать DAG по расписанию и надеяться, что данные к этому моменту уже пришли. Если источник задерживается — пайплайн работает с неполными данными.

    Сенсоры решают эту проблему: задача ждёт наступления условия, а не конкретного времени.

    Сенсор проверяет условие каждые poke_interval секунд. Если файл не появился за timeout — задача падает.

    Типичные ошибки и лучшие практики

    Тяжёлый код на верхнем уровне

    Airflow парсит DAG-файлы регулярно (каждые 30–60 секунд). Весь код, который выполняется вне функций операторов, выполняется при каждом парсинге. Если вы делаете API-вызов или импортируете тяжёлую библиотеку на верхнем уровне — вы замедляете планировщик.

    Не используйте datetime.now() внутри задач

    datetime.now() возвращает текущее время — разное при каждом запуске. Это ломает идемпотентность. Используйте data_interval_start или ds из контекста Airflow — они детерминированы для каждого запуска.

    Не храните состояние на диске

    При использовании Celery Executor или Kubernetes Executor задачи выполняются на разных машинах. Файл, созданный одной задачей, недоступен другой. Передавайте данные через XCom (метаданные) или через удалённое хранилище (S3, пути к файлам).

    default_args для DRY

    Повторяющиеся параметры — owner, retries, retry_delay, conn_id — выносите в default_args:

    Полный пример: ETL-пайплайн в Airflow

    Соберём пайплайн из предыдущей статьи в DAG с тремя задачами, карантином и watermark.

    Обратите внимание: extract_orders возвращает путь к файлу, validate_and_load принимает его как аргумент — Airflow автоматически прокидывает через XCom. Никаких глобальных переменных, никакого состояния на диске между задачами.

    ---

    Airflow — это не просто «планировщик». Это язык описания пайплайнов, в котором каждая задача — транзакция, каждая зависимость — явная, а каждый запуск — воспроизводимый. В следующей статье мы упакуем всё это в Docker-контейнеры, чтобы пайплайн можно было запустить на любой машине одной командой.

    4. Контейнеризация инфраструктуры данных в Docker

    Контейнеризация инфраструктуры данных в Docker

    «У меня работает, а у тебя нет» — фраза, которая убила больше времени, чем баги в продакшене. Инженер данных работает с десятками компонентов: базы данных, Airflow, Kafka, Redis, Spark. Каждый требует своей версии Python, системных библиотек, конфигурационных файлов. Без контейнеризации воспроизведение окружения превращается в ритуал с молитвами. Docker решает эту проблему: каждый компонент живёт в изолированном контейнере с фиксированным окружением, и запускается одной командой.

    Docker для инженера данных: зачем и когда

    Docker — это не «DevOps-инструмент». Для инженера данных это три вещи:

    Воспроизводимость. Ваш коллега клонирует репозиторий, запускает docker compose up, и у него поднимается та же база PostgreSQL той же версии с теми же тестовыми данными. Никаких «установи PostgreSQL 15, создай базу, выполни эти 12 миграций».

    Изоляция. Пайплайн для проекта A работает с Python 3.11 и pandas 2.1. Пайплайн для проекта B — с Python 3.9 и pandas 1.5. В Docker они не конфликтуют, потому что каждый живёт в своём контейнере.

    Переносимость. Контейнер, который работает на вашем ноутбуке, работает на CI-сервере, на staging-сервере и в продакшене. Окружение зашито в образ, а не описано в Wiki-статье из 47 шагов.

    Базовые概念: образ, контейнер, Dockerfile

    Образ (image) — шаблон. Это зафиксированное состояние файловой системы с установленным ПО. Образ неизменяемый: вы не «меняете» образ, а создаёте новый.

    Контейнер (container) — запущенный экземпляр образа. Один образ может порождать много контейнеров. Контейнер имеет свою файловую систему, сетевое пространство и процессы, но ядро ОС общее с хостом.

    Dockerfile — инструкция по сборке образа. Каждая строка Dockerfile — это слой (layer). Docker кэширует слои: если вы изменили только последнюю строку, все предыдущие слои берутся из кэша.

    Dockerfile для ETL-скрипта

    Начнём с простого: контейнеризация Python-скрипта, который делает ETL.

    Принцип: зависимости копируются раньше кода. Если вы изменили одну строку в pipeline.py, Docker пересоберёт только последний слой. Если бы вы скопировали всё одной командой COPY . ., каждый запуск docker build переустанавливал бы все библиотеки.

    Docker Compose: вся инфраструктура одной командой

    Реальный пайплайн — это не один контейнер. Это PostgreSQL + Airflow + MinIO (локальный S3) + ваш ETL-скрипт. Docker Compose описывает мультиконтейнерное приложение в одном YAML-файле.

    Ключевые моменты:

    depends_on с condition: service_healthy. Airflow не запустится, пока PostgreSQL не пройдёт healthcheck. Без этого Airflow упадёт с ошибкой подключения к базе — и вам придётся перезапускать вручную.

    Volumes для данных и кода. Папка ./dags монтируется в контейнер Airflow — вы редактируете DAG на хосте, и они сразу видны в контейнере. Volume pg_data сохраняет данные PostgreSQL между перезапусками.

    Переменные окружения для конфигурации. Пароли, строки подключения, настройки — всё через environment. Нет захардкоженных значений в коде.

    Тестирование пайплайнов в Docker

    Docker позволяет тестировать пайплайн целиком, включая зависимости, до деплоя в продакшен.

    Unit-тесты трансформаций запускайте в том же контейнере, что и пайплайн:

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

    Флаг -v удаляет volumes — каждый запуск integration-теста начинается с чистого листа.

    Многоэтапная сборка: меньше размер, больше безопасность

    Образ с установленным компилятором C, dev-зависимостями и исходниками весит в 5–10 раз больше, чем нужно для запуска. Многоэтапная сборка (multi-stage build) решает это: один этап собирает артефакт, второй — только запускает.

    Docker Compose автоматически подставляет значения из .env. Файл .env добавляется в .gitignore.

    Типичные ошибки

    Запуск всего под root. По умолчанию процессы в контейнере работают от root. Добавляйте пользователя:

    Отсутствие .dockerignore. Без него в контекст сборки попадают .git, __pycache__, .env, виртуальные окружения. Создайте .dockerignore:

    Хранение данных внутри контейнера. Контейнер — эфемерный. При перезапуске всё, что лежит внутри, пропадает. Базы данных, логи, staging-файлы — только в volumes или внешних хранилищах.

    Сборка «толстых» образов. Образ на 2 ГБ — это 5 минут на pull в CI. Используйте slim-базы, многоэтапную сборку и убирайте кэш: pip install --no-cache-dir.

    ---

    Docker — это не дополнение к пайплайну, а его фундамент. Контейнеризация превращает «установи эти 15 компонентов» в docker compose up. В следующей статье мы соберём все знания курса в практический чек-лист, который можно распечатать и использовать как аудит-гайд для каждого нового проекта.

    5. Лучшие практики и чек-листы для Data Engineering

    Лучшие практики и чек-листы для Data Engineering

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

    Чек-лист проектирования пайплайна

    Прежде чем писать первую строку кода, пройдитесь по этому списку. Каждый пункт — конкретное архитектурное решение.

    Разделение ответственности. Ingestion отделена от transformation, transformation от loading. Каждый этап — отдельная задача в Airflow, отдельная функция, отдельный модуль. Это даёт независимый retry, независимое тестирование и возможность заменить один этап без переписывания остальных.

    Моделирование как DAG. Каждый этап имеет явные входы и выходы. Независимые этапы выполняются параллельно. Зависимости описаны декларативно, а не через тайминги.

    Данные ждут данных, а не время. Используйте сенсоры и триггеры вместо жёсткого расписания. Если источник задерживается на два часа, пайплайн должен подождать, а не обработать неполные данные в 3:00 ночи.

    Одна задача — одна ответственность. Ingestion не валидирует. Validation не трансформирует. Каждый этап делает одну вещь и делает её хорошо.

    Явные зависимости. Если пайплайн B зависит от пайплайна A — это должно быть описано в оркестраторе, а не в голове инженера. Не полагайтесь на предположения о времени выполнения.

    Чек-лист качества данных

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

    Валидация схемы на ingestion. При поступлении данных проверяйте: имена колонок, типы, nullable-ограничения. Сдвиг схемы (schema drift) должен ловиться до того, как он распространится downstream.

    Проверка полноты. Обязательные поля не содержат null. Если customer_id может быть null в таблице заказов — downstream-джойны молча потеряют строки.

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

    Карантин, а не /dev/null. Невалидные записи не удаляются. Они попадают в карантинную таблицу с метаданными: какая проверка не пройдена, когда, какая запись. Это даёт возможность диагностировать проблемы в источнике.

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

    | Метрика | Что ловит | Порог алерта | |---|---|---| | Null rate по ключевым полям | Проблемы в источнике | | | Duplicate rate по PK | Двойная загрузка | | | Row count deviation | Пропущенные данные | от среднего | | Freshness (возраст последней записи) | Задержка источника | |

    Чек-лист надёжности и идемпотентности

    Идемпотентность каждого пайплайна. Повторный запуск даёт тот же результат. Используйте partition overwrite или MERGE — никогда не делайте blind INSERT.

    Retry с экспоненциальной задержкой. Транзиентные ошибки (сеть, rate limit) решаются сами. Три-пять попыток с задержкой 1, 2, 4, 8 секунд — затем алерт.

    Dead-letter queue. Записи, которые не удалось обработать, попадают в очередь для разбора, а не в лог, который никто не читает.

    Checkpoint progress. После обработки каждой партии фиксируйте прогресс. При сбое —_resume_ с последнего чекпоинта, а не перезапуск с начала.

    Проектирование под сбой. Каждый компонент упадёт. Заранее определите поведение для каждого сценария: retry, skip-and-log, alert, halt. Не принимайте решения о поведении при сбое во время сбоя.

    Чек-лист управления схемой

    Схема — это API. Имена колонок — поля. Таблицы — эндпоинты. Потребители — клиенты. Изменение схемы без координации так же опасно, как изменение API без версионирования.

    Только аддитивные изменения. Добавляйте новые колонки. Никогда не удаляйте и не переименовывайте без периода деприкации.

    Контракты на границах. Валидируйте входящую схему на ingestion. Валидируйте исходящую схему на serving.

    Версируйте breaking changes. Когда схема должна измениться несовместимо — создайте v2. Потребители мигрируют в своём темпе.

    Документируйте каждую колонку. Имя, тип, описание, источник, владелец. Если инженер не может найти эту информацию за 30 секунд — она не задокументирована.

    Чек-лист тестирования

    Schema-тесты на каждом запуске. Существование колонок, типы данных, not-null ограничения. Быстрые, дешёвые, ловят 80% проблем.

    Uniqueness и null-check на первичных ключах. Два самых влиятельных теста качества данных. Добавьте их сегодня.

    Сравнение row count с базовым уровнем. Алерт, если количество строк отклоняется более чем на 20% от скользящего среднего. Ловит пропуски и неожиданные всплески.

    Тесты трансформаций на фикстурах. Маленькие наборы данных с известным ожидаемым результатом. Запускайте в CI перед деплоем изменений пайплайна.

    Регрессионные тесты бизнес-метрик. Общая выручка, количество уникальных клиентов, конверсия — сравнивайте с предыдущим запуском. Расхождение — алерт.

    Чек-лист наблюдаемости

    Freshness на каждую таблицу. Временная метка самой свежей строки. Алерт при превышении SLA. Эта одна метрика ловит больше проблем, чем любой другой мониторинг.

    Алерты на бизнес-импакт, а не на каждую ошибку. Нарушение SLA, регрессия качества, аномальное изменение объёма — это алерты. Транзиентные retry и плановое обслуживание — нет.

    Структурированное логирование. JSON-формат: имя пайплайна, этап, batch ID, временная метка, количество строк, статус. Ищемо, парсимо, фильтруемо.

    Data lineage. Откуда приходят данные каждой таблицы и куда уходят. Column-level lineage превращает расследование «цифры неправильные» из полудня в 10 минут обхода графа.

    Ежеквартальный аудит наблюдаемости. Актуальны ли алерты? Верны ли пороги? Используются ли дашборды? Убирайте неактуальные алерты и обновляйте базовые уровни.

    Чек-лист Airflow

    Нет тяжёлого кода на верхнем уровне. Импорты pandas, API-вызовы, подключения к базам — только внутри функций операторов. Планировщик парсит DAG-файлы каждые 30–60 секунд.

    Нет datetime.now() внутри задач. Используйте data_interval_start или ds из контекста Airflow. Текущее время меняется при каждом запуске — это ломает идемпотентность.

    Нет состояния на локальном диске. При Celery/Kubernetes Executor задачи выполняются на разных машинах. Передавайте данные через XCom (метаданные) или через удалённое хранилище (пути к файлам).

    default_args для DRY. owner, retries, retry_delay, conn_id — определяйте один раз, применяйте ко всем задачам.

    Каждая задача — идемпотентная транзакция. Повторный запуск задачи не должен создавать дубликаты или неполные результаты.

    Чек-лист Docker

    Многоэтапная сборка. Сборочный этап с компилятором и dev-зависимостями. Финальный этап — slim-образ только с runtime.

    Зависимости раньше кода. COPY requirements.txt и pip install — отдельный слой. Код — отдельный слой. Пересборка при изменении кода не трогает кэш зависимостей.

    .dockerignore. Исключайте .git, __pycache__, .env, .venv, node_modules.

    Переменные окружения для секретов. Пароли, ключи, токены — через .env-файлы, не через Dockerfile. Файл .env в .gitignore.

    Healthcheck для сервисов. Каждый сервис в docker-compose.yml имеет healthcheck. Зависимые сервисы используют depends_on с condition: service_healthy.

    Не храните данные в контейнере. Базы данных, логи, staging — только в volumes или внешних хранилищах.

    Ежеквартальный аудит

    Распечатайте эти чек-листы. Проведите 30-минутную встречу с командой. Отметьте, что уже реализовано, определите три пункта с наибольшим импактом, которые ещё не выполнены, и запланируйте их как инженерную работу — не как aspirational goals на Wiki-странице. Практики работают только тогда, когда они реализованы.

    > Лучшие практики легко написать и сложно применить. Они перечисляют принципы без контекста, советы без приоритизации и правила без объяснений, когда их нарушать. Этот чек-лист — другое. Он привязан к конкретным результатам и организован по категориям, которые имеют наибольшее значение.

    Каждый неотмеченный пункт — это либо технический долг, либо осознанный компромисс. Знайте, какой из двух.