Cilium-historien: En liten kodeendring som førte til bemerkelsesverdig forbedring av nettverksstabilitet
Innledning
For en tid tilbake observerte jeg en PR (Pull Request) for et Cilium-prosjekt fra en tidligere kollega.
bpf:nat: Restore ORG NAT entry if it's not found
Endringene (unntatt testkoden) var minimale, kun en enkelt if
-setningsblokk ble lagt til. Imidlertid var virkningen av denne endringen betydelig, og det faktum at en enkel idé kan bidra enormt til systemstabilitet, fant jeg personlig interessant. Derfor ønsker jeg å forklare dette eksempelet på en måte som gjør det lett forståelig selv for dem uten spesialisert kunnskap innen nettverk.
Bakgrunnskunnskap
Hvis det er et essensielt element for moderne mennesker som er like viktig som smarttelefonen, er det sannsynligvis Wi-Fi-ruteren. En Wi-Fi-ruter kommuniserer med enheter via Wi-Fi-standarden og deler sin offentlige IP-adresse slik at flere enheter kan bruke den. Det teknologiske særegenheten her er hvordan denne "delingen" foregår.
Teknologien som benyttes her er Network Address Translation (NAT). NAT er en teknologi som muliggjør kommunikasjon med eksterne nettverk ved å mappe intern kommunikasjon, bestående av privat IP:Port
, til en ledig offentlig IP:Port
, gitt at TCP- eller UDP-kommunikasjon består av en kombinasjon av IP-adresse og portinformasjon.
NAT
Når en intern enhet forsøker å koble seg til internett, transformerer NAT-enheten enhetens private IP-adresse og portnummerkombinasjon til sin egen offentlige IP-adresse og et tilfeldig, ubrukt portnummer. Denne transformasjonsinformasjonen lagres i en NAT-tabell inne i NAT-enheten.
Anta for eksempel at en smarttelefon (privat IP: a.a.a.a
, port: 50000) i hjemmet forsøker å koble seg til en webserver (offentlig IP: c.c.c.c
, port: 80).
1Smarttelefon (a.a.a.a:50000) ==> Ruter (b.b.b.b) ==> Webserver (c.c.c.c:80)
Når ruteren mottar smarttelefonens forespørsel, vil den se følgende TCP-pakke:
1# TCP-pakke mottatt av ruter, smarttelefon => ruter
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| a.a.a.a | 50000 | c.c.c.c | 80 |
Hvis denne pakken sendes direkte til webserveren (c.c.c.c
), vil responsen ikke returnere til smarttelefonen (a.a.a.a
) som har en privat IP-adresse. Derfor finner ruteren først en tilfeldig port (f.eks. 60000) som ikke er involvert i den nåværende kommunikasjonen, og registrerer den i den interne NAT-tabellen.
1# Intern NAT-tabell for ruter
2| local ip | local port | global ip | global port |
3-----------------------------------------------------
4| a.a.a.a | 50000 | b.b.b.b | 60000 |
Etter å ha registrert en ny oppføring i NAT-tabellen, endrer ruteren kilde-IP-adressen og portnummeret i TCP-pakken mottatt fra smarttelefonen til sin egen offentlige IP-adresse (b.b.b.b
) og det nylig tildelte portnummeret (60000), før den videresender pakken til webserveren.
1# TCP-pakke sendt av ruter, ruter => webserver
2# SNAT utført
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| b.b.b.b | 60000 | c.c.c.c | 80 |
Nå vil webserveren (c.c.c.c
) gjenkjenne forespørselen som kom fra ruterens (b.b.b.b
) port 60000, og sende responspakken tilbake til ruteren som følger:
1# TCP-pakke mottatt av ruter, webserver => ruter
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| c.c.c.c | 80 | b.b.b.b | 60000 |
Når ruteren mottar denne responspakken, finner den den opprinnelige private IP-adressen (a.a.a.a
) og portnummeret (50000) som tilsvarer destinasjons-IP-adressen (b.b.b.b
) og portnummeret (60000) i NAT-tabellen, og endrer deretter pakkens destinasjon til smarttelefonen.
1# TCP-pakke sendt av ruter, ruter => smarttelefon
2# DNAT utført
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| c.c.c.c | 80 | a.a.a.a | 50000 |
Gjennom denne prosessen vil smarttelefonen oppleve det som om den kommuniserer direkte med webserveren ved å ha sin egen offentlige IP-adresse. Takket være NAT kan flere interne enheter bruke internett samtidig med én enkelt offentlig IP-adresse.
Kubernetes
Kubernetes har en av de mest sofistikerte og komplekse nettverksstrukturene blant nylig utviklede teknologier. Og selvfølgelig brukes NAT, som nevnt tidligere, i en rekke sammenhenger. De to viktigste eksemplene er:
Når en Pod utfører kommunikasjon med eksterne klynger
Pods innenfor en Kubernetes-klynge tildeles vanligvis private IP-adresser som kun tillater kommunikasjon innenfor klyngenettverket. Derfor kreves NAT for trafikk som forlater klyngen for at en Pod skal kunne kommunisere med det eksterne internett. I slike tilfeller utføres NAT primært på Kubernetes-noden (hver server i klyngen) der Poden kjører. Når en Pod sender en utgående pakke, blir denne pakken først overført til noden Poden tilhører. Noden endrer deretter pakkens kilde-IP-adresse (Podens private IP) til sin egen offentlige IP-adresse og justerer kildeporten deretter, før den videresender pakken eksternt. Denne prosessen ligner på NAT-prosessen beskrevet for Wi-Fi-rutere.
Anta for eksempel at en Pod (10.0.1.10
, port: 40000) i en Kubernetes-klynge kobler seg til en ekstern API-server (203.0.113.45
, port: 443). Kubernetes-noden vil da motta følgende pakke fra Poden:
1# TCP-pakke mottatt av node, 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 deretter registrere følgende informasjon:
1# Intern NAT-tabell for node (eksempel)
2| local ip | local port | global ip | global port |
3---------------------------------------------------------
4| 10.0.1.10 | 40000 | 192.168.1.5 | 50000 |
og deretter utføre SNAT som følger før den sender pakken ut:
1# TCP-pakke sendt av node, node => API-server
2# SNAT utført
3| src ip | src port | dst ip | dst port |
4-----------------------------------------------------
5| 192.168.1.5 | 50000 | 203.0.113.45 | 443 |
Prosessen som følger er den samme som i eksemplet med smarttelefonruteren.
Når kommunikasjon med Pod skjer via NodePort fra utsiden av klyngen
En metode for å eksponere tjenester eksternt i Kubernetes er å bruke NodePort-tjenester. En NodePort-tjeneste åpner en spesifikk port (NodePort) på alle noder innenfor klyngen og videresender trafikk som kommer inn på denne porten til Pods som tilhører tjenesten. Eksterne brukere kan få tilgang til tjenesten via klyngenodens IP-adresse og NodePort.
I denne konteksten spiller NAT en avgjørende rolle, og spesielt DNAT (Destination NAT) og SNAT (Source NAT) forekommer samtidig. Når trafikk kommer inn fra utsiden til en bestemt nodes NodePort, må Kubernetes-nettverket til slutt levere denne trafikken til Poden som leverer den aktuelle tjenesten. I denne prosessen skjer det først DNAT, hvor pakkens destinasjons-IP-adresse og portnummer endres til Podens IP-adresse og portnummer.
Anta for eksempel at en ekstern bruker (203.0.113.10
, port: 30000) får tilgang til en tjeneste via NodePort (30001
) på en node (192.168.1.5
) i Kubernetes-klyngen. Anta at denne tjenesten internt peker til en Pod med IP-adresse 10.0.2.15
og port 8080
.
1Ekstern bruker (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 dette tilfellet har Kubernetes-noden både IP-adressen 192.168.1.5, som er tilgjengelig eksternt, og IP-adressen 10.0.1.1, som er gyldig innenfor det interne Kubernetes-nettverket. (Policyene knyttet til dette varierer avhengig av CNI-typen som brukes, men i denne artikkelen vil forklaringen baseres på Cilium.)
Når den eksterne brukerens forespørsel ankommer noden, må noden videresende denne forespørselen til Poden som skal behandle den. I denne prosessen bruker noden følgende DNAT-regel for å endre pakkens destinasjons-IP-adresse og portnummer:
1# TCP-pakke noden forbereder å sende til Pod
2# Etter DNAT-applikasjon
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 203.0.113.10 | 30000 | 10.0.2.15 | 8080 |
Et viktig poeng her er at når Poden sender et svar på denne forespørselen, vil kilde-IP-adressen være dens egen IP-adresse (10.0.2.15
), og destinasjons-IP-adressen vil være IP-adressen til den eksterne brukeren som sendte forespørselen (203.0.113.10
). I et slikt tilfelle vil den eksterne brukeren motta et svar fra en ikke-eksisterende IP-adresse som de aldri har bedt om, og vil ganske enkelt DROPe pakken. Derfor utfører Kubernetes-noden ytterligere SNAT når Poden sender responspakken eksternt, for å endre pakkens kilde-IP-adresse til nodens IP-adresse (192.168.1.5 eller den interne nettverks-IP-en 10.0.1.1, i dette tilfellet 10.0.1.1).
1# TCP-pakke noden forbereder å sende til Pod
2# Etter DNAT og SNAT-applikasjon
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 10.0.1.1 | 40021 | 10.0.2.15 | 8080 |
Nå vil Poden, som mottok denne pakken, svare til noden som opprinnelig mottok NodePort-forespørselen, og noden vil reversere de samme DNAT- og SNAT-prosessene for å returnere informasjonen til den eksterne brukeren. Under denne prosessen vil hver node lagre følgende informasjon:
1# Intern DNAT-tabell for node
2| original ip | original port | destination ip | destination port |
3------------------------------------------------------------------------
4| 192.168.1.5 | 30001 | 10.0.2.15 | 8080 |
5
6# Intern SNAT-tabell for node
7| original ip | original port | destination ip | destination port |
8------------------------------------------------------------------------
9| 203.0.113.10 | 30000 | 10.0.1.1 | 42132 |
Hoveddel
Generelt administreres og opereres disse NAT-prosessene i Linux av et subsystem kalt conntrack via iptables. Faktisk håndterer andre CNI-prosjekter som flannel og calico slike problemer ved å utnytte dette. Problemet er imidlertid at Cilium, ved å bruke eBPF-teknologi, ignorerer hele den tradisjonelle Linux-nettverksstakken. 🤣
Som et resultat har Cilium valgt å implementere de nødvendige funksjonene for Kubernetes-situasjoner direkte, blant oppgavene den eksisterende Linux-nettverksstakken utførte, som vist i figuren ovenfor. Derfor administrerer Cilium SNAT-tabellen direkte i form av en LRU Hash Map (BPF_MAP_TYPE_LRU_HASH) for den tidligere nevnte SNAT-prosessen.
1# Cilium SNAT-tabell
2# !Eksempel for enkel forklaring. Den faktiske definisjonen 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 har den nøkkelverdier for rask oppslag, og bruker en kombinasjon av src ip
, src port
, dst ip
, dst port
som nøkkelverdi [https://github.com/cilium/cilium/blob/v1.18.0-pre.1/bpf/lib/common.h#L909-L922].
Problemanalyse
Fenomen - 1: Oppslag
Dette fører til et problem: Når en pakke passerer gjennom eBPF, må en oppslag utføres i Hash Table for å verifisere om pakken krever SNAT- eller DNAT-prosessen for NAT. Som tidligere observert, finnes det to typer pakker i SNAT-prosessen: 1. Pakker som går fra internt til eksternt, og 2. Responspakker som kommer fra eksternt til internt. Disse to pakkene krever NAT-transformasjon og kjennetegnes ved at src ip
, port
og dst ip
, port
-verdiene byttes om.
For rask oppslag er det derfor nødvendig å legge til en ekstra verdi i Hash Table med omvendte kilde- og destinasjonsverdier som nøkkel, eller å utføre to oppslag i den samme Hash Table for alle pakker, selv de som ikke er relatert til SNAT. Cilium har naturligvis valgt å legge inn de samme dataene to ganger, under navnet RevSNAT, for å oppnå bedre ytelse.
Fenomen - 2: LRU
Uavhengig av det ovennevnte problemet, kan ingen maskinvare ha uendelige ressurser, og spesielt i maskinvarelogikk som krever høy ytelse, hvor dynamiske datastrukturer ikke kan brukes, er det nødvendig å fjerne eksisterende data når ressursene er knappe. Cilium har løst dette ved å bruke LRU Hash Map, en grunnleggende datastruktur som leveres som standard i Linux.
Fenomen 1 + Fenomen 2 = Tap av forbindelse
https://github.com/cilium/cilium/issues/31643
Det vil si, for en SNAT-ed TCP (eller UDP) forbindelse:
- Den samme dataen er registrert to ganger i én Hash Table for utgående og innkommende pakker.
- På grunn av LRU-logikken kan en av de to dataene når som helst gå tapt.
Hvis selv én NAT-informasjon (heretter kalt "entry") for en utgående eller innkommende pakke går tapt på grunn av LRU, vil NAT ikke kunne utføres korrekt, noe som kan føre til et fullstendig tap av forbindelsen.
Løsning
Her kommer de tidligere nevnte PR-ene inn i bildet:
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 passerte gjennom eBPF, forsøkte den å slå opp i SNAT-tabellen ved å konstruere en nøkkel fra kombinasjonen av kilde-IP, kildeport, destinasjons-IP og destinasjonsport. Hvis nøkkelen ikke eksisterte, ville ny NAT-informasjon bli generert i henhold til SNAT-reglene og registrert i tabellen. Hvis det var en ny forbindelse, ville dette føre til normal kommunikasjon. Men hvis nøkkelen utilsiktet ble fjernet av LRU, ville en ny NAT utføres med en annen port enn den som ble brukt i den eksisterende kommunikasjonen, noe som ville føre til at mottakeren nektet å motta pakken, og forbindelsen ville bli avsluttet med en RST-pakke.
Metoden som ble brukt i den nevnte PR er enkel:
Når en pakke observeres i én retning, skal entry for den motsatte retningen også oppdateres.
Uansett hvilken retning kommunikasjonen observeres i, blir begge oppføringene oppdatert, noe som reduserer sjansen for at en av oppføringene slettes av LRU-logikken, og dermed reduserer sannsynligheten for et scenario der hele kommunikasjonen bryter sammen på grunn av sletting av bare én oppføring.
Dette er en svært enkel tilnærming som kan virke som en enkel idé, men gjennom denne tilnærmingen har man effektivt løst problemet med at NAT-informasjonen for responspakker utløper for tidlig, noe som fører til at forbindelsen brytes, og har betydelig forbedret systemets stabilitet. Det er også en viktig forbedring som har oppnådd følgende resultater når det gjelder nettverksstabilitet:
Konklusjon
Jeg anser denne PR-en som et utmerket eksempel som demonstrerer hvordan en enkel idé kan føre til betydelige endringer selv innenfor komplekse systemer, med utgangspunkt i grunnleggende CS-kunnskap om hvordan NAT fungerer.
Jeg har selvfølgelig ikke vist eksempler på komplekse systemer direkte i denne artikkelen. Men for å forstå denne PR-en skikkelig, måtte jeg trygle DeepSeek V3 0324
i nesten 3 timer, selv med ordet Please
, og som et resultat fikk jeg kunnskap om Cilium +1 og et diagram som dette nedenfor. 😇
Og mens jeg leste gjennom problemene og PR-ene, følte jeg en urovekkende forutanelse om at problemer kunne ha oppstått på grunn av noe jeg hadde laget tidligere, og som en form for kompensasjon for dette, har jeg skrevet denne artikkelen.
Etterord - 1
Forresten, det finnes en svært effektiv metode for å unngå dette problemet. Den grunnleggende årsaken til problemet er mangel på plass i NAT-tabellen, så løsningen er å øke størrelsen på NAT-tabellen. :-D
Mens noen andre i en lignende situasjon kanskje bare hadde økt NAT-tabellstørrelsen og forsvunnet uten å rapportere problemet, er jeg imponert over og respekterer gyutaebs lidenskap. Han analyserte og forsto problemet grundig, selv om det ikke var direkte relatert til ham, og bidro til Cilium-økosystemet med objektive data.
Dette var motivasjonen for å skrive denne artikkelen.
Etterord - 2
Selv om dette emnet egentlig ikke passer direkte med Gosuda, som spesialiserer seg på Go-språket, er Go-språket og sky-økosystemet nært knyttet sammen, og Cilium-bidragsytere har en viss forståelse av Go-språket. Derfor har jeg valgt å bringe innhold som vanligvis ville vært publisert på en personlig blogg til Gosuda.
Siden en av administratorene (meg selv) har gitt tillatelse, regner jeg med at det er greit.
Hvis du mener det ikke er greit, bør du lagre det som PDF med en gang, for det kan bli slettet når som helst. ;)
Etterord - 3
Denne artikkelen er i stor grad skrevet med hjelp fra Cline og Llama 4 Maveric. Selv om analysen startet med Gemini og jeg tryglet DeepSeek, fikk jeg faktisk hjelp fra Llama 4. Llama 4 er utmerket. Du bør absolutt prøve det.