Shinken : example de Hack rapide du code

Posted in Programmation, Shinken by Nap on May 5, 2010

Un exemple de hack rapide

Dans un précédent post, j’ai déjà parlé des méthodes de développement que j’ai utilisé dans Shinken. Tout ceci a pour but de faciliter le développement par la suite, comme le rajout facile d’une propriété sans avoir à se soucier des héritages, distributions sur les différents éléments de l’architecture, etc etc.

Nous allons en voir un exemple avec un cas simple : le rajout d’un paramètre pour l’historique du flapping dans Shinken. En effet, Nagios et Shinken sont capables de détecter un élément qui fait le “yoyo” entre deux états. Plutôt que de spammer les users avec des notifications UP/DOWN/UP/DOWN, les schedulers sont assez malin (entendez utilisent un bête algorithme) pour s’apercevoir que l’état à changé X% de fois au cours des N derniers tests. La valeur X était déjà paramétrable pour les hôtes et les services, mais celle de N était hardcodée à 20 états.

Le problème c’est que 20 états ce n’est pas assez pour détecter un yoyo sur toute la nuit. Il arrive donc souvent des cas où l’on est averti pour rien, juste car on ne peux pas augmenter cette valeur. C’est dommage :cry:

Et bien plus maintenant :mrgreen: !

Je l’ai rajouté dans Shinken en quelques lignes qui vont vous monter à quel point c’est simple d’aller hacker dans ce code, 5 lignes seulement dans notre cas :-D

La modification

Tout se joue dans deux fichiers sources :

  • config.py : qui gère les paramètres de configurations globaux
  • schedulingitem.py : qui gère les algorithmes d’ordonnancement, des demandes de checks et autres.

Dans le premier (config.py) on remarque un tableau nommé properties vers le début du fichier avec toutes les propriétés possibles pour le fichier principal (nagios.cfg). Qu’à cela ne tienne, on va rajouter notre nouvelle propriété :

‘flap_history’ : {’required’:False, ‘default’:'20′, ‘pythonize’: to_int, ‘class_inherit’ : [(Host, None), (Service, None)]},

On rajoute donc le paramètre flap_history, qui n’est pas obligatoire (required=False), vaut par défaut 20, se transforme en objet int depuis la lecture de la chaîne de caractère dans le fichier, et va être présentée aux classes Host et Service avec ce même nom de flap_history (rôle de None, expliqué un peu plus haut dans le code, si vous mettez un nouveau nom, c’est lui qui sera utilisé pour cette classe).

Et voila, c’est tout pour la configuration ! Et oui, rien de plus. L’Arbiter va lire la configuration, transformer en entier la valeur, mettre 20 si elle est absente et va la fournir aux configurations envoyées aux schedulers automatiquement ! Là, ne pas hacker un tel code, c’est plus que de la fainéantise :lol:

On a réglé le problème de la configuration, maintenant on va utiliser notre nouvelle propriété. Ceci se passe dans deux fonctions du fichier schedulingitem.py : add_flapping_change et update_flapping. La première rajoute le changement d’état sur la pile de 20 éléments et s’assure que la pile fait toujours au plus 20 éléments. La seconde fait le calcul de pourcentage de changement à proprement parlé.

Les schedulingitems sont les hosts/services (une classe commune pour factoriser le code), et l’objet est nommé self en Python. On a dit que la propriété a été envoyée sur les classes Host et Service car c’est un paramètre global à tous les hôtes et services, pas la peine de le multiplier par le nombre d’hôtes et services. En Python, pour accéder à sa classe (qui est un objet comme un autre), il suffit de faire :

self.__class__

Donc là pour obtenir notre valeur, il suffit de faire :

flap_history = self.__class__.flap_history

On remplace les trois occurrences de 20 qui trainent dans le code par flap_history et c’est réglé, on peut tester/commiter 8-) :

if len(self.flapping_changes) > flap_history:

r += i*(1.2-0.8)/flap_history + 0.8

r = r / flap_history

Au final

