Umschreiben eines bestehenden Webdienstes in Rust
Wenn Sie sich über die Trends in der Software-Entwicklung auf dem Laufenden halten, haben Sie wahrscheinlich schon einmal von der Programmiersprache Rust gehört.
Mehrere Mitglieder unseres Entwicklungsteams sind auf den Geschmack dieser Sprache gekommen, und so war es fast unvermeidlich, dass wir irgendwann ein Kernstück unserer Software in Rust neu schreiben würden 😉 .
In diesem Beitrag befassen wir uns mit der Motivation und dem Prozess, der hinter einer solchen Neufassung steht.
Warum es die erste von vielen gewesen sein könnte und was wir aus der ganzen Erfahrung gelernt haben.
Was wir nicht behandeln werden, ist eine Einführung in Rust selbst, abgesehen von den für uns relevanten Trade-Offs.
Es gibt viele gute Ressourcen zwischen einem Exercism-Kurs, Tour of Rust, dem Rust-Subreddit und der offiziellen Rust-Seite. Dies wird kein technischer Beitrag sein, sondern eher ein Beitrag über den Prozess der Einführung neuer Technologien in Ihrem Stack auf sichere und nachhaltige Weise.
Lassen Sie uns eintauchen.
Ist eine Neufassung eine gute Idee?
Es macht Spaß, mit der Technik herumzuspielen und neue Dinge zu lernen.
Wenn es jedoch um ein Produktionssystem geht, das mit realen Kundendaten arbeitet, werden die Dinge sehr viel ernster. Es muss einen wichtigeren Grund als nur "Spaß" geben, um den Aufwand in die Umschreibung eines bestehenden, funktionierenden Systems zu investieren.
Ein guter Grund dafür ist, dass die Plattform, auf der ein bestehendes System aufgebaut wurde, nicht mehr gewartet wird, dass die Mitarbeiter, die das System warten, das Unternehmen verlassen haben oder dass Sie die zugrunde liegende Technologie nicht mehr nutzen/unterstützen wollen.
In Fällen, in denen eine Codebasis über einen langen Zeitraum hinweg viele Änderungen erfahren hat und kaum gewartet wurde, was zu einem Code führt, bei dem die Kosten für Änderungen explodieren, kann es auch sinnvoll sein, neu anzufangen.
Dies gilt insbesondere für Microservices-basierte Systeme in einem kleinen Startup. Zu Beginn eines Dienstes war das Problem, das er lösen sollte, vielleicht noch nicht ganz klar. Wir können also sagen, dass es unter bestimmten Umständen sinnvoll ist, einen bestehenden (Mikro-)Dienst neu zu schreiben.
Wahl der Technologie
Die nächste Frage ist, ob die Neuschreibung in der gleichen oder einer anderen Technologie erfolgen soll. Der offensichtliche Vorteil des ersten Ansatzes ist, dass Sie einen Teil des Codes wiederverwenden können.
Dies birgt die Gefahr, dass die gleichen Probleme wie bei der alten Codebasis erneut auftreten. In manchen Situationen steht die Wahl der Technologie nicht einmal zur Debatte. Die Plattform unterstützt möglicherweise nur eine Teilmenge von Technologien. Ein Beispiel dafür wäre das Umschreiben einer JavaScript-basierten Single-Page-Anwendung. Sie könnten TypeScript, Elm oder eine der Compile-to-JavaScript-Alternativen verwenden. Aber das, was dabei herauskommt, wird zwangsläufig vorerst HTML, CSS und JavaScript sein.
Wenn es darum geht, einen bestehenden Dienst in einer für das Unternehmen neuen Technologie umzuschreiben, die sich noch nicht in der Produktion "bewährt" hat, muss es einen noch besseren Grund dafür geben, dies zu tun. Schauen wir uns also an, was unsere Motivation war, genau das zu tun, nämlich einen bestehenden, funktionierenden Microservice in Rust neu zu schreiben.
Motivation
Bei der Entscheidung, ob der betreffende Dienst in Rust umgeschrieben werden sollte oder nicht, spielten mehrere Faktoren eine Rolle. Diese Faktoren sind teilweise technischer Natur, umfassen aber auch persönliche Vorlieben der direkt beteiligten Personen im Entwicklungsteam. Wir haben den Dienst zunächst in Node.js geschrieben. Für diesen rein IO-basierten Dienst war das keine schlechte Wahl, als wir ihn zum ersten Mal implementierten. Wir waren uns bewusst, dass wir mit Node.js irgendwann an die Grenzen der Skalierbarkeit stoßen würden (CPU-gebundene Tasks), aber wir waren noch nicht so weit, sie zu erreichen. Aber abgesehen von diesem Dienst sind wir mit Node.js als Backend-Technologie nicht wirklich warm geworden.
Nach einer Weile war dieser Dienst der letzte auf Node.js basierende Produktionsdienst in unserem Cluster. Wir haben auch nicht vor, neue Dienste in Node.js zu schreiben - nicht weil Node schlecht ist, sondern aufgrund unserer persönlichen Vorlieben und Erfahrungen. Eine Neuschreibung in einer anderen Technologie hätte also Node.js aus unserem Tech-Stack entfernt und ihn vereinfacht.
Andererseits wird dieser Vorteil durch das Umschreiben mit einer neuen Technologie sofort wieder zunichte gemacht 😉 Wenn man davon ausgeht, dass die neue Technologie in größerem Umfang genutzt wird, bleibt dennoch ein Vorteil bestehen. Letzten Endes gab es (noch) keinen überzeugenden technischen oder wirtschaftlichen Grund, diesen Dienst neu zu schreiben. Der Großteil der Motivation, dies zu tun, kam von den persönlichen Vorlieben der beteiligten Personen.
Gegenleistungen
Rust ist so konzipiert, dass Belange wie Sicherheit und Leistung zu einem früheren Zeitpunkt der Entwicklung berücksichtigt werden. Durch die strengen Kompilierzeitprüfungen und das starre Typsystem werden viele Fehler bereits beim Kompilieren erkannt. Das ist großartig für die Sicherheit und Robustheit, aber es geht auf Kosten der Entwicklungsgeschwindigkeit, vor allem, wenn Sie noch nicht so vertraut mit Rust sind.
Mit der Zeit, wenn man sich mit Rust und dem Entwurf von Systemen in Rust besser auskennt, wird der Compiler zu einem mächtigen Verbündeten. Er wird Probleme zu dem Zeitpunkt erkennen, an dem sie kostengünstig zu beheben sind. Langfristig wird sich der zusätzliche Zeitaufwand wahrscheinlich dadurch amortisieren, dass man später weniger Zeit für die Fehlersuche und -behebung aufwenden muss. Der größte Nachteil von Rust ist seine berüchtigte steile Lernkurve. Es gibt auch nicht wirklich eine Möglichkeit, sie zu umgehen.
Es gibt bereits mehrere Tools und Kurse, die beim Erlernen von Rust helfen. Die Lernerfahrung und der Lernaufwand variieren auch in hohem Maße je nach Hintergrund des Lernenden. Es gibt jedoch eine Lernkurve, die steiler ist und mehr Zeit in Anspruch nimmt als bei anderen modernen Sprachen wie Go oder Kotlin.
Die größte Hürde in Bezug auf das Erlernen scheint der Borrow Checker und das mentale Modell rund um den Besitz von Speicher zu sein. Vor allem, wenn man von einer Garbage-Collector-Sprache kommt und sich eine Zeit lang nicht mit der Speicherverwaltung befasst hat, kann es einige Zeit dauern, bis man ein Gefühl dafür entwickelt. Das Gute daran ist, dass Sie die Dinge, mit denen Sie zuerst zu kämpfen hatten, viel besser verstehen werden.
Dies lässt sich auch auf andere Sprachen und Ökosysteme übertragen - sogar auf solche, die Gargabe gesammelt hat.
Engagement des Teams
Zuallererst, und das gilt für jede technologische Innovation und Veränderung, ist es wichtig, das Engagement des gesamten Teams zu erhalten. Sie müssen sicherstellen, dass Ihre Kollegen im Falle Ihres Ausscheidens nicht auf einer Code-Basis sitzen bleiben, die eine Technologie verwendet, mit der niemand vertraut ist.
In unserem Fall hatten mehrere Leute mit Rust herumgespielt, und wir hatten sogar mehrere interne Coding-Dojos und Hackathons, bei denen wir mit Rust gearbeitet haben. Wenn das Team zustimmt und an Bord ist, besteht der nächste Schritt darin, etwas Zeit einzuplanen. Das ist in der Regel der Punkt, an dem es schwierig wird, denn Zeit ist in den meisten Fällen eine knappe und hart umkämpfte Ressource.
Zeiteinteilung
Wenn es keine unmittelbaren technologischen oder wirtschaftlichen Gründe für eine Neufassung gibt, ist es noch schwieriger, das Management davon zu überzeugen, Zeit für ein solches Vorhaben zu investieren. Und das aus gutem Grund.
Auf Timeularhaben wir zwei Möglichkeiten, dieses Problem zu lösen. Die erste sind unsere Developer Focus Fridays. Jeden zweiten Freitag können die Entwickler auf Timeular selbst entscheiden, woran sie arbeiten möchten. Wenn das bedeutet, dass sie mit einer neuen Sprache experimentieren wollen - großartig! Ein neues Framework für maschinelles Lernen ausprobieren? Kein Problem!
Es gibt im Grunde keine Beschränkungen für das, was Sie tun können. Die Idee ist, die Motivation aufrechtzuerhalten und auch intern einige technische Innovationen und Lernprozesse zu fördern.
Das ist eine gute Zeitspanne, die Sie für Projekte wie dieses einplanen können.
Es gibt noch ein anderes Modell, das in unserem Fall für Funktionen funktioniert hat, die zwar interessant sein könnten, für die wir aber zu diesem Zeitpunkt keinen Zeitaufwand rechtfertigen konnten. Bei diesem Modell besteht die Idee darin, dass Leute, die an der Implementierung von etwas interessiert sind, weil sie es vielleicht technisch anspruchsvoll finden, in ihrer Freizeit daran arbeiten können.
Das Unternehmen verpflichtet sich, jedes Ergebnis zu integrieren und zu unterstützen, wenn es einen Mehrwert schafft und vom Team unterstützt wird, wenn es ausgereift ist.
Fallstricke
In einem solchen Modell ist es sehr, sehr wichtig, klare Grenzen zu setzen. Sie müssen zu 100 % vermeiden, dass normale Entwicklungsarbeit in die Freizeit der Mitarbeiter ausgelagert wird. Ähnlich wie bei den Developer Focus Fridays ist es unbedingt zu vermeiden, dass notwendige Wartungsarbeiten, Fehlerbehebungen oder neue Funktionen aus dem normalen Entwicklungszyklus herausgenommen werden. Ein solcher Missbrauch dieser Werkzeuge wird unweigerlich zu schlechten Anreizen führen, wie z.B. "es ist in Ordnung, hack es zusammen - du kannst in deiner Freizeit Fehler beheben und es hübsch machen". Vermeiden Sie dies um jeden Preis.
In diesem Szenario muss es also zu 100 % freiwillig sein, und wenn nichts dabei herauskommt, ist auch nichts passiert. Wenn sich jedoch etwas Wertvolles herauskristallisiert, verpflichtet sich das Unternehmen, die Zeit zu investieren, um die letzten Schritte zur Produktion zu unternehmen, es zu integrieren und es von da an zu pflegen.
Dieses Modell ist nicht perfekt, da es eine hohe Eigenmotivation und viel freie Zeit voraussetzt. Es ist jedoch eine Möglichkeit, Veränderungen herbeizuführen, die sonst nicht möglich wären, und solange es klare Grenzen gibt, glaube ich, dass es sich für ein Unternehmen lohnt, dieses Modell auszuprobieren.
Bei uns hat es bisher auf jeden Fall funktioniert. In der Kultur von Timeularist Überarbeitung ein absolutes Tabu. Dies spiegelt sich unter anderem in unserer 50-Tage-Urlaubspolitik wider. Aus diesem Grund mussten wir uns über die Ausnutzung der Freizeit unserer Mitarbeiter keine Gedanken machen.
Trotzdem ist es wichtig, sie im Auge zu behalten, selbst in einer Situation wie der unseren. Wir sprachen über die Motivation, die hinter einer Neufassung steht, die Nachteile, das Engagement und wie man die Zeit dafür findet.
Bleibt nur noch die Frage nach der Implementierung und den Auswirkungen auf die Laufzeit.
Umsetzung
Die wichtigste Lehre hier ist, das Ökosystem in Bezug auf die Bibliotheksunterstützung zu prüfen, bevor man sich zu sehr engagiert.
Im Fall von Rust war, als wir anfingen, mit der Idee eines Rust-Webdienstes herumzuspielen, die sehr wichtige async/await-Funktion noch nicht stabilisiert. Bevor das passiert ist, hätten wir nichts in Produktion gegeben.
Nicht unbedingt, weil async/await für den Erfolg der Neufassung ausschlaggebend war.
Aber es war klar, dass sich das Web-Ökosystem nach der Landung viel mehr stabilisieren würde. Diese Annahme erwies sich im Nachhinein als richtig. Es reicht auch nicht aus, nach Bibliotheken zu suchen und zu sehen, ob sie aktiv gepflegt werden. Man muss sie tatsächlich benutzen und ihre Nachteile kennen lernen.
Nur wenn man sicher ist, dass alles, was man braucht, entweder vorhanden ist oder man es selbst bauen kann, sollte man weitermachen. Wenn das Ziel am Ende ein produktionsreifer Dienst ist.
Ich persönlich gehe bei einer vollständigen Neufassung gerne Schritt für Schritt vor. Ich baue isolierte Module, teste und dokumentiere sie isoliert und gehe dann weiter, bis nur noch der Schritt übrig ist, alles miteinander zu verbinden. Dieser Ansatz ist nicht perfekt und es macht auch einen Unterschied, ob man von oben nach unten oder von unten nach oben vorgeht. Die Idee ist, sich an den bestehenden technischen Konzepten zu orientieren. Die Bereichslogik ändert sich bei einer Neufassung nicht wesentlich (und sollte sich auch nicht ändern).
Wenn der vorhandene Dienst über eine gute Suite von Integrationstests verfügt, am besten eine, die durchgängig läuft, kann das ebenfalls sehr hilfreich sein. Sie können diese Tests wiederverwenden, um die Implementierung der Neufassung zu validieren, noch bevor Sie sie in die neue Sprache portieren. Eine weitere Sache, die ich versuche, wenn ich Dienste neu schreibe, ist, so viel wie möglich unterwegs zu dokumentieren. Es wird immer einen zusätzlichen Dokumentationsdurchgang am Ende geben müssen, aber das Hinzufügen grundlegender Dokumente während der Implementierung spart Zeit, da man bereits tief in den Kontext des Moduls eingedrungen ist.
Laufzeit-Erfahrung
Sobald Sie die Implementierung mit der Dokumentation und den automatisierten Tests abgeschlossen haben, werden Sie aus erster Hand erfahren, warum man sagt, dass die letzten 20 % 80 % der Zeit in Anspruch nehmen 😉 .
Da Sie eine bestehende Infrastruktur ersetzen, müssen Sie sicherstellen, dass das neue System stabil ist und einwandfrei funktioniert, da es sich um echte Nutzerdaten handelt. Außerdem müssen Sie sicherstellen, dass die Umstellung vom alten auf den neuen Dienst reibungslos funktioniert. In unserem Fall bedeutete dies, dass wir eine umfassende Qualitätssicherung auf mehreren Ebenen durchführten und den Dienst auch einem umfangreichen Belastungstest unterzogen.
Alle unsere Dienste laufen in Docker-Containern innerhalb eines Kubernetes-Clusters. Es war also überhaupt kein Problem, den Rust-Webdienst in unseren Cluster einzubinden. Sobald wir von der Korrektheit und Stabilität der Neufassung überzeugt waren, haben wir sie implementiert. Nachdem wir so viel Zeit investiert hatten, um sicherzugehen, dass es kugelsicher ist, war dieser Teil nicht sehr aufregend, was auch gut so ist.
In Bezug auf Laufzeitstabilität und Leistung hat Rust bisher gehalten, was es verspricht.
Wir hatten ein paar kleinere Bugs - sehr wenige für das Umschreiben einer Codebasis von mehr als 10k Loc. Es waren alles reine Logikfehler, die vom Compiler nicht erkannt werden konnten.
Die Speicher- und CPU-Auslastung ist besser als beim ersetzten node.js-Dienst, und bisher hatten wir keine Instabilitätsprobleme. Man kann mit Sicherheit sagen, dass unsere Laufzeit-Erfahrung bis zu diesem Zeitpunkt (7 Monate und mehr...) einwandfrei war.
Zukünftiger Ausblick
Da unser erstes Experiment, einen Rust-Webdienst in der Produktion einzusetzen, sehr erfolgreich war, wird diese Neufassung wahrscheinlich nicht lange die einzige bleiben. Zum Zeitpunkt des Verfassens dieses Artikels haben wir vier Rust-Dienste in der Produktion laufen. Die ersten drei Dienste wurden in Anlehnung an den in diesem Beitrag erwähnten Dienst neu geschrieben. Der vierte Dienst ist jedoch ein neuer Dienst. Da die ersten Schritte gut verliefen und wir bisher keine Probleme zur Laufzeit oder bei der Wartung hatten, haben wir uns entschlossen, ein völlig neues Projekt auf der grünen Wiese auf Rust aufzubauen.
Dies war ein weiterer großer Schritt in Bezug auf das Vertrauen in Rust und sein Ökosystem. Wir werden den Wartungsaufwand und die Stabilität unserer Rust-Dienste weiterhin überwachen.
Wenn alles so weitergeht wie bisher, wird Rust sehr wahrscheinlich eine unserer primären Technologien im Backend werden.
Schlussfolgerung
Zum Abschluss dieser Zusammenfassung unserer Erfahrungen mit dem Umschreiben eines bestehenden Dienstes in Rust wollen wir noch einmal die wichtigsten Punkte aufgreifen.
Erstens haben wir sichergestellt, dass das Team sich verpflichtet, die Arbeit zu erledigen und den Dienst aufrechtzuerhalten - das ist der wichtigste Punkt.
Eine weitere nicht-optionale Maßnahme besteht darin, zu prüfen, ob das Ökosystem der Technologie, auf die Sie umsteigen, alles, was Sie brauchen, ausreichend unterstützt.
Wenn das Engagement erst einmal da ist und alles für die Neufassung vorbereitet ist, gibt es einige Praktiken, die wir als hilfreich empfunden haben.
Wenn es schwierig ist, innerhalb des Unternehmens ein zeitliches Engagement zu bekommen, werden Sie kreativ! Interne Programme zur Förderung der Motivation und des Lernens beugen Burnout vor und steigern langfristig die Produktivität.
Wie offen die Unternehmensleitung dafür ist, hängt von der Firma ab, in der Sie arbeiten, aber einen Versuch ist es allemal wert. Was die Umsetzung angeht, so ist es einfacher, den Überblick zu behalten und alles am Laufen zu halten, wenn man einzelne Module nacheinander in Angriff nimmt. Außerdem können Sie beim Schreiben des Codes gleichzeitig Tests und Dokumentation schreiben. Dieses Experiment hat für uns gut funktioniert.
Wir sind gespannt, welche Vorteile und Herausforderungen uns auf unserem Weg mit Rust als erstklassiger Technologie erwarten.