Příběh o Ciliu: Překvapivé zvýšení stability sítě vyvolané drobnou změnou kódu

Úvod
Nedávno jsem si prohlédl PR bývalého kolegy z práce k projektu Cilium.
bpf:nat: Restore ORG NAT entry if it's not found
Samotná úprava (bez testovacího kódu) je malá, spočívá jen v přidání jednoho bloku if. Dopad této úpravy je však obrovský a osobně mě zaujalo, že jediný jednoduchý nápad může významně přispět ke stabilitě systému. Proto bych rád tento případ vyprávěl tak, aby mu porozuměli i lidé bez odborných znalostí v oblasti sítí.
Základní znalosti
Pokud existuje něco, co je pro moderního člověka stejně důležité jako chytrý telefon, pak je to pravděpodobně Wi-Fi router. Router komunikuje se zařízeními prostřednictvím komunikačního standardu Wi-Fi a plní úlohu sdílení své veřejné IP adresy mezi více zařízeními. Zde vzniká technicky zajímavá otázka: jakým způsobem probíhá toto „sdílení“?
Používanou technologií je Network Address Translation (NAT). NAT je technologie, která mapuje interní komunikaci ve tvaru privátní IP:Port na nepoužívanou veřejnou IP:Port a umožňuje tak komunikaci s vnějším světem, neboť komunikace TCP nebo UDP je tvořena kombinací IP adresy a informace o portu.
NAT
Když se interní zařízení pokouší připojit k internetu, NAT zařízení převede kombinaci privátní IP adresy a čísla portu tohoto zařízení na svou veřejnou IP adresu a nepoužívané libovolné číslo portu. Tyto informace o převodu se zapisují do tabulky nazývané NAT tabulka uvnitř NAT zařízení.
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).
1스마트폰 (a.a.a.a:50000) ==> 공유기 (b.b.b.b) ==> 웹 서버 (c.c.c.c:80)
Jakmile router obdrží požadavek od chytrého telefonu, uvidí následující TCP paket.
1# 공유기가 받은 TCP 패킷, 스마트폰 => 공유기
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 k chytrému telefonu s privátní IP adresou (a.a.a.a), proto router nejprve najde libovolný port, který aktuálně není v komunikaci použit (například 60000), a zapíše jej do interní NAT tabulky.
1# 공유기 내부 NAT 테이블
2| local ip | local port | global ip | global port |
3-----------------------------------------------------
4| a.a.a.a | 50000 | b.b.b.b | 60000 |
Po zapsání nového záznamu do NAT tabulky router změní zdrojovou IP adresu a port přijatého TCP paketu na svou veřejnou IP adresu (b.b.b.b) a nově přiděleny port (60000) a odešle jej webovému serveru.
1# 공유기가 보낸 TCP 패킷, 공유기 => 웹 서버
2# SNAT 수행
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| b.b.b.b | 60000 | c.c.c.c | 80 |
Webový server (c.c.c.c) nyní považuje tento požadavek za přicházející z portu 60000 routeru (b.b.b.b) a odešle odpověď routeru následujícím způsobem.
1# 공유기가 받은 TCP 패킷, 웹 서버 => 공유기
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| c.c.c.c | 80 | b.b.b.b | 60000 |
Jakmile router obdrží tento paket s odpovědí, vyhledá v NAT tabulce původní privátní IP adresu (a.a.a.a) a port (50000), které odpovídají cílové IP adrese (b.b.b.b) a portu (60000), a změní cíl paketu na chytrý telefon.
1# 공유기가 보낸 TCP 패킷, 공유기 => 스마트폰
2# DNAT 수행
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| c.c.c.c | 80 | a.a.a.a | 50000 |
Díky tomuto procesu má chytrý telefon dojem, jako by komunikoval s webovým serverem přímo prostřednictvím vlastní veřejné IP adresy. Díky NAT tak může jedna veřejná IP adresa umožnit současné používání internetu více interním zařízením.
Kubernetes
Kubernetes má jednu z nejsofistikovanějších a zároveň nejsložitějších síťových architektur mezi moderními technologiemi. Samozřejmě se zde také široce využívá výše zmíněný NAT. Dva reprezentativní případy jsou následující.
Komunikace z Podu s vnějškem clusteru
Pody uvnitř clusteru Kubernetes obvykle dostávají privátní IP adresy, které umožňují komunikaci pouze uvnitř clusterové sítě. Aby pod mohl komunikovat s vnějším internetem, je tedy pro provoz odcházející mimo cluster nutný NAT. NAT je přitom obvykle prováděn na uzlu Kubernetes (na jednotlivém serveru clusteru), kde daný pod běží. Když pod odešle paket směřující ven, tento paket je nejprve předán uzlu, ke kterému pod patří. Uzel změní zdrojovou IP adresu paketu (privátní IP podu) na svou veřejnou IP adresu, odpovídajícím způsobem upraví zdrojový port a odešle paket ven. Tento proces je podobný NAT procesu popsanému výše u Wi-Fi routeru.
Předpokládejme například, že pod uvnitř clusteru Kubernetes (10.0.1.10, port: 40000) se připojuje k vnějšímu API serveru (203.0.113.45, port: 443). Uzel Kubernetes pak od podu obdrží následující paket.
1# 노드가 받은 TCP 패킷, 파드 => 노드
2| src ip | src port | dst ip | dst port |
3---------------------------------------------------
4| 10.0.1.10 | 40000 | 203.0.113.45 | 443 |
Uzel si zapíše následující informace
1# 노드 내부 NAT 테이블 (예시)
2| local ip | local port | global ip | global port |
3---------------------------------------------------------
4| 10.0.1.10 | 40000 | 192.168.1.5 | 50000 |
a poté provede SNAT a odešle paket ven.
1# 노드가 보낸 TCP 패킷, 노드 => API 서버
2# SNAT 수행
3| src ip | src port | dst ip | dst port |
4-----------------------------------------------------
5| 192.168.1.5 | 50000 | 203.0.113.45 | 443 |
Následný průběh se pak shoduje s případem chytrého telefonu a routeru.
Komunikace z vnějšku clusteru s Podem přes NodePort
Jedním ze způsobů, jak zpřístupnit službu Kubernetes navenek, je použití služby NodePort. Služba NodePort otevírá určitý port (NodePort) na všech uzlech v clusteru a provoz přicházející na tento port předává podům patřícím do dané služby. Externí uživatel může ke službě přistupovat prostřednictvím IP adresy uzlu clusteru a NodePortu.
NAT zde hraje důležitou roli, přičemž zvláště DNAT (Destination NAT) a SNAT (Source NAT) nastávají současně. Když provoz z vnějšku dorazí na NodePort konkrétního uzlu, síť Kubernetes musí tento provoz nakonec předat podu, který danou službu poskytuje. V tomto procesu nejprve nastane DNAT, při kterém se cílová IP adresa a číslo portu paketu změní na IP adresu a číslo portu 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) jednoho uzlu clusteru Kubernetes (192.168.1.5). Předpokládejme dále, že tato služba interně odkazuje na pod s IP adresou 10.0.2.15 a portem 8080.
1외부 사용자 (203.0.113.10:30000) ==> 쿠버네티스 노드 (외부:192.168.1.5:30001 / 내부: 10.0.1.1:42132) ==> 쿠버네티스 파드 (10.0.2.15:8080)
Zde uzel Kubernetes disponuje jak IP adresou 192.168.1.5, která je dostupná zvenčí, tak IP adresou 10.0.1.1, která je platná uvnitř interní sítě Kubernetes. (V závislosti na použitém typu CNI se pravidla v této oblasti liší, ale v tomto článku vycházím z Cilium.)
Když požadavek externího uživatele dorazí na uzel, uzel jej musí předat podu, který jej zpracuje. Uzel k tomu použije následující pravidlo DNAT, které změní cílovou IP adresu a port paketu.
1# 노드가 파드로 보낼려고 준비중인 TCP 패킷
2# DNAT 적용 후
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 203.0.113.10 | 30000 | 10.0.2.15 | 8080 |
Důležité je, že když pod odesílá odpověď na tento požadavek, zdrojová IP adresa je jeho vlastní IP adresa (10.0.2.15) a cílová IP adresa je IP adresa externího uživatele, který požadavek odeslal (203.0.113.10). V takovém případě externí uživatel obdrží odpověď z neexistující IP adresy, který nikdy nežádal, a paket jednoduše zahodí (DROP). Proto uzel Kubernetes při odesílání odpovědi z podu ven dodatečně provede SNAT a změní zdrojovou IP adresu paketu na IP adresu uzlu (192.168.1.5 nebo interní síťovou IP adresu 10.0.1.1; v tomto případě se postupuje s 10.0.1.1).
1# 노드가 파드로 보낼려고 준비중인 TCP 패킷
2# DNAT, SNAT 적용 후
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 10.0.1.1 | 40021 | 10.0.2.15 | 8080 |
Pod, který paket obdrží, nyní odpoví uzlu, od kterého původně přijal požadavek přes NodePort, a uzel provede obrácený proces DNAT a SNAT a vrátí informace externímu uživateli. V tomto procesu si každý uzel uloží následující informace.
1# 노드 내부 DNAT 테이블
2| original ip | original port | destination ip | destination port |
3------------------------------------------------------------------------
4| 192.168.1.5 | 30001 | 10.0.2.15 | 8080 |
5
6# 노드 내부 SNAT 테이블
7| original ip | original port | destination ip | destination port |
8------------------------------------------------------------------------
9| 203.0.113.10 | 30000 | 10.0.1.1 | 42132 |
Hlavní část
Obecně se v Linuxu tyto procesy NAT spravují a provádějí prostřednictvím subsystému conntrack pomocí iptables. Jiné CNI projekty, například flannel nebo calico, tento přístup využívají k řešení výše popsaného problému. Problém je v tom, že Cilium používá technologii eBPF a tento tradiční síťový zásobník Linuxu zcela ignoruje. 🤣

