How Go rettete unseren schwarzen Freitag

Zuvor haben wir darüber gesprochen, wie wir mit zunehmender Auslastung die Verwendung von Python im Backend kritischer Produktionsdienste schrittweise aufgegeben und durch Go ersetzt haben. Und heute möchte ich, Denis Girko, Teamleiter des Madmin-Entwicklungsteams, Details mitteilen: Wie und warum dies am Beispiel einer der wichtigsten Dienstleistungen für unser Unternehmen geschehen ist - Berechnung des Preises unter Berücksichtigung von Rabatten auf Gutscheine.



Die Mechanik der Arbeit mit Gutscheinen wird wahrscheinlich von jedem vertreten, der mindestens einmal in Online-Shops eingekauft hat. Auf einer speziellen Seite oder direkt im Warenkorb geben Sie die Gutscheinnummer ein und die Preise werden entsprechend dem versprochenen Rabatt neu berechnet. Die Berechnung hängt davon ab, welche Art von Rabatt der Gutschein bietet - in Prozent, in Form eines festen Betrags oder unter Verwendung einer anderen Mathematik (zum Beispiel berücksichtigen wir zusätzlich Punkte des Treueprogramms, Verkaufsförderungsaktionen, Warentypen usw.). Natürlich wird die Bestellung bereits mit neuen Preisen ausgestellt.

Die Wirtschaft ist von all diesen Mechanismen der Arbeit mit Preisen begeistert, aber wir möchten den Service aus einem etwas anderen Blickwinkel betrachten.

Wie funktioniert es?


Für die Preisgestaltung unter Berücksichtigung all dieser Schwierigkeiten im Backend haben wir jetzt einen separaten Service. Er war jedoch nicht immer unabhängig. Der Dienst erschien ein oder zwei Jahre nach dem Start des Online-Shops und war bis 2016 Teil eines großen Python-Monolithen, der eine Vielzahl von Komponenten für Marketingaktivitäten (Madmin) enthielt. Später stach er als unabhängiger „Block“ hervor, als er sich der Microservice-Architektur zuwandte.

Wie bei Monolithen üblich, wurde Madmin modifiziert und korrespondierte teilweise mit einer großen Anzahl von Entwicklern. Dort wurden Bibliotheken von Drittanbietern integriert, was die Entwicklung vereinfachte, sich jedoch häufig nicht optimal auf die Leistung auswirkte. Zu diesem Zeitpunkt war uns der Widerstand gegen schwere Lasten während des Verkaufs jedoch nicht wirklich wichtig, da der Service die Aufgabe hervorragend erledigte. Aber 2016 hat alles verändert.



In den USA ist "Black Friday" seit den 60er Jahren des letzten Jahrhunderts bekannt. In Russland begann die Einführung in den 2010er Jahren, während die Aktion von Grund auf neu erstellt werden musste - der Markt war nicht ganz bereit dafür. Die Bemühungen der Organisatoren waren jedoch nicht umsonst, und mit jedem Jahr nahm der Nutzerverkehr auf unserer Website während der Verkaufstage zu. Daher war unsere Kollision mit der Last, die für diese Version des Preisberechnungsdienstes zu hoch war, nur eine Frage der Zeit.

Schwarzer Freitag 2016. Und wir haben sie verschlafen


Da die Verkaufsidee ihr volles Potenzial entfaltet hat, unterscheidet sich „Black Friday“ von jedem anderen Tag des Jahres darin, dass um Mitternacht ein ungefähr wöchentliches Website-Publikum in den Laden kommt. Dies ist eine schwierige Zeit für alle Dienste. Selbst bei denen, die das ganze Jahr über reibungslos funktionieren, treten manchmal Probleme auf.

Jetzt bereiten wir uns auf jeden neuen „Black Friday“ vor und ahmen die erwartete Belastung nach, aber 2016 haben wir uns immer noch anders verhalten. Beim Testen von Madmin vor einem wichtigen Tag haben wir die Lastbeständigkeit anhand von Benutzerverhaltensszenarien an normalen Tagen getestet. Wie sich herausstellte, spiegelt dieser Test nicht die tatsächliche Situation wider, da am "Black Friday" viele Leute mit dem gleichen Gutschein kommen. Infolgedessen blockierte der Preisberechnungsdienst unter Berücksichtigung dieses Preisnachlasses, der eine dreifache Belastung (im Vergleich zu normalen Tagen) nicht bewältigen konnte, die Möglichkeit, Kunden während des heißesten Höhepunkts des Verkaufs zwei Stunden lang zu bedienen.

Der Dienst "ging" eine Stunde vor Mitternacht. Alles begann mit einer Unterbrechung der Verbindung zur Datenbank (zu diesem Zeitpunkt MySQL), wonach nicht alle laufenden Kopien des Preisberechnungsdienstes wieder eine Verbindung herstellen konnten. Und diejenigen, die noch verbunden waren, hielten der Last nicht stand und reagierten nicht mehr, da sie an den Basisschlössern steckten.

Zufällig blieb der Junior dann im Dienst, der zum Zeitpunkt des Dienstausfalls auf dem Weg vom Büro nach Hause war. Er konnte sich erst mit dem Problem verbinden, als er am Ort ankam und die "schwere Artillerie" - den Notdienstoffizier - anrief. Zusammen normalisierten sie die Situation jedoch erst nach zwei Stunden.

Zu Beginn des Verfahrens wurden Einzelheiten darüber bekannt, wie suboptimal der Dienst war. Zum Beispiel stellte sich heraus, dass zur Berechnung eines Coupons 28 Abfragen an die Datenbank gestellt wurden (es ist nicht überraschend, dass alles mit 100% CPU-Auslastung funktionierte). Die oben genannten Benutzer mit demselben Black Friday-Gutschein haben die Situation nicht vereinfacht, zumal wir für alle Gutscheine einen Anwendungszähler hatten. Bei jeder Verwendung wurde die Belastung erhöht, indem auf diesen Zähler verwiesen wurde.

2016 gab uns viele Denkanstöße - hauptsächlich darüber, wie wir unsere Arbeit mit Gutscheinen und Tests so anpassen können, dass diese Situation nicht erneut auftritt. Und in Zahlen wird dieser Freitag am besten durch dieses Bild beschrieben:


Die Ergebnisse des Black Friday 2016

Schwarzer Freitag 2017. Wir haben uns ernsthaft vorbereitet, aber ...


Nachdem wir eine gute Lektion erhalten hatten, bereiteten wir uns im Voraus auf den nächsten „Schwarzen Freitag“ vor, nachdem wir den Service ernsthaft umgebaut und optimiert hatten. Zum Beispiel haben wir schließlich zwei Arten von Coupons erstellt: limit und unbegrenzt. Um Sperren beim gleichzeitigen Zugriff auf die Datenbank zu vermeiden, haben wir den Eintrag in die Datenbank aus dem Skript entfernt, um den beliebten Coupon anzuwenden. Parallel dazu haben wir 1–2 Monate vor „Black Friday“ im Dienst von MySQL auf PostgreSQL umgestellt, wodurch zusammen mit der Codeoptimierung die Anzahl der Aufrufe der Datenbank von 28 auf 4–5 reduziert wurde. Diese Verbesserungen ermöglichten es, den Testdienst auf SLA-Anforderungen auszudehnen - Antwort in 3 Sekunden 95 Perzentil bei 600 U / min.

Da wir keine Ahnung hatten, wie sehr unsere Verbesserungen die Arbeit der alten Version des Service in der Produktion beschleunigten, wurden zu diesem Zeitpunkt zwei Versionen des Python-Codes gleichzeitig für den Black Friday vorbereitet - eine hochoptimierte vorhandene Version und völlig neuer Code, der von Grund auf neu geschrieben wurde. In der Produktion wurde der zweite ausgerollt, der vor diesem Tag und dieser Nacht getestet wurde. Wie sich jedoch "im Kampf" herausstellte, etwas unterbewertet.

Am Tag des "Notfalls" mit dem Eintreffen des Hauptkundenstroms begann die Belastung des Dienstes exponentiell zu wachsen. Einige Anfragen wurden bis zu zwei Minuten bearbeitet. Aufgrund der langen Bearbeitung einiger Anfragen stieg die Belastung für andere Arbeitnehmer.

Unsere Hauptaufgabe war es, solch wertvollen Verkehr für Unternehmen bereitzustellen. Es wurde jedoch klar, dass "Gießen mit Eisen" das Problem nicht löst und die Anzahl der beschäftigten Arbeiter ab jeder Minute 100% erreichen wird. Da wir nicht genau wussten, womit wir konfrontiert waren, beschlossen wir, Harakiri in uWSGI zu aktivieren und einfach lange Anfragen (die länger als 6 Sekunden verarbeitet werden) festzunageln, um Ressourcen für normale freizugeben. Und es half wirklich, Widerstand zu leisten - die Arbeiter wurden nur ein paar Minuten vor ihrer völligen Erschöpfung freigelassen.

Wenig später haben wir die Situation herausgefunden ... Es stellte sich heraus, dass es sich um Anfragen mit sehr großen Körben - von 40 bis 100 Waren - und mit einem bestimmten Gutschein handelte, der Einschränkungen im Sortiment hatte. Diese Situation wurde durch den neuen Code schlecht herausgearbeitet. Es zeigte eine falsche Arbeit mit dem Array, die sich in eine unendliche Rekursion verwandelte. Es ist merkwürdig, dass wir dann einen Koffer mit großen Körben getestet haben, aber nicht in Kombination mit einem kniffligen Gutschein. Als Lösung haben wir einfach auf eine andere Version des Codes umgestellt. Dies geschah zwar drei Stunden vor dem Ende des Schwarzen Freitags. Von diesem Moment an wurden alle Körbe korrekt verarbeitet. Und obwohl wir zu diesem Zeitpunkt den Verkaufsplan abgeschlossen haben, haben wir wundersame globale Probleme aufgrund der fünfmaligen Belastung am üblichen Tag vermieden.

