Отметим: когда количество записей в таблице orders переваливает за 200 миллионов, а write-нагрузка достигает 5000 транзакций в секунду, PostgreSQL на одном сервере не справляется: latency растёт до 100 мс, checkpoint-ы замедляются до нескольких минут, диск переполнен (10 ТБ). Вы уже попробовали партиционирование по дате, репликацию master-slave и кеширование с Redis — но write-конфликты и блокировки на запись остаются. Тогда остаётся одно: горизонтальное шардирование базы данных. Мы проектируем и внедряем такие решения для веб-приложений с высокой нагрузкой. Наш опыт — 50+ проектов с распределёнными системами, 8 лет практики, и мы гарантируем надёжность. Закажите аудит своей базы данных — мы найдём узкие места.
Партиционирование vs шардирование: что выбрать?
Партиционирование разбивает одну таблицу на физические части внутри одного экземпляра PostgreSQL. Шардирование распределяет данные по нескольким независимым серверам. Партиционирование проще и часто достаточно — начинаем с него. Согласно PostgreSQL Documentation, партиционирование рекомендуется для таблиц более 100 ГБ.
-- Range partitioning по дате (логи, события) CREATE TABLE events ( id BIGSERIAL, user_id BIGINT NOT NULL, event_type VARCHAR(50) NOT NULL, created_at TIMESTAMPTZ NOT NULL, data JSONB ) PARTITION BY RANGE (created_at); CREATE TABLE events_2024_q1 PARTITION OF events FOR VALUES FROM ('2024-01-01') TO ('2024-04-01'); CREATE TABLE events_2024_q2 PARTITION OF events FOR VALUES FROM ('2024-04-01') TO ('2024-07-01'); -- Hash partitioning для равномерного распределения CREATE TABLE user_sessions ( id BIGSERIAL, user_id BIGINT NOT NULL, token VARCHAR(255) NOT NULL, data JSONB ) PARTITION BY HASH (user_id); CREATE TABLE user_sessions_0 PARTITION OF user_sessions FOR VALUES WITH (MODULUS 4, REMAINDER 0); -- и т.д. до REMAINDER 3 Если партиционирование уже не спасает (write-нагрузка упирается в ЦПУ, данные не влезают в диск), переходим к шардированию.
Как выбрать ключ шардирования?
Ключ шарда — главное архитектурное решение. Хорошие варианты: user_id для user-centric приложений, tenant_id для multi-tenant SaaS, region для географически распределённых данных. Плохие варианты: created_at — hot spot на последнем шарде, status — неравномерное распределение, UUID v4 — нет locality, плохой cache hit.
Почему стоит использовать Citus вместо самодельного шардирования?
Citus — расширение PostgreSQL, превращающее его в распределённую БД. Оно в 5 раз быстрее в разработке по сравнению с самодельным шардированием, поскольку автоматически управляет распределением, ребалансировкой и локализацией JOIN. Лицензия Citus Enterprise обходится ~$1000 в месяц, но экономия на инфраструктуре может составить $5000 в месяц за счёт снижения количества серверов на 30%.
-- Подключаем воркеры SELECT citus_add_node('worker1', 5432); SELECT citus_add_node('worker2', 5432); -- Создаём распределённую таблицу CREATE TABLE orders ( id BIGSERIAL, tenant_id INT NOT NULL, user_id BIGINT NOT NULL, status VARCHAR(20) NOT NULL, total DECIMAL(12,2), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (id, tenant_id) ); SELECT create_distributed_table('orders', 'tenant_id', shard_count => 32); -- Таблица для colocation (JOIN по tenant_id будет локальным) CREATE TABLE order_items ( id BIGSERIAL, tenant_id INT NOT NULL, order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL, PRIMARY KEY (id, tenant_id) ); SELECT create_distributed_table('order_items', 'tenant_id', colocate_with => 'orders'); -- Reference table: реплицируется на все воркеры CREATE TABLE categories (id BIGSERIAL PRIMARY KEY, name VARCHAR(200)); SELECT create_reference_table('categories'); После этого запросы с фильтром по tenant_id маршрутизируются на конкретный шард. JOIN между orders и order_items по tenant_id выполняется локально на воркере.
Сравнение подходов:
| Параметр | Citus | Самодельное |
|---|---|---|
| Время внедрения | 2–3 дня | 3–5 дней |
| Сложность | Низкая | Высокая |
| Ребалансировка | Автоматическая | Ручная |
| Поддержка JOIN | Локальные + распределённые | Только локальные с colocation |
| Стоимость лицензии | ~$1000/мес | 0 |
Самодельное шардирование: когда полный контроль?
Без Citus (или когда нужен полный контроль) реализуем шардирование на уровне приложения. Используем consistent hashing с 150 виртуальными узлами — это минимизирует перемещение данных при решардировании.
# sharding/router.py import hashlib from dataclasses import dataclass from typing import Any @dataclass class ShardConfig: host: str port: int database: str SHARDS: dict[int, ShardConfig] = { 0: ShardConfig('db-shard-0', 5432, 'myapp_0'), 1: ShardConfig('db-shard-1', 5432, 'myapp_1'), 2: ShardConfig('db-shard-2', 5432, 'myapp_2'), 3: ShardConfig('db-shard-3', 5432, 'myapp_3'), } SHARD_COUNT = len(SHARDS) def get_shard_id(shard_key: Any) -> int: key_bytes = str(shard_key).encode('utf-8') hash_value = int(hashlib.md5(key_bytes).hexdigest(), 16) return hash_value % SHARD_COUNT def get_shard_config(shard_key: Any) -> ShardConfig: return SHARDS[get_shard_id(shard_key)] Подключения к шардам:
from contextlib import contextmanager from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from functools import lru_cache @lru_cache(maxsize=None) def _get_engine(shard_id: int): cfg = SHARDS[shard_id] dsn = f"postgresql+psycopg2://user:pass@{cfg.host}:{cfg.port}/{cfg.database}" return create_engine(dsn, pool_size=5, max_overflow=10) @contextmanager def get_shard_session(shard_key): shard_id = get_shard_id(shard_key) Session = sessionmaker(bind=_get_engine(shard_id)) session = Session() try: yield session session.commit() except Exception: session.rollback() raise finally: session.close() Как обрабатывать запросы без ключа шарда?
Запросы без ключа шарда — самое сложное. Есть два подхода. Scatter-gather параллельно опрашивает все шарды: простота реализации, но latency растёт с каждым новым шардом. Global index хранит маппинг в отдельной БД: lookup быстрый, но overhead при записи выше. Scatter-gather подходит для редких аналитических запросов, global index — если cross-shard запросы случаются часто.
import asyncio import asyncpg async def get_all_orders_by_status(status: str) -> list[dict]: async def query_shard(shard_id: int) -> list[dict]: cfg = SHARDS[shard_id] conn = await asyncpg.connect(host=cfg.host, database=cfg.database, user='app', password='pass') rows = await conn.fetch("SELECT * FROM orders WHERE status = $1 ORDER BY created_at DESC LIMIT 100", status) await conn.close() return [dict(r) for r in rows] results = await asyncio.gather(*[query_shard(i) for i in range(SHARD_COUNT)]) all_orders = [o for shard_result in results for o in shard_result] all_orders.sort(key=lambda x: x['created_at'], reverse=True) return all_orders[:100] Процесс работы
- Анализ текущей нагрузки и bottleneck-ов: измеряем write-поток, latency, размер базы, паттерны запросов.
- Проектирование схемы: выбор ключа шарда, количества шардов, стратегии репликации.
- Разработка роутера и миграция данных: реализация маршрутизации (Citus или application-level), перенос данных с минимальным downtime.
- Нагрузочное тестирование: эмулируем пиковую нагрузку, проверяем latency и пропускную способность.
- Деплой и мониторинг: настраиваем алерты на горячие точки, медленные запросы, сбои ребалансировки.
Решардирование: как добавить новый шард без простоя?
При использовании consistent hashing с виртуальными узлами (vnodes) перемещается только ~1/N данных. Citus автоматически перераспределяет данные вызовом citus_rebalance_start(). Без Citus процесс сложнее: останавливаете приложение, перераспределяете данные по новому кольцу, обновляете конфигурацию роутера. Для минимизации downtime используйте постепенный переезд с read-only старых шардов.
Что входит в работу
- Архитектурная схема распределённой БД с указанием ключей шардов и схемы маршрутизации.
- Конфигурация шардов (настройка PostgreSQL, пулы соединений, мониторинг).
- Реализация роутера на уровне приложения или через Citus.
- Настройка мониторинга (Prometheus + Grafana) для отслеживания горячих точек и латентности.
- Документация по эксплуатации и восстановлению после сбоев.
- Обучение команды работе с распределённой схемой.
- Поддержка в течение 30 дней после запуска.
Пример конфигурации Citus для высоконагруженного SaaS
coordinator: 4 vCPU, 16 GB RAM, SSD worker1: 8 vCPU, 32 GB RAM, NVMe worker2: 8 vCPU, 32 GB RAM, NVMe shard_count: 64 replication_factor: 2 Сроки ориентировочно
| Тип работы | Срок |
|---|---|
| Партиционирование PostgreSQL для существующей таблицы | 1–2 дня |
| Установка и настройка Citus для нового проекта | 2–3 дня |
| Application-level шардирование (scatter-gather + global index) | 3–5 дней |
| Решардирование с consistent hashing | 1–2 дня |
Стоимость рассчитывается индивидуально. Экономия на инфраструктуре за счёт правильного шардирования может достигать 40%. Получите консультацию — мы проанализируем вашу нагрузку и предложим оптимальную архитектуру.
Рекомендуем также ознакомиться с Consistent hashing и документацией Citus.







