Comment Go a sauvé notre Black Friday

Plus tôt, nous avons expliqué comment, à mesure que la charge augmentait, nous avons progressivement abandonné l'utilisation de Python dans le backend des services critiques en production, en le remplaçant par Go. Et aujourd'hui, moi, Denis Girko, chef d'équipe de l'équipe de développement Madmin, souhaite partager les détails: comment et pourquoi cela s'est produit sur l'exemple de l'un des services les plus importants pour notre entreprise - calculer le prix en tenant compte des remises sur les coupons.



Les mécanismes de travail avec les coupons sont probablement représentés par toute personne qui a effectué au moins une fois des achats dans des magasins en ligne. Sur une page spéciale ou directement dans le panier, vous saisissez le numéro du coupon, et les prix sont recalculés en fonction de la remise promise. Le calcul dépend du type de remise que le coupon offre - en pourcentage, sous la forme d'un montant fixe ou en utilisant d'autres mathématiques (par exemple, nous tenons également compte des points du programme de fidélité, des promotions en magasin, des types de marchandises, etc.). Naturellement, la commande est déjà émise avec de nouveaux prix.

Les entreprises sont ravies de tous ces mécanismes de travail avec les prix, mais nous voulons parler du service d'un point de vue légèrement différent.

Comment ça marche


Pour une tarification tenant compte de toutes ces difficultés sur le backend, nous avons désormais un service séparé. Cependant, il n'était pas toujours indépendant. Le service est apparu un an ou deux après le démarrage de la boutique en ligne et, en 2016, il faisait partie d'un grand monolithe Python qui comprenait une grande variété de composants pour l'activité marketing (Madmin). Plus tard, il s'est imposé comme un «bloc» indépendant en s'orientant vers l'architecture de microservices.

Comme c'est généralement le cas avec les monolithes, Madmin a été modifié et correspondait en partie à un grand nombre de développeurs. Des bibliothèques tierces y ont été intégrées, ce qui a simplifié le développement, mais n'a souvent pas eu le meilleur effet sur les performances. Cependant, à cette époque, nous ne nous soucions pas vraiment de la résistance aux charges lourdes pendant les ventes, car le service a fait un excellent travail. Mais 2016 a tout changé.



Aux États-Unis, le «Black Friday» est connu depuis les années 60 du siècle dernier. En Russie, il a commencé à être lancé dans les années 2010, alors que l'action devait être créée à partir de zéro - le marché n'était pas tout à fait prêt pour cela. Cependant, les efforts des organisateurs n'ont pas été vains, et avec chaque année le trafic des utilisateurs sur notre site a augmenté pendant les jours de vente. Et donc, notre collision avec la charge, excessive pour cette version du service de calcul des prix, n'était qu'une question de temps.

Vendredi noir 2016. Et nous l'avons trop dormie


Étant donné que l'idée de vente a fonctionné à son plein potentiel, le «Black Friday» diffère de tout autre jour de l'année en ce sens qu'à minuit, une audience hebdomadaire du site Web arrive dans le magasin. C'est une période difficile pour tous les services. Même dans ceux d'entre eux qui fonctionnent bien tout au long de l'année, des problèmes surgissent parfois.

Nous nous préparons maintenant pour chaque nouveau «Black Friday», en imitant la charge attendue, mais en 2016, nous avons toujours agi différemment. Lors du test de Madmin avant un jour important, nous avons testé la résistance à la charge à l'aide de scénarios de comportement de l'utilisateur les jours normaux. Il s'est avéré que ce test ne reflète pas tout à fait la situation réelle, car le «Black Friday», beaucoup de gens ont le même coupon. Par conséquent, le service de calcul des prix tenant compte de cette remise, incapable de faire face à une triple charge (par rapport aux jours ordinaires), a bloqué notre capacité à servir les clients pendant deux heures au plus fort pic de la vente.

