Beseitigung von Ausfällen bei der Aufgabenverarbeitung durch Ersetzen von RabbitMQ durch Apache Kafka ohne Ausfallzeiten

Die Skalierung der Backend-Infrastruktur für Hyperwachstum ist eine der vielen spannenden Herausforderungen bei der Arbeit bei DoorDash. Mitte 2019 standen wir vor erheblichen Skalierungsherausforderungen und häufigen Ausfällen mit Sellerie und RabbitMQ, zwei Technologien, die das System antreiben, das die asynchrone Arbeit übernimmt und kritische Funktionen unserer Plattform ermöglicht, einschließlich Bestellabwicklung und Dasher-Zuweisungen.

Wir lösten dieses Problem schnell mit einem einfachen, Apache Kafka-basierten asynchronen Aufgabenverarbeitungssystem, das unsere Ausfälle stoppte, während wir weiter an einer robusten Lösung arbeiteten. Unsere erste Version implementiert den kleinsten Satz von Funktionen benötigt, um einen großen Teil der bestehenden Sellerie Aufgaben unterzubringen. In der Produktion haben wir weiterhin Unterstützung für weitere Sellerie-Funktionen hinzugefügt und gleichzeitig neue Probleme behoben, die bei der Verwendung von Kafka auftraten.

Die Probleme, mit denen wir bei der Verwendung von Sellerie und RabbitMQ konfrontiert waren

RabbitMQ und Sellerie waren geschäftskritische Teile unserer Infrastruktur, die über 900 verschiedene asynchrone Aufgaben bei DoorDash ermöglichten, einschließlich Bestellabwicklung, Händlerauftragsübertragung und Dasher-Standortverarbeitung. Das Problem, mit dem DoorDash konfrontiert war, war, dass RabbitMQ aufgrund übermäßiger Belastung häufig ausfiel. Wenn die Aufgabenverarbeitung ausfiel, ging DoorDash effektiv aus und Bestellungen konnten nicht abgeschlossen werden, was zu Umsatzeinbußen für unsere Händler und Dasher und zu einer schlechten Erfahrung für unsere Verbraucher führte. Wir hatten Probleme an folgenden Fronten:

  • Verfügbarkeit: Ausfälle aufgrund von Nachfrage verringerten die Verfügbarkeit.
  • Skalierbarkeit: RabbitMQ konnte nicht mit dem Wachstum unseres Geschäfts skalieren.
  • Beobachtbarkeit: RabbitMQ bot begrenzte Metriken und Sellerie-Worker waren undurchsichtig.
  • Betriebseffizienz: Der Neustart dieser Komponenten war ein zeitaufwändiger, manueller Prozess.

Warum unser asynchrones Task-Processing-System nicht hochverfügbar war

Das größte Problem, mit dem wir konfrontiert waren, waren Ausfälle, die häufig auftraten, als die Nachfrage ihren Höhepunkt erreichte. RabbitMQ würde aufgrund von Last, übermäßiger Verbindungsabwanderung und anderen Gründen ausfallen. Aufträge würden angehalten, und wir müssten unser System neu starten oder manchmal sogar einen völlig neuen Broker und ein manuelles Failover aufrufen, um uns von dem Ausfall zu erholen.

