Deployment zakończony sukcesem, ale strona nie działa
Dashboard informuje, że aplikacja działa, ale użytkownicy widzą błąd. Dowiedz się, dlaczego sukces deploymentu i kondycja aplikacji to różne sygnały oraz jak sprawić, by zielony status deploymentu oznaczał, że aplikacja rzeczywiście odpowiada.
Istnieje szczególny rodzaj złego poranka, który zaczyna się od zielonego znacznika wyboru. Deployment zakończył się sukcesem, ale strona nie działa, dashboard informuje, że aplikacja jest uruchomiona, a ktoś wysyła Ci zrzut ekranu z błędem 502.
To nie jest rzadki edge case. To przewidywalny skutek tego, że platforma raportuje jedną rzecz, a mierzy inną — warto dokładnie zrozumieć, na czym polega problem, ponieważ rozwiązaniem nie jest „sprawdzanie dokładniej”, lecz zmiana tego, co może oznaczać słowo uruchomiona.
Trzy różne pytania, jedna kontrolka statusu
Gdy platforma informuje, że usługa działa, może odpowiadać na dowolne z tych pytań:
- Czy kontener został uruchomiony? Proces istnieje i nie zakończył działania.
- Czy port jest otwarty? Coś nasłuchuje tam, gdzie oczekuje tego platforma.
- Czy aplikacja odpowiada poprawnie? Żądanie otrzymuje odpowiedź oznaczającą, że aplikacja jest gotowa do pracy.
To zupełnie różne gwarancje, a większość incydentów tego typu wynika z tego, że dashboard odpowiada na pytanie 1, podczas gdy Ty zakładasz, że odpowiada na pytanie 3.
Proces Node, który się uruchamia, nie może połączyć się z bazą danych i pozostaje w pętli ponawiania prób, spełnia pytanie 1 bez końca. Nie uległ awarii. Nigdy nie obsłuży żadnego żądania. Kontener jest „uruchomiony” w każdym znaczeniu, które ma znaczenie dla orchestratora.
Luka, w której powstaje awaria
Niebezpieczne okno czasowe występuje między „nowa wersja została uruchomiona” a „nowa wersja może działać”. W tym czasie naiwna platforma przekazała już ruch, ponieważ jedyną mierzoną rzeczą było uruchomienie.
To gorsze niż zwykła awaria, ponieważ komplikuje rollback. Pętla crashy jest głośna: kontener kończy działanie, uruchamia się ponownie, znów kończy działanie, a platforma w końcu to zauważa. Uruchomienie i zawieszenie jest ciche. Nic się nie uruchamia ponownie, nie pojawiają się alarmy, a poprzednia działająca wersja zwykle została już usunięta.
To właśnie ostatnia część powoduje największe szkody. Stara wersja działała poprawnie. Została usunięta, ponieważ uruchomiła się nowa, a uruchomienie pomylono z działaniem.
Jak działa prawdziwa bramka health check
Rozwiązanie ma charakter strukturalny, a nie proceduralny. Ruch nie powinien być przekazywany, dopóki nowa wersja nie odpowie na żądanie.
Na Dockup release przebiega następująco: nowa wersja jest budowana w izolacji, uruchamiana obok wersji, która obecnie obsługuje ruch, a następnie otrzymuje pytanie. Dopiero gdy odpowie, domena wskazuje na nową wersję. Jeśli nigdy nie odpowie, release zatrzymuje się na tym etapie, a poprzednia wersja nadal obsługuje ruch — nikt poza Twoim dashboardem nie dowie się nawet, że podjęto próbę wykonania deploymentu.
Dlatego nieudany deployment na Dockup nie oznacza awarii. Stary kontener nigdy nie został usunięty na założeniu, że nowy będzie działać poprawnie.
# The health gate is per-service configuration, not a platform default you inherit
dockup info my-project/my-api --json
Blok healthCheck w tym wyniku stanowi całą umowę: określa, jaka ścieżka jest odpytywana, jak długo należy czekać na odpowiedź, ile razy ponowić próbę i jak długo czekać między próbami.
Skonfiguruj check tak, aby odpowiadał na pytanie 3
Endpoint health, który bezwarunkowo zwraca kod 200, jest gorszy niż jego brak, ponieważ zmienia prawdziwą bramkę w formalność. Celem checku jest zgłoszenie niepowodzenia, gdy aplikacja nie może wykonywać swojej pracy.
Przydatny endpoint readiness sprawdza rzeczy, bez których aplikacja nie może działać:
// Not this — it proves only that the process is alive
app.get('/healthz', (req, res) => res.send('ok'))
// This — it proves the app can actually serve a request
app.get('/healthz', async (req, res) => {
try {
await db.query('select 1') // the dependency that is usually the problem
if (!cacheReady) throw new Error('cache warming')
res.status(200).json({ ok: true })
} catch (err) {
res.status(503).json({ ok: false, reason: err.message })
}
})
Dwie zasady sprawiają, że działa to w praktyce:
Sprawdzaj zależności, bez których aplikacja nie może obsługiwać żądań, i nic więcej. Jeśli aplikacja może działać w ograniczonym trybie, gdy indeks wyszukiwania jest niedostępny, nie uzależniaj readiness od indeksu wyszukiwania — zablokujesz deploymenty z powodu czegoś, co nie jest awarią.
Zadbaj o niskie koszty wykonania. Endpoint jest wywoływany wielokrotnie podczas każdego release'u. Check readiness, który wykonuje kosztowne zapytanie, stwarza problem z obciążeniem na własne życzenie.
Daj mu wystarczająco dużo czasu, ale nie nieskończenie wiele
O tym, czy bramka pomaga, czy szkodzi, decydują dwa ustawienia:
- Timeout pojedynczej próby powinien być dłuższy niż najwolniejszy uzasadniony cold start. Aplikacja, która łączy się z bazą danych i rozgrzewa cache w osiem sekund, za każdym razem nie przejdzie checku trwającego trzy sekundy, a Ty „naprawisz” problem, wyłączając bramkę — i wrócisz do punktu wyjścia.
- Liczba ponowień powinna obejmować całkowity czas uruchamiania, a nie pojedynczą próbę. Rzeczywisty budżet to interwał × liczba ponowień.
Na Dockup są to healthCheckInterval, healthCheckTimeout i healthCheckRetries. Są konfigurowane per service, ponieważ monolit Rails i sidecar w Go nie uruchamiają się według tego samego harmonogramu.
Gdy aplikacja już nie działa
Jeśli czytasz ten tekst w trakcie incydentu, poniższa kolejność najszybciej prowadzi do rozwiązania problemu:
- Sprawdź, czy aplikacja odpowiada bezpośrednio, z pominięciem domeny. Jeśli odpowiada na swoim porcie, ale nie przez domenę, jest to problem z routingiem, a nie problem z aplikacją — przestań więc debugować kod.
- Przejrzyj runtime logs, a nie build logs. Build zakończył się sukcesem — to założenie wyjściowe. Chcesz sprawdzić, co proces zrobił po uruchomieniu.
- Wykonaj rollback, zanim zaczniesz diagnozować. Diagnozowanie jest tańsze, gdy nikt nie czeka.
dockup logs my-project/my-api --follow # what the running process is saying
dockup deployments my-project/my-api # what was live before this
dockup rollback <deployment-id> my-project/my-api # put that back
Na Dockup rollback jest przełączeniem, a nie ponownym buildem, ponieważ poprzednia wersja nadal znajduje się na dysku. To ma znaczenie o 3 nad ranem: najszybsze odzyskanie działania to takie, które nie wymaga kompilowania czegokolwiek.
Pytanie, które warto zadać platformie
Gdy wybierasz miejsce do uruchamiania produkcji, warto celowo to przetestować: wdrożyć aplikację, która uruchamia się poprawnie, a następnie nie może połączyć się z bazą danych. Obserwuj, co pokazuje dashboard.
Jeśli informuje, że aplikacja jest uruchomiona, wiesz już dokładnie, ile będzie warte to słowo podczas następnego incydentu.
Najczęściej zadawane pytania
Dlaczego mój dashboard informuje, że aplikacja działa, skoro strona jest niedostępna? Ponieważ „uruchomiona” zwykle oznacza, że proces kontenera istnieje, a nie że aplikacja może obsłużyć żądanie. Proces, który utknął na ponawianiu połączenia z bazą danych, spełnia tę definicję bezterminowo.
Czy health check powinien odpytywać bazę danych? Tak, jeśli aplikacja nie może bez niej obsługiwać żądań. Sprawdzaj zależności, których naprawdę potrzebujesz, i pomijaj te, bez których aplikacja może działać w ograniczonym trybie.
Jaka jest różnica między liveness a readiness? Liveness sprawdza, czy proces powinien zostać uruchomiony ponownie. Readiness sprawdza, czy powinien otrzymywać ruch. Bramką, która zapobiega temu problemowi, jest readiness — musi ona zadziałać, zanim ruch zostanie przekazany.
Jak całkowicie uniemożliwić, by nieudany deployment wyłączył stronę? Przekazuj ruch dopiero wtedy, gdy nowa wersja odpowie na prawdziwe żądanie, i zachowaj poprzednią wersję do momentu potwierdzenia przełączenia. Wtedy nieudany release będzie release'em, który nigdy się nie wydarzył, a nie awarią.
