De la solicitud de la piscina para liberar. Informe Yandex.Taxi

Hay un período crítico en el ciclo de lanzamiento del servicio: desde el momento en que se prepara la nueva versión hasta el momento en que está disponible para los usuarios. Las acciones del equipo entre estos dos puntos de control deben ser consistentes de una versión a otra y, si es posible, automatizadas. En su informe, Sergey Pomazanov alberist describió los procesos que siguen a cada solicitud de Yandex.Taxi pool.


- Buenas tardes! Mi nombre es Sergey, soy el jefe del grupo de automatización en Yandex.Taxi. En resumen, la tarea principal de nuestro grupo es minimizar el tiempo que los desarrolladores dedican a resolver sus problemas. Esto incluye todo, desde CI hasta procesos de desarrollo y prueba.

¿Qué hace nuestro desarrollo cuando se escribe el código?

Para probar la nueva funcionalidad, primero verificamos todo localmente. Para las pruebas locales, tenemos un gran conjunto de pruebas. Si aparece un nuevo código, también debe cubrirse con pruebas.



Nuestra cobertura de prueba no es tan buena como nos gustaría, pero tratamos de mantenerla en un nivel suficiente.

Para las pruebas, utilizamos Google Test y un marco de auto escritura Pytest, con el cual probamos no solo la parte "python", sino también la "más". Nuestro marco le permite iniciar servicios, cargar datos en la base de datos antes de cada prueba, actualizar cachés, borrar todas las solicitudes externas, etc. Un marco suficientemente funcional que le permite ejecutar lo que quiera, bloquear cualquier cosa para que no obtengamos accidentalmente solicitudes fuera.

Además de las pruebas funcionales, tenemos pruebas de integración. Te permiten resolver otro problema. Si no está seguro de que su servicio interactuará correctamente con otros servicios, puede ejecutar el soporte y ejecutar un conjunto de pruebas. Hasta ahora tenemos un conjunto básico de pruebas, pero se está expandiendo lentamente.

El stand se basa en la tecnología de Docker y Docker Compose, donde cada contenedor tiene sus propios servicios, y todos interactúan entre sí. Esto sucede en un entorno aislado. Tienen su propia red aislada, su propia base de datos, su propio conjunto de datos. Y las pruebas pasan de tal manera, como si alguien está iniciando una aplicación móvil, hace clic en los botones y hace un pedido. En este momento, los autos virtuales conducen, traen al pasajero, luego el dinero se carga del pasajero, y así sucesivamente. Básicamente, todas las pruebas requieren la interacción de muchos servicios y componentes a la vez.

Naturalmente, probamos solo nuestros servicios y solo nuestros componentes, porque no debemos probar servicios externos y humedecemos todo lo externo.

El stand era lo suficientemente conveniente como para funcionar localmente y obtener un taxi de bolsillo. Puede tomar esta posición, ejecutarla en una máquina local o en una máquina virtual o cualquier otra máquina de desarrollo. Una vez lanzado el stand, puede tomar una aplicación móvil adaptada para un taxi de bolsillo, configurarla en su computadora y hacer pedidos. Todo es exactamente igual que en producción o en otro lugar. Si necesita probar la nueva funcionalidad, simplemente puede introducir su código, se recuperará y se ejecutará en todo el entorno.

Una vez más, puede simplemente tomar y ejecutar el servicio deseado. Para hacer esto, debe elevar la base de datos, llenarla con el contenido necesario o tomar una base de los entornos existentes y conectarla al servicio. Y luego puede contactarlo, hacer algunas consultas, ver si funciona correctamente o no.

Otro punto importante es la verificación de estilo. Si todo es simple para las "ventajas", usamos el formato clang y verificamos si el código coincide o no, entonces para Python usamos hasta cuatro analizadores: Flake8, Pylint, Mypy y, al parecer, autopep8.

Utilizamos estos analizadores principalmente en la entrega estándar. Si hay una oportunidad para elegir un estilo de diseño, entonces usamos el estilo de Google. Lo único que corregimos al agregar el nuestro es una verificación para ordenar las importaciones de modo que las importaciones se ordenen correctamente.

Después de haber creado el código, verificado localmente, puede hacer una solicitud de grupo. Las solicitudes de grupo se crean en GitHub.



