Профили нагрузки: 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-tree | LSM-tree | |
|---|---|---|
| Запись | Random I/O (медленнее) | Sequential I/O (быстрее) |
| Чтение | O(log N), одно обращение | Поиск по нескольким файлам |
| Цена | Медленная запись | Write amplification, медленное чтение |
| Системы | PostgreSQL, MySQL | Cassandra, 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-heavy | Write-heavy |
|---|---|---|
| Индексы | Больше индексов — быстрее поиск | Минимум индексов — быстрее запись |
| Storage engine | B-tree (PostgreSQL, MySQL) | LSM-tree (Cassandra, RocksDB) |
| Масштабирование | Read replicas | Sharding |
| Кэширование | Высокий 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 нагрузок