Docker on steroid avec xhyve et NFS

Publié le 25 avril 2017 par Martin Catty | ops

Cet article est publié sous licence CC BY-NC-SA

Je dois avouer que je suis vraiment fan des solutions de conteneurisation. Le fait de pouvoir packager ses applications est un vrai gage de sérénité.

Dans un contexte SI où vous avez 10+ applications avec des technologies différentes, simplifier et harmoniser votre processus d’onboarding et homogénéiser votre processus de déploiement est également un réel gain de productivité.

Mais il faut garder un œil critique et les technologies comme Docker viennent avec leurs pendants de problèmes.

Aujourd’hui je voudrais adresser le problème des performances dans un contexte OSX. Si vous vous demandez pourquoi je n’utilise pas Docker Native for Mac, c’est parce qu’il apporte lui aussi son lot de limitations dont deux vraiment bloquantes de mon point de vue : une mauvaise gestion du nettoyage des images et des limitations réseau qui sont propres à OSX.

Sympa ce petit paragraphe planqué dans la doc, n’est-ce pas ? L’espace est le nerf de la guerre avec Docker. Si vous n’êtes pas en capacité de nettoyer derrière vous, vous allez au devant de problème sérieux. Par ailleurs l’absence d’interface réseau dédiée empêche d’allouer des IP aux conteneurs et donc de faire du DNS.

Si vous avez paré à ces problèmes en utilisant Docker for Mac, les commentaires sont ouverts.

Avant d’avoir écarté Docker Native for Mac (qui n’existait pas encore quand j’ai commencé à utiliser Docker) je me suis assez naturellement penché sur l’utilisation de VirtualBox. Et j’y suis resté.

À vrai dire tout fonctionne bien avec VirtualBox, à un bémol près, c’est lent. VirtualBox en lui même est un facteur mais ce n’est pas le principal. Ce qui pénalise vraiment les performances ce sont les I/O. En effet le partage de fichiers lors de l’utilisation d’un volume se fait au travers de la VM via vboxfs qui est horriblement lent.

Une solution possible est d’utiliser la bonne veille NFS pour gérer les I/O. Et pour se passer de VirtualBox on peut utiliser l’hyperviseur natif d’OSX (présent depuis Yosemite). Ces deux points peuvent être adressés en utilisant le driver xhyve.

Si vous préférez utiliser des outils magiques qui font tout pour vous, regardez du côté de docker-sync. Personnellement je préfère utiliser les outils bruts, car la magie a toujours un prix (© Rumplestiltskin).

Installation d’une machine virtuelle avec xhyve et NFS

On va commencer par installer le driver docker-machine :

brew install docker-machine-driver-xhyve
sudo chown root:wheel $(brew --prefix)/opt/docker-machine-driver-xhyve/bin/docker-machine-driver-xhyve
sudo chmod u+s $(brew --prefix)/opt/docker-machine-driver-xhyve/bin/docker-machine-driver-xhyve

On va maintenant pouvoir créer la VM à proprement parler :

docker-machine create --xhyve-cpu-count=2 --xhyve-memory-size=8192 --xhyve-disk-size=30000 --xhyve-experimental-nfs-share=true -d xhyve dev

En terme de ressources au niveau CPU et RAM je prends le parti de lui allouer la moitié des ressources disponibles sur ma machine et 30Go de disque. Pour l’ensemble de ces ressources il s’agit de la valeur max que la VM ne dépassera pas.

La machine est provisionnée en utilisant boot2docker, comme lorsqu’on utilise VirtualBox. Elle est maintenant utilisable en l’état, toutefois je vais la customiser un peu, notamment pour en modifier la gestion des DNS et le réseau utilisé.

En effet je souhaite garder ma VM utilisant VirtualBox pour en assurer la transition sans heurt. Je vais donc utiliser un sous réseau différent du réseau de base utilisé par boot2docker (172.17.0.0) pour faire cohabiter mes deux VM en simultané. Je souhaite aussi que mes conteneurs utilisent un résolveur qui tournera en conteneur dans la VM pour se retrouver par leur nom.

Vous vous demandez peut-être pourquoi j’ai besoin de cela plutôt que d’utiliser des links. Simplement car je ne souhaite pas que mes conteneurs ne puissent discuter qu’avec des services auxquels ils sont liés.

Par exemple app1.synbioz.dev doit pouvoir parler avec app2.synbioz.dev sans que j’aie quoi que ce soit à déclarer au préalable. Pour ce faire j’ai besoin d’une résolution de nom.

Pour appliquer ces modifications rentrons donc dans la VM :

$ docker-machine ssh dev
$ sudo -s
# mv /var/lib/boot2docker/profile /var/lib/boot2docker/profile.bak

À l’intérieur de la VM on est un peu pauvre en éditeur, on va donc se rabattre sur ce bon cat.

# cat << EOF > /var/lib/boot2docker/profile
EXTRA_ARGS='
--label provider=xhyve
--bip 172.18.0.1/16
--dns 172.18.0.1
--dns 8.8.8.8
'
CACERT=/var/lib/boot2docker/ca.pem
DOCKER_HOST='-H tcp://0.0.0.0:2376'
DOCKER_STORAGE=aufs
DOCKER_TLS=auto
SERVERKEY=/var/lib/boot2docker/server-key.pem
SERVERCERT=/var/lib/boot2docker/server.pem
EOF
# exit

