backend / go
Patterns de production : graceful shutdown, worker pools
Explication
Ce que vous allez apprendre
- Implémenter un arrêt propre (graceful shutdown) d'un serveur HTTP
- Écouter les signaux système (
SIGTERM,Ctrl+C) avecsignal.Notify - Construire un worker pool pour borner le parallélisme d'un traitement
- Fermer les channels dans le bon ordre pour éviter tout blocage
- Implémenter un rate limiting simple avec
time.Ticker
Dans quel contexte ?
Une équipe déploie un service Go dans un cluster Kubernetes. Lors de chaque mise à jour, Kubernetes envoie un signal SIGTERM au conteneur avant de le tuer définitivement quelques secondes plus tard. Si le service ignore ce signal et s'arrête brutalement, toutes les requêtes en cours de traitement à ce moment précis sont interrompues sans réponse — un comportement inacceptable pour un service qui traite des paiements ou des commandes.
D'abord, il faut intercepter le signal d'arrêt avant qu'il ne tue le processus
signal.Notify(arret, os.Interrupt, syscall.SIGTERM) redirige ces signaux vers un channel Go plutôt que de laisser le comportement par défaut du système d'exploitation tuer immédiatement le processus. Le programme peut alors bloquer sur <-arret jusqu'à recevoir explicitement ce signal, puis démarrer une procédure d'arrêt contrôlée.
Une fois le signal reçu, il faut laisser un délai raisonnable aux requêtes en cours
serveur.Shutdown(ctx) cesse d'accepter de nouvelles connexions immédiatement, mais laisse les requêtes déjà en cours de traitement se terminer normalement, dans la limite du délai fixé par le context passé en paramètre (typiquement 10 à 30 secondes selon la nature du service).
| Étape du graceful shutdown | Action | Pourquoi c'est important |
|---|---|---|
| 1. Intercepter le signal | signal.Notify | Éviter un arrêt brutal immédiat |
| 2. Arrêter d'accepter de nouvelles requêtes | Shutdown(ctx) démarré | Ne pas commencer un travail qu'on ne pourra pas finir |
| 3. Laisser finir les requêtes en cours | Timeout du context | Éviter de couper une transaction à mi-chemin |
| 4. Forcer l'arrêt si le délai expire | Erreur retournée par Shutdown | Éviter un blocage infini si une requête traîne |
Prérequis
Il faut être à l'aise avec context (leçon dédiée) et les channels : le graceful shutdown combine directement ces deux mécanismes déjà étudiés.
Il reste un second pattern de production tout aussi central : le worker pool
Traiter un très grand volume de tâches avec une goroutine par tâche (sans limite) risque de saturer la mémoire ou les ressources externes (connexions base de données, appels API) si le volume est imprévisible. Un worker pool fixe un nombre constant de goroutines qui consomment les tâches depuis un channel partagé, bornant ainsi le parallélisme réel quel que soit le volume total de travail.
Piège fréquent
Fermer le channel de résultats trop tôt, avant que tous les workers aient fini d'y écrire, provoque un panic ("send on closed channel"). La règle stricte : fermer un channel seulement après avoir attendu (wg.Wait()) que TOUS les producteurs potentiels aient terminé leur travail — jamais avant, jamais "au cas où".
Enfin, un dernier pattern pour contrôler un débit
Bonne pratique
time.Ticker envoie un signal à intervalle régulier sur son channel .C, un mécanisme simple et fiable pour implémenter un rate limiting basique sans dépendance externe — particulièrement utile pour respecter la limite de requêtes par seconde imposée par une API tierce.
Maintenant que tu sais construire un service robuste en production, la dernière leçon de ce cours aborde son déploiement concret : compilation cross-platform et conteneurisation Docker.
Commandes & code
Patterns de production : graceful shutdown, worker pools
Les patterns indispensables pour un service Go fiable en production.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
serveur := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Lancer le serveur dans une goroutine pour ne pas bloquer le shutdown
go func() {
log.Println("serveur demarre sur :8080")
if err := serveur.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("erreur serveur: %v", err)
}
}()
// Attendre un signal d'arret (Ctrl+C, ou SIGTERM envoye par Docker/Kubernetes)
arret := make(chan os.Signal, 1)
signal.Notify(arret, os.Interrupt, syscall.SIGTERM)
<-arret
log.Println("signal d'arret recu, fermeture en cours...")
// Laisser 10s aux requetes en cours pour se terminer proprement
ctx, annuler := context.WithTimeout(context.Background(), 10*time.Second)
defer annuler()
if err := serveur.Shutdown(ctx); err != nil {
log.Fatalf("fermeture forcee apres timeout: %v", err)
}
log.Println("serveur arrete proprement")
}// Worker pool borne : traiter un grand volume de taches avec un parallelisme controle
package main
import (
"fmt"
"sync"
)
type Tache struct {
ID int
}
type Resultat struct {
TacheID int
Sortie int
}
func lancerPool(nbWorkers int, taches <-chan Tache, resultats chan<- Resultat) {
var wg sync.WaitGroup
for w := 0; w < nbWorkers; w++ {
wg.Add(1)
go func(idWorker int) {
defer wg.Done()
for t := range taches {
// traitement simule
resultats <- Resultat{TacheID: t.ID, Sortie: t.ID * t.ID}
}
}(w)
}
wg.Wait()
close(resultats) // fermer une fois TOUS les workers termines
}
func main2() {
taches := make(chan Tache, 100)
resultats := make(chan Resultat, 100)
for i := 1; i <= 20; i++ {
taches <- Tache{ID: i}
}
close(taches) // signale qu'aucune nouvelle tache n'arrivera
go lancerPool(5, taches, resultats) // 5 workers max en parallele
for r := range resultats {
fmt.Println(r)
}
}
// Rate limiting simple avec time.Ticker
func limiteurDebit() {
limiteur := time.NewTicker(200 * time.Millisecond)
defer limiteur.Stop()
requetes := []int{1, 2, 3, 4, 5}
for _, r := range requetes {
<-limiteur.C // attend le prochain "tick" avant de traiter
fmt.Println("traitement requete", r)
}
}Résumé
- Graceful shutdown :
signal.Notify+server.Shutdown(ctx)avec timeout, indispensable derrière Kubernetes. - Worker pool = N goroutines fixes consommant un channel partagé : contrôle la charge et la mémoire.
- Toujours fermer les channels côté producteur, jamais côté consommateur.
time.Tickerimplémente un rate limiting simple sans dépendance externe.
Exercices pratiques
Mission : des paiements coupés en pleine transaction pendant un déploiement
Objectif : Aligner le timeout de graceful shutdown sur les contraintes réelles de Kubernetes, et sécuriser l'ordre de fermeture d'un channel de résultats dans un worker pool.
Contexte
Le service de paiements utilise serveur.Shutdown(ctx) avec un timeout de 10 secondes lors de l'arrêt, comme dans la leçon. L'équipe SRE, pour accélérer les déploiements, configure terminationGracePeriodSeconds: 5 dans le manifeste Kubernetes du service. Depuis ce changement, certaines transactions de paiement sont interrompues en plein milieu pendant les déploiements, malgré le code de graceful shutdown en place.