En berättelse om saknade DNS-paket från teknisk support för Google Cloud
Från Google Blog Editor: Har du någonsin undrat hur Google Cloud Technical Solutions (TSE) ingenjörer hanterar dina supportförfrågningar? TSE:s tekniska supportingenjörer är ansvariga för att identifiera och korrigera användarrapporterade problemkällor. Vissa av dessa problem är ganska enkla, men ibland stöter du på en biljett som kräver uppmärksamhet från flera ingenjörer samtidigt. I den här artikeln kommer en av TSE-anställda att berätta om ett mycket knepigt problem från hans senaste praktik - fall av saknade DNS-paket. I den här historien kommer vi att se hur ingenjörerna lyckades lösa situationen och vilka nya saker de lärde sig när de åtgärdade felet. Vi hoppas att den här berättelsen inte bara utbildar dig om en djupt rotad bugg, utan också ger dig insikt i de processer som krävs för att lämna in ett supportärende hos Google Cloud.
Felsökning är både en vetenskap och en konst. Det hela börjar med att bygga en hypotes om orsaken till systemets icke-standardiserade beteende, varefter det testas för styrka. Men innan vi formulerar en hypotes måste vi tydligt definiera och exakt formulera problemet. Om frågan låter för vag, måste du analysera allt noggrant; Detta är "konsten" att felsöka.
Under Google Cloud blir sådana processer exponentiellt mer komplexa, eftersom Google Cloud gör sitt bästa för att garantera användarnas integritet. På grund av detta har TSE-ingenjörer inte tillgång till att redigera dina system, och inte heller möjligheten att se konfigurationer så brett som användare gör. Därför, för att testa någon av våra hypoteser, kan vi (ingenjörer) inte snabbt modifiera systemet.
Vissa användare tror att vi kommer att fixa allt som mekanik i en biltjänst och helt enkelt skicka oss ID:t för en virtuell maskin, medan processen i själva verket sker i ett konversationsformat: samla in information, bilda och bekräfta (eller vederlägga) hypoteser, och i slutändan baseras ett beslutsproblem på kommunikation med klienten.
Problemet i fråga
Idag har vi en historia med ett bra slut. En av anledningarna till den framgångsrika lösningen av det föreslagna fallet är en mycket detaljerad och korrekt beskrivning av problemet. Nedan kan du se en kopia av den första biljetten (redigerad för att dölja konfidentiell information):
Detta meddelande innehåller mycket användbar information för oss:
Specifik virtuell dator specificerad
Själva problemet indikeras - DNS fungerar inte
Det anges var problemet visar sig - VM och container
De steg som användaren tog för att identifiera problemet anges.
Förfrågan registrerades som "P1: Critical Impact - Service Unusable in production", vilket innebär konstant övervakning av situationen 24/7 enligt schemat "Follow the Sun" (du kan läsa mer om prioriteringar av användarförfrågningar), med dess överföring från ett tekniskt supportteam till ett annat med varje tidszonförskjutning. Faktum är att när problemet nådde vårt team i Zürich hade det redan cirklat runt jorden. Vid det här laget hade användaren vidtagit lindrande åtgärder, men var rädd för en upprepning av situationen i produktionen, eftersom grundorsaken ännu inte hade upptäckts.
När biljetten nådde Zürich hade vi redan följande information till hands:
Innehåll /etc/hosts
Innehåll /etc/resolv.conf
Utgång iptables-save
Sammansatt av teamet ngrep pcap-fil
Med dessa data var vi redo att påbörja "utredningen" och felsökningsfasen.
Våra första steg
Först och främst kontrollerade vi loggar och status för metadataservern och såg till att den fungerade korrekt. Metadataservern svarar på IP-adressen 169.254.169.254 och ansvarar bland annat för att kontrollera domännamn. Vi dubbelkollade också att brandväggen fungerar korrekt med den virtuella datorn och inte blockerar paket.
Det var något slags konstigt problem: nmap-kontrollen tillbakavisade vår huvudhypotes om förlusten av UDP-paket, så vi kom mentalt på flera alternativ och sätt att kontrollera dem:
Är den inte för liten? MTU? => Kontrollera utgången ip a show
Påverkar problemet bara UDP-paket eller TCP också? => Kör iväg dig +tcp
Returneras grävgenererade paket? => Kör iväg tcpdump
Fungerar libdns korrekt? => Kör iväg strace för att kontrollera överföringen av paket i båda riktningarna
Här bestämmer vi oss för att ringa användaren för att felsöka problem live.
Under samtalet kan vi kontrollera flera saker:
Efter flera kontroller utesluter vi iptables-regler från listan över skäl
Vi kontrollerar nätverksgränssnitt och routingtabeller och dubbelkollar att MTU:n är korrekt
Det upptäcker vi dig +tcp google.com (TCP) fungerar som det ska, men dig google.com (UDP) fungerar inte
Har kört iväg tcpdump medan du arbetar dig, finner vi att UDP-paket returneras
Vi kör iväg strace dig google.com och vi ser hur gräva korrekt ringer sendmsg() и recvms(), men den andra avbryts av en timeout
Tyvärr kommer slutet av skiftet och vi tvingas eskalera problemet till nästa tidszon. Förfrågan väckte dock intresse hos vårt team, och en kollega föreslår att man skapar det första DNS-paketet med hjälp av den skrapiga Python-modulen.
from scapy.all import *
answer = sr1(IP(dst="169.254.169.254")/UDP(dport=53)/DNS(rd=1,qd=DNSQR(qname="google.com")),verbose=0)
print ("169.254.169.254", answer[DNS].summary())
Detta fragment skapar ett DNS-paket och skickar begäran till metadataservern.
Användaren kör koden, DNS-svaret returneras och applikationen tar emot det, vilket bekräftar att det inte finns några problem på nätverksnivå.
Efter ytterligare en "jorden runt-resa" återvänder förfrågan till vårt team, och jag överför den helt och hållet till mig själv, och tänker att det kommer att vara bekvämare för användaren om förfrågan slutar cirkulera från plats till plats.
Under tiden accepterar användaren att tillhandahålla en ögonblicksbild av systembilden. Detta är mycket goda nyheter: möjligheten att testa systemet själv gör felsökningen mycket snabbare, eftersom jag inte längre behöver be användaren att köra kommandon, skicka mig resultaten och analysera dem, jag kan göra allt själv!
Mina kollegor börjar avundas mig lite. Under lunchen diskuterar vi omställningen, men ingen har någon aning om vad som händer. Lyckligtvis har användaren själv redan vidtagit åtgärder för att mildra konsekvenserna och har ingen brådska, så vi har tid att dissekera problemet. Och eftersom vi har en bild kan vi köra alla tester som intresserar oss. Bra!
Att ta ett steg tillbaka
En av de mest populära intervjufrågorna för positioner som systemingenjör är: "Vad händer när du plingar www.google.com? Frågan är stor, eftersom kandidaten behöver beskriva allt från skalet till användarutrymmet, till systemkärnan och sedan till nätverket. Jag ler: ibland visar sig intervjufrågor vara användbara i verkligheten...
Jag bestämmer mig för att tillämpa denna HR-fråga på ett aktuellt problem. Grovt sett händer följande när du försöker fastställa ett DNS-namn:
Applikationen anropar ett systembibliotek såsom libdns
libdns kontrollerar systemkonfigurationen till vilken DNS-server den ska kontakta (i diagrammet är detta 169.254.169.254, metadataserver)
libdns använder systemanrop för att skapa en UDP-socket (SOKET_DGRAM) och skicka UDP-paket med en DNS-fråga i båda riktningarna
Genom sysctl-gränssnittet kan du konfigurera UDP-stacken på kärnnivå
Kärnan interagerar med hårdvaran för att överföra paket över nätverket via nätverksgränssnittet
Hypervisorn fångar och överför paketet till metadataservern vid kontakt med den
Metadataservern bestämmer genom sin magi DNS-namnet och returnerar ett svar med samma metod
Låt mig påminna dig om vilka hypoteser vi redan har övervägt:
Hypotes: Trasiga bibliotek
Test 1: kör strace i systemet, kontrollera att dig anropar rätt systemanrop
Resultat: Korrekta systemanrop anropas
Test 2: använda srapy för att kontrollera om vi kan bestämma namn som går förbi systembibliotek
Resultat: vi kan
Test 3: kör rpm –V på libdns-paketet och md5sum-biblioteksfiler
Resultat: bibliotekskoden är helt identisk med koden i det fungerande operativsystemet
Test 4: montera användarens rotsystembild på en virtuell dator utan detta beteende, kör chroot, se om DNS fungerar
Resultat: DNS fungerar korrekt
Slutsats baserad på tester: problemet finns inte i biblioteken
Hypotes: Det finns ett fel i DNS-inställningarna
Test 1: kontrollera tcpdump och se om DNS-paket skickas och returneras korrekt efter att du kört dig
Resultat: paket överförs korrekt
Test 2: dubbelkolla servern /etc/nsswitch.conf и /etc/resolv.conf
Resultat: allt stämmer
Slutsats baserad på tester: problemet ligger inte i DNS-konfigurationen
Hypotes: kärna skadad
Testa: installera ny kärna, kontrollera signatur, starta om
Resultat: liknande beteende
Slutsats baserad på tester: kärnan är inte skadad
Hypotes: felaktigt beteende hos användarnätverket (eller hypervisornätverkets gränssnitt)
Test 1: Kontrollera dina brandväggsinställningar
Resultat: brandväggen skickar DNS-paket på både värden och GCP
Test 2: fånga upp trafik och övervaka korrektheten av överföringen och returen av DNS-förfrågningar
Resultat: tcpdump bekräftar att värden har tagit emot returpaket
Slutsats baserad på tester: problemet finns inte i nätverket
Hypotes: metadataservern fungerar inte
Test 1: kontrollera metadataserverloggarna för avvikelser
Resultat: det finns inga anomalier i loggarna
Test 2: Förbigå metadataservern via dig @8.8.8.8
Resultat: Upplösningen bryts även utan att använda en metadataserver
Slutsats baserad på tester: problemet ligger inte i metadataservern
Summan av kardemumman: vi testade alla delsystem utom körtidsinställningar!
Dyk in i Kernel Runtime Settings
För att konfigurera kärnexekveringsmiljön kan du använda kommandoradsalternativ (grub) eller sysctl-gränssnittet. Jag tittade in /etc/sysctl.conf och tänk bara, jag upptäckte flera anpassade inställningar. Kändes som om jag hade tagit tag i något, jag kasserade alla icke-nätverks- eller icke-tcp-inställningar, kvar med bergsinställningarna net.core. Sedan gick jag till var värdbehörigheterna fanns i den virtuella datorn och började tillämpa inställningarna en efter en, en efter en, med den trasiga virtuella datorn, tills jag hittade den skyldige:
net.core.rmem_default = 2147483647
Här är den, en DNS-brytande konfiguration! Jag hittade mordvapnet. Men varför händer detta? Jag behövde fortfarande ett motiv.
Den grundläggande DNS-paketbuffertstorleken konfigureras via net.core.rmem_default. Ett typiskt värde är någonstans runt 200KiB, men om din server tar emot många DNS-paket kanske du vill öka buffertstorleken. Om bufferten är full när ett nytt paket kommer, till exempel för att applikationen inte bearbetar det tillräckligt snabbt, kommer du att börja förlora paket. Vår klient ökade korrekt buffertstorleken eftersom han var rädd för dataförlust, eftersom han använde en applikation för att samla in mätvärden genom DNS-paket. Värdet han satte var det maximala möjliga: 231-1 (om satt till 231 kommer kärnan att returnera "OVALID ARGUMENT").
Plötsligt insåg jag varför nmap och scapy fungerade korrekt: de använde råa uttag! Raw sockets skiljer sig från vanliga sockets: de kringgår iptables och de är inte buffrade!
Men varför orsakar "för stor buffert" problem? Det fungerar helt klart inte som tänkt.
Vid det här laget kunde jag reproducera problemet på flera kärnor och flera distributioner. Problemet dök redan upp på 3.x-kärnan och nu dök det också upp på 5.x-kärnan.
Faktiskt, vid uppstart
sysctl -w net.core.rmem_default=$((2**31-1))
DNS slutade fungera.
Jag började leta efter arbetsvärden genom en enkel binär sökalgoritm och fann att systemet fungerade med 2147481343, men detta nummer var en meningslös uppsättning siffror för mig. Jag föreslog att klienten skulle prova det här numret, och han svarade att systemet fungerade med google.com, men gav fortfarande ett fel med andra domäner, så jag fortsatte min undersökning.
Jag har installerat dropwatch, ett verktyg som borde ha använts tidigare: det visar exakt var i kärnan ett paket hamnar. Boven var funktionen udp_queue_rcv_skb. Jag laddade ner kärnkällorna och la till några funktionprintk för att spåra var exakt paketet hamnar. Jag hittade snabbt rätt tillstånd if, och bara stirrade på det ett tag, för det var då allt äntligen samlades till en hel bild: 231-1, ett meningslöst nummer, en icke-fungerande domän... Det var en kodbit i __udp_enqueue_schedule_skb:
if (rmem > (size + sk->sk_rcvbuf))
goto uncharge_drop;
Var uppmärksam:
rmem är av typ int
size är av typen u16 (osignerad sexton-bitars int) och lagrar paketstorleken
sk->sk_rcybuf är av typen int och lagrar buffertstorleken som per definition är lika med värdet i net.core.rmem_default
När sk_rcvbuf närmar sig 231, kan summering av paketstorleken resultera i heltals överflöde. Och eftersom det är en int blir dess värde negativt, så villkoret blir sant när det ska vara falskt (du kan läsa mer om detta på länk).
Felet kan korrigeras på ett trivialt sätt: genom gjutning unsigned int. Jag tillämpade fixen och startade om systemet och DNS fungerade igen.
Smak av seger
Jag vidarebefordrade mina resultat till klienten och skickade LKML kernel patch. Jag är nöjd: varje pusselbit passar ihop, jag kan förklara exakt varför vi observerade det vi observerade, och viktigast av allt, vi kunde hitta en lösning på problemet tack vare vårt lagarbete!
Det är värt att inse att fallet visade sig vara sällsynt, och lyckligtvis får vi sällan så komplexa förfrågningar från användare.