GoSuda

Cilium-tarina: pienen koodimuutoksen aikaansaama merkittävä verkon vakauden parantuminen

By iwanhae views ...

Johdanto

Hetki sitten selailin entisen työtoverini PR:ää Cilium-projektiin.

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

Muutoksen laajuus (testikoodia lukuun ottamatta) on pieni, käytännössä yhden if-lohkon lisäys. Tämän muutoksen vaikutus on kuitenkin valtava, ja minusta on mielenkiintoista, että yksinkertainen idea voi edistää järjestelmän vakautta merkittävästi. Päätin siksi kirjoittaa tapauksesta tarinan, jonka myös henkilöt ilman verkkoalan asiantuntemusta voivat ymmärtää.

Taustatiedot

Jos nykyihmisten välttämättömyyksistä on yksi, joka on älypuhelimen veroinen, se lienee Wi-Fi-reititin. Wi-Fi-reititin kommunikoi laitteiden kanssa Wi-Fi-standardin kautta ja jakaa sen julkisen IP-osoitteen useiden laitteiden käyttöön. Tässä syntyy tekninen erityiskysymys: miten se "jakaa"?

Tässä käytetään tekniikkaa nimeltä Network Address Translation (NAT). NAT on tekniikka, joka hyödyntää sitä, että TCP- tai UDP-liikenne muodostuu IP-osoitteen ja porttinumeron yhdistelmistä. Se kuvaa sisäisen liikenteen yksityinen IP:Portti -muodossa julkiseen julkinen IP:Portti -muotoon, jota ei tällä hetkellä käytetä, jotta ulkoinen yhteys on mahdollinen.

NAT

NAT-laite muuntaa sisäisen laitteen yksityisen IP-osoitteen ja porttinumeron yhdistelmän omaan julkiseen IP-osoitteeseensa ja käyttämättömään satunnaiseen porttinumeroon, kun laite yrittää muodostaa yhteyden ulkoiseen internetiin. Tämä muunnostieto kirjataan NAT-laitteen sisäiseen NAT-tauluun.

Esimerkiksi oletetaan, että kotona oleva älypuhelin (yksityinen IP: a.a.a.a, portti: 50000) yrittää muodostaa yhteyden verkkopalvelimeen (julkinen IP: c.c.c.c, portti: 80).

1älypuhelin (a.a.a.a:50000) ==> reititin (b.b.b.b) ==> verkkopalvelin (c.c.c.c:80)

Saatuaan älypuhelimen pyynnön reititin näkee seuraavanlaisen TCP-paketin.

1# Reitittimen vastaanottama TCP-paketti, älypuhelin => reititin
2| src ip  | src port | dst ip  | dst port |
3-------------------------------------------
4| a.a.a.a | 50000    | c.c.c.c | 80       |

Jos tämä paketti lähetettäisiin sellaisenaan verkkopalvelimelle (c.c.c.c), vastaus ei palaisi yksityisen IP-osoitteen omaavalle älypuhelimelle (a.a.a.a). Siksi reititin etsii ensin satunnaisen portin (esim. 60000), jota nykyinen yhteys ei käytä, ja kirjaa sen sisäiseen NAT-tauluunsa.

1# Reitittimen sisäinen NAT-taulu
2| local ip  | local port | global ip  | global port |
3-----------------------------------------------------
4| a.a.a.a   | 50000      | b.b.b.b    | 60000       |

Kun reititin on kirjannut uuden merkinnän NAT-tauluun, se muuttaa älypuhelimelta saadun TCP-paketin lähdeosoitteen ja -portin omaan julkiseen IP-osoitteeseensa (b.b.b.b) ja äsken varattuun porttinumeroon (60000) ja lähettää paketin verkkopalvelimelle.

1# Reitittimen lähettämä TCP-paketti, reititin => verkkopalvelin
2# SNAT suoritettu
3| src ip  | src port | dst ip  | dst port |
4-------------------------------------------
5| b.b.b.b | 60000    | c.c.c.c | 80       |

Verkkopalvelin (c.c.c.c) tunnistaa nyt pyynnön reitittimen (b.b.b.b) portista 60000 ja lähettää vastauspaketin reitittimelle seuraavasti.

1# Reitittimen vastaanottama TCP-paketti, verkkopalvelin => reititin
2| src ip  | src port | dst ip  | dst port |
3-------------------------------------------
4| c.c.c.c | 80       | b.b.b.b | 60000    |

Saatuaan tämän vastauspaketin reititin etsii NAT-taulusta kohde-IP-osoitetta (b.b.b.b) ja porttia (60000) vastaavan alkuperäisen yksityisen IP-osoitteen (a.a.a.a) ja portin (50000) ja vaihtaa paketin kohteen älypuhelimeksi.

1# Reitittimen lähettämä TCP-paketti, reititin => älypuhelin
2# DNAT suoritettu
3| src ip  | src port | dst ip  | dst port |
4-------------------------------------------
5| c.c.c.c | 80       | a.a.a.a | 50000    |

Tämän prosessin kautta älypuhelimesta tuntuu siltä, että se kommunikoi verkkopalvelimen kanssa suoraan omalla julkisella IP-osoitteellaan. NAT:n ansiosta useat sisäiset laitteet voivat käyttää internetiä samanaikaisesti yhdellä julkisella IP-osoitteella.

Kubernetes

Kubernetes on yksi viime aikojen monimutkaisimmista ja hienostuneimmista verkkoarkkitehtuureista. Luonnollisesti myös edellä mainittua NAT:ia käytetään monissa kohdissa. Tyypillisimmät esimerkit ovat seuraavat kaksi.

Kun Pod kommunikoi klusterin ulkopuolelle

Kubernetes-klusterin sisällä olevat podit saavat yleensä yksityisen IP-osoitteen, jolla ne voivat kommunikoida vain klusterin verkon sisällä. Siksi podin on käytettävä NAT:ia klusterista ulospäin suuntautuvalle liikenteelle, jotta se voi kommunikoida ulkoisen internetin kanssa. NAT suoritetaan tyypillisesti sillä Kubernetes-solmulla (klusterin yksittäisellä palvelimella), jolla kyseinen pod toimii. Kun pod lähettää ulospäin suuntautuvan paketin, paketti toimitetaan ensin podin sijaitsevalle solmulle. Solmu vaihtaa paketin lähdeosoitteen (podin yksityisen IP-osoitteen) omaan julkiseen IP-osoitteeseensa, muuttaa tarvittaessa myös lähdeporttia ja välittää paketin ulospäin. Prosessi muistuttaa edellä kuvattua Wi-Fi-reitittimen NAT-prosessia.

Oletetaan esimerkiksi, että Kubernetes-klusterin pod (10.0.1.10, portti: 40000) muodostaa yhteyden ulkoiseen API-palvelimeen (203.0.113.45, portti: 443). Tällöin Kubernetes-solmu saa podilta seuraavanlaisen paketin.

1# Solmun vastaanottama TCP-paketti, pod => solmu
2| src ip    | src port | dst ip        | dst port |
3---------------------------------------------------
4| 10.0.1.10 | 40000    | 203.0.113.45  | 443      |

Solmu kirjaa seuraavan tiedon

1# Solmun sisäinen NAT-taulu (esimerkki)
2| local ip    | local port | global ip     | global port |
3---------------------------------------------------------
4| 10.0.1.10   | 40000      | 192.168.1.5   | 50000       |

ja suorittaa tämän jälkeen SNAT:n ennen paketin lähettämistä ulospäin.

1# Solmun lähettämä TCP-paketti, solmu => API-palvelin
2# SNAT suoritettu
3| src ip      | src port | dst ip        | dst port |
4-----------------------------------------------------
5| 192.168.1.5 | 50000    | 203.0.113.45  | 443      |

Tämän jälkeen prosessi noudattaa samaa kulkua kuin edellä kuvattu älypuhelimen ja reitittimen esimerkki.

Kun ulkopuolinen taho kommunikoi podin kanssa NodePortin kautta

Yksi tapa paljastaa palvelu Kubernetes-klusterin ulkopuolelle on käyttää NodePort-palvelua. NodePort-palvelu avaa tietyn portin (NodePort) kaikilla klusterin solmuilla ja välittää tähän porttiin saapuvan liikenteen palveluun kuuluville podeille. Ulkoiset käyttäjät voivat päästä palveluun klusterin solmun IP-osoitteen ja NodePortin kautta.

Tässä tilanteessa NAT:illa on keskeinen rooli, ja erityisesti DNAT (Destination NAT) ja SNAT (Source NAT) tapahtuvat samanaikaisesti. Kun liikenne saapuu ulkopuolelta tietyn solmun NodePortiin, Kubernetes-verkon on lopulta välitettävä liikenne palvelua tarjoavalle podille. Tässä vaiheessa DNAT tapahtuu ensin, jolloin paketin kohde-IP-osoite ja porttinumero muuttuvat podin IP-osoitteeksi ja porttinumeroksi.

Oletetaan esimerkiksi, että klusterin ulkopuolinen käyttäjä (203.0.113.10, portti: 30000) käyttää palvelua Kubernetes-klusterin solmun (192.168.1.5) NodePortin (30001) kautta. Oletetaan lisäksi, että palvelu osoittaa sisäisesti podiin, jonka IP-osoite on 10.0.2.15 ja portti 8080.

1ulkoinen käyttäjä (203.0.113.10:30000) ==> Kubernetes-solmu (ulkoinen:192.168.1.5:30001 / sisäinen: 10.0.1.1:42132) ==> Kubernetes-pod (10.0.2.15:8080)

Tässä Kubernetes-solmulla on sekä ulkoisesti saavutettava IP-osoite 192.168.1.5 että sisäisessä Kubernetes-verkossa kelvollinen IP-osoite 10.0.1.1. (Käytetyn CNI:n mukaan tähän liittyvät käytännöt vaihtelevat, mutta tässä kirjoituksessa selitys perustuu Ciliumiin.)

Kun ulkoisen käyttäjän pyyntö saapuu solmuun, solmun on välitettävä pyyntö käsittelevälle podille. Tällöin solmu soveltaa seuraavaa DNAT-sääntöä, joka muuttaa paketin kohde-IP-osoitteen ja porttinumeron.

1# Solmun podille lähettämään valmisteilla olevaan TCP-pakettiin
2# DNAT soveltamisen jälkeen
3| src ip       | src port | dst ip    | dst port |
4---------------------------------------------------
5| 203.0.113.10 | 30000    | 10.0.2.15 | 8080    |

Tässä on tärkeää huomata, että kun pod lähettää vastauksen tähän pyyntöön, lähde-IP-osoite on sen oma IP-osoite (10.0.2.15) ja kohde-IP-osoite on pyynnön lähettäneen ulkoisen käyttäjän IP-osoite (203.0.113.10). Tässä tilanteessa ulkoinen käyttäjä saisi vastauksen osoitteesta, jota se ei ole koskaan pyytänyt eikä jota ei ole olemassa, ja se hylkäisi paketin. Siksi Kubernetes-solmu suorittaa podin lähettäessä vastauspaketin ulospäin lisäksi SNAT:n ja muuttaa paketin lähde-IP-osoitteen solmun IP-osoitteeksi (192.168.1.5 tai sisäisen verkon IP-osoitteeksi 10.0.1.1; tässä tapauksessa käytetään 10.0.1.1:tä).

1# Solmun podille lähettämään valmisteilla olevaan TCP-pakettiin
2# DNAT ja SNAT soveltamisen jälkeen
3| src ip       | src port | dst ip    | dst port |
4---------------------------------------------------
5| 10.0.1.1     | 40021    | 10.0.2.15 | 8080     |

Tämän paketin saava pod vastaa sille solmulle, josta NodePort-pyyntö alun perin tuli, ja solmu soveltaa samat DNAT- ja SNAT-vaiheet käänteisesti ja palauttaa tiedot ulkoiselle käyttäjälle. Tässä prosessissa kukin solmu tallentaa seuraavat tiedot.

1# Solmun sisäinen DNAT-taulu
2| original ip     | original port | destination ip  | destination port |
3------------------------------------------------------------------------
4| 192.168.1.5     | 30001         | 10.0.2.15       | 8080             |
5
6# Solmun sisäinen SNAT-taulu
7| original ip     | original port | destination ip  | destination port |
8------------------------------------------------------------------------
9| 203.0.113.10    | 30000         | 10.0.1.1        | 42132            |

Pääosa

Yleisesti ottaen Linuxissa nämä NAT-prosessit hallitaan ja suoritetaan iptables-työkalun avulla conntrack-nimisen alijärjestelmän kautta. Esimerkiksi flannel- ja calico-tyyppisissä muissa CNI-projekteissa tätä hyödynnetään edellä kuvatun ongelman ratkaisemiseen. Ongelma on kuitenkin siinä, että Cilium käyttää eBPF-tekniikkaa ja ohittaa perinteisen Linux-verkkopinon kokonaan. 🤣

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

Tämän seurauksena Cilium on valinnut tien, jossa se toteuttaa itse vain ne toiminnot, jotka Kubernetes-ympäristössä tarvitaan, niistä toiminnoista, joita perinteinen Linux-verkkopino hoitaa kuvassa. Siksi Cilium hallitsee edellä mainittua SNAT-prosessia itse ylläpitämällä SNAT-taulua LRU Hash Map -rakenteena (BPF_MAP_TYPE_LRU_HASH).

1# Cilium SNAT-taulu
2# !Yksinkertaistettu esimerkki. Varsinainen määritelmä: 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 ym. metatiedot
4----------------------------------------------
5|            |          |         |          |

Koska kyseessä on hajautustaulu, nopean haun mahdollistamiseksi käytetään avainta, ja avaimena käytetään src ip-, src port-, dst ip- ja dst port-yhdistelmää.

Ongelman tunnistaminen

Ilmiö 1: haku

Tästä seuraa yksi ongelma. eBPF:n läpi kulkevan paketin on haettava yllä mainitusta hajautustaulusta, vaatiiko paketti SNAT- tai DNAT-käsittelyn. Kuten edellä on nähty, SNAT-prosessissa on kahdenlaisia paketteja: 1. sisältä ulos menevä paketti ja 2. sen vastauksena ulkopuolelta sisään tuleva paketti. Molemmat paketit tarvitsevat NAT-muunnoksen, mutta niiden src ip- ja port- sekä dst ip- ja port-arvot ovat päinvastaiset.

Nopean haun vuoksi on joko lisättävä hajautustauluun yksi merkintä lisää, jonka avaimena on src- ja dst-arvot vaihdettuina, tai jokaisen paketin, myös sellaisen joka ei liity SNAT:iin, on haettava samasta hajautustaulusta kahdesti. Cilium valitsi luonnollisesti paremman suorituskyvyn saavuttamiseksi menetelmän, jossa sama data kirjoitetaan kahdesti, ja tämä on toteutettu RevSNAT-nimisenä ratkaisuna.

Ilmiö 2: LRU

Edellisestä ongelmasta riippumatta mikään laitteisto ei ole rajattomasti resursseja omaava, ja erityisesti nopeaa suorituskykyä vaativassa laitteistotason logiikassa dynaamisiakaan tietorakenteita ei voida käyttää. Kun resurssit loppuvat, vanhoja tietoja on poistettava. Cilium ratkaisi tämän käyttämällä Linuxin perusrakenteena tarjoamaa LRU Hash Map -rakennetta.

Ilmiö 1 + ilmiö 2 = yhteyden katoaminen

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

Eli yhden SNAT:tu TCP- (tai UDP-) yhteyden osalta:

  1. Yhteen hajautustauluun on kirjattu kaksi kertaa sama data, sekä ulos menevälle että sisään tulevalle paketille, ja
  2. LRU-logiikan mukaan toinen näistä tietueista voi kadota milloin tahansa

Jos mikä tahansa ulospäin menevän tai sisään tulevan paketin NAT-tieto (jäljempänä entry) poistuu LRU:n vuoksi, NAT ei onnistu oikein, ja tämä voi johtaa koko yhteyden katoamiseen.

Ratkaisu

Tässä kohtaa tulevat esiin aiemmin mainitut PR:t.

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

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

