Перейти к содержимому
К программе курса
Архитектура высоконагруженных веб-приложений
7 / 1164%

Масштабирование БД: репликация, шардирование и борьба с lag

90 минут

Для кого: инженеры, которые уже упёрлись в одну БД и понимают, что «добавить CPU» больше не спасает. Если вы не знаете свой replication lag или продолжаете верить, что multi-master решит всё, — это чтение должно вас встряхнуть.

Провокация №1: вы видите replication lag в реальном времени?

  • Если в Grafana нет графика pg_stat_replication.lag, значит любая репликация — русская рулетка. Завтра маркетинг пошлёт пуш, master выдержит, а реплики 5 секунд будут отдавать устаревшие данные.
  • Правило: если лаг > 500 мс и вы не снижаете трафик/переключаете чтения на master, вы обманываете пользователей.

1. Репликация без иллюзий

1.1 Master → Replica (Primary → Read replicas)

  • Записи — только на primary, чтения — на репликах.
  • Реплики = eventual consistency. Примите это или не используйте их.

Конфиг PostgreSQL:

# postgresql.conf на primary
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10

Мониторинг:

SELECT application_name,
       state,
       pg_xlog_location_diff(pg_current_xlog_location(), replay_lsn) AS bytes_lag
FROM pg_stat_replication;

1.2 Чтение с реплик без попадания в lag

  • Read-after-write: всегда читайте собственные данные с master, либо оборачивайте в кэш с временным «приклеиванием» к master.
  • Sticky на master: пользователь только что обновил профиль? Следующие 5 секунд читайте из master.
function getReadPool(userId: number, currentUserId?: number) {
  if (userId === currentUserId) return masterPool;
  return pickReplica();
}

1.3 Replication lag alerts

Lag, msДействия
< 100норма
100–500смотрим сеть/диск, проверяем wal_sender
> 500переключаем критичные эндпоинты на master, уведомляем on-call
> 2000отключаем реплики из пулов, разбираемся немедленно

Провокация №2: вы понимаете, что multi-master — это не панацея?

  • Multi-master = конфликты, split-brain, сложный консенсус. Если вы его хотите, скорее всего вам нужно просто три региона с primary/replica и good failover.

2. Multi-master (когда действительно нужно)

  • Используйте, если: multi-region активные записи, распределённые пользователи, и вы готовы к CRDT/конфликтам.
  • Примеры: CockroachDB, Spanner (true multi-master, но дорого).
  • Провал: «Сделали master-master на MySQL, получили конфликт PK, потеряли 100 заказов». Да, это реальная история. Если нет автоматического конфликт-резолвинга, не прыгайте туда.

Провокация №3: вы знаете, где проходит граница одного шарда?

Если вы не можете быстро ответить, где лежит пользователь с id=123456 — ваша шард-схема не контролируется.

3. Шардирование: варианты и грабли

3.1 Range Sharding

  • Простая логика (user_id < 1000).
  • Проблема: hot range. Пользователи регятся линейно → первый shard умирает.

Диагностика:

SELECT shard_id, COUNT(*)
FROM users
GROUP BY shard_id;

3.2 Hash Sharding

  • Равномерно распределяет нагрузку, но при добавлении шарда нужна ремиграция.
function shardByHash(key: string) {
  const hash = createHash("md5").update(key).digest();
  return shards[hash[0] % shards.length];
}

3.3 Consistent Hashing

  • Добавляем виртуальные ноды, чтобы при добавлении/удалении сервера мигрировала только часть ключей.
class ConsistentHash {
  // ... как из урока про кэш
}

3.4 Meta-router

  • Заводите сервис/таблицу с mapping user_id → shard. Это ваш single source of truth.
ПолеЗначение
user_id12345
shard_hostshard2.db.local
created_at...

Проверка: при миграции обновите mapping, логируйте обращения к несуществующим shard'ам.

Провокация №4: как обрабатываются cross-shard запросы?

  • Если ответ «никак, мы надеемся, что клиенту не нужно» — ждите инцидента. Рано или поздно понадобится выборка «10 самых новых заказов». Где они? На всех шардах.

4.1 Scatter-Gather

  • Шлём запрос на все шарды, ждём самый медленный.
  • Снимайте метрики scatter_latency. Если > 2s — ищите «медленный» shard.
