Модуль 12. Финал: баттл и защита

Цель модуля: проверить себя по-настоящему. Финальный квиз на 50 вопросов по всем модулям (15 — в интерактивном блоке ниже, 35 — статичным разбором «вопрос → эталонный ответ»), затем — защита MVP «Аналитика OrderFlow»: чек-лист код-ревью и типовые вопросы «почему выбрал X» с эталонными ответами.

Перед началом: Модули 1–11. Защита предполагает собранный MVP (Модуль 11); Модуль 10 (RAG) в экзамен не входит, но вопросы по pgvector бонусом отметит любой интервьюер. Формат: интерактивный квиз (15) + статичный разбор (35) = 50 вопросов. Трудоёмкость: ~5–7 ч (квиз 1.5 ч · доработка проекта 3–4 ч · защита 1 ч).

🔥 Инцидент: ночной звонок

03:14, телефон: дежурный не спит и говорит, что дашборд OrderFlow показывает падение выручки на 60 % за последние два часа. PostgreSQL отвечает по 8 секунд вместо привычных 40 мс, ClickHouse бодр — 150 мс на те же запросы, а лаг консьюмеров Kafka в норме. До утра нужно ответить на один вопрос: клиенты правда перестали покупать — или пайплайн потерял события и занизил выручку на графике?

Первое, что сделали, — перезапустили синкер и подождали пять минут: цифры не изменились. Платежей в Kafka за эти часы столько же, сколько вчера, ошибок в логах ClickHouse нет, а PG при этом заметно тяжелее обычного. Значит, ответ где-то между «мастер деградировал» и «дашборд врёт», и каждая версия стоит проверки инструментом из курса: планы запросов, состояние слотов, чтение из system.parts, честность витрин.

Это экзамен на всё, что было в курсе: от MVCC и планов до CDC и агрегатов ClickHouse. А дальше — настоящий выпускной: финальный баттл из 50 вопросов по всем модулям и защита MVP OrderFlow, где на каждое «почему выбрал X» придётся отвечать с цифрами, альтернативой и ценой решения.

🏆 Часть 1. Интерактивный квиз-баттл (15 вопросов)

Вопросы собраны из всех секций финального баттла: PostgreSQL-внутренности (3), SQL-арсенал (1), расширения и распределённый PG (3), ClickHouse: движки и кластеры (3), пайплайны CDC (3), ситуационные (2). Остальные 35 — ниже, в статичном разборе.

🧩 Quiz "Проверь себя"

1. Вставки в CH затормозили, в логах Too many parts. Корень и лечение?
2. Что входит в идемпотентность Go-синка PostgreSQL → ClickHouse? — выберите все подходящие
3. Нужна доля заказа в дневной выручке, сохранив все строки заказа в выборке. Инструмент?
4. Приложение пишет заказ в PostgreSQL, затем шлёт событие в Kafka и падает между ними. Что получили?
5. SELECT из ReplacingMergeTree показывает дубли. Почему и что делать?
6. DELETE из PG доехал в CH, но строка видна в отчётах. Почему?
7. Одна транзакция открыта 20 часов. Главная беда для PostgreSQL?
8. Диск CH-кластера заполнился, TTL не успевает. Первые шаги?
9. Distributed-таблица analytics.events в кластере 2×2 — что это физически?
10. После failover Debezium пишет «replication slot does not exist». Какого компонента не хватало в архитектуре?
11. Реплика PG отстаёт на минуты, пользователи жалуются на «старые» данные после записи. Что делать немедленно и потом?
12. age(relfrozenxid) большой таблицы приближается к 1.5 млрд. Что это и что делать?
13. orders JOIN payments по tenant_id, обе таблицы в Citus распределены с колокацией по tenant_id. Как выполнится джойн?
14. Запрос «внезапно стал медленным», статистика свежая. Что покажет auto_explain, чего не видно в pg_stat_statements?
15. Что делает pg_partman из перечисленного? — выберите все подходящие

📚 Часть 2. Статичный разбор: 35 вопросов с эталонами

