Как стать автором
Поиск
Написать публикацию
Обновить
94.34
Делаем средства разработки бизнес-приложений

Как мы успешно прошли тест на 30 000 одновременных пользователей в 1C:ERP (и что мы подкрутили в PostgreSQL)

Уровень сложностиСредний
Время на прочтение19 мин
Количество просмотров7.1K

Уже давно стали обыденными внедрения решений на платформе 1С:Предприятие на тысячу одновременных пользователей. Есть внедрения и более масштабные. И масштаб внедрений растёт. Поэтому мы решили убедиться, что платформа выдержит нагрузку нашего самого востребованного на крупных внедрениях решения 1C:ERP на 30 000 одновременно работающих пользователях.

Почему именно 30 000 пользователей, как мы измеряли производительность и как добились желаемой производительности – под катом.

Почему именно 30 000 пользователей?

Согласно требованиям крупнейших компаний России, представленных Национальным центром компетенций по информационным системам управления холдингом (АНО «НЦК ИСУ»), 30 000 пользователей соответствует размерности самых крупных российских организаций с сотнями тысяч сотрудников (далеко не все сотрудники целый день работают в ERP).

Нам нужно было убедиться, что в таких сценариях платформа 1С:Предприятие работает надёжно, качественно и производительно.

А что значит производительно?

Как мы измеряем производительность

В экосистеме 1С:Предприятия сложившийся стандарт оценки производительности — это Apdex (акроним Application Performance inDEX).

Как вычисляется Apdex.

В качестве опорной точки в Apdex выступает целевое (желаемое) время выполнения ключевых операций (это время назначается экспертным путём). Мы определили целевые времена для всех ключевых операций, участвующих в сценарии теста.

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

Если время укладывается в диапазон от целевого до четырёх целевых времён – то это «зона толерантности», где пользователю работать не очень комфортно, но он ещё готов терпеть.

А если мы не уложились в четыре целевых времени, то с точки зрения пользователя программа зависла и это неудовлетворительное время.

Индекс производительности Apdex вычисляется так:

  • Т — целевое время выполнения операции

  • N — общее количество выполнений операции

  • NS — количество выполнений, попавших в диапазон [0, T)

  • NT — количество выполнений, попавших в диапазон [Т, 4*Т)

  • Apdex = (NS + NT/2) / N

Индекс Apdex принимает значения от 0 до 1 и его значение истолковывается следующим образом:

Значение Apdex

Производительность

0,94 — 1,00

Отличная

0,85 — 0,93

Хорошая

0,70 — 0,84

Удовлетворительная

0,50 — 0,69

Плохая

0,00 — 0,49

Неприемлемая

На практике достаточно значения Apdex = 0,85 или выше — это устраивает подавляющее число заказчиков. Это означает, что у нас больше половины всех операций уложилось в целевое время и некоторая часть операций попала в зону толерантности.

Приведём пример. Предположим, что ровно половина операций попала в целевое время (оценка будет 0,5), а вторая половина операций уложилась в диапазон от целевого времени до целевого времени умножить на 4. Соответственно, эта часть операций внесёт вклад 0,25 и значение Apdex получится 0,75. Этого мало — нам требуется, чтобы большее количество операций было в диапазоне до целевого времени и допустимо, чтобы какая-то часть из него выбилась. И крайне нежелательно, чтобы какие-то операции не укладывались в четыре целевых времени.

Тестовая база данных

Поскольку целевая аудитория теста — это крупнейшие организации, были собраны требования крупных компаний по размеру и составу базы данных. Далее требования были усреднены до средней крупной компании со смещением в сторону более крупных. Получилась база размером около 1 Терабайта.

Характеристики базы
  • 28 000 000 договоров

  • 22 000 000 движений себестоимости

  • 20 000 000 бух. проводок

  • 2 500 000 контрагентов

  • 1 000 000 основных средств

  • 700 000 сотрудников

  • 25 000 пользователей

  • 1 800 подразделений

  • 56 организаций (с единой головной организацией)

И по документам в базе:

Документ

Количество

Регистратор расчётов

1 622 037

Заявка на расходование денежных средств

1 067 654

Списание безналичных денежных средств

1 067 386

Счет-фактура полученный

1 027 831