const responses = await Promise.all(
  shards.map((shard) => shard.query(q, params))
);
const merged = mergeAndSort(responses);

4.2 Denormalization / Search DB

  • Дублируем ключевые данные в отдельную БД/ES. Смотрим там, потом обращаемся к нужному shard.
  • Обновление = события «user updated» → обновить search store.

4.3 Прозрачный router vs embedded logic

  • Сервис-router: приложение делает запрос к API, оно уже знает, куда идти.
  • Embedded logic: язык уровня ORM (Prisma, etc.) содержит логику шардирования. Опасно без тестов.

Провокация №5: готовы ли вы к distributed transactions?

  • Если у вас кэш + шардированные таблицы + реплики, рано или поздно будут переводы между шардами. Без процесса — потеряете деньги.

5.1 Two-Phase Commit (2PC)

  • Сильная консистентность, но высокая стоимость.
  • Требует coordinator, сохраняет состояние между этапами, чувствителен к сетевым паузам.

Не используйте, если: нет процессов мониторинга, не умеете перезапускать coordinator, не готовы держать долгие блокировки.

5.2 Saga

  • Последовательность локальных транзакций + компенсирующие шаги.
  • Работает с eventual consistency, но «клиент получает деньги позже».
await saga.run([withdraw(fromShard), deposit(toShard)]);

Чек-лист Sagas:

  1. Все шаги должны быть идемпотентными.
  2. Компенсации — обязательные.
  3. Логируйте «какой шаг застрял».

Провокация №6: ваша шард-схема переживёт добавление нового шарда?

  • Если ответ «мы просто увеличим массив на один элемент» — вы в беде. Нужно мигрировать данные, обновить router, прогреть кэш.

6. Добавление шарда без смерти системы

  1. Подготовьте routing: добавьте shard в конфиг, но не маршрутизируйте туда трафик.
  2. Мигрируйте данные: batch миграции (по 10k записей), проверяйте консистентность.
  3. Грейс-период: пока кэш не прогреется, hedged requests (дублируйте на старый shard).
  4. Снимите метрики: hot_keys, shard_distribution.

Провокация №7: вы считаете стоимость lag и query стоимости?

  • Реплики, шарды, денормализация — всё стоит денег. Monitor cost_per_query + «сколько стоит держать shard».

7. Экономика масштабирования БД: когда репликация и шардирование окупаются

Критический вопрос: Read replica за $730/мес или кэш за $276/мес? Шардирование за $50k работы или вертикальное масштабирование за $500/мес? Считайте, не гадайте.

7.1 Стоимость репликации: read replicas

Сценарий: PostgreSQL db.r5.2xlarge ($730/мес), 80% read queries, CPU 85%

Вариант A: Вертикальное масштабирование

  • Апгрейд до db.r5.4xlarge
  • Стоимость: $1,460/месяц
  • Решает: CPU снижается до 40%

Вариант B: Read replica

  • Primary: db.r5.2xlarge ($730/мес)
  • Replica: db.r5.2xlarge ($730/мес)
  • Стоимость: $1,460/месяц
  • Решает: read load распределяется 50/50

Вариант C: Кэш (Redis)

  • Primary: db.r5.2xlarge ($730/мес)
  • Redis: 3× cache.r6g.large ($276/мес)
  • Стоимость: $1,006/месяц
  • Решает: 85% reads из кэша, CPU БД падает до 30%

Сравнение:

РешениеCost/monthCPU reductionComplexityBest for
Вертикальное$1,46050%НизкаяВременное решение
Read replica$1,46040%СредняяEventual consistency OK
Кэш$1,00670%СредняяCacheable data

Вывод: Кэш на 31% дешевле и эффективнее при высоком hit ratio.


7.2 Экономика шардирования: когда оно окупается

Проблема: Одна БД не справляется, нужно шардирование.

Стоимость шардирования:

КомпонентСтоимость
Инфраструктура (4 шарда)4× db.r5.xlarge = $1,460/мес
Router/Coordinator$100/мес
Разработка3 месяца × $15k/мес = $45k one-time
Миграция данных2 недели × $8k = $16k one-time
DevOps overhead+20% времени = $1,600/мес
Total setup$61k
Total monthly$3,160/мес

Альтернатива: Вертикальное масштабирование

  • db.r5.8xlarge: $2,920/месяц
  • Setup: $0
  • DevOps: 0%

