
Bonjour Ă tous ! Je m'appelle Oleg Sidorenkov, je travaille chez DomClick en tant que responsable de l'Ă©quipe infrastructure. Nous exploitons « Kubik » en production depuis plus de trois ans, et durant ce temps, nous avons vĂ©cu de nombreux moments intĂ©ressants avec lui. Aujourd'hui, je vais vous expliquer comment, en adoptant la bonne approche, vous pouvez tirer encore plus de performance de votre cluster Kubernetes « vanille ». PrĂȘts, partez !
Vous savez tous trÚs bien que Kubernetes est un systÚme évolutif de code ouvert pour l'orchestration des conteneurs ; enfin, ou 5 binaires qui font de la magie en gérant le cycle de vie de vos microservices dans un environnement serveur. De plus, c'est un outil assez flexible que l'on peut assembler comme un ensemble Lego pour une personnalisation maximale selon différentes tùches.
Tout semble aller pour le mieux : mettez les serveurs dans le cluster comme du bois dans un four et ne vous inquiĂ©tez pas. Mais si vous pensez Ă l'Ă©cologie, vous vous demanderez : « Comment puis-je entretenir le feu dans la cheminĂ©e tout en prĂ©servant la forĂȘt ? ». Autrement dit, comment trouver des moyens d'amĂ©liorer l'infrastructure tout en rĂ©duisant les coĂ»ts.
1. Surveillez les ressources des équipes et des applications

Une des méthodes les plus banales mais efficaces est l'introduction de requests/limits. Séparez les applications par namespaces, et les namespaces par équipes de développement. Attribuez aux applications, avant déploiement, des valeurs de consommation en termes de temps CPU, mémoire, et stockage éphémÚre.
resources:
requests:
memory: 2Gi
cpu: 250m
limits:
memory: 4Gi
cpu: 500mPar expĂ©rience, nous avons constatĂ© qu'il ne faut pas gonfler les requests par rapport aux limits de plus du double. Le volume du cluster est calculĂ© en fonction des requests, et si vous attribuez aux applications 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 soudain une charge. Rien de bon. Au minimum, du throttling, et au maximum, vous direz adieu Ă votre worker et aurez une charge cyclique sur les autres nĆuds aprĂšs que les pods aient commencĂ© Ă migraient.
De plus, Ă l'aide de limitranges vous pouvez dĂ©finir dĂšs le dĂ©part des valeurs de ressources pour le conteneur â minimales, maximales 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 2N'oubliez pas de limiter les ressources du namespace pour qu'une équipe 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 0Comme on peut le voir dans la description resourcequotas, si l'équipe ops souhaite déployer des pods qui utiliseront encore 10 CPU, l'ordonnanceur ne le permettra pas et renverra 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=10Pour résoudre un tel problÚme, on peut écrire un outil, par exemple, comme , capable de stocker et de commettre l'état des ressources des équipes.
2. Choisissez un stockage de fichiers optimal

Ici, je voudrais aborder le sujet des volumes persistants et du systĂšme de stockage des nĆuds de travail de Kubernetes. J'espĂšre que personne n'utilise de « Cube » sur HDD en production, mais parfois un SSD standard devient dĂ©jĂ insuffisant. Nous avons Ă©tĂ© confrontĂ©s au problĂšme oĂč les journaux saturent le disque par les opĂ©rations d'entrĂ©e-sortie, et les options de solution ne sont pas trĂšs nombreuses :
Utiliser des SSD hautes performances ou passer Ă NVMe (si vous gĂ©rez vous-mĂȘme votre matĂ©riel).
Réduire le niveau de journalisation.
Effectuer un équilibre « intelligent » des pods qui saturent le disque (
podAntiAffinity).
L'Ă©cran ci-dessus montre ce qui se passe avec le disque lors de l'utilisation du nginx-ingress-controller lorsque la journalisation access_logs est activĂ©e (~12 000 journaux/sec.). Cet Ă©tat peut Ă©videmment mener Ă une dĂ©gradation de toutes les applications sur ce nĆud.
En ce qui concerne les PV, malheureusement, je n'ai pas testé tous les Volumes persistants. Utilisez la meilleure option qui vous convient. Historiquement, une petite partie des services a besoin de volumes RWX, et pour cela, on utilise depuis longtemps le stockage NFS. C'est bon marché et... suffisant. Bien sûr, nous avons rencontré de nombreux problÚmes - mais nous avons appris à l'optimiser, et nous n'avons plus de soucis. Si possible, passez à un stockage d'objets S3.
3. Rassemblez des images optimisées

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 de maniĂšre plus efficace.Â
L'optimisation signifie que les images :
contiennent une seule application ou effectuent une seule fonction ;
sont de petite taille, car les grandes images sont plus mal transmises sur le réseau ;
ont des points de terminaison pour vérifier l'état de santé et la disponibilité, permettant à Kubernetes d'agir en cas de pannes ;
utilisent des systÚmes d'exploitation favorables aux conteneurs (comme Alpine ou CoreOS), qui sont plus résilients aux erreurs de configuration ;
utilisent des constructions multi-étapes, afin que vous puissiez déployer uniquement des applications compilées, et non des sources connexes.
Il existe de nombreux outils et services permettant de vérifier et d'optimiser les images à la volée. Il est important de toujours les maintenir à jour et vérifiés en termes de sécurité. En fin de compte, vous obtenez :
Une réduction de la charge réseau sur l'ensemble du cluster.
Une diminution du temps de démarrage du conteneur.
Un volume réduit de tout votre registre Docker.
4. Utilisez le cache DNS

En ce qui concerne les charges Ă©levĂ©es, il est assez difficile de vivre sans l'optimisation du systĂšme DNS du cluster. Il y a longtemps, les dĂ©veloppeurs de Kubernetes prenaient en charge leur solution kube-dns. Celle-ci a Ă©tĂ© mise en Ćuvre chez nous, mais ce logiciel n'a pas Ă©tĂ© particuliĂšrement optimisĂ© et ne fournissait pas la performance requise, mĂȘme si la tĂąche semblait simple. Puis est arrivĂ© coredns, que nous avons adoptĂ© et qui nous a donnĂ© satisfaction ; il est devenu le service DNS par dĂ©faut dans K8s. Ă un moment donnĂ©, nous avons atteint 40 000 rps sur le systĂšme DNS, et cette solution est devenue insuffisante. Mais, par un heureux hasard, Nodelocaldns est sorti, Ă©galement connu sous le nom de cache local de nĆud, .
Pourquoi utilisons-nous cela ? Il y a un bug dans le noyau Linux qui, lors d'un accĂšs multiple via conntrack NAT par UDP, conduit Ă une condition de course pour l'Ă©criture dans les tables conntrack, et une partie du trafic Ă travers le NAT est perdue (chaque appel Ă travers le Service passe par le NAT). Nodelocaldns rĂ©sout ce problĂšme en Ă©liminant le NAT et en mettant Ă niveau la connexion vers TCP pour les DNS en amont, ainsi qu'en mettant en cache localement les requĂȘtes DNS vers les sources amont (y compris un court cache nĂ©gatif de 5 secondes).
5. Scalez les pods horizontalement et verticalement de maniĂšre automatique

Pouvez-vous affirmer avec confiance que tous vos microservices sont prĂȘts pour une augmentation de charge de deux Ă trois fois ? Comment allouer correctement les ressources Ă vos applications ? Avoir quelques pods en plus de la charge de travail peut sembler redondant, et en garder juste assez vous expose au risque d'un arrĂȘt soudain en cas de pic de trafic sur le service. La voie du juste milieu est facilitĂ©e par des services tels que et .
VPA permet d'augmenter automatiquement les requests/limits de vos conteneurs dans le pod en fonction de l'utilisation rĂ©elle. En quoi cela peut-il ĂȘtre utile ? Si vous avez des pods qui ne peuvent pas ĂȘtre mis Ă l'Ă©chelle horizontalement pour une raison quelconque (ce qui n'est pas tout Ă fait fiable), vous pouvez essayer de confier l'ajustement de ses ressources au VPA. Son atout rĂ©side dans un systĂšme de recommandations basĂ© sur des donnĂ©es historiques et actuelles provenant du metric-server, donc, si vous ne souhaitez pas changer automatiquement les requests/limits, vous pouvez simplement suivre les ressources recommandĂ©es pour vos conteneurs et optimiser les paramĂštres pour Ă©conomiser du CPU et de la mĂ©moire dans le cluster.
L'image est tirée de https://levelup.gitconnected.com/kubernetes-autoscaling-101-cluster-autoscaler-horizontal-pod-autoscaler-and-vertical-pod-2a441d9ad231
Le planificateur dans Kubernetes est toujours basĂ© sur les requests. Quelle que soit la valeur que vous y attribuez, le planificateur cherchera un nĆud appropriĂ© en fonction de celle-ci. Les valeurs des limits sont nĂ©cessaires au kubelet pour comprendre quand limiter ou tuer un pod. Et puisque le seul paramĂštre important est la valeur des requests, le VPA travaillera avec cela. Chaque fois que vous dĂ©finissez le dimensionnement vertical d'une application, vous dĂ©terminez quelles doivent ĂȘtre les requests. Et qu'en est-il des limits ? Ce paramĂštre sera Ă©galement redimensionnĂ© proportionnellement.
Par exemple, voici les configurations normales d'un pod :
ressources:
demandes:
mémoire: 250Mi
cpu: 200m
limites:
mémoire: 500Mi
cpu: 350mLe mécanisme de recommandation détermine que votre application nécessite 300m de CPU et 500Mi pour fonctionner normalement. Vous obtiendrez ces paramÚtres :
ressources:
demandes:
mémoire: 500Mi
cpu: 300m
limites:
mémoire: 1000Mi
cpu: 525mComme mentionné précédemment, il s'agit d'une mise à l'échelle proportionnelle basée sur le ratio demandes/limites dans le manifeste :
CPU: 200m â 300m : ratio 1:1.75 ;
MĂ©moire: 250Mi â 500Mi : ratio 1:2.
Concernant HPA, ici le mécanisme est plus transparent. Des seuils de métriques sont définis, par exemple pour le CPU et la mémoire, et si la valeur moyenne de toutes les répliques dépasse le seuil, l'application est mise à l'échelle en ajoutant +1 pod jusqu'à ce que la valeur redescende en dessous du seuil ou jusqu'à atteindre le nombre maximal de répliques.
L'image est 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, comme le CPU et la mémoire, vous pouvez configurer des seuils sur vos métriques personnalisées à partir de Prometheus et travailler avec elles si vous pensez que cela définit le mieux quand il faut mettre à l'échelle votre application. Une fois l'application stabilisée sous la limite de métrique spécifiée, le HPA commencera à réduire le nombre de pods jusqu'à atteindre le minimum de répliques ou jusqu'à ce que la charge soit satisfaisante par rapport au seuil donné.
6. N'oubliez pas le Node Affinity et le Pod Affinity

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 calculs. Kubernetes permet de spĂ©cifier une spĂ©cialisation des nĆuds et des pods Ă l'aide de Node Affinity et Pod Affinity.
Si vous avez des nĆuds adaptĂ©s aux opĂ©rations gourmandes en calcul, il est prĂ©fĂ©rable de lier les applications Ă ces nĆuds correspondants pour une efficacitĂ© maximale. Pour cela, utilisez nodeSelector avec l'Ă©tiquette du nĆud.
Supposons que vous ayez deux nĆuds : un avec CPUType=HIGHFREQ et un grand nombre de cĆurs rapides, l'autre avec MemoryType=HIGHMEMORY une grande quantitĂ© de mĂ©moire et un dĂ©bit plus rapide. Le plus simple est d'assigner le dĂ©ploiement du pod au nĆud HIGHFREQ, en ajoutant dans la section spec ce sĂ©lecteur :
âŠ
nodeSelector:
CPUType: HIGHFREQUn moyen plus coûteux et spécifique de faire cela est d'utiliser nodeAffinity dans le champ affinity de la section spec. Il existe deux options :
requiredDuringSchedulingIgnoredDuringExecution: configuration stricte (le planificateur ne dĂ©ploiera des pods que sur des nĆuds spĂ©cifiques (et nulle part ailleurs));preferredDuringSchedulingIgnoredDuringExecution: configuration flexible (le planificateur essaiera de dĂ©ployer sur des nĆuds spĂ©cifiques, et s'il n'y parvient pas, il essaiera sur le prochain nĆud disponible).
Vous pouvez spĂ©cifier une syntaxe de gestion des Ă©tiquettes de nĆuds, par exemple, Dans, PasDans, Existe, N'existePas, Gt ou Lt. Cependant, gardez Ă l'esprit 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.
Comme mentionnĂ© prĂ©cĂ©demment, Kubernetes permet de dĂ©finir la liaison des pods actuels. Cela signifie que vous pouvez faire en sorte que certains pods fonctionnent avec d'autres pods dans la mĂȘme zone de disponibilitĂ© (pertinent pour les clouds) ou nĆuds.
Dans podAffinity champs affinity de la section spec les mĂȘmes champs sont disponibles que dans le cas de nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution et preferredDuringSchedulingIgnoredDuringExecution. La seule diffĂ©rence est que matchExpressions lier les pods Ă un nĆud sur lequel un pod avec cette Ă©tiquette est dĂ©jĂ en cours d'exĂ©cution.
De plus, Kubernetes propose le champ podAntiAffinity, qui, au contraire, ne lie pas un pod Ă un nĆud avec certains pods.
Concernant les expressions nodeAffinity on peut donner le mĂȘme conseil : essayez de garder la simplicitĂ© et la logique des rĂšgles, ne tentez pas de surcharger la spĂ©cification des pods avec un ensemble complexe de rĂšgles. Il est trĂšs facile de crĂ©er une rĂšgle qui ne correspondra pas aux conditions du cluster, crĂ©ant une charge supplĂ©mentaire sur le planificateur et rĂ©duisant les performances globales.
7. Taints & Tolerations
Il existe Ă©galement un autre moyen 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 de ne pas permettre Ă certains pods de se dĂ©ployer sur certains nĆuds.
Le mĂ©canisme des taints aide Ă cela â rĂšgles prohibitives. Par exemple, dans certaines situations, il peut ĂȘtre interdit Ă certains nĆuds d'exĂ©cuter des pods. Pour appliquer un taint Ă un nĆud spĂ©cifique, vous devez utiliser l'option taint dans kubectl. SpĂ©cifiez la clĂ© et la valeur, puis un taint comme NoSchedule ou NoExecute:
$ kubectl taint nodes node10 node-role.kubernetes.io/ingress=true:NoScheduleIl convient également de noter que le mécanisme de taint prend en charge trois effets principaux : NoSchedule, NoExecute et PreferNoSchedule.
NoSchedulesignifie que tant qu'il n'y a pas d'enregistrement correspondant dans la spĂ©cification du podtolerations, il ne pourra pas ĂȘtre dĂ©ployĂ© sur le nĆud (dans cet exemplenode10).PreferNoScheduleâ une version simplifiĂ©eNoSchedule. Dans ce cas, le planificateur tentera de ne pas rĂ©partir les pods qui n'ont pas d'enregistrement correspondanttolerationssur le nĆud, mais ce n'est pas une restriction stricte. Si le cluster ne dispose pas de ressources, 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'enregistrement correspondanttolerations.
Il est intĂ©ressant de noter que ce comportement peut ĂȘtre annulĂ© Ă l'aide du mĂ©canisme des tolerations. Cela est pratique lorsqu'il existe un nĆud "interdit" et que vous souhaitez y placer uniquement des services d'infrastructure. Comment faire ? Autorisez uniquement les pods pour lesquels il existe une toleration appropriĂ©e.
Voici à quoi ressemblera la spécification du pod :
spec:
tolerations:
- key: "node-role.kubernetes.io/ingress"
operator: "Equal"
value: "true"
effect: "NoSchedule"Cela ne signifie pas qu'au prochain redeploiement, le pod se retrouvera nĂ©cessairement sur ce nĆud, ce n'est pas un mĂ©canisme d'affinitĂ© de nĆud et nodeSelector. Mais en combinant plusieurs fonctionnalitĂ©s, vous pouvez obtenir une configuration trĂšs flexible de l'ordonnanceur.
8. Configurez la priorité de déploiement des pods
Le fait que vous ayez configurĂ© la liaison des pods aux nĆuds ne signifie pas que tous les pods doivent ĂȘtre traitĂ©s avec la mĂȘme prioritĂ©. Par exemple, vous pouvez vouloir dĂ©ployer certains pods avant les autres.
Kubernetes propose différentes maniÚres de configurer la priorité des pods (Pod Priority and Preemption). La configuration se compose de plusieurs parties : l'objet PriorityClass et la description du champ priorityClassName dans la spécification du pod. Examinons un exemple :
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 99999
globalDefault: false
description: "Cette classe de prioritĂ© ne doit ĂȘtre utilisĂ©e que pour des pods trĂšs importants"Nous crĂ©ons PriorityClass, lui attribuons un nom, une description et une valeur. Plus la 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. Des valeurs plus Ă©levĂ©es sont rĂ©servĂ©es pour des pods systĂšme critiques qui ne peuvent gĂ©nĂ©ralement pas ĂȘtre Ă©vacuĂ©s. L'Ă©vacuation n'aura lieu que si un pod de haute prioritĂ© n'a pas d'endroit oĂč se dĂ©ployer, alors une partie des pods d'un nĆud particulier sera Ă©vacuĂ©e. Si ce mĂ©canisme vous semble trop strict, vous pouvez ajouter l'option preemptionPolicy: Never, et alors il n'y aura pas d'Ă©vacuation, le pod sera placĂ© en tĂȘte de file et attendra que l'ordonnanceur trouve des ressources libres pour lui.
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 de priorité que vous le souhaitez, bien qu'il soit recommandé de ne pas en abuser (par exemple, se limiter aux priorités basse, moyenne et haute).
Ainsi, si nécessaire, vous pourrez améliorer l'efficacité du déploiement de services critiques, tels que nginx-ingress-controller, coredns, etc.
9. Optimisez le cluster ETCD

On peut considĂ©rer ETCD comme le cerveau de tout le cluster. Il est trĂšs important de maintenir cette base de donnĂ©es Ă un niveau Ă©levĂ©, car c'est de lĂ que dĂ©pend la vitesse des opĂ©rations dans le « Cube ». Il est assez standard, et en mĂȘme temps pas mal, de garder le cluster ETCD sur les nĆuds maĂźtres afin d'avoir une latence minimale avec le kube-apiserver. Si cela n'est pas possible, placez ETCD aussi prĂšs que possible, en ayant une bonne bande passante entre les participants. Faites Ă©galement attention au nombre de nĆuds d'ETCD qui peuvent ĂȘtre perdus sans nuire au cluster.

Gardez Ă l'esprit qu'une augmentation excessive du nombre de participants dans le cluster peut amĂ©liorer la tolĂ©rance aux pannes au dĂ©triment de la performance, tout doit ĂȘtre Ă©quilibrĂ©.
En ce qui concerne la configuration du service, les recommandations sont rares :
Ayez un bon matériel, en fonction des dimensions du cluster (vous pouvez lire ).
Ajustez quelques paramÚtres si vous avez réparti le cluster entre plusieurs centres de données ou si votre réseau et vos disques laissent à désirer (vous pouvez lire ).
Conclusion
Cet article dĂ©crit des points que notre Ă©quipe s'efforce de respecter. Ce n'est pas un guide pas Ă pas, mais des options qui peuvent ĂȘtre utiles pour optimiser les coĂ»ts de fonctionnement du cluster. Il est Ă©vident que chaque cluster est unique, et les solutions de configuration peuvent varier considĂ©rablement, c'est pourquoi il serait intĂ©ressant d'avoir votre retour : comment surveillez-vous votre cluster Kubernetes, avec quoi amĂ©liorez-vous son fonctionnement ? Partagez votre expĂ©rience dans les commentaires, nous serions ravis de la connaĂźtre.
Source : habr.com
