Améliorez les performances: Parallel processing with Camel & Spring (Partie 1/2)

Ce billet s'inscrit dans la catégorie optimisation des performances.

Il se fixe comme objectif d'illustrer avec démo détaillée comment améliorer significativement les performances de nos projets.

Et cela sans se lancer dans l'aventure d'écrire des kilomètres de code lourd de gestion du 'parallel processing' car nous allons laisser la puissance du framework Camel opérer.

Sachez néanmoins que de nombreux problèmes de mauvaise performance peuvent être traités sans recours aux implémentations complexes de parallélisme.

Voici deux situations où il faudrait réfléchir sur l'apport du parallélisme :

- Si les tâches ou opérations ne prennent que très peu de temps (échelle quelque sec). La gestion/monitoring des threads devient coûteux.

- Si les ressources mémoire, CPU et IO sont déjà fortement utilisées; sachez que le parallélisme consomme davantage de ressources.

Le billet veut s'inspirer (librement) des deux étapes décrites dans la démo du chapitre 10 du livre "Camel in action".

L'essentiel du code source est fourni ici.

Sachez néanmoins que le code présenté ci-après a été largement adapté et pour certaines parties complètement réécrits.

En effet, comme j'utilise Spring, toutes les parties de code sont adaptées ou réécrites en fonction.

En particulier j'utilise l'implémentation spring ThreadPoolTaskExecutor de l'interface TaskExecutor à la place de la méthode statique de Executors de java.

Le billet est organisée en deux parties:

- La première partie permet d'exécuter en séquentiel notre démo,

- La seconde permet adapte la première partie pour une exécution parallèle.

Nous donnons quelques indications sur le temps d'exécution de chacune des parties.

Avec les bons paramètres nous pouvons diviser par quatre le temps d'exécution voire plus.

Le framework Camel offre avec son java DSL des fonctions permettant d'obtenir simplement de meilleurs performances en parallélisant les tâches.

Comme d'habitude nous le combinons avec le framework Spring v3+ car peut-on faire autrement?

Dans le domaine de performance, il est utile d'identifier les deux limites: CPU-bound et IO-bound.

Distinguer ces deux limites permet d'identifier le bon choix pour améliorer les performances.

En guise de conclusion, dans le domaine d'exécution parallèle, Camel simplifie grandement la vie des développeurs.

Néanmoins, il leur reste la responsabilité de la cohésion des données.

Passons à la mise en pratique.

Nous allons partir d'un projet maven préconfiguré que nous devons compléter pas à pas.

La première partie de ce billet est de montrer le fonctionnement de la démo en séquentiel.

La seconde partie introduit le parallélisme.

USE-CASE / CAS D'ETUDE

Le cas d'étude consiste à construire un ensemble d'instances java (bean du model nommé Tache) depuis des lignes d'un fichier CSV.

Nous le réalisons en exécution sequentielle dans la première partie puis en parallèle dans la seconde en considérant le cas où le fichier csv est trop gros.

