GoSuda

A Cilium története: Hogyan eredményezett egy apró kódmódosítás figyelemre méltó hálózati stabilitási javulást

By iwanhae
views ...

Bevezetés

Nemrégiben belefutottam egy korábbi kollégám Cilium projekthez benyújtott PR-jába.

bpf:nat: Restore ORG NAT entry if it's not found

(A tesztkód kivételével) maga a módosítás mennyisége csekély, mindössze egy if utasításblokk hozzáadását jelenti. Azonban ennek a módosításnak a hatása hatalmas, és személy szerint érdekesnek találom, hogy egy egyszerű ötlet milyen nagyban hozzájárulhat a rendszer stabilitásához. Ezért szeretném elmesélni ezt a történetet úgy, hogy a hálózati szakértelemmel nem rendelkezők is könnyen megérthessék.

Háttérismeretek

Ha van valami, ami legalább annyira fontos a modern emberek számára, 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 megosztja a saját nyilvános IP-címét, hogy több eszköz is használhassa. Az ebből adódó technikai különlegesség az, hogy hogyan történik ez a "megosztás".

Az itt 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 belső kommunikációhoz használt privát IP:Port párost egy jelenleg nem használt nyilvános IP:Port párhoz rendeli, mivel a TCP vagy UDP kommunikáció IP-cím és portinformációk kombinációjából áll.

NAT

Amikor egy belső eszköz megpróbál csatlakozni az internethez, a NAT eszköz átalakítja az adott eszköz privát IP-címének és portszámának kombinációját a saját nyilvános IP-címévé és egy nem használt, véletlenszerű portszámává. Ez az átalakítási információ a NAT eszköz belső NAT táblájában kerül rögzítésre.

Például, tegyük fel, hogy az otthoni okostelefon (privát IP: a.a.a.a, port: 50000) 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 Packet-et fogja látni:

1# 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 talál egy véletlenszerű portot (pl. 60000), amely jelenleg nem vesz részt a kommunikációban, és rögzíti azt a belső NAT táblájában.

1# Router belső NAT táblája
2| local ip  | local port | global ip  | global port |
3-----------------------------------------------------
4| a.a.a.a   | 50000      | b.b.b.b    | 60000       |

A router, miután rögzítette az új bejegyzést a NAT táblában, 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 kiosztott portszámra (60000), majd elküldi azt a webszervernek.

1# 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) úgy érzékeli, hogy a kérés a router (b.b.b.b) 60000-es portjáról érkezett, és a válaszcsomagot a következőképpen küldi el a routernek:

1# 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, a NAT táblában megkeresi a cél IP-cím (b.b.b.b) és portszám (60000) alapján az 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# 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    |

Ezen folyamaton keresztül az okostelefon úgy érzi, mintha közvetlenül nyilvános IP-címmel kommunikálna a webszerverrel. A NAT-nak köszönhetően egyetlen nyilvános IP-címmel több belső eszköz is egyidejűleg használhatja az internetet.

Kubernetes

A Kubernetes az utóbbi időben megjelent technológiák közül az egyik 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. Az alábbi két példa a legjellemzőbb esetekre:

A Pod-on belüli kommunikáció a clusteren kívüli világgal

A Kubernetes clusteren belüli Pod-ok általában privát IP-címeket kapnak, amelyek csak a cluster hálózatán belül kommunikálhatnak. Ezért, ha egy Pod külső internettel akar kommunikálni, NAT-ra van szükség a clusteren kívülre irányuló forgalomhoz. Ebben az esetben a NAT-ot általában azon a Kubernetes Node-on (a cluster egyes szerverein) hajtják végre, amelyen a Pod fut. Amikor egy Pod külső csomagot küld, az először a Pod-hoz tartozó Node-hoz 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ás portot is, majd továbbítja azt kifelé. Ez a folyamat hasonló a Wi-Fi routereknél korábban leírt NAT folyamathoz.

Például, tegyük fel, hogy egy Pod a Kubernetes clusterben (10.0.1.10, port: 40000) egy külső API szerverhez (203.0.113.45, port: 443) akar csatlakozni. Ekkor a Kubernetes Node a következő csomagot kapja a Pod-tól:

1# 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ája (példa)
2| local ip    | local port | global ip     | global port |
3---------------------------------------------------------
4| 10.0.1.10   | 40000      | 192.168.1.5   | 50000       |

Ezt követően SNAT-ot hajt végre, majd elküldi a csomagot kifelé:

1# 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 és a router esetében.

Külső kommunikáció a Pod-dal NodePort-on keresztül a clusteren kívülről

A Kubernetesben a szolgáltatások külső elérhetővé tételének egyik módja a NodePort szolgáltatások 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ó Pod-okhoz továbbí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 fontos szerepet játszik, különösen a DNAT (Destination NAT) és a SNAT (Source NAT) egyidejűleg történik. Amikor külső forgalom érkezik egy adott Node NodePort-jára, a Kubernetes hálózatnak ezt a forgalmat végül a szolgáltatást nyújtó Pod-hoz kell továbbítania. Ebben a folyamatban először DNAT történik, amelynek során a csomag cél IP-címe és portszáma a Pod IP-címére és portszámára változik.

Például, tegyük fel, 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. Ez a szolgáltatás belsőleg egy Pod-ra 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 mind a külsőleg elérhető 192.168.1.5 IP-címmel, mind a belső Kubernetes hálózaton érvényes 10.0.1.1 IP-címmel. (A használt CNI típusától függően ezek a szabályok változhatnak, de ebben a cikkben a Ciliumot vesszük alapul.)

Amikor a külső felhasználó kérése megérkezik a Node-ra, a Node-nak továbbítania kell ezt a kérést a feldolgozó Pod-hoz. 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ára:

1# A Node által a Pod-hoz küldésre előkészített TCP csomag
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, 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), és a cél IP-cím 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ó egy olyan nem létező IP-címről kap választ, amelyre soha nem kért választ, é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-címre, jelen esetben 10.0.1.1-re) módosítsa.

1# A Node által a Pod-hoz küldésre előkészített TCP csomag
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 a NodePort-on keresztül kapta a kérést, és a Node a DNAT és SNAT folyamatokat fordítottan alkalmazva adja vissza 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ája
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ája
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 az ilyen NAT folyamatokat az iptables kezeli és működteti a conntrack 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 ezeket a hagyományos Linux hálózati stackeket. 🤣

https://cilium.io/blog/2021/05/11/cni-benchmark/

Ennek eredményeként a Cilium azt az utat választotta, hogy a fent látható ábrán szereplő, a hagyományos Linux hálózati stack által végzett feladatok közül csak azokat a funkciókat implementálja közvetlenül, amelyekre Kubernetes környezetben szükség van. Ezért az előbb említett SNAT folyamatot illetően 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ábla
2# !Egyszerűsített példa a 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ékekre van szükség, amelyekhez a src ip, src port, dst ip, dst port kombinációját használják kulcsként.

A probléma azonosítása

Jelenség - 1: Keresés

Ennek következtében felmerül egy probléma: az eBPF-en áthaladó csomagoknak ellenőrizniük kell, hogy szükség van-e SNAT vagy DNAT folyamatra a NAT során, ehhez a fenti Hash Table-t kell lekérdezni. Ahogy korábban láttuk, az SNAT folyamat kétféle csomagot tartalmaz: 1. belsőből kifelé irányuló csomagokat, és 2. külsőből befelé irányuló válaszcsomagokat. Ez a két csomag NAT átalakítást igényel, és jellemzője, hogy a src ip, port és dst ip, port értékei felcserélődnek.

Ezért a gyorsabb keresés érdekében vagy hozzá kell adni egy további értéket a Hash Table-hez a src és dst felcserélt értékével kulcsként, vagy minden csomag esetében kétszer kell lekérdezni ugyanazt a Hash Table-t, még akkor is, ha az SNAT-tal nem kapcsolatos. Természetesen a Cilium a jobb teljesítmény érdekében RevSNAT néven a kétszeres adatbeviteli módszert választotta.

Jelenség - 2: LRU

Emellett, a fent említett problémától függetlenül, a hardverek erőforrásai nem korlátlanok, és mivel a hardveres logika gyors teljesítményt igényel, és dinamikus adatszerkezetek sem használhatók, szükség van a meglévő adatok evictálására, amikor az erőforrások szűkösek. A Cilium ezt úgy oldotta meg, hogy a Linux által alapértelmezetten biztosított LRU Hash Map adatszerkezetet használta.

Jelenség 1 + Jelenség 2 = Kapcsolatvesztés

https://github.com/cilium/cilium/issues/31643

Vagyis egy SNAT-olt TCP (vagy UDP) kapcsolatra vonatkozóan:

  1. Ugyanabban a Hash Table-ben kétszer van rögzítve ugyanaz az adat a kimenő és bejövő csomagok számára.
  2. Az LRU logika szerint bármelyik adat bármikor elveszhet.

