Зауважте: коли кількість записів у таблиці 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.







