Идемпотентность и бэкфилл
Идемпотентность - повторный запуск за тот же период даёт тот же результат. Это фундамент 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, остальные разбираются в тренажёре.
- Почему слепой INSERT в задаче опасен при ретрае и что делать вместо него?A)Обычный INSERT при повторном ретрае безопасен и не создаёт дублейB)Ретрай задвоит строки; нужен upsert или delete-insert партицииC)Ретраи в пайплайнах данных не бывают, проблемы практически нетD)Дубли безопасно оставить — их отфильтруют при чтении витрины
показать ответ и разбор
+B)Ретрай задвоит строки; нужен upsert или delete-insert партиции// разбор: Если задача упала после частичной вставки, ретрай добавит строки повторно — получаются дубли, ломающие агрегаты. Идемпотентные приёмы: delete-insert (удалить партицию за период и вставить заново) или upsert/MERGE по ключу. Тогда сколько бы раз задача ни перезапускалась, результат один и тот же.
- Что такое backfill и почему его нельзя делать без партиционирования по дате?A)Backfill — это просто одноразовый запуск сразу всего пайплайна за один текущий деньB)Прошлые периоды не получится пересчитать, backfill не выходитC)Пересчёт истории по датам-партициям; без них будут дубли и пропускиD)Backfill обязательно требует полной остановки всех остальных пайплайнов системы
показать ответ и разбор
+C)Пересчёт истории по датам-партициям; без них будут дубли и пропуски// разбор: Backfill — пересчёт множества прошлых периодов (новая логика, поздние данные). Если данные партиционированы по дате, каждый период пересчитывается независимо и идемпотентно: перезаписал партицию — и всё. Без партиций пересчёт мешается с существующими строками, порождая дубли или пропуски и не давая безопасно повторить.
- Чем 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 по ключу) — и получают тот же результат по эффекту.
- Что такое инкрементальная загрузка по 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 — нет.
- Нужно переналить (reprocess) месяц данных из-за найденного бага. Как не задвоить строки?A)Overwrite/delete-insert по партиции делает переналив идемпотентнымB)Просто запустить обработку месяца ещё раз обычным INSERT — база сама не станет вставлять дублиC)Перед переналивом вручную один раз почистить всю таблицу целиком и залить заново весь месяцD)Добавить к каждой строке случайный идентификатор, чтобы дубли просто не совпадали между собой
показать ответ и разбор
+A)Overwrite/delete-insert по партиции делает переналив идемпотентным// разбор: Безопасный reprocess строят на идемпотентности по партициям: обработка каждой даты сначала удаляет её партицию в целевой таблице и вставляет заново (delete-insert) либо атомарно перезаписывает партицию (INSERT OVERWRITE). Тогда сколько раз ни запусти за 1 марта — в таблице ровно одна версия данных за 1 марта, без дублей. Это требует партиционирования по дате обработки и того, чтобы шаг зависел от logical_date интервала, а не от «сейчас». Слепой повторный INSERT без перезаписи как раз и задваивает.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.