Перейти к содержанию

Успешно завершено

Движение вперед — это всегда движение в неизвестность. Ты покоряешь новую вершину, внедряешь новую функциональность — и в этом же движении, чуть сбоку, что-то тихо рассыпается из того, что должно было работать бесперебойно. Не потому что ты небрежен. Просто у каждого шага есть тень, и она не всегда падает туда, куда смотришь.

Вчера я радовался: микросервис подписки запущен, CI/CD отработал, Nginx настроен. «Развернулось само, без единого ручного действия на сервере». Сегодня утром пользователи не могли подключиться. Ни один.


Первая гипотеза — Nginx. Разумно: именно там вчера что-то менялось при интеграции микросервиса. Начал диагностику.

Оба сервиса живые: Nginx и Xray на Entry- и Core-нодах запущены, ошибок нет. Конфиги на первый взгляд корректные. Журнал ротации — два успешных запуска сегодня. «Ротация завершена успешно, обновлено маршрутов: 1».

Успешно. Завершено.

Я смотрел на эту строчку и чувствовал, что что-то здесь все-таки не так.


Вот что произошло на самом деле.

Архитектура туннеля устроена так: клиент подключается к Entry-ноде по одному gRPC-пути, а Entry переправляет трафик на Core по другому — внутреннему — пути. Это принципиально: два разных serviceName на двух участках, каждый со своей ролью. Клиентский путь — фиксированный, публичный. Внутренний — ротируется автоматически каждый час плюс/минус 30 минут, примерно, для защиты от статистического анализа трафика.

Nginx на Core-ноде обязан знать актуальный внутренний serviceName — именно по нему он маршрутизирует gRPC от Entry к Xray. Скрипт ротации это и делает: берет текущее значение из конфига Xray, ищет его в файлах Nginx и заменяет на новое.

Ключевое слово: ищет. Если в Nginx написано что-то другое — скрипт молча пройдет мимо. Валидация пройдет (конфиг синтаксически верен), сервисы перезагрузятся, лог скажет «успешно». А в Nginx останется неправильный путь.

Именно это и случилось. При деплое микросервиса конфиг Nginx на Core был переписан — и в блок gRPC-маршрутизации случайно попал клиентский serviceName вместо внутреннего. Перепутать их несложно: оба выглядят одинаково, api.v2.rpc.<шестнадцать символов>. Один от другого внешне не отличить. Где были мои глаза?!

С этого момента каждая ротация исправно обновляла Xray и Entry, но Nginx на Core не трогала — нужной строки там попросту не было. Скрипт не знал, что Nginx сломан. Он честно делал всё, что должен, и рапортовал об успехе.

Entry-нода отправляла трафик на Core с актуальным внутренним путем. Nginx на Core не находил подходящего location — и отдавал сайт-прикрытие. Обычный HTML. gRPC-соединение рассыпалось.


Исправление заняло минуту. Заменить неправильный serviceName в Nginx на актуальный, перезагрузить. Следующая ротация отработала уже корректно — нашла нужную строку, обновила.

Инцидент занял меньше суток: сеть сломалась вечером 3 марта, восстановлена утром 4-го. Причина — копипаст не того идентификатора при ручной правке конфига. Классика.


Но меня занимает другое. Скрипт ротации работал «успешно» несколько часов подряд, пока сеть была мертва. Не потому что он плохо написан — логика там аккуратная, с откатами и валидацией. Просто он не проверяет соответствие между тем, что в Xray, и тем, что в Nginx. Он доверяет, что Nginx уже настроен правильно — и только вносит изменения поверх. Если фундамент кривой, он кропотливо кладет кирпичи на кривой фундамент и докладывает: «фундамент принят».

Это не баг в скрипте. Это граница его ответственности — та самая, о которой не думаешь, пока она не дает о себе знать.

И вот здесь я возвращаюсь к тому, с чего начал. Каждый шаг вперед — это не только новая возможность, но и новый слой сложности, который ложится поверх предыдущего. Микросервис подписки — хорошая вещь, нужная. Но его появление потребовало изменений в Nginx, а изменения в Nginx потребовали внимания к деталям, которое в какой-то момент подвело. Система стала чуть сложнее — и это мгновенно аукнулось там, где казалось всё давно отлажено.

Я не делаю из этого трагедии. Такова природа любой живой инфраструктуры: она не статична, она растет — а значит, иногда спотыкается. Важно не то, что произошел инцидент. Важно, что его можно было предупредить. Добавить проверку согласованности конфигов при старте ротации. Сделать smoke-тест после каждого прогона — убедиться, что gRPC-путь действительно проксируется, а не просто синтаксически существует в файле.

Скорее всего, так и сделаю. Но сначала — этот пост.