Так отвечайте на собеседовании — коротко, структурно, с ценой решения.

Секция A. PostgreSQL-внутренности (в финале 8, здесь 5)

  1. Вопрос: checkpoint_timeout подняли с 5 минут до часа — что изменится? Эталон: Реже сброс dirty pages: меньше усреднённой нагрузки на IO и меньше WAL (full_page_writes срабатывает реже). Обратная сторона: дольше восстановление после сбоя (больше WAL проигрывать) и выше риск залпа при чекпоинте. Компромисс: 15–30 минут + checkpoint_completion_target = 0.9 — размазать запись.

  2. Вопрос: Почему SELECT count(*) медленный в PG и быстрый в CH? Эталон: В PG MVCC: видимость каждой строки проверяется по снапшоту — только полный обход (параллельный seq scan помогает, но читает всё). В CH count по неселективному запросу считается по метаданным частей и сжатым колонкам. Мораль: счётчики строк держать агрегатами, не гонять через OLTP.

  3. Вопрос: VACUUM vs VACUUM FULL vs pg_repack — когда что? Эталон: VACUUM — рутина: чистит место для переиспользования, но не возвращает ОС. VACUUM FULL — переписывает таблицу и возвращает место, но под EXCLUSIVE-локом: прод стоит. pg_repack — online-перепись почти без блокировок, нужен свободный диск ≈ размера таблицы. Прод-выбор для лечения bloat — pg_repack.

  4. Вопрос: Что такое xid wraparound и как до него не дойти? Эталон: XID 32-битный и сравнивается «по кругу»; несфрижённые старые строки однажды окажутся «из будущего» и катастрофически изменят видимость. Защита: autovacuum freeze по таблицам с возрастом > autovacuum_freeze_max_age, алерт на age(datfrozenxid) (порог ~1.5 млрд), autovacuum никогда не отключать.

  5. Вопрос: Зачем fillfactor и что даёт HOT-обновление? Эталон: fillfactor < 100 оставляет место на странице для новой версии строки → HOT-апдейт: версия на той же странице, индексы не трогаются (если изменённая колонка не индексная). Меньше WAL и меньше index bloat на write-heavy таблицах. Цена: чуть больше места и польза только при обновлениях.

Секция B. SQL-арсенал (в финале 4, здесь 3)

  1. Вопрос: Рекурсивный CTE: обойти дерево категорий и не зациклиться. Эталон: WITH RECURSIVE + UNION (не UNION ALL — дедупликация путей) + массив пути в SELECT с условием NOT child_id = ANY(path). Плюс max_depth-предохранитель. Циклы в данных — вопрос «когда», не «если».

  2. Вопрос: JSONB: когда GIN, когда jsonb_path, когда обычные колонки? Эталон: GIN — для containment-запросов @>; jsonb_path_query — для структурных выборок; поле, по которому фильтруем постоянно, — честная колонка (или expression index по ->>). JSONB — гибкая периферия схемы, не ядро, по которому джойнятся.

  3. Вопрос: Почему полнотекстовый поиск, а не LIKE '%...%'? Эталон: LIKE с ведущим % не использует обычный B-tree и не понимает морфологию. FTS: to_tsvector('russian', body) + GIN, запрос через websearch_to_tsquery, ранжирование ts_rank с весами по заголовку. Для «похожих строк» (опечатки, SKU) — pg_trgm, это другая задача.

