awesome-everything EN
↑ Обратно к восхождению

Базы данных

Семь актов: от CREATE TABLE до Citus

Суть Один продукт проходит путь от однотабличного MVP до кластера Citus на миллиард строк через семь моментов отказа базы — каждый называет триггер, симптом и рычаг, который его решает.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 12 min

День 0 в SaaS-стартапе. PM просит «поиск пользователей по email». Один инженер, один Postgres, одна таблица. Три года спустя: миллиард строк, шесть шардов Citus и runbook размером с учебник. Между этими двумя точками — семь моментов, когда база ломалась, а команда училась.

Семь пьес этой главы отображаются один к одному на семь актов роста продукта. У каждого акта есть триггер, симптом и рычаг. Пропусти рычаг — следующий акт обойдётся дороже.

Акт 1 — День 0, проектирование схемы. «users(email, name, org_id).» Должен ли email быть UNIQUE? Должен ли org_id быть foreign key? Должен ли prefs быть отдельной таблицей или JSONB? Решения: email — это UNIQUE NOT NULL CITEXT. org_id — это BIGINT REFERENCES orgs(id) ON DELETE CASCADE. prefs — это JSONB с GIN-индексом, когда поиск потребует. Суррогатный id BIGSERIAL изолирует от изменений email. Нарушай правило только под давлением throughput, никогда по умолчанию.

Акт 2 — Неделя 1, 10K строк, первый медленный запрос. Email-search endpoint p95 растёт с 30 мс до 800 мс. Планировщик делает sequential scan. CREATE INDEX users_email_idx ON users(email). Правило leading-column у B-tree означает WHERE email = ? разрешается двумя page reads. p95 падает до 4 мс.

Акт 3 — Месяц 1, 100K строк, планировщик врёт. Половина запросов быстрые, половина — 600 мс. EXPLAIN ANALYZE показывает: планировщик иногда выбирает seq-scan несмотря на индекс. Row estimate ошибается на 30×: stale statistics. ANALYZE users; перестраивает гистограммы. Планировщик выбирает планы из статистики, не из данных; поддержка статистики — это операционная дисциплина.

Акт 4 — Месяц 6, тихий bloat. Nightly report runner держит одну транзакцию открытой четыре часа. VACUUM не может вернуть мёртвые tuples ниже xmin-горизонта. Таблица users вздулась с 200 MB до 80 GB. Исправление: убить long transaction, установить idle_in_transaction_session_timeout = 60s, запустить pg_repack для возврата диска. Постоянное исправление: реплицировать на read replica для отчётов.

Акт 5 — Год 1, 1M пользователей + 50 app pods, connection storm. Каждый pod открывает свой Postgres backend на cold-start. На pod-rollout кластер видит 1000 concurrent backends; kernel scheduler трасится; запросы по 4 мс занимают 4 с. PgBouncer в transaction-mode: 100 server-side backends, 10000 client connections мультиплексированы. Правило размера: pool_size = active_concurrent_transactions × safety_factor, не max_app_workers.

Акт 6 — Год 2, миграция, которая заморозила prod. День мультитенантности. ALTER TABLE users ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 0 берёт AccessExclusiveLock и триггерит полную перезапись таблицы. Prod замерзает на восемь минут. Рецепт expand-contract: добавить nullable column, backfill батчами, add CHECK ... NOT VALID, VALIDATE CONSTRAINT, SET NOT NULL, drop check.

Акт 7 — Год 3, 1B строк, hot shard. Citus развёрнут, shard key — tenant_id. Tenant Acme даёт 40% всех запросов. Шард Acme насыщен, остальные простаивают. Исправление: co-location связанных таблиц на том же shard key; онлайн-решардинг через citus_rebalance_table_shards.

Семь актов — триггер и рычаг
Акт 1 — День 0
Схема: суррогатные ключи, FK, ограничения
Акт 2 — Неделя 1, 10K строк
Индекс на фильтруемую колонку
Акт 3 — Месяц 1, 100K строк
ANALYZE для обновления статистики
Акт 4 — Месяц 6, 200 MB → 80 GB
Убить long tx, pg_repack, timeout
Акт 5 — Год 1, 50 pods
PgBouncer в transaction-mode
Акт 6 — Год 2, мультитенантность
Рецепт expand-contract
Акт 7 — Год 3, 1B строк
Шардирование Citus с co-location

