Cilium-historien: Bemærkelsesværdig forbedring af netværksstabilitet fra en lille kodeændring
Introduktion
For nylig stødte jeg på en PR til et Cilium-projekt fra en tidligere kollega.
bpf:nat: Restore ORG NAT entry if it's not found
Mængden af ændringer (ekskl. testkode) er minimal, blot tilføjelsen af en enkelt if
-sætning. Men indflydelsen af denne ændring er enorm, og det faktum, at en simpel idé kan bidrage så massivt til systems stabilitet, synes jeg personligt er fascinerende. Derfor vil jeg gerne fortælle denne historie på en måde, så selv personer uden specialiseret viden inden for netværk let kan forstå dette eksempel.
Baggrundsviden
Hvis der er noget, der er lige så vigtigt som en smartphone for moderne mennesker, er det nok en Wi-Fi-router. En Wi-Fi-router kommunikerer med enheder via Wi-Fi-standarden og fungerer som en enhed, der deler sin offentlige IP-adresse, så flere enheder kan bruge den. Det tekniske særkende, der opstår her, er, hvordan denne "deling" foregår.
Den teknologi, der anvendes her, er Network Address Translation (NAT). NAT er en teknologi, der muliggør kommunikation med omverdenen ved at mappe intern kommunikation, der består af en kombination af privat IP:Port
til en ubrugt offentlig IP:Port
, da TCP- eller UDP-kommunikation består af en kombination af IP-adresse og portinformation.
NAT
Når en intern enhed forsøger at få adgang til det eksterne internet, omdanner en NAT-enhed den pågældende enheds private IP-adresse og portnummerkombination til sin egen offentlige IP-adresse og et vilkårligt ubrugt portnummer. Disse oversættelsesoplysninger registreres i en NAT-tabel inde i NAT-enheden.
Antag for eksempel, at en smartphone i hjemmet (privat IP: a.a.a.a
, port: 50000) forsøger at oprette forbindelse til en webserver (offentlig IP: c.c.c.c
, port: 80).
1Smartphone (a.a.a.a:50000) ==> Router (b.b.b.b) ==> Webserver (c.c.c.c:80)
Når routeren modtager anmodningen fra smartphonen, vil den se en TCP-pakke som følger:
1# TCP-pakke modtaget af routeren, smartphone => router
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| a.a.a.a | 50000 | c.c.c.c | 80 |
Hvis denne pakke sendes direkte til webserveren (c.c.c.c
), vil svaret ikke vende tilbage til smartphonen (a.a.a.a
), som har en privat IP-adresse. Derfor finder routeren først en vilkårlig port, der ikke er involveret i den nuværende kommunikation (f.eks. 60000), og registrerer den i den interne NAT-tabel.
1# Routerens interne NAT-tabel
2| local ip | local port | global ip | global port |
3-----------------------------------------------------
4| a.a.a.a | 50000 | b.b.b.b | 60000 |
Efter at have oprettet en ny post i NAT-tabellen ændrer routeren kilde-IP-adressen og portnummeret i den TCP-pakke, den modtog fra smartphonen, til sin egen offentlige IP-adresse (b.b.b.b
) og det nyligt tildelte portnummer (60000) og sender den til webserveren.
1# TCP-pakke sendt af routeren, router => webserver
2# SNAT udført
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| b.b.b.b | 60000 | c.c.c.c | 80 |
Nu genkender webserveren (c.c.c.c
) anmodningen som kommende fra routerens (b.b.b.b
) port 60000 og sender svarpakken til routeren som følger:
1# TCP-pakke modtaget af routeren, webserver => router
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| c.c.c.c | 80 | b.b.b.b | 60000 |
Når routeren modtager denne svarpakke, finder den den oprindelige private IP-adresse (a.a.a.a
) og portnummer (50000), der svarer til destinations-IP-adressen (b.b.b.b
) og portnummeret (60000) i NAT-tabellen, og ændrer pakkens destination til smartphonen.
1# TCP-pakke sendt af routeren, router => smartphone
2# DNAT udført
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| c.c.c.c | 80 | a.a.a.a | 50000 |
Gennem denne proces får smartphonen den oplevelse, at den kommunikerer direkte med webserveren med sin egen offentlige IP-adresse. Takket være NAT kan flere interne enheder samtidigt bruge internettet med en enkelt offentlig IP-adresse.
Kubernetes
Kubernetes har en af de mest sofistikerede og komplekse netværksstrukturer blandt de nyere teknologier. Og selvfølgelig anvendes den tidligere nævnte NAT også forskellige steder. To repræsentative eksempler er:
Når en Pod internt kommunikerer med et eksternt klyngenetværk
Pods i et Kubernetes-klyngenetværk får typisk tildelt private IP-adresser, der kun kan kommunikere inden for klyngenetværket. Derfor kræves NAT for trafik, der forlader klyngen, hvis en Pod skal kommunikere med det eksterne internet. I dette tilfælde udføres NAT primært på den Kubernetes Node (hver server i klyngen), hvor Pod'en kører. Når en Pod sender en udgående pakke, videresendes denne pakke først til den Node, Pod'en tilhører. Noden ændrer denne pakkes kilde-IP-adresse (Pod'ens private IP) til sin egen offentlige IP-adresse og ændrer også kildeporten passende, før den videresendes eksternt. Denne proces ligner NAT-processen beskrevet tidligere med Wi-Fi-routeren.
Antag for eksempel, at en Pod (10.0.1.10
, port: 40000) i et Kubernetes-klyngenetværk opretter forbindelse til en ekstern API-server (203.0.113.45
, port: 443). Kubernetes-noden vil modtage en pakke som følger fra Pod'en:
1# TCP-pakke modtaget af noden, Pod => Node
2| src ip | src port | dst ip | dst port |
3---------------------------------------------------
4| 10.0.1.10 | 40000 | 203.0.113.45 | 443 |
Noden registrerer derefter følgende information:
1# Node intern NAT-tabel (eksempel)
2| local ip | local port | global ip | global port |
3---------------------------------------------------------
4| 10.0.1.10 | 40000 | 192.168.1.5 | 50000 |
Derefter udfører den SNAT som følger og sender pakken eksternt:
1# TCP-pakke sendt af noden, Node => API-server
2# SNAT udført
3| src ip | src port | dst ip | dst port |
4-----------------------------------------------------
5| 192.168.1.5 | 50000 | 203.0.113.45 | 443 |
Derefter gennemgås den samme proces som i eksemplet med smartphone-routeren.
Når der kommunikeres med en Pod via NodePort fra uden for klyngen
En måde at eksponere tjenester eksternt i Kubernetes er at bruge NodePort-tjenester. En NodePort-tjeneste åbner en specifik port (NodePort) på alle noder i klyngen og videresender trafik, der kommer ind på denne port, til de Pods, der tilhører tjenesten. Eksterne brugere kan få adgang til tjenesten via IP-adressen på klyngens node og NodePort.
I dette tilfælde spiller NAT en vigtig rolle, og især DNAT (Destination NAT) og SNAT (Source NAT) forekommer samtidig. Når trafik kommer ind på en NodePort på en specifik node fra eksternt, skal Kubernetes-netværket i sidste ende videresende denne trafik til den Pod, der leverer den pågældende tjeneste. I denne proces forekommer DNAT først, hvorved pakkens destinations-IP-adresse og portnummer ændres til Pod'ens IP-adresse og portnummer.
Antag for eksempel, at en ekstern bruger (203.0.113.10
, port: 30000) får adgang til en tjeneste via NodePort (30001
) på en node (192.168.1.5
) i et Kubernetes-klyngenetværk. Antag, at denne tjeneste internt peger på en Pod med IP-adresse 10.0.2.15
og port 8080
.
1Ekstern bruger (203.0.113.10:30000) ==> Kubernetes Node (Ekstern:192.168.1.5:30001 / Intern: 10.0.1.1:42132) ==> Kubernetes Pod (10.0.2.15:8080)
I tilfældet med Kubernetes-noden har den både nodens IP-adresse 192.168.1.5, som er tilgængelig eksternt, og IP-adressen 10.0.1.1, som er gyldig i det interne Kubernetes-netværk. (Politikker relateret til dette varierer afhængigt af den anvendte CNI-type, men i denne artikel vil forklaringen baseres på Cilium.)
Når den eksterne brugers anmodning ankommer til noden, skal noden videresende denne anmodning til den Pod, der skal behandle den. På dette tidspunkt anvender noden følgende DNAT-regler for at ændre pakkens destinations-IP-adresse og portnummer:
1# TCP-pakke, som noden forbereder at sende til Pod'en
2# Efter DNAT
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 203.0.113.10 | 30000 | 10.0.2.15 | 8080 |
Et vigtigt punkt her er, at når Pod'en sender et svar på denne anmodning, er kilde-IP-adressen dens egen IP-adresse (10.0.2.15
), og destinations-IP-adressen er den eksterne brugers IP-adresse (203.0.113.10
), der sendte anmodningen. I dette tilfælde vil den eksterne bruger modtage et svar fra en ikke-eksisterende IP-adresse, som de aldrig har anmodet om, og vil simpelthen DROP'e pakken. Derfor udfører Kubernetes-noden yderligere SNAT, når Pod'en sender svarpakken eksternt, for at ændre pakkens kilde-IP-adresse til nodens IP-adresse (192.168.1.5 eller den interne netværks-IP 10.0.1.1, i dette tilfælde 10.0.1.1).
1# TCP-pakke, som noden forbereder at sende til Pod'en
2# Efter DNAT, SNAT
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 10.0.1.1 | 40021 | 10.0.2.15 | 8080 |
Nu sender Pod'en den modtagne pakke tilbage til den node, der oprindeligt modtog NodePort-anmodningen, og noden anvender den samme omvendte DNAT- og SNAT-proces for at returnere informationen til den eksterne bruger. Under denne proces vil hver node gemme følgende information:
1# Node intern DNAT-tabel
2| original ip | original port | destination ip | destination port |
3------------------------------------------------------------------------
4| 192.168.1.5 | 30001 | 10.0.2.15 | 8080 |
5
6# Node intern SNAT-tabel
7| original ip | original port | destination ip | destination port |
8------------------------------------------------------------------------
9| 203.0.113.10 | 30000 | 10.0.1.1 | 42132 |
Hoveddel
Normalt styres og udføres disse NAT-processer i Linux via iptables af et subsystem kaldet conntrack. Faktisk håndterer andre CNI-projekter som flannel og calico ovenstående problemer ved at bruge dette. Problemet er dog, at Cilium, ved at bruge en teknologi kaldet eBPF, ignorerer hele den traditionelle Linux-netværksstak. 🤣
Som et resultat har Cilium valgt at implementere de funktioner, der er nødvendige i et Kubernetes-miljø, direkte, ud af de opgaver, som den eksisterende Linux-netværksstak udførte i diagrammet ovenfor. Derfor administrerer Cilium SNAT-tabellen for den tidligere nævnte SNAT-proces direkte i form af en LRU Hash Map (BPF_MAP_TYPE_LRU_HASH).
1# Cilium SNAT-tabel
2# ! Eksempel for nem forståelse. Faktisk definition: https://github.com/cilium/cilium/blob/v1.18.0-pre.1/bpf/lib/nat.h#L149-L166
3| src ip | src port | dst ip | dst port | protocol, conntrack osv. metadata
4----------------------------------------------
5| | | | |
Og da det er en Hash Table, bruges en kombination af src ip
, src port
, dst ip
og dst port
som nøgle for hurtig opslag.
Problemidentifikation
Fænomen - 1: Opslag
Et problem, der opstår, er, at når en pakke passerer gennem eBPF, skal den forespørge den ovennævnte Hash Table for at verificere, om den har brug for at udføre SNAT eller DNAT. Som tidligere nævnt er der to typer pakker involveret i SNAT-processen: 1. Pakker, der går fra internt til eksternt, og 2. Pakker, der kommer fra eksternt til internt som svar. Disse to pakker kræver transformation for NAT-processen og er karakteriseret ved, at src- og dst-IP- og portværdierne er omvendt.
For hurtig opslag er det derfor nødvendigt enten at tilføje en ekstra værdi til Hash Table med den omvendte src- og dst-nøgle, eller at udføre den samme Hash Table-opslag to gange for alle pakker, selv dem, der ikke er relateret til SNAT. Naturligvis har Cilium for bedre ydeevne valgt at indsætte de samme data to gange under navnet RevSNAT.
Fænomen - 2: LRU
Og uafhængigt af ovenstående problem kan uendelige ressourcer ikke eksistere på al hardware, og især i hardware-logik, hvor der kræves høj ydeevne, og dynamiske datastrukturer ikke kan bruges, er det nødvendigt at evict eksisterende data, når ressourcerne er knappe. Cilium har løst dette ved at bruge LRU Hash Map, en grundlæggende datastruktur, der leveres som standard i Linux.
Fænomen 1 + Fænomen 2 = Forbindelsestab
https://github.com/cilium/cilium/issues/31643
Det vil sige, for en SNAT'et TCP (eller UDP) forbindelse:
- I en Hash Table er de samme data registreret to gange for udgående og indgående pakker, og
- I en situation, hvor en af de to data til enhver tid kan gå tabt i henhold til LRU-logikken,
Hvis blot én NAT-information (herefter entry) for en udgående eller indgående pakke slettes af LRU, kan NAT ikke udføres korrekt, hvilket fører til fuldstændigt forbindelsestab.
Løsning
Her kommer de tidligere nævnte PR'er ind i billedet.
bpf:nat: restore a NAT entry if its REV NAT is not found
bpf:nat: Restore ORG NAT entry if it's not found
Tidligere, når en pakke passerede gennem eBPF, forsøgte den at udføre et opslag i SNAT-tabellen ved at oprette en nøgle fra kombinationen af src ip, src port, dst ip og dst port. Hvis nøglen ikke eksisterede, blev der oprettet en ny NAT-information i henhold til SNAT-reglerne og registreret i tabellen. Hvis det var en ny forbindelse, ville dette føre til normal kommunikation. Hvis nøglen utilsigtet blev fjernet af LRU, ville en ny NAT blive udført med en anden port end den, der blev brugt til den eksisterende kommunikation, hvilket ville medføre, at modtageren af pakken afviste modtagelsen, og forbindelsen ville blive afsluttet med en RST-pakke.
Her er den tilgang, som PR'en ovenfor har valgt, enkel:
Når en pakke observeres i den ene retning, skal entry for den omvendte retning også opdateres.
Når kommunikation observeres i den ene retning, opdateres begge entries, hvilket reducerer sandsynligheden for, at de bliver mål for LRU-logikkens eviction-prioritet. Dette mindsker muligheden for et scenarie, hvor kun én entry slettes, hvilket fører til fuldstændigt kommunikationssammenbrud.
Dette kan virke som en meget enkel tilgang og en simpel idé, men gennem denne tilgang er det blevet muligt effektivt at løse problemet med, at NAT-information for svarpakker udløber for tidligt, hvilket afbryder forbindelser, og systemets stabilitet er blevet markant forbedret. Det kan også betragtes som en vigtig forbedring, der har opnået følgende resultater med hensyn til netværksstabilitet:
Konklusion
Jeg mener, at denne PR er et fremragende eksempel, der viser, hvordan selv en simpel idé kan medføre store ændringer i et komplekst system, startende med grundlæggende CS-viden om, hvordan NAT fungerer.
Jeg har selvfølgelig ikke direkte vist eksempler på komplekse systemer i denne artikel. Men for at forstå denne PR ordentligt tiggede jeg DeepSeek V3 0324
i næsten 3 timer, endda med ordet Please
, og som et resultat fik jeg viden om Cilium +1 og et diagram som det nedenstående. 😇
Og ved at læse problemstillingerne og PR'erne skriver jeg denne artikel som en kompensation for de ildevarslende fornemmelser om, at et problem måske er opstået på grund af noget, jeg tidligere havde lavet.
Efterskrift - 1
Til orientering er der en meget effektiv måde at undgå dette problem på. Da den grundlæggende årsag til problemet er mangel på plads i NAT-tabellen, er løsningen at øge NAT-tabellens størrelse. :-D
Mens nogen måske ville have øget NAT-tabellens størrelse og forsvundet uden at efterlade en problembeskrivelse, når de stødte på det samme problem, beundrer og respekterer jeg gyutaeb's passion. Han analyserede og forstod problemet grundigt, selvom det ikke var direkte relateret til ham, og bidrog til Cilium-økosystemet med objektive data.
Dette var motivationen for at skrive denne artikel.
Efterskrift - 2
Denne historie er faktisk ikke et emne, der direkte passer til Gosuda, som er specialiseret i Go-sprog. Men da Go-sprog og cloud-økosystemet er tæt forbundet, og Ciliums bidragsydere har en vis forståelse af Go-sprog, har jeg valgt at bringe indhold, der kunne være postet på en personlig blog, til Gosuda.
Da en af administratorerne (mig selv) har givet tilladelse, antager jeg, at det er i orden.
Hvis du mener, det ikke er i orden, bedes du gemme det som PDF med det samme, da det kan blive slettet når som helst. ;)
Efterskrift - 3
Denne artikel blev i høj grad hjulpet af Cline og Llama 4 Maveric. Selvom jeg startede min analyse med Gemini og tiggede DeepSeek, fik jeg faktisk hjælp fra Llama 4. Llama 4 er fantastisk. Du bør helt klart prøve det.