NaplóindexDockup / terepjegyzet
Note / deploy-succeeded-but-site-is-down

Sikeres deploy, mégis elérhetetlen az oldal

A dashboard szerint az alkalmazás fut, a felhasználók mégis hibát látnak. Tudd meg, miért különböző jelek a deploy sikeressége és az alkalmazás állapota, és hogyan érheted el, hogy a zöld deploy azt jelentse: az app valóban válaszol.

Van egy bizonyos rossz reggeltípus, amely egy zöld pipával kezdődik. A deploy sikeres, mégis elérhetetlen az oldal, a dashboard szerint running, valaki pedig éppen egy 502-es hibáról küld képernyőképet.

Ez nem ritka edge case. Kiszámítható következménye annak, hogy a platform valami mást jelent, mint amit mér, ezért érdemes pontosan megérteni: a megoldás nem az, hogy „alaposabban ellenőrzöd” — hanem az, hogy megváltoztatod, mit jelenthet a running állapot.

Három különböző kérdés, egyetlen állapotjelző

Amikor egy platform azt mondja, hogy egy service fut, valójában az alábbi kérdések bármelyikére válaszolhat:

  1. Elindult a container? A process létezik, és nem állt le.
  2. Nyitva van a port? Valami figyel azon a címen, ahol a platform ezt várja.
  3. Megfelelően válaszol az alkalmazás? A request olyan választ kap, amely azt jelenti, hogy az app készen áll a működésre.

Ezek nagyon különböző garanciák, és az ilyen jellegű incidentek többségét az okozza, hogy a dashboard az 1. kérdésre válaszol, miközben te a 3. kérdésre adott választ feltételezted.

Egy elinduló Node process, amely nem tud kapcsolódni az adatbázisához, majd retry loopba kerül, korlátlan ideig megfelel az 1. kérdésnek. Nem omlott össze. Requesteket viszont soha nem fog kiszolgálni. A container az orchestrator szempontjából minden értelemben „fut”.

A rés, ahol a kiesés történik

A veszélyes időszak az új verzió elindulása és aközött telik el, hogy az új verzió már működni is képes. Ebben az időablakban egy naiv platform már átterelte a forgalmat, mert csak az indulást mérte.

Ezt az teszi rosszabbá egy egyszerű crashnél, hogy milyen a rollback története. A crash loop hangos: a container leáll, újraindul, ismét leáll, a platform pedig idővel észreveszi. Az elindulás utáni megakadás néma. Semmi nem indul újra, nem szólal meg riasztás, és a korábban működő verziót addigra általában már le is állították.

Ez az igazi kár. A régi verzió rendben működött. Azért távolították el, mert elindult egy új, és az indulást összetévesztették a működéssel.

Mit csinál egy valódi health gate?

A megoldás strukturális, nem procedurális. A forgalmat nem szabad átterelni addig, amíg az új verzió nem válaszolt egy requestre.

A Dockupon egy release így fut: az új verzió elkülönítve buildelődik, a jelenleg kiszolgáló verzió mellett elindul, majd kap egy kérdést. A domain csak akkor mutat rá, amikor választ ad. Ha soha nem válaszol, a release ott megáll, a korábbi verzió pedig tovább szolgál ki — a dashboardodon kívül senki nem tudja meg, hogy egy deploy-kísérlet történt.

Ezért nem jár kieséssel egy sikertelen deploy a Dockupon. A régi containert soha nem távolították el abból a feltételezésből kiindulva, hogy az új majd rendben működik.

# The health gate is per-service configuration, not a platform default you inherit
dockup info my-project/my-api --json

A kimenetben szereplő healthCheck blokk a teljes szerződés: melyik pathot kell lekérni, mennyit kell várni a válaszra, hányszor kell próbálkozni, és mennyi idő teljen el a próbálkozások között.

Úgy konfiguráld az ellenőrzést, hogy a 3. kérdésre válaszoljon

A feltétel nélkül 200-as választ adó health endpoint rosszabb, mint ha egyáltalán nem lenne, mert egy valódi gate-et gumibélyegzővé alakít. Az ellenőrzés célja, hogy hibát jelezzen, amikor az alkalmazás nem képes ellátni a feladatát.

Egy hasznos readiness endpoint ellenőrzi azokat a dolgokat, amelyek nélkül az app nem tud működni:

