Retour au cours

backend / go

Patterns de production : graceful shutdown, worker pools

Leçon 251 exercice

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) avec signal.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 shutdownActionPourquoi c'est important
1. Intercepter le signalsignal.NotifyÉviter un arrêt brutal immédiat
2. Arrêter d'accepter de nouvelles requêtesShutdown(ctx) démarréNe pas commencer un travail qu'on ne pourra pas finir
3. Laisser finir les requêtes en coursTimeout du contextÉviter de couper une transaction à mi-chemin
4. Forcer l'arrêt si le délai expireErreur 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.

go
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")
}
go
// 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.Ticker implémente un rate limiting simple sans dépendance externe.

Exercices pratiques

1 disponible
1

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.

Résoudre l’exercice →