Príbeh Ciliumu: Ako drobná zmena v kóde viedla k pozoruhodnému zlepšeniu stability siete
Úvod
Nedávno som mal možnosť prezrieť si PR k projektu Cilium od bývalého kolegu.
bpf:nat: Restore ORG NAT entry if it's not not found
(Okrem testovacieho kódu) samotná úprava je minimálna, ide len o pridanie jedného bloku if
príkazu. Avšak vplyv tejto úpravy je obrovský a fakt, že jednoduchá myšlienka môže výrazne prispieť k stabilite systému, mi osobne pripadá zaujímavý. Preto by som sa rád pokúsil vysvetliť tento prípad tak, aby ho ľahko pochopili aj ľudia bez odborných znalostí v oblasti sietí.
Základné znalosti
Ak existuje nejaká nevyhnutnosť pre moderného človeka rovnako dôležitá ako smartfón, potom je to pravdepodobne Wi-Fi router. Wi-Fi router komunikuje so zariadeniami prostredníctvom komunikačného štandardu Wi-Fi a slúži na zdieľanie svojej verejnej IP adresy, aby ju mohli používať viaceré zariadenia. Technická špecifikácia, ktorá tu vzniká, je: ako sa uskutočňuje toto „zdieľanie“?
Technológia, ktorá sa tu používa, je Network Address Translation (NAT). NAT je technológia, ktorá umožňuje internú komunikáciu, ktorá prebieha s použitím privátnej IP:Port
, mapovať na nepoužívanú verejnú IP:Port
, a tým umožniť komunikáciu s externým prostredím, vzhľadom na to, že komunikácia TCP alebo UDP je založená na kombinácii IP adresy a informácie o porte.
NAT
Zariadenie NAT, keď sa interné zariadenie pokúša pripojiť k externému internetu, transformuje kombináciu privátnej IP adresy a čísla portu daného zariadenia na svoju verejnú IP adresu a ľubovoľné nepoužívané číslo portu. Táto informácia o transformácii sa zaznamenáva do NAT tabuľky vo vnútri zariadenia NAT.
Predstavme si napríklad, že smartfón v domácnosti (privátna 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
) s privátnou IP adresou. Preto router najprv nájde ľubovoľný port, ktorý sa práve nepoužíva (napr. 60000), 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í novej položky 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 odošle ho webovému serveru.
1# TCP paket odoslaný routerom, router => webový server
2# Vykonaný 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 odpoveďový paket, vyhľadá v NAT tabuľke pôvodnú privátnu IP adresu (a.a.a.a
) a číslo portu (50000) zodpovedajúce 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# Vykonaný 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 sa smartfónu zdá, akoby komunikoval s webovým serverom priamo s vlastnou verejnou IP adresou. Vďaka NATu môže jedna verejná IP adresa umožniť viacerým interným zariadeniam súčasne používať internet.
Kubernetes
Kubernetes disponuje jednou z najsofistikovanejších a najkomplexnejších sieťových štruktúr spomedzi technológií, ktoré sa objavili v poslednej dobe. A samozrejme, spomínaný NAT sa využíva na rôznych miestach. Typickými príkladmi sú nasledujúce dva prípady.
Keď Pod vnútri klastra komunikuje s externým prostredím klastra
Pody v rámci klastra Kubernetes zvyčajne dostávajú súkromné IP adresy, ktoré im umožňujú komunikovať len v rámci siete klastra. Preto, ak má Pod komunikovať s externým internetom, je pre odchádzajúci prevádzku z klastra potrebný NAT. V tomto prípade sa NAT zvyčajne vykonáva na uzle Kubernetes (každý server v klastri), na ktorom je daný 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 odošle von. Tento proces je podobný procesu NATu opísanému pre Wi-Fi router.
Napríklad, ak Pod v klastri Kubernetes (10.0.1.10
, port: 40000) pristupuje k externému API serveru (203.0.113.45
, port: 443), uzol Kubernetes 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, ako je uvedené nižšie.
1# TCP paket odoslaný uzlom, uzol => API server
2# Vykonaný 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 routera pre smartfón.
Pri komunikácii s Podom z externého klastra cez NodePort
Jedným zo spôsobov, ako sprístupniť služby v Kubernetes externe, je použitie služby NodePort. Služba NodePort otvára špecifický port (NodePort) na všetkých uzloch v klastri a presmerováva 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, pričom DNAT (Destination NAT) a SNAT (Source NAT) sa vykonávajú súčasne. Keď prevádzka prichádza z externého prostredia na NodePort konkrétneho uzla, sieť Kubernetes musí tento prevádzku nakoniec presmerovať na Pod, ktorý poskytuje danú službu. V tomto procese najprv dochádza k DNAT, pri ktorom sa IP adresa a číslo portu cieľa 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
) uzla Kubernetes (192.168.1.5
). Predpokladajme, ž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 192.168.1.5, ktorá je prístupná zvonku, a IP adresu 10.0.1.1, ktorá je platná v internej sieti Kubernetes. (V závislosti od typu použitého CNI sa pravidlá s tým spojené líšia, ale v tomto článku sa vysvetlenie riadi Cilium.)
Keď požiadavka externého používateľa dorazí na uzol, uzol ju musí presmerovať na Pod, ktorý ju spracuje. V tomto momente uzol aplikuje nasledujúce pravidlo DNAT, aby zmenil cieľovú IP adresu a číslo portu paketu.
1# TCP paket, ktorý sa uzol chystá odoslať Podu
2# Po aplikovaní 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ý odoslal požiadavku (203.0.113.10
). V takom prípade externý používateľ dostane odpoveď z neexistujúcej IP adresy, o ktorú nepožiadal, a jednoducho tento paket zahodí (DROP). Preto uzol Kubernetes dodatočne vykoná SNAT, keď Pod odošle odpoveďový paket externe, čí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 použije 10.0.1.1).
1# TCP paket, ktorý sa uzol chystá odoslať Podu
2# Po aplikovaní 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, odošle odpoveď uzlu, ktorý pôvodne prijal požiadavku na NodePort, a uzol aplikuje rovnaký proces DNAT a SNAT v opačnom poradí, aby vrátil informácie externému používateľovi. Počas tohto procesu si každý uzol uloží nasledujúce informácie:
1# Interná tabuľka DNAT 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á tabuľka SNAT uzla
7| original ip | original port | destination ip | destination port |
8------------------------------------------------------------------------
9| 203.0.113.10 | 30000 | 10.0.1.1 | 42132 |
Hlavná časť
V systéme Linux sú tieto procesy NAT zvyčajne spravované a vykonávané podsystémom conntrack prostredníctvom iptables. V skutočnosti iné projekty CNI, ako napríklad flannel alebo calico, využívajú túto funkcionalitu na riešenie vyššie uvedených problémov. Problémom však je, že Cilium používa technológiu eBPF, čím úplne ignoruje tento tradičný sieťový stack Linuxu. 🤣
V dôsledku toho sa Cilium rozhodlo implementovať iba tie funkcie, ktoré sú potrebné pre Kubernetes, z úloh, ktoré predtým vykonával existujúci sieťový stack Linuxu, ako je znázornené na obrázku vyššie. Preto, pokiaľ ide o vyššie spomenutý proces SNAT, Cilium priamo spravuje tabuľku SNAT vo forme LRU Hash Map (BPF_MAP_TYPE_LRU_HASH).
1# Cilium SNAT tabuľka
2# !Príklad pre jednoduchšie vysvetlenie. Skutočná definícia: 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 atď. metadáta
4----------------------------------------------
5| | | | |
A keďže ide o Hash Table, pre rýchle vyhľadávanie existuje kľúčová hodnota, ktorá používa kombináciu src ip
, src port
, dst ip
, dst port
ako kľúčovú hodnotu.
Identifikácia problému
Fenomén - 1: Vyhľadávanie
Vzniká tak jeden problém: keď paket prechádza cez eBPF, na overenie, či je potrebné vykonať proces SNAT alebo DNAT pre proces NAT, je potrebné vykonať vyhľadávanie v uvedenej Hash Table. Ako sme už videli, v procese SNAT existujú dva typy paketov. 1. Pakety smerujúce zvnútra von a 2. Pakety prichádzajúce zvonku dovnútra ako ich odpoveď. Tieto dva pakety vyžadujú transformáciu pre proces NAT a vyznačujú sa tým, že hodnoty src ip, port a dst ip, port sú navzájom prehodené.
Pre rýchle vyhľadávanie je preto potrebné buď pridať do Hash Table ďalšiu hodnotu s kľúčom, kde sú src a dst prehodené, alebo vykonať vyhľadávanie v tej istej Hash Table dvakrát pre všetky pakety, aj keď nesúvisia so SNAT. Cilium, samozrejme, zvolilo prístup vkladania rovnakých dát dvakrát pod názvom RevSNAT pre lepší výkon.
Fenomén - 2: LRU
Okrem vyššie uvedeného problému platí, že žiadny hardvér nemôže mať nekonečné zdroje, a najmä v situácii, keď je potrebný rýchly výkon hardvérovej logiky, nie je možné použiť dynamické dátové štruktúry. Preto je potrebné odstrániť existujúce dáta, keď je nedostatok zdrojov. Cilium to vyriešilo použitím štandardnej dátovej štruktúry poskytovanej systémom Linux, LRU Hash Map.
Fenomén 1 + Fenomén 2 = Strata pripojenia
https://github.com/cilium/cilium/issues/31643
To znamená, že pre jedno SNAT TCP (alebo UDP) pripojenie:
- V jednej Hash Table sú dvakrát zaznamenané rovnaké dáta pre odchádzajúce a prichádzajúce pakety.
- Podľa logiky LRU môže kedykoľvek dôjsť k strate jedného z týchto dvoch dát.
Ak sa stratí čo i len jedna informácia o NAT (ďalej len entry) pre odchádzajúci alebo prichádzajúci paket v dôsledku LRU, nemôže sa správne vykonať NAT, čo môže viesť k strate celého pripojenia.
Riešenie
Tu prichádzajú na scénu 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, sa pokúšalo vyhľadať v SNAT tabuľke vytvorením kľúča z kombinácie src ip, src port, dst ip, dst port. Ak kľúč neexistoval, podľa pravidla SNAT sa vytvorila nová NAT informácia a zaznamenala sa do tabuľky. Ak by išlo o nové pripojenie, viedlo by to k normálnej komunikácii. Ak by však bol kľúč nechtiac odstránený v dôsledku LRU, nová NAT by sa vykonala s iným portom ako ten, ktorý sa používal pre existujúcu komunikáciu, čo by viedlo k odmietnutiu prijímania paketu na prijímajúcej strane a ukončeniu pripojenia s RST paketom.
PR, o ktorom sa hovorí, zaujal jednoduchý prístup:
Akonáhle je paket pozorovaný v akomkoľvek smere, obnovte aj záznam pre opačný smer.
Keď sa komunikácia pozoruje v akomkoľvek smere, oba záznamy sa obnovia, čím sa vzdialia od prioritného cieľa Eviction logiky LRU. Tým sa znižuje pravdepodobnosť scenára, v ktorom by sa odstránil iba jeden záznam, čo by viedlo k zrúteniu celej komunikácie.
Hoci sa tento prístup môže zdať veľmi jednoduchý a ako jednoduchý nápad, prostredníctvom neho sa podarilo efektívne vyriešiť problém prerušenia spojenia v dôsledku predčasného vypršania informácií o NAT pre odpoveďové pakety a výrazne sa zvýšila stabilita systému. Okrem toho 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ý ilustruje, ako jednoduchý nápad môže priniesť obrovské zmeny aj v rámci komplexného systému, počnúc základnými znalosťami CS o tom, ako funguje NAT.
Ach, samozrejme, v tomto článku som vám priamo neukázal príklad komplexného systému. Avšak, aby som správne pochopil tento PR, tri hodiny som úpenlivo prosil DeepSeek V3 0324
, dokonca s pridaním slova Please
, a výsledkom bolo získanie vedomostí o Cilium +1 a nasledujúceho obrázku. 😇
A čítaním problémov a PR som sa rozhodol napísať tento článok ako kompenzáciu za zlé predtuchy, že nejaký problém mohol vzniknúť kvôli niečomu, čo som vytvoril v minulosti.
Následky - 1
Pre informáciu, existuje veľmi efektívny spôsob, ako sa vyhnúť tomuto problému. Základnou príčinou problému je nedostatok miesta v NAT tabuľke, takže stačí zväčšiť veľkosť NAT tabuľky. :-D
Obdivujem a rešpektujem vášeň pána gyutaeba, ktorý, aj keď sa ho tento problém priamo netýkal, dôkladne ho analyzoval, pochopil a prispel k ekosystému Cilium s objektívnymi dátami, zatiaľ čo niekto iný by si v rovnakej situácii len zväčšil NAT tabuľku a utiekol bez zanechania problému.
Toto bol dôvod, prečo som sa rozhodol napísať tento článok.
Následky - 2
Tento príbeh sa síce priamo netýka Gosudy, ktorá sa špecializuje na jazyk Go. Avšak jazyk Go a cloudový ekosystém sú úzko prepojené a prispievatelia Cilium majú určitú znalosť jazyka Go, takže som sa rozhodol preniesť obsah, ktorý by som inak zverejnil na osobnom blogu, na Gosudu.
Keďže som na to dostal súhlas 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 nie je v poriadku, radšej si to ihneď uložte ako PDF, pretože sa to môže kedykoľvek odstrániť. ;)
Následky - 3
Pri písaní tohto článku mi výrazne pomohli Cline a Llama 4 Maveric. Hoci som analýzu začal s Gemini a prosil som DeepSeek, skutočnú pomoc som dostal od Llama 4. Llama 4 je skvelá. Určite ju vyskúšajte.