Crear una solicitud de grupo en GitHub ofrece muchas oportunidades que TeamCity nos brinda. TeamCity ejecuta automáticamente todas las pruebas mencionadas anteriormente, las verifica automáticamente y en la solicitud del grupo se escribe sobre el estado del pasaje, ya sea que las pruebas pasaron o no. Es decir, sin visitar TeamCity, puede ver si ha pasado o no, y haciendo clic en el enlace para comprender qué salió mal y qué debe arreglarse.

Si no tiene suficientes taxis y pruebas de bolsillo, desea verificar la interacción real con algún servicio real, tenemos un entorno de prueba que repite la producción. Tenemos dos de estos entornos de prueba. Uno es para el desarrollo móvil de probadores, y el segundo es para desarrolladores. El entorno de prueba es lo más cercano posible a la producción, y si se realizan solicitudes a servicios externos, también se realizan desde entornos de prueba. La única limitación es que el entorno de prueba va a la prueba de recursos externos siempre que sea posible. Y el entorno de producción entra en producción.

Más sobre el entorno de prueba, lo hacemos simplemente a través de TeamCity. Es necesario poner la etiqueta adecuada en GitHub, y después de configurarlo, haga clic en el botón "Recopilar personalizado". Entonces lo llamamos. Luego, todas las solicitudes de grupo con esta etiqueta contendrán, y luego comenzará el ensamblaje automático de paquetes con agrupamiento.

Además de las pruebas de rutina, a veces se requieren pruebas de carga. Si está editando código que es parte de un servicio altamente cargado, podemos hacer pruebas de carga para esto. En Python, hay pocos servicios altamente cargados, algunos de ellos los reescribimos en C ++, pero aun así, aún permanecen, a veces hay un lugar para estar. La prueba de carga se realiza a través del sistema Lunapark. Utiliza Yandex.Tank, está disponible gratuitamente, puede descargarlo y verlo. El tanque le permite disparar a algún servicio, construir gráficos, hacer diferentes métodos de carga y mostrar qué carga estaba actualmente en el servicio y qué recursos utilizó. Es suficiente hacer clic en el botón a través de TeamCity, se recogerá el paquete y luego será posible rodarlo cuando sea necesario. O simplemente llénelo manualmente y ejecútelo allí.



Mientras está probando su código, uno de los desarrolladores puede en este momento comenzar a mirar su código y participar en su revisión.

A qué prestamos atención en el proceso:



Uno de los puntos importantes: la funcionalidad debe estar deshabilitada. Esto significa que no importa cuál sea el código, había errores o no, tal vez esta funcionalidad no funciona de la manera original, tal vez los gerentes querían algo más, o tal vez esta funcionalidad está tratando de poner otro servicio que no estaba listo a nuevas cargas, y necesita la capacidad de apagarlo rápidamente y poner todo en un estado normal.

También tenemos una regla que establece que cuando se implementa una nueva funcionalidad, se debe desactivar y activar solo después de que se implemente en todos los clústeres y todos los centros de datos.

No olvide que tenemos una API que utilizan las aplicaciones móviles que pueden no actualizarse durante mucho tiempo. Si hacemos algunos cambios incompatibles con versiones anteriores en nuestra API, algunas aplicaciones pueden fallar y no podemos obligar a todas las aplicaciones a simplemente descargar y actualizar. Esto afectará negativamente nuestra reputación. Por lo tanto, todas las nuevas funciones deben ser compatibles con versiones anteriores. Esto se aplica no solo a la API externa, sino también a la interna, porque no puede implementar simultáneamente todo el código en todos los centros de datos, en todas las máquinas, en todos los clústeres. En cualquier caso, tanto el código antiguo como el nuevo vivirán con nosotros al mismo tiempo. Como resultado, recibimos algunas consultas que no se pueden procesar en algún lugar, y tendremos errores.

También debe pensar en lo siguiente: si de repente su código no funciona o si escribió un nuevo microservicio en el que hay problemas potenciales, debe estar preparado para las consecuencias y poder degradarse. Mi colega hablará sobre esto en la próxima presentación.

Si realiza un cambio en los servicios altamente cargados y no tiene que esperar al final de algunas operaciones, puede hacer algunas cosas de forma asincrónica en algún lugar en segundo plano o como un proceso separado. Es mejor hacerlo de esta manera, porque un proceso separado tiene menos impacto en la producción, y el sistema funcionará más estable en general.



