
Hallo allerseits! Mein Name ist Maxim Ryndin, ich bin der Teamleiter von zwei Teams in Gett - Billing und Infrastructure. Ich möchte über die Produktwebentwicklung sprechen, die wir bei Gett hauptsächlich mit Go verwenden. Ich werde Ihnen erzählen, wie wir 2015-2017 auf diese Sprache umgestellt haben, warum wir sie überhaupt gewählt haben, auf welche Probleme wir während des Übergangs gestoßen sind und welche Lösungen wir gefunden haben. Und ich werde Ihnen im nächsten Artikel über die aktuelle Situation berichten.
Für diejenigen, die es nicht wissen: Gett ist ein internationaler Taxidienst, der 2011 in Israel gegründet wurde. Gett ist jetzt in 4 Ländern vertreten: Israel, Großbritannien, Russland und den USA. Die Hauptprodukte unseres Unternehmens sind mobile Anwendungen für Kunden und Fahrer, ein Webportal für Firmenkunden, auf dem Sie ein Auto bestellen können, und eine Reihe interner Admin-Panels, über die unsere Mitarbeiter Tarifpläne erstellen, neue Fahrer verbinden, Betrugsfälle überwachen und vieles mehr. Ende 2016 wurde in Moskau ein globales F & E-Büro eröffnet, das im Interesse des gesamten Unternehmens arbeitet.
Wie wir zu Go kamen
Im Jahr 2011 war das Hauptprodukt des Unternehmens eine monolithische Anwendung auf Ruby on Rails, da dieses Framework zu dieser Zeit sehr beliebt war. Es gab erfolgreiche Beispiele für Unternehmen, die schnell auf Ruby on Rails entwickelt und gestartet wurden, sodass dies mit dem Geschäftserfolg verbunden war. Das Unternehmen entwickelte sich, neue Fahrer und Benutzer kamen zu uns, die Lasten wuchsen. Und die ersten Probleme traten auf.
Damit die Client-Anwendung den Standort des Fahrzeugs anzeigt und seine Bewegung wie eine glatte Kurve aussieht, müssen Fahrer häufig ihre Koordinaten senden. Daher war der Endpunkt, der für den Empfang von Koordinaten von Fahrern verantwortlich war, fast immer am stärksten belastet. Und das Webserver-Framework in Ruby on Rails hat dies schlecht gemacht. Es war möglich, nur umfangreich zu skalieren und neue Anwendungsserver hinzuzufügen, was teuer und ineffizient ist. Infolgedessen haben wir die funktionale Sammlung von Koordinaten in einem separaten Dienst herausgenommen, der ursprünglich in JS geschrieben wurde. Für eine Weile löste dies das Problem. Als die Last jedoch zunahm und wir 80.000 U / min erreichten, hörte der Dienst auf Node.js auf, uns zu retten.
Dann haben wir einen Hackathon ausgerufen. Alle Mitarbeiter des Unternehmens hatten an einem Tag die Möglichkeit, einen Prototyp zu schreiben, der die Koordinaten der Fahrer sammeln sollte. Hier sind die Benchmarks für zwei Versionen dieses Dienstes: Laufen auf Prod und Umschreiben auf Go.
In fast jeder Hinsicht zeigte der Service on Go die besten Ergebnisse. Der Dienst auf Node.js verwendete einen Cluster, eine Technologie zur Verwendung aller Kerne einer Maschine. Das heißt, das Experiment war ein Plus oder Minus ehrlich. Obwohl Node.js den Nachteil einer Single-Threaded-Laufzeit hat, hat dies keine Auswirkungen auf die Ergebnisse.
Allmählich wuchsen unsere Produktanforderungen. Wir haben immer mehr Funktionen entwickelt, und sobald wir auf ein solches Problem gestoßen sind: Wenn Sie an einer Stelle Code hinzufügen, kann an einer anderen Stelle, an der das Projekt stark verbunden ist, etwas kaputt gehen. Wir haben uns entschlossen, diese Geißel zu überwinden, indem wir auf eine serviceorientierte Architektur umgestiegen sind. Die Leistung verschlechterte sich jedoch infolgedessen: Wenn der Ruby on Rails-Interpreter bei der Ausführung des Codes auf eine Netzwerkanforderung stößt, wird diese blockiert und der Worker ist inaktiv. Und Netzwerk-E / A-Operationen sind immer mehr geworden.
Aus diesem Grund haben wir uns für Go als eine der wichtigsten Entwicklungssprachen entschieden.
Merkmale unserer Produktentwicklung
Erstens haben wir sehr unterschiedliche Produktanforderungen. Da unsere Autos in drei Ländern mit völlig unterschiedlichen Gesetzen fahren, müssen sehr unterschiedliche Funktionen implementiert werden. In Israel ist es beispielsweise gesetzlich vorgeschrieben, dass die Kosten einer Reise von einem Taxameter berücksichtigt werden - dies ist ein Gerät, das alle paar Jahre die obligatorische Zertifizierung besteht. Wenn der Fahrer die Fahrt beginnt, drückt er die Taste „Los“. Wenn er fertig ist, drückt er die Taste „Stopp“ und gibt den vom Taxameter angezeigten Preis in die Anwendung ein.
In Russland gibt es keine derart strengen Gesetze. Hier können wir die Preispolitik selbst konfigurieren. Binden Sie es beispielsweise an die Dauer der Reise oder an die Entfernung. Wenn wir dieselbe Funktionalität implementieren möchten, führen wir sie manchmal zuerst in einem Land ein und passen sie dann in anderen Ländern an und führen sie ein.
Unsere Produktmanager stellen Anforderungen in Form von Produktgeschichten. Wir versuchen, uns an einen solchen Ansatz zu halten. Dies hinterlässt beim Testen automatisch Spuren: Wir verwenden die verhaltensgesteuerte Entwicklungsmethode, damit eingehende Produktanforderungen auf Testsituationen projiziert werden können. Für Leute, die weit davon entfernt sind zu programmieren, ist es einfacher, nur die Testergebnisse zu lesen und zu verstehen, was was ist.
Wir wollten auch die Verdoppelung einiger Arbeiten beseitigen. Wenn wir einen Dienst haben, der irgendeine Art von Funktionalität implementiert, und wir einen zweiten Dienst schreiben müssen, der alle Probleme, die wir im ersten gelöst haben, erneut löst und wieder in Überwachungs- und Migrationstools integriert, ist dies ineffektiv.
Wir lösen Probleme
Framework
Ruby on Rails basiert auf der MVC-Architektur. Zum Zeitpunkt des Übergangs wollten wir es wirklich nicht aufgeben, um den Entwicklern, die nur auf diesem Framework programmieren können, das Leben zu erleichtern. Das Ändern der Werkzeuge erhöht den Komfort nicht. Wenn Sie auch die Architektur der Anwendung ändern, entspricht dies dem Drücken einer Person, die nicht weiß, wie man von einem Boot aus schwimmt. Wir wollten den Entwicklern nicht auf diese Weise schaden, deshalb haben wir eines der wenigen MVC-Frameworks namens
Beego verwendet .
Wir haben versucht, Beego wie in Ruby on Rails für das serverseitige Rendern zu verwenden. Die auf dem Server gerenderte Seite hat uns jedoch nicht wirklich gefallen. Ich musste eine Komponente wegwerfen, und heute produziert Beego nur JSON aus dem Backend, und das gesamte Rendering wird von React auf der Vorderseite ausgeführt.
Mit Beego können Sie ein Projekt automatisch erstellen. Für einige Entwickler war es sehr schwierig, von einer Skriptsprache auf die Notwendigkeit des Kompilierens umzusteigen. Es gab lustige Geschichten, als eine Person eine Funktion implementierte, und nur durch Codeüberprüfung oder sogar versehentlich herausgefunden, dass Sie, wie sich herausstellt, einen Go-Build durchführen müssen. Und die Aufgabe ist bereits geschlossen.
In Beego wird ein Router aus einem Kommentar generiert, in den der Entwickler den Pfad zu den Controller-Aktionen schreibt. Wir haben eine zweideutige Einstellung zu dieser Idee, denn wenn beispielsweise ein Tippfehler, der Router, erneut eingegeben wurde, ist es für diejenigen, die in diesem Ansatz nicht ausgefeilt sind, schwierig, einen Fehler zu finden. Manchmal konnten die Leute die Gründe selbst nach mehreren Stunden aufregenden Debuggens nicht herausfinden.
Datenbank
Wir verwenden PostgreSQL als Datenbank. Es gibt eine solche Praxis - das Datenbankschema vom Anwendungscode aus zu steuern. Dies ist aus mehreren Gründen praktisch: Jeder kennt sie; Sie sind einfach bereitzustellen, die Datenbank ist immer mit dem Code synchronisiert. Und wir wollten diese Brötchen auch behalten.
Wenn Sie mehrere Projekte und Teams haben, müssen Sie manchmal in die Projekte anderer Personen kriechen, um die Funktionalität zu implementieren. Und es ist sehr verlockend, der Tabelle eine Spalte hinzuzufügen, in der 10 Millionen Datensätze erscheinen können. Und eine Person, die nicht in dieses Projekt vertieft ist, ist sich möglicherweise der Größe des Tisches nicht bewusst. Um dies zu verhindern, haben wir eine Warnung vor gefährlichen Migrationen ausgegeben, die die Datenbank für die Aufzeichnung blockieren könnten, und den Entwicklern die Möglichkeit gegeben, diese Warnung zu entfernen.
Die Migration
Wir haben uns für die
Migration mit
Swan entschieden , einer
gepatchten Gans , bei der wir einige Verbesserungen vorgenommen haben. Diese beiden Tools möchten, wie viele Migrationstools, alles in einer Transaktion ausführen, sodass Sie bei Problemen problemlos ein Rollback durchführen können. Manchmal müssen Sie einen Index erstellen, und die Tabelle ist gesperrt. PostgreSQL verfügt über einen Parameter, der dies
concurrently
vermeidet. Das Problem ist, dass, wenn Sie in PostgreSQL
concurrently
mit der Erstellung eines Index beginnen und sogar in einer Transaktion ein Fehler auftritt. Zuerst wollten wir ein Flag hinzufügen, um keine Transaktion zu öffnen. Und am Ende haben sie das getan:
COMMIT; CREATE INDEX CONCURRENTLY huge_index ON huge_table (column_one, column_two); BEGIN;
Wenn jemand einen Index mit dem
concurrently
Parameter hinzufügt, erhält er diesen Hinweis. Beachten Sie, dass
commit
und
begin
nicht verwechselt werden. Dieser Code schließt die Transaktion, die das Migrationstool geöffnet hat, rollt dann den Index mit dem
concurrently
Parameter und öffnet dann eine andere Transaktion, sodass das Tool etwas schließt.
Testen
Wir versuchen, uns an eine verhaltensgetriebene Entwicklung zu halten. In Go kann dies mit dem
Ginkgo- Tool erfolgen. Es ist gut, weil es die üblichen Schlüsselwörter für BDD, "beschreiben", "wann" und andere enthält und es Ihnen auch ermöglicht, vom Produktmanager geschriebenen Text einfach auf Testsituationen zu projizieren, die im Quellcode gespeichert sind. Aber wir sind auf ein Problem gestoßen: Leute, die aus der Welt von Ruby on Rails kamen, glauben, dass es in jeder Programmiersprache etwas Ähnliches wie ein Fabrikmädchen gibt - eine Fabrik zur Schaffung von Anfangsbedingungen. In Go gab es jedoch nichts Vergleichbares. Am Ende haben wir beschlossen, das Rad nicht neu zu erfinden: Kurz vor jedem Test, in den Haken vor und nach dem Test, füllen wir die Datenbank mit den erforderlichen Daten und bereinigen sie dann, damit keine Nebenwirkungen auftreten.
Überwachung
Wenn Sie einen Produktionsservice haben, auf den Benutzer zugreifen, müssen Sie dessen Arbeit verfolgen: Gibt es fünfhundert Fehler oder werden Anforderungen schnell verarbeitet? In der Welt von Ruby on Rails wird NewRelic sehr oft dafür verwendet, und viele unserer Entwickler haben es gut besessen. Sie verstanden, wie das Tool funktionierte und wo sie nach Problemen suchen sollten. Mit NewRelic können Sie die Verarbeitungszeit von Anforderungen über HTTP analysieren, langsame externe Anrufe und Anforderungen an die Datenbank identifizieren, Datenflüsse überwachen, intelligente Fehleranalysen und Warnungen bereitstellen.
NewRelic verfügt über die Apdex-Aggregatfunktion, die vom Histogramm der Verteilung der Dauer der Antworten und einigen Werten abhängt, die Sie für normal halten und die ganz am Anfang festgelegt werden. Diese Funktion hängt auch von der Fehlerstufe in der Anwendung ab. NewRelic berechnet Apdex und gibt eine Warnung aus, wenn sein Wert unter ein bestimmtes Niveau fällt.
NewRelic ist auch gut darin, kürzlich einen offiziellen Go-Agenten zu haben. So sieht die allgemeine Überwachungsübersicht aus:

