GoSuda

Cilium-tarina: Pieni koodimuutos johti merkittäviin verkon vakauden parannuksiin

By iwanhae
views ...

Johdanto

Näin jokin aika sitten entisen kollegani Cilium-projektiin liittyvän PR:n (Pull Request).

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

(Testikoodia lukuun ottamatta) muutoksen määrä on vähäinen, vain yhden if-lausekkeen lisääminen. Mutta tämä muutos on vaikuttanut valtavasti, ja minusta oli henkilökohtaisesti mielenkiintoista, kuinka yksinkertainen idea voi edistää merkittävästi järjestelmän vakautta. Siksi aion kertoa tästä tapauksesta niin, että myös verkkotekniikan asiantuntijat ymmärtävät sen helposti.

Taustatietoa

Jos on jokin modernin ihmisen välttämättömyys, joka on yhtä tärkeä kuin älypuhelin, 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ä syntyy tekninen erikoisuus: miten "jakaminen" tapahtuu?

Tässä käytetty tekniikka on nimeltään Network Address Translation (NAT). NAT on tekniikka, joka mahdollistaa ulkoisen kommunikaation kartoittamalla sisäisen liikenteen, joka koostuu yksityinen IP:Portti-yhdistelmästä, käyttämättömään julkinen IP:Portti-yhdistelmään, koska TCP- tai UDP-viestintä 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 käyttämättömäksi 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 sellaisenaan verkkopalvelimelle (c.c.c.c), vastausta ei palaisi yksityisen IP-osoitteen omaavalle älypuhelimelle (a.a.a.a). Siksi reititin etsii ensin satunnaisen portin (esim. 60000), jota ei tällä hetkellä käytetä, ja tallentaa sen sisäiseen NAT-taulukkoon.

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 varatuksi 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 tulleen 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-taulukosta vastaavan alkuperäisen yksityisen IP-osoitteen (a.a.a.a) ja porttinumeron (50000) kohde-IP-osoitteen (b.b.b.b) ja porttinumeron (60000) perusteella 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 ansiosta älypuhelin kokee kommunikoivansa verkkopalvelimen kanssa ikään kuin sillä itsellään olisi julkinen IP-osoite. NATin ansiosta useat sisäiset laitteet voivat käyttää Internetiä samanaikaisesti yhdellä julkisella IP-osoitteella.

Kubernetes

Kubernetesilla on yksi hienostuneimmista ja monimutkaisimmista verkkorakenteista viime aikoina julkaistuista teknologioista. Ja tietysti, edellä mainittua NATia hyödynnetään monissa paikoissa. Kaksi tyypillistä esimerkkiä ovat seuraavat:

Kun Pod kommunikoi klusterin ulkopuolelle

Kubernetes-klusterin sisäisille Pod-yksiköille määritetään yleensä yksityiset IP-osoitteet, jotka voivat kommunikoida vain klusteriverkon sisällä. Siksi, jos Podin on kommunikoitava ulkoisen internetin kanssa, tarvitaan NATia klusterin ulkopuolelle menevälle liikenteelle. Tässä tapauksessa NAT suoritetaan yleensä Kubernetes-solmussa (klusterin jokaisella palvelimella), jossa kyseinen Pod on käynnissä. Kun Pod lähettää paketin ulos, paketti välitetään ensin solmulle, johon Pod kuuluu. Solmu muuttaa paketin lähde-IP-osoitteen (Podin yksityinen IP) omaksi julkiseksi IP-osoitteekseen ja muuttaa myös lähdeportin asianmukaisesti, ennen kuin se välittää paketin ulos. Tämä prosessi on samanlainen kuin edellä Wi-Fi-reitittimen yhteydessä kuvattu NAT-prosessi.

Esimerkiksi, jos Kubernetes-klusterin Pod (10.0.1.10, portti: 40000) yrittää muodostaa yhteyden ulkoiseen API-palvelimeen (203.0.113.45, portti: 443), Kubernetes-solmu vastaanottaa Podilta seuraavan 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       |

Sen jälkeen se suorittaa SNATin ja lähettää paketin ulos seuraavasti:

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 on sama kuin edellä mainitussa älypuhelimen ja reitittimen tapauksessa.

Kun kommunikoidaan Podin kanssa klusterin ulkopuolelta NodePortin kautta

Yksi tapa altistaa palvelut ulkopuolelle Kubernetesissa on käyttää NodePort-palvelua. NodePort-palvelu avaa tietyn portin (NodePort) kaikissa klusterin solmuissa ja välittää tähän porttiin tulevan liikenteen palveluun kuuluville Pod-yksiköille. 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 välittää tämän liikenteen lopulta Podille, joka tarjoaa kyseisen palvelun. Tässä prosessissa tapahtuu ensin DNAT, joka muuttaa paketin kohteen 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.

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äytettävän CNI-tyypin mukaan tähän liittyvät käytännöt vaihtelevat, mutta tässä tekstissä selitys perustuu Ciliumiin.)