Le diagramme de classe suivant est utile pour les deux parties (vous pouvez aggrandir l'image):

diagr_class_parallelCamel.PNG

PREMIERE PARTIE

ETAPE 1. CREATION DU PROJET MAVEN PRECONFIGURE POUR CAMEL

Dans le répertoire workspace, créer un projet maven en mode intéractif avec la commande suivante (en une seule ligne):

mvn archetype:generate -Dfilter=camel-archetype-java 
   -DarchetypeVersion=2.10.3

Répondre à la première question en acceptant la valeur par défaut.

Saisir le nom de votre package pour la seconde question. Noter que ce nom sera utilisé plus loin.

Entrer le nom de votre projet en réponse à la troisième question sur artifactId.

ETAPE 2. IMPORTER LE PROJET DANS ECLIPSE

Cette étape est optionnelle car si vous utilisez l'IDE STS de Spring, vous pourriez importer votre projet maven directement dans STS.

Pour contruire le projet pour eclipse, lancer cette commande:

mvn eclipse:eclipse

Ensuite, importer le projet dans eclipse.

ETAPE 3. AJOUTER DES DEPENDANCES DANS LE POM

Nous complétons le pom.xml avec cette dépendance:

<!-- Camel-spring Component -->
	<dependency>
		<groupId>org.apache.camel</groupId>
		<artifactId>camel-spring</artifactId>
		<version>2.10.3</version>
		<scope>compile</scope>
	</dependency>

NOTE. Le contenu complet du pom sera fourni en annexe.

ETAPE 4. CONFIGURER LE PLUGIN CAMEL

<!-- run the route via 'mvn camel:run' -->
	<plugin>
		<groupId>org.apache.camel</groupId>
		<artifactId>camel-maven-plugin</artifactId>
		<version>2.10.3</version>
	</plugin>

ETAPE 5. AJOUTER LE FICHIER DE CONFIGURATION DE SPRING

Ajouter le fichier camel-context.xml dans src/main/resources/META-INF/spring (si besoin créer/ajouter les répertoires manquants).

Il ne contient que la déclaration du contexte camel ainsi que l'unique route existante:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Configures the Camel Context -->
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:camel="http://camel.apache.org/schema/spring"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://camel.apache.org/schema/spring 
    http://camel.apache.org/schema/spring/camel-spring.xsd">
     <!-- camel context -->
      <camel:camelContext 
		xmlns="http://camel.apache.org/schema/spring">
	<camel:routeBuilder ref="myRouteBuilder"/>
	</camel:camelContext>
</beans>

Vous devez adapter son contenu en fonction de votre contexte et le nom de package déclaré au moment de créer le projet maven.

Vous pouvez tester la construction de votre projet maven avec la commande sur la console:

mvn camel:run

Pour quitter le programme tapez CTRL-C.

Si vous jetez un oeil sur le dossier target vous verrez un sous-dossier nommé messages contenant le contenu filtré des fichiers xml du répertoire src/data.

Ok, or tout cela n'est pas notre sujet aujourd'hui mais nous avons maintenant un projet maven préconfiguré pour camel.

NOTE IMPORTANTE

Pour la suite, les deux classes java MainApp et MyRouteBuilder ne seront plus utiles.

Vous pouvez les supprimer mais n'oubliez de retirer toute référence sur MainApp dans les plugins du pom.xml.

Il est temps de revenir à notre sujet.

Commençons par écrire un bean model nommé Inventory annoté avec les annotations du composant camel-bindy

ETAPE 6. ECRIRE LE BEAN MODEL ANNOTE

Voici donc le code source du bean model, de l'interface et de sa classe d'implémentation:

Le bean model est Inventory annoté avec les annotations du composant camel-bindy:

package fr.netapsys.blogs.camel.beans;
import java.io.Serializable;
import org.apache.camel.dataformat.bindy.annotation.CsvRecord;
import org.apache.camel.dataformat.bindy.annotation.DataField;
@CsvRecord(separator = ",", skipFirstLine = true)
public class Inventory implements Serializable {
	private static final long serialVersionUID = 1L;
	@DataField(pos = 1)
	private String supplierId;
	@DataField(pos = 2)
	private String partId;
	@DataField(pos = 3)
	private String name;
	@DataField(pos = 4)
	private String amount;
	public Inventory(String supplierId, String partId,
                                  String name,String amount) {
		this.supplierId = supplierId;
		this.partId = partId;
		this.name = name;
		this.amount = amount;
	}
//... omis les getters/setters et toString
}

Ce bean utilise le composant camel-bindy qui sera ajouté dans dépendances plus loin.

Il est annoté pour être mappé avec les lignes du fichier inventory.csv.

ETAPE 8. FICHIER CSV

Voici un exemple du fichier csv:

supplierId,partId,name,amount
1,2,toto,123.90
2,2,titi,89000.45

ETAPE 9. DESSINER UNE (BELLE) ROUTE CAMEL

Comme promis, nous nous inspirons du cas d'étude du chapitre 10 du livre Camel in action.

Comme vous pouvez constater, nous l'avons complètement adapté pour écrire encore moins de code.

A titre d'exemple, la transformation des lignes CSV en un POJO est désormais géré par le composant camel-bindy.

A l'aide de camel java DSL, nous dessinons notre route dans la classe MyRoute comme suit:

package fr.netapsys.blogs.camel.routes;
import org.apache.camel.LoggingLevel;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.dataformat.bindy.csv.BindyCsvDataFormat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import fr.netapsys.blogs.camel.impl.InventoryServiceImpl;
import fr.netapsys.blogs.camel.processors.MyProcessor;
@Component
public class MyRoute extends RouteBuilder {
 @Autowired 
 private MyProcessor myProcessor;
 
 public void configure() throws Exception {
  from("file:src/data/?fileName=inventory.csv&noop=true")
   .log(LoggingLevel.INFO,"MyRoute",
    "Starting process inventory.csv at ${date:now:dd/MM/yy HH:mm:ss}")
   .unmarshal(new BindyCsvDataFormat("fr.netapsys.blogs.camel.beans"))
   .process(myProcessor)
   .split(body())
   .streaming()
   .bean(InventoryServiceImpl.class, "updateInventory")
  .end()
  .log(LoggingLevel.INFO,"MyRoute",
     "Done processing file at ${date:now:dd/MM/yy HH:mm:ss}.");	
  }	
}

Explications:

La route de camel commence par lire le fichier inventory.csv défini via l'argument fileName dans le répertoire src/data.

Le second argument noop=true indique à camel de ne pas déplacer le fichier après son traitement. Sinon, par défaut, camel déplace le fichier dans le répertoire caché .camel.

La fonction unmarshal transforme le contenu du fichier csv en une map d'instances Inventory.

Puis le processor récupère la liste des instances Inventory et définit le body de l'exchange.

Le code de la classe MyProcessor est donné ci-après.

Le split(body()) décompose à juste titre la liste d'objets en une instance Inventory.

Enfin, c'est la méthode updateInventory qui opère sur chaucne des instances de la liste.

Pour finir, une simple trace log termine la route camel.

Le service InventoryServiceImpl doit implémenter la seule méthode updateInventory.

L'étape suivante présente les détails d'implémentation.

Mais avant cela, voici le code du processor Myprocessor:

package fr.netapsys.blogs.camel.processors;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.springframework.stereotype.Component;
import fr.netapsys.blogs.camel.beans.Inventory;

@Component
public class MyProcessor implements Processor{
  @Override
  public void process(Exchange exchange) throws Exception {
   List<Map<String, Inventory>> listMapInv=
                 exchange.getIn().getBody(List.class);
   List<Inventory> listInv=new ArrayList<>();	
   for(Map<String,Inventory> map:listMapInv){ 
     listInv.add(
        (Inventory)map.get("fr.netapsys.blogs.camel.beans.Inventory"));
   }
   exchange.getIn().setBody(listInv);
 }
}

Cette classe implémente le contrat Processor de camel d'où la définition de la méthode process().

Dans process(), nous récupérons les instances Inventory à partir du fichier csv dans une liste, puis le body de l'exchange est défini avec cette liste.

ETAPE 10. ECRIRE LE SERVICE METIER

Contrairement à l'exemple du chapitre 10 du livre "Camel in action", notre interface est mono car il déclare une seule méthode.

package fr.netapsys.blogs.camel.interfaces;
import fr.netapsys.blogs.camel.beans.Inventory;
public interface InventoryService {
  void updateInventory(final Inventory inv) throws Exception;
}

Son implémentation est:

package fr.netapsys.blogs.camel.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import fr.netapsys.blogs.camel.beans.Inventory;
import fr.netapsys.blogs.camel.interfaces.InventoryService;
public class InventoryServiceImpl implements InventoryService {
  @Override
  public void updateInventory(Inventory inventory) throws Exception {
     // simulate updating using some CPU processing
     Thread.sleep(100);
  }
}

ETAPE 11. AJOUTER LES DEPENDANCES DANS LE POM

Ajouter la dépendance sur le composant camel-bindy:

<dependency>
  <groupId>org.apache.camel</groupId>
  <artifactId>camel-bindy</artifactId>
  <version>2.10.3</version>
</dependency>

ETAPE 12.TESTER L'EXECUTION SEQUENTIELLE

Voici une première classe abstraite pour faciliter l'écriture des tests JUnit.

package fr.netapsys.blogs.camel.demo.tests;
import org.apache.camel.CamelContext;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:META-INF/spring/camel-context.xml"})
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
public abstract class TestParent  {
	@Autowired protected ApplicationContext sprinCtx;
	@Autowired protected CamelContext camelCtx;	
}

Nous écrivons un premier test JUnit comme suit:

package fr.netapsys.blogs.camel.demo.tests;
import org.junit.Test;
public class TestSequentielRouteTest extends TestParent {	
	@Test
	public void testSeqFile() throws InterruptedException{
		Thread.sleep(3000);
	}
}

Comme vous pouvez le constater nous avons là aussi simplifier l'écriture du test par rapport à celui du chapitre 10 du livre "Camel in action".

La classe test JUnit hérite de la classe abstraite TestParent qui se charge de démarrer le contexte de spring.

Ainsi, la route camel est automatiquement lancée par le contexte camel démarré par Spring.

SECONDE PARTIE: EXECUTION PARALLELE

Voici les étapes que nous développerons dans la deuxième partie de ce billet.

ETAPE 13. REDESSINER LA ROUTE DE CAMEL

ETAPE 14. DECLARER LA ROUTE DANS SPRING

ETAPE 15. CONFIGURER L'EXECUTOR DE SPRING

ETAPE 16. TESTER L'EXECUTION PARALLELE

A très bientôt.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.