Le service "est passé" une heure avant minuit. Tout a commencé par une interruption de la connexion à la base de données (MySQL à l'époque), après quoi toutes les copies en cours d'exécution du service de calcul des prix n'ont pas pu se reconnecter. Et ceux qui étaient toujours connectés n'ont pas résisté à la charge et ont cessé de répondre, étant coincés sur les verrous de la base.

Par coïncidence, le cadet est resté en service à ce moment-là qui, au moment de la chute du service, se rendait du bureau. Il n'a pu se connecter au problème que lorsqu'il est arrivé sur les lieux et a appelé «l'artillerie lourde» - l'officier de service d'urgence. Ensemble, ils n'ont cependant normalisé la situation qu'après deux heures.

Au début de la procédure, des détails ont commencé à s'ouvrir sur le caractère sous-optimal du service. Par exemple, il s'est avéré que pour calculer un coupon, 28 requêtes ont été effectuées dans la base de données (il n'est pas surprenant que tout fonctionne avec une utilisation à 100% du processeur). Les utilisateurs mentionnés ci-dessus avec le même coupon Black Friday n'ont pas simplifié la situation, d'autant plus que nous avions un compteur d'applications pour tous les coupons - donc chaque utilisation augmentait la charge en se référant à ce compteur.

2016 nous a donné matière à réflexion - principalement sur la manière d'ajuster notre travail avec des coupons et des tests afin que cette situation ne se reproduise plus. Et en chiffres, ce vendredi est mieux décrit par cette image:


Les résultats du Black Friday 2016

Vendredi noir 2017. Nous nous préparions sérieusement, mais ...


Ayant reçu une bonne leçon, nous nous sommes préparés à l'avance pour le prochain «Black Friday», après avoir sérieusement reconstruit et optimisé le service. Par exemple, nous avons finalement créé deux types de coupons: limité et illimité - afin d'éviter les verrous sur l'accès simultané à la base de données, nous avons supprimé l'entrée de la base de données du script pour appliquer le coupon populaire. En parallèle, 1 à 2 mois avant le Black Friday, nous sommes passés de MySQL à PostgreSQL dans le service, ce qui, avec l'optimisation du code, a réduit le nombre d'appels de base de données de 28 à 4 à 5. Ces améliorations nous ont permis d'étendre le service de test aux exigences de SLA - réponse en 3 secondes, 95 centile à 600 RPS.

Ne sachant pas à quel point nos améliorations ont accéléré le travail de l'ancienne version du service en production, à ce moment-là, deux versions de code Python étaient en cours de préparation pour le Black Friday - une version existante hautement optimisée et un code entièrement nouveau écrit à partir de zéro. En production, le second a été déployé, qui a été testé avant ce jour et cette nuit. Cependant, comme il s'est avéré "au combat", un peu sous-testé.

Le jour de «l'urgence» avec l'arrivée du flux principal de clients, la charge sur le service a commencé à croître de façon exponentielle. Certaines demandes ont été traitées jusqu'à deux minutes. En raison du long traitement de certaines demandes, la charge des autres travailleurs a augmenté.

Notre tâche principale était de servir un trafic aussi précieux pour les entreprises. Mais il est devenu évident que «couler avec du fer» ne résout pas le problème et à tout moment le nombre de travailleurs occupés atteindra 100%. Ne sachant alors pas exactement à quoi nous étions confrontés, nous avons décidé d'activer harakiri dans uWSGI et de simplement clouer les longues demandes (qui sont traitées pendant plus de 6 secondes) pour libérer des ressources pour les normales. Et cela a vraiment aidé à résister - les travailleurs ont commencé à être libérés quelques minutes avant d'être complètement épuisés.

Un peu plus tard, nous avons compris la situation ... Il s'est avéré qu'il s'agissait de demandes avec de très grands paniers - de 40 à 100 produits - et avec un coupon spécifique ayant des restrictions sur la gamme. Cette situation a été mal réglée par le nouveau code. Il a montré un travail incorrect avec le tableau, qui s'est transformé en récursion infinie. Il est curieux que nous ayons ensuite testé un étui avec de grands paniers, mais pas en combinaison avec un coupon délicat. Comme solution, nous sommes simplement passés à une version différente du code. Certes, cela s'est produit trois heures avant la fin du Black Friday. À partir de ce moment, tous les paniers ont commencé à être traités correctement. Et bien que nous ayons terminé le plan de vente à ce moment-là, nous avons évité des problèmes mondiaux miraculeux en raison de la charge cinq fois par jour.

Vendredi noir 2018


En 2018, pour les services très chargés qui desservent le site, nous avons progressivement commencé à implémenter Go. Compte tenu de l'histoire des précédents vendredis noirs, le service de calcul des remises a été l'un des premiers candidats au traitement.



Bien sûr, nous pourrions enregistrer la version de Python qui était déjà «testée au combat», et avant le nouveau «Black Friday», nous pourrions désactiver les bibliothèques lourdes et jeter du code non optimal. Cependant, Golang avait déjà pris racine à cette époque et semblait plus prometteur.

Nous sommes passés à un nouveau service cet été, donc avant la prochaine vente, nous avons réussi à bien le tester, y compris sur un profil de charge croissant.

Lors des tests, il s'est avéré que la faiblesse en termes de charges élevées reste notre base. Des transactions trop longues ont conduit au fait que nous avons sélectionné l'ensemble du pool de connexions et que les demandes ont été mises en file d'attente. Nous avons donc dû refaire un peu la logique de l'application, en réduisant l'utilisation de la base de données au minimum (en y faisant référence uniquement lorsqu'il n'y a rien à faire) et en mettant en cache les répertoires de la base de données et les données sur les coupons populaires le Black Friday.

Certes, cette année, nous nous sommes trompés avec des prévisions de charge à la hausse: nous nous préparions à une croissance de 6 à 8 fois dans les pics et avons réalisé un bon travail de services juste pour un tel volume de demandes (caches ajoutés, fonctions expérimentales désactivées à l'avance, simplifié certaines choses, déployé des nœuds Kubernetes supplémentaires et même des serveurs de bases de données pour les répliques, qui n'étaient finalement pas nécessaires). En fait, la montée de l'intérêt des utilisateurs a été moindre, donc tout s'est déroulé comme d'habitude. Le temps de réponse du service n'a pas dépassé 50 ms à 95 centile.

Pour nous, l'une des caractéristiques les plus importantes est la façon dont l'application évolue lorsqu'il n'y a pas suffisamment de ressources pour une copie. Go utilise les ressources matérielles plus efficacement, donc avec la même charge, vous devez exécuter moins de copies (en fin de compte, servir plus de demandes sur les mêmes ressources matérielles). Cette année, au plus fort de la vente, 16 instances de l'application fonctionnaient, ce qui a traité en moyenne 300 demandes par seconde avec des pointes allant jusqu'à 400 demandes par seconde, soit environ deux fois plus que la charge habituelle. Notez que l'année dernière, un service Python a nécessité 102 instances.

Il semblerait que le service on Go de la première approche ait fermé tous nos besoins. Mais Golang n'est pas une "solution unique à tous les problèmes". Il ne pouvait pas se passer de certaines fonctionnalités. Par exemple, nous avons dû limiter le nombre de threads que le service peut démarrer sur le nœud multiprocesseur Kubernetes, afin que lors de sa mise à l'échelle, il n'interfère pas avec les applications "voisines" en production (par défaut, Go n'a aucune limite sur le nombre de processeurs qu'il prendra). Pour ce faire, nous avons configuré GOMAXPROCS dans toutes les applications sur Go. Nous serons ravis de vous dire à quel point cela a été utile - dans notre équipe, ce n’était là qu’une des hypothèses concernant la manière de faire face à la dégradation des «voisins».

Une autre «configuration» est le nombre de connexions détenues en tant que Keep-Alive. Les clients HTTP et DB réguliers dans Go par défaut ne détiennent que deux connexions, donc s'il y a beaucoup de demandes simultanées et que vous devez économiser sur le trafic de la configuration de la connexion TCP, il est logique d'augmenter cette valeur en définissant respectivement MaxIdleConnsPerHost et SetMaxIdleConns.

Cependant, même avec ces «rebondissements» manuels, Golang nous a fourni une grande marge de performance pour les ventes futures.

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


All Articles