Продолжаем исследовать настройки памяти в Postgres и сегодня поговорим о связке настроек в ОС и СУБД. Речь пойдет о связке hugepages (Linux) и shared_buffers (Postgres).
Триггером для исследования послужила реальная жизненная ситуация у нашего клиента на поддержке производительности его системы. Однажды утром… (неплохое начало) поступило обращение, что Postgres перестал запускаться вроде бы без видимых на то предпосылок: «мамой клянусь, ничего не делали с базой».
Но как это часто бывает, всё-таки изменяли. Не в Postgres, а в Linux. Очень часто бывает, что настройками в ОС и в СУБД занимаются разные специалисты, не согласовывая свои действия. А даже если и согласовывая, то не всегда догадываются о значительном влиянии этих настроек друг на друга. Как вы уже поняли, разбирать будем параметр настройки Linux – hugepages и его взаимодействие с параметром Postgres shared_buffers.
В одной из прошлых статей мы разбирали влияние на стабильность Postgres другой настройки Linux – OOM Killer (Записки оптимизатора 1С (ч.17). Как избежать падения Postgres при большом потреблении памяти запросами). Теперь будем разбираться с другой.
Следует сказать пару слов о Huge Pages. Это механизм Linux, который использует страницы памяти больше стандартных 4 кБ. На x86_64 самые распространённые размеры 2 Мб и 1 Гб. Идея в том, чтобы вместо множества мелких блоков памяти работать с меньшим числом крупных. Механизм нужен, чтобы снизить накладные расходы при работе с большими непрерывными блоками памяти. Когда приложение (например, база данных или виртуальная машина) использует много памяти, система создаёт тысячи 4-х кБ-страниц. Это раздувает таблицы страниц, повышает нагрузку на буфер ассоциативной трансляции процессора (TLB) и ведёт к частым «промахам» (TLB miss), из-за чего процессор тратит время на обновление сопоставлений. Huge Pages уменьшают количество записей в таблицах страниц и снижают давление на TLB. Например, для 1 Гб памяти нужно более 260 тыс. страниц по 4 кБ. С Huge Pages количество записей резко сокращается: для того же 1 Гб памяти, но с 2-МБ страницами нужно уже 512 записей. В общем, в этой части арифметика понятна.
Теперь переходим к связке с shared_buffers.
Обратная сторона медали Huge Pages
При всём вроде бы положительном эффекте от использования huge_pages в информационных системах с большим количеством оперативной памяти есть несколько подводных камней, о которых мало кто догадывается, ибо информация в интернете представлена очень разрозненно и неинформативно (я бы даже сказал малополезно – букв много, а что с ними делать после прочтения непонятно).
Первое, что нужно понимать – Huge Pages в связке с Postgres оптимизируют только разделяемую память (shared_buffers). Оперативная память, выделяемая под сортировки (work_mem) или хеш-таблицы соединений, по-прежнему использует стандартные страницы. Таким образом, если у вас много тяжелых запросов с большим work_mem, вы все равно можете упереться в особенности работы Linux с 4-х килобайтными страницами и Huge Pages здесь не помогут. Этот нюанс критически важен для правильной настройки всей системы.
Второе, о чем необходимо помнить – это статическое резервирование памяти. И вот здесь как раз самый главный подводный камень. Huge Pages резервируются в памяти физически и «намертво». Если вы выделили 400 Гб под огромные страницы, ядро не сможет отдать эти 400 Гб другим процессам. Чтобы это пояснить, предлагаю вернуться к блоку наших статей, посвященному настройкам параметров по работе Postgres с оперативной памятью. Мы там вводили понятие «пирог памяти». Позволю себе привести фрагмент.
Весь пирог – это тот объем памяти, который отдан одному инстансу PostgreSQL, а его куски – shared_buffers, maintenance_work_mem, temp_buffers и work_mem и есть основные потребители этой памяти. На картинке ниже условно изображен наш пирог. Цифр на нем нет. Специально, т.к., во-первых, некоторые параметры задаются на общий объем, а некоторые параметры задаются на сессию – объем динамически меняется. А, во-вторых, всё нужно считать, чем мы и будем заниматься на протяжении всего цикла статей.

