Версия ЛЭРС УЧЁТ: 3.63.3
Сервер PostgreSQL
Версия не последняя, но тут Обновления системы ЛЭРС УЧЕТ ничего по данной проблеме
Equipment_SetNodeConnections (75516.91 ms) - Profiling Results.zip (472,6 КБ)
не найдено.
Описание проблемы
При сохранении дома (узла учёта) с большим количеством оборудования операция завершается с ошибкой таймаута:
Истекло время ожидания запроса. Время ожидания истекло до завершения операции или сервер не отвечает.
Шаги воспроизведения
- Открыть дом с большим количеством приборов в интерфейсе ЛЭРС
- Добавить оборудование
- Нажать «Сохранить»
- Через ~30–90 секунд — ошибка таймаута
| Параметр | Значение |
|---|---|
| Узел ID=53 | 1 173 устройства → таймаут |
| Узел ID=61 | 1 676 устройств → таймаут (75 сек) |
| Пустой узел | Работает мгновенно |
Проблема строго пропорциональна количеству устройств в узле.
Диагностика
Для выявления причины были выполнены:
- Включение Profiler через
PUT /api/v0.1/Admin/SystemParameters(allowViewProfiler: true) - Повторение действия: открытие дома и нажатие «Сохранить»
- Анализ результатов профайлера для вызова
POST /api/v0.1/Core/Equipment/Node/61/Connections(один из эндпойнтов, вызываемых при сохранении дома) — HTML-файл прилагается - Анализ схемы и статистики PostgreSQL
Результаты профайлера (узел ID=61, 1 676 устройств)
Общее время запроса: 75 517 мс (75 секунд)
| Группа запросов | Кол-во | Суммарное время | Avg |
|---|---|---|---|
SELECT FROM PollConnection WHERE EXISTS(equipmentId=N) |
1 676 | 71 570 мс | 42.7 мс |
| Начальная загрузка узла | 4 | 218 мс | — |
| Валидация подключений | 24 | 51 мс | — |
| Прочие | ~8 | 130 мс | — |
| Итого SQL | 1 712 | 71 969 мс | — |
| Итого запрос | — | 75 517 мс | — |
97% времени занимает один тип запроса, выполняемый по одному разу на каждое устройство узла.
Корень проблемы
Антипаттерн N+1 запросов
Судя по данным профайлера, при обработке запроса система итерируется по всем устройствам узла и для каждого выполняет отдельный SQL-запрос к таблице PollConnection:
-- Выполняется 1 676 раз, по одному на каждое устройство
SELECT p."Id", p."CommDeviceEquipmentId", p."CommDevicePortId", ...,
e1."Id", e1."SerialNumber", e1."Password", ...,
c."EquipmentId", c."FirmwareVersion", ...,
i."Id", i."AccessToken", ...,
t."Id", t."EquipmentId", t."PollConnectionId"
FROM public."PollConnection" AS p
LEFT JOIN public."PollPortGroup" AS p0 ON p."PollPortGroupId" = p0."Id"
LEFT JOIN public."Equipment" AS e1 ON p."CommDeviceEquipmentId" = e1."Id"
LEFT JOIN public."IntegrationParameters" AS i ON p."IntegrationParametersId"= i."Id"
LEFT JOIN public."IntegrationType" AS i0 ON i."IntegrationTypeId" = i0."Id"
LEFT JOIN public."Complex" AS c ON e1."Id" = c."EquipmentId"
LEFT JOIN (
SELECT e3."Id", e2."EquipmentId", e2."PollConnectionId"
FROM public."EquipmentPollConnection" AS e2
INNER JOIN public."Equipment" AS e3 ON e2."EquipmentId" = e3."Id"
) AS t ON p."Id" = t."PollConnectionId"
WHERE EXISTS (
SELECT 1
FROM public."EquipmentPollConnection" AS e
INNER JOIN public."Equipment" AS e0 ON e."EquipmentId" = e0."Id"
WHERE p."Id" = e."PollConnectionId"
AND e0."Id" = 15571 -- ← меняется на каждой итерации цикла
)
ORDER BY p."Id", p0."Id", e1."Id", i."Id", i0."Id", c."EquipmentId", t."EquipmentId", ...
Запрос содержит 6 JOIN-ов и коррелированный подзапрос EXISTS. При удалённой СУБД каждый вызов занимает в среднем 42.7 мс. При 1 676 устройствах суммарное время составляет 71 570 мс, что приводит к таймауту.
Почему один вызов занимает 42.7 мс
Медленная работа отдельного запроса объясняется двумя факторами.
Фактор 1: новое соединение с БД на каждый вызов.
Профайлер фиксирует паттерн Connection Open → ExecuteReader → Connection Close, повторяющийся 1 676 раз. Каждое открытие соединения с удалённой СУБД — это отдельный сетевой round-trip: TCP handshake, аутентификация PostgreSQL, выделение backend-процесса. Это добавляет 10–15 мс накладных расходов сверх времени самого запроса.
Фактор 2: запрос дважды сканирует одну и ту же таблицу.
Запрос фильтрует подключения по одному прибору через EXISTS, а затем для найденных подключений снова сканирует EquipmentPollConnection в подзапросе t, чтобы вернуть все соседние приборы — без какого-либо фильтра:
-- Первый скан: найти подключения прибора 15571
WHERE EXISTS (
SELECT 1 FROM "EquipmentPollConnection" e
JOIN "Equipment" e0 ON e."EquipmentId" = e0."Id"
WHERE p."Id" = e."PollConnectionId" AND e0."Id" = 15571
)
-- Второй скан: для найденных подключений вернуть ВСЕ соседние приборы
-- (таблица EquipmentPollConnection — 33 009 строк — без фильтра)
LEFT JOIN (
SELECT e3."Id", e2."EquipmentId", e2."PollConnectionId"
FROM "EquipmentPollConnection" e2
JOIN "Equipment" e3 ON e2."EquipmentId" = e3."Id"
) AS t ON p."Id" = t."PollConnectionId"
Итого на один вызов: два прохода по EquipmentPollConnection (33 009 строк), 6 JOIN-ов с широкими таблицами (~50 столбцов в SELECT), сортировка по 8 полям.
| Фактор | Оценка вклада |
|---|---|
| Открытие соединения (сеть) | ~10–15 мс |
Двойной скан EquipmentPollConnection |
~15–20 мс |
| 6 JOIN-ов + сортировка по 8 полям | остаток |
| Среднее на один вызов | 42.7 мс |
| × 1 676 вызовов | 71 570 мс |
Вложения
New_Request-1771522675549.html— результат Profiler дляPOST /Equipment/Node/61/Connections(один из вызовов при сохранении дома; 75 516 мс, 1 712 SQL-запросов)