Cilium-tarina: Pieni koodimuutos, suuri verkon vakauden paraneminen
Johdanto
Näin hiljattain entisen työkaverini PR:n Cilium-projektista.
bpf:nat: Restore ORG NAT entry if it's not found
(Testikoodia lukuun ottamatta) muutoksen määrä on vähäinen, vain yhden if
-lauseen lohkon lisääminen. Kuitenkin tämän muutoksen vaikutus on valtava, ja mielestäni on henkilökohtaisesti mielenkiintoista, kuinka yksinkertainen idea voi merkittävästi edistää järjestelmän vakautta. Siksi aion kertoa tästä tapauksesta siten, että myös verkon asiantuntemusta omaamattomat henkilöt voivat helposti ymmärtää sen.
Taustatietoa
Jos älypuhelimen lisäksi on jokin muu nykyihmisten välttämättömyys, se on luultavasti Wi-Fi-reititin. Wi-Fi-reititin kommunikoi laitteiden kanssa Wi-Fi-standardin kautta ja jakaa julkisen IP-osoitteensa useiden laitteiden käyttöön. Tässä syntyvä tekninen erikoisuus on: miten "jakaminen" tapahtuu?
Tässä käytetty teknologia on Network Address Translation (NAT). NAT on teknologia, joka mahdollistaa ulkoisen kommunikoinnin kartoittamalla sisäisen kommunikoinnin, joka koostuu yksityinen IP:portti
-yhdistelmästä, tällä hetkellä käyttämättömään julkinen IP:portti
-yhdistelmään, koska TCP- tai UDP-kommunikointi perustuu IP-osoitteen ja porttitiedon yhdistelmään.
NAT
Kun sisäinen laite yrittää muodostaa yhteyden ulkoiseen Internetiin, NAT-laite muuntaa kyseisen laitteen yksityisen IP-osoitteen ja porttinumeron yhdistelmän omaksi julkiseksi IP-osoitteekseen ja vapaaksi satunnaiseksi porttinumeroksi. Tämä muunnostieto tallennetaan NAT-laitteen sisäiseen NAT-taulukkoon.
Oletetaan esimerkiksi, 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)
Kun reititin vastaanottaa älypuhelimen pyynnön, se näkee seuraavan 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 suoraan verkkopalvelimelle (c.c.c.c
), vastausta ei palaisi älypuhelimelle (a.a.a.a
), jolla on yksityinen IP-osoite. Siksi reititin etsii ensin satunnaisen portin (esim. 60000), joka ei ole tällä hetkellä käytössä tiedonsiirrossa, ja tallentaa sen sisäiseen NAT-taulukkoonsa.
1# Reitittimen sisäinen NAT-taulukko
2| local ip | local port | global ip | global port |
3-----------------------------------------------------
4| a.a.a.a | 50000 | b.b.b.b | 60000 |
Kun reititin on tallentanut uuden merkinnän NAT-taulukkoon, se muuttaa älypuhelimesta vastaanotetun TCP-paketin lähde-IP-osoitteen ja porttinumeron omaksi julkiseksi IP-osoitteekseen (b.b.b.b
) ja vastaavasti uudeksi allokoiduksi porttinumeroksi (60000) ja lähettää sen 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 |
Nyt verkkopalvelin (c.c.c.c
) tunnistaa pyynnön tulevaksi 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 |
Kun reititin vastaanottaa tämän vastauspaketin, se etsii NAT-taulukostaan kohde-IP-osoitetta (b.b.b.b
) ja porttinumeroa (60000) vastaavan alkuperäisen yksityisen IP-osoitteen (a.a.a.a
) ja porttinumeron (50000) ja muuttaa 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 älypuhelin kokee kommunikoivansa verkkopalvelimen kanssa suoraan, ikään kuin sillä olisi oma julkinen IP-osoite. NATin ansiosta useat sisäiset laitteet voivat käyttää Internetiä samanaikaisesti yhdellä julkisella IP-osoitteella.
Kubernetes
Kubernetesilla on viime aikoina julkaistuista teknologioista hienostunein ja monimutkaisin verkkorakenne. Ja luonnollisesti myös edellä mainittua NATia hyödynnetään monissa paikoissa. Kaksi tyypillisintä esimerkkiä ovat seuraavat:
Kun Pod kommunikoi klusterin ulkopuolisen kanssa
Kubernetes-klusterin sisäisille podeille annetaan yleensä yksityisiä IP-osoitteita, jotka mahdollistavat kommunikoinnin vain klusteriverkon sisällä. Siksi podin on käytettävä NATia, jotta se voi kommunikoida ulkoisen Internetin kanssa. Tässä tapauksessa NAT suoritetaan yleensä Kubernetes-solmussa (klusterin jokaisessa palvelimessa), jossa kyseinen pod on käynnissä. Kun pod lähettää paketin ulos, paketti toimitetaan ensin podin solmuun. Solmu muuttaa paketin lähde-IP-osoitteen (podin yksityinen IP-osoite) omaksi julkiseksi IP-osoitteekseen ja muuttaa myös lähdeportin asianmukaisesti ennen paketin lähettämistä ulos. Tämä prosessi on samanlainen kuin edellä Wi-Fi-reitittimen yhteydessä kuvattu NAT-prosessi.
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 vastaanottaa 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 seuraavat tiedot:
1# Solmun sisäinen NAT-taulukko (esimerkki)
2| local ip | local port | global ip | global port |
3---------------------------------------------------------
4| 10.0.1.10 | 40000 | 192.168.1.5 | 50000 |
Ja suorittaa sitten SNATin seuraavasti ennen paketin lähettämistä ulos:
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 etenee samalla tavalla kuin älypuhelimen ja reitittimen tapauksessa.
Kun klusterin ulkopuolinen kommunikoi Podin kanssa NodePortin kautta
Yksi tapa avata Kubernetes-palvelu ulkopuolisille on käyttää NodePort-palvelua. NodePort-palvelu avaa tietyn portin (NodePort) kaikissa klusterin solmuissa ja ohjaa tämän portin kautta tulevan liikenteen palveluun kuuluville podeille. Ulkoiset käyttäjät voivat käyttää palvelua klusterin solmun IP-osoitteen ja NodePortin kautta.
Tässä NATilla on tärkeä rooli, ja erityisesti DNAT (Destination NAT) ja SNAT (Source NAT) tapahtuvat samanaikaisesti. Kun liikenne saapuu tietyn solmun NodePortiin ulkopuolelta, Kubernetes-verkko ohjaa tämän liikenteen lopulta palvelua tarjoavaan podiin. Tässä prosessissa tapahtuu ensin DNAT, joka muuttaa paketin kohde-IP-osoitteen ja porttinumeron podin IP-osoitteeksi ja porttinumeroksi.
Oletetaan esimerkiksi, että klusterin ulkopuolinen käyttäjä (203.0.113.10
, portti: 30000) käyttää palvelua Kubernetes-klusterin yhden solmun (192.168.1.5
) NodePortin (30001
) kautta. Oletetaan, että tämä palvelu osoittaa sisäisesti Podiin, jonka IP-osoite on 10.0.2.15
ja portti 8080
.
1Ulkopuolinen 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 saavutettavissa oleva IP-osoite 192.168.1.5 että sisäisessä Kubernetes-verkossa kelvollinen IP-osoite 10.0.1.1. (Käytetyn CNI-tyypin mukaan tähän liittyvät käytännöt vaihtelevat, mutta tässä artikkelissa selitys perustuu Ciliumiin.)
Kun ulkoisen käyttäjän pyyntö saapuu solmuun, solmun on välitettävä tämä pyyntö sitä käsittelevälle Podille. Tällöin solmu soveltaa seuraavia DNAT-sääntöjä muuttaakseen paketin kohde-IP-osoitteen ja porttinumeron:
1# Solmun Podille lähetettäväksi valmisteltava TCP-paketti
2# DNAT sovellettu
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 203.0.113.10 | 30000 | 10.0.2.15 | 8080 |
Tärkeä seikka tässä on, 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ällöin ulkoinen käyttäjä vastaanottaa vastauksen IP-osoitteesta, jota se ei ole koskaan pyytänyt ja joka ei ole olemassa, ja DROPaa kyseisen paketin. Siksi Kubernetes-solmu suorittaa ylimääräisen SNATin, kun pod lähettää vastauspaketin ulos, muuttaen paketin lähde-IP-osoitteen solmun IP-osoitteeksi (192.168.1.5 tai sisäisen verkon IP-osoite 10.0.1.1, tässä tapauksessa 10.0.1.1).
1# Solmun Podille lähetettäväksi valmisteltava TCP-paketti
2# DNAT, SNAT sovellettu
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 10.0.1.1 | 40021 | 10.0.2.15 | 8080 |
Nyt kyseisen paketin vastaanottanut Pod vastaa solmulle, joka alun perin vastaanotti pyynnön NodePortin kautta, ja solmu soveltaa samaa DNAT- ja SNAT-prosessia käänteisesti palauttaakseen tiedot ulkoiselle käyttäjälle. Tässä prosessissa kukin solmu tallentaa seuraavat tiedot:
1# Solmun sisäinen DNAT-taulukko
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-taulukko
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äitä NAT-prosesseja hallitaan ja suoritetaan iptablesin kautta conntrack-alijärjestelmän avulla. Itse asiassa muut CNI-projektit, kuten flannel tai calico, käyttävät tätä ratkaistakseen edellä mainittuja ongelmia. Ongelmana on kuitenkin se, että Cilium käyttää eBPF-teknologiaa ja ohittaa kokonaan nämä perinteiset Linux-verkkopinot. 🤣
Tämän seurauksena Cilium valitsi suoraan toteuttaa Kubernetes-tilanteessa tarvittavat toiminnot niistä tehtävistä, jotka perinteinen Linux-verkkopino hoiti, kuten yllä olevassa kuvassa näkyy. Siksi Cilium hallinnoi edellä mainittua SNAT-prosessia suoraan SNAT-taulukon avulla LRU Hash Map (BPF_MAP_TYPE_LRU_HASH) -muodossa.
1# Cilium SNAT-taulukko
2# !Esimerkki yksinkertaisen selityksen vuoksi. Todellinen 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 jne. metatiedot
4----------------------------------------------
5| | | | |
Ja koska kyseessä on Hash Table, se käyttää nopeaa hakua varten avainta, joka koostuu src ip
, src port
, dst ip
, dst port
-yhdistelmästä yhdistelmänä.
Ongelman tunnistaminen
Ilmiö - 1: Haku
Tästä johtuu yksi ongelma: eBPF:n läpi kulkevien pakettien on tarkistettava yllä olevaa Hash Tablea varmistaakseen, tarvitsevatko ne SNAT- tai DNAT-prosessia NAT-prosessin osana. Kuten olemme nähneet, SNAT-prosessissa on kahdenlaisia paketteja: 1. sisältä ulos menevät paketit ja 2. ulkoa sisään tulevat vastauspaketit. Nämä kaksi pakettia vaativat muunnosta NAT-prosessissa, ja niiden src ip, port ja dst ip, port arvot vaihtuvat.
Siksi nopeaa hakua varten on joko lisättävä yksi arvo lisää Hash Tableen avaimella, jossa src ja dst on käännetty, tai suoritettava sama Hash Table -haku kahdesti jokaiselle paketille, vaikka ne eivät liittyisikään SNATiin. Luonnollisesti Cilium on valinnut paremman suorituskyvyn vuoksi menetelmän, jossa sama data syötetään kahdesti nimellä RevSNAT.
Ilmiö - 2: LRU
Ja edellä mainitusta ongelmasta riippumatta, koska millään laitteistolla ei voi olla rajattomia resursseja, ja erityisesti nopeaa suorituskykyä vaativassa laitteistotason logiikassa, jossa dynaamisia tietorakenteita ei voida käyttää, on olemassa tarve poistaa olemassa olevia tietoja resurssien puutteen vuoksi. Cilium on ratkaissut tämän käyttämällä LRU Hash Mapia, joka on Linuxin perusarvoisesti tarjoama tietorakenne.
Ilmiö 1 + Ilmiö 2 = Yhteyden katkeaminen
https://github.com/cilium/cilium/issues/31643
Eli yhden SNAT-muunnetun TCP- (tai UDP-) yhteyden osalta:
- Yhdessä Hash Table -taulukossa sama data on tallennettu kahdesti ulos meneville ja sisään tuleville paketeille.
- LRU-logiikan mukaisesti toinen näistä tiedoista voi kadota milloin tahansa.
Jos jompikumpi ulos menevän tai sisään tulevan paketin NAT-tiedosta (jäljempänä entry) katoaa LRU:n vaikutuksesta, NATia ei voida suorittaa normaalisti, mikä voi johtaa koko yhteyden katkeamiseen.
Ratkaisu
Tässä vaiheessa 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, kun paketti kulkee eBPF:n läpi, se yrittää tehdä haun SNAT-taulukosta käyttäen avaimena src ip, src port, dst ip, dst port -yhdistelmää. Jos avainta ei löydy, uusi NAT-tieto luodaan SNAT-säännön mukaisesti ja tallennetaan taulukkoon. Jos kyseessä on uusi yhteys, tämä johtaa normaaliin tiedonsiirtoon. Jos avain on poistettu vahingossa LRU:n vuoksi, NAT suoritetaan uudelleen käyttäen eri porttia kuin alkuperäisessä viestinnässä, mikä johtaa siihen, että vastaanottava puoli kieltäytyy vastaanottamasta pakettia ja yhteys katkeaa RST-paketin myötä.
Tässä PR:n lähestymistapa on yksinkertainen:
Kun paketti havaitaan mihin tahansa suuntaan, sen vastakkaisen suunnan entry päivitetään uudelleen.
Kun kommunikaatiota havaitaan kumpaan tahansa suuntaan, molemmat entryt päivitetään uudelleen, jolloin ne siirtyvät kauemmaksi LRU-logiikan poistoprioriteettikohteista. Tämä vähentää mahdollisuutta, että vain toinen entry poistetaan ja koko kommunikaatio romahtaa.
Tämä on hyvin yksinkertainen lähestymistapa ja saattaa vaikuttaa suoraviivaiselta idealta, mutta tällaisella lähestymistavalla on onnistuttu tehokkaasti ratkaisemaan ongelma, jossa vastauspakettien NAT-tiedot vanhenevat ensin ja yhteys katkeaa, mikä parantaa merkittävästi järjestelmän vakautta. Sitä voidaan pitää myös tärkeänä parannuksena, joka on saavuttanut seuraavat tulokset verkon vakauden osalta:
Johtopäätös
Mielestäni tämä PR on erinomainen esimerkki siitä, kuinka yksinkertainen idea voi tuoda suuria muutoksia jopa monimutkaisessa järjestelmässä, alkaen perus-CS-tiedosta NATin toiminnasta.
Ah, en tietenkään esitellyt suoraan monimutkaisen järjestelmän tapausta tässä artikkelissa. Mutta ymmärtääkseni tämän PR:n kunnolla, minun piti "pyytää" DeepSeek V3 0324
:lta lähes 3 tuntia, jopa käyttäen sanaa Please
, ja tuloksena sain +1 tiedon Ciliumista ja seuraavan kuvan. 😇
Ja lukiessani ongelmia ja PR:iä, kirjoitin tämän artikkelin ikään kuin korvauksena niistä pahoista aavistuksista, että jokin aikaisemmin luomani saattoi aiheuttaa ongelman.
Jälkikirjoitus - 1
Huomautettakoon, että tähän ongelmaan on olemassa hyvin tehokas kiertotie. Koska ongelman perimmäinen syy on NAT-taulukon tilan puute, NAT-taulukon kokoa on vain suurennettava. :-D
Kun joku toinen olisi kohdannut saman ongelman ja paennut ilman jälkiä vain suurentamalla NAT-taulukon kokoa, gyutaeb on perusteellisesti analysoinut ja ymmärtänyt ongelman, vaikka se ei suoranaisesti liittynytkään häneen, ja hän on myötävaikuttanut Cilium-ekosysteemiin objektiivisten tietojen avulla. Ihailen ja kunnioitan hänen intohimoaan.
Tämä oli syy, miksi päätin kirjoittaa tämän artikkelin.
Jälkikirjoitus - 2
Tämä tarina ei itse asiassa liity suoraan Go-kieltä ammattimaisesti käsittelevään Gosudaan. Kuitenkin Go-kieli ja pilviekosysteemi ovat läheisessä yhteydessä toisiinsa, ja Ciliumin kehittäjillä on jonkin verran Go-kielen tuntemusta, joten päätin tuoda henkilökohtaiseen blogiin sopivan sisällön Gosudaan.
Luulen, että se on OK, koska yksi ylläpitäjistä (minä itse) antoi luvan.
Jos olet sitä mieltä, että se ei ole OK, tallenna se nopeasti PDF-muodossa, koska et tiedä milloin se poistetaan. ;)
Jälkikirjoitus - 3
Tämän artikkelin kirjoittamisessa sain paljon apua Clinelta ja Llama 4 Mavericilta. Vaikka aloitin analyysin Geminillä ja "pyysin" DeepSeekiltä, sain lopulta apua Llama 4:ltä. Llama 4 on hyvä. Kannattaa ehdottomasti kokeilla.