Налаштування шардування бази даних для веб-застосунку

Коли обсяги даних і навантаження на запис починають перевищувати можливості одного сервера, стандартні методи оптимізації перестають допомагати. Ми проєктуємо та впроваджуємо горизонтальне шардування баз даних для веб-застосунків, розподіляючи навантаження між кількома вузлами. Наша команда реалізує проєкт під ключ — від аудиту та вибору стратегії до впровадження та подальшої підтримки.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Налаштування шардування бази даних для веб-застосунку
Складний
~1-2 тижні
Часті запитання

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

Процес роботи

  1. Аналіз поточного навантаження та bottleneck-ів: вимірюємо write-потік, latency, розмір бази, патерни запитів.
  2. Проєктування схеми: вибір ключа шарда, кількості шардів, стратегії реплікації.
  3. Розробка роутера та міграція даних: реалізація маршрутизації (Citus або application-level), перенесення даних з мінімальним downtime.
  4. Навантажувальне тестування: емулюємо пікове навантаження, перевіряємо latency та пропускну здатність.
  5. Деплой та моніторинг: налаштовуємо алерти на гарячі точки, повільні запити, збої ребалансування.

Решардування: як додати новий шард без простою?

При використанні 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.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1141
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060