Настройка шардирования базы данных для веб-приложения

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

Разработка и обслуживание любых видов сайтов:

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Настройка шардирования базы данных для веб-приложения

Бэкенд-разработка: Laravel, Node.js, Django, Rails, GoСложный~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
    1543
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1396
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1375
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1143
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060