Aiemmin paketin kulkiessa eBPF:n läpi SNAT-taulusta haettiin avain, joka muodostettiin src ip-, src port-, dst ip- ja dst port -yhdistelmästä. Jos avainta ei löytynyt, luotiin SNAT-säännön mukaan uusi NAT-tieto ja kirjattiin taulukkoon. Uuden yhteyden tapauksessa tämä johtaa normaaliin liikenteeseen. Jos avain taas oli poistunut LRU:n vuoksi tahattomasti, uusi NAT tehtiin eri portilla kuin aiempi yhteys käytti, jolloin vastaanottava osapuoli hylkäisi paketin, ja yhteys päättyi RST-paketin myötä.

Tässä PR:n lähestymistapa on yksinkertainen:

Kun paketti havaitaan jompaankumpaan suuntaan, päivitetään myös sen vastakkaisen suunnan entry.

Kun liikenne havaitaan kummassa tahansa suunnassa, molemmat entryt päivittyvät, jolloin ne siirtyvät pois LRU-logiikan poistamisen ensisijaisesta kohteesta. Näin pienennetään todennäköisyyttä, että vain toinen entry poistuisi ja koko liikenne kaatuisi.

Vaikka lähestymistapa on hyvin yksinkertainen ja idea saattaa näyttää vaatimattomalta, sen avulla voidaan tehokkaasti ratkaista tilanne, jossa vastauspaketin NAT-tieto vanhenee ensin ja yhteys katkeaa, sekä parantaa järjestelmän vakautta merkittävästi. Lisäksi se on verkon vakauden kannalta merkittävä parannus, joka on tuottanut seuraavanlaisia tuloksia.

benchmark

Päätelmät

Uskon, että tämä PR on erinomainen esimerkki siitä, miten yksinkertainen idea voi tuottaa suuren muutoksen myös monimutkaisessa järjestelmässä, alkaen perus-CS-tiedosta siitä, miten NAT toimii.

Toki en tässä kirjoituksessa esitellyt suoraan monimutkaisen järjestelmän esimerkkejä. Mutta jotta ymmärsin tämän PR:n kunnolla, pyysin DeepSeek V3 0324 -mallia lisäämällä sanan Please lähes kolme tuntia, ja sain tuloksena lisää tietoa Ciliumista sekä alla olevan kuvan. 😇

diagram

Lisäksi kirjoitin tämän tekstin hieman syyllisyydentunteesta: luettuani aiheeseen liittyviä issueita ja PR:iä minulle heräsi epämiellyttävä aavistus siitä, että jokin aiemmin tekemäni asia oli saattanut aiheuttaa ongelman.

Jälkikirjoitus 1

Tähän ongelmaan on hyvin tehokas kiertotie. Ongelman perimmäinen syy on NAT-taulun tilan puute, joten riittää, kun NAT-taulun kokoa kasvatetaan. :-D

Joku olisi voinut kohdata saman ongelman, kasvattaa NAT-taulun kokoa ilman issuen luomista ja lähteä pois. Itse en ole siihen suoraan liittynyt, mutta gyutaeb-käyttäjän innostus oli huomattavaa: hän analysoi ongelman perusteellisesti, ymmärsi sen ja antoi panoksensa Cilium-ekosysteemiin objektiivisilla näyttöön perustuvilla tiedoilla. Ihailen ja arvostan tätä.

Tämä oli se syy, joka sai minut kirjoittamaan tämän tekstin.

Jälkikirjoitus 2

Tämä tarina ei oikeastaan sovi Go-kieleen keskittyvän Gosudan aihepiiriin. Go-kieli ja pilviekosysteemi liittyvät kuitenkin läheisesti toisiinsa, ja Ciliumin kontribuoijat hallitsevat Go-kieltä jossain määrin, joten päätin tuoda henkilökohtaiseen blogiin sopivan sisällön Gosudaan.

Ylläpitäjistä yksi (minä itse) on antanut tähän luvan, joten uskon, että se on kunnossa.

Jos ajattelet toisin, tämä voidaan poistaa milloin tahansa, joten tallenna se mieluiten PDF-muodossa mahdollisimman pian. ;)

Jälkikirjoitus 3

Tämän tekstin kirjoittamisessa auttoivat paljon Cline ja Llama 4 Maverick. Vaikka aloitin analyysin Geminillä ja pyysin apua DeepSeekiltä, todellinen apu tuli Llama 4:ltä. Llama 4 on hyvä, kokeile ehdottomasti.