Заказ поставщику

778 551

Приобретение товаров и услуг

777 796

Заказ клиента

723 716

Счет-фактура выданный

567 056

Поступление безналичных денежных средств

537 417

Расходный ордер на товары

339 371

Реализация товаров и услуг

334 494

Приходный ордер на товары

285 622

Приобретение услуг и прочих активов

206 605

Внутреннее потребление

157 894

Прочее оприходование товаров

137 313

Реализация услуг и прочих активов

136 486

Принятие к учёту основных средств

129 407

ВСЕГО ДОКУМЕНТОВ

9 896 636

Тестовая инфраструктура

Архитектурно платформа 1С:Предприятие — это трехзвенная система, сердце которой — кластер серверов, где выполняется основная часть бизнес-логики.

Для работы с терабайтной базой мы смонтировали кластер из шести серверов: четыре рабочих сервера, один центральный и один — сервер лицензирования.

Спецификация серверов кластера:

Intel(R) Xeon(R) Gold 6426Y 2500 MHz L1/L2/L3 1280/32768/38400 KB
16 cores/32 threads, ОЗУ 256 Гб, 1 SAS SSD 1920 ГБ Samsung PM1643a

Для эмуляции пользовательской нагрузки мы использовали 32 сервера-нагрузчика, на каждом стояло по шесть виртуальных машин, для теста запускалось по 160 пользовательских сеансов на виртуальную машину.

Спецификация серверов-нагрузчиков:

Intel(R) Xeon(R) Gold 6226R 2900 MHz L1/L2/L3 1024/16384/22528 KB 16 cores/32 threads,
ОЗУ 256 Гб, 1 SAS SSD 1920 ГБ Samsung PM1643a

Статистика по потреблению памяти

Платформа активно использует сеансовые данные, которые представляет собой файлы, хранящиеся на диске и скопированные в оперативную память. Они могут не поместиться в память и считываться с диска, и тогда производительность системы может «просесть». Желательно, чтобы памяти под сеансовые данные хватало. Для этого нам в тестах требуется не менее 40 ГБ оперативной памяти.

Потребление памяти на серверах:

  • Управляющий сервис кластера ragent — 0,5 Гб

  • Сервис сеансовых данных — 3,5 Гб

  • Сервис журнала регистрации — 1,5 Гб

  • Главный менеджер кластера (сервис конфигурации кластера, сервис состояния кластера, сервис управления предметами отладки) — 1,5 Гб

  • Сервис получения списка сеансов — 0,5 Гб

  • Сервис заданий — 0,5 Гб

  • Сервис уведомлений клиента — 0,5 Гб

  • Остальные сервисы: < 0,3 Гб

  • Размер сеансовых данных — 25 Гб

Для СУБД PostgreSQL мы использовали сервер с 96-ядерным процессором и памятью 1 ТБ. Всё, что было возможно (данные, индексы, WAL, временные файлы), мы распределили по разным дискам, чтобы максимально распределить нагрузку.

Про сервер СУБД

ЦПУ: Ampere Altra Max M96-28, 2800 MHz 96 cores
Память: 1 Тб Samsung DDR4 64 Гб 3200 MT/s
Накопитель: SSD 7.7 Tb NVMe Samsung PM1733a

SAMSUNG MZWLR7T6HBLA	7T		/mnt/pg_data	Каталог кластера postgres
SAMSUNG MZWLR7T6HBLA	7T		/mnt/pg_tables	Табличное пространство v81c_data (данные)
SAMSUNG MZWLR7T6HBLA	7T		/mnt/pg_index	Табличное пространство v81c_index (индексы)
SAMSUNG MZWLR7T6HBLA	7T		/mnt/pg_wal		Журналы транзакций
SAMSUNG MZQL2960HCJR	894,3G	/mnt/pg_log		Логи postgres
SAMSUNG MZWLR7T6HBLA	7T		/mnt/pg_temp1	Табличное пространство временных таблиц
SAMSUNG MZWLR3T8HCLS	3,5T	/mnt/pg_temp2	Табличное пространство временных таблиц
SAMSUNG MZWLR3T8HCLS	3,5T	/mnt/pg_temp9	Табличное пространство временных таблиц
SAMSUNG MZQL2960HCJR	894,3G	[SWAP]