Nous avons vu la définition et l’utilisation d’un paramètre global dans Shinken. C’est très simple, et il ne faut pas se priver :-) Nous verrons une autre fois le rajout d’un paramétrage dans un service ou un hôte, c’est encore plus simple qu’ici car il n’y a pas besoin de passer par l’accrochage dans les classes Host et Service. Mais ce sera pour une prochaine fois, j’ai un git push à faire.

Si vous souhaitez hacker vous aussi, n’hésitez pas :

git clone git://shinken.git.sourceforge.net/gitroot/shinken/shinken

J’accepte volontiers les patchs :)

Juste pour info, dans le code de Nagios, si on voulait faire la même chose, il faudrait changer la macro MAX_STATE_HISTORY_ENTRIES en simple variable :

grep -r MAX_STATE_HISTORY_ENTRIES * | wc -l
30

30 lignes au minimum juste pour la partie traitement! Je n’ose même pas regarder combien de lignes ceci va coûter pour la configuration, mais bien plus d’une, ça c’est sûr… :-?

Notez bien que je ne dis pas ça pour me moquer des développeurs de Nagios, comme quoi le code est imbitable non, car je n’aurais sûrement pas fait mieux en C.  C’est juste pour illustrer que parfois, utiliser des techniques de développements avancées (le développement dynamique qui a un peu irrité lors de l’annonce de Shinken sur la mailing list), ça ne fait pas de mal à l’efficacité…

Déduplication : bloc fixe VS bloc variable

Posted in Programmation by Nap on March 14, 2010 7 Comments

Intérêt de la dé-duplication

J’ai testé il y a quelques temps le filesystem lessfs (site officiel du projet). C’est un filesystem très simple à mettre en place, de type Fuse (donc en user space) qui permet de monter un espace de dé-duplication à la volée.  Cette fonctionnalité permet de gagner une place considérable lorsque l’on a des données qui se ressemble fortement.

Elle est complémentaire de la compression. Là où vous aller gagner sur un fichier avec la compression, si vous en avez deux, vous aller stocker deux fois la taille compressée. Avec une passe de dé-duplication avant, vous n’aurez qu’une fois chaque bloc, puis vous pouvez compresser ce qui reste.

Deux méthodes : bloc de taille fixe ou variable

Taille fixe

Les blocs justement. Dans lessfs, ce sont des blocs de taille fixe. Donc on applique un algorithme très simple :

  • on coupe la donnée en bloc de NKo (prenons 4Ko)
  • on fait un hash de chaque bloc
  • si on a déjà un hash, on change le bloc par un simple pointeur vers le bloc déjà sauvegardé
  • sinon on sauvegarde le bloc et son hash

Simple. Efficace? Pas si sûr. Bien entendu, si vous faites une copie d’un fichier, celle-ci ne va quasiment rien vous coûter. Mais faire des copies intactes de vos fichiers arrive parfois avec des sauvegardes, et encore…

Taille variable

Si l’on veut être plus efficace, il faut faire une recherche dans les données d’un bloc déjà vu. Mais là où avant on cherchait avec un début de bloc tous les 4Ko, là on cherche pour tous les octets. En effet, si vos blocs ne sont pas parfaitement alignés, vous ne reconnaîtrez pas votre bloc, car il a pris un simple offset de quelques octets!

Bien sûr, ce genre de recherche est bien plus couteux en terme de CPU, 4K calculs fois plus. (En fait u peu moins, dès que vous raccrochez un wagon de blocs déjà connu, un seul calcul suffit).

Exemple de gain

Un exemple?

J’ai codé rapidement un petit script en Python qui réalise ces deux types de dé-duplications :

  • recherche des mêmes blocs de 4Ko avec recherche par fenêtre glissante
  • recherche brut de frondrie, bloc de 4k

Voici les résultats sur un répertoire plein de fichiers de type office and co:

****** Stats Varible: Deplicated 342756761/465877423 = 73.00% Dedup+compress 426510002 =91.00%
****** Stats Fix: Deplicated 59596755/465877423 = 12.00% Dedup+compress 68349038 =14.00%

