Введение
Оптимизация высоконагруженных систем на 1С нередко сводится к долгим и увлекательным поискам скрытых причин. В данном случае мы, будучи уверенными, что «проблема в железе», внезапно обнаружили совершенно иное. При внедрении 1С:ERP на одном из предприятий выяснилось, что расчёт себестоимости на рабочем сервере занимает 17 часов, а на тестовом — 10 часов (причём тестовый сервер слабее по характеристикам). Логично было предположить аппаратные проблемы, однако реальность, как водится, оказалась хитрее.
Предпосылки и гипотезы
Данные о серверах
-
Рабочий сервер: Xeon Platinum 8358 (16 ядер), 200 ГБ памяти.
-
Тестовый сервер: Xeon Gold 6148 (12 ядер), 160 ГБ памяти.
Важно отметить:
-
Технология Hyper-Threading была отключена на обоих серверах.
-
Функция Turbo Boost также была деактивирована.
-
Энергосберегающие режимы были отключены.
-
Программное обеспечение, включая операционную систему, установленные библиотеки, обновления, СУБД и платформу 1С, было абсолютно идентичным на обоих серверах.
-
Сервер СУБД и кластер 1С размещались на одной и той же машине.
-
Оба сервера были виртуальными, что накладывало свои ограничения на диагностику.
Из-за ограниченного доступа к оборудованию и политики безопасности предприятия, мы не могли получить исчерпывающую информацию о параметрах гипервизора и схемах распределения ресурсов. Установка стороннего ПО для диагностики была также исключена.
Анализ загрузки оборудования показал, что ресурсных ограничений нет: CPU и ОЗУ не были перегружены; дисковая подсистема тоже не имела проблем. Причём на тестовом сервере нагрузка ЦП даже выше, так как там постоянно работают разработчики. Но расчёт себестоимости всё равно идёт быстрее.
В протоколе расчёта себестоимости мы увидели, что чтение данных (обращение к БД) происходит быстрее на рабочем сервере, а вот все операции решения СЛАУ (систем линейных алгебраических уравнений) — в 1,5 раза медленнее.
Манипуляции с различными параметрами расчета СЛАУ не привели к изменению ситуации.
Первичные гипотезы:
-
Проблемы с производительностью процессора. Возможные причины:
-
Особенности виртуализации: возможно, виртуальная машина на рабочем сервере неэффективно использует ресурсы CPU.
-
Ограничения или тонкие настройки гипервизора, неправильная настройка пула ресурсов.
-
Неоптимальная работа процессорного кэша (хотя на процессоре рабочего сервера он больше).
-
-
Проблемы с производительностью памяти.
-
Низкая пропускная способность памяти.
-
Некорректная работа с NUMA-нодами, когда потоки обращаются к "не своей" памяти, увеличивая задержки.
-
Особенности кэширования данных.
-
Мы старались либо подтвердить аппаратный характер проблемы, либо опровергнуть эту версию и найти скрытый изъян в ПО.
Исследование
Поскольку версия о неэффективной работе процессора была приоритетной, мы разработали многопоточную обработку для 1С, которая в каждом потоке выполняла множество математических операций (умножение, деление, возведение в степень). Предполагалось, что если мы увидим, как рабочий сервер выполняет эти операции медленнее при 12 потоках (максимум ядер на тестовом сервере), то подтвердим гипотезу и сможем передать задачу системным администраторам.
Однако результаты оказались неожиданными: время старта каждого фонового задания на рабочем сервере значительно варьировалось. Если на тестовом сервере запуск фонового задания занимал не более 1 секунды, то на рабочем это время колебалось от 1 до 15 секунд. Это заставило нас вспомнить жалобы пользователей, отмечавших, что отражение документа в регламентированном учете иногда занимает долгое время.
Анализ технологического журнала (ТЖ) выявил интересную особенность: фоновое задание стартовало быстро (событие отражалось в ТЖ), но возврат из серверного вызова (CALL), который инициировал поток, имел длительность, совпадающую с нашими замерами времени старта, и между ними не было других связанных событий. В ТЖ были включены CALL, SCALL, CONN, SESSN, SDBL. На этом этапе мы решили временно приостановить анализ ТЖ и сосредоточиться на подтверждении гипотезы о низкой производительности процессора.
Для этого мы написали скрипт на Python, который, по сути, выполнял аналогичные действия: создавал заданное количество потоков и проводил множество математических операций. И, о чудо! Все запускалось без задержек, а рабочий сервер выполнял операции на 10-15% быстрее. Но мы решили, что эти тесты не вполне показательны из-за ограничений GIL (Global Interpreter Lock) в Python. Поэтому мы модифицировали скрипт для запуска не потоков, а отдельных процессов, обеспечивая реальную параллельную работу. Результат остался неизменным — рабочий сервер оказался быстрее. Быстро подтвердить гипотезу о неэффективной работе процессора не удалось.
Тогда появилось предположение, что проблема связана с созданием потоков внутри процесса. Для его проверки мы разработали утилиту на C++, функционально аналогичную нашей обработке на 1С — запускающую заданное количество потоков и выполняющую множество математических операций. Результат: потоки создавались мгновенно, и рабочий сервер опережал тестовый на 10-15%.
Это привело нас к выводу, что с процессором всё в порядке, планировщик процессов и потоков функционирует корректно. Следовательно, оставалась гипотеза о проблемах с памятью. Вероятность проблемы в самой платформе мы исключили, учитывая идентичность версий на обоих серверах.
Для проверки этого предположения мы разработали новую утилиту на C++ со следующими режимами тестирования:
-
Оценка поведения кэш-памяти ЦПУ при различных объемах данных
-
Тест пропускной способности памяти
Тестирование работы с памятью во всех режимах показало, что рабочий сервер выполняет операции быстрее на 10-15%.
Это поставило нас в тупик. Неужели проблема не связана с памятью? И что делать дальше? Казалось бы, проще всего передать задачу специалистам по инфраструктуре, чтобы они разбирались с аппаратным обеспечением, виртуализацией и прочими компонентами... Может, просто переустановить платформу? Или вернуться к анализу ТЖ и разобраться в причинах медленного старта фоновых заданий? Но ведь версии ПО ОДИНАКОВЫЕ!
В этот момент мы решили копнуть немнго глубже.
Углубляемся
Первым делом мы сохранили веса и свободные члены самого большого уравнения. Получилось примерно 15 млн коэффициентов. Не будем углубляться в детали методов решения уравнений и их реализации в платформе 1С, хотя для нашего исследования пришлось изучить всю доступную информацию по этому вопросу, чтобы предположить как работают внутренние механизмы работы платформы.
Мы написали небольшую обработку, принимающую на вход файл с коэффициентами и запускающую расчет СЛАУ методами платформы. Это позволило проводить исследования на локальном компьютере.
С помощью Process Explorer мы изучили стек активных потоков в момент расчета СЛАУ (их было 2, так как было 2 независимых уравнения) в надежде определить, на какую операцию уходит основное время выполнения. И здесь обнаружилась ключевая зацепка: потоки практически все время находились на вызове метода memcpy+0xee из vcruntime140.dll. Дизассемблированный код по этому смещению выглядел следующим образом:
memcpy_repmovs proc near
push rdi
push rsi
mov rdi, rcx
mov rsi, rdx
mov rcx, r8
rep movsb
pop rsi
pop rdi
retn
memcpy_repmovs endp
Получается, основное время расчета СЛАУ тратилось на перемещение блоков памяти, а конкретно — на выполнение инструкции rep movsb. Интересно, зачем?
Подключившись отладчиком, мы обнаружили, что memcpy_repmovs вызывается из стандартной функции CRT memmove. Причем сама реализация memmove использует различные способы для перемещения блоков памяти в зависимости от их размера.
Мы дополнили нашу утилиту тестирования памяти тестом memmove с различными параметрами и выполнили тестирование на серверах с параметрами, полученными при отладке. Результаты оказались примерно одинаковыми. (Полагаю, внимательный читатель уже догадался, что можно предпринять для исправления ситуации).
Вернувшись к отладке, мы разобрались в происходящем.
При расчете СЛАУ происходило следующее (опишем только процессы, связанные с вызовом memmove, и для упрощения будем избегать специализированной терминологии ассемблера):
-
Инициализировалось первоначальное значение размера перемещаемого блока памяти (регистр r8), равное количеству наших коэффициентов — примерно 15 млн, т.е. работа велась с коэффициентами, расположенными в памяти.
-
Далее постепенно перемещался блок памяти "назад", со сдвигом на 4 байта за раз, с уменьшением размера перемещаемого блока на 8 байт (r8). Это напоминало попытку освободить хвост блока, постепенно перенося его вперёд.
Если наши коэффициенты занимали примерно 60 МБ памяти, и на каждом шаге размер перемещаемого блока уменьшался на 8 байт, то общее количество шагов составляло около 1,8 миллиона (при обходе до конца). Это означает, что при расчете СЛАУ с 15 млн коэффициентов приходилось перемещать порядка 13 ТБ данных в каждом из независимых потоках (проверьте расчеты, возможно, я ошибся).
Насколько это эффективно?
Скрытый текст
Ответ очевиден — крайне неэффективно.
Продолжая отладку и анализ дизассемблированного кода библиотеки, мы обнаружили, что memmove использует различные способы перемещения блоков памяти в зависимости от их размера. Нас заинтересовал следующий фрагмент кода:
cmp r8, 80h
jbe XmmCopySmall
bt cs:__favor, 1
jnb XmmCopyUp
jmp short memcpy_repmovs
Особое внимание привлекла инструкция bt cs:__favor, 1. В нашем случае __favor (глобальная переменная CRT) имела значение 2, что направляло выполнение в memcpy_repmovs, хотя существовал вариант использования SSE-инструкций.
Мы решили попробовать в отладчике перед началом алгоритма обнулить __favor. Хотя уже было ясно, что нужно исследовать другую версию vcruntime140.dll, мы все же провели этот эксперимент.
Параллельно мы взяли vcruntime140.dll не из поставки 1С, а из свежей версии Microsoft Visual C++ Redistributable, установленной в c:windowssystem32.
Дизассемблирование этой версии показало, что для нашего размера блока памяти используется иная логика:
MoveAbove32_3: ; Сюда прыгает, если (r8 > 0x20)
lea r9, [rdx+r8]
cmp rcx, rdx
cmovbe r9, rcx
cmp rcx, r9
jb CopyDown ;
cmp cs:__isa_available, 3
jb NoAVX ; Если нет AVX, переходим к SSE
cmp r8, 2000h ; 0x2000 = 8192 байт
jbe short MoveWithYMM_1 ; Если размер <= 8 КБ → AVX
cmp r8, 180000h ; 0x180000 = ~1.5 МБ
ja short MoveWithYMM_1 ; Если размер > 1.5 МБ → тоже AVX
test byte ptr cs:__favor, 2
jnz memcpy_repmovs ; Если установлен бит __favor, идём в repmovs
MoveWithYMM_1:
В этой версии присутствовала поддержка AVX-инструкций. Примечательно, что если процессор поддерживает AVX, то для нашего размера блока всегда будет выполняться перемещение с использованием AVX-инструкций вне зависимости от значения __favor.
Результаты тестов расчета СЛАУ с 15 млн коэффициенто на локальном компьютере:
-
vcruntime140.dll (14.16.27033.0) из состава платформы 8.3.23.1912: 1 час 40 минут
-
vcruntime140.dll (14.40.33810.0) из поставки Visual C++ Redistributable: 30 минут
После замены версии библиотеки полный расчет себестоимости сократился с 17 до 9 часов.
Также после обновления библиотеки мы повторно запустили многопоточную обработку, созданную в начале исследования, и обнаружили, что все запуски фоновых заданий занимали менее одной секунды.
Вопрос лишь в том, как на тестовом сервере версия библиотеки vcruntime140.dll для того же релиза платформы оказалась иной. На моей локальной машине для этой платформы версия библиотеки совпадала с версией тестового сервера (14.16.27033.0).
Выводы
-
Неэффективное управление памятью в динамических структурах платформы 1С существенно замедляет скорость работы. Проведенные тесты с объектом ТекстовыйДокумент показали, что при добавлении строк в цикле наблюдаются аналогичные вызовы memmove с несколько иной логикой, но с таким же эффектом — постоянное перемещение блоков памяти, приводящее к значительным замедлениям.
-
Современные инструкции процессора (AVX, SSE) могут значительно ускорить операции с памятью. В нашем случае использование AVX-инструкций вместо базовой реализации movsb дало 6-кратное ускорение для конкретной операции.
-
Даже при идентичных версиях платформы 1С компоненты среды исполнения могут различаться, что приводит к непредсказуемым различиям в производительности. Необходимо контролировать не только версию платформы, но и версии всех используемых библиотек.
-
Текущий механизм расчета СЛАУ в платформе 1С имеет значительный потенциал для оптимизации. Если эта оптимизация будет реализована на глобальном уровне, то подобные улучшения затронут и другие участки работы платформы, связанные с управлением памятью.
-
При диагностике проблем производительности не следует ограничиваться анализом только основных компонентов системы. Иногда причина кроется в неочевидных местах, таких как версии вспомогательных библиотек или специфика их взаимодействия с аппаратным обеспечением.
-
Использование специализированных инструментов для анализа работы приложений (Process Explorer, отладчики) может предоставить критически важную информацию, недоступную при стандартном мониторинге системы.
Автор: x_v_i_i