Go Product Development: One Project History



Bonjour à tous! Je m'appelle Maxim Ryndin, je suis le chef d'équipe de deux équipes de Gett - Billing and Infrastructure. Je veux parler du développement Web de produits, que Gett utilise principalement Go. Je vais vous dire comment en 2015-2017 nous sommes passés à cette langue, pourquoi nous l'avons choisie, quels problèmes nous avons rencontrés lors de la transition et quelles solutions nous avons trouvées. Et je vais vous parler de la situation actuelle dans le prochain article.

Pour ceux qui ne connaissent pas: Gett est un service de taxi international qui a été fondé en Israël en 2011. Gett est maintenant représenté dans 4 pays: Israël, la Grande-Bretagne, la Russie et les États-Unis. Les principaux produits de notre société sont les applications mobiles pour les clients et les conducteurs, un portail Web pour les clients d'entreprise où vous pouvez commander une voiture et un tas de panneaux d'administration internes à travers lesquels nos employés établissent des plans tarifaires, connectent de nouveaux conducteurs, surveillent les cas de fraude et bien plus encore. Fin 2016, un bureau mondial de R&D a été ouvert à Moscou, qui travaille dans l'intérêt de toute l'entreprise.

Comment nous sommes arrivés


En 2011, le produit principal de l'entreprise était une application Ruby on Rails monolithique, car à cette époque, ce framework était très populaire. Il y avait des exemples réussis d'entreprises rapidement développées et lancées sur Ruby on Rails, donc c'était associé au succès des entreprises. L'entreprise se développait, de nouveaux pilotes et utilisateurs sont venus vers nous, les charges ont augmenté. Et les premiers problèmes ont commencé à apparaître.

Pour que l'application client affiche l'emplacement de la voiture et que son mouvement ressemble à une courbe lisse, les conducteurs doivent souvent envoyer leurs coordonnées. Par conséquent, le point d'extrémité responsable de la réception des coordonnées des conducteurs était presque toujours le plus lourdement chargé. Et le cadre du serveur Web dans Ruby on Rails a fait un mauvais travail. Il a été possible d'évoluer de manière extensive, en ajoutant de nouveaux serveurs d'applications, ce qui est coûteux et inefficace. En conséquence, nous avons retiré la collection fonctionnelle de coordonnées dans un service distinct, qui était à l'origine écrit en JS. Pendant un certain temps, cela a résolu le problème. Cependant, à mesure que la charge augmentait, lorsque nous avons atteint 80 000 tr / min, le service sur Node.js a cessé de nous sauver.

Ensuite, nous avons déclaré un hackathon. Tous les employés de l'entreprise ont eu l'opportunité en une journée d'écrire un prototype qui devait collecter les coordonnées des chauffeurs. Voici les repères de deux versions de ce service: fonctionnant sur prod et réécrit sur Go.


À presque tous égards, le service sur Go a donné les meilleurs résultats. Le service sur Node.js utilisait un cluster, c'est une technologie pour utiliser tous les cœurs d'une machine. Autrement dit, l'expérience était un plus ou moins honnête. Bien que Node.js présente l'inconvénient d'un runtime à thread unique, il n'a aucun effet sur les résultats.

Progressivement, nos demandes de produits ont augmenté. Nous avons développé de plus en plus de fonctionnalités, et une fois que nous avons rencontré un tel problème: lorsque vous ajoutez un morceau de code à un endroit, quelque chose peut se casser à un autre endroit où le projet est fortement connecté. Nous avons décidé de surmonter ce fléau en passant à une architecture orientée services. Mais les performances se sont détériorées en conséquence: lorsqu'une requête réseau est rencontrée par l'interpréteur Ruby on Rails lorsque le code est exécuté, elle est bloquée et le travailleur est inactif. Et les opérations d'E / S réseau sont devenues de plus en plus nombreuses.

En conséquence, nous avons décidé d'adopter Go comme l'un des principaux langages de développement.

Caractéristiques de notre développement de produits


Premièrement, nous avons des exigences de produits très différentes. Étant donné que nos voitures roulent dans trois pays avec des lois complètement différentes, il est nécessaire de mettre en œuvre des ensembles de fonctionnalités très différents. Par exemple, en Israël, il est exigé par la loi que le coût d'un voyage soit pris en compte par un taximètre - c'est un appareil qui passe la certification obligatoire toutes les quelques années. Lorsque le conducteur commence le voyage, il appuie sur le bouton "go", et quand il a terminé, il appuie sur le bouton "stop" et saisit le prix indiqué par le taximètre dans l'application.

