Health check се проваля, но приложението работи
Когато health check се проваля при deployment, а приложението работи без проблем локално, причината обикновено е една от пет. Проверете binding-а, пътя, порта, времето за стартиране и зависимостите в реда, който най-бързо ще ви насочи към проблема.
Има един особено неприятен вид блокиране, при който неуспешен health check при deployment спира всеки release, въпреки че приложението е напълно изправно според всичко, до което имате достъп. Работи локално. Работи и в Docker локално. Логовете показват, че слуша. А платформата отчита поредица от грешки, понякога двадесет една след друга, без нито една заявка да се появи в access log-а.
Именно последната подробност е важна и веднага стеснява кръга от възможности. Ако приложението ви никога не е записало заявката в лог, проверката не е достигнала до него — следователно нищо в кода на приложението не може да обясни проблема.
Ето петте причини, подредени така, че да откриете проблема възможно най-бързо.
1. Свързани сте към localhost
Това е най-честата причина и обяснява напълно симптома „никога не пристига трафик“.
В контейнер 127.0.0.1 означава собствения loopback интерфейс на този контейнер. Health check, който идва отвън, не може да достигне до него. Процесът слуша, логовете ви го потвърждават, но socket-ът е недостъпен отвсякъде, откъдето има значение.
// Unreachable from outside the container
app.listen(3000, '127.0.0.1')
// Correct
app.listen(3000, '0.0.0.0')
Framework-овете имат различни стойности по подразбиране, а някои от тях са променили тази стойност между основните версии. Проверете към какъв адрес действително се свързва framework-ът ви, вместо да разчитате на спомените си.
# Confirm from inside the running container
dockup exec "ss -ltn || netstat -ltn" my-project/my-api
Ако адресът за слушане е 127.0.0.1:3000, а не 0.0.0.0:3000, сте открили проблема и нищо останало от този списък няма значение.
2. Портът, който платформата проверява, не е портът, на който обслужвате заявки
Тук участват два порта и лесно можете да ги объркате: портът, на който процесът ви слуша вътре в контейнера, и портът, към който платформата пренасочва трафика. Ако приложението ви чете PORT от environment-а, а някъде в Dockerfile сте задали 3000 твърдо, двата порта може да се разминават без никакво предупреждение.
Надеждният подход е да оставите платформата да ви каже:
const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')
След това задайте порта на service-а веднъж — в платформата — и спрете да поддържате номера на две места.
3. Пътят връща нещо различно от успешен статус
Health check path-ът се сравнява точно, а изненадващо много проблеми са причинени от redirect. Ако приложението ви пренасочва /healthz към /healthz/ или налага HTTPS с 301, checker, който приема само 2xx за успех, ще се проваля всеки път, докато браузърът следва redirect-а и ви показва работеща страница.
Три конкретни капана:
- Redirect-и за наклонена черта накрая.
/healthz→/healthz/е 301. - Принудителен HTTPS. Вътрешната проверка обикновено пристига през обикновен HTTP на loopback. Безусловният HTTPS redirect ще я провали.
- Auth middleware. Глобален authentication guard, който се изпълнява преди routing-а, ще върне 401 и за health path-а.
Изключете health path-а изрично от auth и от налагането на HTTPS. Това е единственият route, който трябва да бъде скучен.
4. Проверката приключва преди cold start-ът да завърши
Ако проверката се провали няколко пъти, а след това мине, или се проваля при deploy, но минава при повторен опит, проблемът е във времето, а не в конфигурацията.
Бюджетът, който ви е необходим, не е за един опит — той е интервал × брой повторения. Приложение, което се нуждае от дванадесет секунди, за да се свърже с database и да загрее cache, има нужда от общ бюджет над дванадесет секунди. В противен случай ще проваляте всеки release и накрая ще изключите gate-а, с което премахвате единственото нещо, което стои между счупения build и потребителите ви.
dockup info my-project/my-api --json | grep -A6 healthCheck
Задайте timeout над най-бавния легитимен единичен опит и настройте броя повторения така, че интервалът × броят повторения да надвишава с достатъчен запас най-бавното легитимно стартиране. Измерете времето за стартиране, вместо да го предполагате — логовете съдържат timestamps.
5. Приложението действително не е готово
Последният случай е именно причината, поради която съществува проверката: приложението ви се е стартирало, не е успяло да достигне зависимост и прави нови опити. То не е crash-нало, така че нищо не го рестартира. Не може да обслужва заявки, затова проверката се проваля. Системата работи точно както е проектирана и ви казва, че този release не трябва да получава трафик.
Начинът да различите този случай от останалите четири е, че приложението ви е записало заявката в лога и е отговорило с non-2xx статус. Ако заявката се появява в логовете, причините от 1 до 3 са изключени.
Диагностичният ред, който пести време
# 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
Стъпка 3 разрешава повечето от тези проблеми. curl отвътре на контейнера премахва всички мрежови променливи наведнъж: ако връща 200 там, а платформата все още отчита грешка, проблемът е в адреса или порта, а не в приложението. Ако връща 301 или 401, сте открили причината, без изобщо да променяте платформата.
Защо си струва да запазите gate-а
След четвъртия неуспешен deploy е изкушаващо да изключите health check-а и да пуснете release-а. Струва си да си припомните какво точно изключвате.
В Dockup health gate-ът е това, което не допуска счупен release до потребителите ви. Новата версия се build-ва и стартира, докато текущата продължава да обслужва заявки; трафикът се прехвърля едва когато новата версия започне да отговаря. Ако изключите gate-а, отново активирате сценария, при който контейнер, който се стартира, но не може да работи, заменя изправен контейнер.
Проверка, която се проваля при четири поредни release-а, е досадна. Проверка, която винаги минава, е проверка, която няма да спре deployment-а, който действително има значение.
Често задавани въпроси
Защо health check-ът се проваля, когато приложението работи локално?
Почти винаги защото контейнерът е свързан към 127.0.0.1 вместо към 0.0.0.0. Локално се свързвате през същия loopback; отвън контейнера този адрес е недостъпен.
Трябва ли health endpoint-ът да изисква authentication? Не. Изключете го от глобалния auth middleware, иначе checker-ът ще получи 401 и deploy-ът ще се провали, въпреки че приложението работи.
Какъв timeout трябва да използвам? По-дълъг от най-бавния ви легитимен единичен опит, с достатъчно повторения за най-бавния ви легитимен cold start. Извадете времето за стартиране от логовете, вместо да го предполагате.
Безопасно ли е да изключа health check-а, за да пусна release-а? Това ще пусне release-а, но ще премахне защитата, която не допуска счупена версия до трафика. Вместо това поправете проверката — в повечето случаи причината е bind адрес или redirect и отстраняването ѝ отнема минути.
