Некоторое время назад мы уже рассказывали о сложностях и вызовах, с которыми столкнулись при проведении тестов 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.

Кроме того, в последующих запусках мы внесли ряд изменений в нашу конфигурацию: 

Замеры на ветке с патчами на инвалидации позволили увеличить 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-узла при дальнейших замерах.

Комментарии (11)


  1. vvm13xx
    12.10.2026 18:17

    "В запуске на 15 000 ВРМ утилизация СУБД достигала практически 50%. По опыту предыдущего теста на 30 000 ВРМ от «1С» мы закрепили процессы postgres в рамках одного NUMA узла, а это значит, что в текущем тесте при такой конфигурации мы уже перестали укладываться в доступные ресурсы одной NUMA ноды.

    Вытаскиваем сервис СУБД из NUMA, перезаливаем базу, запускаем тест. С ростом нагрузки получаем рост утилизации на сервере БД под 100%: "

    Вот на этом споткнулся. Пришлось аж с гуглём советоваться. По виду фразы, "нода" и "узел" не одно и то же. Потом, "вытащить из NUMA" могло означать переезд с многосокетного сервера на односокетный. Но гугль сказал, что автор имел в виду, что сервисы отвязали от NUMA-узла. По мне, очевидно, что таким образом сервис СУБД в NUMA втащили...


    1. mizhka
      12.10.2026 18:17

      Нода и узел - это одно и тоже. Более того, в данном случаи, 1 NUMA = 1 процессор, поскольку на каждом процессоре было по 1 memory controller-у.


  1. Coocooroo
    12.10.2026 18:17

    удивительно, что 1С, в лице Нуралиева, не рассказали вам, как это работает на SMP. известный факт, что 1С рассчитана на однопроцный комп, стоящий под столом. Конечно, я утрирую, но с многоядерностью, многопроцессорностью, что сервера приложений, что сервера СУБД - в 1С, ничего не изменилось. В доке по одной из СУБД так и было написано "цена параллелизма =1"


    1. mizhka
      12.10.2026 18:17

      Стоит отметить, что многие операции, связанные с NUMA, берёт на себя операционная система, а не пользовательский софт. То есть организация виртуальной памяти, scheduling процессов - это то чем оперирует ядро, а пользовательский софт может только пользоваться теми механизмами что ядро ОС предоставляет. Так что винить 1С уж точно нельзя. Скорее тут констация находки и вариант оптимального использования приведены.


  1. danolivo
    12.10.2026 18:17

    Цифры как-то подозрительно не бьются с предыдущей публикацией. Такое различие, а из нового существенного заявлен только вынос временных таблиц из системного каталога.

    Понятно, что другие запросы, размеры БД и тд. Но хотелось бы видеть прогресс - путь, пройденный компанией. Может быть как-то принять один и тот же бенчмарк, пусть и несовершенный, но повторяемый - положить его в репу 1С и сделать общедоступным?

    Ну и да - когда речь идет о СУБД, улучшение теста через специальное железо не выглядит критически важным. В конце-концов, софтверная компания производит софт, а железо - сфера ответственности и гордости компаний из совсем другой отрасли, нет? Поэтому стоило бы подробно описать железку, и где ее можно купить на рынке.


    1. mizhka
      12.10.2026 18:17

      Вариативность пользовательских нагрузок велика, и иметь один и тот же бенчмарк для всего - это явно неверная цель. Бенчмарки как и прогресс идут вперед, и это же отлично. Есть цель, есть возможность сделать мир лучше, нет препятствий.

      Про железо - а оно тоже самое что и было в предыдущей публикации, там есть все спеки


      1. danolivo
        12.10.2026 18:17

        Как мы все знаем, любой бенчмарк измеряет в попугаях, которые особо ни о чем не говорят. Единственное, что они таки позволяют понять - это сравнить старый и новый релиз, сравнить два разных продукта, иметь повторяемость (основа научного подхода, между прочим!). Просто назвать цифру без привязки хоть к чему-то - это как сделать здание без фундамента.

        Например, что мешало взять недостающие mchar/fulleq/fastrunc и прогнать на текущем ванильном мастере все ровно то же - так мы хотя бы смогли почувствовать, какой профит дает использование энтерпрайзного постгреса от вендоров.

        Ну и выкладывать нужно дамп нагрузки - как минимум, чтобы твой же клиент смог настроив свой инстанс прогнать и проверить, все ли он настроил корректно или что-то упустил.

        А если бы прогон был не на специфическом железе, а на каком-то стандартизованном GCP 96-cpu, то это точно добавило бы тексту практическую ценность.

        И вроде ж написано достаточно открыто - даже баги релизной версии не стали скрывать. Так что бы не позволить сообществу получить живые цифры, которые дали бы почувствовать реальный технический уровень энтерпрайза и отставание ваниллы?


        1. mizhka
          12.10.2026 18:17

          Давай дождёмся второй и третьей части статьи :) Что-то из этих вопросов там будет объяснено.


          1. danolivo
            12.10.2026 18:17

            Ok, будет здорово. А то без привязки к чему-либо и возможности воспроизвести, читается как голимый маркетинг, к тому же и агрессивный - уж больно цифра близка к 1. Насколько я помню формулу - там вроде больше 1 и нельзя было.


            1. mizhka
              12.10.2026 18:17

              Верно, больше 1 и не может быть. Да и суть не в том, что APDEX близок к 1, это скорее необходимый критерий, а суть в объеме нагрузок и болях нагрузочных исследований.


              1. danolivo
                12.10.2026 18:17

                Ну, в постановке задачи ничего такого не было написано, поэтому я прочитал как обычный пользователь: помятуя майскую цифру в 0.85 или около того сделал очевидный вывод, что текущая версия почти что идеальна для 1С - дальше любые улучшения уже все попадут в 1-2%. А поскольку в тексте заявлено как-то мало фичей, для такого скачка, то оно-то и триггернуло ;).