Neuf conseils de performance Kubernetes

Neuf conseils de performance Kubernetes

Salut tout le monde! Je m'appelle Oleg Sidorenkov, je travaille chez DomClick en tant que chef d'équipe infrastructure. Nous utilisons le Cube à vendre depuis plus de trois ans, et pendant ce temps, nous avons vécu de nombreux moments intéressants avec lui. Aujourd'hui, je vais vous dire comment, avec la bonne approche, vous pouvez tirer encore plus de performances de Kubernetes vanille pour votre cluster. À vos marques, prêt? Partez!

Vous savez tous très bien que Kubernetes est un système open source évolutif pour l'orchestration de conteneurs ; Eh bien, ou 5 binaires qui font de la magie en gérant le cycle de vie de vos microservices dans un environnement serveur. De plus, il s'agit d'un outil assez flexible qui peut être assemblé comme un constructeur Lego pour une personnalisation maximale pour différentes tâches.

Et tout semble aller bien : jetez des serveurs dans le cluster, comme du bois de chauffage dans une chambre de combustion, et ne connaissez pas le chagrin. Mais si vous êtes pour l'environnement, alors vous penserez : "Comment puis-je garder le feu dans le poêle et regretter la forêt ?". En d'autres termes, comment trouver des moyens d'améliorer les infrastructures et de réduire les coûts.

1. Gardez une trace des ressources de l'équipe et de l'application

Neuf conseils de performance Kubernetes

L'une des méthodes les plus banales mais efficaces est l'introduction de demandes/limites. Séparez les applications par espaces de noms et les espaces de noms par équipes de développement. Définissez l'application avant de déployer des valeurs pour la consommation de temps processeur, de mémoire, de stockage éphémère.

resources:
   requests:
     memory: 2Gi
     cpu: 250m
   limits:
     memory: 4Gi
     cpu: 500m

Par expérience, nous sommes arrivés à la conclusion: cela ne vaut pas la peine de gonfler les demandes de limites de plus de deux fois. La taille du cluster est calculée en fonction des requêtes, et si vous définissez l'application sur une différence de ressources, par exemple, de 5 à 10 fois, imaginez ce qui arrivera à votre nœud lorsqu'il sera rempli de pods et recevra soudainement une charge. Rien de bon. Au minimum, étranglement, et au maximum, dites au revoir au travailleur et obtenez une charge cyclique sur le reste des nœuds après que les pods commencent à bouger.

De plus, avec l'aide limitranges vous pouvez définir des valeurs de ressource pour le conteneur au début - minimum, maximum et par défaut :

➜  ~ kubectl describe limitranges --namespace ops
Name:       limit-range
Namespace:  ops
Type        Resource           Min   Max   Default Request  Default Limit  Max Limit/Request Ratio
----        --------           ---   ---   ---------------  -------------  -----------------------
Container   cpu                50m   10    100m             100m           2
Container   ephemeral-storage  12Mi  8Gi   128Mi            4Gi            -
Container   memory             64Mi  40Gi  128Mi            128Mi          2

N'oubliez pas de limiter les ressources de l'espace de noms afin qu'une seule commande ne puisse pas prendre toutes les ressources du cluster :

➜  ~ kubectl describe resourcequotas --namespace ops
Name:                   resource-quota
Namespace:              ops
Resource                Used          Hard
--------                ----          ----
limits.cpu              77250m        80
limits.memory           124814367488  150Gi
pods                    31            45
requests.cpu            53850m        80
requests.memory         75613234944   150Gi
services                26            50
services.loadbalancers  0             0
services.nodeports      0             0

Comme vous pouvez le voir dans la description resourcequotas, si la commande ops souhaite déployer des pods qui consommeront 10 processeurs supplémentaires, le planificateur ne l'autorisera pas et générera une erreur :

Error creating: pods "nginx-proxy-9967d8d78-nh4fs" is forbidden: exceeded quota: resource-quota, requested: limits.cpu=5,requests.cpu=5, used: limits.cpu=77250m,requests.cpu=53850m, limited: limits.cpu=10,requests.cpu=10

Pour résoudre un problème similaire, vous pouvez écrire un outil, par exemple, comme cette, qui peut stocker et valider l'état des ressources de commande.

2. Choisissez le meilleur stockage de fichiers

Neuf conseils de performance Kubernetes

Ici, je voudrais aborder le sujet des volumes persistants et du sous-système de disque des nœuds de travail Kubernetes. J'espère que personne n'utilise le "Cube" sur le disque dur en production, mais parfois même un SSD ordinaire ne suffit déjà pas. Nous avons fait face à un tel problème que les logs tuaient le disque par des opérations d'E/S, et il n'y a pas beaucoup de solutions ici :

  • Utilisez des SSD hautes performances ou passez à NVMe (si vous gérez votre propre matériel).

  • Diminuez le niveau de journalisation.

  • Faites un équilibrage "intelligent" des pods qui violent le disque (podAntiAffinity).

La capture d'écran ci-dessus montre ce qui se passe sous nginx-ingress-controller avec un disque lorsque access_logs est activé (~ 12k logs/sec). Un tel état, bien sûr, peut conduire à la dégradation de toutes les applications sur ce nœud.

Quant au PV, hélas, je n'ai pas tout essayé. espèce Volumes persistants. Utilisez la meilleure option qui vous convient. Il est historiquement arrivé dans notre pays qu'une petite partie des services ait besoin de volumes RWX, et il y a longtemps, ils ont commencé à utiliser le stockage NFS pour cette tâche. Pas cher et... suffisant. Bien sûr, nous avons mangé de la merde avec lui - soyez en bonne santé, mais nous avons appris à l'accorder et sa tête ne lui fait plus mal. Et si possible, passez au stockage d'objets S3.

3. Créez des images optimisées

Neuf conseils de performance Kubernetes

Il est préférable d'utiliser des images optimisées pour les conteneurs afin que Kubernetes puisse les récupérer plus rapidement et les exécuter plus efficacement. 

L'optimisation signifie que les images :

  • contenir une seule application ou exécuter une seule fonction ;

  • petite taille, car les images volumineuses sont moins bien transmises sur le réseau ;

  • avoir des points de terminaison de santé et de préparation que Kubernetes peut utiliser pour prendre des mesures en cas de temps d'arrêt ;

  • utiliser des systèmes d'exploitation adaptés aux conteneurs (comme Alpine ou CoreOS) qui sont plus résistants aux erreurs de configuration ;

  • utilisez des builds en plusieurs étapes afin de ne pouvoir déployer que des applications compilées et non les sources qui les accompagnent.

Il existe de nombreux outils et services qui vous permettent de vérifier et d'optimiser les images à la volée. Il est important de toujours les tenir à jour et en sécurité. En conséquence, vous obtenez :

  1. Réduction de la charge réseau sur l'ensemble du cluster.

  2. Réduction du temps de démarrage du conteneur.

  3. Taille plus petite de l'ensemble de votre registre Docker.

4. Utilisez un cache DNS

Neuf conseils de performance Kubernetes

Si nous parlons de charges élevées, alors sans régler le système DNS du cluster, la vie est plutôt moche. Il était une fois, les développeurs de Kubernetes supportaient leur solution kube-dns. Il a également été mis en œuvre dans notre pays, mais ce logiciel n'était pas particulièrement à l'écoute et n'offrait pas les performances requises, bien que, semble-t-il, la tâche soit simple. Ensuite, coredns est apparu, vers lequel nous sommes passés et ne connaissions pas le chagrin, plus tard, il est devenu le service DNS par défaut dans les K8. À un moment donné, nous avons grandi jusqu'à 40 XNUMX rps pour le système DNS, et cette solution n'était pas non plus suffisante. Mais, par un hasard chanceux, Nodelocaldns est sorti, alias node local cache, alias Cache DNS local du nœud.

Pourquoi l'utilisons-nous ? Il existe un bogue dans le noyau Linux qui, lorsque plusieurs accès via conntrack NAT sur UDP, conduisent à une condition de concurrence pour l'écriture dans les tables conntrack, et une partie du trafic via NAT est perdue (chaque voyage via le service est NAT). Nodelocaldns résout ce problème en supprimant le NAT et en mettant à niveau la connectivité TCP vers le DNS en amont, ainsi qu'en mettant en cache les requêtes DNS en amont localement (y compris un court cache négatif de 5 secondes).

5. Mettre à l'échelle les pods horizontalement et verticalement automatiquement

Neuf conseils de performance Kubernetes

Pouvez-vous affirmer avec certitude que tous vos microservices sont prêts pour une augmentation de charge de deux à trois fois ? Comment bien allouer des ressources à vos applications ? Garder quelques pods en cours d'exécution au-delà de la charge de travail peut être redondant, et les garder dos à dos risque de provoquer une interruption due à une augmentation soudaine du trafic vers le service. Le juste milieu aide à réaliser le charme de la multiplication de services tels que Autoscaler de pod horizontal и Autoscaler de pods verticaux.

VPA vous permet d'augmenter automatiquement les demandes/limites de vos conteneurs dans un pod en fonction de l'utilisation réelle. Comment peut-il être utile ? Si vous avez des pods qui, pour une raison quelconque, ne peuvent pas être mis à l'échelle horizontalement (ce qui n'est pas entièrement fiable), vous pouvez essayer de faire confiance à VPA pour modifier ses ressources. Sa fonctionnalité est un système de recommandation basé sur les données historiques et actuelles de metric-server, donc si vous ne souhaitez pas modifier automatiquement les demandes/limites, vous pouvez simplement surveiller les ressources recommandées pour vos conteneurs et optimiser les paramètres pour économiser le processeur et la mémoire. dans le cluster.

Neuf conseils de performance KubernetesImage tirée de https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231

Le planificateur de Kubernetes est toujours basé sur les requêtes. Quelle que soit la valeur que vous y mettez, le planificateur recherchera un nœud approprié en fonction de celle-ci. La valeur des limites est nécessaire au kublet afin de savoir quand limiter ou tuer un pod. Et puisque le seul paramètre important est la valeur des requêtes, VPA fonctionnera avec. Chaque fois que vous mettez à l'échelle votre application verticalement, vous définissez quelles doivent être les requêtes. Et qu'adviendra-t-il alors des limites ? Ce paramètre sera également mis à l'échelle proportionnellement.

Par exemple, voici les paramètres typiques du pod :

resources:
   requests:
     memory: 250Mi
     cpu: 200m
   limits:
     memory: 500Mi
     cpu: 350m

Le moteur de recommandation détermine que votre application a besoin de 300 Mo de CPU et de 500 Mi pour fonctionner correctement. Vous obtiendrez ces paramètres :

resources:
   requests:
     memory: 500Mi
     cpu: 300m
   limits:
     memory: 1000Mi
     cpu: 525m

Comme mentionné ci-dessus, il s'agit d'une mise à l'échelle proportionnelle basée sur le rapport demandes/limites dans le manifeste :

  • Processeur : 200m → 300m : rapport 1:1.75 ;

  • Mémoire : 250Mi → 500Mi : rapport 1:2.

en ce qui concerne HPA, alors le mécanisme de fonctionnement est plus transparent. Des seuils sont définis pour des métriques telles que le processeur et la mémoire, et si la moyenne de toutes les répliques dépasse le seuil, l'application évolue de +1 pod jusqu'à ce que la valeur tombe en dessous du seuil ou jusqu'à ce que le nombre maximal de répliques soit atteint.

Neuf conseils de performance KubernetesImage tirée de https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231

En plus des métriques habituelles telles que le processeur et la mémoire, vous pouvez définir des seuils sur vos métriques Prometheus personnalisées et les utiliser si vous pensez que c'est le moyen le plus précis de déterminer quand mettre à l'échelle votre application. Une fois que l'application se stabilise en dessous du seuil de métrique spécifié, HPA commencera à réduire les pods au nombre minimum de répliques ou jusqu'à ce que la charge atteigne le seuil spécifié.

6. N'oubliez pas l'affinité de nœud et l'affinité de pod

Neuf conseils de performance Kubernetes

Tous les nœuds ne fonctionnent pas sur le même matériel et tous les pods n'ont pas besoin d'exécuter des applications gourmandes en ressources de calcul. Kubernetes vous permet de spécifier la spécialisation des nœuds et des pods à l'aide Affinité de nœud и Affinité de pod.

Si vous avez des nœuds adaptés aux opérations à forte intensité de calcul, pour une efficacité maximale, il est préférable de lier les applications aux nœuds appropriés. Pour ce faire, utilisez nodeSelector avec étiquette de nœud.

Disons que vous avez deux nœuds : un avec CPUType=HIGHFREQ et un grand nombre de cœurs rapides, un autre avec MemoryType=HIGHMEMORY plus de mémoire et des performances plus rapides. Le moyen le plus simple consiste à attribuer un déploiement de pod à un nœud HIGHFREQen ajoutant à la rubrique spec un sélecteur comme celui-ci :

…
nodeSelector:
	CPUType: HIGHFREQ

Une façon plus coûteuse et spécifique de le faire est d'utiliser nodeAffinity dans le champ affinity section spec. Il y a deux options :

  • requiredDuringSchedulingIgnoredDuringExecution : réglage dur (le planificateur ne déploiera les pods que sur des nœuds spécifiques (et nulle part ailleurs) );

  • preferredDuringSchedulingIgnoredDuringExecution: paramètre souple (le planificateur essaiera de se déployer sur des nœuds spécifiques, et s'il échoue, il essaiera de se déployer sur le prochain nœud disponible).

Vous pouvez spécifier une syntaxe spécifique pour la gestion des étiquettes de nœud, par exemple, In, NotIn, Exists, DoesNotExist, Gt ou Lt. Cependant, rappelez-vous que des méthodes complexes dans de longues listes d'étiquettes ralentiront la prise de décision dans des situations critiques. En d'autres termes, ne compliquez pas trop.

Comme mentionné ci-dessus, Kubernetes vous permet de définir la liaison des pods actuels. Autrement dit, vous pouvez faire fonctionner certains pods avec d'autres pods dans la même zone de disponibilité (pertinente pour les clouds) ou les nœuds.

В podAffinity marges affinity section spec les mêmes champs sont disponibles que dans le cas de nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution и preferredDuringSchedulingIgnoredDuringExecution. La seule différence est que matchExpressions liera les pods à un nœud qui exécute déjà un pod avec cette étiquette.

Plus Kubernetes offre un champ podAntiAffinity, qui, en revanche, ne lie pas un pod à un nœud avec des pods spécifiques.

À propos des expressions nodeAffinity Le même conseil peut être donné : essayez de garder les règles simples et logiques, n'essayez pas de surcharger la spécification du pod avec un ensemble complexe de règles. Il est très facile de créer une règle qui ne correspond pas aux conditions du cluster, ce qui impose une charge supplémentaire au planificateur et dégrade les performances globales.

7. Altérations et tolérances

Il existe une autre façon de gérer le planificateur. Si vous avez un grand cluster avec des centaines de nœuds et des milliers de microservices, il est très difficile d'empêcher certains pods d'être hébergés par certains nœuds.

Le mécanisme des souillures - règles d'interdiction - y contribue. Par exemple, vous pouvez empêcher certains nœuds d'exécuter des pods dans certains scénarios. Pour appliquer une teinte à un nœud spécifique, utilisez l'option taint dans kubectl. Spécifiez la clé et la valeur, puis souiller comme NoSchedule ou NoExecute:

$ kubectl taint nodes node10 node-role.kubernetes.io/ingress=true:NoSchedule

Il convient également de noter que le mécanisme de souillure prend en charge trois effets principaux : NoSchedule, NoExecute и PreferNoSchedule.

  • NoSchedule signifie que jusqu'à ce qu'il y ait une entrée correspondante dans la spécification du pod tolerations, il ne peut pas être déployé sur le nœud (dans cet exemple node10).

  • PreferNoSchedule - version simplifiée NoSchedule. Dans ce cas, le planificateur essaiera de ne pas allouer les pods qui n'ont pas d'entrée correspondante. tolerations par nœud, mais ce n'est pas une limite stricte. S'il n'y a pas de ressources dans le cluster, les pods commenceront à se déployer sur ce nœud.

  • NoExecute - cet effet déclenche une évacuation immédiate des pods qui n'ont pas d'entrée correspondante tolerations.

Curieusement, ce comportement peut être annulé en utilisant le mécanisme de tolérances. C'est pratique lorsqu'il y a un nœud "interdit" et que vous devez y placer uniquement des services d'infrastructure. Comment faire? N'autoriser que les gousses pour lesquelles il existe une tolérance appropriée.

Voici à quoi ressemblerait la spécification du pod :

spec:
   tolerations:
     - key: "node-role.kubernetes.io/ingress"
        operator: "Equal"
        value: "true"
        effect: "NoSchedule"

Cela ne signifie pas que lors du prochain redéploiement, le pod touchera exactement ce nœud, ce n'est pas le mécanisme d'affinité de nœud et nodeSelector. Mais en combinant plusieurs fonctionnalités, vous pouvez obtenir une configuration de planificateur très flexible.

8. Définir la priorité de déploiement du pod

Ce n'est pas parce que vous avez configuré des liaisons pod à nœud que tous les pods doivent être traités avec la même priorité. Par exemple, vous souhaiterez peut-être déployer certains pods avant d'autres.

Kubernetes propose différentes manières de définir la priorité et la préemption des pods. Le décor est composé de plusieurs parties : objet PriorityClass et descriptions des champs priorityClassName dans la spécification du pod. Prenons un exemple :

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 99999
globalDefault: false
description: "This priority class should be used for very important pods only"

Nous créons PriorityClass, donnez-lui un nom, une description et une valeur. Plus haute value, plus la priorité est élevée. La valeur peut être n'importe quel entier 32 bits inférieur ou égal à 1 000 000 000. Les valeurs supérieures sont réservées aux pods système critiques, qui ne peuvent généralement pas être anticipés. L'expulsion ne se produira que si le pod hautement prioritaire n'a nulle part où faire demi-tour, puis certains des pods d'un nœud particulier seront évacués. Si ce mécanisme est trop rigide pour vous, vous pouvez ajouter l'option preemptionPolicy: Never, puis il n'y aura pas de préemption, le pod sera le premier dans la file d'attente et attendra que le planificateur lui trouve des ressources libres.

Ensuite, nous créons un pod, dans lequel nous spécifions le nom priorityClassName:

apiVersion: v1
kind: Pod
metadata:
  name: static-web
  labels:
    role: myrole
 spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP
  priorityClassName: high-priority
          

Vous pouvez créer autant de classes prioritaires que vous le souhaitez, bien qu'il soit recommandé de ne pas vous laisser emporter par cela (par exemple, limitez-vous à une priorité faible, moyenne et élevée).

Ainsi, si nécessaire, vous pouvez augmenter l'efficacité du déploiement de services critiques, tels que nginx-ingress-controller, coredns, etc.

9. Optimisez votre cluster ETCD

Neuf conseils de performance Kubernetes

L'ETCD peut être appelé le cerveau de l'ensemble du cluster. Il est très important de maintenir le fonctionnement de cette base de données à un niveau élevé, car la vitesse des opérations dans le "Cube" en dépend. Une solution assez standard, et en même temps bonne, serait de garder un cluster ETCD sur les nœuds maîtres afin d'avoir un délai minimum pour kube-apiserver. Si ce n'est pas possible, placez l'ETCD le plus près possible, avec une bonne bande passante entre les participants. Faites également attention au nombre de nœuds d'ETCD qui peuvent tomber sans nuire au cluster.

Neuf conseils de performance Kubernetes

Gardez à l'esprit qu'une augmentation excessive du nombre de participants au cluster peut augmenter la tolérance aux pannes au détriment des performances, tout doit être modéré.

Si nous parlons de la mise en place du service, il y a quelques recommandations :

  1. Avoir un bon matériel, basé sur la taille du cluster (vous pouvez lire ici).

  2. Ajustez quelques paramètres si vous avez réparti un cluster entre une paire de contrôleurs de domaine ou si votre réseau et vos disques laissent beaucoup à désirer (vous pouvez lire ici).

Conclusion

Cet article décrit les points que notre équipe essaie de respecter. Il ne s'agit pas d'une description détaillée des actions, mais d'options qui peuvent être utiles pour optimiser la surcharge d'un cluster. Il est clair que chaque cluster est unique à sa manière, et les solutions de tuning peuvent varier considérablement, il serait donc intéressant d'avoir un retour de votre part : comment surveillez-vous votre cluster Kubernetes, comment améliorez-vous ses performances. Partagez votre expérience dans les commentaires, ce sera intéressant de le savoir.

Source: habr.com