GoSuda

Cilium 이야기: 작은 코드 변경이 만들어낸 놀라운 네트워크 안정성 향상

By iwanhae
views ...

Giriş

Geçtiğimiz günlerde eski bir iş arkadaşımın Cilium projesine yaptığı bir PR'ı inceledim.

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

(Test kodları 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 sistemin kararlılığına ne kadar önemli katkılar sağlayabileceği bana kişisel olarak ilginç geldi. Bu nedenle, ağ alanında uzmanlığı olmayan kişilerin de bu durumu kolayca anlayabilmesi için bu konuyu açıklamaya karar verdim.

Arka Plan Bilgisi

Akıllı telefonlar kadar önemli olan modern insanların vazgeçilmez bir eşyası varsa, muhtemelen Wi-Fi yönlendiricileridir. Wi-Fi yönlendiriciler, Wi-Fi iletişim standardı aracılığıyla cihazlarla iletişim kurar ve sahip oldukları genel IP adresini birden fazla cihazın kullanabilmesi için paylaşma görevini üstlenir. Burada ortaya çıkan teknik özellik, "nasıl paylaştığı"dır.

Burada kullanılan teknoloji Network Address Translation (NAT)'tır. NAT, TCP veya UDP iletişiminin IP adresi ve port bilgisi kombinasyonundan oluştuğu gerçeğinden hareketle, özel IP:Port ile gerçekleşen dahili iletişimi, o an kullanılmayan bir genel IP:Port ile eşleştirerek dış dünya ile iletişim kurmayı sağlayan bir teknolojidir.

NAT

Bir 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üştürme bilgisi, NAT cihazının içindeki NAT tablosu adı verilen bir yere 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ı telefonun isteğini aldığında aşağıdaki TCP paketini görecektir.

1# Yönlendiricinin aldığı TCP paketi, akıllı telefon => yönlendirici
2| src ip  | src port | dst ip  | dst port |
3-------------------------------------------
4| a.a.a.a | 50000    | c.c.c.c | 80       |

Bu paket doğrudan web sunucusuna (c.c.c.c) gönderilirse, özel IP adresine sahip akıllı telefona (a.a.a.a) bir yanıt dönmeyeceği için, yönlendirici önce mevcut iletişime dahil olmayan rastgele bir port (örneğin: 60000) bulur ve bunu dahili NAT tablosuna kaydeder.

1# Yönlendirici içi NAT tablosu
2| local ip  | local 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ştiriliyor
3| src ip  | src port | dst ip  | dst port |
4-------------------------------------------
5| b.b.b.b | 60000    | c.c.c.c | 80       |

Şimdi web sunucusu (c.c.c.c), yönlendirici (b.b.b.b) üzerindeki 60000 numaralı porttan gelen bir istek olarak algılar ve yanıt paketini aşağıdaki gibi yönlendiriciye gönderir.

1# Yönlendiricinin aldığı TCP paketi, web sunucusu => yönlendirici
2| src ip  | src port | dst ip  | dst 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 adresi (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ştiriliyor
3| src ip  | src port | dst ip  | dst 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ş gibi web sunucusuyla iletişim kurduğunu 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 hassas ağ yapısına sahiptir. Ve elbette, daha önce bahsedilen NAT da birçok yerde kullanılmaktadır. Temsil edici iki örnek aşağıdadır.

Pod içerisinden küme dışıyla iletişim kurulduğunda

Kubernetes kümesi içindeki Pod'lar genellikle küme ağı içinde yalnızca iletişim kurabilen özel IP adresleri atanır. Bu nedenle, bir Pod'un dış internetle iletişim kurabilmesi için küme dışına giden trafik için NAT'a ihtiyaç vardır. 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 adresiyle değiştirir ve kaynak portu da uygun şekilde ayarlayarak dışarıya iletir. Bu süreç, daha önce Wi-Fi yönlendiricide açıklanan NAT süreciyle benzerdir.

Örneğin, bir 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| src ip    | src port | dst ip        | dst port |
3---------------------------------------------------
4| 10.0.1.10 | 40000    | 203.0.113.45  | 443      |

Düğüm aşağıdaki içeriği kaydettikten sonra

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

Aşağıdaki gibi SNAT uyguladıktan sonra paketi dışarıya gönderecektir.

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

Bundan sonraki süreç, akıllı telefon yönlendirici örneğinde olduğu gibi aynı adımları takip edecektir.

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

Kubernetes'te hizmetleri dışarıya açmanın yollarından biri NodePort hizmetlerini kullanmaktır. NodePort hizmeti, küme içindeki tüm düğümlerin belirli bir portunu (NodePort) açık tutar ve bu porta gelen trafiği hizmete ait Pod'lara iletir. Dış kullanıcılar, kümedeki düğümün IP adresi ve NodePort aracılığıyla hizmete erişebilirler.

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 hizmeti 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ıyla değiştirilir.

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

1Harici Kullanıcı (203.0.113.10:30000) ==> Kubernetes Düğümü (Harici: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 olmak üzere her ikisine de sahiptir. (Kullanılan CNI türüne göre bu konudaki politikalar değişebilir, ancak bu yazıda Cilium'u temel alarak açıklama yapılmıştır.)

Dış kullanıcının isteği düğüme ulaştığında, düğüm bu isteği işleyecek Pod'a iletmelidir. Bu durumda düğüm, paketin hedef IP adresi 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| src ip       | src port | dst ip    | dst port |
4---------------------------------------------------
5| 203.0.113.10 | 30000    | 10.0.2.15 | 8080    |

Burada önemli olan nokta, 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 harici kullanıcının IP adresi (203.0.113.10) olmasıdır. Bu durumda, harici kullanıcı, 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, paketin kaynak IP adresini düğümün IP adresi (192.168.1.5 veya dahili ağ IP'si olan 10.0.1.1, bu durumda 10.0.1.1 olarak devam eder) ile değiştirmek için ek olarak SNAT gerçekleştirir.

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

Artık bu paketi alan Pod, başlangıçta NodePort aracılığıyla isteği alan düğüme yanıt verir ve düğüm, aynı DNAT ve SNAT süreçlerini tersine uygulayarak bilgiyi harici kullanıcıya geri gönderir. Bu süreçte, her düğüm aşağıdaki bilgileri saklayacaktır.

1# Düğüm içi DNAT tablosu
2| original ip     | original port | destination ip  | destination port |
3------------------------------------------------------------------------
4| 192.168.1.5     | 30001         | 10.0.2.15       | 8080             |
5
6# Düğüm içi SNAT tablosu
7| original ip     | original port | destination ip  | destination 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 bu sorunu yukarıdaki şekilde çözmek için bunu kullanır. Ancak sorun şu ki, Cilium eBPF adlı bir teknoloji kullandığı için bu geleneksel Linux ağ yığınını tamamen göz ardı etmektedir. 🤣

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

Sonuç olarak Cilium, yukarıdaki resimde gösterilen geleneksel Linux ağ yığınının yaptığı işlerden yalnızca Kubernetes ortamında gerekli olan fonksiyonları doğrudan uygulamayı tercih etmiştir. 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 açıklama 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| src ip     | src port | dst ip  | dst port | protocol, conntrack vb. diğer meta veriler
4----------------------------------------------
5|            |          |         |          |

Ve bir Hash Table olduğu için hızlı sorgulama amacıyla bir anahtar değeri bulunmaktadır; src ip, src port, dst ip, dst port kombinasyonu anahtar değeri olarak kullanılmaktadır.

Sorunun Farkına Varma

Durum - 1: Sorgulama

Bu durumdan kaynaklanan bir sorun var: eBPF'den geçen bir paketin NAT sürecinde SNAT veya DNAT işlemi yapılması gerekip gerekmediğini doğrulamak için yukarıdaki Hash Table'ı sorgulaması gerekiyor, ancak daha önce gördüğümüz gibi SNAT sürecinde iki tür paket vardır: 1. İçeriden dışarıya çıkan paketler ve 2. Dışarıdan içeriye gelen yanıt paketleri. Bu iki paketin NAT süreci için dönüşüme ihtiyacı vardır ve src ip, port ile dst ip, port değerlerinin yer değiştirmesi gibi bir özelliği vardır.

Bu nedenle, hızlı sorgulama için src ve dst'nin tersine çevrilmiş değerini anahtar olarak Hash Table'a bir değer daha eklemek ya da SNAT ile ilgili olmasa bile tüm paketler için aynı Hash Table'ı iki kez sorgulamak gerekmektedir. Doğal olarak Cilium, daha iyi performans için RevSNAT adıyla aynı veriyi iki kez ekleme yöntemini benimsemiştir.

Durum - 2: LRU

Ve bu sorundan bağımsız olarak, tüm donanımlarda sınırsız kaynak bulunamaz ve özellikle hızlı performans gerektiren donanım seviyesi bir mantıkta dinamik veri yapıları da kullanılamadığı durumlarda, kaynaklar yetersiz kaldığında mevcut verilerin evict edilmesi (çıkarılması) gerekmektedir. Cilium, bunu Linux'un temel olarak sağladığı LRU Hash Map adlı veri yapısını 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 Table'da giden ve gelen paketler için aynı veri iki kez kaydedilmiştir.
  2. LRU mantığına göre, her an iki veriden biri kaybolabilir.

Eğer dışarıya çıkan veya dışarıdan gelen pakete ait NAT bilgisi (bundan sonra entry olarak anılacaktır) LRU tarafından silinirse, NAT işlemi düzgün bir şekilde gerçekleştirilemez ve bu durum, tüm bağlantının kaybolmasına 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 tablosunda src ip, src port, dst ip, dst port kombinasyonuyla bir anahtar oluşturularak sorgulama yapılırdı. Eğer anahtar mevcut değilse, SNAT kuralına göre yeni bir NAT bilgisi oluşturulur ve tabloya kaydedilirdi. Yeni bir bağlantı durumunda bu normal bir iletişimle sonuçlanırken, eğer anahtar LRU tarafından istenmeden kaldırılmışsa, mevcut iletişimde kullanılan porttan farklı bir portla yeniden NAT işlemi yapılır ve paketi alan taraf almayı reddeder, bu da RST paketiyle birlikte bağlantının sonlanmasına yol açar.

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

Hangi yönde olursa olsun bir paket gözlemlendiğinde, ters yöndeki entry de yeniden güncellensin.

Hangi yönde olursa olsun bir iletişim gözlemlendiğinde, her iki entry de yeniden güncellenerek LRU mantığındaki Eviction öncelik hedefinden uzaklaşır ve bu sayede yalnızca bir tarafın entry'sinin silinerek tüm iletişimin çökmesi senaryosunun olasılığı azaltılır.

Bu, çok basit bir yaklaşım ve kolay bir fikir gibi görünebilir, ancak bu yaklaşım sayesinde yanıt paketlerine ait NAT bilgisinin erken sona ermesiyle bağlantının kesilmesi sorunu 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 başarıları elde eden önemli bir iyileştirme olarak kabul edilebilir.

benchmark

Sonuç

Bu PR'ın, NAT'ın nasıl çalıştığına dair temel CS bilgisinden başlayarak, karmaşık bir sistem içinde 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 sistemin örneklerini doğrudan göstermedim. Ancak bu PR'ı tam olarak anlamak için DeepSeek V3 0324'e Please kelimesini bile ekleyerek yaklaşık 3 saat 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 sorunlar ortaya çıkmış olabileceğine dair kötü bir önseziye karşılık olarak bu yazıyı yazıyorum.

İnceleme - 1

Bu sorundan kaçınmanın çok etkili bir yolu vardır. Sorunun temel nedeni NAT tablosu alanının yetersiz olması olduğundan, NAT tablosu boyutunu artırmak yeterlidir. :-D

Birileri aynı sorunla karşılaştığında, hiçbir sorun kaydı bırakmadan NAT tablosu boyutunu artırıp kaçmışken, kendisiyle doğrudan ilgili olmayan bir sorun olmasına rağmen, titizlikle analiz edip anlamış, nesnel kanıtlarla birlikte Cilium ekosistemine katkıda bulunmuş gyutaeb Bey'in tutkusuna hayran kaldım ve kendisine saygı duyuyorum.

Bu yazıyı yazmaya karar vermemin sebebi buydu.

İnceleme - 2

Bu hikaye aslında Go dilini profesyonel olarak kullanan Gosuda ile doğrudan ilgili bir konu değil. Ancak Go dili ve bulut ekosistemi yakından ilişkilidir ve Cilium'a katkıda bulunanların Go diline belli bir seviyede hakim oldukları düşünülerek, kişisel bloguma koyabileceğim bir içeriği Gosuda'ya taşımaya karar verdim.

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

Eğer sorun olduğunu düşünüyorsanız, ne zaman silineceği belli olmadığı için hemen PDF olarak kaydetmenizi tavsiye ederim. ;)

İnceleme - 3

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