Citus — расширение PostgreSQL, которое превращает обычный сервер в распределённую базу: данные разбиваются на шарды и раскладываются по рабочим узлам, а координатор принимает запросы и собирает результат. Вы остаётесь в знакомом SQL с транзакциями, индексами и драйверами PostgreSQL. Разберём архитектуру, установку стенда, выбор ключа шардирования, ограничения и диагностику.
Как устроен Citus
В кластере есть координатор и несколько воркеров. Координатор хранит метаданные: какие таблицы распределены, на какие шарды и узлы они лежат. Сами данные лежат на воркерах. Клиент подключается только к координатору и не знает о шардах: расширение само разбирает запрос, отправляет задачи на нужные узлы и собирает ответ.
Ключевое понятие — колонка распределения: по её значению строка попадает в конкретный шард. Запрос с фильтром по этой колонке уходит ровно на один шард. Без фильтра приходится опрашивать все шарды и агрегировать результат — дороже, но всё ещё один SQL-запрос.
Быстрый стенд в Docker
Для экспериментов хватит трёх контейнеров в одной сети. Образ citusdata/citus уже содержит расширение и настроенный shared_preload_libraries.
docker network create citus-net
docker run -d --name coord --network citus-net -p 5432:5432 \
-e POSTGRES_PASSWORD=secret citusdata/citus
docker run -d --name worker1 --network citus-net \
-e POSTGRES_PASSWORD=secret citusdata/citus
docker run -d --name worker2 --network citus-net \
-e POSTGRES_PASSWORD=secret citusdata/citus
Подключитесь к координатору: psql «postgres://postgres:secret@localhost:5432/postgres». Воркеры доступны по именам worker1 и worker2 внутри Docker-сети, поэтому в SQL указывайте именно их, а не localhost.
Инициализация кластера
Расширение создаётся на координаторе, затем туда добавляются воркеры. На каждом воркере расширение должно быть установлено — в готовом образе это уже сделано.
CREATE EXTENSION IF NOT EXISTS citus;
SELECT citus_set_coordinator_host('coord', 5432);
SELECT citus_add_node('worker1', 5432);
SELECT citus_add_node('worker2', 5432);
SELECT nodename, nodeport, isactive FROM pg_dist_node;
citus_set_coordinator_host прописывает координатор в метаданные, чтобы он участвовал в запросах как обычный узел. Старые имена master_add_node сохранены для совместимости, но в новом коде используйте префикс citus_. Последний запрос должен показать три активных узла.
Выбор ключа шардирования
Это самое важное решение при миграции. Хороший ключ — тот, по которому чаще всего фильтруют запросы и который равномерно распределяет данные. Для SaaS это обычно tenant_id: данные клиента лежат на одном шарде, а запросы не выходят за его пределы.
Плохой выбор — низкоселективная колонка вроде status: шарды получатся перекошенными, а запросы всё равно обойдут кластер. Не шардируйте по суррогатному id, если бизнес-запросы идут по клиенту.
Типы таблиц
В Citus есть распределённые таблицы, reference-таблицы и локальные. Первые делятся на шарды по колонке, вторые целиком копируются на каждый узел, третьи остаются на координаторе.
SET citus.shard_count = 16;
SET citus.shard_replication_factor = 2;
CREATE TABLE orders (
tenant_id bigint NOT NULL,
order_id bigint NOT NULL,
total numeric(12,2) NOT NULL,
PRIMARY KEY (tenant_id, order_id)
);
SELECT create_distributed_table('orders', 'tenant_id');
CREATE TABLE countries (code char(2) PRIMARY KEY, name text NOT NULL);
SELECT create_reference_table('countries');
Число шардов задаётся до создания таблицы и влияет на всю её жизнь, поэтому выбирайте с запасом: 16–32 шарда на пару воркеров. Таблицы с колонкой распределения одного типа и одинаковым числом шардов попадают в одну группу размещения, и соединения между ними идут локально. Репликация factor = 2 защищает от потери узла, но занимает вдвое больше места.
Как читать планы запросов
EXPLAIN в Citus показывает количество задач, которые координатор отправит воркерам. Это главный индикатор качества запроса.
EXPLAIN (COSTS OFF) SELECT * FROM orders WHERE tenant_id = 42;
-- Custom Scan (Citus Adaptive)
-- Task Count: 1
EXPLAIN (COSTS OFF) SELECT count(*) FROM orders;
-- Custom Scan (Citus Adaptive)
-- Task Count: 16
Task Count: 1 означает router-план: запрос уйдёт на один шард. Task Count, равный числу шардов, — признак распределённого сканирования. Для отчётов это нормально, для OLTP — повод добавить фильтр по колонке распределения. SET citus.log_remote_commands = on покажет команды на воркерах.
Изоляция тенантов и ребалансировка
Если один клиент вырос и мешает соседям, его шард можно вынести на отдельный узел.
SELECT isolate_tenant_to_new_shard('orders', 42, 'CASCADE');
SELECT citus_move_shard_placement(102040, 'worker1', 5432, 'worker2', 5432);
-- 102040 — пример shardid из citus_shards
SELECT citus_rebalance_start();
SELECT citus_rebalance_wait();
isolate_tenant_to_new_shard создаёт отдельный шард для тенанта и с режимом CASCADE переносит связанные таблицы. citus_move_shard_placement двигает конкретный шард между узлами, а citus_rebalance_start в Citus 11+ запускает фоновую перебалансировку; citus_rebalance_wait ждёт её завершения. В старых версиях использовалась блокирующая rebalance_table_shards().
Колоночное хранение
Для аналитики есть колоночный access method: данные хранятся по столбцам и лучше сжимаются.
CREATE TABLE events (
tenant_id bigint NOT NULL,
created_at timestamptz NOT NULL,
payload jsonb
) USING columnar;
ALTER TABLE events SET ACCESS METHOD columnar;
Колоночные таблицы выигрывают при сканировании больших диапазонов и в агрегатах, но плохо подходят для частых точечных UPDATE. Их тоже можно распределять по tenant_id, если аналитика идёт в разрезе клиентов.
Ограничения, о которых стоит знать заранее
- Первичный ключ и уникальные индексы должны включать колонку распределения.
- Внешние ключи работают, только если обе таблицы распределены по связанным колонкам.
- Соединения между разными группами размещения требуют репартиционирования и citus.enable_repartition_joins.
- Последовательности локальны для шарда, поэтому глобальный autoincrement не гарантируется.
- Координатор — точка отказа; для отказоустойчивости его резервируют.
- Часть DDL и служебных команд выполняется на воркерах через run_command_on_workers.
- Распределённые транзакции используют двухфазный коммит и стоят дороже локальных.
Число шардов после создания таблицы менять неудобно, поэтому тестируйте нагрузку до миграции. Бэкапить нужно и координатор с метаданными, и каждый воркер.
Диагностика
SELECT citus_version();
SELECT * FROM citus_tables;
SELECT table_name, shardid, shard_size, nodename
FROM citus_shards ORDER BY shard_size DESC LIMIT 10;
SELECT * FROM citus_stat_tenants LIMIT 10;
- citus_tables показывает тип таблицы, колонку распределения и число шардов.
- citus_shards помогает найти перекошенные шарды по размеру.
- citus_stat_tenants в Citus 11+ показывает активность по тенантам, если включён сбор статистики.
- citus_stat_activity и pg_stat_activity на воркерах показывают, что выполняется сейчас.
- При ошибке «cannot find shard» проверьте pg_dist_node и сеть до воркеров.
Итог
Citus даёт горизонтальное масштабирование PostgreSQL без смены стека: схема проектируется вокруг tenant_id, таблицы распределяются, а планы проверяются по числу задач. Начните с Docker-стенда, проверьте router-запросы на Task Count: 1 и только потом переносите продакшен. Главные риски — неудачный ключ шардирования и ограничения уникальности.