shared_buffers – это параметр, который устанавливает, сколько выделенной памяти будет использоваться для кэширования данных. Обычно под shared_buffers отдают львиную часть всей оперативной памяти – от 25 до 50%. Если сравнивать с MS SQL Server, то это близкий аналог buffer pool, размер которого ограничивается параметром max server memory.
Про остальные куски пирога, если кто запамятовал, предлагаю почитать непосредственно в статье, а мы возвращаемся к huge pages. Так вот, если считать, что на нашем идеальном сервере кроме одного инстанса PG больше ничего не работает, то этот пирог стоит несколько расширить, добавив еще один кусок – Huge pages. И вид пирога будет разным в зависимости от того, как выставлен баланс между hugepages в Linux и shared_buffers в Postgres.
Поскольку Postgres умеет работать с hugepages, то правильно его выставлять таким, чтобы вся shared_memory попадала в эту область памяти. Т.е. наш пирог будет выглядеть как-то так:

Темно-красная область – это та самая huge pages, в которую поместилась целиком область shared_buffers.
Рекомендуем ее делать не точно по размеру shared_buffer, а чуть с запасом на пару-тройку гигабайт.
А вот так будет выглядеть пирог памяти, если shared_buffers не поместится в область huge_pages:

Оставил на рисунке только значимые области, остальные помещены внутрь «Все остальное», чтобы не отвлекали от главного. Итого что получается. Если администратор что-то перепутал с размерами этих параметров или недоглядел, то Linux просто зарезервировал весьма весомый кусок памяти за огромными страницами и Postgres не сможет его использовать. Фактически размер оперативной памяти, доступный Postgres, снизился на эти сотни гигабайт (мы же говорим о высоконагруженных системах с хорошим железом) и это вообще не очевидно. Память вроде есть, а её нет.
Почему Postgres может не работать из-за настроек Huge Pages
Теперь покажем почему Postgres может не запуститься. Речь о ситуации перезапуска Postgres после изменения настройки hugepages в Linux или же об изменении настройки shared_buffers в Postgres. И то, и то может привести к неприятным последствиям.
По умолчанию в Linux настройка hugepages = 0, т.е. HP не используется. Для включения требуется указать размер области памяти HP, введя количество страниц памяти (не байты, не мегабайты, а количество страниц). По умолчанию размер huge page страницы для x86 архитектуры равен 2 Мб (но можно изменить на 1 Гб), соответственно количество страниц потребуется рассчитать простыми арифметическими действиями. Но к этому вернемся чуть позже.
В Postgres за настройку HP отвечает аналогичный параметр huge_pages, принимающий три значения:
try (по умолчанию) – PG будет пытаться использовать huge pages, но при недоступности использует обычные страницы размером 4 кБ.
on – PG будет требовать использовать huge pages. Если их недостаточно, PG не запустится.
off – PG не будет использовать huge pages.
Для того, чтобы в Linux правильно установить количество страниц, Postgres сам их рассчитывает, исходя из размера страницы huge page (тот, что 2 Мб по умолчанию) и размера shared_buffers. Для этого у PG есть вычисляемый параметр, который рассчитывается при старте системы – shared_memory_size_in_huge_pages. В принципе, его и можно переносить в настройки Linux, но немного увеличив.
Теперь становится понятно почему PG может не запуститься. Проще всего показать это с помощью таблички, в которой представлены несколько комбинаций параметра huge_pages и размера Huge Pages.