Break-even analysis:

Monthly cost difference: $3,160 - $2,920 = $240/мес
 
Setup cost: $61,000
Break-even: $61,000 / $240 = 254 месяца (21 год!)

Вывод: Шардирование НЕ окупается экономически. Делайте его только когда:

  • Вертикальное масштабирование достигло лимита (biggest instance)
  • Compliance требует data isolation
  • Географическое распределение обязательно

7.3 Когда шардирование действительно нужно

Признаки:

  1. Biggest instance не справляется

    • db.r5.24xlarge (96 vCPU, 768GB) = $17,520/мес
    • CPU > 80%, дальше масштабировать некуда
  2. Data size > 10TB

    • Storage costs: 10TB × $0.115/GB = $1,150/мес
    • Backup time > 4 hours
    • VACUUM time > 6 hours
  3. Compliance/Legal

    • GDPR: данные EU пользователей в EU
    • 152-ФЗ: данные РФ пользователей в РФ
    • Multi-tenancy: изоляция данных клиентов
  4. Geo-distribution

    • Users в US, EU, Asia
    • Latency requirements < 50ms

В этих случаях шардирование оправдано, несмотря на высокую стоимость.


7.4 Стоимость multi-region БД

Сценарий: Users в 3 регионах (US, EU, Asia)

Вариант A: Одна БД в US + read replicas

  • Primary US: db.r5.2xlarge ($730/мес)
  • Replica EU: db.r5.2xlarge ($730/мес)
  • Replica Asia: db.r5.2xlarge ($730/мес)
  • Cross-region data transfer: 500GB × $0.09/GB = $45/мес
  • Total: $2,235/месяц

Проблемы:

  • Write latency в EU/Asia: 150-300ms
  • Eventual consistency

Вариант B: Multi-region primary (Aurora Global)

  • Primary US: db.r5.2xlarge ($1,460/мес — Aurora premium)
  • Secondary EU: db.r5.2xlarge ($1,460/мес)
  • Secondary Asia: db.r5.2xlarge ($1,460/мес)
  • Cross-region replication: $0 (included)
  • Total: $4,380/месяц

Плюсы:

  • ✅ Write latency EU/Asia: <1 second (failover)
  • ✅ Disaster recovery

Минусы:

  • 🔴 Почти в 2 раза дороже

Вариант C: Шардирование по регионам

  • US shard: db.r5.xlarge ($365/мес)
  • EU shard: db.r5.xlarge ($365/мес)
  • Asia shard: db.r5.xlarge ($365/мес)
  • Router: $100/мес
  • Total infrastructure: $1,195/месяц
  • DevOps: $61k setup + $1,600/мес

TCO (3 года):

РешениеSetupMonthly3-year total
Read replicas$0$2,235$80,460
Aurora Global$0$4,380$157,680
Sharding$61k$2,795$161,620

Вывод: Read replicas выигрывают экономически. Sharding — только если нужна data isolation по регионам.


7.5 Реальный кейс: избежали шардирования, сэкономили $200k

Компания: SaaS B2B, 10TB данных, 1000 RPS

Было:

  • БД: db.r5.8xlarge ($2,920/мес)
  • CPU: 90% peak
  • Plan: шардирование (оценка $250k setup + $5k/мес)

Проблемы:

  • ❌ Не пробовали оптимизацию
  • ❌ Нет партиционирования
  • ❌ Нет архивирования старых данных

Что сделали (3 месяца, $45k работы):

  1. Table partitioning (по датам)

    • Разбили 3 biggest tables на партиции (по месяцам)
    • Query performance: +300%
    • VACUUM time: 6 часов → 30 минут
  2. Архивирование холодных данных

    • Данные >2 лет → S3 Glacier
    • Active data: 10TB → 3TB
    • Storage cost: $1,150/мес → $345/мес
  3. Кэширование + read replica

    • Redis для горячих данных
    • Read replica для аналитики
    • DB CPU: 90% → 40%

Результат:

  • БД: downgrade до db.r5.4xlarge ($1,460/мес)
  • Redis: $276/мес
  • Read replica: $1,460/мес
  • Storage (3TB): $345/мес
  • Total: $3,541/месяц

Сравнение с шардированием:

  • Шардирование: $250k setup + $5k/мес = $430k за 3 года
  • Оптимизация: $45k setup + $3.5k/мес = $171k за 3 года
  • Экономия: $259k за 3 года

