Продолжаем исследовать настройки памяти в 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С (ч.1). Странное поведение MS SQL Server 2019: длительные операции TRUNCATE

  2. Записки оптимизатора 1С (ч.2). Полнотекстовый индекс или как быстро искать по подстроке

  3. Записки оптимизатора 1С (ч.3). Распределенные взаимоблокировки в 1С системах

  4. Записки оптимизатора 1С (ч.4). Параллелизм в 1С, настройки, ожидания CXPACKET

  5. Записки оптимизатора 1С (ч.5). Ускорение RLS-запросов в 1С системах

  6. Записки оптимизатора 1С (ч.6). Логические блокировки MS SQL Server в 1С: Предприятие

  7. Записки оптимизатора 1С (ч.7). «Нелогичные» блокировки MS SQL для систем 1С предприятия

  8. Записки оптимизатора 1С (ч.8). Нагрузка на диски сервера БД при работе с 1С. Пора ли делать апгрейд?

  9. Записки оптимизатора 1С (ч.9). Влияние сетевых интерфейсов на производительность высоконагруженных ИТ-систем

  10. Записки оптимизатора 1С (ч.10): Как понять, что процессор — основная боль на вашем сервере MS SQL Server?

  11. Записки оптимизатора 1С (ч.11). Не всегда очевидные проблемы производительности на серверах 1С

  12. Записки оптимизатора 1С (ч.12).  СрезПоследних в 1C:Предприятие на PostgreSQL. Почему же так долго?

  13. Записки оптимизатора 1С (ч.13). Что не так в журнале регистрации 1С в формате SQLitе?

  14. Записки оптимизатора 1С (ч.14.1). Любите свою базу данных и не забывайте обслуживать

  15. Записки оптимизатора 1С (ч.14.2). Пересчет индексов на SSD-дисках. Делаем или игнорируем?

  16. Записки оптимизатора 1С (ч.14.3). Отличия в обслуживании статистик в MS SQL и в PostgreSQL

  17. Записки оптимизатора 1С (ч.15). Параллелизм запросов 1С в PostgreSQL

  18. Записки оптимизатора 1С (ч.16). Риски падения Postgres: потребление и высвобождение памяти процессами postgres

  19. Записки оптимизатора 1С(ч.17). Как избежать падения Postgres при большом потреблении памяти запросами

  20. Записки оптимизатора 1С (ч.18.1). Ошибки 1С в части производительности и стабильности ИС

  21. Записки оптимизатора 1С (ч.18.2). Ошибки 1С из-за конфликта блокировок

  22. Записки оптимизатора 1С (ч.19). Как настройка Huge Pages в Linux влияет на устойчивость работы Postgres