Kubernetes – Déploiement de Solrcloud

Logo Kubernetes Solr

Dans le premier article de cette série dédiée à Kubernetes, nous avons installé un Cluster Kubernetes privé. Dans le second article, nous avons proposé une introduction à l’architecture d’un cluster Kubernetes et nous en avons présenté les principaux concepts. Nous sommes donc en mesure dans cet article de décrire le déploiement Statefulset de Solrcloud dans Kubernetes. Les points suivants vont être couverts :

  • Choix du positionnement des pods Zookeeper et Solr sur les différents serveurs
    • labels
    • pod affinity
  • Mise en place de pods (containers) avec persistance des données
    • Storage classe
    • Persistent volume
    • Service
    • Configmap
    • StatefulSet et volumeClaimTemplates
  • Redémarrage automatique des pods non fonctionnels
    • Liveness and Readiness Probes
  • Réservation et limites des ressources des pods Zookeeper et Solr
  • Scalabilité

Les pods Zookeeper et Solr embarqueront des exporter Prometheus tel que décrit dans l’article « Monitoring SolrCloud avec Prometheus et Grafana« . Nous serons en mesure de bien monitorer les Pods lors de nos tests.

Installation initiale

Pour rappel, le cluster Kubernetes à notre disposition est constitué de 1 Master et 3 Nodes. L’environnement SolrCloud que nous allons déployer sera dans un premier temps constitué de 3 serveurs Zookeeper et 2 serveurs Solr. Dans un second temps afin de tester la fonctionnalité de passage à l’échelle de Kubernetes nous ajouterons 2 serveurs Solr pour un total de 4.

Toutes les commandes kubectl sont exécutées sous le compte applicatif « k8s » d’un poste client.

Les commandes kubectl ci-après utilisent des fichiers yaml passés en paramètre. Pour obtenir ces fichiers, il faut cloner le repository git suivant : https://github.com/bejean/solrcloud_kubernetes.git

Choix de positionnement des pods

Nous évitons de déployer plusieurs pods Zookeeper sur un même node. Les 3 nodes sont donc utilisés pour Zookeeper. Par contre, nous déployons des serveurs Solr (d’abord 2 puis 4) sur 2 nodes uniquement. Pour cela nous affectons des labels aux pods. Le choix du nom du label et de la valeur associée est arbitraire et sert à mettre en place les règles de nodeAffinity.

Un premier label « target_zkensemble » positionné à « yes » sur les 3 nodes

$ kubectl label nodes k8s-worker-node1 target_zkensemble=yes 
$ kubectl label nodes k8s-worker-node2 target_zkensemble=yes
$ kubectl label nodes k8s-worker-node3 target_zkensemble=yes

Un second label « target_solr » positionné à « yes » sur 2 nodes uniquement

$ kubectl label nodes k8s-worker-node1 target_solrcloud=yes
$ kubectl label nodes k8s-worker-node2 target_solrcloud=yes

Création d’un NameSpace

Les déploiements de pods peuvent de faire dans des espaces nommés. Cela permet de déployer un même environnement plusieurs fois dans des espaces étanches (de développement, de test, de pre-production ou de production). Chaque déploiement peut se faire avec des particularités comme par exemple le nombre de serveurs, la taille des espaces de stockage, le nombre de CPU ou les limites mémoires.

Nous allons déployer notre environnement SolrCloud dans un espace nommé « solrcloud1 ». Pour cela nous créons un namespace.

$ kubectl create namespace solrcloud1

Mise en place du volume persistant local

Ceci nécessite la définition d’une storageclass et d’une définition de volume persistant.

  • Pour Zookeeper
$ kubectl create -f templates/zkensemble-storageclass.yaml
$ kubectl create -f templates/zkensemble-persistentVolume.yaml

zkensemble-storageclass.yaml

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: zkensemble-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

zkensemble-persistentVolume.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: zkensemble-vol1
spec:
  capacity:
    storage: 5Gi
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: zkensemble-storage
  local:
    path: /k8s-storage/zkensemble-vol
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: target_zkensemble
          operator: In 
          values:
          - "yes"

Le lien entre la StorageClass et le PersistentVolume se fait sur les propriétés « name » et « storageClassName ». la propriété « accessMode » positionnée à « ReadWriteOnce » indique qu’un seul pod peut lire et écrire dans le volume.