On a donc 73% de gain avec des tailles de blocs variables, 91% si on les compresse par dessus. La méthode fixe bourrine n’arrive elle qu’à un faible 12%.

Bon bah il faut demander à lessfs d’appliquer cet algo? Pas si simple, de un c’est ultra consommateur en CPU, donc il faut le faire en post-process, pas à la volée. Et surtout l’algo utilisé semble avoir été breveté par EMC… Et après ça qu’on vienne encore me sortir que les brevets sont fait pour protéger l’innovation…. l’investissement oui, l’innovation non…

Pour ceux qui ont la chance de ne pas habiter dans ce merveilleux pays des brevets logiciels, vous pouvez tester le script.

Partie à trois : Python, __slots__ et metaclass

Posted in Programmation by Nap on February 15, 2010 3 Comments

Les langages dynamiques sont pratiques pour se frotter facilement à de nouveaux paradigmes de programmations. Aucune technique n’étant parfaite, l’aspect dynamique se paye.

Le prix à payer pour les langages dynamiques

Bien souvent on pense au coût CPU pour ces langages, mais cette ressource n’est pas la seule à prendre cher. Là où un accès à une structure est en 0(1) en C ou C++, il peut être plus élevé dans des langages où les propriétés des objets ne sont pas identiques entre les instances. Il en est de même pour la RAM : les objets pouvant avoir de nouvelles propriétés à chaud, leur accès se fait en vérifiant le dictionnaire __dict__ des objets. Le dictionnaire est fort simple:

class Test:
   def __init__(self, x, y):
      self.x = x
      self.y = y
point = Test(1, 2)
print ‘Initialement’, point.__dict__
point.z = 3
print ‘Apres’, point.__dict__

Donne :

