После прошлой статьи про Huge Pages мы получили несколько вопросов, в которых особо пытливые читатели возмущались отсутствием в статье реальных замеров «С HP» и «Без HP» и сравнением результатов. Материалов по этой теме в интернете крайне мало, а подобных замеров и подавно. Те же, что есть, все какие-то синтетические (в основном переводы зарубежных источников), оторванные от реальности, от работы информационных систем и, тем более, от 1С систем. Поэтому мы, как дотошные исследователи, решили закрыть этот гештальт и проверить всё самим.

Проведем лабораторную работу.

Давайте еще раз про Huge Pages – что такое и зачем они нужны. Это важно для исследования.

Процессор работает с виртуальной памятью, а не с физической. Чтобы программа могла обратиться к данным, специальный блок внутри процессора TLB (Translation Lookaside Buffer) хранит соответствие виртуальных адресов физическим. Обычно память разбита на страницы по 4 килобайта. Когда страниц становится очень много (а при больших объёмах оперативки их миллионы), TLB перестаёт вмещать все записи. Процессор вынужден при каждом промахе обходить дерево таблиц страниц (Page Tables) в оперативной памяти. И это сразу долго. А в условиях вложенной виртуализации (Two-Dimensional Paging), когда СУБД работает на виртуальной машине, нагрузка на процессор усиливается еще больше.

Huge Pages — это механизм, который позволяет использовать страницы по 2 мегабайта (или даже 1 гигабайт). Одна такая страница заменяет 512 обычных. Количество записей в TLB резко падает, промахов становится меньше, процессор тратит меньше тактов на поиск адресов. В теории это должно ускорять работу с памятью. Но насколько — вопрос.

Как мы тестировали

Если вы ожидаете найти в этом исследовании ответ на вопрос типа «А вот как ускорится/замедлится после включения HP расчет какой-нибудь себестоимости в УПП», то нет, вам не сюда. Huge Pages использует в PG только раздел памяти shared buffers и больше ничего. Поэтому наша методика предполагает использование сугубо синтетических тестов с запросами SQL, использующих только shared buffers. А реальная 1С-ка будет использовать все куски «пирога памяти» и вдобавок кучу временных таблиц и логики 1С на стороне сервера приложения. Поэтому синтетика, но с возможностью позиционирования в первом приближении на реальность 1С.

Стенд

  1. Физический хост: Гипервизор Proxmox VE, процессор Intel Xeon E5-2697 v2 (архитектура Ivy Bridge-EP, 12 ядер / 24 потока, 30 МБ кэш L3).
  2. Виртуальная машина (ВМ): ОС Debian 13, выделено 8 ядер (тип процессора в Proxmox выставлен как host для сквозного проброса инструкций), 100 ГБ RAM.
  3. СУБД: PostgreSQL 17 (ванильная). Параметр shared_buffers = 40 ГБ, work_mem = 1GB, max_parallel_workers_per_gather = 0.Опыт был повторен также и для одной из вендорских СУБД с точно таким же результатом.
  4. Тестовые данные: Синтетическая таблица [test_hp_table]  из 10 колонок с типом bigint объемом ~20 ГБ + уникальный B-Tree индекс объемом ~3,7 ГБ (суммарно ~24 ГБ, что гарантирует полное размещение данных в shared_buffers и исключает влияние дисковой подсистемы I/O на результаты тестов).

Методика тестирования

Таблицу test_hp_table заполнили 180 млн. записей.

Заполнение таблицы [test_hp_table], создание индекса и пересчет статистики
-- 1. Выделяем 4 ГБ памяти на быструю сборку индекса (только на сессию)
SET maintenance_work_mem = '4GB';

-- 2. Создаем нелогируемую таблицу
CREATE UNLOGGED TABLE test_hp_table (
    id bigint,
    col1 bigint,
    col2 bigint,
    col3 bigint,
    col4 bigint,
    col5 bigint,
    col6 bigint,
    col7 bigint,
    col8 bigint,
    col9 bigint
);

ALTER TABLE test_hp_table SET (autovacuum_enabled = false);

-- 3. Быстро заливаем 180 млн строк (без индексов)
INSERT INTO test_hp_table (id, col1, col2, col3, col4, col5, col6, col7, col8, col9)
SELECT 
    g,
    (random() * 9223372036854775807)::bigint,
    (random() * 9223372036854775807)::bigint,
    (random() * 9223372036854775807)::bigint,
    (random() * 9223372036854775807)::bigint,
    (random() * 9223372036854775807)::bigint,
    (random() * 9223372036854775807)::bigint,
    (random() * 9223372036854775807)::bigint,
    (random() * 9223372036854775807)::bigint,
    (random() * 9223372036854775807)::bigint
