Le javascript en multithread? Oui, avec les Web Workers!

 

web_workers3

Historiquement, le javascript est un langage s'exécutant dans un environnement mono-thread (c'est-à-dire que tous les événements s'effectuent les uns après les autres). Lorsque une fonction longue était réalisée, l'interface graphique ne répondait plus, tout était figé. Cela est particulièrement gênant dans le cas où des animations ont lieu, ou si une vidéo est en cours de lecture.

Je vais vous présenter dans ce billet la solution qui a été trouvée pour répondre à ce problème : l'introduction des normes HTML5 et plus particulièrement les "Web Workers".

Optimisation Magento – Mise à plat des tables Customer

magento_logo

Un des atouts de la plateforme E-commerce Magento est son architecture de base de données dite "EAV". Cette architecture offre une grande flexibilité lorsqu'il s'agit par exemple d'ajouter un attribut sur l'entité Client ou Produit. L'avantage vient du fait que chaque ajout d'attribut ne va modifier en rien la structure des tables car la liste des attributs ainsi que leurs valeurs sont stockées dans des tables bien distinctes.

Audit PHP : sécurité et bonnes pratiques

PHP_LOGO

Créé en 1994, PHP (Hypertext Preprocessor) est un langage de programmation très répandu sur Internet.

C'est un langage puissant, facile à aborder et permissif.
De ce fait, il est fréquent de retrouver des codes PHP qui ne respectent pas les normes élémentaires de sécurité et de bonnes pratiques.

Cet article est un aide-mémoire qui dresse une liste (non exhaustive) des points à contrôler dans le cadre d'une revue de code.

Sortie de ZeroTurnaround XRebel 1.0

Pour faire du profiling d'application Java, il existe plusieurs solutions : Il est possible d'utiliser simplement les outils fournis avec la JVM (jmap, jhat, jstat, ... pour la JVM de Sun par exemple) qui bien qu'en ligne de commande offrent beaucoup d'informations pertinentes sur ce qui se passe dans la JVM. On peut aussi se tourner vers des outils de profiling "Desktop" type VisualVM, NetBeans ou YourKit Java Profiler.

Récupération du « Load average » avec JMeter

Introduction

Je ne ferais pas ici une présentation complète de JMeter et JMeter Plugins, qui permet entre autres de récupérer les métriques serveurs pendant l’exécution des tests comme la charge CPU, la mémoire utilisée… Mais ce plugin ne permet pas de récupérer une métrique plus intéressante que la charge du CPU du serveur, le « Load average ».

C’est la récupération de cette métrique qui nous intéressera dans cet article.

Le « Load Average »

Le « load average » désigne, sous les systèmes UNIX, une moyenne de la charge système sur une certaine période. C’est donc une mesure de la quantité de travail que fait le système durant la période considérée. Conventionnellement les périodes utilisées sont 1, 5 et 15 minutes. Cette mesure est disponible via la commande « top » ou « uptime », ou encore via le fichier système « /proc/loadavg ».

Performance statistics : Perf4J pour le code java

Le framework Perf4J offre une façon élégante de faire des statistiques de performance de notre code java.

Le site Perf4J donne cette analogie qui résume bien son apport :

''Perf4J is a set of utilities for calculating and displaying performance statistics for Java code.
For developers who are familiar with logging frameworks such as log4j or logback, an analogy helps to describe Perf4J:

Perf4J is to System.currentTimeMillis() as log4j is to System.out.println()''

Ainsi comme le log4J (ou toute autre implémentation de trace log) est devenu un standard (norme), le Perf4J pourrait aussi l'être.

Notez que le terme de performance est employé dans un sens très large. Seul le temps de réponse nous préoccupe.

La démo présentée ci-après est construite avec Perf4J basé sur l'AOP de Spring ce qui permet de ne pas polluer le code existant.

Pour réaliser cette démo de façon non intrusive, l'annotation @Profiled de Perf4J et l'AOP de Spring sont utilisés.

Les versions de Spring 2+ et jdk5+ sont utilisées.

Passons à la mise en pratique de ce framework avec l'AOP Spring.

Soirée JVM Performance – Paris JUG

Lors de la soirée « JVM Performance » organisée le 11/09/2012 par le ParisJUG, William Louth (@williamlouth sur twitter et william (at) inspired.com par email) de la société jinspired nous a sensibilisé à la problématique d'exploitation de la JVM.

Constat

Il part du constat, qu'en général, lorsqu'une alerte est détectée sur une JVM (alerte rouge nagios par exemple), la réaction des exploitants est :

  • Attendre que cela respasse au vert !
  • Faire un kill -9 de la jvm