Initialement {’y': 2, ‘x’: 1}
Apres {’y': 2, ‘x’: 1, ‘z’: 3}

C’est sympa, c’est dynamique. Mais ceci a un coût en Mémoire : ici nous avons un seul objet, mais si nous avons 1000 points, chacun aura son propre __dict__ indépendant, et surtout les chaînes ‘x’, ‘y’ et ‘z’ seront dupliquées dans chaque instance. Imaginons que nous avons 1000000 de points à conserver, la consommation de RAM va être de 176mo sur notre exemple (Python 2.6.4). Si nous prenons des classes avec des noms de propriétés plus grandes que ‘x’, on peut atteindre des sommets en terme de consommation de RAM pour finalement pas grand chose.

Le module guppy (disponible sur pypi de mémoire) peut être très pratique pour observer qui consomme de la RAM dans notre application. Son utilisation est fort simple :

from guppy import hpy
hp=hpy()
print hp.heap()

Sa sortie est (relativement) éloquente :

Partition of a set of 2024657 objects. Total size = 173885852 bytes.
Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
0 999999  49 135999864  78 135999864  78 dict of __main__.Test
1 999999  49 31999968  18 167999832  97 __main__.Test
2    127   0  4073248   2 172073080  99 list
3  10686   1   744928   0 172818008  99 str
4   5540   0   203368   0 173021376 100 tuple
5    347   0   115160   0 173136536 100 dict (no owner)
6   1539   0   104652   0 173241188 100 types.CodeType
7     64   0   100480   0 173341668 100 dict of module
8    175   0    94840   0 173436508 100 dict of type
9    194   0    86040   0 173522548 100 type

78% de la consommation mémoire est due aux __dict__ de nos points, les valeurs de ces instances consommant quant à elles 18%.

__slots__ : c’est les soldes pour Python

Lorsque l’on sait à l’avance quelles vont être les possibilités des noms de propriétés de nos instances, il peut être pratique de recourir à l’utilisation des __slots__. C’est un tuple dans la classe où les noms des propriétés vont être mises en commun pour toutes les instances de la classe. Attention, son utilisation est fort simple, mais elle limite certaines possibilités de Python par la suite, comme certains problèmes avec tout ce qui touche la sérialisation d’objet par exemple.

Si vous souhaitez l’utiliser, c’est fort simple, il suffit de rajouter le tuple à la classe si elle hérite d’object :

class Test(object):
   __slots__ = (’x', ‘y’, ‘z’)
   def __init__(self, x, y):
      self.x = x
      self.y = y

Si simple? Non en fait. le __slots__ va remplacer __dict__ qui va tout simplement disparaitre! Notre code va lamentablement échouer avec:

Initialement
Traceback (most recent call last):
File “test_slot.py”, line 10, in <module>
print ‘Initialement’, point.__dict__
AttributeError: ‘Test’ object has no attribute ‘__dict__’

Pour contourner cela, il suffit de rajouter __dict__ au slots:

class Test(object):
   __slots__ = (’__dict__’, ‘x’, ‘y’, ‘z’)
   def __init__(self, x, y):
      self.x = x
      self.y = y

On relance, la consommation passe à 47Mo. (Les gains sont encore plus importants avec des chaînes de plus d’un caractère :) ). Pour Shinken par exemple, avec 100000 services, j’étais à plus de 2Go de RAM consommée, avec les slots, je suis tombé à moins 50Mo environs…

Metaclass : une classe pour en modifier d’autres

En Python, on a déjà vu que les classes sont des objets comme les autres. Qui dit objet dit instanciation. Lors de cette instanciation, il peut être pratique de changer des choses à la volée. C’est justement le rôle des metaclass. C’est une classe qui va contrôler la création d’une autre. Elles peuvent être utilisées pour par exemple tracer automatiquement tous les appels de méthode d’une classe. Pour un tel exemple, voir sur http://www.afpy.org/Members/kerflyn/metaclass qui présente très bien cela.

On mixe le tout

Vous allez me dire: bon c’est bien les metaclass, mais c’est quoi le rapport avec les __slots__? Et bien c’est pratique lorsque l’on a beaucoup de propriétés dans une classe, comme par exemple Service ou Host de Shinken. Jusqu’à maintenant, lorsque je rajoutait une nouvelle propriété à ces classes, je rajoutais une ligne dans le tableau properties ou running_properties, mais je devais penser à rajouter ce même paramètre dans le tuple  __slots__ de la classe. Autant dire qu’une fois sur deux, j’oubliais. De plus, ça fait un gros pâté en début de classe, et je n’aime pas ça.

Je suis tombé sur http://code.activestate.com/recipes/435880/ qui présente comment générer automatiquement le tuple __slots__ pour ses classes en regardant tout simplement les variables fournies à __init__ (il semble créer d’ailleurs une liste qui doit être changée en tuple par l’interpréteur). Bon pour les Host et Service, il n’y a qu’un seul paramètre, un tableau de construction. Mais ça m’a donné l’idée d’adapter ce code pour qu’il utilise les tableaux properties et running_properties de mes classes qui contiennent toutes les propriétés de mes objets.

Edit : Merci à Bertrand Mathieu pour la simplification du code par set.

Ceci donne au final la classe AutoSlots suivante :

class AutoSlots(type):
   def __new__(cls, name, bases, dct):
      slots = dct.get(’__slots__’, set())
      #Now get properties from properties and running_properties
      if ‘properties’ in dct:
         slots.update((p for p in dct['properties']))
      if ‘running_properties’ in dct:
         lots.update((p for p in dct['running_properties']))
      dct['__slots__'] = tuple(slots)
      return type.__new__(cls, name, bases, dct)

Qui est appelée avec :

class Service(SchedulingItem):
   #AutoSlots create the __slots__ with properties and
   #running_properties names
   __metaclass__ = AutoSlots

[..]

Maintenant les __slots__ sont construits à la volée, et il n’y a plus de risque d’oublier des paramètres et mes classes Host/Service se re-concentrent un peu sur ce qu’elles doivent faire, et non sur une astuce pour contourner une consommation excessive de RAM par Python.