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 /]