het schalen van de backend-infrastructuur om hyper-groei aan te kunnen is een van de vele spannende uitdagingen van het werken bij DoorDash. Medio 2019 werden we geconfronteerd met aanzienlijke schaaluitdagingen en frequente uitval met selderij en RabbitMQ, twee technologieën die het systeem aandrijven dat asynchrone werkzaamheden uitvoert en kritieke functionaliteiten van ons platform mogelijk maakt, waaronder order checkout en Dasher-opdrachten.
we losten dit probleem snel op met een eenvoudig, op Apache Kafka gebaseerd asynchrone taakverwerkingssysteem dat onze uitval stopte terwijl we bleven herhalen op een robuuste oplossing. Onze eerste versie geïmplementeerd de kleinste set van functies die nodig zijn om een groot deel van de bestaande selderij taken tegemoet te komen. Eenmaal in productie, we bleven ondersteuning toevoegen voor meer selderij functies, terwijl het aanpakken van nieuwe problemen die zijn ontstaan bij het gebruik van Kafka.
de problemen die we ondervonden met behulp van selderij en RabbitMQ
RabbitMQ en selderij waren bedrijfskritische onderdelen van onze infrastructuur die meer dan 900 verschillende asynchrone taken op DoorDash aangedreven, waaronder order checkout, merchant order transmission en Dasher location processing. Het probleem waar DoorDash mee te maken kreeg was dat RabbitMQ vaak naar beneden ging vanwege overmatige belasting. Als de verwerking van taken naar beneden ging, ging DoorDash effectief naar beneden en bestellingen konden niet worden voltooid, wat resulteerde in inkomstenverlies voor onze handelaren en Dashers, en een slechte ervaring voor onze consumenten. We werden geconfronteerd met problemen op de volgende fronten:
- beschikbaarheid: uitval veroorzaakt door de vraag verminderde beschikbaarheid.
- schaalbaarheid: RabbitMQ kon niet opschalen met de groei van ons bedrijf.
- waarneembaarheid: RabbitMQ bood beperkte statistieken en Selderiewerkers waren ondoorzichtig.
- operationele efficiëntie: het opnieuw opstarten van deze componenten was een tijdrovend, handmatig proces.
waarom ons asynchrone taakverwerkingssysteem niet erg beschikbaar was
Dit grootste probleem waar we mee te maken hadden waren uitval, en ze kwamen vaak wanneer de vraag op zijn hoogtepunt was. RabbitMQ zou naar beneden gaan als gevolg van belasting, overmatige verbinding churn, en andere redenen. Orders zouden worden gestopt, en we zouden moeten ons systeem opnieuw op te starten of soms zelfs brengen een geheel nieuwe makelaar en handmatig failover om te herstellen van de uitval.
om dieper in te gaan op de beschikbaarheidsproblemen, vonden we de volgende sub-issues:
- Selery stelt gebruikers in staat om taken in de toekomst te plannen met een countdown of ETA. Ons zware gebruik van deze countdowns resulteerde in merkbare toename van de belasting op de makelaar. Sommige van onze storingen waren direct gerelateerd aan een toename van taken met countdowns. Uiteindelijk besloten we om het gebruik van countdowns te beperken ten gunste van een ander systeem dat we hadden voor het plannen van werk in de toekomst.
- plotselinge verkeersuitbarstingen zouden RabbitMQ in een gedegradeerde toestand brengen waar het taakverbruik aanzienlijk lager was dan verwacht. In onze ervaring, kon dit alleen worden opgelost met een RabbitMQ bounce. RabbitMQ heeft een concept van Flow Control waar het de snelheid van verbindingen die te Snel publiceren zal verminderen, zodat wachtrijen kunnen bijhouden. Flow Control was vaak, maar niet altijd, betrokken bij deze beschikbaarheidsdegradaties. Wanneer Flow Control begint, zien de uitgevers het effectief als netwerk latency. Netwerk latency verkort onze responstijden; als de latency toeneemt tijdens piekverkeer, kunnen significante vertragingen resulteren in cascade als aanvragen zich stroomopwaarts opstapelen.
- onze python uwsgi web workers hadden een functie genaamd harakiri die was ingeschakeld om processen te doden die een time-out overschreden. Tijdens uitval of vertragingen, harakiri resulteerde in een verbinding churn naar de RabbitMQ makelaars als processen werden herhaaldelijk gedood en herstart. Met duizenden webwerkers die op een bepaald moment draaien, zou elke traagheid die harakiri veroorzaakte op zijn beurt nog meer bijdragen aan traagheid door extra belasting toe te voegen aan RabbitMQ.
- tijdens de productie hebben we verschillende gevallen meegemaakt waarin de verwerking van taken bij de Selderijverbruikers stopte, zelfs bij afwezigheid van een aanzienlijke belasting. Ons onderzoek leverde geen bewijs op van enige resource beperkingen die de verwerking zouden hebben gestopt, en de werknemers hervatten de verwerking zodra ze werden afgewezen. Dit probleem is nooit wortel veroorzaakt, hoewel we vermoeden een probleem in de selderij arbeiders zelf en niet RabbitMQ.
over het algemeen waren al deze beschikbaarheidskwesties onaanvaardbaar voor ons, aangezien hoge betrouwbaarheid een van onze hoogste prioriteiten is. Omdat deze uitval ons veel kostte in termen van gemiste orders en geloofwaardigheid hadden we een oplossing nodig die deze problemen zo snel mogelijk zou aanpakken.
waarom onze oudere oplossing niet schaalde
het volgende grootste probleem was schalen. DoorDash groeit snel en we bereikten snel de grenzen van onze bestaande oplossing. We moesten iets vinden dat gelijke tred zou houden met onze voortdurende groei, omdat onze legacy oplossing de volgende problemen had:
het raken van de verticale schaling limiet
We gebruikten de grootste beschikbare single-node RabbitMQ oplossing die voor ons beschikbaar was. Er was geen pad om verticaal verder te schalen en we begonnen dat knooppunt al tot zijn grenzen te duwen.
De High Availability-modus beperkte onze capaciteit
door replicatie verminderde de primaire-secundaire High Availability (HA) – modus de doorvoer in vergelijking met de single node-optie, waardoor we nog minder ruimte hebben dan alleen de single node-oplossing. We konden ons niet veroorloven om de handel doorvoer voor beschikbaarheid.
ten tweede verminderde de primaire-secundaire HA-modus in de praktijk de ernst van onze uitval niet. Failovers duurde meer dan 20 minuten om te voltooien en zou vaak vast komen te zitten waarvoor handmatige interventie. Berichten werden vaak verloren in het proces ook.
We hadden al snel geen ruimte meer omdat DoorDash bleef groeien en onze taakverwerking tot zijn grenzen pushte. We hadden een oplossing nodig die horizontaal kon schalen naarmate onze verwerkingsbehoeften groter werden.
hoe selderij en RabbitMQ beperkte waarneembaarheid boden
weten wat er gaande is in elk systeem is fundamenteel voor het waarborgen van de beschikbaarheid, schaalbaarheid en operationele integriteit.
terwijl we door de hierboven geschetste problemen navigeerden, merkten we dat:
- We beperkt waren tot een kleine set RabbitMQ metrics die voor ons beschikbaar waren.
- we hadden beperkte zichtbaarheid in de Selderijwerkers zelf.
we moesten in staat zijn om real-time metrics van elk aspect van ons systeem te zien, wat betekende dat de observability beperkingen ook moesten worden aangepakt.
de operationele efficiëntieuitdagingen
we werden ook geconfronteerd met verschillende problemen met operating RabbitMQ:
- we moesten vaak onze RabbitMQ-knoop failover naar een nieuwe om de aanhoudende degradatie die we zagen op te lossen. Deze operatie was handmatig en tijdrovend voor de betrokken ingenieurs en moest vaak ‘ s avonds laat, buiten de piekuren, worden uitgevoerd.er waren geen interne selderij-of RabbitMQ-experts bij DoorDash op wie we konden steunen om een schaalingsstrategie voor deze technologie te ontwikkelen.
Technische tijd besteed aan het werken en onderhouden van RabbitMQ was niet duurzaam. We hadden iets nodig dat beter voldeed aan onze huidige en toekomstige behoeften.
mogelijke oplossingen voor onze problemen met selderij en RabbitMQ
met de hierboven beschreven problemen hebben we de volgende oplossingen overwogen:
- Verander de selderiebroker van RabbitMQ in Redis of Kafka. Dit zou ons in staat stellen om verder te gaan met behulp van selderij, met een andere en potentieel meer betrouwbare backing datastore.
- voeg multi-broker ondersteuning toe aan onze Django app, zodat consumenten kunnen publiceren naar N verschillende brokers op basis van welke logica we wilden. Taakverwerking krijgt verdeeld over meerdere makelaars, zodat elke makelaar een fractie van de initiële belasting zal ervaren.
- Upgrade naar nieuwere versies van Selery en RabbitMQ. Nieuwere versies van selderij en RabbitMQ werden verwacht om betrouwbaarheid problemen op te lossen, het kopen van ons tijd als we waren al het extraheren van componenten uit onze Django monoliet in parallel.
- migreren naar een aangepaste oplossing ondersteund door Kafka. Deze oplossing kost meer moeite dan de andere opties die we hebben opgesomd, maar heeft ook meer potentieel om elk probleem dat we hadden met de legacy-oplossing op te lossen.
elke optie heeft zijn voor – en nadelen:
Optie | Pluspunten | Nadelen |
Redis als makelaar |
|
|
Kafka als makelaar |
|
|
Meerdere makelaars |
|
|
Upgrade versies |
|
|
Aangepaste Kafka oplossing |
|
|
onze strategie voor onboarding Kafka
gezien onze vereiste uptime van het systeem hebben we onze onboardingstrategie gebaseerd op de volgende principes om de betrouwbaarheid voordelen in de kortst mogelijke tijd te maximaliseren. Deze strategie omvatte drie stappen:
- op de grond raken: We wilden gebruik maken van de basisprincipes van de oplossing die we aan het bouwen waren terwijl we aan het herhalen waren op andere delen ervan. We vergelijken deze strategie met het besturen van een raceauto tijdens het ruilen in een nieuwe brandstofpomp.
- ontwerpkeuzes voor een naadloze adoptie door ontwikkelaars: we wilden verspilde moeite van alle ontwikkelaars die mogelijk het gevolg zijn geweest van het definiëren van een andere interface tot een minimum beperken.
- incrementele uitrol met nul downtime: In plaats van een grote flitsende release die voor het eerst in het wild wordt getest met een hogere kans op storingen, hebben we ons gericht op het verzenden van kleinere onafhankelijke functies die gedurende een langere periode individueel in het wild kunnen worden getest.
de grond raken door
over te schakelen naar Kafka betekende een belangrijke technische verandering in onze stack, maar een die hard nodig was. We hadden geen tijd te verliezen omdat we elke week zaken verloren als gevolg van de instabiliteit van onze erfenis RabbitMQ oplossing. Onze eerste en belangrijkste prioriteit was het creëren van een minimaal levensvatbaar product (MVP) om ons tussentijdse stabiliteit te brengen en ons de ruimte te geven die nodig is om te herhalen en voor te bereiden op een meer uitgebreide oplossing met bredere acceptatie.
onze MVP bestond uit producenten die task Fully Qualified Names (Fqn ‘ s) publiceerden en argumenten naar Kafka inlegden, terwijl onze consumenten die berichten lazen, de taken importeerden uit de fqn en ze synchroon uitvoerden met de opgegeven argumenten.
figuur 1: De Minimal Viable Product (MVP) architectuur die we besloten te bouwen omvatte een interim-staat waar we zouden publiceren wederzijds exclusieve taken aan zowel de legacy (red stashed lines) en de nieuwe systemen (green solid lines), voordat de uiteindelijke staat waar we zouden stoppen met het publiceren van taken aan RabbitMQ.
ontwerpkeuzes voor een naadloze adoptie door ontwikkelaars
soms is de adoptie van ontwikkelaars een grotere uitdaging dan ontwikkeling. We maakten dit gemakkelijker door het implementeren van een wrapper voor selderij ‘ s @task annotatie die dynamisch gerouteerd taak inzendingen naar beide systemen op basis van dynamisch configureerbare functie vlaggen. Nu kan dezelfde interface worden gebruikt om taken voor beide systemen te schrijven. Met deze beslissingen in de plaats, engineering teams moesten geen extra werk te doen om te integreren met het nieuwe systeem, met uitzondering van het implementeren van een enkele functie vlag.
we wilden ons systeem uitrollen zodra onze MVP klaar was, maar het ondersteunde nog niet dezelfde functies als selderij. Selderij stelt gebruikers in staat om hun taken te configureren met parameters in hun taakannotatie of wanneer ze hun taak indienen. Om ons in staat te stellen om sneller te starten, hebben we een witte lijst van compatibele parameters gemaakt en ervoor gekozen om het kleinste aantal functies te ondersteunen die nodig zijn om een meerderheid van taken te ondersteunen.
Figuur 2: We hebben snel het taakvolume verhoogd naar de op Kafka gebaseerde MVP, te beginnen met taken met een laag risico en een lage prioriteit. Sommige van deze taken werden uitgevoerd tijdens de daluren, wat de pieken van de hierboven afgebeelde metriek verklaart.
zoals te zien is in Figuur 2, lanceerden we met de twee bovenstaande beslissingen onze MVP na twee weken van ontwikkeling en bereikten we een vermindering van 80% in RabbitMQ taaklast nog een week na de lancering. We hebben ons primaire probleem van uitval snel aangepakt en in de loop van het project ondersteunden we steeds meer esoterische functies om de uitvoering van de resterende taken mogelijk te maken.
incrementele uitrol, nul downtime
de mogelijkheid om dynamisch van Kafka-clusters te wisselen en te schakelen tussen RabbitMQ en Kafka zonder bedrijfsimpact was uiterst belangrijk voor ons. Dit vermogen heeft ons ook geholpen bij een verscheidenheid aan bewerkingen, zoals clusteronderhoud, load shedding en geleidelijke migraties. Om deze uitrol te implementeren, hebben we dynamische functievlaggen gebruikt, zowel op het niveau van het indienen van berichten als aan de kant van het berichtverbruik. De kosten om hier volledig dynamisch te zijn, waren om onze werknemersvloot op dubbele capaciteit te houden. De helft van deze vloot was gewijd aan RabbitMQ en de rest aan Kafka. Het runnen van de arbeidersvloot met dubbele capaciteit was zeker belastend op onze infrastructuur. Op een gegeven moment hebben we zelfs een compleet nieuwe Kubernetes cluster opgezet om al onze werknemers te huisvesten.
in de beginfase van de ontwikkeling heeft deze flexibiliteit ons goed gediend. Zodra we meer vertrouwen hadden in ons nieuwe systeem, keken we naar manieren om de belasting van onze infrastructuur te verminderen, zoals het uitvoeren van meerdere verbruikende processen per machine. Terwijl we verschillende onderwerpen overschakelden, konden we beginnen met het verminderen van het aantal werknemers voor RabbitMQ met behoud van een kleine reservecapaciteit.
geen oplossing is perfect, naar behoefte herhalen
met onze MVP in productie, hadden we de ruimte die nodig is om ons product te herhalen en te polijsten. We gerangschikt elke ontbrekende selderij functie door het aantal taken dat het gebruikt om ons te helpen beslissen welke te implementeren eerste. Functies die slechts door een paar taken worden gebruikt, zijn Niet geïmplementeerd in onze aangepaste oplossing. In plaats daarvan hebben we deze taken herschreven om die specifieke functie niet te gebruiken. Met deze strategie hebben we uiteindelijk alle taken van selderij verwijderd.
het gebruik van Kafka introduceerde ook nieuwe problemen die onze aandacht nodig hadden:
- Head-of-the-line blocking wat resulteerde in vertragingen bij het verwerken van taken
- implementaties triggered partition rebalancing wat ook resulteerde in vertragingen
Kafka ‘ s head-of-the-line blocking probleem
Kafka onderwerpen worden zo gepartitioneerd dat een enkele consument (per consumentengroep) berichten leest voor de toegewezen partities in de volgorde waarin ze aangekomen. Als een bericht in een enkele partitie te lang duurt om te worden verwerkt, zal het verbruik van alle berichten achter het in die partitie vertragen, zoals te zien in Figuur 3, hieronder. Dit probleem kan desastreus zijn in het geval van een onderwerp met hoge prioriteit. We willen berichten in een partitie kunnen blijven verwerken in het geval dat er vertraging optreedt.
Figuur 3: In Kafka ‘ s head-of-the-line blokkering probleem, een traag bericht in een partitie (in rood) blokkeert alle berichten achter het krijgen verwerkt. Andere partities zouden blijven verwerken zoals verwacht.
hoewel parallellisme fundamenteel een Pythonprobleem is, zijn de concepten van deze oplossing ook van toepassing op andere talen. Onze oplossing, afgebeeld in Figuur 4, hieronder, was om één Kafka-consumentenproces en meerdere taakuitvoeringsprocessen per werknemer te huisvesten. Het Kafka-consumer proces is verantwoordelijk voor het ophalen van berichten van Kafka, en ze te plaatsen op een lokale wachtrij die wordt gelezen door de taak-uitvoering processen. Het blijft consumeren totdat de lokale wachtrij een door de gebruiker gedefinieerde drempel bereikt. Deze oplossing maakt het mogelijk berichten in de partitie te stromen en slechts één taak-uitvoering proces zal worden geblokkeerd door de trage bericht. De drempel beperkt ook het aantal in-flight berichten in de lokale wachtrij (die verloren kunnen gaan in het geval van een systeemcrash).
Figuur 4: Onze niet-blokkerende Kafka-werknemer bestaat uit een lokale berichtenwachtrij en twee soorten processen: een Kafka-consumer proces en meerdere taak-uitvoerder processen. Terwijl een kafka-consument kan lezen van meerdere partities, voor de eenvoud zullen we afbeelden slechts een. Dit diagram laat zien dat een langzaam verwerkend bericht (in rood) slechts een enkele taak-uitvoerder blokkeert totdat het is voltooid, terwijl andere berichten achter het in de partitie nog steeds worden verwerkt door andere taak-uitvoerders.
de disruptiviteit van implementaties
We implementeren onze Django app meerdere keren per dag. Een nadeel van onze oplossing die we merkten is dat een implementatie leidt tot een herbalance van partitietoewijzingen in Kafka. Ondanks het gebruik van een andere consumentengroep per onderwerp om de rebalance scope te beperken, veroorzaakten implementaties nog steeds een tijdelijke vertraging in de berichtverwerking omdat het taakverbruik moest stoppen tijdens het rebalanceren. Vertragingen kunnen in de meeste gevallen aanvaardbaar zijn wanneer we geplande releases uitvoeren, maar kunnen catastrofaal zijn wanneer we bijvoorbeeld een nooduitgave doen om een bug te hotfixen. Het gevolg zou de invoering van een trapsgewijze vertraging van de verwerking zijn.
nieuwere versies van Kafka en klanten ondersteunen incrementele coöperatieve herbalancering, wat de operationele impact van een herbalancering aanzienlijk zou verminderen. Het upgraden van onze klanten om dit soort rebalancing te ondersteunen zou onze oplossing van keuze zijn in de toekomst. Helaas wordt incrementele coöperatieve rebalancing nog niet ondersteund in onze gekozen Kafka klant.
Key wins
met de afronding van dit project realiseerden we significante verbeteringen in termen van uptime, schaalbaarheid, observeerbaarheid en decentralisatie. Deze overwinningen waren cruciaal om de verdere groei van ons bedrijf te waarborgen.
No More herhaalde uitval
We stopten de herhaalde uitval bijna zodra we begonnen met het uitrollen van deze aangepaste Kafka aanpak. Uitval resulteerde in extreem slechte gebruikerservaringen.
- door slechts een kleine subset van de meest gebruikte selderij functies in onze MVP te implementeren, konden we werkcode in twee weken naar productie verzenden.
- met de MVP konden we de belasting op RabbitMQ en selderij aanzienlijk verminderen terwijl we onze oplossing bleven uitharden en nieuwe functies implementeerden.
Taakverwerking was niet langer de beperkende factor voor groei
met Kafka in het hart van onze architectuur, hebben we een taakverwerkingssysteem gebouwd dat zeer beschikbaar en horizontaal schaalbaar is, waardoor DoorDash en haar klanten hun groei kunnen voortzetten.
Massively augmented observability
omdat dit een aangepaste oplossing was, konden we op bijna elk niveau meer metrics bakken. Elke wachtrij, werknemer en taak was volledig waarneembaar op een zeer gedetailleerd niveau in productie-en ontwikkelingsomgevingen. Deze verhoogde waarneembaarheid was een enorme overwinning, niet alleen in productie-zin, maar ook in termen van ontwikkelaar productiviteit.
operationele decentralisatie
met de observability verbeteringen konden we onze waarschuwingen templatiseren als Terraform modules en expliciet eigenaren toewijzen aan elk onderwerp en impliciet alle 900-plus taken.
een gedetailleerde handleiding voor het taakverwerkingssysteem maakt informatie toegankelijk voor alle engineers om operationele problemen met hun onderwerpen en werknemers te debuggen en om, indien nodig, Algemene Kafka clusterbeheeroperaties uit te voeren. Dagelijkse activiteiten zijn zelfbediening en ondersteuning is zelden nodig van ons Infrastructuurteam.
conclusie
om samen te vatten, we raakten het plafond van ons vermogen om RabbitMQ te schalen en moesten zoeken naar alternatieven. Het alternatief was een op Kafka gebaseerde oplossing op maat. Hoewel er een aantal nadelen aan het gebruik van Kafka, vonden we een aantal oplossingen, hierboven beschreven.
wanneer kritieke workflows sterk afhankelijk zijn van asynchrone taakverwerking, is het waarborgen van schaalbaarheid van het grootste belang. Bij soortgelijke problemen, voel je vrij om inspiratie te halen uit onze strategie, die ons 80% van het resultaat met 20% van de inspanning. Deze strategie is in het algemeen een tactische aanpak om snel betrouwbaarheidsproblemen te verminderen en de broodnodige tijd te winnen voor een robuustere en strategische oplossing.de auteurs danken Clement Fang, Corry Haines, Danial Asif, Jay Weinstein, Luigi Tagliamonte, Matthew Anger, Shaohua Zhou en Yun-Yu Chen voor hun bijdrage aan dit project.
Foto van tian kuan op Unsplash