FROM generate_series(1, 180000000) AS g;

-- 4. Строим индекс в один проход
CREATE UNIQUE INDEX idx_test_hp_id ON test_hp_table(id);

-- 5. Актуализируем статистику для планировщика
ANALYZE test_hp_table;Code language: PostgreSQL SQL dialect and PL/pgSQL (pgsql)

Чтобы исключить влияние дисков, перед каждым тестом принудительно загружали таблицу и индексы в буферный кэш через расширение pg_prewarm.

CREATE EXTENSION pg_prewarm;

SELECT pg_prewarm('test_hp_table');
SELECT pg_prewarm('idx_test_hp_id');Code language: PostgreSQL SQL dialect and PL/pgSQL (pgsql)

Для генерации транзакционной нагрузки использовалась утилита pgbench в режиме конкурентного случайного поиска по индексу (Select-only). С целью минимизации накладных расходов на переключение контекста процессора (Context Switches), конфигурация потоков бенчмарка была жестко приведена к архитектуре ВМ в соотношении 1:1 (8 клиентов, 8 потоков):

sudo -u postgres pgbench -f /var/lib/postgresql/test_query.sql -c 8 -j 8 -T 30
Code language: PostgreSQL SQL dialect and PL/pgSQL (pgsql)

Запросы, которые использовались в утилите pgbench для создания нагрузки:

Сценарий 1. Простой SELECT по индексу (почти чистое чтение блоков из shared buffers)

set id random(1, 180000000)
SELECT * FROM test_hp_table WHERE id = :id;Code language: PostgreSQL SQL dialect and PL/pgSQL (pgsql)

Сценарий 2. SELECT с JOIN, сортировкой и агрегацией (смешанная операция чтения блоков и другой математики)

set id random(1, 179999900)

SELECT 
    c.category_name,
    COUNT(t.id) as total_count,
    AVG(t.col1) as avg_col1,
    SUM(t.col2) as sum_col2,
    MAX(t.col3) - MIN(t.col4) as spread
FROM test_hp_table t
JOIN test_hp_categories c ON c.cat_id = (t.col5 % 100000) + 1
WHERE t.id BETWEEN :id AND :id + 50
GROUP BY c.category_name
ORDER BY spread DESC, c.category_name ASC;Code language: PostgreSQL SQL dialect and PL/pgSQL (pgsql)

Для снятия метрик использовали утилиту ядра Linux perf в режиме глобального мониторинга (ключ -a) всей системы (system-wide), запущенной в отдельной сессии. Анализируемые метрики:

  1. page-faults – ошибки страниц.
    Количество исключений (исключительных ситуаций) процессора, возникающих, когда поток обращается к странице памяти в своем виртуальном адресном пространстве, которая в данный момент не отображена в физическую память (RAM) или защищена. В контексте работающего под нагрузкой PostgreSQL данная метрика чаще всего отражает Minor Page Faults (мягкие ошибки) — ситуации, когда память уже выделена в буферном пуле, но для конкретного процесса postgres еще не построена запись в его локальной таблице страниц.
  2. dTLB-loads – обращения к TLB.
    Общее количество операций чтения данных из памяти, которые привели к обращению в Data Translation Lookaside Buffer (dTLB). В контексте СУБД показывает общую интенсивность работы СУБД с кэшем трансляции адресов. Это базовый показатель активности подсистемы памяти.
  3. dTLB-load-misses – промахи TLB.
    Количество запросов к памяти, адресов которых не оказалось в кэше dTLB. В этот момент процессор вынужден приостановить выполнение инструкций программы и запустить аппаратный или программный обработчик Page Walk (обход дерева таблиц страниц), который ищет адрес в медленной оперативной памяти (RAM), тратя на это драгоценные такты процессора.
  4. cycles:k – такты процессора в ядре.
    Количество циклов (тактов) процессора, затраченных на выполнение инструкций в привилегированном режиме (кольцо защиты 0 / Kernel Space).
    В идеальной конфигурации СУБД ядро должно лишь обеспечивать сетевой стек и редкий дисковый ввод-вывод, а этот показатель должен стремиться к минимуму. Однако при отключенных Huge Pages счетчик cycles:k раздувается за счет того, что процессор тратит миллиарды тактов на логику ядра Linux по управлению памятью: обработку page-faults, синхронизацию таблиц страниц между ядрами (TLB Shootdowns) и обход деревьев страниц в памяти.
  5. cycles:u  – такты в пользовательском пространстве.
    Количество циклов процессора, затраченных на выполнение инструкций в пространстве пользователя (кольцо защиты 3 / User Space).
    Это полезная вычислительная мощность вашего сервера. Сюда входят парсинг SQL-запросов, планирование, выполнение соединений (JOIN), сортировки, логика MVCC и работа самого демона PostgreSQL и процессов клиентов.

