GoSuda

Cilium Story: Enestående forbedringer i netværksstabilitet fra en lille kodeændring

By iwanhae
views ...

Introduktion

For nylig stødte jeg på en PR for et Cilium-projekt fra en tidligere kollega.

bpf:nat: Restore ORG NAT entry if it's not found

Mængden af ændringer (ekskl. testkoden) var lille, blot tilføjelsen af en enkelt if-sætning. Men indvirkningen af denne ændring var enorm, og det faktum, at en simpel idé kan bidrage så meget til systemets stabilitet, fandt jeg personligt interessant. Derfor vil jeg forsøge at forklare denne sag, så selv personer uden specialiseret viden inden for netværk nemt kan forstå den.

Baggrundsviden

Hvis der er en uundværlig moderne genstand, der er lige så vigtig som en smartphone, er det sandsynligvis en Wi-Fi-router. En Wi-Fi-router kommunikerer med enheder via Wi-Fi-kommunikationsstandarden og deler sin offentlige IP-adresse, så flere enheder kan bruge den. Den tekniske særegenhed, der opstår her, er, hvordan denne "deling" finder sted.

Den anvendte teknologi her er Network Address Translation (NAT). NAT er en teknologi, der muliggør ekstern kommunikation ved at mappe intern kommunikation, der består af privat IP:Port, til en ubrugt offentlig IP:Port, givet at 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 internettet, oversætter NAT-enheden kombinationen af enhedens private IP-adresse og portnummer til sin egen offentlige IP-adresse og et tilfældigt, ubrugt portnummer. Denne oversættelsesinformation registreres i en NAT-tabel inde i NAT-enheden.

Lad os for eksempel antage, at en smartphone derhjemme (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)

Routeren vil, når den modtager anmodningen fra smartphonen, se følgende TCP-pakke:

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), da den har en privat IP-adresse. Derfor finder routeren først en tilfældig port (f.eks. 60000), der ikke er involveret i den aktuelle kommunikation, 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 registreret 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 en svarpakke 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øler smartphonen, at den kommunikerer direkte med webserveren med sin egen offentlige IP-adresse. Takket være NAT kan flere interne enheder bruge internettet samtidigt 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. De to repræsentative eksempler er som følger:

Når en Pod internt kommunikerer med et eksternt klyngenetværk

Pods inden for et Kubernetes-klyngenetværk tildeles typisk 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, leveres 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 for Wi-Fi-routere.

Lad os for eksempel antage, at en Pod (10.0.1.10, port: 40000) inden for et Kubernetes-klyngenetværk forsøger at oprette forbindelse til en ekstern API-server (203.0.113.45, port: 443). Kubernetes-noden vil modtage en pakke som denne 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 vil derefter registrere følgende information:

1# Noden 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       |

Og derefter udføre SNAT og sende pakken eksternt som følger:

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      |

Den efterfølgende proces følger samme forløb som eksemplet med smartphone-routeren.

Når der kommunikeres med en Pod via NodePort fra uden for klyngen

En af måderne at eksponere en service eksternt i Kubernetes er ved at bruge en NodePort-service. En NodePort-service å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 nodens IP-adresse og NodePort.

I dette tilfælde spiller NAT en vigtig rolle, og især opstår DNAT (Destination NAT) og SNAT (Source NAT) samtidig. Når trafik kommer ind på en specifik nodes NodePort udefra, skal Kubernetes-netværket videresende denne trafik til den Pod, der i sidste ende leverer tjenesten. I denne proces udføres DNAT først for at ændre pakkens destinations-IP-adresse og portnummer til Pod'ens IP-adresse og portnummer.

Lad os for eksempel antage, at en ekstern bruger (203.0.113.10, port: 30000) får adgang til en service via NodePort (30001) på en node (192.168.1.5) i et Kubernetes-klyngenetværk. Lad os antage, at denne service internt peger på en Pod med IP-adressen 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)

