Przez tygodnie zadanie cron w Elevare Digital budziło się zgodnie z harmonogramem, sprawdzało swoją kolejkę i logowało sukces. Zatwierdzało dokładnie zero szkiców. Dziewiętnaście fragmentów treści czekało w bezruchu. Zespół dowiedział się o tym dopiero później, gdy ta cicha przerwa przerodziła się z dziwactwa w niewielki backlog. Nic się nie zawiesiło. Żaden alert nie został wyzwolony. System był technicznie sprawny, ale funkcjonalnie martwy.

To cichy horror autonomicznych potoków (pipelines). Kiedy usuwasz człowieka z pętli decyzyjnej, usuwasz również osobę, która zauważy, że nic się nie dzieje.

Potok, który działał sam z siebie

Elevare Digital prowadzi w pełni zautomatyzowany przepływ pracy (workflow) z treściami. Agenci programowi generują szkice. Zaplanowane zadanie cron pełniące rolę zatwierdzającego działa jak strażnik, przeglądając te szkice i przesyłając zatwierdzone elementy bezpośrednio do publikacji. Żaden człowiek nie otwiera pulpitu nawigacyjnego, aby błogosławić każdą partię. Cały sens polega na tym, aby maszyna zajmowała się żmudną pracą, podczas gdy zespół zajmuje się innymi problemami.

W tym modelu zaufanie staje się twoim głównym interfejsem. Ufasz, że harmonogram zadziała. Ufasz, że zadanie zostanie wykonane. Ufasz kodowi wyjścia. Gdy logi pokazują miarowy rytm odpowiedzi 200 OK, zakładasz, że praca postępuje. Przez tygodnie ten rytm był idealny. Cron uruchamiał się na czas, za każdym razem. Po prostu nigdy nie wykonywał właściwej pracy.

Dziewiętnaście szkiców i brak alarmu

Odkrycie było przypadkowe. Ktoś w końcu zauważył, że kolejka publikacji ucichła, lub być może sprawdził metrykę w dalszej części procesu (downstream) i zobaczył linię płaską. To, co znaleźli, to zapas dziewiętnastu szkiców leżących całkowicie nietkniętych. Zatwierdzający działał sumiennie, logując sukces każdego dnia, ale nie przetworzył żadnego z nich.

W manualnym przepływie pracy recenzent-człowiek zauważyłby pustą skrzynkę odbiorczą lub nagromadzenie oczekujących elementów już pierwszego dnia. W wersji zautomatyzowanej brak aktywności wyglądał dokładnie tak samo jak brak pracy. Cron nie miał menedżera, którego mógłby rozczarować. Po prostu meldował się w pracy i wychodził wcześniej do domu.

Dwa błędy, jeden pusty wynik

Przyczyną awarii były dwa czynniki. Żaden z nich nie był błędem składni, przekroczeniem czasu oczekiwania (timeout) ani awarią zależności. Oba były błędami semantycznymi, które w oczach silnika zapytań sprowadziły dziewiętnaście poprawnych wierszy do zera.

Po pierwsze, niezgodność typów. Agent generujący szkice zapisywał rekordy oznaczone jako article. Cron zatwierdzający wysyłał zapytania konkretnie o typy thread. To rodzaj dryfu, który następuje, gdy producenci i konsumenci ewoluują na równoległych torach. Jeden zespół — lub jeden agent — uznał, że wynikiem jest artykuł. Inny napisał konsumenta, zakładając, że będzie on pobierał wątki (threads). Żaden system typów nie zgłosił błędu podczas kompilacji, ponieważ były to prawdopodobnie luźne tagi tekstowe, być może pola JSON lub nieograniczone wartości varchar. Baza danych po prostu nie znalazła dopasowań i zwróciła pusty zestaw. Dla silnika nie jest to stan błędu. To poprawna odpowiedź na błędne pytanie.

Po drugie, operacja inner join w zapytaniu zatwierdzającego po cichu „połknęła” całe wiersze. Jeśli zapytanie łączyło tabelę szkiców z inną tabelą — być może w celu pobrania metadanych, flag statusu lub reguł routingu — a warunek połączenia nie został spełniony, inner join zachował się dokładnie tak, jak zaprojektowano. Wykluczył wiersze, które nie pasowały. W zbiorze wynikowym nie pojawiły się żadne osierocone wiersze. Żadne wartości null nie zasygnalizowały problemu. Dziewiętnaście szkiców przeszło przez zapytanie jak woda przez sito, a warstwa aplikacji otrzymała nieskazitelnie pustą listę.

Ponieważ zapytanie nie zwróciło żadnych wierszy, funkcja zakończyła działanie bez błędów. Żadne wyjątki nie zostały rzucone. Odpowiedź HTTP brzmiała 200 OK. Cron zalogował sukces i wrócił do snu.

Pułapka „przetworzono zero”

Oto sedno problemu. W systemie opartym na kolejkach konsument często znajduje zero wierszy do przetworzenia. Kolejka się opróżnia. Pracownik kończy szybko. Log wyświetla processed: 0, a zespół odczytuje to jako dobrą wiadomość: nadążamy z popytem. To zdrowy stan.

