Explorer vos json dans le terminal avec fx

Fx est une application node installable via npm ou yarn qui permet de visualiser et d’interagir avec un json directement dans le terminal.

Ce qui est intéressant c’est qu’il est possible d’explorer le json en précisant un path à afficher.

Voici un exemple sur un fichier package.json :

Il est même possible d’aller un peu plus loin et de lui donner une ou plusieurs fonctions anonymes en paramètre.

Un petit outil sympa qui va vous éviter de copier/ coller votre json dans un IDE pour le remettre en forme et naviguer dedans.

Monter un cluster swarm avec des Raspberry pi et HypriotOS

HypriotOs est une version de raspbian dans laquelle docker, et docker swarm sont pré installés. La particularité est que HypriotOs tourne sur des architectures ARM telles que les Raspberry, le Nvidia shield et quelques autres cartes comme les ODROID C2.

Dans cet article, nous allons voir comment monter un petit cluster swarm avec trois noeuds (1 manager et 2 workers). Une fois la partie serveur montée, nous ferons tourner des services dessus à la fois en lignes de commandes et via une interface graphique.

Swarm

Swarm est l’orchestrateur historiquement intégré dans docker. Il permet de distribuer automatiquement les containers sur l’un ou l’autre des noeuds du cluster en fonction de l’indisponibilité ou de la charge d’un noeud.  Docker inc a été testé pour scal

ler jusqu’à 1 000 nœuds et  50 000 conteneurs sans dégradation de performance. A mon avis nous allons avoir du mal à faire tourner autant de conteneurs sur nos Raspberry.

Il existe deux types de noeuds, les managers et les workers. Pour faire simple, les managers peuvent administrer ce qui tourne sur le cluster et répartir le travail sur les workers tandis que les workers sont de simples exécutants qui lancent des containers.

De base, le cluster va faire du mesh routing. Cela signifie, par exemple, que si vous lancez un serveur web sur le port 80 quelque part sur votre cluster, vous pourrez y accéder en attaquant n’importe quelle machine (worker ou manager) qui compose le cluster. Swarm va aussi nous proposer un système de load balancing afin de pouvoir distribuer la charge entre différentes instances d’un service. 

https://docs.docker.com/engine/swarm/ingress/#publish-a-port-for-a-service

Les deux points précédents sont disponibles, sans configuration, dès la création de votre cluster.

Choix des matériaux

Pour monter mon cluster avec trois noeuds il va me falloir le matériel suivant :

Matériel pour un cluster swarm de raspberry pie

Installer HypriotOs sur les cartes

Pour les utilisateurs de Linux et MacOs il existe un outil proposé par la communauté de Hypriot pour flasher les cartes. Malheureusement, pour les utilisateurs de Windows, les choses sont un peu plus compliquées.

Pour linux et mac

Hypriot flash est un utilitaire en lignes de commandes qui va s’occuper de tout, de la décompression de l’image à l’installation sur la carte SIM. Pour l’installation, tout est précisé dans le readme du dépôt.

Télécharger la dernière image de hypriotos

wget https://github.com/hypriot/image-builder-rpi/releases/download/v1.9.0/hypriotos-rpi-v1.9.0.img.zip

Vérifier le checksum de l’image

➜  cat ../Downloads/hypriotos-rpi-v1.9.0.img.zip.sha256
be21a702887817d8c84c053f9ee4c54e04fd55e9fb9692edc3904801b14c33a8  hypriotos-rpi-v1.9.0.img.zip

# sur linux sha256sum / sur mac gsha256sum

➜  gsha256sum hypriotos-rpi-v1.9.0.img.zip
be21a702887817d8c84c053f9ee4c54e04fd55e9fb9692edc3904801b14c33a8  hypriotos-rpi-v1.9.0.img.zip

Flasher les cartes SD

Pour notre manager:

flash hypriotos-rpi-v1.9.0.img.zip --hostname manager-01

Pour nos workers :

flash hypriotos-rpi-v1.9.0.img.zip --hostname worker-01
flash hypriotos-rpi-v1.9.0.img.zip --hostname worker-02

Votre mot de passe root vous sera demandé pendant l’installation de l’image sur la carte.

Vous devriez avoir une sortie console proche de celle ci:

flash --hostname worker-01 hypriotos-rpi-v1.9.0.img.zip
Using cached image /tmp/hypriotos-rpi-v1.9.0.img

Is /dev/disk2 correct? yes
Unmounting /dev/disk2 ...
Unmount of all volumes on disk2 was successful
Unmount of all volumes on disk2 was successful
Flashing /tmp/hypriotos-rpi-v1.9.0.img to /dev/rdisk2 ...
No 'pv' command found, so no progress available.
Press CTRL+T if you want to see the current info of dd command.
Password:
930+1 records in
930+1 records out
975765504 bytes transferred in 111.983538 secs (8713473 bytes/sec)
Mounting Disk
Mounting /dev/disk2 to customize...
Set hostname=worker-01
Unmounting /dev/disk2 ...
"disk2" ejected.
Finished.

Pour windows

Je n’ai jamais flashé mes cartes SD depuis Windows. Mais vous pourrez trouver une procédure sur le site de hypriot. 

Dans le cas de Windows il faudra, si possible, éditer le fichier user-data  (c’est un yml sans extension) à la racine de la carte SD pour changer le hostname qui est présent dedans. Il faudra en nommer un manager-01 et les deux autres worker-01 et worker-02 histoire de pouvoir suivre dans la suite de l’article.

Les branchements

Rien de bien complexe, les câbles ethernet dans les prises ethernets, les cables d’alimentation dans les micro usb et les cartes SD dans les slots prévus à cet effet. Et hop, le tour est joué. Vous devriez pouvoir détecter les machines sur votre réseau local.

Branchements raspberry pi pour un cluster swarm

Se connecter en SSH sur les différents noeuds

Sur mac, j’utilise l’application LanScan Pro depuis des années pour scanner mon réseau local.

Détection des machines du cluster swarm sur le réseau

J’ai bien mes trois Raspberry qui sont présents sur le réseau avec les ip 192.168.1.22, 23 et 24.

Pour se connecter en ssh sur les Raspberry il faut utiliser le login/ mot de passe par défaut de hypriotOS. A savoir pirate/ hypriot.

Première connexion SSH sur hypriot

A cette étape je vous conseille de faire les mises à jour avant d’aller plus loin. Ce serait dommage de faire toute sa configuration sur une version qui a des failles de sécu. De plus il est préférable d’avoir les versions de docker sur l’ensemble du cluster.

Le temps des mises à jour vous avez le temps d’aller prendre un café 😀

Les versions de docker

Création du cluster swarm

Creation du manager

Sur le rasp nommé manager-01 je vais créer le cluster swarm :

docker swarm init

La commande va créer le cluster, vous enregistrer en temps que leader et vous donner la commande pour ajouter des workers.

La création du cluster swarm
La liste des noeuds du cluster après création

Ajout des workers

Pour ajouter les workers on va utiliser la commande donnée au moment de la création du cluster en lançant sur les Raspberry worker-01 et worker-02.

Si vous avez perdu la commande vous pouvez la récupérer en vous connectant sur un manager et en exécutant la ligne de commande :

docker swarm join-token worker
Récupération de la commande pour joindre un cluster swarm

On va donc se connecter en ssh sur worker-01 et worker-02 et lancer la commande de join. Vous devriez avoir le résultat suivant :

Joindre un cluster swarm

Sur le manager on va voir apparaitre les noeuds en tant que worker si on utilise la commande docker node ls

La liste des noeuds d’un cluster swarm

Installation et utilisation de Portainer

Portainer est un outil de visualisation et d’administration et de visualisation open source pour docker. Il est assez intéressant dans notre cas car il va nous permettre de visualiser ce qu’il se passe sur l’ensemble de notre cluster depuis une interface WEB. 

Installation de portainer

On va lancer un container Portainer sur le noeud manager-01.

docker run -d -p 9000:9000 --name portainer --restart always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer

Portainer est maintenant compatible avec notre architecture arm, il est donc possible d’utiliser l’image officielle. 

Une fois le container lancé, vous pouvez vous rendre, via votre navigateur, sur le port 9000 de votre manager. Pour moi http://192.168.1.22:9000.

Vous allez être redirigé sur la page de création du compte administrateur. Il  est préférable de ne pas conserver le nom d’utilisateur par défaut.

Installation portainer

La seconde étape de l’installation est le choix du type de connexion à docker. Nous allons choisir local car nous avons monté le socket docker lors de la création du container.

Installation portainer

Maintenant que Portainer est configuré nous allons pouvoir consulter notre cluster swarm, mais aussi lancer des services si nous le souhaitons. 

On remarque déjà, que notre cluster est présent et que nos 3 noeuds sont bien détectés.

Cluster swarm dans portainer