Il n'y a pas de lois aussi strictes en Russie. Ici, nous pouvons configurer nous-mêmes la politique de prix. Par exemple, liez-le à la durée du voyage ou à la distance. Parfois, lorsque nous voulons implémenter la même fonctionnalité, nous la déployons d'abord dans un pays, puis l'adaptons et la déployons dans d'autres pays.

Nos chefs de produits définissent les exigences sous la forme d'histoires de produits, nous essayons d'adhérer à une telle approche. Cela laisse automatiquement sa marque sur les tests: nous utilisons la méthodologie de développement axée sur le comportement afin que les exigences des produits entrants puissent être projetées dans des situations de test. Il est plus facile pour les personnes qui sont loin de programmer de simplement lire les résultats du test et de comprendre quoi.

Nous voulions également nous débarrasser de la duplication de certains travaux. Après tout, si nous avons un service qui implémente une sorte de fonctionnalité et que nous devons écrire un deuxième service, en résolvant tous les problèmes que nous avons résolus dans le premier, en réintégrant les outils de surveillance et de migration, alors cela sera inefficace.

Nous résolvons des problèmes


Cadre


Ruby on Rails est construit sur l'architecture MVC. Au moment de la transition, nous ne voulions vraiment pas y renoncer afin de faciliter la vie des développeurs qui ne peuvent programmer que sur ce framework. Changer les outils n'ajoute pas de confort sans cela, et si vous changez également l'architecture de l'application, c'est la même chose que pousser une personne qui ne sait pas nager depuis un bateau. Nous ne voulions pas blesser les développeurs de cette façon, nous avons donc pris l'un des rares frameworks MVC à l'époque appelé Beego .

Nous avons essayé d'utiliser Beego, comme dans Ruby on Rails, pour effectuer un rendu côté serveur. Cependant, la page affichée sur le serveur ne nous a pas vraiment plu. J'ai dû jeter un composant, et aujourd'hui Beego ne produit que du JSON à partir du backend, et tout le rendu est effectué par React sur le front.

Beego vous permet de construire un projet automatiquement. Il était très difficile pour certains développeurs de passer d'un langage de script à la nécessité de compiler. Il y a eu des histoires drôles quand une personne a implémenté une sorte de fonctionnalité, et ce n'est qu'en examinant le code ou même accidentellement découvert que, il s'avère, vous devez faire une Go-build. Et la tâche est déjà terminée.

Dans Beego, un routeur est généré à partir d'un commentaire dans lequel le développeur écrit le chemin d'accès aux actions du contrôleur. Nous avons une attitude ambiguë à cette idée, car si une faute de frappe, par exemple, le routeur a été retapée, il est difficile pour ceux qui ne sont pas sophistiqués dans cette approche de trouver une erreur. Les gens, parfois, ne pouvaient pas comprendre les raisons même après plusieurs heures de débogage passionnant.

Base de données


Nous utilisons PostgreSQL comme base de données. Il existe une telle pratique - pour contrôler le schéma de base de données à partir du code d'application. Ceci est pratique pour plusieurs raisons: tout le monde les connaît; ils sont faciles à déployer, la base de données est toujours synchronisée avec le code. Et nous voulions aussi garder ces petits pains.

Lorsque vous avez plusieurs projets et équipes, parfois pour implémenter la fonctionnalité, vous devez explorer les projets d'autres personnes. Et il est très tentant d'ajouter une colonne au tableau, dans laquelle 10 millions d'enregistrements peuvent apparaître. Et une personne qui n'est pas plongée dans ce projet peut ne pas être consciente de la taille de la table. Pour éviter cela, nous avons émis un avertissement sur les migrations dangereuses qui pourraient bloquer la base de données pour l'enregistrement et avons donné aux développeurs les moyens de supprimer cet avertissement.

La migration