// 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 })
  }
})

Két szabály teszi ezt a gyakorlatban is működőképessé:

Ellenőrizd azokat a dependencyket, amelyek nélkül nem tudsz kiszolgálni, és semmi mást. Ha az app képes megfelelően működni akkor is, amikor a search index nem érhető el, ne a search indextől tedd függővé a readiness állapotát — különben olyan miatt blokkolod a deployokat, ami nem jelent kiesést.

Legyen olcsó. Az endpointot minden release során ismételten meghívjuk. Egy drága queryt futtató readiness check önmagad által előidézett terhelési problémát okoz.

Adj neki elegendő időt, de ne korlátlanul

Két beállítás dönti el, hogy a gate segít vagy árt:

  • Az egy próbálkozásra jutó timeout legyen hosszabb a leglassabb, de még normális cold startodnál. Egy olyan app, amelynek nyolc másodperc kell az adatbázishoz való kapcsolódáshoz és a cache felmelegítéséhez, minden alkalommal elbukik egy három másodperces ellenőrzésen, te pedig úgy „javítod meg”, hogy kikapcsolod a gate-et — ezzel visszajutsz a kiindulópontra.
  • A retry-k a teljes indulási időt fedjék le, ne csak egy próbálkozást. Az intervallum × retry-k száma adja a tényleges keretet.

A Dockupon ezek a healthCheckInterval, healthCheckTimeout és healthCheckRetries, és service-enként állíthatók, mert egy Rails monolith és egy Go sidecar nem ugyanazon az ütemezésen indul el.

Amikor már elérhetetlen

Ha ezt egy incident közepén olvasod, a leggyorsabban célra vezető sorrend a következő:

  1. Ellenőrizd, hogy az app közvetlenül válaszol-e, a domaint megkerülve. Ha a portján válaszol, de a domainen keresztül nem, akkor routingproblémáról van szó, nem alkalmazásproblémáról, ezért hagyd abba a kód hibakeresését.
  2. A runtime logokat olvasd, ne a build logokat. A build sikeres volt — ez az alapfeltevés. Arra vagy kíváncsi, mit csinált a process az indulása után.
  3. Még a diagnosztika előtt rollbackelj. A diagnózis olcsóbb, ha senki nem várja közben a megoldást.
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

A Dockupon a rollback váltás, nem új build, mert az előző verzió továbbra is a lemezen van. Ez hajnali háromkor sokat számít: a leggyorsabb recovery az, amelyhez nem kell semmit lefordítani.

Ezt a kérdést tedd fel egy platformnak

Amikor kiválasztod, hol futtasd a productiont, érdemes ezt szándékosan letesztelni: deployolj egy olyan alkalmazást, amely sikeresen elindul, majd nem tud kapcsolódni az adatbázisához. Figyeld meg, mit mond a dashboard.

Ha azt írja, hogy running, most már pontosan tudod, mennyit ér ez a szó a következő incident során.

Gyakran ismételt kérdések

Miért írja a dashboard, hogy running, amikor az oldal elérhetetlen? Mert a „running” általában azt jelenti, hogy a container process létezik, nem pedig azt, hogy az alkalmazás képes kiszolgálni egy requestet. Egy adatbázis-kapcsolatra váró, folyamatosan retryzó process korlátlan ideig megfelel ennek a definíciónak.

A health checknek el kell érnie az adatbázist? Igen, ha az alkalmazás enélkül nem tud requesteket kiszolgálni. Azokat a dependencyket ellenőrizd, amelyekre valóban szükséged van, a többit pedig hagyd ki, ha nélkülük képes vagy korlátozottan működni.

Mi a különbség a liveness és a readiness között? A liveness azt kérdezi, hogy újra kell-e indítani a processt. A readiness azt kérdezi, hogy kaphat-e forgalmat. A hibát megelőző gate a readiness, és ennek a forgalom átterelése előtt kell lefutnia.

Hogyan akadályozhatom meg, hogy egy hibás deploy egyáltalán elérhetetlenné tegye az oldalt? Csak akkor váltsd át a forgalmat, amikor az új verzió egy valódi requestre válaszol, és tartsd meg a korábbi verziót az átváltás megerősítéséig. Így a sikertelen release olyan release lesz, amely soha nem történt meg, nem pedig kiesés.