Параметры PostgreSQL:

temp_tablespaces = 'tmp1,tmp2,tmp9'

ssl = off

shared_buffers = 256GB
temp_buffers = 256MB
work_mem = 256MB
maintenance_work_mem = 1024MB
max_files_per_process = 8000

lc_messages = 'C'

client_min_messages = warning

logging_collector = on
log_directory = '/mnt/pg_log/files'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_rotation_size = 1GB
log_temp_files = 4kB
log_checkpoints = on
log_connections = on
log_disconnections = on
log_lock_waits = on
log_timezone = 'Europe/Moscow'

wal_level = minimal
max_wal_senders = 0
synchronous_commit = off

max_locks_per_transaction = 1000
max_connections = 9000

max_parallel_workers_per_gather = 0
max_worker_processes = 2
max_parallel_workers = 1
max_logical_replication_workers = 0

listen_addresses = '*'
port = 5432

bgwriter_delay = 20ms
bgwriter_lru_maxpages = 500
bgwriter_lru_multiplier = 4.0

commit_delay = 1000
commit_siblings = 5

checkpoint_timeout = 20min
max_wal_size = 12GB
min_wal_size = 1GB
checkpoint_completion_target = 0.9

random_page_cost = 1.1
effective_cache_size = 512GB

from_collapse_limit = 20
join_collapse_limit = 20

autovacuum_max_workers = 80
autovacuum_naptime = 20s
autovacuum_vacuum_cost_delay = 10ms
autovacuum_vacuum_cost_limit = -1
autovacuum_vacuum_scale_factor = 0.01
autovacuum_vacuum_cost_limit = 0.005

jit = off
track_activity_query_size = 128 # Max 1048576
enable_temp_memory_catalog = on # ! PostgreSQL-1C

На всех серверах была установлена ОС Linux, СУБД PostgreSQL (от 1С) версии 16.4-49.1C.

Что мы оптимизировали в PostgreSQL

Как было сказано выше, в качестве СУБД в наших тестах мы использовали PostgreSQL от 1С. Кстати, тут — полный список СУБД, с которыми мы работаем.

Мы использовали версию PostgreSQL от 1С, потому что в ней мы можем поправить те узкие места, когда производительность системы начинает напрямую зависеть от сервера СУБД.

Про PostgreSQL от 1С

Особенности PostgreSQL от 1С:

  • Добавлены новые типы данных mchar и mvarchar

    • Точно воспроизводят поведение типов CHAR и VARCHAR у MS SQL Server

  • Расширения:

    • fasttruncate

    • fulleq

    • dbcopies_decoding

    • online_analyze

    • plantuner

  • Множество оптимизаций под сценарии, типичные для 1С

В ходе тестов был выявлен ряд «бутылочных горлышек» производительности PostgreSQL, которые нам удалось расширить.

Часть наших оптимизаций связана с тем, что «ванильный» PostgreSQL одинаково работает со всеми таблицами — и с постоянно существующими в БД, и с временными. В некоторых ситуациях эта унифицированность избыточна.

Оптимизация отправки сообщений об инвалидации

В PostgreSQL для каждого соединения с БД (сессии) создается процесс (бэкенд), так что эти термины в известной степени можно считать взаимозаменяемыми.

В PostgreSQL есть несколько рабочих областей памяти. Есть области памяти, зарезервированные индивидуально под каждый бэкенд — они доступны только бэкенду, для которого выделены. И есть области памяти, которые доступны для всех бэкендов одновременно.

Этот довольно сложный механизм необходим для того, чтобы разные бэкенды могли одновременно работать с одними и теми же таблицами. Например, один бэкенд может выбирать данные из таблицы, а другой её обновлять. Чтобы у каждого бэкенда было своё «видение» этой таблицы, PostgreSQL использует этот механизм.

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

Другие бэкенды смотрят в эту очередь — а не поменялось ли там что‑то в интересующих их таблицах. Если что‑то поменялось — бэкенды инвалидируют свои данные, то есть выбрасывают невалидные данные из своей личной памяти и могут обратиться в общую память и взять оттуда свежие обновлённые порции данных. Как раз эта очередь служит механизмом синхронизации между различными бэкендами.

