Usando módulos rigorosos em projetos Python em larga escala: experiência no Instagram. Parte 1

Estamos publicando a primeira parte da tradução do próximo artigo de uma série sobre como o Instagram funciona com o Python. O primeiro artigo desta série falou sobre os recursos do código do servidor do Instagram, que é um monólito que muda frequentemente e como as ferramentas de verificação de tipo estático ajudam a gerenciar esse monólito. O segundo material é sobre digitar a API HTTP. Aqui, falaremos sobre abordagens para resolver alguns dos problemas que o Instagram encontrou usando o Python em seu projeto. O autor do material espera que a experiência do Instagram seja útil para quem pode encontrar problemas semelhantes.



Visão geral da situação


Vejamos o seguinte módulo, que, à primeira vista, parece completamente inocente:

import re from mywebframework import db, route VALID_NAME_RE = re.compile("^[a-zA-Z0-9]+$") @route('/') def home():     return "Hello World!" class Person(db.Model):     name: str 

Qual código será executado se alguém importar este módulo?

  • Primeiro, o código associado à expressão regular que compila a string em um objeto de modelo será executado.
  • Em seguida, o decorador @route será executado. Se confiarmos no que vemos, podemos assumir que aqui, talvez, a representação correspondente esteja registrada no sistema de mapeamento de URL. Isso significa que a importação usual deste módulo leva ao fato de que em outro lugar o estado global do aplicativo está mudando.
  • Agora vamos executar todo o código do corpo da classe Person . Pode conter qualquer coisa. O Model classe base pode ter um método de metaclasse ou __init_subclass__ , que, por sua vez, pode conter algum outro código que é executado ao importar nosso módulo.

Problema # 1: lenta inicialização e reinicialização do servidor


A única linha de código para este módulo que (possivelmente) não é executada quando é importada é return "Hello World!" . É verdade que com certeza não podemos dizer isso! Como resultado, verifica-se que, importando este módulo simples que consiste em oito linhas (e ainda nem o utiliza em nosso programa), podemos fazer com que centenas ou mesmo milhares de linhas de código Python sejam lançadas. E isso sem mencionar que a importação deste módulo causa uma modificação do mapeamento de URL global localizado em algum outro local do programa.

O que fazer Antes de nós, isso faz parte da conseqüência do fato de o Python ser uma linguagem interpretada dinâmica. Isso nos permite resolver com êxito vários problemas usando métodos de metaprogramação . Mas o que há de errado com esse código?

De fato, esse código está em perfeita ordem. Isso ocorre desde que alguém o use em bases de código relativamente pequenas, nas quais pequenas equipes de programadores trabalham. Esse código não causa problemas, desde que quem o utilize tenha a garantia de manter um certo nível de disciplina na maneira como exatamente os recursos do Python são usados. Mas alguns aspectos desse dinamismo podem se tornar um problema se houver milhões de linhas de código no projeto nas quais centenas de programadores estão trabalhando, muitos dos quais não têm conhecimento profundo de Python.

Por exemplo, um dos grandes recursos do Python é a velocidade das etapas envolvidas no desenvolvimento em fases. Ou seja, o resultado das alterações no código pode ser visto literalmente imediatamente após essas alterações, sem a necessidade de compilar o código. Mas se estamos falando de um projeto de vários milhões de linhas (e de um diagrama de dependência bastante confuso deste projeto), então esse sinal de adição do Python começa a se transformar em um sinal de menos.

Demora mais de 20 segundos para iniciar nosso servidor. E, às vezes, quando não prestamos a devida atenção à otimização, esse tempo aumenta para cerca de um minuto. Isso significa que o desenvolvedor precisa de 20 a 60 segundos para ver os resultados das alterações feitas no código. Isso se aplica ao que você pode ver no navegador e até à velocidade de execução de testes de unidade. Infelizmente, esse tempo é suficiente para uma pessoa se distrair com alguma coisa e esquecer o que havia feito antes. A maior parte desse tempo, literalmente, é gasta na importação de módulos e na criação de funções e classes.

