Cilium príbeh: Pozoruhodné zvýšenie stability siete vďaka malej zmene kódu

Úvod
Nedávno som si pozrel PR môjho bývalého kolegu z predchádzajúcej práce k projektu Cilium.
bpf:nat: Restore ORG NAT entry if it's not found
Samotná zmena (bez testovacieho kódu) spočíva len v pridaní jedného bloku if. Vplyv tejto úpravy je však obrovský a osobne ma zaujalo, že jediný jednoduchý nápad môže výrazne prispieť k stabilite systému. Preto sa pokúsim tento príbeh vyrozprávať tak, aby ho pochopili aj ľudia bez odborných znalostí v oblasti sietí.
Základné pojmy
Ak existuje zariadenie, ktoré je pre dnešných ľudí rovnako nevyhnutné ako smartfón, je ním pravdepodobne Wi-Fi smerovač. Smerovač komunikuje so zariadeniami prostredníctvom štandardu Wi-Fi a plní úlohu sprostredkovateľa, ktorý zdieľa svoju verejnú IP adresu medzi viacerými zariadeniami. Technicky zaujímavou otázkou je, ako toto "zdieľanie" prebieha.
Technológiou, ktorá to umožňuje, je Network Address Translation (NAT). NAT využíva skutočnosť, že komunikácia TCP alebo UDP je tvorená kombináciou IP adresy a portu. Mapuje interné komunikačné páry súkromná IP:Port na nepoužívané páry verejná IP:Port, čím umožňuje komunikáciu s vonkajším svetom.
NAT
Keď sa zariadenie v internej sieti pokúsi pripojiť k internetu, NAT zariadenie zmení kombináciu súkromnej IP adresy a čísla portu tohto zariadenia na svoju verejnú IP adresu a nepoužité dynamické číslo portu. Tieto informácie o prevode sa zapisujú do tabuľky označovanej ako NAT tabuľka vnútri NAT zariadenia.
Predstavme si napríklad situáciu, keď sa smartfón v domácnosti (súkromná IP: a.a.a.a, port: 50000) pokúsi pripojiť k webovému serveru (verejná IP: c.c.c.c, port: 80).
1스마트폰 (a.a.a.a:50000) ==> 공유기 (b.b.b.b) ==> 웹 서버 (c.c.c.c:80)
Keď smerovač dostane požiadavku zo smartfónu, uvidí nasledujúci 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 |
Ak by sa tento paket odoslal priamo webovému serveru (c.c.c.c), odpoveď by sa nevrátila smartfónu s privátnou IP adresou (a.a.a.a), preto smerovač najprv nájde voľný port, ktorý momentálne nepoužíva žiadna komunikácia (napríklad 60000), a zapíše ho do internej NAT tabuľky.
1# 공유기 내부 NAT 테이블
2| local ip | local port | global ip | global port |
3-----------------------------------------------------
4| a.a.a.a | 50000 | b.b.b.b | 60000 |
Po zápise nového záznamu do NAT tabuľky smerovač zmení zdrojovú IP adresu a port z TCP paketu prijatého od smartfónu na svoju verejnú IP adresu (b.b.b.b) a novo pridelený port (60000) a odošle ho 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) teraz rozpozná požiadavku prichádzajúcu z portu 60000 smerovača (b.b.b.b) a odošle odpoveď smerovaču nasledovne.
1# 공유기가 받은 TCP 패킷, 웹 서버 => 공유기
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| c.c.c.c | 80 | b.b.b.b | 60000 |
Keď smerovač prijme túto odpoveď, vyhľadá v NAT tabuľke pôvodnú súkromnú IP adresu (a.a.a.a) a port (50000) zodpovedajúce cieľovej IP adrese (b.b.b.b) a portu (60000) a zmení cieľ paketu na smartfón.
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 |
Vďaka tomuto procesu má smartfón pocit, akoby komunikoval s webovým serverom priamo prostredníctvom vlastnej verejnej IP adresy. Vďaka NAT môže jedna verejná IP adresa umožniť súčasné používanie internetu viacerým interným zariadeniam.
Kubernetes
Kubernetes má jednu z najsofistikovanejších a najzložitejších sieťových štruktúr medzi nedávno vzniknutými technológiami. Samozrejme, aj NAT, o ktorom sme hovorili, sa v ňom využíva na viacerých miestach. Dva reprezentatívne prípady sú nasledujúce.
Komunikácia Podu s externým prostredím klastra
Pody v klastri Kubernetes zvyčajne dostávajú súkromné IP adresy, ktoré sú použiteľné iba v rámci siete klastra. Ak sa pod chce spojiť s externým internetom, pri odchádzajúcej premávke z klastra je potrebný NAT. Tento NAT sa zvyčajne vykonáva na uzle Kubernetes (serveri v klastri), na ktorom daný pod beží. Keď pod odošle paket smerujúci von, paket najprv dorazí na uzol, na ktorom pod beží. Uzol zmení zdrojovú IP adresu paketu (súkromnú IP podu) na svoju verejnú IP adresu, primerane upraví zdrojový port a odošle ho ďalej. Tento proces je podobný NAT procesu opísanému vyššie pri Wi-Fi smerovači.
Predpokladajme napríklad, že pod v klastri Kubernetes (10.0.1.10, port: 40000) sa pripája k externému API serveru (203.0.113.45, port: 443). Uzol Kubernetes potom dostane od podu nasledujúci paket.
1# 노드가 받은 TCP 패킷, 파드 => 노드
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# 노드 내부 NAT 테이블 (예시)
2| local ip | local port | global ip | global port |
3---------------------------------------------------------
4| 10.0.1.10 | 40000 | 192.168.1.5 | 50000 |
a následne vykoná SNAT a odošle paket smerom von.
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 |
Ďalší priebeh potom prebieha rovnako ako v prípade smartfónu za Wi-Fi smerovačom.
Prístup z vonkajšej siete k Podu cez NodePort
Jedným zo spôsobov, ako sprístupniť službu von v Kubernetes, je použitie služby NodePort. Služba NodePort otvorí určitý port (NodePort) na všetkých uzloch v klastri a prevádza premávku prichádzajúcu na tento port na pody patriace k službe. Externý používateľ môže pristupovať ku službe prostredníctvom IP adresy uzla v klastri a NodePort.
Pri tejto komunikácii zohráva NAT dôležitú úlohu, pričom DNAT (Destination NAT) a SNAT (Source NAT) prebiehajú súčasne. Keď premávka z vonkajšej siete dorazí na NodePort konkrétneho uzla, sieť Kubernetes ju musí nakoniec odovzdať podu, ktorý službu poskytuje. V tomto procese najprv nastane DNAT, ktorým 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 ku službe prostredníctvom NodePort (30001) uzla klastra Kubernetes (192.168.1.5). Predpokladajme tiež, že služba interne smeruje na pod s IP adresou 10.0.2.15 a portom 8080.
1외부 사용자 (203.0.113.10:30000) ==> 쿠버네티스 노드 (외부:192.168.1.5:30001 / 내부: 10.0.1.1:42132) ==> 쿠버네티스 파드 (10.0.2.15:8080)
Uzol Kubernetes v tomto prípade má súčasne externe dostupnú IP adresu 192.168.1.5 a IP adresu 10.0.1.1, ktorá je platná vo vnútornej sieti Kubernetes. (Podľa použitého CNI sa tieto pravidlá môžu líšiť, no tento článok vychádza z Cilium.)
Keď požiadavka externého používateľa dorazí na uzol, uzol ju musí odovzdať podu, ktorý ju spracuje. Uzol pritom aplikuje nasledujúce pravidlo DNAT, ktorým zmení cieľovú 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 keď pod odpovedá na túto požiadavku, zdrojová IP adresa je jeho vlastná IP adresa (10.0.2.15) a cieľová IP adresa je IP adresa externého používateľa, ktorý požiadavku odoslal (203.0.113.10). V takom prípade externý používateľ dostane odpoveď z neexistujúcej IP adresy, ktorú nikdy nežiadal, a paket jednoducho zahodí (DROP). Preto uzol Kubernetes pri odosielaní odpovede z podu smerom von vykoná aj SNAT a zmení zdrojovú IP adresu paketu na IP adresu uzla (192.168.1.5 alebo vnútornú IP adresu 10.0.1.1; v tomto prípade sa použije 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, ktorý tento paket dostane, potom odpovie uzlu, ktorý pôvodne prijal požiadavku cez NodePort, a uzol vykoná rovnaký postup DNAT a SNAT v opačnom smere a vráti informácie externému používateľovi. Pri tomto procese si každý uzol uloží nasledujúce informácie.
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á časť
Vo všeobecnosti sa v systéme Linux tieto procesy NAT spravujú a vykonávajú pomocou iptables prostredníctvom subsystému nazývaného conntrack. Iné CNI projekty, ako sú flannel alebo calico, využívajú tento mechanizmus na riešenie podobných problémov. Problém však spočíva v tom, že Cilium používa technológiu eBPF a úplne ignoruje tradičný sieťový zásobník Linuxu. 🤣

Výsledkom je, že Cilium z úloh, ktoré na obrázku vyššie plnil tradičný sieťový zásobník Linuxu, implementuje priamo len funkcie potrebné v prostredí Kubernetes. Preto Cilium pre SNAT tabuľku spravuje priamo 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| | | | |
Keďže ide o hash tabuľku, na rýchle vyhľadávanie sa používa kľúč, ktorý tvorí kombinácia src ip, src port, dst ip a dst port ako kľúč.
Identifikácia problému
Javus 1: Vyhľadávanie
Vzniká tak jeden problém: aby paket prechádzajúci cez eBPF zistil, či je potrebné vykonať SNAT alebo DNAT, musí vyhľadať v tejto hash tabuľke. Ako sme videli, pri SNAT existujú dva typy paketov: 1. paket smerujúci z vnútra von a 2. jeho odpoveď prichádzajúca zvonku dovnútra. Oba tieto pakety vyžadujú preklad NAT, no zároveň sa u nich vzájomne zamieňajú hodnoty src IP, portu a dst IP, portu.
Preto je na rýchle vyhľadávanie potrebné buď do hash tabuľky pridať ďalší záznam, ktorého kľúčom bude kombinácia s vymenenými src a dst hodnotami, alebo pri každom pakete, aj pri tom, ktorý s SNAT nesúvisí, dvakrát prehľadávať tú istú hash tabuľku. Cilium si pre lepší výkon zvolilo spôsob, pri ktorom sa tie isté údaje zapisujú dvakrát, a to pod názvom RevSNAT.
Javus 2: LRU
Nezávisle od tohto problému platí, že žiadny hardvér nemá nekonečné zdroje. Najmä v logike na úrovni hardvéru, ktorá vyžaduje vysoký výkon, nemožno použiť dynamické dátové štruktúry, preto je pri nedostatku zdrojov potrebné odstraňovať existujúce dáta. Cilium to rieši použitím LRU Hash Map, čo je základná dátová štruktúra poskytovaná Linuxom.
Javus 1 + Javus 2 = Strata spojenia
https://github.com/cilium/cilium/issues/31643
Pre jedno SNAT spojenie TCP (alebo UDP) to znamená, že:
- v jednej hash tabuľke sú pre odchádzajúci aj prichádzajúci paket zapísané rovnaké údaje dvakrát a
- podľa logiky LRU môže kedykoľvek dôjsť k strate jedného z týchto záznamov,
a ak sa niektorý záznam o NAT (ďalej len entry) pre odchádzajúci alebo prichádzajúci paket odstráni pomocou LRU, NAT sa nemusí správne vykonať, čo môže viesť k strate celého spojenia.
Riešenie
Tu sa objavujú vyššie 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
Pôvodne, keď paket prechádza cez eBPF, vytvorí sa z kombinácie src ip, src port, dst ip a dst port kľúč, ktorým sa vyhľadáva v SNAT tabuľke. Ak kľúč neexistuje, podľa pravidiel SNAT sa vytvorí nový záznam o NAT a zapíše sa do tabuľky. Ak ide o nové spojenie, ide o normálnu komunikáciu. Ak však bol kľúč náhodne odstránený pomocou LRU, komunikácia bude prebiehať cez iný port ako pôvodne, prijímajúca strana ju odmietne a spojenie sa ukončí paketom RST.
PR prináša jednoduchý prístup:
Keď sa zaznamená paket v ľubovoľnom smere, aktualizujme aj entry pre opačný smer.
Keď sa zaznamená komunikácia v ľubovoľnom smere, obe entry sa aktualizujú, čím sa vzdialia od prioritného cieľa eviction v LRU logike a znižuje sa pravdepodobnosť, že sa odstráni len jedna z nich, čo by viedlo k zrúteniu celej komunikácie.
Hoci ide o veľmi jednoduchý prístup a na prvý pohľad sa môže zdať ako jednoduchý nápad, týmto spôsobom sa účinne rieši problém, keď sa entry pre odpoveď najskôr vyprší a spojenie sa preruší, a výrazne sa zvyšuje stabilita systému. Z hľadiska sieťovej stability ide o dôležité zlepšenie s nasledujúcimi výsledkami.

Záver
Myslím si, že tento PR je veľmi dobrý príklad toho, ako môže jednoduchý nápad priniesť veľkú zmenu aj v zložitom systéme, počnúc základnými poznatkami o tom, ako NAT funguje.
Samozrejme, v tomto článku som priamo neukázal príklad zložitého systému. Aby som však tomuto PR naozaj porozumel, DeepSeek V3 0324 som takmer tri hodiny prosil, dokonca aj s prídavkom slova Please, a výsledkom bolo jedno poznanie o Ciliu navyše a nasledujúci obrázok. 😇
A keďže som čítal issues a PR, vznikla vo mne nepríjemná predtucha, že niečo, čo som kedysi vytvoril, mohlo spôsobiť tieto problémy, a ako akúsi kompenzáciu za tieto obavy som napísal tento článok.
Poznámka 1
Tento problém má veľmi účinné obchádzkové riešenie. Keďže hlavnou príčinou problému je nedostatok miesta v NAT tabuľke, stačí zväčšiť veľkosť NAT tabuľky. :-D
Niekto, kto by narazil na rovnaký problém, by bez nahlásenia issue len zväčšil NAT tabuľku a odišiel. Napriek tomu, že sa tento problém priamo netýkal jeho práce, gyutaeb dôkladne analyzoval a pochopil ho, prispel do ekosystému Cilium s objektívnymi dátovými podkladmi a ja obdivujem a rešpektujem jeho nadšenie.
Toto bol dôvod, prečo som sa rozhodol napísať tento článok.
Poznámka 2
Táto téma v skutočnosti nie je priamo späť s Gosuda, ktorá sa špecializuje na jazyk Go. Jazyk Go a cloudový ekosystém sú však úzko späté a prispievatelia do Cilium majú určité znalosti jazyka Go, preto som tento obsah z osobného blogu preniesol aj na Gosuda.
Myslím si, že je to pravdepodobne v poriadku, keďže na to dal súhlas jeden zo správcov (ja sám).
Ak si myslíte, že to nie je v poriadku, môže byť tento obsah kedykoľvek odstránený, preto si ho radšej čo najskôr uložte ako PDF. ;)
Poznámka 3
Pri písaní tohto článku mi výrazne pomohli Cline a Llama 4 Maverick. Hoci som analýzu začal s Gemini a prosil som DeepSeek, skutočnú pomoc som dostal od Llama 4. Llama 4 je dobrá, rozhodne ju vyskúšajte.