Search and destroy: bottlenecks in Erlang

par Gautier DI FOLCO, janvier 2014.

Introduction

Implanter des systèmes distribués est une tâche complexe qui requiert de bons outils et une bonne méthode afin que ces systèmes puissent passer à l'échelle.

Nous verrons comment réaliser un Two-phase commit en Erlang et comment, en deux itérations, nous pouvons le rendre plus performant.

Erlang

Erlang est un langage de programmation fonctionnel basé sur le modèle à acteur, développé par Ericsson, afin de pouvoir construire des systèmes distribués et fiables. Erlang était destiné au domaine des télécommunications.

Modèle à acteur

Le modèle à acteur est une approche qui vise à concevoir un système sous la forme d'un ensemble de processus concurrents.

Ces processus sont nommés Acteurs; Ils communiquent par envoi de messages (ou message passing).

Chaque acteur est l'unique propriétaire de ses propres données et il possède une messagerie comme point d'entrée où sont stockés les messages qui lui sont adressés et qu'il traitera par ordre d'arrivée.

Programmation fonctionnelle

La programmation fonctionnelle est à opposer à la programmation procédurale : il s'agit d'exprimer les relations entre les données plutôt que de décrire l'ordre des traitements qui leur seront appliqués.

Le point principal à retenir est que les modifications de variables sont impossibles et, de ce fait, les boucles sont simulées par récursion; De plus, pour pouvoir représenter la progression d'un taitement, il faut impérativement construire une machine à états finis.

Machine virtuelle

De la même manière que les programmes JAVA et C# sont compilés vers du bytecode, pour être exécutés par une machine virtuelle, respectivement la JVM et la CLR; les programmes Erlang sont compilés vers un format BEAM pour être exécutés par le Erlang runtime system.

L'avantage de ce système est que la gestion des serveurs et des nœuds du système (ainsi que tous les aspects de gestion de pannes et de répartition de charge) soit confiée à la machine virtuelle et non au programme, ce qui le simplifie grandement.

Tous ces aspects font que la machine virtuelle est en mesure de répartir la charge et les acteurs en fonction de leur communication, ce qui assure un passage à l'échelle quasi-linéaire, pour peu que le système ne comporte pas de problème de gouleaux d'étranglement.

Two-phase commit

Pour les besoins de notre étude, nous allons implanter un Two-phase commit dans le cadre d'une gestion de comptes bancaires, de la manière suivante :

Méthodologie de suppression des gouleaux d'étranglement

Pour supprimer les gouleaux d'étranglement, il faut chercher les acteurs qui ont beaucoup de données ou qui reçoivent beaucoup de messages et en extraire une responsabilité dans un nouvel acteur.

Itérations

En partant de ce concept, trois versions, détaillées ci-dessous, ont été dérivées.

Toutes les sources se trouvent à cette adresse : https://github.com/blackheaven/bottleneck-buster.

Itération 1 : Approche naïve

Comme son nom l'indique, il s'agit juste d'une implantation brute de l'algorithme.

Les sources de la version se trouvent dans le répertoire dirty.

Itération 2 : Extraction de la transaction

Le but ici est de décharger le Coordinateur, qui était devenu le gouleau d'étranglement du système, du fait qu'il conduisait toutes les transactions, en créant un acteur chargé de gérer une Transaction.

Le rôle du Coordinateur se limite à trouver la Banque du Client destinataire.

Qui plus est, la Transaction est en mesure de se relancer par elle-même, en cas d'échec.

Les sources de la version se trouvent dans le répertoire clean.

Itération 3 : Extraction du Compte client

Le but ici est de décharger les Banques, devenues, à leur tour, le gouleau d'étranglement du système, car elles géraient tous les mouvements bancaires, par la création d'un acteur chargé de la gestion d'un Compte client.

Le rôle de la Banque se borne à trouver les Comptes clients.

Les sources de la version se trouvent dans le répertoire extreme.

Bilan

Comparaison des performances

Pour comparer les performances, nous allons utiliser le module sim qui se chargera de créer un Coordinateur et un nombre paramétrable de Banques et de Clients par Banque.

Puis, pour générer un nombre suffisant de Transactions, le module va ordonner, pour chaque Client, un transfert vers tous les autres Clients.

Pour ce faire, dans le Shell d'Erlang (lancé via $ erl) nous entrerons les commandes suivantes :


Erlang R16B03 (erts-5.10.4) [source] [smp:2:2] [async-threads:10] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
1> c(coordinator). c(bank). c(client). c(sim).
{ok,coordinator}
2> c(bank). c(client). c(sim).
{ok,bank}
3> c(client). c(sim).
{ok,client}
4> c(sim).
{ok,sim}
5> sim:run(1,1). % Préchauffe
ok
6> timer:tc(sim, run, [5, 10]). % Crée 5 banques avec 10 clients par banque
{470612,ok} % Temps en µs
        

Pour chaque mesure, 3 lancements successifs ont été réalisés et la moyenne obtenue donne le résultat reporté.