Все замеры выполнялись для двух конфигураций: обычные страницы 4 КБ и Huge Pages 2 МБ.

Результаты измерений

Сценарий 1. Простой Select

МетрикаБез Huge Pages (4 КБ)Huge Pages 2 МБВлияние оптимизаций
Пропускная способность (TPS)54 263 транз/сек58 564 транз/сек+7,93%
Средняя задержка (Latency)0,147 мс0,137 мс-6,80% времени ответа
page faults (за 25 сек)1 397 5511 044Снижение в 1338 раз
dTLB-loads (за 25 сек)67,08 млрд69,36 млрд+2,28 млрд обращений к памяти
Процент промахов dTLB2,73%2,49%Снижение на 0,24%
cycles:k (такты ядра)180,46 млрд163,42 млрдСэкономлено 17,04 млрд (9,4%) тактов ядра
cycles:u (такты пользователя)407,13 млрд422,89 млрд+15,76 млрд (3,7%) тактов в работу СУБД

Цифры получились наглядные, поэтому разберём их по частям.

Наглядно измеримые улучшения

1. Ликвидация накладных расходов ядра (page-faults)
В реальном прогоне на страницах 4 КБ за 25 секунд возникло 1 397 551 ошибок page fault. Ядро при каждом вынуждено прерывать выполнение, выявлять и регистрировать малейшие изменения страниц, а также управлять сложной структурой страниц физической памяти. При использовании страниц 2 МБ число ошибок страниц сократилось до 1 044. То есть почти исчезло.

2. Перераспределение ресурсов процессора
Анализ метрик показал, что страницы 4 КБ и страницы 2 МБ дают разный баланс тактов. На тесте с Select-only ядро тратит на 9,4% меньше тактов (cycles:k: 180,46 млрд → 163,42 млрд), а пользовательские процессы получают на 3,9% больше процессорного времени (cycles:u: 407,13 млрд → 422,89 млрд). Это чистое преимущество, которое напрямую конвертируется в рост TPS.

Особенности архитектуры процессора (почему Huge Pages выбран 2 МБ, а не 1 ГБ)

Тестирование гигабайтных страниц (1 ГБ) в рамках данного исследования показало меньшую эффективность относительно страниц 2 МБ (57,6k TPS против 58,5k TPS). На поколениях процессоров Intel Ivy Bridge аппаратный кэш TLB 2-го уровня (STLB) оптимизирован под сквозное кэширование страниц размером 4 КБ и 2 МБ. Поддержка страниц 1 ГБ ограничена малым количеством регистров в L1 dTLB, что при полностью случайной выборке данных вызывает повышенную плотность промахов на уровне регистров CPU. Для данного поколения «железа» размер 2 МБ является оптимальным вариантом.

Сценарий 2. Select с JOIN, сортировкой и группировкой

Для второго сценария нам понадобится вторая таблица [test_hp_categories]:

-- Создаем таблицу справочника
CREATE UNLOGGED TABLE test_hp_categories (
    cat_id bigint PRIMARY KEY,
    category_name text
);

-- Заполняем её 100 000 строками
INSERT INTO test_hp_categories (cat_id, category_name)
SELECT g, 'Category name number ' || g
FROM generate_series(1, 100000) AS g;

-- Собираем статистику
ANALYZE test_hp_categories;
Code language: PostgreSQL SQL dialect and PL/pgSQL (pgsql)
МетрикаБез Huge Pages
(4 КБ)
Huge Pages 2 МБ (Оптимизация)Чистый эффект от HP 2МБ
Пропускная способность (TPS)10 32310 795+4,57% скорости
Средняя задержка (Latency)0.775 мс0.741 мс-4,38% времени ответа
page-faults (за 25 сек)473 5754 146Снижение нагрузки в 114 раз!
dTLB-loads (за 25 сек)204,86 млрд213,41 млрд+8,55 млрд операций с памятью
Процент промахов dTLB0,45%0,33%Снижение на 0,12%
cycles:k (Такты Ядра Linux)51,15 млрд42,99 млрдСэкономлено 8,16 млрд тактов ядра
cycles:u (Такты СУБД / Кода)533,56 млрд543,65 млрд+10,09 млрд тактов ушло в вычисления

