quand on fait docker stop ça envoie un SIGTERM au pid 1
pour savoir qui est pid 1 ou si il y a des zombies dans le container on peut lancer :
docker exec dronehook ps fx
si dans le dockerfile on fait
CMD node server.js
le pid 1 sera node, dans ce cas il est possible d'intercepter les signaux depuis node avec ce code
process.on('SIGTERM', function() { console.log('got SIGTERM'); })
si dans le dockerfile on fait
CMD npm start
le pid 1 sera npm, Il ne transmet pas du tout les signaux. ou alors ils sont interceptés par sh qui est lancé par npm start. mais rien n'est reçupar node.
si c'est nodemon, c'est nul aussi car il ne gere pas de façon tres propre les signaux qu'il reçoit. par exemple quand il reçoit SIGTERM il envoie sigterm puis sigint puis sigint. le brutal :)
il est posible de forcer le signal que nodemon envoie lorqu'il detecte des fichiers modifiés, par ex : --signal SIGHUP
mais ça résoud rien.
seule solution propre, appeller node directement depuis le dockerfile, sans npm, ni nodemon mais aucune protection contre les zombies
quand un process meurt il devient un zombie
si le parent fait un wait, il obtient le code de retour du process, et le zombie disparait
nodejs fait surement le wait tout seul on dirait car j'arrive pas à créer de zombies.
quand le process meurt il envoie un signal au parent (SIGCHLD)
du coup la solution est de mettre tini, qui appelle node directement.
il ne pose pas les pb de npm/nodemon car il transmet bien les signaux et il gere les zombies
ENTRYPOINT ["tini", "--"] CMD ["node", "server.js"]
pour créer un zombie :
(sleep 1 & exec /bin/sleep 10)
au bout de 10 secondes ce zombie doit etre recupéré par tini, ou trainer pour toujours si on a pas mis tini
Remarque : créer des zombies c'est mal en soit (il faudrait juste fixer le code pour éviter ça), mais pour des outils comme jenkins ou des runners, c'est inevitable car Jenkins execute du code qui ne lui appartient pas (i.e. les scripts de builds), alors il ne leur est pas possible de les fixer.
C'est pourquoi Jenkins utilise Tini: pour nettoyer après les scripts qui créent des zombies.
remarque
Pourquoi ne pas utiliser Bash en tant que PID 1 vu qu'il fait en réalité la même chose (récupérer les zombies), ?
Un problème est que si vous exécutez Bash en tant que PID 1, alors tous les signaux que vous envoyez à votre conteneur Docker (par exemple en utilisant docker stop ou docker kill) finissent par être envoyés à Bash, qui ne les transmet nulle part (à moins que vous ne le codiez vous-même).
Tini résout cela en "transmettant les signaux" : si vous envoyez un signal à Tini, il envoie le même signal à votre processus enfant (Jenkins dans votre cas).
Un deuxième problème est que une fois que votre processus s'est terminé, Bash va également se terminer. Si vous n'êtes pas prudent, Bash pourrait se terminer avec un code de sortie 0. Cela pourrait empêcher les stratégies de redémarrage de Docker de fonctionner comme vous vous y attendez). Ce que vous voulez réellement, c'est que Bash renvoie le même code de sortie que votre processus.
Notez que vous pouvez résoudre ce problème en créant des gestionnaires de signaux dans Bash pour effectuer réellement la transmission et renvoyer un code de sortie approprié. En revanche, cela demande plus de travail, alors qu'ajouter Tini ne nécessite que quelques lignes dans votre Dockerfile.
une fois que le signal est bien reçu par node (tini + node), il suffit de l'intercepter et de stopper express + les autres trucs qui occupent l'event loop (bdd, etc) le process s'eteint alors tranquillement sans qu'on ait besoin de forcer avec process.exit(0);
process.on('SIGTERM', function() { console.log('got SIGTERM'); server.close(() => { console.log('closed nice') }); })
Traefik route le trafic vers chacun de nos conteneurs
Lorsqu'on defini un healthcheck sur un conteneur, traefik ne redirigera que si health = ok
Si on a plusieurs conteneurs qui representent le même service traefik redirige vers l'un d'eux a la fois en round robin. (l'un apres l'autre)
On peut donc faire en sorte que le healthcheck renvoie ko.
En captant le signal sigterm par exemple, ainsi une fois que docker a executé le healthcheck il constate que le conteneur n'est plus healthy.
traefik s'en rend alors compte lui aussi, et ne redirige donc plus le trafic sur ce conteneur.
il est donc maintenant possible de faire server.close()
et enfin une fois que tous les clients sont deconnectés on peut eteindre le conteneur.
Il faut donc que docker soit tres patient lors du docker stop (par defaut 10 sec)
on demarre 4 conteneurs du même service
docker-compose up -d --scale dronehook=4 dronehook
on veut par exemple en éteindre 3 :
le probleme c'est qu'on ne peut pas faire ce call directement car on ne sait pas quels conteneurs seraient éteints, et en plus cela causerait des perte des connexions aux utilisateurs connectés
docker-compose up -d --scale dronehook=1 dronehook
il faut plutot dire à 3 d'entre eux de prevenir qu'ils sont en mauvaise santé afin que traefik arrete de rediriger le trafic vers eux.
(il faut créer la route /sethealth/ko dans l'application)
docker exec -ti docker_dronehook_2 curl http://localhost:3000/sethealth/ko docker exec -ti docker_dronehook_3 curl http://localhost:3000/sethealth/ko docker exec -ti docker_dronehook_4 curl http://localhost:3000/sethealth/ko
à présent chacun d'entre eux renvoie ko sur leur /health
docker exec -ti docker_dronehook_4 curl http://localhost:3000/health
traefik ne redirige donc plus vers eux
on peut donc eteindre ces 3 conteneurs
docker rm -f docker_dronehook_2 docker_dronehook_3 docker_dronehook_4;
il est possible de lancer ce script en paralele afin d'etre certain qu'aucun client n'a perdu sa connection
'use strict'; var requestPromise = require('./lib/requestPromise'); var globali = 0; async function work() { var i = globali; globali++; var { response, body } = await requestPromise({url: 'https://dronehook.raphaelpiccolo.com/'}); console.log(`${new Date().toJSON().substr(11)} ${i} body=${body}`); } setInterval(work, 500);
Si on veut mettre à jour un projet on doit forcement à un moment faire "docker-compose pull project"
puis on lance "docker-compose up -d" et on se rend compte qu'il y a à nouveau une interuption du service car docker eteint le service puis le relance, (même si on lui demande de upscaler).
La seule solution automatique consiste a créer un swarm qui sera responsable de puller la nouvelle image et de gérer le renouvellement des conteneurs. Si via la techinque du docker scaling ci dessus on parvient à ne plus avoir d'interuptions, swarm permet de garder ce comportement même en cas de changement d'image.
il faut donc créer un swarm : https://blog.raphaelpiccolo.com/post/851
puis utiliser le compose suivant afin d'acomplir un deploiement zero downtime
en gros :
compose file :
version: '3.7' services: errorpage: image: registry.raphaelpiccolo.com/errorpage:1.0.13 working_dir: /usr/app environment: - "NODE_ENV=production" healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:3000/health'] # the time the container can take to close itself before swarm forces it. stop_grace_period: 130s deploy: mode: replicated replicas: 2 # en cas de mise a jour (changement d'image / scaling / changement de config) on crée le nouveau conteneur puis on eteint l'ancien update_config: failure_action: rollback parallelism: 1 delay: 10s order: start-first # en cas d'erreur de deploiement, swarm remettra l'ancienne image rollback_config: parallelism: 1 delay: 10s order: stop-first labels: - "traefik.enable=true" - "traefik.http.services.errorpage.loadbalancer.server.port=3000" - "traefik.http.routers.errorpage.rule=Host(`error.clone.gextra.net`)" - "traefik.http.routers.errorpage.tls.certresolver=le" - "traefik.http.routers.errorpage.entrypoints=https" networks: # Use the public network created to be shared between Traefik and # any other service that needs to be publicly available with HTTPS - public networks: # Use the previously created public network "public", shared with other # services that need to be publicly available via this Traefik public: external: true