Cinq ratés lors du déploiement de la première application sur Kubernetes

Cinq ratés lors du déploiement de la première application sur KubernetesÉchec par Aris Dreamer

Beaucoup de gens pensent qu'il suffit de transférer l'application vers Kubernetes (soit en utilisant Helm, soit manuellement) - et ce sera le bonheur. Mais tout n'est pas si simple.

Équipe Solutions Cloud Mail.ru traduit un article de l'ingénieur DevOps Julian Gindy. Il raconte les pièges auxquels son entreprise a été confrontée lors du processus de migration afin que vous ne marchiez pas sur le même râteau.

Première étape : configurer les demandes et les limites de pod

Commençons par configurer un environnement propre dans lequel nos pods fonctionneront. Kubernetes est excellent pour la planification et le basculement des pods. Mais il s'est avéré que le planificateur ne peut parfois pas placer un pod s'il est difficile d'estimer le nombre de ressources dont il a besoin pour fonctionner correctement. C'est là que les demandes de ressources et de limites apparaissent. Il y a beaucoup de débats sur la meilleure approche pour définir les demandes et les limites. Parfois, il semble que ce soit vraiment plus un art qu'une science. Voici notre approche.

Requêtes de pod est la valeur principale utilisée par le planificateur pour placer le pod de manière optimale.

De Documentation Kubernetes : L'étape de filtrage définit un ensemble de nœuds où un pod peut être planifié. Par exemple, le filtre PodFitsResources vérifie si un nœud dispose de suffisamment de ressources pour satisfaire les demandes de ressources spécifiques d'un pod.

Nous utilisons les demandes d'application de manière à pouvoir estimer le nombre de ressources en fait L'application en a besoin pour fonctionner correctement. De cette façon, le planificateur peut placer les nœuds de manière réaliste. Au départ, nous voulions sur-planifier les demandes pour garantir suffisamment de ressources pour chaque pod, mais nous avons remarqué que le temps de planification augmentait considérablement et que certains pods n'étaient pas entièrement planifiés, comme s'il n'y avait aucune demande de ressources pour eux.

Dans ce cas, le planificateur "éliminait" souvent les pods et ne pouvait pas les replanifier car le plan de contrôle n'avait aucune idée de la quantité de ressources dont l'application aurait besoin, ce qui est un élément clé de l'algorithme de planification.

Limites des pods est une limite plus claire pour un pod. Il représente la quantité maximale de ressources que le cluster allouera au conteneur.

Encore une fois, de documents officiels : Si un conteneur a une limite de mémoire de 4 Gio, le kubelet (et l'environnement d'exécution du conteneur) l'appliquera. Le runtime empêche le conteneur d'utiliser plus que la limite de ressources spécifiée. Par exemple, lorsqu'un processus dans un conteneur essaie d'utiliser plus que la quantité de mémoire autorisée, le noyau du système termine le processus avec une erreur « mémoire insuffisante » (OOM).

Un conteneur peut toujours utiliser plus de ressources que la demande de ressources spécifiée, mais il ne peut jamais utiliser plus que la limite. Cette valeur est difficile à définir correctement, mais elle est très importante.

Idéalement, nous souhaitons que les besoins en ressources d'un pod changent au cours du cycle de vie d'un processus sans interférer avec les autres processus du système - c'est le but de la définition des limites.

Malheureusement, je ne peux pas donner d'instructions précises sur les valeurs à définir, mais nous adhérons nous-mêmes aux règles suivantes :

  1. À l'aide d'un outil de test de charge, nous simulons un niveau de trafic de base et observons l'utilisation des ressources du pod (mémoire et processeur).
  2. Définissez les demandes de pod sur une valeur arbitrairement basse (avec une limite de ressources d'environ 5 fois la valeur des demandes) et observez. Lorsque les demandes sont à un niveau trop bas, le processus ne peut pas démarrer, ce qui provoque souvent des erreurs d'exécution Go cryptiques.

Je note que des limites de ressources plus élevées rendent la planification plus difficile car le pod a besoin d'un nœud cible avec suffisamment de ressources disponibles.

Imaginez une situation où vous avez un serveur Web léger avec une limite de ressources très élevée, comme 4 Go de mémoire. Ce processus devra probablement être mis à l'échelle horizontalement, et chaque nouveau pod devra être planifié sur un nœud avec au moins 4 Go de mémoire disponible. Si aucun nœud de ce type n'existe, le cluster doit introduire un nouveau nœud pour traiter ce pod, ce qui peut prendre un certain temps. Il est important d'obtenir une différence minimale entre les demandes de ressources et les limites pour garantir une mise à l'échelle rapide et fluide.

Deuxième étape : Configurer les tests de vivacité et de préparation

C'est un autre sujet subtil qui est souvent discuté dans la communauté Kubernetes. Il est important de bien comprendre les tests de vivacité et de préparation car ils fournissent un mécanisme pour un fonctionnement stable du logiciel et minimisent les temps d'arrêt. Cependant, ils peuvent sérieusement affecter les performances de votre application s'ils ne sont pas configurés correctement. Vous trouverez ci-dessous un résumé de ce que sont les deux échantillons.

Vivacité indique si le conteneur est en cours d'exécution. En cas d'échec, le kubelet tue le conteneur et la politique de redémarrage est activée pour celui-ci. Si le conteneur n'est pas équipé d'une sonde de vivacité, l'état par défaut sera succès - comme indiqué dans Documentation Kubernetes.

Les sondes de vivacité doivent être bon marché, c'est-à-dire qu'elles ne consomment pas beaucoup de ressources, car elles s'exécutent fréquemment et doivent informer Kubernetes que l'application est en cours d'exécution.

Si vous définissez l'option pour qu'elle s'exécute toutes les secondes, cela ajoutera 1 requête par seconde. Sachez donc que des ressources supplémentaires seront nécessaires pour traiter ce trafic.

Dans notre entreprise, les tests Liveness testent les composants de base d'une application, même si les données (par exemple, à partir d'une base de données ou d'un cache distant) ne sont pas entièrement disponibles.

Nous avons configuré un point de terminaison "santé" dans les applications qui renvoie simplement un code de réponse 200. Cela indique que le processus est en cours d'exécution et capable de gérer les demandes (mais pas encore le trafic).

Échantillon Préparation indique si le conteneur est prêt à répondre aux requêtes. Si la vérification de l'état de préparation échoue, le contrôleur de point de terminaison supprime l'adresse IP du pod des points de terminaison de tous les services correspondant au pod. Ceci est également indiqué dans la documentation de Kubernetes.

Les sondes de préparation consomment plus de ressources, car elles doivent atteindre le backend de manière à montrer que l'application est prête à accepter les requêtes.

Il y a beaucoup de débats dans la communauté sur l'opportunité d'accéder directement à la base de données. Compte tenu de la surcharge (les vérifications sont fréquentes, mais elles peuvent être contrôlées), nous avons décidé que pour certaines applications, la disponibilité à servir le trafic n'est prise en compte qu'après avoir vérifié que les enregistrements sont renvoyés de la base de données. Des essais de préparation bien conçus ont assuré des niveaux de disponibilité plus élevés et éliminé les temps d'arrêt pendant le déploiement.

Si vous décidez d'interroger la base de données pour tester l'état de préparation de votre application, assurez-vous qu'elle est la moins chère possible. Prenons cette requête :

SELECT small_item FROM table LIMIT 1

Voici un exemple de la façon dont nous configurons ces deux valeurs dans Kubernetes :

livenessProbe: 
 httpGet:   
   path: /api/liveness    
   port: http 
readinessProbe:  
 httpGet:    
   path: /api/readiness    
   port: http  periodSeconds: 2

Vous pouvez ajouter quelques options de configuration supplémentaires :

  • initialDelaySeconds - combien de secondes s'écouleront entre le lancement du conteneur et le début du lancement des sondes.
  • periodSeconds — intervalle d'attente entre les exécutions d'échantillons.
  • timeoutSeconds — le nombre de secondes après lesquelles la nacelle est considérée comme étant en situation d'urgence. Délai normal.
  • failureThreshold est le nombre d'échecs de test avant qu'un signal de redémarrage ne soit envoyé au pod.
  • successThreshold est le nombre d'essais réussis avant que le pod ne passe à l'état prêt (après un échec lorsque le pod démarre ou récupère).

Troisième étape : définir les stratégies réseau par défaut du pod

Kubernetes a une topographie de réseau "plate", par défaut tous les pods communiquent directement entre eux. Dans certains cas, cela n'est pas souhaitable.

Un problème de sécurité potentiel est qu'un attaquant pourrait utiliser une seule application vulnérable pour envoyer du trafic à tous les pods du réseau. Comme dans de nombreux domaines de la sécurité, le principe du moindre privilège s'applique ici. Idéalement, les politiques de réseau devraient indiquer explicitement quelles connexions entre les pods sont autorisées et lesquelles ne le sont pas.

Par exemple, voici une stratégie simple qui refuse tout trafic entrant pour un espace de noms particulier :

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:  
 name: default-deny-ingress
spec:  
 podSelector: {}  
 policyTypes:  
   - Ingress

Visualisation de cette configuration :

Cinq ratés lors du déploiement de la première application sur Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Plus de détails ici.

Quatrième étape : comportement personnalisé avec des crochets et des conteneurs d'initialisation

L'un de nos principaux objectifs était de fournir des déploiements dans Kubernetes sans temps d'arrêt pour les développeurs. Ceci est difficile car il existe de nombreuses options pour arrêter les applications et libérer leurs ressources utilisées.

Des difficultés particulières ont surgi avec Nginx. Nous avons remarqué que lors du déploiement de ces pods en séquence, les connexions actives étaient interrompues avant de se terminer avec succès.

Après des recherches approfondies sur Internet, il s'est avéré que Kubernetes n'attend pas que les connexions Nginx s'épuisent pour éteindre le pod. Avec l'aide du crochet de pré-arrêt, nous avons implémenté la fonctionnalité suivante et nous nous sommes complètement débarrassés des temps d'arrêt :

lifecycle: 
 preStop:
   exec:
     command: ["/usr/local/bin/nginx-killer.sh"]

Et ici nginx-killer.sh:

#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
   echo "Waiting while shutting down nginx..."
   sleep 10
done

Un autre paradigme extrêmement utile est l'utilisation de conteneurs init pour gérer le lancement d'applications spécifiques. Ceci est particulièrement utile si vous avez un processus de migration de base de données gourmand en ressources qui doit être exécuté avant le démarrage de l'application. Vous pouvez également spécifier une limite de ressources supérieure pour ce processus sans définir une telle limite pour l'application principale.

Un autre schéma courant consiste à accéder aux secrets dans le conteneur init, qui fournit ces informations d'identification au module principal, ce qui empêche l'accès non autorisé aux secrets depuis le module d'application principal lui-même.

Comme d'habitude, une citation de la documentation: les conteneurs init exécutent en toute sécurité du code utilisateur ou des utilitaires qui autrement compromettraient la sécurité de l'image du conteneur de l'application. En séparant les outils inutiles, vous limitez la surface d'attaque de l'image de conteneur de l'application.

Cinquième étape : configuration du noyau

Enfin, parlons d'une technique plus avancée.

Kubernetes est une plate-forme extrêmement flexible qui vous permet d'exécuter des charges de travail comme bon vous semble. Nous avons un certain nombre d'applications très efficaces qui sont extrêmement gourmandes en ressources. Après avoir effectué des tests de charge approfondis, nous avons constaté que l'une des applications avait du mal à suivre la charge de trafic attendue lorsque les paramètres par défaut de Kubernetes étaient en vigueur.

Cependant, Kubernetes vous permet d'exécuter un conteneur privilégié qui ne modifie que les paramètres du noyau pour un pod spécifique. Voici ce que nous avons utilisé pour modifier le nombre maximal de connexions ouvertes :

initContainers:
  - name: sysctl
     image: alpine:3.10
     securityContext:
         privileged: true
      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

Il s'agit d'une technique plus avancée qui n'est souvent pas nécessaire. Mais si votre application a du mal à faire face à une charge importante, vous pouvez essayer de modifier certains de ces paramètres. Plus d'informations sur ce processus et la définition de différentes valeurs - comme toujours dans la documentation officielle.

En conclusion

Bien que Kubernetes puisse sembler être une solution prête à l'emploi, quelques étapes clés doivent être suivies pour assurer le bon fonctionnement des applications.

Tout au long de la migration vers Kubernetes, il est important de suivre le « cycle de test de charge » : exécutez l'application, testez-la sous charge, observez les métriques et le comportement de mise à l'échelle, ajustez la configuration en fonction de ces données, puis répétez ce cycle à nouveau.

Soyez réaliste quant au trafic attendu et essayez d'aller au-delà pour voir quels composants se cassent en premier. Avec cette approche itérative, seules quelques-unes des recommandations énumérées peuvent suffire pour réussir. Ou une personnalisation plus approfondie peut être nécessaire.

Posez-vous toujours ces questions :

  1. Combien de ressources les applications consomment-elles et comment ce montant va-t-il changer ?
  2. Quelles sont les véritables exigences de mise à l'échelle ? Quelle quantité de trafic l'application va-t-elle gérer en moyenne ? Qu'en est-il du trafic de pointe ?
  3. À quelle fréquence le service devra-t-il évoluer ? À quelle vitesse les nouveaux pods doivent-ils être opérationnels pour recevoir du trafic ?
  4. Avec quelle élégance les pods se ferment-ils ? Est-ce vraiment nécessaire ? Est-il possible de réaliser un déploiement sans temps d'arrêt ?
  5. Comment minimiser les risques de sécurité et limiter les dommages causés par les pods compromis ? Certains services disposent-ils d'autorisations ou d'accès dont ils n'ont pas besoin ?

Kubernetes fournit une plate-forme incroyable qui vous permet d'utiliser les meilleures pratiques pour déployer des milliers de services dans un cluster. Cependant, toutes les applications sont différentes. Parfois, la mise en œuvre nécessite un peu plus de travail.

Heureusement, Kubernetes fournit les paramètres nécessaires pour atteindre tous les objectifs techniques. En utilisant une combinaison de demandes et de limites de ressources, des sondes Liveness et Readiness, des conteneurs init, des politiques réseau et un réglage personnalisé du noyau, vous pouvez obtenir des performances élevées ainsi qu'une tolérance aux pannes et une évolutivité rapide.

Quoi lire d'autre :

  1. Bonnes pratiques et bonnes pratiques pour l'exécution de conteneurs et de Kubernetes dans des environnements de production.
  2. Plus de 90 outils utiles pour Kubernetes : déploiement, gestion, surveillance, sécurité, etc..
  3. Notre chaîne Autour de Kubernetes dans Telegram.

Source: habr.com

Ajouter un commentaire