Å Eliminere Oppgavebehandlingsbrudd ved Å Erstatte RabbitMQ Med Apache Kafka uten Nedetid

Skalering av backend-infrastruktur for å håndtere hypervekst er en av de mange spennende utfordringene med Å jobbe Hos DoorDash. I midten av 2019 møtte vi betydelige skaleringsutfordringer og hyppige utbrudd som involverte Selleri og RabbitMQ, to teknologier som driver systemet som håndterer det asynkrone arbeidet som muliggjør kritiske funksjoner i plattformen vår, inkludert bestillingskasse og Dasher-oppdrag. Vi løste raskt dette problemet med et enkelt, Apache Kafka-basert asynkront oppgavebehandlingssystem som stoppet strømbruddene våre mens vi fortsatte å iterere på en robust løsning. Vår første versjon implementerte det minste settet av funksjoner som trengs for å imøtekomme en stor del av eksisterende Sellerioppgaver. En gang i produksjonen fortsatte vi å legge til støtte for Flere Sellerifunksjoner mens vi adresserte nye problemer som oppstod ved Bruk Av Kafka.

problemene vi møtte ved Hjelp Av Selleri og RabbitMQ

RabbitMQ og Selleri var misjonskritiske deler av vår infrastruktur som drev over 900 forskjellige asynkrone oppgaver På DoorDash, inkludert bestill kassa, kjøpmann for overføring, Og Dasher plassering behandling. Problemet DoorDash møtte var At RabbitMQ ofte gikk ned på grunn av overdreven belastning. Hvis oppgavebehandling gikk ned, Gikk DoorDash effektivt ned og ordrer kunne ikke fullføres, noe som resulterte i tap av inntekter for våre selgere og Dashers, og en dårlig opplevelse for våre forbrukere. Vi møtte problemer på følgende fronter:

  • Tilgjengelighet: Strømbrudd forårsaket av etterspørsel redusert tilgjengelighet.
  • Skalerbarhet: RabbitMQ kunne ikke skalere med veksten av vår virksomhet.
  • Observerbarhet: RabbitMQ tilbød begrensede beregninger og Selleriarbeidere var ugjennomsiktige.
  • Driftseffektivitet: Omstart av disse komponentene var en tidkrevende, manuell prosess.

Hvorfor vårt asynkrone oppgavebehandlingssystem ikke var svært tilgjengelig

Dette største problemet vi møtte var strømbrudd, og de kom ofte når etterspørselen var på topp. RabbitMQ ville gå ned på grunn av belastning, overdreven tilkobling churn, og andre grunner. Ordrer ville bli stoppet, og vi måtte starte systemet på nytt eller noen ganger til og med få opp en helt ny megler og manuelt failover for å gjenopprette fra strømbrudd.

På dykking dypere inn i tilgjengelighet problemer, fant vi følgende sub-problemer:

  • Selleri tillater brukere å planlegge oppgaver i fremtiden med en nedtelling eller ETA. Vår tunge bruk av disse nedtellingene resulterte i merkbare belastningsøkninger på megleren. Noen av våre brudd var direkte relatert til en økning i oppgaver med countdowns. Vi bestemte oss til slutt for å begrense bruken av countdowns til fordel for et annet system vi hadde på plass for å planlegge arbeid i fremtiden.
  • Plutselige utbrudd av trafikk ville forlate RabbitMQ i en degradert tilstand der oppgavekonsumet var betydelig lavere enn forventet. I vår erfaring kan dette bare løses med En RabbitMQ bounce. RabbitMQ har et konsept For Flytkontroll der det vil redusere hastigheten på tilkoblinger som publiserer for fort, slik at køene kan holde tritt. Strømningskontroll var ofte, men ikke alltid, involvert i disse tilgjengelighetsnedbrytningene. Når Flytkontrollen slår inn, ser utgiverne det effektivt som nettverksforsinkelse. Nettverksforsinkelse reduserer responstidene våre; hvis ventetiden øker under topptrafikk, kan betydelige forsinkelser føre til at kaskade når forespørsler hoper seg oppstrøms.
  • våre python uWSGI webarbeidere hadde en funksjon kalt harakiri som var i stand til å drepe alle prosesser som oversteg en timeout. Under strømbrudd eller forsinkelser, harakiri resulterte i en forbindelse churn Til RabbitMQ meglere som prosesser ble gjentatte ganger drept og startet på nytt. Med tusenvis av webarbeidere som kjører til enhver tid, vil enhver langsomhet som utløste harakiri i sin tur bidra enda mer til langsomhet ved å legge ekstra belastning Til RabbitMQ.
  • i produksjon opplevde vi flere tilfeller der oppgavebehandling i Selleri forbrukerne stoppet, selv i fravær av betydelig belastning. Vår undersøkelsesinnsats ga ikke bevis på noen ressursbegrensninger som ville ha stoppet behandlingen, og arbeiderne gjenopptok behandlingen når de ble spratt. Dette problemet ble aldri forårsaket, selv om vi mistenker et problem i Selleriarbeiderne selv og ikke RabbitMQ.Totalt sett var alle disse tilgjengelighetsproblemene uakseptable for oss, da høy pålitelighet er en av våre høyeste prioriteringer. Siden disse strømbruddene kostet oss mye i form av ubesvarte ordrer og troverdighet, trengte vi en løsning som ville løse disse problemene så snart som mulig.

    hvorfor vår eldre løsning ikke skalerte

    det neste største problemet var skala. DoorDash vokser raskt, og vi nådde raskt grensene for vår eksisterende løsning. Vi trengte å finne noe som ville holde tritt med vår fortsatte vekst siden vår eldre løsning hadde følgende problemer:

    Å Treffe den vertikale skaleringsgrensen

    vi brukte den største tilgjengelige single-node RabbitMQ-løsningen som var tilgjengelig for oss. Det var ingen vei å skalere vertikalt lenger, og vi begynte allerede å presse den noden til sine grenser.på grunn av replikering reduserte PRIMÆR-sekundær Høy Tilgjengelighet (HA) – modus gjennomstrømning sammenlignet med alternativet enkeltnode, noe som ga oss enda mindre takhøyde enn bare enkeltnodeløsningen. Vi hadde ikke råd til å handle gjennomstrømning for tilgjengelighet.

    for det andre reduserte PRIMÆR-sekundær HA-modus i praksis ikke alvorlighetsgraden av våre utbrudd. Failovers tok mer enn 20 minutter å fullføre og ville ofte bli sittende fast krever manuell inngripen. Meldinger ble ofte tapt i prosessen også.

    Vi var raskt kjører ut av takhøyde Som DoorDash fortsatte å vokse og presse vår oppgave behandling til sine grenser. Vi trengte en løsning som kunne skaleres horisontalt etter hvert som prosessbehovene våre vokste.

    Hvordan Selleri og RabbitMQ tilbød begrenset observerbarhet

    Å Vite hva som skjer i et hvilket som helst system er grunnleggende for å sikre tilgjengelighet, skalerbarhet og operativ integritet.

    Som vi navigert problemene skissert ovenfor, la vi merke til at:

    • Vi var begrenset Til et lite sett Med RabbitMQ beregninger tilgjengelig for oss.
    • Vi hadde begrenset synlighet i Selleriarbeiderne selv.

    Vi trengte å kunne se sanntidsmålinger av alle aspekter av systemet vårt, noe som medførte at observerbarhetsbegrensningene også måtte tas opp.

    de operative effektivitet utfordringer

    Vi møtte også flere problemer med drift RabbitMQ:

    • Vi måtte ofte failover Vår RabbitMQ node til en ny for å løse den vedvarende nedbrytning vi observert. Denne operasjonen var manuell og tidkrevende for de involverte ingeniørene og måtte ofte gjøres sent på kvelden, utenom topptider.Det var ingen Interne Selleri-eller RabbitMQ-eksperter På DoorDash som vi kunne lene oss på for å hjelpe til med å utarbeide en skaleringsstrategi for denne teknologien.

    Engineering tid brukt drift Og vedlikehold RabbitMQ var ikke bærekraftig. Vi trengte noe som bedre møtte våre nåværende og fremtidige behov.

    Potensielle løsninger på våre problemer Med Selleri og RabbitMQ

    med problemene som er skissert ovenfor, vurderte vi følgende løsninger:

    • Bytt Selleri megler Fra RabbitMQ Til Redis eller Kafka. Dette vil tillate oss å fortsette å bruke Selleri, med en annen og potensielt mer pålitelig backing datastore.Legg multi-megler støtte Til Vår Django app slik at forbrukerne kan publisere Til n forskjellige meglere basert på hva logikk vi ønsket. Oppgavebehandling vil bli delt over flere meglere, slik at hver megler vil oppleve en brøkdel av den opprinnelige belastningen.
    • Oppgrader til nyere versjoner Av Selleri og RabbitMQ. Nyere versjoner Av Selleri Og RabbitMQ ble forventet å fikse pålitelighetsproblemer, og kjøpte oss tid da vi allerede utvunnet komponenter fra Vår Django monolith parallelt.
    • Migrere Til en tilpasset løsning støttet Av Kafka. Denne løsningen tar mer innsats enn de andre alternativene vi har oppført, men har også mer potensial til å løse alle problemer vi hadde med legacy-løsningen.

    Hvert alternativ har sine fordeler og ulemper:

    Alternativ Fordeler Ulemper
    • Forbedret tilgjengelighet med ElasticCache og multi-az støtte
    • forbedret broker observerbarhet med elasticcache som megler
    • forbedret driftseffektivitet
    • in-house operativ erfaring og kompetanse med redis
    • en broker swap er rett-foward som et støttet alternativ i selleri
    • harakiri Connection churn ikke signifikant redusere redis ytelse
    • inkompatibel Med Redis gruppert modus
    • Enkelt node Redis ikke skalere horisontalt
    • Ingen Selleri observerbarhet forbedringer
    • denne løsningen løser ikke det observerte problemet der Selleri arbeidere stoppet behandling oppgaver
    Kafka Som megler
    • Kafka kan være svært tilgjengelig
    • Kafka er horisontalt skalerbar
    • Forbedret Observerbarhet med kafka som megler
    • forbedret driftseffektivitet
    • doordash hadde in-house kafka ekspertise
    • en meglerbytte Er Straight-foward som et støttet alternativ i selleri
    • Kafka støttes ikke av Selleri ennå
    • løser ikke det observerte problemet der Selleriarbeidere slutter å behandle oppgaver
    • ingen forbedringer av selleriets observerbarhet
    • Til tross for intern erfaring, hadde Vi ikke operert Kafka i skala På DoorDash.
    Flere meglere
  • Forbedret tilgjengelighet
  • Horisontal skalerbarhet
  • ingen forbedringer av observerbarhet
  • ingen operasjonelle effektivitetsforbedringer
  • løser ikke det observerte problemet der Selleriarbeidere slutter å behandle oppgaver
  • løser ikke problemet med Harakiri-indusert tilkobling churn
  • oppgradere versjoner
    • kan forbedre problemet der rabbitmq blir sittende fast i en degradert tilstand
    • kan forbedre problemet der selleri Arbeidere sitter fast ikke garantert å fikse våre observerte bugs

    • vil ikke umiddelbart fikse våre problemer med tilgjengelighet, skalerbarhet, observerbarhet, og operativ effektivitet
    • Nyere versjoner Av RabbitMQ og Selleri kreves nyere versjoner Av Python.
    • kafka kan være svært tilgjengelig
    • Kafka er horisontalt skalerbar
    • Forbedret observerbarhet med Kakfa som megler
    • Forbedret driftseffektivitet
    • In-house Kafka ekspertise
    • en meglerendring er rett-foward
    • harakiri connection churn reduserer ikke kafka-ytelsen betydelig
    • Løser Det Observerte Problemet der selleriarbeidere slutter å behandle oppgaver
    • krever Mer til tross for in-house erfaring, hadde vi ikke operert Kafka i skala På DoorDash

    vår strategi for onboarding Kafka

    Gitt vår nødvendige system oppetid, utviklet vi vår onboarding strategi basert på følgende prinsipper for å maksimere pålitelighet fordeler På kortest mulig tid. Denne strategien involverte tre trinn:

    • Treffer bakken kjører: Vi ønsket å utnytte det grunnleggende av løsningen vi bygde som vi var iterating på andre deler av det. Vi sammenligner denne strategien med å kjøre en racerbil mens du bytter i en ny drivstoffpumpe.Designvalg for en sømløs adopsjon av utviklere: Vi ønsket å minimere bortkastet innsats fra alle utviklere som kan ha resultert fra å definere et annet grensesnitt.
    • Inkrementell utrulling med null nedetid: I stedet for en stor prangende utgivelse som ble testet i naturen for første gang med høyere sjanse for feil, fokuserte vi på å sende mindre uavhengige funksjoner som kunne testes individuelt i naturen over en lengre periode.

    Treffer bakken kjører

    Bytte Til Kafka representerte en stor teknisk endring i vår stabel, men en som var sårt trengte. Vi hadde ikke tid til å kaste bort siden hver uke vi var å miste virksomheten på grunn av ustabilitet i vår arv RabbitMQ løsning. Vår først og fremst prioritet var å skape et minimum viable product (mvp) for å gi oss midlertidig stabilitet og gi oss takhøyde som trengs for å iterere og forberede seg på en mer omfattende løsning med bredere adopsjon.VÅR MVP besto av produsenter som publiserte task Fully Qualified Names (Fqns) og syltet argumenter til Kafka mens våre forbrukere leste disse meldingene, importerte oppgavene fra FQN, og utførte dem synkront med de angitte argumentene.

    Minimal Viable Product(Mvp) arkitekturen vi bestemte oss for å bygge inkluderte en midlertidig tilstand der vi skulle publisere gjensidig eksklusive oppgaver til både arven (røde stiplede linjer) og de nye systemene (grønne faste linjer), før den endelige tilstanden der Vi ville slutte å publisere oppgaver Til RabbitMQ.1

    Figur 1: Den Minimale Viable Product(Mvp) arkitekturen vi bestemte oss for å bygge, inkluderte en midlertidig tilstand der vi skulle publisere gjensidig eksklusive oppgaver til både arven (røde stiplede linjer) og de nye systemene (grønne faste linjer), før den endelige tilstanden der Vi ville slutte å publisere oppgaver Til RabbitMQ.

    Designvalg for en sømløs adopsjon av utviklere

    noen ganger er utvikleradopsjon en større utfordring enn utvikling. Vi gjorde dette enklere ved å implementere en wrapper for Selleries @ task annotasjon som dynamisk rutet oppgaveinnleveringer til enten system basert på dynamisk konfigurerbare funksjonsflagg. Nå kan det samme grensesnittet brukes til å skrive oppgaver for begge systemene. Med disse beslutningene på plass måtte ingeniørteamene ikke gjøre noe ekstra arbeid for å integrere med det nye systemet, uten å implementere et enkelt funksjonsflagg.

    vi ønsket å rulle ut systemet vårt så snart VÅR MVP var klar, men det støttet ennå ikke alle de samme funksjonene som Selleri. Selleri lar brukerne konfigurere sine oppgaver med parametere i sin oppgave merknad eller når de sender sin oppgave. For å gjøre det mulig for oss å starte raskere, opprettet vi en hviteliste med kompatible parametere og valgte å støtte det minste antallet funksjoner som trengs for å støtte et flertall av oppgavene.

    vi raskt trappet opp oppgaven volum Til Kafka-baserte MVP, starter med lav-risiko og lav prioritet oppgaver først. Noen av disse var oppgaver som kjørte på off-peak timer, noe som forklarer toppene av metriske avbildet ovenfor.

    Figur 2: Vi økte raskt oppgavevolumet til Kafka-baserte MVP, og startet med lavrisiko og lavprioriterte oppgaver først. Noen av disse var oppgaver som kjørte på off-peak timer, noe som forklarer toppene av metriske avbildet ovenfor.Som det fremgår Av Figur 2, med de to beslutningene ovenfor, lanserte VI VÅR MVP etter to uker med utvikling og oppnådde en 80% reduksjon I RabbitMQ-oppgavebelastning en uke etter lanseringen. Vi behandlet vårt primære problem med strømbrudd raskt, og i løpet av prosjektet støttet flere og flere esoteriske funksjoner for å muliggjøre utførelse av de gjenværende oppgavene.

    Inkrementell utrulling, null nedetid

    muligheten til å bytte Kafka-klynger og bytte mellom RabbitMQ og Kafka dynamisk uten forretningsmessig påvirkning var ekstremt viktig for oss. Denne evnen har også hjulpet oss i en rekke operasjoner som cluster vedlikehold, last shedding, og gradvise migreringer. For å implementere denne utrullingen benyttet vi dynamiske funksjonsflagg både på meldingsinnleveringsnivå og på meldingsforbrukssiden. Kostnaden for å være fullt dynamisk her var å holde vår arbeiderflåte kjører på dobbel kapasitet. Halvparten av denne flåten var viet Til RabbitMQ, og resten Til Kafka. Å kjøre arbeidsflåten på dobbel kapasitet var definitivt beskatning på infrastrukturen vår. På et tidspunkt spunnet vi til og med opp En Helt ny Kubernetes-klynge bare for å huse alle våre arbeidere.

    i den innledende utviklingsfasen tjente denne fleksibiliteten oss godt. Når vi hadde mer tillit til vårt nye system, så vi på måter å redusere belastningen på infrastrukturen vår, for eksempel å kjøre flere forbrukerprosesser per maskin. Som vi overført ulike emner over, vi var i stand til å begynne å redusere arbeideren teller For RabbitMQ samtidig opprettholde en liten reservekapasitet.

    Ingen løsning er perfekt, iterere etter behov

    med VÅR MVP i produksjon, hadde vi takhøyde for å iterere på og polere vårt produkt. Vi rangerte hver Manglende Sellerifunksjon etter antall oppgaver som brukte Den til å hjelpe oss med å bestemme hvilke som skulle implementeres først. Funksjoner brukt av bare noen få oppgaver ble ikke implementert i vår tilpassede løsning. I stedet skrev vi om disse oppgavene for ikke å bruke den spesifikke funksjonen. Med denne strategien flyttet vi til slutt alle oppgaver av Selleri.

    Ved Hjelp Av Kafka introduserte Også nye problemer som trengte vår oppmerksomhet:

    • Head-of-the-line blokkering som resulterte i oppgavebehandling forsinkelser
    • Distribusjoner utløst partisjon rebalansering som også resulterte i forsinkelser

    Kafka ‘ s head-of-the-line blokkering problem

    Kafka emner er partisjonert slik at en enkelt forbruker (per forbrukergruppe) leser meldinger for sine tildelte partisjoner i den rekkefølgen de ankom. Hvis en melding i en enkelt partisjon tar for lang tid å bli behandlet, vil den stoppe forbruket av alle meldinger bak den i den partisjonen, som vist i Figur 3 nedenfor. Dette problemet kan være spesielt katastrofalt i tilfelle et høyt prioritert emne. Vi ønsker å kunne fortsette å behandle meldinger i en partisjon i tilfelle en forsinkelse skjer.

    I Kafkas blokkeringsproblem blokkerer en langsom melding i en partisjon (i rødt) alle meldinger bak den fra å bli behandlet. Andre partisjoner vil fortsette å behandle som forventet.

    Figur 3: I Kafka ‘ s head-of-the-line blokkeringsproblem blokkerer en sakte melding i en partisjon (i rødt) alle meldinger bak den fra å bli behandlet. Andre partisjoner vil fortsette å behandle som forventet.

    mens parallellisme er fundamentalt Et Python-problem, gjelder konseptene for denne løsningen også for andre språk. Vår løsning, avbildet i Figur 4, under, var å huse En Kafka-forbrukerprosess og flere oppgaveprosesser per arbeidstaker. Kafka-forbrukerprosessen er ansvarlig for å hente meldinger Fra Kafka, og plassere dem på en lokal kø som leses av oppgaveutføringsprosessene. Den fortsetter å forbruke til den lokale køen treffer en brukerdefinert terskel. Denne løsningen tillater meldinger i partisjonen å flyte, og bare en oppgaveutføringsprosess vil bli stoppet av den langsomme meldingen. Terskelen begrenser også antall meldinger om bord i den lokale køen (som kan gå seg vill i tilfelle systemkrasj).

    Figur 4: Vår Ikke-blokkerende Kafka-Arbeider består av en lokal meldingskø og to typer prosesser: en kafka-forbrukerprosess og flere oppgaveeksekutorprosesser. Mens en kafka-forbruker kan lese fra flere partisjoner, for enkelhet vil vi skildre bare en. Dette diagrammet viser at en sakte prosesseringsmelding (i rødt) bare blokkerer en enkelt oppgave-eksekutor til den er fullført, mens andre meldinger bak den i partisjonen fortsetter å bli behandlet av andre oppgaveutøvere.Figur 4: Vår Ikke-blokkerende Kafka-Arbeider består av en lokal meldingskø og to typer prosesser: en kafka-forbruker prosess og flere oppgave-eksekutor prosesser. Mens en kafka-forbruker kan lese fra flere partisjoner, for enkelhet vil vi skildre bare en. Dette diagrammet viser at en sakte prosesseringsmelding (i rødt) bare blokkerer en enkelt oppgave-eksekutor til den er fullført, mens andre meldinger bak den i partisjonen fortsetter å bli behandlet av andre oppgaveutøvere.

    disruptiveness av deploys

    vi distribuerer Vår Django app flere ganger om dagen. En ulempe med vår løsning som vi la merke til er at en distribusjon utløser en rebalansering av partisjonsoppdrag i Kafka. Til tross for å bruke en annen forbrukergruppe per emne for å begrense rebalanseringsomfanget, forårsaket distribusjoner fortsatt en kortvarig nedgang i meldingsbehandling da aktivitetsforbruket måtte stoppe under rebalansering. Forsinkelser kan være akseptabelt i de fleste tilfeller når vi utfører planlagte utgivelser, men kan være katastrofale når vi for eksempel gjør en nødutgivelse for å hurtigreparere en feil. Konsekvensen vil være innføringen av en cascading behandling nedgang. Nyere versjoner Av Kafka og klienter støtter inkrementell samvirkende rebalansering, noe som vil redusere den operasjonelle effekten av en rebalansering. Oppgradering av våre kunder for å støtte denne typen rebalansering vil være vår valgløsning fremover. Dessverre støttes ikke inkrementell samarbeidsrebalansering i Vår Valgte Kafka-klient ennå.

    Key wins

    med avslutningen av dette prosjektet innså vi betydelige forbedringer når det gjelder oppetid, skalerbarhet, observerbarhet og desentralisering. Disse gevinstene var avgjørende for å sikre fortsatt vekst i vår virksomhet.

    Ingen flere gjentatte brudd

    vi stoppet gjentatte brudd nesten så snart vi begynte å rulle ut denne egendefinerte Kafka-tilnærmingen. Avbrudd resulterte i ekstremt dårlige brukeropplevelser.

    • Ved å implementere bare en liten delmengde av De mest brukte Sellerifunksjonene i VÅR Mvp, kunne vi sende arbeidskode til produksjon om to uker.
    • Med MVP på plass var vi i stand til å redusere belastningen på RabbitMQ og Selleri som vi fortsatte å herde vår løsning og implementere nye funksjoner.

    Oppgavebehandling var ikke lenger den begrensende faktoren for vekst

    Med Kafka i hjertet av vår arkitektur, bygde vi et oppgavebehandlingssystem som er svært tilgjengelig og horisontalt skalerbart, slik At DoorDash og kundene kan fortsette sin vekst.

    Massivt utvidet observerbarhet

    Siden dette var en tilpasset løsning, kunne vi bake i flere beregninger på nesten alle nivåer. Hver kø, arbeidstaker og oppgave var fullt observerbar på et meget granulært nivå i produksjons-og utviklingsmiljøer. Denne økte observerbarheten var en stor seier, ikke bare i produksjonssans, men også når det gjelder utviklerproduktivitet.

    operativ desentralisering

    med observerbarhetsforbedringene kunne vi templat våre varsler som Terraform-moduler og eksplisitt tildele eiere til hvert enkelt emne og implisitt alle 900-pluss oppgaver.

    en detaljert driftsveiledning for oppgavebehandlingssystemet gjør informasjon tilgjengelig for alle ingeniører til å feilsøke operasjonelle problemer med sine emner og arbeidere, samt utføre generelle Kafka cluster-management operasjoner, etter behov. Den daglige driften er selvbetjent, og det er sjelden Behov for støtte fra Infrastrukturteamet vårt.

    Konklusjon

    for å oppsummere, vi traff taket av vår evne Til å skalere RabbitMQ og måtte se etter alternativer. Alternativet vi gikk med var en tilpasset Kafka – basert løsning. Mens det er noen ulemper med Å bruke Kafka, fant vi en rekke løsninger, beskrevet ovenfor.

    når kritiske arbeidsflyter er avhengige av asynkron oppgavebehandling, er det viktig å sikre skalerbarhet. Når du opplever lignende problemer, vær så snill å ta inspirasjon fra vår strategi, som ga oss 80% av resultatet med 20% av innsatsen. Denne strategien, i det generelle tilfellet, er en taktisk tilnærming til raskt å redusere pålitelighetsproblemer og kjøpe sårt trengte tid for en mer robust og strategisk løsning.

    Takk

    forfatterne vil gjerne takke Clement Fang, Corry Haines, Danial Asif, Jay Weinstein, Luigi Tagliamonte, Matthew Anger, Shaohua Zhou og Yun-Yu Chen for å ha bidratt til dette prosjektet.

    Foto av tian kuan på Unsplash

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert.