GoSuda

Cilium Hikayesi: Küçük bir Kod Değişikliğinin Yarattığı Olağanüstü Ağ İstikrarı İyileşmesi

By iwanhae
views ...

Giriş

Kısa bir süre önce eski bir iş arkadaşımın Cilium projesine yaptığı bir PR'ı inceledim.

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

(Test kodu hariç) yapılan değişiklik miktarı, yalnızca bir if bloğu eklemek kadar azdı. Ancak bu değişikliğin etkisi çok büyüktü ve basit bir fikrin sistem istikrarına ne kadar büyük katkı sağlayabileceği bana kişisel olarak ilginç geldi. Bu nedenle, ağ alanında uzman bilgisi olmayan kişilerin bile bu durumu kolayca anlayabilmesi için bu konuyu anlatmaya karar verdim.

Arka Plan Bilgisi

Modern insanların akıllı telefonlar kadar önemli bir ihtiyacı varsa, bu muhtemelen Wi-Fi yönlendirici olacaktır. Wi-Fi yönlendirici, Wi-Fi iletişim standardı aracılığıyla cihazlarla iletişim kurar ve sahip olduğu genel IP adresini birden fazla cihazın kullanabilmesi için paylaşma görevini üstlenir. Burada ortaya çıkan teknik özellik, "paylaşımın" nasıl gerçekleştiğidir.

Burada kullanılan teknoloji Network Address Translation (NAT) teknolojisidir. NAT, TCP veya UDP iletişiminin IP adresi ve port bilgilerinin birleşimiyle gerçekleştiği gerçeğinden yola çıkarak, özel IP:Port ile gerçekleşen dahili iletişimi, şu anda kullanılmayan genel IP:Port ile eşleştirerek dış dünyayla iletişim kurmayı sağlayan bir teknolojidir.

NAT

NAT cihazı, dahili bir cihaz dış internete erişmeye çalıştığında, o cihazın özel IP adresi ve port numarası kombinasyonunu kendi genel IP adresi ve kullanılmayan rastgele bir port numarasına dönüştürür. Bu dönüşüm bilgisi, NAT cihazının içindeki NAT tablosuna kaydedilir.

Örneğin, evdeki bir akıllı telefonun (özel IP: a.a.a.a, port: 50000) bir web sunucusuna (genel IP: c.c.c.c, port: 80) bağlanmaya çalıştığını varsayalım.

1Akıllı Telefon (a.a.a.a:50000) ==> Yönlendirici (b.b.b.b) ==> Web Sunucusu (c.c.c.c:80)

Yönlendirici, akıllı telefondan gelen isteği aldığında aşağıdaki TCP Paketini görecektir:

1# Yönlendiricinin aldığı TCP paketi, Akıllı Telefon => Yönlendirici
2| kaynak ip  | kaynak port | hedef ip  | hedef port |
3-------------------------------------------
4| a.a.a.a | 50000    | c.c.c.c | 80       |

Bu paketi doğrudan web sunucusuna (c.c.c.c) gönderirse, özel IP adresine sahip akıllı telefona (a.a.a.a) yanıt gelmeyeceği için, yönlendirici önce mevcut iletişimde kullanılmayan rastgele bir portu (örneğin: 60000) bulur ve bunu dahili NAT tablosuna kaydeder.

1# Yönlendirici dahili NAT tablosu
2| yerel ip  | yerel port | global ip  | global port |
3-----------------------------------------------------
4| a.a.a.a   | 50000      | b.b.b.b    | 60000       |

Yönlendirici, NAT tablosuna yeni bir giriş kaydettikten sonra, akıllı telefondan aldığı TCP paketinin kaynak IP adresini ve port numarasını kendi genel IP adresi (b.b.b.b) ve yeni atanan port numarası (60000) ile değiştirerek web sunucusuna gönderir.

1# Yönlendiricinin gönderdiği TCP paketi, Yönlendirici => Web Sunucusu
2# SNAT gerçekleştirildi
3| kaynak ip  | kaynak port | hedef ip  | hedef port |
4-------------------------------------------
5| b.b.b.b | 60000    | c.c.c.c | 80       |

