A Cilium története: Hogyan eredményezett egy apró kódmódosítás lenyűgöző hálózati stabilitási javulást
Bevezetés
Nemrégiben volt alkalmam megtekinteni egy PR-t (Pull Requestet) egy korábbi kollégám Cilium projektjével kapcsolatban.
bpf:nat: Az ORG NAT bejegyzés visszaállítása, ha nem található
(A tesztkód kivételével) maga a módosítás mértéke csekély, csupán egy if
utasítás blokk hozzáadása. Azonban az általa kiváltott hatás óriási, és személy szerint érdekesnek találtam, hogy egy egyszerű ötlet milyen hatalmas mértékben hozzájárulhat a rendszer stabilitásához, ezért szeretnék erről beszélni, hogy még azok is könnyen megértsék, akik nem rendelkeznek hálózati szakértelemmel.
Háttérismeretek
Ha van valami, ami a modern ember számára legalább olyan fontos, mint az okostelefon, az valószínűleg a Wi-Fi router. A Wi-Fi router a Wi-Fi kommunikációs szabványon keresztül kommunikál az eszközökkel, és azt a szerepet tölti be, hogy megossza a nyilvános IP-címét több eszköz között. Az itt felmerülő technikai sajátosság az, hogy hogyan történik ez a „megosztás”.
Az ehhez használt technológia a Network Address Translation (NAT). A NAT egy olyan technológia, amely lehetővé teszi a külső kommunikációt azáltal, hogy a TCP vagy UDP kommunikációt, amely IP-címek és portinformációk kombinációjából áll, egy nem használt „nyilvános IP:Port” címre képezi le a „privát IP:Port” címről történő belső kommunikációt.
NAT
Amikor egy belső eszköz megpróbál hozzáférni a külső internethez, a NAT eszköz átalakítja az eszköz privát IP-címét és portszám-kombinációját a saját nyilvános IP-címévé és egy nem használt, véletlenszerű portszámmá. Ez az átalakítási információ a NAT eszközön belüli NAT táblázatba kerül rögzítésre.
Tegyük fel például, hogy egy okostelefon (privát IP: a.a.a.a
, port: 50000) a házban megpróbál csatlakozni egy webszerverhez (nyilvános IP: c.c.c.c
, port: 80).
1Okostelefon (a.a.a.a:50000) ==> Router (b.b.b.b) ==> Webszerver (c.c.c.c:80)
Amikor a router megkapja az okostelefon kérését, a következő TCP Packetet fogja látni:
1# A router által fogadott TCP csomag, okostelefon => router
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| a.a.a.a | 50000 | c.c.c.c | 80 |
Ha ezt a csomagot változatlanul elküldenénk a webszervernek (c.c.c.c
), akkor a privát IP-címmel rendelkező okostelefon (a.a.a.a
) nem kapna választ, ezért a router először megkeres egy véletlenszerű portot (pl. 60000), amely nem vesz részt a jelenlegi kommunikációban, és rögzíti azt a belső NAT táblázatban.
1# Router belső NAT táblázat
2| local ip | local port | global ip | global port |
3-----------------------------------------------------
4| a.a.a.a | 50000 | b.b.b.b | 60000 |
Miután a router új bejegyzést rögzített a NAT táblázatban, módosítja az okostelefonról kapott TCP csomag forrás IP-címét és portszámát a saját nyilvános IP-címére (b.b.b.b
) és az újonnan hozzárendelt portszámra (60000), majd elküldi a webszervernek.
1# A router által küldött TCP csomag, router => webszerver
2# SNAT végrehajtása
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| b.b.b.b | 60000 | c.c.c.c | 80 |
Most a webszerver (c.c.c.c
) a router (b.b.b.b
) 60000-es portjáról érkező kérésként ismeri fel, és a válaszcsomagot a következőképpen küldi el a routernek:
1# A router által fogadott TCP csomag, webszerver => router
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| c.c.c.c | 80 | b.b.b.b | 60000 |
Amikor a router megkapja ezt a válaszcsomagot, megkeresi a NAT táblázatban a cél IP-címhez (b.b.b.b
) és portszámhoz (60000) tartozó eredeti privát IP-címet (a.a.a.a
) és portszámot (50000), majd módosítja a csomag célját az okostelefonra.
1# A router által küldött TCP csomag, router => okostelefon
2# DNAT végrehajtása
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| c.c.c.c | 80 | a.a.a.a | 50000 |
Ennek a folyamatnak köszönhetően az okostelefon úgy érzi, mintha közvetlenül kommunikálna a webszerverrel a saját nyilvános IP-címén keresztül. A NAT-nak köszönhetően egyetlen nyilvános IP-címmel több belső eszköz is egyszerre használhatja az internetet.
Kubernetes
A Kubernetes a közelmúltban megjelent technológiák közül a legkifinomultabb és legösszetettebb hálózati struktúrával rendelkezik. És természetesen az előbb említett NAT is számos helyen felhasználásra kerül. A két legjellemzőbb eset a következő:
Amikor a Pod a clusteren kívüli kommunikációt végez
A Kubernetes clusteren belüli Podok általában privát IP-címeket kapnak, amelyekkel csak a cluster hálózatán belül kommunikálhatnak. Ezért, ha egy Pod kommunikálni akar a külső internettel, NAT-ra van szükség a clusteren kívülre irányuló forgalomhoz. Ebben az esetben a NAT-ot elsősorban azon a Kubernetes Node-on (a cluster egyes szerverein) hajtják végre, amelyen a Pod fut. Amikor egy Pod külső irányú csomagot küld, az először a Podhoz tartozó Node-ra kerül. A Node módosítja a csomag forrás IP-címét (a Pod privát IP-címét) a saját nyilvános IP-címére, és megfelelően módosítja a forrásportot is, majd továbbítja azt kifelé. Ez a folyamat hasonló a Wi-Fi routereknél korábban ismertetett NAT folyamathoz.
Tegyük fel például, hogy egy Pod (10.0.1.10
, port: 40000) egy Kubernetes clusteren belül csatlakozik egy külső API szerverhez (203.0.113.45
, port: 443). A Kubernetes Node a következő csomagot kapja a Podtól:
1# A Node által fogadott TCP csomag, Pod => Node
2| src ip | src port | dst ip | dst port |
3---------------------------------------------------
4| 10.0.1.10 | 40000 | 203.0.113.45 | 443 |
A Node a következő információkat rögzíti:
1# Node belső NAT táblázat (példa)
2| local ip | local port | global ip | global port |
3---------------------------------------------------------
4| 10.0.1.10 | 40000 | 192.168.1.5 | 50000 |
Ezután SNAT-ot hajt végre, és elküldi a csomagot kifelé, a következőképpen:
1# A Node által küldött TCP csomag, Node => API szerver
2# SNAT végrehajtása
3| src ip | src port | dst ip | dst port |
4-----------------------------------------------------
5| 192.168.1.5 | 50000 | 203.0.113.45 | 443 |
Ezt követően a folyamat ugyanaz, mint az okostelefon router esetében.
Amikor a külső cluster NodePort-on keresztül kommunikál a Pod-dal
A Kubernetesben a szolgáltatások külső felé történő közzétételének egyik módja a NodePort szolgáltatás használata. A NodePort szolgáltatás megnyit egy adott portot (NodePort) a cluster összes Node-ján, és az ezen a porton érkező forgalmat a szolgáltatáshoz tartozó Podokhoz irányítja. A külső felhasználók a cluster Node-jának IP-címén és a NodePort-on keresztül érhetik el a szolgáltatást.
Ebben az esetben a NAT kulcsfontosságú szerepet játszik, és különösen a DNAT (Destination NAT) és a SNAT (Source NAT) egyszerre történik. Amikor külső forgalom érkezik egy adott Node NodePort-jához, a Kubernetes hálózatnak ezt a forgalmat végül a szolgáltatást nyújtó Podhoz kell továbbítania. Ennek során először DNAT történik, amely megváltoztatja a csomag cél IP-címét és portszámát a Pod IP-címére és portszámára.
Tegyük fel például, hogy egy külső felhasználó (203.0.113.10
, port: 30000) a Kubernetes cluster egyik Node-jának (192.168.1.5
) NodePort-ján (30001
) keresztül fér hozzá egy szolgáltatáshoz. Tegyük fel, hogy ez a szolgáltatás belsőleg egy olyan Podra mutat, amelynek IP-címe 10.0.2.15
és portja 8080
.
1Külső felhasználó (203.0.113.10:30000) ==> Kubernetes Node (külső:192.168.1.5:30001 / belső: 10.0.1.1:42132) ==> Kubernetes Pod (10.0.2.15:8080)
Itt a Kubernetes Node rendelkezik a Node külsőleg elérhető IP-címével, a 192.168.1.5-tel, és a Kubernetes belső hálózatán belül érvényes 10.0.1.1 IP-címmel is. (A használt CNI típusától függően a kapcsolódó irányelvek változhatnak, de ebben a cikkben a Ciliumot vesszük alapul.)
Amikor a külső felhasználó kérése megérkezik a Node-hoz, a Node-nak továbbítania kell ezt a kérést a Podhoz, amely feldolgozza azt. Ekkor a Node a következő DNAT szabályokat alkalmazza a csomag cél IP-címének és portszámának módosításához:
1# TCP csomag, amelyet a Node a Podnak küldeni készül
2# DNAT alkalmazása után
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 203.0.113.10 | 30000 | 10.0.2.15 | 8080 |
Itt fontos megjegyezni, hogy amikor a Pod választ küld erre a kérésre, a forrás IP-cím a saját IP-címe (10.0.2.15
) lesz, a cél IP-cím pedig a kérést küldő külső felhasználó IP-címe (203.0.113.10
) lesz. Ebben az esetben a külső felhasználó olyan nem létező IP-címről kap választ, amelyre nem kért, és egyszerűen DROP-olja a csomagot. Ezért a Kubernetes Node további SNAT-ot hajt végre, amikor a Pod válaszcsomagot küld kifelé, hogy a csomag forrás IP-címét a Node IP-címére (192.168.1.5 vagy a belső hálózati IP, 10.0.1.1, ebben az esetben a 10.0.1.1-re) módosítsa.
1# TCP csomag, amelyet a Node a Podnak küldeni készül
2# DNAT, SNAT alkalmazása után
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 10.0.1.1 | 40021 | 10.0.2.15 | 8080 |
Most a csomagot fogadó Pod válaszol a Node-nak, amely eredetileg NodePort-on keresztül kapta a kérést, és a Node pontosan ugyanazt a DNAT és SNAT folyamatot fordítottan alkalmazza, hogy visszaküldje az információt a külső felhasználónak. Ebben a folyamatban minden Node a következő információkat tárolja:
1# Node belső DNAT táblázat
2| original ip | original port | destination ip | destination port |
3------------------------------------------------------------------------
4| 192.168.1.5 | 30001 | 10.0.2.15 | 8080 |
5
6# Node belső SNAT táblázat
7| original ip | original port | destination ip | destination port |
8------------------------------------------------------------------------
9| 203.0.113.10 | 30000 | 10.0.1.1 | 42132 |
Fő rész
Általában Linuxban ezeket a NAT folyamatokat az iptables kezeli és működteti egy conntrack nevű alrendszeren keresztül. Valójában más CNI projektek, mint például a flannel vagy a calico, ezt használják a fent említett problémák kezelésére. A probléma azonban az, hogy a Cilium az eBPF technológiát használja, és teljesen figyelmen kívül hagyja a hagyományos Linux hálózati stack-et. 🤣
Ennek eredményeként a Cilium azt az utat választotta, hogy a fenti ábrán látható hagyományos Linux hálózati stack által végzett feladatok közül csak a Kubernetes környezetben szükséges funkciókat implementálja közvetlenül. Ezért az előzőekben említett SNAT folyamat tekintetében a Cilium közvetlenül kezeli az SNAT táblázatot LRU Hash Map (BPF_MAP_TYPE_LRU_HASH) formájában.
1# Cilium SNAT táblázat
2# !Példa az egyszerű magyarázathoz. A tényleges definíció: 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 és egyéb metaadatok
4----------------------------------------------
5| | | | |
És mivel ez egy Hash Table, a gyors keresés érdekében kulcsértékek léteznek, amelyekhez a src ip
, src port
, dst ip
, dst port
kombinációját használják kulcsként.
Problémafelismerés
1. jelenség: Keresés
Ennek eredményeként felmerül egy probléma: az eBPF-en áthaladó csomagok esetében ellenőrizni kell, hogy szükség van-e SNAT vagy DNAT folyamatra, ehhez pedig lekérdezést kell végrehajtani a fenti Hash Table-ből. Mint láttuk, az SNAT folyamat során kétféle csomag létezik: 1. belsőből kifelé irányuló csomag, és 2. a válaszként kívülről befelé érkező csomag. Ez a két csomag NAT átalakítást igényel, és a src ip, port és dst ip, port értékei felcserélődnek.
Ezért a gyorsabb keresés érdekében vagy egy további értéket kell hozzáadni a Hash Table-hez a felcserélt src és dst értékekkel mint kulccsal, vagy minden csomag esetében kétszer kell lekérdezni ugyanazt a Hash Table-t, még akkor is, ha a csomag nem kapcsolódik az SNAT-hoz. Természetesen a Cilium a jobb teljesítmény érdekében RevSNAT néven azt a módszert választotta, hogy ugyanazt az adatot kétszer szúrja be.
2. jelenség: LRU
Emellett, a fenti problémától függetlenül, minden hardveren végtelen erőforrások nem létezhetnek, és különösen egy olyan hardver szintű logikánál, amely gyors teljesítményt igényel, és ahol dinamikus adatstruktúrák sem használhatók, szükség van a meglévő adatok evictálására, amikor hiány van erőforrásokból. A Cilium ezt úgy oldotta meg, hogy az alapértelmezett Linux által biztosított adatstruktúrát, az LRU Hash Map-et használja.
1. jelenség + 2. jelenség = Kapcsolatvesztés
https://github.com/cilium/cilium/issues/31643
Vagyis egy SNAT-olt TCP (vagy UDP) kapcsolatra vonatkozóan:
- Ugyanaz az adat kétszer van rögzítve egy Hash Table-ben a kimenő és bejövő csomagok számára.
- Az LRU logika szerint bármikor elveszhet az adatok egyike.
Ha a külső irányú vagy a belső irányú csomag NAT információi (továbbiakban entry) közül legalább az egyik törlődik az LRU által, akkor a NAT nem tudja megfelelően végrehajtani a feladatát, ami a teljes kapcsolat elvesztéséhez vezethet.
Megoldás
Itt jönnek szóba az előbb említett PR-ek.
bpf:nat: egy NAT bejegyzés visszaállítása, ha a REV NAT nem található
bpf:nat: Az ORG NAT bejegyzés visszaállítása, ha nem található
Korábban, amikor egy csomag áthaladt az eBPF-en, a SNAT táblázatból próbáltak megkeresni egy kulcsot a forrás IP, forrás port, cél IP és cél port kombinációjával. Ha a kulcs nem létezett, akkor a SNAT szabályok szerint új NAT információt generáltak és rögzítettek a táblázatban. Ha új kapcsolatról volt szó, ez normális kommunikációhoz vezetett volna, de ha a kulcs véletlenül eltávolításra került az LRU miatt, akkor a NAT új portot rendelt volna hozzá, ami eltér a korábban használt porttól, és a fogadó fél elutasította volna a csomagot, ami RST csomaggal a kapcsolat megszakadásához vezetett volna.
Itt a fenti PR által alkalmazott megközelítés egyszerű:
Ha egy csomagot bármely irányból megfigyelünk, akkor az ellenkező irányú bejegyzést is frissítsük.
Ha a kommunikációt bármely irányból megfigyelik, mindkét bejegyzés frissül, eltávolítva őket az LRU logika kiürítési prioritási célpontjai közül, ezáltal csökkentve annak esélyét, hogy csak az egyik bejegyzés törlődjön, ami a teljes kommunikáció összeomlását eredményezné.
Ez egy nagyon egyszerű megközelítés, és egyszerű ötletnek tűnhet, de ezzel a megközelítéssel hatékonyan megoldották azt a problémát, hogy a válaszcsomagok NAT információi előbb lejárnak, ami a kapcsolat megszakadásához vezet, és jelentősen javították a rendszer stabilitását. Emellett a hálózati stabilitás szempontjából is fontos javulást eredményezett az alábbi ábrán látható eredményekkel.
Konklúzió
Úgy gondolom, hogy ez a PR kiváló példája annak, hogy egy egyszerű ötlet milyen nagy változást hozhat még egy összetett rendszeren belül is, kezdve az alapvető CS ismeretektől, hogy hogyan működik a NAT.
Ó, persze, nem mutattam be közvetlenül az összetett rendszer példáját ebben a cikkben. De ahhoz, hogy megfelelően megértsem ezt a PR-t, közel 3 órán keresztül könyörögtem a DeepSeek V3 0324
-nek, még a Please
szót is hozzátettem, és ennek eredményeként Cilium tudás +1-et szereztem, valamint a következő képet kaptam: 😇
És miközben olvastam a problémákat és a PR-eket, írtam ezt a cikket, hogy kompenzáljam a baljóslatú érzéseimet, miszerint valami általam korábban létrehozott dolog okozhatta a problémát.
Utóirat - 1
Mellesleg, erre a problémára létezik egy nagyon hatékony elkerülési módszer. Mivel a probléma alapvető oka a NAT táblázat helyhiánya, egyszerűen növelni kell a NAT táblázat méretét. :-D
Míg valaki más, amikor ugyanazzal a problémával találkozott, nem hagyott problémát, csak megnövelte a NAT táblázat méretét és elmenekült, addig gyutaeb úr, aki alaposan elemezte és megértette a problémát, objektív adatokat szolgáltatott, és hozzájárult a Cilium ökoszisztémához, annak ellenére, hogy a probléma nem érintette közvetlenül, lenyűgözött a szenvedélyével, és tisztelem őt.
Ez volt az oka annak, hogy úgy döntöttem, megírom ezt a cikket.
Utóirat - 2
Ez a történet valójában nem illik közvetlenül a Gosuda témájához, amely a Go nyelvre specializálódott. Azonban a Go nyelv és a felhő ökoszisztéma szorosan kapcsolódik egymáshoz, és a Cilium hozzájárulói bizonyos mértékig jártasak a Go nyelvben, ezért elhoztam ide egy olyan tartalmat, amelyet személyes blogon is közzétehettem volna.
Mivel az egyik adminisztrátor (én magam) engedélyt adott, valószínűleg rendben lesz.
Ha úgy gondolja, hogy nem, akkor gyorsan mentse el PDF-be, mert nem tudhatja, mikor törlődik. ;)
Utóirat - 3
A cikk megírásában nagy segítséget kaptam a Cline és a Llama 4 Maveric-től. Bár a Gemini-vel kezdtem az elemzést, és a DeepSeek-nek könyörögtem, valójában a Llama 4 segített. A Llama 4 kiváló. Mindenképpen próbálja ki.