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 и только потом переносите продакшен. Главные риски — неудачный ключ шардирования и ограничения уникальности.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте