Tracé de courbes pour les benchmarks avec Matplotlib
Vincent Bernat
La semaine passée, j’ai mené de nombreuses mesures avec un Spirent Avalanche qui est un boîtier permettant de tester les performances d’un équipement réseau (tel qu’un répartiteur de charge ou un serveur web). Le module dédié aux rapports n’est pas très flexible et ne produit pas des courbes de très bonne qualité. Heureusement, tous les résultats sont exportés au format CSV.
Matplotlib est une bibliothèque Python permettant de produire des courbes et des graphiques de manière simple dans la plupart des cas. C’est un bon remplacement à gnuplot et il n’y a pas besoin de connaître énormément de choses sur Python pour l’utiliser. La documentation comprend un guide de l’utilisateur qui permet de se mettre en selle rapidement.
Mise à jour (04.2017)
Cet article est particulièrement vieux et couvre une version antique de Matplotlib.
Introduction rapide#
Matplotlib peut s’utiliser avec IPython :
$ ipython -pylab Python 2.6.7 (r267:88850, Jul 10 2011, 08:11:54) Type "copyright", "credits" or "license" for more information. Welcome to pylab, a matplotlib-based Python environment. For more information, type 'help(pylab)'. > In [1]: plot([1,1,4,5,10,11])
Une fois que tout est prêt, il est très simple de construire un script équivalent :
#!/usr/bin/env python from matplotlib.pylab import * plot([1,1,4,5,10,11]) savefig("my-plot.pdf")
Extraire les résultats du CSV#
Les données sont contenues dans le fichier realtime.csv
que l’on
renomme et compresse avec gzip
. Il contient les données brutes ainsi
que quelques indications supplémentaires sur le benchmark (nom,
description, paramètres, …). Ces indications doivent être
sautées. csv2rec()
permet de faire simplement cette opération :
from matplotlib.pylab import * import sys, os import gzip skip = 0 for line in gzip.open(sys.argv[1]): if line.startswith("Seconds Elapsed,"): break skip = skip + 1 ava = csv2rec(gzip.open(sys.argv[1]),skiprows=skip)
Désormais, pour accéder aux mesures correspondant au nombre de
secondes écoulées depuis le début du test, il suffit d’utiliser
ava['elapsed_seconds']
.
Structure générale#
Je veux dessiner quatre courbes :
- nombre de transactions par seconde (réussies, échouées, tentées),
- temps de réponse par page (minimum, en moyenne, maximum),
- charge CPU de l’Avalanche,
- bande passante utilisée.
La courbe la plus importante est la première. La seconde est un peu moins importante tandis que les deux dernières sont assez annexes et permettent principalement de vérifier qu’il n’y a pas eu un goulot d’étranglement dans la chaîne lors du bench. Les quatre courbes vont être disposées ainsi :
Matplotlib permet de placer plusieurs graphiques au sein d’une même figure. Celles-ci partageront le même axe des abscisses. Le résultat sera sauvegardé en PDF.
# Create the figure (A4 format) figure(num=None, figsize=(8.27, 11.69), dpi=100) ax1 = subplot2grid((4, 2), (0, 0), rowspan=2, colspan=2) # […] ax2 = subplot2grid((4, 2), (2, 0), colspan=2, sharex=ax1) # […] ax3 = subplot2grid((4, 2), (3, 0), sharex=ax1) # […] ax4 = subplot2grid((4, 2), (3, 1), sharex=ax1) # […] # Save to PDF savefig("%s.pdf" % TITLE)
Courbe de bande passante#
Commençons par le cas le plus simple : l’utilisation de la bande passante.
# Plot 4: Bandwidth ax4 = subplot2grid((4, 2), (3, 1), sharex=ax1) plot(ava['seconds_elapsed'], ava['incoming_traffic_kbps']/1000., 'b-', label='Incoming traffic') plot(ava['seconds_elapsed'], -ava['outgoing_traffic_kbps']/1000., 'r-', label='Outgoing traffic') grid(True, which="both", linestyle="dotted") ylabel("Mbps", fontsize=7) xticks(fontsize=7) yticks(fontsize=7)
Dans le graphique positionné en (3,1)
, nous traçons le trafic
entrant (en bleu, b-
) et sortant (en rouge, r-
) en ordonnée et le
nombre de secondes écoulées en abscisse. Voici le résultat :
La plupart des fonctions de Matplotlib sont disponibles à la fois
comme méthodes d’un objet ou de manière globale. Dans ce dernier cas,
elles s’appliquent au dernier objet créé. Dans notre exemple,
plot()
est appelé après la création de ax4
. On aurait pu écrire
ax4.plot()
.
Courbe de CPU#
Les données relatives à l’utilisation CPU ont besoin d’être normalisées. Une fois cette normalisation effectuée, le tracé de la courbe est très similaire au cas précédent.
# CPU max = np.max(ava['average_cpu_utilization']) order = 10**np.floor(np.log10(max)) max = np.ceil(max/order)*order cpu = (max - ava['average_cpu_utilization'])*100/max # Plot 3: CPU ax3 = subplot2grid((4, 2), (3, 0), sharex=ax1) plot(ava['seconds_elapsed'], cpu, 'r-', label="Avalanche CPU") grid(True, which="both", linestyle="dotted") ylabel("Avalanche CPU%", fontsize=7) xticks(fontsize=7) yticks(fontsize=7)
Temps de réponse#
Nous disposons de trois métriques liées au temps de réponse : minimum, moyenne et maximum. Comme ces métriques peuvent varier rapidement d’ordre de grandeur, une échelle logarithmique est utilisée :
# Plot 2: response time ax2 = subplot2grid((4, 2), (2, 0), colspan=2, sharex=ax1) plot(ava['seconds_elapsed'], ava['minimum_response_time_per_page_msec'], 'b-', label="Minimum response time") plot(ava['seconds_elapsed'], ava['maximum_response_time_per_page_msec'], 'r-', label="Maximum response time") plot(ava['seconds_elapsed'], ava['average_response_time_per_page_msec'], 'g-', linewidth=2, label="Average response time") legend(loc='upper left', fancybox=True, shadow=True, prop=dict(size=8)) grid(True, which="major", linestyle="dotted") yscale("log") ylabel("Response time (msec)", fontsize=9) xticks(fontsize=9) yticks(fontsize=9)
C’est aussi la première courbe avec une légende. Voici le résultat :
Transactions par seconde#
Il s’agit de la courbe la plus importante.
# Plot 1: TPS ax1 = subplot2grid((4, 2), (0, 0), rowspan=2, colspan=2) plot(ava['seconds_elapsed'], ava['desired_load_transactionssec'], '-', color='0.7', label="Desired Load") plot(ava['seconds_elapsed'], ava['successful_transactionssecond'], 'g:', label="Successful") plot(ava['seconds_elapsed'], smooth(ava['successful_transactionssecond']), 'g-', linewidth=2) plot(ava['seconds_elapsed'], ava['attempted_transactionssecond'], 'b-', label="Attempted") plot(ava['seconds_elapsed'], ava['aborted_transactionssecond'], 'k-', label="Aborted") plot(ava['seconds_elapsed'][:-1], ava['unsuccessful_transactionssecond'][:-1], 'r-', label="Unsuccessful") legend(loc='upper left', fancybox=True, shadow=True, prop=dict(size=10)) grid(True, which="both", linestyle="dotted") ylabel("Transactions/s")
Le nombre de transactions réussies est dessiné deux fois : c’est une métrique qui contient pas mal de bruit quand l’équipement commence à saturer et on va donc la lisser avec l’aide de NumPy :
import numpy as np def smooth(x, win=4): s = np.r_[x[win-1:0:-1],x,x[-1:-win:-1]] w = np.ones(win, 'd') y = np.convolve(w/w.sum(),s,mode='valid') return y[(win-1)/2:-(win-1)/2]
Il s’agit d’un lissage linéaire sur une fenêtre fixe réalisé à l’aide d’un produit de convolution. En voici le résultat :
Les données d’origine sont tracées en pointillés et en vert tandis que le lissage est tracé avec une ligne verte plus épaisse. Il y a également des annotations qui sont apparues. Voici comment celles-ci sont réalisées :
# Noticeable points count = 0 def highlight(index, reason): global count if index and index > 0: x,y = (ava['seconds_elapsed'][index], smooth(ava['successful_transactionssecond'])[index]) plot([x], [y], 'ko') annotate('%d TPS\n(%s)' % (y,reason), xy=(x,y), xytext=(20, -(count+4.7)*22), textcoords='axes points', arrowprops=dict(arrowstyle="-", connectionstyle="angle,angleA=0,angleB=80,rad=10"), horizontalalignment='left', verticalalignment='bottom', fontsize=8) count = count + 1 highlight(np.argmax(smooth(ava['successful_transactionssecond'])), "Max TPS") highlight(np.argmax(cpu > 99), "CPU>99%") highlight(np.argmax(ava['average_response_time_per_page_msec'] > 500), ">500ms") highlight(np.argmax(ava['average_response_time_per_page_msec'] > 100), ">100ms")
np.argmax()
retourne le premier indice correspondant au maximum. Un
point important à noter est que
ava['average_response_time_per_page_msec'] > 100
, retourne un
tableau avec 1 si la valeur était supérieure à 100 et 0 sinon. Ainsi,
np.argmax()
retourne le premier indice pour lequel la valeur est
supérieure à 100 ms.
La fonction highlight()
va ajouter un point (plot([x], [y], 'ko')
)
sur la courbe lisée des transactions réussies ainsi qu’une annotation
textuelle.
Pour voir le résultat complet, jetez un œil sur ce benchmark de nginx en tant que terminaison TLS.