En l’état notre cluster est composé de 12 coeurs et de 3 Go de ram.

Cluster visualizer

Lancer un service sur swarm

Pour lancer un service, il y a deux possibilité. Soit passer par portainer soit le faire en lignes de commandes. A vous de voir ce que vous préférez. J’ai préparé une petite image pour faire cette démo. Je m’excuse d’avance du poids de l’image (120Mo) pour faire un hello world…

J’ai buildé une image apache/ php qui affiche le hostname courant (celui du container). L’idée est de déployer un service sur le cluster et de voir comment il réagit et quel est le comportement que l’on obtient.

Déployer un service en lignes de commandes

Nous allons lancer une instance de notre image sur le cluster. Nous n’avons pas à nous soucier du choix de la machine sur laquelle l’application va tourner car le routing mesh nous permettra d’y accéder depuis n’importe quel noeud.  Mais vous pouvez le savoir dans portainer en retournant sur la vue Cluster visualisation.

docker service create -p 80:80 --name hello-world docker.io/lahaxearnaud/swarm-hello-world
Création d’un service dans swarm

Il est possible de préciser beaucoup d’options pour limiter les ressources que le container pourra utiliser, la façon de vérifier que l’application est toujours up (health check)…

Déployer un service avec Portainer

Toutes les options que l’on peut donner via le cli sont disponibles via portainer pour la création de service. La mise en place de nouveau service est très simplifié grâce à l’interface graphique.

Creation d’un service dans portainer

Pour notre test nous n’avons pas besoin des configurations avancées.

Tester le load balancing

Nous avons maintenant un service qui tourne sur notre cluster et qui sert une « page » web contenant le nom du container qui le fait tourner. Pour tester le load balancing on fait une série d’appels et on va regarder si on obtient toujours le même nom de container.

for ((i=1;i<=10;i++)); do curl "http://192.168.1.22"; echo ""; done

Sans surprise pour le moment je vais obtenir 10 fois la même valeur, car nous n’avons qu’une instance qui tourne :

Dans un premier temps nous allons faire scaller notre application pour faire tourner 9 instances.

docker service scale hello-world=9

Vous pouvez aussi le faire via portainer en cliquant sur le service hello-world dans la section service et en modifiant la valeur dans le champs Replicas.

Scaller un service avec portainer

Il va falloir attendre un peu que l’image soit téléchargée sur les différents Raspberry et que les containers soient lancés. Mais une fois que tout est prêt, on va pouvoir relancer notre commande.

On se rend bien compte que la charge est distribuée entre les différentes machines. Sur les 10 hits on a touché les 9 instances.

Consulter les logs

Sur le manager vous pouvez faire un tail sur les logs d’un service via la commande docker service log -ft hello-world.

Visualisation des logs d’un service swarm

Dans portainer par contre il semblerait qu’il y ait un bug et qu’il ne remonte que les logs des instances présentes sur le manager. 

Que mettre sur le cluster ?

Pour ma part, j’utilise swarm depuis au moins deux ans pour ma domotique. Comme je voulais jouer un peu avec l’IoT j’ai mis des capteurs un peu partout dans l’appartement (luminosité, température, mouvements, capteurs d’ouverture de porte et de fenêtre…) et chaque élément envoie des messages dans un rabbitmq. Mon cluster contient l’ensemble des consommateurs et l’application symfony qui sert l’interface graphique. Les consommateurs sont des workers symfony 4 qui utilisent le composant symfony messenger

Avoir cette architecture en cluster permet de perdre ou d’ajouter un ou plusieurs serveurs sans devoir déplacer des applications à la main. Là où il faut être attentif c’est que swarm peut continuer de fonctionner en perdant un worker, mais la perte des managers est plus problématique. Il est préférable d’avoir plusieurs managers pour éviter que le cluster se retrouve sans manager en cas de panne. Un nombre impair de managers est préférable pour s’assurer que l’algorithme (Raft) arrive à élire un nouveau leader.

Retour d’expérience

La technologie est sympa et la fiabilité est au rendez-vous sur tout ce qui est worker et application web. Par contre, j’ai rencontré pas mal de problèmes en voulant mettre un rabbitmq, un elastic search ou encore un mysql dans le cluster. Après plusieurs crashes je les ai installés sur une machine séparée. Je ne sais pas à quel niveau se situe le problème, mais depuis que c’est sur un rasp séparé il n’y a plus de problèmes.

L’autre point est que j’ai une livebox à la maison, c’est elle qui me sert de DHCP (c’est dans ma todo de faire autrement). Elle a tendance à sortir du réseau les machines avec des ips fixes au bout de quelques jours. Je me suis retrouvé à plusieurs reprises avec un cluster qui était HS car toutes les machines étaient déconnectées.

Le fait d’être sur une architecture ARM nous empêche d’utiliser la plupart des images disponibles sur les hubs. Il faut donc passer du temps à faire ses propres images. Ce n’est pas forcement un mal !

Faq

Comment retirer proprement un noeud d’un cluster swarm ?

Si c’est un manager il faut se poser la question de promouvoir un worker en manager avant de commencer docker node promote worker-02 puis vous devez retirer le role manager au noeud à supprimer docker node demote manager-01. La seconde étape est de dire à l’orchestrateur d’enlever tout ce qui tourne sur le noeud docker node update --availability drain worker-01. Une fois qu’il n’y a plus rien sur le noeud on peut le sortir du cluster via la commande docker node rm worker-01. De cette manière il n’y aura pas d’interruption de service.

Comment supprimer le cluster swarm ?

Dans un premier temps il faut supprimer les services qui tournent dessus : docker service rm mon-service. Une fois que c’est fait vous pouvez supprimer les noeuds un à un via docker node rm -f node-name. Puis sur le manager faire un docker swarm leave -f.

Est-ce qu’il est possible de deployer une image locale ?

Non, il n’est pas possible de déployer une image qui n’est pas dans un registry. Mais vous pouvez monter votre propre registry et l’utiliser pour déployer vos images sur vos workers.

Est-il possible de déployer un fichier docker-compose sur swarm ?

Oui c’est possible via docker stack deploy

Est-ce que l’on peut monter des volumes dans les services ?

Oui vous pouvez, maintenant il y aura autant de volume que de noeuds sur lesquels tourneront vos applications.

Analyse de code statique avec phpstan

Phpstan est un outil en ligne de commande qui va vous permettre de détecter automatiquement les erreurs les plus simples en scannant l’intégralité de votre projet.

Démonstration phpstan

De base, l’outil va analyser une application PHP sans tenir compte des spécificités des différents frameworks et librairies. C’est pourquoi il existe des extensions comme PHPstan Symfony pour affiner l’analyse.

Si comme moi vous voulez l’utiliser sur un projet qui a beaucoup de legacy (plus de 10 ans) vous allez avoir un rapport gigantesque. Il faut trouver des solutions pour réduire le rapport à la pull request (merge request) que l’on doit relire. 

Il existe justement un outil qui permet de faire cette corrélation entre le diff et le rapport de PHPstan. Reviewdog est une application en go qui permet de prendre en entrée le rapport d’un outil d’analyse de code comme eslint, golint ou dans notre cas PHPstan.

Sans passer par reviewdog :

➜  vendor/bin/phpstan analyse src -l 4 --no-progress --error-format=raw | wc -l

     292

292 anomalies sur la globalité du projet.

En passant par reviewdog :

➜ vendor/bin/phpstan analyse src -l 4 --no-progress --error-format=raw | reviewdog -diff="git diff master" -f=phpstan | wc -l

	2

Il se reste que deux anomalies, et elles sont directement liées au diff entre la branche courante et master.

Reviewdog peut se connecter directement sur votre gitlab ou github pour ajouter des commentaires dans les pull requests.

Tout ceci ne remplace pas une relecture par un « humain » mais cela va nous donner des indicateurs de qualité et va vous délester des vérifications les plus simples (Est-ce que cette méthode/ classe existe ?). Selon le niveau de vérification que l’on donne à PHPstan on va pouvoir vérifier plus ou moins de choses, comme les dockblocks par exemple.

Ce qui est assez intéressant c’est que PHPstan est extensible et qu’il est donc possible de créer nos propres vérifications. Il serait donc possible, par exemple, de remonter une alerte quand on utilise une classe ou méthode marquée comme dépréciée dans notre code.

Comment vérifier le contenu d’une image docker

Au travail nous avons pour politique de builder et de maintenir nos propres images docker. Mais pour un projet perso j’utilise des images docker publiques. Je m’efforce d’utiliser au maximum des images officielles mais certaines contributions de la communauté sont parfaites pour mes besoins. Le problème est de savoir avec certitude ce que contient un image. Une image téléchargée sur un hub est une boîte noire qu’il faut inspecter avant de l’utiliser.

Cas d’images corrompues