Почему порядок важен.

База под растущим продуктом — это как растущий город. Схема — зонирование. Индексы — карта улиц. Execution plans — диспетчер трафика. MVCC — несколько полос, чтобы машины проезжали без столкновений. Connection pool — парковка. Миграции — строительные бригады, которые не должны закрывать все дороги сразу. Шардирование — присоединение новых районов, когда один квартал не справляется с трафиком. Пропущенный урок означает: город растёт, но не тот слой перегружен.

Каждый акт открывает следующий только если предыдущий пройден правильно. Пропусти Акт 1 (схема) — Акты 2–7 воюют с неэффективными join-ами. Пропусти Акт 2 (индексы) — решения Акта 3 принимаются вслепую. Пропусти Акт 3 (статистика) — единственный рычаг Акта 4 это vacuum. Пропусти Акт 4 (bloat) — потоки пула Акта 5 видят замедляющиеся table scans. Пропусти Акт 5 (пул) — миграции Акта 6 задыхаются от connection storms. Пропусти Акт 6 (lock safety) — шардирование Акта 7 невозможно. Порядок — это ограничение, наложенное физикой и внутренностями Postgres.

Почему это работает

Цена позднего исправления экспоненциальна: ошибки Акта 1 стоят дней, ошибки Акта 7 — месяцев решардинга. Команды, пропускающие ранние акты, платят катастрофические цены позже. Команды, чрезмерно инженерящие ранние акты (шардинг 10 GB датасета), тратят ресурсы на несуществующие проблемы. Искусство — знать свою траекторию роста и выбирать момент для каждого акта так, чтобы он срабатывал прямо перед тем, как failure modes предыдущего акта становятся болью в production.

Викторина

На 10K строк email-search endpoint вдруг медленный. Самый дешёвый первый рычаг?

Викторина

Диск заполнен, но количество строк не изменилось. Наиболее вероятная причина?

Викторина

Почему шардинг — рычаг года 3, а не года 1?

Расставь шаги по порядку

Упорядочи семь scale-tier рычагов от самого раннего (День 0) до самого позднего (Год 3):

  1. 1 Спроектировать реляционную схему (таблицы, ключи, ограничения)
  2. 2 Добавить правильный индекс для запроса
  3. 3 Убедиться, что execution plan использует индекс; запустить ANALYZE
  4. 4 Найти long transaction, блокирующую VACUUM
  5. 5 Поставить connection pooler перед Postgres
  6. 6 Мигрировать схему безопасно с expand-contract
  7. 7 Сделать шардинг самой большой таблицы через узлы
Вспомните перед уходом
  1. 01
    Назови семь рычагов по порядку и дай один симптом, который сигнализирует о каждом уровне.
  2. 02
    Почему порядок актов важен — почему нельзя 'добавить индексы позже'?
  3. 03
    Проследи городскую метафору: сопоставь каждый слой (схема, индекс, планы, MVCC, пул, миграции, шардинг) с элементом города.
Итог

База продукта проходит через семь уровней роста, каждый со своим триггером и единственным правильным рычагом. Решения схемы, принятые в День 0, компонуются в каждый последующий акт — суррогатный ключ и правильные FK-ограничения это дешёвые greenfield-решения, которые становятся дорогостоящими ретрофитами на масштабе. Индексы исправляют запросы при 10K строк; отсутствие индекса при 1B строк — это многочасовой concurrent build под трафиком. Устаревшая статистика заставляет планировщик игнорировать существующие индексы; ANALYZE — это операционная дисциплина. Bloat от долгой транзакции может вырастить таблицу 200 MB до 80 GB за дни; рычаг — убить long transaction, а не добавлять диск. Connection storms при pod-rollout требуют connection pooler, а не больше backends. Миграции схемы с AccessExclusiveLock ставят в очередь каждый запрос; expand-contract избегает заморозки. Шардинг распределяет нагрузку, но умножает каждую операционную задачу — это последнее средство, применяемое намеренно после всех предыдущих рычагов. Порядок — ограничение, наложенное внутренностями Postgres: пропусти один акт и каждый последующий обходится экспоненциально дороже.

Связанные уроки
встречается в258
Продолжить восхождение ↑Акты 1–3 в глубину: схема, индексы и статистика планировщика
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources6
expand
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06

Trademarks belong to their respective owners. Editorial reference only.