Apache Camel et performance

Après avoir bien pris en main Camel, je dois dire que je suis tout à fait satisfait des possibilités offertes (voir mon précédent article). Cependant, j'ai remarqué quelques comportements peu adaptés au traitement des volumétries importantes de données.

En effet, il semblerait que certains composants et/ou outils proposés par défaut causent des pics de mémoire voir même systématiquement des crash OutOfMemory. Je vais donc essayer ici de les décrire afin de permettre aux futurs développeurs de les prendre en compte dès le départ. Je précise tout de même que j'utilise actuellement la version 2.8.3.

Marshalers et OutOfMemory

Voici un exemple de code simpliste situant le problème :

from("file://...").unmarshal().zip().to("file://...");

Ici, le but est de dezipper un fichier pour écrire son contenu vers la destination spécifiée. Utilisez un fichier de 500Mo par exemple et l'application devrait planter assez rapidement!

En effet, la méthode unmarshall().zip() va charger en mémoire (!) un DeflaterOutputStream correspondant au contenu total du fichier AVANT de l'écrire. Qui plus est, un fichier plus modeste (quelques Mo), ne causant pas de crash, restera chargé en mémoire jusqu'à l'arrivé du garbage collector.

L'intérêt de conserver en mémoire ces informations peut être compréhensible dans le cas du traitement de petits fichiers, mais dès lors que l'on pousse l'implémentation vers des volumétries importantes, il vaut mieux privilégier une autre stratégie (Camel ne prévoyant à priori pas la possibilité d'écrire le flux de données au fur et à mesure de se décompression). Un code plus robuste ressemblerait à :

from("file://...").convertBodyTo(File.class).to("bean://com.MyDezipperBean");

Qui se contente de rediriger le fichier vers un bean personnalisé qui permettra de mieux gérer les flux de données, notamment grâce au classique java.util.zip.*

Notez qu'il en va de même pour le CSV. L'appel à la méthode unmarshal().csv() produira le même comportement. Préférez donc l'utilisation de org.apache.commons.csv.CSVParser par l'intermédiaire d'un bean, beaucoup plus à-même de gérer les fichiers CSV volumineux.

Split et montées en charge

Imaginons la nécessité de découper un gros fichier CSV entrant en plusieurs plus petits (quelques soient les conditions et méthodes employées). Une implémentation évidente ressemblerait à :

from("file://...").convertBodyTo(File.class)
  .split().method("com.services.CsvService","splitDatas")
  .to("file://...");

com.services.CsvService est un bean recevant le fichier CSV initial, le parcourant pour découper le contenu selon X règles métier puis retournant une liste de contenus CSV destinés à être écrits dans des fichiers séparés.

En observant de près le comportement de l'application, on aura vite fait de se rendre compte de la rapide montée en charge due essentiellement au stockage en mémoire des informations CSV (lors de leur transfert sous forme de message du bean vers l'endpoint fichier). Une fois de plus donc, le cas du traitement de fichiers nombreux et/ou conséquents peut vite devenir problématique. Une solution de contournement peut consister à faire écrire directement au bean les fichiers (pour ne pas avoir à les re-transporter sur Camel) puis éventuellement de faire transiter simplement leur chemin si besoin.

Voici donc les problématiques que j'ai rencontrées à ce jour. Après avoir été plusieurs fois agréablement surpris par l'intelligence des composants et des diverses implémentations de EIP proposées par Camel, je dois avouer que j'ai quelque peu été dérouté par ces comportements.

Hope this helps!


Fichier(s) joint(s) :

6 commentaires:

Stéphane a dit…

Bonjour,

La page http://camel.apache.org/splitter.html indique le paramètre streaming pour le split.

Est-ce que cela ne résout pas le problème ?

Paul-Emmanuel Faidherbe a dit…

Effectivement, cela vaudrait le coup d'être testé, je ne sais pas exactement comment est implémenté ce streaming... J'essaierai d'approfondir le sujet par curiosité!

Paul-Emmanuel Faidherbe a dit…

Après quelques tests, il s'avère que cette fonction permet bien de réduire la montée en charge dans ce contexte. Mais elle n'est utilisable que si on laisse la main à Camel pour le split, comme dans l'exemple :
from("direct:start").split(body().tokenize(",")).streaming()
Dans le cas où on utilise un bean pour le split, la signature de la méthode à utiliser étant :
public List<Message> myDoSplit(Exchange ex)
Les données restent stockées en mémoire le temps de la construction de la réponse (liste de messages). Je ne sais pas s'il est possible d'envoyer les messages au fur et à mesure de leur élaboration dans ce contexte...

Claus Ibsen a dit…

The Splitter in Streaming mode, can use a custom bean to control this behavior. Just let the bean return an Iterator, which the splitter uses in streaming mode.

For example we do this with the new tokenizePair
http://davsclaus.blogspot.com/2011/11/splitting-big-xml-files-with-apache.html

See the source code for examples how this can be implemented
https://svn.apache.org/repos/asf/camel/trunk/camel-core/src/main/java/org/apache/camel/support/TokenPairExpressionIterator.java

The unmarshal zip data format, does *not* yet support files. I will improve the documentation to mention this.

Paul-Emmanuel Faidherbe a dit…

Well, first of all, thank you Claus for your contribution and for reading me!
I read your article and found this is a great improvement for Camel : I will try to upgrade my application as soon as I can.
But is the Iterator solution available for CSV content?
For the documentation, it may be useful to add paragraphs on how Camel behaves with the different methods signatures, like returning an Iterator, or a list of messages, an Exchange, or void...

Claus Ibsen a dit…

The streaming with a bean that returns an iterator should work for all of the 2.x releases.

Yes we should improve the documentation about that.