Als wir uns eingehender mit den Verfügbarkeitsproblemen befassten, fanden wir die folgenden Unterprobleme:

  • Sellerie ermöglicht es Benutzern, zukünftige Aufgaben mit einem Countdown oder einer ETA zu planen. Unsere starke Nutzung dieser Countdowns führte zu spürbaren Laststeigerungen auf dem Broker. Einige unserer Ausfälle standen in direktem Zusammenhang mit einer Zunahme von Aufgaben mit Countdowns. Letztendlich entschieden wir uns, die Verwendung von Countdowns zugunsten eines anderen Systems einzuschränken, das wir für die zukünftige Planung von Arbeiten eingerichtet hatten.
  • Plötzliche Datenverkehrsausbrüche würden RabbitMQ in einen verschlechterten Zustand versetzen, in dem der Taskverbrauch erheblich niedriger war als erwartet. Nach unserer Erfahrung konnte dies nur mit einem RabbitMQ-Bounce behoben werden. RabbitMQ verfügt über ein Konzept der Flusskontrolle, bei dem die Geschwindigkeit von Verbindungen verringert wird, die zu schnell ausgeführt werden, sodass Warteschlangen mithalten können. Die Flusskontrolle war oft, aber nicht immer, an diesen Verfügbarkeitseinbußen beteiligt. Wenn die Flusskontrolle einsetzt, sehen die Publisher dies effektiv als Netzwerklatenz. Die Netzwerklatenz reduziert unsere Antwortzeiten; Wenn die Latenz während des Spitzenverkehrs zunimmt, können erhebliche Verlangsamungen auftreten, die kaskadieren, wenn sich Anfragen Upstream häufen.
  • Unsere Python uWSGI Web Worker hatten eine Funktion namens Harakiri, die aktiviert wurde, um alle Prozesse zu beenden, die ein Timeout überschritten. Bei Ausfällen oder Verlangsamungen führte Harakiri zu einer Verbindungsabwanderung zu den RabbitMQ-Brokern, da Prozesse wiederholt beendet und neu gestartet wurden. Mit Tausenden von Web-Workern, die zu einem bestimmten Zeitpunkt ausgeführt werden, würde jede Langsamkeit, die Harakiri auslöste, wiederum noch mehr zur Langsamkeit beitragen, indem RabbitMQ zusätzlich belastet wird.
  • In der Produktion haben wir mehrere Fälle erlebt, in denen die Aufgabenverarbeitung in den Sellerie-Verbrauchern gestoppt wurde, selbst wenn keine signifikante Belastung auftrat. Unsere Ermittlungsbemühungen ergaben keine Hinweise auf Ressourcenbeschränkungen, die die Verarbeitung gestoppt hätten, und die Arbeiter nahmen die Verarbeitung wieder auf, sobald sie zurückgeworfen wurden. Dieses Problem wurde nie von der Wurzel verursacht, obwohl wir ein Problem in den Sellerie-Arbeitern selbst und nicht in RabbitMQ vermuten.

Insgesamt waren all diese Verfügbarkeitsprobleme für uns nicht akzeptabel, da hohe Zuverlässigkeit eine unserer höchsten Prioritäten ist. Da diese Ausfälle uns viel in Bezug auf verpasste Aufträge und Glaubwürdigkeit kosteten, brauchten wir eine Lösung, die diese Probleme so schnell wie möglich beheben würde.

Warum unsere Legacy-Lösung nicht skaliert wurde

Das nächstgrößte Problem war die Skalierung. DoorDash wächst schnell und wir sind schnell an die Grenzen unserer bestehenden Lösung gestoßen. Wir mussten etwas finden, das mit unserem anhaltenden Wachstum Schritt halten würde, da unsere Legacy-Lösung die folgenden Probleme hatte:

Die vertikale Skalierungsgrenze erreichen

Wir verwendeten die größte verfügbare RabbitMQ-Lösung mit einem Knoten, die uns zur Verfügung stand. Es gab keinen Weg, vertikal weiter zu skalieren, und wir begannen bereits, diesen Knoten an seine Grenzen zu bringen.

Der Hochverfügbarkeitsmodus beschränkte unsere Kapazität

Aufgrund der Replikation reduzierte der Primär-Sekundär-Hochverfügbarkeitsmodus (HA) den Durchsatz im Vergleich zur Einzelknotenoption, sodass wir noch weniger Headroom haben als nur die Einzelknotenlösung. Wir konnten es uns nicht leisten, den Durchsatz gegen Verfügbarkeit zu tauschen.Zweitens hat der Primär-Sekundär-HA-Modus in der Praxis die Schwere unserer Ausfälle nicht verringert. Failovers dauerten mehr als 20 Minuten und blieben häufig hängen, was manuelle Eingriffe erforderte. Auch Nachrichten gingen dabei oft verloren.

Uns ging schnell die Kopffreiheit aus, als DoorDash weiter wuchs und unsere Aufgabenverarbeitung an ihre Grenzen brachte. Wir brauchten eine Lösung, die horizontal skaliert werden konnte, wenn unsere Verarbeitungsanforderungen wuchsen.

Wie Sellerie und RabbitMQ eine begrenzte Beobachtbarkeit boten

Zu wissen, was in einem System vor sich geht, ist von grundlegender Bedeutung für die Gewährleistung seiner Verfügbarkeit, Skalierbarkeit und betrieblichen Integrität.

Als wir durch die oben beschriebenen Probleme navigierten, stellten wir Folgendes fest:

  • Wir waren auf eine kleine Anzahl von RabbitMQ-Metriken beschränkt, die uns zur Verfügung standen.
  • Wir hatten nur eingeschränkte Sicht auf die Sellerie-Arbeiter selbst.

Wir mussten in der Lage sein, Echtzeit-Metriken für jeden Aspekt unseres Systems zu sehen, was bedeutete, dass auch die Einschränkungen der Beobachtbarkeit angegangen werden mussten.

Die Herausforderungen der betrieblichen Effizienz

Wir hatten auch mehrere Probleme mit dem Betrieb von RabbitMQ:

  • Wir mussten unseren RabbitMQ-Knoten häufig auf einen neuen Failover umstellen, um die anhaltende Verschlechterung zu beheben, die wir beobachteten. Dieser Vorgang war manuell und zeitaufwändig für die beteiligten Ingenieure und musste oft spät in der Nacht außerhalb der Spitzenzeiten durchgeführt werden.
  • Bei DoorDash gab es keine internen Sellerie- oder RabbitMQ-Experten, auf die wir uns stützen konnten, um eine Skalierungsstrategie für diese Technologie zu entwickeln.

Der Zeitaufwand für den Betrieb und die Wartung von RabbitMQ war nicht nachhaltig. Wir brauchten etwas, das unseren aktuellen und zukünftigen Bedürfnissen besser entsprach.

Mögliche Lösungen für unsere Probleme mit Sellerie und RabbitMQ

Bei den oben beschriebenen Problemen haben wir die folgenden Lösungen in Betracht gezogen:

  • Ändern Sie den Sellerie-Broker von RabbitMQ in Redis oder Kafka. Dies würde es uns ermöglichen, Sellerie weiterhin mit einem anderen und möglicherweise zuverlässigeren Sicherungsdatenspeicher zu verwenden.
  • Fügen Sie unserer Django-App Multi-Broker-Unterstützung hinzu, damit Verbraucher auf der Grundlage der von uns gewünschten Logik bei N verschiedenen Brokern veröffentlichen können. Die Aufgabenverarbeitung wird über mehrere Broker verteilt, sodass jeder Broker einen Bruchteil der anfänglichen Belastung erfährt.
  • Upgrade auf neuere Versionen von Sellerie und RabbitMQ. Von neueren Versionen von Sellerie und RabbitMQ wurde erwartet, dass sie Zuverlässigkeitsprobleme beheben und uns Zeit sparen, da wir bereits parallel Komponenten aus unserem Django-Monolith extrahierten.
  • Migrieren Sie zu einer benutzerdefinierten Lösung, die von Kafka unterstützt wird. Diese Lösung erfordert mehr Aufwand als die anderen Optionen, die wir aufgelistet haben, hat aber auch mehr Potenzial, jedes Problem zu lösen, das wir mit der Legacy-Lösung hatten.

Jede Option hat ihre Vor- und Nachteile:

Option Vorteile Nachteile
Redis als Broker
  • Verbesserte Verfügbarkeit mit ElasticCache und Multi-AZ-Unterstützung
  • Verbesserte Broker-Beobachtbarkeit mit ElasticCache als Broker
  • Verbesserte betriebliche Effizienz
  • Interne Betriebserfahrung und Expertise mit Redis
  • Ein Broker-Swap ist als unterstützte Option in Sellerie sofort verfügbar
  • Die Abwanderung von Harakiri-Verbindungen beeinträchtigt die Redis-Leistung nicht wesentlich
  • Inkompatible mit Redis clustered mode
  • Single node Redis skaliert nicht horizontal
  • Keine Verbesserungen der Sellerie-Beobachtbarkeit
  • Diese Lösung behebt nicht das beobachtete Problem, bei dem Sellerie-Worker die Verarbeitung von Aufgaben gestoppt haben
Kafka als Broker
  • Kafka kann hochverfügbar sein
  • Kafka ist horizontal skalierbar
  • Verbesserte Beobachtbarkeit mit Kafka der Broker
  • Verbesserte betriebliche Effizienz
  • DoorDash verfügte über internes Kafka-Know-how
  • Ein Broker-Swap ist als unterstützte Option in Sellerie sofort verfügbar
  • Harakiri Connection churn beeinträchtigt die Leistung von Kafka nicht signifikant
  • Kafka wird von Sellerie noch nicht unterstützt
  • Behebt nicht das beobachtete Problem, bei dem Sellerie-Mitarbeiter die Verarbeitung von Aufgaben beenden
  • Keine Verbesserungen der Beobachtbarkeit von Sellerie
  • Trotz interner Erfahrung hatten wir Kafka bei DoorDash nicht in großem Maßstab betrieben.
Mehrere Broker
  • Verbesserte Verfügbarkeit
  • Horizontale Skalierbarkeit
  • Keine Verbesserungen der Beobachtbarkeit
  • Keine Verbesserungen der Betriebseffizienz
  • Behebt nicht das beobachtete Problem, bei dem Sellerie-Mitarbeiter die Verarbeitung von Aufgaben beenden
  • Behebt nicht das Problem mit Harakiri-induzierter Verbindungsabwanderung
Upgrade-Versionen
  • Könnte das Problem verbessern, bei dem RabbitMQ in einem verschlechterten Zustand stecken bleibt
  • Könnte das Problem verbessern, bei dem Sellerie-Arbeiter stecken bleiben
  • Könnte uns Spielraum verschaffen, um eine längerfristige Strategie umzusetzen
  • Nicht garantiert, dass unsere beobachteten Fehler behoben werden
  • Wird unsere Probleme mit Verfügbarkeit, Skalierbarkeit, Beobachtbarkeit und Betriebseffizienz nicht sofort beheben
  • Neuere Versionen von RabbitMQ und Sellerie erforderten neuere Versionen von Python.
  • Behebt nicht das Problem mit Harakiri-induzierter Verbindungsabwanderung
Benutzerdefinierte Kafka-Lösung
  • Kafka kann hochverfügbar sein
  • Kafka ist horizontal skalierbar
  • Verbesserte Beobachtbarkeit mit Kakfa als Broker
  • Verbesserte Betriebseffizienz
  • Interne Kafka-Expertise
  • Ein Broker, der die Änderung ist geradlinig
  • Harakiri-Verbindungsabwanderung beeinträchtigt die Kafka-Leistung nicht wesentlich
  • Behebt das beobachtete Problem, bei dem Sellerie-Mitarbeiter die Verarbeitung von Aufgaben beenden
  • Erfordert mehr als alle anderen Optionen zu implementieren
  • Trotz interner Erfahrung hatten wir Kafka bei DoorDash nicht in großem Umfang betrieben

Unsere Strategie für das Onboarding von Kafka

Angesichts unserer erforderlichen Systemverfügbarkeit haben wir unsere Onboarding-Strategie auf der Grundlage der folgenden prinzipien zur Maximierung der Zuverlässigkeitsvorteile in kürzester Zeit. Diese Strategie umfasste drei Schritte:

  • Erste Schritte: Wir wollten die Grundlagen der von uns entwickelten Lösung nutzen, während wir andere Teile davon iterierten. Wir vergleichen diese Strategie mit dem Fahren eines Rennwagens beim Austausch einer neuen Kraftstoffpumpe.
  • Designentscheidungen für eine nahtlose Übernahme durch Entwickler: Wir wollten die Verschwendung von Aufwand seitens aller Entwickler minimieren, die sich aus der Definition einer anderen Schnittstelle ergeben könnte.
  • Inkrementeller Rollout ohne Ausfallzeiten: Anstatt eine große auffällige Version zum ersten Mal in freier Wildbahn mit einer höheren Wahrscheinlichkeit von Fehlern zu testen, konzentrierten wir uns auf den Versand kleinerer unabhängiger Funktionen, die über einen längeren Zeitraum einzeln in freier Wildbahn getestet werden konnten.

Erste Schritte

Der Wechsel zu Kafka stellte eine große technische Änderung in unserem Stack dar, die jedoch dringend benötigt wurde. Wir hatten keine Zeit zu verlieren, da wir jede Woche aufgrund der Instabilität unserer alten RabbitMQ-Lösung Geschäfte verloren. Unsere erste und wichtigste Priorität war es, ein Minimum Viable Product (MVP) zu schaffen, das uns vorläufige Stabilität bringt und uns den nötigen Spielraum gibt, um eine umfassendere Lösung mit breiterer Akzeptanz zu iterieren und vorzubereiten.

Unser MVP bestand aus Produzenten, die Task Fully Qualified Names (FQNs) und eingelegte Argumente an Kafka veröffentlichten, während unsere Konsumenten diese Nachrichten lasen, die Tasks aus dem FQN importierten und sie synchron mit den angegebenen Argumenten ausführten.

Die MVP-Architektur (Minimal Viable Product), für die wir uns entschieden haben, beinhaltete einen Zwischenzustand, in dem wir gegenseitig ausschließende Aufgaben sowohl für das Legacy-System (rote gestrichelte Linien) als auch für das neue System veröffentlichen würden (grüne durchgezogene Linien), vor dem endgültigen Zustand, in dem wir die Veröffentlichung von Aufgaben in RabbitMQ einstellen würden.1

Abbildung 1: Die MVP-Architektur (Minimal Viable Product), für die wir uns entschieden haben, beinhaltete einen Zwischenzustand, in dem wir gegenseitig ausschließende Aufgaben sowohl für das Legacy-System (rote gestrichelte Linien) als auch für das neue System (grüne durchgezogene Linien) veröffentlichen würden Der endgültige Zustand, in dem wir Aufgaben nicht mehr in RabbitMQ veröffentlichen würden.

Designentscheidungen für eine nahtlose Übernahme durch Entwickler

Manchmal ist die Übernahme durch Entwickler eine größere Herausforderung als die Entwicklung. Wir haben dies vereinfacht, indem wir einen Wrapper für die @task Annotation von Sellerie implementiert haben, der Aufgabenübermittlungen basierend auf dynamisch konfigurierbaren Feature-Flags dynamisch an eines der beiden Systeme weiterleitete. Jetzt könnte dieselbe Schnittstelle verwendet werden, um Aufgaben für beide Systeme zu schreiben. Mit diesen Entscheidungen mussten die Engineering-Teams keine zusätzliche Arbeit für die Integration in das neue System leisten, abgesehen von der Implementierung eines einzelnen Feature-Flags.

Wir wollten unser System ausrollen, sobald unser MVP fertig war, aber es unterstützte noch nicht alle Funktionen wie Sellerie. Mit Sellerie können Benutzer ihre Aufgaben mit Parametern in ihrer Aufgabenanmerkung oder beim Senden ihrer Aufgabe konfigurieren. Damit wir schneller starten können, haben wir eine Whitelist kompatibler Parameter erstellt und uns dafür entschieden, die kleinste Anzahl von Funktionen zu unterstützen, die für die meisten Aufgaben erforderlich sind.

Wir haben das Aufgabenvolumen schnell auf das Kafka-basierte MVP erhöht, beginnend mit Aufgaben mit geringem Risiko und niedriger Priorität. Einige davon waren Aufgaben, die außerhalb der Stoßzeiten ausgeführt wurden, was die Spitzen der oben dargestellten Metrik erklärt.

Abbildung 2: Wir haben das Aufgabenvolumen schnell auf das Kafka-basierte MVP erhöht, beginnend mit Aufgaben mit geringem Risiko und niedriger Priorität. Einige davon waren Aufgaben, die außerhalb der Stoßzeiten ausgeführt wurden, was die Spitzen der oben dargestellten Metrik erklärt.

