Depurando aplicativos Golang muito carregados ou como procuramos um problema no Kubernetes que não estava lá

No mundo moderno das nuvens Kubernetes, de uma maneira ou de outra, é preciso enfrentar erros de software que não são cometidos por você ou seu colega, mas você terá que resolvê-los. Este artigo pode ajudar um iniciante no mundo de Golang e Kubernetes a entender algumas maneiras de depurar seu próprio software e o estrangeiro.

imagem

Meu nome é Viktor Yagofarov, estou desenvolvendo a nuvem Kubernetes no DomKlik e hoje quero falar sobre como resolvemos o problema com um dos principais componentes do cluster de produção k8s (Kubernetes).

Em nosso cluster de combate (no momento da redação):

  • 1890 pods e 577 serviços foram lançados (o número de microsserviços reais também está na região desta figura)
  • Os controladores de ingresso atendem a cerca de 6k RPS e a mesma quantia passa pelo Ingress diretamente para o hostPort .


Problema


Há alguns meses, nossos pods começaram a ter um problema na resolução de nomes DNS. O fato é que o DNS funciona principalmente sobre UDP, e no kernel do Linux existem alguns problemas com o conntrack e o UDP. DNAT ao acessar os endereços de serviço do k8s Service apenas agrava o problema com raças conntrack . Vale acrescentar que em nosso cluster no momento do problema havia cerca de 40k RPS para servidores DNS, CoreDNS.

imagem

Decidiu-se usar o servidor DNS de cache local NodeLocal DNS (nodelocaldns) criado especialmente pela comunidade em cada nó de trabalho do cluster, que ainda está na versão beta e foi projetado para resolver todos os problemas. Resumindo: livre-se do UDP ao conectar-se ao DNS do cluster, remova o NAT e adicione uma camada de cache adicional.

Na primeira iteração da implementação nodelocaldns, usamos a versão 1.15.4 (que não deve ser confundida com a versão do cubo ), que veio com o «kubernetes-installer» Kubespray - estamos falando da nossa fork Fork da Southbridge.

Quase imediatamente após a introdução, os problemas começaram: a memória fluiu e a lareira reiniciou de acordo com os limites de memória (OOM-Kill). No momento de reiniciar isso, todo o tráfego no host foi perdido, pois em todos os pods o /etc/resolv.conf apontava exatamente para o endereço IP do nodelocaldns.

Definitivamente, essa situação não agradou a todos e nossa equipe de OPS tomou várias medidas para eliminá-la.

Como eu mesmo sou novo na Golang, fiquei muito interessado em seguir todo esse caminho e me familiarizar com os aplicativos de depuração nessa maravilhosa linguagem de programação.

Estamos à procura de uma solução


Então vamos!

A versão 1.15.7 foi baixada no cluster de desenvolvimento , que já é considerado beta, e não alfa como 1.15.4, mas a donzela não possui esse tráfego no DNS (40k RPS). É triste.

No processo, desatamos o nodelocaldns da Kubespray e escrevemos um gráfico especial do Helm para uma implementação mais conveniente. Ao mesmo tempo, eles escreveram um manual para o Kubespray, que permite alterar as configurações do kubelet sem digerir todo o estado do cluster por hora; além disso, isso pode ser feito no sentido do ponto (verificando primeiro um pequeno número de nós).

Em seguida, lançamos a versão do nodelocaldns 1.15.7 para prod. A situação, infelizmente, foi repetida. A memória estava fluindo.

O repositório oficial do nodelocaldns tinha uma versão marcada com 1.15. 8, mas, por algum motivo, não consegui fazer o docker puxar esta versão e pensei que ainda não havia coletado a imagem oficial do Docker, portanto, essa versão não deve ser usada. Este é um ponto importante, e retornaremos a ele.

Depuração: Etapa 1


Por um longo tempo, eu não conseguia descobrir como montar minha versão do nodelocaldns em princípio, já que o Makefile do nabo caiu com erros incompreensíveis na imagem do docker, e eu realmente não entendi como criar um projeto Go com o govendor , que foi resolvido de maneira estranha em diretórios imediatamente para várias opções diferentes de servidor DNS. O ponto é que eu comecei a aprender o Go quando o controle de versão normal já havia aparecido .