Les exploitants ont finalement peu d'outil et ne peuvent pas vraiment analyser ce qui se passe à un instant T sur la JVM. La problématique est encore plus grande lorsqu'on essaye de connaître la qualité de service ressentie par les utilisateurs alors qu'on est en mode distribué dans le « cloud ».

Les logiciels que nous développons ne prennent pas en compte l'environnement dans lequel ils tournent (puissance de chaque nœud par exemple). Les traitements ne s'adaptent pas à l'environnement mais essayent de délivrer un résultat le plus rapidement possible.

Par exemple, lorsqu'on doit persister une information dans une base de données, le goulot d'étranglement étant cette base, il est probable qu'à sollicitation importante, certains Threads commencent à se bloquer. Or, un Thread qui se bloque, c'est de la mémoire de la JVM qui reste utilisée et on imagine très bien quelle sera la conséquence pour le Garbage Collector (GC) qui va commencer à passer de plus en plus souvent pour libérer de la mémoire.  Le GC va consommer de plus en plus de ressource processeur et au final, les Threads se bloqueront définitivement (spirale infernale).

L'idée défendue par William peut consister par exemple à ralentir certains traitements lorsqu'on constate que les accès à la base commencent à ralentir. Il s'agit d'implémenter une auto régulation par le logiciel lui même (Software Regulation Control).

La collecte

Pour cela, il est nécessaire de collecter de l'information sur les différents traitements et de les observer dans la durée afin de pouvoir anticiper l'avenir probable si rien est fait.

Le coût de la collecte

Le problème ici est que la collecte d'information et son stockage est très coûteux. William a illustré par un exemple assez simple mais très illustratif.


public class Client {
public static void main(String[] args) {
int NBTEST = 100;

long all = 0;
for (int i=0; i < NBTEST; i++) {
long start = System.nanoTime();
c1();
all += System.nanoTime() - start;
}
System.out.println("Mean: " + all / NBTEST);
}

public static void c1() {
}
}

L'objectif de ce code est de mesurer le temps d'exécution de l'appel à c1().

Sur mon PC portable assez lent, on obtient une moyenne de l'ordre de 1,5 ms.

Mais quel est le coût de cette mesure ?

Pour cela, William ajoute simplement (et seulement), deux appels à nanoTime() dans la méthode c1() qui devient donc :


public static void c1() {
System.nanoTime();
System.nanoTime();
}

La moyenne passe à environ 3 ms sur la même machine. On peut facilement conclure que le temps de la collecte est assez important.

Imaginez maintenant ce qui peut se passer lorsque les traitements s'empilent :


public static void c1() {
System.nanoTime();
c2();
System.nanoTime();
}

public static void c2() {
System.nanoTime();
c3();
System.nanoTime();
}

public static void c3() {
System.nanoTime();
c4();
System.nanoTime();
}

public static void c4() {
System.nanoTime();
System.nanoTime();
}

Notre système de mesure va introduire une gène dans la capacité de nos JVM à réaliser les vrais traitements pour les utilisateurs. Ici en l’occurrence, la méthode c4() ne fait toujours rien et pourtant nous avons consommé du temps ! Imaginez qu'il faille en plus persister les données collectées dans un fichier de log ou une base ou que sais-je encore !

L'idée développée par William est qu'il faut tenter de minimiser cet effort de collecte. Comment ?

Tracer l'utile seulement

La première piste pourrait être de ne tracer que là où on constate des ralentissements. Par exemple, se placer à une couche interface (servlet) que l'on peut imaginer être notre méthode c1() et mesurer tous les temps de chaque niveau c1() de notre application. Puis, lorsqu'on constate un incident, on regarde quel c1() a posé problème et alors, dans ce cas, on ajoute du code sur tous les niveaux c2() sollicités par ce c1(). On relivre l'application en production et on attend à nouveau que cela se dégrade puis ainsi de suite jusqu'à identifier le problème au niveau le plus fin.

Évidemment, cette méthode n'est pas envisageable dans un contexte de production !

Le crédit-temps pour les traces

L'autre piste consiste plutôt à s'attribuer une sorte de « crédit-temps » de perturbation et à dynamiquement adapter les mesures à chaque itération de collecte. Cela signifie que pour la première itération, on va s'attacher à évaluer un c1() en dessous, un c2() et un c3(). Puis, voyant notre crédit-temps épuisé, on va stopper là la collecte pour cet appel. A l'appel suivant, on partira du même c3() puis on ira vers c4() et on prendra un autre c1(). Et ainsi de suite. A bout de quelques itérations, nous devrions avoir les mesures pour l'ensemble de notre système.

L'axe du temps est donc utilisé pour collecter de la donnée et identifier les goulots d'étranglement avec un temps de nuisance maîtrisé !

Une amélioration possible consiste à essayer d'éliminer le bruit inutile de notre collecte. En effet, si nous savons par expérience qu'un traitement (niveau c2() par exemple) est de l'ordre de 1 ms en moyenne, et que lors d'une mesure niveau c2(), nous constatons que nous avons moins de 1,2 ms par exemple, il est alors inutile d'aller collecter les données dans les sous-niveaux puisque nous savons déjà que tout se déroule correctement.

A l'inverse, si notre c2() commence à dériver dans le temps, il va être intéressant de collecter l'information sur ses sous-niveaux pour lesquels nous appliquerons la même stratégie de collecte.

Au bout de quelques itérations, la collecte devrait être ciblée vers les points « anormaux » de notre traitement et donc, être efficace à 99 % !

Si nous arrivons à identifier les méthodes coûteuses, il est ensuite plus aisé de demander aux développeurs d'optimiser ces parties de code. Optimiser ne veut pas dire nécessairement d'améliorer la dite méthode mais peut-être de regarder au dessus si l'appel est véritablement nécessaire.

Par exemple, si nous reprenons notre exemple, l'appel à la méthode c4() est sans doute inutile puisque c4() ne fait rien ! On ne peut pas optimiser d'avantage c4() mais on peut se passer de son appel !

C'est le même principe par exemple pour une requête SQL. La requête peut-être optimisée mais jusqu'à un certain stade seulement. Pour améliorer ensuite, il faudra regarder à un niveau plus haut dans la couche des services.

Adapter le logiciel

Une fois la collecte réalisée, nous pouvons tenter de nous en servir pour auto-adapter notre logiciel à son environnement.

Par exemple, William prend le cas d'un utilisateur qui arrive sur un site assez chargé. Il ouvre la première page et patiente une dizaine de secondes. Il ouvre une seconde et une troisième et attend à chaque fois une dizaine de secondes. De fait, il se dit que ce site « craint ».

L'idée serait alors de fabriquer des lignes prioritaires (comme les FAST LANE dans les aéroports) de façon à ce qu'un utilisateur qui a déjà patienté une fois voit son prochain traitement prioritaire par rapport à d'autres n'ayant jamais patienté.

Pour cela, il va être nécessaire d'être capable d'allouer la puissance de traitement dynamiquement en fonction du contexte. Ce qui veut dire ralentir certains traitements au profit d'autres (on peut imaginer des sleeps par exemple).

A la manière de ce qu'on connaît avec la gestion de projet et le fameux « chemin critique », on peut identifier dans certains traitements le chemin critique qui implique que tout retard dans l'un de ses composants entraînera un retard total de la chaîne. Un Thread n'a pas cette notion. Lorsqu'on lui demande un traitement, il va tenter de l'exécuter le plus vite possible sans tenir compte du contexte environnant. Or, il peut ne pas être sur le chemin critique et peut s'exécuter plus tardivement.

Conclusion

William a donc exposé pendant près de deux heures la problématique de la collecte d'information sur l'état de la JVM et proposé des pistes pour adapter nos logiciels à cet état plutôt que d'observer impuissants la dégradation de nos JVM (se soldant en général par un arrêt-relance un peu brutal).

Les outils d'instrumentation que sa société propose permettent de répondre à ces besoins : Voir le site www.jinspired.com. Attention, le coût n'est pas anodin (à partir de 5k€ par CPU et licence globale pour un site à 115k€) sur lesquels il faut ensuite ajouter un coût de maintenance.

Toutefois, le rapport coût / efficacité peut s'avérer intéressant sur du long terme et peut-être grâce au tuning live vous faire économiser l'acquisition de quelques machines supplémentaires pour tenir la charge ! A étudier donc.

Tester unitairement la performance en Java

L'importance accordée aux tests lors du développement joue un rôle primordial dans la réussite et la qualité d'une application. Si dans bon nombre d'applications il existe des tests unitaires, les tests de performance sont souvent absents ou interviennent tardivement (bien souvent lorsque des problèmes commencent à être remontés).
Pour diagnostiquer au plus tôt ces problèmes, il existe des outils permettant de tester unitairement la performance. Parmi ceux disponibles, j'ai retenu ContiPerf.

Petit tour d'horizon des fonctionnalités offertes par l'outil.