Почему в Сценарии 2 прирост уменьшился с 8% до 4,5%?

Это ожидаемо. Пока запрос просто ищет строку, бутылочное горлышко – трансляция адресов. Как только добавляется математика, сортировки, соединения, процессорное время уходит на другие операции и выигрыш от HP размывается. Основные причины:

  1. Сдвиг фокуса процессора. В прошлом легком тесте (SELECT * WHERE id = :id) процессор выполнял запрос мгновенно. Трансляция памяти (поиск адресов 4 КБ) занимала огромную долю от общего времени работы. Настроив Huge Pages, мы убрали эту долю, получив рывок в ~8%. В новом тяжелом тесте процессор тратит колоссальное количество времени на чисто вычислительную работу в пространстве пользователя (cycles:u вырос с 422 до 543 миллиардов тактов!). Ему нужно не просто достать данные, а прогнать алгоритм Hash Join, посчитать AVG(), SUM(), MAX(), MIN() и отсортировать пачку строк.
  2. Падение доли промахов TLB. Процент промахов dTLB в выводе perf stat упал с 2,49% (в легком тесте) до 0,33% — 0,45% (в тяжелом). Почему? Потому что теперь процессор, обратившись к одной странице памяти, «застревает» в ней надолго, выполняя вычисления над строками, вместо того чтобы хаотично прыгать по всей RAM. Относительная нагрузка на TLB снизилась сама по себе.

Основной вывод по второму тесту: абсолютная эффективность неизменна!

Несмотря на то, что в процентах прирост TPS стал скромнее (4.57%), абсолютные показатели доказывают, что Huge Pages работают точно так же эффективно:

  • Ядро Linux сэкономило 8,16 млрд тактов (cycles:k). Оно снова полностью избавилось от рутины обслуживания сотен тысяч мелких страниц (page-faults упал с 473к до 4к).
  • Пространство пользователя получило дополнительные 10,09 млрд тактов (cycles:u).

Процессор сэкономил фиксированный объем «энергии» на трансляции адресов и ровно этот объем отдал PostgreSQL на математику. Из-за этого база успела провернуть на 8,5 миллиардов больше операций чтения данных (dTLB-loads) за те же 25 секунд.

В качестве дополнительной проверки мы увеличили huge page, shared buffers и объем таблицы в запросе в 3 раза и проверили исследование по первому сценарию. Результат получили практически такой же.

Выводы

Huge Pages определенно полезны, но не панацея! Максимальный эффект, который нам удалось получить на тестовом стенде –  это 8% на простом чтении по индексу. В тяжёлом тесте с JOIN, агрегацией и сортировкой прирост упал до 4,5%. Нужно понимать, что в реальной работе 1С такие простые запросы почти не встречаются. Обычно там есть многоэтажные соединения, вычисления, работа с временными таблицами. Поэтому на практике ускорение будет ещё скромнее.

Настройка HP оправдана, если у вас большой объём оперативной памяти. Мы бы сказали, от 50 ГБ и выше. Если меньше, эффект можно не заметить. И обязательно нужен кто-то, кто будет присматривать за этим хозяйством: правильно выставить vm.nr_hugepages, прописать huge_pages = on в postgresql.conf, убедиться, что память выделилась.

Но главное – не ждите чудес. Оптимизация запросов, правильные индексы, грамотная схема БД дают выигрыш в разы и на порядки больше. Huge Pages – это тонкая настройка, которая в лучшем случае улучшит общую производительность на несколько процентов.

Update

После осмысления написанного пришло понимание, чего не хватает в существующих инструментах мониторинга Postgres — отсюда и весь сыр-бор с Huge Pages. Именно поэтому Perfexpert, будучи самым полным и эффективным мониторингом производительности для систем 1С на Postgres и MSSQL, обязан содержать эти метрики. Они позволят видеть фактический эффект от использования Huge Pages в конкретной информационной системе, выводя тему из плоскости чистой теории.


Ссылки на остальные части Записок оптимизатора 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С в формате SQLite?
  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
  23. Записки оптимизатора 1С (ч.20). На сколько реально настройки Huge Pages для Postgres могут ускорить запросы 1С

Меню

Что будем искать? Например,репликация