Her har Kubernetes-noden både den eksternt tilgængelige IP-adresse 192.168.1.5 og den internt gyldige IP-adresse 10.0.1.1 i Kubernetes-netværket. (Politikker relateret til dette varierer afhængigt af den anvendte CNI-type, men i denne artikel vil forklaringen baseres på Cilium.)

Når en ekstern brugers anmodning ankommer til noden, skal noden videresende denne anmodning til den Pod, der skal behandle den. I dette tilfælde 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 anvendelse af DNAT
3| src ip        | src port | dst ip    | dst port |
4---------------------------------------------------
5| 203.0.113.10  | 30000    | 10.0.2.15 | 8080     |

Det vigtige her er, at når Pod'en sender et svar på denne anmodning, vil kilde-IP-adressen være dens egen IP-adresse (10.0.2.15), og destinations-IP-adressen vil være IP-adressen på den eksterne bruger, der sendte anmodningen (203.0.113.10). I dette tilfælde vil den eksterne bruger modtage et svar fra en ikke-eksisterende IP-adresse, som den ikke har anmodet om, og vil simpelthen DROP'e pakken. Derfor udfører Kubernetes-noden yderligere SNAT, når Pod'en sender svarpakker 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 anvendelse af DNAT, SNAT
3| src ip        | src port | dst ip    | dst port |
4---------------------------------------------------
5| 10.0.1.1      | 40021    | 10.0.2.15 | 8080     |

Nu vil Pod'en, der har modtaget pakken, svare til den node, der oprindeligt modtog anmodningen via NodePort, og noden vil anvende den omvendte DNAT- og SNAT-proces for at returnere informationen til den eksterne bruger. Under denne proces vil hver node gemme følgende information:

1# Nodens interne 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# Nodens interne 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 administreres og opereres disse NAT-processer i Linux af et subsystem kaldet conntrack via iptables. Faktisk bruger andre CNI-projekter som flannel og calico dette til at håndtere de nævnte problemer. Problemet er dog, at Cilium, ved at bruge teknologien eBPF, fuldstændig ignorerer den traditionelle Linux-netværksstak. 🤣

https://cilium.io/blog/2021/05/11/cni-benchmark/

Som et resultat har Cilium valgt at implementere de funktioner, der er nødvendige for Kubernetes-situationer, direkte fra de opgaver, som den eksisterende Linux-netværksstak udførte, som vist i ovenstående figur. Derfor administrerer Cilium SNAT-tabellen direkte i form af en LRU Hash Map (BPF_MAP_TYPE_LRU_HASH) for den tidligere nævnte SNAT-proces.

1# Cilium SNAT-tabel
2# ! Eksempel for nem forståelse. Den faktiske definition er: 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 og andre metadata
4----------------------------------------------
5|            |          |         |          |

Og som en Hash Table bruger den en kombination af src ip, src port, dst ip, dst port som nøgle for hurtig opslag.

Problemidentifikation

Fænomen - 1: Opslag

Dette fører til et problem: for at verificere, om en pakke, der passerer gennem eBPF, skal udføre SNAT- eller DNAT-processen, skal Hash Table'et slås op. Som vi har set, er der to typer pakker involveret i SNAT-processen: 1. pakker, der går fra intern til ekstern, og 2. pakker, der kommer fra ekstern til intern som svar. Disse to pakker kræver NAT-konvertering og har den egenskab, at src ip, port og dst ip, port værdierne er ombyttede.

Derfor er det for hurtig opslag nødvendigt enten at tilføje endnu en post til Hash Table'et med de ombyttede src- og dst-værdier som nøgle, eller at udføre den samme Hash Table-opslag to gange for alle pakker, selv for dem, der ikke er relateret til SNAT. Cilium har naturligvis valgt metoden med at indsætte de samme data to gange under navnet RevSNAT for at opnå bedre ydeevne.

Fænomen - 2: LRU

Udover ovenstående problem kan ingen hardware have uendelige ressourcer, og især i hardware-logik, der kræver høj ydeevne, hvor dynamiske datastrukturer ikke kan bruges, er det nødvendigt at fjerne 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 betyder, at for en enkelt SNAT-forbindelse (enten TCP eller UDP):

  1. De samme data er registreret to gange i én Hash Table for udgående og indgående pakker.
  2. I en situation, hvor en af de to data til enhver tid kan fjernes af LRU-logikken,

Hvis blot én NAT-information (herefter entry) for enten udgående eller indgående pakker slettes af LRU, kan det medføre, at NAT ikke udføres korrekt, hvilket fører til et totalt 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 slå op i SNAT-tabellen ved at oprette en nøgle ud fra kombinationen af src ip, src port, dst ip og dst port. Hvis nøglen ikke eksisterede, ville den oprette nye NAT-informationer i henhold til SNAT-reglerne og registrere dem i tabellen. Hvis det var en ny forbindelse, ville dette føre til normal kommunikation. Men hvis nøglen utilsigtet blev fjernet af LRU, ville NAT blive udført på en ny port, forskellig fra den port, der blev brugt til den eksisterende kommunikation, hvilket ville medføre, at modtageren af pakken afviste den, og forbindelsen ville blive afsluttet med en RST-pakke.

Her er den tilgang, som PR'en anvendte, enkel.

Når en pakke observeres i den ene retning, skal posten for den modsatte retning også opdateres.

Når kommunikation observeres i den ene retning, opdateres begge entries, hvilket reducerer deres prioritet for eviction i LRU-logikken. Dette mindsker sandsynligheden for et scenarie, hvor kun den ene entry slettes, hvilket forårsager et totalt kommunikationssammenbrud.

Dette kan virke som en meget simpel tilgang og en simpel idé, men gennem denne tilgang er problemet med forbindelsestab på grund af for tidlig udløb af NAT-information for svarpakker effektivt løst, 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.

benchmark

Konklusion

Jeg mener, at denne PR er et glimrende eksempel på, hvordan en simpel idé kan medføre en enorm forandring selv i et komplekst system, startende fra grundlæggende CS-viden om, hvordan NAT fungerer.

Åh, og jeg har selvfølgelig ikke direkte vist eksempler på komplekse systemer i denne artikel. Men for at forstå denne PR ordentligt har jeg i næsten 3 timer bønfaldt DeepSeek V3 0324 med ordet Please, og som et resultat har jeg opnået Cilium-viden +1 og følgende diagram. 😇

diagram

Og ved at læse issues og PR'er skriver jeg denne artikel som en kompensation for de ildevarslende anelser om, at issues kunne være opstået på grund af noget, jeg tidligere havde skabt.

Anmeldelse - 1

Forresten findes der en meget effektiv måde at undgå dette problem på. Den grundlæggende årsag til problemet er mangel på plads i NAT-tabellen, så løsningen er at øge størrelsen på NAT-tabellen. :-D

Jeg beundrer og respekterer gyutaebs passion, som grundigt analyserede, forstod og bidrog til Cilium-økosystemet med objektive data, selvom problemet ikke direkte var relateret til ham, i en situation hvor nogen andre, der stødte på det samme problem, blot ville have øget NAT-tabellens størrelse og stukket af uden at efterlade et issue.

Dette var motivationen for at skrive denne artikel.

Anmeldelse - 2

Denne historie er faktisk ikke direkte relevant for Gosuda, som primært beskæftiger sig med Go-sprog. Men Go-sprog og cloud-økosystemet er tæt forbundet, og bidragydere til Cilium har en vis forståelse for Go-sprog, så jeg har taget indhold, der kunne være lagt på en personlig blog, og bragt det til Gosuda.

Da en af administratorerne (mig selv) har givet tilladelse, går det nok.

Hvis du mener, det ikke er i orden, så gem det hurtigt som PDF, da det kan blive slettet når som helst. ;)

Anmeldelse - 3

Denne artikel er i høj grad udarbejdet med hjælp fra Cline og Llama 4 Maveric. Selvom jeg startede analysen med Gemini og bønfaldt DeepSeek, fik jeg faktisk hjælp fra Llama 4. Llama 4 er god. Prøv den endelig.