Ваш звіт завершується приблизно за секунду на хості бази даних і за чотири через 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; порахуйте виклики до і після одного запуску; і стежте за повторними передачами, поки рухається великий результат. Кожен крок виключає одну сторону шляху. Жоден не вимагає вгадувати, який компонент перезапускати.

Схожі статті