Das ewige Problem der technischen Verschuldung


Dies ist eines der coolsten Projektreliefs. Im Bild - ein Diagramm der Gesamtzeit, die die CPU für die Verarbeitung aller Benutzeranforderungen benötigt. Am Ende sehen Sie den Übergang zu PHP 7.0. seit Version 5.6. Dies ist das Jahr 2016, das am Nachmittag ab dem 24. November wechselt.

Aus der Sicht der Berechnungen ist Tutu.ru in erster Linie eine Gelegenheit, ein Ticket von Punkt A nach Punkt B zu kaufen. Dazu erstellen wir eine große Anzahl von Flugplänen, speichern die Antworten vieler Airline-Systeme zwischen und stellen regelmäßig unglaublich lange Verbindungsabfragen an die Datenbank. Im Allgemeinen sind wir in PHP geschrieben und waren bis vor kurzem vollständig damit beschäftigt (wenn die Sprache richtig vorbereitet ist, können Sie sogar Echtzeitsysteme darauf aufbauen). In letzter Zeit wurden leistungskritische Bereiche auf Go überarbeitet.

Wir haben ständig technische Schulden . Und das geht schneller als wir möchten. Die gute Nachricht: Sie müssen nicht alles abdecken. Schlecht: Mit zunehmender unterstützter Funktionalität steigt auch die technische Verschuldung proportional.

Im Allgemeinen ist die technische Verschuldung eine Zahlung für einen Fehler bei der Entscheidungsfindung. Sie haben so etwas wie den Architekten nicht vorhergesagt, dh Sie haben einen Prognosefehler gemacht oder eine Entscheidung unter den Bedingungen unzureichender Informationen getroffen. Irgendwann verstehen Sie, dass Sie etwas im Code ändern müssen (häufig auf Architekturebene). Dann können Sie sofort wechseln, aber Sie können warten. Wenn Sie warten, stiegen die Zinsen auf die technischen Schulden. Daher ist es empfehlenswert, es von Zeit zu Zeit neu zu strukturieren. Nun, oder erklären Sie sich für bankrott und schreiben Sie den ganzen Block erneut.

Wie alles begann: Monolith und allgemeine Funktionen


Das Tutu.ru-Projekt begann 2003 als reguläre Runet-Website dieser Zeit. Das heißt, es war eine Reihe von Dateien anstelle einer Datenbank, eine PHP-Seite auf der Vorderseite von HTML + JS. Es gab ein paar exzellente Hacks von meinem Kollegen Yuri, aber er wird es ihm eines Tages besser sagen. Ich trat dem Projekt 2006 bei, zunächst als externer Berater, der sowohl mit Ratschlägen als auch mit einem Kodex helfen konnte, und wechselte dann 2009 als technischer Direktor in den Staat. Zuallererst war es notwendig, die Dinge in Richtung Flugtickets zu ordnen: Es war der am meisten belastete und komplexeste Teil der Architektur.

Ich erinnere Sie daran, dass es 2006 einen Zugfahrplan gab und die Möglichkeit bestand, ein Bahnticket zu kaufen. Wir haben uns entschlossen, den Bereich Flugtickets als separates Projekt zu gestalten, das heißt, all dies wurde nur an der Front vereint. Alle drei Projekte (Zugfahrpläne, Eisenbahn und Luftfahrt) wurden schließlich auf ihre eigene Weise geschrieben. Zu dieser Zeit schien uns der Code normal, aber etwas unvollendet. Nicht-Perfektionist. Dann wurde er alt, bedeckte sich mit Krücken und verwandelte sich in Eisenbahnrichtung bis 2010 in einen Kürbis.

In der Eisenbahn hatten wir keine Zeit, technische Schulden zu machen. Refactoring war unrealistisch: Die Probleme lagen in der Architektur. Wir haben uns entschlossen, alles noch einmal abzureißen und zu wiederholen, aber es war auch bei einem Live-Projekt schwierig. Infolgedessen wurden nur die alten URLs vorne belassen und dann Block für Block neu geschrieben. Als Grundlage haben wir die Ansätze verwendet, die ein Jahr zuvor bei der Entwicklung des Luftverkehrssektors verwendet wurden.

In PHP umgeschrieben. Dann war klar, dass dies nicht der einzige Weg war, aber es gab keine vernünftigen Alternativen für uns. Sie haben es gewählt, weil sie bereits Erfahrung und Erfolge hatten. Es war klar, dass dies eine gute Sprache in den Händen von leitenden Entwicklern ist. Von den Alternativen waren C und C ++ wahnsinnig produktiv, aber jeder Wiederaufbau oder jede Änderung an ihnen ähnelte einem Albtraum. Okay, nicht erinnert. Waren ein Albtraum.

MS und alle .NET aus Sicht eines Hochlastprojekts wurden nicht einmal berücksichtigt. Dann gab es überhaupt keine anderen Optionen als Linux-basiert. Java ist eine gute Option, erfordert jedoch Ressourcen aus dem Speicher, vergibt niemals Junior-Fehler und hat es dann nicht möglich gemacht, Releases schnell zu veröffentlichen - na ja, oder das wussten wir nicht. Selbst jetzt betrachten wir Python nicht als Backend, sondern nur für Datenmanipulationsaufgaben. JS - rein unter der Front. Damals (und heute) gab es keine Ruby on Rails-Entwickler. Go war weg. Es gab immer noch Perl, aber Experten bewerteten es als nicht vielversprechend für die Webentwicklung, so dass sie es auch aufgaben. PHP bleibt übrig.

Die nächste ganzheitliche Geschichte ist PostgreSQL vs. MySQL. Irgendwo besser, woanders. Im Allgemeinen war es dann eine gute Praxis, das zu wählen, was sich als besser herausstellte, also entschieden wir uns für MySQL und seine Gabeln.

Der Entwicklungsansatz war monolithisch, dann gab es einfach keine anderen Ansätze, sondern mit der orthogonalen Struktur der Bibliotheken. Dies sind die Anfänge des modernen API-zentrierten Ansatzes, bei dem jede Bibliothek eine Außenfassade hat, für die Sie direkt aus anderen Teilen des Projekts in den Code ziehen können. Bibliotheken wurden in „Ebenen“ geschrieben, wenn jede Ebene am Eingang ein bestimmtes Format hat und ein bestimmtes Format weiter an den Code übergibt und Unit-Tests zwischen ihnen gedreht werden. Das heißt, so etwas wie testgetriebene Entwicklung, aber pixelig und beängstigend.

All dies wurde auf mehreren Servern gehostet, was eine Skalierung unter Last ermöglichte. Gleichzeitig kreuzte sich die Codebasis verschiedener Projekte auf Systemebene ziemlich stark. Dies bedeutete in der Tat, dass Änderungen im Eisenbahnprojekt Auswirkungen auf unsere eigenen Flugzeuge haben könnten. Und oft berührt. In der Eisenbahn war es beispielsweise notwendig, die Arbeit mit Zahlungen zu erweitern - dies ist eine Überarbeitung der gemeinsamen Bibliothek. Und das Flugzeug arbeitet damit, daher sind gemeinsame Tests erforderlich. Wir haben die Abhängigkeiten mit Tests überprüft, und dies war mehr oder weniger normal. Bereits 2009 war die Methode weit fortgeschritten. Trotzdem könnte die Last eine weitere aus einer Ressource hinzufügen. Es gab eine Überschneidung in den Datenbanken, die zu unangenehmen Auswirkungen in Form von Bremsen auf der gesamten Website mit lokalen Problemen in einem Produkt führte. Railway hat das Flugzeug mehrmals auf der Festplatte getötet, weil die Datenbank stark abgefragt wurde.

Wir haben skaliert, indem wir Instanzen hinzugefügt und zwischen ihnen ausgeglichen haben. Monolith wie es ist.

Reifenalter


Dann gingen wir einen eher marginalen Weg. Einerseits haben wir begonnen, Dienste zu isolieren (heute wird dieser Ansatz als Mikroservice bezeichnet, aber wir kannten das Wort „Mikro“ nicht), aber für die Interaktion haben wir begonnen, den Bus für die Datenübertragung anstelle von REST oder gRPC zu verwenden, wie dies jetzt der Fall ist. Wir haben AMQP als Protokoll und RabbitMQ als Nachrichtenbroker ausgewählt. Zu diesem Zeitpunkt hatten wir den Start von Daemons für PHP (ja, es gibt eine voll funktionsfähige Fork () - Implementierung und alles andere für die Arbeit mit Prozessen) bekanntermaßen gemeistert, da wir im Monolithen lange Zeit so etwas wie Gearman verwendet haben, um Anfragen an Reservierungssysteme zu parallelisieren .

Sie machten einen Makler auf dem Kaninchen, und es stellte sich heraus, dass all dies nicht wirklich unter Last lebt. Eine Art von Netzwerkverlusten, erneuten Übertragungen, Verzögerungen. Zum Beispiel verhält sich ein Cluster von mehreren Brokern "out of the box" etwas anders als vom Entwickler angegeben (es ist noch nie zuvor passiert). Im Allgemeinen haben sie viel gelernt. Aber am Ende haben wir die für die Dienste erforderlichen SLAs erhalten. Beispielsweise hat der am meisten ausgelastete RPS-Dienst mit 400 U / s den 99. Perzentil-Roundtrip von Client zu Client, einschließlich der Bus- und Dienstverarbeitung in der Größenordnung von 35 ms. Insgesamt beobachten wir jetzt im Bus ca. 18 krps.

Dann kam die Richtung der Busse. Wir haben es sofort ohne Monolithen auf die Servicearchitektur geschrieben. Da alles von Grund auf neu geschrieben wurde, stellte sich heraus, dass es sehr gut, schnell und bequem war, obwohl es notwendig war, die Werkzeuge für einen neuen Ansatz ständig zu verfeinern. Ja, all dies drehte sich auf virtuellen Maschinen, in denen PHP-Dämonen über den Bus kommunizieren. Dämonen begannen in Docker-Containern, aber es gab keine Lösungen für die Orchestrierung wie Openshift oder Kubernetes. 2014 haben wir gerade erst angefangen, darüber zu sprechen, aber wir haben einen solchen Vertriebsansatz nicht in Betracht gezogen.

Wenn Sie vergleichen, wie viele Bustickets im Vergleich zu Flug- oder Bahntickets verkauft werden, erhalten Sie einen Tropfen auf den heißen Stein. Und in Zügen und Flugzeugen war es schwierig, auf eine neue Architektur umzusteigen, da es funktionierende Funktionen gab, eine echte Last, und es gab immer die Wahl, etwas Neues zu tun oder Geld für die Tilgung einer technischen Schuld auszugeben.

Der Wechsel zu Diensten ist eine gute Sache, aber eine lange, aber Sie müssen sich jetzt mit der Last und Zuverlässigkeit befassen. Parallel dazu ergriffen sie gezielte Maßnahmen, um die Lebensdauer des Monolithen zu verbessern. Die Backends wurden in Produkttypen unterteilt, d. H. Sie begannen, die Weiterleitung von Anforderungen in Abhängigkeit von ihrem Typ flexibler zu steuern: Luft getrennt von der Eisenbahn usw. Es war möglich, die Last vorherzusagen und unabhängig zu skalieren. Als sie wussten, dass beispielsweise bei Eisenbahnen der Höhepunkt des Neujahrsumsatzes erreicht wurde, wurden mehrere Instanzen virtueller Maschinen hinzugefügt. Es begann dann genau 45 Tage vor dem letzten Arbeitstag des Jahres und am 14. und 15. November hatten wir eine doppelte Ladung. Jetzt haben FPK und andere Fluggesellschaften mit dem Verkaufsstart für 60, 90 und sogar 120 Tage viele Tickets gemacht, und dieser Höhepunkt hat sich ausgebreitet. Aber am letzten Arbeitstag im April werden elektrische Züge immer vor Mai belastet, und es gibt immer noch Spitzen. Aber über die Saisonalität der Fahrkarten und die Art der Migration der Demobilisierung werden meine Kollegen von der Eisenbahn besser berichten, und ich werde weiter über die Architektur sprechen.

Irgendwann im Jahr 2014 begannen sie, eine große Datenbank mit vielen kleinen Datenbanken zu derbanen. Dies war wichtig, weil es gefährlich wuchs und der Sturz kritisch war. Wir haben damit begonnen, separate kleine Datenbanken (für 5-10 Tabellen) für eine bestimmte Funktionalität zuzuweisen, damit andere Dienste weniger Störungen verursachen und all dies einfacher skaliert werden kann. Es ist erwähnenswert, dass wir zum Lastenausgleich und zur Skalierung Replikate zum Lesen verwendet haben. Das Wiederherstellen von Replikaten für eine große Basis nach einem Replikationsfehler konnte Stunden dauern, und die ganze Zeit musste ich "auf Bewährung und auf einem Flügel fliegen". Erinnerungen an solche Perioden verursachen immer noch eine unangenehme Kälte irgendwo zwischen den Ohren. Jetzt haben wir ungefähr 200 Instanzen verschiedener Basen, und die Verwaltung so vieler Installationen mit unseren Händen ist eine mühsame und unzuverlässige Angelegenheit. Daher verwenden wir Github Orchestrator, der die Arbeit mit Replikaten und proxySql automatisiert, um die Last zu verteilen und vor dem Ausfall einer bestimmten Datenbank zu schützen.

