«Всё работает, но медленно»: восемь причин, которые не ищут
Метрики зелёные: top показывает простаивающий CPU, free -m — свободную память, диск не пишет с потолка, а ответ всё равно приходит через секунду-две вместо ожидаемых десятков миллисекунд. Первый инстинкт — добавить ядер и памяти, но это не поможет, потому что причина не в нехватке ресурсов, а в том, как сервер и код их используют. Ниже — восемь конкретных причин «тихой» медленности, которые редко проверяют в первую очередь, и способ найти каждую одной-двумя командами.
Содержание
Почему «ресурсов хватает» не значит «всё в порядке»
Классический мониторинг считает busy-время: сколько процентов CPU занято, сколько памяти использовано, сколько I/O в секунду. Все восемь причин из этой статьи почти не трогают эти счётчики — они живут в wait-времени: процесс не работает, а ждёт. Ждёт ответа DNS-сервера, ждёт завершения TLS-рукопожатия, ждёт, пока гипервизор вернёт ему украденный такт CPU, ждёт синхронной записи в лог. На графиках это выглядит как низкая загрузка и высокая задержка одновременно — комбинация, которая обычно и сбивает с толку при первом взгляде на дашборд.
Поэтому вместо «сколько занято» нужно спрашивать «на чём простаивает» — и здесь работают другие инструменты: strace -T, vmstat 1, EXPLAIN ANALYZE, tcpdump, профилировщик приложения. Ни один из них не входит в стандартный набор «проверить сервер», поэтому эти причины и остаются незамеченными неделями.
Сеть и внешние зависимости
1. DNS-резолвинг без кеша на каждый запрос. Если приложение резолвит имя хоста (базы, S3-совместимого хранилища, внешнего API) при каждом обращении вместо того, чтобы закешировать результат на время TTL, каждый запрос получает лишний round-trip до DNS-сервера — при плохой связности до резолвера это десятки, а иногда сотни миллисечунд сверху. Особенно часто это встречается в контейнерах на musl libc (Alpine), где резолвер по умолчанию не кеширует ответы вообще. Проверка: запустите tcpdump -i any -n port 53 во время обращения приложения и посчитайте, сколько DNS-запросов приходится на один HTTP-запрос — если больше одного на один и тот же хост, кеша нет. Второй способ — strace -f -e trace=network -p <PID> 2>&1 | grep -i connect, где будут видны повторные resolve одного и того же имени.
2. TLS-хендшейк без session resumption на каждое соединение. Полный TLS-хендшейк — это два round-trip'а плюс проверка цепочки сертификатов; если клиент не переиспользует session ticket или session ID, а устанавливает новое соединение на каждый запрос, эти миллисекунды накапливаются на каждом обращении к базе, к внешнему API, к бэкенду за реверс-прокси. Подробнее о том, что происходит до первого байта ответа, — в статье про TLS-рукопожатие. Проверка: curl -o /dev/null -s -w 'connect: %{time_connect} tls: %{time_appconnect} total: %{time_total}\n' https://ваш-хост — если time_appconnect заметно больше time_connect при каждом повторном запросе, resumption не работает. Через openssl s_client -connect host:443 -reconnect можно явно увидеть, переиспользуется ли сессия (Reused, TLSv1.3 в выводе против полного хендшейка).
3. Медленный внешний API, от которого зависит ответ. Если бэкенд синхронно дожидается ответа от платёжного шлюза, геолокационного сервиса или любого стороннего API перед тем, как отдать ответ пользователю, вся задержка этого API становится задержкой вашего сервера — а вы её не контролируете и не видите в собственных метриках CPU/RAM. Проверка: добавьте тайминг вокруг исходящих HTTP-вызовов (даже временный console.time/time.perf_counter в коде) или посмотрите upstream_response_time в логах nginx, если запрос идёт через прокси. Если у вас уже настроен внешний мониторинг доступности — начните с общей диагностики: сайт тормозит на VPS — как это проверить по шагам.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверБаза данных
4. N+1 запросы к базе. ORM незаметно превращает один логический запрос («покажи посты с авторами») в один запрос на список плюс по одному дополнительному запросу на каждую строку — вместо одного JOIN получаются десятки почти одинаковых запросов с разными ID. Это не видно ни в CPU, ни в свободной памяти, только в количестве обращений к базе на один HTTP-запрос. Проверка: временно включите логирование всех запросов на время одного запроса к API — в PostgreSQL это log_min_duration_statement = 0 (или разовый SET log_statement = 'all' в сессии), в MySQL — general_log = ON; затем посчитайте, сколько строк лога приходится на одно действие пользователя. Если видите десятки похожих SELECT ... WHERE id = $1 с разными значениями — это N+1.
5. Отсутствующий индекс под конкретный запрос. Таблица выросла, а индекс под новый паттерн фильтрации (WHERE status = ? AND created_at > ?) так и не появился — вместо Index Scan база делает Seq Scan по всей таблице, и с ростом данных запрос линейно замедляется, оставаясь незаметным при малой нагрузке. Проверка: EXPLAIN ANALYZE на реальный медленный запрос — ищите Seq Scan на таблице с большим числом строк там, где ожидался Index Scan или Bitmap Heap Scan. В PostgreSQL расширение pg_stat_statements (SELECT query, total_exec_time, calls FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10) быстро покажет, какие запросы съедают больше всего суммарного времени. Как именно индекс ускоряет и в каких случаях, наоборот, замедляет запись, — в статье про индексы и когда они не помогают; там же логика поиска подходящего запроса разобрана подробнее, чем здесь можно уместить, — см. также разбор медленных запросов MySQL, если стек на MySQL.
Система, память и код
6. Swap из-за неправильного размера heap. Если JVM (или аналогичная среда — .NET, V8 с явным ограничением памяти) настроена с -Xmx, который вместе с памятью соседних процессов и файловым кешом превышает физическую RAM, ОС начинает свопить неактивные страницы heap — сборщик мусора начинает подкачивать данные с диска, и паузы GC растягиваются в разы без видимого роста нагрузки на CPU. Проверка: vmstat 1 — колонки si/so (swap in/out) должны быть нулевыми в стабильном режиме; если они регулярно ненулевые при работающем JVM-процессе, сверьте -Xmx с реальным доступным объёмом через ps aux --sort=-%mem и free -m. Здравый ориентир при выборе размера — не выделять heap больше 70–75% доступной RAM с учётом других процессов; как рассчитать размер под конкретную конфигурацию сервера, разобрано в статье про правильный размер swap для VPS.
7. Steal time на переподписанном VPS. Если провайдер продал больше виртуальных ядер, чем физически может обслужить хост, ваша виртуальная машина периодически ждёт своей очереди у гипервизора — CPU внутри VM показывает низкую загрузку, а часть времени просто «украдена» соседями. Это одна из немногих причин в этом списке, которую нельзя исправить на уровне приложения — только сменой тарифа или хоста. Проверка: vmstat 1 — колонка st (steal); устойчиво двузначные значения в обычной нагрузке говорят о перепродаже ресурсов хоста. Подробный разбор, как отличить разовый всплеск от системной проблемы, — в статье про steal time и «соседей», которые едят ваш CPU.
8. Синхронное логирование, блокирующее event loop. В однопоточных средах с событийным циклом (Node.js, Python asyncio) синхронная запись лога — вызов вроде fs.writeSync, блокирующий logging.FileHandler без буферизации, транспорт логгера без async-режима — останавливает обработку всех остальных запросов на время записи на диск или отправки по сети. При медленном диске или логировании на сетевой том (NFS, смонтированный volume) это превращается в задержку для всех параллельных клиентов, а не только для того запроса, который писал лог. Проверка: профилировщик приложения (--prof в Node.js с последующим --prof-process, py-spy dump в Python) покажет время, проведённое внутри вызова логгера; более грубый способ — strace -T -p <PID> и поиск системных вызовов write()/fsync() с большим временем выполнения именно в момент записи лога.
В каком порядке проверять
Проверять все восемь причин подряд без системы — долго. Разумный порядок: сначала то, что проверяется одной командой за секунды, потом то, что требует включения логирования или профилирования.
| Причина | Как проверить | Время на первую проверку |
|---|---|---|
| Steal time | vmstat 1, колонка st | 10 секунд |
| Swap от heap | vmstat 1, колонки si/so | 10 секунд |
| TLS без resumption | curl -w с таймингами | 1 минута |
| DNS без кеша | tcpdump port 53 при запросе | 2–3 минуты |
| Отсутствующий индекс | EXPLAIN ANALYZE на медленный запрос | 5 минут |
| N+1 запросы | временный лог всех запросов к БД | 5–10 минут |
| Синхронное логирование | профилировщик приложения | 15–20 минут |
| Медленный внешний API | тайминги вокруг исходящих вызовов | зависит от того, где вставлять |
Первые две строки не требуют изменений в коде и проверяются мгновенно — с них и стоит начинать, прежде чем разбирать логи приложения или включать профилировщик.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Может ли быть несколько причин одновременно?
Да, и это частый случай: например, отсутствующий индекс делает запрос медленным, а сверху N+1 умножает эту медленность на количество строк в списке. Проверяйте по порядку из таблицы выше, а не только первую найденную причину.
Нужно ли включать полное логирование запросов на продакшене?
Кратковременно — да, это стандартная практика для диагностики; включайте на минимальное время, необходимое чтобы поймать один медленный запрос, и выключайте сразу после, поскольку log_min_duration_statement = 0 в PostgreSQL сам по себе создаёт заметную нагрузку на диск при большом трафике.
Steal time можно исправить настройками ОС?
Нет — это происходит на уровне гипервизора хостинг-провайдера, изнутри VM его не убрать. Единственные варианты — сменить тариф на менее переподписанный или перейти на выделенный сервер, где такой проблемы нет в принципе.
С чего начать, если не знаешь, какая из восьми причин у меня?
С таблицы порядка проверки выше: vmstat 1 на 30–60 секунд покажет steal time и swap за одну команду и сразу исключит или подтвердит две причины из восьми, прежде чем переходить к более трудоёмким проверкам.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →