GoSuda

Cilium-historien: En liten kodeendring som ga bemerkelsesverdig forbedring av nettverksstabiliteten

By iwanhae views ...

Innledning

For en stund siden tok jeg en titt på en PR i Cilium-prosjektet fra en tidligere kollega.

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

Selve endringen (bortsett fra testkoden) er ganske liten, og består i å legge til omtrent én if-blokk. Likevel er virkningen av denne endringen enorm, og jeg synes det er personlig interessant at en enkel idé kan gi et stort bidrag til systemstabilitet. Derfor vil jeg forsøke å forklare historien slik at også personer uten spesialkunnskap om nettverk kan forstå dette eksemplet.

Bakgrunnskunnskap

Hvis det finnes en nødvendighet for moderne mennesker som er like viktig som smarttelefonen, er det nok Wi-Fi-ruteren. Wi-Fi-ruteren kommuniserer med enheter via Wi-Fi-standarden, og har som oppgave å dele sin offentlige IP-adresse slik at flere enheter kan bruke den. Det tekniske spørsmålet som oppstår her er: Hvordan "deler" den adressen?

Teknologien som brukes her er Network Address Translation (NAT). NAT er en teknologi som, med utgangspunkt i at TCP- eller UDP-kommunikasjon består av kombinasjoner av IP-adresse og portinformasjon, kartlegger intern kommunikasjon i formatet privat IP:Port til en offentlig IP:Port som ikke er i bruk, slik at kommunikasjon med omverdenen blir mulig.

NAT

Når en intern enhet forsøker å koble til det eksterne internettet, konverterer NAT-enheten kombinasjonen av enhetens private IP-adresse og portnummer til sin egen offentlige IP-adresse og et ubrukt, tilfeldig portnummer. Denne konverteringsinformasjonen registreres i en tabell i NAT-enheten som kalles NAT-tabellen.

La oss for eksempel anta at en smarttelefon i hjemmet (privat IP: a.a.a.a, port: 50000) forsøker å koble 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 forespørselen fra smarttelefonen, vil den se en TCP-pakke som dette.

1# TCP-pakke mottatt av ruteren, 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 videre til webserveren (c.c.c.c) som den er, vil svaret ikke komme tilbake til smarttelefonen med den private IP-adressen (a.a.a.a). Derfor finner ruteren først en port (f.eks. 60000) som ikke er i bruk i den aktuelle kommunikasjonen, og registrerer den i den interne NAT-tabellen.

1# Intern NAT-tabell i ruteren
2| local ip  | local port | global ip  | global port |
3-----------------------------------------------------
4| a.a.a.a   | 50000      | b.b.b.b    | 60000       |

Etter at ruteren har registrert den nye oppføringen i NAT-tabellen, endrer den kildens IP-adresse og portnummer i TCP-pakken fra smarttelefonen til sin egen offentlige IP-adresse (b.b.b.b) og den nytildelte portnummeret (60000), og sender den videre til webserveren.

1# TCP-pakke sendt av ruteren, 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å oppfatter webserveren (c.c.c.c) forespørselen som kommende fra port 60000 på ruteren (b.b.b.b), og sender svarpakken tilbake til ruteren på følgende måte.

1# TCP-pakke mottatt av ruteren, 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 svarpakken, slår den opp i NAT-tabellen etter målets IP-adresse (b.b.b.b) og portnummer (60000) for å finne den opprinnelige private IP-adressen (a.a.a.a) og portnummeret (50000), og endrer deretter pakkens mål til smarttelefonen.

1# TCP-pakke sendt av ruteren, 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 oppleves det som om smarttelefonen kommuniserer direkte med webserveren ved hjelp av 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 avanserte og samtidig mest komplekse nettverksarkitekturene blant teknologiene som har kommet de siste årene. Selvsagt brukes også NAT, som nevnt tidligere, flere steder i Kubernetes. De to mest sentrale eksemplene er følgende.

Når en Pod kommuniserer med omverdenen utenfor klyngen