Il y a l’exemple de l’utilisateur docker123321 qui a diffusé 17 images contenant des backdoors sur dockerhub. Parmi les images il y a tomcat, mysql ou encore cron. Avec quasiment 5 millions de téléchargements docker123321 a réussi à miner plus de 500 Moneros en plus des portes d’entrées qu’il a créées sur le serveur.

Contenu d’une image docker

Une image docker est une succession de couches (layers) qui contiennent une liste de modifications du système de fichiers. On peut faire une analogie avec GIT où chaque layer serait un commit. Au moment de la création d’un container un layer est ajouté au-dessus de ceux de l’image. Mis à part ce layer « applicatif » les layers sont en read only.

Inspecter les layers avec Dive

Dive est un programme écrit en Go qui va vous permettre d’en savoir plus sur une image avant de l’utiliser. Il ne va pas permettre de s’assurer à 100% que l’image n’est pas corrompue mais il va pouvoir nous donner des information précieuses. 

Pour l’installation je vous laisse vous référer au readme du projet.

Voici un exemple d’utilisation sur l’image jonbaldie/varnish que j’utilise pour un projet perso. 

➜ dive jonbaldie/varnish

Sur l’image on a 5 layers :

  1. L’image de base
  2. L’ajout d’un script d’installation (/install.sh)
  3. Le résultat de l’execution du script d’install
  4. L’ajout du script de boot (/start.sh)
  5. Le chmod du script de boot

Vérifier les scripts présents dans l’image

Sur la base des informations données par Dive, il ne nous reste plus qu’à vérifier, si possible, le contenu des fichiers install.sh et start.sh

Pour un fichier toujours présent dans le dernier layer

Le fichier start.sh n’est pas supprimé après l’installation. Il est donc simple de le consulter:

➜  docker run  --rm -ti jonbaldie/varnish cat /start.sh

Pour un fichier non présent dans le dernier layer

Pour le fichier install.sh il ne va malheureusement pas être possible de l’afficher car il est supprimé pendant le build. Le docker history de l’image ne me donne pas d’image Id correspondant à ce layer car je n’ai pas buildé l’image sur ma machine.

Docker history sur jonbaldie/varnish

Il n’est pas possible de lancer un container sur une image qui contient le install.sh. Et c’est bien là le problème ! C’est une boîte noire.

Pour arriver à consulter ce fichier il va falloir exporter l’image et naviguer dans les layers « à la main ».

Pour voir le fichier on va récupérer le tar id du layer qui nous intéresse via Dive :

On sait donc maintenant qu’il faut regarder le contenu du tar « c06fe384a155fd3501bdb5689a4d79a18c80a63243038184f457793490b7ddde » pour trouver mon fichier install.sh.

docker save jonbaldie/varnish -o layers.tar
tar xvjf layers.tar
cd c06fe384a155fd3501bdb5689a4d79a18c80a63243038184f457793490b7ddde
tar xvjf layer.tar
cat install.sh
view raw explore layers hosted with ❤ by GitHub
Récupérer un fichier dans un layer docker

Vérifier l’image de base

Dans notre cas l’image est construite à partir d’une debian officielle. Mais comment s’en assurer ? Est-ce que cette debian est à jour ?

Dans un premier temps on va chercher la version de debian installée :

➜  varnish docker run --rm jonbaldie/varnish cat /etc/os-release
Trouver la version de debian utilisée dans une image docker

Malheureusement je ne sais pas comment vérifier que cette debian est bien une version officielle. Je ne peux pas non plus m’assurer que les mises à jour de sécurité sont faites.

Le seul indicateur de qualité à ce niveau est de regarder les builds. Cette image est buildée automatiquement sur la Docker Cloud’s infrastructure. Ce point est primordial dans le choix d’une image non officielle car cela nous donne l’assurance que le docker file affiché est bien celui qui est utilisé pour le build.

Sur le dernier build par exemple nous avons plusieurs informations à disposition : 

Building in Docker Cloud's infrastructure...
Cloning into '.'...
KernelVersion: 4.4.0-93-generic
Arch: amd64
BuildTime: 2017-08-17T22:50:04.828747906+00:00
ApiVersion: 1.30
Version: 17.06.1-ce
MinAPIVersion: 1.12
GitCommit: 874a737
Os: linux
GoVersion: go1.8.3
Starting build of index.docker.io/jonbaldie/varnish:latest...

Step 1/9 : FROM debian
 ---> 874e27b628fd

Step 2/9 : MAINTAINER Jonathan Baldie "jon@jonbaldie.com"
 ---> Running in 3691027bb23a
 ---> 6e1fd6e2af21
Removing intermediate container 3691027bb23a

Step 3/9 : ADD install.sh install.sh
 ---> 2d9d517255c6
Removing intermediate container 1882f5bc4a22

Step 4/9 : RUN chmod +x install.sh && sh ./install.sh && rm install.sh
 ---> Running in 77dd464fc5be
Ign:1 http://deb.debian.org/debian stretch InRelease
//...
Get:8 http://security.debian.org stretch/updates/main amd64 Packages [227 kB]
Fetched 10.0 MB in 4s (2119 kB/s)
Reading package lists...
Reading package lists...
Building dependency tree...
Reading state information...

The following additional packages will be installed:
  binutils cpp cpp-6 gcc gcc-6 libasan3 libatomic1 libbsd0 libc-dev-bin
  libc6-dev libcc1-0 libcilkrts5 libedit2 libgcc-6-dev libgmp10 libgomp1
  libgpm2 libisl15 libitm1 libjemalloc1 liblsan0 libmpc3 libmpfr4 libmpx2
  libncurses5 libquadmath0 libtsan0 libubsan0 libvarnishapi1 linux-libc-dev
  manpages manpages-dev
Suggested packages:
  binutils-doc cpp-doc gcc-6-locales gcc-multilib make autoconf automake
  libtool flex bison gdb gcc-doc gcc-6-multilib gcc-6-doc libgcc1-dbg
  libgomp1-dbg libitm1-dbg libatomic1-dbg libasan3-dbg liblsan0-dbg
  libtsan0-dbg libubsan0-dbg libcilkrts5-dbg libmpx2-dbg libquadmath0-dbg
  glibc-doc gpm man-browser varnish-doc

The following NEW packages will be installed:
  binutils cpp cpp-6 gcc gcc-6 libasan3 libatomic1 libbsd0 libc-dev-bin
  libc6-dev libcc1-0 libcilkrts5 libedit2 libgcc-6-dev libgmp10 libgomp1
  libgpm2 libisl15 libitm1 libjemalloc1 liblsan0 libmpc3 libmpfr4 libmpx2
  libncurses5 libquadmath0 libtsan0 libubsan0 libvarnishapi1 linux-libc-dev
  manpages manpages-dev varnish

