Ваш отчёт завершается примерно за секунду на хосте базы данных и за четыре через VPN, а число, которое печатает psql, указывает прямо на базу данных. Это число — секундомер на стороне клиента, а не измерение сервера. Оно охватывает инструкцию на пути наружу, ожидание ответа и строки на обратном пути. Разделить эти три — единственный способ понять, какую сторону настраивать.

Что на самом деле измеряет таймер psql

\timing on запускает секундомер незадолго до того, как клиент запишет инструкцию в сокет, и останавливает, когда прочитана последняя строка этого результата. Выполнение самого сервера находится где-то внутри этого окна, как и каждая миллисекунда, которую пакеты проводят в туннеле. Ничто в выводе их не разделяет. EXPLAIN тоже печатает число, но его создаёт сервер, и оно исключает ожидание клиента. Два числа, два разных вопроса.

Начните одну сессию, оставьте измерение времени включённым и выполните нужную инструкцию изнутри неё. Повторное использование сессии удерживает стоимость подключения вне первого измерения.

psql "host=db.internal port=5432 dbname=appdb user=app" \
  -c "\timing on" \
  -c "select count(*) from orders where created_at > now() - interval '1 day';"

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

Сравните одну инструкцию на трёх путях

Выполните ту же инструкцию через Unix-сокет, через адрес LAN и через имя хоста VPN и сравните три. Если медленным является только последний, запрос не является переменной.

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

psql -h /var/run/postgresql -U app -d appdb -c "\timing on" -c "select count(*) from orders;"
psql -h 10.40.0.12 -U app -d appdb -c "\timing on" -c "select count(*) from orders;"
psql -h db.vpn.internal -U app -d appdb -c "\timing on" -c "select count(*) from orders;"

В LAN разница обычно достаточно мала, чтобы её игнорировать; через VPN часто нет. Сортировка трёх результатов затем скажет, на какое ожидание нападать.

Медленный локальный сокет означает работу сервера или планирование. Медленный результат LAN означает запрос или объём, который он перемещает. Только медленный результат VPN означает задержку, MTU или доступную полосу пропускания на пути, и никакие изменения индексов это не исправят.

Достаньте собственное время выполнения сервера

Сервер может сообщить, сколько времени он потратил, и именно это число нужно сопоставлять с клиентским. EXPLAIN с ANALYZE выполняет инструкцию и печатает время планирования и выполнения внутри плана, а BUFFERS добавляет поведение кеша, которое часто объясняет тёплый запуск в отличие от холодного. Не направляйте его на инструкции записи, если не собираетесь писать. На реплике только для чтения это самое безопасное место для начала, и справка psql документирует область действия таймера на стороне клиента.

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

select calls, round(mean_exec_time::numeric, 2) as mean_ms, rows
from pg_stat_statements
order by mean_exec_time desc
limit 5;

Имена столбцов различаются по версии: PostgreSQL 13 и новее показывают mean_exec_time, а более старые версии называют его mean_time, поэтому проверьте с помощью \d pg_stat_statements, если этот запрос выдаёт ошибку. Эти значения исключают планирование и исключают путь до вашего клиента. Именно это исключение делает их полезными для сравнения.

Считайте круговые обходы, а не только миллисекунды

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

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

select sum(calls) as calls from pg_stat_statements;
-- run the report here
select sum(calls) as calls from pg_stat_statements;

Если дельта велика и каждая инструкция дешева на сервере, пакетная обработка — это исправление, которое окупается. Объедините зависимые запросы, используйте одну инструкцию на основе множеств или ведите цикл из курсора. В интерактивном psql, \set FETCH_COUNT 1000 большой результат приходит партиями и меняет поведение передачи и памяти клиента. Это не уменьшает работу сервера, поэтому не спасёт медленный запрос.

Проверьте туннель на повторные передачи и остановки MTU

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

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

ss -tin dst 10.40.0.12
netstat -s | grep -iE "retransmit|segments retransmitted"
ping -M do -s 1400 -c 3 10.40.0.12

Повторные передачи, растущие во время передачи, вместе с ping, который успешен на 1400 байт, но неудачен на 1472, указывают на проблему MTU, а не базы данных. Понижение MTU интерфейса туннеля или зажатие MSS на шлюзе — обычное исправление, и те же три команды подтверждают его после. Если повторные передачи остаются плоскими, пока передача всё ещё медленная, смотрите на полосу пропускания, разрешённую туннелю, а не на потери.

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

Похожие статьи