Şimdi web sunucusu (c.c.c.c), yönlendiricinin (b.b.b.b) 60000 numaralı portundan gelen bir istek olarak tanır ve yanıt paketini aşağıdaki gibi yönlendiriciye gönderir:

1# Yönlendiricinin aldığı TCP paketi, Web Sunucusu => Yönlendirici
2| kaynak ip  | kaynak port | hedef ip  | hedef port |
3-------------------------------------------
4| c.c.c.c | 80       | b.b.b.b | 60000    |

Yönlendirici bu yanıt paketini aldığında, NAT tablosundan hedef IP adresi (b.b.b.b) ve port numarasına (60000) karşılık gelen orijinal özel IP adresini (a.a.a.a) ve port numarasını (50000) bulur ve paketin hedefini akıllı telefona değiştirir.

1# Yönlendiricinin gönderdiği TCP paketi, Yönlendirici => Akıllı Telefon
2# DNAT gerçekleştirildi
3| kaynak ip  | kaynak port | hedef ip  | hedef port |
4-------------------------------------------
5| c.c.c.c | 80       | a.a.a.a | 50000    |

Bu süreç sayesinde akıllı telefon, sanki doğrudan genel bir IP adresine sahipmiş ve web sunucusuyla iletişim kuruyormuş gibi hisseder. NAT sayesinde, tek bir genel IP adresiyle birden fazla dahili cihaz aynı anda interneti kullanabilir.

Kubernetes

Kubernetes, son zamanlarda ortaya çıkan teknolojiler arasında en karmaşık ve sofistike ağ yapısına sahiptir. Elbette daha önce bahsedilen NAT da birçok yerde kullanılmaktadır. Temsili örneklerden ikisi şunlardır:

Pod içinden küme dışına iletişim kurulduğunda

Kubernetes kümesi içindeki Pod'lar genellikle küme ağı içinde yalnızca iletişim kurabilecekleri özel IP adresleri alır. Bu nedenle, bir Pod'un dış internetle iletişim kurabilmesi için küme dışına giden trafik için NAT gereklidir. Bu durumda NAT, genellikle Pod'un çalıştığı Kubernetes düğümünde (kümenin her bir sunucusu) gerçekleştirilir. Bir Pod dışarıya doğru bir paket gönderdiğinde, bu paket önce Pod'un ait olduğu düğüme iletilir. Düğüm, bu paketin kaynak IP adresini (Pod'un özel IP'si) kendi genel IP adresine dönüştürür ve kaynak portu da uygun şekilde değiştirerek dışarıya iletir. Bu süreç, daha önce Wi-Fi yönlendiricide açıklanan NAT sürecine benzer.

Örneğin, Kubernetes kümesi içindeki bir Pod'un (10.0.1.10, port: 40000) harici bir API sunucusuna (203.0.113.45, port: 443) eriştiğini varsayarsak, Kubernetes düğümü Pod'dan aşağıdaki paketi alacaktır:

1# Düğümün aldığı TCP paketi, Pod => Düğüm
2| kaynak ip    | kaynak port | hedef ip        | hedef port |
3---------------------------------------------------
4| 10.0.1.10 | 40000    | 203.0.113.45  | 443      |

Düğüm aşağıdaki bilgileri kaydettikten sonra:

1# Düğüm dahili NAT tablosu (örnek)
2| yerel ip    | yerel port | global ip     | global port |
3---------------------------------------------------------
4| 10.0.1.10   | 40000      | 192.168.1.5   | 50000       |

Aşağıdaki gibi SNAT'ı gerçekleştirir ve paketi dışarıya gönderir:

1# Düğümün gönderdiği TCP paketi, Düğüm => API Sunucusu
2# SNAT gerçekleştirildi
3| kaynak ip      | kaynak port | hedef ip        | hedef port |
4-----------------------------------------------------
5| 192.168.1.5 | 50000    | 203.0.113.45  | 443      |

Bundan sonraki süreç, akıllı telefon ve yönlendirici örneğindeki ile aynıdır.

Küme dışından NodePort aracılığıyla Pod ile iletişim kurulduğunda

Kubernetes'te servisleri dış dünyaya açmanın yollarından biri NodePort servisi kullanmaktır. NodePort servisi, kümedeki tüm düğümlerin belirli bir portunu (NodePort) açık tutar ve bu porta gelen trafiği servise ait Pod'lara iletir. Dış kullanıcılar, kümedeki bir düğümün IP adresi ve NodePort aracılığıyla servise erişebilir.

Bu durumda NAT önemli bir rol oynar ve özellikle DNAT (Destination NAT) ve SNAT (Source NAT) aynı anda gerçekleşir. Dışarıdan belirli bir düğümün NodePort'una trafik geldiğinde, Kubernetes ağı bu trafiği nihayetinde bu servisi sağlayan Pod'lara iletmelidir. Bu süreçte önce DNAT gerçekleşir ve paketin hedef IP adresi ve port numarası Pod'un IP adresi ve port numarasına değiştirilir.

Örneğin, küme dışındaki bir kullanıcının (203.0.113.10, port: 30000) Kubernetes kümesindeki bir düğümün (192.168.1.5) NodePort'u (30001) aracılığıyla bir servise eriştiğini varsayalım. Bu servisin dahili olarak IP adresi 10.0.2.15 ve portu 8080 olan bir Pod'u işaret ettiğini varsayalım.

1Dış Kullanıcı (203.0.113.10:30000) ==> Kubernetes Düğümü (Dış:192.168.1.5:30001 / Dahili: 10.0.1.1:42132) ==> Kubernetes Pod'u (10.0.2.15:8080)

Burada Kubernetes düğümü, dışarıdan erişilebilen düğümün IP adresi 192.168.1.5 ve dahili Kubernetes ağında geçerli olan IP adresi 10.0.1.1'in her ikisine de sahiptir. (Kullanılan CNI türüne göre bu konuyla ilgili politikalar değişebilir, ancak bu yazıda Cilium temel alınarak açıklama yapılacaktır.)

Dış kullanıcının isteği düğüme ulaştığında, düğüm bu isteği işleyecek Pod'a iletmelidir. Bu sırada düğüm, paketin hedef IP adresini ve port numarasını değiştirmek için aşağıdaki DNAT kuralını uygular:

1# Düğümün Pod'a göndermeye hazırlandığı TCP paketi
2# DNAT uygulandıktan sonra
3| kaynak ip     | kaynak port | hedef ip  | hedef port |
4---------------------------------------------------
5| 203.0.113.10 | 30000    | 10.0.2.15 | 8080    |

Burada önemli olan, Pod'un bu isteğe yanıt gönderirken, kaynak IP adresinin kendi IP adresi (10.0.2.15) ve hedef IP adresinin isteği gönderen dış kullanıcının IP adresi (203.0.113.10) olmasıdır. Bu durumda, dış kullanıcı daha önce hiç talep etmediği, var olmayan bir IP adresinden yanıt alır ve bu paketi sadece DROP eder. Bu nedenle, Kubernetes düğümü, Pod dışarıya yanıt paketi gönderdiğinde ek olarak SNAT gerçekleştirerek paketin kaynak IP adresini düğümün IP adresine (192.168.1.5 veya dahili ağ IP'si olan 10.0.1.1, bu durumda 10.0.1.1 olarak devam eder) değiştirir.

1# Düğümün Pod'a göndermeye hazırlandığı TCP paketi
2# DNAT, SNAT uygulandıktan sonra
3| kaynak ip     | kaynak port | hedef ip  | hedef port |
4---------------------------------------------------
5| 10.0.1.1     | 40021    | 10.0.2.15 | 8080    |

Şimdi bu paketi alan Pod, NodePort üzerinden ilk isteği alan düğüme yanıt verir ve düğüm, aynı DNAT ve SNAT süreçlerini tersine uygulayarak bilgiyi dış kullanıcıya geri gönderir. Bu süreçte her düğüm aşağıdaki bilgileri saklayacaktır:

1# Düğüm dahili DNAT tablosu
2| orijinal ip     | orijinal port | hedef ip  | hedef port |
3------------------------------------------------------------------------
4| 192.168.1.5     | 30001         | 10.0.2.15       | 8080             |
5
6# Düğüm dahili SNAT tablosu
7| orijinal ip     | orijinal port | hedef ip  | hedef port |
8------------------------------------------------------------------------
9| 203.0.113.10    | 30000         | 10.0.1.1        | 42132            |

Ana Konu

Genellikle Linux'ta bu NAT süreçleri, iptables aracılığıyla conntrack adlı bir alt sistem tarafından yönetilir ve çalıştırılır. Aslında flannel veya calico gibi diğer CNI projeleri de bu sorunları çözmek için bu özelliği kullanır. Ancak sorun, Cilium'un eBPF adlı bir teknoloji kullanarak bu geleneksel Linux ağ yığınını tamamen göz ardı etmesidir. 🤣

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

Sonuç olarak Cilium, yukarıdaki resimde gösterilen mevcut Linux ağ yığınının yaptığı işlerden yalnızca Kubernetes ortamında gerekli olan işlevleri doğrudan uygulamayı seçti. Bu nedenle, daha önce bahsedilen SNAT süreci için Cilium, SNAT tablosunu LRU Hash Map (BPF_MAP_TYPE_LRU_HASH) şeklinde doğrudan yönetmektedir.

1# Cilium SNAT Tablosu
2# !Kolay anlaşılması için örnek. Gerçek tanım: https://github.com/cilium/cilium/blob/v1.18.0-pre.1/bpf/lib/nat.h#L149-L166
3| kaynak ip     | kaynak port | hedef ip  | hedef port | protokol, conntrack vb. diğer meta veriler
4----------------------------------------------
5|            |          |         |          |

Ve bir Hash Table olduğu için hızlı sorgulama amacıyla anahtar değerleri mevcuttur ve kaynak ip, kaynak port, hedef ip, hedef port birleşimini anahtar değer olarak kullanmaktadır.

Sorun Tanımlaması

Durum - 1: Sorgulama

Bu durumun bir sonucu olarak ortaya çıkan bir sorun var: eBPF'den geçen bir paketin NAT sürecinde SNAT veya DNAT gerçekleştirmesi gerekip gerekmediğini doğrulamak için yukarıdaki Hash Tablosunu sorgulaması gerekiyor. Daha önce de gördüğümüz gibi, SNAT sürecinde iki tür paket bulunur: 1. İçeriden dışarıya giden paketler ve 2. Dışarıdan içeriye gelen yanıt paketleri. Bu iki paketin NAT dönüşümüne ihtiyacı vardır ve kaynak IP, port ile hedef IP, port değerleri birbirinin tersidir.

Bu nedenle, hızlı sorgulama için kaynak ve hedefin ters çevrilmiş değerini anahtar olarak Hash Tablosuna bir değer daha eklemek veya SNAT ile ilgisi olmayan paketler için bile tüm paketler için aynı Hash Tablosunu iki kez sorgulamak gerekmektedir. Cilium elbette daha iyi performans için RevSNAT adıyla aynı veriyi iki kez yerleştirme yöntemini benimsemiştir.

Durum - 2: LRU

Ve yukarıdaki sorundan ayrı olarak, tüm donanımlarda sınırsız kaynak bulunamaz ve özellikle hızlı performans gerektiren donanım düzeyinde mantık olduğu için, dinamik veri yapıları da kullanılamayan bir durumda kaynaklar yetersiz kaldığında mevcut verilerin çıkarılması gerekir. Cilium, bunu Linux'ta varsayılan olarak sağlanan temel veri yapısı olan LRU Hash Map'i kullanarak çözmüştür.

Durum 1 + Durum 2 = Bağlantı Kaybı

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

Yani, bir SNAT'lanmış TCP (veya UDP) bağlantısı için:

  1. Bir Hash Tablosunda giden ve gelen paketler için aynı verinin iki kez kaydedilmiş olması ve
  2. LRU mantığına göre herhangi bir zamanda bu verilerden birinin kaybolabileceği bir durumda,

Dışarıya giden veya dışarıdan gelen paketlere ilişkin NAT bilgilerinden (bundan sonra giriş olarak adlandırılacaktır) birinin bile LRU tarafından silinmesi durumunda, NAT'ın düzgün bir şekilde gerçekleştirilememesi nedeniyle tüm bağlantının kesilmesine yol açabilir.

Çözüm

Burada daha önce bahsedilen aşağıdaki PR'lar devreye giriyor.

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

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

