Le 503 à chaque déploiement, et pourquoi le corriger aurait corrompu ma base
le symptome
À chaque déploiement d'une de mes applications, quelques secondes après le push,
le navigateur affichait no available server — la page d'erreur de
Traefik quand aucun backend n'est joignable. Le service revenait seul au bout
d'une poignée de secondes.
la mauvaise piste
Ma première hypothèse était mauvaise, et je l'ai suivie trop longtemps : je venais d'empiler six migrations de schéma non déployées, et j'étais convaincu que l'une d'elles faisait planter le conteneur au démarrage. J'ai perdu du temps à préparer des procédures de rollback pour un problème qui n'existait pas.
Les logs ont tranché en trois lignes :
$ kubectl -n staging get pods NAME READY STATUS RESTARTS AGE fridge-app-65bf689567-ljks6 1/1 Running 0 109s $ kubectl -n staging logs deploy/fridge-app --tail=5 fridge à l'écoute sur 0.0.0.0:8080
Zéro redémarrage. Le pod démarrait parfaitement, migrations comprises. Je testais simplement pendant la fenêtre de bascule.
la cause reelle
Le déploiement utilise strategy: Recreate avec une seule réplique.
Kubernetes arrête donc l'ancien pod avant de démarrer le nouveau : entre
les deux, Traefik n'a plus aucun backend. D'où le 503, systématique et bref.
la correction evidente, et pourquoi elle est piegeuse
Le réflexe est immédiat — passer en rolling update avec surge :
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
Le nouveau pod démarre avant l'arrêt de l'ancien, zéro coupure. Sauf que
l'application stocke ses données dans SQLite sur un volume persistant.
Avec maxSurge: 1, deux processus écrivent quelques secondes durant
sur le même fichier. SQLite tolère plusieurs lecteurs, mais un seul écrivain :
le résultat probable est un journal WAL incohérent, au mieux des écritures perdues.
Le pool de connexions Go est bien limité à SetMaxOpenConns(1) — mais
cette garantie vaut à l'intérieur d'un processus, pas entre deux. Et le PVC
en ReadWriteOnce ne bloque rien : les deux pods sont sur le même nœud,
Kubernetes monte donc le volume deux fois sans protester.
ce que j'ai fait a la place
J'ai gardé Recreate et attaqué l'autre bout du problème : réduire la
durée de la fenêtre plutôt que la supprimer. La sonde de disponibilité attendait
3 secondes puis testait toutes les 10 — un pod prêt en 1 seconde pouvait donc
rester déclaré indisponible pendant 13.
readinessProbe:
exec: { command: ["/app/fridge-app", "--healthcheck"] }
initialDelaySeconds: 1
periodSeconds: 2
failureThreshold: 3
Coupure ramenée d'environ quinze secondes à trois ou quatre. Pour une application familiale, c'est le bon compromis.
ce que je retiens
- Lire les logs avant de théoriser. Trois lignes de
kubectlont invalidé une heure d'hypothèses. - Un commentaire dans un manifeste est une décision, pas une décoration. Le mien disait déjà « 1 réplique STRICTE, deux writers SQLite = corruption ». Je l'avais écrit, puis oublié.
- La vraie solution « zéro coupure » existe (LiteFS, ou passer à PostgreSQL) mais coûterait le mono-binaire sans dépendance qui fait tout l'intérêt de ce projet. Toutes les dettes ne sont pas à rembourser.