Профили нагрузки: read-heavy и write-heavy

Предпосылки: CAP-теорема (CP/AP, partition tolerance), модели консистентности (eventual, read-your-writes), разрешение конфликтов (CRDT), репликация (leader-based, replication lag), шардинг (shard key, resharding), базовое понимание B-tree индексов (ускоряют чтение, замедляют запись).

Распределённый консенсус | Load Balancing

Два сервиса интернет-магазина обрабатывают по 50 000 операций в секунду. Каталог товаров отвечает на поисковые запросы покупателей, сервис аналитики записывает каждый клик, просмотр и скролл. Оба упираются в потолок производительности одного сервера — но решения, которые спасают каталог, бесполезны для аналитики и наоборот. Разница не в масштабе, а в характере нагрузки: соотношении операций чтения к операциям записи.

Два полюса нагрузки

Каталог — типичная read-heavy система: товары обновляются десятки раз в день, а просматриваются миллионы раз. URL shortener, Wikipedia, лента Twitter/X, CDN — всё это примеры read-heavy: данные создаются однажды, а потом читаются на порядки чаще.

Сервис аналитики — write-heavy: сотни тысяч событий в секунду записываются, а агрегированные отчёты запрашиваются раз в минуту. Системы логирования, IoT-телеметрия, аналитические пайплайны — везде записей больше, чем чтений.

Между полюсами существуют balanced-системы (чат, collaborative editing), но именно полярные профили создают архитектурное давление: оптимизация одного типа операций ухудшает другой, и компромиссы приходится выбирать осознанно.

Индексы: первый компромисс

Каталог должен быстро искать товары по названию, категории, цене, рейтингу. Каждый критерий поиска — отдельный B-tree индекс, ускоряющий поиск с O(N) до O(log N). Пять индексов на таблицу — оправданная цена, когда чтений на порядки больше, чем записей.

Но когда мерчанты загружают обновления каталога, каждый INSERT — это шесть записей на диск: сама строка плюс обновление каждого из пяти индексов. В read-heavy системе это незаметно: обновления редки, а ускорение миллионов чтений стоит замедления десятков записей.

Для аналитики картина обратная. Сотни тысяч записей в секунду — и каждый индекс прямой удар по пропускной способности. Системы вроде ClickHouse и TimescaleDB минимизируют количество индексов и используют другие стратегии ускорения чтения: сортировку данных при записи, партиционирование по времени, колоночное хранение.

Storage engine: B-tree vs LSM-tree

Минимизировать индексы — полумера. Более фундаментальный выбор — сам storage engine, определяющий, как данные физически попадают на диск.

B-tree (PostgreSQL, MySQL InnoDB) оптимизирован для чтения: данные отсортированы, поиск за O(log N), чтение диапазонов — последовательное. Цена — запись требует random I/O: каждый INSERT ищет нужную страницу на диске, читает, модифицирует и записывает обратно. O (произвольный доступ) — обращение к случайным местам на диске, в отличие от O (последовательного чтения/записи подряд). Для HDD random I/O требует физического перемещения головки, что на порядки медленнее последовательного доступа. Для каталога товаров B-tree — подходящий trade-off: чтений на порядки больше, чем записей.

LSM-tree (Cassandra, RocksDB, LevelDB, HBase) оптимизирован для записи: данные сначала копятся в памяти, затем сбрасываются на диск последовательно (sequential I/O). Sequential I/O на HDD быстрее random примерно в 100 раз; на SSD разница меньше и сильно зависит от размера блока, глубины очереди и типа операции, но sequential всё равно быстрее. Для аналитического пайплайна с сотнями тысяч записей в секунду это критичное преимущество. Цена — чтение медленнее: данные разбросаны по нескольким файлам на диске, и для поиска одной строки может потребоваться проверить несколько из них. Вторая цена — write amplification: данные перезаписываются многократно при фоновом уплотнении (compaction); для leveled compaction (RocksDB, LevelDB) типичные значения 10–30x, для tiered/universal — существенно ниже.

B-treeLSM-tree
ЗаписьRandom I/O (медленнее)Sequential I/O (быстрее)
ЧтениеO(log N), одно обращениеПоиск по нескольким файлам
ЦенаМедленная записьWrite amplification, медленное чтение
СистемыPostgreSQL, MySQLCassandra, RocksDB, HBase

Масштабирование: read replicas vs sharding

Storage engine оптимизирует один узел, но один узел имеет предел. Каталог и аналитика масштабируются за этот предел по-разному.

Каталог масштабирует чтение через read replicas: один primary принимает записи, несколько реплик обслуживают чтения. Добавление реплик линейно увеличивает пропускную способность чтения, при этом запись не замедляется (за исключением стоимости репликации).

Read-heavy: масштабирование чтения

    ┌─────────┐
    │ Primary │ ← все записи
    └────┬────┘
         │ репликация
    ┌────┴─────┬──────────┐
    v          v          v
┌────────┐ ┌────────┐ ┌────────┐
│Replica │ │Replica │ │Replica │ ← чтения распределены
└────────┘ └────────┘ └────────┘

Для аналитики read replicas не помогают: все записи всё равно идут на один primary. Масштабирование записи требует шардинга — разделения данных между несколькими независимыми узлами, каждый из которых принимает записи для своего подмножества данных.

