Programadores de encanador, ou a história de um vazamento e as dificuldades de lidar com ele

Era terça-feira, 25 de fevereiro. O difícil lançamento da versão no sábado, 22 de fevereiro, já estava no passado. Parecia que tudo de pior estava para trás e nada indicava problemas. Mas tudo mudou ao mesmo tempo em que um erro de monitoramento resultou em um vazamento de memória no processo do coordenador do serviço de controle de acesso.

De onde? As últimas grandes mudanças na base de códigos do coordenador ocorreram na versão anterior há mais de dois meses e, depois disso, nada de notável aconteceu com a memória. Mas, infelizmente, os cronogramas de monitoramento eram inflexíveis - a memória do coordenador obviamente começou a vazar em algum lugar, uma grande poça ostentava o andar de serviço, o que significa que a equipe de encanamento tinha muito trabalho a fazer.



Primeiro, fazemos uma pequena digressão. Entre outras coisas, o VLSI permite acompanhar o horário de trabalho e monitorar o acesso por modelo de rosto, impressão digital ou cartões de acesso. Ao mesmo tempo, o VLSI se comunica com os controladores de ponto final (travas, torniquetes, terminais de acesso, etc.). Um serviço separado se comunica com os dispositivos. É passivo, interage com dispositivos de controle de acesso com base em seus próprios protocolos implementados por HTTP (S). Ele foi escrito com base na pilha padrão de serviços em nossa empresa: banco de dados PostgreSQL, o Python 3 é usado para lógica de negócios, estendida com métodos C / C ++ de nossa plataforma.

Um nó típico de serviço da web consiste nos seguintes processos:

  • Monitor é o processo raiz.
  • Um coordenador é um processo filho de um monitor.
  • Processos de trabalho.

O Monitor é o primeiro processo de serviço da web. Sua tarefa é chamar fork (), iniciar o processo do coordenador filho e monitorar seu trabalho. O coordenador é o principal processo do serviço da web, é ele quem recebe solicitações de dispositivos externos, envia respostas e equilibra a carga. O coordenador envia a solicitação aos processos de trabalho para execução, eles a executam, transferem a resposta para a memória compartilhada e informam ao coordenador que a tarefa está concluída e você pode obter o resultado.

Quem é o culpado e o que fazer?


Portanto, o coordenador do serviço de controle de acesso difere dos coordenadores de outros serviços em nossa empresa pela presença de um servidor da web. Os coordenadores de outros serviços trabalharam sem vazamentos; portanto, o problema teve que ser procurado em nossa configuração. Pior, nas bancadas de teste, a nova versão existia por um longo tempo e ninguém notou problemas de memória nelas. Eles começaram a olhar mais de perto e descobriram que a memória flui apenas em um dos estandes, e mesmo assim com um sucesso variável - o que significa que o problema ainda não é tão fácil de reproduzir.



O que fazer? Como encontrar uma razão? Primeiro, pegamos um despejo de memória e o enviamos a especialistas da plataforma para análise. O resultado - não há nada no lixão: nenhuma razão, nenhuma dica em que direção procurar mais. Verificamos as alterações no código do coordenador da versão anterior - de repente, fizemos algumas edições terríveis, mas não entendemos isso imediatamente? Mas não - apenas alguns comentários foram adicionados ao código do coordenador, mas alguns métodos foram movidos para novos arquivos - em geral, nada de criminoso.

Eles começaram a olhar para nossos colegas - desenvolvedores do núcleo de nosso serviço. Eles negaram com confiança a própria possibilidade de envolvimento em nosso infortúnio, mas se ofereceram para implantar o monitoramento de pequenos rastros no serviço. Assim que dito, no próximo hotfix, estamos finalizando o serviço, testando rapidamente, lançando-o na batalha.

E o que vemos? Agora nossa memória se esvai não apenas rapidamente, mas muito rapidamente - o crescimento se tornou exponencial. O primeiro pico é atribuído a forças malignas e fatores relacionados, mas o segundo pico, várias horas após o primeiro, deixa claro que leva muito tempo para esperar o próximo hotfix ser lançado com um comportamento de emergência do serviço. Portanto, pegamos os resultados do tracemalloc e corrigimos o serviço, revertendo as edições com o monitoramento para retornar pelo menos ao crescimento linear.



