Learning Platform
Урок 13.05 · 23 мин
Продвинутый
FDWpostgres_fdwShardingCitusDistributed

В предыдущих уроках мы партиционировали таблицу внутри одного сервера — для управления размером, ускорения VACUUM и удаления старых данных через DROP. Но что если один сервер перестал справляться с нагрузкой? Если CPU держит запросы 100%, диск близок к лимиту, репликация буксует — это сигнал, что данных физически слишком много для одной машины. Нужен scale-out: разнести данные по нескольким серверам.

Постгрес из коробки не умеет настоящий sharding — нет single point of truth, который координирует запросы между нодами. Но есть обходной путь: declarative partitioning + postgres_fdw, где каждая partition — это foreign table, ссылающаяся на отдельный физический сервер. Этот урок про то, как это работает, какие у этого подхода честные границы, и почему в продакшене обычно выбирают Citus.

Foreign Data Wrapper: основы

FDW
— это абстракция «таблица, физически живущая в другой системе». Можно создать foreign table, который выглядит как обычная таблица, но при SELECT Postgres отправляет запрос на целевой источник, получает строки и вставляет их в план.

postgres_fdw — самый распространённый: целевой источник — другой PostgreSQL-сервер.

postgres_fdw в действии

Координирующий сервер (coordinator) хранит схему и роутит запросы; данные лежат на data nodes. SELECT прозрачен для приложения.

Coordinator: pgsrv-mainparent table + foreign partitions
data node: pgsrv-euorders_eu (real heap)
postgres_fdwforeign table on main
data node: pgsrv-usorders_us (real heap)
postgres_fdwforeign table on main
data node: pgsrv-asiaorders_asia (real heap)
postgres_fdwforeign table on main
SELECT * FROM orders WHERE region = 'eu' ...planner pushdown'ит фильтр на pgsrv-eu

Главные понятия:

  • Foreign server — описание удалённого сервера: host, port, dbname.
  • User mapping — соответствие локального пользователя удалённому (логин/пароль).
  • Foreign table — таблица-«заглушка», представляющая удалённый объект.

Синтаксис: как выглядит установка

Pglite в браузере не поддерживает postgres_fdw — для FDW нужно настоящее network-соединение к другому Postgres, чего в WASM нет. Поэтому в этом уроке мы покажем синтаксис и логику, но без <SqlSandbox> для запуска. Изучай и применяй на реальной БД.

Подготовка одного FDW:

-- 1. На coordinator: установить extension
CREATE EXTENSION postgres_fdw;

-- 2. Описать удалённый сервер
CREATE SERVER pgsrv_eu
  FOREIGN DATA WRAPPER postgres_fdw
  OPTIONS (host 'pg-eu.internal', port '5432', dbname 'orders_db');

-- 3. Прописать кто как туда ходит
CREATE USER MAPPING FOR app_user
  SERVER pgsrv_eu
  OPTIONS (user 'app_user', password 'secret');

-- 4. Создать foreign table
CREATE FOREIGN TABLE orders_eu (
  id           BIGINT,
  customer_id  INT,
  region       TEXT,
  placed_at    TIMESTAMPTZ,
  total_cents  INT
)
  SERVER pgsrv_eu
  OPTIONS (schema_name 'public', table_name 'orders');

После этого SELECT * FROM orders_eu WHERE customer_id = 42 на coordinator превращается в SELECT ... WHERE customer_id = 42 на pgsrv-eu и возвращает только подходящие строки. Это удобно само по себе — у тебя cross-server queries. Но интересное начинается, когда мы поверх этого построим partitioned table.

Sharding через partitioning + FDW

Идея: создаём partitioned table на coordinator, а partitions — это foreign tables, каждая на своём data node. INSERT в parent роутится в нужную foreign partition по partition key.

Симуляция sharding локально (без FDW): partitioned orders по LIST (region) с 3 partition, каждая на отдельной locally-эмулированной таблице. В реальном setup partitions будут foreign tables на 3 серверах.

PostgreSQL

В продакшене вместо CREATE TABLE orders_eu PARTITION OF orders FOR VALUES IN (...) ты пишешь:

CREATE FOREIGN TABLE orders_eu PARTITION OF orders
  FOR VALUES IN ('DE', 'FR', 'IT', 'NL')
  SERVER pgsrv_eu
  OPTIONS (schema_name 'public', table_name 'orders_eu_data');

Это работает: pglite не позволяет проверить, но синтаксис стандартен с Postgres 11+. SELECT с фильтром по region pushdown’ится в нужный shard и читает только его. Pruning работает между foreign partitions точно так же, как между локальными.

Что pushdown работает, а что нет

Поведение postgres_fdw в плане pushdown’а — это самая болезненная часть. Идеальный план — когда вся «тяжёлая работа» (filter, join, aggregate) делается на data node, а coordinator только склеивает результат. На практике pushdown срабатывает не всегда.

Что pushdown'ится в postgres_fdw

WHERE с константами и операторами над колонками — pushdown. Join, aggregate, ORDER BY, LIMIT — частично. Функции и cast — почти никогда.

Pushdown работаетхорошо
WHERE col = constвсегда
WHERE col IN (...)всегда
ORDER BY col LIMIT Nесли планировщик решит
JOIN foreign-foreign на одном сервереда (если включено)
Aggregate (sum, count)да (postgres_fdw >= PG10)
Pushdown НЕ работаетплохо
Функции на колонкеWHERE date_trunc(...) = ...
JOIN cross-serverданные тянутся к coordinator
Кастомные UDFесли не помечены как pushable
window functionsвсегда локально
Диагностика: EXPLAIN VERBOSERemote SQL: ... покажет что улетело на data node

