Что такое huge pages и почему база данных без них теряет проценты на пустом месте
Вы выделили СУБД 32 или 64 ГБ под буферный кеш, сервер вроде бы не упирается ни в диск, ни в сеть, а профайлер всё равно показывает, что заметная доля процессорного времени уходит куда-то мимо самого запроса. Один из подозреваемых, о котором вспоминают не сразу — это трансляция адресов памяти. Стандартные страницы по 4 КБ, которыми Linux управляет памятью по умолчанию, на больших объёмах превращаются в узкое место само по себе, и huge pages — это именно тот инструмент, который убирает эту накладную стоимость.
Содержание
- Как процессор находит байт в физической памяти
- Почему маленькие страницы — это много записей
- TLB и что происходит при промахе
- Как huge pages решают именно эту проблему
- Почему СУБД особенно чувствительна к этому
- Как включить huge pages на сервере под базу данных
- Подводные камни и когда не включать не глядя
Как процессор находит байт в физической памяти
Программа работает с виртуальными адресами — это иллюзия непрерывного адресного пространства, которую создаёт ядро для каждого процесса. Реальные данные лежат в физической памяти по совершенно другим адресам, и перед каждым обращением к памяти процессор обязан перевести виртуальный адрес в физический. Делается это не побайтово, а постранично: вся память нарезана на страницы фиксированного размера, и для каждой виртуальной страницы процесса существует запись в таблице страниц (page table), которая говорит, какой физической странице она соответствует.
На x86-64 таблица страниц — это не плоский массив, а дерево из нескольких уровней (обычно четыре, иногда пять на новых процессорах с расширенной адресацией). Чтобы перевести один адрес, процессору в худшем случае нужно сделать несколько последовательных обращений в память — по одному на каждый уровень дерева — просто чтобы понять, где физически лежат нужные данные. И только после этого он читает или пишет сам байт. Если это происходило бы при каждом обращении к памяти, любая программа была бы в разы медленнее, чем есть.
Почему маленькие страницы — это много записей
Стандартный размер страницы в Linux на x86-64 — 4 КБ. Это разумный компромисс для универсальной ОС: мелкая гранулярность позволяет точно управлять памятью, экономить её и легко выгружать в swap ровно то, что нужно. Но у мелкой гранулярности есть обратная сторона — количество страниц, которое приходится обслуживать.
Возьмём процесс СУБД с буферным кешем в 32 ГБ. При страницах по 4 КБ это означает порядка восьми миллионов отдельных записей в таблице страниц только для одного этого региона памяти — и это не считая остального адресного пространства процесса: кода, стека, других сегментов данных. Каждая такая запись — это несколько байт метаданных (физический адрес, флаги доступа, признак «грязной» и присутствующей в памяти страницы и так далее), и всё это дерево таблиц само по себе занимает заметный объём памяти и требует обхода при каждом промахе.
Чем больше памяти использует процесс, тем длиннее и «шире» становится это дерево, и тем больше шансов, что нужная запись не окажется под рукой в быстром кеше процессора. Именно об этом кеше — TLB — разговор дальше, потому что именно он определяет, во что реально выливается разница между 4 КБ и 2 МБ на практике.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверTLB и что происходит при промахе
Ходить в память за таблицей страниц при каждом обращении — слишком дорого, поэтому в процессоре есть отдельный маленький и очень быстрый кеш — Translation Lookaside Buffer (TLB). Он хранит уже вычисленные пары «виртуальная страница → физическая страница» для последних использованных адресов. Пока нужная трансляция есть в TLB — это TLB hit — перевод адреса занимает считаные такты и практически не заметен на фоне остальной работы.
Проблема в том, что TLB — это кеш ограниченного размера прямо на кристалле процессора, и он физически не может вместить трансляции для всех страниц процесса с десятками гигабайт памяти. Точное число записей в TLB зависит от конкретной модели процессора и меняется от поколения к поколению, но в любом случае оно на порядки меньше, чем количество 4-килобайтных страниц в буферном кеше СУБД. Из этого прямо следует: при активной работе с большим объёмом памяти происходит регулярный TLB miss — процессору приходится откладывать текущую операцию и идти делать полный обход таблицы страниц (это называется page table walk), чтобы получить нужную трансляцию, и лишь потом продолжать то, ради чего всё затевалось.
Это не крах производительности — сервер не «зависает» и не выдаёт ошибок. Это тихая, распределённая по всем операциям накладная стоимость: каждый TLB miss добавляет несколько дополнительных обращений к памяти поверх основного. При случайном доступе к большому массиву данных — а это типичный паттерн для буферного кеша СУБД — доля таких промахов существенно выше, чем при последовательном чтении, потому что рабочий набор адресов «прыгает» по памяти и не помещается в TLB целиком. Реальные измерения по разным нагрузкам показывают разброс от почти незаметного до вполне ощутимого процента общей производительности, и конкретная цифра для вашего сервера зависит от паттерна доступа, объёма буферного кеша и модели процессора — универсального числа здесь нет и обещать его не стоит.
Как huge pages решают именно эту проблему
Идея простая: если страница больше, то для того же объёма памяти нужно меньше страниц, а значит — меньше записей в таблице страниц и меньше шансов промахнуться мимо TLB. На x86-64 Linux поддерживает страницы по 2 МБ (huge pages) и по 1 ГБ (gigantic pages, обычно нужны отдельная поддержка со стороны BIOS/процессора и более редкое применение). Страница в 2 МБ покрывает в 512 раз больше памяти, чем обычная 4-килобайтная — соответственно, те же 32 ГБ буферного кеша описываются не восемью миллионами записей, а на три порядка меньшим их числом.
Меньше записей — короче дерево обходов при промахе и, что важнее, сильно выше шанс, что нужная трансляция вообще уже сидит в TLB, потому что один и тот же по размеру TLB теперь покрывает на два-три порядка больший объём памяти. Именно поэтому эффект от huge pages особенно заметен на процессах с большим, активно используемым и не помещающимся в обычный TLB рабочим набором памяти — это ровно портрет СУБД с крупным буферным кешем.
В Linux есть два независимых механизма для huge pages, и путать их не стоит:
- HugeTLB (статические huge pages) — вы заранее резервируете фиксированное число страниц по 2 МБ или 1 ГБ через sysctl, они выделяются из общего пула памяти и становятся недоступны для остальных нужд системы, пока явно не освобождены. Приложение должно само явно попросить память именно из этого пула (через
shmgetс флагомSHM_HUGETLB, черезmmapсMAP_HUGETLB, либо через файл на смонтированнойhugetlbfs). Ни ядро, ни другие процессы не «увидят» и не тронут эту память без явного запроса. - Transparent Huge Pages (THP) — прозрачный механизм, при котором ядро само пытается собирать обычные страницы в huge pages «на лету» для любого подходящего процесса, без изменений в самом приложении. Удобно, но управляется фоновым процессом
khugepaged, который периодически дефрагментирует память, чтобы найти непрерывные блоки под huge pages — и эта дефрагментация сама по себе может создавать паузы в неподходящий момент, особенно под нагрузкой.
Почему СУБД особенно чувствительна к этому
Буферный кеш СУБД — это ровно тот случай, для которого huge pages придуманы: один большой, долгоживущий, активно и хаотично используемый регион памяти. PostgreSQL держит в shared_buffers данные и индексы, к которым идут обращения по случайным смещениям в зависимости от того, какие запросы прилетают; MySQL/InnoDB делает то же самое со своим buffer pool. В обоих случаях это ровно тот паттерн доступа — большой объём, произвольный порядок обращений — при котором TLB промахивается чаще всего, а значит, и выигрыш от сокращения числа страниц потенциально выше, чем на процессе с маленьким или последовательно читаемым рабочим набором.
Есть и второй эффект, отдельный от TLB: если СУБД использует HugeTLB, эта область памяти зарезервирована и явно помечена как невыгружаемая — ядро не станет пытаться вытеснить её в swap ни при каких обстоятельствах, потому что технически это отдельный пул, а не обычная страница с признаком «можно выгрузить». Для буферного кеша, который в норме и не должен уходить в своп, это дополнительная гарантия предсказуемости поведения под память-интенсивной нагрузкой, а не просто побочный эффект.
Как включить huge pages на сервере под базу данных
Проверить текущее состояние можно через /proc/meminfo:
grep -i huge /proc/meminfo
Это покажет строки вида HugePages_Total, HugePages_Free, HugePages_Rsvd и Hugepagesize (обычно 2048 кБ на x86-64). Если HugePages_Total равен нулю — статический пул не зарезервирован.
Зарезервировать пул huge pages можно через sysctl, указав нужное число 2-мегабайтных страниц:
sysctl -w vm.nr_hugepages=16384
16384 страницы по 2 МБ — это 32 ГБ, число нужно подбирать под реальный объём shared_buffers или buffer pool с небольшим запасом, а не наугад. Чтобы настройка пережила перезагрузку, значение добавляют в /etc/sysctl.conf или файл в /etc/sysctl.d/. Стоит иметь в виду: резервирование может не пройти полностью, если память уже фрагментирована — тогда часть страниц не выделится, и об этом честно скажет разница между запрошенным и фактическим HugePages_Total после команды.
Для PostgreSQL параметр huge_pages в postgresql.conf управляет тем, как сервер обращается с пулом:
huge_pages = try
Значение try (обычно оно и стоит по умолчанию) означает «использовать huge pages, если они доступны, иначе тихо работать на обычных страницах» — безопасный вариант на время проверки. on требует huge pages жёстко и не даст серверу запуститься, если пула не хватает — таким стоит выставлять его только после того, как убедились, что резервирования достаточно.
Для MySQL/InnoDB huge pages включаются на уровне ОС тем же sysctl, а в my.cnf добавляется:
large-pages=1
Здесь важна дисциплина: innodb_buffer_pool_size должен укладываться в зарезервированный объём huge pages, а системному пользователю, от которого работает mysqld, нужны права на использование этой памяти — обычно через vm.hugetlb_shm_group или соответствующий lock-memory лимит в /etc/security/limits.conf.
С Transparent Huge Pages для баз данных исторически принято поступать наоборот — не включать бездумно, а явно ограничивать. Проверить текущий режим:
cat /sys/kernel/mm/transparent_hugepage/enabled
Для процессов с большим долгоживущим буферным кешем разумный компромисс — режим madvise, при котором ядро собирает huge pages только для областей памяти, которые сама программа явно об этом попросила, а не пытается угадывать это для всей системы сразу:
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
Подводные камни и когда не включать не глядя
Huge pages — не бесплатный выигрыш без ограничений, и здесь стоит быть честным по всем пунктам.
- Зарезервированная память недоступна остальным. Статический пул HugeTLB забирает память из общего пула сразу, даже если СУБД её ещё не использует. На сервере, где память распределена впритык между несколькими сервисами, резервирование слишком большого пула может привести к тому, что остальным процессам будет физически не хватать памяти для обычных нужд — здесь пригодится разбор того, как Linux обещает память, которой у него нет, он напрямую связан с планированием такого резервирования.
- THP может навредить именно там, где должен помогать. Фоновая дефрагментация
khugepagedв режимеalwaysиногда создаёт задержки в самый неподходящий момент — под латентно-чувствительной нагрузкой это не редкость, поэтому многие руководства по СУБД советуют переводить THP вmadviseили полностью отключать его, а не полагаться на автоматику по умолчанию. - Приложение должно уметь просить huge pages явно (для HugeTLB). Это не универсальный тумблер, который просто «ускоряет всё». Если процесс не запрашивает
SHM_HUGETLBилиMAP_HUGETLB, зарезервированный пул просто будет простаивать, а обычная память по-прежнему будет использовать 4-килобайтные страницы. - Выигрыш неравномерен по нагрузкам. На процессах с небольшим рабочим набором, который и так помещается в TLB целиком, разница будет минимальной или неощутимой — huge pages не панацея, а инструмент под конкретный симптом (частые TLB misses на большом объёме активно используемой памяти), а не общая настройка «для скорости».
- Мониторить эффект стоит по факту, а не по ожиданиям. Помимо
/proc/meminfo, полезно смотреть на счётчики промахов TLB черезperf stat -e dTLB-load-misses(еслиperfдоступен и ядро собрано с поддержкой соответствующих событий) — это единственный способ увидеть реальную, а не предполагаемую разницу на вашей нагрузке. Заодно стоит понимать, почемуpsиtopпоказывают разные цифры памяти для одного и того же процесса — это помогает не путать эффект huge pages с обычными артефактами измерения потребления памяти.
Если вы настраиваете сервер под большую базу с нуля, разумно сразу закладывать huge pages в план по памяти, а не добавлять их постфактум — заодно стоит свериться с более широким чек-листом: частые ошибки при тюнинге PostgreSQL на сервере закрывает соседние параметры, которые обычно настраивают в одном заходе — shared_buffers, effective_cache_size, work_mem и лимиты памяти на уровне ОС.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Обязательно ли включать huge pages на любом сервере с базой данных?
Нет. Эффект заметен там, где буферный кеш большой и активно используется случайным образом — на маленьких базах или базах с преимущественно последовательным доступом выигрыш может быть незаметным, а хлопот с настройкой и мониторингом прибавится.
Что будет, если зарезервировать huge pages больше, чем реально нужно СУБД?
Лишняя память просто окажется недоступна другим процессам, включая файловый кеш и остальные сервисы на сервере — она не «пропадёт», но и не будет ничем полезна, пока не используется тем, кто её запросил.
Можно ли включить huge pages без перезапуска СУБД?
Сам пул через sysctl -w резервируется на лету, но чтобы СУБД начала его использовать, обычно нужен перезапуск процесса — на горячую переключить уже выделенную обычную память на huge pages нельзя.
THP и HugeTLB — это одно и то же?
Нет, это два разных механизма. THP работает прозрачно для любого процесса и управляется ядром автоматически, HugeTLB — это явно зарезервированный пул, которым пользуются только процессы, специально его запросившие. Их можно использовать по отдельности или вместе, но настраиваются они разными параметрами.
Есть ли риск, что huge pages сделают только хуже?
Прямого риска для производительности самой СУБД почти нет, но неудачно настроенный THP с агрессивной дефрагментацией под нагрузкой либо слишком большой статический пул, отобранный у остальной системы, могут ухудшить общую картину — поэтому имеет смысл менять настройки постепенно и сверяться с метриками, а не полагаться на то, что «huge pages всегда лучше».
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →