«Положим в память — станет быстрее» звучит как аксиома, но за ней прячется целый набор компромиссов. Оперативная память действительно на порядки быстрее диска и сети, но она дороже, ограничена по объёму и по умолчанию энергозависима. Поэтому in-memory computing — это не «включить турбо», а осознанный выбор: что именно держать в памяти, как переживать перезапуск, чем платить за консистентность и когда вычисления стоит переносить прямо к данным.
Это первая, обзорная статья серии «Вычисления в оперативной памяти». Дальше мы разберём шесть разнородных систем — Redis, Tarantool, Picodata, Aerospike, Apache Ignite, etcd — каждую в её роли. Здесь — система координат и трейдофы.
В статье
- Иерархия памяти: порядки задержек
- Когда RAM помогает, а когда это дорогое заблуждение
- Главные трейдофы
- Перенос вычислений к данным
- Таксономия: шесть систем по роли
- Что дальше
- Источники
Иерархия памяти: порядки задержек
«Память быстрее диска» — верно, но без порядков величины это лозунг, а не аргумент. Классическая иерархия — регистр → кэш процессора (L1/L2/L3) → оперативная память → SSD → сеть → вращающийся диск — на каждом переходе теряет один-два-три порядка величины по задержке одиночного доступа. Обращение к регистру или L1-кэшу измеряется в наносекундах; обращение к RAM — на пару порядков дороже, но всё ещё в наносекундах; случайное чтение с SSD — уже микросекунды, то есть ещё на два-три порядка дороже RAM; сетевой round-trip внутри одного дата-центра добавляет ещё порядок; а произвольный доступ к вращающемуся диску (позиционирование головки) — это миллисекунды, шесть-семь порядков от регистра. Отсюда и вся идея in-memory computing: убрать самый дорогой переход (диск, сеть до другого хранилища) из горячего пути, оставив только RAM.
Числа в этом разделе — не измерение стенда серии, а общеизвестная инженерная раскладка задержек, восходящая к таблице Джеффа Дина (Google) и растиражированная как «Latency Numbers Every Programmer Should Know»; актуализированная интерактивная версия с поправкой на разные поколения железа — у Колина Скотта. Абсолютные значения там меняются от железа к железу и от года к году (NVMe современного класса кардинально быстрее SSD десятилетней давности), но порядки соотношений между уровнями иерархии — регистр/кэш ≪ RAM ≪ SSD ≪ сеть ≪ диск — остаются устойчивыми уже больше десяти лет.
Честная оговорка про этот стенд серии: он не измеряет абсолютную latency и не будет. Вся серия работает под Docker Desktop на Windows (виртуализованный сетевой и дисковый путь через WSL2/Hyper-V) — это оценочно на два порядка дороже нативного Linux TCP-loopback, и time.Since() вокруг быстрых операций на этой платформе в 34,9–46,4% замеров возвращал ровно ноль. Абсолютные цифры оттуда были бы не про Redis, Tarantool или Aerospike, а про накладные расходы виртуализации конкретной машины — поэтому они нигде в серии не публикуются как факт о производительности системы. Там, где сравнение всё же нужно, серия считает относительные величины внутри одного прогона на одной машине (система А против системы Б, до/после) — таким получится честное сравнение в шестой статьеготовится, с 26 сентября, даже без единого измерения в микросекундах.
Когда RAM помогает, а когда это дорогое заблуждение
Ускорение от памяти не универсально — оно зависит от формы нагрузки, а не только от факта «данные в RAM».
Работает хорошо, когда:
- рабочий набор помещается в память целиком или почти целиком — не весь датасет обязан лежать в RAM, важно, чтобы туда помещалось то, что реально читается;
- доступ сильно неравномерен (hot data vs cold data) — типичный прикладной трафик далёк от равномерного. На датасете серии (200 000 товаров, 100 000 просмотров, зипфово распределённых) топ-1 товар собрал 19,5% всех просмотров, топ-10 — 47,8%, топ-100 — 70,0%, а хотя бы один просмотр вообще получили только 12 367 из 200 000 товаров. То есть у 93,8% каталога (187 633 позиции) за весь прогон не было ни одного обращения. На этом датасете держать в RAM пришлось бы не 200 000 товаров, а порядка тысяч горячих — экономика совершенно другая. Важная оговорка: распределение здесь синтетическое, сгенерированное
rand.NewZipf, поэтому «несколько тысяч» — иллюстрация выбранного распределения, а не рекомендация для произвольного приложения. Реальный размер рабочего набора задаётся окном наблюдения, TTL, скоростью обновления каталога и требуемым hit rate — по одному синтетическому Zipf его не выведешь; - путь read-heavy и латентность на нём критична — чтение из RAM не ждёт диска и сети до внешнего хранилища;
- вычисление можно исполнить рядом с данными, а не тащить их в приложение построчно — к этой идее серия вернётся отдельно ниже.
Не работает или работает не так, как ожидалось, когда:
- доступ равномерный, без выраженного hot set — кэш перед таким источником не столько ускоряет, сколько добавляет второй источник правды без реальной экономии обращений к первому;
- рабочий набор растёт быстрее, чем RAM-бюджет — то, что вчера помещалось целиком, завтра не помещается, и начинается вытеснение. Насколько по-разному на это реагируют разные системы — предмет отдельного нагрузочного эксперимента в шестой статьеготовится, с 26 сентября, здесь не даётся забежать вперёд с числами;
- durability важнее скорости — если потеря данных при рестарте недопустима, «просто RAM» не решение без persistence-слоя, а он снова возвращает часть стоимости диска, которую RAM должен был убрать;
- нужна строгая консистентность между узлами, а не быстрый локальный ответ — здесь RAM экономит на задержке чтения, но не на задержке согласования: кворум и раунд-трип до большинства узлов никуда не деваются независимо от того, где физически лежат данные. Координация (тема пятой статьи) доводит это до предела — у etcd согласованность обеспечивает Raft-протокол, а не резидентность данных в памяти: физические значения там как раз хранятся на диске, в памяти держится только вспомогательный индекс.
Главные трейдофы
Durability: что переживает рестарт
RAM по умолчанию энергозависима — без отдельного механизма persistence весь набор данных исчезает при перезапуске процесса или падении узла. Спектр решений в системах серии широкий: от снапшотов и append-only журнала (RDB/AOF у Redis, ст. 2) до WAL и периодического снапшота у Tarantool (ст. 3) и нативной persistence у Ignite. Общий принцип один: durability не бесплатна — это либо запись на диск в критическом пути (WAL перед подтверждением записи), либо риск потерять последние операции между снапшотами. Каждая система решает этот баланс по-своему, и это тема отдельных статей дальше; здесь важно зафиксировать сам компромисс как ось координат.
Стоимость RAM: одни и те же 200 000 записей, разная цена
Это самый конкретный из трейдофов — его можно показать в байтах на одном и том же датасете (200 000 записей products, на диске в PostgreSQL — 34 МБ), не дожидаясь отдельных статей про Tarantool и Aerospike.
Стенд tarantool измеряет резидентную память живым box.slab.info(): те же 200 000 кортежей в движке memtx (полностью в RAM) занимают 15 861 160 байт (15,13 МБ) — стабильно на 22 прогонах подряд. Тот же набор через движок vinyl (LSM, данные на диске) после стабилизации держит в резидентной памяти только 288 359 байт (0,28 МБ) — это page_index и bloom_filter, а не сами данные. Разница — 55,0× (15 861 160 / 288 359). Цена такой экономии RAM — 15,63 МБ на диске постоянно (у memtx между чекпоинтами на диске в норме 0 байт) и запись через LSM вместо прямого доступа к резидентной структуре.
Стенд aerospike показывает ту же развилку под другим углом. Первичный индекс Aerospike стоит фиксированные 64,00 байта на запись в RAM — стенд подтвердил саму величину и линейность по числу записей (10 000/50 000/100 000/200 000, идентично на 12 прогонах), а независимость от размера данных в записи взята из документации Aerospike: размер бинов на стенде не варьировался. И величина, и её независимость от бинов задокументированы вендором, стенд их не открывал. На 200 000 записей это уже 12 800 000 байт (12,8 МБ) только под индекс. Дальше — развилка namespace: если данные тоже держать в RAM (ram), общий расход памяти — 41 598 000 байт; если данные вынести на flash, а в RAM оставить только индекс (flash), расход падает до 12 800 000 байт — то есть до 30,8% от варианта «всё в RAM» (экономия 69,2%), причём оба варианта отдают одни и те же данные без расхождений.
Три числа на одном датасете (200 000 записей) дают наглядную шкалу — в байтах, чтобы шкала оставалась одной: 15 861 160 байт RAM (Tarantool memtx, всё резидентно) — 12 800 000 байт RAM + данные на flash (Aerospike, индекс резидентно) — 288 359 байт RAM + 15,63 МБ на диске (Tarantool vinyl, почти всё на диске). Это не рейтинг «кто лучше» — это одна и та же дилемма, решённая тремя разными архитектурными выборами, и правильный выбор зависит от того, сколько стоит гигабайт RAM в вашей инфраструктуре против гигабайта диска/flash и насколько критична предсказуемость латентности при промахе мимо RAM.
Консистентность: от eventual до линеаризуемой
Кэш перед источником правды (Redis как кэш) по конструкции допускает окно рассогласования — это eventual consistency в её самой практической форме, и обращаться с этим окном аккуратно — тема второй статьи. На другом полюсе — etcd, где Raft-консенсус даёт линеаризуемые чтения ценой кворумной записи: любое большинство узлов должно подтвердить операцию, прежде чем она считается совершённой. Между этими полюсами лежат промежуточные модели: Tarantool/Picodata с распределённым консенсусом на уровне кластера, Ignite с настраиваемым уровнем консистентности кэша. Согласованность — это не переключатель «вкл/выкл», а континуум, и место каждой системы серии на нём — часть её роли, а не случайная деталь конфигурации.
Объём и шардирование: что происходит, когда набор перерастает узел
RAM одного узла конечна и обычно кончается раньше, чем данные приложения. Дальше — горизонтальное разделение: vshard-шардирование у Picodata, партиционирование с affinity у Ignite, распределение по namespace/кластеру у Aerospike. У каждого подхода своя цена — от сетевого round-trip между шардами до операционной сложности поддержки кластера вместо одного процесса. Это тоже не абстракция «когда-нибудь»: разница между «3 инстанса» и «4 инстанса» при заданном фактор-репликации может быть разницей между кластером, который реально шардирует данные, и кластером, который формально жив, но половина его мощности простаивает — к этому стоит быть внимательным заранее, а не выяснять постфактум.
Перенос вычислений к данным
Стандартный путь работы с любым хранилищем — вытащить данные к приложению и обработать их там: SELECT строк, агрегация в коде сервиса. Для небольших выборок это не имеет значения. Для агрегаций над тысячами и десятками тысяч строк — имеет: каждая строка, уехавшая по сети, это сериализация, передача, десериализация, и всё это ради того, чтобы в итоге остаться в памяти приложения как один агрегированный результат.
Альтернатива — исполнить логику агрегации не в приложении, а прямо там, где лежат данные, и забрать по сети только результат. Две системы серии реализуют эту идею на противоположных рантаймах: Tarantool встраивает Lua application server прямо в процесс базы данных — хранимая процедура выполняется в том же адресном пространстве, что и сами кортежи (третья статья). Apache Ignite делает похожее на распределённом кластере — задача compute grid отправляется на узел, где физически лежит нужная партиция данных (collocated compute), вместо того чтобы тянуть партицию к вычислению (четвёртая статья).
Идея одна, но цена переноса логики к данным не сводится к «стало быстрее» — у неё есть встречная цена: логика теперь живёт внутри хранилища (язык, отладка, версионирование хранимого кода), а не в привычном стеке приложения. Насколько велик выигрыш и во что он обходится на практике — не риторический вопрос, а конкретный измеримый эксперимент: шестая статья серииготовится, с 26 сентября прогоняет одну и ту же агрегацию наивным путём и путём переноса вычисления к данным на обеих системах одновременно и показывает оба числа — экономию по объёму данных, ушедших по сети, и отдельно экономию по времени, — потому что это, забегая вперёд, не одно и то же число.
Таксономия: шесть систем по роли
Шесть систем серии закрывают разные роли, а не конкурируют за одно и то же место. Ось X — модель хранения (чистая RAM ↔ гибрид RAM/диск), ось Y — что система оптимизирует в первую очередь (throughput/данные ↔ координация/согласованность).
quadrantChart
title Роль x модель хранения
x-axis "Чистая RAM" --> "Гибрид RAM/диск"
y-axis "Throughput / данные" --> "Координация / согласованность"
quadrant-1 "масштаб + координация"
quadrant-2 "координация"
quadrant-3 "кэш и in-memory DB"
quadrant-4 "гибрид / грид"
Redis: [0.12, 0.20]
Tarantool: [0.28, 0.48]
Picodata: [0.38, 0.62]
Aerospike: [0.80, 0.28]
Ignite: [0.65, 0.38]
etcd: [0.48, 0.90]
Оси не режут системы на строгие лагеря. Tarantool и особенно Picodata лежат ощутимо выше Redis по оси координации не случайно — Picodata держит топологию кластера через Raft, то есть у неё есть настоящая координационная составляющая поверх in-memory DB, хотя её основная роль — не координация, а данные и логика рядом с ними. А верхний правый угол («гибрид + координация») пуст не потому, что диаграмма неполная: ни одна из шести систем серии не совмещает дисковый гибрид большого объёма с ролью координатора малых критичных метаданных — это разные экстремумы задачи.
Позиция etcd по оси X здесь честнее читать как «не измеряется этой осью», чем как точку на градиенте «чистая RAM ↔ гибрид RAM/диск»: у остальных пяти систем эта ось буквально означает, сколько байт самих данных резидентно в оперативной памяти, а у etcd резидентен не сам датасет, а только вспомогательный индекс — указатели на ревизии, физически хранящиеся в persistent B+tree (bbolt) на диске. По объёму резидентных данных etcd ближе к «полностью на диске», чем любая гибридная система здесь, — но не потому, что он экономит RAM на масштабе, как Aerospike или Ignite, а потому, что не держит датасет в памяти вовсе, ни в каком объёме. Пятая статьяготовится, с 25 сентября разбирает это подробно — там же честная граница: etcd в этой серии не шестой пример «данные в памяти → скорость», а её исключение.
| Система | Роль | Когда брать | Чем платишь |
|---|---|---|---|
| Redis | Кэш и структуры данных в памяти | Read-heavy путь с явным hot set; нужны готовые атомарные структуры (sorted set, HyperLogLog, streams) поверх простого key-value | Второй источник правды и окно рассогласования с ним; весь резидентный набор стоит RAM; однопоточная модель исполнения команд |
| Tarantool / Picodata | In-memory DB + встроенный app-server | Нужен не кэш, а источник правды с логикой прямо рядом с данными (Lua stored procedures); нужен предсказуемый распределённый SQL и шардирование (Picodata) | Логика на Lua на сервере (язык, отладка, связность); операционная сложность кластера и HA у Picodata |
| Aerospike | Высокопроизводительный гибрид RAM/flash | Датасет растёт быстрее разумного RAM-бюджета, но латентность и предсказуемость на масштабе всё равно критичны | Sizing вокруг фиксированной цены индекса (64 Б/запись) и выбора namespace; архитектура заточена под конкретный профиль доступа |
| Apache Ignite | Data grid + compute grid | Нужен распределённый кэш вместе с вычислением рядом с партицией (collocated compute), распределённый SQL, ACID поверх кластера | Вес JVM/GC на каждом узле; сложность эксплуатации кластера; собственная модель affinity/партиционирования |
| etcd | Координация, не throughput | Конфигурация, service discovery, распределённые блокировки, лидер-элекшн — малые критичные метаданные, а не датасет приложения | Throughput принципиально ограничен кворумной записью; несвойственная нагрузка (большие значения, частая запись) ломает систему быстро и некрасиво |
Что дальше
Дальше серия разбирает каждую роль по отдельности, начиная с самой знакомой:
- Статья 2 — Redis: кэш и структуры данных как ускорительготовится, с 22 сентября: паттерны кэширования, защита от stampede, структуры данных как вынесенное в Redis вычисление.
- Статья 3 — Tarantool и Picodata: данные и вычисления рядомготовится, с 23 сентября: перенос логики к данным на практике, memtx/vinyl, WAL и распределённый Picodata.
- Статья 4 — Aerospike и Apache Ignite: масштаб и гридготовится, с 24 сентября: гибрид RAM/flash и data+compute grid — две разные стратегии для объёма, который не помещается в один узел.
- Статья 5 — etcd: координация ≠ ускорениеготовится, с 25 сентября: намеренный контраст с пятью предыдущими системами — данные etcd физически на диске, в памяти только индекс, а согласованность обеспечивает Raft; и что ломается, если использовать etcd не по назначению.
- Статья 6 — нагрузочный стенд и карта выбораготовится, с 26 сентября: финал — три сценария, которые переживают оверхед платформы: рабочий набор за границей RAM, цена выноса вычислений к данным и etcd под несвойственной нагрузкой; плюс честная карта выбора «задача → система».
Рядом с этой серией стоит держать хаб «Данные: карта хранилищ и подходов»готовится, с 22 сентября — там in-memory место в общей картине хранилищ, а не только внутри собственной серии. Серия про Redis идёт значительно глубже, чем вторая статья здесь успевает: структуры данных и их кодировки, где Redis помогает, а где только усложняет. Раздел про Raft и координацию продолжается в Consensus Landscape — тур по алгоритмам консенсуса, к которому отсылает пятая статья про etcd. А HyperLogLog, который появится как одна из структур Redis во второй статье, — частный случай темы вероятностных структур данных: экономия памяти ценой приблизительности — тот же класс компромисса, что и весь in-memory computing, только в миниатюре.
Источники
- Джонас Бонер, «Latency Numbers Every Programmer Should Know» — общеизвестная таблица порядков задержек по уровням иерархии памяти: gist.github.com/jboner/2841832.
- Колин Скотт, интерактивная визуализация той же таблицы с поправкой на разные поколения железа: colin-scott.github.io/personal_website/research/interactive_latency.html.
- Документация Aerospike по устройству первичного индекса (оверхед на запись) — aerospike.com/docs.
- Документация Tarantool по движкам хранения memtx и vinyl — tarantool.io/en/doc.
- Стенды серии: digital-cookbook/performance/inmemory — общий датасет и стенды по каждой системе, числа этой статьи — из сценариев
tarantool(memtx-vinyl) иaerospike(namespaces,index-cost).
Комментарии