Масштабирование БД: репликация, шардирование и борьба с lag
Для кого: инженеры, которые уже упёрлись в одну БД и понимают, что «добавить 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_id | 12345 |
| shard_host | shard2.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:
- Все шаги должны быть идемпотентными.
- Компенсации — обязательные.
- Логируйте «какой шаг застрял».
Провокация №6: ваша шард-схема переживёт добавление нового шарда?
- Если ответ «мы просто увеличим массив на один элемент» — вы в беде. Нужно мигрировать данные, обновить router, прогреть кэш.
6. Добавление шарда без смерти системы
- Подготовьте routing: добавьте shard в конфиг, но не маршрутизируйте туда трафик.
- Мигрируйте данные: batch миграции (по 10k записей), проверяйте консистентность.
- Грейс-период: пока кэш не прогреется, hedged requests (дублируйте на старый shard).
- Снимите метрики:
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/month | CPU reduction | Complexity | Best for |
|---|---|---|---|---|
| Вертикальное | $1,460 | 50% | Низкая | Временное решение |
| Read replica | $1,460 | 40% | Средняя | Eventual consistency OK |
| Кэш | $1,006 | 70% | Средняя | 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 Когда шардирование действительно нужно
Признаки:
-
Biggest instance не справляется
- db.r5.24xlarge (96 vCPU, 768GB) = $17,520/мес
- CPU > 80%, дальше масштабировать некуда
-
Data size > 10TB
- Storage costs: 10TB × $0.115/GB = $1,150/мес
- Backup time > 4 hours
- VACUUM time > 6 hours
-
Compliance/Legal
- GDPR: данные EU пользователей в EU
- 152-ФЗ: данные РФ пользователей в РФ
- Multi-tenancy: изоляция данных клиентов
-
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 года):
| Решение | Setup | Monthly | 3-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 работы):
-
Table partitioning (по датам)
- Разбили 3 biggest tables на партиции (по месяцам)
- Query performance: +300%
- VACUUM time: 6 часов → 30 минут
-
Архивирование холодных данных
- Данные >2 лет → S3 Glacier
- Active data: 10TB → 3TB
- Storage cost: $1,150/мес → $345/мес
-
Кэширование + 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. Практика (никаких отговорок)
- Lag alarm:
- Настройте метрику
replication_lagи алерт > 500 мс. - Смоделируйте lag: искусственно нагружайте master, смотрите, как API переключается на master.
- Настройте метрику
- Read-after-write policy:
- Реализуйте логику «если пользователь обновил профиль → читаем с master 5 секунд».
- Покройте тестами.
- Shard router:
- Создайте таблицу mapping
user_id → shard_id. - Напишите консистентный клиент (idempotent), который при ошибке логирует попытку обращения к несуществующему shard.
- Создайте таблицу mapping
- Cross-shard query drill:
- Реализуйте scatter-gather и измерьте
latencyв условиях, когда один shard медленный. - Покажите, как вы деградируете: например, отдаёте частичный результат с предупреждением.
- Реализуйте scatter-gather и измерьте
- Saga rehearsal:
- Имплементируйте Saga для перевода между shard'ами и force error на шаге 2.
- Докажите, что компесация сработала, баланс вернулся.
Безопасность: экспериментируйте только на staging. Lag-инъекции, миграция шардов, Sagas — всё должно быть отработано в песочнице. Обязательно включайте аудит: все операции должны быть логированы, иначе не поймёте, где потеряли данные.
Что дальше
Следующая глава — очереди и асинхронная обработка: будем говорить о backpressure, DLQ, гарантии доставки. Без контроля над БД масштабирование не имеет смысла, но и БД нужно разгружать. Готовьтесь к очередному внезапному проверочному списку.