Временные таблицы, создаваемые в ходе работы, уникальны тем, что доступны только из той сессии, в которой были созданы. При этом в «ванильном» PostgreSQL при изменении данных временных таблиц отправляется стандартное сообщение об изменении данных в очередь сообщений.

Эти сообщения благополучно доставлялись другим бэкендам; но так как у других бэкендов всё равно нет возможности получить доступ к данным «чужих» временных таблиц, то им незачем обрабатывать эти сообщения, да и отправлять эти сообщения не имеет смысла. Доработка заключалась в «вырезании» отправки сообщений, касающихся изменения данных временных таблиц.

Нет «лишних» сообщений — нет лишней нагрузки, нет обработки этих сообщений, нет расхода ресурсов и процессорного времени. Небольшая, но очень эффективная доработка.

Оптимизация блокировок временных таблиц

Следующая оптимизация также связана с временными таблицами, и она касается блокировок временных таблиц.

Блокировки таблиц — это механизм многоуровневых рекурсивных блокировок, который нужен, чтобы разграничить одновременно выполняемые из разных бэкендов действия над таблицами. Например, когда несколько сессий читают данные из одной и той же таблицы, то они могут делать это одновременно. Но если какая-то сессия хочет обновить таблицу, то другая сессия, которая хочет прочесть данные из этой таблицы, должна подождать конца обновления с учётом MVCC (multiversion concurrency control).

В «ванильном» PostgreSQL блокировки используются и для временных таблиц. Но поскольку доступ к временным таблицам возможен только из той сессии, в которой эти временные таблицы созданы — смысла в блокировках нет. Поэтому в нашей версии мы отключили блокировки временных таблиц и тем самым убрали ненужные нагрузку и потребление ресурсов.

Оптимизация блокировки SpinLock

Следующая доработка более техническая. Она заключается в том, что на определённых архитектурах (особенно это касается ARM-архитектуры с большим количеством ядер, особенно если ядер больше сотни) в коде было «горячее» место, сильно нагружающее процессор. Мы это место обнаружили и поправили. Проблема была в коде для синхронизации последовательного доступа к памяти использовалась блокировка spinlock.

Цитируем статью от Postgres Professional:

«Спин-блокировки или спинлоки (spinlock) предназначены для захвата на очень короткое время (несколько инструкций процессора) и защищают отдельные участки памяти от одновременного изменения.

Спин-блокировки реализуются на основе атомарных инструкций процессора, таких, как compare-and-swap. Если блокировка занята, ожидающий процесс выполняет активное ожидание — команда повторяется («крутится» в цикле, отсюда и название) до тех пор, пока не выполнится успешно. Это имеет смысл, поскольку спин-блокировки применяются в тех случаях, когда вероятность конфликта оценивается как очень низкая.»

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

Порядок выполнения операций важен для корректного функционирования упомянутой выше очереди сообщений.

Сначала сообщение помещается в буфер очереди, а потом обновляется информация о количестве сообщений в этом буфере. Этот порядок операций был очень важен, потому что если мы сначала обновим количество сообщений, а потом поместим сообщение в буфер, то между этими двумя операциями мог вклиниться какой-то поток (другой бэкенд) и, сначала увидев увеличившееся количество сообщений, начинал пытаться его читать. А по факту сообщения в очереди ещё нет, и это могло привести к неприятным ситуациям. Чтобы упорядочить эти действия использовался spinlock. Блокировка spinlock не позволяет нескольким потокам выполнить один и тот же код. Spinlock использовался как обёртка вокруг счётчика сообщений для того, чтобы упорядочить доступ к счётчику сообщений для того, чтобы потоки не конфликтовали между собой.

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

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

Оптимизация операции ANALYZE для «широких» таблиц

Следующая доработка заключается в оптимизации операции ANALYZE для случаев, когда эта операция выполняется над «широкой» таблицей, в которой много столбцов и в которой присутствуют столбцы с переменной длиной.

Команда ANALYZE TABLE собирает статистику о содержимом таблицы. Эта статистика используется для определения наиболее эффективных планов выполнения запросов. Команда анализирует таблицы и индексы, собирает данные о распределении данных (например, количество уникальных значений в столбце, самые распространённые значения и т.п.) и сохраняет их в специальных системных таблицах.

