Как мы довели Postgres Pro до APDEX 0,986 на 30 000 пользователей 1С:ERP. Часть 1/3

Некоторое время назад мы уже рассказывали о сложностях и вызовах, с которыми столкнулись при проведении тестов ERP компании «1С» на 30 000 виртуальных рабочих мест. Эти тесты мы проводили совместно с коллегами из «ИТ-Экспертизы», которые, в свою очередь, разработали собственный альтернативный тест ERP, на основе профилей нагрузки, собранных с реальных информационных систем заказчиков. В результате удалось сформировать универсальный профиль, отражающий типичный характер работы пользователей «1С:ERP» в крупнейших организациях России. В отличие от оригинального теста, альтернативный тест охватывает полный цикл оперативной работы ERP, включая расчет себестоимости и закрытие месяца. Кроме того, нагрузка в рамках теста гораздо более интенсивна
В этом цикле статей мы хотели бы поделиться нашим опытом и рассказать о том, как в рамках этого теста нам удалось достичь APDEX = 0,986 при нагрузке 30 000 виртуальных рабочих мест в конфигурации с асинхронной репликацией и вынесением части отчётов на реплику. За всё время тестирования мы провели более 100 тестовых запусков, проверили множество гипотез, попробовали различные конфигурации и внесли целый ряд изменений и патчей. Мы не будем останавливаться на каждом проведённом эксперименте и расскажем о наиболее значимых результатах и решениях, которые позволили нам улучшить итоговую производительность СУБД.
Отметим, что все улучшения, о которых пойдёт речь ниже вошли, в версию Postgres Pro Enterprise for 1C 18.6.1.
Описание теста
Тест 1C:ERP от компании «ИТ-Экспертиза» состоит из 20 сценариев, охватывающих основные бизнес-процессы предприятия: продажи и закупки, складские операции, обеспечение заказов, производство, взаиморасчёты, управление денежными средствами, бухгалтерский и налоговый учёт. Сценарии выполняют различные пользовательские роли: менеджеры отделов продаж и закупок, сотрудники складов и производства, экономисты, бухгалтеры и другие специалисты. Для каждого сценария задано определённое количество одновременно работающих пользователей, которое зависит от профиля нагрузки и варьирует от единиц до сотен пользователей.
Наиболее интенсивная нагрузка приходится на операции формирования заказов, внутреннего товародвижения и формирования складских отчётов. Ряд финансовых и бухгалтерских операций выполняется меньшим количеством пользователей, что соответствует их реальной частоте использования.
Сам профиль нагрузки формируется из этих 20 сценариев, которые выполняются циклически, с постепенным увеличением нагрузки в течение пяти этапов. Каждый этап длится один час. В течение этапа 20 сценариев последовательно выполняются по кругу: после завершения последнего сценария цикл начинается заново. Каждый следующий этап запускается со сдвигом на один час, при этом набор сценариев остаётся неизменным. Таким образом, общая продолжительность тестирования составляет пять часов, а максимальная нагрузка и целевое количество виртуальных пользователей достигаются на последнем, пятом этапе. Количество пользователей и их распределение по сценариям задаются в рамках конкретного теста в тест-центре:

Первые запуски и ошибка relation deleted while still in use
Итак, мы приступили к первым запускам альтернативного теста. Вся наша инфраструктура осталась прежней, а для работы с новой БД мы развернули рядом второй инстанс СУБД, работающий со своей PGDATA.
Сразу же применили параметры PostgreSQL, которые хорошо себя показали на тестовых запусках коллег из «ИТ-Экспертизы»:
autovacuum_analyze_scale_factor = 0.01
autovacuum_analyze_threshold = 1000
autovacuum_max_workers = 20
autovacuum_vacuum_cost_delay = 10
autovacuum_vacuum_cost_limit = 7000
autovacuum_vacuum_insert_scale_factor = 0.01
autovacuum_vacuum_insert_threshold = 2000
autovacuum_vacuum_scale_factor = 0.01
autovacuum_vacuum_threshold = 1000По сложившейся традиции на первых запусках присматриваемся к тесту и первые тесты проводим на профилях нагрузки с 5 000, 10 000 и 15 000 ВРМ.
Первые тесты показали следующие результаты:
5 000 ВРМ, APDEX = 1
10 000 ВРМ, APDEX = 0.968
15 000 ВРМ, APDEX = 0.704Что такое APDEX и как он рассчитывается мы уже рассказывали в нашей прошлой статье. Коротко напомним: APDEX — это метрика удовлетворённости пользователей, которая оценивает время выполнения операций и показывает, насколько быстро система отвечает на запросы. Наша цель остаётся прежней: добиться APDEX выше 0,85. При этом в текущем тесте, как можно заметить, возможно достижение максимального значения APDEX = 1.
В запуске на 15 000 ВРМ утилизация СУБД достигала практически 50%. По опыту предыдущего теста на 30 000 ВРМ от «1С» мы закрепили процессы postgres в рамках одного NUMA узла, а это значит, что в текущем тесте при такой конфигурации мы уже перестали укладываться в доступные ресурсы одной NUMA ноды.
Вытаскиваем сервис СУБД из NUMA, перезаливаем базу, запускаем тест. С ростом нагрузки получаем рост утилизации на сервере БД под 100%:

Также присутствуют периодические ошибки на последней ступеньке теста на стороне БД:
relation 39773977 deleted while still in use
relation "pg_temp.tt254" does not exist at character 15
could not find pg_class tuple for index 24547652При возникновении подобных ошибок PostgreSQL закрывал соединения к 1С, в которых они наблюдались. Спустя какое-то время, по мере их накопления, это приводило к падению кластера 1С.
Ошибка проявлялась при включении параметра enable_temp_memory_catalog, отвечающего за использование системного каталога в оперативной памяти для временных таблиц и других временных объектов.
Замечание про enable_temp_memory_catalog. Для работы с 1С эта фича особенно важна: после заполнения каждой временной таблицы, 1С вызывает для неё analyze, что приводит к изменениям в таблице системного каталога pg_statistic. Другая известная проблема связана с созданием индексов для временных таблиц: соответствующие записи в pg_index по умолчанию создаются в общем системном каталоге. Таким образом, использование каталога в оперативной памяти позволяет снизить накладные расходы на дополнительные записи в системном каталоге, связанные с временными объектами. Мы рекомендуем включение этой опции при работе с Postgres Pro Enterprise версии 16 и выше.
Для поиска причины ошибки мы включили сбор backtrace для функции 'RelationClearRelation' и получили стек вызовов, который показал, что ошибка возникает при обработке инвалидации системного каталога. В процессе обработки инвалидации происходит повторный вход в функцию temp_catalog_beginscan(), то есть она фактически вызывалась рекурсивно.
Для воспроизведения ошибки мы вручную инициировали InvalidateSystemCaches() в нужных точках выполнения FASTTRUNCATE. В результате получили ту же ошибку:
ERROR: relation 57388 deleted while still in useВ качестве решения мы сделали патч, в котором в функции get_index_entry() ввели явное сопоставление типов атрибутов системного каталога с соответствующими функциями сравнения, что позволило избежать дополнительного обращения к системному каталогу, которое приводило к повторному входу в temp_catalog_beginscan(). Именно при таком повторном входе функция возвращала NULL из-за защиты от рекурсии, что в итоге приводило к ошибке relation ... deleted while still in use. После устранения причины повторного входа защита от рекурсии с помощью nested больше не требовалась.
Повторный запуск теста на 15 000 ВРМ подтвердил: патч помог и ошибка “relation XXXXXX deleted while still in use" более не наблюдалась. Итоговый APDEX составил 0.841. Тем не менее на последнем часе работы теста по-прежнему была видна высокая и нестабильная утилизация CPU. Поскольку наш план — запуск 30 000 пользователей, такой график выглядел как проблема:

Дальнейшая борьба с инвалидациями
К счастью, в ходе нашего теста мы собираем perf-ы из которых потом автоматически строятся flamegraph-ы, к которым мы и обратимся. Мы начали подозревать, что причина высокой утилизации CPU может быть вызвана взрывным количеством инвалидаций во время теста: среди наиболее заметных функций видна ReceiveSharedInvalidMessages(), которая получает и обрабатывает инвалидационные сообщения из очереди:

Как известно, бекенды PostgreSQL разделяют кешированные данные каталога. Когда бекенд изменяет данные каталога, он отправляет сообщения об инвалидации через общую очередь инвалидаций, чтобы другие бекенды знали, что их закешированные данные устарели и должны быть перезагружены.
При большом количестве одновременно работающих бекендов существенное количество таких сообщений может стать заметным источником накладных расходов. Каждое сообщение должно быть обработано каждым подключенным к базе бекендом: если бекенд не успевает обрабатывать поступающие сообщения, ему приходится сбрасывать кеш и заново обращаться к системным каталогам. При большом количестве подключений это может приводить к дополнительной нагрузке на CPU и конкуренции за доступ к каталогам.
Данная проблема особенно актуальна для нагрузок, в которых активно создаются и удаляются временные таблицы. Поскольку временные объекты доступны только тому бекенду, в котором они были созданы, рассылка сообщений об их изменениях другим сессиям не имеет практического смысла.
Для снижения количества таких сообщений в Postgres Pro Enterprise ранее уже был реализован ряд изменений, в частности была увеличена очередь инвалидаций и ограничена рассылка сообщений для некоторых типов изменений системного каталога. Использование in-memory каталога для временных объектов, о котором писалось ранее, также снижает число посылаемых инвалидационных сообщений. Тем не менее проведённый нами анализ показал, что значительная часть сообщений об инвалидации всё ещё рассылалась.
В результате мы подготовили патч, который уменьшает количество посылаемых сообщений об инвалидации кешей для временных объектов, а также подавляет посылку инвалидаций smgr и инвалидаций snapshot, которые генерируются для pg_depend:
if (RelFileLocatorBackendIsTemp(rlocator))
return;
if (!(enable_temp_memory_catalog && isTemp))
RegisterSnapshotInvalidation(info, dbId, tupleRelId);Дополнительно мы доработали обработку инвалидаций: патч позволяет быстро определить, относится ли инвалидация к временной таблице или типу. Для временных таблиц используется TempRelOidCache, что позволяет избежать лишних обращений к внутреннему кешу PostgreSQL для системных каталогов (catcache), а для типов проверяется принадлежность к временному namespace:
else if (tupleRelId == TypeRelationId)
{
Form_pg_type typetup = (Form_pg_type) GETSTRUCT(tuple);
isTemp = isTempOrTempToastNamespace(typetup->typnamespace) &&
!isMtmTempNamespace();
invalidateRelCache = false;
}Данное улучшение было добавлено в существующую настройку skip_temp_rel_lock.
Кроме того, в последующих запусках мы внесли ряд изменений в нашу конфигурацию:
geqo = off
from_collapse_limit = 14
join_collapse_limit = 14
Замеры на ветке с патчами на инвалидации позволили увеличить APDEX для 15 000 ВРМ до 0.933.
Проблема с конфигурацией сети на сервере БД
После избавления от инвалидаций на последнем часе нагрузки всё ещё был виден резкий скачок Load Average на базе данных:

При этом первая NUMA-нода была загружена больше чем на 80%, а нулевая NUMA-нода недозагружена, порядка 60%:

Это же было видно и на heatmap-графике загрузки логических CPU, на котором красным цветом показана степень загрузки каждого ядра. Часть CPU была под постоянной нагрузкой (красные), тогда как другая - простаивала (синие):

Неравномерность загрузки по NUMA-нодам и большой Load Average при недозагруженности одного из NUMA-узлов можно было объяснить текущей конфигурацией сетевого стека. Вывод lstopo показывал, что сетевой интерфейс ens38f2np2, через который шел весь трафик принадлежит NUMA-1:
7: ens38f2np2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether 40:a6:b7:6a:ea:7a brd ff:ff:ff:ff:ff:ff
RX: bytes packets errors dropped missed mcast
13259408975020 27480430376 0 296 0 127162672
TX: bytes packets errors dropped carrier collsns
63774219040154 57219603048 0 0 0 0
altname enp177s0f2np2
Вывод lstopo:
Machine (2016GB total)
Package L#0
NUMANode L#0 (P#0 1008GB)
HostBridge
PCIBridge
PCI 17:00.0 (Ethernet)
Net "ens81f0np0"
PCI 17:00.1 (Ethernet)
Net "ens81f1np1"
Package L#1
NUMANode L#1 (P#1 1008GB)
HostBridge
PCIBridge
PCI b1:00.0 (Ethernet)
Net "ens38f0np0"
PCI b1:00.1 (Ethernet)
Net "ens38f1np1"
PCI b1:00.2 (Ethernet)
Net "ens38f2np2"
PCI b1:00.3 (Ethernet)
Net "ens38f3np3"Этим объяснялось и то, что 99% softirq сетевых прерываний приходилось именно на ядра 1-й NUMA-ноды. Поскольку в сервере была установлена ещё одна сетевая карта, для балансировки нагрузки между NUMA-нодами и повышения общей пропускной способности мы решили сделать L3 bond поверх двух сетевых интерфейсов, подключённых к разным NUMA-нодам.
После выполненной настройки, запуск теста на 15 000 ВРМ показал APDEX = 0.973 и показатели Load Average спали:

А вот так стало выглядеть распределение утилизации CPU на СУБД по NUMA-узлам:

Проблема с балансировкой процессов серверов приложений 1С между NUMA-нодами
Проблему с неравномерной утилизацией NUMA-узлов мы заметили в том числе и на серверах приложений 1C, однако симптомы там были другими. Несмотря на утилизацию серверов 1С около 50%, один из процессоров всегда был загружен на 100% (утилизация ядер отмечена красным), а вот другой при этом практически простаивал (синий):

В ходе наблюдений оказалось, что во время тестов рабочие процессы rphost, обслуживающие клиентские приложения и взаимодействующие с сервером баз данных, распределялись только по CPU одного из двух NUMA-узлов. Поскольку каждый сервер приложений был развёрнут как VM на bare-metal-сервере с двумя NUMA-узлами, фактически половина доступных аппаратных ресурсов никак не участвовала в нагрузке. Как оказалось, это довольно известное в мире 1С ограничение самой платформы и единой методики его решения насколько мы знаем пока что нет. В нашем случае мы решили написать скрипт, который распределяет все запущенные и вновь создаваемые процессы 1С между всеми доступными процессорами с помощью taskset:
pgrep rphost | sed '1~2s/^/32-63,96-127 /;2~2s/^/0-31,64-95 /' | xargs -n 2 echo sudo -u usr1cv8 taskset -apcРаспределение процессов с помощью crontab скрипта дало положительный эффект: после выполнения скрипта, стали задействоваться CPU обеих NUMA-нод. График ниже показывает загрузку ядер одного из серверов кластера 1С до и после применения скрипта:

Данный workaround мы использовали на постоянной основе до тех пор, пока не перешли к размещению VM в пределах одного NUMA-узла при дальнейших замерах.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.