1 октября 2023
PostgreSQL не масштабируется одной кнопкой - он упирается в три разных потолка: вычислительный, по записи и по числу соединений. Каждый потолок снимается своим набором решений, и большинство ошибок в проектировании БД - попытка решить проблему «не того» уровня: команда уходит в шардирование, когда у неё упёрлось число backend-процессов, или ставит реплики, когда тормозит конкретный неиндексированный запрос. Эта статья - не каталог механизмов, а карта, которая помогает понять, где именно у вас потолок и какое следующее решение реально передвинет узкое место, а какое только добавит операционной сложности.
Где PostgreSQL упирается в потолок
Разговор о масштабировании СУБД часто начинается с перечисления техник: "у нас есть репликация, партиционирование, шардирование, пулинг". Это словарь, но не карта. Карта начинается с вопроса: какой именно ресурс заканчивается у вас прямо сейчас? PostgreSQL ломается по трём независимым осям, и техника, которая снимает один потолок, никак не помогает с двумя другими.
Потолок по соединениям. Каждый клиент PostgreSQL получает отдельный backend-процесс. Это решение архитектуры PostgreSQL, восходящее к ранним 1990-м, и оно не меняется в обозримом будущем. Каждый процесс - это десятки мегабайт RSS, контекст-свитч, доля shared_buffers. На современном железе сотни активных backend-ов уже создают заметный CPU overhead на context switching и lock contention; тысячи - почти всегда означают деградацию. Бенчмарк PgBouncer и многократные публикации Brandur Leach (детальные технические разборы Postgres-внутренностей со времён Heroku и Stripe, сейчас - Crunchy Data) дают одну и ту же цифру: при примерно 300-500 активных соединениях производительность начинает падать вне зависимости от железа, потому что узкое место - не CPU и не диск, а внутренние блокировки на ProcArrayLock.
Потолок по записи (write amplification). Любая запись в PostgreSQL - это запись в WAL, потом в shared_buffers, потом checkpoint выносит грязные страницы на диск, а MVCC создаёт новую версию строки и оставляет старую для autovacuum. На write-heavy нагрузке узкое место - не "сколько IOPS у диска", а "успевает ли autovacuum за порождением dead tuples и не превращается ли WAL в бутылочное горло на одной physical-replication ссылке". Андрес Фройнд (Andres Freund, core PostgreSQL committer) в серии докладов на PGCon последних лет подробно показывал, как именно WAL fsync и replay становятся первыми ограничителями на современном NVMe-железе: однопоточный walsender, однопоточный walreceiver, и одна реплика не может опередить мастер.
Потолок по объёму данных и по запросам к ним. Это уже другое: миллиардная таблица, на которой index-only scan занимает секунды, autovacuum висит часами, а pg_dump практически невозможен. Вертикальное железо здесь помогает плохо - дисковый объём растёт, но индексы не помещаются в RAM, а каждое обновление статистики занимает реальное время. Здесь работают партиционирование (если есть естественный ключ для разрезания) и шардирование (если данные физически перестают помещаться на одну машину).
Bruce Momjian, co-founder PostgreSQL Global Development Group и один из старейших активных committer-ов проекта (с 1996 года), в публичных презентациях формулирует это просто: "Vertical scaling solves most problems for most people; horizontal scaling solves a smaller set of problems for fewer people, at much higher cost". Эта позиция - не консерватизм, а реалистичная оценка того, во что обходится распределённая транзакция, отсутствие глобальных foreign key и руководство шардированным кластером в three a.m. инциденте. Перед каждой техникой имеет смысл сначала спросить: какой потолок я снимаю и точно ли он у меня - именно этот?
pg_stat_activity покажет, сколько backend-ов реально активно. pg_stat_statements - какие запросы съедают время. pg_stat_bgwriter и pg_stat_wal - как идут чекпоинты и сколько генерится WAL. Без этих цифр любое решение про "переходить ли на шарды" - угадывание.
AKF Scale Cube Мартина Эбботта, Майкла Фишера и Тома Кивена ("The Art of Scalability", Addison-Wesley, 2-е издание, 2015) даёт удобную таксономию: ось X - дублирование (читай: реплики и пулы стейтлесс-приложений), ось Y - функциональная декомпозиция (микросервисы, выделение отдельных БД для отдельных bounded contexts), ось Z - шардирование по ключу (один и тот же тип данных, разделённый по tenant_id, region_id, hash). Большинство компаний живут на оси X неограниченно долго, проходят на ось Y по мере выделения сервисов, и только малая часть когда-либо доходит до оси Z. Эта пропорция важнее, чем любые технические детали отдельных решений.
Пулинг соединений как первый шаг
Если по симптомам похоже, что у вас "БД не справляется", и вы при этом ещё не поставили пулер, дальнейшие архитектурные решения почти всегда преждевременны. Пулинг соединений - не оптимизация и не "продвинутая техника"; это базовая гигиена, без которой PostgreSQL ведёт себя предсказуемо плохо в любом сценарии с десятками воркеров на стороне приложения.
Причина, как уже было выше - архитектура процессов. PostgreSQL форкает отдельный backend на каждое соединение, и эта операция стоит десятки миллисекунд плюс затраты по памяти на жизненном цикле. Современное Rails/Django/Node-приложение с Puma/Gunicorn/PM2 на десяти инстансах по 5 воркеров уже создаёт 50 потенциальных соединений; добавьте Sidekiq/Celery/BullMQ с пулом 25 воркеров на инстанс - и вы у 300 соединений из приложения, которое логически делает одну операцию в секунду. Без пулера каждый воркер держит свой backend, даже если 95% времени тот idle. С PgBouncer в transaction mode все 300 воркеров делят, скажем, 25 реальных backend-ов, и потолок ProcArrayLock сдвигается далеко за горизонт реальной нагрузки.
PgBouncer (pgbouncer.org/usage.html) поддерживает три режима пулинга, и выбор между ними - не вкусовщина, а функциональный компромисс. Session pooling выделяет backend на всю клиентскую сессию: безопасно для prepared statements и SET переменных, но бесполезно с точки зрения экономии соединений. Statement pooling возвращает соединение в пул после каждого statement: даёт максимальную плотность, но ломает почти всё, что использует транзакции. Transaction pooling - типичный выбор для веб-приложений (хотя дефолт самого PgBouncer - session): соединение закрепляется на время одной транзакции, что совместимо с обычным CRUD-кодом, но требует отказа от server-side prepared statements (или явной поддержки на стороне драйвера) и сессионных переменных.
Пример минимальной конфигурации PgBouncer (pgbouncer.ini) - не как production-рецепт, а как иллюстрация того, насколько маленьким может быть конфиг для большой выгоды:
[databases]
mydatabase = host=localhost port=5432 dbname=mydatabase
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
default_pool_size = 25
max_client_conn = 1000
min_pool_size = 5
В этом конфиге PgBouncer держит ровно 25 backend-соединений к PostgreSQL и обслуживает до 1000 клиентских соединений из приложения. Узкое место сдвигается с числа процессов в PostgreSQL на длительность транзакций: пока транзакции короткие, 25 соединений хватает на сотни RPS. Длинные транзакции (отчётный SELECT, который держит соединение секунды) сразу видны в SHOW POOLS - они забивают пул и приложение начинает ждать.
Альтернатива на тот же случай - PgPool-II с балансировкой между репликами и автоматическим failover. Настраивается сложнее, документация местами противоречива, и большинство команд ставит его не за пулинг, а за чтение-балансировку и failover. Для одного только пулинга PgBouncer почти всегда правильный выбор - он делает одну вещь и делает её предсказуемо.
Репликация для чтения: компромиссы консистентности и сложности
Если пулер стоит, а нагрузка по чтению продолжает упираться в один сервер, следующий шаг - реплики. Это второй после пулинга по применимости приём: его эксплуатирует, наверное, каждый продакшен PostgreSQL крупнее одного nano-инстанса. Реплика снимает с мастера часть SELECT-нагрузки, даёт hot standby для failover и страхует от полной потери данных. Цена - eventual consistency и операционная сложность маршрутизации запросов.
PostgreSQL поддерживает два вида репликации, и они решают разные задачи. Физическая (streaming) репликация копирует WAL-блоки побайтово: реплика - точная копия мастера на уровне страниц, без выбора, что реплицировать. Запись на реплику невозможна; чтение - сколько угодно, но с лагом, обычно миллисекунды на здоровой ссылке и до часов в инцидентах. Логическая репликация работает на уровне строк через decoding WAL: можно реплицировать отдельные таблицы, в обе стороны, между разными мажорными версиями PostgreSQL и даже в другие СУБД. Цена - отсутствие реплицирования DDL, ограничения по типам данных и значительно более тонкая работа в случае конфликтов.
Маршрутизация запросов - отдельная задача, которую PostgreSQL не решает. Приложение должно само понимать, какой запрос идёт на мастер, а какой может пойти на реплику. Rails/ActiveRecord 7.x предлагает connects_to с именами ролей writing/reading и автоматическое переключение в блоке after-write. Django имеет database routers. В большинстве микросервисных архитектур это решается на уровне DSN: два разных URL, два пула, явная аннотация на репозитории. Самая частая ошибка - читать после записи "из той же транзакции бизнес-логики" с реплики и получать чужой стейл; защита - sticky session на мастер на время после записи.
Для тестового стенда логической репликации достаточно двух контейнеров с PostgreSQL 16, которым выставлен wal_level = logical:
services:
db_master:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
POSTGRES_DB: mydatabase
ports:
- "5432:5432"
db_replica:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
POSTGRES_DB: mydatabase
depends_on:
- db_master
На мастере создаётся PUBLICATION, на реплике - SUBSCRIPTION, схема (DDL) предварительно повторяется на обеих сторонах вручную. С этого момента INSERT/UPDATE/DELETE на таблицах публикации применяются на реплике с задержкой в десятки миллисекунд. Запись на реплике технически возможна (логическая репликация её не блокирует), и это - известный источник аварий: если на реплике случайно поменяли строку, а потом мастер прислал свою версию, изменения с реплики молча перетираются. Поэтому для большинства "просто чтобы был read replica" сценариев лучше брать физическую репликацию: она read-only by design.
Серьёзная сложность с репликами - что они не дают линеаризуемости. Кайл Кингсбури (Aphyr) в Jepsen-тестах подробно показал, в каких именно сценариях PostgreSQL-репликация даёт стейл-чтения и lost updates: краткое содержание - default isolation Read Committed позволяет видеть на реплике строку, которая на мастере уже обновлена, и любое решение приложения "сначала прочитать с реплики, потом записать на мастер" уязвимо к race condition. Полные отчёты Jepsen по PostgreSQL и его деривативам (Aurora, YugabyteDB, CockroachDB) опубликованы на https://jepsen.io/analyses и обязательны к прочтению, если вы строите критичную для денег логику поверх реплик.
Партиционирование против шардирования: дерево решений
Эти два слова часто путают, и путаница стоит дорого, потому что одно решение остаётся локальным в рамках одной БД, а второе превращает кластер в распределённую систему со всеми вытекающими. Различить их можно одним вопросом: после применения техники у меня одна база данных или несколько?
Партиционирование - это разделение одной большой таблицы на несколько физических таблиц-партиций в пределах одной БД. Декларативное партиционирование PostgreSQL (с версии 10, существенно улучшено в 11-15) поддерживает три стратегии: RANGE (по диапазону, типичный кейс - даты), LIST (по явному списку значений), HASH (равномерное распределение по hash). Запросы к родительской таблице автоматически направляются в нужные партиции через partition pruning; добавление и удаление партиций не требует переписывания основной таблицы. Robert Haas, один из ключевых разработчиков declarative partitioning в PostgreSQL, неоднократно подчёркивал в докладах на PGCon и PgConf.EU, что партиционирование решает три конкретные задачи: сокращение времени запросов, которые обращаются только к "свежим" данным; быстрое удаление старых данных через DROP PARTITION вместо DELETE с массовым autovacuum; уменьшение размера индексов на каждой партиции.
Шардирование - это разделение данных между несколькими физическими БД, обычно несколькими физическими серверами. Каждый шард - полноценный PostgreSQL со своими процессами, своим WAL, своим autovacuum. Координатор (или приложение) знает, какой ключ к какому шарду относится, и направляет запросы. Шардирование снимает потолок по объёму и по записи, но добавляет три типа сложности, которых нет в одиночном PostgreSQL: распределённые транзакции (требуют 2PC и страдают на write-amplification), глобальные ограничения целостности (foreign key между шардами недоступны без надстроек), и rebalancing при добавлении/удалении нод.
Практическое дерево решений выглядит так. Если данные логически разделимы по диапазону или категории (новости по дате, события по tenant_id), помещаются в один сервер по объёму, но запросы стали медленными или vacuum не успевает - партиционирование. Если объём данных физически перестал помещаться на самый большой разумный сервер, или write-нагрузка упёрлась в WAL/checkpoint, и при этом данные легко режутся по ключу с минимумом cross-shard запросов - шардирование. Если ничего из этого, но "вроде же надо горизонтально масштабироваться" - почти наверняка нет, и пора смотреть на пулер, реплики, индексы и план запросов.
Простой пример range-партиционирования в PostgreSQL для иллюстрации синтаксиса (стилизованный пример на основе типичных паттернов news-приложения):
CREATE TABLE news (
id serial,
title VARCHAR(255),
type VARCHAR(50),
content TEXT,
publish_date DATE,
PRIMARY KEY (id, publish_date)
) PARTITION BY RANGE (publish_date);
CREATE TABLE news_2023 PARTITION OF news
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');
CREATE TABLE news_2024 PARTITION OF news
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');
CREATE TABLE news_2025 PARTITION OF news
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
Главное здесь не синтаксис, а ключ партиционирования: publish_date попадает в составной первичный ключ, потому что PostgreSQL требует, чтобы ключ партиционирования входил в primary key и unique constraints. Это техническая деталь, но из-за неё многие existing-схемы нельзя партиционировать без изменения модели данных. Markus Winand в "SQL Performance Explained" и на use-the-index-luke.com подробно разбирает, как такие изменения в ключе влияют на план запросов и почему "просто добавить партиционирование на работающую таблицу" - почти всегда трёхмесячный проект, а не квартальный спринт.
Шардирование в PostgreSQL без сторонних расширений требует ручной разметки: приложение само знает, что user_id 1...1000000 живёт на shard0, 1000001...2000000 на shard1, и так далее. Это даёт максимум контроля, но и максимум кода в инфраструктурном слое: маршрутизация, fan-out для cross-shard запросов, rebalancing при росте. Citus (citusdata.com/blog; с 2019 года часть Microsoft, сейчас известен как Azure Cosmos DB for PostgreSQL) автоматизирует большую часть этого: SELECT create_distributed_table('news', 'tenant_id') превращает обычную таблицу в распределённую, а координатор берёт на себя планирование запросов. Документация Citus и блог инженерной команды - один из лучших источников по тому, как именно ведёт себя распределённый PostgreSQL под нагрузкой и где у него тонкие места.
Антипаттерны шардирования: где это ломается
Шардирование - не "следующий уровень" после реплик; это качественно другая система с другими failure modes. Большинству команд оно не нужно никогда, и попытка ввести его преждевременно стоит больше, чем позднее. Перечислим явно, где именно решение ломается на практике.
Антипаттерн 1: шардирование вместо оптимизации запросов. Команда видит медленные запросы, видит высокую нагрузку на CPU мастера и решает, что "пора шардить". Через полгода у них шесть шардов, каждый из которых тормозит на тех же запросах, потому что причина была не в одном узле, а в неиндексированном WHERE LOWER(email) = ? или sequential scan по 50-миллионной таблице. Шардирование умножает все существующие неэффективности и добавляет к ним сетевую задержку. Markus Winand в "SQL Performance Explained" формулирует это явно: 80% случаев проблем с производительностью PostgreSQL решаются индексом, который должен был быть с самого начала. Шардирование - дорогой способ обойти отсутствующий индекс.
Антипаттерн 2: распределённые транзакции в OLTP. Citus и любая другая шардирующая надстройка решают проблему cross-shard транзакций через двухфазную фиксацию (2PC). 2PC технически работает, но добавляет round-trips и блокировки на всех участниках. Если бизнес-операция типично затрагивает 2-3 шарда (классический случай "пользователь A переводит деньги пользователю B, а они на разных шардах"), 2PC становится узким местом, и latency p99 растёт в разы. Документация Citus прямо рекомендует выбирать ключ так, чтобы 95%+ операций были single-shard; если такой ключ не находится, шардирование - неправильный инструмент. Хороший задокументированный пример того же класса проблем - Notion (см. ниже): их выбор пал на шардирование по workspace_id именно потому, что 99% запросов локальны для одного workspace; без такой природы данных шардирование умножает round-trips, а не снимает их.
Антипаттерн 3: шардирование без data locality. Хешированное распределение по случайному ключу (например, по hash(id)) равномерно нагружает все шарды, но любой запрос с фильтром по другому полю превращается в fan-out. Geospatial-данные, time-series, multi-tenant с большими тенантами - все требуют осознанного выбора ключа, который соответствует доступу. Notion в своём публичном посте о шардировании (https://www.notion.com/blog/sharding-postgres-at-notion, 2021) описал, как они выбрали workspace_id как ключ, потому что 99% запросов локальны для workspace - и это сразу определило, что шардирование вообще имеет смысл. Без такого свойства шардирование ничего не выигрывает.
Антипаттерн 4: шардирование "под рост", которого ещё нет. Команда читает блог Notion, делает выводы и шардирует базу с 200 GB данных и 500 RPS на запись. Через год у них всё та же база, но теперь с операционным накладным расходом на пять шардов: пять отдельных backup-цепочек, пять окон обслуживания, пять потенциально расходящихся в схеме инстансов, пять отдельных мониторингов. Notion подошли к шардированию, когда дисковый объём начал удваиваться каждые полгода и autovacuum перестал успевать - то есть когда у них уже были конкретные метрики, упирающиеся в потолок, а не план роста. GitLab в своей многолетней серии о database evolution (about.gitlab.com/blog) прошёл годы на одном PostgreSQL, прежде чем перейти к decomposition по доменам (CI, Registry и т.д.), и до сих пор основной БД остаётся одна, без шардирования.
Антипаттерн 5: rebalancing как retroactive проблема. Изначально шардов 4, нагрузка распределяется ровно. Через год добавили двух больших клиентов, и они оба попали на shard2; теперь shard2 в два раза горячее остальных. Rebalancing - перенос диапазона ключей с одного шарда на другой - всегда означает downtime или окно eventual consistency, и его нельзя сделать незаметным. В Citus есть shard_rebalancer, в собственной реализации это пишется руками. Команда, которая не подумала о rebalance в момент решения шардить, через два года имеет hot shards и многомесячный проект миграции - типичный путь, описанный в публичных постмортемах нескольких SaaS-компаний.
Антипаттерн 6: операционная сложность недооценивается на 3-5x. Один PostgreSQL - один backup, один pg_dump для аналитики, один pg_upgrade на следующую мажорную версию. Шесть шардов - шесть параллельных backup-окон, координированный pg_upgrade, схема, которая может разойтись между шардами, если кто-то накатил миграцию неправильно. Эту мысль регулярно повторяют core-разработчики PostgreSQL в публичных докладах: одиночный PostgreSQL - одна из самых простых в эксплуатации production-СУБД, шардированный PostgreSQL - одна из самых сложных в эксплуатации систем, какие вы можете собрать. Это не аргумент против шардирования - это аргумент за то, что выбор должен быть осознанным и не сделанным "на всякий случай".
Реальные пути: как Notion, GitLab и Discord жили со своими потолками
Каждая из этих компаний прошла собственный путь масштабирования и публично его описала. Истории разные, но паттерн один: реальные команды доходят до шардирования или альтернативной СУБД годами после первого "нам надо горизонтально масштабироваться", и решение всегда привязано к конкретной модели данных, а не к абстрактному размеру нагрузки.
Notion. В 2021 опубликовали "Herding elephants: lessons learned from sharding Postgres at Notion" (notion.com/blog). К моменту начала шардирования у них была одна Postgres-база, дисковый объём которой удваивался каждые 6-12 месяцев, существенный write-amplification на блоках (Notion-документ хранится как дерево блоков, и одно редактирование может породить сотни UPDATE-ов), и autovacuum, который перестал успевать. Решение - шардирование по workspace_id на 480 логических шардов поверх 32 физических баз (стандартный паттерн: логических шардов кратно больше, чем физических, чтобы потом перебалансировать без изменения ключа). Ключевое слово - workspace_id: Notion-документ всегда живёт в одном workspace, 99% запросов локальны workspace, и поэтому шардирование оказалось работоспособным. Без такого свойства шардирование не было бы решением.
GitLab. Многолетняя публичная серия в инженерном блоге (https://about.gitlab.com/blog/) показывает другой паттерн: вместо горизонтального шардирования - функциональная декомпозиция (ось Y по Эбботту-Фишеру). GitLab выделил CI/CD-данные в отдельную физическую БД, потом контейнерный реестр, потом отдельные домены ещё. Основная БД остаётся одной (с pgBouncer, реплики, large multi-TB volumes), но cross-domain-нагрузка ушла. Это позволило прожить намного дольше без необходимости шардирования и сохранить операционную простоту - каждый из доменных PostgreSQL отдельно проще, чем один шардированный. Урок: горизонтальное шардирование одной таблицы - не единственный способ снять нагрузку, иногда декомпозиция доменов проще и эффективнее.
Discord. История с двумя главами и важным уточнением: для message storage Discord никогда не использовал PostgreSQL, и пример полезен именно как контраст. В 2017 опубликовали "How Discord stores billions of messages" - переход с MongoDB на Cassandra для сообщений, потому что MongoDB переставал держать read-heavy нагрузку на канал и hot partitions. В 2023 опубликовали "How Discord stores trillions of messages" (discord.com/blog) - переход с Cassandra на ScyllaDB из-за GC pauses и hot partitions уже на Cassandra. PostgreSQL у Discord продолжает работать там, где модель данных подходит: метаданные пользователей, серверов, каналов, аутентификация. Урок не «PostgreSQL плох для сообщений», а более общий: выбор хранилища зависит не от размера, а от формы доступа. Если 95% запросов - чтение последних N записей из одного «канала», который пишется тысячами клиентов одновременно, ни Postgres, ни Mongo архитектурно не оптимизированы; wide-column СУБД с time-window-партиционированием подходит лучше. Эту границу полезно увидеть до того, как вы начали шардировать «не туда».
Стилизованный пример на основе типичных паттернов. Типичная команда B2B SaaS в 2024-2025 проходит примерно такой путь. Старт - один Postgres на 8 vCPU / 32 GB RAM, один инстанс приложения, прямые соединения. На пороге 50-100 RPS появляется PgBouncer в transaction mode и сразу решает проблему "почему у нас 800 backend-ов на 50 RPS". На пороге 500 RPS добавляется physical read replica, и аналитические запросы уезжают на неё; основной мастер свободнеет. На пороге 5000 RPS железо мастера апгрейдится до 32 vCPU / 128 GB, добавляется вторая read replica, начинается работа над "топ-20 медленных запросов" по pg_stat_statements - это даёт ещё 2-3x запаса. На объёме 1-2 ТБ начинается партиционирование самых больших таблиц (события, аудит, логи) по дате с автоматическим DROP старых партиций. И только если всё это перестаёт хватать (что для большинства SaaS-продуктов не наступает никогда), начинается серьёзный разговор о шардировании или о выделении части данных в специализированное хранилище.
Рамка действий: от симптома к решению
Если упростить весь предыдущий разговор до одной таблицы - получится примерно следующая декомпозиция симптомов и адекватных следующих шагов. Эта таблица - не алгоритм; реальные ситуации почти всегда сочетают несколько симптомов сразу, и порядок шагов имеет значение.
| Симптом | Что обычно происходит | Следующий шаг |
|---|---|---|
| "too many connections", сотни активных backend-ов | Каждый воркер приложения держит свой backend, ProcArrayLock contention | PgBouncer в transaction mode |
| Один SELECT тормозит, остальное - нормально | Отсутствует или неэффективен индекс, либо план запроса плохой | EXPLAIN ANALYZE, индекс, переписать запрос (Markus Winand) |
| Чтение в целом нагружает мастер, аналитика тяжёлая | Read-heavy профиль с одной точкой обработки | Physical read replica + маршрутизация в приложении |
| Vacuum не успевает, dead tuples растут | Большая горячая таблица, autovacuum со стандартными настройками | Тюнинг autovacuum, возможно партиционирование таблицы |
| WAL генерится быстрее, чем реплика успевает применить | Write-amplification, single-threaded walreceiver на реплике | Уменьшить write rate (батчинг), возможно отдельная Aurora-style СУБД |
| Объём одной таблицы > 1 ТБ, индексы не в RAM | Доступ к "горячим" данным размывается по всему индексу | Range partitioning по естественному ключу (обычно дата) |
| Объём БД физически не помещается на максимальный сервер | Реальный потолок vertical scaling | Шардирование - если есть single-shard ключ; иначе декомпозиция |
| "Нам надо как Notion / Discord / Stripe" | Преждевременная оптимизация, копирование чужого финального состояния | Вернуться к измерению реальных метрик |
Сводный путь, который проходит большинство успешных команд: оптимизация запросов и индексов → connection pooling → vertical scaling → physical replication для чтения → partitioning больших таблиц → функциональная декомпозиция доменов. Шардирование не входит в этот путь как обязательный шаг и для большинства продуктов не наступает никогда. Если ваше архитектурное решение опирается на "когда-нибудь мы зашардим" как страховку - стоит честно проверить, какой ключ это будет, какие cross-shard операции у вас в коде сегодня и сколько лет реальной нагрузки нужно для оправданности этого шага.
Bruce Momjian в одном из своих публичных докладов формулировал общий принцип так: "PostgreSQL is a tool, not a religion. Use it where it fits, replace it where it doesn't, and don't pretend the operational cost of distributed PostgreSQL is zero." Это полезная рамка не только для шардирования: для любого решения о масштабировании имеет смысл явно посчитать стоимость операционной сложности (на год, на пять лет), сравнить с реальным выигрышем по метрикам и принять решение с открытыми глазами.
pg_stat_statements почти всегда показывает 5-10 запросов, которые съедают 80% CPU; добавление индекса или переписывание - один день работы и месяцы запаса по нагрузке. Если после этого вы всё ещё упираетесь, делайте следующий шаг - но уже зная, какой именно потолок вы снимаете.
Источники и смежные статьи
- Martin L. Abbott, Michael T. Fisher, Tom Keeven. The Art of Scalability: Scalable Web Architecture, Processes, and Organizations for the Modern Enterprise. akfpartners.com/growth-blog/scale-cube - официальное описание AKF Scale Cube. Книга: Addison-Wesley, 2-е издание, 2015.
- Markus Winand. SQL Performance Explained, 2012. Книга и сайт use-the-index-luke.com. Базовая литература по индексированию и плану запросов.
- Bruce Momjian. Публичные презентации, в том числе серии "PostgreSQL Internals" и "Mastering PostgreSQL Administration". momjian.us.
- Andres Freund. Доклады на PGCon 2019-2022 о производительности WAL, replication и connection scaling.
- Robert Haas. Доклады на PGCon и PgConf.EU о declarative partitioning и parallel query.
- PgBouncer documentation. pgbouncer.org/usage.html. Раздел Pool modes - про различия session/transaction/statement.
- PostgreSQL documentation: DDL partitioning и Logical replication.
- Citus engineering blog. citusdata.com/blog. Практические разборы шардирования и распределённого планирования запросов.
- Notion engineering. "Herding elephants: lessons learned from sharding Postgres at Notion" (2021). notion.com/blog/sharding-postgres-at-notion.
- Discord engineering. "How Discord stores billions of messages" (2017) и "How Discord stores trillions of messages" (2023). discord.com/blog/how-discord-stores-trillions-of-messages.
- GitLab engineering blog. Многолетняя серия о database evolution. about.gitlab.com/blog.
- Kyle Kingsbury (Aphyr). Jepsen-анализы PostgreSQL и его деривативов. jepsen.io/analyses.
- Утилита pgtune - быстрая стартовая точка для подбора
postgresql.confпод железо.