ANALYZE работал по следующему алгоритму. Сначала выбирался некоторый ограниченный набор данных из таблицы. Далее для каждого столбца из этого набора данных по очереди считается статистика по содержимому данных в этом столбце.

Данные столбца записаны в память последовательно, первый столбец находится в начале выделенной для записи таблицы области памяти. Для второго столбца мы знаем, что длина первого столбца, например, 4 байта (для типа INT) — значит, второй столбец лежит в четырёх байтах от начала области памяти.

С появлением столбцов переменной длины (varchar, mchar, text и т. п.) чтобы узнать, где находится следующий столбец за столбцом с переменной длиной, нам надо узнать реальную длину данных, находящихся в столбце этой записи таблицы, и приплюсовать к предыдущему значению смещения. Если таких столбцов много, то, чтобы получить, например, доступ к данным 201-го столбца, нам нужно пройти по всем предыдущим двумстам столбцам, узнать длину данных в них, просуммировать, и только после этого мы попадём на 201-й столбец. И так нужно сделать для всей таблицы (или набора данных из неё). И для каждой следующей строчки операцию придётся выполнить заново. В результате операция ANALYZE занимала существенное время.

Чтобы ускорить операцию, мы поменяли алгоритм.

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

Теперь при выполнении команды ANALYZE мы не подсчитываем длину данных в каждом столбце для каждой анализируемой строки, а используем заранее рассчитанный указатель из таблицы указателей.

Благодаря таблице указателей можно быстро найти положение нужного столбца в памяти, не вычисляя длину предыдущих столбцов. Это позволяет алгоритму команде ANALYZE работать иногда в десятки раз быстрее.

Оптимизация типовых выражений LIKE для типа данных mchar

Эта оптимизация касается регулярных выражений для типа данных mchar. Регулярные выражения — это маски или определённые шаблоны, содержащие текстовые формулы, и искомый текст должен соответствовать этим формулам. Поиск по регулярным выражениям — достаточно сложная и ресурсоёмкая операция, потому что в общем случае это проверка строк с помощью формального языка.

Платформа 1С:Предприятие в ходе работы часто использует достаточно простую операцию текстового поиска внутри строки (s LIKE ‘%содержание%’), т.е. искомая строка должна содержать некоторый текст (подстроку). Эта операция значительно проще по вычислительным затратам, чем полноценный поиск на соответствие регулярному выражению.

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

Оптимизация функции fsync при использовании временных таблиц

Следующая оптимизация — это оптимизация использования функции fsync при использовании временных таблиц. fsync — это специальная операция ОС, которая позволяет принудительно записать данные из кэша файловой системы на диск. Когда мы программно пишем данные в файл — на самом деле сначала записываемые данные попадают в кэш файловой системы в ядре операционной системы, после чего в какой‑то момент операционная система данные из кэша записывает непосредственно на диск.

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

Наша оптимизация касается временных таблиц. Временные таблицы помимо того, что они живут только в одной сессии, они ещё и временные: они уничтожаются при завершении сессии, их данные никуда не записываются и нигде не сохраняются. То есть потеря данных временных таблиц не несёт никакой угрозы целостности базы данных.

Оптимизация заключается в том, что для тех транзакций, в которых не было записей в постоянные таблицы (т.е. были только записи во временные таблицы), не делается вызов fsync. Таким образом, мы экономим время на дисковых операциях и на вызовах процессора.

Оптимизация хранения информации о временных таблицах

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

Информация о таблицах в PostgreSQL хранится в системном каталоге PostgreSQL. Системный каталог — это также набор таблиц; есть таблица, содержащая информацию о таблицах в базе, есть таблица, содержащая информацию о столбцах таблиц, есть таблица для хранения информации о ключах таблиц, таблица, хранящая информацию об индексах и т. д. Все эти системные таблицы находятся в схеме pg_catalog.

В PostgreSQL вся информация о временных таблицах также сохраняется в системных таблицах.

Как вы уже наверное поняли, платформа 1С:Предприятие очень «любит» временные таблицы, активно создаёт и удаляет их в ходе работы. И каждое такое создание, и удаление временной таблицы сопровождается записью и изменением каталога системных таблиц. Системные же таблицы не временные, они общие для всех сессий, синхронизируется по всем правилам механизмами PostgreSQL. Поэтому операция создания и удаления таблицы (неважно, временной или нет) — достаточно сложная, «тяжёлая», и в определённые моменты нагрузка на сервер может стать существенной.

