Príbeh Ciliumu: Pozoruhodné zlepšenie stability siete vďaka malej zmene kódu
Úvod
Nedávno som si prezeral PR k projektu Cilium od bývalého kolegu z práce.
bpf:nat: Restore ORG NAT entry if it's not found
Množstvo úprav (okrem testovacieho kódu) je malé, ide len o pridanie jedného bloku if príkazu. Avšak dopad tejto zmeny je obrovský a osobne ma zaujala skutočnosť, že jednoduchá myšlienka môže výrazne prispieť k stabilite systému, a preto by som chcel tento prípad vysvetliť tak, aby ho ľahko pochopili aj ľudia bez odborných znalostí v oblasti sietí.
Základné znalosti
Ak existuje niečo rovnako dôležité ako smartfón pre moderného človeka, tak je to pravdepodobne WiFi router. WiFi router komunikuje so zariadeniami prostredníctvom komunikačného štandardu WiFi a slúži na zdieľanie svojej verejnej IP adresy medzi viacerými zariadeniami. Technická zvláštnosť spočíva v tom, ako presne sa toto "zdieľanie" realizuje.
Technológia, ktorá sa tu používa, je Network Address Translation (NAT). NAT je technológia, ktorá umožňuje internú komunikáciu realizovanú kombináciou súkromná IP:Port mapovať na aktuálne nepoužívanú verejná IP:Port s cieľom komunikovať s externým prostredím, pričom vychádza z predpokladu, že komunikácia TCP alebo UDP je tvorená kombináciou IP adresy a informácií o porte.
NAT
Zariadenie NAT pripája interné zariadenie k externému internetu tak, že prekladá kombináciu súkromnej IP adresy a čísla portu tohto zariadenia na svoju verejnú IP adresu a ľubovoľné číslo portu, ktoré sa práve nepoužíva. Tieto informácie o preklade sa zaznamenávajú do NAT tabuľky vo vnútri zariadenia NAT.
Predpokladajme napríklad, že smartfón v domácnosti (súkromná IP: a.a.a.a, port: 50000) sa pokúša pripojiť k webovému serveru (verejná IP: c.c.c.c, port: 80).
1Smartfón (a.a.a.a:50000) ==> Router (b.b.b.b) ==> Webový server (c.c.c.c:80)
Keď router prijme požiadavku zo smartfónu, uvidí nasledujúci TCP Packet.
1# TCP paket prijatý routerom, smartfón => router
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| a.a.a.a | 50000 | c.c.c.c | 80 |
Ak by tento paket bol odoslaný priamo na webový server (c.c.c.c), odpoveď by sa nevrátila smartfónu (a.a.a.a), ktorý má súkromnú IP adresu. Preto router najprv nájde ľubovoľný port (napríklad: 60000), ktorý sa práve nepoužíva na komunikáciu, a zaznamená ho do internej NAT tabuľky.
1# Interná NAT tabuľka routera
2| local ip | local port | global ip | global port |
3-----------------------------------------------------
4| a.a.a.a | 50000 | b.b.b.b | 60000 |
Po zaznamenaní nového záznamu do NAT tabuľky, router zmení zdrojovú IP adresu a číslo portu TCP paketu prijatého zo smartfónu na svoju verejnú IP adresu (b.b.b.b) a novo priradené číslo portu (60000), a potom ho odošle na webový server.
1# TCP paket odoslaný routerom, router => webový server
2# Vykonanie SNAT
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| b.b.b.b | 60000 | c.c.c.c | 80 |
Teraz webový server (c.c.c.c) rozpozná požiadavku ako prichádzajúcu z portu 60000 routera (b.b.b.b) a odošle odpoveďový paket routeru nasledovne.
1# TCP paket prijatý routerom, webový server => router
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| c.c.c.c | 80 | b.b.b.b | 60000 |
Keď router prijme tento odpovedný paket, vyhľadá v NAT tabuľke pôvodnú súkromnú IP adresu (a.a.a.a) a číslo portu (50000), ktoré zodpovedajú cieľovej IP adrese (b.b.b.b) a číslu portu (60000), a zmení cieľ paketu na smartfón.
1# TCP paket odoslaný routerom, router => smartfón
2# Vykonanie DNAT
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| c.c.c.c | 80 | a.a.a.a | 50000 |
Prostredníctvom tohto procesu má smartfón pocit, akoby priamo komunikoval s webovým serverom pomocou vlastnej verejnej IP adresy. Vďaka NAT môže viacero interných zariadení používať internet súčasne s jednou verejnou IP adresou.
Kubernetes
Kubernetes má jednu z najsofistikovanejších a najkomplexnejších sieťových štruktúr spomedzi nedávno vyvinutých technológií. A samozrejme, spomínaný NAT sa využíva aj na rôznych miestach. Dva typické príklady sú nasledujúce.
Pri komunikácii z Podu s externým prostredím klastra
Pody v klastri Kubernetes sú zvyčajne priradené súkromnými IP adresami, ktoré môžu komunikovať iba v rámci siete klastra. Preto, ak má Pod komunikovať s externým internetom, je potrebný NAT pre odchádzajúci prevádzku z klastra. V tomto prípade sa NAT vykonáva hlavne na uzle Kubernetes (každý server v klastri), kde je Pod spustený. Keď Pod odošle paket smerujúci von, tento paket sa najprv prenesie na uzol, ku ktorému Pod patrí. Uzol zmení zdrojovú IP adresu tohto paketu (súkromnú IP Podu) na svoju verejnú IP adresu a vhodne zmení aj zdrojový port, a potom ho prenesie von. Tento proces je podobný procesu NAT opísanému pre WiFi router.
Predpokladajme napríklad, že Pod (10.0.1.10, port: 40000) v klastri Kubernetes sa pripája k externému API serveru (203.0.113.45, port: 443). Uzol Kubernetes potom prijme od Podu nasledujúci paket.
1# TCP paket prijatý uzlom, Pod => Uzol
2| src ip | src port | dst ip | dst port |
3---------------------------------------------------
4| 10.0.1.10 | 40000 | 203.0.113.45 | 443 |
Uzol zaznamená nasledujúce informácie:
1# Interná NAT tabuľka uzla (príklad)
2| local ip | local port | global ip | global port |
3---------------------------------------------------------
4| 10.0.1.10 | 40000 | 192.168.1.5 | 50000 |
Potom vykoná SNAT a odošle paket von.
1# TCP paket odoslaný uzlom, Uzol => API server
2# Vykonanie 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 rovnaký ako v prípade smartfónového routera.
Pri komunikácii z externého prostredia klastra s Podom cez NodePort
Jedným zo spôsobov, ako sprístupniť služby Kubernetes externému prostrediu, je použitie služby NodePort. Služba NodePort otvorí špecifický port (NodePort) na všetkých uzloch v klastri a presmeruje prevádzku prichádzajúcu na tento port na pody patriace k službe. Externí používatelia môžu pristupovať k službe prostredníctvom IP adresy uzla klastra a NodePortu.
V tomto prípade hrá NAT dôležitú úlohu a súčasne dochádza k DNAT (Destination NAT) a SNAT (Source NAT). Keď prevádzka prichádza z externého prostredia na NodePort konkrétneho uzla, sieť Kubernetes musí túto prevádzku nakoniec doručiť podu, ktorý poskytuje danú službu. V tomto procese najprv dochádza k DNAT, kde sa cieľová IP adresa a číslo portu paketu zmenia na IP adresu a číslo portu podu.
Predpokladajme napríklad, že externý používateľ (203.0.113.10, port: 30000) pristupuje k službe prostredníctvom NodePortu (30001) na uzle Kubernetes (192.168.1.5). Predpokladáme, že táto služba interne odkazuje na pod s IP adresou 10.0.2.15 a portom 8080.
1Externý používateľ (203.0.113.10:30000) ==> Uzol Kubernetes (externý:192.168.1.5:30001 / interný: 10.0.1.1:42132) ==> Pod Kubernetes (10.0.2.15:8080)
V tomto prípade má uzol Kubernetes IP adresu uzla prístupnú z externého prostredia 192.168.1.5 a tiež IP adresu 10.0.1.1, ktorá je platná v rámci internej siete Kubernetes. (V závislosti od typu použitého CNI sa pravidlá týkajúce sa tohto môžu líšiť, ale v tomto článku sa vysvetlenie zakladá na Cilium.)
Keď požiadavka externého používateľa dorazí na uzol, uzol musí túto požiadavku preposlať podu, ktorý ju spracuje. V tomto okamihu uzol aplikuje nasledujúce pravidlá DNAT na zmenu cieľovej IP adresy a čísla portu paketu.
1# TCP paket, ktorý uzol pripravuje na odoslanie do podu
2# Po aplikácii 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 keď Pod odošle odpoveď na túto požiadavku, zdrojová IP adresa bude jeho vlastná IP adresa (10.0.2.15) a cieľová IP adresa bude IP adresa externého používateľa, ktorý požiadavku odoslal (203.0.113.10). V takom prípade externý používateľ dostane odpoveď od neexistujúcej IP adresy, o ktorú nežiadal, a jednoducho tento paket zahodí (DROP). Preto uzol Kubernetes dodatočne vykoná SNAT, keď Pod odošle odpoveďový paket externému prostrediu, čím zmení zdrojovú IP adresu paketu na IP adresu uzla (192.168.1.5 alebo internú sieťovú IP adresu 10.0.1.1, v tomto prípade sa pokračuje s 10.0.1.1).
1# TCP paket, ktorý uzol pripravuje na odoslanie do podu
2# Po aplikácii DNAT, SNAT
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 10.0.1.1 | 40021 | 10.0.2.15 | 8080 |
Teraz pod, ktorý prijal tento paket, odpovie uzlu, ktorý pôvodne prijal požiadavku cez NodePort. Uzol potom reverzne aplikuje rovnaké procesy DNAT a SNAT a vráti informácie externému používateľovi. Počas tohto procesu si každý uzol uloží nasledujúce informácie.
1# Interná DNAT tabuľka uzla
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 tabuľka uzla
7| original ip | original port | destination ip | destination port |
8------------------------------------------------------------------------
9| 203.0.113.10 | 30000 | 10.0.1.1 | 42132 |
Jadro veci
V systéme Linux sa tieto procesy NAT zvyčajne spravujú a vykonávajú subsystémom conntrack prostredníctvom iptables. V skutočnosti iné projekty CNI, ako napríklad flannel alebo calico, využívajú tento mechanizmus na riešenie vyššie uvedených problémov. Problém však spočíva v tom, že Cilium ignoruje celý tento tradičný sieťový stack Linuxu, pretože používa technológiu eBPF. 🤣

V dôsledku toho sa Cilium rozhodlo priamo implementovať iba tie funkcie, ktoré sú potrebné v prostredí Kubernetes z úloh, ktoré predtým vykonával existujúci sieťový stack Linuxu, ako je znázornené na obrázku vyššie. Preto pre vyššie uvedený proces SNAT spravuje Cilium SNAT tabuľku priamo vo forme LRU Hash Map (BPF_MAP_TYPE_LRU_HASH).
1# Cilium SNAT tabuľka
2# !Príklad pre jednoduché vysvetlenie. Skutočná definícia je: 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 a iné metadáta
4----------------------------------------------
5| | | | |
A keďže ide o Hash Table, pre rýchle vyhľadávanie existuje kľúč, ktorý používa kombináciu src ip, src port, dst ip, dst port.
Identifikácia problému
Fenomén - 1: Vyhľadávanie
V dôsledku toho vzniká jeden problém: paket prechádzajúci cez eBPF musí vyhľadať v Hash Table, aby overil, či potrebuje vykonať SNAT alebo DNAT pre proces NAT. Ako sme už videli, existujú dva typy paketov pre proces SNAT: 1. pakety smerujúce zvnútra von a 2. pakety prichádzajúce zvonku dovnútra ako odpoveď. Oba tieto pakety vyžadujú preklad prostredníctvom NAT a vyznačujú sa tým, že hodnoty src ip, port a dst ip, port sú vymenené.
Pre rýchle vyhľadávanie je preto potrebné buď pridať ďalšiu hodnotu do Hash Table s vymenenými src a dst hodnotami ako kľúčom, alebo vykonať vyhľadávanie v rovnakej Hash Table dvakrát pre všetky pakety, aj keď nesúvisia so SNAT. Cilium samozrejme, pre lepší výkon, zvolilo prístup vkladania rovnakých dát dvakrát pod názvom RevSNAT.
Fenomén - 2: LRU
Okrem vyššie uvedeného problému, keďže žiadny hardvér nemôže mať nekonečné zdroje, a najmä v hardvérovej logike, kde sa vyžaduje vysoký výkon a nie je možné použiť dynamické dátové štruktúry, je potrebné evictovať existujúce dáta, keď je zdrojov nedostatok. Cilium to vyriešilo použitím základnej dátovej štruktúry poskytovanej systémom Linux, LRU Hash Map.
Fenomén 1 + Fenomén 2 = Strata spojenia
https://github.com/cilium/cilium/issues/31643
To znamená, že pre jedno SNAT TCP (alebo UDP) spojenie:
- V jednej Hash Table sú rovnaké dáta zaznamenané dvakrát pre odchádzajúci a prichádzajúci paket.
- V situácii, keď môže byť kedykoľvek jeden z dvoch údajov stratený v dôsledku logiky LRU,
ak sa stratí čo i len jeden NAT záznam (ďalej len "entry") pre odchádzajúci alebo prichádzajúci paket v dôsledku LRU, nemusí sa NAT vykonať správne, čo môže viesť k úplnej strate spojenia.
Riešenie
Tu prichádzajú na rad už spomínané 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
Predtým, keď paket prechádzal cez eBPF, pokúšal sa vyhľadať v SNAT tabuľke pomocou kľúča vytvoreného kombináciou zdrojovej IP adresy, zdrojového portu, cieľovej IP adresy a cieľového portu. Ak kľúč neexistoval, vytvorili sa nové informácie NAT podľa pravidiel SNAT a zaznamenali sa do tabuľky. V prípade nového spojenia by to viedlo k normálnej komunikácii. Ak by bol kľúč neúmyselne odstránený LRU, vykonalo by sa nové NAT pomocou iného portu ako toho, ktorý sa používal pre existujúcu komunikáciu, čo by viedlo k odmietnutiu prijatia paketu na prijímacej strane a ukončeniu spojenia s RST paketom.
Prístup, ktorý zvolil tento PR, je jednoduchý.
Akonáhle je paket pozorovaný v akomkoľvek smere, obnovte aj záznam pre opačný smer.
Keď sa komunikácia pozoruje v ktoromkoľvek smere, oba záznamy sa obnovia, čím sa vzdialia od prioritných cieľov evikcie v logike LRU. Tým sa znižuje pravdepodobnosť scenára, v ktorom by sa odstránil iba jeden záznam, čo by viedlo k narušeniu celej komunikácie.
Hoci sa to môže zdať ako veľmi jednoduchý prístup a jednoduchá myšlienka, prostredníctvom tohto prístupu sa podarilo efektívne vyriešiť problém prerušenia spojenia v dôsledku predčasného vypršania informácií NAT pre odpovedné pakety a výrazne sa zlepšila stabilita systému. Dá sa povedať, že ide o dôležité zlepšenie, ktoré dosiahlo nasledujúce výsledky z hľadiska stability siete.

Záver
Domnievam sa, že tento PR je vynikajúcim príkladom, ktorý demonštruje, ako sa jednoduchá myšlienka, vychádzajúca zo základných znalostí CS o fungovaní NAT, môže prejaviť v komplexných systémoch a priniesť významné zmeny.
Ach, samozrejme, v tomto článku som priamo nepredstavil prípad komplexného systému. Avšak, aby som správne pochopil tento PR, musel som sa takmer 3 hodiny prosiť DeepSeek V3 0324 s pridaním slova Please, a výsledkom sú znalosti o Cilium +1 a nasledujúci obrázok. 😇
A čítaním problémov a PR som sa rozhodol napísať tento článok, poháňaný pocitom predzvesti, že niečo, čo som vytvoril v minulosti, mohlo spôsobiť problémy, a kompenzáciou za to.
Záver - 1
Mimochodom, pre tento problém existuje veľmi efektívny spôsob, ako sa mu vyhnúť. Keďže základnou príčinou problému je nedostatok miesta v NAT tabuľke, stačí zväčšiť veľkosť NAT tabuľky. :-D
Obdivujem a rešpektujem vášeň pána gyutaeb, ktorý namiesto toho, aby niekto iný, kto narazil na rovnaký problém, nič nenahlásil, len zväčšil veľkosť NAT tabuľky a utiekol, dôkladne analyzoval, pochopil a prispel k ekosystému Cilium s objektívnymi podkladmi, aj keď sa ho problém priamo netýkal.
To bol dôvod, prečo som sa rozhodol napísať tento článok.
Záver - 2
Tento príbeh v skutočnosti priamo nesúvisí s Gosudou, ktorá sa špecializuje na jazyk Go. Avšak, keďže jazyk Go a cloudový ekosystém sú úzko prepojené a prispievatelia Cilium majú určitú znalosť jazyka Go, rozhodol som sa preniesť obsah, ktorý by som inak uverejnil na svojom osobnom blogu, sem na Gosudu.
Keďže som dostal povolenie od jedného z administrátorov (mňa samého), myslím, že by to malo byť v poriadku.
Ak si myslíte, že to v poriadku nie je, rýchlo si to uložte do PDF, pretože neviem, kedy to bude odstránené. ;)
Záver - 3
Pri písaní tohto článku som výrazne využil pomoc Cline a Llama 4 Maveric. Hoci som začal analýzu s Gemini a prosil DeepSeek, skutočnú pomoc som dostal od Llama 4. Llama 4 je skvelá. Určite ju vyskúšajte.