Schwarzer Freitag 2018


Bis 2018 haben wir für hoch ausgelastete Dienste, die die Site bedienen, schrittweise mit der Implementierung von Go begonnen. In Anbetracht der Geschichte früherer Schwarzer Freitage war der Rabattberechnungsdienst einer der ersten Kandidaten für die Verarbeitung.



Natürlich könnten wir die Version von Python speichern, die bereits "im Kampf getestet" wurde, und vor dem neuen "Black Friday" könnten wir schwere Bibliotheken ausschalten und nicht optimalen Code wegwerfen. Zu diesem Zeitpunkt hatte Golang jedoch bereits Wurzeln geschlagen und sah vielversprechender aus.

Wir haben diesen Sommer auf einen neuen Service umgestellt, sodass wir ihn vor dem nächsten Verkauf gut testen konnten, auch bei zunehmendem Lastprofil.

Beim Testen stellte sich heraus, dass die Schwäche in Bezug auf hohe Lasten unsere Basis bleibt. Zu lange Transaktionen führten dazu, dass wir den gesamten Verbindungspool ausgewählt haben und Anforderungen in die Warteschlange gestellt wurden. Daher mussten wir die Logik der Anwendung ein wenig überarbeiten, die Nutzung der Datenbank auf ein Minimum reduzieren (nur dann darauf verweisen, wenn nichts dagegen zu tun ist) und Verzeichnisse aus der Datenbank und Daten zu Coupons zwischenspeichern, die am Black Friday beliebt sind.

In diesem Jahr haben wir zwar einen großen Fehler bei den Lastprognosen gemacht: Wir haben uns auf ein 6-8-faches Wachstum der Spitzen vorbereitet und nur für ein solches Anforderungsvolumen gute Arbeit geleistet (zusätzliche Caches hinzugefügt, experimentelle Funktionen im Voraus deaktiviert, einige Dinge vereinfacht, zusätzliche Kubernetes-Knoten bereitgestellt und sogar Datenbankserver für Replikate, die am Ende nicht benötigt wurden). Tatsächlich war der Anstieg des Benutzerinteresses geringer, sodass alles wie gewohnt verlief. Die Antwortzeit des Dienstes überschritt 50 ms bei 95 Perzentilen nicht.

Für uns ist eines der wichtigsten Merkmale, wie die Anwendung skaliert wird, wenn nicht genügend Ressourcen für eine Kopie vorhanden sind. Go nutzt Hardwareressourcen effizienter, sodass Sie bei gleicher Auslastung weniger Kopien ausführen müssen (was letztendlich mehr Anforderungen auf denselben Hardwareressourcen bedient). In diesem Jahr, zum Höhepunkt des Verkaufs, arbeiteten 16 Instanzen der Anwendung, die durchschnittlich 300 Anfragen pro Sekunde mit Spitzenwerten von bis zu 400 Anfragen pro Sekunde verarbeiteten, was ungefähr dem Zweifachen der üblichen Auslastung entspricht. Beachten Sie, dass für einen Python-Dienst im letzten Jahr 102 Instanzen erforderlich waren.

Es scheint, dass der Service on Go vom ersten Ansatz an alle unsere Bedürfnisse erfüllt hat. Aber Golang ist keine "One-Stop-Lösung für alle Probleme". Auf einige Funktionen konnte es nicht verzichten. Zum Beispiel mussten wir die Anzahl der Threads begrenzen, die der Dienst auf dem Kubernetes-Multiprozessorknoten starten kann, damit beim Skalieren "benachbarte" Anwendungen in der Produktion nicht beeinträchtigt werden (standardmäßig hat Go keine Begrenzung für die Anzahl der Prozessoren). Dazu setzen wir GOMAXPROCS in allen Anwendungen auf Go. Wir werden gerne kommentieren, wie nützlich dies war - in unserem Team war dies nur eine der Hypothesen zum Umgang mit der Verschlechterung von "Nachbarn".

Ein weiteres „Setup“ ist die Anzahl der Verbindungen, die als Keep-Alive gehalten werden. Normale http- und DB-Clients in Go halten standardmäßig nur zwei Verbindungen. Wenn also viele Anforderungen gleichzeitig vorliegen und Sie den Datenverkehr beim Einrichten der TCP-Verbindung sparen müssen, ist es sinnvoll, diesen Wert durch Festlegen von MaxIdleConnsPerHost bzw. SetMaxIdleConns zu erhöhen.

Trotz dieser manuellen „Wendungen“ bot uns Golang eine große Leistungsspanne für zukünftige Verkäufe.

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


All Articles