Utiliser Karaf dans une architecture en cluster

image_pdfimage_print

karaf-logoLors de la première partie du tuto “Développement OSGI pour serveur Karaf“, nous avions vu que le serveur Apache Karaf avait un certains nombres de qualités intéressantes dont la capacité à créer des instances filles en quelques lignes de commande.
Mais j’avais omis un petit détail qui peut avoir son importance, cette qualité va lui permettre de travailler en mode DOSGi.

Prérequis

Si les notions de création de projet OSGi orienté Apache Karaf ne vous sont pas familières je vous propose d’aller faire un tour du coté des tutoriaux suivants :

Si vous avez besoin de vous référencer à quelque chose d’existant au cours de ce tutorial je vous propose le tarball du projet : drinkerProject.tar.gz.

Qu’est ce que le mode DOSGi ?

DOSGi signifie Distributed OSGI, soit le développement d’application utilisant des composants distribués sur un cluster. En résumé chacun des noeuds du cluster sera en capacité d’écouter et d’émettre des informations vers les autres noeuds.

Mise en place d’un cluster (généralités)

La capacité d’OSGi est apportée entre autres par un module de Apache Karaf appelé Cellar. Ce module propose, au travers de la brique Hazelcast, de manager et synchroniser les instances de karaf composant un cluster. Il permet entre autres :

  • La découverte automatique des noeuds (en outre par multicast ou peer2peer)
  • La création de groupe de cluster, chacun d’eux aura sa propre configuration ainsi que son propre jeu d’installation
  • La distribution des configurations : à chaque modification sur un noeud celle-ci est directement relayée vers les autres noeuds du groupe.
  • La distribution des features : à chacune des installations sur un noeud les features et les informations de repositories seront partagées sur l’ensemble des noeuds du groupe
  • La distribution des bundles : à chacune des installations sur un noeud les bundles seront partagés sur l’ensemble des noeuds du groupe
  • Une interface Web permet de gérer et superviser les groupes de cluster
  • Pour finir, Cellar embarque la possibilité de fonctionner en mode DOSGi, afin de pouvoir contacter des composants présents dans le cluster

Cellar s’installe comme n’importe quelle application dans karaf, rendez vous dans la console :

features:addurl mvn:org.apache.karaf.cellar/apache-karaf-cellar/2.3.2/xml/features
features:install cellar

Capture du 2013-11-29 23:11:19

Nous voila avec une instance Karaf “cellarisée” !
Il nous faudra juste la configurer et créer des instances filles pour mettre en place un cluster de façon relativement simple.

Pour se faire, cellar embarque une certaine quantité d’outils en ligne de commande que l’on peut obtenir en préfixant l’appel par “cluster:<TAB>” :

Capture du 2013-11-29 23:14:53

Configuration de base

Dans le cadre de ce tuto, je vous propose de travailler avec des “instances fille”. Il s’agit d’une des facilités de karaf permettant de créer des copies partielles de serveur n’intégrant que :

  • Les fichiers de configuration
  • Le dossier de données qui contiennent toutes les informations d’exécution
  • Les fichiers de logs
  • Les fichiers temporaires

Chacune des instances peut être lancée indépendamment des autres et posséder ses propres applications.

Donc notre configuration sera la suivante :

schema_karaf_children_instances

Création des instances filles

Pour créer une nouvelle instance, nous allons utiliser un des outils pratiques de Karaf : admin:create.

Voici l’exemple pour l’instance “barman_1″ :

Capture du 2013-11-30 22:09:01

Donc l’opération sera à refaire pour  :

  • “client”
  • “barman_2″

Une fois les instances filles créées, il va falloir leur installer Cellar avec la démarche vue dans la partie “Mise en place d’un cluster (généralités)”. Mais avant ça, il va falloir démarrer et se connecter à chaque instance.

Par exemple voici la procédure pour “barman_1″ (la même chose est à faire sur les deux autres instances) :

Capture du 2013-12-02 11:55:07

