Příběh Ciliumu: Jak malá změna kódu vedla k úžasnému zlepšení stability sítě
Úvod
Nedávno jsem si prohlížel PR kolegy z bývalé práce týkající se projektu Cilium.
bpf:nat: Restore ORG NAT entry if it's not found
Množství změn (s výjimkou testovacího kódu) je malé, přidává se pouze jeden blok if příkazu. Avšak dopad této změny je obrovský a osobně mě fascinuje, jak jednoduchá myšlenka může významně přispět ke stabilitě systému. Proto bych se chtěl pokusit vysvětlit tento případ tak, aby mu snadno porozuměli i lidé bez odborných znalostí v oblasti sítí.
Základní znalosti
Pokud existuje nějaká nezbytná věc pro moderního člověka, která je stejně důležitá jako chytrý telefon, pak je to pravděpodobně WiFi router. WiFi router komunikuje se zařízeními prostřednictvím komunikačního standardu WiFi a sdílí svou veřejnou IP adresu, aby ji mohlo používat více zařízení. Technickou zvláštností, která zde vzniká, je otázka: jak probíhá toto „sdílení“?
Technologie, která se zde používá, je Network Address Translation (NAT). NAT je technologie, která umožňuje interní komunikaci skládající se z privátní IP:port komunikovat s vnějším světem tím, že ji mapuje na nepoužívanou veřejnou IP:port, jelikož TCP nebo UDP komunikace se skládá z kombinace IP adresy a informací o portu.
NAT
Když se interní zařízení pokusí připojit k externímu internetu, zařízení NAT převede kombinaci privátní IP adresy a portu tohoto zařízení na svou vlastní veřejnou IP adresu a nepoužívaný náhodný port. Tyto informace o převodu jsou zaznamenány v NAT tabulce uvnitř zařízení NAT.
Předpokládejme například, že se chytrý telefon v domácnosti (privátní IP: a.a.a.a, port: 50000) pokouší připojit k webovému serveru (veřejná IP: c.c.c.c, port: 80).
1Chytrý telefon (a.a.a.a:50000) ==> Router (b.b.b.b) ==> Webový server (c.c.c.c:80)
Když router obdrží požadavek od chytrého telefonu, uvidí následující TCP Packet.
1# TCP paket přijatý routerem, chytrý telefon => router
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| a.a.a.a | 50000 | c.c.c.c | 80 |
Pokud by byl tento paket odeslán přímo na webový server (c.c.c.c), odpověď by se nevrátila chytrému telefonu (a.a.a.a), který má privátní IP adresu. Proto router nejprve najde náhodný port, který se aktuálně nepoužívá pro komunikaci (např. 60000), a zaznamená jej do své interní NAT tabulky.
1# Interní NAT tabulka routeru
2| local ip | local port | global ip | global port |
3-----------------------------------------------------
4| a.a.a.a | 50000 | b.b.b.b | 60000 |
Po zaznamenání nové položky do NAT tabulky router změní zdrojovou IP adresu a port z TCP paketu přijatého ze smartphonu na svou veřejnou IP adresu (b.b.b.b) a nově přidělené číslo portu (60000) a odešle jej na webový server.
1# TCP paket odeslaný routerem, router => webový server
2# Provedeno SNAT
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| b.b.b.b | 60000 | c.c.c.c | 80 |
Nyní webový server (c.c.c.c) rozpozná požadavek jako přicházející z portu 60000 routeru (b.b.b.b) a odešle odpověď routeru, jak je uvedeno níže.
1# TCP paket přijatý routerem, webový server => router
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| c.c.c.c | 80 | b.b.b.b | 60000 |
Když router obdrží tento odpovědní paket, vyhledá v NAT tabulce původní privátní IP adresu (a.a.a.a) a číslo portu (50000) odpovídající cílové IP adrese (b.b.b.b) a číslu portu (60000) a změní cíl paketu na chytrý telefon.
1# TCP paket odeslaný routerem, router => chytrý telefon
2# Provedeno DNAT
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| c.c.c.c | 80 | a.a.a.a | 50000 |
Tímto procesem se chytrý telefon cítí, jako by komunikoval s webovým serverem přímo s vlastní veřejnou IP adresou. Díky NATu může více interních zařízení používat internet současně s jednou veřejnou IP adresou.
Kubernetes
Kubernetes disponuje jednou z nejsofistikovanějších a nejsložitějších síťových architektur mezi nedávno vyvinutými technologiemi. A samozřejmě, výše zmíněný NAT je využíván na mnoha místech. Dva reprezentativní případy jsou následující:
Když Pod komunikuje s externím prostředím clusteru
Pody uvnitř Kubernetes clusteru obvykle získávají privátní IP adresy, které umožňují komunikaci pouze v rámci sítě clusteru. Proto je pro odchozí provoz z Podu do externího internetu nutný NAT. V tomto případě se NAT provádí převážně na Kubernetes uzlu (každý server v clusteru), na kterém je Pod spuštěn. Když Pod odešle paket směřující ven, tento paket je nejprve doručen na uzel, ke kterému Pod patří. Uzel změní zdrojovou IP adresu tohoto paketu (privátní IP Podu) na svou veřejnou IP adresu a vhodně změní také zdrojový port, než jej předá ven. Tento proces je podobný procesu NAT popsanému dříve u WiFi routeru.
Předpokládejme například, že Pod v Kubernetes clusteru (10.0.1.10, port: 40000) se připojuje k externímu API serveru (203.0.113.45, port: 443). Kubernetes uzel pak obdrží od Podu následující paket:
1# TCP paket přijatý uzlem, Pod => uzel
2| src ip | src port | dst ip | dst port |
3---------------------------------------------------
4| 10.0.1.10 | 40000 | 203.0.113.45 | 443 |
Uzel poté zaznamená následující obsah:
1# Interní NAT tabulka uzlu (příklad)
2| local ip | local port | global ip | global port |
3---------------------------------------------------------
4| 10.0.1.10 | 40000 | 192.168.1.5 | 50000 |
a provede SNAT, než odešle paket ven, jak je uvedeno níže.
1# TCP paket odeslaný uzlem, uzel => API server
2# Provedeno SNAT
3| src ip | src port | dst ip | dst port |
4-----------------------------------------------------
5| 192.168.1.5 | 50000 | 203.0.113.45 | 443 |
Následný proces je stejný jako v případě chytrého telefonu a routeru.
Když externí prostředí clusteru komunikuje s Podem prostřednictvím NodePortu
Jedním ze způsobů, jak zveřejnit službu v Kubernetes, je použití služby NodePort. Služba NodePort otevírá specifický port (NodePort) na všech uzlech v clusteru a předává provoz přicházející na tento port Podům patřícím k dané službě. Externí uživatelé mohou přistupovat ke službě prostřednictvím IP adresy uzlu clusteru a NodePortu.
V tomto případě hraje NAT důležitou roli a dochází současně k DNAT (Destination NAT) a SNAT (Source NAT). Když provoz z vnějšku dorazí na NodePort konkrétního uzlu, síť Kubernetes musí tento provoz nakonec předat Podu, který poskytuje danou službu. Během tohoto procesu nejprve dochází k DNAT, kde se IP adresa a port cíle paketu změní na IP adresu a port Podu.
Předpokládejme například, že externí uživatel (203.0.113.10, port: 30000) přistupuje ke službě prostřednictvím NodePortu (30001) na uzlu Kubernetes clusteru (192.168.1.5). Předpokládejme, že tato služba interně odkazuje na Pod s IP adresou 10.0.2.15 a portem 8080.
1Externí uživatel (203.0.113.10:30000) ==> Kubernetes uzel (externí:192.168.1.5:30001 / interní: 10.0.1.1:42132) ==> Kubernetes Pod (10.0.2.15:8080)
V tomto případě má Kubernetes uzel jak externě přístupnou IP adresu uzlu 192.168.1.5, tak interní IP adresu 10.0.1.1, která je platná v síti Kubernetes. (Zásady související s tímto se liší v závislosti na typu použitého CNI, ale v tomto článku se zaměřujeme na Cilium.)
Když požadavek externího uživatele dorazí na uzel, uzel musí tento požadavek předat Podu, který jej zpracuje. V tomto okamžiku uzel aplikuje následující pravidlo DNAT, aby změnil cílovou IP adresu a číslo portu paketu.
1# TCP paket připravený uzlem k odeslání Podu
2# Po aplikaci DNAT
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 203.0.113.10 | 30000 | 10.0.2.15 | 8080 |
Zde je důležité, že když Pod odešle odpověď na tento požadavek, zdrojová IP adresa bude jeho vlastní IP adresa (10.0.2.15) a cílová IP adresa bude IP adresa externího uživatele, který odeslal požadavek (203.0.113.10). V takovém případě externí uživatel obdrží odpověď z neexistující IP adresy, o kterou nikdy nežádal, a jednoduše tento paket ZAHODÍ. Proto Kubernetes uzel dodatečně provede SNAT, když Pod odešle odpovědní paket ven, a změní zdrojovou IP adresu paketu na IP adresu uzlu (192.168.1.5 nebo interní síťovou IP 10.0.1.1, v tomto případě 10.0.1.1).
1# TCP paket připravený uzlem k odeslání Podu
2# Po aplikaci DNAT, SNAT
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 10.0.1.1 | 40021 | 10.0.2.15 | 8080 |
Jakmile Pod obdrží tento paket, odpoví na uzel, který původně obdržel požadavek na NodePort, a uzel provede obrácený proces DNAT a SNAT, aby vrátil informace externímu uživateli. Během tohoto procesu každý uzel uloží následující informace.
1# Interní DNAT tabulka uzlu
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 tabulka uzlu
7| original ip | original port | destination ip | destination port |
8------------------------------------------------------------------------
9| 203.0.113.10 | 30000 | 10.0.1.1 | 42132 |
Jádro věci
V Linuxu jsou tyto NAT procesy typicky spravovány a prováděny subsystémem conntrack prostřednictvím iptables. Projekty CNI jako flannel nebo calico skutečně využívají tento mechanismus k řešení výše uvedených problémů. Problém však spočívá v tom, že Cilium používá technologii eBPF a zcela ignoruje tento tradiční síťový zásobník Linuxu. 🤣