Наша оптимизация реализует «теневой» системный каталог, который размещён в оперативной памяти. Этот теневой системный каталог по структуре дублирует стандартный системный каталог. При этом если в системный каталог помещается запись, относящаяся к временной таблице, то эта запись перенаправляется из системного каталога в эту новую структуру — «теневой» системный каталог. А при чтении из системного каталога информация из этой новой структуры (хранящей информацию о временных таблицах) добавляется к результатам чтения.

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

Это достаточно агрессивная оптимизация; она работает только при установленном у сервера СУБД специальном новом параметре enable_temp_memory_catalog (чтобы был простой способ её отключить и работать по стандартному алгоритму). Если при включённом параметре мы сделаем SELECT * из системного каталога, то мы не увидим там временных таблиц.

Благодаря этой оптимизации на некоторых операциях мы достигли ускорения в десятки, а иногда и в сотни раз.

Что мы оптимизировали в платформе 1С:Предприятие

Ряд оптимизаций для ускорения работы мы сделали и в платформе.

Размер пула временных таблиц

Несмотря на сделанные нами (и описанные выше) оптимизации, создание временной таблицы остается относительно ресурсоемкой операцией, особенно при большом количестве конкурирующих сеансов.

Работу с временными таблицами можно оптимизировать (помимо оптимизации исходного кода СУБД) и другими средствами.

Первая вещь, которую можно изменить — это размер пула временных таблиц.

Когда требуется временная таблица определённой структуры (выполняется команда CREATE TEMPORARY TABLE) — PostgreSQL создаёт временную таблицу, а после окончания её использования (после выполнения DROP TEMPORARY TABLE) таблица очищается и помещается в пул временных таблиц в памяти. Когда в следующий раз понадобится временная таблица с такой структурой — она будет извлечена из пула (если там есть подходящая таблица).

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

Мы провели ряд экспериментов и определили, что при нагрузках, характерных для крупных организаций, оптимальным будет размер пула около 2 000 временных таблиц на соединение.

На графиках мы видим, что после того, как мы увеличили размер пула, стало меньше операций, время выполнения которых равно «целевое время» * 4 или дольше. Зато выросло количество операций, которые уложились в целевое время. «Хвост» долгих операций (выделенный жёлтым) стал более плоским.

По оси X - интервал времени выполнения операции (в целевых временах), по оси Y - количество операций, уложившихся в интервал
По оси X - интервал времени выполнения операции (в целевых временах), по оси Y - количество операций, уложившихся в интервал

Закрытие соединений с БД

Одна из оптимизаций связана с операцией закрытия соединения с базой данных.

Ранее время жизни соединения с СУБД было фиксировано и составляло 30 минут. По истечении 30 минут соединение с СУБД закрывалось и при необходимости открывалось новое. При закрытии соединения PostgreSQL начинает очистку временных таблиц для этого соединения.

На практике это могло выглядеть так. На сервере стартует новый рабочий процесс 1С (исполняемый файл, обслуживающий клиентские приложения и взаимодействующий с сервером баз данных). В одном рабочем процессе может быть открыто много соединений с БД. Через 30 минут все соединения закрываются — и в этот момент мы видим и на графике производительности всплески, говорящие нам о замедлении выполнения ключевых операций. Сервер PostgreSQL занят — он чистит временные таблицы многих одновременно закрывающихся соединений с БД.

Чтобы избежать такой ситуации мы сделали время жизни соединения в случайном диапазоне от 30 до 60 минут. Результат на графике. Розовая линия — это время выполнения операций после этой оптимизации.

TRUNCATE пустых временных таблиц

В ходе теста мы обнаружили, что от 30% до 50% временных таблиц не содержит ни одной строки. При этом операция TRUNCATE для них всё равно выполняется при вызове её платформой. Несмотря на то, что платформа использует FAST TRUNCATE (который более оптимален, чем простой TRUNCATE), эта операция всё равно не бесплатная и хорошо видна при высоких нагрузках. А что, если не очищать таблицы, в которых нет ни одной строки?

