skalowanie infrastruktury zaplecza w celu obsługi hiperrozwoju jest jednym z wielu ekscytujących wyzwań pracy w DoorDash. W połowie 2019 roku mieliśmy do czynienia ze znaczącymi wyzwaniami skalowania i częstymi awariami związanymi z Celery i RabbitMQ, dwiema technologiami zasilającymi system, który obsługuje pracę asynchroniczną, umożliwiając krytyczne funkcjonalności naszej platformy, w tym zlecenia kasowe i zadania Dasher.
szybko rozwiązaliśmy ten problem za pomocą prostego, asynchronicznego systemu przetwarzania zadań opartego na Apache Kafka, który zatrzymał nasze przestoje, podczas gdy my kontynuowaliśmy iterację nad solidnym rozwiązaniem. Nasza pierwsza wersja zaimplementowała najmniejszy zestaw funkcji potrzebnych do obsługi dużej części istniejących zadań selera. Po rozpoczęciu produkcji nadal dodawaliśmy wsparcie dla większej liczby funkcji selera, jednocześnie rozwiązując nowe problemy, które pojawiły się podczas korzystania z Kafki.
problemy, z którymi borykaliśmy się przy użyciu selera i RabbitMQ
RabbitMQ i selera były kluczowymi elementami naszej infrastruktury, które zasilały ponad 900 różnych zadań asynchronicznych w DoorDash, w tym kasowanie zamówień, przesyłanie zamówień kupca i przetwarzanie lokalizacji Dasher. Problem, z którym borykał się DoorDash, polegał na tym, że RabbitMQ często spadał z powodu nadmiernego obciążenia. Jeśli przetwarzanie zadań poszło w dół, DoorDash skutecznie upadł, a zamówienia nie mogły zostać zrealizowane, co spowodowało utratę przychodów dla naszych sprzedawców i Dasherów oraz słabe doświadczenie dla naszych konsumentów. Napotkaliśmy problemy na następujących frontach:
- dostępność: przestoje spowodowane popytem ograniczona dostępność.
- skalowalność: RabbitMQ nie mógł skalować się wraz z rozwojem naszej działalności.
- Obserwowalność: RabbitMQ oferowało ograniczone metryki, a pracownicy selera byli nieprzejrzysti.
- efektywność operacyjna: ponowne uruchomienie tych komponentów było czasochłonnym, ręcznym procesem.
dlaczego nasz Asynchroniczny system przetwarzania zadań nie był wysoce dostępny
największym problemem, z którym mieliśmy do czynienia, były przerwy w dostawie prądu i często pojawiały się, gdy popyt był szczytowy. RabbitMQ spadnie z powodu obciążenia, nadmiernego zerwania połączenia i innych powodów. Zlecenia zostałyby wstrzymane, a my musielibyśmy ponownie uruchomić nasz system, a czasami nawet wprowadzić zupełnie nowego brokera i ręcznie przełączać awaryjnie, aby odzyskać po awarii.
po głębszym zgłębieniu problemów z dostępnością znaleźliśmy następujące podpunkty:
- Celery pozwala użytkownikom planować zadania w przyszłości z odliczaniem lub ETA. Nasze intensywne korzystanie z tych odliczeń spowodowało zauważalny wzrost obciążenia brokera. Niektóre z naszych przestojów były bezpośrednio związane ze wzrostem liczby zadań z odliczaniem. Ostatecznie zdecydowaliśmy się ograniczyć korzystanie z odliczania na rzecz innego systemu, który wprowadziliśmy do planowania pracy w przyszłości.
- nagłe wybuchy ruchu pozostawiłyby RabbitMQ w stanie zdegradowanym, gdzie zużycie zadań było znacznie niższe niż oczekiwano. Z naszego doświadczenia wynika, że można to rozwiązać tylko za pomocą odbicia RabbitMQ. RabbitMQ ma koncepcję kontroli przepływu, w której zmniejszy prędkość połączeń, które publikują zbyt szybko, aby kolejki mogły nadążyć. Kontrola przepływu była często, ale nie zawsze, zaangażowana w te degradacje dostępności. Kiedy pojawia się Kontrola przepływu, wydawcy postrzegają ją jako opóźnienie sieci. Opóźnienie sieci skraca czas odpowiedzi; jeśli opóźnienie wzrasta podczas szczytowego ruchu, znaczne spowolnienie może spowodować, że kaskada jako żądania piętrzą się w górę.
- nasi pracownicy Pythona uWSGI mieli funkcję o nazwie harakiri, która umożliwiała zabijanie procesów, które przekroczyły limit czasu. Podczas przestojów lub spowolnień harakiri spowodowało zerwanie połączenia z brokerami RabbitMQ, ponieważ procesy były wielokrotnie zabijane i uruchamiane ponownie. Przy tysiącach pracowników działających w sieci w danym momencie, każde spowolnienie, które wywołało harakiri, przyczyniłoby się jeszcze bardziej do spowolnienia, dodając dodatkowe obciążenie do RabbitMQ.
- w produkcji doświadczyliśmy kilku przypadków, w których przetwarzanie zadań u konsumentów selera zatrzymało się, nawet przy braku znacznego obciążenia. Nasze wysiłki śledcze nie przyniosły dowodów na jakiekolwiek ograniczenia zasobów, które zatrzymałyby przetwarzanie, a pracownicy wznowili przetwarzanie po ich odbiciu. Ten problem nigdy nie został spowodowany, choć podejrzewamy, że problem dotyczy samych pracowników selera, a nie RabbitMQ.
Ogólnie rzecz biorąc, wszystkie te problemy z dostępnością były dla nas nie do przyjęcia, ponieważ wysoka niezawodność jest jednym z naszych najwyższych priorytetów. Ponieważ te przestoje kosztowały nas wiele pod względem nieodebranych zamówień i wiarygodności, potrzebowaliśmy rozwiązania, które rozwiązałoby te problemy tak szybko, jak to możliwe.
dlaczego nasze starsze rozwiązanie nie skalowało
kolejnym największym problemem była skala. DoorDash szybko się rozwija i szybko osiągaliśmy granice naszego istniejącego rozwiązania. Musieliśmy znaleźć coś, co nadąży za naszym ciągłym rozwojem, ponieważ nasze starsze rozwiązanie miało następujące problemy:
uderzając w pionowy limit skalowania
korzystaliśmy z największego dostępnego rozwiązania RabbitMQ z jednym węzłem. Nie było ścieżki do dalszego skalowania w pionie i już zaczynaliśmy przesuwać ten węzeł do jego granic.
tryb wysokiej dostępności ograniczył naszą pojemność
z powodu replikacji, tryb pierwotnej i wtórnej wysokiej dostępności (ha) zmniejszył przepustowość w porównaniu z opcją pojedynczego węzła, pozostawiając nam jeszcze mniej miejsca niż tylko rozwiązanie pojedynczego węzła. Nie mogliśmy sobie pozwolić na wymianę przepustowości na dostępność.
Po Drugie, pierwotny-wtórny tryb HA w praktyce nie zmniejszył nasilenia naszych przestojów. Przejście awaryjne trwało ponad 20 minut i często wymagało ręcznej interwencji. Wiadomości były często tracone w procesie, jak również.
szybko zabrakło nam miejsca, ponieważ DoorDash nadal się rozwijał i przesuwał nasze przetwarzanie zadań do granic możliwości. Potrzebowaliśmy rozwiązania, które mogłoby skalować się poziomo wraz ze wzrostem potrzeb przetwarzania.
to, jak Celery i RabbitMQ oferowały ograniczoną obserwowalność
Wiedza o tym, co dzieje się w każdym systemie, ma fundamentalne znaczenie dla zapewnienia jego dostępności, skalowalności i integralności operacyjnej.
poruszając się po powyższych zagadnieniach zauważyliśmy, że:
- ograniczyliśmy się do małego zestawu dostępnych dla nas metryk RabbitMQ.
- mieliśmy Ograniczony wgląd w samych pracowników selera.
musieliśmy być w stanie zobaczyć w czasie rzeczywistym metryki każdego aspektu naszego systemu, co oznaczało, że ograniczenia obserwowalności również musiały zostać rozwiązane.
wyzwania związane z wydajnością operacyjną
napotkaliśmy również kilka problemów z obsługą RabbitMQ:
- często musieliśmy przełączać węzeł RabbitMQ na Nowy, aby rozwiązać obserwowaną przez nas trwałą degradację. Operacja ta była ręczna i czasochłonna dla zaangażowanych inżynierów i często musiała być wykonywana późno w nocy, poza godzinami szczytu.
- w firmie DoorDash nie było ekspertów selera ani RabbitMQ, na których moglibyśmy się oprzeć, aby pomóc w opracowaniu strategii skalowania dla tej technologii.
Czas Pracy i utrzymania RabbitMQ nie był trwały. Potrzebowaliśmy czegoś, co lepiej zaspokoi nasze obecne i przyszłe potrzeby.
potencjalne rozwiązania naszych problemów z selerem i RabbitMQ
W przypadku problemów opisanych powyżej rozważaliśmy następujące rozwiązania:
- Zmiana brokera selera z RabbitMQ na Redis lub Kafka. Pozwoli nam to na dalsze korzystanie z selera, z innym i potencjalnie bardziej niezawodnym zapasem danych.
- Dodaj obsługę wielu brokerów do naszej aplikacji Django, aby konsumenci mogli publikować do N różnych brokerów w oparciu o dowolną logikę, jaką chcieliśmy. Przetwarzanie zadań zostanie podzielone na wielu brokerów, więc każdy broker doświadczy ułamka początkowego obciążenia.
- Upgrade do nowszych wersji selera i RabbitMQ. Nowsze wersje selera i RabbitMQ miały rozwiązać problemy z niezawodnością, kupując nam czas, ponieważ równolegle wydobywaliśmy komponenty z naszego monolitu Django.
- migracja do niestandardowego rozwiązania wspieranego przez Kafkę. To rozwiązanie wymaga więcej wysiłku niż inne opcje, które wymieniliśmy, ale ma również większy potencjał, aby rozwiązać każdy problem, który mieliśmy w przypadku starszego rozwiązania.
każda opcja ma swoje plusy i minusy:
opcja | plusy | minusy |
Redis jako broker |
|
|
Kafka jako broker |
|
|
wielu brokerów |
|
|
wersje aktualizacji |
|
|
niestandardowe rozwiązanie Kafki |
|
|
nasza strategia wdrażania Kafki
biorąc pod uwagę wymagany czas pracy systemu, opracowaliśmy nasza strategia wdrażania opiera się na następujących zasadach, aby zmaksymalizować korzyści z niezawodności w jak najkrótszym czasie. Strategia ta obejmowała trzy etapy:
- : Chcieliśmy wykorzystać podstawy rozwiązania, które tworzyliśmy, podczas iteracji w innych jego częściach. Porównujemy tę strategię do jazdy samochodem wyścigowym podczas wymiany nowej pompy paliwa.
- Wybór projektu dla bezproblemowego przyjęcia przez programistów: chcieliśmy zminimalizować zmarnowany wysiłek ze strony wszystkich programistów, który mógł wynikać z zdefiniowania innego interfejsu.
- przyrostowe wdrażanie bez przestojów: Zamiast po raz pierwszy testować Duże, krzykliwe wydanie w środowisku naturalnym z większą szansą na niepowodzenia, skupiliśmy się na dostarczaniu mniejszych niezależnych funkcji, które można indywidualnie testować w środowisku naturalnym przez dłuższy okres czasu.
uderzenie w ziemię bieganie
przejście na Kafkę stanowiło poważną zmianę techniczną w naszym stosie, ale bardzo potrzebną. Nie mieliśmy czasu do stracenia, ponieważ co tydzień traciliśmy działalność z powodu niestabilności naszego starszego rozwiązania RabbitMQ. Naszym pierwszym i najważniejszym priorytetem było stworzenie minimum viable product (MVP), aby zapewnić nam tymczasową stabilność i dać nam przestrzeń niezbędną do iteracji i przygotowania do bardziej kompleksowego rozwiązania z szerszym zastosowaniem.
Nasz MVP składał się z producentów, którzy publikowali nazwy zadań w pełni kwalifikowanych (FQN) i marynowali argumenty do Kafki, podczas gdy nasi konsumenci czytali te wiadomości, importowali zadania z FQN i wykonywali je synchronicznie z podanymi argumentami.
Rysunek 1: Architektura Minimal Viable Product (MVP), którą zdecydowaliśmy się zbudować, zawierała stan przejściowy, w którym publikowalibyśmy wzajemnie wykluczające się zadania zarówno dla starszych systemów (czerwone przerywane linie), jak i dla nowych systemów (zielone linie stałe), przed stanem końcowym, w którym przestalibyśmy publikować zadania do RabbitMQ.
Wybór projektu dla bezproblemowej adopcji przez deweloperów
czasami adopcja deweloperów jest większym wyzwaniem niż rozwój. Ułatwiliśmy to, implementując wrapper dla adnotacji Celery @task, która dynamicznie kierowała zgłoszenia zadań do dowolnego systemu w oparciu o dynamicznie konfigurowalne flagi funkcji. Teraz ten sam interfejs może być użyty do pisania zadań dla obu systemów. Po podjęciu tych decyzji zespoły inżynierskie nie musiały wykonywać żadnej dodatkowej pracy w celu integracji z nowym systemem, z wyjątkiem wdrożenia flagi pojedynczej funkcji.
chcieliśmy wdrożyć nasz system, gdy tylko nasz MVP będzie gotowy, ale nie obsługuje jeszcze wszystkich tych samych funkcji, co Celery. Celery pozwala użytkownikom konfigurować swoje zadania z parametrami w adnotacji zadań lub kiedy przesyłają swoje zadanie. Aby umożliwić nam szybsze uruchamianie, stworzyliśmy białą listę kompatybilnych parametrów i zdecydowaliśmy się obsługiwać najmniejszą liczbę funkcji potrzebnych do obsługi większości zadań.
Rysunek 2: Szybko zwiększyliśmy wolumen zadań do MVP opartego na Kafce, zaczynając od zadań o niskim ryzyku i niskim priorytecie. Niektóre z nich były zadaniami, które działały poza godzinami szczytu, co wyjaśnia skoki wskaźników przedstawionych powyżej.
jak widać na rysunku 2, dzięki dwóm powyższym decyzjom uruchomiliśmy nasz MVP po dwóch tygodniach rozwoju i osiągnęliśmy 80% redukcję obciążenia zadania RabbitMQ kolejny tydzień po uruchomieniu. Szybko poradziliśmy sobie z naszym głównym problemem awarii, a w trakcie projektu wspieraliśmy coraz więcej ezoterycznych funkcji umożliwiających realizację pozostałych zadań.
Incremental rollout, zero przestojów
możliwość dynamicznego przełączania klastrów Kafki i przełączania się między RabbitMQ a Kafką bez wpływu na biznes była dla nas niezwykle ważna. Ta zdolność pomogła nam również w różnych operacjach, takich jak konserwacja klastra, zrzucenie obciążenia i stopniowa migracja. Aby wdrożyć to wdrożenie, wykorzystaliśmy dynamiczne flagi funkcji zarówno na poziomie przesyłania wiadomości, jak i po stronie zużycia wiadomości. Kosztem pełnej dynamiki było utrzymanie naszej floty pracowników na podwójnej wydajności. Połowę floty przeznaczono na RabbitMQ, a resztę na Kafkę. Prowadzenie floty pracowników z podwójną przepustowością zdecydowanie obciążało naszą infrastrukturę. W pewnym momencie stworzyliśmy nawet zupełnie nowy klaster Kubernetes, aby pomieścić wszystkich naszych pracowników.
w początkowej fazie rozwoju ta elastyczność dobrze nam służyła. Gdy zaufaliśmy naszemu nowemu systemowi, przyjrzeliśmy się sposobom zmniejszenia obciążenia naszej infrastruktury, takim jak uruchamianie wielu procesów zużywających na maszynę pracownika. Zmieniając różne tematy, mogliśmy zacząć zmniejszać liczbę pracowników w RabbitMQ, zachowując niewielką rezerwę mocy produkcyjnych.
żadne rozwiązanie nie jest idealne, iteruj w razie potrzeby
dzięki naszemu MVP w produkcji mieliśmy przestrzeń potrzebną do iterowania i polerowania naszego produktu. Każdą brakującą funkcję selera uszeregowaliśmy według liczby zadań, które ją wykorzystały, aby pomóc nam zdecydować, które z nich wdrożyć jako pierwsze. Funkcje używane tylko przez kilka zadań nie zostały zaimplementowane w naszym niestandardowym rozwiązaniu. Zamiast tego napisaliśmy te zadania ponownie, aby nie używać tej konkretnej funkcji. Dzięki tej strategii ostatecznie przenieśliśmy wszystkie zadania z selera.
Korzystanie z Kafki wprowadziło również nowe problemy, które wymagały naszej uwagi:
- blokowanie Head-of-the-line, które spowodowało opóźnienia przetwarzania zadań
- wdrożenia spowodowały przywrócenie równowagi partycji, co również spowodowało opóźnienia
problem blokowania head-of-the-line Kafki
tematy Kafki są podzielone na partycje w taki sposób, że pojedynczy konsument (na grupę konsumentów) odczytuje wiadomości dla przypisanych partycji w kolejności, w jakiej dotarły. Jeśli wiadomość na jednej partycji trwa zbyt długo, aby zostać przetworzona, zatrzyma ona zużycie wszystkich wiadomości znajdujących się za nią na tej partycji, jak pokazano na rysunku 3 poniżej. Problem ten może być szczególnie katastrofalny w przypadku tematu o wysokim priorytecie. Chcemy być w stanie nadal przetwarzać wiadomości na partycji w przypadku wystąpienia opóźnienia.
Rysunek 3: W problemie blokowania head-of-the-line Kafki, powolny komunikat na partycji (na Czerwono) blokuje wszystkie wiadomości znajdujące się za nią przed przetworzeniem. Inne partycje będą nadal przetwarzać zgodnie z oczekiwaniami.
chociaż równoległość jest zasadniczo problemem Pythona, koncepcje tego rozwiązania mają zastosowanie również do innych języków. Naszym rozwiązaniem, przedstawionym na rysunku 4 poniżej, było przeprowadzenie jednego procesu Kafka-consumer i wielu procesów realizacji zadań na pracownika. Proces Kafka-consumer jest odpowiedzialny za Pobieranie wiadomości z Kafki i umieszczanie ich w lokalnej kolejce, która jest odczytywana przez procesy wykonujące zadania. Trwa on do momentu, gdy lokalna Kolejka osiągnie próg zdefiniowany przez użytkownika. Rozwiązanie to pozwala na przepływ komunikatów na partycji i tylko jeden proces wykonania zadania zostanie zatrzymany przez powolny komunikat. Próg ten ogranicza również liczbę wiadomości podczas lotu w lokalnej kolejce (które mogą zostać utracone w przypadku awarii systemu).
Rysunek 4: nasz nieblokujący pracownik Kafki składa się z lokalnej kolejki komunikatów i dwóch typów procesów: proces typu kafka-konsument oraz wiele procesów typu task-executor. Podczas gdy Kafka-konsument może czytać z wielu partycji, dla uproszczenia przedstawimy tylko jedną. Ten diagram pokazuje, że wiadomość o powolnym przetwarzaniu (na Czerwono) blokuje tylko jednego wykonawcę zadania, dopóki nie zakończy się, podczas gdy inne wiadomości za nim na partycji nadal są przetwarzane przez innych wykonawców zadań.
destrukcyjność wdrożeń
wdrażamy naszą aplikację Django kilka razy dziennie. Jedną wadą naszego rozwiązania, którą zauważyliśmy, jest to, że deployment wyzwala Zrównoważenie przypisań partycji w Kafce. Pomimo zastosowania innej grupy konsumentów na temat w celu ograniczenia zakresu równoważenia, wdrożenia nadal powodowały chwilowe spowolnienie przetwarzania wiadomości, ponieważ zużycie zadań musiało się zatrzymać podczas równoważenia. Spowolnienie może być dopuszczalne w większości przypadków, gdy wykonujemy planowane wydania, ale może być katastrofalne, gdy, na przykład, robimy wydanie awaryjne, aby naprawić błąd. Konsekwencją byłoby wprowadzenie spowolnienia przetwarzania kaskadowego.
nowsze wersje Kafki i klientów wspierają przyrostowe przywracanie równowagi kooperacyjnej, co znacznie zmniejszyłoby operacyjny wpływ równoważenia. Modernizacja naszych klientów w celu wsparcia tego rodzaju równoważenia byłaby naszym rozwiązaniem z wyboru w przyszłości. Niestety, przyrostowe równoważenie kooperacyjne nie jest jeszcze obsługiwane w naszym wybranym kliencie Kafka.
kluczowe zwycięstwa
Po zakończeniu tego projektu zrealizowaliśmy znaczące ulepszenia w zakresie czasu pracy, skalowalności, obserwowalności i decentralizacji. Te zwycięstwa były kluczowe dla zapewnienia dalszego rozwoju naszej firmy.
koniec z powtarzającymi się awariami
zatrzymaliśmy powtarzające się awarie prawie jak tylko zaczęliśmy wdrażać to niestandardowe podejście Kafka. Awarie powodowały bardzo słabe wrażenia użytkownika.
- implementując tylko mały podzbiór najczęściej używanych funkcji selera w naszym MVP byliśmy w stanie wysłać kod roboczy do produkcji w ciągu dwóch tygodni.
- dzięki wdrożeniu MVP byliśmy w stanie znacznie zmniejszyć obciążenie RabbitMQ i selery, ponieważ nadal hartowaliśmy nasze rozwiązanie i wdrażaliśmy nowe funkcje.
przetwarzanie zadań nie było już czynnikiem ograniczającym wzrost
mając Kafkę w sercu naszej architektury, zbudowaliśmy system przetwarzania zadań, który jest wysoce dostępny i skalowalny poziomo, pozwalając firmie DoorDash i jej klientom na dalszy rozwój.
znacznie zwiększona obserwowalność
ponieważ było to niestandardowe rozwiązanie, byliśmy w stanie upiec więcej wskaźników na prawie każdym poziomie. Każda Kolejka, pracownik i zadanie były w pełni widoczne na bardzo szczegółowym poziomie w środowiskach produkcyjnych i programistycznych. Ta zwiększona obserwowalność była ogromną wygraną nie tylko w sensie produkcyjnym, ale także pod względem produktywności programistów.
decentralizacja operacyjna
dzięki ulepszeniom obserwowalności byliśmy w stanie templatyzować nasze alerty jako moduły Terraform i jawnie przypisywać właścicieli do każdego tematu i, w domyśle, wszystkich ponad 900 zadań.
szczegółowy przewodnik obsługi systemu przetwarzania zadań udostępnia informacje wszystkim inżynierom w celu debugowania problemów operacyjnych z ich tematami i pracownikami, a także wykonywania ogólnych operacji zarządzania klastrami Kafka, w razie potrzeby. Codzienne operacje są samodzielne, a wsparcie rzadko jest potrzebne od naszego zespołu ds. infrastruktury.
wnioski
podsumowując, osiągnęliśmy pułap naszej zdolności do skalowania RabbitMQ i musieliśmy szukać alternatyw. Alternatywą, którą wybraliśmy, było niestandardowe rozwiązanie oparte na Kafce. Chociaż korzystanie z Kafki ma pewne wady, znaleźliśmy kilka obejść opisanych powyżej.
gdy krytyczne przepływy pracy w dużym stopniu polegają na asynchronicznym przetwarzaniu zadań, zapewnienie skalowalności ma kluczowe znaczenie. W przypadku podobnych problemów, zachęcamy do skorzystania z naszej strategii, która dała nam 80% wyniku przy 20% wysiłku. Strategia ta, w ogólnym przypadku, jest podejściem taktycznym, aby szybko złagodzić problemy z niezawodnością i kupić bardzo potrzebny czas na bardziej solidne i strategiczne rozwiązanie.
podziękowania
autorzy chcieliby podziękować Clementowi Fangowi, Corry ’ emu Hainesowi, Danialowi Asifowi, Jayowi Weinsteinowi, Luigiemu Tagliamonte, Matthew Angerowi, Shaohua Zhou i Yun-Yu Chenowi za wkład w ten projekt.
fot. Tian kuan na Unsplash