V důsledku toho se Cilium rozhodl implementovat pouze ty funkce, které jsou nezbytné pro Kubernetes, z úkolů, které dříve vykonával tradiční síťový zásobník Linuxu, jak je znázorněno na obrázku. Cilium tak přímo spravuje SNAT tabulku ve formě LRU Hash Map (BPF_MAP_TYPE_LRU_HASH) pro dříve zmíněný proces SNAT.
1# Cilium SNAT tabulka
2# !Příklad pro snazší vysvětlení. Skutečná definice: 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 | protokol, conntrack atd. metadata
4----------------------------------------------
5| | | | |
A protože se jedná o hash tabulku, pro rychlé vyhledávání existuje klíč, který používá kombinaci src ip, src port, dst ip, dst port jako klíč.
Identifikace problému
Problém 1: Vyhledávání
V důsledku toho vzniká jeden problém: paket procházející eBPF musí provést vyhledávání v této hash tabulce, aby ověřil, zda je potřeba provést proces SNAT nebo DNAT. Jak jsme již viděli, existují dva typy paketů v procesu SNAT: 1. pakety opouštějící interní síť do externí a 2. pakety přicházející z externí sítě jako odpověď na ně. Oba tyto pakety vyžadují transformaci v procesu NAT a vyznačují se výměnou hodnot src ip, port a dst ip, port.
Pro rychlé vyhledávání je proto nutné buď přidat do hash tabulky další hodnotu klíče s obrácenými src a dst, nebo provést dvakrát vyhledávání ve stejné hash tabulce pro všechny pakety, i když s SNAT nesouvisí. Cilium samozřejmě pro lepší výkon zvolil metodu vkládání stejných dat dvakrát pod názvem RevSNAT.
Problém 2: LRU
Nezávisle na výše uvedeném problému, jelikož nekonečné zdroje nemohou existovat v žádném hardwaru, a zejména v logice hardwarové úrovně, která vyžaduje vysoký výkon, je nutné vyřadit stávající data, když dojde k nedostatku zdrojů, v situaci, kdy nelze použít ani dynamické datové struktury. Cilium to vyřešil použitím LRU Hash Map, základní datové struktury poskytované Linuxem.
Problém 1 + Problém 2 = Ztráta spojení
https://github.com/cilium/cilium/issues/31643
To znamená, že pro jedno SNATed TCP (nebo UDP) spojení:
- Stejná data jsou zaznamenána dvakrát v jedné Hash Table pro odchozí a příchozí pakety.
- Vzhledem k logice LRU může kdykoli dojít ke ztrátě jednoho z těchto dat.
Pokud se jedna z informací NAT (dále jen "entry") pro odchozí nebo příchozí pakety ztratí kvůli LRU, nelze NAT správně provést, což může vést k úplné ztrátě spojení.
Řešení
Zde přichází na řadu dříve zmíněné PR.
bpf:nat: restore a NAT entry if its REV NAT is not found
bpf:nat: Restore ORG NAT entry if it's not found
Dříve, když paket procházel eBPF, pokusil se vyhledat v SNAT tabulce pomocí klíče vytvořeného z kombinace src ip, src port, dst ip, dst port. Pokud klíč neexistoval, byla podle pravidel SNAT vytvořena nová NAT informace a zaznamenána do tabulky. V případě nového spojení by to vedlo k normální komunikaci. Pokud by však klíč byl neúmyslně odstraněn LRU, provedl by se nový NAT s jiným portem než ten, který se používal pro existující komunikaci, což by způsobilo, že příjemce paketu odmítl příjem a spojení by bylo ukončeno s RST paketem.
Zde je přístup, který toto PR zvolilo, jednoduchý.
Jakmile je paket pozorován v jakémkoli směru, obnovte také záznam pro opačný směr.
Kdykoli je komunikace pozorována v jakémkoli směru, oba záznamy se aktualizují, čímž se snižuje jejich priorita pro vyřazení v logice LRU. Tím se snižuje pravděpodobnost scénáře, kdy by byl odstraněn pouze jeden záznam a celá komunikace by selhala.
Ačkoli se to může zdát jako velmi jednoduchý přístup a jednoduchý nápad, tento přístup účinně vyřešil problém přerušení spojení v důsledku předčasného vypršení informací o NAT pro odpovědní pakety a výrazně zlepšil stabilitu systému. Lze jej považovat za významné zlepšení, které dosáhlo následujících výsledků z hlediska stability sítě.

