Memcslap - benchmarking de Memcached et architecture optimale

Niveau : intermédiaire

J'ai présenté dans un précédent article le service Memcached, qui est un outil extraordinaire pour accélérer les accès aux sites web (et aux applications tout court, puisque des librairies sont disponibles pour les langages "non web" les plus courants).
Ensuite, j'ai expliqué comment intégrer l'utilisation de Memcached à Joomla, un CMS moderne, et plus sécurisé que Wordpress.

Pour résumer Memcached, pour ceux qui ne le connaitraient pas, c'est un service qui stocke les données (clé-valeur) en mémoire vive, ce qui en fait un moyen extrêmement performant pour le cache applicatif et les valeurs temporaires (le stockage permanent doit toujours passer par un SGBD, en parallèle).

Maintenant que nous avons les bases pour l'installer et pour l'interfacer avec un framework, nous allons nous intéresser au benchmarking du service, via l'outil "Memcslap", ce qui nous donnera les connaissances pour bâtir une architecture cohérente.

Architecture testée

Matériel

Dans mon lab, j'ai une petite machine (un Shuttle SH67H) sur ESXi (Nested ESXi pour être précis) pour effectuer mes tests :
- processeur Intel Core I7-3770 (quad HT @ 3,40 GHz)
- 32 GO de RAM, soit 4 x 8GO PC3-12800 (@1600MHz)
- les VMs sont stockées sur une grappe RAID1 SSD, en NFS (sur le réseau). Mais dans le cadre du benchmarking de Memcached, les disques de stockage ne seront pas sollicités
- 2 cartes réseau Intel Gigabit CT. Pas les plus haut de gamme, mais dans la HCL (Hardware Compatibility List) de VMWare, donc officiellement supportées
- un switch Cisco SG300-10, qui me permet de faire de l'agrégation de liens sur la couche 3, IP
- les VMs que nous allons utiliser ont toutes 8 vCPU et 1GO de RAM

Logiciels

Je fais du Nested ESXi, j'ai des machines virtuelles dans plusieurs ESXi virtualisés dans un ESXi.
Cette architecture me sert pour  tester des cas de figure sur vCenter, mais elle ne peut être comparée à une infrastructure de production, en terme de performances.
Les VMs sont principalement sous Ubuntu 12.04 et 14.04.
Le service Memcached bénéficie d'une VM dédiée.

Configuration

Serveur

Le service Memcached utilise 128MO de mémoire, et écoute sur le port 11211, en TCP et UDP.

# cat /etc/memcached.conf
-d
logfile /var/log/memcached.log
-m 128
-p 11211
-u memcache
# Pour utiliser Memcached en réseau, commenter la ligne suivante
#-l 127.0.0.1

Le serveur doit être hors production afin d'en tirer des résultats justes, bien entendu, et parce qu'on ne fait jamais de benchmarks en production ! ;-)

Clients

Nous allons installer l'utilitaire memcslap (Memcached "Slap", pour pouvoir donner des claques, en anglais) sur toutes les machines qui serviront aux benchmarks :

# apt-get install libmemcached-tools

Benchmark

Toutes les machines virtuelles sur lesquelles nous allons lancer les tests ont 8 vCPU, tout comme le serveur Memcached cible.

Nous choisissons dont 8 threads (fils d'exécution) pour maximiser les résultats.

-T 8

Nous choisissons d'utiliser la valeur "concurrency" 64
Chaque thread va générer 8 requêtes (j'explique cette valeur plus tard), et comme nous avons 8 threads, nous aurons 64 connexions concurrentes.

-c 64

Par défaut, chaque clé a une taille (key size) de 64 octets, et chaque valeur a une taille de 1024 octets (value size).
Puisque ce qui nous intéresse, c'est que les valeurs recherchées soient toutes dans Memcached, aucune valeur ne doit en être absente.
Autrement dit, en langage SGBD, le taux de "cache miss" doit être de 0%, et le taux de "cache hit" de 100%.
L'espace mémoire utilisé par Memcslap doit donc être plus petit que la mémoire allouée au service Memcached, ici 128 MO.

La formule pour un cache miss de 0% nous est donnée dans le manuel de l'utilitaire :
cache_size / concurrency / (key_size + value_size) * 0.5

Ce qui nous donne :
128 * 1024² / 64 / (64 + 1024) * 0.5 = 963,765

J'ai choisi la valeur concurrency et donc le nombre de requêtes par thread en connaissance de cause, sachant que malheureusement, la plus petite valeur pour le paramètre window size est de 1000 (1k), et incrémenté que par 1000 ...
Nous décidons donc de faire les tests sur le plus petit espace mémoire admissible par l'utilitaire, tout en maximisant les paramètres qui nous intéressent, à savoir 8 threads, qui génèrent 8 connexions chacun, et une taille de clés et de valeurs raisonnables.

--win_size=1k

Nous nous connectons en UDP, qui est plus léger pour faire de nombreuses connexions en parallèle :

-U

Et nous décidons de rafraîchir le tableau statistique toutes les secondes :

-S 1s

En localhost

Tentons un essai sur le serveur lui-même, avec les valeurs citées :

# memcslap --servers 127.0.0.1 -S 1s -T 8 -c 64 -U --win_size=1k

Total Statistics
Type     TPS(ops/s)   Net(M/s)   Min(us)  Max(us)    Avg(us)    Std_dev
Period   319704       308.1      15       14697      189        384.90
Global   296909       331.7      9        91870      205        665.27

Nous voyons qu'en utilisant l'interface de loopback, qui dispose d'une bande passante infinie, nous arrivons à 300k TPS (transactions par seconde), ce qui n'est pas mal du tout, pour une machine de test !
A titre de comparaison, une carte PCI Fusion-io IoDrive2 monte à 290k IOPS, soit peut-être dans les 200K TPS dans des conditions similaires, mais qui coûte ... 24000€ HT !
De quoi ruiner votre entreprise, alors que vous pourriez avoir des performances équivalentes gratuitement via Memcached ...

Essayons maintenant avec TCP/IP, en enlevant le paramètre -U :

# memcslap --servers 127.0.0.1 -S 1s -T 8 -c 64 --win_size=1k
Total Statistics
Type     TPS(ops/s)   Net(M/s)   Min(us)  Max(us)    Avg(us)    Std_dev
Period   307117       270.3      9        8052       201        249.18
Global   260547       318.7      8        162387     238        1124.53

Le résultat est légèrement moins bon, mais toujours correct. Le cumul des connexions TCP alourdit l'ensemble des échanges, y compris sur l'interface loopback.

En réseau

Maintenant que nous avons vu les résultats sur le serveur lui-même, tentons les mêmes benchmarks depuis une machine du réseau, tout d'abord en UDP :

# memcslap --servers 192.168.0.81 -S 1s -T 8 -c 64 -U --win_size=1k

Total Statistics
Type     TPS(ops/s)   Net(M/s)   Min(us)  Max(us)    Avg(us)    Std_dev
Period   107165       101.2      119      10996      595        337.29
Global   97559        111.2      82       38818      653        489.65

Et en TCP :

# memcslap --servers 192.168.0.81 -S 1s -T 8 -c 64 --win_size=1k

Total Statistics
Type     TPS(ops/s)   Net(M/s)   Min(us)  Max(us)    Avg(us)    Std_dev
Period   58535        59.8       436      6716       1092       374.79
Global   57642        60.7       277      51302      1108       576.51

Nous constatons qu'en UDP, les machines atteignent rapidement le gigabit (100+ MO/s), alors qu'en TCP, c'est l'établissement des sessions qui ralentit fortement les transactions.
Le temps de réponse moyen est plus que doublé.

En incluant l'option "multi-get" dans TCP, via le paramètre -d, c'est à dire en regroupant plusieurs demandes de valeurs dans la même requête, nous voyons les performances s'écrouler, au lieu d'augmenter.
En effet, dans ces tests TCP, nous ne sommes pas limités par le réseau, comme c'est le cas pour UDP, mais par la capacité de traitement (cpu, bande passante mémoire, etc).
En augmentant la complexité des requêtes, nous économisons certes de la bande passante, qui ne nous bloque d'ailleurs pas, tout en alourdissant les calculs.
De ce fait, les performances se détériorent à fur à mesure que l'on augmente le nombre de get dans une requête, alors que le "batching" est une technique souvent employée pour améliorer les capacités de traitement des services ...

Conclusions

Memcached est un outil très performant (à vrai dire, même en TCP, les TPS équivalent ou surpassent les performances d'un disque SSD), mais qui nécessite une architecture cible avec de bonnes performances réseau.

Le serveur Memcached doit être équipé au pire avec des cartes gigabit en agrégation de liens (IP), au mieux en cartes 10 Gbps (toujours en agrégation), si l'on veut tirer parti de ses performances.
Les requêtes doivent être faites en UDP, ce qui permet d'économiser le cpu et la bande passante mémoire sur l'élaboration des sessions TCP, afin de maximiser les TPS globaux du service.
Si les clients sont raccordés en gigabit, il faut compter environ 100K TPS par client (ce qui est déjà une valeur très élevée !), le réseau ne vous permettra pas d'aller plus loin.
Il est aussi possible de raccorder les clients en 10 Gbps, ce qui permettra de passer ce cap.
Toutefois, il convient aussi de surveiller l'usage du processeur et de la mémoire côté serveur.

Si le réseau sature, la solution est d'augmenter le nombre de clients (scaling out), ou leur bande passante (scaling up), selon les objectifs que l'on se fixe.
Si c'est le serveur Memcached qui sature, il est largement préférable d'augmenter leur nombre (scaling out)

Partager l'article

Submit to FacebookSubmit to Google PlusSubmit to Twitter

Vous n'avez pas le droit de poster des commentaires

A propos de l'auteur

Milosz SZOTMilosz SZOT est ingénieur systèmes & réseaux spécialisé dans Linux et l'hébergement de sites web à fort trafic.

En savoir plus