Clustering Tomcat - Kubernetes

Cette page est en complément de Clustering Tomcat. Elle apporte plus de détails sur la configuration de clusters sur Kubernetes.

Les configurations ont été testée avec Minikube.

Déploiement simple

Dans un premier temps, on va partir d’un déploiement simple, avec une seule instance de Tomcat, dans sa configuration par défaut.

clustering k8s simple

On commence à configurer un Ingress en frontal, qui renverra les requêtes sur le service tomcat0 qu’on devra configurer ensuite.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress
spec:
  ingressClassName: nginx
  tls:
  - secretName: tls-key
  rules:
  - host: tomcat.jtips.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: tomcat0
            port:
              number: 8080

Pour le service, on lui associe un déploiement qui démarre un conteneur de l’image officielle de Tomcat.

apiVersion: v1
kind: Service
metadata:
  name: tomcat0
  labels:
    component: tomcat
spec:
  clusterIP: None
  selector:
    app: tomcat
  ports:
  - port: 8080
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat
spec:
  selector:
    matchLabels:
      app: tomcat
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
      - name: tomcat
        image: tomcat:11
        ports:
        - name: http
          containerPort: 8080
          protocol: TCP

Configuration personnalisée

Le but est de tester les différentes configurations de cluster pour Tomcat. Kubernetes n’est que le support de ces tests.

Pour la suite, je vais utiliser une image construite localement, avec un fichier server.xml spécifique.

FROM tomcat:11

ARG SERVER_XML_FILE=server-standalone.xml

COPY conf/$SERVER_XML_FILE conf/server.xml
COPY msg.war webapps/msg.war

ENV CATALINA_OPTS="-XX:MaxRAMPercentage=75"

EXPOSE 8080 4000

Avec ce fichier Dockerfile, je prépare une série de fichier de configuration pour Tomcat et construire plusieurs images.

minikube image build --tag jtips/tomcat ./tomcat
minikube image build --tag jtips/tomcat:k8s --build-opt="build-arg=SERVER_XML_FILE=server-k8s.xml" ./tomcat
...

Découverte en multicast

clustering k8s multicast

Commençons par la configuration par défaut du clustering pour Tomcat. On ajoute un élément <Cluster> vide dans <Engine>.

<!-- conf/server-simple.xml --->
<Server port="-1">
  ...
  <Engine name="Catalina" defaultHost="localhost">
    <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
    ...
  </Engine>
  ...
</Server>
minikube image build --tag jtips/tomcat:simple --build-opt="build-arg=SERVER_XML_FILE=server-simple.xml" ./tomcat

On reconfigure le déploiement pour qu’il utilise notre image, avec plusieurs replicas.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat
spec:
  selector:
    matchLabels:
      app: tomcat
  replicas: 2
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
      - name: tomcat
        image: jtips/tomcat:simple
        ports:
        - name: http
          containerPort: 8080
          protocol: TCP

Cette configuration fonctionne mal. On peut utiliser l’application, mais les sessions ne sont pas synchronisées entre les instances.

Ça s’explique par le fait que dans cette configuration, les instances se découvrent via des messages multicast, et que Kubernetes ne le supporte pas.

Découverte en DNS

clustering k8s dns

Les instances ont leurs propres adresses IP, mais partage un nom DNS commun, qui est le nom du service. Tomcat peut exploiter ça en remplaçant l’élément de <Membership> par défaut par le CloudMembershipService.

<!-- conf/server-dns.xml --->
<Server port="-1">
  ...
  <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
    <Channel className="org.apache.catalina.tribes.group.GroupChannel">
      <Membership className="org.apache.catalina.tribes.membership.cloud.CloudMembershipService"
                  membershipProviderClassName="org.apache.catalina.tribes.membership.cloud.DNSMembershipProvider"/>
    </Channel>
  </Cluster>
  ...
</Server>

Dans l’extrait ci-dessus, l’attribut membershipProviderClassName n’est pas nécessaire car DNSMembershipProvider est la valeur par défaut.

minikube image build --tag jtips/tomcat:dns --build-opt="build-arg=SERVER_XML_FILE=server-dns.xml" ./tomcat

Pour configurer le provider, ça passe par des variables d’environnement. DNS_MEMBERSHIP_SERVICE_NAME est utilisée pour rechercher les instances par leur nom de service. Sans cette variable, il utilise le nom tomcat.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat
spec:
  selector:
    matchLabels:
      app: tomcat
  replicas: 2
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      containers:
      - name: tomcat
        image: jtips/tomcat:dns
        imagePullPolicy: Never
        ports:
        - name: http
          containerPort: 8080
          protocol: TCP
        - name: cluster
          containerPort: 4000
          protocol: TCP
        env:
        - name: DNS_MEMBERSHIP_SERVICE_NAME
          value: tomcat0

Avec cette configuration, la synchronisation de sessions fonctionne, et l’utilisateur retrouve la même session quelle que soit l’instance sur laquelle le répartiteur l’oriente.

Découverte par l’API de Kubernetes

clustering k8s api

En passant l’attribut membershipProviderClassName à KubernetesMembershipProvider, Tomcat utilise l’API de Kubernetes pour découvrir les autres membres. Ça apporte plus de souplesse que la découverte par DNS. En les cherchant par un label, les instances de Tomcat peuvent être dans des services différents. Par contre, elles doivent être dans le même namespace.

<!-- conf/server-dns.xml --->
<Server port="-1">
  ...
  <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
    <Channel className="org.apache.catalina.tribes.group.GroupChannel">
      <Membership className="org.apache.catalina.tribes.membership.cloud.CloudMembershipService"
                  membershipProviderClassName="org.apache.catalina.tribes.membership.cloud.KubernetesMembershipProvider"/>
    </Channel>
  </Cluster>
  ...
</Server>
minikube image build --tag jtips/tomcat:k8s --build-opt="build-arg=SERVER_XML_FILE=server-k8s.xml" ./tomcat

Pour configurer le provider, ça passe à nouveau par des variables d’environnement.

  • KUBERNETES_NAMESPACE indique le namespace, sa valeur par défaut est tomcat.

  • KUBERNETES_LABELS indique le filtre sur les labels.

Par ailleurs, pour que Tomcat puisse accéder à l’API, il faut que son pod ait un compte de service (serviceAccountName) avec les bons droits.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tomcat
spec:
  selector:
    matchLabels:
      app: tomcat
  replicas: 2
  template:
    metadata:
      labels:
        app: tomcat
    spec:
      serviceAccountName: tomcat
      containers:
      - name: tomcat
        image: jtips/tomcat:k8s
        imagePullPolicy: Never
        ports:
        - name: http
          containerPort: 8080
          protocol: TCP
        - name: cluster
          containerPort: 4000
          protocol: TCP
        env:
        - name: KUBERNETES_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        - name: KUBERNETES_LABELS
          value: "app=tomcat"

Le compte de service est défini à part et associé à un rôle qui lui donne des droits en lecture sur l’API.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: tomcat
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tomcat-role
  namespace: default
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["endpoints"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tomcat-rolebinding
  namespace: default
subjects:
- kind: ServiceAccount
  name: tomcat
  namespace: default
roleRef:
  kind: Role
  name: tomcat-role
  apiGroup: rbac.authorization.k8s.io