NullPointerException

Blog d’un groupe crypto-terroriste individuel auto-radicalisé sur l’Internet digital

Devoxx 2013 - journée 1

Certains l’ont peut-être oublié, mais la Devoxx se tient actuellement sur Paris du 27 au 29 mars.

Bilan de la 1ère journée.

Monitoring Open Source pour Java avec JmxTrans, Graphite et Nagios

Cette présentation par Cyrille Le Clerc et Henri Gomez avait pour but de faire le tour d’une solution de monitoring pour application Java.

Contexte

Le constat a été sans appel après un bref tour de salle : peu de monde met en place une solution de monitoring de ses applications, freiné par une complexité estimée trop importante. Et pour beaucoup de développeurs (Dev), c’est uniquement le boulot des opérationnels (Ops).

En plus de ça, quand un monitoring est quand même mis en place, celui-ci est généralement peu pérenne (dette technique importante, pas de réflexion globale), pas sécurisé (pages web en accès publique). Et pour ne rien arranger, sur les applications plus complexes, certaines contraintes techniques viennent compliquer encore la tache à réaliser, par exemple la présence de load-balancer ou de firewall.

La solution présentée va gérer le monitoring de bout en bout :

  • publication des compteurs via JMX
  • collecte des compteurs via JMXTrans
  • stockage des compteurs via Graphite
  • génération des rapports via Graphite
  • alerting via Seyren

Création des compteurs : JMX

Le 1er problème auquel se heurte une personne qui doit mettre en place du monitoring est assez simple : comment je peux mettre à disposition mes compteurs au reste du système.

La solution qui vient assez généralement, c’est de tout stocker régulièrement dans des fichiers de log au format texte, qui seront ensuite analysés par des outils ad-hoc. Avec quelques valeurs simples à stocker, la solution pourrait être bonne. Avec des valeurs complexes (décimaux) ou beaucoup de données (log de plusieurs Go), elle s’écroule juste complètement. La solution juste après, c’est de faire des pages web dédiées aux statistiques. Mais bonjour la sécurité, c’est assez complexe d’avoir 2 backends d’authentification (le public et un interne), donc généralement, il n’y a juste pas de sécurité, on compte sur l’obfuscation d’URL pour penser que le concurrent n’y aura pas accès.

Pourtant, Java vient en standard avec une solution intégrée de publication de compteurs : JMX. Cette API permet de publier en quelques lignes de code des valeurs qui seront observables par tout outil capable de communiquer avec JMX, par exemple VisualVM, et le framework Spring nous facilite le boulot encore une fois :

@ManagedResource("app:type=ToMonitor,name=ToMonitor")
@Component
public class ToMonitor {
	private final AtomicInteger someValue = new AtomicInteger();

	@ManagedMetric(metricType = MetricType.COUNTER)
	public int getSomeValue() {
		return someValue.get();
	}
}

Hop-là, ici on demande à Spring d’exposer la valeur ToMonitor.SomeValue sur JMX. Toute application qui parle JMX peut maintenant requéter notre application pour lui demander des informations. On remarque au passage que comme JMX est aussi massivement utilisé dans Java, on a accès à des stats aussi bien technique (RAM utilisé, espace disque, charge CPU, temps de garbage collection…) qu’à des données métiers (nombre de visiteurs, taux de transformation, chiffre d’affaire horaire…).

Collecte des compteurs : JMXTrans

JMX c’est bien, mais seules les applications JMX peuvent communiquer avec. Le protocole choisi (RMI-IIOP) est propre à Java… Si vous avez des outils de collecte en Java, pas de soucis pour la collecte donc, si vous avez des outils de collecte autres, passez votre chemin, y’a rien à voir !

C’est là qu’intervient JMXTrans. Cette librairie Java va se charger de la collecte des compteurs pour les publier là où vous le voudrez. Encore une fois, 1 fichier de configuration et c’est parti :

{
	"queries": [{
		"objectName": "app:type=ToMonitor,name=ToMonitor",
		"attributes": [{
			"name": "SomeValue",
			"resultAlias": "app.someValue"
		}]
	}],
	"outputWriters": [{
		"@class": "org.jmxtrans.embedded.output.GraphitePickleWriter",
		"settings": {
			"host": "${graphite.host:localhost}",
			"port": "${graphite.port:2004}",
			"enabled": "${graphite.enabled:true}",
			"namePrefix": "${graphite.namePrefix:servers.#hostname#.}"
		}
	}],
	"queryIntervalInSeconds": "${jmxtrans.queryIntervalInSeconds:31}",
	"numQueryThreads": "${jmxtrans.numQueryThreads:7}",
	"numExportThreads": "${jmxtrans.numExportThreads:3}",
	"exportIntervalInSeconds": "${jmxtrans.exportIntervalInSeconds:9}",
	"exportBatchSize": "${jmxtrans.exportBatchSize:51}"
}

Ici, on demande la collecte du compteur précédent et sa publication dans Graphite (cf plus loin).

JMXTrans existe sous 2 formes. En standalone, c’est une application débarquée qui se charge de la collecte, et peut donc prendre en charge plusieurs applications. En mode embedded, c’est une librairie embarquée dans l’application, qui se chargera de publier périodiquement les données dans l’outil de stockage. Le mode standalone n’est pas très adapté si on ne connait pas bien la topologie des applications à l’avance (exit le cloud donc) mais c’est la seule possible pour des applications legacy qu’on ne peut pas modifier. C’est donc un mode plutôt Ops. À l’inverse, le mode embedded est plus scalable et orienté plutôt Dev.

Stockage des valeurs et génération des rapports : Graphite

Ça faisait déjà un petit moment que je commençais à entendre parler de cette chose qu’est Graphite. Pour l’avoir maintenant vu à l’œuvre, ça envoie du lourd !

Graphite est un système clef-en-main de stockage de données « time series » (en gros des millions de couples date/valeur) et de tracé des graphiques correspondants.

Auparavant, j’utilisais généralement RRDTool pour ça, mais j’étais très vite bloqué par les limitations de ce système. 1ère limitation, RRD impose que la totalité des données à gérer soit connue dès le départ, le fichier de stockage étant créé en conséquence. Si on souhaite ajouter des données a posteriori, il faut créer un nouveau fichier de stockage, tenter de faire rentrer les anciennes données dans les nouvelles (aux forceps généralement) et relancer le tout. Idem, les graphiques générés sont définis en dur, donc si on souhaite voir seulement une portion de données sous un certain point de vue, il faut sortir l’éditeur de code et tout repenser ad-hoc (au passage, j’espère que vous aimez l’arithmétique en notation polonaise inverse…).

Graphite règle tous ces problèmes. Toute donnée qui lui sera envoyée sera stockée, sans besoin de la déclarer au préalable. Une nouvelle application à gérer ? Pas de problème, envoyez simplement les données ! Ensuite, la génération d’un graphique ne nécessite aucune connaissance particulière en programmation. L’interface de construction est accessible à tous, et chacun peut extraire le rapport qu’il souhaite sous le point de vue qu’il souhaite. Les ops auront accès à tous les détails nécessaires sur le dernier crash application tandis que la direction pourra extraire les courbes de tendance du CA ou la comparaison avec la semaine passée. Exemple de rendu : Graphite render

Le seul problème de Graphite est son installation. C’est une application en Python, donc qui s’intègre généralement assez mal avec les distributions GNU/Linux existantes. Il est conseillé de l’installer sur une VM pour ne pas casser son système existant !

Alerting : Seyren

Maintenant qu’on a tous les graphiques qu’on veut, il serait bien qu’on soit alerté si quelque chose se passe mal. Parce qu’on ne va pas passer sa vie à rafraîchir son navigateur pour voir si la tendance est bonne ! C’est là qu’entre en jeu Seyren.

Seyren va aller consulter les données de Graphite et envoyer des notifications en cas de valeurs anormales définies par l’utilisateur. Pour les habitués de Nagios, pas de changement d’habitude, des niveaux configurables d’alerte et d’erreur et des notifications par mail, SMS ou plein d’autres choses en cas d’alerte (ah si, la simplicité risque de vous dépayser :D).

Conclusion

J’avoue que je faisais parti des personnes qui considéraient le monitoring comme pénible, long et difficile à mettre en place. À l’issu de cette conférence, je pense que je vais revoir mon jugement et me mettre sérieusement à pousser du monitoring là où je peux. J’ai vraiment l’impression qu’on peut arriver aujourd’hui à monitorer convenablement les choses à peu de frais.

Collections de printemps

Après la conférence sur le monitoring, place à un marronier qui n’en est pas un : Collections de printemps, présenté par José Paumard, avec l’assistance de Rémi Forax pour la technique. Une plongée au cœur de l’API Collection de Java, son passé et surtout son futur.

L’API Collection aujourd’hui

1ère partie de la conférence, l’état de l’art de l’API Collection sur Java 7. Aujourd’hui, elle est principalement constituée de 3 implémentations : ArrayList, LinkedList et HashMap. José Paumard va nous faire réviser nos classiques, avec une descente dans les entrailles du JDK, et de belles découvertes au passage.

1ère étape, la classe ArrayList. C’est elle qui se rapproche le plus du bon vieux tableau alloué en mémoire contigüe en C/C++. Mais à la différence du C, sa taille n’est pas fixe à l’initialisation : elle va être capable de s’adapter au contenu. C’est d’ailleurs ce qui va lui valoir de mauvaises performances dans certains cas. Théoriquement, l’insertion et l’accès est en O(1), et devrait la rendre très efficace. Mais la gestion de la mémoire passant par là, le résultat peut être tout autre si cette classe est mal utilisée. En effet, lors d’une insertion, il se peut que le tableau alloué soit plein. La JVM va alors allouer un nouveau tableau plus grand, puis tout recopier. Performances désastreuses en vue ! Pour limiter ce problème, on peut allouer directement la bonne taille à l’initialisation si on la connait. Idem à la suppression, la JVM va devoir recopier tous les éléments suivants l’élément supprimé pour combler le trou. Pas top non plus sur des listes de grande taille. Enfin, dernier problème de ArrayList, le tableau ne fait que grossir, jamais il ne diminue. Insérer 1 million + 1 éléments et en supprimer 1 million ensuite laisse un tableau de plusieurs millions de cases en mémoire… Bilan, classe à utiliser avec des données pas trop dynamiques en taille et pour lesquelles on peut estimer une taille à l’avance.

2ème étape, la classe LinkedList. Ici, c’est une simple liste doublement chaînée comme on apprend à en faire en école. L’insertion est en O(1) (c’est juste une double affectation de pointeur), et l’accès en O(n/2). Ce dernier chiffre m’a un peu surpris (je le pensais en O(n), ce qui est quand même algorithmiquement équivalent), mais la JVM est assez intelligente pour savoir par quel bout de la liste elle doit attaquer, la tête ou la queue. Dans le pire cas, elle n’a donc que la moitié de la liste à parcourir. Tout ceci me conforte dans mon idée que cette classe devrait être l’implémentation par défaut à choisir quand on ne maîtrise pas trop les données possibles dans la liste.

3ème étape, la classe HashMap. Pas grand chose de nouveau à l’horizon, O(1) à tous les étages. Sinon quelque chose dont je ne tenais pas compte avant. Comme pour une ArrayList (la HashMap est basée en interne dessus), une map va devoir se redimensionner quand elle aura atteint une certaine capacité (dans la pratique 75% de remplissage d’un tableau de taille puissance de 2). Mais du coup, elle va devoir recalculer tous les hash pour replacer correctement les éléments existants dans les bonnes cases. Et là, ça coince… Le choix de la fonction de hash Java a été faite pour minimiser les collisions pour une taille donnée, mais conduit à un déplacement de 95% des éléments lors d’un redimensionnement ! Autant pour une petite map, ça ne posera pas trop de problème, autant sur une map conséquente, ça va ramer sévère…

On finit enfin par un tour dans le monde de l’accès concurrent aux données, via le package java.util.concurrent. Ou comment garantir un accès correct et rapide à des collections dans un contexte multi-threadé. Le code de la classe ConcurrentHashMap est particulièrement intéressant niveau gestion des threads, avec des verrous par bloc de données pour éviter un lock massif de toute la table, et des algos de best-effort.

Mon petit coup de cœur de la journée, l’analyse d’une structure de données dont je n’avais jamais entendu parler : les skip list, via ConcurrentSkipListMap, des listes chaînées spéciales (en fait plus ou moins des arbres en interne) en O(n log(n)) en accès. Avec la gestion de la concurrence en best-effort en prime, c’est un code qui mériterait d’être présenté en école : alors qu’il est explicitement interdit d’y stocker du null, les algorithmes passent leur temps à vérifier la nullité de ce qu’ils accèdent, signe d’un accès concurrent, et passent alors en best-effort pour aider le petit copain et recommencent du début si ça a échoué et que c’est l’autre qui a fait le travail ! Rémi Forax souligne même que ces algos ne fonctionnent que parce que les CPU allouent de gros morceaux de temps de calcul aux threads, sinon ça finirait littéralement en boucle infinie ! Si vous avez de l’aspirine sous la main, le code est ici.

Java 8 et les lambdas

Dans cette partie, on va parler de la future version de Java, Java 8 (qui a pris du retard), et de sa grosse nouveauté, les lambdas expressions (JSR-335).

Pour bien montrer l’énorme intérêt de cette nouveauté, rien de mieux qu’un exemple concret que tout développeur Java a du rencontrer. Prenons une liste de Personne. On souhaite calculer la somme des âges des personnes majeures. Version Java 7 :

int getAgeSumForAdults(List<People> peoples) {
	int sum = 0;
	for (People people : peoples) {
		if ( people.getAge() >= 18 ) {
			sum += people.getAge();
		}
	}
	return sum
}

Lourd n’est-ce pas ? Et encore, théoriquement j’aurais du l’écrire :

List<People> getAdults(List<People> peoples) {
	List<People> adults = new LinkedList<>();
	for (People people : peoples) {
		if ( people.getAge() >= 18 ) {
			adults.add(people);
		}
	}
	return adults;
}

List<Integer> getAges(List<People> peoples) {
	List<Integer> ages = new LinkedList<>();
	for (People people : peoples) {
		ages.add(people.getAge());
	}
	return ages;
}

int sum(List<Integer> integers) {
	int sum = 0;
	for (Integer i : integers) {
		sum += i;
	}
	return sum;
}

int getSumForAdults(List<People> peoples) {
	List<People> adults = getAdults(peoples);
	List<Integer> ages = getAges(adults);
	return sum(ages);
}

Et oui, parce que le concept d’extraire les adultes des personnes ou de sommer des entiers, on risque d’en avoir besoin pas mal ailleurs dans le code ! Donc qu’il faut mutualiser pour éviter la duplication de code. Mais ça devient d’une lourdeur sans nom… 30 lignes de code pour si peu…

En fait, derrière tout ça se cache de la programmation fonctionnelle, et non plus impérative.

  • getAdults est un filter, une fonction qui prend une liste de quelque chose et retourne un sous-ensemble de cette liste.
  • getAges est un mapper, une fonction qui applique une fonction à une liste et retourne la liste obtenue
  • sum est un reducer, une fonction qui prend une liste en paramètre et retourne une valeur

Il n’existait rien jusqu’en Java 7 pour représenter de telles choses en Java. Ce ne sont pas réellement des objets (on ne peut pas les instancier) mais on peut les affecter à des variables typées « fonctions ». Depuis Java 8, ceci est devenu possible, avec les lambdas expressions. Et le code devient immédiatement plus concis :

int getSumForAdults(List<People> peoples) {
	return peoples.stream().filter(p -> p.getAge() >= 18)
						   .map(p -> p.getAge())
						   .reduce(0, (s, a) -> s + a));
}

Ouch, la claque ! Là j’ai tout mis dans une seule méthode, mais on pourrait très bien extraire chaque fonction dans une variable pour les réutiliser ensuite comme des « méthodes » plus classiques. « Obtenir les adultes à partir d’une liste de personnes » devient donc un « objet » et une variable applicable à toutes listes de personnes, et non plus une méthode conventionnelle.