7.6 Cost optimization checklist для масштабирования БД

  • Оптимизация перед репликацией: Индексы могут избавить от replica
  • Кэш перед replica: Часто дешевле и эффективнее
  • Партиционирование: Может отложить шардирование на годы
  • Архивирование: Холодные данные в S3 ($0.023/GB vs $0.115/GB)
  • Read replica только если: Eventual consistency приемлема
  • Шардирование только если: Вертикаль исчерпана или compliance требует
  • Multi-region: Считайте cross-region transfer costs
  • Aurora vs RDS: Aurora часто дешевле для multi-AZ
  • Reserved Instances: 40-60% экономии для stable БД

7.7 Формула: replica, кэш или шардирование?

def choose_scaling_strategy(
    db_cost_current,
    read_percentage,
    cache_hit_ratio_expected,
    data_size_tb,
    max_instance_cost=17520  # db.r5.24xlarge
):
    """
    Выбирает стратегию масштабирования БД
    """
    # Стоимость вариантов
    cache_cost = 276  # Redis базовый
    replica_cost = db_cost_current  # Реплика = primary instance
    shard_setup = 61000  # One-time
    shard_monthly = db_cost_current * 2 + 1600  # 2 шарда + DevOps
 
    # Эффективность кэша
    cache_efficiency = read_percentage * cache_hit_ratio_expected
    if cache_efficiency > 0.7:  # Кэш снимет >70% нагрузки
        return "CACHE", f"Кэш снимет {cache_efficiency*100:.0f}% нагрузки за ${cache_cost}/мес"
 
    # Replica если eventual consistency OK
    if read_percentage > 0.7:
        return "READ_REPLICA", f"Read replica за ${replica_cost}/мес"
 
    # Вертикаль до biggest instance
    if db_cost_current < max_instance_cost:
        vertical_cost = db_cost_current * 2
        return "SCALE_UP", f"Апгрейд до ${vertical_cost}/мес"
 
    # Шардирование как last resort
    if data_size_tb > 10 or db_cost_current >= max_instance_cost:
        break_even = shard_setup / (shard_monthly - db_cost_current) if shard_monthly > db_cost_current else float('inf')
        return "SHARD", f"Шардирование (окупится за {break_even:.0f} мес, если вообще)"
 
    return "OPTIMIZE", "Сначала оптимизируйте запросы"
 
# Пример
choose_scaling_strategy(
    db_cost_current=730,  # db.r5.2xlarge
    read_percentage=0.80,
    cache_hit_ratio_expected=0.85,
    data_size_tb=2
)
# Output: ("CACHE", "Кэш снимет 68% нагрузки за $276/мес")

8. Практика (никаких отговорок)

  1. Lag alarm:
    • Настройте метрику replication_lag и алерт > 500 мс.
    • Смоделируйте lag: искусственно нагружайте master, смотрите, как API переключается на master.
  2. Read-after-write policy:
    • Реализуйте логику «если пользователь обновил профиль → читаем с master 5 секунд».
    • Покройте тестами.
  3. Shard router:
    • Создайте таблицу mapping user_id → shard_id.
    • Напишите консистентный клиент (idempotent), который при ошибке логирует попытку обращения к несуществующему shard.
  4. Cross-shard query drill:
    • Реализуйте scatter-gather и измерьте latency в условиях, когда один shard медленный.
    • Покажите, как вы деградируете: например, отдаёте частичный результат с предупреждением.
  5. Saga rehearsal:
    • Имплементируйте Saga для перевода между shard'ами и force error на шаге 2.
    • Докажите, что компесация сработала, баланс вернулся.

Безопасность: экспериментируйте только на staging. Lag-инъекции, миграция шардов, Sagas — всё должно быть отработано в песочнице. Обязательно включайте аудит: все операции должны быть логированы, иначе не поймёте, где потеряли данные.

Что дальше

Следующая глава — очереди и асинхронная обработка: будем говорить о backpressure, DLQ, гарантии доставки. Без контроля над БД масштабирование не имеет смысла, но и БД нужно разгружать. Готовьтесь к очередному внезапному проверочному списку.

Масштабирование БД: репликация, шардирование и борьба с lag — Архитектура высоконагруженных веб-приложений — Potapov.me