Неделями cron-задача в Elevare Digital запускалась по расписанию, проверяла очередь и регистрировала успешное выполнение. Она одобряла ровно ноль черновиков. При этом девятнадцать единиц контента лежали в ожидании. Команда узнала об этом лишь спустя время, когда безмолвный пробел превратился из странности в небольшой завал. Ничего не упало. Никакие алерты не сработали. Система была технически исправна, но функционально мертва.
В этом заключается тихий ужас автономных конвейеров. Когда вы убираете человека из процесса, вы также убираете того, кто может заметить, что ничего не происходит.
Конвейер, который работал сам по себе
Elevare Digital использует полностью автоматизированный рабочий процесс создания контента. Программные агенты генерируют черновики. Запланированная cron-задача для одобрения выступает в роли контролера: она проверяет эти черновики и отправляет одобренные элементы прямиком на публикацию. Ни один человек не открывает панель управления, чтобы подтвердить каждую партию. Весь смысл в том, чтобы машина взяла на себя рутину, пока команда занимается другими задачами.
В такой модели доверие становится вашим основным интерфейсом. Вы доверяете планировщику. Вы доверяете выполнению задачи. Вы доверяете коду выхода. Когда логи показывают ровное «сердцебиение» ответов 200 OK, вы предполагаете, что работа движется. В течение нескольких недель это сердцебиение было идеальным. Cron запускался вовремя, каждый раз. Он просто никогда не выполнял саму работу.
Девятнадцать черновиков и никакой тревоги
Открытие было случайным. Кто-то в конце концов заметил, что очередь публикации затихла, или, возможно, проверил метрику на последующем этапе и увидел прямую линию на графике. То, что они обнаружили, оказалось завалом из девятнадцати черновиков, к которым никто не прикоснулся. Задача-аппрувер работала прилежно, ежедневно регистрируя успех, но не обработала ни одного из них.
В ручном рабочем процессе рецензент заметил бы пустой входящий ящик или скопление ожидающих элементов в первый же день. В автоматизированной версии отсутствие активности выглядело в точности как отсутствие работы. У cron-задачи не было менеджера, которого можно было бы разочаровать. Она просто отмечалась на работе и уходила пораньше.
Два бага, один пустой результат
У этого сбоя было два «родителя». Ни один из них не был синтаксической ошибкой, таймаутом или сбоем зависимости. Оба были семантическими ошибками, которые в глазах движка запросов превратили девятнадцать валидных строк в ничто.
Во-первых, несоответствие типов. Агент, генерирующий черновики, записывал записи с тегом article. Cron-аппрувер запрашивал данные строго по типу thread. Это тот самый вид «дрейфа», который происходит, когда производители и потребители развиваются параллельно. Одна команда — или один агент — решила, что результатом является статья. Другая написала потребителя, исходя из того, что он будет принимать треды. Никакая система типов не выдала ошибку компиляции, потому что это, скорее всего, были нестрогие строковые теги, возможно, поля JSON или значения varchar без ограничений. База данных просто не нашла совпадений и вернула пустой набор. Для движка это не является ошибкой. Это правильный ответ на неправильный вопрос.
Во-вторых, inner join в запросе аппрувера бесследно поглотил строки. Если запрос соединял таблицу черновиков с другой таблицей — например, для поиска метаданных, флагов статуса или правил маршрутизации — и условие соединения не выполнялось, inner join вел себя именно так, как было задумано. Он исключал несовпадающие строки. В результирующем наборе не появилось «осиротевших» строк. Никакие null-значения не сигнализировали о проблеме. Девятнадцать черновиков прошли сквозь запрос, как вода сквозь сито, и уровень приложения получил чистый, пустой список.
Поскольку запрос не вернул строк, функция завершилась штатно. Исключения не всплыли. HTTP-ответ был 200 OK. Cron залогировал успех и снова ушел в спячку.
Ловушка обработанного нуля
Вот в чем суть проблемы. В системах на основе очередей потребитель часто находит ноль строк для обработки. Очередь пустеет. Воркер завершает работу быстро. В логе читается processed: 0, и команда воспринимает это как хорошую новость: мы справляемся с нагрузкой. Это здоровое состояние.
Но processed: 0 кодирует две совершенно разные реальности:
- Здоровое состояние: обработано ноль, потому что в очереди ноль. Очередь пуста. Система простаивает по задумке.
- Неисправное состояние: обработано ноль, потому что потребитель не видит работу. В очереди девятнадцать строк. Система слепа, а не простаивает.
Без независимой проверки глубины очереди эти два состояния выдают идентичную телеметрию. Они выглядят одинаково на дашбордах, так же «пахнут» в агрегаторах логов и вызывают ту же тишину в PagerDuty. Вы построили стратегию мониторинга, которая обнаруживает, когда рабочий кричит, а не когда он молча проходит мимо горы реальной работы.
Устранение разрыва
Elevare Digital решила проблему, изменив подход к мониторингу. Они перестали полагаться исключительно на частоту ошибок и статусы успеха. Вместо этого они начали настроить оповещения о разрыве между объемом доступной работы и объемом выполненной работы.
После каждой партии они теперь проводят простую проверку инварианта:
- Если
processedравно 0 и количество строк в ожидании (pending) больше 0, срабатывает оповещение высокого уровня критичности.
Это правило намеренно не учитывает причину. Ему неважно, была ли ошибка вызвана неверным фильтром, сломанным соединением (join) или опечаткой в строке перечисления (enum). Оно учитывает только то, что работа существует, но она не выполняется. Это переводит мониторинг из плоскости «Жаловался ли процесс?» в плоскость «Сдвинулась ли работа?»
Чтобы поддержать этот подход, они рассматривают глубину очереди как первоклассную метрику, которую отслеживают во времени, а не просто проверяют время от времени. Если производитель (producer) продолжает добавлять строки, в то время как потребитель (consumer) постоянно сообщает об успехе, тренд глубины очереди становится неопровержимым доказательством проблемы. Статический снимок может лгать, но постоянно растущая очередь — никогда.
Уроки для автономных систем
Инцидент с Elevare содержит несколько практических правил для всех, кто управляет автоматизированными конвейерами (pipelines).
Логируйте просканированные строки отдельно от обработанных. Потребитель может выполнить запрос, который затрагивает сорок строк, отфильтровать их все из-за неверных критериев и сообщить processed: 0. Если вы логируете только итоговое количество, вы упускаете «призрачное взаимодействие». Метрика просканированных строк показывает, что исполнитель пришел, посмотрел на работу и ушел в замешательстве. Этот разрыв между просканированными и обработанными строками часто является вашим самым ранним сигналом.
Отслеживайте глубину очереди как временной ряд. Пустая очередь — это нормально. Очередь, которая монотонно растет, пока статус воркеров остается «зеленым» (нормальным), — нет. Постройте график зависимости глубины от пропускной способности потребителя. Если эти показатели расходятся, немедленно начинайте расследование, даже если все проверки состояния (health checks) проходят успешно.
Тестируйте потребителей на реальных данных производителя, а не только на моках (mocks). Юнит-тесты с мок-данными несут в себе предположения тестировщика. Если фабрика моков создает типы thread, а потребитель ожидает типы thread, ваши тесты пройдут успешно, в то время как в продакшене всё сломается. Запускайте интеграционные тесты, которые извлекают реальные записи из выходных данных производителя. Убедитесь, что потребитель действительно видит то, что записывает производитель.
Относитесь к типам данных и значениям enum как к контрактам. Нестрогие строковые теги в JSON-объектах удобны до тех пор, пока они не становятся невидимыми точками отказа. Определяйте схемы явно. Используйте общие константы. Валидируйте полезную нагрузку (payload) на стыке производителя и потребителя. Если контракт нарушается, система должна громко сообщить об ошибке на границе, а не молча упасть внутри условия WHERE.
Главный вывод
Автономные системы не ломаются так, как люди. Они не берут больничный, не выбрасывают исключения при каждом удобном случае и не оставляют очевидных дампов памяти при сбоях. Они возвращают 200 OK и позволяют данным гнить. Если ваши оповещения настроены только на «крики», вы пропустите самые дорогие сбои — те, при которых всё выглядит нормально, но работа не выполняется.
Проектируйте свою наблюдаемость (observability) так, чтобы отслеживать разрыв. Сравнивайте объем входящей работы с объемом исходящей. Если они перестают совпадать, считайте, что машина вам лжет. Потому что иногда идеальный лог об успехе — это единственный симптом системы, которая полностью ослепла.