Comme on le voit il est facile de se connecter aux instances filles à partir de l’instance mère… et si vous oubliez le nom d’une instance n’oubliez pas que dans karaf le shell possède une complétion avec <TAB>.

Donc maintenant, si nous allons sur n’importe qu’elle instance fille et que nous faisons un cluster:node-list, nous obtenons la liste suivante :

Capture du 2013-12-02 17:15:31

Remarque : Par défaut, toutes les instances sur lesquelles cellar est installé sont visibles par tous les noeuds du cluster ! Sans configuration supplémentaire, tous les noeuds appartiennent au même groupe. La ligne préfixé par * n’est autre que l’instance sur laquelle nous sommes connecté.

Un peu de code

Nous souhaitons obtenir une hiérarchie de ce genre :

515b2aa5

DrinkService : L’interface de notre service

package fr.conceptit.tuto.drink.api;

public interface DrinkService {
	public String canIHaveADrinkPlease(String drink);
}

DrinkServiceImpl : L’implémentation du service

package fr.conceptit.tuto.drink.service;

import java.text.MessageFormat;

import fr.conceptit.tuto.drink.api.DrinkService;

public class DrinkServiceImpl implements DrinkService {
	private static final String msg = "Your glass of {0} is served ;-)";

	public String canIHaveADrinkPlease(String drink) {
		return MessageFormat.format(msg, drink);
	}
}

Drinker : Notre consommateur

package fr.conceptit.tuto.drink.client;

import fr.conceptit.tuto.drink.api.DrinkService;

public class Drinker {
	private DrinkService drinkService;

	public DrinkService getDrinkService() {
		return drinkService;
	}

	public void setDrinkService(DrinkService drinkService) {
		this.drinkService = drinkService;
	}

	public String request(String drink) {
		return "# " + drinkService.canIHaveADrinkPlease(drink);
	}
}

DrinkerCommand : Notre extension de shell

ppackage fr.conceptit.tuto.drink.client.command;

import org.apache.felix.gogo.commands.Action;
import org.apache.felix.gogo.commands.Argument;
import org.apache.felix.gogo.commands.Command;
import org.apache.felix.service.command.CommandSession;

import fr.conceptit.tuto.drink.client.Drinker;

@Command(scope = "drinker", name = "command", description = "Can i have a drink , Please !")
public class DrinkerCommand implements Action {
	@Argument(index = 0, name = "drink", required = true, description = "Type of drink", multiValued = false)
	private String drink;

	private Drinker drinker;

	public Object execute(CommandSession session) throws Exception {
		System.out.println(drinker.request(drink));
		return null;
	}

	public String getDrink() {
		return drink;
	}

	public void setDrink(String drink) {
		this.drink = drink;
	}

	public Drinker getDrinker() {
		return drinker;
	}

	public void setDrinker(Drinker drinker) {
		this.drinker = drinker;
	}
}

Personnellement, j’ai créé 1 projet avec 3 sous projets :

  • DrinkerProject : Contient les 3 autres projets + un dossier de ressources dans lequels on place le fichier features.xml.
    • drinker-api : Ne contient que l’interface et génère un jar tout ce qu’il y a de plus simple. Afin de partager cette dernière vers les deux autres modules.
    • drinker-service : Contient l’implémentation du service ainsi que le fichier de définition blueprint de notre module.
    • drinker-client : Contient le client et l’extension de shell et utilise le service défini dans le projet “DrinkerService”.

Le fichier features.xml contient les entrées suivantes :

  • all-in-one : installation totale des bundles en une fois (pour un test mono instance par exemple)
  • drinker-client : installation des bundles utilisés pour le client
  • drinker-client : installation des bundles utilisés pour les services
<?xml version="1.0" encoding="UTF-8"?>

<features name="drinker-project" xmlns="http://karaf.apache.org/xmlns/features/v1.0.0">

	<feature name='all-in-one' description='Drinker Service Client'
		version='${project.version}' resolver='(obr)'>
		<bundle>mvn:fr.conceptit.tuto.drink/drinker-api/${project.version}
		</bundle>
		<bundle>mvn:fr.conceptit.tuto.drink/drinker-service/${project.version}
		</bundle>
		<bundle>mvn:fr.conceptit.tuto.drink/drinker-client/${project.version}
		</bundle>
	</feature>

	<feature name='drinker-client' description='Drinker Service Client'
		version='${project.version}' resolver='(obr)'>
		<bundle>mvn:fr.conceptit.tuto.drink/drinker-api/${project.version}
		</bundle>
		<bundle>mvn:fr.conceptit.tuto.drink/drinker-client/${project.version}
		</bundle>
	</feature>

	<feature name='drinker-service' description='Drinker Service Service'
		version='${project.version}' resolver='(obr)'>
		<bundle>mvn:fr.conceptit.tuto.drink/drinker-api/${project.version}
		</bundle>
		<bundle>mvn:fr.conceptit.tuto.drink/drinker-service/${project.version}
		</bundle>
	</feature>
</features>

Test des bundles

Si vous souhaitez tester votre code sans toucher à vos 2 instances toutes propres, vous pouvez créer une instance “test”, la démarrer, vous y connecter et lancer les commandes suivante  :

features:addurl mvn:fr.conceptit.tuto.drink/drink-dosgi/1.0.0-SNAPSHOT/xml/features
features:install all-in-one
drinker:command vodka

Si la dernière commande vous renvoie “Your glass of vodka is served ;)” c’est que vos bundles sont fonctionnels et que tout est ok pour la suite.

Séparation des rôles

Nous allons séparer nos instances par métier et donc par groupe :

  • grp_bar : comportant barman_1 et barman_2
  • grp_crowd : comportant client

Rien de plus simple à faire avec Cellar et ses commandes cluster:group-*.

Capture du 2013-12-09 20:22:38

Pour explication, la commande cluster:group-create grp_crowd et cluster:group-create grp_bar permet de créer nos deux groupes.
La commande cluster:group-set grp_bar et cluster:group-set grp_crowd permet d’attribuer un groupe à chaque noeud. Facile, non ?!

Mais, mise à part à bien ranger nos instances, à quoi vont servir ces groupes ? Et bien ils vont permettre de faciliter notre gestion des installations et des configurations grâce encore une fois aux commandes émanantes de “cluster:”.

Nous voulons installer le service sur les instances “barman_*” et notre client sur l’instance client et voir se qui se passe. Pour commencer, installons les serveurs avec la commande suivantes :

cluster:feature-url-add grp_bar mvn:fr.conceptit.tuto.drink/drink-dosgi/1.0.0-SNAPSHOT/xml/features
cluster:feature-install grp_bar drinker-service

L’utilisation de la commande cluster:feature-install [groupe] [feature] permet d’installer la feature sur l’ensemble des instances appartenant au groupe. Pratique, non ?

Pour vérifier si la feature et donc ses bundles sont bien démarrés, il suffit de faire la commande osgi:list et de vérifier la colonne “State”, si l’état est “Active” c’est bon sinon il y a un problème de lancement :

karaf@barman_2> osgi:list
START LEVEL 100 , List Threshold: 50
   ID   State         Blueprint      Level  Name
[  64] [Active     ] [            ] [   80] Drink :: Service API (1.0.0.SNAPSHOT)
[  65] [Active     ] [Created     ] [   80] Drink :: Client :: Bundle (1.0.0.SNAPSHOT)

Passons au client avec son bundle “drink-consumer” :

karaf@client> cluster:feature-url-add grp_bar mvn:fr.conceptit.tuto.drink/drink-dosgi/1.0.0-SNAPSHOT/xml/features
cluster:feature-install grp_crowd drinker-client
karaf@client> cluster:feature-install grp_crowd drinker-client
karaf@client> osgi:list
START LEVEL 100 , List Threshold: 50
   ID   State         Blueprint      Level  Name
[  64] [Active     ] [            ] [   80] Drink :: Service API (1.0.0.SNAPSHOT)
[  65] [Active     ] [Created     ] [   80] Drink :: Client :: Bundle (1.0.0.SNAPSHOT)

Comme on peut le voir, les bundles sont bien actifs et apparemment aucune erreur n’est levée !

Si tout est OK, testons notre appel avec la commande drink:command champagne. Malheureusement l’appel va se faire mais le retour ne sera pas celui escompté  car nous obtenons une string “null”.

Ce comportement est normal. En effet, les services sont sur 2 instances différentes et le client sur une 3ième … aucune chance qu’ils se connaissent et par conséquent aucune chance que le service ne réponde au client.

C’est la capacité DOSGi de Karaf avec Cellar qui va nous donner la solution à ce problème.

Donc le DOSGi dans tout ça ?

Il va permettre de répondre de façon relativement simple au problème “d’absentéisme” mentionné ci-dessus. Précédemment, nous avons installé Cellar sur chacun de nos serveurs “barman”. Mais vu que nous n’avons installé que la base de Cellar, nous ne pouvons pas encore faire du DOSGI (Les prochaines versions inclurons directement la fonctionnalité). Mais les développeurs ont eu la bonne idée de rajouter une feature se nommant “cellar-dosgi”. Donc empressons nous de l’installer !

Mais avant il faut que le service soit rendu compatible avec cette fonctionnalité, ce n’est pas très compliqué cela se limite en une ligne lors de la définition blueprint du service :

<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
	<!-- recupération de l'ID du noeud dans le cluster -->
	<reference id="clusterManager" interface="org.apache.karaf.cellar.core.ClusterManager"/>
    <bean id="node" factory-ref="clusterManager" factory-method="getNode"/>
    <bean id="nodeId" factory-ref="node" factory-method="getId"/>

	<bean id="drinkService" class="fr.conceptit.tuto.drink.service.DrinkServiceImpl">
		<argument ref="nodeId"/>
	</bean>

	<!-- Exposition du service de gestion -->
	<service ref="drinkService" interface="fr.conceptit.tuto.drink.api.DrinkService">
		<service-properties>
			<entry key="service.exported.interfaces" value="*"/>
		</service-properties>
	</service>
</blueprint>

L’entrée “service.exported.interfaces” est utilisée pour rendre distribué le service. Et ça sera tout … rien d’autre à faire de ce côté !

Nous allons nous concentrer sur les instances appartenant au groupe “grp_bar”.
Sur chacune des instances installons cellar-dosgi de la façon classique :

karaf@barman_1> features:install cellar-dosgi
karaf@barman_2> features:install cellar-dosgi
Remarque : Pensez à mettre à jour la feature sur chacun des serveurs pour prendre en compte les derniers ajouts faits dans le fichier blueprint.

Pour vérifier que l’installation a bien fonctionné, nous allons utiliser une nouvelle commande apporté par cellar-dosgi permettant de voir les services distribués :

karaf@barman_2> cluster:service-list
Service Class                                   Provider Node
fr.conceptit.tuto.drink.service.DrinkService    tux.local:5702
                                                tux.local:5703

Nous pouvons donc observer que nos deux instances “barman_*” possède un service distribué identifié par l’interface “fr.conceptit.tuto.drink.service.DrinkService”.

A présent, si nous nous rendons sur l’instance “client” et que nous exécutons notre commande notre réponse sera celle attendue :

Capture du 2013-12-10 13:57:10

Petit plus

Pour une raison de traçabilité ou seulement par curiosité, il serait bon de savoir quel noeud nous a répondu. Il suffira de modifier 2 choses dans notre projet “service” afin de questionner directement le cluster.

  1. Récupérer le manager de cluster et lui demander l’ID du noeud
  2. Ajouter cet ID dans la réponse émise par le service.

Pour récupérer l’ID on va tout simplement utiliser l’IOC de blueprint en demandant la récupération du manager de Cellar. Cela va se faire via la configuration blueprint :

<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
	<!-- recupération de l'ID du noeud dans le cluster -->
	<reference id="clusterManager" interface="org.apache.karaf.cellar.core.ClusterManager"/>
    <bean id="node" factory-ref="clusterManager" factory-method="getNode"/>
    <bean id="nodeId" factory-ref="node" factory-method="getId"/>

	<bean id="drinkService" class="fr.conceptit.tuto.drink.service.DrinkServiceImpl">
		<argument ref="nodeId"/>
	</bean>

	<!-- Exposition du service de gestion -->
	<service ref="drinkService" interface="fr.conceptit.tuto.drink.api.DrinkService">
		<service-properties>
			<entry key="service.exported.interfaces" value="*"/>
		</service-properties>
	</service>
</blueprint>

Ligne 1 : Récupération du service “ClusterManager” via son interface.
Ligne 2 : Utilisation le bean de référence du ClusterManager comme une factory pour récupérer l’objet Node (qui définie le noeud en cours) via la méthode getNode.
Ligne 3 : Utilisation du Node comme une factory pour récupérer l’ID via la méthode getId de l’objet.

Pour l’injecter dans le service on ajoute dans la classe un constructeur avec un argument de type String et dans le blueprint on ajoute une injection par constructeur :

public class DrinkServiceImpl implements DrinkService {
	private static final String msg = "Your glass of {0} is served by {1};-)";
	private String nodeId;

	public DrinkServiceImpl(String nodeId) {
		this.nodeId = nodeId;
	}

	public String canIHaveADrinkPlease(String drink) {
		return MessageFormat.format(msg, drink, nodeId);
	}
}

Une fois cela fait, puis après compilation et redéploiement, on peut se rendre compte du travaille de la mise en cluster des services en faisant des appels successifs :
Capture du 2013-12-10 16:39:39

On voit bien que la réponse ne provient pas tout le temps du même noeud. Vous pouvez à présent faire le test de couper une instance serveur et de la relancer et voire le comportement émanant du DOSGi.

Conclusion

Nous avons pu voir ici quelques facilités offertes par karaf au niveau de la création d’instances filles et surtout sa capacité à distribuer des services au sein d’un cluster. Cette capacité à être monté en cluster permet à votre infrastructure à base de Karaf de répondre aux montées en charge ou aux coupures possibles d’un sous-réseau. Ainsi cela rend possible une continuité de service grandement appréciable dans le cadre d’une plateforme haute disponibilité comme dans le cas d’une intégration EDI partenaire ou clients.

2 comments

  1. eheb says:

    j’utilise karaf 3.0 et votre tutorial n’est pas compatible dès l’installaton de cellar :
    Error executing command: Could not start bundle mvn:org.apache.karaf.cellar/org.
    apache.karaf.cellar.bundle/2.3.2 in feature(s) cellar-bundle-2.3.2: Unresolved c
    onstraint in bundle org.apache.karaf.cellar.bundle [143]: Unable to resolve 143.
    0: missing requirement [143.0] osgi.wiring.package; (&(osgi.wiring.package=org.a
    pache.karaf.features)(version>=2.2.0)(!(version>=3.0.0)))

    et les commandes osgi: features: sont differentes
    Et le meme feature en karaf 3.0. n’existe pas ou pas trouvé
    avez-vous une maj de l’article pour karaf 3 ou quelques notes à me donner ?

    • Pierre-Yves says:

      Je viens de regarder brièvement et je ne suis pas parvenir à installer Cellar sur Karaf 3.0 même avec les bonnes commandes :
      feature:repo-add cellar -> permet d’ajouter la repo cellar pour la dernière version existante.
      feature:install cellar -> permet d’installer la feature cellar.

      Dans les documentation de Cellar, je ne vois pas mentionné Karaf 3.0. Ce qui me fait penser qu’il n’y a pas de compatibilité actuellement.

      Je vous conseillerai, pour des raisons de pratique, d’utiliser la version 2.3.3 de Karaf afin de suivre le tutorial au mieux.

      Bien sûre, je vais me renseigner sur l’utilisation de Cellar avec Karaf 3.0.

Leave a Reply

Your email address will not be published. Required fields are marked *