Write-heavy: масштабирование записи

    ┌────────┐  ┌────────┐  ┌────────┐
    │Shard 1 │  │Shard 2 │  │Shard 3 │
    │ A–H    │  │ I–P    │  │ Q–Z    │
    └────────┘  └────────┘  └────────┘
         ↑           ↑           ↑
    записи для   записи для  записи для
    ключей A–H   ключей I–P  ключей Q–Z

Шардинг сложнее: появляются вопросы выбора ключа шардирования, перебалансировки при добавлении узлов, cross-shard запросов. Read replicas проще в эксплуатации, но решают только проблему чтения.

Кэширование: эффективность зависит от профиля

Реплики распределяют нагрузку между узлами, но каждый запрос всё равно обращается к диску. Кэш (Redis, Memcached, CDN) хранит результат в памяти, чтобы не повторять обращение к диску при следующем запросе.

Каталог — идеальный кандидат для кэширования. Карточка товара запрашивается тысячи раз между обновлениями. Кэш с TTL 60 секунд при 1000 запросах в секунду означает: одно обращение к БД, 59 999 ответов из кэша. Cache hit rate приближается к 100%, нагрузка на БД падает на порядки.

В аналитике данные меняются непрерывно. Закэшированное значение устаревает до того, как его кто-то прочитает. Каждая запись инвалидирует кэш, а чтения происходят редко — кэш не успевает окупить себя.

Read-heavy (каталог, 1 обновление : 10 000 чтений):
    Запись товара → кэш, TTL 24h
    10 000 чтений → все из кэша
    Hit rate ≈ 99.99%

Write-heavy (аналитика, 1000 записей/сек):
    Каждая запись инвалидирует кэш
    Чтение: кэш почти всегда пустой
    Hit rate ≈ 0%

Консистентность: разные профили — разные риски

Кэширование и репликация вводят задержку между записью и видимостью данных, но характер рисков зависит от профиля.

Каталог с репликами сталкивается с replication lag: мерчант обновил цену, но покупатель на реплике видит старую. Для большинства read-heavy сценариев eventual consistency допустима — покупатель увидит новую цену через секунду. Проблема «не вижу свою правку» решается через read-your-writes: мерчант читает с primary сразу после обновления, остальные покупатели — с реплик.

Аналитика с шардингом сталкивается с другим вызовом: глобальные ограничения (уникальность, лимиты) нельзя проверить в пределах одного шарда, а cross-shard транзакции дороги. Выбор между CP и AP из CAP-теоремы здесь проявляется острее: CP жертвует доступностью ради согласованности, AP допускает расхождения с последующим слиянием через CRDT или другие стратегии.

Буферизация и батчинг: приём для write-heavy

Когда даже шардированная запись не справляется с пиками, записи накапливают и обрабатывают пачками.

Вместо 1000 INSERT в секунду — один bulk INSERT из 1000 строк раз в секунду. Это эффективнее: меньше round-trip до базы, меньше обновлений индексов, меньше вызовов fsync (принудительный сброс данных из буфера ОС на диск). Цена — данные доступны не мгновенно, а с задержкой буферизации.

Аналитический пайплайн использует очередь сообщений (Kafka, RabbitMQ) как буфер между источником событий и хранилищем. Продюсеры пишут в очередь с максимальной скоростью, консьюмеры читают и пишут в БД в контролируемом темпе. Во время пика — вместо 100 000 записей в секунду база получает стабильные 10 000, остальные ждут в очереди.

Без буферизации:
    Клиенты ──(100K/сек)──> БД  → перегрузка

С буфером (очередь):
    Клиенты ──(100K/сек)──> Kafka ──(10K/сек)──> БД  → стабильная нагрузка
                               │
                          данные ждут в очереди
                          (задержка секунды-минуты)

Сводка архитектурных решений

РешениеRead-heavyWrite-heavy
ИндексыБольше индексов — быстрее поискМинимум индексов — быстрее запись
Storage engineB-tree (PostgreSQL, MySQL)LSM-tree (Cassandra, RocksDB)
МасштабированиеRead replicasSharding
КэшированиеВысокий hit rate, большой эффектНизкий hit rate, буферизация записей
КонсистентностьEventual / read-your-writes достаточноКонфликты записей — CP или CRDT
БуферизацияРедко нужнаБатчинг, очереди — стандартный приём

Как определить профиль

На этапе проектирования профиль оценивается из бизнес-логики: сколько пользователей читают на одного пишущего? Как часто данные обновляются? Допустима ли задержка между записью и видимостью?

На работающей системе профиль измеряется: соотношение SELECT к INSERT/UPDATE/DELETE в PostgreSQL (pg_stat_statements), GET к SET в Redis (INFO commandstats), read IOPS к write IOPS на уровне диска.

Профиль может меняться со временем и отличаться для разных частей одной системы — как в примере с каталогом и аналитикой. Лента соцсети — read-heavy (посты читаются чаще, чем создаются), но подсистема уведомлений — write-heavy (каждое действие порождает десятки записей). Каталог товаров — read-heavy днём (покупатели просматривают), но write-heavy ночью (импорт обновлений от поставщиков).

Sources

  • Kleppmann, 2017, Designing Data-Intensive Applications, Chapter 3: Storage and Retrieval — B-tree vs LSM-tree, write amplification
  • O’Neil et al., 1996, The Log-Structured Merge-Tree (LSM-Tree) — оригинальная статья LSM-tree
  • Fitzpatrick, 2004, Distributed Caching with Memcached — паттерны кэширования для read-heavy нагрузок

Распределённый консенсус | Load Balancing