Alkalmazás-életciklus
PHP-ban a folyamat életciklusa nem a te problémád volt: a php-fpm elindította a workert, lefuttatta a kérést, és ha deployoltál, a supervisor gondoskodott a cseréről. Go-ban a binárisod maga a szerver: neki kell figyelnie a SIGTERM-et, kivárnia az in-flight kéréseket, lezárnia a connection poolt és kiflusholnia a telemetriát, mielőtt kilép. Ez tipikusan az a ~60 sor boilerplate, amit minden service main.go-jába bemásolnak, és amit mindig kicsit másképp rontanak el.
A kit ezt egyszer írja meg, az app csomagban, és ettől a téma megint nem-téma: a generált projektekben a graceful shutdown egyszerűen ott van, és most láthatod is, mi történik.
app.Run és a Params
Az app csomag engine-agnosztikus: se HTTP-t, se más transzportot nem ismer. Egyetlen belépési pontja van:
// Closer egy nevesített cleanup-függvény, graceful shutdownkor fut.
type Closer struct {
Name string
Fn func(context.Context) error
}
// Params egy processzfutást ír le.
type Params struct {
ListenAddr string // csak a startup-loghoz
ShutdownTimeout time.Duration // az in-flight drain kerete
Serve func() error // blokkoló listen; goroutine-ban fut
Shutdown func(context.Context) error // nincs új kérés, in-flight drain
Closers []Closer // rendezett cleanup-lista
Flush func(context.Context) error // telemetria-flush, legutolsóként
}
func Run(ctx context.Context, p Params) error
A Run blokkol, amíg a processz le nem áll, és a következő gépezetet hajtja:
signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM): aSIGINT/SIGTERMa contextet cancelezi. Mivel a szülőctx-ből származik, a szülő context cancelje is shutdownt vált ki: tesztben így állítasz le egy futó service-t signal nélkül.Servegoroutine-ban indul. Ha magától hibával tér vissza (pl. foglalt a port), aRunapp: listen:hibával lép ki, de a closerek és a flush ilyenkor is lefutnak.- Signalra: a
Runvisszaállítja az alapértelmezett signal-kezelést (ezért egy másodikCtrl-Cazonnal, kegyelem nélkül öli a processzt), majd meghívja aShutdown-t egyShutdownTimeout-os contexttel. Itt drainelnek az in-flight kérések. - Cleanup saját kerettel: a closerek és a flush egy friss, szintén
ShutdownTimeout-os contextet kapnak. Ha a HTTP-drain felélte a teljes keretet, a cleanup akkor sem nulla idővel indul: a pool-lezárás és a telemetria-export nem esik áldozatul egy lassú kérésnek. (Mindkét shutdown-contextcontext.WithoutCancel-lel válik le a szülőről: egy már cancelezett szülő nem vágja rövidre a rendezett leállást.) - Closerek LIFO sorrendben, majd legvégül a
Flush. - Tiszta leállásnál
nila visszatérési érték; a részhibákerrors.Join-nal gyűlnek, tehát egy hibázó closer nem nyeli el a többiét.
Miért LIFO, és miért utolsó a flush?
A closerek fordított regisztrációs sorrendben futnak, mert a függőségek így záródnak helyes irányban: amit előbb nyitottál (pl. a pgx pool), arra még szüksége lehet annak, amit később építettél rá (pl. egy rá támaszkodó consumer). Amit utoljára nyitottál, azt zárod először, pontosan ahogy a defer működik egy függvényen belül.
A telemetria-flush pedig azért a legutolsó, mert a closerek maguk is bocsátanak ki telemetriát: egy pool-lezárás loga, egy cleanup közben nyitott span csak akkor jut el az OTLP-endpointig vagy a Sentrybe, ha az exporterek még élnek. Ha a flush a closerek előtt futna, a leállás utolsó másodpercei vakfoltba esnének.
Ki ül rajta?
Mindhárom chassis ugyanezt az app.Run-t tölti ki, csak a Params tartalma más:
| Params | server.Run | worker.Run | realtime.Run |
|---|---|---|---|
Serve | srv.ListenAndServe | asynq szerver + outbox relay + ütemező | a kapcsolatokat fogadó centrifuge node |
Shutdown | srv.Shutdown | loopok leállítása, in-flight taskok drainelése | node.Shutdown, tartott kapcsolatok drainelése |
Closers | a WithCloser opciók | a WithCloser opciók | a WithCloser opciók |
Flush | telemetry.Setup shutdown-függvénye | ugyanaz | ugyanaz |
Mindhárom a telemetry.Setup-pal indít, és a visszakapott shutdown-függvényt adja át Flush-ként, így a "Sentry és OTel flush a legvégén" garancia bináris-típustól függetlenül azonos. A workernél az in-flight drain mást jelent: a WORKER_SHUTDOWN_TIMEOUT alatt be nem fejezett taskokat az asynq visszateszi a sorba, és később újra kézbesíti (at-least-once); a realtime-nál pedig azt, hogy minden tartott WebSocket-kapcsolat reconnect-jogosult close-kódot kap, mielőtt a processz kilép (Realtime).
Ha egy saját, negyedik chassis-t építenél (pl. egy gRPC-szervert), ugyanígy az app.Run-ra teheted: a csomagnak nincs HTTP-függősége.
A shop main.go-ja
A gyakorlatban az életciklushoz egyetlen ponton nyúlsz hozzá: closert regisztrálsz mindenre, amit a main-ben nyitottál. A shop mintaalkalmazás belépési pontjában ez a pgx pool:
pool := pg.MustNewPool(ctx, cfg.DB)
err := server.Run(ctx, cfg.Server, func(r chi.Router) error {
// ... modulregisztráció ...
return nil
}, server.WithCloser("pgxpool", func(context.Context) error {
pool.Close()
return nil
}))
Egy SIGTERM érkezésekor tehát a sorrend: a shop nem fogad több kérést → a folyamatban lévő PlaceOrder még lefut (a tranzakciójával és az outbox-írásával együtt) → a pgxpool closer lezárja a poolt → a telemetria kiflusholja a kérés spanjeit. Kubernetes alatt ez pontosan az a viselkedés, amit egy rolling deploy elvár: a ShutdownTimeout-ot (SHUTDOWN_TIMEOUT, default 10s) érdemes a pod terminationGracePeriodSeconds-énél rövidebbre hangolni.
"pgxpool") nem dísz: hibázó closernél a Runapp: closer "pgxpool": ... formában adja vissza a hibát, így a logból azonnal látszik, melyik cleanup bukott el.Mit jelent ez a mindennapokban
- Nem írsz signal-kezelést. Soha. A
server.Run/worker.Run/realtime.Runhívása maga az életciklus. - A
main-ben nyitott erőforrásokhoz closer jár: a generátorok a pgx poolét maguktól bekötik, a sajátjaidat (pl. egy Valkey-klienst) te adod hozzáWithCloser-rel, a nyitás sorrendjében. - A dupla
Ctrl-Cmenekülőútvonal marad: ha fejlesztés közben beragadna egy drain, a második signal azonnal kilő.
A HTTP-oldali lépéssort (telemetria → router-építés → regisztráció → listen → shutdown) a szerver oldal mutatja be; a worker-oldali drain részleteit a Worker oldal; a kapcsolat-drainelés részleteit a Realtime oldal.