Секция C. Расширения и распределённый PG (в финале 8, здесь 5)

  1. Вопрос: Citus reference table: что даёт и чем платим? Эталон: Маленький справочник реплицируется на все шарды — джойны с distributed-таблицами локальны. Цена: каждая запись — распределённая операция (2PC), поэтому reference — для читаемых справочников, не для горячих на запись данных.

  2. Вопрос: Что в Citus не работает «как в одиночном PG» без дизайна? Эталон: UNIQUE/PK/FK должны включать ключ шардирования; глобальные ограничения — только через reference-таблицы; кросс-шардные джойны без колокации — перемешивание или gather на координатор; распределённые транзакции — 2PC с ценой латентности. Дизайн начинается с ключа шардирования, не с таблиц.

  3. Вопрос: Patroni: зачем DCS (etcd) и что делает watchdog? Эталон: DCS — источник истины о лидере: кворум выборов ловит расщепление мозга. Watchdog на узле убивает PostgreSQL, если тот считает себя мастером, а кворум этого не подтверждает, — fencing зомби-мастера, который иначе писал бы в сторону.

  4. Вопрос: Transaction pooling (PgBouncer/Odyssey): что ломается и как жить с prepared statements? Эталон: Сессию-состояние пережить нельзя: SET (только SET LOCAL), advisory locks — в явной сессии, listen/notify — отдельно. Protocol-level prepared statements поддерживаются современным PgBouncer (1.21+); для старых — pgx в режиме без именованных statements. Правило: transaction mode — контрактом, а не надеждой.

  5. Вопрос: postgres_fdw / clickhouse_fdw: где уместны? Эталон: Разовые кросс-системные джойны, ад-хок сверка, скриптовые отчёты. Не для прод-пайплайнов: статистика удалённой стороны неизвестна, данные тащатся по сети без оптимизаций. Пайплайн данных — CDC или батч, FDW — лупа, а не транспорт.

Секция D. ClickHouse: движки и кластеры (в финале 10, здесь 7)

  1. Вопрос: ORDER BY vs PARTITION BY в MergeTree — разделение ролей? Эталон: ORDER BY (ключ сортировки) — главный индекс: гранулы упорядочены, primary key отсекает при чтении; выбирается под WHERE главных запросов. PARTITION BY — физические директории: prune целых партиций, TTL, DROP PARTITION; по времени. Ошибка — партиционировать по высококардинальному полю: тысячи мелких партиций = merge pressure.

  2. Вопрос: Skip-индекс bloom_filter: когда спасает и когда бесполезен? Эталон: Спасает точечные фильтры по колонке вне ключа сортировки (user_id, тег): отсекает гранулы без совпадений. Бесполезен для диапазонов (это работа minmax) и для колонок из ключа сортировки (primary key уже отсекает). Это не B-tree: ORDER BY он не ускоряет.

  3. Вопрос: Кодеки DoubleDelta/Gorilla — для каких колонок? Эталон: DoubleDelta — монотонные ряды (timestamp, счётчики): дельты дельт почти константы. Gorilla — float с локальной стабильностью (метрики, координаты). Поверх — общий ZSTD/LZ4. На случайных данных спецкодек может увеличить размер — меряйте на своих данных.

  4. Вопрос: SummingMergeTree vs AggregatingMergeTree: в чём ловушка первого? Эталон: Summing складывает все числовые колонки при мердже и «какую-то» строку оставляет для нечисловых — годится только для чисто аддитивных мер без деталей. AggregatingMergeTree хранит состояния (sumState, quantileState), комбинируемые корректно (sumSum, quantileMerge). p99 через Summing физически не построить.

  5. Вопрос: ASOF JOIN: сценарий и ограничения. Эталон: «Последнее известное значение на момент события»: цена акции на момент сделки, курс валюты. Требует упорядоченности по времени и ровно один inequality-ключ; остальных ключей джойна может быть несколько, но inequality — один. Идеален для витрин цен, опасен для «всего подряд».

  6. Вопрос: Зачем CHKeeper и сколько узлов? Эталон: Координация ReplicatedMergeTree: выбор лидера мерджей, блокировки, дедуп вставок, метаданные реплик. Raft-кворум: 3 узла (переживает 1 отказ), 5 — для больших кластеров. Это не ZooKeeper — легче, но роль та же; без кворума репликация останавливается.

  7. Вопрос: TTL строк/колонок и tiered storage: как работает и чем платим? Эталон: TTL DELETE чистит при мерджах; ttl_only_drop_parts = 1 позволяет удалять целые части — радикально дешевле. TTL ... TO S3 — холодные части на дешёвом хранилище, читаются медленнее. Изменение TTL на живом объёме — перезапись (мутация): проектируйте TTL сразу.