Parecia que tínhamos os resultados do tracemalloc trabalhando na memória alocada no Python, agora vamos examiná-los e encontrar o culpado do vazamento, mas não estava lá - os dados coletados não têm os picos de 5,5 GB que vimos nos gráficos de monitoramento. A memória máxima usada é de apenas 250 MB e até traceMalloc consome 130 MB deles. Isso é parcialmente explicável - tracemalloc permite que você veja a dinâmica da memória no Python, mas não sabe sobre a alocação de memória nos pacotes C e C ++ implementados por nossa plataforma. Não foi possível encontrar algo interessante nos dados obtidos, a memória é alocada em volumes aceitáveis ​​para objetos comuns, como fluxos, strings e dicionários - em geral, nada suspeito. Decidimos remover tudo que era supérfluo dos dados, deixando apenas o consumo total de memória e tempo e visualizá-lo.Embora a visualização não tenha ajudado a responder às perguntas “o que está acontecendo” e “por que”, com sua ajuda, vimos uma correlação com os dados do monitoramento - o que significa que definitivamente temos um problema em algum lugar e precisamos procurá-lo.



Naquela época, nossa equipe de encanadores ficou sem ideias sobre onde procurar um vazamento. Felizmente, o pássaro cantou para nós que uma grande mudança na plataforma aconteceu - a versão Python mudou de 3.4 para 3.7, e esse é um campo de pesquisa enorme.

Decidimos procurar problemas relacionados a vazamentos de memória no Python 3.7 na Internet, porque com certeza alguém já encontrou esse comportamento. Ainda assim, o Python 3.7 foi publicado há muito tempo, mudamos para ele apenas com a atualização atual. Felizmente, a resposta para a nossa pergunta foi encontrada rapidamente, e também houve problemas e solicitações para corrigir o problema, e ela própria estava nas alterações feitas pelos desenvolvedores do Python.
O que aconteceu?

A partir da versão 3.7, o comportamento da classe ThreadingMixIn mudou, do qual herdamos do servidor da web para processar cada solicitação em um encadeamento separado. Na classe ThreadingMixIn, eles adicionaram a entrada de todos os threads criados em uma matriz. Devido a essas alterações, as instâncias de classe que manipulam as conexões do dispositivo não são liberadas após a conclusão e o coletor de lixo no Python não pode limpar a memória dos threads gastos. Isso foi o que levou ao crescimento linear da memória alocada em proporção direta ao número de solicitações ao nosso servidor.

Aqui está, o código insidioso do módulo Python com um grande buraco (o código no Python 3.5 é mostrado à esquerda antes das alterações, à direita - na versão 3.7, depois):



Descobrindo o motivo, eliminamos facilmente o vazamento: em nossa classe de herdeiros, alteramos o valor da bandeira que retornava o antigo comportamento, e isso é tudo - uma vitória! Os fluxos são criados como antes, sem gravar na variável de classe, mas observamos uma imagem agradável nos gráficos de monitoramento - o vazamento foi corrigido!



É bom escrever sobre isso depois de uma vitória. Provavelmente não somos os primeiros a encontrar esse problema depois de mudar para o Python 3.7, mas provavelmente não o último. Para nós mesmos, concluímos que precisamos:

  • Adote uma abordagem mais séria para avaliar as possíveis consequências de grandes mudanças, especialmente se outras decisões aplicadas dependerem de nós.
  • No caso de alterações globais na plataforma, como, por exemplo, alterar a versão do Python, verifique seu código para possíveis problemas.
  • Responda a qualquer mudança suspeita nos agendamentos de monitoramento, não apenas dos serviços de combate, mas também dos de teste. Apesar do coletor de lixo atual, também há vazamentos de memória no Python.
  • É necessário ter cuidado com ferramentas de análise de memória como tracemalloc, pois usá-las incorretamente pode piorar as coisas.
  • Você precisa estar preparado para o fato de que a detecção de vazamentos de memória exigirá paciência, perseverança e um pouco de trabalho de detetive.

Bem, gostaria de expressar minha gratidão a todos que ajudaram a lidar com o trabalho de encanamento de emergência e, mais uma vez, retornar à sua antiga capacidade de trabalho ao nosso serviço!

All Articles