6 juillet 2018 | Kubernetes | Arnaud Tournier

Comment déployer un cluster JBoss hautement disponible sur Kubernetes - partie 1

Temps de lecture estimé : 11 minutes

Il y a des applications vraiment faciles à mettre à l’échelle sur Kubernetes, comme ces serveurs Web très simples qui ne font que se connecter à des bases de données répliquées ou des services externes puis qui ajoutent une fine couche de traitement métier.

Cependant parfois, certains applicatifs sont plus difficiles. Nous allons explorer le déploiement d’un cluster JBoss sur Kubernetes. Nous configurerons notre déploiement afin de bénéficier des fonctionalités de clustering de JBoss (état partagé, distribution des EJB…) et des avantages liés à Kubernetes (élasticité du cluster, auto-healing, monitoring etc).

Grâce à cela, nous obtiendrons un cluster JBoss HA élastique sur Kubernetes. Ceci est parfait pour porter votre applicatif Java EE sur le cloud (qu’il soit public, privé ou même hybride !).

Note : cet article se focalise sur le déploiement d’un cluster JBoss, la même technique s’applique au déploiement d’un cluster Keycloak (qui était le sujet original de mon travail).

Comment fonctionne un cluster JBoss ?

Un cluster est un groupe d’ordinateurs (nœuds) servant la même application. Le logiciel exécuté sur ces nœuds doit coordonner les charges utiles et l’état des données. Pour bénéficier de l’élasticité, l’application doit être plus rapide ou prendre en charge davantage d’utilisateurs en ajoutant simplement de nouveaux nœuds dans le cluster. C’est là que les difficultés arrivent : à mesure que de nouveaux nœuds sont ajoutés au cluster, la coordination entre les nœuds nécessite de plus en plus d’opérations, ce qui réduit l’avantage global de l’ajout de nouveaux serveurs. On peut généralement s’attendre à une progression de performance linéaire ou asymptotique en fonction du nombre de nœuds dans le cluster.

Ajoutez à cela que la coordination dans un cluster est très complexe, il n’est pas surprenant que de plus en plus de développeurs utilisent des services managés (gérant les problèmes de partage et de coordination d’état) et implémentent leurs applications en couche fine au-dessus. Cela leur permet de s’abstraire complètement de la coordination et du partage d’état.

Les clusters JBoss sont en quelque sorte intermédiaires dans la mesure où ils fournissent des services distribués de haut niveau prêts à l’emploi (sessions partagées sur le cluster, cache db distribué, distribution EJB, HA singleton, transactions distribuées, etc.).

En ce qui concerne le clustering, JBoss Application Server peut fonctionner dans trois modes :

  • Mode autonome: le JBoss AS fonctionne comme le seul nœud servant l’application. Aucun regroupement du tout dans ce mode.
  • Mode HA autonome: les nœuds AS sont connectés et partagent la charge de travail de l’application. Chaque nœud a sa propre configuration, qui doit être cohérente avec les autres nœuds du cluster.
  • Mode domaine: les nœuds AS sont connectés et une instance centrale gère la configuration pour tous les autres nœuds qui sont alors des esclaves.

Vous pouvez lire des informations détaillées sur le fonctionnement d’un cluster JBoss dans la documentation officielle.

En termes simples, JBoss AS fournit des fonctionnalités de cluster de haut niveau en s’appuyant sur le cache de valeurs-clés en mémoire Infinispan distribué.

À son tour, Infinispan repose sur JGroups pour la découverte des noeuds et le transport de messages dans le cluster.

JGroups a pour mission de :

  • découvrir les noeuds du cluster : ce qui signifie qu’il doit savoir à tout moment quels sont les noeuds actifs dans le cluster.
  • transporter et acheminer les messages : ce qui signifie qu’il doit choisir et utiliser un protocole de communication sous-jacent.

JGroups est très configurable et utilise une pile de protocoles par défaut, qui peut être personnalisée en fonction de besoins spécifiques (c’est ce que nous allons faire!).

Par défaut, la pile de protocole JGroups est composée de:

  • UDP multicast pour le transport de messages. Lorsque le réseau prend en charge ce protocole, les messages sont diffusés sur tous les nœuds en même temps (multicast), ce qui accélère la communication.
  • PING multicast pour la découverte de noeud. La configuration par défaut de JGroups suppose que les nœuds peuvent diffuser des requêtes ping pour découvrir les nœuds en cours d’exécution.
  • d’autres protocoles utilisés pour réconcilier les clusters scindés (en raison de partitions réseau) et d’autres choses comme ça. Nous n’entrerons pas dans les détails de cette partie de la pile car ce sont les deux premiers niveau qui ont besoin de configuration lors de l’exécution d’un cluster JBoss AS sur Kubernetes sur GCP.

Donc, pour résumer ce que nous avons dit jusqu’à maintenant:

  • JBoss AS dispose d’une fonctionnalité de clustering qui vous permet de dimensionner votre application en ajoutant de nouveaux nœuds dans votre cluster,
  • Il utilise Infinispan pour fournir des fonctionnalités de cluster, qui à son tour utilise JGroups pour établir l’appartenance au cluster et le transport des messages,
  • La pile standard utilise UDP multicast pour le transport et la découverte de noeud,
  • Le problème est que ce type de protocole de réseau (UDP multicast) n’est pas toujours disponible sur la plate-forme de cloud public (à titre d’exemple, il n’est pas disponible sur GCP).
  • Cela nous oblige à personnaliser la pile réseau JGroups si nous voulons qu’un cluster JBoss AS fonctionne sur une plateforme GCP K8S.

Le cluster JBoss AS est capable de fournir un équilibreur de charge frontal pour répartir la charge de l’application de manière homogène entre les nœuds. Nous désactiverons cette fonctionnalité car elle est fournie par Kubernetes sous la forme de Services.

Une première solution

Dans cette première approche, nous utiliserons JDBC_PING pour la découverte des noeuds, et utiliserons TCP à la place de l’UDP multicast pour le transport des messages entre les noeuds du cluster.

C’est une solution très simple, pas optimale, mais facile à utiliser et qui fonctionne ! Nous allons dans un prochain article explorer deux autres (et meilleurs) moyens de le faire.

Avantages : JDBC_PING est inclus par défaut dans toutes les distributions de JBoss (donc la configuration est vraiment facile).

Inconvénients: vous devez déployer une base de données SQL (si vous avez déjà une source de données JEE, vous pouvez la réutiliser).

L’idée est que, au lieu d’utiliser l’UDP multicast pour la découverte et l’appartenance des nœuds (c’est-à-dire répondre à la question: “quelles sont les IP des nœuds que nous voulons dans le cluster?”), JDBC_PING utilisera une table SQL dans laquelle chaque nœud publiera son adresse IP et récupérera les adresses IP des autres noeuds.

Déploiement d’un serveur SQL

Tout serveur SQL avec une connexion JDBC peut être utilisé. Dans cet exemple, nous utiliserons un serveur Postgres, mais la même technique fonctionnera avec MySQL, MariaDB, SQL Server, ou autre.

Déployez un serveur SQL (nous prenons un serveur Postgres dans cet exemple) qui devrait être accessible depuis les nœuds de votre cluster, que ce soit un service managé ou non.

Créez un utilisateur sur ce serveur pour les nœuds JBoss AS exécutés sur k8s afin de publier et d’accéder à la table contenant les informations sur les nœuds.

Configuration de JBoss AS

Le fichier de configuration du cluster est standalone-ha.xml, dans votre distribution JBoss.

Nous allons commencer par le fichier standalone-ha.xml, voici les changements nécessaires :

S’assurer que les extensions Infinispan et JGroups sont chargés

Assurez vous d’avoir ces deux lignes au début du fichier :

<extension module="org.jboss.as.clustering.infinispan"/>
<extension module="org.jboss.as.clustering.jgroups"/>

Assurez vous également de retirer ou commenter cette ligne, car nous n’utiliserons pas l’extension modcluster (qui utilise un serveur web Apache comme load balancer).

<!-- <extension module="org.jboss.as.modcluster"/> -->

De plus, si vous avez un noeud <subsystem xmlns="urn:jboss:domain:modcluster:3.0"> dans votre configuration, vous pouvez simplement l’effacer.

Activer les logs (facultatif)

Si vous désirez être innondés de logs et voir tout ce qui se passe dans les mécanismes internes de jgroups, et d’infinispan, vous pouvez ajouter ceci :

<logger category="org.jgroups">
  <level name="TRACE"/>
</logger>
<logger category="org.jboss.as.clustering">
  <level name="TRACE"/>
</logger>
<logger category="org.infinispan">
  <level name="TRACE"/>
</logger>

Ajouter une datasource JDBC

Dans mon exemple, nous utilisons une base de données Postgres, voilà la configuration :

<datasource jndi-name="java:jboss/datasources/ClusteringDS" pool-name="ClusteringDS" enabled="true" use-java-context="true" use-ccm="true">
    <connection-url>jdbc:postgresql://${env.POSTGRES_DB_HOST}:${env.POSTGRES_DB_PORT}/jbossclustering</connection-url>
    <driver>postgresql</driver>
    <security>
        <user-name>${env.DB_USER}</user-name>
        <password>${env.DB_PASSWORD}</password>
    </security>
    <validation>
        <check-valid-connection-sql>SELECT 1</check-valid-connection-sql>
        <background-validation>true</background-validation>
        <background-validation-millis>60000</background-validation-millis>
    </validation>
</datasource>

Comme vous pouvez le constater, il n’y a rien de spécial ici : l’url de connexion à la base, le driver JDBC utilisé et les crédentials pour se connecter à la base.

Comme vous l’avez probablement deviné, jbossclustering est le nom de la base de donnée que nous utilisons pour partager les informations des noeuds.

Configurer le sous-système JGroups

<subsystem xmlns="urn:jboss:domain:jgroups:5.0">
    <!-- ici le nom du cluster, qui permet de discriminer les noeuds dans le cas où on déploie plusieurs clusters -->
    <channels default="jboss-cluster-ee">
        <channel name="jboss-cluster-ee" stack="tcp"/>
    </channels>

    <!-- voici la spécification de la pile de protocole de JGroups -->
    <stacks>
        <!-- on utilise tcp et pas udp -->
        <stack name="tcp">
            <transport type="TCP" socket-binding="jgroups-tcp">
                <!-- notez que nous avons besoin que jgroups publie les IP des noeuds et non leur nom (car on aurait le nom du pod et non du noeud) -->
                <property name="external_addr">${env.HOST_IP}</property>
                <property name="use_ip_addrs">true</property>
            </transport>
            <!-- ici nous spécifions que nous utilisons JDBC_PING comme mécanisme de découverte -->
            <protocol type="org.jgroups.protocols.JDBC_PING">
                <!-- nous utilisons la datasource configurée préalablement -->
                <property name="datasource_jndi_name">java:jboss/datasources/ClusteringDS</property>

                <!-- ici on configure les statements SQL pour Postgres, vous pouvez les traduire selon votre base de données ! -->
                <property name="initialize_sql">
                    CREATE TABLE IF NOT EXISTS JGROUPSPING (
                        own_addr varchar(200) NOT NULL,
                        bind_addr varchar(200) NOT NULL,
                        created timestamp NOT NULL,
                        cluster_name varchar(200) NOT NULL,
                        ping_data BYTEA,
                        constraint PK_JGROUPSPING PRIMARY KEY (own_addr, cluster_name)
                    )
                </property>
                <property name="insert_single_sql">
                    INSERT INTO JGROUPSPING (own_addr, bind_addr, created, cluster_name, ping_data) values (?,'${env.HOST_IP}',NOW(), ?, ?)
                </property>
                <property name="delete_single_sql">
                    DELETE FROM JGROUPSPING WHERE own_addr=? AND cluster_name=?
                </property>
                <property name="select_all_pingdata_sql">
                    SELECT ping_data FROM JGROUPSPING WHERE cluster_name=?
                </property>
            </protocol>
            <protocol type="MERGE3"/>
            <protocol type="FD_SOCK"/>
            <protocol type="FD_ALL"/>
            <protocol type="VERIFY_SUSPECT"/>
            <protocol type="pbcast.NAKACK2"/>
            <protocol type="UNICAST3"/>
            <protocol type="pbcast.STABLE"/>
            <protocol type="pbcast.GMS">
                <property name="print_physical_addrs">true</property>
                <property name="print_local_addr">true</property>
            </protocol>
            <protocol type="MFC"/>
            <protocol type="FRAG2"/>
        </stack>
    </stacks>
</subsystem>

La variable d’environnement HOST_IP contient l’IP du pod sur lequel l’instance JBoss est déployée. Elle est provisionnée par Kubernetes grâce à cette configuration du déploiement :

env:
- name: HOST_IP
    valueFrom:
    fieldRef:
        fieldPath: status.podIP

Ensuite il faut configurer la socket jgroups-binding utilisée pour la communication inter-noeuds (vous avez peut-être noté que nous y avons fait référence dans la configuration de JGroups). Je vais utiliser ici l’interface réseau publique de JBoss (public) car les noeuds sont protégés de l’extérieur par le cluster Kubernetes.

<socket-binding name="jgroups-tcp" interface="public" port="7600"/>

Déployer vos instances JBoss AS

Ensuite, vous devez packager JBoss AS, son fichier de configuration (standalone-ha.xml) et vos binaires applicatifs dans une image Docker et déployez la avec Kubernetes (n’oubliez pas de lancer JBoss en mode standalone HA en utilisant standalone-ha.xml en lieu et place de standalone.xml).

Voic les parties principales de mon déploiement :

apiVersion: extensions/v1beta1
kind: Deployment
spec:
  # yeah we can deploy several instances of our JBoss application
  replicas: 3
  template:
    spec:
      containers:
      - name: 'my-jboss-as-application'
        image: my-jboss-as-application:latest
        # don't forget probes please, maybe I will write an article on that topic...
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
            scheme: HTTP
        readinessProbe:
          failureThreshold: 3
          httpGet:
            path: /ready
            port: 800
            scheme: HTTP
        env:
        - name: HOST_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP
        - name: POSTGRES_DB_HOST
          value: "127.0.0.1"
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: db-user-credentials
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-user-credentials
              key: password

Et voilà, vous avez un cluster JBoss hautement disponible et élastique !

Vous pouvez le scaler facilement comme ceci :

kubectl scale deployment my-jboss-application --replicas 5

Deux noeuds seront ajoutés à votre déploiement, ils vont ensuite alimenter la table SQL et trouver leurs petits copains du cluster (n’oubliez pas de configurer les health et liveness probes, c’est très très important).

Vous pouvez même configurer le horizontal pod autoscaler et le cluster autoscaler pour déployer de nouvelles instances JBoss AS quand le traffic vers votre application croit de manière significative et les détruire lorsque la charge rediminue. Cela est vraiment plaisant et c’est quelque chose que vous n’avez pas avec JBoss sur étagère !

C’est une combinaison gagnant-gagnant entre JBoss et Kubernetes !

Conclusion and follow up

Nous avons configuré JGroups, la bibliothèque de communication sous-jacente utilisée par les fonctionnalités de clustering de JBoss AS:

  • utilisation de JDBC PING pour la découverte de nœuds (utilise une table SQL pour gérer les informations de nœuds de cluster).
  • utilisation TCP au lieu de UDP multicast (parce que UDP multicast n’est pas disponible sur GCP)
  • désactivation du LoadBalancer JBoss avec mod_cluster (nous allons accéder au cluster JBoss via le service K8S, donc nous n’en avons pas vraiment besoin)

Ce n’était pas vraiment compliqué, juste quelques piles logicielles à traverser ! Pour que cela fonctionne, j’ai dû lire beaucoup de docs et de code source, et donc je vous l’offre dans l’espoir que ce sera plus facile pour vous que pour moi.

Dans la prochaine partie de cette série, nous présenterons deux autres solutions:

  • utiliser l’API K8s pour obtenir les nœuds du cluster (en utilisant KUBE_PING)
  • utiliser un service interne ad-hoc (ou etcd?) pour implémenter le service de découverte de noeud

Vos commentaires (positifs, négatifs et neutres) sont les bienvenus ! N’hésitez pas à en laisser un si vous rencontrer des difficultées.

comments powered by Disqus