De la demande de pool à la libération. Signaler Yandex.Taxi

Il y a une période critique dans le cycle de publication d'un service - du moment où une nouvelle version est préparée au moment où elle devient disponible pour les utilisateurs. Les actions d'équipe entre ces deux points de contrôle doivent être cohérentes d'une version à l'autre et, si possible, automatisées. Dans son rapport, l'albériste Sergey Pomazanov a décrit les processus qui suivent chaque demande de pool Yandex.Taxi.


- Bonsoir! Je m'appelle Sergey, je suis le chef du groupe d'automatisation chez Yandex.Taxi. En bref, la tâche principale de notre groupe est de minimiser le temps que les développeurs passent à résoudre leurs problèmes. Cela comprend tout, du CI aux processus de développement et de test.

Que fait notre développement lorsque le code est écrit?

Pour tester la nouvelle fonctionnalité, nous vérifions d'abord tout localement. Pour les tests locaux, nous avons un large éventail de tests. Si un nouveau code apparaît, il doit également être couvert par des tests.



Notre couverture de test n'est pas aussi bonne que nous le souhaiterions, mais nous essayons de la maintenir à un niveau suffisant.

Pour les tests, nous utilisons Google Test et un framework de pytest auto-écrit, avec lequel nous testons non seulement la partie "python", mais aussi la partie "plus". Notre cadre vous permet de démarrer des services, de télécharger des données dans la base de données avant chaque test, de mettre à jour les caches, d'effacer toutes les demandes externes, etc. Un cadre suffisamment fonctionnel qui vous permet d'exécuter tout ce que vous voulez, de verrouiller tout pour que nous n'obtenions aucun accident demandes à l'extérieur.

En plus des tests fonctionnels, nous avons des tests d'intégration. Ils vous permettent de résoudre un autre problème. Si vous n'êtes pas sûr que votre service interagira correctement avec d'autres services, vous pouvez exécuter le stand et exécuter un ensemble de tests. Jusqu'à présent, nous avons un ensemble de tests de base, mais il se développe lentement.

Le stand est construit sur la technologie de Docker et Docker Compose, où chaque conteneur a ses propres services, et ils interagissent tous les uns avec les autres. Cela se produit dans un environnement isolé. Ils ont leur propre réseau isolé, leur propre base de données, leur propre ensemble de données. Et les tests réussissent de la sorte, comme si quelqu'un lance une application mobile, clique sur des boutons, passe une commande. En ce moment, les voitures virtuelles conduisent, amènent le passager, puis l'argent est débité du passager, etc. Fondamentalement, tous les tests nécessitent l'interaction de nombreux services et composants à la fois.

Naturellement, nous testons uniquement nos services et uniquement nos composants, car nous ne devons pas tester les services externes, et nous mouillons tout ce qui est externe.

Le stand était assez pratique pour courir localement et en sortir un taxi de poche. Vous pouvez prendre cette position, l'exécuter sur une machine locale ou sur une machine virtuelle ou toute autre machine de développement. Après avoir lancé le stand, vous pouvez prendre une application mobile adaptée pour un taxi de poche, la configurer sur votre ordinateur et passer des commandes. Tout est exactement le même qu'en production ou ailleurs. Si vous avez besoin de tester la nouvelle fonctionnalité, vous pouvez simplement glisser votre code, il reprendra et fonctionnera dans tout l'environnement.

Encore une fois, vous pouvez simplement prendre et exécuter le service souhaité. Pour ce faire, vous devez augmenter la base de données, la remplir avec le contenu nécessaire ou prendre une base dans des environnements existants et la connecter au service. Et puis vous pouvez simplement le contacter, faire quelques requêtes, voir si cela fonctionne correctement ou non.

Un autre point important est la vérification de style. Si tout est simple pour les «plus», nous utilisons le format clang et vérifions si le code le correspond ou non, alors pour Python nous utilisons jusqu'à quatre analyseurs: Flake8, Pylint, Mypy et, semble-t-il, autopep8.

Nous utilisons ces analyseurs principalement dans la livraison standard. S'il est possible de choisir un style de conception, nous utilisons le style Google. La seule chose que nous avons corrigée en ajoutant la nôtre est une vérification du tri des importations afin que les importations soient triées correctement.

Après avoir créé le code, vérifié localement, vous pouvez faire une demande de pool. Les demandes de pool sont créées sur GitHub.



La création d'une demande de pool sur GitHub offre de nombreuses opportunités que TeamCity nous offre. TeamCity exécute automatiquement tous les tests mentionnés ci-dessus, les vérifie automatiquement et dans la demande de pool elle-même, il est écrit sur l'état du passage, que les tests aient réussi ou non. Autrement dit, sans visiter TeamCity, vous pouvez voir si elle a réussi ou non, et en cliquant sur le lien pour comprendre ce qui s'est mal passé et ce qui doit être corrigé.