On redémarre la VM :

$ docker-machine restart dev

et on se positionne sur le démon :

eval $(docker-machine env dev)

Vérifions que tout fonctionne correctement en lançant un nginx dont le port 80 sera publié, c’est à dire mappé de l’hôte vers le conteneur.

$ docker run --rm -p 80:80 nginx

On vérifie que tout fonctionne bien :

$ open "http://$(docker-machine ip dev)"

Cela devrait vous ouvrir la page de nginx sur l’IP 192.168.64.1 (si vous créez différentes VM cette IP s’incrémentera).

Notre objectif est maintenant de faire en sorte que www.synbioz.dev nous affiche la page d’accueil de nginx sans avoir à publier de port sur l’hôte (ce qui nous empêcherait de faire tourner plusieurs nginx par exemple, problème existant avec Docker Native).

Lançons maintenant notre résolveur avec dnsdock :

$ docker run -d -v /var/run/docker.sock:/var/run/docker.sock --name dnsdock --restart=always -p 172.18.0.1:53:53/udp tonistiigi/dnsdock

Je vous passe ici le fonctionnement du résolveur mais si cela vous intéresse n’hésitez pas à le demander en commentaire.

Récupérons l’adresse IP de notre dnsdock :

$ docker inspect --format '' dnsdock
172.18.0.2

On voit que le conteneur démarre bien sur notre réseau en 18 et non en 17. Voyons si on peut le pinger :

$ ping 172.18.0.2
PING 172.18.0.2 (172.18.0.2): 56 data bytes
Request timeout for icmp_seq 0

Notre hôte ne sait pas comment router ces paquets vers nos conteneurs. C’est normal, notre table de routage ne comprend pas de route pour. Nous allons donc l’ajouter en demandant au système de passer par la VM lorsqu’il veut acheminer des paquets vers les IP en 172.18.x.y.

# /sbin/route -n add -net 172.18.0.0 -netmask 255.255.0.0 -gateway $(docker-machine ip dev)

Attention cette route n’est pas persistée lorsque vous redémarrez votre machine. Pour faire en sorte qu’elle soit appliquée à chaque redémarrage je vous conseille d’utiliser un fichier plist.

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE plist PUBLIC '-//Apple//DTD PLIST 1.0//EN' 'http://www.apple.com/DTDs/PropertyList-1.0.dtd'>
<plist version='1.0'>
<dict>
  <key>Label</key>
  <string>com.docker.route</string>
  <key>ProgramArguments</key>
  <array>
    <string>bash</string>
    <string>-c</string>
    <string>sudo /sbin/route -n add -net 172.18.0.0 -netmask 255.255.0.0 -gateway $(docker-machine ip dev)</string>
  </array>
  <key>KeepAlive</key>
  <false/>
  <key>RunAtLoad</key>
  <true/>
  <key>LaunchOnlyOnce</key>
  <true/>
</dict>
</plist>

Placez-le dans /Library/LaunchDaemons/com.docker.route.default.plist. On ne peut pas le placer dans notre ~/Library car la commande a besoin des droits du super utilisateur. Il ne vous reste qu’à l’activer :

# launchctl load /Library/LaunchDaemons/com.docker.route.default.plist

Attention ces fichiers plist ont comme pré-requis que votre VM soit démarrée. Personnellemente je préfère ne pas démarrer ma VM au démarrage de ma machine et ne les utilise donc pas.

On va ensuite désactiver le filtrage des adresses IP sur cette interface réseau, de sorte à pouvoir échanger sur des IP 172.18.x.y et pas uniquement 192.168.64.z.

Regardez quelle est la propriété member de ifconfig bridge100. Chez moi c’est en6, si vous avez autre chose adaptez la commande suivante.

# ifconfig bridge100 -hostfilter en6

Vous pouvez vous faire un fichier plist pour cette commande également, en gardant à l’esprit que vous devrez aussi automatiser le démarrage de la VM au lancement de votre machine.

Vous devriez maintenant pouvoir pinger vos conteneurs :

$ ping 172.18.0.2
PING 172.18.0.2 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: icmp_seq=0 ttl=63 time=0.329 ms

Relançons notre Nginx en lui donnant un alias DNS de sorte à ce que dnsdock puisse faire le mapping entre celui-ci et l’IP du conteneur nouvellement lancé :

$ docker run --rm -e DNSDOCK_ALIAS=www.synbioz.dev nginx

Voyons si notre résolveur sait nous retourner l’IP de notre conteneur :

$ dig @172.18.0.1 www.synbioz.dev

; <<>> DiG 9.8.3-P1 <<>> @172.18.0.1 www.synbioz.dev
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6337
;; flags: qr rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;www.synbioz.dev.		IN	A

;; ANSWER SECTION:
www.synbioz.dev.	0	IN	A	172.18.0.3

;; Query time: 5 msec
;; SERVER: 172.18.0.1#53(172.18.0.1)
;; WHEN: Wed Apr 12 10:53:40 2017
;; MSG SIZE  rcvd: 64

Visiblement tout fonctionne bien. Il ne reste plus qu’à indiquer à notre système d’utiliser ce résolveur pour le TLD .dev.

# echo "nameserver 172.18.0.1" > /etc/resolver/dev

C’est terminé, vous voilà prêt à faire tourner vos applications dans votre VM avec de biens meilleures performances.


L’équipe Synbioz.
Libres d’être ensemble.