Секция E. Пайплайны CDC (в финале 10, здесь 7)

  1. Вопрос: Почему логический слот «держит» WAL и что при его потере? Эталон: Слот — обещание не выбрасывать WAL, пока консьюмер не забрал: мёртвый консьюмер = растущий pg_wal до переполнения диска. Удалённый или потерянный слот — история с его позиции недоступна: пересоздание + пересинк (snapshot); «дыру» в WAL не зачитать.

  2. Вопрос: Зачем heartbeat.interval.ms в Debezium, если трафика мало? Эталон: Heartbeat двигает точку слота на тихих базах: без него restart_lsn не ползёт, и диск переполнится при «мёртвом» CDC без единой ошибки. Плюс живые метрики здоровья коннектора. Дешёвая страховка от классического инцидента.

  3. Вопрос: Failover прошёл, слот потерян: дыра в CH за N минут. Как штопать? Эталон: Определить окно: последний LSN в CH против текущего. Дозалить данные из PG за окно (SELECT по updated_at + вставка с версией = LSN) — ReplacingMergeTree схлопнет дубли с уже имеющимися. По Kafka «угадывать» бессмысленно: часть событий потеряна — лечим данными из источника.

  4. Вопрос: schema.history.internal и auto.evolve: что умеют и чего не умеют? Эталон: Debezium хранит историю DDL и пишет схему в сообщения; auto.evolve досоздаёт missing-колонки у консьюмеров. Не умеют: менять PK, даункасты типов, семантику переименований. Поэтому additive-first и контракт схем — ваша дисциплина (Модуль 8.2), а не настройка коннектора.

  5. Вопрос: Одна транзакция на 4GB в PG: что случится в пайплайне? Эталон: Debezium отдаст её как один длинный поток событий: слот надолго «застынет», consumer lag взлетит, возможны проблемы с размером батчей. Лечение — дисциплина батч-операций: дробить большие миграции данных на транзакции.

  6. Вопрос: Kafka engine в CH против собственного Go-синка: критерии выбора? Эталон: Kafka engine + MV — меньше кода, быстрее прототип: но потребление хрупко к схеме, нет сложных трансформаций, ретраи и DLQ управляются хуже, меньше наблюдаемости. Go-синк: полный контроль (батчинг, идемпотентность, метрики, DLQ) ценой ~500 строк ответственности. Для MVP курса — Go-синк; для прототипа — честно Kafka engine.

  7. Вопрос: Как доказать, что PG и CH совпадают (reconciliation)? Эталон: Три уровня: 1) счётчики вставок по минутам/часам PG vs CH — постоянный дашборд; 2) контрольные суммы по окнам (хэш ключей) по расписанию; 3) полный сверочный прогон по горячему окну при инциденте. Всё это метрика с алертом, не ручная сверка раз в квартал.

