mercredi 15 avril 2015

Quand Java n'aime pas Linux

D'abord, désolé pour ce titre racoleur !

Linux est un très bon système cependant ce titre relate une réalité à propos de certaines applications Java (sous forme de batch en .jar ou d’applications Web .war ou .ear) qui sont sensibles à l’ordre de chargement des fichiers.


Commençons par un petit Quizz.  
Avez-vous déjà testé le résultat de la méthode list() de java.io.File sur différents systèmes d’exploitations et sur différents systèmes de fichiers également ?
Comme beaucoup de développeurs en entreprise ou freelance, vous travaillez essentiellement sous Windows ou MacOS X.


Voyons ce que nous dis la JavaDoc de la méthode list() de java.io.File (je mets en gras et en rouge la partie qui nous intéresse) :


public String[] list()
Returns an array of strings naming the files and directories in the directory denoted by this abstract pathname.
If this abstract pathname does not denote a directory, then this method returns null. Otherwise an array of strings is returned, one for each file or directory in the directory. Names denoting the directory itself and the directory's parent directory are not included in the result. Each string is a file name rather than a complete path.
There is no guarantee that the name strings in the resulting array will appear in any specific order; they are not, in particular, guaranteed to appear in alphabetical order.
...


En gros, rien ne garantie que le résultat du tableau renvoyé sera dans un ordre quelconque notamment un ordre alphabétique.


Toujours sceptique ? Voici le résultat du listage d’un répertoire sous Windows, Mac OS X, AIX, Linux:


...
File file = new File(strFolder);
if (file.exists() && file.canRead() && file.isDirectory()) {
String[] fileList = file.list();
           System.out.println("reading folder : " + file + "\n");
for (String fileToDisplay : fileList) {
       System.out.println("\t" + fileToDisplay);
}
}


OS
Windows
Mac OS X
AIX v6 (IBM)
Linux (Redhat 6.5)
Linux (Cent OS 7)
File
System
exemple du listage par
list() de la classe File

a
AB
b
o
P
y
Z

Applications
apps
..
dev
Developer
etc
...
Users
usr
var

AB
P
Z
a
b
o
y

o
Z
AB
y
P
a
b

boot
sys
etc
var
...
bin
lib64
home
mnt
opt
ordre constaté
ordre a-Z
indépendemment de la casse
ordre a-Z
indépendemment de la casse
ordre A-Z puis a-z

dépendant de la casse
ordre semblant “aléatoire”
ordre semblant “aléatoire”
Attention, ici, l’ordre semblant aléatoire de ext à ext4 et de XFS est dû à une combinaison d’un hachage du nom de fichier avec celui UNIQUE de la racine de la partition sur laquelle les fichiers sont installés.  Si le nom de fichier ne change pas (et c’est le cas d’un WAR et EAR décompressé), l’ordre non alphabétique reste exactement le même à chaque redémarrage de la JVM ou d’un appel à  list() de java.io.File()


Est-ce grave ?
Oui, l’application peut ne plus fonctionner selon qu’elle charge le bon fichier ou pas (par exemple un fichier properties ou potentiellement une classe Java spécifique).
C’est lors d’une mission de migration d’applications Java (développement interne et des progiciels) d’AIX à Linux que j’ai constaté cela.
Chez le client, rien n’avait été vu avant côté développement car sous Windows et aussi l’ancienne production en AIX le comportement est quasiment similaire (ordre de A à Z).  C’est de manière aléatoire qu’arrivaient certains problèmes, non repérés de suite car l’ordre n’est jamais le même en Linux d’une VM à l’autre et d’une partition FileSystem à l’autre. On pouvait donc avoir eu de la chance en recette Linux et aller en prod en erreur.

Nous avons eu le cas de la banque en ligne dont l’analyse à durer plus d’un mois afin d’être sûr du problème. Pourquoi autant de temps d’analyse ? Plusieurs circonstances dont le fait d’être en juillet / août mais aussi le fait principal que cela avait déjà marché une première fois en environnement de pilote Linux.  
C’est en tentant de basculer vers la vraie prod que les soucis étaient arrivés. Des événements ont été mis en avant par chacune des équipes (développement, socle et production) pour creuser l’analyse dans un sens où l’erreur est “sûrement” chez l’autre. 
Du coup, chaque équipe cherchait des arguments et contre-arguments pour ne pas être en cause. En fait et au final, les serveurs étaient bien installés, l’application était la même qu’en pilote Linux. La cause était ailleurs...

A quoi c’est dû ?
La sensibilité à l’ordre de chargement des fichiers via soit un framework maison, soit Spring (avec l’option classpath: ), soit directement par le serveur d’application lui-même ne peut intervenir que si l’application possède des fichiers en doublons et dont le contenu est différent.


Pour rappel, voici les différents problèmes liées à l’ordre des fichiers :
  • même nom de fichier .properties dans 2 répertoires différents avec un contenu différent le tout  gérer via un chargement à la “classpath: “. Ici, dans un classloader maison d’un framework maison.  Selon l’ordre de chargement, c’était soit le bon fichier, soit le mauvais qui était pris en compte selon la manière dont l’EAR avait été dézipper sous Linux.
  • même jar d’un framework éditeur mais avec 2 versions mineures présentes dans le WEB-INF\lib. Pareil, selon l’ordre de chargement des jars, sur certaines VM Linux, on avait une version n-1 chargée en premier ou sur d‘autre la version n
  • même classe (même package) dans 2 jars différents avec un contenu différent dans le WEB-INF\lib.  Ce problème a l’air simple mais il a fallu de multiples analyses et éliminer d’autres pistes avant d’envisager puis d’être sûr de cette conclusion.  Cela est une pratique très courante chez les éditeurs de progiciels Java de “patchers” soit leur propre jars applicatifs soit celle d’API / framework standard par la surcharge d’une classe (Ex: XmlCipher) ajoutant un comportement différent de la classe du jar original mais ici, selon l’ordre de chargement des jars du WEB-INF\lib du serveur d’application, cela sera la bonne ou la mauvaise implémentation de cette classe qui sera chargée en premier.
  • Attention car les bibliothèques natives (.so, .dll ou .dylib) appelées par Java via JNI sont également soumises à ce problème d’ordre des fichiers.


Ce constat est vrai aussi pour les applications construites par maven sans maîtrise de leurs dépendances (dont les transitives). Pour rappel, il n’existe pas que le scope “compile” mais attention aussi au scope “runtime” qu’on ne découvre qu’à l’exécution.
Ceux qui ont déjà dû jouer avec les SLF4J et les common logging dans leur gestion des dépendances (rien que sur les logs) savent de quoi je parles.
Pour résumer, mieux vaut éviter d’importer trop de JARs par dépendance directe ou transitive dont des classes Java avec un même package et un contenu différent pourrait se retrouver embarqué avec une chance sur deux que la “mauvaise” classe soit chargée avant.


Comment éviter cela et réconcilier applications & progiciels Java avec Linux ?
  • Détecter tous fichiers (et classe+package Java) en doublons dont le contenu est différents
  • Supprimer le (ou les) doublons de manière à ce qu’un seul fichier, qu’une seule classe java (de même package) soit intégrée au livrable Java
  • Une solution non pérenne est de forcer à trier les fichiers par ordre alphabétique juste après le list() de java.io.File. Cela peut se concevoir sur un framework maison, plus difficilement sur Spring et encore moins en forçant cela dans le serveur d’application (surtout s’il s’agit de WebSphere ou équivalent en terme de richesse/complexité).


Existe-t-il un outil pour faciliter ce travail de vérification ?
J’ai crée un outil dédié à cette problématique qui peut lever des alertes mais il peut y avoir des faux positifs. Je dois le rendre plus intuitif et plus flexible pour le distribuer.
Il existe sinon le projet TattleTale de JBoss pour vérifier déjà certains points.


En fait, mais pourquoi les File System (ext à ext4 et XFS) réagissent-ils comme cela ?
Le listage des entrées du répertoire sur ext (à ext4) et sur XFS s’appuie sur un mécanisme complexe de Hash Tree Directory. Voir ici pour les plus curieux. :
On combine la signature (hachage) du nom de fichier selon 3 algos possibles (Legacy, Tea et Half MD4)  avec un “Directory Hash Seed” du répertoire racine de la partition du File System.  Cette signature (“hash seed”) pour la racine de la partition est unique et la recopie de fichiers d’une partition à l’autre engendrera un ordre de listage différent.


Voici un exemple d’une partition ext2 vue avec l’outil “debugfs” :
Filesystem magic number:  0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features:   ext_attr resize_inode filetype sparse_super
Filesystem flags:         signed_directory_hash

Default directory hash:   half_md4
Directory Hash Seed:      6d5319ae-528e-494e-873b-8453642d8990


Ci-contre, un test de hachage avec DebugFS en fonction de l’algo sur le même fichier:
debugfs:  dx_hash -h tea F.properties     
                        Hash of F.Properties is 0x3e056e5e (minor 0x3e89959f)
debugfs:  dx_hash -h half_md4 F.properties
                        Hash of F.properties is 0x9df9c1d0 (minor 0x56df8f21)
debugfs:  dx_hash -h legacy F.properties
                        Hash of F.properties is 0x6defc07e (minor 0x0)
debugfs:  dx_hash -h half_md4 -s 6d5319ae-528e-494e-873b-8453642d8990 F.properties
                        Hash of F.properties is 0x92b564ba (minor 0x4315a3ab)
debugfs:  dx_hash -h half_md4 -s 6d5319ae-528a-494e-873b-8453642d8999 F.properties
                        Hash of F.properties is 0x2df1194a (minor 0xe0ebfce6)


Pour ceux qui veulent aller plus loin, voici le code de Linux sur le File System ext4 pour la fonction readdir() : https://github.com/torvalds/linux/blob/master/fs/ext4/dir.c


Les applications autre que Java sont-elles concernées aussi ?
En fait, elles aussi sont concernées.  Un programme C effectuant ce listage des répertoires utilise la fonction système “readdir()” qui à son tour réagira selon le FileSystem installé.
C’est donc toute application quelque soit le langage (et pas seulement Java) qui peuvent avoir des difficultés à migrer sous Linux.  Encore faut-il bien écrire son application et éviter les doublons.

Conclusion
En une seule phrase: Java s’avère être plus sensible au changement de File System que d’OS.
Le changement d’OS peut faire changer le file.encoding, la Locale et impacte les appels JNI en cas de bibliothèques C native en 32 ou 64bits mais ce périmètre est en général connu et maîtrisé.
Ici, le File System perturbe les applications packagées en supposant que l’ordre de parcours de répertoire et de fichiers sera toujours en ordre alphabétique.
Comme souvent, le problème se situe dans des habitudes de programmation et d’intégration qui abaisse la vigilance et provoque l’apparition de certaines mauvaises pratiques.

Aucun commentaire:

Enregistrer un commentaire