Nous avons décidé de migrer en utilisant Swan , qui est une oie rapiécée , dans laquelle nous avons apporté quelques améliorations. Ces deux, comme de nombreux outils de migration, veulent tout faire en une seule transaction, de sorte qu'en cas de problème, vous pouvez facilement revenir en arrière. Il arrive parfois que vous ayez besoin de créer un index et que la table soit verrouillée. PostgreSQL a un paramètre concurrently qui évite cela. Le problème est que si dans PostgreSQL vous commencez à construire un index concurrently , et même dans une transaction, une erreur apparaîtra. Au début, nous voulions ajouter un indicateur afin de ne pas ouvrir de transaction. Et à la fin, ils ont fait ceci:

 COMMIT; CREATE INDEX CONCURRENTLY huge_index ON huge_table (column_one, column_two); BEGIN; 

Maintenant, lorsque quelqu'un ajoute un index avec le paramètre concurrently , il obtient cet indice. Notez que commit et begin ne begin pas confondus. Ce code ferme la transaction que l'outil de migration a ouverte, puis fait rouler l'index avec le paramètre concurrently , puis ouvre une autre transaction pour que l'outil ferme quelque chose.

Test


Nous essayons d'adhérer au développement axé sur le comportement. Dans Go, cela peut être fait en utilisant l'outil Ginkgo . C'est bien car il a les mots-clés habituels pour BDD, «décrire», «quand» et autres, et il vous permet également de simplement projeter du texte écrit par le chef de produit sur des situations de test qui sont stockées dans le code source. Mais nous avons rencontré un problème: des gens venus du monde de Ruby on Rails croient que dans n'importe quel langage de programmation, il y a quelque chose de similaire à une fille d'usine - une usine pour créer les conditions initiales. Cependant, il n'y avait rien de tel dans Go. Au final, nous avons décidé de ne pas réinventer la roue: juste avant chaque test, dans les crochets avant et après le test, nous remplissons la base de données avec les données nécessaires, puis la nettoyons pour qu'il n'y ait pas d'effets secondaires.

Suivi


Si vous avez un service de production auquel les gens accèdent, vous devez suivre son travail: y a-t-il cinq cents erreurs ou les demandes sont-elles traitées rapidement. Dans le monde de Ruby on Rails, NewRelic est très souvent utilisé pour cela, et beaucoup de nos développeurs l'ont bien possédé. Ils ont compris comment l'outil fonctionnait, où chercher en cas de problème. NewRelic vous permet d'analyser le temps de traitement des demandes via HTTP, d'identifier les appels externes lents et les demandes à la base de données, de surveiller les flux de données, de fournir une analyse et des alertes d'erreur intelligentes.

NewRelic a la fonction d'agrégation Apdex, qui dépend de l'histogramme de la distribution de la durée des réponses et de certaines valeurs que vous pensez être normales et qui sont définies au tout début. Cette fonctionnalité dépend également du niveau d'erreurs dans l'application. NewRelic calcule Apdex et émet un avertissement si sa valeur tombe en dessous d'un certain niveau.
NewRelic est également doué pour avoir récemment un agent officiel Go. Voici à quoi ressemble l'aperçu général de la surveillance:



À gauche, un diagramme de traitement des requêtes, chacun étant divisé en segments. Les segments incluent la mise en file d'attente des demandes, le traitement du middleware, la durée de séjour dans l'interpréteur Ruby on Rails et l'accès aux référentiels.

Le graphique Apdex est affiché en haut à droite. En bas à droite - la fréquence de traitement des demandes.

L'intrigue est que dans Ruby on Rails pour connecter NewRelic, vous devez ajouter une ligne de code et ajouter vos informations d'identification à la configuration. Et tout fonctionne comme par magie. Cela est possible du fait que dans Ruby on Rails il y a un patch de singe, qui n'est pas dans Go, donc il y a beaucoup à faire manuellement.

Tout d'abord, nous avons voulu mesurer la durée de traitement des demandes. Cela a été fait en utilisant les crochets fournis par Beego.

 beego.InsertFilter("*", beego.BeforeRouter, StartTransaction, false) beego.InsertFilter("*", beego.AfterExec, NameTransaction, false) beego.InsertFilter("*", beego.FinishRouter, EndTransaction, false) 