Pods i en Kubernetes-klynge får vanligvis private IP-adresser som bare kan brukes til kommunikasjon innenfor klyngens nettverk. Derfor trenger en Pod som skal kommunisere med det eksterne internettet NAT for trafikk som går ut av klyngen. NAT utføres vanligvis på Kubernetes-noden (hver server i klyngen) der Pod-en kjører. Når en Pod sender en pakke til en ekstern adresse, blir pakken først videresendt til noden som Pod-en tilhører. Noden endrer kildens IP-adresse i pakken (Pod-ens private IP) til sin egen offentlige IP-adresse, endrer kildeporten på en passende måte, og videresender pakken til omverdenen. Denne prosessen er lik NAT-prosessen som ble beskrevet tidligere for Wi-Fi-ruteren.

La oss for eksempel anta at en Pod i Kubernetes-klyngen (10.0.1.10, port: 40000) kobler til en ekstern API-server (203.0.113.45, port: 443). Da mottar Kubernetes-noden følgende pakke fra Pod-en.

1# TCP-pakke mottatt av 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 følgende informasjon.

1# Intern NAT-tabell i noden (eksempel)
2| local ip    | local port | global ip     | global port |
3---------------------------------------------------------
4| 10.0.1.10   | 40000      | 192.168.1.5   | 50000       |

Deretter utfører den SNAT og sender pakken ut.

1# TCP-pakke sendt av noden, 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      |

Etter dette følger samme prosess som i eksemplet med smarttelefonen og ruteren.

Når ekstern kommunikasjon med en Pod skjer via NodePort fra utsiden av klyngen

En måte å eksponere en tjeneste utenfor Kubernetes på er å bruke en NodePort-tjeneste. En NodePort-tjeneste åpner en bestemt port (NodePort) på alle noder i klyngen, og videresender trafikk som kommer inn på denne porten til Pod-ene som tilhører tjenesten. Eksterne brukere kan få tilgang til tjenesten ved hjelp av IP-adressen til en av klyngens noder og NodePort-en.

NAT spiller en viktig rolle her, og spesielt oppstår DNAT (Destination NAT) og SNAT (Source NAT) samtidig. Når trafikk fra utsiden kommer inn på NodePort-en til en bestemt node, må Kubernetes-nettverket til slutt videresende denne trafikken til Pod-en som leverer tjenesten. I denne prosessen skjer først DNAT, og destinasjonens IP-adresse og portnummer i pakken endres til Pod-ens IP-adresse og portnummer.

La oss for eksempel anta at en ekstern bruker i omverdenen (203.0.113.10, port: 30000) får tilgang til tjenesten via NodePort (30001) på en node i Kubernetes-klyngen (192.168.1.5). Vi antar at denne tjenesten internt peker mot 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 fra utsiden, og IP-adressen 10.0.1.1, som er gyldig i det interne Kubernetes-nettverket. (Retningslinjene varierer avhengig av hvilken CNI-type som brukes, men i denne artikkelen forklares det med utgangspunkt i Cilium.)

Når forespørselen fra den eksterne brukeren kommer frem til noden, må noden videresende den til Pod-en som skal behandle den. Da bruker noden følgende DNAT-regel for å endre pakkens destinasjons-IP-adresse og portnummer.

1# TCP-pakke som noden forbereder for sending til Pod-en
2# Etter DNAT
3| src ip       | src port | dst ip    | dst port |
4---------------------------------------------------
5| 203.0.113.10 | 30000    | 10.0.2.15 | 8080    |

Det viktige her er at når Pod-en sender svar på denne forespørselen, blir kildens IP-adresse Pod-ens egen IP-adresse (10.0.2.15), og destinasjonens IP-adresse blir IP-adressen til den eksterne brukeren som sendte forespørselen (203.0.113.10). I så fall mottar den eksterne brukeren et svar fra en IP-adresse den aldri har kontaktet, og den vil ganske enkelt DROP-e pakken. Derfor utfører Kubernetes-noden i tillegg SNAT når Pod-en sender svarpakken ut, og endrer kildens IP-adresse til nodens IP-adresse (enten 192.168.1.5 eller den interne nettverks-IP-en 10.0.1.1; i dette tilfellet brukes 10.0.1.1).

