Talk notes

This commit is contained in:
2024-01-08 18:17:09 +01:00
parent 3c384205c9
commit a7e6aa8089
17 changed files with 205 additions and 88 deletions

172
notes/NOTES.md Normal file
View File

@@ -0,0 +1,172 @@
# Millionen langlebiger TCP-Verbindungen Herausforderungen und Lösungen bei Update-Prozessen
Ihr wollt in euren Urlaub starten, seid mit gepackten Koffern am Bahnhof. Beim Warten auf den verspäteten Zug habt ihr jede
Menge Zeit zum überlegen. Ihr fragt euch: "Ist der Herd wirklich abgeschaltet?". Anders als in früheren Urlauben habt ihr aber
einen smarten Herd daheim. Ihr öffnet eure App, stellt fest, dass alles in Ordnung ist, und könnt beruhigt in Urlaub starten.
IoT (also "Internet of Things") beschreibt genau diese Art von Geräten. Mein Name ist Bene. Ich bin Softwareentwickler mit Fokus
auf Softwarearchitektur bei Scandio. Wir bei Scandio entwickeln für unsere Kunden maßgeschneiderte intelligente und kreative
Softwarelösungen. Wir haben dabei ausgeprägten Stärken unter anderem im Bereich "Internet of Things".
Ein Projekt aus diesem Bereich will ich heute vorstellen. Dabei geht es darum, wie die IoT-Geräte unseres Kunden mit
verschiedenen Backends kommunizieren und um die Herausforderungen bei Aktualisierung des Systems, das zwischen den Geräten und
den Backends liegt.
## Inhalt
Ein kurzer Überblick über den Vortrag: Nach dem Projektsetup (also Hintergrund, Motivation und Ziel des Projekts) beschreibe ich
euch die Problemstellung etwas genauer und dann natürlich auch die Lösungen, also: wie sieht die Architektur des Systems jetzt
ab, was genau passiert, wenn das System Updates bekommt, und zum Schluss noch: was wären unsere Wunschträume für die
Architektur, wenn Zeit und Geld keine Rolle spielen würde oder wir ganz von vorne anfangen würden.
## Projektsetup
Das Projekt, von dem ich heute rede, hat den Namen "Heimdall".
Heimdall ist in der nordischen Mythologie der Wächter der Götterbrücke Bifröst (das ist der Regenbogen im Bild) und Heimdall hat
außergewöhnliche Wahrnehmungsfähigkeiten, hier durch das Hören symbolisiert. Genauso wie Heimdall die Verbindung zwischen den
Welten überwacht, fungiert unser Heimdall-System als ein Gateway, das die Brücke zwischen einer Vielzahl von IoT-Geräten und
verschiedenen Backend-Systemen bildet. Ähnlich der mythischen Figur hat unser Heimdall die Aufgabe, die Sicherheit, Stabilität
und Effizienz dieser Verbindungen zu gewährleisten.
Heimdall wird seit 2018 entwickelt und hat ein vollständig extern entwickeltes und verwaltetes System abgelöst. Bei diesem alten
System hatte unser Kunde keinen Zugriff auf den Source Code oder Entwicklungsdetails. Daher wurde die Entscheidung für eine
Neuentwicklung getroffen. Die fehlenden Informationen über das alte System gingen sogar so weit, dass wir bei der Migration zu
unserem neuen System regelmäßig das alte System überlastet haben, da wir einfach keine Informationen bekommen konnten, welche
Last das alte System aushält.
Ich will hier einmal schematisch zeigen, welche Aufgabe Heimdall hat. Eure smarte Herdplatte von vorhin muss regelmäßig an das
Herdplattenstatusbackend die Information senden, welche Herdplatten auf welcher Stufe angeschaltet sind. Gleichzeitig schickt
der smarte Kühlschrank eures Nachbarn Bilder an ein Backend, das herausfinden soll, wie viele Eier noch im Kühlschrank sind, und
die smarte Dunstabzugshaube eurer Tante meldet an das entsprechende Backend, dass die Filter mal wieder eine Reinigung
bräuchten. Heimdall sitzt zwischen den Geräten und den Backends und sorgt dafür, dass die richtigen Nachrichten das richtige
Backend erreichen, und dass umgekehrt auch Nachrichten in die Gegenrichtung versendet werden können: beispielsweise kann das
Herdplattenbackend die Herdplatte anweisen, sich abzuschalten. Die Geräte halten dazu eine Websocket-Verbindung zu Heimdall
aufrecht. Über diese Verbindungen werden Nachrichten ausgetauscht.
## Problemstellung
Das wichtigste Qualitätsziel bei Heimdall ist, dass diese Verbindungen zwischen Gerät und Heimdall so selten wie möglich
unterbrochen werden. Das hat zwei Gründe: Zum einen kann ich als Nutzer nicht sehen, auf welcher Stufe mein Herd gerade kocht,
wenn der Herd nicht mit Heimdall verbunden ist. Zum anderen finden einige Prozesse nach jeder Neuverbindung statt. Das kann
potenziell zu Überlastung von Heimdall oder den Backends führen, wenn die Neuverbindungen zu häufig stattfinden.
Ein Szenario gibt es aber, in dem Verbindungen auf keinen Fall gehalten werden können: nämlich wenn der Server, mit dem die
Hausgeräte verbunden sind, ein Update bekommt und daher ersetzt werden muss. Deswegen ist eins der größten Ziele bei der
Entwicklung der Architektur von Heimdall, dass solche Neustarts bei den meisten Konfigurationsänderungen des Systems nicht
notwendig sind, sondern die Konfigurationsänderung am Live-System durchgeführt werden können.
## Lösungen
Wir schauen uns jetzt mal an, wie Heimdall aufgebaut ist.
Das Kernstück von Heimdall ist der sogenannte "Web Socket Manager", kurz WSM. Im Wesentlichen öffnet ein Gerät eine
Websocket-Verbindung zum WSM. Der WSM leitet die Nachrichten des Geräts an das Backend weiter und umgekehrt werden Nachrichten
vom Backend an das Gerät zurückgeleitet.
In Wirklichkeit gibt es allerdings mehrere WSM-Pods, also mehrere Instanzen des WSM. Zum einen wird dadurch die Skalierbarkeit
des Systems verbessert, und zum anderen verlieren im Falle eines Crashs nicht alle Geräte die Verbindung. Vor diesen Instanzen
liegt ein Load Balancer. Der routet die Verbindungen zum WSM im Round-Robin-Algorithmus, also: eine Verbindung wird an die erste
Instanz geroutet, die nächste an die zweite Instanz, und nach der letzten Instanz beginnt das Spiel von vorne. Bei synchronen
Aufrufen an das Backend kommt die Antwort an die richtige WSM-Instanz zurück. Allerdings sollte man synchrone Aufrufe wegen der
stärkeren Kopplung besser vermeiden und durch asynchrone Aufrufe ersetzen. Außerdem könnten Backends auch direkt Nachrichten an
Geräte senden wollen.
Hier kommen der Forwarding-Service (kurz FORS) und eine Adress-Datenbank ins Spiel. Jeder WSM-Pod schreibt nach einer geöffneten
Verbindung mit einem Gerät die Geräte-ID zusammen mit der eigenen Adresse in die Adress-Datenbank. Der FORS fragt jetzt für ein
Gerät die richtige Adresse ab und kann Nachrichten vom Backend an die richtige FORS-Instanz weiterleiten. Konkret schaut der
Ablauf also so aus: Gerät 1 baut eine Websocket-Verbindung zu WSM-Pod 1 auf. In der Folge teilt WSM-Pod 1 der Adress-Datenbank
mit: Gerät 1 ist mit dem Pod verbunden, der Adresse 1 besitzt. An der Kommunikation vom Gerät zum Backend hat sich nichts
geändert. Wenn jetzt aber das Backend eine Nachricht an das Gerät senden möchte, dann wird die Nachricht an unseren
Forwarding-Service geschickt. Der Forwarding-Service fragt die konkrete Adresse des richtigen WSM-Pods bei der Adress-Datenbank
an und leitet die Nachricht entsprechend weiter. Dann ist die Nachricht beim richtigen WSM gelandet und kann an das Gerät über
die Websocket-Verbindung geschickt werden.
Dazu kommt, dass es in Wirklichkeit nicht nur ein Backend gibt. Ein Message Mapping kann vom WSM genutzt werden, um zu
entscheiden, an welches Backend ein konkreter Nachrichtentyp mit einer konkreten Version gesendet werden soll. Der Ablauf hier
ist also: Das Gerät (der Einfachheit halber habe ich in diesem Bild nur noch ein Gerät) schickt die Nachricht mit dem Typ
"/foo/config" und der Version 1 an den WSM. Der WSM findet mit Hilfe des Message Mappings heraus, dass Backend 1 für die
Verarbeitung der entsprechenden Nachrichten verantwortlich ist und leitet die Nachricht entsprechend weiter. Wir sind also wie
eine Postunternehmen dafür zuständig, dass die richtigen Nachrichten an den richtigen Empfänger gelangen, aber nicht dafür, dass
die Nachrichten inhaltlich auch sinnvoll sind. Es findet also bei Heimdall keinerlei Validierung der Nachrichten statt. Das
Message Mapping ändert sich regelmäßig. Deshalb ist es wichtig, dass eine Rekonfiguration ohne Neustart der WSM-Pods möglich
ist, da sonst ja Verbindungen gekappt werden müssten.
Der WSM hat noch diverse andere Aufgaben: Er führt den TLS-Handshake durch. Dabei wird auch validiert, dass das
Client-Zertifikat von einer bekannten Issuer-CA signiert wurde. Bei manchen dieser Issuer soll der Expiry-Timestamp des
Client-Zertifikats ignoriert werden. Das hat den Hintergrund, dass auch die Kommunikation zu Firmware-Updates über Heimdall
stattfindet. Insbesondere könnten alte Geräte keine Firmware-Updates bekommen, wenn ihre Client-Zertifikate abgelehnt würden.
Auf der anderen Seite kann aber das Client-Zertifikat nicht ausgetauscht werden, ohne ein Firmware-Update durchzuführen. Um
dieses Problem zu lösen, werden eben in manchen Fällen auch abgelaufene Zertifikate akzeptiert. Manche Client-Zertifikate sollen
außerdem sofort abgelehnt werden, weil die entsprechenden Geräte defekt sind. Das wird über eine Datenbank mit blockierten
Zertifikaten geregelt. Für das Blockieren eines Zertifikats ist wie beim Message Mapping kein Neustart des WSM notwendig, sodass
auch hier Neuverbindungen vermieden werden. Andere Geräte sind in einem Quarantäne-Status und für diese Geräte dürfen nur
bestimmte Nachrichten versendet und empfangen werden, beispielsweise wieder Nachrichten im Zusammenhang mit Firmware-Updates.
Wie die blockierten Zertifikate wird das über eine eigene Datenbank konfiguriert und benötigt keinen WSM-Neustart. Nach einem
erfolgreichen TLS-Handshake muss der WSM das Websocket-Upgrade (also den Protokollwechsel von HTTP zu Websocket) durchführen.
Schließlich gibt es noch einen Corporate Handshake, der vom Protokoll unseres Kunden vorgesehen ist. Bevor dieser Handshake
abgeschlossen ist, dürfen keine anderen Nachrichten in beide Richtungen gesendet oder weitergeleitet werden.
Hier ist noch einmal eine Zusammenfassung des gesamten Heimdall-Systems, soweit es für den Vortrag relevant ist. Die Grundidee
ist also, dass der WSM so selten wie möglich neugestartet werden muss. Das erreichen wir, indem Konfiguration, die sich
regelmäßig ändern könnte, externalisiert wird und ohne Neustarts geändert werden kann.
Was passiert jetzt aber, wenn der WSM doch einmal neugestartet werden muss oder abstürzt? Wenn ein WSM-Pod kontrolliert beendet
wird, werden die Verbindungen zu den entsprechenden Geräten nacheinander beendet. Dann verbinden sich die Geräte mit anderen
Instanzen neu. Aktuell dauert dieser Prozess in unserer größten Stage mit etwa 2,5 Millionen Geräten knapp 3 Stunden. Da wir ja
Round-Robin für das Load Balancing nutzen, hat der Ersatz-Pod, der hier gestartet wurde, danach deutlich weniger offene
Verbindungen als die alten Pods. In der Praxis führt das nicht zu Problemen, denn die Last ist bei neuen Verbindungen mit
Abstand am höchsten.
Falls ein Pod (beispielsweise aufgrund eines Bugs) unkontrolliert beendet wird, versuchen sich alle Geräte auf einmal neu zu
verbinden. Das würde zur Überlastung des Systems führen, daher gibt es einen Rate Limiter, der dafür sorgt, dass sich Geräte
nicht zu schnell wieder verbinden können. Natürlich ist das ein ungünstiger Zustand, da die Funktionalität der noch nicht wieder
verbundenen Geräte eine ganze Weile eingeschränkt sein kann.
Jetzt kommen wir mal zum geplanten Fall: eine neue WSM-Version soll deployed werden. Das Deployment läuft dann so ab: In einem
ersten Schritt werden 1/3 der Instanzen als Canary-Deployment durch Instanzen der neuen Version ersetzt. Diese neue Version
läuft parallel zu der alten für eine Woche, bevor auch die übrigen Instanzen ersetzt werden. So können Fehler frühzeitig erkannt
werden.
Das bedeutet übrigens nicht, dass nach dem Canary-Deployment 1/3 der Verbindungen auf der neuen Version stattfinden. Die
Wahrheit ist hier etwas komplizierter: die Geräte, die sich nach dem Abschalten mancher der alten Instanzen neu verbinden,
verbinden sich ja genauso häufig mit jeder der Canary-Instanzen wie mit jeder der alten Stable-Instanzen. In dem 7-Tage-Zeitraum
gleichen sich die Anzahl der verbundenen Geräte dann etwas aus, weil Geräte regelmäßig auch aus anderen Gründen die Verbindung
verlieren und sich neu verbinden. Danach werden die übrigen 2/3 ersetzt und wie vorhin haben dann die zuerst deployeten
Canary-Instanzen mehr offene Verbindungen als die zuletzt deployeten.
Jetzt habt ihr gesehen, wie das System tatsächlich aussieht. Natürlich haben wir in den gut 5 Jahren, in denen das Projekt
existiert, auch dazugelernt und würden einige Dinge anders gestalten. Am Anfang des Vortrags habe ich ja schon gesagt, dass das
Ziel wäre, den WSM so selten wie möglich updaten zu müssen. Dafür ist es hilfreich, wenn der WSM so wenige Aufgaben wie möglich
bekommt. In einer idealen Welt sollte daher der WSM der kleinste Service im gesamten System sein. Die Realität ist: der WSM ist
der mit Abstand größte Service des Heimdall-Systems.
Woran liegt das? Aktuell hat der WSM einige Aufgaben:
* TLS und alles, was dazugehört: die Terminierung der TLS-Verbindung, die Validierung der Clientzertifikate, die Ausnahmen zur
Validierung der Zertifikate und das Blocklisting von Zertifikaten.
* Der Websocket-Upgrade, also die Entgegennahme der initialen HTTP-Verbindung und der Wechsel zum Websocket-Protokoll
* Der Corporate Handshake, der zu Beginn jeder Verbindung stattfinden muss
* Das Message Routing an die Backends inklusive Offenhalten der Verbindungen und Warten auf Antworten
* Die Quarantäne-Logik
* Einige andere Workarounds
Um also den WSM möiglichst klein zu bekommen, müssen möglichst viele dieser Aufgaben ausgelagert werden. Oder umgekehrt: die
Aufgaben, die unbedingt notwendig zum Halten der Verbindung sind, sollten aus dem WSM extrahiert werden. Hier wäre daher mein
Vorschlag, um die Verbindungen noch länger halten zu können. Vor die übrigen Systeme wird ein Service gesetzt, den ich hier
"Connection Holder" genannt habe und der nur dafür zuständig ist, die TCP-Verbindung zu den Geräten zu halten, die
TLS-Verschlüsselung zu terminieren und zu Validieren, dass die Client-Zertifikate von einem bekannten Issuer ausgestellt wurden.
Nach der TLS-Terminierung wird die TCP-Verbindung einfach an das Äquivalent des jetzigen WSMs weitergetunnelt. Wenn die
WSM-Instanz beendet wird, kann der Tunnel an einer andere WSM-Instanz weitergeroutet werden, ohne die Verbindung zum Gerät
abzubrechen. Damit der WSM Zertifikatslogik wie Blocklisting durchführen kann, muss zu Beginn des Verbindungsaufbaus zwischen
Connection Holder und WSM das validierte Client-Zertifikat mitgeschickt werden. Außerdem müssen die Verbindungen eine eindeutige
ID bekommen, sodass die WSM-Instanzen beispielsweise persistieren können, ob für eine gegebene Verbindung bereits der Corporate
Handshake durchgeführt wurde. Natürlich könnten auch andere Aufgaben aus dem WSM ausgelagert werden, hier mal exemplarisch das
Message Routing inklusive Warten auf die Antwort des Backends.
Zusammenfassend können wir schon im aktuellen System zuverlässig dafür sorgen, dass Konfiguration schnell und unkompliziert ohne
Neuverbindungen geändert werden kann. Neuverbindungen komplett zu vermeiden dürfte nicht möglich sein, aber wir haben Ideen, wie
wir uns in diese Richtung verbessern können. Ich hoffe, ich konnte euch einen guten Einblick in die Herausforderungen geben, vor
die uns die langlaufende Verbindungen in Zusammenhang mit Systemupdates stellen.
Hier seht ihr noch einmal, wie ihr mich erreichen könnt. Danke fürs Zuhören!