Ha a kimenő vagy bejövő csomagok NAT információjának (továbbiakban entry) egyike is elveszik az LRU miatt, akkor a NAT nem tud megfelelően működni, ami a teljes kapcsolat elvesztéséhez vezethet.

Megoldás

Itt lépnek képbe a korábban említett PR-ek.

bpf:nat: restore a NAT entry if its REV NAT is not found

bpf:nat: Restore ORG NAT entry if it's not found

Korábban, amikor egy csomag áthaladt az eBPF-en, megpróbált kulcsot generálni a SNAT táblából a src ip, src port, dst ip, dst port kombinációjával, és lekérdezést hajtott végre. Ha a kulcs nem létezett, akkor a SNAT szabályok szerint új NAT információt generált és rögzített a táblában. Új kapcsolat esetén ez normális kommunikációhoz vezetett, de ha a kulcs véletlenül eltávolításra került az LRU miatt, akkor az új NAT-ot az eredeti kommunikációhoz használt porttól eltérő porton hajtották végre, ami miatt a fogadó fél elutasította a csomagot, és a kapcsolat RST csomaggal lezárult.

Az említett PR-ek megközelítése egyszerű:

Ha egy csomagot bármely irányban megfigyelnek, akkor a fordított irányú bejegyzést is frissíteni kell.

Ha a kommunikációt bármely irányban megfigyelik, mindkét bejegyzés frissül, így azok távolabb kerülnek az LRU logika Eviction prioritási célpontjaitól. Ezáltal csökken annak a valószínűsége, hogy csak az egyik bejegyzés törlődik, ami a teljes kommunikáció összeomlásához vezetne.

Ez egy nagyon egyszerű megközelítésnek és egy egyszerű ötletnek tűnhet, de ez a megközelítés hatékonyan megoldotta azt a problémát, hogy a válaszcsomagok NAT információja idő előtt lejárt, ami a kapcsolat megszakadásához vezetett, és jelentősen javította a rendszer stabilitását. Ezenkívül ez egy fontos fejlesztés a hálózati stabilitás szempontjából, amely a következő eredményeket érte el:

benchmark

Konklúzió

Úgy gondolom, hogy ez a PR egy kiváló példa arra, hogy egy egyszerű ötlet milyen nagy változásokat hozhat egy összetett rendszeren belül, kezdve az alapvető CS ismeretekkel arról, hogyan működik a NAT.

Ó, persze, nem mutattam be közvetlenül az összetett rendszerek eseteit ebben a cikkben. De ahhoz, hogy ezt a PR-t megfelelően megértsem, majdnem 3 órát könyörögtem a DeepSeek V3 0324-nek, még a "Please" szót is hozzátéve, és ennek eredményeként Cilium tudást +1-et és a következő képet kaptam. 😇

diagram

És azzal a kompenzációs érzéssel írom ezt a cikket, hogy valami, amit korábban készítettem, valószínűleg problémákat okozott, miután elolvastam a problémákat és a PR-eket.

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ábla helyhiánya, a NAT tábla méretét kell növelni. :-D

Csodálom és tisztelem gyutaeb úr lelkesedését, aki alaposan elemezte és megértette a problémát, objektív bizonyítékokkal támasztotta alá, és hozzájárult a Cilium ökoszisztémájához, annak ellenére, hogy a probléma nem érintette közvetlenül őt, miközben mások, akik ugyanazzal a problémával találkoztak, anélkül, hogy bejelentették volna, egyszerűen megnövelték a NAT tábla méretét és elmenekültek.

Ez volt az a pillanat, amikor elhatároztam, hogy megírom ezt a cikket.

Utóirat - 2

Ez a történet valójában nem kapcsolódik közvetlenül a Go nyelvre szakosodott Gosudához. Azonban a Go nyelv és a felhő ökoszisztéma szorosan összefügg, és a Cilium hozzájárulói bizonyos szinten jártasak a Go nyelvben, ezért úgy gondoltam, hogy egy személyes blogbejegyzés tartalmát áthozom a Gosudához.

Mivel az egyik adminisztrátor (én magam) engedélyezte, azt hiszem, rendben lesz.

Ha úgy gondolja, hogy nem, akkor gyorsan mentse el PDF-be, mielőtt törlésre kerülne. ;)

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-től kaptam segítséget. A Llama 4 nagyszerű. Feltétlenül próbálja ki.