Auf der linken Seite befindet sich ein Abfrageverarbeitungsdiagramm, das jeweils in Segmente unterteilt ist. Zu den Segmenten gehören Anforderungswarteschlangen, Middleware-Verarbeitung, Verweildauer im Ruby on Rails-Interpreter und Zugriff auf Repositorys.
Das Apdex-Diagramm wird oben rechts angezeigt. Unten rechts - Häufigkeit der Bearbeitung von Anfragen.
Die Intrige ist, dass Sie in Ruby on Rails zum Verbinden von NewRelic eine Codezeile hinzufügen und Ihre Anmeldeinformationen zur Konfiguration hinzufügen müssen. Und alles funktioniert auf magische Weise. Dies ist möglich, weil in Ruby on Rails Affen-Patches vorhanden sind, die nicht in Go enthalten sind. Daher gibt es manuell viel zu tun.
Zunächst wollten wir die Dauer der Anforderungsverarbeitung messen. Dies wurde mit den von Beego bereitgestellten Haken durchgeführt.
beego.InsertFilter("*", beego.BeforeRouter, StartTransaction, false) beego.InsertFilter("*", beego.AfterExec, NameTransaction, false) beego.InsertFilter("*", beego.FinishRouter, EndTransaction, false)
Der einzige nicht triviale Punkt war, dass wir die Eröffnung der Transaktion und ihre Benennung geteilt haben. Warum haben wir das gemacht? Ich wollte die Dauer der Anforderungsverarbeitung unter Berücksichtigung der für das Routing aufgewendeten Zeit messen. Gleichzeitig benötigen wir Berichte, die nach den Endpunkten aggregiert sind, an die die Anforderungen gesendet wurden. Zum Zeitpunkt des Öffnens der Transaktion haben wir jedoch noch kein URL-Muster definiert, nach dem eine Übereinstimmung stattfinden wird. Wenn eine Anfrage eintrifft, öffnen wir daher eine Transaktion und schließen sie am Hook ab, nachdem wir den Controller ausgeführt haben, benennen ihn und schließen ihn nach der Verarbeitung. Daher sehen unsere Berichte heute so aus:

Wir haben ein ORM namens GORM verwendet, weil wir die Abstraktion beibehalten und Entwickler nicht zwingen wollten, reines SQL zu schreiben. Dieser Ansatz hat sowohl Vor- als auch Nachteile. In der Welt von Ruby on Rails gibt es einen ORM Active Record, der die Leute wirklich verwöhnt. Entwickler vergessen, dass Sie reines SQL schreiben und nur mit ORM-Aufrufen arbeiten können.
db.Callback().Create().Before("gorm:begin_transaction"). Register("newrelicStart", startSegment) db.Callback().Create().After("gorm:commit_or_rollback_transaction"). Register("newrelicStop", endSegment)
Um die Dauer der Abfrageausführung in der Datenbank bei Verwendung von GORM zu messen, müssen Sie das Datenbankobjekt verwenden. Rückruf sagt, dass wir einen Rückruf registrieren möchten. Es sollte beim Erstellen einer neuen Entität aufgerufen werden - ein Aufruf zum
Create
. Dann geben wir genau an, wann Callback gestartet werden soll.
Before
ist dafür mit dem Argument
begin_transaction
verantwortlich:
begin_transaction
ist ein Zeitpunkt zum Zeitpunkt des
begin_transaction
der Transaktion. Als nächstes registrieren
newrelicStart
unter dem Namen
newrelicStart
die Funktion
startSegment
, die einfach den Go-Agenten aufruft und ein neues Segment für den Zugriff auf die Datenbank öffnet.
ORM ruft diese Funktion auf, bevor wir die Transaktion öffnen und damit das Segment öffnen. Wir müssen dasselbe tun, um das Segment zu schließen: Hängen Sie einfach den Rückruf auf.
Zusätzlich zu PostgreSQL verwenden wir Redis, das auch nicht glatt ist. Für diese Überwachung haben wir einen Wrapper über einen Standardclient geschrieben und dasselbe für den Aufruf externer Dienste getan. Folgendes ist passiert:

So sieht die Überwachung für eine in Go geschriebene Anwendung aus. Links sehen Sie einen Bericht über die Dauer der Abfrageverarbeitung, der aus Segmenten besteht: Ausführung des Codes selbst in Go, Zugriff auf die PostgreSQL- und Replica-Datenbanken. Anrufe an externe Dienste werden in diesem Diagramm nicht angezeigt, da es nur sehr wenige davon gibt und sie im Durchschnitt einfach unsichtbar sind. Wir haben auch Informationen zu Apdex und der Häufigkeit der Anforderungsverarbeitung. Im Allgemeinen erwies sich die Überwachung als sehr informativ und nützlich für die Verwendung.
Bei Datenströmen können wir dank unserer Wrapper über den HTTP-Client Anforderungen an externe Dienste verfolgen. Das Anforderungsschema für Werbedienstleistungen ist hier angegeben: Es bezieht sich auf vier unserer anderen Dienste und zwei Repositorys.

Fazit
Heute haben wir mehr als 75% der Produktionsservices in Go geschrieben. Wir führen keine aktive Entwicklung in Ruby durch, sondern unterstützen sie nur. Und diesbezüglich möchte ich Folgendes bemerken:
- Befürchtungen, dass die Entwicklungsgeschwindigkeit abnimmt, wurden nicht bestätigt. Programmierer haben die neue Technologie jeweils in ihrem eigenen Modus eingeführt, aber im Durchschnitt wurde die Entwicklung auf Go nach ein paar Wochen aktiver Arbeit so vorhersehbar und schnell wie auf Ruby on Rails.
- Die Leistung von Go-Anwendungen unter Last ist im Vergleich zu früheren Erfahrungen angenehm überraschend. Wir haben die Nutzung der Infrastruktur in AWS erheblich gespart und die Anzahl der verwendeten Instanzen erheblich reduziert.
- Der Technologiewandel hat Programmierer erheblich ermutigt, und dies ist ein wichtiger Teil eines erfolgreichen Projekts.
- Heute haben wir Beego und Gorm bereits verlassen, mehr dazu im nächsten Artikel.
Zusammenfassend möchte ich sagen, dass Sie, wenn Sie nicht auf Go schreiben, unter Problemen mit hoher Arbeitsbelastung leiden und sich mit dem Verkehr langweilen, in diese Sprache wechseln. Vergessen Sie nicht, mit dem Unternehmen zu verhandeln.