В общем видно, что нужно крайне внимательно устанавливать параметры работы Postgres с HP.
Даже если размеры посчитаны правильно и shared buffers полностью помещается в huge pages, выставленное значение huge_pages = OFF просто отнимет от общего объема весомый кусок оперативки – синяя строка в таблице.
Ну и причина возможного незапуска Postgres из-за Hugу Pages также понятна. Shared buffers не помещается в HP, при этом в настройках Postgres установлено принудительное помещение SB в HP, чего он выполнить никак не может и соответственно не запускается – красная строка в таблице.
Натурный эксперимент
Теперь смоделируем несколько ситуаций на стенде для закрепления материала.
Стенд тот же, что и в экспериментах ранее с OOM Killer:
Debian 13 + ванильный Postgres 17.9
ВМ с 4 Гб ОЗУ без swap.
Настройки postgresql.conf:
vm.overcommit_memory=0
work_mem = 1GB
shared_buffers = 360 МB (стартовая точка)
В тестовой БД создаем таблицу со случайными данными:
CREATE TABLE table1 (column1 TEXT, column2 INT); DO $$ DECLARE _id int :=0; BEGIN WHILE _id < 10000000 LOOP INSERT INTO table1 (column1, column2) VALUES (array_to_string(array( SELECT SUBSTR('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',((random()*(36-1)+1):: INTEGER),1) FROM generate_series(1,256)),''), round(random()*100 )); _id := _id+1; END LOOP; END $$;
Далее из консоли psql с помощью скрипта (script.sql) будем запускать тяжелый запрос, требующий много памяти.
SELECT t3.* FROM table1 t1 FULL JOIN table1 t2 ON t1.column2=t2.column2 FULL JOIN table1 t3 ON t3.column2=t2.column2 LIMIT 1;
Для проведения исследования выделим в системе под HP 400 Мб, что составляет 200 страниц:
sysctl -w vm.nr_hugepages=200

Для проверки выполним команду: cat /proc/meminfo | grep Huge

Видно, что всего 200 страниц, размер страницы 2 Мб и общий размер – 400 Мб. Все страницы доступны для использования.
Postgres может использовать HP для shared buffers, поэтому выделенный под это объём ОЗУ должен помещаться в HP, чтобы Postgres мог его использовать.
Пример 1.
Установим параметр huge_pages = try (это значение по умолчанию) и shared_buffers = 360MB.
После перезапуска Postgres выполним в соседнем окне запрос и убедимся, что HP используются:
psql -c "SHOW huge_pages_status;"

Запустим наш тестовый запрос и понаблюдаем за работой в htop. В свежих версиях можно включить отображение, что и было сделано.
В одном окне выполняется запрос psql -f script.sql -tA:

В другом проверим потребление ресурсов:

Видно, что PG использует 32 Мб из имеющихся 400 Мб HP. Больше ему для выполнения не нужно, а остальной объем потребленной памяти – это уже кусок пирога work_mem, который вне диапазона Huge Pages. Иными словами, эти 400 Мб (200 HP) мы как бы отняли у ОС только для программ/процессов, которые умеют работать с HP, в конкретном случае – shared buffers у Postgres. Если выделить слишком много HP, а shared buffers оставить небольшим, то в отсутствие других программ, умеющих работать с HP, оставшиеся страницы памяти никем не будут использоваться и фактически это будет означать лишение сервера части ОЗУ.
Посмотрим ещё раз на статистику использования HP после завершения запроса:

Видно, что свободно 184 страницы, соответственно, 16 страниц занято. Это же мы видели в htop, что соответствует занятым 32 Мб. Так же видно, что зарезервировано 179 страниц, что на одну страницу меньше, чем объём shared buffers.
Пример 2
Выше упоминал, что количество HP должно быть чуть больше, чем размер shared buffers, иначе его не хватит. Во втором примере установим размер shared_buffers = 400MB. Т.е. ровно столько, сколько выделено HP. Перезапустим Postgres и проверим, будет ли он размещать shared buffers в huge pages:

Видно, что postgres не использует HP! Теперь память, выделенная под это, будет полностью простаивать. Запустим наш скрипт и посмотрим, что покажет htop:

Как видно, htop подтверждает, что HP не используются.
А теперь применим настройку huge_pages = on и посмотрим на поведение postgres (случай с красной строкой в таблице). Как мы уже видели в предыдущем примере, выделенного объёма HP в 200 страниц недостаточно для размещения shared buffers размером 400 Мб, поэтому, при попытке перезапуска, postgres просто не запустится! При попытке выполнить скрипт с тестовым запросом, получим ошибку:

Поэтому очень важно выделять количество HP чуть больше, чем требуется, иначе можно будет столкнуться с ситуацией, когда, вроде как для оптимизации, администратор решит задействовать HP и столкнётся с тем, что его postgres вообще откажется запускаться. Таким образом, настройку huge_pages = on желательно производить только в том случае, если вы точно знаете, для чего это делаете и почему недостаточно более безопасной в этом случае и установленной по умолчанию настройки huge_pages = try.
Резюме
Резюме простое. Если вы решили использовать Huge pages, то следите постоянно за тем, чтобы размер HP был чуть больше размера shared buffers. B при смене одного из этих параметров не забывайте сразу проверять другой и сравнивать их.
И по поводу настройки huge_pages (try, on, off).
Вариант, когда huge_pages = on – пожалуй более правильный, хотя и более требовательный к настройке. Режим «ON» гарантирует, что буферный пул (shared_buffers) всегда будет работать на максимальной скорости, используя преимущества huge pages, и вы не пропустите ситуацию, когда по какой-то причине они (huge pages) стали недоступны – Postgres не запустится. Режим же TRY просто "промолчал" бы, отобрав у оперативной памяти большой кусок). Если готовы к такому сценарию, то выбирайте huge_pages = on. Если же не готовы к возможному не запуску PG, тогда ваш вариант huge_pages = try – он более гибок, уже выставлен по умолчанию, но имеет подводные камни, впрочем, как и любой другой режим.
Ссылки на остальные части Записок оптимизатора 1С:
Записки оптимизатора 1С (ч.1). Странное поведение MS SQL Server 2019: длительные операции TRUNCATE
Записки оптимизатора 1С (ч.2). Полнотекстовый индекс или как быстро искать по подстроке
Записки оптимизатора 1С (ч.3). Распределенные взаимоблокировки в 1С системах
Записки оптимизатора 1С (ч.4). Параллелизм в 1С, настройки, ожидания CXPACKET
Записки оптимизатора 1С (ч.5). Ускорение RLS-запросов в 1С системах
Записки оптимизатора 1С (ч.6). Логические блокировки MS SQL Server в 1С: Предприятие
Записки оптимизатора 1С (ч.7). «Нелогичные» блокировки MS SQL для систем 1С предприятия
Записки оптимизатора 1С (ч.8). Нагрузка на диски сервера БД при работе с 1С. Пора ли делать апгрейд?
Записки оптимизатора 1С (ч.9). Влияние сетевых интерфейсов на производительность высоконагруженных ИТ-систем
Записки оптимизатора 1С (ч.10): Как понять, что процессор — основная боль на вашем сервере MS SQL Server?
Записки оптимизатора 1С (ч.11). Не всегда очевидные проблемы производительности на серверах 1С
Записки оптимизатора 1С (ч.12). СрезПоследних в 1C:Предприятие на PostgreSQL. Почему же так долго?
Записки оптимизатора 1С (ч.13). Что не так в журнале регистрации 1С в формате SQLitе?
Записки оптимизатора 1С (ч.14.1). Любите свою базу данных и не забывайте обслуживать
Записки оптимизатора 1С (ч.14.2). Пересчет индексов на SSD-дисках. Делаем или игнорируем?
Записки оптимизатора 1С (ч.14.3). Отличия в обслуживании статистик в MS SQL и в PostgreSQL
Записки оптимизатора 1С (ч.15). Параллелизм запросов 1С в PostgreSQL
Записки оптимизатора 1С (ч.18.1). Ошибки 1С в части производительности и стабильности ИС
Записки оптимизатора 1С (ч.18.2). Ошибки 1С из-за конфликта блокировок

