Ratschläge für Einsteiger in verteilte Systeme
(somethingsimilar.com)- Die zentrale Schwierigkeit verteilter Systeme ist weniger die Latenz als partielles Versagen; selbst einfache Ausnahmebehandlung auf einer einzelnen Maschine wird über mehrere Maschinen hinweg Teil des Protokolldesigns
- Robuste verteilte Systeme haben Bugs, die erst auf mehreren realen Maschinen, unter Netzwerkbedingungen im Rechenzentrum und mit großen Datensätzen sichtbar werden; sie sind daher teurer als Systeme auf einer einzelnen Maschine
- Um betreibbare Systeme zu bauen, sollte man Koordination reduzieren und Unabhängigkeit erhöhen; Backpressure, partielle Verfügbarkeit, Metriken und Perzentil-Kennzahlen sollten zu den Grundwerkzeugen gehören
- Feature Flags, das Design von ID-Räumen, Datenlokalität und das Herauslösen von Services sind praktische Techniken, um bei Migration und Skalierung Fehlerbereiche und Koordinationskosten zu senken
- Das CAP-Theorem ist weniger ein Ausgangspunkt für den Systembau als ein Werkzeug zur Designkritik; in verteilten Systemen ist zudem die soziale Koordination zwischen Teams und Organisationen oft ebenso schwierig wie die technischen Probleme
Was man beim Einstieg in verteilte Systeme leicht übersieht
- Viele Lektionen von Engineers für verteilte Systeme stammen aus Fehlern unter Production-Traffic und aus operativen Narben; Einsteiger brauchen jedoch einen direkteren, umsetzbaren Kontext
- Die Fallacies of Distributed Computing und das CAP theorem helfen beim Selbststudium, sind für weniger erfahrene Engineers aber zu abstrakt, um unmittelbar ins Handeln zu kommen
- Verteilte Systeme benötigen mehr Maschinen und Kapital und ziehen daher leicht mehr Teams und größere Organisationen hinein
- Soziale Probleme gehören im Allgemeinen zu den schwierigsten Teilen der Arbeit von Softwareentwicklern und können bei der Entwicklung verteilter Systeme besonders deutlich hervortreten
- Manchmal sind soziale Lösungen effizienter und zufriedenstellender als technische, doch Hintergrund, Ausbildung und Erfahrung neigen Engineers eher zu technischen Lösungen hin
Für Ausfälle und Kosten entwerfen
- Verteilte Systeme fallen häufig aus, und schlimmer noch: Sie fallen partiell aus
- Wenn das Unlock eines Mutex in einem einzelnen Prozess fehlschlägt, kann man den Prozess möglicherweise abstürzen lassen; wenn das Unlock eines verteilten Mutex fehlschlägt, muss dieser Fall Teil des Lock-Protokolls sein
- Ansätze wie „einfach auf beide Maschinen schreiben“ oder „bis zum Erfolg erneut versuchen“ behandeln den Fall nicht ausreichend, dass nur ein Write erfolgreich ist und der andere fehlschlägt
- Es kommt zu Situationen wie Switch-Ausfällen, verschwundenen Leadern durch GC-Pausen, remote fehlgeschlagenen Socket-Writes, die erfolgreich aussehen, oder einer einzelnen langsamen Festplatte, die das gesamte Cluster-Kommunikationsprotokoll ausbremst
- Robuste verteilte Lösungen verursachen höhere Kosten als Lösungen auf einer einzelnen Maschine
- Es gibt Fehler, die nur auf mehreren Maschinen auftreten; VMs und Cloud senken zwar die Kosten, sind aber nicht so billig wie Design, Implementierung und Tests auf einem bereits vorhandenen einzelnen Computer
- Fehler, die erst bei Datensatzgrößen, die nicht mehr sinnvoll auf eine gemeinsam genutzte Maschine passen, oder unter Netzwerkbedingungen eines Rechenzentrums sichtbar werden, erfordern eine echte verteilte Umgebung
- Simulationen sind nützlich, ersetzen aber nicht alle Bugs verteilter Systeme
- Robuste verteilte Open-Source-Systeme sind deutlich seltener als robuste Systeme für einzelne Maschinen
- Die Kosten, viele Maschinen über lange Zeit laufen zu lassen, belasten Open-Source-Communities
- Hobbyentwickler und Dilettanten bauen Open Source mit vorhandenen Maschinen und Freizeit; daher ist es schwer, Entwickler zu finden, die mehrere Maschinen starten, betreiben und dafür bezahlen
- Unternehmens-Engineers schließen einige Lücken, aber die Prioritäten ihrer Organisation müssen nicht mit den Prioritäten der Organisation der Nutzer übereinstimmen
Koordination reduzieren und Fehler absorbieren
- Koordination zwischen Maschinen sollte möglichst vermieden werden
- Der Kern horizontaler Skalierbarkeit besteht darin, Daten so unabhängig zu platzieren, dass Maschinen möglichst wenig miteinander kommunizieren und sich einigen müssen
- Jedes Mal, wenn zwei Maschinen sich über etwas einigen müssen, wird die Service-Implementierung schwieriger
- Die Geschwindigkeit der Informationsübertragung hat eine Obergrenze, Netzwerkkommunikation ist instabiler als gedacht, und Intuitionen über Konsens können falsch sein
- Es hilft, Two Generals, Byzantine Generals und die Schwierigkeit der Paxos-Implementierung zu verstehen
- Wenn ein Problem in den Arbeitsspeicher passt, ist es für Engineers verteilter Systeme meist nahezu trivial
- Schnelle Verarbeitung ist schwieriger, wenn Daten nicht ein paar Pointer-Dereferenzierungen entfernt liegen, sondern mehrere Switches weiter
- Algorithmen und Implementierungen für einzelne Maschinen gibt es reichlich, doch Literatur und Implementierungen für verteilte Systeme sind deutlich spärlicher
- „Langsam“ ist das am schwersten zu debuggende Problem
- Eines oder mehrere der Systeme, die an einer Nutzeranfrage beteiligt sind, können langsam sein, oder ein Teil einer Transformationspipeline über mehrere Maschinen hinweg kann langsam sein
- Schon die Problembeschreibung liefert fast keine Hinweise auf den Fehlerort, und es kann partielles Versagen verborgen sein, das in den üblichen Graphen nicht auftaucht
- Bevor die Performance-Verschlechterung sehr deutlich wird, ist es schwer, genügend Ressourcen wie Zeit, Geld und Werkzeuge zu bekommen
- Dapper und Zipkin wurden aus genau diesem Grund entwickelt
- Im gesamten System muss Backpressure implementiert werden
- Backpressure bedeutet, dass das bedienende System dem anfragenden System einen Fehler signalisiert und das anfragende System diesen Fehler verarbeitet, um Überlastung bei sich selbst und beim Gegenüber zu verhindern
- Die Begrenzung der Ressourcennutzung während Überlast oder Systemausfällen ist ein Grundbaustein robuster verteilter Systeme
- Häufige Implementierungen verwerfen neue Nachrichten oder geben Nutzern einen Fehler zurück, wenn Ressourcen begrenzt sind oder ein Fehler auftritt; in beiden Fällen sollten Metriken erhöht werden
- Für Verbindungen und Requests an andere Systeme sind Timeouts und exponentielles Backoff unverzichtbar
- Ohne Backpressure kommt es leicht zu Kaskadenausfällen oder unbeabsichtigtem Nachrichtenverlust
- Man muss Wege finden, partielle Verfügbarkeit anzubieten
- Partielle Verfügbarkeit ist die Fähigkeit, auch dann noch Teilergebnisse zurückzugeben, wenn ein Teil des Systems ausfällt
- Suchsysteme wägen zwischen Ergebnisqualität und der Zeit ab, die Nutzer warten müssen; nach Ablauf einer Frist können sie nur die bis dahin gesammelten Ergebnisse zurückgeben
- Bei privaten Nachrichten kann es besser sein, dass nur einige Nutzer eine Störung haben, als dass für alle Nutzer einzelne Nachrichten verschwinden
- Man muss entscheiden, wie stark Fehlerdomänen getrennt werden sollen, damit ein Ausfall bei privaten Nachrichten keine unabhängigen Funktionen wie öffentliche Bild-Uploads beeinträchtigt
Beobachtung, Kapazität und Deployment als Betriebswerkzeuge nutzen
- Metriken sind ein unverzichtbares Mittel, um zu verstehen, was ein System in Production tatsächlich tut
- Metriken wie Latenzperzentile, inkrementierende Zähler für bestimmte Aktionen und Änderungsraten verringern die Lücke zwischen dem angenommenen und dem tatsächlichen Systemverhalten
- Zu wissen, wie sich das Systemverhalten am 20. Tag von dem am 15. Tag unterscheidet, trennt erfolgreiche Engineering-Arbeit von gescheiterter Magie
- Metriken sind nötig, um Probleme und Verhalten zu verstehen, reichen aber nicht aus, um zu sagen, was als Nächstes zu tun ist
- Logdateien sind nützlich, aber neigen dazu zu lügen
- Einige Fehlerklassen können einen großen Anteil des Dateispeichers in Logs ausmachen, obwohl ihr tatsächlicher Request-Anteil niedrig ist
- Erfolgslogs sind meist redundant und können die Festplatte füllen; Engineers schätzen häufig falsch ein, welche Fehlerlogs nützlich sind
- Logs sollte man am besten so schreiben, als würden sie von jemandem gelesen, der den Code nie gesehen hat
- Wenn man Auffälligkeiten aus Logs überbewertet, ohne sie durch Metriken zu bestätigen, kann ein Ausfall länger dauern
- Man sollte Perzentile statt Durchschnitte verwenden
- Das 50., 99., 99,9. und 99,99. Perzentil sind in den meisten verteilten Systemen genauer und nützlicher als der Durchschnitt
- Der Durchschnitt setzt voraus, dass die Bewertungsgröße einer glockenförmigen Verteilung folgt; unter den Kennzahlen, die Engineers wichtig sind, ist das nur sehr selten der Fall
- Wenn die Latenz verteilter Systeme keiner glockenförmigen Verteilung folgt, führt die durchschnittliche Latenz zu falschen Entscheidungen und falschem Verständnis
- Man sollte die Fähigkeit zur Kapazitätsabschätzung lernen
- Zu wissen, wie viele Maschinen für eine Aufgabe nötig sind, entscheidet zwischen einem langlebigen System und einem, das nach drei Monaten ersetzt werden muss
- Für Ende 2012 brauchte man grobe Überschlagsrechnungen wie: Eine typische Maschine hat 24 GB Arbeitsspeicher, 4–5 GB braucht das OS, mindestens einige GB die Request-Verarbeitung, und eine Tweet-ID belegt 8 Byte
- Jeff Deans Numbers Everyone Should Know hilft dabei, Erwartungen zu kalibrieren
- Feature Flags sind eine Methode, Infrastruktur auszurollen
- Feature Flags sind nicht nur für Frontend-A/B-Tests nützlich, sondern auch sehr mächtig beim Austausch von Infrastruktur
- Der „Big Cutover“-Ansatz hat viele Projekte scheitern lassen, weil spät entdeckte Bugs zu Rollbacks zwangen
- Beim Wechsel von einer einzelnen Datenbank zu einem Service, der einen neuen Store verbirgt, kann man Writes zum neuen Service langsam parallel hochfahren, nach abgeschlossenem Backfill Reads testen, ohne sie für Nutzerantworten zu verwenden, Reads aus altem und neuem System vergleichen und anschließend die tatsächlichen Reads schrittweise erhöhen
- Wenn Probleme auftreten, kann man den Flag-Wert sofort senken oder auf 0 zurücksetzen und mit steuerbarer Traffic-Menge debuggen und experimentieren
- Feature Flags tauschen lokale Komplexität im Code gegen globale Einfachheit und Resilienz ein
- Man muss akzeptieren, dass mehrere Versionen von Infrastruktur und Daten nicht die Ausnahme, sondern die Regel sind
Datenmodell und Grenzdesign
- ID-Räume bestimmen die Form eines Systems
- Je mehr IDs nötig sind, um ein einzelnes Datenelement zu erreichen, desto mehr Möglichkeiten gibt es, Daten zu partitionieren
- Je weniger IDs ein Datenelement benötigt, desto leichter lässt sich die Ausgabe des Systems konsumieren
- Die Twitter API v1 fragte Tweets über eine einzelne numerische 64-Bit-ID ab, erstellte und löschte sie; diese ID war nicht mit anderen Daten verknüpft
- Wenn die Zahl der Tweets wächst, kann man Tweets desselben Nutzers auf derselben Maschine speichern, um Nutzer-Tweet-Timelines und Abonnement-Timelines effizient aufzubauen; da die öffentliche API jedoch jeden Tweet nur über die Tweet-ID adressierbar machte, wird ein Lookup-Service nötig, der weiß, welchem Nutzer welche Tweet-ID gehört
- Alternativ könnte man für Tweet-Lookups eine User-ID verlangen oder die User-ID in die Tweet-ID selbst aufnehmen; Letzteres hat jedoch den Preis, dass die Tweet-ID nicht mehr k-sortable numeric ist
- Die ID-Struktur kann sich auf Deanonymisierung privater Daten, unerwartetes Crawling, Probleme mit automatisch inkrementierenden IDs und Angriffe wie Insecure Direct Object References auswirken
- Datenlokalität sollte genutzt werden
- Je näher Datenverarbeitung und Caching am persistenten Speicher liegen, desto effizienter ist die Verarbeitung und desto leichter lassen sich Cache-Konsistenz und Geschwindigkeit aufrechterhalten
- Das Netzwerk hat mehr Fehler und Latenz als Pointer-Dereferenzierung oder
fread(3) - Lokalität gilt nicht nur im Raum, sondern auch in der Zeit
- Wenn mehrere Nutzer nahezu gleichzeitig denselben teuren Request stellen, kann man ihn zusammenfassen; wenn gleichartige Datenanfragen zeitlich dicht beieinander auftreten, kann man sie zu einem größeren Request bündeln
- Das senkt Kommunikations-Overhead und erleichtert das Fehlermanagement
- Gecachte Daten zurück in den persistenten Speicher zu schreiben, ist schlecht
- Besonders in Systemen, die von Menschen mit wenig Erfahrung in verteilten Systemen erstmals entworfen wurden, kann dieser Fehler auftreten
- Bei Implementierungen, die von „Russian-doll caching“ sprechen, ist die Wahrscheinlichkeit hoch, auffällige Bugs zu finden
- Ein typisches Symptom ist, dass Nutzerinformationen wie screenname, email oder hashed password auf frühere Werte zurückfallen
- Computer können mehr leisten, als man denkt
- Selbst leichte Webserver von Ende 2012 hatten mehr als 6 Prozessoren, 24 GB Arbeitsspeicher und sehr viel Plattenspeicher
- Auch relativ komplexe CRUD-Anwendungen auf modernen Sprach-Runtimes können auf einer einzelnen Maschine tausende requests per second innerhalb von einigen hundert Millisekunden verarbeiten
- In den meisten Fällen sind einige hundert requests per second pro Maschine aus Sicht der Betriebskompetenz kein Wert, mit dem man prahlen sollte
- Wenn man eine Anwendung profiliert und messungsbasierte Effizienzverbesserungen einführt, ist höhere Performance nicht schwer zu erreichen
- Das CAP-Theorem sollte als Werkzeug zur Designkritik verwendet werden, nicht als Bauanleitung für Systeme
- Das CAP-Theorem ist zu allgemein und der Raum möglicher Lösungen zu groß, um daraus als Erstprinzip ein funktionierendes System abzuleiten
- Wenn man ein Systemdesign wiederholt daraufhin prüft, welche Einschränkungen CAP den Subsystemen auferlegt, kann man zu einem besseren Design gelangen
- Von C, A und P kann man CA nicht wählen
- Das Herauslösen von Services bringt Vorteile bei Kapselung und Deployment
- Mit Service ist hier ein verteiltes System gemeint, das Logik auf einer höheren Ebene als ein Speichersystem enthält und normalerweise eine Request-Response-API hat
- Man sollte fortlaufend prüfen, ob bestimmter Code leichter zu ändern wäre, wenn er nicht im Inneren des Systems, sondern in einem separaten Service liegt
- Herausgelöste Services bieten Kapselung wie eine Library, können Änderungen aber schneller und einfacher ausrollen, als die Libraries von Client-Systemen zu aktualisieren
- Kleine Services haben weniger Code- und Betriebsabhängigkeiten, und strenge Grenzen reduzieren Abkürzungen, die in Libraries erlaubt wären
- Wenn es mehrere Client-Systeme gibt, muss man bei einer gemeinsamen Library die Deployments jedes Clients koordinieren; das wird schwieriger, wenn Datenbeschädigung von der Deployment-Reihenfolge abhängen kann
- Wenn die Client-Systeme von unterschiedlichen Maintainers betreut werden, steigen auch die sozialen Koordinationskosten, weil Prioritäten nicht zusammenpassen und Zustimmung zu Upgrades eingeholt werden muss
- Ein typischer Anwendungsfall ist, eine sich ändernde Speicherschicht hinter einem Service mit bequemerer und kleinerer API-Oberfläche zu verstecken
1 Kommentare
Hacker-News-Kommentare
Der Beitrag hätte unbedingt CALM (Konsistenz als logische Monotonie) behandeln sollen. Das ist viel leichter zu verstehen als CAP und ein grundlegenderes Resultat, mit dem auch weniger erfahrene Leute sehr robuste verteilte Systeme bauen können
Idempotenz, CRDTs, WAL und Raft sind alles Spezialfälle des CALM-Prinzips
https://arxiv.org/pdf/1901.01930
Hier fehlt etwas, das ich als allgemeines Prinzip sehr schätze: Exactly-once Delivery ist unmöglich
At-most-once oder At-least-once ist möglich, aber man muss wählen, welche Fehlerarten man in Kauf nimmt, und entsprechend entwerfen
In einer Cluster-Datenbank kann man Daten aus einer Zeile in eine andere verschieben, und das kann man als zugestellte Nachricht interpretieren
Wenn das Gesamtsystem idempotent ist oder man das gesamte verteilte System als eine einzelne Einheit behandeln kann, die zusammen rollbackfähig ist, kann man Exactly-once Delivery erreichen. Das heißt, es darf keine Seiteneffekte auf andere Systeme außerhalb der Domäne geben
Beides fällt unter eine Art logischer Monotonie. Idempotenz ist leicht zu erkennen, und Transaktionalität beruht ebenfalls auf Monotonie durch Algorithmen wie WAL und Raft
Der Beitrag hätte CALM (Konsistenz als logische Monotonie) behandeln sollen. Das ist viel leichter zu verstehen als CAP und ein grundlegenderes Resultat
https://arxiv.org/pdf/1901.01930
An Overview of End-to-End Exactly-Once Processing in Apache Flink (with Apache Kafka, too!) — https://flink.apache.org/2018/02/28/an-overview-of-end-to-en...
Flink's Fault Tolerance Guarantees — https://nightlies.apache.org/flink/flink-docs-release-1.20/d...
Als Korollar zu „Wenn man das Problem in den Speicher bekommt, ist es vermutlich trivial“ gilt: Was in den Speicher passt, ist viel größer, als man denkt
Ich dachte, ich wüsste, was viel RAM ist, aber dann begannen die großen Clouds, 12-TB-VMs für SAP HANA anzubieten, und ich habe meine Meinung geändert
Der Beitrag streift das mit „Computer können mehr, als du denkst“ zwar kurz, spricht dort aber nur von 24-GB-Maschinen. Selbst wenn man berücksichtigt, dass das 2012 war, gab es damals wohl schon etliche Maschinen mit dem Zehnfachen an RAM
Ich teile dieses Dokument mit den vielversprechendsten Leuten, mit denen ich zusammenarbeite
Als ich bei Lookout gearbeitet habe, hat Jeff Hodges diesen Essay in einem Vortrag geteilt und am Ende das Korollar hinzugefügt: „Tut nicht so, als wäre Engineering unpolitisch“
Leute, die denken, Code spreche für sich selbst, übersehen wichtige Aspekte, die beeinflussen, wie etwas gebaut wird und ob überhaupt reale Ergebnisse erzielt werden
Auch 10 Jahre später gibt es nicht viele, die die Schnittmenge aus Engineering Leadership und dem, was man üblicherweise als Kernkompetenzen von SRE/DevOps ansieht, so prägnant verstehen
Frühere Diskussionen
https://news.ycombinator.com/item?id=5055371
346 points|jcdavis|vor 12 Jahren|42 comments
https://news.ycombinator.com/item?id=12245909
386 points|kiyanwang|vor 8 Jahren|133 comments
Tolle Liste, und mir gefallen die praktischen und realistischen Erklärungen. Kein Buzzword-Bingo und kein „Microservices“
Vieles von diesem Rat gilt meiner Meinung nach auch für Systeme auf einer einzelnen Maschine. Es kann viele gewissermaßen verteilte Teilkomponenten geben, etwa IPC zwischen Programmen oder die Koordination von Threads in einem Prozess
Auch die Vorstellung eines einheitlichen Speichers auf einer einzelnen Maschine ist in gewissem Maß falsch, aber die Hardware kann trotzdem bessere Garantien liefern als eine „echte“ verteilte Umgebung
Vieles von dem Rat im Beitrag, der „verteilt“ und „einzelne Maschine“ vergleicht, passt auch ziemlich gut auf den Vergleich Multithreading vs. Single-Threading
Auf einer anderen Achse gilt: Wenn man ein Programm baut und verteilt, damit mehrere Leute es ausführen, ist das ebenfalls eine Art „verteilte“ Situation. Es gibt verschiedene Versionen draußen in der Wildnis, und man muss sich um Kompatibilität und Upgrades kümmern, weshalb auch die im Beitrag erwähnten Feature-Flags relevant sind
Verteiltheit ist eher ein Spektrum, das von einer einzelnen CPU über mehrere CPUs und eng gekoppelte Mehrrechnersysteme bis zu weltweit verteilten Computern reicht, mit vielen Punkten und vielen Dimensionen dazwischen
Mehrere Prozesse auf derselben Maschine, die über localhost kommunizieren, sind ein sehr bekanntes Beispiel für ein verteiltes System, und viele lernen dort tatsächlich zum ersten Mal verteilte Systeme kennen
Dann würden alle ihren gesamten Code als schöne MPI-Programme mit verteiltem Speicher schreiben. False Sharing wäre verschwunden, und man wäre gezwungen, Kommunikationsmuster explizit und gründlich zu durchdenken
Ich hatte in den letzten Jahren kurz die Gelegenheit, mit dem Autor dieses Beitrags zusammenzuarbeiten. Jeff war einer der einsichtigsten und positivsten Menschen, von denen ich je gelernt habe
Er war erstaunlich offen in Bezug auf seine eigenen Schwierigkeiten und sehr zugänglich, wenn es um Mentoring und Ratschläge ging
Bei „Verteilte Systeme sind anders, weil sie oft scheitern“ ist der Kern nicht die bloße Ausfallrate, sondern die Ausfallrate in einem System aus vielen Knoten
Und „Probleme verteilter Systeme“ treten nicht nur bei mehreren über ein Netzwerk verbundenen Servern auf. Wenn man eine Menge miteinander verbundener Knoten hat, können Dateien auf logisch verbundenen Festplatten oder Puffer verschiedener Ein-/Ausgabegeräte ähnliche Probleme haben
Einige Oldtimer spotten über die übermäßige Komplexität, die beim Abmildern solcher Probleme entsteht, und beklagen, dass alles viel einfacher wäre, wenn man Software einfach auf einem einzelnen Server laufen ließe
Realistisch betrachtet war das schon zu Zeiten von AS/400 oder VAXft kaum richtig und gilt erst recht nicht in der noch chaotischeren Multi-User-/Multi-Prozess-Unix-Welt
„Wenn man das Problem in den Speicher bekommt, ist es vermutlich trivial“ ist ein häufiger Irrtum unter manchen Engineers für verteilte Systeme
Es ist überhaupt nicht trivial; die zu behandelnden Probleme verschieben sich nur in einen komplementären Bereich
Dieser Irrtum führt leicht dazu, dass man etwas, das auf einer einzelnen Maschine mit ordentlicher Optimierung machbar wäre, stattdessen mit einem 100-Knoten-Cluster bearbeitet