Si vous n'avez pas assez de taxis de poche et de tests, vous voulez vérifier l'interaction réelle avec un service réel, nous avons un environnement de test qui répète la production. Nous avons deux de ces environnements de test. Le premier est destiné au développement mobile des testeurs et le second aux développeurs. L'environnement de test est aussi proche que possible de la production, et si des demandes sont adressées à des services externes, elles le sont également à partir d'environnements de test. La seule limitation est que l'environnement de test est mis à l'essai des ressources externes chaque fois que possible. Et l'environnement de production entre en production.

Pour en savoir plus sur l'environnement de test, nous le faisons tout simplement via TeamCity. Il est nécessaire de mettre l'étiquette appropriée sur GitHub, et une fois qu'il est défini, cliquez sur le bouton "Collecter personnalisé". Nous l'appelons donc. Ensuite, toutes les demandes de pool avec cette étiquette se poursuivront, puis l'assemblage automatique des packages avec le clustering commencera.

En plus des tests de routine, des tests de charge sont parfois requis. Si vous modifiez du code faisant partie d'un service très chargé, nous pouvons effectuer des tests de chargement pour cela. En Python, il existe peu de services très chargés, certains d'entre eux ont été réécrits en C ++, mais néanmoins, ils restent, parfois il y a de la place pour être. Les tests de charge ont lieu via le système Lunapark. Il utilise Yandex.Tank, il est disponible gratuitement, vous pouvez le télécharger et le regarder. Le réservoir vous permet de tirer sur certains services, de créer des graphiques, d'effectuer différentes méthodes de chargement et de montrer quelle charge était actuellement sur le service et quelles ressources il a utilisées. Il suffit de cliquer sur le bouton via TeamCity, le colis sera récupéré, puis il sera possible de le rouler si nécessaire. Ou remplissez-le simplement et exécutez-le manuellement.



Pendant que vous testez votre code, l'un des développeurs peut à ce moment commencer à regarder votre code et à procéder à sa révision.

Ce à quoi nous prêtons attention dans le processus:



L'un des points importants - la fonctionnalité doit être désactivée. Cela signifie que quel que soit le code, il y avait des bogues ou non, peut-être que cette fonctionnalité ne fonctionne pas comme prévu à l'origine, peut-être que les gestionnaires voulaient autre chose, ou peut-être que cette fonctionnalité essayait de mettre un autre service qui n'était pas prêt à de nouvelles charges, et vous devez pouvoir l'éteindre rapidement et mettre tout dans un état normal.

Nous avons également une règle selon laquelle lors du déploiement de nouvelles fonctionnalités, elles doivent être désactivées et activées uniquement après leur déploiement dans tous les clusters et tous les centres de données.

N'oubliez pas que nous avons une API utilisée par les applications mobiles qui peuvent ne pas être mises à jour pendant longtemps. Si nous apportons des modifications incompatibles dans notre API, certaines applications peuvent tomber en panne et nous ne pouvons pas forcer toutes les applications à simplement télécharger et mettre à jour. Cela affectera négativement notre réputation. Par conséquent, toutes les nouvelles fonctionnalités doivent être rétrocompatibles. Cela s'applique non seulement à l'API externe, mais également à l'API interne, car vous ne pouvez pas déployer simultanément tout le code vers tous les centres de données, toutes les machines, tous les clusters. Dans tous les cas, l'ancien code et le nouveau cohabiteront avec nous en même temps. Par conséquent, nous obtenons des requêtes qui ne peuvent pas être traitées quelque part et nous aurons des erreurs.

Vous devriez également penser à la chose suivante: si soudainement votre code ne fonctionne pas ou si vous avez écrit un nouveau microservice dans lequel il y a des problèmes potentiels, vous devez être préparé aux conséquences et être capable de se dégrader. Mon collègue en parlera lors de la prochaine présentation.

Si vous modifiez des services hautement chargés et que vous n'avez pas à attendre la fin de certaines opérations, vous pouvez effectuer certaines tâches de manière asynchrone quelque part en arrière-plan ou en tant que processus distinct. Il vaut mieux le faire de cette façon, car un processus distinct a moins d'impact sur la production et le système fonctionnera globalement plus stable.