1# TCP-pakke som noden forbereder for sending til Pod-en
2# Etter DNAT og SNAT
3| src ip       | src port | dst ip    | dst port |
4---------------------------------------------------
5| 10.0.1.1     | 40021    | 10.0.2.15 | 8080     |

Når Pod-en som mottok pakken, svarer, sender den svaret tilbake til noden som opprinnelig mottok forespørselen via NodePort. Noden reverserer da den samme DNAT- og SNAT-prosessen og sender informasjonen tilbake til den eksterne brukeren. I denne prosessen lagrer hver node følgende informasjon.

1# Intern DNAT-tabell i noden
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 i noden
7| original ip     | original port | destination ip  | destination port |
8------------------------------------------------------------------------
9| 203.0.113.10    | 30000         | 10.0.1.1        | 42132            |

Hoveddel

Vanligvis håndteres disse NAT-prosessene i Linux via iptables, og de styres og utføres av et delsystem kalt conntrack. Faktisk bruker andre CNI-prosjekter som flannel og calico dette for å løse problemene beskrevet ovenfor. Problemet er imidlertid at Cilium bruker en teknologi kalt eBPF, og dermed ignorerer hele den tradisjonelle Linux-nettverksstakken. 🤣

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

Som et resultat har Cilium valgt å implementere direkte kun de funksjonene fra den tradisjonelle Linux-nettverksstakken som trengs i en Kubernetes-kontekst, slik figuren ovenfor viser. Derfor administrerer Cilium selv SNAT-tabellen for SNAT-prosessen nevnt tidligere, i form av en LRU Hash Map (BPF_MAP_TYPE_LRU_HASH).

1# Cilium SNAT-tabell
2# !Eksempel for enkel forklaring. Faktisk definisjon: 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 m.m. annen metadata
4----------------------------------------------
5|            |          |         |          |

Siden dette er en hashtabell, brukes en nøkkel for raskt oppslag, og nøkkelen består av kombinasjonen src ip, src port, dst ip og dst port (se nøkkeldefinisjonen).

Problemidentifikasjon

Fenomen 1: Oppslag

Det oppstår dermed ett problem. For å avgjøre om en pakke som passerer eBPF-laget må få utført SNAT eller DNAT, må den slå opp i hashtabellen ovenfor. Som vi så tidligere finnes det to typer pakker i SNAT-prosessen: 1. pakker som går fra det interne nettverket til omverdenen, og 2. svarpakkene som kommer tilbake fra omverdenen til det interne nettverket. Begge pakketypene må konverteres ved NAT, men kildens og destinasjonens IP-adresse og port er byttet om.

For å oppnå raskt oppslag må man derfor enten legge til en ekstra oppføring i hashtabellen der src og dst er byttet om og brukes som nøkkel, eller slå opp i den samme hashtabellen to ganger for alle pakker, også de som ikke har noe med SNAT å gjøre. Cilium har naturligvis valgt den første metoden for bedre ytelse, og har brukt et navn kalt RevSNAT for å legge samme data inn to ganger.

Fenomen 2: LRU

I tillegg til problemet ovenfor finnes det ikke uendelige ressurser i noen maskinvare. Spesielt i maskinvarenære logikker der det kreves høy ytelse, kan man heller ikke bruke dynamiske datastrukturer, så når ressursene er knappe, må eksisterende data fjernes (evict). Cilium løste dette ved å bruke LRU Hash Map, en grunnleggende datastruktur som Linux tilbyr.

Fenomen 1 + fenomen 2 = tap av forbindelse

https://github.com/cilium/cilium/issues/31643

Med andre ord, for en enkelt SNAT-behandlet TCP- (eller UDP-) forbindelse:

  1. er samme data registrert to ganger i én hashtabell, både for utgående og innkommende pakker, og
  2. kan den ene av de to dataene når som helst forsvinne på grunn av LRU-logikken,