Wie jetzt


Im Allgemeinen haben wir nach und nach begonnen, asynchrone Aufgaben zuzuweisen und ihre Starts im Ereignishandler zu trennen, damit einer den anderen nicht stört.

Als PHP 7 herauskam, sahen wir in den Tests einen sehr großen Fortschritt bei der Leistung und der Reduzierung des Ressourcenverbrauchs. Der Umzug mit ein wenig Hämorrhoiden erfolgte, das gesamte Projekt vom Beginn der Tests bis zur vollständigen Übersetzung der gesamten Produktion dauerte etwas mehr als sechs Monate, danach sank der Ressourcenverbrauch jedoch fast doppelt. CPU-Ladezeitleiste - oben im Beitrag.

Der Monolith hat bis heute überlebt und macht nach meiner Schätzung ungefähr 40% der Codebasis aus. Es ist anzumerken, dass die Aufgabe, den gesamten Monolithen durch Dienste zu ersetzen, nicht explizit festgelegt ist. Wir bewegen uns pragmatisch: Alles Neue wird auf Microservices ausgeführt. Wenn Sie die alte Funktionalität in einem Monolithen verfeinern müssen, versuchen wir, sie auf die Servicearchitektur zu übertragen, wenn nur die Verfeinerung nicht wirklich sehr klein ist. Gleichzeitig wird der Monolith in Tests behandelt, sodass wir ihn zweimal pro Woche mit einem ausreichenden Qualitätsniveau einsetzen können. Die Funktionen werden auf unterschiedliche Weise behandelt, Unit-Tests sind ziemlich vollständig, UI-Tests und Akzeptanztests decken fast die gesamte Portalfunktionalität ab (wir haben ungefähr 15.000 Testfälle), API-Tests sind mehr oder weniger vollständig. Wir führen fast keine Lasttests durch. Genauer gesagt ähnelt unsere Inszenierung in der Struktur der Produktion, jedoch nicht in der Leistung, und ist mit derselben Überwachung ausgestattet. Wir erzeugen eine Last. Wenn wir feststellen, dass sich der vorherige Lauf der alten Version zeitlich unterscheidet, sehen wir, wie kritisch er ist. Wenn die neue Version und die alte ungefähr gleich sind, veröffentlichen wir sie im Produkt. In jedem Fall gehen alle Funktionen unter dem Schalter aus, so dass Sie ihn jederzeit ausschalten können, wenn etwas schief geht.

Schwere Funktionen werden immer für 1% der Benutzer getestet. Dann gehen wir zu 2%, 5%, 10% und erreichen so alle Benutzer. Das heißt, wir können immer die atypische Last sehen, bevor der Anstieg die Server tötet, und die Verbindung im Voraus trennen.

Bei Bedarf haben wir 4-5 Monate für ein Reengineering-Projekt gebraucht (und werden es auch brauchen), wenn sich das Team auf eine bestimmte Aufgabe konzentriert. Dies ist ein guter Weg, um den gordischen Knoten zu durchtrennen, wenn lokales Refactoring nicht mehr hilft. So haben wir es vor ein paar Jahren mit Luft gemacht: Wir haben die Architektur überarbeitet, haben es getan - sofort sofortige Beschleunigung in der Entwicklung, konnten viele neue Funktionen einführen. Zwei Monate nach dem Reengineering wuchsen sie aufgrund der Funktionen für die Kunden um eine Größenordnung. Sie fingen an, die Preise genauer zu verwalten, Partner zu verbinden, alles wurde schneller. Freude. Ich muss sagen, jetzt ist es Zeit, dasselbe noch einmal zu tun, aber das ist Schicksal: Die Art und Weise, Anwendungen zu erstellen, ändert sich, neue Lösungen, Ansätze und Tools erscheinen. Um im Geschäft zu bleiben, müssen Sie wachsen.

