JournalindexDockup / fältanteckning
Note / deploy-succeeded-but-site-is-down

Deployen lyckades, men sajten ligger nere

Dashboarden visar att tjänsten körs, men användarna möts av ett fel. Lär dig varför en lyckad deploy och applikationens hälsa är olika signaler, och hur du får en grön deploy att betyda att appen faktiskt svarar.

Det finns en särskild sorts dålig morgon som börjar med en grön bock. Deployen lyckades, men sajten ligger nere, dashboarden visar körs, och någon skickar dig en skärmdump av ett 502-fel.

Det här är inget sällsynt specialfall. Det är det förutsägbara resultatet av att en plattform rapporterar en sak och mäter en annan, och det är värt att förstå exakt varför — lösningen är inte att "kontrollera noggrannare", utan att ändra vad ordet körs faktiskt får betyda.

Tre olika frågor, en statusindikator

När en plattform säger att en tjänst körs kan den svara på vilken som helst av följande frågor:

  1. Startade containern? Processen finns och har inte avslutats.
  2. Är porten öppen? Något lyssnar där plattformen förväntar sig det.
  3. Svarar applikationen korrekt? En request får ett svar som betyder att appen är redo att fungera.

Det här är helt olika garantier, och de flesta incidenter av den här typen uppstår för att dashboarden svarar på fråga 1 medan du antog fråga 3.

En Node-process som startar, misslyckas med att ansluta till sin databas och fastnar i en retry-loop uppfyller fråga 1 för alltid. Den har inte kraschat. Den kommer aldrig att hantera en request. Containern är "körande" i alla avseenden som orkestratorn bryr sig om.

Glappet där driftstörningen uppstår

Det farliga tidsfönstret är mellan "den nya versionen har startat" och "den nya versionen fungerar". Under det fönstret har en naiv plattform redan börjat skicka trafik, eftersom det enda den mätte var om tjänsten startade.

Det som gör detta värre än en vanlig krasch är rollback-historien. En crash loop märks: containern avslutas, startas om, avslutas igen, och plattformen upptäcker det till slut. En process som startar och sedan hänger är tyst. Ingenting startas om, inga larm går och den tidigare fungerande versionen har oftast redan tagits bort.

Det sista är den verkliga skadan. Den gamla versionen fungerade. Den togs bort eftersom en ny version hade startat, och att starta hade förväxlats med att fungera.

Vad en riktig health gate gör

Lösningen är strukturell, inte procedurbaserad. Trafiken ska inte flyttas förrän den nya versionen har svarat på en request.

På Dockup fungerar en release så här: den nya versionen byggs isolerat, startas bredvid versionen som hanterar trafiken och får sedan svara på en fråga. Först när den svarar pekar domänen på den. Om den aldrig svarar stoppas releasen där, och den tidigare versionen fortsätter att hantera trafiken — ingen utanför din dashboard märker ens att en deploy försöktes.

Det är därför en misslyckad deploy på Dockup inte är en driftstörning. Den gamla containern togs aldrig bort med antagandet att den nya skulle fungera.

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

healthCheck-blocket i resultatet är hela avtalet: vilken path som anropas, hur länge man ska vänta på ett svar, hur många gånger man ska försöka och hur länge det ska vara mellan försöken.

Konfigurera kontrollen så att den besvarar fråga 3

En health endpoint som alltid returnerar 200 är sämre än ingen alls, eftersom den förvandlar en riktig gate till en stämpelmaskin. Poängen med kontrollen är att den ska misslyckas när applikationen inte kan utföra sitt jobb.

En användbar readiness endpoint verifierar sådant som appen inte kan fungera utan:

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

Två regler gör att detta fungerar i praktiken:

Kontrollera beroenden du inte kan hantera requests utan, och inget annat. Om appen kan fungera med begränsad funktionalitet när sökindexet ligger nere ska du inte låta readiness bero på sökindexet — då blockerar du deployer för något som inte är en driftstörning.

Håll kontrollen billig. Endpointen anropas upprepade gånger under varje release. En readiness-kontroll som kör en dyr query skapar ett onödigt belastningsproblem.

Ge den tillräckligt med tid, men inte obegränsat med tid

Två inställningar avgör om gaten hjälper eller stjälper:

  • Timeout per försök ska vara längre än din långsammaste legitima cold start. En app som ansluter till en databas och värmer upp en cache på åtta sekunder kommer att misslyckas med en kontroll på tre sekunder varje gång, och du kommer att "lösa" det genom att stänga av gaten — vilket tar dig tillbaka till utgångsläget.
  • Antalet försök ska täcka den totala starttiden, inte ett enskilt försök. Intervall × antal försök är den verkliga tidsbudgeten.

På Dockup heter de healthCheckInterval, healthCheckTimeout och healthCheckRetries, och de gäller per tjänst eftersom en Rails-monolit och en Go-sidecar inte startar enligt samma schema.

När den redan ligger nere

Om du läser detta mitt under en incident är följande ordning snabbast för att lösa den:

  1. Kontrollera om appen svarar direkt, förbi domänen. Om den svarar på sin port men inte via domänen är det ett routingproblem, inte ett applikationsproblem, och du bör sluta felsöka koden.
  2. Läs runtime-loggarna, inte build-loggarna. Builden lyckades — det är utgångspunkten. Det du vill veta är vad processen gjorde efter att den startade.
  3. Gör rollback innan du felsöker. Felsökningen blir billigare när ingen tittar.
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

På Dockup är en rollback ett byte i stället för en rebuild, eftersom den tidigare versionen fortfarande finns på disk. Det spelar roll klockan tre på morgonen: den snabbaste återställningen är den som inte behöver kompilera något.

Frågan du bör ställa till en plattform

När du väljer var du ska köra produktion är det här något bra att testa medvetet: deploya en applikation som startar korrekt men sedan inte kan nå sin databas. Titta på vad dashboarden visar.

Om den visar körs vet du nu exakt vad det ordet kommer att vara värt under nästa incident.

Vanliga frågor

Varför visar dashboarden att tjänsten körs när sajten ligger nere? För att "körs" vanligtvis betyder att containerprocessen finns, inte att applikationen kan hantera en request. En process som fastnat i försök att ansluta till en databas uppfyller den definitionen på obestämd tid.

Bör en health check anropa databasen? Ja, om applikationen inte kan hantera requests utan den. Kontrollera de beroenden du verkligen behöver och hoppa över dem som appen kan fungera med begränsad funktionalitet utan.

Vad är skillnaden mellan liveness och readiness? Liveness frågar om processen bör startas om. Readiness frågar om den ska ta emot trafik. Gaten som förhindrar det här felet är readiness, och den måste köras innan trafiken flyttas.

Hur hindrar jag en dålig deploy från att ta ner sajten över huvud taget? Byt bara trafik när den nya versionen svarar på en riktig request och behåll den tidigare versionen tills bytet har bekräftats. Då blir en misslyckad release en release som aldrig hände, i stället för en driftstörning.