Migration ohne Downtime: die Canary-Strategie, die wir genutzt haben
Kontext
In einem Fashion-E-Commerce-Unternehmen, in dem ich gearbeitet habe, betreuten wir die Integration des Produktkatalogs mit internationalen Retail-Partnern. Wenn jemand auf der Plattform eines solchen Partners ein Produkt kaufte, kam dieser Kauf als Bestellung in unserem System an, und darüber liefen täglich tausende Bestellungen. Das System, das sie verarbeitete, war ein Legacy-System außerhalb der Cloud: Es funktionierte, war aber schwer zu warten, schwer bei Nachfragespitzen zu skalieren und teuer im Betrieb. Wir hatten eine neue Architektur in AWS aufgebaut (Microservices mit Spring Boot, SQS, SNS, S3), und das Ziel war, die kritischen Partner vom alten System auf das neue zu migrieren. Das Problem war, dass man nicht einfach das eine ausschalten und das andere von einem Tag auf den anderen einschalten konnte. Jede Bestellung war echtes Geld und echte Verpflichtungen gegenüber echten Kunden. Es gab keinen Spielraum für Fehler.
Was eine Canary-Strategie ist
Der Name kommt von den Kanarienvögeln, die Bergleute früher mit in die Kohlebergwerke nahmen. Überlebte der Kanarienvogel, war die Luft sicher. Starb er, gab es giftiges Gas, und man ging nicht hinein. In der Softwarewelt ist die Idee dieselbe: Man führt das neue System kontrolliert ein, verarbeitet zunächst nur einen kleinen Bruchteil des echten Traffics und beobachtet, ob alles gut läuft, bevor man mehr schickt. Geht etwas schief, betrifft es nur diesen Bruchteil, nicht das Ganze.
Wie wir es gemacht haben
Zuerst paralleles Verarbeiten. Die Bestellungen kamen weiterhin wie gewohnt über das alte System herein, ohne dass es jemand bemerkte. Intern wurden dieselben Bestellungen aber auch an die neue Plattform in AWS geschickt, deren Ergebnis noch für nichts Echtes verwendet wurde. Wir haben nur verglichen: Ist das Ergebnis des neuen Systems identisch mit dem des alten? Stimmten sie überein, bestätigte das, dass das neue System richtig funktionierte.
Mit wachsendem Vertrauen begannen wir, echten Traffic umzuleiten, zunächst einen sehr kleinen Prozentsatz, wobei wir bei jedem Schritt mit Grafana und CloudWatch überwachten: Latenz, Fehler, Volumen, Datenkonsistenz. Wich etwas vom Normalzustand ab, konnten wir sofort abbrechen und zum alten System zurückkehren, ohne dass es jemand bemerkte.
Als das neue System bereits eine Weile lang echten Traffic problemlos verarbeitet hatte, leitete der Router selbst schrittweise 100% des echten Traffics auf die neue Plattform um. Der Partner musste auf seiner Seite nie etwas ändern, er zeigte immer auf denselben Endpunkt wie schon immer; der Systemwechsel dahinter blieb für ihn völlig unsichtbar. Und als alle Partner migriert waren, schalteten wir das alte System kontrolliert ab, nachdem wir geprüft hatten, dass kein Restfluss mehr davon abhing.
Das Problem, das wir nicht vorhergesehen hatten
Hier wird die Geschichte erst richtig interessant. Als die Migration schon weit fortgeschritten war, tauchten Fehler bei der Verarbeitung von Retouren auf. Die Ursache: Ein Kunde gab ein Produkt zurück, das er Wochen oder Monate zuvor gekauft hatte, als die ursprüngliche Bestellung noch vom alten System verarbeitet worden war, mit einem anderen Datenmodell als dem neuen. Das neue System versuchte, eine Retoure zu einer Bestellung zu verarbeiten, die es nicht verstand, weil die Felder nicht übereinstimmten.
Wir bauten eine temporäre Logik in den Retouren-Microservice ein, die erkennen konnte, in welchem Format die ursprüngliche Bestellung vorlag, und sie je nach Fall passend verarbeitete, indem sie quasi beide Sprachen gleichzeitig sprach. Wochenlang beobachteten wir, wie viele Legacy-Fälle noch eintrafen. Am Anfang waren es viele, alle alten Bestellungen, die noch innerhalb der Rückgabefrist lagen. Mit der Zeit nahmen sie ab, weil die neuen Bestellungen bereits mit dem richtigen Modell verarbeitet wurden. Als keine Legacy-Fälle mehr eintrafen, entfernten wir die duale Kompatibilitätslogik. Das System blieb sauber zurück, mit einem einzigen Datenmodell, ohne unnötigen temporären Code.
Was ich gelernt habe
Bei einer Migration ist temporäre Kompatibilität notwendig, aber gefährlich. Lässt man sie für immer bestehen, füllt sich das System überall mit bedingten Verzweigungen und wird unwartbar. Man muss von Anfang an planen, wann und wie man sie wieder entfernt. Die Migration endet nicht, wenn das neue System funktioniert, sie endet, wenn das alte System vollständig abgeschaltet und das neue sauber ist.
Und eine zweite, allgemeinere Lektion: Testdaten spiegeln nie die ganze Komplexität der Produktion wider. Seitdem frage ich bei jeder Migration immer, ob wir mit anonymisierten echten Daten getestet haben und ob wir die Edge Cases des alten Systems berücksichtigt haben, nicht nur den Idealfall.
Ergebnis
Null Downtime während der gesamten Migration. Null verlorene Bestellungen. Altes System kontrolliert abgeschaltet, und neues System läuft sauber in AWS, ohne temporären Restcode.