Зачем нужен jemalloc: выделение памяти в масштабе
jemalloc управляет выделением памяти в одних из крупнейших систем мира. Как он снижает фрагментацию и почему Meta делает на него ставку.

Каждый раз, когда ваша программа вызывает malloc(), кто-то должен решить, какой кусок виртуальной памяти вернуть. Для маленькой программы это тривиальная задача, а в масштабе превращается в серьёзную инженерную проблему. Плохой аллокатор фрагментирует память, расходует RAM, создаёт конкуренцию за блокировки между потоками и вызывает скачки задержек, когда ОС забирает страницы. Хороший аллокатор ничего из этого не делает. jemalloc как раз хороший аллокатор, и Meta только что подтвердила приверженность ему, потому что при их масштабе разница между хорошим и посредственным аллокатором измеряется миллиардами долларов на железо.
Большинство разработчиков никогда не думают о выделении памяти. Они вызывают new или malloc и получают указатель. Но в масштабе Meta, с миллиардами запросов в день, миллионами серверов и петабайтами RAM, поведение аллокатора напрямую влияет на эффективность железа, хвостовые задержки и операционные расходы.
Что на самом деле делает аллокатор памяти
Аллокатор памяти находится между вашей программой и операционной системой. ОС выделяет память крупными блоками (страницами, обычно 4 КБ или 2 МБ), а программе нужны небольшие куски переменного размера: строка на 24 байта здесь, буфер на 4096 байт там. Задача аллокатора заключается в том, чтобы нарезать страницы, выданные ОС, на куски нужного размера и переиспользовать освобождённые для будущих выделений.
Наивный подход, при котором на каждое выделение запрашивается новая страница у ОС, а при освобождении она возвращается, катастрофически медленный. У системных вызовов есть накладные расходы, страницы намного больше большинства выделений, и вы потратили бы 4 КБ памяти на строку в 24 байта.
Настоящие аллокаторы держат пул памяти и выделяют из него мелкие куски. Задачи здесь такие: минимизировать фрагментацию (промежутки между выделениями, которые слишком малы, чтобы их использовать), минимизировать конкуренцию за блокировки (когда несколько потоков выделяют память одновременно) и минимизировать накладные расходы (метаданные на каждое выделение должны быть малы по сравнению с самим выделением).
Как работает jemalloc
jemalloc (его создал Джейсон Эванс, отсюда «je») изначально разрабатывался для FreeBSD, а позже был принят Meta (тогда Facebook) как аллокатор по умолчанию для C- и C++-сервисов. Его архитектура решает три основные задачи с помощью конкретных техник.
Потоковые кэши устраняют конкуренцию. Каждый поток получает собственный кэш мелких выделений. Когда поток вызывает malloc() для небольшого объекта, выделение полностью обслуживается из локального кэша потока: без блокировок, без атомарных операций и без конкуренции. Только когда потоковый кэш исчерпан, поток обращается к общей арене за пополнением.
jemalloc allocation flow:
Thread calls malloc(64)
↓
Check thread cache for 64-byte size class
→ Hit: return cached chunk (no lock, no contention)
→ Miss: refill from arena
↓
Arena (shared, but per-thread affinity)
→ Find a partially-full slab for 64-byte objects
→ Carve out a chunk
→ Return to thread cache
↓
Return chunk to caller
Thread caches handle 95%+ of allocations without any locking.
Классы размеров снижают фрагментацию. Вместо того чтобы выделять ровно запрошенное число байт, jemalloc округляет до ближайшего класса размера. Классы подобраны аккуратно: 8, 16, 32, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256 и так далее, с шагом, который растёт вместе с размером. Это значит, что выделение в 50 байт получает блок в 64 байта (потери 23%). Звучит плохо, но на самом деле это хорошо: все блоки по 64 байта взаимозаменяемы, поэтому внутри одного класса размера внешней фрагментации нет.
Слэбы группируют выделения одного размера. Каждый слэб представляет собой непрерывную область памяти, поделённую на блоки одного класса размера. Слэб для объектов по 64 байта содержит только блоки по 64 байта. Это устраняет самый неприятный вид фрагментации, когда освобождённую память нельзя переиспользовать, потому что она зажата между активными выделениями разных размеров.
Проблема фрагментации
Фрагментация памяти тихо убивает долгоживущие сервисы. Свежезапущенный сервер использует память эффективно: выделения упакованы плотно. После дней или недель смешанных выделений и освобождений куча становится похожей на швейцарский сыр, с множеством мелких свободных промежутков между активными выделениями. Общий свободный объём может составлять 2 ГБ, но самый большой непрерывный свободный участок при этом занимает 64 КБ.
И внешняя фрагментация (непригодные промежутки между выделениями), и внутренняя (потери внутри выделений из-за округления) важны, но внешняя хуже. Внутренняя ограничена шагом классов размеров: в худшем случае вы теряете около 25% на каждом выделении. Внешняя фрагментация ничем не ограничена и со временем только растёт.
Слэбная архитектура jemalloc в значительной мере устраняет внешнюю фрагментацию для мелких выделений, которых подавляющее большинство. Поскольку все объекты в слэбе одного размера, освобождение одного из них создаёт дыру ровно нужного размера для следующего выделения того же класса. Непригодных промежутков не возникает.
Для больших выделений (обычно более 14 КБ) jemalloc использует другую стратегию: они получают собственные страницы, а освобождённые страницы можно вернуть ОС или переиспользовать для других классов размеров. Здесь фрагментация по-прежнему может возникать, но большие выделения встречаются относительно редко.
jemalloc против glibc malloc против tcmalloc
Три основных аллокатора в экосистеме Linux идут на разные компромиссы.
- glibc malloc (ptmalloc2) — стандартный выбор в большинстве дистрибутивов Linux. Для масштабирования по потокам он использует арены, но классов размеров у него меньше, чем у jemalloc, поэтому в долгоработающих сервисах фрагментации больше. Его главное преимущество в том, что он используется по умолчанию: никакой настройки не нужно.
- tcmalloc (Google) — malloc с потоковым кэшированием, изначально разработанный для C++-сервисов Google. У него отличный потоковый локальный кэш и низкие накладные расходы. Он хорошо подходит для нагрузок с большим количеством короткоживущих выделений и хуже для нагрузок с высоким давлением на память, где фрагментация важна.
- jemalloc оптимизирован под низкую фрагментацию и предсказуемое поведение при постоянной нагрузке. Он использует более продуманные классы размеров и управление слэбами, чем остальные. Цена: чуть большие накладные расходы на выделение (больше метаданных) в обмен на лучшую эффективность использования памяти со временем.
Выбор зависит от нагрузки. Для короткоживущих процессов все три работают примерно одинаково. Для долгоживущих серверов со смешанными паттернами выделения (веб-серверы, базы данных, кэши) устойчивость jemalloc к фрагментации обычно побеждает: при той же нагрузке вы используете на 10–30% меньше RAM, чем с glibc malloc, а в масштабе это напрямую превращается в экономию на железе.
Почему Meta это важно
В масштабе Meta снижение потребления памяти на 10% в их C++-сервисах экономит миллионы долларов на железе. Не в год, а в месяц. Когда вы эксплуатируете миллионы серверов, на каждом из которых сервисы выделяют и освобождают память миллиарды раз в день, эффективность аллокатора становится статьёй бюджета.
Новые инвестиции Meta в jemalloc сосредоточены на нескольких направлениях: лучшая поддержка huge pages (меньше промахов TLB на системах с большим объёмом памяти), улучшенный возврат памяти ОС (уменьшение резидентной памяти при падении нагрузки) и более качественные инструменты профилирования (понимание того, где память используется и где она тратится впустую).
Профилирование особенно интересно. В jemalloc встроено профилирование кучи: можно попросить его сэмплировать выделения и получить профиль, показывающий, где память выделялась, сколько её активно, сколько освобождено и насколько фрагментирована куча. Такое профилирование почти не добавляет накладных расходов в продакшене, что делает возможной его постоянную работу на боевых серверах.
Использование jemalloc в ваших проектах
Переход на jemalloc в C- и C++-программах обычно тривиален. В Linux можно либо подключить его напрямую при линковке, либо использовать LD_PRELOAD, чтобы подставить его во время выполнения, без изменений в коде.
# Install jemalloc
sudo apt install libjemalloc-dev # Debian/Ubuntu
brew install jemalloc # macOS
# Use LD_PRELOAD — works with any program, no recompilation
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so ./my_server
# Or link at compile time
gcc -o my_server my_server.c -ljemalloc
# Enable profiling
export MALLOC_CONF="prof:true,prof_prefix:jeprof"
./my_server
# Then analyze: jeprof --svg ./my_server jeprof.*.heap > heap.svg
Несколько известных проектов используют jemalloc по умолчанию: Redis, Rust (на некоторых платформах jemalloc является аллокатором по умолчанию), Firefox (jemalloc изначально разрабатывался для FreeBSD, на которую ориентировался Firefox), а также многие игровые движки.
Для языков с управляемой памятью (Python, Java, Go) выделением занимается рантайм, и jemalloc напрямую здесь не применим. Но идеи (потоковое кэширование, классы размеров, слэбное выделение) есть в каждом современном рантайме и сборщике мусора. Например, аллокатор рантайма Go использует дизайн, вдохновлённый tcmalloc, с кэшами на каждый P (процессор) и классами размеров.
Невидимая инфраструктура
Выделение памяти — это инфраструктура, которая незаметна, когда работает, и катастрофична, когда нет. Сервер, который постепенно теряет память из-за фрагментации, в конце концов получит OOM-kill, и причина не появится ни в одном логе приложения, потому что она находится ниже уровня приложения.
Для большинства приложений аллокатор по умолчанию подходит. Но если вы запускаете долгоживущие серверы, сталкиваетесь с необъяснимым ростом памяти или работаете в масштабе, где важна эффективность железа, понимание своего аллокатора и, возможно, переход на более подходящий становятся одним из самых выгодных изменений в инфраструктуре. jemalloc не магия. Это инженерия: продуманный дизайн структур данных, осознанные компромиссы и внимание к деталям, которые важны в масштабе.