EXPLAIN (VERBOSE) покажет точный SQL, который Postgres отправит на data node — это главный инструмент дебага FDW. Если ты видишь, что вместо WHERE customer_id = 42 отправляется SELECT * FROM orders и фильтрация делается локально — это убитая производительность.

Включить агрессивный pushdown для конкретного сервера:

ALTER SERVER pgsrv_eu OPTIONS (
  use_remote_estimate 'true',   -- ходить за статистикой на remote
  fetch_size '10000',            -- сколько строк тянуть за один RTT
  async_capable 'true'           -- PG14+: parallel fetch с разных shards
);

Cross-shard JOIN: главная проблема

Идеальный случай sharding — когда каждый запрос трогает только один shard. На LIST по region запрос «дай все заказы из EU» — это single shard query, идёт на pgsrv-eu и возвращает результат за нормальное время.

Проблема — cross-shard queries. Запрос «дай топ-10 customers по обороту за год» нельзя выполнить на одном shard: пользователи могут быть из разных регионов. Coordinator должен либо собрать данные со всех shards и сделать aggregation у себя, либо как-то распределить вычисление.

postgres_fdw + partitioning делает это неуклюже: он скачает партиции на coordinator и сделает локальный JOIN. Если данных много — медленно и память забивается.

Когда уже Citus?

Citus
— это extension, который превращает Postgres в настоящую distributed database. В отличие от FDW + partitioning, Citus:

  • Распределяет JOIN и aggregate по shards параллельно (не тянет всё на coordinator).
  • Поддерживает reference tables — таблицы, реплицированные на каждый shard (lookup-таблицы, словари).
  • Делает distributed transactions через 2PC между shards.
  • Имеет coordinator с per-tenant routing.

Чек-лист: если у тебя

  • 1 TB данных, не помещающихся в один сервер,

  • большая часть запросов укладывается в один shard по естественному ключу (tenant_id, customer_id),
  • редкие cross-shard аналитические запросы — приемлемо медленнее,

→ Citus. Если у тебя простая регионально-разнесённая система с одним типом cross-shard запроса (e.g. админский dashboard на coordinator), и больше нужно изолировать данные по регулятору, чем масштабироваться — postgres_fdw + partitioning норм.

Альтернативы:

  • YugabyteDB — Postgres-совместимая distributed база с шардированием на storage layer; не extension, а отдельный fork.
  • CockroachDB — wire-compatible с Postgres, но переписан с нуля, distributed by design.
  • Vitess — для MySQL, не релевантно, но как пример sharding-слоя.

Honest tradeoffs

Чтобы было трезво:

Sharding tradeoffs

Кажется, что sharding решает всё. На практике он переносит проблему: даёт горизонтальный scale в обмен на сложность всех cross-shard операций.

Плюсы sharding
Линейный scaleкаждый новый shard добавляет CPU/disk
Локальные запросыsingle-shard queries быстры
Изоляцияотказ одного shard не валит весь
Регуляторикаданные EU физически в EU
Минусы sharding
Cross-shard queriesмедленные, нет нормальных JOIN
Global unique constraintsнет: только в пределах shard
Распределённые транзакции2PC, повышенная сложность
Reshardingсложная операция, требует downtime
OperationsN x backup, N x monitoring, N x upgrade

Правило большого пальца: пока влезаешь в одну машину — не шардируй. Современный сервер с 1 TB NVMe и 256 GB RAM выдерживает базы в десятки TB и сотни тысяч TPS. Гораздо чаще проблема не в железе, а в плохих запросах, отсутствии индексов или дрянной схеме. Sharding — это последний шаг, к которому идут после всех остальных оптимизаций.

Проверка знанийKnowledge check
У тебя SaaS, multi-tenant, 99% запросов — это поиск по tenant_id. Данных уже 4 TB, один сервер не справляется. Что выбираешь: postgres_fdw + LIST partitioning по tenant_id или Citus с distributed tables hashed по tenant_id?
ОтветAnswer
Citus, и вот почему. Тебе нужно (1) равномерное распределение нагрузки по нодам — Citus делает hash partitioning автоматически, postgres_fdw + LIST требует ручного балансирования: одни tenants крупные, другие мелкие, ты руками решаешь, кого на какой shard поселить, и каждый ресхард — миграция. (2) Pushdown агрегаций — Citus умеет параллельно агрегировать на data nodes и собирать итог на coordinator; postgres_fdw такие операции делает плохо. (3) Reference tables — Citus реплицирует справочники (например plans, features) на каждый shard для локальных JOIN; через postgres_fdw такого механизма нет, придётся либо вручную реплицировать, либо страдать от cross-shard JOIN. (4) Operations — Citus имеет встроенный rebalancer; FDW требует ручных миграций. postgres_fdw + partitioning подойдёт, если shards разделены по очень стабильному критерию (регулятор, регион) и cross-shard аналитика делается отдельным OLAP-стеком.

Чек-лист

  • postgres_fdw + declarative partitioning даёт «poor man’s sharding» — partitions живут как foreign tables на разных серверах.
  • Pushdown работает для простых WHERE и aggregate; функции, cast и cross-shard JOIN — нет. Проверяй через EXPLAIN (VERBOSE).
  • Single-shard queries работают отлично; cross-shard тянут данные на coordinator и медленные.
  • Citus — реальная distributed база поверх Postgres, с pushdown JOIN, reference tables, 2PC и rebalancer.
  • Не шарди раньше времени: один современный сервер выдерживает десятки TB. Сначала индексы, planning, schema.
  • Алгоритм решения: regulatory + region-isolated → FDW; high-scale OLTP + analytics → Citus; распределённый ACID → YugabyteDB/CockroachDB.
Стратегия шардирования и Distributed engine Разрешение коллизий: open addressing vs chaining

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что такое postgres_fdw в контексте sharding?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5