Výsledkem je, že Cilium z úloh, které dříve zajišťoval tradiční síťový zásobník Linuxu (viz obrázek výše), přijal cestu přímé implementace pouze těch funkcí, které jsou v prostředí Kubernetes potřebné. Proto Cilium pro výše zmíněný proces SNAT spravuje tabulku SNAT přímo jako LRU Hash Map (BPF_MAP_TYPE_LRU_HASH).
1# Cilium SNAT 테이블
2# !쉬운 설명을 위한 예시. 실제 정의는: 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 등 기타등등 메타데이터
4----------------------------------------------
5| | | | |
Jelikož jde o hash tabulku, pro rychlé vyhledávání se používá klíč, a to kombinace src ip, src port, dst ip a dst port jako klíč.
Identifikace problému
Jev 1: Vyhledávání
Vzniká tak jeden problém: aby paket procházející eBPF určil, zda je třeba provést SNAT nebo DNAT, musí prohledat výše uvedenou hash tabulku. Jak jsme viděli dříve, u SNAT existují dva typy paketů: 1. paket odcházející zevnitř ven a 2. paket přicházející zvenčí dovnitř jako odpověď. Oba tyto pakety vyžadují překlad NAT, ale mají tu zvláštnost, že hodnoty src ip, port a dst ip, port jsou vzájemně prohozeny.
Pro rychlé vyhledávání tedy buď musíme do hash tabulky přidat ještě jeden záznam, jehož klíčem jsou prohozené hodnoty src a dst, nebo musíme pro každý paket, i pro pakety nesouvisející se SNAT, prohledat stejnou hash tabulku dvakrát. Cilium samozřejmě pro lepší výkon zvolilo přístup, který stejná data zapisuje dvakrát, a to pod názvem RevSNAT.
Jev 2: LRU
Kromě výše uvedeného problému platí, že žádný hardware nemá nekonečné zdroje, a zejména u logiky na úrovni hardwaru, kde je vyžadován vysoký výkon a nelze použít dynamické datové struktury, je nutné při nedostatku zdrojů odstraňovat stávající data (evict). Cilium to řeší použitím LRU Hash Map, což je základní datová struktura poskytovaná přímo Linuxem.
Jev 1 + Jev 2 = ztráta spojení
https://github.com/cilium/cilium/issues/31643
Jinými slovy, u jednoho SNAT spojení TCP (nebo UDP):
- jsou v jedné hash tabulce pro odcházející i přicházející paket zaznamenána stejná data dvakrát, a
- v závislosti na logice LRU může kterákoli z těchto dvou položek kdykoli zaniknout,
pokud je kterýkoli NAT záznam (dále jen entry) pro odcházející nebo přicházející paket odstraněn mechanismem LRU, nelze správně provést NAT, což může vést k ztrátě celého spojení.
Řešení
Zde se objevují dva výše 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, se pokus o vyhledání provedl v tabulce SNAT s klíčem vytvořeným z kombinace src ip, src port, dst ip a dst port. Pokud klíč neexistoval, vytvořil se podle pravidel SNAT nový NAT záznam a zapsal se do tabulky. U nového spojení to povede k běžné komunikaci, ale pokud byl klíč odstraněn omylem kvůli LRU, bude paket použit jiný port než ten, který se používal dříve, a přijímající strana komunikaci odmítne, načež spojení skončí s paketem RST.
Přístup, který zvolilo výše uvedené PR, je jednoduchý:
Kdykoli je zaznamenán paket v libovolném směru, nechť se také znovu aktualizuje entry pro opačný směr.
Pokud je zaznamenána komunikace v libovolném směru, obě entry se znovu aktualizují, čímž se vzdálí od priority vyřazení mechanismu LRU, a tím se snižuje pravděpodobnost, že se odstraní pouze jedna z entry a zhroutí se celá komunikace.
Jde o velmi jednoduchý přístup a na první pohled může působit jako prostý nápad, ale díky němu lze účinně vyřešit problém, kdy se NAT informace pro odpovědní paket předčasně vyprší a spojení se přeruší, a výrazně zvýšit stabilitu systému. Z hlediska síťové stability jde také o významné zlepšení, které přineslo následující výsledky.

