Health check kończy się niepowodzeniem, ale aplikacja działa
Nieudany health check podczas wdrażania, mimo że aplikacja działa lokalnie bez problemów, zwykle ma jedną z pięciu przyczyn. Sprawdź kolejno adres nasłuchiwania, ścieżkę, port, czas uruchamiania i zależności, aby jak najszybciej znaleźć problem.
Istnieje szczególny rodzaj utknięcia, w którym nieudany health check podczas wdrażania blokuje każde wydanie, mimo że aplikacja — pod każdym możliwym do sprawdzenia względem — działa bez zarzutu. Działa lokalnie. Działa lokalnie w Dockerze. Logi pokazują, że nasłuchuje. Platforma zgłasza jednak kolejne niepowodzenia, czasem dwadzieścia z rzędu, a w logu dostępu nie pojawia się ani jedno żądanie.
Ten ostatni szczegół jest najważniejszy i od razu zawęża pole poszukiwań. Jeśli aplikacja nigdy nie zarejestrowała żądania, oznacza to, że check do niej nie dotarł — a więc nic w kodzie aplikacji tego nie wyjaśni.
Oto pięć przyczyn, uporządkowanych tak, aby jak najszybciej znaleźć problem.
1. Nasłuchujesz na localhost
To zdecydowanie najczęstsza przyczyna i dokładnie wyjaśnia objaw „żaden ruch nigdy nie dociera”.
W kontenerze 127.0.0.1 oznacza własny loopback tego kontenera. Health check przychodzący z zewnątrz kontenera nie może do niego dotrzeć. Proces nasłuchuje, logi to potwierdzają, ale socket jest nieosiągalny z żadnego miejsca, które ma tu znaczenie.
// Unreachable from outside the container
app.listen(3000, '127.0.0.1')
// Correct
app.listen(3000, '0.0.0.0')
Frameworki mają różne wartości domyślne, a w kilku z nich zmieniły się one między głównymi wersjami. Sprawdź, na jakim adresie framework faktycznie nasłuchuje, zamiast polegać na tym, co pamiętasz.
# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
Jeśli adres nasłuchiwania to 127.0.0.1:3000, a nie 0.0.0.0:3000, problem został znaleziony i żadna z pozostałych pozycji na tej liście nie ma już znaczenia.
2. Port sprawdzany przez platformę nie jest portem, na którym serwujesz aplikację
W grę wchodzą dwa porty i łatwo je pomylić: port, na którym proces nasłuchuje wewnątrz kontenera, oraz port, na który platforma kieruje ruch. Jeśli aplikacja odczytuje PORT ze środowiska, a w którymś miejscu Dockerfile masz na sztywno wpisane 3000, te wartości mogą się po cichu różnić.
Niezawodny wzorzec polega na tym, aby pozwolić platformie przekazać port:
const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')
Następnie ustaw port usługi raz, na platformie, i przestań utrzymywać ten numer w dwóch miejscach.
3. Ścieżka zwraca coś innego niż status sukcesu
Ścieżka health check jest dopasowywana dokładnie, a zaskakująco wiele niepowodzeń wynika z przekierowania. Jeśli aplikacja przekierowuje /healthz do /healthz/ albo wymusza HTTPS za pomocą 301, checker, który uznaje za sukces wyłącznie statusy 2xx, za każdym razem zgłosi niepowodzenie, podczas gdy przeglądarka podąży za przekierowaniem i pokaże działającą stronę.
Trzy konkretne pułapki:
- Przekierowania związane z końcowym ukośnikiem.
/healthz→/healthz/to 301. - Wymuszanie HTTPS. Wewnętrzny check zwykle przychodzi po zwykłym HTTP przez loopback. Bezwarunkowe przekierowanie do HTTPS spowoduje jego niepowodzenie.
- Middleware uwierzytelniania. Globalny guard autoryzacji uruchamiany przed routingiem zwróci 401 również dla ścieżki health check.
Jawnie wyklucz ścieżkę health check zarówno z uwierzytelniania, jak i z wymuszania HTTPS. To jedyna trasa, która powinna być całkowicie nudna.
4. Check jest szybszy niż uruchamianie aplikacji
Jeśli check kończy się niepowodzeniem kilka razy, a potem przechodzi, albo nie działa podczas wdrażania, ale przechodzi po ponowieniu, przyczyną jest timing, a nie konfiguracja.
Potrzebny budżet to nie czas jednej próby — to interwał × liczba ponowień. Aplikacja, która potrzebuje dwunastu sekund na połączenie z bazą danych i rozgrzanie cache, wymaga łącznego budżetu przekraczającego dwanaście sekund. W przeciwnym razie każde wydanie zakończy się niepowodzeniem, a ostatecznie wyłączysz bramkę, eliminując jedyną rzecz, która chroni użytkowników przed uszkodzonym buildem.
dockup info my-project/my-api --json | grep -A6 healthCheck
Ustaw timeout powyżej czasu najwolniejszej uzasadnionej pojedynczej próby, a liczbę ponowień tak, aby interwał × liczba ponowień z zapasem przekraczały czas najwolniejszego uzasadnionego uruchamiania. Zmierz czas uruchamiania zamiast zgadywać — logi zawierają znaczniki czasu.
5. Aplikacja rzeczywiście nie jest gotowa
Ostatni przypadek to właśnie ten, dla którego istnieje check: aplikacja się uruchomiła, nie mogła połączyć się z zależnością i ponawia próby. Nie uległa awarii, więc nic jej nie restartuje. Nie może obsługiwać żądań, więc check kończy się niepowodzeniem. System działa dokładnie tak, jak został zaprojektowany, i informuje, że to wydanie nie powinno jeszcze otrzymywać ruchu.
Sposobem na odróżnienie tego przypadku od pozostałych czterech jest to, że aplikacja zarejestrowała żądanie i odpowiedziała statusem innym niż 2xx. Jeśli żądanie pojawia się w logach, przyczyny od 1 do 3 są wykluczone.
Kolejność diagnostyki, która oszczędza czas
# 1. Did the request reach the app at all?
dockup logs my-project/my-api --follow
# 2. What is the process actually bound to?
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
# 3. Does the path answer from inside the container?
dockup exec "curl -si localhost:3000/healthz" my-project/my-api
# 4. What is the gate configured to expect?
dockup info my-project/my-api --json
To krok 3 rozwiązuje większość tych problemów. curl uruchomiony wewnątrz kontenera eliminuje wszystkie zmienne sieciowe jednocześnie: jeśli zwraca 200, a platforma nadal zgłasza niepowodzenie, problem dotyczy adresu lub portu, a nie aplikacji. Jeśli zwraca 301 albo 401, przyczyna została znaleziona bez dotykania konfiguracji platformy.
Dlaczego warto zachować bramkę
Po czwartym nieudanym wdrożeniu kuszące może być wyłączenie health check i wypuszczenie wydania. Warto jednak pamiętać, co w ten sposób wyłączasz.
W Dockup bramka zdrowia jest mechanizmem, który nie dopuszcza uszkodzonego wydania do użytkowników. Nowa wersja jest budowana i uruchamiana, podczas gdy bieżąca nadal obsługuje ruch; ruch zostaje przełączony dopiero wtedy, gdy nowa wersja zacznie odpowiadać. Wyłączenie bramki ponownie włącza scenariusz, w którym uruchamiający się, ale niedziałający kontener zastępuje ten, który działał poprawnie.
Check, który kończy się niepowodzeniem przy czterech kolejnych wydaniach, jest irytujący. Check, który zawsze przechodzi, nie zatrzyma wdrożenia, które naprawdę powinien zatrzymać.
Najczęściej zadawane pytania
Dlaczego health check kończy się niepowodzeniem, gdy aplikacja działa lokalnie?
Niemal zawsze dlatego, że kontener nasłuchuje na 127.0.0.1 zamiast na 0.0.0.0. Lokalnie łączysz się przez ten sam loopback, ale z zewnątrz kontenera ten adres jest nieosiągalny.
Czy endpoint health powinien wymagać uwierzytelniania? Nie. Wyklucz go z globalnego middleware uwierzytelniania, bo w przeciwnym razie checker otrzyma 401, a wdrożenie zakończy się niepowodzeniem, mimo że aplikacja działa poprawnie.
Jakiego timeoutu powinienem użyć? Dłuższego niż czas najwolniejszej uzasadnionej pojedynczej próby, z liczbą ponowień obejmującą najwolniejsze uzasadnione uruchamianie aplikacji. Odczytaj czas uruchamiania z logów zamiast zgadywać.
Czy bezpiecznie jest wyłączyć health check, aby odblokować wydanie? Odblokuje to wydanie, ale usunie ochronę, która powstrzymuje przejęcie ruchu przez uszkodzoną wersję. Zamiast tego napraw check — w większości przypadków przyczyną jest adres nasłuchiwania albo przekierowanie, a rozwiązanie problemu zajmuje kilka minut.
