MAATRIX / Блог / Проверка доступности ходила через кеш CDN и не видела, что бэкенд лежит

Проверка доступности ходила через кеш CDN и не видела, что бэкенд лежит

MAATRIX

Статус-страница зелёная, аптайм-монитор шлёт «всё ок» каждую минуту, а в поддержку валятся тикеты «сайт не открывается». Знакомая ситуация — и почти всегда причина в том, что проверка доступности ходит не туда, куда думает дежурный. В этом разборе — реальная механика инцидента, где CDN больше двух часов подряд отдавал мониторингу кешированную страницу вместо честной ошибки бэкенда, и что после этого поменяли в настройке проверок.

Что сломалось

Вечером в пятницу в конце августа деплой на бэкенде провалился наполовину: один из трёх процессов приложения не поднялся после отката миграции, начал отвечать 502 на часть запросов, а балансировщик перед ним продолжал считать его живым, потому что health-check внутри контейнера проверял только процесс, а не готовность отвечать на HTTP. Пользователи, которых балансировщик направлял на сломанный инстанс, получали ошибку. Внешний аптайм-монитор (в этом случае — Uptime Kuma, опрашивающий публичный домен раз в минуту) продолжал показывать 200 OK без единого пропуска.

Первый сигнал пришёл не от мониторинга, а от службы поддержки — за 40 минут скопилось около полутора десятков обращений «не открывается страница оплаты» и «сайт висит». Дежурный инженер открыл статус-панель, увидел сплошную зелёную линию за последние сутки и потратил четверть часа на то, чтобы убедиться: мониторинг действительно не врёт технически, просто проверяет не то, что нужно.

Что видели в логах и метриках

Когда разбор дошёл до логов, картина стала интереснее, чем «монитор проверяет не тот URL». Проверка доступности как раз ходила на нужный адрес — на главную страницу сайта, за которой стоит и оплата, и остальной функционал, через общий домен и общий CDN. Но между монитором и бэкендом стоял кеширующий прокси, и именно на его уровне ответы расходились:

  • в логах origin (бэкенда) за проблемное окно — почти 40% запросов с кодом 502 и 503;
  • в логах edge-узла CDN за то же окно — почти сплошные 200, и заголовок CF-Cache-Status: HIT или STALE на большей части ответов;
  • запросы от IP-адреса аптайм-монитора в логах origin почти не встречались — то есть до бэкенда они физически не доходили.

Это и была первая настоящая зацепка: монитор бил в публичный домен, публичный домен резолвился на CDN, а CDN далеко не каждый раз шёл к origin — он отдавал то, что уже лежало у него в кеше.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Гипотезы, которые отбросили

Прежде чем дошли до кеша, перебрали несколько версий, каждая по 10-15 минут:

  • DNS указывает не туда. Проверили dig и curl -v с самого монитора — резолвинг был корректным, домен вёл на правильный CDN-провайдер, а не на старый IP.
  • Файрвол или security-группа режут именно трафик от IP мониторинга. Тоже нет: если бы резались пакеты, монитор получал бы таймаут или connection refused, а не честный HTTP 200 с телом страницы.
  • Баг в самом скрипте мониторинга — он проверяет закешированный ответ на своей стороне. Отпало сразу, как только вручную выполнили curl -sI https://домен/ с чистого хоста без всякого локального кеша — результат был тот же 200, значит проблема не в клиенте.
  • Синтетическая проверка бьёт в статический health-эндпоинт, который вообще не связан с реальным приложением. Похоже на правду, но не совсем: URL, который проверял монитор, был не выделенным /health, а именно главной страницей — той самой, что видят пользователи. Просто эта страница оказалась закешированной.

Отброшенные гипотезы всё равно оказались полезны — они сузили круг до одного места: между монитором и бэкендом что-то возвращает старый ответ, не спрашивая источник заново.

Настоящая причина: stale-if-error на общем кеш-пути

На CDN уже несколько месяцев была включена директива stale-if-error, добавленная ради устойчивости к кратковременным сбоям origin — разумная сама по себе идея, чтобы пользователи не видели ошибку сервера, если бэкенд споткнулся на пару секунд:

Cache-Control: public, max-age=120, stale-while-revalidate=60, stale-if-error=86400

stale-if-error=86400 означает: если при попытке обновить кеш origin ответил ошибкой (5xx или таймаутом), CDN до суток подряд может отдавать клиенту последнюю успешную версию страницы вместо честной ошибки. Для обычного посетителя это благо — он не видит белый экран смерти, пока команда чинит бэкенд. Но для проверки доступности это яд: монитор, который бьёт в тот же публичный URL, физически не может увидеть, что origin недоступен, пока в кеше жив хоть один успешный ответ младше суток.

Совпало две вещи: у страницы был относительно свежий закешированный успешный ответ (последнее удачное обновление кеша случилось за пару минут до деплоя), и именно из-за stale-if-error CDN не стал форсированно идти к origin при каждом запросе монитора — он честно следовал заголовкам кеширования, которые сами разработчики и настроили. С точки зрения CDN всё работало ровно так, как описано в конфиге. Проблема была не в CDN, а в том, что для health-check использовали URL, живущий по тем же правилам кеша, что и обычный трафик.

Отдельно проверили origin-логи за момент самого сбоя — оказалось, что реальный процент 502 колебался: балансировщик направлял часть запросов на живой инстанс (оттуда шёл 200 и попадал в кеш), часть — на сломанный (502). CDN кешировал успешные ответы и продолжал раздавать их при следующих ошибках origin, сглаживая картину ещё сильнее.

