Ibland är mer mindre. När du minskar belastningen ökar latensen

Som i de flesta inlägg, det finns ett problem med en distribuerad tjänst, låt oss kalla denna tjänst för Alvin. Den här gången upptäckte jag inte problemet själv, berättade killarna från kundsidan.

En dag vaknade jag av ett missnöjt mejl på grund av långa förseningar med Alvin, som vi planerade att lansera inom en snar framtid. Specifikt upplevde klienten 99:e percentilens latens i området 50 ms, långt över vår latensbudget. Detta var överraskande eftersom jag testade tjänsten mycket, särskilt på latens, vilket är ett vanligt klagomål.

Innan jag testade Alvin, körde jag många experiment med 40 10 frågor per sekund (QPS), alla visade en latens på mindre än 40 ms. Jag var redo att förklara att jag inte höll med om deras resultat. Men när jag tittade en gång till på brevet, märkte jag något nytt: jag hade inte precis testat villkoren de nämnde, deras QPS var mycket lägre än mitt. Jag testade på 1k QPS, men de bara vid XNUMXk. Jag körde ett annat experiment, den här gången med lägre QPS, bara för att blidka dem.

Eftersom jag bloggar om detta har du säkert redan kommit på att deras siffror stämde. Jag testade min virtuella klient om och om igen, med samma resultat: ett lågt antal förfrågningar ökar inte bara latensen, utan ökar också antalet förfrågningar med en latens på mer än 10 ms. Med andra ord, om vid 40k QPS cirka 50 förfrågningar per sekund översteg 50 ms, så var det vid 1k QPS 100 förfrågningar över 50 ms varje sekund. Paradox!

Ibland är mer mindre. När du minskar belastningen ökar latensen

Begränsar sökningen

När man står inför ett latensproblem i ett distribuerat system med många komponenter är det första steget att skapa en kort lista över misstänkta. Låt oss gräva lite djupare i Alvins arkitektur:

Ibland är mer mindre. När du minskar belastningen ökar latensen

En bra utgångspunkt är en lista över genomförda I/O-övergångar (nätverkssamtal/diskuppslagningar, etc.). Låt oss försöka ta reda på var förseningen är. Förutom den uppenbara I/O med klienten tar Alvin ett extra steg: han kommer åt datalagret. Den här lagringen fungerar dock i samma kluster som Alvin, så latensen där bör vara mindre än hos klienten. Så, listan över misstänkta:

  1. Nätverkssamtal från klient till Alvin.
  2. Nätverkssamtal från Alvin till datalagret.
  3. Sök på disk i datalagret.
  4. Nätverkssamtal från datalagret till Alvin.
  5. Nätverkssamtal från Alvin till en kund.

Låt oss försöka stryka ut några punkter.

Datalagring har inget med det att göra

Det första jag gjorde var att konvertera Alvin till en ping-ping-server som inte behandlar förfrågningar. När den tar emot en förfrågan returnerar den ett tomt svar. Om latensen minskar är en bugg i implementeringen av Alvin eller datalager inget ovanligt. I det första experimentet får vi följande graf:

Ibland är mer mindre. När du minskar belastningen ökar latensen

Som du kan se finns det ingen förbättring när du använder ping-ping-servern. Detta innebär att datalagret inte ökar latensen och listan över misstänkta halveras:

  1. Nätverkssamtal från klient till Alvin.
  2. Nätverkssamtal från Alvin till en kund.

Bra! Listan krymper snabbt. Jag trodde att jag nästan hade kommit på orsaken.

gRPC

Nu är det dags att presentera dig för en ny spelare: gRPC. Detta är ett bibliotek med öppen källkod från Google för kommunikation under processen RPC. även om gRPC väl optimerad och allmänt använd, detta var första gången jag använde det på ett system av denna storlek och jag förväntade mig att min implementering skulle vara suboptimal - minst sagt.

tillgänglighet gRPC i stacken gav upphov till en ny fråga: kanske är det min implementering eller jag själv gRPC orsakar latensproblem? Lägga till en ny misstänkt till listan:

  1. Kunden ringer till biblioteket gRPC
  2. Bibliotek gRPC gör ett nätverksanrop till biblioteket på klienten gRPC på servern
  3. Bibliotek gRPC kontaktar Alvin (ingen funktion vid pingisserver)

För att ge dig en uppfattning om hur koden ser ut, så skiljer sig min klient/Alvin-implementering inte mycket från klient-servern. asynkrona exempel.

Obs: Listan ovan är lite förenklad eftersom gRPC gör det möjligt att använda din egen (mall?) gängningsmodell, där exekveringsstacken är sammanflätad gRPC och användarimplementering. För enkelhetens skull kommer vi att hålla oss till denna modell.

Profilering fixar allt

Efter att ha kryssat över datalagren trodde jag att jag nästan var klar: "Nu är det enkelt! Låt oss tillämpa profilen och ta reda på var förseningen inträffar." jag stort fan av precisionsprofilering, eftersom CPU:er är väldigt snabba och oftast inte är flaskhalsen. De flesta förseningar uppstår när processorn måste sluta bearbeta för att göra något annat. Exakt CPU-profilering gör just det: den registrerar allt exakt kontextväxlar och gör det tydligt var förseningar uppstår.

Jag tog fyra profiler: med hög QPS (låg latens) och med en pingisserver med låg QPS (hög latens), både på klientsidan och på serversidan. Och för säkerhets skull tog jag också en provprocessorprofil. När jag jämför profiler letar jag vanligtvis efter en anomal samtalsstack. Till exempel, på den dåliga sidan med hög latens finns det många fler kontextväxlar (10 gånger eller mer). Men i mitt fall var antalet kontextväxlar nästan detsamma. Till min fasa fanns det inget märkvärdigt där.

Ytterligare felsökning

Jag var desperat. Jag visste inte vilka andra verktyg jag kunde använda, och min nästa plan var i huvudsak att upprepa experimenten med olika varianter snarare än att tydligt diagnostisera problemet.

Tänk om

Redan från början var jag oroad över den specifika 50ms latensen. Det här är en väldigt stor tid. Jag bestämde mig för att klippa ut bitar ur koden tills jag kunde ta reda på exakt vilken del som orsakade detta fel. Sedan kom ett experiment som fungerade.

Som vanligt verkar det i efterhand som att allt var självklart. Jag placerade klienten på samma maskin som Alvin - och skickade en förfrågan till localhost. Och ökningen av latensen är borta!

Ibland är mer mindre. När du minskar belastningen ökar latensen

Något var fel med nätverket.

Att lära sig nätverkstekniker

Jag måste erkänna: min kunskap om nätverksteknik är fruktansvärd, särskilt med tanke på att jag arbetar med dem varje dag. Men nätverket var den främsta misstänkta, och jag behövde lära mig hur man felsöker det.

Lyckligtvis älskar Internet de som vill lära sig. Kombinationen av ping och tracert verkade vara en tillräckligt bra start för att felsöka nätverkstransportproblem.

Först startade jag PsPing till Alvins TCP-port. Jag använde standardinställningarna - inget speciellt. Av mer än tusen ping översteg ingen 10 ms, med undantag för den första för uppvärmning. Detta strider mot den observerade ökningen av latens på 50 ms vid den 99:e percentilen: där, för varje 100:e begäran, borde vi ha sett ungefär en begäran med en latens på 50 ms.

Sedan försökte jag tracert: Det kan finnas ett problem vid en av noderna längs rutten mellan Alvin och klienten. Men spårämnet återvände också tomhänt.

Så det var inte min kod, gRPC-implementeringen eller nätverket som orsakade förseningen. Jag började oroa mig för att jag aldrig skulle förstå detta.

Vilket OS har vi nu

gRPC används ofta på Linux, men exotiskt på Windows. Jag bestämde mig för att prova ett experiment som fungerade: jag skapade en virtuell Linux-maskin, kompilerade Alvin för Linux och distribuerade den.

Ibland är mer mindre. När du minskar belastningen ökar latensen

Och här är vad som hände: Linux-pingisservern hade inte samma fördröjningar som en liknande Windows-värd, även om datakällan inte var annorlunda. Det visar sig att problemet ligger i gRPC-implementeringen för Windows.

Nagles algoritm

Hela den här tiden trodde jag att jag saknade en flagga gRPC. Nu förstår jag vad det egentligen är gRPC Windows-flaggan saknas. Jag hittade ett internt RPC-bibliotek som jag var säker på skulle fungera bra för alla flaggor Winsock. Sedan la jag till alla dessa flaggor till gRPC och distribuerade Alvin på Windows, i en lappad Windows-pingisserver!

Ibland är mer mindre. När du minskar belastningen ökar latensen

nästan Klart: Jag började ta bort de tillagda flaggorna en i taget tills regressionen återkom så att jag kunde lokalisera orsaken. Det var ökänt TCP_NODELAY, Nagles algoritmomkopplare.

Nagles algoritm försöker minska antalet paket som skickas över ett nätverk genom att fördröja överföringen av meddelanden tills paketstorleken överstiger ett visst antal byte. Även om detta kan vara trevligt för den genomsnittliga användaren, är det destruktivt för realtidsservrar eftersom operativsystemet kommer att försena vissa meddelanden, vilket orsakar fördröjningar på låg QPS. U gRPC denna flagga sattes i Linux-implementeringen för TCP-sockets, men inte i Windows. Jag är den här rättad.

Slutsats

Den högre latensen vid låg QPS orsakades av OS-optimering. I efterhand upptäckte profilering inte latens eftersom det gjordes i kärnläge snarare än i användarläge. Jag vet inte om Nagles algoritm kan observeras genom ETW-fångst, men det skulle vara intressant.

När det gäller localhost-experimentet rörde det förmodligen inte själva nätverkskoden och Nagles algoritm körde inte, så latensproblemen försvann när klienten nådde Alvin via localhost.

Nästa gång du ser en ökning i latens när antalet förfrågningar per sekund minskar, bör Nagles algoritm finnas på din lista över misstänkta!

Källa: will.com

Lägg en kommentar