Pavel Selivanov me ajudou muito com o problema.pauljamm, pelo qual muito obrigado a ele. Consegui montar minha versão.

Em seguida, parafusamos o perfilador pprof , testamos a montagem na donzela e a lançamos no produto.

Um colega da equipe de bate-papo realmente ajudou a entender a criação de perfil, para que você possa se apegar convenientemente ao utilitário pprof através da URL da CLI e estudar a memória e os threads de processo usando os menus interativos no navegador, pelos quais agradecemos também a ele.

À primeira vista, com base na saída do criador de perfil, o processo estava indo bem - a maior parte da memória estava alocada na pilha e, ao que parece, era constantemente usada pelas rotinas Go .

Mas, em algum momento, ficou claro que os “maus” lares dos nodelocaldns tinham muitos segmentos ativos em comparação com os “saudáveis”. E os fios não desapareceram em lugar nenhum, mas continuaram pendurados na memória. Nesse momento, o palpite de Pavel Selivanov de que "os fios estão fluindo" foi confirmado.

imagem

Depuração: Etapa 2


Tornou-se interessante por que isso está acontecendo (os threads estão fluindo) e a próxima etapa do estudo do processo nodelocaldns foi iniciada.

O código do analisador estático staticcheck mostrou que há alguns problemas apenas no estágio de criação de um encadeamento na biblioteca , que é usado no nodelocaldns (ele usa o CoreDNS, que usa o nodelocaldns'om). Pelo que entendi, em alguns lugares não é transmitido um ponteiro para a estrutura , mas uma cópia de seus valores .

Foi decidido fazer um núcleo do processo "ruim" usando o utilitário gcore e ver o que havia dentro.

Preso no coredump com a ferramenta dlv do tipo gdbPercebi seu poder, mas percebi que procuraria uma razão dessa maneira por muito tempo. Portanto, carreguei o coredump no Goland IDE e analisei o estado da memória do processo.

Depuração: Etapa 3


Foi muito interessante estudar a estrutura do programa, vendo o código que os cria. Em cerca de 10 minutos, ficou claro que muitas rotinas go criam algum tipo de estrutura para conexões TCP, as marcam como falsas e nunca as excluem (lembra-se de 40k RPS?).

imagem

imagem

Nas capturas de tela, você pode ver a parte problemática do código e a estrutura que não foi limpa quando a sessão UDP foi fechada.

Além disso, a partir do coredump, o culpado de um número tão grande de RPS ficou conhecido pelos endereços IP nessas estruturas (obrigado por ajudar a encontrar um gargalo no nosso cluster :).

Decisão


Durante a luta contra esse problema, descobri com a ajuda de colegas da comunidade Kubernetes que ainda existe a imagem oficial do nodelocaldns 1.15.8 no Docker (e na verdade tenho mãos tortas e de alguma forma fiz o docker errado puxar, ou o WIFI foi mal-intencionado) momento de atração).

Nesta versão, as versões das bibliotecas que ele usa são bastante “perturbadas”: especificamente, o “culpado” “apnalizou” cerca de 20 versões acima!

Além disso, a nova versão já possui suporte para criação de perfil através do pprof e é ativada através do Configmap, não sendo necessário remontar nada.

Uma nova versão foi baixada primeiro no dev e depois no prod.
III ... vitória !
O processo começou a devolver sua memória ao sistema e os problemas pararam.

No gráfico abaixo, você pode ver a figura: "DNS do fumante x DNS de uma pessoa saudável ".

imagem

achados


A conclusão é simples: verifique duas vezes o que você está fazendo várias vezes e não desdenhe a ajuda da comunidade. Como resultado, passamos mais tempo com o problema por vários dias do que podíamos, mas recebemos uma operação à prova de falhas do DNS em contêineres. Obrigado por ler até este ponto :)

Links úteis:

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