Tous les tests ont été réalisés à l'aide d'un ordinateur doté d'un i7 870 @ 2.93 GHz avec l'HyperThreading activé, ce qui représente 8 cœurs logique, ainsi que 2 lots de G.Skill Kit Extreme3 2 x 4 Go PC10600 Ripjaws CAS 9, soit 4 barettes de mémoire vive s'élevant 16 Gio.

L'ordinateur tourne sous FreeBSD 9.2-AMD64 GENERIC et Erlang est en version 16.b.03.

Comparaison par itération et nombre de clients/banques

Il s'agit de mettre en évidence la capacité du système à gérer d'avantage de demandes avec un nombre fixe de ressources, pour observer si sa montée en charge est linéaire ou non.

Chaque ligne représente le nombre de Banques et chaque colonne le nombre de Clients par Banque.

Chaque cellule indique le résultat en secondes.

Itération 1 : Approche naïve
# 2 4 6 8 10
10.0000.0000.0010.0010.002
20.0000.0010.0050.0160.041
30.0010.0050.0270.0880.246
40.0010.0170.0910.3200.990
50.0030.0540.2730.9652.892
Itération 2 : Extraction de la transaction
# 2 4 6 8 10
10.0000.0000.0000.0010.002
20.0000.0010.0010.0040.006
30.0000.0010.0030.0080.017
40.0010.0020.0070.0150.032
50.0010.0040.0120.0260.047
Itération 3 : Extraction du Compte client
# 2 4 6 8 10
10.0000.0000.0010.0010.001
20.0000.0010.0010.0030.005
30.0000.0010.0030.0070.011
40.0010.0020.0050.0140.022
50.0010.0040.0100.0270.054

On constate que le fait d'avoir extrait la Transaction a augmenté drastiquement les performances, puisqu'on observe des performances jusqu'à 60 fois supérieures.

En revanche, l'écart n'est pas significatif pour l'extraction du Compte client, mais nous pouvons pondérer ceci par le fait que les échelles de temps étant tellement faibles, le moindre événement système peut fausser les résultats. Il faudrait comparer avec un jeu d'essais plus importants.

Comparaison par nombre de cœurs alloués

Il s'agit de mettre en évidence la capacité du système à gérer une demande fixe (8 Banques avec 10 Clients chacune) avec un nombre croissant de ressources, pour observer sa capacité à passer à l'échelle.

Chaque ligne représente une itération et chaque colonne le nombre de cœurs CPU alloués.

Chaque cellule indique le résultat en secondes.

# 1 2 3 4 5 6 7 8
10.0010.0160.0860.3090.9432.2394.1318.265
20.0010.0030.0070.0140.0210.0390.0660.114
30.0010.0030.0060.0140.0230.0390.0730.112

Les remarques sont les mêmes qu'à la section précédente : on constate que la perte en performances (par rapport à un gain linéaire en performances), due à l'ajout de ressources, est jusqu'à 70 fois moindre pour les itérations 2 et 3.

De la même manière, on ne constate pas de différences flagrantes entre les deux dernières itérations.

Comparaison de la taille des codes

Ici, nous tentons de déterminer si l'accroissement de la vitesse du système et de sa capacité à passer à l'échelle ne s'est pas fait au détriment d'un ajout de complexité dans le-dit système.

Nous allons donc comparer le nombre de lignes de code non-vides nécessaires à chaque acteur.

Chaque ligne représente une itération et chaque colonne un acteur.

Chaque cellule indique le nombre de lignes de code.

# Coordinateur Banque Client Transaction Compte client Total
1447623--143
214761527-132
31439152928125

Le système a vu son nombre de lignes diminué de plus de 10%, ce qui implique une simplification de celui-ci. De plus, le nombre moyen de lignes par acteur est, lui, passé de 48 à 25, ce qui indique une diminution de près de la moitié, rendant ainsi les acteurs plus simples, plus compréhensibles et donc plus maintenables.

Conclusion

Nous avons vu qu'Erlang nous permettait de construire, simplement et rapidement, des systèmes concurrents et distribués : cela s'avère utile pour tester des protocoles et des architectures.

Nous avons vu que la diminution du nombre de données par acteur et leur simplification augmentait la capacité du système à passer à l'échelle.

Notes

Toutes les sources se trouvent à cette adresse : https://github.com/blackheaven/bottleneck-buster/.

Cette étude n'a pas pris en compte toute une partie de la détection des gouleaux d'étranglement basée sur le profiling pour deux raisons :

* Lorsque l'auteur a décidé de proposer ce sujet, il envisageait de parler de l'intervention faite par Concurix lors du Erlang Factory SF Bay Area 2013, durant laquelle un outil de visualisation graphique des gouleaux d'étranglement a été présenté. Malheureusement, le-dit outil comporte deux parties : une partie à installer en local (nommée concurix_runtime) et une autre partie hébergée sur les serveurs de Concurix, désactivée suite à un changement d'API, mais qui sera, après un échange de courriels avec Concurix, remise en place dans les prochains mois.