Почему это не поймали раньше

Отдельный вопрос, который разбирали уже после инцидента: почему за месяцы работы такой конфигурации никто не заметил дыру в мониторинге. Ответ обычный — раньше origin ни разу не падал настолько, чтобы это стало заметно дольше пары запросов. При коротких сбоях (несколько секунд) stale-if-error как раз и делает то, для чего его включали — сглаживает проблему для пользователя, и она проходит незамеченной, потому что реально проходит быстро. Уязвимость проявилась только тогда, когда сбой оказался длительным (частично сломанный деплой держался до ручного вмешательства), а до этого никто не проверял мониторинг на устойчивость именно к длинному отказу происхождения.

Второй фактор — health-check внутри Docker-контейнера проверял только то, что процесс приложения запущен и слушает порт, а не то, что он реально отвечает на HTTP-запрос с ожидаемым статусом. Из-за этого оркестратор не перезапускал сломанный инстанс и не выводил его из балансировки — так частичный сбой и продержался до момента, пока дежурный не вмешался руками. Это отдельная и довольно частая грабля — она разобрана в статье про настройку healthcheck, который проверял не то, из-за чего контейнер считался живым.

Что изменили после инцидента

Правки разделили на три группы — по мониторингу, по кешу и по health-check внутри приложения.

Мониторинг перестал ходить через CDN на кешируемый URL. Завели отдельный поддомен origin-check.домен, который резолвится напрямую на IP бэкенда, минуя CDN, и добавили туда служебный эндпоинт /healthz, отвечающий актуальным статусом (проверяет соединение с базой и очередью, а не просто «процесс жив»). Основная синтетическая проверка теперь бьёт именно туда, а публичный URL через CDN оставили как вторую, дополнительную проверку — она полезна для отслеживания latency и доступности с точки зрения реального пользователя, но перестала быть единственным источником правды об аптайме.

У health-эндпоинта прописали заголовки, которые запрещают его кешировать в принципе:

location /healthz {
    add_header Cache-Control "no-store, no-cache, must-revalidate" always;
    proxy_pass http://backend_upstream;
    proxy_cache off;
}

Так даже если кто-то в будущем случайно проверит этот URL через CDN, кеш не встанет между проверкой и origin.

stale-if-error оставили — как решение он по-прежнему разумен для пользовательского опыта при коротких сбоях, убирать его не стали. Но добавили ограничение по времени: снизили значение с 86400 до 900 секунд, чтобы длительный сбой origin не мог маскироваться кешем дольше 15 минут ни для кого, включая обычных посетителей — компромисс между устойчивостью к кратким сбоям и риском долго показывать пользователям неактуальную страницу.

Алертинг завязали не только на код ответа с публичного URL, но и на долю 5xx в логах самого origin — через агрегацию access-логов бэкенда в существующий стек метрик. Даже если внешняя проверка снова каким-то образом окажется закешированной, рост ошибок на origin теперь триггерит алерт независимо от того, что видит клиент снаружи. Про то, как вообще устроен слой кеширования перед сервером и что он от вас прячет, если не следить, есть отдельный разбор — что такое CDN изнутри и где физически лежит кешированная картинка, и практическая настройка — CDN перед сервером: как включить и не потерять контроль над кешем.

Отдельно пересмотрели, как настроено кеширование в самом nginx на origin — там тоже была директива, которая при недоступности апстрима отдавала устаревший файл кеша (proxy_cache_use_stale), и её тоже привели в соответствие с новым таймаутом. Если разбираетесь с похожей настройкой, пригодится статья про частые ошибки кеширования в nginx и их решения.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Разве stale-if-error — это плохая практика, раз она спрятала аварию?

Нет, сама по себе директива полезна: она защищает пользователей от кратковременных сбоев origin. Проблема была не в её наличии, а в том, что для проверки доступности использовали URL, подчиняющийся тем же правилам кеша, и в слишком большом значении TTL (сутки), которое не ограничивало маскировку по времени.

Как быстро проверить, кешируется ли ответ, который использует ваш мониторинг?

Выполните curl -sI https://ваш-домен/путь и посмотрите заголовки ответа — у большинства CDN есть свой маркер вроде CF-Cache-Status, X-Cache или Age. Если видите HIT, STALE или ненулевой Age, значит ответ может прийти из кеша, а не от origin, и для health-check это неприемлемо.

Можно ли просто добавить Cache-Control: no-cache к параметрам запроса от монитора вместо правки заголовков на сервере?

Некоторые CDN действительно уважают Cache-Control: no-cache в запросе клиента и обходят кеш, но полагаться на это рискованно — поведение отличается между провайдерами и версиями конфигурации. Надёжнее сделать некешируемым сам ответ на уровне сервера или использовать отдельный URL, который в принципе не проходит через кеширующий слой.

Нужно ли теперь всегда мониторить origin напрямую, в обход CDN?

Для критичной проверки доступности — да, это единственный способ увидеть реальное состояние бэкенда без искажений на пути. Проверку через публичный домен и CDN стоит оставить как вторую метрику — она показывает, что реально видит пользователь, и это тоже важно, просто не должна быть единственным источником данных об аптайме.

Как проверить, что health-check внутри Docker или Kubernetes действительно тестирует готовность приложения, а не только то, что процесс запущен?

Health-check должен делать реальный HTTP-запрос к приложению и проверять код ответа (а в идеале — и содержимое, например, статус подключения к базе), а не просто убеждаться, что порт слушает соединения. TCP-проверка порта не отличает работающий сервис от процесса, который принял соединение, но зависает при первом запросе.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →