Как мы искали причину медленного расчёта СЛАУ при расчёте себестоимости в 1С:ERP и нашли её в неожиданном месте

в 11:16, , рубрики: 1c erp, , оптимизация, производительность, себестоимость, Система линейных уравнений, СЛАУ

Введение

Оптимизация высоконагруженных систем на 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, и для упрощения будем избегать специализированной терминологии ассемблера):

  1. Инициализировалось первоначальное значение размера перемещаемого блока памяти (регистр r8), равное количеству наших коэффициентов — примерно 15 млн, т.е. работа велась с коэффициентами, расположенными в памяти.

  2. Далее постепенно перемещался блок памяти "назад", со сдвигом на 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. Неэффективное управление памятью в динамических структурах платформы 1С существенно замедляет скорость работы. Проведенные тесты с объектом ТекстовыйДокумент показали, что при добавлении строк в цикле наблюдаются аналогичные вызовы memmove с несколько иной логикой, но с таким же эффектом — постоянное перемещение блоков памяти, приводящее к значительным замедлениям.

  2. Современные инструкции процессора (AVX, SSE) могут значительно ускорить операции с памятью. В нашем случае использование AVX-инструкций вместо базовой реализации movsb дало 6-кратное ускорение для конкретной операции.

  3. Даже при идентичных версиях платформы 1С компоненты среды исполнения могут различаться, что приводит к непредсказуемым различиям в производительности. Необходимо контролировать не только версию платформы, но и версии всех используемых библиотек.

  4. Текущий механизм расчета СЛАУ в платформе 1С имеет значительный потенциал для оптимизации. Если эта оптимизация будет реализована на глобальном уровне, то подобные улучшения затронут и другие участки работы платформы, связанные с управлением памятью.

  5. При диагностике проблем производительности не следует ограничиваться анализом только основных компонентов системы. Иногда причина кроется в неочевидных местах, таких как версии вспомогательных библиотек или специфика их взаимодействия с аппаратным обеспечением.

  6. Использование специализированных инструментов для анализа работы приложений (Process Explorer, отладчики) может предоставить критически важную информацию, недоступную при стандартном мониторинге системы.

Автор: x_v_i_i

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js