Секция F. Ситуационные задачи (в финале 10, здесь 8)

  1. Вопрос: Утро понедельника: главный дашборд пуст, в CH нет данных с 03:10. Первые действия? Эталон: 1) Жив ли синк, что в его ошибках. 2) Слот в PG: активен? не потерян на failover? 3) Kafka UI: топик растёт (консьюмер умер) или стоит (сломан Debezium/PG)? Так локализуете сегмент за минуты, чините его и дозаливаете данные окном-пересинком. Не «перезапустить всё подряд».

  2. Вопрос: Кто-то «чинит» данные мутациями в CH: очередь на 10k, вставки деградировали. Действия? Эталон: Остановить источник мутаций. Оценить system.mutations, зависшие — KILL MUTATION. Дать мерджам выдохнуть (вставки — во временное окно). Затем процесс: мутации только через runbook с лимитом одновременных, точечные исправления — через версии синка. Это и инцидент, и сигнал сломанного процесса.

  3. Вопрос: Продакт: «хочу реалтайм-аналитику с задержкой меньше секунды». Ваш разговор? Эталон: Вопрос «какая задача решается секундой?». Для дашборда 5–30 сек лага незаметны, а «меньше секунды» стоит: синхронные вставки, мелкие батчи → parts и merge pressure → дорогой кластер. «Почти-реалтайм 2–5 сек» часто решает задачу в десять раз дешевле — показываете график лага и предлагаете SLI вместо лозунга.

  4. Вопрос: pg_stat_statements: mean 0.4ms, но total — 40% времени БД. Что за зверь? Эталон: N+1 из приложения: миллионы дешёвых вызовов. Чинить кодом: батч-выборка (WHERE id = ANY($1)), джойн, кэш. Метрика-подсказка — огромный calls при крошечном mean; индекс тут не поможет, он уже хорош.

  5. Вопрос: После ALTER TABLE в PG в дашборде CH-колонка пустая. Где рассинхрон? Эталон: Контракт схем не выполнили: проверить, отдаёт ли Debezium поле (сообщение в Kafka), знает ли маппинг синка, есть ли колонка в CH. Историю — перезалить окном с версией. Урок: additive-first и порядок «читатели → продюсер» (Модуль 8.2) — это про такой день.

  6. Вопрос: Диск PG-мастера 92%, раздут pg_wal. Порядок действий? Эталон: 1) Виновник: pg_replication_slots — неактивный/отставший слот (обычно умерший Debezium). 2) Решение по слоту: поднять консьюмер или осознанно удалить слот (теряется CDC-история → пересинк). 3) WAL руками не удалять никогда. 4) Алерты на размер pg_wal и лаг слота — чтобы в следующий раз узнать раньше.

  7. Вопрос: Бизнес: «события храним 7 лет, дашборд смотрит 90 дней». Дизайн? Эталон: Один ClickHouse, два температурных слоя: raw с TTL 7 лет на дешёвом хранилище (S3 tiered storage), MV-агрегаты под дашборд — на быстрых дисках (они крошечные). Не «вторая СУБД», а температура. Плюс retention Kafka и пересинк из PG как страховка источника.

  8. Вопрос: Одна реплика шарда CH недоступна. Что видят пользователи? Эталон: Distributed-запросы едут на живую реплику (load_balancing, max_replica_delay_for_distributed_queries) — почти незаметно, если лаг живой реплики мал. Вставки через Distributed ждут живого шарда. Опасность — «тихая» деградация: кластер уже без запаса, алерт на доступность реплик обязателен до, а не после второй поломки.

🛡 Часть 3. Защита MVP «Аналитика OrderFlow»

Чек-лист код-ревью перед защитой

Финальный проход по чек-листу из Модуля 11 — плюс пункты защиты:

  • Каждый checkpoint S1–S6 подтверждён артефактом: скриншот Grafana, вывод EXPLAIN/команды в README.
  • Сбой-тесты автоматизированы: kill мастера, kill синкера, pause sink — инвариант «ноль потерь, дубли переживаются» прогоняется скриптом.
  • Три лага на дашборде: слот, consumer group, счётчики PG vs CH — с алертами.
  • Витрины восстановимы перезаливкой из raw — процедура описана и отработана.
  • Постмортем намеренного инцидента S6 в README: таймлайн, обнаружение, действие, профилактика.
  • Каждый ADR отвечает на «почему», с альтернативой и ценой — не только на «как поднять».

Типовые вопросы защиты и эталонные ответы

«Почему ReplacingMergeTree, а не Collapsing?» Эталон: Collapsing требует строгих пар insert/cancel (sign ±1) и терпит непарность только до мерджа: одно потерянное событие — строка навсегда «живая» или «удалённая». Наш поток (Kafka at-least-once, фейловеры) парности не гарантирует. ReplacingMergeTree + версия из LSN: дубли безвредны, порядок доставки не важен, DELETE — новая версия с флагом. Цена: дубли видны до мерджа — FINAL/argMax в витринах.

«Почему CDC, а не dual-write?» Эталон: dual-write — два независимых утверждения без общей транзакции: падение между ними — вечный рассинхрон, который ретраем не ловится. CDC читает WAL — единственный источник правды PG: порядок гарантирован, потеря возможна только вместе с потерей самого PG. Цена CDC — лаг и инфраструктура (Debezium, слоты), и она мониторится (Модуль 6.5), а не прячется.

«Что сломается при 10x нагрузке?» Эталон: называю конкретную первую точку из S6 — [ваша цифра: например, батч синка не успевает → consumer lag; за ним merge pressure в CH; параллельно connection storm мимо PgBouncer]. План: консьюмеры по числу партиций, крупнее батчи, async_insert в CH, pool size. «Поднимем ещё подов» — не ответ; у меня есть график лага под нагрузкой и точка перегиба.

«Когда добавите Citus?» Эталон: по лестнице Модуля 9: сначала партиции (уже есть) и read-replicas (есть) — они дешевле. Citus — когда упрусь в запись/объём одного узла и рабочее множество перестанет влезать. Ключ шардирования осознан заранее (tenant_id), FK/unique спроектированы с ним — миграция планом на два спринта, а не авралом.

«Почему Kafka между Debezium и ClickHouse, а не напрямую?» Эталон: Kafka — буфер и лог: простой CH/синка копится и доигрывается (retention), несколько проекций читают один поток, replay для пересинков. Прямой путь (Kafka engine или http) экономит компонент, но отнимает буфер, replay и наблюдаемость. Цена Kafka — второй stateful-компонент; при малых масштабах честно рассмотреть движковую таблицу (Модуль 4.8) — и я могу сказать, на каких объёмах поменяю решение.

«Где у вас exactly-once?» Эталон: нигде «по сети»: at-least-once до CH, идемпотентный эффект — ReplacingMergeTree по версии LSN + FINAL в чтении, оффсеты коммятся после вставки. Тест S4: kill -9 синкера посреди батча — дублей в отчётах нет. Это exactly-once по бизнес-эффекту, а не по маркетинговому слайду.

🎓 Итоги курса

За двенадцать модулей вы прошли путь от «зачем две СУБД» до защищённого MVP с HA-кластером PostgreSQL, CDC-пайплайном через Kafka и кластером ClickHouse 2×2. Теперь вы можете:

  • Выбирать хранилище по профилю нагрузки — и называть цену решения вслух.
  • Читать PostgreSQL изнутри: MVCC и bloat, планы и буферы, блокировки, пулы.
  • Осознанно применять экосистему расширений: Citus, Patroni, pg_partman, pg_failover_slots, Debezium, pgvector.
  • Строить MergeTree-модели: ключи сортировки, MV-агрегаты, дедупликация версиями, слои raw → витрины.
  • Эксплуатировать пару СУБД: бэкапы с PITR, мониторинг с алертами на реальные болезни, миграции без остановки пайплайна, безопасность.
  • Масштабировать и защищать решение в споре: «что сломается при 10x» — не угроза, а ваш бэклог.

Что читать и смотреть дальше

  • «PostgreSQL 16 изнутри» — Е. Рогов (postgrespro): систематизация Модуля 2 — MVCC, WAL, vacuum из первых рук.
  • «PostgreSQL: Up and Running» — Р. Отворски, Е. Калинина: практика эксплуатации без воды.
  • «ClickHouse in Action» — Setchenkov: движки, кластеры, рецепты под реальные нагрузки.
  • «Designing Data-Intensive Applications» — M. Kleppmann, глава 11 «Streams»: фундамент, на котором стоят все CDC-пайплайны курса.
  • blog.altinity.com — эксплуатация ClickHouse-кластеров в бою: мерджи, рескелинг, Keeper, обновления.
  • Документация: postgresql.org/docs, clickhouse.com/docs, github.com/pgvector, debezium.io/documentation.

Если «Аналитика OrderFlow» дожила до защиты — у вас на руках работающий стенд из двух миров данных с честным пайплайном между ними, которым можно гордиться в портфолио. До встречи в следующем курсе.