El código del servidor de Instagram está escrito exclusivamente en Python. Bueno, básicamente lo es. Usamos un poco de Cython, y las dependencias incluyen una gran cantidad de código C ++ que se puede operar desde Python como con las extensiones C.

Nuestra aplicación de servidor es un monolito, que es una base de código grande que consta de varios millones de líneas e incluye varios miles de puntos finales Django (
aquí hay una charla sobre el uso de Django en Instagram). Todo esto se carga y sirve como una entidad única. Se han asignado varios servicios del monolito, pero nuestro plan no incluye una separación fuerte del monolito.
Nuestro sistema de servidor es un monolito que cambia muy a menudo. Todos los días, cientos de programadores realizan cientos de confirmaciones de código. Implementamos continuamente estos cambios, haciéndolo cada siete minutos. Como resultado, el proyecto se implementa en producción unas cien veces al día. Nos esforzamos por garantizar que pase menos de una hora entre obtener un commit en la rama maestra y desplegar el código correspondiente en producción (
aquí hay una charla sobre esto realizada en PyCon 2019).
Es muy difícil mantener esta enorme base de código monolítico, realizar cientos de confirmaciones diariamente y, al mismo tiempo, no llevarlo a un estado de caos completo. Queremos hacer de Instagram un lugar donde los programadores puedan ser productivos y puedan preparar rápidamente nuevas funciones útiles del sistema.
Este material se centra en cómo usamos el linting y la refactorización automática para facilitar la administración de una base de código Python.
Si está interesado en probar algunas de las ideas mencionadas en este material, debe saber que recientemente lo transferimos a la categoría de proyectos de código abierto
LibCST , que subyace en muchas de nuestras herramientas internas para la reestructuración de códigos y la creación automática de códigos.
→
La segunda parteLinting: documentación que aparece donde se necesita
Linting ayuda a los programadores a encontrar y diagnosticar problemas y antipatrones que los propios desarrolladores pueden no conocer sin darse cuenta en el código. Esto es importante para nosotros debido a que las ideas relevantes con respecto a la estructura del código son más difíciles de distribuir, mientras más programadores trabajan en un proyecto. En nuestro caso, estamos hablando de cientos de especialistas.
Variedades de lintingLinting es solo uno de los muchos tipos de análisis de código estático que utilizamos en Instagram.
La forma más primitiva de implementar reglas de linting es usar expresiones regulares. Las expresiones regulares son fáciles de escribir, pero Python
no es
un lenguaje "regular" . Como resultado, es muy difícil (y a veces imposible) buscar patrones de manera confiable en el código Python usando expresiones regulares.
Si hablamos de las formas más complejas y avanzadas para implementar linter, entonces hay herramientas como
mypy y
Pyre . Estos son dos sistemas para verificar estáticamente los tipos de código de Python que pueden realizar un análisis profundo del programa. Instagram usa Pyre. Estas son herramientas poderosas, pero son difíciles de expandir y personalizar.
Cuando hablamos de linting en Instagram, generalmente nos referimos a trabajar con reglas simples basadas en un árbol de sintaxis abstracta. Es precisamente algo como esto que subyace a nuestras propias reglas de enlace para el código del servidor.
Cuando Python ejecuta un módulo, comienza iniciando el analizador y pasándole el código fuente. Esto crea un árbol de análisis, un tipo de árbol de sintaxis concreta (CST). Este árbol es una representación sin pérdidas del código fuente de entrada. Cada detalle se guarda en este árbol, como comentarios, corchetes y comas. Basado en CST, puede restaurar completamente el código original.
Python Parse Tree (una variación de CST) generado por lib2to3Desafortunadamente, este enfoque lleva a la creación de un árbol complejo, lo que hace que sea difícil extraer de él información semántica que nos interese.
Python compila el árbol de análisis en un árbol de sintaxis abstracta (AST). Parte de la información sobre el código fuente se pierde durante esta conversión. Estamos hablando de "información sintáctica adicional", como comentarios, corchetes, comas. Sin embargo, la semántica del código en el AST se conserva.
Árbol de sintaxis abstracta de Python generado por el módulo astDesarrollamos
LibCST , una biblioteca que nos brinda lo mejor de los mundos de CST y AST. Proporciona una representación del código en el que se almacena toda la información al respecto (como en CST), pero es fácil extraer información semántica al respecto de dicha representación del código (como cuando se trabaja con AST).
Representación de un árbol de sintaxis LibCST específicoNuestras reglas de conexión utilizan el
árbol de sintaxis LibCST para encontrar patrones en el código. Este árbol de sintaxis, de alto nivel, es fácil de explorar, le permite deshacerse de los problemas que acompañan el trabajo con el lenguaje "irregular".
Supongamos que en un determinado módulo hay una dependencia cíclica debido al tipo de importación. Python resuelve este problema colocando comandos de importación de tipos en un bloque
if TYPE_CHECKING
. Esta es una protección contra la importación de cualquier cosa en tiempo de ejecución.
Más tarde, alguien agregó otro tipo de importación y otro bloque
if
al código. Sin embargo, quien hizo esto podría no saber que dicho mecanismo ya existe en el módulo.
¡Puede deshacerse de esta redundancia utilizando la regla linter!
Comencemos inicializando el contador de bloques "protectores" que se encuentran en el código.
class OnlyOneTypeCheckingIfBlockLintRule(CstLintRule): def __init__(self, context: Context) -> None: super().__init__(context) self.__type_checking_blocks = 0
Luego, cumpliendo la condición correspondiente, incrementamos el contador y verificamos que no haya más de un bloque en el código. Si no se cumple esta condición, generamos una advertencia en el lugar apropiado del código, llamando al mecanismo auxiliar utilizado para generar dichas advertencias.
def visit_If(self, node: cst.If) -> None: if node.test.value == "TYPE_CHECKING": self.__type_checking_blocks += 1 if self.__type_checking_blocks > 1: self.context.report( node, "More than one 'if TYPE_CHECKING' section!" )
Reglas de alineación similares funcionan mirando el árbol LibCST y recopilando información. En nuestro linter, esto se implementa utilizando el patrón Visitor. Como habrás notado, las reglas anulan los métodos de
visit
y dejan los métodos asociados con el tipo de nodo. Estos "visitantes" se llaman en un orden específico.
class MyNewLintRule(CstLintRule): def visit_Assign(self, node): ...
Se llaman métodos de visita antes de visitar descendientes de nodos. Los métodos de abandono se llaman después de visitar a todos los descendientes.Nos adherimos a los principios del trabajo, de acuerdo con los cuales las tareas simples se resuelven primero. Nuestra primera regla de linter propia se implementó en un solo archivo, contenía un "visitante" y utilizaba un estado compartido.
Un archivo, un "visitante", utilizando estado compartidoLa clase
Single Visitor
debe tener información sobre el estado y la lógica de todas nuestras reglas de enlace que no están relacionadas con ella. Además, no siempre es obvio qué estado corresponde a una regla particular. Este enfoque se muestra bien en una situación en la que hay literalmente algunas de sus propias reglas de alineación, pero tenemos alrededor de un centenar de estas reglas, lo que complica enormemente el soporte del patrón de
single-visitor
único.
Es difícil saber qué estado y lógica están asociados con cada una de las comprobaciones.Por supuesto, como una de las posibles soluciones a este problema, uno podría considerar la definición de varios "visitantes" y la organización de un esquema de trabajo tal que cada uno de ellos miraría el árbol completo cada vez. Sin embargo, esto llevaría a una seria caída en la productividad, y la interfaz es un programa que debería funcionar rápidamente.
Cada regla de linter puede atravesar repetidamente un árbol. Al procesar un archivo, las reglas se ejecutan secuencialmente. Sin embargo, este enfoque, que a menudo atraviesa el árbol, llevaría a una seria caída en el rendimiento.En lugar de implementar algo similar en nosotros mismos, nos inspiramos en los linters utilizados en los ecosistemas de otros lenguajes de programación, como
ESLint de JavaScript, y creamos un registro centralizado de "visitantes" (Registro de Visitantes).
Registro centralizado de "visitantes". Podemos determinar efectivamente qué nodo está interesado en cada regla de la interfaz, ahorrando tiempo en los nodos que no están interesados en ella.Cuando se inicializa la regla linter, todas las anulaciones de los métodos de la regla se almacenan en el registro. Cuando rodeamos el árbol, observamos a todos los "visitantes" registrados y los llamamos. Si el método no está implementado, significa que no necesita llamarlo.
Esto reduce el consumo de recursos informáticos del sistema cuando se le agregan nuevas reglas de enlace. Por lo general, verificamos con un linter una pequeña cantidad de archivos modificados recientemente. Pero podemos verificar todas las reglas en toda la base de códigos del servidor de Instagram en paralelo en solo 26 segundos.
Después de resolver los problemas de rendimiento, creamos un marco de prueba que apuntaba a adherirse a técnicas de programación avanzadas, que requieren pruebas en situaciones en las que algo debería tener algo de calidad y en situaciones en las que algo no debería tener algo de calidad debería.
class MyCustomLintRuleTest(CstLintRuleTest): RULE = MyCustomLintRule VALID = [ Valid("good_function('this should not generate a report')"), Valid("foo.bad_function('nor should this')"), ] INVALID = [ Invalid("bad_function('but this should')", "IG00"), ]
Continuación →
segunda parteEstimados lectores! ¿Usas linters?
