Adventures with Docker - S01E04 - The Swarm
Résumé des épisodes précédents
Je suis arrivé à un ensemble brouillon, mais qui nous a permis de voir comment fonctionnait Docker, et de rencontrer les premières limitations.
Je vous encourage à retourner voir les premiers articles que j’ai légerement modifié, et où j’ai rajouté des schémas. Le plus récent était celui-ci, qui vous donnera normalement une bonne idée de la situation courante:
Le dernier problème en date est de déterminer comment les containers vont se voir réciproquement. En particulier, comment les clients vont parler aux serveurs, et pour cela, comment ils vont avoir connaissance des adresses de ces serveurs.
Introduction
Avant d’aller dans le vif du sujet, je voudrais d’abord signaler que j’ai commencé un projet “Saas” sur Gitlab : https://gitlab.com/associative/saas.
Vous pourrez trouver plus d’informations sur le projet “Associative” sur le site web: https://www.associative.fr/.
Quoiqu’il en soit, cette version du Saas devrait vous permettre de l’installer chez vous, si vous voulez faire quelques expériences. Elle n’est pas encore fonctionnelle, mais il n’est pas impossible de la faire marcher: si vous êtes motivés, faîtes-moi signe, je vous aiderai dans la mesure du possible.
Digression sur les sessions
Une question qui vient assez vite se poser lorsqu’on envisage d’installer plusieurs serveurs pour une même application (ce que l’on appelle aujourd’hui “Le redimensionnement horizontal” et autrefois “L’attaque en rang d’oignons”) est celle de la gestion des sessions.
Hé oui, qui dit applications web dit sessions (en première approximation, il est vrai) et qui dit sessions, dit qu’il va falloir que les serveurs partagent l’accès aux sessions, c’est-à-dire que si l’utilisateur passe du serveur 167 au serveur 326, il ne perdra pas sa précieuse session - même si en général (mais il y a plusieurs écoles, c’est évident) cette session ne contient pas grand chose de plus qu’un jeton d’authentification.
Où donc mettre ces sessions ?
La première idée qui nous viendrait à l’esprit c’est la base de données ! Car une application web vient souvent avec une base de données, pour ne pas dire “toujours”. Et que ce soit du relationnel ou pas ne change pas grand chose à l’affaire.
On a une base de données, pourquoi ne pas y stocker les sessions ?
Les avis divergent à ce sujet. La base de données peut être “lente”, ou disons, plus lente que le système par défaut, qui est la lecture/écriture dans un fichier, directement sur le disque du serveur.
Les autres solutions sont basées sur des technologies comme Memcached ou Redis.
Memcached n’est pas un cluster, c’est une base distribuée. La répartition se fait suivant une règle fixe, ce qui donne le fonctionnement suivant:
Chaque client connait l’ensemble des instances de Memcached, et la librairie memcached décide, sans concertation avec les autres clients, où stocker telle ou telle clé. “Sans concertation” signifie qu’un autre client avec une liste identique de memcached ira chercher les clés, en fonction de leur nom, au bon endroit. Si la liste n’est pas dans le même ordre : cela ne fonctionne pas.
On a donc le problème suivant: si on a 3 serveurs memcached et que le deuxième tombe, 1/3 des clés sont perdues - et bien perdues, puisque c’est stocké en mémoire
- et les clients vont rencontrer des problèmes: impossible de se connecter sur le serveur numéro 2 ce qui provoque des timeouts (pas terrible quand le but est d’aller vite, n’est-ce pas ?). Si on reconfigure les clients (une intervention humaine, ou un script prévu à cet effet) pour restreindre la liste aux deux serveurs restants, alors la règle de répartition change, donc certaines clés sont perdues dans la redistribution.
A ma connaissance, memcached ne dispose pas de mécanisme permettant de faire de la haute disponibilité. D’ailleurs, ce n’est pas vraiment son but. Souvenez-vous que par défaut, il écoutait uniquement sur l’interface 127.0.0.1. Ce simple fait contenait un message…
Redis propose un système de cluster: mais la rançon à payer pour une haute disponibilité, c’est 6 serveurs au total, 3 maîtres et 3 esclaves.
En conclusion provisoire sur la digression…
Avant d’examiner la solution sous l’angle technique, il faut d’abord se poser les questions en termes fonctionnels. Quels seront les profils des utilisateurs ? Dans notre cas, une plateforme de Saas, tous seront connectés. La session sera donc indispensable. L’utilisation pourra varier selon les cas, par exemple, le logiciel de comptabilité ne sera employé que sur de courtes périodes, si l’on suppose que le volume de l’activité ne nécessite pas un emploi à temps plein. Si on considère le logiciel de gestion de ludothèque, il sera employé tout au long des journées où celle-ci est ouverte.
Pour l’instant, je vais me contenter de laisser le Memcached sur le serveur. Mais la question reste ouverte.
Le vif du sujet : Docker Swarm
Je vais donc mettre en place le Docker Swarm, qui normalement est la solution miracle à tous les problèmes (non, je ne me fais pas d’illusions).
Pour commencer je pense qu’il vaudrait mieux que j’arrête tout ce qui est en train de tourner sur ma machine en ce moment.
La commande pour faire tourner le Swarm est celle-ci. Elle doit comprendre une adresse IP, et comme je suis sur un portable, je vais mettre 127.0.0.1 parce que les autres sont de nature changeante.
docker swarm init --advertise-addr 127.0.0.1
Swarm initialized: current node (8mu904sfrqkyv5jv0nbo2r6zh) is now a manager. To add a worker to this swarm, run the following command: docker swarm join --token SWMTKN-1-6ceng4c89ie5[...]424m0w7s4plm 127.0.0.1:2377 To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
Voilà, j’ai un manager. Normalement, il faut rajouter des workers, mais bon, vu la taille de machine, je vais tout faire tourner sur le même. Au pire, on pourra toujours rajouter des workers par la suite, et mettre à l’épreuve le dimensionnement dynamique.
docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS 8mu904sfrqkyv5jv0nbo2r6zh * alderaan Ready Active Leader
Déployer des services sur le Swarm
Le déploiement de service est assez simple; en gros c’est une commande pour démarrer un docker, sauf qu’il est inclus dans un Swarm.
docker service create --replicas 1 --name helloworld alpine ping docker.com
Il y a ensuite différentes possibilités pour faire de multiples réplicats, pour arrêter des services, pour rajouter des noeuds, etc. etc.
Ce qui nous intéresse ici, c’est comment peut-on faire démarrer deux docker dans le Swarm et les faire se connecter ensemble sans avoir besoin de calculer des adresses IP…
Il faut deux ports ouverts entre tous les nodes:
Port 7946 TCP/UDP for container network discovery.
Port 4789 UDP for the container ingress network.
Et pour quand on va publier des services pour l’extérieur, il faudra aussi que les ports soient ouverts.
Pour l’instant le frontal en amont sera Traefik; je vais donc lancer l’image avec une commande de ce genre :
docker service create \ --name <SERVICE-NAME> \ --publish <PUBLISHED-PORT>:<TARGET-PORT> \ <IMAGE>
docker service create \ --name traefik \ --publish 8081:80 --publish 8080:8080 \ --replicas 1 \ saas_traefik
Ça semble tourner, j’ai deux avertissements:
Le premier concerne la version de traefik: vu qu’elle n’est pas fixe, les différentes versions qui s’exécutent peuvent diverger.
image saas_traefik:latest could not be accessed on a registry to record its digest. Each node will access saas_traefik:latest independently, possibly leading to different nodes running different versions of the image.
Ça c’est le retour normale de la commande :
vhyh7o8mt7rg98uapi6cu1pzy
Et ça c’est le deuxième avertissement; il faut que je modifie ma commande pour rajouter –detach=true, sinon ça marchera plus dans le futur.
Since --detach=false was not specified, tasks will be created in the background. In a future release, --detach=false will become the default.
Les commandes pour voir ce qui tourne :
docker service ls ID NAME MODE REPLICAS IMAGE PORTS vhyh7o8mt7rg traefik replicated 1/1 saas_traefik:latest *:8081->80/tcp,*:8080->8080/tcp
Pour en voir un en particulier :
docker service ps traefik ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS v5uuti8xtsmi traefik.1 saas_traefik:latest alderaan Running Running 6 minutes ago
La commande inspect
(sans --pretty
, elle renvoie du json.) :
docker service inspect --pretty traefik ID: vhyh7o8mt7rg98uapi6cu1pzy Name: traefik Service Mode: Replicated Replicas: 1 Placement: UpdateConfig: Parallelism: 1 On failure: pause Monitoring Period: 5s Max failure ratio: 0 Update order: stop-first RollbackConfig: Parallelism: 1 On failure: pause Monitoring Period: 5s Max failure ratio: 0 Rollback order: stop-first ContainerSpec: Image: saas_traefik:latest Resources: Endpoint Mode: vip Ports: PublishedPort = 8081 Protocol = tcp TargetPort = 80 PublishMode = ingress PublishedPort = 8080 Protocol = tcp TargetPort = 8080 PublishMode = ingress
Intéressant: on peut publier un port d’un service qui existe déja:
docker service update \ --publish-add <PUBLISHED-PORT>:<TARGET-PORT> \ <SERVICE>
Le traefik est joignable sur le port 8080 pour l’admin, mais comme il n’y a pas de backend, du coup… 404 sur le port 8081.
Donc, maintenant, il faut rajouter un service. Commençons par le CDN. Je mets deux replicas pour la manière…
docker service create --name running_cdn \ --replicas 2 \ -l traefik.backend=cdn \ -l traefik.frontend.rule="Host:alderaan;PathPrefixStrip:/cdn/" \ -l traefik.port=80 cdn
Maintenant le doit apparaître sur traefik: ce n’est pas le cas. C’est quoi le problème ?
Il y a une option --docker.swarmmode
; d’autre part, il semble qu’il faille
créer un reseau spécifique pour le swarm…
Je recommence, déjà en arrêtant les services:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4ffd8bbcb282 cdn:latest "nginx -g 'daemon ..." 9 minutes ago Up 9 minutes 80/tcp running_cdn.2.2swjz0xkzulnos3cu2eykxrne 42e51714614c cdn:latest "nginx -g 'daemon ..." 9 minutes ago Up 9 minutes 80/tcp running_cdn.1.lol52dsy7v8fh1sl4nuwkoc9h a10b47867961 saas_traefik:latest "/traefik" 29 minutes ago Up 29 minutes 80/tcp traefik.1.v5uuti8xtsmihmg29e6073ezi
$ docker service ls ID NAME MODE REPLICAS IMAGE PORTS n8ljuywcox6k running_cdn replicated 2/2 cdn:latest vhyh7o8mt7rg traefik replicated 1/1 saas_traefik:latest *:8081->80/tcp,*:8080->8080/tcp $ docker service rm running_cdn running_cdn $ docker service rm traefik traefik $ docker service ls ID NAME MODE REPLICAS IMAGE PORTS $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
On relance le service traefik, qui doit tourner sur le manager, binder la socket de docker, et tourner avec l’option –docker.swarmmode.
docker service create \ --name running_traefik \ --publish 8081:80 --publish 8080:8080 \ --replicas 1 \ --detach=true \ --constraint=node.role==manager \ --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ saas_traefik --docker.swarmmode
Puis les instances de CDN:
docker service create --name running_cdn \ --replicas 2 \ --label traefik.backend=cdn \ --label traefik.frontend.rule="Host:alderaan;PathPrefixStrip:/cdn/" \ --label traefik.port=80 cdn
Et… ça fonctionne ! Bon enfin, on voit les CDN sur le frontend admin de traefik.
Pour le web : internal server error - comment diable…
Le problème, c’est le réseau. Dans l’admin de traefik, je vois ça:
server-running_cdn-1 http://:80 0
server-running_cdn-2 http://:80 0
Ce qui signifie sans doute que traefik n’a pas la moindre idée où joindre les deux cocos. Ok, on arrête tout le monde, on crée un réseau pour leurs petites affaires, et c’est reparti:
Le réseau:
docker network create --driver=overlay traefik-net
On rajoute l’option --network traefik-net
pour les services:
docker service create \ --name running_traefik \ --publish 8081:80 --publish 8080:8080 \ --replicas 1 \ --detach=true \ --constraint=node.role==manager \ --network traefik-net \ --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ saas_traefik --docker.swarmmode
Puis les instances de CDN:
docker service create --name running_cdn \ --replicas 2 \ --label traefik.backend=cdn \ --label traefik.frontend.rule="Host:alderaan;PathPrefixStrip:/cdn/" \ --label traefik.port=80 cdn \ --network traefik-net \ --detach=true
Il m’a redonné la main, mais il semble qu’il n’arrive pas à les lancer…
docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
3360n9b8c2a4 running_cdn replicated 0/2 cdn:latest
3qig0s7rop9g running_traefik replicated 1/1 saas_traefik:latest *:8081->80/tcp,*:8080->8080/tcp
O/2 replicas, c’est pas bon signe…
Il y a pas mal d’erreurs dans le fichier daemon.log, mais j’arrive pas à savoir lesquelles sont arrivées en premier.
Je vais donc regarder le fichier et lancer les services en même temps.
En instanciant le service traefik, j’ai ces erreurs:
Aug 5 11:10:34 alderaan dockerd[31840]: time="2017-08-05T11:10:34.114186403+02:00" level=error msg="Not continuing with pull after error: errors:\ndenied: requested access to the resource is denied\nunauthorized: authentication required\n" Aug 5 11:10:34 alderaan dockerd[31840]: time="2017-08-05T11:10:34.114293122+02:00" level=info msg="Ignoring extra error returned from registry: unauthorized: authentication required" Aug 5 11:10:34 alderaan dockerd[31840]: time="2017-08-05T11:10:34.114389085+02:00" level=info msg="Translating \"denied: requested access to the resource is denied\" to \"repository saas_traefik not found: does not exist or no pull access\"" Aug 5 11:10:34 alderaan dockerd[31840]: time="2017-08-05T11:10:34.114490356+02:00" level=error msg="pulling image failed" error="repository saas_traefik not found: does not exist or no pull access" module="node/agent/taskmanager" node.id=8mu904sfrqkyv5jv0nbo2r6zh service.id=6abtofqlelhhzqahp5mhekawi task.id=ltbbh1sdb1m31qyyl9abtawu1 Aug 5 11:10:34 alderaan systemd-udevd[5350]: Could not generate persistent MAC address for veth75ebdae: No such file or directory Aug 5 11:10:34 alderaan systemd-udevd[5349]: Could not generate persistent MAC address for veth57c1524: No such file or directory Aug 5 11:10:34 alderaan systemd-udevd[5363]: Could not generate persistent MAC address for veth220f0a0: No such file or directory Aug 5 11:10:34 alderaan systemd-udevd[5362]: Could not generate persistent MAC address for veth1505fd5: No such file or directory Aug 5 11:10:35 alderaan systemd-udevd[5385]: link_config: could not get ethtool features for vx-001001-eop8p Aug 5 11:10:35 alderaan systemd-udevd[5385]: Could not set offload features of vx-001001-eop8p: No such device Aug 5 11:10:35 alderaan systemd-udevd[5385]: Could not generate persistent MAC address for vx-001001-eop8p: No such file or directory Aug 5 11:10:35 alderaan systemd-udevd[5403]: Could not generate persistent MAC address for vetha4eb34a: No such file or directory Aug 5 11:10:35 alderaan systemd-udevd[5404]: Could not generate persistent MAC address for vethb073e9b: No such file or directory Aug 5 11:10:37 alderaan avahi-daemon[503]: Joining mDNS multicast group on interface veth220f0a0.IPv6 with address fe80::249c:72ff:fe51:94ea. Aug 5 11:10:37 alderaan avahi-daemon[503]: New relevant interface veth220f0a0.IPv6 for mDNS. Aug 5 11:10:37 alderaan avahi-daemon[503]: Registering new address record for fe80::249c:72ff:fe51:94ea on veth220f0a0.\*.
Ça n’empèche pas traefik de tourner, mais bon…
Bref, regardons ce qu’il se passe quand on lance les CDN :
J’ai les même message de pull qui marche pas, mais ça c’est pas tant grave.
J’ai aussi les histoires de udevd
qui se plaint des MAC address, mais pareil,
ça semble pas être un soucis pour traefik. Enfin, peut-être que c’en est pas un pour
lui mais que pour la communication entre lui et les backends, c’est pas la même.
Par contre, j’ai ça :
Aug 5 11:15:16 alderaan dockerd[31840]: time="2017-08-05T11:15:16.17292655+02:00" level=error msg="containerd: start container" error="oci runtime error: container_linux.go:262: starting container process caused \"exec: \\\"--network\\\": executable file not found in $PATH\"\n" id=215619fc8a85876d5a94e16a77f6cb7741c087fc375c2868b93902de6f0c0b3b Aug 5 11:15:16 alderaan dockerd[31840]: time="2017-08-05T11:15:16.175587123+02:00" level=error msg="Create container failed with error: oci runtime error: container_linux.go:262: starting container process caused \"exec: \\\"--network\\\": executable file not found in $PATH\"\n" Aug 5 11:15:16 alderaan dockerd[31840]: time="2017-08-05T11:15:16.220435654+02:00" level=error msg="containerd: start container" error="oci runtime error: container_linux.go:262: starting container process caused \"exec: \\\"--network\\\": executable file not found in $PATH\"\n" id=21f33015c9a7014cd66a8c96f906bd60f822a5e0f5a5571bfcde82120d67a71d Aug 5 11:15:16 alderaan dockerd[31840]: time="2017-08-05T11:15:16.221481542+02:00" level=error msg="Create container failed with error: oci runtime error: container_linux.go:262: starting container process caused \"exec: \\\"--network\\\": executable file not found in $PATH\"\n" Aug 5 11:15:16 alderaan dockerd[31840]: time="2017-08-05T11:15:16.988712765+02:00" level=error msg="fatal task error" error="starting container failed: oci runtime error: container_linux.go:262: starting container process caused \"exec: \\\"--network\\\": executable file not found in $PATH\"\n" module="node/agent/taskmanager" node.id=8mu904sfrqkyv5jv0nbo2r6zh service.id=a9rqfx3e8cg6ej2ntq6hk9vlp task.id=41f84plc2rblv9pwazoiefkyl Aug 5 11:15:17 alderaan dockerd[31840]: time="2017-08-05T11:15:17.068196369+02:00" level=error msg="fatal task error" error="starting container failed: oci runtime error: container_linux.go:262: starting container process caused \"exec: \\\"--network\\\": executable file not found in $PATH\"\n" module="node/agent/taskmanager" node.id=8mu904sfrqkyv5jv0nbo2r6zh service.id=a9rqfx3e8cg6ej2ntq6hk9vlp task.id=pfbi6qylga0pjpn0agnq6hjl5
Et ça, ça me semble pas du tout bon. On dirait qu’il pense que –network est une commande qu’il doit exécuter (ofc ce n’est pas le cas…)
Mais pourquoi donc ? AHhhhhhhhh diantre ! J’ai mis le –network après cdn !!!
docker service create --name running_cdn \ --replicas 2 \ --label traefik.backend=cdn \ --label traefik.frontend.rule="Host:alderaan;PathPrefixStrip:/cdn/" \ --network traefik-net \ --detach=true \ --label traefik.port=80 cdn
Là ça semble aller tout de suite mieux: les backend CDN sont lancés, ils sont dans l’interface de traefik avec des adresses correctes, et … le service web répond !
Est-ce que ça load-balance ??? Ah oui, ça marche ! En suivant les logs de part et d’autre, on le voit bien.
Le screenshot obligatoire :
Ok, suffit la plaisanterie, on va lancer une instance du saas.
docker service create --name running_saas \ --replicas 1 \ --label traefik.backend=saas \ --label traefik.frontend.rule="Host:alderaan;PathPrefixStrip:/saas" \ --label traefik.port=80 \ --network traefik-net \ --detach=true saas
J’allais dire, la machine est un peu essouflée, en fait, atom et firefox prennent plus de mémoire à eux deux que tout le docker…
Le saas est lancé, ça ok. Mais vous savez quoi ? Je suis sur qu’il va pas arriver à retrouver la base de données. Purée, il l’a trouvé, comment il fait ça ? Je veux même pas le savoir à ce stade.
Ce qui me m’interesse surtout c’est comment les applis vont joindre le saas dans le swarm ! Apparemment il n’y a qu’un seul moyen de le savoir !
Il faut construire une image avec le logiciel ludo. Après, comme pour le saas, on ira bidouiller la conf à l’intérieur jusqu’à ce que ça marche;
Le Dockerfile:
FROM generic-php-saas COPY webroot/ /var/www/html/ COPY webroot/config/config-saas.php /var/www/html/config/config.php
Build:
/var/www/ludotm/ludotheque$ docker build -t ludotheque .
Create service :
docker service create --name ludotm_ludotheque \ --replicas 1 \ --label traefik.backend=ludotm_ludotheque \ --label traefik.frontend.rule="Host:alderaan;PathPrefixStrip:/ludotm/ludotheque" \ --label traefik.port=80 \ --network traefik-net \ --detach=true ludotheque
Nickel ! Le logiciel de lutotheque fonctionne; et c’est bien le saas qui stocke les sessions grâce à l’adresse running_saas.
Petit bemol : j’ai construit une image nommée ludotheque avec la config en dur d’une lutotheque en particulier, alors que l’instance devrait être parametrée au lancement.
Essayons de plus en plus fort:
Mettre un deuxième docker pour rendre le service ludotheque.
Première façon, juste histoire de se chauffer: on arrête le service et on en remet un autre avec deux replicas.
$ docker service rm ludotm_ludotheque $ docker service create --name ludotm_ludotheque \ --replicas 2 \ --label traefik.backend=ludotm_ludotheque \ --label traefik.frontend.rule="Host:alderaan;PathPrefixStrip:/ludotm/ludotheque" \ --label traefik.port=80 \ --network traefik-net \ --detach=true ludotheque
Mettons que j’en veuille un troisième !
docker service scale ludotm_ludotheque=3
Et voilà, j’ai cru que ça pourrait pas être aussi simple, mais si. Je peux scaler en plus ou en moins, c’est top.
Ça load-balance entre les deux ludotheques, mais dans la réalité absolue, c’est plutôt le saas qu’il faudrait scaler, non ?
Je scale !
docker service scale running_saas=2
Le service des sessions est load-balancé entre les deux instances du saas, est-ce que c’est pas BEAUTIFUL ?
Pour la suite
C’est pas fini, voilà les trucs qui restent à faire:
-
Construire une image générique de la ludotheque qui puisse se paramétrer lors du lancement du service.
-
Accès MySQL: du coup l’accès se fait dans les fichier de config avec l’adresse IP en 172.17… et ça c’est pas correct. Il faut que je trouve moyen de faire tourner un docker MySQL qui se base sur les fichiers du host mais qui propose le service mysql_master, par exemple, pour les autres services du swarm.
-
Accès memcached: comme je l’ai déjà dit, c’est pas bon non plus. Il faut que je mette en place des serveurs redis sous forme de docker, que je pourrai scaler.
La suite, c’est ici, dans l’épisode 5 : Adventures with Docker - Part 5 - The Testing Environment