сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · ETL и оркестрация

Идемпотентность и бэкфилл

Идемпотентность и бэкфил

Идемпотентность - повторный запуск за тот же период даёт тот же результат. Это фундамент DE: без неё ретраи, бэкфилы и «перезапусти вчерашнее» становятся страшными. Собес проверяет, умеешь ли ты писать джобу так, чтобы повтор не плодил дубли.

Стержень: джоба владеет своей партицией и переписывает её целиком, а границы берёт из параметра запуска, не из now().

// Формулировки: «как сделать загрузку идемпотентной?», «как бэкфилить историю?», «что делать с поздними данными?».

Два паттерна идемпотентности

Паттерн номер один - overwrite партиции за период запуска: джоба владеет своей партицией целиком и атомарно её переписывает. Ретрай просто перезаписывает те же данные, дублей не возникает.

Когда партициями не нарезать - MERGE/upsert по бизнес-ключу: повтор обновляет те же строки, а не добавляет новые.

// Антипаттерн - голый INSERT без очистки периода: каждый ретрай удваивает данные, и хранилище тихо копит фантомы, которые всплывут при сверке.

-- идемпотентно: владею партицией периода
DELETE FROM t WHERE dt = :run_date;
INSERT INTO t
SELECT ... WHERE dt = :run_date;
overwrite партиции
перезапись своего периода целиком - база идемпотентности
MERGE / upsert
вставка или обновление по ключу без дублей

Границы периода и бэкфил

Границы обрабатываемого периода берут из параметров запуска (logical date), а не из now(). Джоба, читающая «последние сутки от сейчас», при перезапуске обработает другой кусок данных - идемпотентность потеряна.

Бэкфил - прогон истории тем же кодом. Джобу пишут сразу параметризованной периодом; отдельный «скрипт для истории» гарантирует расхождение логик боевого и исторического расчёта.

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

watermark
отметка «докуда обработано» для инкрементальных загрузок
бэкфил
прогон истории тем же параметризованным кодом

Поздние данные и селф-чек

Late data - записи за прошлые даты доезжают позже. Обрабатывают либо скользящим окном (каждый запуск пересчитывает последние N дней), либо триггером пересборки затронутых партиций. Инкремент по updated_at без ловли поздних правок молча теряет строки.

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

// Reconciliation (сверка загруженного с источником по счётчикам и суммам) ловит именно такие тихие потери, которые не роняют пайплайн, но портят данные.

late arriving data
записи, пришедшие после закрытия своего периода
reconciliation
сверка загруженного с источником по счётчикам/суммам

Как отвечать: «Как сделать пайплайн идемпотентным?»

Главный принцип - джоба должна владеть своим куском данных и переписывать его целиком, а не дописывать. Практически это overwrite партиции за период запуска: перед загрузкой очищаю партицию этого периода и пишу заново, тогда ретрай просто перезапишет те же данные без дублей. Если данные не нарезаются партициями чисто, использую MERGE по бизнес-ключу - повтор обновит те же строки. Второе обязательное условие - границы периода беру из параметра запуска, из logical_date, а не из now(): иначе перезапуск вчерашнего сегодня обработает другой интервал. Благодаря этому один и тот же код работает и в проде, и на бэкфиле истории - просто с разными датами. Плюс закладываю обработку поздних данных скользящим окном и делаю селф-чек после записи, потому что зелёная джоба с пустой партицией опаснее упавшей.

Почему это сильный ответ: назван принцип (владеть и перезаписывать), оба паттерна (overwrite/MERGE), критичная роль logical_date вместо now(), и связка с бэкфилом и селф-чеком.