Kun ulkoisen käyttäjän pyyntö saapuu solmulle, solmun on välitettävä se Podille, joka käsittelee pyynnön. Tässä vaiheessa solmu soveltaa seuraavia DNAT-sääntöjä muuttaakseen paketin kohteen IP-osoitteen ja porttinumeron:

1# TCP-paketti, jonka solmu valmistelee lähetettäväksi Podille
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ää 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ässä tapauksessa ulkoinen käyttäjä vastaanottaa vastauksen olemattomasta IP-osoitteesta, jota hän ei ole pyytänyt, ja pudottaa kyseisen paketin. Siksi Kubernetes-solmu suorittaa lisäksi SNATin, kun Pod lähettää vastauspaketteja 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ähettämä TCP-paketti
2# DNAT ja 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. Solmu puolestaan soveltaa DNAT- ja SNAT-prosesseja käänteisesti ja palauttaa tiedot ulkoiselle käyttäjälle. Tämän prosessin aikana 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ääasia

Yleensä Linuxissa näitä NAT-prosesseja hallitaan ja suoritetaan iptablesin kautta conntrack-nimisen alijärjestelmän avulla. Itse asiassa muut CNI-projektit, kuten flannel tai calico, käyttävät tätä ratkaistakseen edellä mainitut ongelmat. Ongelmana on kuitenkin se, että Cilium käyttää eBPF-tekniikkaa ja jättää kaikki nämä perinteiset Linux-verkkopinot täysin huomiotta. 🤣

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

Tämän seurauksena Cilium valitsi lähestymistavan, jossa se toteuttaa suoraan vain ne toiminnot, jotka ovat välttämättömiä Kubernetes-ympäristössä ja jotka perinteinen Linux-verkkopino olisi aiemmin hoitanut, kuten 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 selvennyksen vuoksi. Todellinen määritelmä on: 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|            |          |         |          |

Koska kyseessä on Hajautustaulu, siinä on avaimet nopeaa hakua varten. Avaimena käytetään src ip, src port, dst ip, dst port yhdistelmää.

Ongelman tunnistus

Ilmiö - 1: Haku

Tästä johtuen syntyy yksi ongelma: eBPF:n läpi kulkevien pakettien on suoritettava haku edellä mainitusta hajautustaulusta varmistaakseen, onko paketin suoritettava SNAT- tai DNAT-prosessi NAT-prosessin yhteydessä. Kuten olemme nähneet, SNAT-prosessissa on kahta tyyppistä pakettia: 1. sisältä ulos menevät paketit ja 2. ulkoa sisään tulevat vastauspaketit. Molemmat näistä paketeista edellyttävät NAT-muunnosta, ja niiden src ip, port ja dst ip, port -arvot vaihtuvat.

Nopean haun vuoksi on joko lisättävä toinen arvo hajautustauluun käänteisillä src- ja dst-arvoilla avaimena, tai kaikkien pakettien, myös SNATiin liittymättömien, osalta on suoritettava kaksi hakua samasta hajautustaulusta. Cilium valitsi tietysti paremman suorituskyvyn vuoksi menetelmän, jossa sama data syötetään kaksi kertaa nimellä RevSNAT.

Ilmiö - 2: LRU

Edellä mainitusta ongelmasta riippumatta millään laitteistolla ei voi olla äärettömiä resursseja, ja erityisesti laitteistotason logiikassa, jossa vaaditaan nopeaa suorituskykyä, ei voida käyttää dynaamisia tietorakenteita. Tällaisessa tilanteessa on poistettava olemassa olevia tietoja, kun resursseja on vähän. Cilium ratkaisi tämän käyttämällä Linuxin oletuksena tarjoamaa perustietorakennetta, LRU Hash Mapia.

Ilmiö 1 + Ilmiö 2 = Yhteyden katkeaminen

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

Eli, kun on kyse yhdestä SNATatusta TCP- (tai UDP-) yhteydestä:

  1. Samassa hajautustaulussa on kaksi identtistä merkintää lähtevälle ja saapuvalle paketille.
  2. LRU-logiikan mukaan toinen näistä merkinnöistä voi poistua milloin tahansa.

Jos yksi NAT-tiedosta (eli entry) lähtevän tai saapuvan paketin osalta katoaa LRU-mekanismin vuoksi, NAT-toimintoa ei voida suorittaa oikein, mikä johtaa koko yhteyden katkeamiseen.