Daha önce, bir paket eBPF'den geçtiğinde, SNAT tablosundan kaynak IP, kaynak port, hedef IP ve hedef port kombinasyonuyla bir anahtar oluşturularak sorgulama yapılırdı. Eğer anahtar bulunamazsa, SNAT kuralına göre yeni bir NAT bilgisi oluşturulur ve tabloya kaydedilirdi. Yeni bir bağlantı olması durumunda bu durum normal bir iletişime yol açarken, eğer LRU tarafından anahtar istenmeden silinmişse, mevcut iletişimde kullanılan porttan farklı bir portla yeni bir NAT gerçekleştirilir ve paketi alan tarafın alımı reddetmesiyle RST paketi ile bağlantı sona ererdi.

Burada yukarıdaki PR'ın yaklaşımı basittir:

Hangi yönde olursa olsun bir paket gözlemlendiğinde, ters yön için de bir giriş güncelleyelim.

İletişim hangi yönde olursa olsun gözlemlendiğinde, her iki yönün girişleri de güncellenir ve LRU mantığının Eviction öncelik listesinden uzaklaşır, bu da yalnızca bir girişin silinerek tüm iletişimin çökmesi senaryosunun olasılığını azaltır.

Bu oldukça basit bir yaklaşım ve basit bir fikir gibi görünse de, bu yaklaşım sayesinde yanıt paketlerine ilişkin NAT bilgilerinin süresinin dolması ve bağlantının kesilmesi sorununu etkili bir şekilde çözülmüş ve sistemin kararlılığı büyük ölçüde artırılmıştır. Ayrıca, ağ kararlılığı açısından aşağıdaki gibi önemli bir gelişme kaydedilmiştir.

benchmark

Sonuç

Bu PR'ın, NAT'ın nasıl çalıştığına dair temel CS bilgisinden başlayarak, karmaşık bir sistemin içinde bile basit bir fikrin ne kadar büyük bir fark yaratabileceğini gösteren çok iyi bir örnek olduğunu düşünüyorum.

Ah, elbette bu yazıda karmaşık sistem örneğini doğrudan göstermedim. Ancak bu PR'ı düzgün bir şekilde anlamak için DeepSeek V3 0324'e Please kelimesini bile ekleyerek neredeyse 3 saat boyunca yalvardım ve sonuç olarak Cilium hakkında +1 bilgi ve aşağıdaki gibi bir diyagram elde ettim. 😇

diagram

Ve sorunları ve PR'ları okurken, daha önce yaptığım bir şey yüzünden bir sorun çıkmış olabileceği yönündeki kötü hislerime karşı bir telafi olarak bu yazıyı yazıyorum.

İnceleme - 1

Bu arada, bu sorun için çok etkili bir kaçış yolu vardır. Sorunun temel nedeni NAT tablosu alanının yetersiz olmasıdır, bu yüzden NAT tablosunun boyutunu artırmak yeterlidir. :-D

Bir başkası aynı sorunla karşılaştığında sorun kaydı bile bırakmadan NAT tablosu boyutunu artırıp kaçmışken, kendisiyle doğrudan ilgili bir sorun olmamasına rağmen titizlikle analiz edip anlayarak ve objektif kanıt verileriyle Cilium ekosistemine katkıda bulunan gyutaeb Bey'in tutkusuna hayran kaldım ve kendisine saygı duyuyorum.

Bu yazıyı yazmaya karar vermemin sebebi buydu.

İnceleme - 2

Bu hikaye, Go dilini profesyonel olarak kullanan Gosuda ile doğrudan ilişkili bir konu değil. Ancak Go dili ve bulut ekosistemi yakından ilişkilidir ve Cilium'a katkıda bulunanların Go dilinde belirli bir yetkinliği olduğu için, kişisel bloguma koyabileceğim bir içeriği Gosuda'ya taşımak istedim.

Yöneticilerden birinin (benim) izni olduğu için muhtemelen sorun olmayacaktır.

Sorun olacağını düşünüyorsanız, ne zaman silineceği belli olmadığı için hemen PDF olarak kaydedin. ;)

İnceleme - 3

Bu yazıyı yazarken Cline ve Llama 4 Maveric'ten büyük ölçüde yardım aldım. Analize Gemini ile başlamış olsam da, DeepSeek'e yalvarmış olsam da asıl yardımı Llama 4'ten aldım. Llama 4 harika. Kesinlikle denemelisiniz.