На чём валят

  • INSERT без очистки периода: каждый ретрай удваивает данные.
  • now() внутри логики: перезапуск за вчера обработал сегодняшние данные.
  • Бэкфил всей истории в 50 параллельных прогонов - положенный источник.
  • Инкремент по updated_at без индекса и без ловли поздних правок - потерянные строки.
  • Партиция перезаписана пустотой из-за бага фильтра - селф-чек бы поймал.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 11, остальные разбираются в тренажёре.

  1. #idempotency_backfill1 / 5
    Почему слепой INSERT в задаче опасен при ретрае и что делать вместо него?
    A)Обычный INSERT при повторном ретрае безопасен и не создаёт дублей
    B)Ретрай задвоит строки; нужен upsert или delete-insert партиции
    C)Ретраи в пайплайнах данных не бывают, проблемы практически нет
    D)Дубли безопасно оставить — их отфильтруют при чтении витрины
    показать ответ и разбор
    +B)Ретрай задвоит строки; нужен upsert или delete-insert партиции

    // разбор: Если задача упала после частичной вставки, ретрай добавит строки повторно — получаются дубли, ломающие агрегаты. Идемпотентные приёмы: delete-insert (удалить партицию за период и вставить заново) или upsert/MERGE по ключу. Тогда сколько бы раз задача ни перезапускалась, результат один и тот же.

  2. #idempotency_backfill2 / 5
    Что такое backfill и почему его нельзя делать без партиционирования по дате?
    A)Backfill — это просто одноразовый запуск сразу всего пайплайна за один текущий день
    B)Прошлые периоды не получится пересчитать, backfill не выходит
    C)Пересчёт истории по датам-партициям; без них будут дубли и пропуски
    D)Backfill обязательно требует полной остановки всех остальных пайплайнов системы
    показать ответ и разбор
    +C)Пересчёт истории по датам-партициям; без них будут дубли и пропуски

    // разбор: Backfill — пересчёт множества прошлых периодов (новая логика, поздние данные). Если данные партиционированы по дате, каждый период пересчитывается независимо и идемпотентно: перезаписал партицию — и всё. Без партиций пересчёт мешается с существующими строками, порождая дубли или пропуски и не давая безопасно повторить.

  3. #idempotency_backfill3 / 5
    Чем at-least-once доставка отличается от exactly-once и почему это важно?
    A)Гарантия at-least-once обеспечивает, что каждое сообщение придёт строго ровно один раз без дублей
    B)At-least-once допускает повторы (нужна идемпотентность); exactly-once — ровно один эффект
    C)Это два синонимичных названия одной гарантии доставки сообщений
    D)Exactly-once означает, что часть сообщений теряется по пути
    показать ответ и разбор
    +B)At-least-once допускает повторы (нужна идемпотентность); exactly-once — ровно один эффект

    // разбор: At-least-once гарантирует, что сообщение не потеряется, но может прийти повторно (после сбоя/переотправки). Exactly-once — что эффект применится ровно один раз. Настоящий exactly-once дорог и не всегда возможен, поэтому на практике берут дешёвый at-least-once плюс идемпотентный обработчик (upsert по ключу) — и получают тот же результат по эффекту.

  4. #idempotency_backfill4 / 5
    Что такое инкрементальная загрузка по high-water mark?
    A)Грузить только записи новее последней обработанной отметки (updated_at/id), а не всё заново
    B)На каждом запуске обязательно полностью перегружать сразу всю таблицу источника целиком с абсолютного нуля
    C)Грузить строки в полностью случайном порядке вообще без всякой отметки времени
    D)Хранить только самую последнюю строку таблицы, удаляя все прошлые записи
    показать ответ и разбор
    +A)Грузить только записи новее последней обработанной отметки (updated_at/id), а не всё заново

    // разбор: High-water mark — сохранённая отметка (максимальный updated_at или id), до которой данные уже загружены. Следующий запуск берёт только записи новее неё, резко сокращая объём против полной перезагрузки. Тонкость — записи, обновлённые задним числом (backdated): по updated_at их поймает CDC или окно пере-захвата, по возрастающему id — нет.

  5. #idempotency_backfill5 / 5
    Нужно переналить (reprocess) месяц данных из-за найденного бага. Как не задвоить строки?
    A)Overwrite/delete-insert по партиции делает переналив идемпотентным
    B)Просто запустить обработку месяца ещё раз обычным INSERT — база сама не станет вставлять дубли
    C)Перед переналивом вручную один раз почистить всю таблицу целиком и залить заново весь месяц
    D)Добавить к каждой строке случайный идентификатор, чтобы дубли просто не совпадали между собой
    показать ответ и разбор
    +A)Overwrite/delete-insert по партиции делает переналив идемпотентным

    // разбор: Безопасный reprocess строят на идемпотентности по партициям: обработка каждой даты сначала удаляет её партицию в целевой таблице и вставляет заново (delete-insert) либо атомарно перезаписывает партицию (INSERT OVERWRITE). Тогда сколько раз ни запусти за 1 марта — в таблице ровно одна версия данных за 1 марта, без дублей. Это требует партиционирования по дате обработки и того, чтобы шаг зависел от logical_date интервала, а не от «сейчас». Слепой повторный INSERT без перезаписи как раз и задваивает.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.