0 upgraded, 33 newly installed, 0 to remove and 1 not upgraded.
Need to get 30.5 MB of archives.
After this operation, 123 MB of additional disk space will be used.
Get:1 http://deb.debian.org/debian stretch/main amd64 libbsd0 amd64 0.8.3-1 [83.0 kB]
//...
Get:33 http://deb.debian.org/debian stretch/main amd64 varnish amd64 5.0.0-7+deb9u1 [690 kB]

 [91mdebconf: delaying package configuration, since apt-utils is not installed
 [0m
Fetched 30.5 MB in 1s (18.9 MB/s)

Selecting previously unselected package libbsd0:amd64.
Preparing to unpack .../00-libbsd0_0.8.3-1_amd64.deb ...
//...
Setting up varnish (5.0.0-7+deb9u1) ...
Processing triggers for libc-bin (2.24-11+deb9u1) ...
vcl 4.0;
backend default {
    .host = "127.0.0.1";
    .port = "8080";
}
 ---> 0953378c3b8d
Removing intermediate container 77dd464fc5be

Step 5/9 : VOLUME /var/lib/varnish /etc/varnish
 ---> Running in c56c31df17fb
 ---> e7f1ae57b0f0
Removing intermediate container c56c31df17fb

Step 6/9 : EXPOSE 80
 ---> Running in e1f015e6366e
 ---> 6000a2d9d149
Removing intermediate container e1f015e6366e

Step 7/9 : ADD start.sh /start.sh
 ---> 085cec1148a7

Removing intermediate container 73e8ab753261
Step 8/9 : RUN chmod +x /start.sh
 ---> Running in 8ab33da3607a
 ---> 06f67d6546ac
Removing intermediate container 8ab33da3607a

Step 9/9 : CMD /start.sh
 ---> Running in 3910129787df
 ---> afa9f74c8d64

Removing intermediate container 3910129787df
Successfully built afa9f74c8d64
Successfully tagged jonbaldie/varnish:latest
Pushing index.docker.io/jonbaldie/varnish:latest...
Done!
Build finished

Première information, l’image est bien construite à partir de l’image debian officielle comme on peut le voir à la step 1: « Step 1/9 : FROM debian ».

Seconde information, l’image a été buildée le 17 Août 2017. Toutes les failles détectées depuis cette date sur varnish et debian stretch ne sont donc pas patchées.

Pour les images qui ne sont pas buildées automatiquement sur la plateforme je ne sais pas si c’est possible de vérifier l’intégrité de l’image de base. J’ai tenté plusieurs approches mais comme l’image utilise une « latest » et qu’elle a été remplacée par une nouvelle version je ne vois pas comment faire pour retrouver mon layer. Si vous avez une solution, je suis preneur.

 Builder ses propres images

La solution qui présente le moins de risques est de builder soi-même ses images à partir des Dockerfiles mis à disposition par les contributeurs. De cette manière on peut faire une revue complète du code et des dépendances.  En buildant moi-même mon image je pourrais avoir une version plus à jour de debian et de varnish.

➜ git clone https://github.com/jonbaldie/varnish.git
➜ cd varnish
➜ docker build -t alahaxe/varnish:latest .
Builder une image docker depuis les sources github

Conclusion

Il faut absolument se méfier des images mises à disposition sur le Dockerhub. Avant d’utiliser une image on doit se poser au minimum les questions suivantes :

  1. Est-ce que l’image est officielle ou buildée automatiquement ?
    • Si non, est-ce que les sources sont disponibles ?
      • Si oui, est-ce que le dockerfile présenté est vraiment celui qui a servi à builder l’image ?
  2. De quand date la dernière build ?

Au moindre doute il est préférable de builder sa propre version de l’image ou d’en utiliser une autre.

Intervention à Epitech Nancy

Avec le boulot, on a décidé de partager notre expérience dans les écoles et événements autour du WEB sur la région Nancéienne. Il n’est pas exclu, par la suite, de participer à des événements plus grands comme le forum PHP, le PHP Tour, le blendmix web ou autres événements du genre. Mais comme nous ne sommes pas très habitués à ce genre d’événements autant commencer plus modestement. Depuis des années nous assistons aux conférences, mais passer de l’autre côté du pupitre n’est pas une chose que l’on improvise.

Le 15 octobre nous avons fait notre première présentation technique « en extérieur » dans les locaux d’Epitech Nancy. Nous avons parlé de l’utilisation que nous faisons du cache HTTP sur notre infrastructure SOA.

C’est une très bonne expérience qui nous oblige à vulgariser notre travail pour pouvoir l’expliquer correctement à un groupe. En construisant notre présentation, nous avons dû nous replonger dans la documentation de varnish et des ESI afin d’éviter les approximations ou les contre-vérités. Il est parfois simple d’utiliser une technologie, avec une connaissance partielle, mais pour expliquer le fonctionnement, il faut la maîtriser totalement.

Côté étudiants, nous avons eu des retours plutôt positifs. Nous sommes repartis avec une liste de thématiques, propres à notre infra, qui pourraient être intéressantes à présenter la prochaine fois.

Une première expérience que l’on va essayer de répéter rapidement avec les écoles du secteur.

Symfony Messenger et rabbitmq

Symfony 4 apporte un nouveau composant qui va nous permettre de brancher notre application sur un, ou des, brokers de messages. Grâce à ce composant, on va pouvoir accélérer notre application en traitant de façon asynchrone tout ce qui n’est pas strictement nécessaire à l’affichage de nos pages. Ce module maintenu par la Core Team Symfony va à terme remplacer les bundles existants. 

Use case

Un exemple couramment utilisé pour expliquer pourquoi c’est nécessaire d’utiliser ce genre de technique est le suivant:

Un utilisateur passe une commande sur mon site de e-commerce. Il a payé sa commande et attend juste le message de confirmation. Si je traite tout en synchrone je vais devoir réaliser plusieurs traitements avant de lui rendre la main:

  1. Changer le statut de sa commande
  2. Mettre à jour les stocks et invalider les caches qui correspondent
  3. Envoyer un email de confirmation au client
  4. Prévenir le service de préparation de cette nouvelle commande
  5. Envoyer des metrics pour suivre le volume des ventes

Sur les 5 tâches citées, le client n’a besoin que de la première de façon immédiate. Les autres peuvent se faire dans les secondes ou minutes qui suivent. 

Au-delà du simple point de performance il y a aussi le problème de responsabilité unique. Si toute cette logique est dans le contrôleur, vous allez devoir gérer tout un ensemble de règles disparates. Le contrôleur va forcement devenir inmaintenable au bout d’un moment.

AMQP

Pour pouvoir différer les autres tâches, il va falloir que je pousse des messages dans des queues (type rabbitmq, SQS) et que des workers viennent les consommer pour réaliser les traitements.

Sur cet exemple, de Microsoft, on voit bien le principe d’un broker de message. Un ou plusieurs producteurs (senders) envoient des messages dans une queue et ils sont dépilés par un ou plusieurs consommateurs (receivers).

Dans le meilleur des mondes, vous n’avez qu’une technologie de service de queue et peut-être même qu’un seul serveur à interroger. Dans ce cas, c’est assez simple et vous utilisez sûrement déjà un client php pour le faire.

Mais si vous êtes à cheval sur plusieurs technos/ serveurs, c’est vite un casse-tête car il va falloir gérer plusieurs protocoles et serializers.

Symfony Messenger

Le composant Messenger va vous permettre de gérer les problèmes de techno et de serveurs grâce à de la configuration YML. Dans votre implémentation vous n’aurez pas à vous soucier de comment va partir votre message, ni de comment le sérializer. Le composant est compatible avec les brokers de messages AMQP (la plupart).

  • Message : Un objet PHP serializable
  • Bus : Le composant qui va s’occuper de la communication avec le queue manager et d’appliquer les middlewares que l’on aura pu enregistrer (logs, debug,…)
  • Handler : La classe qui va recevoir un message à exécuter. C’est cette classe qui va tenir la logique métier.
  • Receiver : Déserialize le message reçu via le bus et le transmet au bon handler.
  • Sender : Sérialize le message et le transmet au queue manager via le bus.

Nous allons appliquer ce principe pour le cas de l’envoi du mail de confirmation de commande. Il faudra répéter le pattern pour chacun des autres types d’action à effectuer.

Mise en application

Installation du composant

Le composant s’installe à l’aide de composer via la commande :

composer require symfony/messenger

Symfony flex va s’occuper automatiquement d’enregistrer le bundle et créer le fichier de configuration par défaut.  Le composant vient avec son intégration à la Symfony toolbar et au profiler. Il est donc possible de voir en détail les messages dispatchés lors d’un hit.

Configuration du composant

Je vais prendre le cas d’un rabbitmq. Il va falloir veiller à installer et activer l’extension php amqp.

Une fois que c’est fait, nous pouvons éditer le fichier de configuration de messenger pour y ajouter nos transports.

parameters:
messenger.transport.default_serialization_context: {groups: [messenger]}
framework:
messenger:
transports:
# les Data Source Name pour mes différents transports
amqp_mailer: '%env(MESSENGER_TRANSPORT_DSN_MAILER)%'
amqp_default: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
# En fonction du type de message on peut router le message
# pour ma part j'utilise des interface
'App\Message\Interfaces\Mailer': amqp_mailer
# Tout ce qui n'a pas matché avance passera ici
'*': amqp_default

Dans votre fichier .env à la racine de votre dépôt, il faudra ajouter les différents Data Source Name.

###> symfony/messenger ###
MESSENGER_TRANSPORT_DSN_MAILER=amqp://LOGIN:PASSWORD@HOST:5672/%2f/mails
MESSENGER_TRANSPORT_DSN=amqp://LOGIN:PASSWORD@HOST:5672/%2f/default
###< symfony/messenger ###
view raw .env hosted with ❤ by GitHub

Création de notre message

Le message est un simple objet php.

namespace App\Message\Interfaces;
interface Message
{
}
view raw message hosted with ❤ by GitHub
L’interface par défaut de mes messages (Optionnel mais pratique à l’usage)
<?php
namespace App\Message\Interfaces;
interface Mailer extends Message
{
}
view raw mailer hosted with ❤ by GitHub
L’interface des objets de type mail
<?php
namespace App\Message;
use App\Message\Interfaces\Mailer;
class ConfirmCommandMailer implements Mailer
{
/**
* @var int
*/
protected $commandId;
/**
* ConfirmCommandMailer constructor.
* @param int $commandId
*/
public function __construct(int $commandId)
{
$this->commandId = $commandId;
}
/**
* @return int
*/
public function getCommandId() :int
{
return $this->commandId;
}
}

Dans mon message j’envoie les id des différentes entités. Selon vos besoins vous pouvez directement mettre les entités.

Envoie du message dans le broker

Maintenant que nous avons notre objet de message nous allons pouvoir le pousser.

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Messenger\MessageBusInterface;
use App\Message\ConfirmCommandMailer;
class CommandController extends Controller
{
/**
* @var MessageBusInterface
*/
private $bus;
/**
* CommandController constructor.
*/
public function __construct(MessageBusInterface $bus)
{
$this->bus = $bus;
}
/**
* @Route("/customer/{idCustomer}/command/{idCommand}/confirm", name="confirm_command", methods={"POST"})
* @param $name
*/
public function index($idCustomer, $idCommand)
{
// check des params
// changement du statut de la commande
//…
// on envoie notre message dans le broker
$this->bus->dispatch(new ConfirmCommandMailer($idCommand));
// rendu du tpl
}
}

Traitement du message

Pour le moment, votre application ne fonctionnera pas car Symfony Messenger refusera de prendre en compte un message dont il ne connait pas le handler.

<?php
namespace App\Message\Handler;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use App\Message\ConfirmCommandMailer;
use \Swift_Mailer;
/**
* Class ConfirmCommandMailerHandler
*/
class ConfirmCommandMailerHandler
{
/**
* @var Swift_Mailer
*/
protected $mailer;
/**
* @var EntityManagerInterface
*/
protected $entityManager;
/**
* ConfirmCommandMailerHandler constructor.
* @param \Swift_Mailer $mailer
* @param EntityManagerInterface $entityManager
*/
public function __construct(Swift_Mailer $mailer, EntityManagerInterface $entityManager)
{
$this->mailer = $mailer;
$this->entityManager = $entityManager;
}
/**
* @param ConfirmCommandMailer $message
*/
public function __invoke(ConfirmCommandMailer $message)
{
$command = $this->entityManager->getRepository(Command::class)->find($message->getCommandId());
// verifier que les données sont cohérantes
$message = (new \Swift_Message('Commande confirmée'))
->setFrom('no-reply@example.com')
->setTo($command->getCustomer()->getEmail())
->setBody(
"Commande #" . $command->getId() . " confirmée"
)
;
$this->mailer->send($message);
}
}

Il faut maintenant enregistrer notre handler dans le container avec le tag ‘messenger.message_handler’.

App\Message\Handler\ConfirmCommandMailerHandler:
tags: ['messenger.message_handler']
view raw service.yml hosted with ❤ by GitHub

À partir de ce moment, Symfony va vous autoriser à dispatcher les messages ConfirmCommandMailer. Pour cet exemple, j’ai utilisé  Swift Mailer mais libre à vous d’utiliser une autre librairie.

Le composant va utiliser la reflection PHP pour détecter le handler qui doit être utilisé pour un message. Il va regarder le type du paramètre passé à la fonction __invoke. 

Lancer le worker

Maintenant que l’on a tout ce qu’il nous faut, il ne reste plus qu’à lancer notre consommateur. Il faudra lancer au minimum autant de workers que de channels.

bin/console messenger:consume-messages amqp_mailer

Il y a pas mal d’options disponibles pour limiter la durée de vie du daemon, la mémoire allouée, le temps de pause entre chaque message traité… Pour ma part je lance toujours au moins deux consommateurs pour un type de message et je fais en sorte qu’ils se tuent automatiquement tous les n messages traités.

Voici une démonstration sous forme de GIF de ce que l’on vient de faire. Pour simplifier la démonstration, j’ai fait une seconde commande Symfony qui pousse notre message dans la queue.

En production

En production il faut automatiser le lancement du daemon et le relancer en cas de crash. Pour ce faire vous pouvez utiliser supervisor avec la config suivante :

[program:amqp_mailer]
command=php /symfony/bin/console messenger:consume-messages amqp_mailer
startsecs = 0
stdout_logfile=/tmp/supervisord-amqp.log
stdout_logfile_maxbytes=10MB
view raw gistfile1.txt hosted with ❤ by GitHub

Généralement je fais tourner les workers dans des conteners docker dans un cluster swarm. De cette manière je peux gérer le nombre de consommateurs par type de message à la volée. Voici un exemple de Docker file qui peut faire tourner un worker symfony messenger. Je l’ai fait pour un projet perso, il n’est donc pas parfaitement optimisé pour de la vraie prod. 

FROM php:7.2
# Composer
RUN php -r "copy('https://getcomposer.org/installer&#39;, 'composer-setup.php');" \
&& php -r "if (hash_file('SHA384', 'composer-setup.php') === '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
&& php composer-setup.php –install-dir=/bin –filename=composer \
&& php -r "unlink('composer-setup.php');"
# amqp
RUN apt-get update && apt-get install -y librabbitmq-dev libssh-dev \
&& docker-php-ext-install opcache bcmath sockets \
&& pecl install amqp \
&& docker-php-ext-enable amqp
# core tools
RUN apt-get install -y git-core
# php exts
RUN docker-php-ext-install pdo pdo_mysql zip mbstring
RUN mkdir /app
WORKDIR /app
env QUEUE_NAME amqp_default
ENTRYPOINT ["sh", "-c", "/app/bin/console messenger:consume-messages ${QUEUE_NAME} –limit=100 –time-limit=900 -n"]
Exemple de docker file simple pour un worker Symfony messenger

Et voici comment l’utiliser dans le cadre d’un docker compose :

version: '3'
services:
worker_mailer:
image: ma-super-image-de-worker:latest
environment:
– QUEUE_NAME=amqp_mailer
volumes:
– "/PATH/VERS/MON/APP/SF:/app:rw"
deploy:
mode: replicated
replicas: 2

Vous pouvez maintenant faire un docker-compose up ou le lancer sur un cluster swarm via la commande docker stack deploy.

Limitation

Pour le moment il n’y a pas de solution « out of the box » pour gérer le re-jeux des messages en cas d’erreur. C’est à vous de catcher les erreurs et soit de les pousser dans une autre queue, d’écrire un log, une métrique, ou de les stocker en base pour les identifier au besoin. C’est une lacune assez importante de la librairie qui devrait être corrigée.

Les problèmes possibles

Que veux dire l’erreur : Attempted to load class « AMQPConnection » from the global namespace.
Did you forget a « use » statement?

Ce message vous signale que vous n’avez pas l’extension amqp d’activée sur votre machine. Il faut installer et activer l’extension php-amqp.

Comment résoudre l’erreur: [ErrorException] Declaration of SymfonyFlexParallelDownloader::getRemoteContents($originUrl, $fileUrl, $context) should be compatible with ComposerUtilRemoteFilesystem::getRemoteContents($originUrl, $fileUrl, $context, ?array & $responseHeaders = NULL)

Il faut exécuter composer update –no-plugins pour mettre Symfony flex à jour.

Comment corriger l’erreur : [SymfonyComponentMessengerExceptionNoHandlerForMessageException]
No handler for message « AppMessageConfirmCommandMailer ».

Il faut veiller à deux points pour trouver la source de cette erreur. Premièrement que vous avez bel et bien créé un handler avec une méthode __invoke qui prend un objet de type AppMessageConfirmCommandMailer en premier et unique paramètre. Et dans un second temps que vous avez bien ajouté votre handler dans votre fichier services.yml avec le tag « messenger.message_handler »

Du machine learning dans mes cocktails

Dans cet article, nous allons parler Mojitos, machine learning et PHP, trois sujets que l’on mélange assez rarement, mais qui dans le cas du site Cocktailand sont rassemblés.

Use case

Je souhaite afficher la recette de cocktail qui ressemble le plus à celle que le visiteur est en train de lire. Comme je suis finalement quelqu’un d’assez fainéant, je n’ai pas envie de maintenir des listes de cocktails associés manuellement. Il faut donc trouver un moyen de calculer automatiquement cette liste pour les quelques 600 recettes et que cette liste soit mise à jour automatiquement pour intégrer les nouvelles recettes.

Données à disposition:

  • Les ingrédients de la recette, avec les volumes pour chacun
  • Est-ce que le cocktail est alcoolisé ou non
  • La catégorie du cocktail
  • Le nombre de vues de la recette de cocktail
Vision simplifiée de la base de données 

KNearestNeighbors

Afin de résoudre ce problème, je vais utiliser un algorithme de classification nommé KNearestNeighbors mis à disposition en PHP via la libraire php-ml avec une méthode de calcul de distance custom entre les cocktails.

Cet algo, aussi appelé le K-NN, permet de classifier des objets en fonction de la classe des autres objets déjà classifiés à proximité. La proximité est calculée par une méthode du type Euclidienne, Hamming,  Manhattan ou encore selon des règles spécifiques au domaine. Il suffit que la méthode soit idempotente et qu’elle retourne une valeur numérique pour qu’elle soit utilisable.

 Le K de cet algorithme est la valeur de validation croisée qui va permettre de choisir la classe à associer à l’objet. 

Pour déterminer la classe de l’objet vert, on va regarder la classe des objets à proximité, selon la valeur de K.

Pour K=1 on va choisir la classe de l’élément le plus proche.

Pour K=n on va choisir la classe la plus représentée dans les éléments sélectionnés.

Méthodologie

Plusieurs étapes sont nécessaires pour utiliser cet algorithme sur un jeu de données :

  1. Normaliser les données pour construire notre modèle
  2. Construire le modèle
  3. Ecrire ou Choisir la méthode de calcul de distance
  4. Choisir notre K
  5. Utiliser le modèle pour faire de la prédiction

Normalisation des données

Cette étape va nous permettre de supprimer tout ce qui n’apporte pas de sens et transformer toutes les données qui composent un cocktail par des entiers. Pour mon modèle j’ai choisi de prendre en compte seulement les informations suivantes :

  • Les ingrédients, en les représentant par leur id en base
  • Le fait de savoir si le cocktail est alcoolisé ou non (0 ou 1)

Afin de limiter le bruit dans le modèle j’ai décidé de supprimer des ingrédients non discriminants comme l’eau, la glace et le sucre. L’idée est de ne pas laisser l’algorithme penser que deux cocktails sont similaires car on y met des glaçons. Une fois que j’ai ma liste d’ingrédients nettoyée, il faut normaliser les données afin de les transformer en tableau d’entier. C’est un prérequis de l’implémentation de php-ml.

Pour le moment, je vais mettre de côté la popularité du cocktail, mais je pourrais la segmenter en 3 groupes peu/ moyen/ très populaire. Pour ce classement, il faudrait simplement ramener le nombre de vues en un nombre de 1 à 3 (via les percentiles 25% et 75% par exemple) et de valoriser cette information au moment du calcul de distance.

Construire le modèle

Pour cette étape, je vais créer une matrice qui aura en nombre de lignes le nombre de cocktails du site et en nombre de colonnes le nombre maximum d’ingrédients plus un d’un cocktail sur le site. Cette étape peut être particulièrement coûteuse en mémoire.
Une ligne de la matrice contient donc en premier index 0 ou 1 selon si le cocktail contient de l’alcool et dans les colonnes suivantes les ids des ingrédients qui composent le cocktail. Les cellules vides de la matrice sont remplies par des 0 car il est nécessaire d’avoir des lignes de la même taille.
Sur l’implémentation proposée, il y a un second tableau qui doit contenir les labels (classes) associées aux différentes lignes. J’ai choisi d’y mettre l’id (casté en string) du cocktail.
Comme cette étape est un peu coûteuse, je serialize la matrice dans un fichier pour pouvoir m’en resservir au besoin.

Une fois la matrice générée nous pouvons entrainer notre modèle (avec la méthode « train » ou « fit » selon vos librairies) sur notre jeu de données.

Choisir notre K

Dans notre cas, chaque élément est porteur de sa propre classe. C’est un cas un peu particulier, mais qui ne pose pas spécialement de problème pour cet algo. Mais au vu du fonctionnement de la valeur de validation croisée, il faut que nous utilisions un K à 1. Une valeur supérieure n’aurait pas de sens, car nous aurions autant d’ex aequo que la valeur de K.

Ecrire la méthode de calcul de distance

Comme notre modèle est assez spécifique et que faire une ACP sur notre matrice n’aurait pas de sens, j’ai pris le parti d’écrire ma propre méthode de calcul de distance.
La méthode de calcul de distance prend en paramètre deux lignes de la matrice et doit retourner un float (la distance). Libre à nous de réfléchir à la meilleure façon de faire ce calcul ou d’utiliser une méthode disponible de base.
Voici mon implémentation :

<?php
namespace App\Services;
use Phpml\Math\Distance;
class CocktailsDistance implements Distance
{
/**
* @param array $a
* @param array $b
*
* @return float
*/
public function distance(array $a, array $b):float
{
// un sans et un avec alcool ?
$distance = array_shift($a) !== array_shift($b)?10:0;
$count = count($a);
for ($i = 0; $i < $count; ++$i) {
// +2 de distance par différence de nombre d'ingrédient
if ($a[$i] === 0 && $b[$i] !== 0) {
$distance += 2;
continue;
}
// +3 si un ingrédient de a n'est pas présent dans le cocktail b
$distance += in_array($a[$i], $b, true) ? 0 : 3;
}
return $distance;
}
}

On remarque que j’utilise des nombres magiques dans mon algorithme (2, 3, 5, 10) . Les valeurs me permettent de donner plus ou moins d’importance à un type de différence. Une grande discrimination est donnée pour une différence au niveau de la présence ou non d’alcool.

Exemple sur un jeu de données

[table id=2 /]

Voici la matrice qui est générée pour ce jeu de données avec sur la droite le nom des cocktails associés à chaque ligne:

Dans les colonnes du premier tableau, nous avons dans la première colonne la présence ou non d’alcool et dans les autres les ids des ingrédients qui composent la recette.

Nous allons maintenant utiliser notre modèle pour trouver le cocktail le plus proche du Mojito.

// récupération de mon entité, avec doctrine ici
$mojito = $this->entityManager->getRepository(Cocktail::class)->find(666);
// nous génère la matrice en enlevant le mojito
$matrices = $this->cocktailMachineLearning->generateMatricesWithoutCocktails([$mojito]);
// initialisation de l'algo
$classifier = new KNearestNeighbors(1, new CocktailsDistance());
// entrainement de l'algo sur les données normalisées
$classifier->train($matrices['samples'], $matrices['labels']);
// normalisation du mojito
$normalizeMojito = [
intval($mojito->isAlcohol())
];
foreach($mojito->getIngredients() as $ingredientWithMeasure) {
$normalizeMojito[] = $ingredientWithMeasure->getIngredient()->getId();
}
sort($normalizeMojito);
while (count($normalizeMojito) < $model['maxNbIngredients']) {
$normalizeMojito[] = 0;
}
// prediction de la recette la plus proche
$cocktailLabel = $classifier->predict($normalizeMojito);
dump(cocktailLabel);
view raw prediction hosted with ❤ by GitHub

La sortie en console est la suivante:

C’est bien le résultat que l’on aurait imaginé, mais pour comprendre pourquoi ce résultat, il faut s’intéresser aux distances que l’algo a calculé pour chaque cocktail. Voici ce que l’on obtient :

On remarque que la discrimination sur l’absence d’alcool est forte. C’est une volonté de ma part pour éviter, si possible, de proposer des recettes avec alcool pour un cocktail sans alcool.

Sur le site le volume de données à traiter est beaucoup plus gros évidement mais le principe reste le même. 

Voici le résultat en production sur la recette du mojito:

Pour arriver à ce résultat sur un grand jeu de données, je répète l’algorithme n fois en retirant du modèle les recettes déjà sélectionnées.

Automatisation

La génération de la matrice est assez coûteuse, il faut quasiment dumper toute la base de données et travailler dessus. Il n’est donc pas possible de le faire « à la demande ». J’ai pris le parti de faire une commande Symfony qui est exécutée plusieurs fois par jour pour mettre à jour les données normalisées dans un fichier plat. Et pour la partie entraînement et prédiction, le résultat est mis en cache HTTP à l’aide d’un ESI pour 24h. Cela signifie qu’un nouveau cocktail ne remontera dans les suggestions que maximum 24h après son ajout à moins de flusher le cache varnish manuellement. Dans le cadre de Cocktailand cette latence est tout à fait acceptable. 

Cocktailand – Gérer ses images sous symfony

La page d’accueil de cocktailand affiche en moyenne 35 images. Dans sa première version, je n’avais pas pris la peine de soigner la gestion des images. J’avais pour objectif de sortir le produit minimum viable, je me suis donc attardé sur les fonctionnalités principales:

  • L’ajout des recettes de cocktail
  • L’affichage des recettes de cocktails
  • La catégorisation
  • Le moteur de recherche

Cette méthodologie permet de sortir rapidement un site de base et de l’enrichir de façon successive.

Le fait d’avoir mis en place varnish et les esis faisait que la page s’affichait très rapidement mais les images mettaient du temps à s’afficher.

Dans cet article je vais faire le point sur les différentes techniques à mettre en place pour réduire au maximum le temps de chargement des images sur votre site WEB.

Optimiser les images

La première chose à faire est de redimensionner votre image à une taille raisonnable.

Il est rare d’avoir besoin d’une image avec une résolution plus grande que 1024×768 pixels.

La seconde étape est d’optimiser vos images à l’aide d’outils comme imageoptimcompressor.io ou tout autre outil du genre.

Vous pouvez régler l’outil pour avoir ou non de la perte de qualité au profit d’un gain de taille.

Il n’est pas rare de gagner 70% de taille sur une image.

Sur 35 images de 1Mo, on gagnerait 25Mo.

Cache http

Maintenant que nous avons optimisé la taille de nos images au maximum et que le visiteur les a téléchargées, nous allons dire à son navigateur de les conserver pour la prochaine fois.

Côté varnish j’ai pour habitude d’avoir cette règle qui surcharge le retour de nginx pour ajouter une durée de cache de 24h.

sub vcl_backend_response {
if (bereq.url ~ "\.(jpe?g|png|gif|pdf|tiff?|css|js|ttf|woff2?|otf|eot|svg)$") {
set beresp.ttl = std.duration(beresp.http.age+"s",0s) + 24h;
}
}
view raw vcl image hosted with ❤ by GitHub

Cette règle est géniale si chaque fichier est immuable et a une url unique. Cela implique que lors de votre build vous ajoutiez un hash unique dans le nom de vos JS et CSS. Si vous avez des fichiers qui changent au cours de la journée, cette solution n’est pas faite pour vous.

Http 2

Http 2 permet d’optimiser très largement le temps de téléchargement des assets, car il conserve la même connexion au serveur. HTTP 1 pour sa part initialise une connexion par fichier à télécharger. Cette démonstration http2 vs http1 va vous permettre de vous rendre compte du gain par rapport à http 1.

Le site va télécharger une mosaïque qui est composée de 100 petites images en http 1 puis en http 2. Le gain est environ de 60% de temps de chargement sur mon macbook pro avec une connexion fibre.

Si vous ne savez pas comment faire, ou que vous n’êtes pas techniquement sûr de comment gérer http2 sur votre serveur, vous pouvez utiliser cloudflare. Par défaut, cloudflare va gérer les connexions http 2 tout en continuant de faire des appels http 1 sur votre serveur. Il va s’occuper de multiplexer les requêtes et tout se passera de manière transparente pour vous. Ce n’est pas parfait, mais c’est mieux que rien…

Pré-calculer les différentes tailles

Pour la partie serveur, nous avons maintenant une stack idéale pour afficher rapidement nos images.

Il ne reste que quelques détails à régler. Dans la première partie je vous ai dit de redimensionner vos images au format maximum que vous pourrez un jour utiliser.

Mais pour avoir de bonnes performances, il va falloir aller plus loin.

C’est le moment de pré-calculer l’ensemble des tailles d’images que vous allez utiliser. L’idée est de ne faire télécharger que le strict nécessaire à l’utilisateur. Si nous avons besoin d’une image de 64×64 pixels il est préférable d’envoyer une image qui fait déjà la bonne taille plutôt que de télécharger une grosse image et de laisser le navigateur la redimensionner. Vous allez gagner en bande passante mais aussi en CPU.

Par exemple, sur cocktailand, une image peut actuellement être utilisée avec les tailles suivantes:

  • 64×64
  • 240×180
  • 270×200
  • 400×400

Pour un peu plus de 1000 images sur le site, le travail n’est pas faisable à la main. D’autant plus que je ne m’interdis pas d’ajouter de nouvelles tailles si je modifie le design ou les supports.

Sur Symfony, il existe un bundle liip/imagine-bundle qui permet cette automatisation. Le bundle va générer à la volée les miniatures lors de la première demande puis les stocker sur le serveur pour les prochaines demandes.

Une fois le bundle installé dans votre projet, vous n’avez qu’à enregistrer vos formats dans la configuration et à utiliser le filtre twig.

liip_imagine:
resolvers:
default:
web_path:
web_root: "%kernel.project_dir%/public"
# en écrivant ici on garde le cache entre chaque release
cache_prefix: "images/cache"
loaders:
default:
filesystem:
data_root: "%kernel.project_dir%/public/"
cache: default
data_loader: default
# valid drivers options include "gd" or "gmagick" or "imagick"
driver: "gd"
# define your filter sets under this option
filter_sets:
# an example thumbnail transformation definition
# https://symfony.com/doc/current/bundles/LiipImagineBundle/basic-usage.html#create-thumbnails
thumbnail:
jpeg_quality: 75
png_compression_level: 8
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [64, 64]
mode: outbound
allow_upscale: true
large:
jpeg_quality : 90
png_compression_level: 6
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [400, 400]
mode: outbound
allow_upscale: true
home_header_small:
jpeg_quality : 80
png_compression_level: 7
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [300, 300]
mode: outbound
allow_upscale: true
home_header_big:
jpeg_quality : 80
png_compression_level: 7
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [900, 300]
mode: outbound
allow_upscale: true
page_header:
jpeg_quality : 80
png_compression_level: 7
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [200, 200]
mode: outbound
allow_upscale: true
article_media:
jpeg_quality : 80
png_compression_level: 7
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [200, 220]
mode: outbound
allow_upscale: true
pub_media:
jpeg_quality : 80
png_compression_level: 7
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [250, 250]
mode: outbound
allow_upscale: true
avatar_media:
jpeg_quality : 80
png_compression_level: 7
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [200, 250]
mode: outbound
allow_upscale: true
medium_desktop:
jpeg_quality : 80
png_compression_level: 6
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [240, 180]
mode: outbound
allow_upscale: true
medium_tablet:
jpeg_quality : 85
png_compression_level: 6
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [270, 200]
mode: outbound
allow_upscale: true
medium_mobile:
jpeg_quality : 90
png_compression_level: 5
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [400, 400]
mode: outbound
allow_upscale: true
tiny:
jpeg_quality: 60
png_compression_level: 8
filters:
auto_rotate: ~
strip: ~
thumbnail:
size: [35, 35]
mode: outbound
allow_upscale: true
view raw liip_imagine hosted with ❤ by GitHub

On pourra remarquer que plus l’image est petite, plus j’ai fait baisser la qualité.

Résultat pour une image déclinée dans plusieurs des tailles:

Respectivement les tailles sont de 2Ko, 12Ko et 39Ko. Il y a donc un facteur 20 entre la miniature et le plus grand format utilisé sur le site.

Lazy loading/ image set

Le dernier point permet un énorme gain de performance, mais il peut aussi impacter votre SEO si vous le faites mal. L’idée est de ne faire télécharger au visiteur que les images qui seront immédiatement visible à l’écran. Si l’image n’est pas visible à l’écran, elle ne sera pas téléchargée au chargement, mais au scroll quand elle entrera dans le viewport. Le problème réside dans le fait de mettre en place cette technique et en laissant Google (ou autre) voir les images. Le paquet npm vanilla-lazyload est vraiment sympa car il répond aux deux points précédents et en plus il permet de mettre en place des « images responsives » via les srcset.

Une fois le paquet installé vous n’avez quasiment rien à faire pour initialiser la librairie.

const LazyLoad = require('vanilla-lazyload');
let lazyLoad = new LazyLoad();
view raw LazyLoad hosted with ❤ by GitHub

Une fois la librairie mise en place et instanciée côté javascript, il ne reste plus qu’à utiliser les data attributes des images pour l’utiliser. Voici un exemple d’utilisation avec imagine.

<img alt="Recette simple du Mojito alsacien"
data-src="{{ asset(cocktail.image)|imagine_filter('medium_desktop') }}"
data-srcset="{{ asset(cocktail.image)|imagine_filter('medium_mobile') }} 200w, {{ asset(cocktail.image)|imagine_filter('medium_tablet') }} 400w"
sizes="(min-width: 20em) 35vw, 100vw">
view raw data-srcset hosted with ❤ by GitHub

Conclusion

Une fois l’ensemble des techniques mises en place, le chargement de la home ne télécharge « que » 1.2 Mo d’images, publicités incluses. Voici des données issues de webPageTest concernant la home.

Le premier chargement de la page pour un visiteur:

Le second chargement de la page pour un visiteur:

On remarque que le nombre d’assets téléchargés lors du second chargement est plus faible. Si on regarde en détails la liste des fichiers téléchargés, on constate, avec chagrin, que plus rien ne provient de cocktailand. Il ne reste en effet que le tracking et la publicité.

Cocktailand – Ajouter du cache HTTP dans mon symfony

C’est quoi un ESI ?

Les ESI ou Edge Side Includes sont un balisage supporté par Varnish qui permet de gérer des temps de cache différents pour des blocs de la même page.

Dans le cadre de Cocktailand, certains blocs sont actualisés régulièrement comme le « Cocktail du jour » mais d’autres ne changent quasiment jamais comme la liste des catégories.

Voici donc le découpage que j’ai fait sur la page principale. Pour la barre de menu, c’est bien évidemment le contenu du méga menu que j’ai voulu mettre en évidence.

Il est donc intéressant de ne pas avoir à invalider toute la page lorsque le cocktail du jour est changé. Le second avantage en termes de performance est que les blocs peuvent être utilisés sur différentes pages. Cela signifie qu’un ESI présent sur toutes les pages du site ne sera généré qu’une seule fois. Lors des autres appels, Varnish utilisera son cache.

Configuration de varnish

Pour Cocktailand, la configuration de varnish est assez simple car je ne fais pas de purge et parce qu’il n’y a pas d’espace connecté sur le site.

Voici la configuration que j’ai:

vcl 4.0;
import std;
backend default {
// le hostname du nginx dans ma stack
.host = "front";
.port = "80";
}
sub vcl_recv {
set req.http.Surrogate-Capability = "abc=ESI/1.0";
unset req.http.Cookie;
}
//Ensuite, ce block est appelé après la réception des headers de réponse.
//Nous supprimons le header et activons les ESI
sub vcl_backend_response {
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
if (bereq.url ~ "\.(jpe?g|png|gif|pdf|tiff?|css|js|ttf|woff2?|otf|eot|svg)$") {
set beresp.ttl = std.duration(beresp.http.age+"s",0s) + 24h;
}
}
sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
}
view raw varnish vcl hosted with ❤ by GitHub

La mise en place dans Symfony 4

ESI

Le support des ESI dans Symfony est intégré nativement dans le framework.

Dans le fichier config/packages/framework.yaml il faut activer les ESIs.

framework:
esi: { enabled: true }
fragments: { path: /_fragment }
view raw framework.yaml hosted with ❤ by GitHub

Toutes vos routes qui servent des ESI devront commencer par _fragment.

Dans vos vues twig, vous avez des helpers à disposition pour poser vos tags.

{{ render_esi(url('popular_cocktails')) }}
view raw render_esi hosted with ❤ by GitHub

Cache http

En utilisant le package sensio/framework-extra-bundle on peut gérer le cache sur les controller avec des annotations.

/**
* @Route("/cocktail/recette/{name}-{id}", name="cocktail_detail", requirements={"id"="\d+", "name"="[0-9a-z-]+"})
* @Cache(public=true, maxage=86400, mustRevalidate=false, maxStale=86400)
**/
public function index($name, $id)
{
// …
}
view raw @Cache hosted with ❤ by GitHub

Temps de réponse

Une fois les ESIs et le cache HTTP mis en place, on peut analyser les performances avec Webpagetest.

L’outil va nous donner des informations sur le temps de réponse de l’application et surtout des indications sur ce qu’il faudrait améliorer.

Par exemple, sur ce test, il me dit que je peux potentiellement améliorer la gestion des fonts et des images.

// @todo activer gzip sur les fonts

Ce que l’on peut remarquer, c’est que le site commence à envoyer le HTML après 231ms. Dans ce temps il y a en moyenne 40ms de DNS Lookup, 30ms de connexion et 80ms de négociation SSL. Malheureusement sur cette partie je n’ai pas la main, c’est donc l’overhead de base pour toute page du site. Mais comme j’utilise HTTP2 je vais mutualiser toute cette partie pour les images assets servies sur le même domaine.

Les nouvelles problématiques

Tout mettre en cache, c’est bien pour les performances, mais malheureusement certaines informations ont besoin de « temps réel ».

Quand un visiteur ajoute une note sur une recette, il est nécessaire que cette note mise à jour si le visiteur rafraîchit la page.

Il y a plusieurs solutions:

  • Faire des bans de cache lors de l’ajout d’une note
  • Afficher les données du cache et rafraîchir les données en AJAX

Bien évidemment, la première solution est celle qui devrait être implémentée. Maintenant, c’est pas mal de code et de configuration sur varnish. C’est d’autant plus compliqué que, dans sa version gratuite, Varnish ne permet pas de faire un ban sur plusieurs instances.

Faire les bans impliquerait de maintenir une liste des varnishs qui tournent (si jamais je fais scaller cette brique) et de faire n appels curl pour faire un ban partout.

Pour le moment, j’ai fait le choix de la requête AJAX non cachée. Si jamais le site gagne en popularité, il faudra retravailler sur ce point.

Conclusion

Sur un blog ou un site dans lequel le visiteur ne peut quasiment pas interagir avec vos données, c’est très simple de mettre en place du cache varnish et le gain de performance est énorme. Maintenant, si vous avez des besoins plus complexes (site transactionnel, forum,…) vous allez devoir mettre en place une mécanique d’invalidation de cache.

Cocktailand – La stack technique


A la vue de cet article, on me dira surement que la stack technique du site Cocktailand est overkill.

Ma réponse est simple: « Ouaip carrément !« 

Au-delà du plaisir de travailler sur la thématique des cocktails, je travaille sur ce projet aussi pour me faire plaisir techniquement et pour tester des technologies ou des services en mode SAS.

Comme j’ai toujours la flemme de documenter mes sides projects, cet article fera office de documentation « technique ».

Je vais donc découper cet article en trois parties:

  • L’hébergement
  • L’application
  • Les services SAS utilisés

L’hébergement

Scaleway

Depuis la beta de scaleway, filiale de Online, je prends tous mes serveurs persos chez eux. Ils sont à la fois fiables, efficaces et abordables. Il n’y a pas de frais d’installation et on peut libérer un serveur à tout moment. Pour Cocktailand, j’ai pris deux serveurs pour démarrer, un pour la prod et un pour les backups qui n’a pas d’accès à Internet (ni entrant ni sortant). Pour toute la partie métrique graphite/ grafana, j’ai pluggué le site sur mon infra existante.

Le serveur qui héberge le site est un START1-S. Ce n’est pas une bête de course, mais au besoin je passerai à la gamme au-dessus.

Cloudflare

Cloudflare est un gestionnaire de DNS en ligne qui a pour particularité d’avoir un temps de propagation proche de 0. Dans son offre de base, qui est gratuite, il permet de gérer plusieurs noms de domaine, il autorise les wildcards dans les sous-domaines, mais il propose aussi tout un tas d’autres fonctionnalités intéressantes. En effet, dans l’offre gratuite, il propose:

  • Une protection DDOS
  • Une optimisation des assets à la volée
  • Une redirection http vers https automatique
  • Une modification des urls des assets des pages de http vers https (un garde-fou sympa si on est full https)
  • Une redirection automatique vers un site mobile (s’il est différent)
  • La possibilité de bannir des ips (manuellement ou via une API)

Pour ma part, je l’utilise depuis longtemps et je n’ai jamais eu à me plaindre du moindre dysfonctionnement.

Docker compose

La stack du site est entièrement gérée avec docker.

Chaque brique de la partie docker (varnish, nginx, phpfpm ) peut scaller horizontalement.

Pour le moment, le serveur MySQL ainsi que les images ne sont pas scallables. Je travaillerai sur les deux points si jamais le besoin s’en fait sentir. Normalement, avec l’utilisation intensive de varnish qui est faite, la base de données n’est pas énormément sollicitée.

L’application

Le site est développé avec le framework Symfony. J’ai choisi SF pour sa stabilité, sa fiabilité, mais aussi car j’ai l’habitude de travailler avec au quotidien. Je sais que, bien utilisé, il est capable de tenir la charge.

Le site fait un usage assez intensif des ESI afin de permettre une gestion du cache http assez fine. Quasiment chaque bloc de la home est un ESI. Il est par exemple possible d’avoir un temps de cache de quelques heures pour le cocktail du jour et un cache d’une journée sur le top des cocktails les plus consultés.

Images

La problématique n’est pas de trouver des recettes de cocktails, mais de trouver des images libres de droits. Je n’ai malheureusement pas eu le temps de confectionner par moi-même l’ensemble des cocktails présents sur le site.

Voici donc une liste non exhaustive des sites sur lesquels je vais chercher des images quand je n’en ai pas:

Les services SAS utilisés

Comme je l’ai dit plus tôt, Cocktailand a aussi pour but de tester des technologies ou des services en SAS.

Algolia

Surnommé le « Google des apps », Algolia est un service externe de moteur de recherche. Il remplace le bon vieux cluster ElasticSearch que j’aurais pu mettre sur mon serveur.

En plus d’être extrêmement performant, algolia a un service client au top.

L’offre gratuite me permet largement de faire tourner mon moteur de recherche sans restriction et avec des temps de réponse aux alentours de 50ms. La seule contrepartie demandée par algolia est de préciser que la recherche est fournie par eux en dessous des résultats.

Très franchement, je suis agréablement surpris par cette techno.

Logz.io

Logz est un agrégateur de logs en mode SAS. Comme je n’ai rien de critique dans les logs, je me suis permis de les externaliser. On retrouve une bonne vieille stack ELK.

Pour envoyer les logs, il m’a suffi d’ajouter leur container docker rsyslog dans ma stack docker-compose. Une fois les bonnes variables d’environnements mises en place les logs remontent chez eux en quasi temps réel.

Au-delà de la simple consultation et recherche dans les logs, ils proposent un outil d’alerting. Il est par exemple possible d’envoyer un mail quand un grand nombre d’exceptions ou de fatales sont remontées.

Coût de la plateforme

Comme le site est encore récent et qu’il ne génère aucun revenu, j’ai fait le maximum pour réduire les coûts.

Voici un état des lieux des dépenses engendrées par Cocktailand.

[table id=1 /]