Ratkaisu

Tässä nousevat esiin aiemmin mainitut PR:t (Pull Requestit).

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 kulki eBPF:n läpi, se yritti tehdä haun SNAT-taulukosta luomalla avaimen lähde-IP:n, lähdeportin, kohde-IP:n ja kohdeportin yhdistelmästä. Jos avainta ei löytynyt, se loi uuden NAT-tiedon SNAT-sääntöjen mukaisesti ja tallensi sen taulukkoon. Jos kyseessä oli uusi yhteys, tämä johti normaaliin kommunikaatioon. Jos avain oli poistunut LRU-mekanismin vuoksi tahattomasti, se suoritti NATin uudelleen käyttämällä eri porttia kuin alkuperäisessä kommunikaatiossa, jolloin vastaanottaja kieltäytyi vastaanottamasta pakettia ja yhteys katkesi RST-paketin myötä.

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

Kun paketti havaitaan jompaankumpaan suuntaan, päivitetään myös sen käänteissuunnan merkintä.

Kun kommunikaatio havaitaan kumpaan tahansa suuntaan, molemmat merkinnät päivitetään, jolloin ne siirtyvät kauemmas LRU-logiikan poistoprioriteetista. Tämä vähentää mahdollisuutta, että vain toinen merkintä poistuu ja koko yhteys romahtaa.

Tämä on hyvin yksinkertainen lähestymistapa ja saattaa vaikuttaa yksinkertaiselta ajatukselta, mutta tällä lähestymistavalla on pystytty tehokkaasti ratkaisemaan ongelma, jossa vastauspakettien NAT-tiedot vanhenevat ennenaikaisesti ja yhteys katkeaa. Se on parantanut merkittävästi järjestelmän vakautta. Lisäksi se on tärkeä parannus, joka on saavuttanut seuraavat tulokset verkon vakauden kannalta.

benchmark

Johtopäätös

Mielestäni tämä PR on erinomainen esimerkki siitä, miten yksinkertainen idea voi tuoda suuren muutoksen monimutkaisessa järjestelmässä, alkaen perustavanlaatuisesta CS-tiedosta siitä, miten NAT toimii.

En tietenkään esittänyt tässä artikkelissa suoraan esimerkkejä monimutkaisista järjestelmistä. Mutta ymmärtääkseni tämän PR:n kunnolla, minun piti "kerjätä" DeepSeek V3 0324 -mallia lähes 3 tuntia, jopa käyttäen sanaa "Please", ja sen tuloksena sain Cilium-tietämykseen +1 ja alla olevan kuvan. 😇

diagram

Lueskelessani ongelmia ja PR:iä, minulle tuli epämiellyttäviä aavistuksia siitä, että jokin aiemmin luomani asia olisi saattanut aiheuttaa ongelman. Kirjoitan tämän tekstin ikään kuin korvaukseksi niistä tunteista.

Jälkikirjoitus - 1

Huomaa, että tähän ongelmaan on olemassa erittäin tehokas kiertotapa. Koska ongelman perimmäinen syy on NAT-taulukon tilan puute, NAT-taulukon kokoa voi kasvattaa. :-D

Kun joku toinen olisi törmännyt samaan ongelmaan, mutta ei olisi jättänyt ongelmasta merkintää ja olisi vain kasvattanut NAT-taulukon kokoa ja paennut, ihailen ja kunnioitan gyutaebin intoa. Hän analysoi ja ymmärsi ongelman perusteellisesti, vaikka se ei liittynyt suoraan häneen, ja antoi panoksensa Cilium-ekosysteemiin objektiivisten tietojen avulla.

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äyttävään Gosudaan. Go-kieli ja pilviekosysteemi ovat kuitenkin tiiviisti yhteydessä toisiinsa, ja Ciliumin kehittäjillä on jonkin verran Go-kielitaitoa, joten päätin tuoda henkilökohtaisen blogini sisällön Gosudaan.

Luulen, että se on OK, koska yksi ylläpitäjistä (minä itse) antoi luvan.

Jos et ole samaa mieltä, tallenna se PDF-muotoon nopeasti, sillä se saattaa poistua milloin tahansa. ;)

Jälkikirjoitus - 3

Tämän artikkelin kirjoittamisessa sain paljon apua Cline- ja Llama 4 Maveric -malleilta. Vaikka aloitin analyysin Geminillä ja "kerjäsin" DeepSeekiltä, sainkin lopulta apua Llama 4:ltä. Llama 4 on hyvä. Kannattaa ehdottomasti kokeilla sitä.