Wie in Abbildung 2 zu sehen ist, haben wir mit den beiden obigen Entscheidungen unser MVP nach zweiwöchiger Entwicklung gestartet und eine weitere Woche nach dem Start eine Reduzierung der RabbitMQ-Aufgabenlast um 80% erreicht. Wir haben uns schnell mit unserem Hauptproblem der Ausfälle befasst und im Laufe des Projekts immer mehr esoterische Funktionen unterstützt, um die Ausführung der verbleibenden Aufgaben zu ermöglichen.

Inkrementeller Rollout, keine Ausfallzeiten

Die Möglichkeit, Kafka-Cluster zu wechseln und dynamisch zwischen RabbitMQ und Kafka zu wechseln, ohne geschäftliche Auswirkungen zu haben, war für uns äußerst wichtig. Diese Fähigkeit half uns auch bei einer Vielzahl von Vorgängen wie Clusterwartung, Lastabwurf und schrittweisen Migrationen. Um diesen Rollout zu implementieren, haben wir dynamische Feature-Flags sowohl auf der Ebene der Nachrichtenübermittlung als auch auf der Seite des Nachrichtenverbrauchs verwendet. Die Kosten für die vollständige Dynamik hier waren, unsere Arbeiterflotte mit doppelter Kapazität am Laufen zu halten. Die Hälfte dieser Flotte war RabbitMQ und der Rest Kafka gewidmet. Der Betrieb der Arbeiterflotte mit doppelter Kapazität war definitiv eine Belastung für unsere Infrastruktur. Irgendwann haben wir sogar einen völlig neuen Kubernetes-Cluster eingerichtet, um alle unsere Mitarbeiter unterzubringen.

In der Anfangsphase der Entwicklung hat uns diese Flexibilität gut getan. Sobald wir mehr Vertrauen in unser neues System hatten, haben wir nach Möglichkeiten gesucht, die Belastung unserer Infrastruktur zu reduzieren, z. B. mehrere verbrauchende Prozesse pro Worker-Maschine auszuführen. Als wir verschiedene Themen übergingen, konnten wir die Anzahl der Mitarbeiter für RabbitMQ reduzieren und gleichzeitig eine kleine Reservekapazität beibehalten.

Keine Lösung ist perfekt, iterieren Sie nach Bedarf

Mit unserem MVP in der Produktion hatten wir den nötigen Spielraum, um unser Produkt zu iterieren und zu polieren. Wir haben jede fehlende Sellerie-Funktion nach der Anzahl der Aufgaben geordnet, die sie verwendet haben, um zu entscheiden, welche zuerst implementiert werden sollen. Funktionen, die nur von wenigen Aufgaben verwendet werden, wurden in unserer benutzerdefinierten Lösung nicht implementiert. Stattdessen haben wir diese Aufgaben neu geschrieben, um diese spezielle Funktion nicht zu verwenden. Mit dieser Strategie haben wir schließlich alle Aufgaben von Sellerie verschoben.

Die Verwendung von Kafka führte auch zu neuen Problemen, die unsere Aufmerksamkeit erforderten:

  • Head-of-the-Line-Blockierung, die zu Verzögerungen bei der Aufgabenverarbeitung führte
  • Bereitstellungen lösten ein Partitions-Rebalancing aus, das ebenfalls zu Verzögerungen führte

Kafkas Head-of-the-Line-Blockierungsproblem

Kafka-Themen sind so partitioniert, dass ein einzelner Verbraucher (pro Verbrauchergruppe) Nachrichten für seine zugewiesenen Partitionen in der Reihenfolge liest, in der sie eingetroffen sind. Wenn die Verarbeitung einer Nachricht in einer einzelnen Partition zu lange dauert, wird die Verarbeitung aller dahinter liegenden Nachrichten in dieser Partition gestoppt (siehe Abbildung 3 unten). Dieses Problem kann bei einem Thema mit hoher Priorität besonders katastrophal sein. Wir möchten in der Lage sein, Nachrichten in einer Partition weiter zu verarbeiten, falls eine Verzögerung auftritt.

In Kafkas Head-of-the-Line-Blockierungsproblem blockiert eine langsame Nachricht in einer Partition (in rot) die Verarbeitung aller Nachrichten dahinter. Andere Partitionen würden weiterhin wie erwartet verarbeitet.

Abbildung 3: In Kafkas Head-of-the-Line-Blockierungsproblem blockiert eine langsame Nachricht in einer Partition (in rot) die Verarbeitung aller Nachrichten dahinter. Andere Partitionen würden weiterhin wie erwartet verarbeitet.

Während Parallelität grundsätzlich ein Python-Problem ist, sind die Konzepte dieser Lösung auch auf andere Sprachen anwendbar. Unsere Lösung, die in Abbildung 4 unten dargestellt ist, bestand darin, einen Kafka-Consumer-Prozess und mehrere Task-Execution-Prozesse pro Worker unterzubringen. Der Kafka-Consumer-Prozess ist dafür verantwortlich, Nachrichten von Kafka abzurufen und in eine lokale Warteschlange zu stellen, die von den Taskausführungsprozessen gelesen wird. Es verbraucht weiter, bis die lokale Warteschlange einen benutzerdefinierten Schwellenwert erreicht. Diese Lösung lässt Nachrichten in der Partition fließen und nur ein Taskausführungsprozess wird durch die langsame Nachricht gestoppt. Der Schwellenwert begrenzt auch die Anzahl der In-Flight-Nachrichten in der lokalen Warteschlange (die im Falle eines Systemabsturzes verloren gehen können).

Abbildung 4: Unser nicht blockierender Kafka-Worker besteht aus einer lokalen Nachrichtenwarteschlange und zwei Arten von Prozessen: einem Kafka-Consumer-Prozess und mehreren Task-Executor-Prozessen. Während ein Kafka-Consumer von mehreren Partitionen lesen kann, stellen wir der Einfachheit halber nur eine dar. Dieses Diagramm zeigt, dass eine langsam verarbeitende Nachricht (in rot) nur einen einzelnen Task-Executor blockiert, bis er abgeschlossen ist, während andere Nachrichten dahinter in der Partition weiterhin von anderen Task-Executoren verarbeitet werden.

Abbildung 4: Unser nicht blockierender Kafka-Worker besteht aus einer lokalen Nachrichtenwarteschlange und zwei Arten von Prozessen: ein Kafka-Consumer-Prozess und mehrere Task-Executor-Prozesse. Während ein Kafka-Consumer von mehreren Partitionen lesen kann, stellen wir der Einfachheit halber nur eine dar. Dieses Diagramm zeigt, dass eine langsam verarbeitende Nachricht (in rot) nur einen einzelnen Task-Executor blockiert, bis er abgeschlossen ist, während andere Nachrichten dahinter in der Partition weiterhin von anderen Task-Executoren verarbeitet werden.

Die Disruptivität von Deploys

Wir stellen unsere Django-App mehrmals am Tag bereit. Ein Nachteil unserer Lösung, den wir festgestellt haben, ist, dass eine Bereitstellung eine Neuverteilung der Partitionszuweisungen in Kafka auslöst. Trotz der Verwendung einer anderen Verbrauchergruppe pro Thema zur Begrenzung des Neuausgleichsbereichs führten Bereitstellungen immer noch zu einer vorübergehenden Verlangsamung der Nachrichtenverarbeitung, da die Aufgabennutzung während des Neuausgleichs gestoppt werden musste. Verlangsamungen können in den meisten Fällen akzeptabel sein, wenn wir geplante Releases durchführen, können aber katastrophal sein, wenn wir beispielsweise eine Notfallversion durchführen, um einen Fehler zu beheben. Die Folge wäre die Einführung einer kaskadierenden Verarbeitungsverlangsamung. Neuere Versionen von Kafka und Clients unterstützen ein inkrementelles kooperatives Rebalancing, das die operativen Auswirkungen eines Rebalancing massiv reduzieren würde. Ein Upgrade unserer Kunden zur Unterstützung dieser Art von Rebalancing wäre in Zukunft unsere Lösung der Wahl. Leider wird das inkrementelle kooperative Rebalancing in unserem ausgewählten Kafka-Client noch nicht unterstützt.

Schlüsselgewinne

Mit dem Abschluss dieses Projekts haben wir signifikante Verbesserungen in Bezug auf Verfügbarkeit, Skalierbarkeit, Beobachtbarkeit und Dezentralisierung erzielt. Diese Gewinne waren entscheidend für das weitere Wachstum unseres Geschäfts.

Keine wiederholten Ausfälle mehr

Wir haben die wiederholten Ausfälle fast gestoppt, sobald wir mit der Einführung dieses benutzerdefinierten Kafka-Ansatzes begonnen haben. Ausfälle führten zu extrem schlechten Benutzererfahrungen.

  • Durch die Implementierung nur einer kleinen Teilmenge der am häufigsten verwendeten Sellerie-Funktionen in unserem MVP konnten wir den Arbeitscode in zwei Wochen in die Produktion bringen.
  • Mit dem MVP konnten wir die Belastung von RabbitMQ und Sellerie erheblich reduzieren, während wir unsere Lösung weiter aushärteten und neue Funktionen implementierten.

Die Aufgabenverarbeitung war nicht länger der limitierende Faktor für das Wachstum

Mit Kafka als Herzstück unserer Architektur haben wir ein Aufgabenverarbeitungssystem entwickelt, das hochverfügbar und horizontal skalierbar ist und es DoorDash und seinen Kunden ermöglicht, ihr Wachstum fortzusetzen.

Massiv erweiterte Beobachtbarkeit

Da es sich um eine benutzerdefinierte Lösung handelte, konnten wir auf fast jeder Ebene mehr Metriken einspeisen. Jede Warteschlange, jeder Worker und jede Aufgabe war in Produktions- und Entwicklungsumgebungen auf einer sehr detaillierten Ebene vollständig beobachtbar. Diese erhöhte Beobachtbarkeit war ein großer Gewinn nicht nur in Bezug auf die Produktion, sondern auch in Bezug auf die Entwicklerproduktivität.

Operative Dezentralisierung

Mit den Verbesserungen der Beobachtbarkeit konnten wir unsere Warnungen als Terraform-Module templatisieren und jedem einzelnen Thema und implizit allen über 900 Aufgaben explizit Eigentümer zuweisen.

Eine detaillierte Bedienungsanleitung für das Task-Processing-System macht Informationen für alle Ingenieure zugänglich, um betriebliche Probleme mit ihren Themen und Mitarbeitern zu debuggen und bei Bedarf allgemeine Kafka-Cluster-Management-Operationen durchzuführen. Der tägliche Betrieb ist Self-Service und Support wird selten von unserem Infrastrukturteam benötigt.

Fazit

Zusammenfassend haben wir die Obergrenze unserer Fähigkeit erreicht, RabbitMQ zu skalieren, und mussten nach Alternativen suchen. Die Alternative, die wir gewählt haben, war eine benutzerdefinierte Kafka-basierte Lösung. Obwohl die Verwendung von Kafka einige Nachteile hat, haben wir eine Reihe von Problemumgehungen gefunden, die oben beschrieben wurden.

Wenn kritische Workflows stark von asynchroner Aufgabenverarbeitung abhängen, ist die Gewährleistung der Skalierbarkeit von größter Bedeutung. Lassen Sie sich bei ähnlichen Problemen von unserer Strategie inspirieren, die uns 80% des Ergebnisses mit 20% des Aufwands bescherte. Diese Strategie ist im Allgemeinen ein taktischer Ansatz, um Zuverlässigkeitsprobleme schnell zu mildern und dringend benötigte Zeit für eine robustere und strategischere Lösung zu gewinnen.

Danksagung

Die Autoren danken Clement Fang, Corry Haines, Danial Asif, Jay Weinstein, Luigi Tagliamonte, Matthew Anger, Shaohua Zhou und Yun-Yu Chen für ihren Beitrag zu diesem Projekt.

Foto von tian kuan auf Unsplash

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.