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.