Les répertoires qui correspondent au « path » du PersistenteVolume doivent être créés sur les nodes cibles. La section « nodeAffinity » indique la sélection des nodes cibles sur la base des labels créés précédemment. Les volumes ne sont pas créés, ils sont mis en attente jusqu’au moment du déploiement des statefulset.

  • Pour Solr
$ kubectl create -f templates/solrcloud-storageclass.yaml
$ kubectl create -f templates/solrcloud-persistentVolume.yaml

Note : les fichiers « zkensemble-persistentVolume.yaml » et « solrcloud-persistentVolume.yaml » contiennent la déclaration de respectivement 3 et 4 volumes pour les différents pods qui seront créés lors du déploiement.

Les services

Les services sont en charge de permettre aux Pods d’un StatefulSet de communiquer entre eux et/ou de pouvoir être accessibles de l’extérieur du cluster. Permettre au Pods de communiquer entre eux est impératif pour les fonctionnement de Zookeeper et Solrcloud. Cette communication intra-culster est possible grâce à un service Headless. Chaque Pod est accessible au moyen d’un nom DNS « <POD_NAME>.<SERVICE_NAME>.default.svc.cluster.local » ou plus simplement « <POD_NAME>.<SERVICE_NAME> ».

Nous mettons dans un premier temps en place ces services afin que membres Zookeeper et les nœuds Solr puissent communiquer entre eux au sein du cluster. Un serveur Prometheus installer dans le cluster peut également collecter les information des exporter.

Un service headless se caractérise par la propriété « clusterIP » positionnée à « None ». Les déclarations des services headless pour Zookeeper et Solr sont les suivants :

zkensemble-service-headless.yaml

apiVersion: v1
kind: Service
metadata:
  name: zkensemble
  labels:
    app: zookeeper-app
spec:
  clusterIP: None
  ports:
  - name: server
    protocol: TCP
    port: 2888
    targetPort: 2888
  - name: leader-election
    protocol: TCP
    port: 3888
    targetPort: 3888
  - name: client
    protocol: TCP
    port: 2181
    targetPort: 2181
  - name: node-exporter
    protocol: TCP
    port: 9100
    targetPort: 9100
  - name: jmx-exporter
    protocol: TCP
    port: 8080
    targetPort: 8080
  - name: zk-exporter
    protocol: TCP
    port: 7080
    targetPort: 7080
  selector:
    app: zookeeper-app

solrcloud-service-headless.yaml

apiVersion: v1
kind: Service
metadata:
  name: solrcloud
  labels:
    app: solr-app
spec:
  clusterIP: None
  ports:
  - name: client
    protocol: TCP
    port: 8983
    targetPort: 8983
  - name: node-exporter
    protocol: TCP
    port: 9100
    targetPort: 9100
  - name: jmx-exporter
    protocol: TCP
    port: 8080
    targetPort: 8080
  - name: solr-exporter
    protocol: TCP
    port: 9854
    targetPort: 9854
  selector:
    app: solr-app

Ils sont créés ainsi :

$ kubectl create -f templates/zkensemble-headless-service.yaml
$ kubectl create -f templates/solrcloud-headless-service.yaml

Dans un second temps, si cela est nécessaire, il sera possible de rendre accessibles les Pods Solr et Zookeeper de l’extérieur du cluster au moyen d’un service de publication. Pour Zookeeper et Solr respectivement, les port 2181 et 8983 seront alors déclarés dans le service de publication et non plus dans le service headless.

zkensemble-service-publishing.yaml

apiVersion: v1
kind: Service
metadata:
  name: zk-service
  labels:
    app: zookeeper-app
spec:
  ports:
  - name: client
    protocol: TCP
    port: 2181
    targetPort: 2181
  type: LoadBalancer
  selector:
    app: zookeeper-app

solrcloud-service-publishing.yaml

apiVersion: v1
kind: Service
metadata:
  name: solr-service
  labels:
    app: solr-app
spec:
  ports:
  - name: client
    protocol: TCP
    port: 8983
    targetPort: 8983
  type: NodePort
  selector:
    app: solr-app

Configmap

Les ConfigMaps sont des fichiers de configuration. On y place toutes sortes de propriétés qui sont passées comme variables d’environnement aux containers qui sont créés lors du déploiement. Il peut s’agir par exemple des niveaux de log à activer, des chemins des répertoires de données et de logs, de la version des logiciels à installer, …

Les ConfigMap sont spécifiques à un déploiement. Ils sont donc associés au namespace que nous avons créé.

  • Pour Zookeeper
kubectl --namespace=solrcloud1 create configmap zkensemble-config --from-env-file=templates/zkensemble-configmap.properties 
  • Pour Solr
$ kubectl --namespace=solrcloud1 create configmap solrcloud-config --from-env-file=templates/solrcloud-configmap.properties 

StatefulSet

Le StatefulSet est la description des pods qui vont être déployés. Il contient la définition des éléments suivants :

  • Le nom du StatefulSet
  • Le nombre de réplicas
  • La règle de positionnement des pods sur les nodes
  • L’image Docker de base à utiliser pour les containers
  • La ou les commandes à exécuter dans le container lors de son démarrage
  • Les tests de bonne santé pour la recréation automatiquement d’un pod non fonctionnel
  • Les variables d’environnement
  • le volumeClaim pour la création des volumes persistants

Pour nos containers nous avons choisi de partir d’images Docker Centos 8 et non pas d’images existantes Zookeeper ou Solr. Dans les containers Centos 8 minimalistes, notre script de démarrage lance l’installation des différents pre-requis, récupère les scripts d’installation et les ressources de configuration dans Github, puis lance le script d’installation de Zookeeper ou de Solr et des exporters Prometheus.

Dans les pre-requis, nous avons openjdk11 et git, des utilitaires (wget), les pre-requis de Zookeeper ou de Solr (lsof, which) et différents outils de diagnostique ou de monitoring  (pmap, systat, netcat, …).

Les Statefulset sont également associés au namespace que nous avons créé.

Le déploiement des Statefulset n’est pas immédiat surtout lorsque des tests de bonne santés sont en place. Il faut donc bien attendre que les pods Zookeeper soient fonctionnels avant de lancer le déploiement du Statefulset des pods Solr.

  • Pour Zookeeper
$ kubectl --namespace=solrcloud1 create -f templates/zkensemble-statefulset.yaml
  • Pour Solr
$ kubectl --namespace=solrcloud1 create -f templates/solrcloud-statefulset.yaml

Les pods Zookeeper sont accessibles via les noms DNS « zk-ensemble-n.zkensemble.default.svc.cluster.local » et les pods Solr via les noms DNS « solrcloud-n.solrcloud.default.svc.cluster.local ». Le nom du Pod est constitué du nom du StatefulSet suivi d’un suffixe -1, -2, …, -n correspondant au numéro du replica dans le StatefulSet.

Réservation et limites des ressources

Dans la définition du StatefulSet, il est possible de définir les ressources requises et/ou maximum. Il s’agit du nombre de CPU et de la mémoire.

Dans un environnement privé (bare-metal) la CPU s’exprime en unité de millième d’hyperthread. 1 = 1000m = 1 hyperthread, 0.1 = 100m = 0.1 hyperthread. Par exemple afin de d’indiquer qu’un Pod Solr requière au minimum 2 CPUs et 64Mo de RAM pour démarrer et au maximum 4 CPUs et et 128Mo, on indique :

La mémoire s’exprime en octet avec les suffixes possibles E, P, T, G, M et K. 134217728, 131072K et 128M sont équivalents.

spec:
  containers:
  - name: solrcloud
    image: centos:8
    resources:
      requests:
        memory: "64M"
        cpu: "200m"
      limits:
        memory: "128M"
        cpu: "400m"

Redémarrage automatique des pods non fonctionnels

Un des intérêts de Kubernetes est de pouvoir détecter des anomalies sur des Pods et de les recréer automatiquement. Cela se fait au moyen de la déclaration de sondes (probe) du conteneur. Une Sonde est un diagnostic exécuté périodiquement par kubelet sur un conteneur. Il existe deux types de sondes les livenessProbe et les readinessProbe.

livenessProbe : Indique si le conteneur est en cours d’exécution. Si la liveness probe échoue, kubelet tue le conteneur et le conteneur est soumis à sa politique de redémarrage (restart policy). Si un Conteneur ne fournit pas de liveness probe, l’état par défaut est Success.

readinessProbe : Indique si le conteneur est prêt à servir des requêtes. Si la readiness probe échoue, le contrôleur de points de terminaison (Endpoints) retire l’adresse IP du Pod des points de terminaison de tous les Services correspondant au Pod. L’état par défaut avant le délai initial est Failure. Si le Conteneur ne fournit pas de readiness probe, l’état par défaut est Success.

Pour exécuter un diagnostic, kubelet appelle un Handler implémenté par le Conteneur. Trois techniques sont disponibles pour mettre en place une sonde :

  • Exec: Exécute la commande spécifiée à l’intérieur du conteneur. Le diagnostic est considéré réussi si la commande se termine avec un code de retour de 0.
  • TCPSocket: Exécute un contrôle TCP sur l’adresse IP du conteneur et sur un port spécifié. Le diagnostic est considéré réussi si le port est ouvert.
  • HTTPGet: Exécute une requête HTTP Get sur l’adresse IP du conteneur et sur un port et un chemin spécifiés. Le diagnostic est considéré réussi si la réponse fournit un code de retour supérieur ou égal à 200 et inférieur à 400.

Pour Zookeeper, nous avons implémenté les probes au moyen de l’exécution d’un appel netcat sur les commandes Four Letter Words « runok » et « imok ».

Pour Solr, nous avons implémenté les probes au moyen d’appels HTTP sur les endpoint /solr/admin/info/health et /solr/admin/info/system.

Pour un Pod Solr, si une erreur OutOfMemory se produit, le POD est redémarré automatiquement. L’analyse des logs de Solr et de la JVM permet de déterminer la cause du problème et d’adapter la configuration du Pod.

Scalabilité

Un autre intérêt de Kubernetes est la possibilité de déployer rapidement des Pods complémentaires lors d’une augmentation permanente ou temporaire des volumes de données ou de requêtes. Après avoir ajouté (si nécessaire) un ou plusieurs serveurs Node et de leur avoir assigné un label pour la Node affinity, il suffit de modifier le paramètre « replicas » dans la définition du StatefulSet SolrCloud et de le redéployer

export NS="solrcloud1"
kubectl --namespace=$NS create -f templates/solrcloud-statefulset.yaml

Il faut ensuite modifier le sharding et la replication des collections Solr au moyen de la Collection API.

Synthèse de l’installation

L’installation se déroule donc successivement pour Zookeeper et Solr en suivant les étapes suivantes :

  • Positionnement des labels sur les nodes ciblés par l’installation (nodeaffinity)
  • Déclaration du stockage persistant
  • Création du service Headless
  • Création du ConfigMap
  • Création du StatefulSet (les Pods)

Le script « deploy.sh » fourni dans le repository Github automatise ce traitement :

#!/bin/bash

NS="solrcloud1"

# Namespace
kubectl create namespace $NS

#
# Zookeeper
#

# Node Affinity Label
kubectl label nodes k8s-worker-node1 target_zkensemble=yes 
kubectl label nodes k8s-worker-node2 target_zkensemble=yes
kubectl label nodes k8s-worker-node3 target_zkensemble=yes

# Persistente Storage
kubectl create -f templates/zkensemble-storageclass.yaml
kubectl create -f templates/zkensemble-persistentVolume.yaml

# Headless Service
kubectl create -f templates/zkensemble-service-headless.yaml

# ConfigMap & StatefulSet in target Namespace
kubectl --namespace=$NS create configmap zkensemble-config --from-env-file=templates/zkensemble-configmap.properties 
kubectl --namespace=$NS create -f templates/zkensemble-statefulset.yaml

#
# SolrCloud
#

# Node Affinity Label
kubectl label nodes k8s-worker-node1 target_solrcloud=yes
kubectl label nodes k8s-worker-node2 target_solrcloud=yes
kubectl label nodes k8s-worker-node3 target_solrcloud=yes

# Persistente Storage
kubectl create -f templates/solrcloud-storageclass.yaml
kubectl create -f templates/solrcloud-persistentVolume.yaml

# Headless Service
kubectl create -f templates/solrcloud-service-headless.yalm

# ConfigMap & StatefulSet in target Namespace
kubectl --namespace=$NS create configmap solrcloud-config --from-env-file=templates/solrcloud-configmap.properties 
kubectl --namespace=$NS create -f templates/solrcloud-statefulset.yaml

Conclusion

A l’issue de cette série de trois articles sur Kubernetes, nous disposons d’un cluster afin de valider l’optimisation de Solrcloud dans cet environnement et de bien comprendre l’usage et le partage des ressources entre les Pods. Nous relaterons notre expérience dans de futurs articles.