Le seul point non trivial était que nous partagions l'ouverture de la transaction et sa dénomination. Pourquoi avons-nous fait ça? Je voulais mesurer la durée du traitement des demandes en tenant compte du temps passé sur le routage. Dans le même temps, nous avons besoin de rapports agrégés par les points de terminaison auxquels les demandes sont arrivées. Mais au moment de l'ouverture de la transaction, nous n'avons pas encore défini de modèle d'URL par lequel une correspondance se produira. Par conséquent, lorsqu'une demande arrive, nous ouvrons une transaction, puis sur le crochet, après avoir exécuté le contrôleur, le nommer et après le traitement, le fermer. Par conséquent, aujourd'hui, nos rapports ressemblent à ceci:


Nous avons utilisé un ORM appelé GORM parce que nous voulions garder l'abstraction et ne pas forcer les développeurs à écrire du SQL pur. Cette approche présente à la fois des avantages et des inconvénients. Dans le monde de Ruby on Rails, il existe un ORM Active Record qui a vraiment gâté les gens. Les développeurs oublient que vous pouvez écrire du SQL pur et ne fonctionner qu'avec des appels ORM.

 db.Callback().Create().Before("gorm:begin_transaction"). Register("newrelicStart", startSegment) db.Callback().Create().After("gorm:commit_or_rollback_transaction"). Register("newrelicStop", endSegment) 

Pour mesurer la durée d'exécution des requêtes dans la base de données lors de l'utilisation de GORM, vous devez prendre l'objet db . Le rappel indique que nous voulons enregistrer un rappel. Il doit être appelé lors de la création d'une nouvelle entité - un appel à Create . Ensuite, nous indiquons exactement quand lancer Callback. Before est responsable de cela avec l'argument gorm : begin_transaction est un certain moment au moment où la transaction est ouverte. Ensuite, avec le nom newrelicStart enregistrons la fonction startSegment , qui appelle simplement l'agent Go et ouvre un nouveau segment pour accéder à la base de données.

ORM appellera cette fonction avant d'ouvrir la transaction, ouvrant ainsi le segment. Nous devons faire de même pour fermer le segment: il suffit de suspendre le rappel.

En plus de PostgreSQL, nous utilisons Redis, qui n'est pas non plus fluide. Pour cette surveillance, nous avons écrit un wrapper sur un client standard et fait de même pour appeler des services externes. Voici ce qui s'est passé:



Voici à quoi ressemble la surveillance pour une application écrite en Go. À gauche, un rapport sur la durée du traitement des requêtes, composé de segments: exécution du code lui-même dans Go, accès aux bases de données PostgreSQL et Replica. Les appels vers des services externes ne sont pas affichés sur ce graphique, car ils sont très peu nombreux et sont simplement invisibles lorsqu'ils sont moyennés. Nous avons également des informations sur Apdex et la fréquence de traitement des demandes. En général, la surveillance s'est avérée assez informative et utile à l'utilisation.

Quant aux flux de données, grâce à nos wrappers sur le client HTTP, nous pouvons suivre les requêtes vers des services externes. Le schéma de demande de service de promotion est indiqué ici: il fait référence à quatre de nos autres services et à deux référentiels.



Conclusion


Aujourd'hui, nous avons plus de 75% des services de production écrits en Go, nous n'effectuons pas de développement actif dans Ruby, mais nous le supportons uniquement. Et à cet égard, je tiens à noter:

  • Les craintes d'une diminution de la vitesse de développement n'ont pas été confirmées. Les programmeurs se sont lancés dans la nouvelle technologie chacun dans son propre mode, mais, en moyenne, après quelques semaines de travail actif, le développement sur Go est devenu aussi prévisible et rapide que sur Ruby on Rails.
  • Les performances des applications Go sous charge sont agréablement surprenantes par rapport à l'expérience passée. Nous avons considérablement économisé sur l'utilisation de l'infrastructure dans AWS, réduisant considérablement le nombre d'instances utilisées.
  • Le changement de technologie a considérablement encouragé les programmeurs, et c'est un élément important d'un projet réussi.
  • Aujourd'hui, nous avons déjà quitté Beego et Gorm, plus à ce sujet dans le prochain article.

En résumé, je veux dire que si vous n'écrivez pas sur Go, vous souffrez de problèmes de charges de travail élevées et vous ennuyez avec le trafic, allez dans cette langue. N'oubliez pas de négocier avec l'entreprise.

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


All Articles