og hvis en NAT-oppføring (heretter kalt entry) for enten utgående eller innkommende pakker fjernes av LRU, kan NAT ikke utføres korrekt, noe som kan føre til at hele forbindelsen går tapt.

Løsning

Her dukker de følgende PR-ene opp, som jeg nevnte tidligere.

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 eBPF, ble det laget en nøkkel av kombinasjonen src ip, src port, dst ip og dst port, og det ble forsøkt et oppslag i SNAT-tabellen. Hvis nøkkelen ikke fantes, ble ny NAT-informasjon opprettet etter SNAT-reglene og registrert i tabellen. For en ny forbindelse er dette normal kommunikasjon. Men hvis nøkkelen ble fjernet utilsiktet av LRU, ville pakken få en annen port enn den som ble brukt tidligere i kommunikasjonen, og mottakeren ville avvise den. Forbindelsen ville da bli avsluttet med en RST-pakke.

PR-en tar en enkel tilnærming:

Når en pakke observeres i én retning, skal entry for motsatt retning også oppdateres.

Når kommunikasjon observeres i én retning, oppdateres begge entry-ene, og de fjernes dermed fra prioriteringen for LRU-evict. Dermed reduseres sannsynligheten for at bare den ene entry-en slettes og hele kommunikasjonen bryter sammen.

Dette er en svært enkel tilnærming, og ideen kan virke triviell. Likevel kan den løse problemet der NAT-informasjonen for svarpakkene utløper først og forbindelsen brytes, og den kan betydelig forbedre systemets stabilitet. Det er også en viktig forbedring som har gitt målbare resultater for nettverksstabilitet.

benchmark

Konklusjon

Jeg mener at denne PR-en er et veldig godt eksempel på hvordan en enkel idé kan gi stor endring, selv i et komplekst system, og at den starter med grunnleggende informatikkunnskap om hvordan NAT fungerer.

Ja, selvfølgelig har jeg ikke direkte vist frem et komplekst system i denne artikkelen. For å forstå denne PR-en ordentlig tryglet jeg likevel DeepSeek V3 0324 i nesten tre timer, og la til ordet Please. Resultatet var at jeg fikk +1 i kunnskap om Cilium, og en figur som vises nedenfor. 😇

diagram

Og etter å ha lest issues og PR-er, skrev jeg denne artikkelen som en slags kompensasjon for de urovekkende forventningene om at noe jeg har laget tidligere kan ha forårsaket disse issue-ene.

Etterord - 1

Forresten finnes det en svært effektiv måte å unngå dette issue-et på. Årsaken til problemet er mangel på plass i NAT-tabellen, så man kan løse det ved å øke størrelsen på NAT-tabellen. :-D

Den som møter samme issue og bare øker NAT-tabellen uten å rapportere issue-et, slik at de kan komme seg unna, bør få beundring og respekt fra meg for innsatsen til gyutaeb, som analyserte problemet grundig, forsto det, og bidro til Cilium-økosystemet med objektive data, selv om issue-et ikke var direkte knyttet til vedkommende.

Det var denne opplevelsen som fikk meg til å bestemme meg for å skrive denne artikkelen.

Etterord - 2

Denne historien passer egentlig ikke så godt til Gosuda, som fokuserer spesielt på programmeringsspråket Go. Likevel er Go og skyøkosystemet nært knyttet sammen, og bidragsyterne til Cilium har en viss kompetanse i Go, så jeg har tatt noe innhold som kan publiseres på en personlig blogg og flyttet det til Gosuda.

Jeg tenker at det sannsynligvis er greit, fordi en av administratorene (meg selv) har gitt tillatelse.

Hvis du mener at dette ikke er greit, kan innholdet bli slettet når som helst, så lagre det gjerne som PDF med en gang. ;)

Etterord - 3

Under skrivingen av denne artikkelen fikk jeg mye hjelp av Cline og Llama 4 Maveric. Selv om jeg startet analysen med Gemini og tryglet DeepSeek, kom den virkelige hjelpen fra Llama 4. Llama 4 er bra. Prøv den gjerne.