Au-delà de la concision du code, les lambdas ont aussi un autre gros avantage : la parallélisation massive. Alors qu’on pourrait croire que le code précédent va dérouler 3 boucles en cascade comme dans le cas n°2, en fait le compilateur JIT va être capable de lancer 3 threads qui vont communiquer pour faire le travail en même temps :

  1. le 1er s’occupe de filtrer sur l’âge et donne à manger au 2nd au fil de l’eau
  2. le 2nd extrait l’âge de ceux que le thread 1 lui envoi au fil de l’eau, et passe le résultat au voisin
  3. le 3ème fait la somme de tout ce que le thread 2 lui donne en entrée

En un seul parcours de liste, on a notre résultat, et via un calcul multi-thread qui peut profiter à fond des multi-cœurs de nos PC de bureau d’aujourd’hui. Et si demain on trouve une méga-optimisation qui révolutionne le traitement parallèle, aucun code à changer, c’est le changement de compilateur JIT qui se chargera de nous faire profiter des nouveautés ! Ceci permet aussi de travailler sur des flux très gros voire infini. Alors que Java 7 aurait du tout parcourir pour commencer à faire bosser le suivant, ici Java 8 va tout traiter au fil de l’eau.

Conclusion

Le petit rappel sur l’API Collection n’était pas si inutile que ça au final, et permet de se rendre compte du niveau technique du JDK.

Côté Java 8 et les lambdas, vivement la release officielle, les applications vont vraiment devenir sympa à coder et Java va peut-être enfin se débarasser de son étiquette de langage ultra-verbeux.

eXtreme testing avec Byteman

Ici, une petite présentation rapide de Byteman par Mathilde Lemée. Ou comment tester tout ce qu’il y a de plus crade à tester ! Panne de mémoire, disque plein, réseau hors service, ralentissement de l’application, exception totalement improbable… Comment arriver à tester des cas en principe intestables facilement ou sans mettre réellement en danger le système ? C’est l’objectif de Byteman.

Byteman est un agent Java qui va être capable d’instrumenter le bytecode d’une application et d’en modifier le comportement.

Via des annotations ou des scripts Byteman, on va demander à l’agent de réagir à certaines conditions et d’injecter du code à executer. Vous souhaitez faire sauter une IOException sur une connexion réseau ?

@BMRule(name="throw IOException at 1st transform",
		targetClass = "TextLineProcessor",
		targetMethod = "processPipeline",
		action = "throw new java.io.IOException()")
@Test
public void testErrorInPipeline() throws Exception {
}

Pouf, au prochain appel à TextLineProcessor.processPipeline, on aura une jolie IOException, comme si le réseau avait sauté ou si le disque était plein. Envie d’être plus vicieux ?

@BMRules(rules={@BMRule(name="create countDown for TextLineProcessor",
				targetClass = "TextLineProcessor",
				targetMethod = "<init>",
				action = "createCountDown($0, 2)"),
				@BMRule(name="throw IOException at 3rd transform",
				targetClass = "TextLineProcessor",
				targetMethod = "processPipeline",
				targetLocation = "AT CALL transform(String)",
				condition = "countDown($0)",
				action = "throw new java.io.IOException()")})
@Test
public void testErrorInStuffedPipeline() throws Exception {
}

Pouf, les 2 premiers accès, pas de soucis, le 3ème sera fatal à l’application !

Byteman regorge de déclencheurs de toute sorte : nombre d’exécution, flag, condition Java, timer, identité du caller… Et en prime, comme c’est un agent Java, il a accès à strictement tout dans l’application, variables d’instance privées ou variables locales comprises ! Bon d’accord, le code est au final assez cryptique voire dangeureux (au secours, du code Java dans des String !!!), mais à tests à la con, méthodes à la con.

Byteman va sûrement rejoindre ma collection d’outils pour les tests unitaires (Hamcrest, Mockito et les autres), et je refuserai donc dorénavant les faibles couvertures de code pour cause « d’exceptions qui sont indéclenchables » :)

À demain pour un nouvel épisode !

Journée 2Journée 3

Comments !