Il est également important que toutes les données que nous recevons de l'extérieur, nous ne devons pas leur faire confiance, nous devons les valider, les vérifier d'une manière ou d'une autre, etc. Toutes les données que nous avons doivent être divisées en groupes que nous avons formés ou des données brutes qui n'ont pas été validées. Cela inclut toutes les données qui pourraient potentiellement être obtenues à partir d'autres services externes ou directement auprès des utilisateurs, car tout pourrait potentiellement venir. Peut-être que quelqu'un a spécialement envoyé une demande malveillante, et tout devrait être vérifié avec nous.



Il existe encore des cas où, sur demande, le service peut ne pas répondre au bon moment. Peut-être que la connexion s'est rompue ou que quelque chose s'est mal passé, il peut y avoir de nombreuses situations. L'application mobile ne sait pas ce qui s'est finalement passé, fait juste une nouvelle demande.

Il est très important que, dans le processus de ces nouvelles demandes, quel que soit le nombre, au final, tout fonctionne comme prévu à l'origine avec une seule demande. Nous ne devons pas avoir d'effets spéciaux. Il faut également garder à l'esprit que nous avons plus d'un service, nous avons de nombreuses machines, de nombreux centres de données, nous avons des bases distribuées et des courses sont possibles pour tout le monde. Le code doit être écrit de façon à ce qu'il s'exécute à plusieurs endroits en même temps afin que nous n'ayons pas de courses.

Un point tout aussi important est la capacité de diagnostiquer les problèmes. Les problèmes existent toujours, en tout, et vous devez comprendre où ils se sont produits. Dans une situation idéale, l'existence du problème n'a pas été apprise par le service d'assistance, mais par le suivi. Et en analysant certaines situations, nous pourrions éventuellement comprendre ce qui s'est passé en lisant simplement les journaux sans lire le code. Même la personne qui n'a jamais vu le code, de sorte que par les journaux à la fin, il pourrait l'obtenir.

Et dans le cas idéal, si la situation est très compliquée, vous devez être en mesure de vérifier par les journaux dans quelle direction le programme s'est finalement déroulé, et ce qui est arrivé pour simplifier considérablement le débriefing. Parce que la situation est allée à la suite dans le passé, et maintenant il est peu probable qu'elle puisse se reproduire, il n'y a déjà pas de données ou d'autres données ou d'autres situations.

Si vous effectuez de nouvelles opérations dans la base de données ou en créez une nouvelle, vous devez considérer qu'il peut y avoir beaucoup de données. Vous allez peut-être écrire un nombre infini d'enregistrements dans cette base de données, et si vous ne pensez pas à les archiver, il peut y avoir des problèmes, la base de données commencera simplement à se développer indéfiniment, et il n'y aura plus de ressources, pas de disques et de partitionnement. Il est important de pouvoir archiver les données et de stocker uniquement les données opérationnelles nécessaires pour le moment. Et il est également nécessaire d'effectuer des requêtes d'index sur toutes les bases de données. Une requête non indexée peut mettre toute la production. Une petite demande à la collection centrale la plus chargée peut tout mettre. Vous devez être très prudent.

Nous n'acceptons pas les optimisations prématurées. Si quelqu'un essaie de faire une sorte d'usine une méthode très universelle qui gère potentiellement les cas pour l'avenir, peut-être qu'un jour quelqu'un voudra l'étendre - ce n'est pas notre coutume, car il est possible qu'elle se développe ce sera complètement faux, et peut-être que ce code finira par être enterré, ou peut-être que tout cela n'est pas nécessaire, mais complique seulement la lecture et la compréhension du code. Parce que lire et comprendre le code est très important. Il est important que le code soit très simple et facile.

Si vous ajoutez une nouvelle base de données dans votre code ou apportez une modification à l'API, nous avons une documentation qui est partiellement générée à partir du code, partiellement réalisée sur le Wiki. Ces informations sont importantes pour rester à jour. Sinon, cela peut induire quelqu'un en erreur ou causer des problèmes à d'autres développeurs. Parce que le code est écrit seul, mais il est beaucoup pris en charge.

Une partie importante est le respect du style général. L'essentiel dans ce cas est l'uniformité. Lorsque tout le code est écrit de manière uniforme, il est facile à comprendre, facile à lire et vous n'avez pas besoin de vous plonger dans tous les détails et nuances. Un code uniformément écrit peut accélérer le processus de développement entier potentiellement à l'avenir.

Un autre point que nous ne vérifions pas spécifiquement pour les avis est que nous ne recherchons pas de bugs. Parce que l'auteur doit être engagé dans la recherche de bugs. S'il y a des bogues lors de la révision, bien sûr, ils écriront à ce sujet, mais il ne devrait pas y avoir de recherche ciblée, c'est entièrement la responsabilité de la personne qui écrit le code.

De plus, lorsque votre code est écrit, la révision est terminée, vous êtes prêt à le geler, mais il arrive souvent que vous deviez effectuer des actions supplémentaires, migrer vers la base de données.