Ale processed: 0 koduje dwie zupełnie różne rzeczywistości:

  • Stan zdrowy: Zero przetworzonych, ponieważ nie ma oczekujących. Kolejka jest pusta. System jest bezczynny z założenia.
  • Stan uszkodzony: Zero przetworzonych, ponieważ konsument nie widzi pracy. W kolejce jest dziewiętnaście wierszy. System jest ślepy, a nie bezczynny.

Bez niezależnego sprawdzania głębokości kolejki, oba te stany emitują identyczną telemetrię. Wyglądają tak samo na dashboardach, pachną tak samo w agregatorach logów i wywołują tę samą ciszę w PagerDuty. Zbudowałeś strategię monitorowania, która wykrywa, kiedy pracownik krzyczy, a nie kiedy przemyka obok stosu prawdziwej pracy, szepcząc.

Zmniejszenie luki

Elevare Digital rozwiązało problem, zmieniając to, co monitoruje. Przestali polegać wyłącznie na współczynnikach błędów i statusach sukcesu. Zamiast tego zaczęli generować alerty na podstawie różnicy między dostępną pracą a pracą wykonaną.

Po każdym pakiecie (batchu) uruchamiają teraz proste sprawdzenie niezmiennika:

  • Jeśli liczba przetworzonych (processed) wynosi 0 i liczba oczekujących wierszy (pending rows) jest większa niż 0, wygeneruj alert o wysokim priorytecie.

Ta reguła jest celowo niezależna od przyczyny. Nie obchodzi jej, czy błąd wynikał ze złego filtra, uszkodzonego złączenia (join), czy błędnie wpisanego ciągu wyliczeniowego (enum). Liczy się tylko to, że praca istnieje, a żadna nie została wykonana. Przesuwa to monitoring z pytania „Czy proces zgłosił błąd?” na „Czy praca posunęła się naprzód?”.

Aby to wspierać, traktują głębokość kolejki jako metrykę pierwszorzędną, śledzoną w czasie, a nie tylko jako doraźne sprawdzenie. Jeśli producent stale dodaje wiersze, podczas gdy konsument nieustannie raportuje sukces, trend wzrostowy głębokości staje się jednoznacznym dowodem problemu. Statyczna migawka może kłamać, ale narastający backlog nigdy nie kłamie.

Lekcje dla systemów autonomicznych

Incydent w Elevare zawiera kilka praktycznych zasad dla każdego, kto zarządza bezobsługowymi potokami (pipelines).

Loguj przeskanowane wiersze oddzielnie od przetworzonych. Konsument może wykonać zapytanie, które dotyczy czterdziestu wierszy, odfiltrować je wszystkie przez błędne kryteria i zaraportować processed: 0. Jeśli logujesz tylko końcową liczbę, umyka Ci interakcja widmo. Metryka przeskanowanych wierszy (scanned-rows) ujawnia, że pracownik się pojawił, spojrzał na zadanie i odszedł zdezorientowany. Ta luka między przeskanowanymi a przetworzonymi wierszami jest często Twoim najwcześniejszym sygnałem.

Śledź głębokość kolejki jako szereg czasowy. Kolejka, która jest tymczasowo pusta, jest w porządku. Kolejka, która rośnie monotonicznie, podczas gdy statusy workerów pozostają „zielone”, już nie. Wykreśl głębokość względem przepustowości konsumenta. Gdy te dwie wartości zaczynają się rozchodzić, natychmiast przeprowadź dochodzenie, nawet jeśli wszystkie testy sprawności (health checks) przechodzą pomyślnie.

Testuj konsumentów na rzeczywistych danych producenta, a nie tylko na mockach. Testy jednostkowe z użyciem danych typu mock niosą ze sobą założenia testera. Jeśli fabryka mocków produkuje typy thread, a konsument oczekuje typów thread, Twoje testy przejdą, podczas gdy produkcja zawiedzie. Uruchamiaj testy integracyjne, które pobierają rzeczywiste rekordy z wyjścia producenta. Upewnij się, że konsument naprawdę widzi to, co zapisuje producent.

Traktuj typy danych i wartości enum jako kontrakty. Luźne tagi tekstowe w obiektach JSON są wygodne, dopóki nie staną się niewidocznymi punktami awarii. Definiuj schematy w sposób jawny. Udostępniaj stałe. Waliduj ładunki (payloads) na styku producenta i konsumenta. Jeśli kontrakt zostanie złamany, system powinien zgłosić błąd głośno na granicy, a nie po cichu wewnątrz klauzuli WHERE.

Prawdziwy wniosek

Systemy autonomiczne nie zawodzą tak jak ludzie. Nie biorą zwolnień lekarskich, nie rzucają wyjątkami za każdym razem i nie zostawiają oczywistych zrzutów pamięci (crash dumps). Zwracają 200 OK i pozwalają zapasom gnić. Jeśli Twoje alerty reagują tylko na „krzyki”, przeoczysz najdroższe awarie – te, w których wszystko wygląda w porządku, a żadna praca nie zostaje wykonana.

Zaprojektuj swoją obserwowalność (observability) tak, aby monitorowała tę lukę. Porównaj ilość pracy, która wchodzi, z ilością pracy, która wychodzi. Gdy te dwie wartości przestają się zgadzać, załóż, że maszyna Cię okłamuje. Ponieważ czasami idealny log sukcesu jest jedynym objawem systemu, który całkowicie oślepł.