Репликация, шардирование и масштабирование
Кратко о теме
Заголовок раздела «Кратко о теме»Почти вся эта подтема выводится из одного факта: PostgreSQL сначала пишет намерение в журнал, а потом меняет страницы данных. Журнал называется WAL (Write-Ahead Log), он последовательный, состоит из сегментов по 16 МБ, и каждая запись в нём адресуется LSN — монотонно растущим смещением в журнале. Коммит считается зафиксированным, когда его WAL-запись доведена до диска (fsync), а не когда изменённые страницы попали в файлы данных; страницы догоняются позже чекпоинтером. Из этого одного механизма растут три вещи: crash recovery (после падения повторно проигрываем WAL от последнего чекпоинта), физическая репликация (тот же самый поток WAL шлём на другой сервер и проигрываем там) и логическая репликация (декодируем WAL обратно в логические изменения строк).
Вторая опора — MVCC. PostgreSQL не перезаписывает строку при UPDATE, а создаёт новую версию и помечает старую как невидимую после транзакции xmax. Значит, UPDATE и DELETE не освобождают место, а порождают мёртвые версии строк, которые физически остаются в страницах. Убирает их VACUUM; делает это автоматически autovacuum. Отсюда же берётся вторая обязанность вакуума — «заморозка» (freeze) старых транзакционных идентификаторов, потому что XID 32-битный и без заморозки счётчик рано или поздно переполнится. Так что vacuum — это не «дефрагментация ради красоты», а обязательное условие работоспособности кластера.
Третья опора — иерархия способов масштабирования, и на собеседовании важно назвать их в правильном порядке, а не сразу кричать «шардирование». Сначала выжимаем однин узел: запросы и индексы, схема, пул соединений, кэш, вертикальный апгрейд. Потом отделяем чтение: реплики для read-only нагрузки — это масштабирование чтения, но не записи. Потом разрезаем таблицы вертикально (вынести редко используемые/тяжёлые колонки, разнести домены по разным базам) и горизонтально в пределах одного сервера (партиционирование). И только когда упирается запись или объём данных на одном узле — шардирование, то есть распределение непересекающихся подмножеств данных по разным независимым серверам.
Ключевая пара, которую путают чаще всего: репликация — это копии одних и тех же данных, шардирование — разные данные в разных местах. Репликация даёт отказоустойчивость и масштабирование чтения, но не помогает с записью (каждая реплика применяет весь поток изменений) и не помогает с объёмом (на каждом узле лежит полная база). Шардирование даёт масштабирование записи и объёма, но не даёт отказоустойчивости — падение шарда уносит его часть данных. В проде их всегда комбинируют: N шардов, каждый — реплицированная группа из 2–3 узлов.
И четвёртое: любая репликация, кроме полностью синхронной с remote_apply, означает отставание реплики, а значит — чтение устаревших данных. Это не баг, это фундаментальный компромисс из CAP/PACELC: либо ждём подтверждения от реплик (растёт латентность записи и падает доступность при их недоступности), либо не ждём (получаем окно рассогласования и риск потери последних коммитов при аварийном переключении).
Вопросы и ответы
Заголовок раздела «Вопросы и ответы»Что такое автовакуум в Postgres?
Заголовок раздела «Что такое автовакуум в Postgres?»Коротко. Autovacuum — это фоновая подсистема PostgreSQL, которая автоматически запускает VACUUM и ANALYZE для таблиц, накопивших достаточно мёртвых версий строк или изменений. Она решает две задачи: возвращает место, занятое мёртвыми кортежами, в свободное пространство таблицы (FSM) и «замораживает» старые XID, не давая счётчику транзакций переполниться.
Глубже. Архитектурно это autovacuum launcher, который раз в autovacuum_naptime (по умолчанию 1 минута) просыпается и по каждой базе решает, кого обслужить, и до autovacuum_max_workers (по умолчанию 3) рабочих процессов. Порог срабатывания для таблицы: autovacuum_vacuum_threshold (50) + autovacuum_vacuum_scale_factor (0.2) × число строк, то есть по умолчанию вакуум приходит после изменения ~20 % таблицы — для больших таблиц это слишком редко, и scale_factor обычно снижают до 0.01–0.05 через ALTER TABLE ... SET (autovacuum_vacuum_scale_factor = 0.02). С PostgreSQL 13 есть отдельный триггер по вставкам (autovacuum_vacuum_insert_threshold, по умолчанию 1000) — раньше append-only таблицы не вакуумировались вообще, пока не наступал anti-wraparound. Чтобы не убивать диск, воркер тормозит себя cost-based-задержкой (autovacuum_vacuum_cost_delay, с PG12 по умолчанию 2 мс, autovacuum_vacuum_cost_limit 200) — это первое, что крутят, когда автовакуум «не успевает». Отдельная ветка — anti-wraparound autovacuum: при возрасте таблицы больше autovacuum_freeze_max_age (200 млн транзакций) вакуум запускается принудительно, даже если автовакуум выключен, и его нельзя игнорировать; в PG14 добавили failsafe-режим (vacuum_failsafe_age), который в критической ситуации отключает cost delay и пропускает работу с индексами, лишь бы успеть заморозить.
Какие критерии и стратегии партиционирования данных использовали в проектах?
Заголовок раздела «Какие критерии и стратегии партиционирования данных использовали в проектах?»Коротко. Основной критерий — по какому предикату режется 90 % запросов и как удаляются старые данные. Практически всегда это RANGE по времени для журналов/событий (партиция на день или месяц, старые отцепляются DETACH вместо DELETE), LIST по региону/типу для географически или доменно разделённых данных и HASH по tenant_id/user_id, когда нужно просто равномерно разложить большую таблицу.
Глубже. Каркас ответа, если спрашивают «в проектах»: назвать таблицу и её размер, назвать ключ партиционирования и почему он совпадает с фильтром запросов, назвать выигрыш (partition pruning, дешёвое удаление истории, вакуум по частям, локальные индексы меньше и лезут в память) и назвать цену. Цена реальная: партиционный ключ обязан входить в любое UNIQUE/PRIMARY KEY ограничение на партиционированной таблице, глобальных уникальных индексов нет; слишком мелкие партиции (тысячи) удорожают планирование и раздувают число блокировок и файлов; enable_partitionwise_join и enable_partitionwise_aggregate по умолчанию выключены, их включают осознанно; при обновлении строки со сменой партиции происходит перемещение (delete+insert), что ломает ctid-зависимую логику. Полезные вещи из свежих версий: ATTACH PARTITION CONCURRENTLY/DETACH PARTITION CONCURRENTLY (PG14) позволяют отцеплять историю без долгой AccessExclusiveLock, а runtime pruning отсекает партиции уже на этапе выполнения по параметрам плана.
CREATE TABLE events ( id bigint GENERATED ALWAYS AS IDENTITY, tenant_id bigint NOT NULL, ts timestamptz NOT NULL, payload jsonb, PRIMARY KEY (id, ts) -- ключ партиционирования обязан входить в PK) PARTITION BY RANGE (ts);
CREATE TABLE events_2026_08 PARTITION OF events FOR VALUES FROM ('2026-08-01') TO ('2026-09-01');Что такое репликация и шардирование базы данных?
Заголовок раздела «Что такое репликация и шардирование базы данных?»Коротко. Репликация — копирование одних и тех же данных на несколько узлов: у каждой реплики полный набор данных. Шардирование — разрезание данных на непересекающиеся части по ключу: каждый шард хранит только свой кусок. Репликация масштабирует чтение и даёт отказоустойчивость, шардирование масштабирует запись и объём.
Глубже. Их принято сравнивать по четырём осям. Объём данных: репликация не помогает (на каждом узле полная копия), шардирование делит объём на N. Запись: репликация не помогает (все реплики применяют весь поток WAL, то есть выполняют ту же работу по записи), шардирование делит поток записи на N. Чтение: репликация помогает линейно, шардирование помогает только для запросов, попадающих в один шард. Надёжность: репликация повышает (есть с чего поднять), шардирование понижает (N узлов — N точек отказа, и падение одного делает недоступной 1/N данных). Поэтому в проде это не «или-или»: сначала шардируем на N групп, потом каждую группу реплицируем.
Database scaling methods
Заголовок раздела «Database scaling methods»Коротко. По возрастанию сложности: оптимизация запросов и индексов → пул соединений и кэш → вертикальное масштабирование (CPU/RAM/NVMe) → read-реплики → партиционирование и архивация холодных данных → функциональное (вертикальное) разделение по доменам → шардирование → специализированные хранилища под конкретные нагрузки (OLAP-колонночные, поисковые, time-series).
Глубже. Смысл порядка в том, что каждый следующий шаг на порядок дороже в эксплуатации, а выигрыш от первых шагов часто на порядок больше. Один пропущенный индекс или seq scan в горячем запросе легко даёт 100×, а шардирование даёт максимум N× ценой распределённых транзакций, ресharding-а и отсутствия глобальных джойнов. Отдельно стоит назвать «разгрузочные» приёмы, которые не масштабируют базу, но снимают с неё нагрузку: кэш перед базой (Redis, кэш приложений), материализованные представления и предагрегаты, вынос аналитики в отдельный контур через CDC (логическая репликация → ClickHouse/DWH), батчинг и очереди перед записью, чтобы сгладить пики.
Что такое репликация БД?
Заголовок раздела «Что такое репликация БД?»Коротко. Репликация — это поддержание согласованных копий данных на нескольких серверах: изменения, сделанные на основном узле, автоматически переносятся на реплики. Нужна для отказоустойчивости (есть узел, с которого поднимаемся при аварии), масштабирования чтения, снятия бэкапов и тяжёлой аналитики без нагрузки на основной сервер, а также для катастрофоустойчивости — реплика в другом ЦОД.
Глубже. Классификация, которую ждут на собеседовании, идёт по трём независимым осям. По уровню переноса: физическая (побайтовые изменения страниц через WAL) и логическая (изменения строк). По моменту подтверждения: асинхронная (коммит не ждёт реплику) и синхронная (ждёт). По топологии: master–standby (одна точка записи), каскадная (реплика реплики), multi-master (несколько точек записи с разрешением конфликтов). В PostgreSQL «из коробки» есть физическая streaming-репликация и логическая (PUBLICATION/SUBSCRIPTION с PG10); multi-master только через внешние решения.
Что такое шардирование БД?
Заголовок раздела «Что такое шардирование БД?»Коротко. Шардирование — горизонтальное разбиение данных по ключу на независимые узлы (шарды), каждый из которых хранит и обслуживает только свою часть данных. Клиент или прокси по значению ключа вычисляет, на каком шарде живёт запись, и идёт туда.
Глубже. Ключевое отличие от партиционирования — шарды живут на разных серверах и не имеют общего планировщика, каталога и транзакционного менеджера. Отсюда все ограничения: нет обычных JOIN между шардами (нужен либо fan-out и склейка в приложении, либо дублирование справочников на всех шардах), нет глобальных уникальных индексов и обычных sequence (нужны UUIDv7/ULID/Snowflake или диапазоны ID на шард), нет обычных транзакций через шарды (нужен two-phase commit или saga/идемпотентность), и добавление шарда требует переливки данных. Поэтому шардирование внедряют, когда упёрлись именно в объём данных или в поток записи на одном узле, — раньше это чистый убыток.
Что такое VACUUM?
Заголовок раздела «Что такое VACUUM?»Коротко. VACUUM — команда, которая сканирует таблицу, помечает страницы с мёртвыми (невидимыми ни одной активной транзакции) версиями строк как содержащие свободное место, чистит соответствующие записи в индексах, обновляет карту видимости и замораживает старые XID. Место при этом не возвращается операционной системе — оно переиспользуется той же таблицей.
Глубже. Обычный VACUUM берёт ShareUpdateExclusiveLock: конкурентные SELECT/INSERT/UPDATE/DELETE не блокируются, блокируются только другой вакуум, ANALYZE и часть DDL. Работает он в два-три прохода: собирает TID мёртвых кортежей, чистит по ним индексы, потом освобождает сами кортежи в куче; при большом числе мёртвых строк и малом maintenance_work_mem проходов по индексам будет несколько (в PG17 структуру хранения TID переделали на TidStore, сняв старое ограничение в 1 ГБ и заметно сократив число проходов). Побочные, но важные эффекты: обновление relfrozenxid, обновление карты видимости — без неё не работают index-only scan, и усечение хвостовых полностью пустых страниц, для которого вакуум на короткое время пытается взять AccessExclusiveLock (это тот самый источник внезапных конфликтов и лага на репликах). Ключевое, что путают: вакуум не дефрагментирует таблицу и не уменьшает файл в общем случае — для этого нужен VACUUM FULL или pg_repack.
Почему триггер NOTIFY не срабатывает на реплике?
Заголовок раздела «Почему триггер NOTIFY не срабатывает на реплике?»Коротко. Потому что физическая реплика не выполняет SQL-операторы и не запускает триггеры — она лишь побайтово применяет WAL-записи об изменениях страниц. Триггер отработал на мастере, а на реплику приехал уже результат его работы. Плюс сам механизм LISTEN/NOTIFY не журналируется в WAL: очередь уведомлений живёт в разделяемой памяти и служебном каталоге, а не в WAL, поэтому нотификации в принципе не переносятся на standby.
Глубже. Два независимых уровня отказа, и на собеседовании стоит назвать оба. Первый: на hot standby весь кластер в состоянии recovery и доступен только на чтение, а NOTIFY считается пишущей операцией — попытка выполнить его в recovery упирается в ошибку. Второй: даже если бы standby хотел, ему нечего исполнять — redo-поток состоит из записей вида «на странице X, в слоте Y, вот такие байты», в нём нет ни строк SQL, ни вызовов функций. Поэтому паттерн «триггер на таблице шлёт NOTIFY, приложение слушает» работает только на узле-мастере, и подписчиков надо вешать на него (или переключать LISTEN при failover). Отдельно: при логической репликации триггеры на подписчике по умолчанию тоже не срабатывают, потому что apply-worker работает в режиме session_replication_role = replica, и чтобы триггер отработал, его нужно явно объявить ALTER TABLE ... ENABLE ALWAYS TRIGGER (или ENABLE REPLICA TRIGGER). Если задача — надёжно доставлять события наружу, правильный ответ не NOTIFY, а transactional outbox + чтение через логическую репликацию/CDC: NOTIFY не переживает рестарт, не имеет подтверждений и молча теряется, если очередь переполнена.
Что такое шардинг и репликация?
Заголовок раздела «Что такое шардинг и репликация?»Коротко. См. выше «Что такое репликация и шардирование базы данных»: репликация — копии одних и тех же данных ради отказоустойчивости и масштабирования чтения; шардинг — разные данные на разных узлах ради масштабирования записи и объёма. Формулировка вопроса другая, ответ тот же; полезно добавить, что на практике они ортогональны и применяются вместе.
Глубже. Хорошее завершение ответа — одна фраза про типовую целевую топологию: «N шард-групп, в каждой один primary и 1–2 standby, роутинг по ключу шардирования на уровне приложения или прокси, чтение неактуальных данных допускаем только там, где это явно разрешено бизнес-требованием».
Какие репликации существуют и в чем их различия?
Заголовок раздела «Какие репликации существуют и в чем их различия?»Коротко. Физическая (streaming) — переносит изменения страниц через WAL, реплика побайтово идентична мастеру, весь кластер целиком, только чтение. Логическая — декодирует WAL в изменения строк и применяет их как обычные DML, можно выбирать отдельные таблицы, реплика доступна на запись, версии Postgres могут различаться. Плюс ортогональная ось: синхронная/асинхронная, и топологии master–standby / каскадная / multi-master.
Глубже. Сравнение по существу. Физическая: дешевле по CPU, лаг обычно меньше, переносит всё (включая DDL и последовательности), но требует одинаковой мажорной версии и архитектуры, реплицирует кластер целиком и не даёт писать на реплике. Логическая: гибкая (частичный набор таблиц, фильтры строк и колонок с PG15, разные мажорные версии — на ней делают апгрейды с почти нулевым простоем, консолидация нескольких баз в одну, CDC в аналитику), но дороже, не переносит DDL, требует wal_level = logical, у таблицы должен быть PK или REPLICA IDENTITY FULL, а последовательности не синхронизируются и их приходится доводить руками при переключении. Ещё различают репликацию по способу передачи: streaming (непрерывный поток от walsender) и log shipping через archive_command/restore_command (перенос готовых 16-МБ сегментов, лаг до сегмента) — их часто комбинируют, чтобы реплика могла догнаться из архива, если отстала слишком сильно.
Какие есть способы синхронизации реплик в pg?
Заголовок раздела «Какие есть способы синхронизации реплик в pg?»Коротко. Три уровня. Streaming-репликация в реальном времени (walsender → walreceiver) — основной способ. Архивная доставка сегментов WAL (archive_command на мастере, restore_command на реплике) — как резервный канал и для PITR. И начальная синхронизация — pg_basebackup для физической реплики или initial table copy (COPY) для логической. Плюс настройка степени синхронности через synchronous_commit.
Глубже. synchronous_commit — это и есть «способы синхронизации» в узком смысле: off (не ждём даже локального fsync, окно потери — до wal_writer_delay), local (ждём только локальный диск), remote_write (реплика получила и отдала в ОС), on (реплика сделала fsync — значение по умолчанию, и оно означает синхронность только если узел перечислен в synchronous_standby_names), remote_apply (реплика уже применила запись, то есть чтение с неё гарантированно увидит коммит). Кворум задаётся там же: ANY 2 (s1, s2, s3) — ждём любых двух, FIRST 1 (s1, s2) — приоритетный список. Чтобы мастер не удалил WAL, который реплика ещё не забрала, используют physical replication slots (pg_create_physical_replication_slot) — с оговоркой, что забытый слот от мёртвой реплики забьёт диск мастера, и это ограничивают через max_slot_wal_keep_size (PG13+). Чтобы длинные запросы на реплике не отменялись из-за vacuum-конфликтов, включают hot_standby_feedback = on — ценой удержания мёртвых строк на мастере.
Возможна ли ситуация когда реплика отстает от мастера?
Заголовок раздела «Возможна ли ситуация когда реплика отстает от мастера?»Коротко. Да, и при асинхронной репликации это норма, а не исключение. Отставание измеряется в байтах WAL и в секундах: pg_stat_replication на мастере (sent_lsn, write_lsn, flush_lsn, replay_lsn, write_lag, flush_lag, replay_lag) и pg_last_xact_replay_timestamp() на реплике.
Глубже. Причины делятся на «не доехало» и «не применилось». Не доехало: узкая или загруженная сеть между ЦОД, всплеск записи на мастере (массовый UPDATE, CREATE INDEX, VACUUM FULL, pg_repack — все они генерируют много WAL). Не применилось: redo на реплике выполняет один startup-процесс последовательно, тогда как на мастере параллельно работали десятки backend-ов, поэтому при интенсивной записи реплика упирается в один ядро и в случайные чтения с диска (частично лечится recovery_prefetch, включённым по умолчанию с PG15, и большим shared_buffers); длинный запрос на hot standby держит блокировку и конфликтует с redo-записью — либо запрос отменяется по max_standby_streaming_delay, либо, если лимит большой, redo стоит и лаг растёт. Практический вывод для приложения: с реплики нельзя читать «своё только что записанное» без явных мер — либо read-your-writes через мастер, либо ожидание нужного LSN на реплике (pg_wal_lsn_diff / pg_last_wal_replay_lsn()), либо synchronous_commit = remote_apply для критичных транзакций.
Работал ли с шардированием?
Заголовок раздела «Работал ли с шардированием?»Коротко. Это вопрос про опыт — отвечать надо конкретикой: что шардировали, по какому ключу, чем роутили, как решали кросс-шардовые запросы и как переливали данные. Если реального опыта нет, честно сказать это и сразу перейти к тому, как бы вы это спроектировали, — отказ «не работал» без продолжения закрывает ветку разговора.
Глубже. Каркас сильного ответа: (1) исходная боль в цифрах — «таблица 4 ТБ, 30k INSERT/s, один primary упирался в диск и в autovacuum, реплики не помогали, потому что проблема была в записи»; (2) выбор ключа — «шардировали по tenant_id, потому что 95 % запросов однотенантные, а распределение по тенантам мы проверили заранее и вынесли пять крупнейших в отдельные шарды»; (3) механика — «hash(tenant_id) → один из 4096 виртуальных бакетов → бакет привязан к физическому шарду через таблицу-маршрутизатор, поэтому добавление шарда — это переезд бакетов, а не пересчёт всего»; (4) кросс-шардовые сценарии — «отчёты собираются fan-out-запросами в Go с errgroup и таймаутом, справочники дублируются на все шарды логической репликацией»; (5) миграция — «двойная запись, фоновая переливка, сверка, переключение чтения, удаление старого»; (6) что бы сделали иначе. Типичная ошибка кандидата — назвать «шардированием» партиционирование в одной базе или несколько независимых БД разных сервисов.
Для какой задачи шардирование лучше подходит, чем репликация?
Заголовок раздела «Для какой задачи шардирование лучше подходит, чем репликация?»Коротко. Когда упирается запись или объём. Реплики не масштабируют запись — каждая из них применяет весь поток изменений, то есть выполняет ту же самую работу, что и мастер, — и не масштабируют объём, потому что на каждом узле лежит полная копия базы. Шардирование делит и то, и другое на N.
Глубже. Конкретные признаки, что нужен именно шардинг: primary насыщен по IOPS/CPU на записи, а не на чтении; рабочий набор перестал помещаться в память и index scan превратился в случайные чтения; таблица настолько велика, что автовакуум и CREATE INDEX перестали укладываться в окно; бэкап и восстановление занимают неприемлемое время; данные естественно изолированы по тенанту/пользователю и кросс-сущностные запросы редки. Обратный признак — читающая нагрузка при умеренной записи и умеренном объёме: там реплики (плюс кэш) дают тот же эффект в разы дешевле.
Do you know anything about auto-vacuum (in Postgres)?
Заголовок раздела «Do you know anything about auto-vacuum (in Postgres)?»Коротко. См. выше «Что такое автовакуум в Postgres?». Отличие только в языке вопроса: если собеседование на английском, ответ тот же — фоновая подсистема (launcher + до autovacuum_max_workers воркеров), которая по порогам threshold + scale_factor × reltuples запускает VACUUM и ANALYZE, освобождает место от мёртвых кортежей и предотвращает transaction ID wraparound.
Глубже. На английском собеседовании обычно ждут ещё пару фраз про диагностику: pg_stat_user_tables (n_dead_tup, last_autovacuum, autovacuum_count), pg_stat_progress_vacuum для наблюдения за текущим прогоном, pg_stat_activity с wait_event для поиска блокировок, и понимание, что «autovacuum is not keeping up» лечится не выключением автовакуума (это самый частый и самый разрушительный совет), а более агрессивными настройками на конкретной таблице и уменьшением cost delay.
Can sharding be done within one database?
Заголовок раздела «Can sharding be done within one database?»Коротко. В строгом смысле нет: шардирование по определению — распределение данных по независимым узлам. В пределах одного инстанса аналог называется партиционированием (declarative partitioning) — оно даёт pruning, локальные индексы и дешёвое удаление истории, но не масштабирует ни CPU, ни диск, ни поток записи, потому что всё исполняет один сервер.
Глубже. Есть промежуточные варианты, и их полезно назвать. Первый — партиционированная таблица, часть партиций которой является foreign-таблицами через postgres_fdw: логически это одна таблица в одной базе, физически данные лежат на других серверах; PostgreSQL умеет pruning и (при async_capable = true, PG14+) асинхронно опрашивать удалённые партиции, но полноценного распределённого планировщика и транзакций там нет. Второй — расширение Citus, которое превращает узел в координатор и раскладывает шарды по воркерам, сохраняя единую точку входа. Третий, чисто прикладной, — «шардирование по схемам/таблицам» внутри одного сервера как подготовка к будущему разъезду: код уже роутит по ключу, а физически всё пока в одной базе. Последнее часто и имеют в виду в вопросе — и это правильный ответ «да, как подготовительный шаг, но выигрыша в производительности он сам по себе не даёт».
What is sharding?
Заголовок раздела «What is sharding?»Коротко. Sharding is horizontal partitioning of a dataset across multiple independent database nodes: each shard holds a disjoint subset of rows selected by a shard key, and the router (application, proxy or coordinator) resolves the key to a node. Скажем это же по-русски: разрезание данных по ключу на независимые серверы ради масштабирования объёма и записи.
Глубже. Три вещи, которые стоит добавить сразу, чтобы ответ выглядел зрелым: shard key определяет всё (его нельзя дёшево поменять потом), кросс-шардовые операции — джойны, глобальная уникальность, транзакции — либо запрещены, либо требуют отдельного механизма (2PC, saga, идемпотентные ключи), и resharding надо продумать до запуска — обычно через consistent hashing или фиксированное большое число виртуальных бакетов, которые переезжают между физическими узлами.
Caching, sharding, and load balancing;
Заголовок раздела «Caching, sharding, and load balancing;»Коротко. Три разных инструмента против трёх разных узких мест: кэш снимает повторяющееся чтение перед базой, шардирование делит данные и поток записи между узлами, балансировка распределяет соединения между уже существующими узлами (в базах — прежде всего разводит чтение на реплики, а запись на primary).
Глубже. Кэш: уровни (кэш планов и shared_buffers в самой БД, кэш приложения in-process, внешний Redis/Memcached), стратегии (cache-aside — самая частая, write-through, write-behind), и вечные проблемы — инвалидация, stale reads, cache stampede (лечится single-flight/golang.org/x/sync/singleflight, jitter в TTL и вероятностным ранним обновлением) и холодный старт после рестарта. Балансировка для PostgreSQL: L4-балансировщик или виртуальный IP перед primary для отказоустойчивости, отдельный DNS/endpoint для реплик, разведение read/write в приложении (в Go — два пула *pgxpool.Pool) либо через прокси, умеющее разбирать запросы (PgBouncer сам не роутит по типу запроса, для этого берут pgpool-II, pgcat, ProxySQL в мире MySQL). Важный нюанс, о котором забывают: перед шардированием и балансировкой почти всегда стоит поставить пул соединений — PostgreSQL создаёт процесс на соединение, и тысяча коннектов от Go-сервисов убивает сервер быстрее, чем сам объём данных.
Как масштабировать базу: синхронная репликация, шардирование?
Заголовок раздела «Как масштабировать базу: синхронная репликация, шардирование?»Коротко. Синхронная репликация — это про надёжность и про гарантию нулевой потери коммитов при failover, а не про производительность: она делает запись медленнее (коммит ждёт сеть и fsync на реплике). Масштабируют базу асинхронные реплики для чтения и шардирование для записи и объёма; синхронную репликацию включают там, где нельзя терять транзакции.
Глубже. На этом вопросе интервьюер обычно проверяет, понимаете ли вы стоимость синхронности. Каждый коммит с synchronous_commit = on и синхронной репликой добавляет как минимум один сетевой round-trip плюс fsync на реплике — внутри одного ЦОД это доли миллисекунды, между ЦОД в разных регионах это уже десятки миллисекунд на транзакцию. Плюс доступность: с одной синхронной репликой и FIRST 1 её падение вешает запись на мастере до таймаута/переконфигурации, поэтому корректная конфигурация — минимум две потенциально синхронных реплики и кворум ANY 1 (s1, s2). Разумный компромисс, который стоит назвать: synchronous_commit можно ставить на уровне транзакции, то есть критичные операции (платежи) писать синхронно, а массовую телеметрию — с local или off.
Как работает репликация?
Заголовок раздела «Как работает репликация?»Коротко. На мастере каждая транзакция сначала записывает изменения в WAL. Процесс walsender читает WAL и потоком отдаёт его по репликационному протоколу процессу walreceiver на реплике; тот пишет полученное в свой WAL и подтверждает, а startup-процесс проигрывает эти записи (redo), применяя изменения к страницам. Реплика периодически шлёт назад свои LSN — write/flush/replay, — по которым мастер понимает лаг и решает, отпускать ли синхронный коммит.
Глубже. Начинается всё с базовой копии: pg_basebackup снимает согласованный снимок каталога данных и стартовую позицию WAL, дальше реплика догоняется потоком. Чтобы мастер не переиспользовал сегменты WAL, которые реплика ещё не забрала, создают replication slot; альтернатива — wal_keep_size и архив. Для логической репликации схема другая: слот с плагином вывода (pgoutput) декодирует WAL в поток логических изменений, учитывая только зафиксированные транзакции, apply-worker на подписчике выполняет их как обычные INSERT/UPDATE/DELETE по REPLICA IDENTITY. С PG16 большие транзакции могут применяться параллельно несколькими apply-воркерами и стало возможным вести логическую репликацию с standby-узла.
Как можно масштабировать базы данных?
Заголовок раздела «Как можно масштабировать базы данных?»Коротко. См. выше «Database scaling methods». По сути два измерения: вертикально (мощнее железо на одном узле) и горизонтально (больше узлов) — а горизонтально, в свою очередь, отдельно для чтения (реплики, кэш) и отдельно для записи и объёма (шардирование, партиционирование, вынос доменов в отдельные базы).
Глубже. Чтобы ответ не был перечислением, стоит привязать выбор к метрике, которая упёрлась: CPU на запросах → индексы/переписывание запросов/реплики; IOPS на записи → диски, wal_compression, батчинг, шардирование; размер рабочего набора → память, партиционирование, архивация; число соединений → пул; латентность одиночного запроса → в первую очередь план запроса, никакое горизонтальное масштабирование его не ускорит.
Для чего нужен WAL-журнал в PostgreSQL?
Заголовок раздела «Для чего нужен WAL-журнал в PostgreSQL?»Коротко. WAL обеспечивает durability и атомарность: изменение сначала попадает в журнал и на диск, и только потом (лениво, чекпоинтером) — в файлы данных. Благодаря этому после сбоя кластер восстанавливается проигрыванием WAL с последнего чекпоинта, а сам журнал служит источником для потоковой репликации, PITR-восстановления и логического декодирования (CDC).
Глубже. Три следствия, которые стоит проговорить. Первое — производительность: последовательная запись в журнал дешевле, чем случайная запись страниц, поэтому коммиту достаточно одного fsync журнала. Второе — устойчивость к «рваным» страницам: full_page_writes заставляет писать в WAL полный образ страницы при первом изменении после чекпоинта, поэтому WAL резко пухнет сразу после чекпоинта. Третье — управляемость: wal_level (minimal / replica / logical) определяет, что вообще возможно — при minimal нет ни репликации, ни архивного восстановления; max_wal_size и checkpoint_timeout управляют частотой чекпоинтов (частые — меньше объём recovery, но больше full-page writes); wal_compression уменьшает объём. Практическая ловушка: забытый или неактивный replication slot и archive_command, который перестал работать, — обе ситуации приводят к тому, что WAL не удаляется и pg_wal съедает диск, а переполнение pg_wal останавливает кластер.
Можно ли дважды записать одну и ту же запись в мультимастер?
Заголовок раздела «Можно ли дважды записать одну и ту же запись в мультимастер?»Коротко. Да. В multi-master нет единой точки сериализации, поэтому две ноды могут одновременно принять «одну и ту же» вставку, и при обмене изменениями получится либо конфликт уникальности, либо два дубля с разными идентификаторами. Защита — не в самой репликации, а в дизайне: глобально уникальные идентификаторы, генерируемые клиентом (UUIDv7/ULID) или из непересекающихся диапазонов, плюс идемпотентные ключи операции и INSERT ... ON CONFLICT DO NOTHING.
Глубже. Разберём два разных сценария, которые вопрос смешивает. Сценарий «одинаковый PK на двух нодах»: обе вставки локально проходят, при применении чужого изменения возникает конфликт уникального индекса — асинхронный multi-master не может его предотвратить, только разрешить постфактум (last-write-wins по timestamp, что означает молчаливую потерю одной записи, либо ручной разбор, либо CRDT-типы данных). Сценарий «две логически одинаковые операции с разными ID»: репликация вообще не увидит конфликта, обе строки доедут, и в базе будет дубль — это лечится только идемпотентностью на уровне бизнес-ключа (UNIQUE (tenant_id, idempotency_key)), причём в multi-master уникальный индекс тоже не спасает от гонки, если вставки разошлись по разным нодам одновременно, — он лишь превратит дубль в конфликт, который надо обработать. В PostgreSQL встроенного multi-master нет; с PG16 логическая репликация умеет двунаправленный режим (origin = none, чтобы изменения не ходили по кругу), но документация прямо предупреждает, что разрешение конфликтов на приложении, и типовая рекомендация — так делить пространство ключей, чтобы конфликтов физически не возникало (каждая нода пишет только «свои» строки).
Какие есть типы масштабирования баз данных?
Заголовок раздела «Какие есть типы масштабирования баз данных?»Коротко. Вертикальное (scale up) — увеличиваем ресурсы одного узла. Горизонтальное (scale out) — добавляем узлы; оно распадается на репликацию для чтения, функциональное/вертикальное разделение по доменам и таблицам и шардирование (горизонтальное разделение строк).
Глубже. Вертикальное просто и не требует изменений в коде, но упирается в потолок железа и цена растёт нелинейно, плюс остаётся единственная точка отказа и перезагрузка при апгрейде. Горизонтальное почти бесконечно, но требует менять модель данных и код: read-реплики требуют осознать stale reads, функциональное разделение убивает джойны между доменами и требует saga вместо транзакции, шардирование добавляет роутинг и resharding. Отдельно стоит упомянуть, что в вертикальном масштабировании PostgreSQL быстрее всего окупаются память (рабочий набор в shared_buffers + page cache) и NVMe с низкой латентностью fsync — CPU-ядра помогают меньше, потому что одиночный запрос параллелится ограниченно, а redo на реплике вообще однопоточный.
Чем master отличается от slave?
Заголовок раздела «Чем master отличается от slave?»Коротко. Master (в современной терминологии — primary) принимает записи, генерирует WAL и рассылает его; slave (standby/replica) применяет чужой WAL и в режиме hot standby обслуживает только read-only запросы. На standby нельзя выполнять DML, DDL, NOTIFY, нельзя создавать обычные (не temp-only) объекты и получать XID для записи; временные таблицы там тоже недоступны.
Глубже. Технические различия, которые полезно назвать: на primary работают backend-процессы, checkpointer и walsender; на standby — walreceiver и startup-процесс, который в цикле проигрывает redo. Standby всегда находится в состоянии recovery (pg_is_in_recovery() = true) и выходит из него только при промоушене (pg_promote() или триггер-файл), после чего у него увеличивается timeline ID, и старый мастер уже нельзя просто так подключить обратно — нужен pg_rewind или полный rebuild. Ещё различие в поведении запросов: на hot standby долгие запросы могут быть принудительно отменены с ошибкой canceling statement due to conflict with recovery, чего на мастере не бывает. Терминология: в документации PostgreSQL с давних пор используются primary/standby, «master/slave» считается устаревшим — на собеседовании это мелочь, но она показывает, что вы читали доки.
Как выбрать шард?
Заголовок раздела «Как выбрать шард?»Коротко. Шард выбирается детерминированной функцией от ключа шардирования. Три базовых схемы: hash-шардинг (hash(key) mod N или, лучше, hash(key) → виртуальный бакет → бакет → шард), range-шардинг (диапазоны значений ключа) и directory/lookup — отдельная таблица-справочник «ключ → шард», которая даёт максимальную гибкость ценой ещё одного обращения (обычно кэшируемого).
Глубже. mod N — самая частая ошибка: при добавлении узла меняется остаток почти для всех ключей и переезжает почти вся база. Лечится либо consistent hashing, либо фиксированным большим числом виртуальных бакетов (1024–4096), которые заранее «нарезаны» и потом целиком переезжают между физическими шардами — при добавлении узла двигается только 1/N бакетов, а таблица маршрутизации маленькая и легко кэшируется в приложении. Хороший ключ: высокая кардинальность, равномерное распределение, присутствует в подавляющем большинстве запросов и совпадает с границей транзакции (все строки одной бизнес-операции живут на одном шарде). Плохие ключи: монотонный id/timestamp (весь новый трафик в один шард), поле с сильным перекосом (страна, где 80 % трафика — одна), поле, которое меняется (смена значения = физический переезд строки). Отдельная практика — «дробление» слишком крупных тенантов: составной ключ (tenant_id, bucket) или вынос крупных тенантов на выделенные шарды через directory-схему.
Что делать, если шардирование не помогает?
Заголовок раздела «Что делать, если шардирование не помогает?»Коротко. Сначала понять, что именно не помогло: перекос по шардам, неудачный ключ (запросы идут fan-out на все шарды) или проблема вообще не в базе. Дальше по ситуации: перебалансировать бакеты и вынести горячих тенантов, сменить или дополнить ключ шардирования (в том числе завести вторую «проекцию» данных с другим ключом), убрать кросс-шардовые запросы кэшем и денормализацией, а если нагрузка по своей природе аналитическая — вынести её в подходящее хранилище вместо того, чтобы масштабировать OLTP.
Глубже. Диагностический порядок, который стоит проговорить вслух: измерить нагрузку по шардам (если 1–2 шарда несут 80 % — это перекос, а не нехватка шардов); посмотреть долю запросов, идущих в один шард против fan-out (если fan-out массовый, шардирование только добавило латентность, так как ответ ждёт самый медленный шард — tail latency); проверить, не съедает ли выигрыш координатор/роутер; проверить топ запросов в pg_stat_statements внутри отдельного шарда (часто выясняется, что и без шардирования хватило бы индекса). Варианты действий дальше: увеличить число шардов и одновременно исправить схему бакетов; сделать read-реплики внутри шарда, если внутри шарда упёрлось чтение; вынести отдельные тяжёлые сущности в специализированные хранилища (ClickHouse для аналитики, Elasticsearch для поиска, отдельная time-series база для метрик); пересмотреть требования — иногда правильный ответ «не хранить это в OLTP-базе вообще», агрегировать на лету и держать в базе только свежее окно.
Как масштабировать конкретный шард?
Заголовок раздела «Как масштабировать конкретный шард?»Коротко. Шард — это обычная база, поэтому внутри него применяются те же приёмы: вертикальный апгрейд, оптимизация запросов и индексов, read-реплики этого шарда, партиционирование его таблиц, архивация холодных данных. Если это не помогает — шард делят: расщепляют его диапазон/бакеты и переливают часть данных на новый узел.
Глубже. Именно ради дешёвого сплита и вводят виртуальные бакеты: «разделить шард» тогда означает перевесить часть бакетов на новый узел, а не пересчитывать хеши. Механика переезда без простоя стандартная: настроить логическую репликацию нужных таблиц (или двойную запись из приложения) на новый узел, дождаться синхронизации, свериться, коротко заблокировать запись по переезжающему ключевому диапазону, довести хвост, переключить маршрутизацию, снять старую копию. Отдельно стоит назвать защиту от «горячего» шарда, который нельзя поделить, потому что нагрузка сидит на одном ключе (один гигантский тенант): тут помогает только сплит по составному ключу внутри тенанта, кэш перед базой или выделенный узел под этого клиента.
Что делать, если один инстанс не справляется?
Заголовок раздела «Что делать, если один инстанс не справляется?»Коротко. По порядку: измерить, где узкое место (CPU, IOPS, память, блокировки, число соединений), исправить очевидное — планы запросов, индексы, пул соединений, — добавить кэш, поднять железо, вынести чтение на реплики, партиционировать и убрать историю. Шардирование — последний шаг, а не первый.
Глубже. Инструменты для «измерить»: pg_stat_statements (топ по total_exec_time и по вызовам), EXPLAIN (ANALYZE, BUFFERS) на топовых запросах, pg_stat_activity с wait_event_type/wait_event для понимания, ждёт ли система диск, блокировку или клиента, pg_stat_user_tables для seq scan и мёртвых строк, pg_stat_bgwriter/pg_stat_io (PG16) для картины ввода-вывода, ОС-метрики по iowait и fsync-латентности. Очень частый реальный ответ на этот вопрос — не «шардировать», а «поставить PgBouncer в transaction pooling»: если Go-сервисы держат по 100 соединений каждый, PostgreSQL тратит всё на переключение контекста между процессами. Второй по частоте — «включить нормальный автовакуум на пухнущей таблице». Третий — «добавить составной индекс и убрать SELECT *».
Какие есть способы масштабирования баз данных?
Заголовок раздела «Какие есть способы масштабирования баз данных?»Коротко. См. выше «Как можно масштабировать базы данных» и «Database scaling methods»: вертикально — мощнее узел; горизонтально — реплики для чтения, партиционирование и архивация для объёма в пределах узла, функциональное разделение по доменам и шардирование для записи и объёма; плюс разгрузка через кэш, пул соединений и вынос аналитики в отдельное хранилище.
Глубже. Если вопрос повторяется в рамках одного собеседования, это обычно приглашение углубиться в конкретику, а не повторить список. Хороший ход — взять один пункт и разобрать его до цифр: например, «read-реплики: три реплики дают примерно троекратный потолок чтения, но не дают нулевой лаг; в нашем случае лаг держался в пределах 200 мс, поэтому профиль пользователя читали с реплики, а корзину — только с мастера».
Трехзональная репликация: Каждый кластер должен жить ровно в трех ЦОД (то есть для кластера создаются три копии/реплики, по одной в каждом из трех дата-центров).
Заголовок раздела «Трехзональная репликация: Каждый кластер должен жить ровно в трех ЦОД (то есть для кластера создаются три копии/реплики, по одной в каждом из трех дата-центров).»Коротко. Это требование на размещение: три копии в трёх независимых зонах отказа, чтобы потеря целого ЦОД оставляла работающий кворум из двух узлов. Три — минимальное число для кворумного большинства (2 из 3) и для автоматического failover без split-brain; два ЦОД для этого не годятся, потому что при разрыве сети ни одна сторона не может отличить «сосед умер» от «связь пропала».
Глубже. Практическая реализация в PostgreSQL: один primary и два standby, по одному на зону, синхронность настраивается кворумно — synchronous_standby_names = 'ANY 1 (az2, az3)', тогда коммит подтверждается после fsync на любой одной удалённой копии, и потеря одного ЦОД не останавливает запись. Автопереключение делает внешний менеджер (Patroni) с DCS (etcd/Consul), который сам должен быть распределён по тем же трём зонам — иначе кворум консенсуса теряется вместе с одной зоной, и это классическая ошибка проектирования. Что важно проговорить: межзональная синхронная репликация добавляет к каждому коммиту RTT между ЦОД, поэтому зоны в рамках одного региона (единицы миллисекунд) — норма, а «три ЦОД в трёх регионах» уже требует либо асинхронности, либо готовности к десяткам миллисекунд на транзакцию. Для планировщика размещения из этого требования получаются жёсткие инварианты: реплики одного кластера всегда в разных зонах, ни одна зона не содержит двух копий, и при выборе узла для новой реплики зоны двух существующих исключаются.
Следить, чтобы не нарушить SLA/доступность: например, недопустимо одновременно мигрировать все реплики одного кластера так, чтобы база данных оставалась вообще без работающих узлов;
Заголовок раздела «Следить, чтобы не нарушить SLA/доступность: например, недопустимо одновременно мигрировать все реплики одного кластера так, чтобы база данных оставалась вообще без работающих узлов;»Коротко. Это ограничение на планировщик миграций: операции над узлами одного кластера должны сериализоваться так, чтобы в любой момент оставался живой кворум. Формально — «за раз не более одной реплики кластера в состоянии миграции» плюс проверка, что остальные узлы здоровы и не отстают, перед началом следующей.
Глубже. Как это выглядит в виде правил, которые надо назвать на системном дизайне: (1) budget на недоступность по кластеру — аналог PodDisruptionBudget, maxUnavailable = 1; (2) предусловие перед началом шага — все прочие реплики в состоянии streaming, лаг ниже порога, нет активного failover, свежий бэкап; (3) primary мигрируется последним и только через контролируемый switchover (промоушен реплики, потом переезд бывшего мастера), а не через выключение; (4) операция должна быть возобновляемой и иметь таймаут с откатом, иначе зависшая миграция навсегда съедает бюджет; (5) при потере зоны все миграции в этой и в других зонах кластера немедленно приостанавливаются; (6) отдельная блокировка на кластер (leases в etcd/БД планировщика), чтобы два оператора или два экземпляра планировщика не начали шаги параллельно. Если это Go-сервис, естественная реализация — очередь задач с ключом сериализации по cluster_id, распределённая блокировка и state machine с идемпотентными шагами, потому что процесс планировщика может упасть в середине.
Какие стратегии репликации вы знаете?
Заголовок раздела «Какие стратегии репликации вы знаете?»Коротко. По уровню данных: физическая (WAL/страницы) и логическая (строки), плюс исторический вариант — statement-based (репликация SQL-запросов, есть в MySQL, в PostgreSQL нет). По подтверждению: асинхронная, синхронная и полусинхронная/кворумная. По топологии: primary–standby, каскадная, multi-master, а также «pull» через архив WAL против «push» через streaming.
Глубже. Statement-based стоит упомянуть отдельно, потому что вопрос часто ждёт именно этой триады (statement / row / mixed из MySQL binlog): её проблема — недетерминированные выражения (now(), random(), LIMIT без ORDER BY, триггеры с побочными эффектами) дают на реплике другой результат, поэтому индустрия ушла в row-based. Кворумная синхронность — отдельная стратегия, которая в PostgreSQL задаётся как ANY k (...), а в системах на Raft/Paxos (etcd, CockroachDB, YugabyteDB) встроена в протокол: коммит подтверждается большинством, и failover встроен в консенсус, а не прикручен снаружи. Ещё одна ось, которую забывают: что реплицируем — весь кластер (физическая) или подмножество (логическая с фильтрами таблиц, строк и колонок, PG15+), и куда — в такую же СУБД или в другую систему через CDC (Debezium поверх логического слота).
Какие подходы к шардированию существуют? Как разделить данные? На что стоит обратить внимание?
Заголовок раздела «Какие подходы к шардированию существуют? Как разделить данные? На что стоит обратить внимание?»Коротко. Подходы: hash-шардинг (равномерность, но нет диапазонных запросов), range-шардинг (диапазонные запросы дёшевы, но легко получить горячий шард), directory/lookup-шардинг (справочник «ключ → шард», максимальная гибкость и цена лишнего обращения) и геошардинг/шардинг по тенанту как частные случаи. Обращать внимание надо на выбор ключа, перекос, кросс-шардовые запросы и на то, как вы будете добавлять шарды.
Глубже. Чек-лист, который стоит проговорить: ключ должен присутствовать почти во всех запросах (иначе fan-out), быть неизменным, иметь высокую кардинальность и равномерное распределение — распределение обязательно проверить на реальных данных до внедрения, а не предполагать. Дальше — что делать со справочниками (дублировать на все шарды логической репликацией или держать отдельно и джойнить в приложении), с глобальной уникальностью (UUIDv7/ULID/Snowflake вместо bigserial), с транзакциями через шарды (по возможности исключить дизайном; если нельзя — 2PC с оговоркой про блокировки и зависшие prepared-транзакции либо saga с компенсациями), с миграцией схемы (её теперь надо катить на N узлов согласованно), с бэкапами и мониторингом (N кластеров вместо одного) и с resharding (виртуальные бакеты, заложенные с самого начала). Отдельный пункт — вторичные запросы не по ключу шардирования: их обслуживают либо fan-out с агрегацией, либо отдельным индексом-проекцией (та же таблица, шардированная по другому ключу, наполняемая через CDC).
Предположим, что вы решили выбрать стратегию шардирования данных по диапазону, распределяя равное количество записей. Какие проблемы могут из-за этого возникнуть?
Заголовок раздела «Предположим, что вы решили выбрать стратегию шардирования данных по диапазону, распределяя равное количество записей. Какие проблемы могут из-за этого возникнуть?»Коротко. Равное количество записей на момент нарезки не означает равную нагрузку и не остаётся равным со временем. Если ключ монотонный (id, timestamp), вся новая запись идёт в последний шард — он становится горячим по записи, а остальные простаивают; при этом «старые» шарды читаются редко, то есть перекошена и нагрузка на чтение.
Глубже. Полный список проблем, который стоит выдать. Hot spot на последнем диапазоне для монотонных ключей — самая тяжёлая. Перекос по нагрузке при равном числе строк: одна запись может быть в тысячу раз «активнее» другой (крупный тенант, популярный автор). Дрейф баланса: диапазоны, нарезанные один раз, расходятся со временем из-за неравномерного роста и удалений, приходится периодически пересплитывать и переливать — а сплит диапазона означает перенос данных и обновление маршрутизации под нагрузкой. Границы: значения на стыке и запросы, пересекающие границу, дают запросы к двум шардам. Неравномерность распределения самого ключа (строковые ключи, отсортированные по алфавиту, дают перекос на популярные префиксы). Наконец, дефолтный ответ интервьюеру на «как чинить»: либо hash от ключа (теряем диапазонные запросы), либо композитный подход — hash-префикс + диапазон внутри (hash(tenant) → шард, а внутри шарда RANGE-партиционирование по времени), либо range с автоматическим сплитом и балансировкой, как это делают CockroachDB/HBase, где диапазоны («ranges»/«regions») делятся и переезжают автоматически по фактической нагрузке.
Чем отличается vacuum от vacuum full?
Заголовок раздела «Чем отличается vacuum от vacuum full?»Коротко. Обычный VACUUM помечает место от мёртвых кортежей как свободное для повторного использования внутри той же таблицы, работает конкурентно (ShareUpdateExclusiveLock) и почти не возвращает место операционной системе. VACUUM FULL физически переписывает таблицу в новый файл без мёртвых строк, возвращает место в ОС и перестраивает индексы, но берёт AccessExclusiveLock (таблица недоступна даже для чтения) и требует до двух объёмов таблицы на диске.
Глубже. Из этого следует практика: VACUUM FULL в проде на большой таблице — почти всегда неправильный ответ, потому что это полная недоступность на время, пропорциональное размеру. Когда таблица реально раздута (проверяется по pgstattuple или оценкой через pg_stat_user_tables + известные запросы bloat), берут pg_repack или pg_squeeze, которые делают ту же работу с короткой блокировкой только в момент переключения. Ещё различия: VACUUM FULL обновляет статистику не полностью (ANALYZE всё равно нужен и не выполняется автоматически), не может выполняться параллельно с обычной работой и, в отличие от обычного вакуума, нарушает порядок физического размещения — впрочем, для этого есть CLUSTER, который переписывает таблицу в порядке индекса (тоже с AccessExclusiveLock).
Как происходит репликация в БД?
Заголовок раздела «Как происходит репликация в БД?»Коротко. См. выше «Как работает репликация»: изменения фиксируются в журнале на источнике, передаются на приёмник по сети и применяются там — либо на уровне физических страниц (redo из WAL), либо на уровне логических изменений строк.
Глубже. Если вопрос задан в общем виде, а не про PostgreSQL, полезно назвать аналоги: в MySQL это binlog (row/statement/mixed) + IO-thread и SQL-thread на реплике, с GTID для корректного failover; в MongoDB — oplog и replica set с выборами лидера; в системах на Raft (etcd, CockroachDB, YugabyteDB, TiDB) репликация встроена в консенсус — лидер реплицирует записи журнала и коммитит после подтверждения большинства, что даёт синхронность и автоматический failover в одном механизме. Общая схема везде одна: «журнал изменений на источнике → доставка → применение», различаются гранулярность записи в журнале и момент подтверждения коммита.
В каких базах есть автопереключение мастера/слэйва? (В пг нет, делается инструментами снаружи)
Заголовок раздела «В каких базах есть автопереключение мастера/слэйва? (В пг нет, делается инструментами снаружи)»Коротко. Верно: в PostgreSQL встроенного автоматического failover нет — есть только примитивы (pg_promote(), pg_rewind, слоты), а решение о переключении принимает внешний менеджер: Patroni (де-факто стандарт, поверх etcd/Consul/ZooKeeper), repmgr, pg_auto_failover, PAF на Pacemaker, или облачный managed-сервис. Встроенный автофейловер есть в MySQL InnoDB Cluster / Group Replication, MongoDB replica set, Redis Sentinel и Redis Cluster, Elasticsearch, а также во всех системах на Raft/Paxos — etcd, CockroachDB, YugabyteDB, TiDB, — где выбор лидера часть протокола консенсуса.
Глубже. Причина, по которой PostgreSQL не делает это сам, — принципиальная: для безопасного failover нужен внешний арбитр с кворумом, иначе получается split-brain, когда обе стороны разорванной сети считают себя мастером и принимают записи. Patroni ровно это и решает: узлы борются за лидерский ключ с TTL в распределённом хранилище, потерявший лидерство узел сам себя демотирует (в том числе через watchdog), а клиенты ходят через HAProxy/endpoint, который знает текущего лидера по HTTP-проверке роли. Что важно упомянуть про цену автоматики: асинхронная репликация + автофейловер = потенциальная потеря последних коммитов (RPO > 0), поэтому либо включают синхронный кворум, либо явно принимают риск; и что после промоушена старый мастер нельзя вернуть простым стартом — нужен pg_rewind или пересборка, иначе разойдутся timeline.
В чем суть репликации данных?
Заголовок раздела «В чем суть репликации данных?»Коротко. См. выше «Что такое репликация БД». Суть — в поддержании нескольких актуальных копий одних и тех же данных на независимых узлах, чтобы отказ узла не означал ни потерю данных, ни остановку сервиса, а читающая нагрузка могла распределяться.
Глубже. Один тезис, который отличает хороший ответ: репликация — это всегда обмен согласованности на доступность и латентность. Полностью синхронная копия означает, что запись идёт со скоростью самого медленного участника; полностью асинхронная означает окно, в котором подтверждённые клиенту коммиты ещё не доехали и будут потеряны при аварийном переключении. Поэтому проектирование репликации сводится к двум числам: допустимый RPO (сколько данных можно потерять) и допустимый RTO (сколько можно быть недоступным) — и уже под них выбирается конфигурация.
Какие плюсы и минусы шардирования?
Заголовок раздела «Какие плюсы и минусы шардирования?»Коротко. Плюсы: линейное масштабирование объёма и записи, меньшие индексы и рабочий набор на узел, локализация отказов и «шумных соседей», возможность географического размещения данных рядом с пользователем и под требования локализации. Минусы: резкий рост сложности приложения и эксплуатации, отсутствие кросс-шардовых джойнов, транзакций и глобальной уникальности, болезненный resharding, дороже мониторинг, бэкапы и миграции схемы, и снижение надёжности при отсутствии репликации внутри шарда.
Глубже. Несколько неочевидных минусов, которые ценят на собеседовании. Tail latency: запрос, идущий на все шарды, ждёт самый медленный — при 10 шардах p99 системы приближается к p99.9 отдельного узла. Ограниченная гибкость аналитики: любой отчёт «по всем пользователям» превращается в отдельную инженерную задачу, поэтому аналитику обычно уносят в отдельное хранилище через CDC. Необратимость выбора ключа: сменить ключ шардирования постфактум — это, по сути, миграция всей базы. И организационная цена: команда должна уметь эксплуатировать N кластеров, а не один, включая согласованный rollout миграций и раскатку конфигураций.
Расскажи про репликацию и шардирование
Заголовок раздела «Расскажи про репликацию и шардирование»Коротко. См. выше — репликация делает копии одних данных ради отказоустойчивости и масштабирования чтения, шардирование делит разные данные по узлам ради масштабирования записи и объёма; на практике их совмещают: N шардов, каждый — реплицированная группа.
Глубже. Это типичный «открывающий» вопрос, и хороший ответ — короткая структурированная выкладка на 2–3 минуты, а не всё, что вы знаете. Каркас: (1) определения и главное различие в одну фразу; (2) что чем масштабируется — чтение/запись/объём/надёжность; (3) как это устроено в PostgreSQL конкретно: WAL → walsender/walreceiver, физическая против логической, synchronous_commit и кворум, отсутствие встроенного шардинга и failover, Patroni и Citus как внешние ответы; (4) практические компромиссы — лаг реплики и stale reads, выбор ключа шардирования и resharding; (5) короткий пример из опыта. После этого замолчать и дать интервьюеру выбрать, куда углубляться, — почти всегда он спросит про лаг реплик или про выбор ключа.
Частые ошибки на собесе
Заголовок раздела «Частые ошибки на собесе»- Говорить, что реплики «масштабируют базу», не уточняя, что только чтение: поток записи на каждой реплике ровно такой же, как на мастере.
- Путать партиционирование и шардирование: партиции живут в одном инстансе и не добавляют ни CPU, ни дисков.
- Считать, что синхронная репликация означает «всегда актуальные данные на реплике»:
synchronous_commit = onгарантирует только fsync WAL на реплике, но не то, что запись уже применена и видна — это даёт лишьremote_apply. - Утверждать, что
VACUUMуменьшает размер таблицы на диске, или наоборот — предлагатьVACUUM FULLкак рутинную операцию в проде, не упоминаяAccessExclusiveLockиpg_repack. - Считать автовакуум чем-то опциональным и предлагать его выключить «чтобы не мешал»: без него неизбежны раздувание таблиц и anti-wraparound-остановка кластера.
- Начинать ответ на «как масштабировать» с шардирования, пропуская индексы, пул соединений, кэш и вертикальный апгрейд — почти всегда именно там лежит основной выигрыш.
- Предлагать
hash(key) mod Nбез виртуальных бакетов или consistent hashing, не задумываясь, что при добавлении узла переедет почти вся база. - Считать, что в PostgreSQL есть встроенный автофейловер или встроенный multi-master.
- Забывать, что триггеры и
LISTEN/NOTIFYне работают на физической реплике, а на логической подписке триггеры по умолчанию не срабатывают безENABLE ALWAYS TRIGGER.
Что почитать
Заголовок раздела «Что почитать»- PostgreSQL Documentation, глава 26 «High Availability, Load Balancing, and Replication» — https://www.postgresql.org/docs/current/high-availability.html
- PostgreSQL Documentation, глава 29 «Reliability and the Write-Ahead Log» и 30 «Logical Replication» — https://www.postgresql.org/docs/current/wal-intro.html , https://www.postgresql.org/docs/current/logical-replication.html
- PostgreSQL Documentation, «Routine Vacuuming» (25.1) — пороги автовакуума, freeze и wraparound: https://www.postgresql.org/docs/current/routine-vacuuming.html
- PostgreSQL Documentation, «Table Partitioning» — https://www.postgresql.org/docs/current/ddl-partitioning.html
- Martin Kleppmann, «Designing Data-Intensive Applications», главы 5 (Replication) и 6 (Partitioning) — лучшее системное изложение компромиссов.
- Patroni documentation — как устроен внешний автофейловер для PostgreSQL: https://patroni.readthedocs.io/