Závěr
Domnívám se, že toto PR je vynikajícím příkladem toho, jak jednoduchá myšlenka může přinést obrovské změny i v komplexním systému, počínaje základními znalostmi CS o tom, jak NAT funguje.
Ach, samozřejmě, v tomto článku jsem přímo neukázal příklady složitých systémů. Ale abych správně porozuměl tomuto PR, musel jsem prosit DeepSeek V3 0324 téměř 3 hodiny, dokonce slovem „Please“, a výsledkem bylo zvýšení znalostí o Cilium o +1 a získání následujícího obrázku. 😇
A při čtení problémů a PR jsem se rozhodl napsat tento článek jako kompenzaci za zlověstné tušení, že problémy mohly vzniknout kvůli něčemu, co jsem kdysi vytvořil.
Poznámka – 1
Mimochodem, existuje velmi účinný způsob, jak se tomuto problému vyhnout. Jelikož základní příčinou problému je nedostatek místa v NAT tabulce, stačí zvětšit velikost NAT tabulky. :-D
Obdivuji a respektuji vášeň gyutaeb, který, i když se ho tento problém přímo netýkal, důkladně ho analyzoval, pochopil a přispěl k ekosystému Cilium s objektivními daty, zatímco někdo jiný, kdo se s tímto problémem setkal, by pravděpodobně jen zvětšil velikost NAT tabulky a utekl, aniž by zanechal problém.
To byl důvod, proč jsem se rozhodl napsat tento článek.
Poznámka – 2
Tento příběh se ve skutečnosti přímo nehodí k Gosudě, která se specializuje na jazyk Go. Avšak jazyk Go a cloudový ekosystém jsou úzce propojeny a přispěvatelé Cilium mají určitou znalost jazyka Go, takže jsem se rozhodl přinést obsah, který bych mohl zveřejnit na osobním blogu, do Gosudy.
Protože jsem dostal svolení od jednoho z administrátorů (sebe sama), myslím, že to bude v pořádku.
Pokud si myslíte, že to v pořádku není, raději si to rychle uložte jako PDF, protože nevím, kdy to bude smazáno. ;)
Poznámka – 3
Při psaní tohoto článku jsem značně využil pomoc Cline a Llama 4 Maveric. Ačkoli jsem s analýzou začal pomocí Gemini a prosil DeepSeek, skutečnou pomoc jsem získal od Llama 4. Llama 4 je skvělá. Určitě ji vyzkoušejte.