Estamos publicando la primera parte de la traducción de otro artículo de la serie sobre cómo funciona Instagram con Python. El
primer artículo de esta serie habló sobre las características del código del servidor de Instagram, que es un monolito que cambia con frecuencia y cómo las herramientas de verificación de tipos estáticos ayudan a administrar este monolito.
El segundo material se trata de escribir la API HTTP. Aquí hablaremos sobre los enfoques para resolver algunos de los problemas que Instagram encontró al usar Python en su proyecto. El autor del material espera que la experiencia de Instagram sea útil para aquellos que puedan tener problemas similares.

Resumen de la situación
Veamos el siguiente módulo, que, a primera 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
¿Qué código se ejecutará si alguien importa este módulo?
- Primero, se ejecutará el código asociado con la expresión regular que compila la cadena en un objeto de plantilla.
- Luego se
@route
decorador @route
. Si confiamos en lo que vemos, entonces podemos suponer que aquí, tal vez, la representación correspondiente se registra en el sistema de mapeo de URL. Esto significa que la importación habitual de este módulo lleva al hecho de que en otro lugar el estado global de la aplicación está cambiando. - Ahora vamos a ejecutar todo el código del cuerpo de la clase
Person
. Puede contener cualquier cosa. El Model
clase base puede tener una metaclase o __init_subclass__
método __init_subclass__
, que, a su vez, puede contener algún otro código que se ejecute al importar nuestro módulo.
Problema # 1: inicio lento del servidor y reinicio
La única línea de código para este módulo que (posiblemente) no se ejecuta cuando se importa es
return "Hello World!"
. Es cierto, ¡con certeza no podemos decir esto! Como resultado, resulta que al importar este módulo simple que consta de ocho líneas (y aún sin usarlo en nuestro programa), podemos provocar el lanzamiento de cientos o incluso miles de líneas de código Python. Y esto sin mencionar que la importación de este módulo provoca una modificación de la asignación de URL global ubicada en algún otro lugar del programa.
Que hacer Ante nosotros es parte de la consecuencia del hecho de que Python es un lenguaje interpretado dinámico. Esto nos permite resolver con éxito varios problemas utilizando métodos de
metaprogramación . Pero, sin embargo, ¿qué tiene de malo este código?
De hecho, este código está en perfecto orden. Esto es así siempre y cuando alguien lo use en bases de código relativamente pequeñas, en las que trabajan pequeños equipos de programadores. Este código no causa problemas siempre y cuando la persona que lo usa tenga garantizado mantener un cierto nivel de disciplina en cómo se usan exactamente las características de Python. Pero algunos aspectos de este dinamismo pueden convertirse en un problema si hay millones de líneas de código en el proyecto en las que están trabajando cientos de programadores, muchos de los cuales no tienen un conocimiento profundo de Python.
Por ejemplo, una de las grandes características de Python es la velocidad de los pasos involucrados en el desarrollo por fases. Es decir, el resultado de los cambios en el código se puede ver literalmente inmediatamente después de realizar dichos cambios, sin la necesidad de compilar el código. Pero si estamos hablando de un proyecto con un tamaño de varios millones de líneas (y un diagrama de dependencia bastante confuso de este proyecto), entonces esta ventaja de Python comienza a convertirse en una desventaja.
Se tarda más de 20 segundos en iniciar nuestro servidor. Y a veces, cuando no prestamos la debida atención a la optimización, este tiempo aumenta a aproximadamente un minuto. Esto significa que el desarrollador necesita 20-60 segundos para ver los resultados de los cambios realizados en el código. Esto se aplica a lo que puede ver en el navegador, e incluso a la velocidad de ejecución de las pruebas unitarias. Desafortunadamente, esta vez es suficiente para que una persona se distraiga con algo y se olvide de lo que había hecho antes. La mayor parte de este tiempo, literalmente, se gasta en importar módulos y crear funciones y clases.
En cierto modo, esto es lo mismo que esperar los resultados de compilar un programa escrito en algún otro idioma. Pero por lo general, la compilación se puede hacer de forma
incremental . El punto es que puede recompilar solo lo que ha cambiado y lo que depende directamente del código cambiado. Como resultado, generalmente la compilación de proyectos, realizada después de hacer pequeños cambios, es rápida. Pero cuando se trabaja con Python, debido a que los comandos de importación pueden tener cualquier tipo de efectos secundarios, no hay una manera confiable y segura de reiniciar el servidor de forma incremental. Al mismo tiempo, la escala de los cambios no es importante y cada vez que tengamos que reiniciar completamente el servidor, importando todos los módulos, volviendo a crear todas las clases y funciones, volviendo a compilar todas las expresiones regulares, etc. Por lo general, desde el momento del último reinicio del servidor, el 99% del código no ha cambiado, pero aún tenemos que hacer lo mismo una y otra vez para ingresar los cambios.
Además de ralentizar a los desarrolladores, esto lleva al desperdicio improductivo de grandes cantidades de recursos del sistema. El hecho es que estamos trabajando en un modo de implementación continua de cambios, lo que significa una recarga constante del código del servidor de producción.
De hecho, aquí está nuestro primer problema: inicio lento y reinicio del servidor. Este problema surge debido al hecho de que el sistema tiene que realizar constantemente una gran cantidad de acciones repetitivas durante la importación de código.
Problema # 2: Efectos secundarios de los comandos de importación inseguros
Aquí hay otra tarea que, como resultó, los desarrolladores a menudo resuelven al importar módulos. Esto es cargar configuraciones desde el almacenamiento de red de configuraciones:
MY_CONFIG = get_config_from_network_service()
Además de ralentizar el inicio del servidor, tampoco es seguro. Si el servicio de red no está disponible, esto no solo conducirá al hecho de que recibimos mensajes de error con respecto a la imposibilidad de cumplir con algunas solicitudes. Esto hará que el servidor no se inicie.
Espesemos los colores e imaginemos que alguien agregó al módulo responsable de inicializar un servicio de red importante, un código que se ejecuta durante la importación. El desarrollador simplemente no sabía dónde agregarle este código, por lo que lo colocó en un módulo que se importa en las primeras etapas de inicio del servidor. Resultó que este esquema funciona, por lo que la solución se consideró exitosa y el trabajo continuó.
Pero luego alguien más agregó en otro lugar el equipo de importación, que a primera vista era inofensivo. Como resultado, a través de una cadena de importaciones con una profundidad de doce módulos, esto condujo al hecho de que el módulo que descarga la configuración de la red ahora se importa al módulo que inicializa el servicio de red correspondiente.
Ahora resulta que estamos tratando de usar el servicio antes de que se inicialice. El sistema se bloquea naturalmente. En el mejor de los casos, si estamos hablando de un sistema en el que las interacciones son completamente deterministas, esto puede llevar al hecho de que el desarrollador pasará una o dos horas descubriendo cómo un cambio menor condujo a una falla en algo, con él, Parece desconectado. Pero en situaciones más complejas, esto puede conducir a una "caída" del proyecto en producción. Sin embargo, no hay formas universales de usar
linter para combatir tales problemas o prevenirlos.
La raíz del problema radica en dos factores, cuya interacción conduce a consecuencias devastadoras:
- Python permite que los módulos tengan efectos secundarios arbitrarios e inseguros que ocurren durante la importación.
- El orden de importación del código no se establece explícitamente y no se controla. En la escala de un proyecto, una especie de "importación integral" consiste en los comandos de importación contenidos en todos los módulos. En este caso, el orden de importación de los módulos puede variar según el punto de entrada del sistema utilizado.
Continuará ...Estimados lectores! ¿Ha encontrado problemas con respecto al inicio lento de proyectos de Python?