También es importante que todos los datos que recibimos del exterior, no debemos confiar en ellos, debemos validarlos, verificarlos de alguna manera, etc. Todos los datos que tenemos deben dividirse en grupos que hemos formado. o datos sin procesar que no pasaron la validación. Esto incluye todos los datos que podrían obtenerse de otros servicios externos o directamente de los usuarios, ya que cualquier cosa podría llegar. Quizás alguien envió especialmente una solicitud maliciosa, y todo debería verificarse con nosotros.



Todavía hay casos en que, previa solicitud, el servicio puede no responder en el momento adecuado. Tal vez la conexión se rompió o algo salió mal, puede haber muchas situaciones. La aplicación móvil no sabe lo que finalmente sucedió, solo hace una nueva solicitud.

Es muy importante que en el proceso de estas nuevas solicitudes, sin importar cuántas haya, al final todo funcione como se esperaba originalmente con una sola solicitud. No deberíamos tener ningún efecto especial. También debe tenerse en cuenta que tenemos más de un servicio, tenemos muchas máquinas, muchos centros de datos, tenemos bases distribuidas y las carreras son posibles para todos. El código debe escribirse de modo que si se ejecuta en varios lugares al mismo tiempo, para que no tengamos carreras.

Un punto igualmente importante es la capacidad de diagnosticar problemas. Los problemas siempre existen, en todo, y debe comprender dónde ocurrieron. En una situación ideal, la existencia del problema no se aprendió a través del servicio de soporte, sino a través del monitoreo. Y al analizar algunas situaciones, eventualmente podríamos entender lo que sucedió simplemente leyendo los registros sin leer el código. Incluso la persona que nunca vio el código, de modo que por los registros al final podría obtenerlo.

Y en el caso ideal, si la situación es muy complicada, debe ser capaz de verificar por los registros en qué dirección finalizó el programa y qué sucedió para simplificar en gran medida el informe. Debido a que la situación se produjo como resultado en el pasado, y ahora es poco probable que pueda reproducirse, ya no hay datos u otros datos u otras situaciones.

Si está realizando nuevas operaciones en la base de datos o creando una nueva, debe tener en cuenta que puede haber muchos datos. Tal vez escriba una cantidad infinita de registros en esta base de datos, y si no piensa en archivarlos, entonces puede haber problemas, la base de datos simplemente comenzará a crecer indefinidamente y no habrá más recursos, ni discos ni fragmentos. Es importante poder archivar datos y almacenar solo los datos operativos que se necesitan en este momento. Y también es necesario realizar consultas de índice a todas las bases de datos. Una consulta no indexada puede poner toda la producción. Una pequeña solicitud a la colección central más cargada puede poner todo. Tienes que tener mucho cuidado.

No aceptamos optimizaciones prematuras. Si alguien está tratando de hacer que algún tipo de fábrica sea un método muy universal que potencialmente maneje casos para el futuro, tal vez algún día alguien quiera expandirlo; esta no es nuestra costumbre, porque es posible que se desarrolle estará completamente equivocado, y tal vez este código eventualmente será enterrado, o tal vez no sea necesario, pero solo complica la lectura y comprensión del código. Porque leer y comprender el código es muy importante. Es importante que el código sea muy simple y fácil.

Si agrega una nueva base de datos en su código o realiza un cambio en la API, tenemos documentación que se genera parcialmente a partir del código, parcialmente en el Wiki. Esta información es importante para mantenerse al día. De lo contrario, puede confundir a alguien o causar problemas a otros desarrolladores. Porque el código está escrito solo, pero es muy compatible.

Una parte importante es la observancia del estilo general. Lo principal en este caso es la uniformidad. Cuando todo el código está escrito de manera uniforme, es fácil de entender, fácil de leer y no necesita profundizar en todos los detalles y matices. El código escrito uniformemente puede acelerar todo el proceso de desarrollo potencialmente en el futuro.

Otro punto que no verificamos específicamente para revisiones es que no estamos buscando errores. Porque el autor debe participar en la búsqueda de errores. Si hay errores durante la revisión, por supuesto, escribirán al respecto, pero no debe haber una búsqueda intencional, esto es responsabilidad exclusiva de la persona que escribe el código.

Además, cuando se escribe su código, se completa la revisión, está listo para congelarlo, pero a menudo sucede que necesita realizar acciones adicionales, migrar a la base de datos.



Para las migraciones, escribimos un script de Python que puede comunicarse con el back-end. El backend, a su vez, tiene una conexión con todas nuestras bases y puede llevar a cabo todas las operaciones necesarias. El script se inicia a través del panel de administración de inicio del script, luego se ejecuta, puede ver su registro y resultados. Y si necesita operaciones de haz a largo plazo, entonces no puede actualizar todo a la vez, debe hacerlo con fragmentos de 1000-10000 con algunas pausas, para no poner accidentalmente la base con estas operaciones.



Cuando el código está escrito, revisado, probado, todas las migraciones se llevan a cabo, puede fusionarlo con seguridad en GitHub y continuar lanzándolo.

Para algunos servicios, tenemos una regulación según la cual debemos implementar en un momento determinado, pero una parte importante de los servicios podemos implementar en cualquier momento.

Todo esto se hace con TeamCity.



Todo comienza con la construcción de paquetes. TeamCity hace git flow o su apariencia. Nos estamos alejando lentamente del flujo de git hacia nuestras mejores prácticas, que pensamos que eran más convenientes. TeamCity produce todo esto, recoge paquetes, los llena. Esperamos más cuando las pruebas pasarán en estos paquetes. Se requiere pasar las pruebas para implementar la versión. Si las pruebas fallan, primero debe resolverlo y ver qué finalmente salió mal. Las pruebas utilizadas son las mismas, regulares e integradas. Verifican el paquete ya ensamblado, listo, exactamente lo que entrará en producción. Esto es solo por si acaso, de repente hay problemas en el paquete ensamblado, de repente algo no se copia, de repente algo falta.

También existe el requisito de que creamos un ticket de lanzamiento en nuestro rastreador, donde cada desarrollador debe darse de baja de cómo probó este código, y debe contener todas las tareas que deben completarse.

Esto también se hace automáticamente en TeamCity, que revisa la lista de confirmaciones. Tenemos el requisito de que en cada confirmación debe haber una palabra clave "Relatos" seguida del nombre de la tarea. Un script escrito en Python pasa automáticamente por todo esto, compila una lista de tareas que se han resuelto, forma una lista de autores y crea un ticket de lanzamiento, instando a todos los autores a darse de baja de sus pruebas y confirmar que están listos para "ir" en el lanzamiento.



Cuando todos están listos, se recolectan las confirmaciones y luego se implementa, primero, en el preestable. Esta es una pequeña parte de la producción. Para cada servicio se utilizan varios centros de datos, en cada centro de datos puede haber varias máquinas. Una de las máquinas es preestable y el código se implementa primero solo en una o un par de máquinas.

Cuando el código se desinfla, seguimos los gráficos, los registros y lo que sucede al final del servicio. Si todo está bien, si los gráficos muestran que todo es estable, y todos han comprobado que su funcionalidad funciona como debería, entonces pasa al resto del entorno, lo que llamamos estable. Al llegar al establo, todo es igual: miramos los gráficos, los registros y verificamos que todo esté bien para nosotros.

El lanzamiento ha pasado, todo está bien. Y si algo salió mal, si de repente problemas?



Recopilamos la revisión. Se realiza con el mismo principio que git flow, es decir, una rama de la rama maestra. Se crea una solicitud de grupo separada desde el maestro, que realiza correcciones, y luego el script lanzado desde TeamCity la congela, realiza todas las operaciones necesarias, recopila todos los paquetes de la misma manera y continúa.



Al final, me gustaría hablar sobre la dirección en la que nos estamos moviendo. Estamos avanzando hacia un repositorio único, cuando muchos servicios viven en un repositorio a la vez. Cada uno de ellos tiene cálculos independientes: en pruebas, en lanzamientos. Para las solicitudes de grupo, incluso cuando se utiliza TeamCity, verificamos qué archivos se vieron afectados, a qué servicios pertenecen. De acuerdo con el gráfico de dependencia, determinamos qué pruebas debemos ejecutar en última instancia y qué verificar. Nos esforzamos por lograr el máximo aislamiento de los servicios entre nosotros. Hasta ahora, no funciona muy bien, pero nos esforzamos por lograr que muchos servicios puedan vivir en un repositorio, tener un código común y que esto no cause problemas y simplifique la vida del desarrollo. Eso es todo, gracias a todos.

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


All Articles