Závěr
Domnívám se, že toto PR je velmi dobrým příkladem toho, jak se od základních poznatků o tom, jak NAT funguje, dostaneme k tomu, že i v komplexním systému může jediný jednoduchý nápad přinést velkou změnu.
Ano, v tomto článku jsem přímo neukázal složitý systém. Abych však tomuto PR skutečně porozuměl, prosil jsem DeepSeek V3 0324 téměř tři hodiny, a to i se slovem Please, a výsledkem bylo +1 ke znalostem o Ciliu a následující obrázek. 😇
A také jako odškodné za neblahé tušení, že jsem kdysi vytvořil něco, co mohlo způsobit tyto problémy, jsem při čtení issues a PR sepsal tento článek.
Poznámka 1
Tento problém má velmi účinné řešení. Hlavní příčinou je nedostatek prostoru v NAT tabulce, takže stačí zvětšit velikost NAT tabulky. :-D
Obdivuji a respektuji nadšení uživatele gyutaeb, který se se stejným problémem setkal, neudělal k němu ani záznam a nezmizel jen se zvětšenou NAT tabulkou, ale přestože se problém přímo netýkal jeho práce, důkladně jej analyzoval, pochopil, podložil objektivními daty a přispěl do ekosystému Cilium.
Právě to bylo důvodem, proč jsem se rozhodl tento článek napsat.
Poznámka 2
Toto téma ve skutečnosti úzce nesouvisí s Gosuda, která se specializuje na jazyk Go. Jazyk Go a cloudový ekosystém jsou však úzce propojeny a přispěvatelé do Ciliu mají určitou znalost jazyka Go, proto jsem obsah, který by mohl být zveřejněn na osobním blogu, přenesl sem na Gosuda.
Myslím, že to je v pořádku, protože jsem k tomu dostal svolení od jednoho z administrátorů (ode mě samotného).
Pokud si myslíte, že to v pořádku není, prosím, uložte si to rychle jako PDF, protože nikdy nevíte, kdy bude smazáno. ;)
Poznámka 3
Při psaní tohoto článku jsem výrazně využil Cline a Llama 4 Maverick. I když jsem analýzu začal s Gemini a prosil DeepSeek, skutečnou pomoc jsem dostal od Llama 4. Llama 4 je dobrá, určitě ji vyzkoušejte.