Мы проверили это и обнаружили, что такая оптимизация неплохо улучшает производительность. Мы получили на наших тестах прирост производительности от 5% до 10%.

Как мы это сделали? Мы добавили ещё один запрос SELECT, выполняемый перед TRUNCATE. Если SELECT вернул ноль строк, то TRUNCATE не выполняется. Как показали замеры, влияние дополнительного запроса SELECT совершенно неощутимо на фоне выигрыша, который мы получили.

Было:

INSERT INTO pg_temp.tt10….
…",RowsAffected=0,Result=…
SELECT FASTTRUNCATE(‘pg_temp.tt10’);

Стало:

INSERT INTO pg_temp.tt10….
…",RowsAffected=0,Result=…
SELECT 1 FROM pg_temp.tt10 LIMIT 1;

Пакетная обработка запросов

Все вышеописанные изменения дали нам результат, очень близкий к целевому (напомним это Apdex = 0,85). Но чуть-чуть не хватало.

И тут мы вспомнили про задачу, которую делали вне связи с этим тестом – про пакетную обработку запросов.

Платформа исторически выполняет каждый запрос к СУБД по отдельными SQL командами, т.е. для каждой SQL команды платформа обращается к серверу СУБД.

Это привносит накладные расходы, при этом величина этих расходов очень сильно зависит от того, что за запрос выполняется. То есть если запрос выполняется минуту, то эти накладные расходы совершенно не ощутимы, они составляют доли процентов. Но если запрос маленький и быстрый, то round-trip до сервера СУБД и обратно занимает существенную долю времени –десятки процентов и даже больше (в зависимости от скорости сети, от того, где расположен сервер СУБД – на одном сервере с сервером 1С:Предприятия или на отдельном).

Чтобы решить эту проблему мы реализовали пакетирование некоторых запросов. PostgreSQL поддерживает пакетную обработку запросов, т.е. выполнение нескольких SQL команд (разделённых ';') за одно обращение к серверу СУБД. Использование пакетной обработки запросов позволяет сократить количество обращений платформы к серверу СУБД и, соответственно, суммарную сетевую задержку, а также общее время выполнения при обработке множества SQL команд, т.е. по итогу повысить производительность системы.

Платформа анализирует запросы к СУБД и те из них, которые могут быть выполнены в составе пакета, помещает в пакет.

Разумеется, не все запросы можно пакетировать. Но для пакетирования хорошо подходят, например, запросы, работающие с временными таблицами, когда результат запроса сам по себе нам не нужен, а нужен для использования следующим запросом. В реальных сценариях мы увидели цепочки до 100 запросов, помещённых в один пакет.

Не на всех ключевых операциях это сработало одинаково, но на некоторых операциях это дало хороший эффект. На графике распределения времени ключевых операций мы видим, что график сместился влево на 0,1 секунды, и пользователи увидели соответствующее ускорение операций.

Итого

В совокупности все эти изменения со стороны платформы 1С:Предприятие и со стороны PostgreSQL позволили нам успешно пройти тест 1С:ERP на 30 000 одновременных конкурирующих пользователей в одной базе.

Тест успешно отработал 10 часов и показал производительность Apdex = 0.858, то есть цель теста была успешно достигнута.

Цифры:

  • 30 000 одновременно активных пользователей

  • Время работы теста: 10 часов

  • Количество выполненных операций: 885 000

Кластер серверов 1С: 5 серверов по 64 ядра CPU и 256 Гб RAM.

Структура базы (с т. з. бизнеса):

  • 54 филиала, выделенных на отдельные балансы

  • 1800 подразделений

  • 3500 складов

  • 80 000 номенклатурных позиций

  • 700 000 сотрудников

  • 90 000 партнеров

  • 2 500 000 контрагентов

  • 25 000 000 договоров

  • 1 000 000 основных средств

Наша статистика показывает, что 30 000 одновременно работающих пользователей соответствует организации с 250 000 сотрудников.

Тест показал, что платформа 1С:Предприятие готова для автоматизации крупнейших предприятий и организаций России.

Теги:
Хабы:
+37
Комментарии53

Публикации

Информация

Сайт
v8.1c.ru
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия