Débogage des applications Golang lourdement chargées ou comment nous avons cherché un problème dans Kubernetes qui n'était pas là

Dans le monde moderne des nuages ​​Kubernetes, d'une manière ou d'une autre, il faut faire face à des erreurs logicielles qui ne sont pas faites par vous ou votre collègue, mais vous devrez les résoudre. Cet article peut aider un nouveau venu dans le monde de Golang et Kubernetes à comprendre certaines façons de déboguer ses propres logiciels et les logiciels étrangers.

image

Je m'appelle Viktor Yagofarov, je développe le cloud Kubernetes chez DomKlik, et aujourd'hui je veux parler de la façon dont nous avons résolu le problème avec l'un des composants clés de notre cluster de production k8s (Kubernetes).

Dans notre cluster de combat (au moment de la rédaction):

  • 1890 pods et 577 services ont été lancés (le nombre de microservices réels est également de l'ordre de ce chiffre)
  • Les contrôleurs d' entrée desservent environ 6 000 RPS et environ le même montant passe directement par Ingress à hostPort .


Problème


Il y a quelques mois, nos pods ont commencé à rencontrer un problème avec la résolution des noms DNS. Le fait est que DNS fonctionne principalement sur UDP, et dans le noyau Linux, il y a quelques problèmes avec conntrack et UDP. DNAT lors de l'accès aux adresses de service du service k8s ne fait qu'exacerber le problème avec les races conntrack . Il convient d'ajouter que dans notre cluster au moment du problème, il y avait environ 40 000 RPS vers les serveurs DNS, CoreDNS.

image

Il a été décidé d'utiliser le serveur DNS de mise en cache local NodeLocal DNS (nodelocaldns) spécialement créé par la communauté sur chaque nœud de travail du cluster, qui est toujours en version bêta et est conçu pour résoudre tous les problèmes. En bref: supprimez UDP lors de la connexion au cluster DNS, supprimez NAT, ajoutez une couche de cache supplémentaire.

Dans la première itération de l'implémentation nodelocaldns, nous avons utilisé la version 1.15.4 (à ne pas confondre avec la version du cube ), qui accompagnait le «kubernetes-installer» Kubespray - nous parlons de notre société Fork Fork de Southbridge.

Presque immédiatement après l'introduction, les problèmes ont commencé: la mémoire a coulé et le foyer a redémarré en fonction des limites de la mémoire (OOM-Kill). Au moment du redémarrage, tout le trafic sur l'hôte était perdu, car dans tous les pods /etc/resolv.conf pointait exactement vers l'adresse IP de nodelocaldns.

Cette situation ne convenait certainement pas à tout le monde et notre équipe OPS a pris un certain nombre de mesures pour l'éliminer.

Comme je suis moi-même nouveau à Golang, j'étais très intéressé à aller jusqu'au bout et à me familiariser avec le débogage d'applications dans ce merveilleux langage de programmation.

Nous recherchons une solution


Alors allons-y!

La version 1.15.7 a été téléchargée sur le cluster de développement , qui est déjà considéré comme bêta, et non alpha comme 1.15.4, mais la jeune fille n'a pas un tel trafic dans DNS (40k RPS). C'est triste.

Au cours du processus, nous avons délié les nodelocaldns de Kubespray et écrit un graphique spécial Helm pour un déploiement plus pratique. En même temps, ils ont écrit un livre de jeu pour Kubespray, qui vous permet de modifier les paramètres du kubelet sans digérer l' état du cluster entier à l'heure; de plus, cela peut être fait de manière ponctuelle (en vérifiant d'abord sur un petit nombre de nœuds).

Ensuite, nous avons déployé la version de nodelocaldns 1.15.7 en prod. Hélas, la situation s'est répétée. La mémoire coulait.

Le référentiel officiel nodelocaldns avait une version balisée avec 1.15. 8, mais pour une raison quelconque, je ne pouvais pas faire en sorte que docker tire sur cette version et je pensais que je n'avais pas encore collecté l'image officielle de Docker, donc cette version ne devrait pas être utilisée. C'est un point important et nous y reviendrons.

Débogage: étape 1


Pendant longtemps, je n'ai pas pu comprendre comment assembler ma version de nodelocaldns en principe, car le Makefile du navet s'est écrasé avec des erreurs incompréhensibles à partir de l'image de docker, et je n'ai pas vraiment compris comment construire astucieusement un projet Go avec govendor , qui a été trié de manière étrange dans les répertoires tout de suite pour plusieurs options de serveur DNS différentes. Le fait est que j'ai commencé à apprendre Go alors que le versionnage de dépendance standard était déjà apparu .

Pavel Selivanov m'a beaucoup aidé avec le problème.pauljamm, pour lequel merci beaucoup à lui. J'ai réussi à assembler ma version.

Ensuite, nous avons vissé le profileur pprof , testé l'assemblage sur la vierge et l' avons déployé dans la prod.

Un collègue de l'équipe de chat a vraiment aidé à comprendre le profilage afin que vous puissiez facilement vous accrocher à l'utilitaire pprof via l'URL CLI et étudier la mémoire et les threads de processus à l'aide des menus interactifs du navigateur, dont un grand merci également à lui.

À première vue, sur la base de la sortie du profileur, le processus fonctionnait bien - la plupart de la mémoire était allouée sur la pile et, semble-t-il, était constamment utilisée par les routines Go .

Mais à un moment donné, il est devenu clair que les «mauvais» foyers des nodelocaldns avaient trop de fils actifs par rapport aux «sains». Et les fils n'ont disparu nulle part, mais ont continué à rester en mémoire. À ce moment, le pressentiment de Pavel Selivanov que "les fils coulent" a été confirmé.

image

Débogage: étape 2


Il est devenu intéressant de savoir pourquoi cela se produit (les fils circulent), et la prochaine étape de l'étude du processus nodelocaldns a commencé.

Le contrôle statique du code de l' analyseur statique a montré qu'il y avait des problèmes au stade de la création d'un thread dans la bibliothèque , qui est utilisé dans nodelocaldns (il inkluda CoreDNS, qui inkluda nodelocaldns'om). Si je comprends bien, à certains endroits, pas un pointeur vers la structure est transmis , mais une copie de leurs valeurs .

Il a été décidé de faire un coredump du «mauvais» processus en utilisant l'utilitaire gcore et de voir ce qu'il y avait à l'intérieur.

Coincé dans coredump avec un outil dlv de type gdbJ'ai réalisé son pouvoir, mais j'ai réalisé que je chercherais une raison de cette façon pendant très longtemps. Par conséquent, j'ai chargé coredump dans l'IDE Goland et analysé l'état de la mémoire de processus.

Débogage: étape 3


C'était très intéressant d'étudier la structure du programme, de voir le code qui les crée. En environ 10 minutes, il est devenu clair que de nombreuses routines de création créent une sorte de structure pour les connexions TCP, les marquent comme fausses et ne les suppriment jamais (rappelez-vous environ 40 000 RPS?).

image

image

Dans les captures d'écran, vous pouvez voir la partie problématique du code et la structure qui n'a pas été effacée lorsque la session UDP a été fermée.

De plus, à partir de coredump, le coupable d'un tel nombre de RPS est devenu connu par les adresses IP dans ces structures (merci d'avoir aidé à trouver un goulot d'étranglement dans notre cluster :).

Décision


Au cours de la lutte contre ce problème, j'ai trouvé avec l'aide de collègues de la communauté Kubernetes que l'image Docker officielle de nodelocaldns 1.15.8 existe toujours (et j'ai en fait des mains tordues et j'ai en quelque sorte mal tiré le docker, ou le WIFI était méchant dans moment de traction).

Dans cette version, les versions des bibliothèques qu'il utilise sont fortement «bouleversées»: plus précisément, le «coupable» «apnalise» une vingtaine de versions au-dessus!

De plus, la nouvelle version prend déjà en charge le profilage via pprof et est activée via Configmap, vous n'avez rien à réassembler.

Une nouvelle version a été téléchargée d'abord en dev puis en prod.
III ... Victoire !
Le processus a commencé à restituer sa mémoire au système et les problèmes ont cessé.

Dans le graphique ci-dessous, vous pouvez voir l'image: «DNS du fumeur vs DNS d'une personne en bonne santé. "

image

résultats


La conclusion est simple: revérifiez ce que vous faites plusieurs fois et ne dédaignez pas l'aide de la communauté. En conséquence, nous avons passé plus de temps sur le problème pendant plusieurs jours que nous ne le pouvions, mais nous avons reçu une opération de sécurité intégrée DNS dans des conteneurs. Merci d'avoir lu

jusqu'ici :) Liens utiles:

1. www.freecodecamp.org/news/how-i-investigated-memory-leaks-in-go-using-pprof-on-a-large-codebase-4bec4325e192
2 . habr.com/en/company/roistat/blog/413175
3. rakyll.org

All Articles