Die Hauptaufgabe des Reengineerings besteht für uns darin, die Entwicklung weiter zu beschleunigen. Wenn nichts Neues benötigt wird, ist kein Reengineering erforderlich. Keine Notwendigkeit, eine neue zu erfinden: Es macht keinen Sinn, in die Modernisierung zu investieren. Während ein moderner Stack und eine moderne Architektur beibehalten werden, können die Mitarbeiter schneller arbeiten, eine neue Verbindung wird schneller hergestellt, das System verhält sich vorhersehbarer, Entwickler sind mehr an der Arbeit an einem Projekt interessiert. Jetzt gibt es eine Aufgabe, den Monolithen fertigzustellen, ohne ihn vollständig wegzuwerfen, damit jedes Produkt Updates hochladen kann, unabhängig von anderen. Das heißt, Sie erhalten eine bestimmte CI / CD in einem Monolithen.

Heute verwenden wir nicht nur Kaninchen, sondern auch REST und gRPC, um Informationen zwischen Diensten auszutauschen. Einige der Microservices sind in Golang geschrieben: Die Rechengeschwindigkeit und die Speicherhandhabung sind dort hervorragend. Es gab einen Aufruf zur Implementierung der NodeJS-Unterstützung, aber am Ende haben wir den Node nur für das Server-Rendering verlassen, und die Geschäftslogik wurde in PHP und Go belassen. Grundsätzlich können wir mit dem gewählten Ansatz Dienste in nahezu jeder Sprache entwickeln. Wir haben uns jedoch entschlossen, den Zoo einzuschränken, um die Komplexität des Systems nicht zu erhöhen.

Nun gehen wir zu Microservices, die in Docker-Containern unter der OpenShift-Orchestrierung funktionieren. Die Aufgabe innerhalb von anderthalb Jahren - 90% aller drehen sich innerhalb der Plattform. Warum? Es ist also schneller bereitzustellen, Versionen schneller zu überprüfen, und es gibt weniger Unterschiede beim Verkauf in der Entwicklungsumgebung. Der Entwickler kann mehr über die von ihm implementierte Funktion nachdenken und nicht darüber, wie die Umgebung bereitgestellt wird, wie sie konfiguriert wird, wo sie gestartet wird, dh mehr Nutzen. Wieder - betriebliche Probleme: Es gibt viele Microservices, die vom Management automatisiert werden müssen. Manuell - sehr hohe Kosten, das Risiko von Fehlern bei der manuellen Steuerung und die Plattform bieten eine normale Skalierung.

Jedes Jahr steigt die Arbeitsbelastung um 30 bis 40%: Immer mehr Menschen beherrschen Tricks mit dem Internet, gehen nicht mehr an physische Kassen, sondern erweitern bestehende um neue Produkte und Funktionen. Mittlerweile kommen täglich rund 1 Million Nutzer auf das Portal. Natürlich erzeugen nicht alle Benutzer die gleiche Last. Etwas erfordert überhaupt keine Rechenressourcen, und beispielsweise sind Suchvorgänge eine ziemlich ressourcenintensive Komponente. Dort erhöht ein einzelnes Häkchen „plus oder minus drei Tage“ in der Luftfahrt die Belastung um das 49-fache (beim Hin- und Herblicken beträgt die Matrix 7 x 7). Alles andere im Vergleich zur Suche nach einem Ticket innerhalb des Eisenbahnsystems und der Luft ist recht einfach. Das Einfachste an Ressourcen ist die Abenteuer- und Tourensuche (es gibt nicht den einfachsten Cache in Bezug auf die Architektur, aber es gibt immer noch viel weniger Touren als Ticketkombinationen), dann den Zugfahrplan (der mit Standardmitteln leicht zwischengespeichert werden kann) und erst dann alles andere .

Natürlich häufen sich die technischen Schulden immer noch. Von allen Seiten. Die Hauptsache ist, rechtzeitig zu verstehen, wo Sie es schaffen können, sich umzugestalten, und alles wird in Ordnung sein, wo Sie nichts anfassen müssen (manchmal passiert es: Wir leben mit Legacy, wenn keine Änderungen geplant sind), aber irgendwo müssen wir uns beeilen und neu entwickeln, denn ohne diese Zukunft wird nicht. Natürlich machen wir Fehler, aber im Allgemeinen gibt es Tutu.ru seit 16 Jahren, und ich mag die Dynamik des Projekts.

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


All Articles