De certa forma, é o mesmo que esperar pelos resultados da compilação de um programa escrito em outro idioma. Mas geralmente a compilação pode ser feita de forma incremental . O ponto é que você pode recompilar apenas o que mudou e o que depende diretamente do código alterado. Como resultado, geralmente a compilação de projetos, executada após pequenas alterações, é rápida. Mas, ao trabalhar com o Python, como os comandos de importação podem ter qualquer tipo de efeitos colaterais, não há uma maneira confiável e segura de reiniciar incrementalmente o servidor. Ao mesmo tempo, a escala das alterações não é importante e cada vez que precisamos reiniciar completamente o servidor, importando todos os módulos, recriando todas as classes e funções, recompilando todas as expressões regulares e assim por diante. Normalmente, a partir do momento da última reinicialização do servidor, 99% do código não foi alterado, mas ainda precisamos fazer a mesma coisa repetidamente para inserir as alterações.

Além de retardar os desenvolvedores, isso leva ao desperdício improdutivo de grandes quantidades de recursos do sistema. O fato é que estamos trabalhando em um modo de implantação contínua de alterações, o que significa recarregamento constante do código do servidor de produção.

Por uma questão de fato, aqui está o nosso primeiro problema: inicialização e reinicialização lenta do servidor. Esse problema surge devido ao fato de o sistema precisar executar constantemente uma grande quantidade de ações repetitivas durante a importação do código.

Problema nº 2: efeitos colaterais de comandos de importação não seguros


Aqui está outra tarefa que, como se viu, os desenvolvedores geralmente resolvem ao importar módulos. Isso está carregando configurações do armazenamento em rede de configurações:

 MY_CONFIG = get_config_from_network_service() 

Além de diminuir a inicialização do servidor, também é inseguro. Se o serviço de rede não estiver disponível, isso não levará apenas ao fato de recebermos mensagens de erro sobre a incapacidade de atender a algumas solicitações. Isso fará com que o servidor falhe ao iniciar.

Vamos engrossar as cores e imaginar que alguém tenha adicionado ao módulo responsável pela inicialização de um importante serviço de rede, algum código que é executado durante a importação. O desenvolvedor simplesmente não sabia onde adicionar esse código a ele, então ele o colocou em um módulo importado nos estágios iniciais de inicialização do servidor. Verificou-se que esse esquema funciona, portanto a solução foi considerada bem-sucedida e o trabalho continuou.

Mas então alguém adicionou em outro lugar a equipe de importação, que à primeira vista era inofensiva. Como resultado, através de uma cadeia de importações com uma profundidade de doze módulos, isso levou ao fato de que o módulo que baixava as configurações da rede agora é importado para o módulo que inicializa o serviço de rede correspondente.

Agora acontece que estamos tentando usar o serviço antes que ele seja inicializado. O sistema trava naturalmente. Na melhor das hipóteses, se estamos falando de um sistema em que as interações são completamente determinísticas, isso pode levar ao fato de que o desenvolvedor passará uma ou duas horas tentando descobrir como uma pequena mudança levou a uma falha em algo, com ele, parece desconectado. Mas em situações mais complexas, isso pode levar a uma "queda" do projeto na produção. No entanto, não existem maneiras universais de usar o linter para combater esses problemas ou para evitá-los.

A raiz do problema está em dois fatores, cuja interação leva a consequências devastadoras:

  1. O Python permite que os módulos tenham efeitos colaterais arbitrários e inseguros que ocorrem durante a importação.
  2. A ordem de importação do código não está definida explicitamente e não é controlada. Na escala de um projeto, um tipo de "importação abrangente" é o que consiste nos comandos de importação contidos em todos os módulos. Nesse caso, a ordem de importação dos módulos pode variar dependendo do ponto de entrada do sistema.

Para continuar ...

Caros leitores! Você encontrou problemas em relação à inicialização lenta de projetos Python?


Source: https://habr.com/ru/post/pt475240/


All Articles