20 сентября 2026 года сайт LIONEX наконец начал считать собственные HTTP-запросы по маршрутам. На следующий день мы впервые посмотрели на полные сутки данных и увидели, что самый популярный адрес сайта — не главная и не блог. Им оказался /api/health, служебный маршрут, который ни один человек никогда не открывает. За сутки он получил 13 666 запросов, то есть 60 % всего трафика приложения.
Почему метрики запросов до 20 сентября не записывались вообще, мы рассказывали в статье о четырёх тревогах, которых не было. Здесь — первая находка, которую дали эти метрики, две настройки, которые её убрали, и способ проверить то же самое у себя за несколько минут.
Что показали первые сутки
Health-маршрут существует для платформы, а не для посетителей. Docker, настроенный через Coolify, периодически обращается к нему и по ответу решает, жив ли контейнер. Несколько плохих ответов подряд — и контейнер перезапускается.
Интервал этой проверки стоял 5 секунд. По настройкам это 17 280 вызовов в сутки; метрики за 21 сентября записали 13 666, меньше расчёта, но того же порядка. Сама частота ещё не беда: ответить «процесс жив» можно почти бесплатно. Беда сидела в коде маршрута. На каждый вызов он делал в базу SELECT 1, так что теоретически столько же, 17 280 запросов в сутки, база получала только для того, чтобы подтвердить, что она на месте.
Эта база стоит на том же сервере, что и ещё десяток наших проектов. В тот день load average сервера достиг 8,36 при пороге тревоги 8. Скажем прямо: какую долю этой нагрузки давал healthcheck, мы не измеряли и причиной его не называем. Видели мы другое — на общей машине круглосуточно крутилась лишняя работа, о которой никто не знал.
Почему никто не замечал
Отдельный вызов длился миллисекунды, ошибок не давал и в логах не светился. Заметить его можно было только одним способом: посчитать запросы по маршрутам. До 20 сентября этого не делали, а без такого распределения служебный адрес может забирать большую часть трафика, и ни один график этого не покажет.
Хуже то, что по отдельности всё выглядело разумно. Пять секунд были значением в панели деплоя, их никто не выбирал сознательно. Запрос в базу когда-то дописали с добрыми намерениями, «чтобы проверка честно смотрела на базу»: health, который базы не касается, может называть здоровым контейнер, уже не способный отдать ни одной страницы. Каждое решение по отдельности защитимо. Вместе они давали запрос в общую базу каждые пять секунд, без конца.
Два рычага, и нужны оба
Первый — интервал. 21 сентября мы изменили его с 5 на 30 секунд. Количество попыток оставили 10, так что до перезапуска контейнер теперь должен проваливать проверки примерно 5 минут (10 × 30 с). Раньше хватало 50 секунд (10 × 5 с).
У этого решения есть цена, и её стоит назвать: настоящий отказ мы теперь замечаем медленнее. Для сайта агентства без очереди заказов это приемлемо. Для платёжного сервиса, где каждая минута простоя стоит денег, — возможно, нет, и там мы взвешивали бы иначе.
Второй рычаг — сам маршрут. Успешный результат проверки базы теперь запоминается на 60 секунд в переменной процесса. Healthcheck получает ответ сразу, а база — не больше одного запроса в минуту.
Одного интервала было бы мало: с ним каждый вызов по-прежнему шёл бы в базу, просто реже. Один кеш убрал бы нагрузку на базу, но платформа и дальше дёргала бы процесс каждые пять секунд: 17 280 вызовов в сутки, каждый с запуском curl внутри контейнера. Вместе они разводят две разные вещи: как часто платформу интересует состояние и как часто ответ действительно доходит до базы.
Переменная процесса здесь сработала потому, что сайт работает как долгоживущий сервер Node: процесс стоит часами, и значение сохраняется между запросами. В serverless, где функция поднимается под каждый запрос, такой кеш ничего бы не запомнил.
Почему ошибку не кешируем
Запоминаем только успех. Если база не ответила, маршрут возвращает 503 с текстом ошибки, сбрасывает отметку успеха, и следующий вызов снова идёт в базу.
Логика несимметрична намеренно. Успех — обычное состояние, и его кеширование снимает почти всю лишнюю работу. Неудача — ровно тот момент, когда по ответу платформа решает, перезапускать ли контейнер, поэтому каждая проверка после неё должна быть свежей. Если бы отметка успеха после сбоя оставалась, контейнер считался бы здоровым ещё минуту после падения базы — именно тогда, когда его нужно перезапускать.
Одна граница остаётся и так: между последней удачной проверкой и следующим настоящим запросом к базе проходит до 60 секунд, и сбой в этом промежутке health увидит не сразу. На фоне пятиминутного окна до перезапуска мы считаем это приемлемым.
Проверка
Сначала локально: 13 вызовов подряд дали один запрос в базу, а вызов через 65 секунд снова пошёл в базу. 22 сентября тесты маршрута переписали: старый тест описывал версию, которой давно не существовало, и вообще не запускался, потому что проекту не хватало пакета jsdom. Новые проверяют ответ 200 и «ok», когда база на месте; отсутствие второго запроса в базу в течение 60 секунд; 503 с текстом ошибки, когда база недоступна; и то, что ошибка не кешируется.
Конфиг контейнера в тот же день: Interval 30 с, Timeout 5 с, Retries 10.
Цифры «после» и доля, которая почти не изменилась
22 сентября Prometheus за 15 минут вне окна деплоя показал 31,5 запроса к /api/health (дробь — следствие того, как Prometheus считает прирост счётчика на отрезке). Это около 126 в час, или примерно 3 000 в сутки вместо 13 666.
Кеш ограничивает число запросов в базу одним в минуту, то есть не больше 1 440 в сутки вместо 17 280. Эта цифра рассчитана из настроек, отдельно запросы к базе мы не измеряли.
Доля же почти не сдвинулась. За те же 15 минут health дал 31,5 из 55 запросов приложения, то есть по-прежнему большинство. Трафик сайта агентства невелик, и служебный адрес занимает в нём заметное место при любом интервале. Проблема никогда не заключалась в доле. Она заключалась в том, что каждый из этих вызовов шёл в общую базу.
Ловушка замера: деплой в окне
Первая попытка измерить «после» дала 690 запросов за час, будто мы ничего не меняли. Причина нашлась в самом окне: туда попал деплой нового образа. Во время развёртывания платформа сама часто опрашивает health нового контейнера, а в системе метрик какое-то время живут две серии, старого и нового контейнера. За 15 минут после деплоя вышло 31,5.
Вывод простой: измерять вне окна деплоя, иначе легко «доказать», что правка не сработала.
Как проверить у себя
Посмотрите интервал. Для Docker:
docker inspect -f '{{json .Config.Healthcheck}}' <контейнер>Interval там в наносекундах:
5000000000— это 5 секунд. В Kubernetes то же самое лежит вperiodSecondsдля liveness и readiness.Прочитайте код health-маршрута. Что он делает на каждый вызов: идёт в базу, в Redis, во внешний API? Умножьте каждое такое действие на количество вызовов в сутки:
вызовов в сутки = 86 400 / интервал в секундах 86 400 / 5 = 17 280Разведите «процесс жив» и «готов обслуживать». Kubernetes прямо разделяет liveness и readiness. В Docker проверка одна, поэтому рабочий компромисс такой: кешировать успех и не кешировать ошибку.
Хотя бы раз посчитайте запросы по маршрутам — из логов прокси или из метрик приложения. Без этого такие вещи не видны в принципе.
Умножьте Retries на Interval. Результат — сколько времени контейнер работает сломанным до перезапуска. Это число стоит выбрать сознательно, а не получить в наследство от значений по умолчанию.
Границы
Всё описанное касается одного сайта на одной платформе, Coolify с Docker. Цифры «до» взяты за одни сутки, «после» — за 15 минут плюс конфиг контейнера. Количество запросов к базе после правки рассчитано, а не измерено.
И главное ограничение: мы не утверждаем, что сайт стал быстрее или что нагрузка сервера упала. Этого мы не измеряли. Убрали мы лишнюю постоянную работу, которую никто не видел, и больше ничего.
Если хотите, чтобы кто-то посмотрел healthcheck, тревоги и учёт запросов на вашем сайте, это часть работы по мониторингу доступности. Настройка контейнеров и самого сервера входит в администрирование сервера.
Для себя мы вынесли одно правило: служебный маршрут — тоже трафик, и считать его нужно так же, как страницы для людей.




