Deploy-ът е успешен, но сайтът не работи
Dashboard-ът ви показва running, а потребителите виждат грешка. Научете защо успешният deploy и здравето на приложението са различни сигнали и как да направите така, че зеленият deploy да означава, че приложението действително отговаря.
Има един специфичен вид лошо утро, което започва със зелена отметка. Deploy-ът е успешен, но сайтът не работи, dashboard-ът показва running, а някой ви изпраща screenshot с грешка 502.
Това не е рядък edge case. Това е предвидимият резултат от платформа, която отчита едно, а измерва друго, и си струва да разберете защо, защото решението не е „проверявайте по-внимателно“ — трябва да промените какво точно има право да означава думата running.
Три различни въпроса, един статус индикатор
Когато дадена платформа казва, че един service работи, тя може да отговаря на всеки от следните въпроси:
- Стартира ли контейнерът? Процесът съществува и не е приключил.
- Отворен ли е port-ът? Нещо слуша там, където платформата очаква.
- Отговаря ли приложението правилно? Заявката получава отговор, който означава, че приложението е готово за работа.
Това са коренно различни гаранции и повечето инциденти от този тип възникват, когато dashboard-ът отговаря на въпрос 1, а вие приемате, че отговаря на въпрос 3.
Node процес, който стартира, не успява да се свърже с базата данни и остава в retry loop, отговаря на въпрос 1 завинаги. Той не е крашнал. Никога няма да обслужи заявка. Контейнерът е „running“ във всеки смисъл, който има значение за orchestrator-а.
Пропастта, в която се случва прекъсването
Опасният период е между „новата версия стартира“ и „новата версия може да работи“. В този период една наивна платформа вече е пренасочила traffic-а, защото единственото, което е измерила, е стартирането.
Това е по-лошо от обикновен crash заради начина, по който работи rollback-ът. Crash loop-ът е шумен: контейнерът приключва, стартира отново, приключва и накрая платформата забелязва проблема. Boot-and-hang е безшумен. Нищо не се рестартира, няма аларми, а предишната работеща версия обикновено вече е премахната.
Именно това е истинската щета. Старата версия е била наред. Тя е премахната, защото новата е стартирала, а стартирането е било объркано с работещо приложение.
Как работи истинският health gate
Решението е структурно, а не процедурно. Traffic-ът не трябва да се пренасочва, преди новата версия да е отговорила на заявка.
В Dockup един release протича така: новата версия се build-ва изолирано, стартира се заедно с версията, която в момента обслужва traffic, а след това ѝ се задава въпрос. Едва когато отговори, домейнът се насочва към нея. Ако никога не отговори, release-ът спира дотук, а предишната версия продължава да обслужва заявките — никой извън dashboard-а ви дори няма да разбере, че е правен опит за deploy.
Затова неуспешният deploy в Dockup не е прекъсване на услугата. Старият контейнер никога не е бил премахван с предположението, че новият ще работи.
# The health gate is per-service configuration, not a platform default you inherit
dockup info my-project/my-api --json
Блокът healthCheck в този output е целият contract: кой path се заявява, колко време да се чака за отговор, колко пъти да се прави опит и колко време да има между опитите.
Настройте проверката така, че да отговаря на въпрос 3
Health endpoint, който винаги връща 200, е по-лош от липсата на такъв, защото превръща истинската gate проверка в формалност. Целта на проверката е да се провали, когато приложението не може да изпълнява работата си.
Полезният readiness endpoint проверява нещата, без които приложението не може да работи:
// 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 })
}
})
Два правила правят това практично:
Проверявайте зависимостите, без които не можете да обслужвате заявки, и нищо повече. Ако приложението ви може да работи с ограничена функционалност, когато search index-ът не работи, не правете readiness проверката зависима от него — ще блокирате deploy-ите заради нещо, което не е outage.
Проверявайте евтино. Endpoint-ът се извиква многократно при всеки release. Readiness проверка, която изпълнява скъпа заявка, създава излишен проблем с натоварването.
Дайте му достатъчно време, но не безкрайно
Две настройки определят дали gate-ът ще помогне или ще навреди:
- Timeout на опит трябва да надвишава най-бавния ви нормален cold start. Приложение, което се свързва с база данни и загрява cache за осем секунди, ще се проваля при проверка от три секунди всеки път и ще го „оправите“, като изключите gate-а — с което ще се върнете в началната точка.
- Retries трябва да покриват общото време за стартиране, а не един опит. Interval × retries е реалният budget.
В Dockup това са healthCheckInterval, healthCheckTimeout и healthCheckRetries, като настройките са за всеки service поотделно, защото един Rails monolith и един Go sidecar не стартират по един и същ график.
Когато сайтът вече не работи
Ако четете това по време на incident, следният ред ще реши проблема най-бързо:
- Проверете дали приложението отговаря директно, като заобиколите домейна. Ако отговаря на своя port, но не и през домейна, това е routing проблем, а не проблем в приложението, така че спрете да debug-вате кода си.
- Прочетете runtime log-овете, а не build log-овете. Build-ът е успешен — това е изходната ни предпоставка. Трябва да разберете какво е направил процесът, след като е стартирал.
- Направете rollback, преди да започнете диагностика. Диагностиката е по-евтина, когато никой не чака.
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
В Dockup rollback-ът е превключване, а не нов build, защото предишната версия все още е на диска. Това има значение в 3 през нощта: най-бързото възстановяване е онова, при което не се налага да компилирате нищо.
Въпросът, който трябва да зададете на една платформа
Когато избирате къде да стартирате production средата си, това е добър тест, който да направите умишлено: deploy-нете приложение, което стартира успешно, но след това не успява да достигне базата си данни. Вижте какво показва dashboard-ът.
Ако показва running, вече знаете точно колко ще струва тази дума при следващия ви incident.
Често задавани въпроси
Защо dashboard-ът ми показва running, когато сайтът не работи? Защото „running“ обикновено означава, че процесът на контейнера съществува, а не че приложението може да обслужи заявка. Процес, който е заседнал в опити за свързване с базата данни, отговаря на тази дефиниция неограничено дълго.
Трябва ли health check-ът да проверява базата данни? Да, ако приложението ви не може да обслужва заявки без нея. Проверявайте зависимостите, от които действително се нуждаете, и пропускайте онези, без които можете да работите с ограничена функционалност.
Каква е разликата между liveness и readiness? Liveness проверява дали процесът трябва да бъде рестартиран. Readiness проверява дали процесът трябва да получава traffic. Gate-ът, който предотвратява този проблем, е readiness и той трябва да се изпълни, преди да бъде пренасочен traffic.
Как мога изобщо да попреча на лош deploy да свали сайта? Пренасочвайте traffic-а едва след като новата версия отговори на реална заявка и запазвайте предишната версия, докато превключването не бъде потвърдено. Така един неуспешен release ще бъде release, който никога не е влизал в употреба, а не outage.