Pour les migrations, nous écrivons un script Python qui peut communiquer avec le backend. Le backend, quant à lui, est connecté à toutes nos bases et peut effectuer toutes les opérations nécessaires. Le script est lancé via le panneau d'administration de lancement de script, puis il est exécuté, vous pouvez voir son journal et ses résultats. Et si vous avez besoin d'opérations de faisceau à long terme, vous ne pouvez pas tout mettre à jour en même temps, vous devez le faire avec des morceaux de 1000 à 10000 avec quelques pauses, afin de ne pas accidentellement mettre la base avec ces opérations.



Lorsque le code est écrit, révisé, testé, toutes les migrations sont effectuées, vous pouvez le fusionner en toute sécurité dans GitHub et continuer à le publier.

Pour certains services, nous avons une réglementation selon laquelle nous devons déployer à un certain moment, mais une partie importante des services que nous pouvons déployer à tout moment.

Tout cela se fait avec TeamCity.



Tout commence par la construction de packages. TeamCity ne git flow ou son apparence. Nous nous éloignons lentement de Git Flow vers nos meilleures pratiques, qui nous semblaient plus pratiques. TeamCity produit tout cela, collecte les paquets, les remplit. Nous attendons plus loin lorsque les tests passeront sur ces packages. La réussite des tests est nécessaire pour déployer la version. Si les tests échouent, vous devez d'abord le comprendre et voir ce qui a finalement mal tourné. Les tests utilisés sont les mêmes, réguliers et d'intégration. Ils vérifient le paquet déjà assemblé, prêt, exactement ce qui va entrer en production. C'est juste au cas où, soudainement, il y a des problèmes dans le paquet assemblé, tout à coup quelque chose n'est pas copié, tout à coup quelque chose s'est avéré être manquant.

Il est également nécessaire de créer un ticket de sortie dans notre tracker, où chaque développeur doit se désabonner de la façon dont il a testé ce code, et il doit contenir toutes les tâches qui doivent être effectuées.

Cela se fait également automatiquement dans TeamCity, qui passe par la liste des commits. Nous avons exigé que dans chaque commit, il y ait un mot-clé «Relates» suivi du nom de la tâche. Un script écrit en Python passe automatiquement par tout cela, compile une liste de tâches qui ont été résolues, forme une liste d'auteurs et crée un ticket de version, exhortant tous les auteurs à se désabonner de leurs tests et à confirmer qu'ils sont prêts à "aller" dans la version.



Lorsque tout le monde est prêt, les confirmations sont collectées, puis le déploiement a lieu, d'abord - dans la pré-écurie. Il s'agit d'une petite partie de la production. Pour chaque service, plusieurs centres de données sont utilisés, dans chaque centre de données il peut y avoir plusieurs machines. L'une des machines est une pré-stable et le code n'est déployé en premier que sur une ou deux machines.

Lorsque le code est dégonflé, nous suivons les graphiques, les journaux et ce qui se passe finalement sur le service. Si tout va bien, si les graphiques montrent que tout est stable et que tout le monde a vérifié que sa fonctionnalité fonctionne comme il se doit, alors il roule sur le reste de l'environnement, que nous appelons stable. Lors du déploiement à l'écurie, tout est pareil: nous regardons les graphiques, les logs et vérifions que tout va bien pour nous.

Le déploiement est passé, tout va bien. Et si quelque chose tournait mal, si soudainement des problèmes?



Nous collectons le correctif. Cela se fait sur le même principe que git flow, c'est-à-dire une branche de la branche principale. Une demande de pool distincte est créée à partir du maître, qui effectue des corrections, puis le script lancé à partir de TeamCity la fige, effectue toutes les opérations nécessaires, collecte tous les packages de la même manière et continue.



En fin de compte, je voudrais parler de la direction dans laquelle nous allons. Nous nous dirigeons vers un référentiel unique, lorsque de nombreux services vivent dans un seul référentiel à la fois. Chacun d'eux a des calculs indépendants: dans les tests, dans les versions. Pour les demandes de pool, même lorsque TeamCity est utilisé, nous vérifions quels fichiers ont été affectés, à quels services ils appartiennent. Selon le graphique des dépendances, nous déterminons quels tests nous devons finalement exécuter et ce qu'il faut vérifier. Nous nous efforçons d'isoler au maximum les services les uns des autres. Jusqu'à présent, cela ne fonctionne pas très bien, mais nous nous efforçons de le faire pour que de nombreux services puissent vivre dans un référentiel, avoir un code commun, et que cela ne cause pas de problèmes et simplifie la vie du développement. C'est tout, merci à tous.

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


All Articles