Kisah Cilium: Peningkatan Stabilitas Jaringan yang Luar Biasa dari Perubahan Kode Kecil
Pendahuluan
Beberapa waktu lalu, saya melihat PR proyek Cilium dari mantan rekan kerja saya.
bpf:nat: Restore ORG NAT entry if it's not found
(Tidak termasuk kode pengujian) jumlah modifikasi itu sendiri kecil, hanya menambahkan satu blok if
. Namun, dampak dari modifikasi ini sangat besar, dan secara pribadi saya merasa menarik bagaimana satu ide sederhana dapat memberikan kontribusi besar terhadap stabilitas sistem. Oleh karena itu, saya ingin menceritakan kisah ini agar orang-orang yang tidak memiliki pengetahuan khusus di bidang jaringan pun dapat dengan mudah memahami kasus ini.
Latar Belakang
Jika ada kebutuhan pokok modern yang sama pentingnya dengan smartphone, mungkin itu adalah router Wi-Fi. Router Wi-Fi berkomunikasi dengan perangkat melalui standar komunikasi Wi-Fi dan berfungsi untuk membagikan alamat IP publik yang dimilikinya agar dapat digunakan oleh beberapa perangkat. Keunikan teknis yang muncul di sini adalah bagaimana "berbagi" itu dilakukan?
Teknologi yang digunakan di sini adalah Network Address Translation (NAT). NAT adalah teknologi yang memungkinkan komunikasi eksternal dengan memetakan komunikasi internal yang terdiri dari IP pribadi:Port
ke IP publik:Port
yang saat ini tidak digunakan, mengingat komunikasi TCP atau UDP terdiri dari kombinasi alamat IP dan informasi port.
NAT
Ketika perangkat internal mencoba mengakses internet eksternal, perangkat NAT mengubah kombinasi alamat IP pribadi dan nomor port perangkat tersebut menjadi alamat IP publiknya sendiri dan nomor port acak yang tidak digunakan. Informasi transformasi ini dicatat dalam tabel NAT di dalam perangkat NAT.
Sebagai contoh, mari kita asumsikan smartphone di rumah (IP pribadi: a.a.a.a
, port: 50000) mencoba mengakses server web (IP publik: c.c.c.c
, port: 80).
1Smartphone (a.a.a.a:50000) ==> Router (b.b.b.b) ==> Server Web (c.c.c.c:80)
Ketika router menerima permintaan dari smartphone, ia akan melihat paket TCP sebagai berikut:
1# Paket TCP yang diterima router, smartphone => router
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| a.a.a.a | 50000 | c.c.c.c | 80 |
Jika paket ini dikirim langsung ke server web (c.c.c.c
), respons tidak akan kembali ke smartphone (a.a.a.a
) yang memiliki alamat IP pribadi. Oleh karena itu, router pertama-tama menemukan port acak yang tidak terlibat dalam komunikasi saat ini (misalnya: 60000) dan mencatatnya dalam tabel NAT internal.
1# Tabel NAT internal router
2| local ip | local port | global ip | global port |
3-----------------------------------------------------
4| a.a.a.a | 50000 | b.b.b.b | 60000 |
Setelah mencatat entri baru dalam tabel NAT, router mengubah alamat IP sumber dan nomor port dari paket TCP yang diterima dari smartphone menjadi alamat IP publiknya (b.b.b.b
) dan nomor port yang baru dialokasikan (60000), lalu mengirimkannya ke server web.
1# Paket TCP yang dikirim router, router => server web
2# Melakukan SNAT
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| b.b.b.b | 60000 | c.c.c.c | 80 |
Sekarang, server web (c.c.c.c
) mengenali ini sebagai permintaan dari port 60000 router (b.b.b.b
), dan mengirimkan paket respons ke router sebagai berikut:
1# Paket TCP yang diterima router, server web => router
2| src ip | src port | dst ip | dst port |
3-------------------------------------------
4| c.c.c.c | 80 | b.b.b.b | 60000 |
Ketika router menerima paket respons ini, ia mencari alamat IP pribadi asli (a.a.a.a
) dan nomor port (50000) yang sesuai dengan alamat IP tujuan (b.b.b.b
) dan nomor port (60000) dalam tabel NAT, lalu mengubah tujuan paket menjadi smartphone.
1# Paket TCP yang dikirim router, router => smartphone
2# Melakukan DNAT
3| src ip | src port | dst ip | dst port |
4-------------------------------------------
5| c.c.c.c | 80 | a.a.a.a | 50000 |
Melalui proses ini, smartphone merasa seolah-olah ia sendiri memiliki alamat IP publik dan berkomunikasi langsung dengan server web. Berkat NAT, beberapa perangkat internal dapat menggunakan internet secara bersamaan dengan satu alamat IP publik.
Kubernetes
Kubernetes memiliki struktur jaringan yang paling canggih dan kompleks di antara teknologi-teknologi terbaru. Dan tentu saja, NAT yang disebutkan sebelumnya juga digunakan di berbagai tempat. Dua contoh representatif adalah sebagai berikut:
Ketika Pod di dalam klaster berkomunikasi dengan dunia luar klaster
Pod di dalam klaster Kubernetes umumnya diberi alamat IP pribadi yang hanya dapat berkomunikasi di dalam jaringan klaster. Oleh karena itu, jika sebuah Pod ingin berkomunikasi dengan internet eksternal, NAT diperlukan untuk lalu lintas keluar klaster. Dalam hal ini, NAT terutama dilakukan di Node Kubernetes (setiap server di klaster) tempat Pod tersebut berjalan. Ketika Pod mengirimkan paket keluar, paket tersebut pertama-tama diteruskan ke Node tempat Pod berada. Node mengubah alamat IP sumber paket ini (IP pribadi Pod) menjadi alamat IP publiknya sendiri, dan juga mengubah port sumber dengan tepat sebelum meneruskannya ke luar. Proses ini mirip dengan proses NAT yang dijelaskan sebelumnya pada router Wi-Fi.
Sebagai contoh, asumsikan sebuah Pod di dalam klaster Kubernetes (10.0.1.10
, port: 40000) mengakses server API eksternal (203.0.113.45
, port: 443). Node Kubernetes akan menerima paket berikut dari Pod:
1# Paket TCP yang diterima Node, Pod => Node
2| src ip | src port | dst ip | dst port |
3---------------------------------------------------
4| 10.0.1.10 | 40000 | 203.0.113.45 | 443 |
Node kemudian mencatat informasi berikut:
1# Tabel NAT internal Node (contoh)
2| local ip | local port | global ip | global port |
3---------------------------------------------------------
4| 10.0.1.10 | 40000 | 192.168.1.5 | 50000 |
Setelah itu, Node melakukan SNAT sebagai berikut dan mengirimkan paket ke luar:
1# Paket TCP yang dikirim Node, Node => Server API
2# Melakukan SNAT
3| src ip | src port | dst ip | dst port |
4-----------------------------------------------------
5| 192.168.1.5 | 50000 | 203.0.113.45 | 443 |
Setelah itu, prosesnya akan sama dengan kasus router smartphone yang disebutkan sebelumnya.
Ketika berkomunikasi dengan Pod melalui NodePort dari luar klaster
Salah satu cara untuk mengekspos layanan di Kubernetes ke dunia luar adalah dengan menggunakan layanan NodePort. Layanan NodePort membuka port tertentu (NodePort) di semua Node dalam klaster, dan meneruskan lalu lintas yang masuk melalui port ini ke Pod yang terkait dengan layanan tersebut. Pengguna eksternal dapat mengakses layanan melalui alamat IP Node klaster dan NodePort.
Dalam hal ini, NAT memainkan peran penting, dan khususnya, DNAT (Destination NAT) dan SNAT (Source NAT) terjadi secara bersamaan. Ketika lalu lintas masuk ke NodePort dari Node tertentu dari luar, jaringan Kubernetes harus meneruskan lalu lintas ini ke Pod yang menyediakan layanan tersebut. Dalam proses ini, DNAT pertama-tama terjadi, mengubah alamat IP tujuan dan nomor port paket menjadi alamat IP dan nomor port Pod.
Sebagai contoh, asumsikan pengguna eksternal (203.0.113.10
, port: 30000) mengakses layanan melalui NodePort (30001
) dari Node Kubernetes (192.168.1.5
). Layanan ini secara internal menunjuk ke Pod dengan alamat IP 10.0.2.15
dan port 8080
.
1Pengguna Eksternal (203.0.113.10:30000) ==> Node Kubernetes (Eksternal:192.168.1.5:30001 / Internal: 10.0.1.1:42132) ==> Pod Kubernetes (10.0.2.15:8080)
Dalam kasus Node Kubernetes, ia memiliki kedua alamat IP Node yang dapat diakses dari luar, yaitu 192.168.1.5, dan alamat IP yang valid di jaringan Kubernetes internal, yaitu 10.0.1.1. (Kebijakan terkait ini bervariasi tergantung pada jenis CNI yang digunakan, tetapi dalam artikel ini, penjelasan akan dilakukan berdasarkan Cilium.)
Ketika permintaan dari pengguna eksternal tiba di Node, Node harus meneruskan permintaan ini ke Pod yang akan memprosesnya. Pada saat ini, Node menerapkan aturan DNAT berikut untuk mengubah alamat IP tujuan dan nomor port paket.
1# Paket TCP yang sedang disiapkan Node untuk dikirim ke Pod
2# Setelah menerapkan DNAT
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 203.0.113.10 | 30000 | 10.0.2.15 | 8080 |
Poin penting di sini adalah ketika Pod mengirimkan respons terhadap permintaan ini, alamat IP sumbernya adalah alamat IP-nya sendiri (10.0.2.15
) dan alamat IP tujuannya adalah alamat IP pengguna eksternal yang mengirim permintaan (203.0.113.10
). Dalam kasus ini, pengguna eksternal akan menerima respons dari alamat IP yang tidak pernah diminta dan tidak ada, dan akan menjatuhkan paket tersebut. Oleh karena itu, ketika Pod mengirimkan paket respons ke luar, Node Kubernetes secara tambahan melakukan SNAT untuk mengubah alamat IP sumber paket menjadi alamat IP Node (192.168.1.5 atau IP jaringan internal 10.0.1.1, dalam kasus ini 10.0.1.1).
1# Paket TCP yang sedang disiapkan Node untuk dikirim ke Pod
2# Setelah menerapkan DNAT, SNAT
3| src ip | src port | dst ip | dst port |
4---------------------------------------------------
5| 10.0.1.1 | 40021 | 10.0.2.15 | 8080 |
Sekarang, Pod yang menerima paket tersebut akan merespons ke Node yang awalnya menerima permintaan melalui NodePort, dan Node akan membalikkan proses DNAT dan SNAT yang sama untuk mengembalikan informasi kepada pengguna eksternal. Dalam proses ini, setiap Node akan menyimpan informasi sebagai berikut.
1# Tabel DNAT internal Node
2| original ip | original port | destination ip | destination port |
3------------------------------------------------------------------------
4| 192.168.1.5 | 30001 | 10.0.2.15 | 8080 |
5
6# Tabel SNAT internal Node
7| original ip | original port | destination ip | destination port |
8------------------------------------------------------------------------
9| 203.0.113.10 | 30000 | 10.0.1.1 | 42132 |
Isi Utama
Secara umum, di Linux, proses NAT ini dikelola dan dioperasikan oleh subsistem yang disebut conntrack melalui iptables. Bahkan, proyek CNI lain seperti flannel atau calico memanfaatkan ini untuk menangani masalah di atas. Namun, masalahnya adalah Cilium mengabaikan seluruh tumpukan jaringan Linux tradisional ini dengan menggunakan teknologi eBPF. 🤣
Akibatnya, Cilium memilih untuk mengimplementasikan secara langsung hanya fitur-fitur yang diperlukan dalam situasi Kubernetes dari tugas-tugas yang sebelumnya dilakukan oleh tumpukan jaringan Linux tradisional, seperti yang terlihat pada gambar di atas. Oleh karena itu, terkait proses SNAT yang disebutkan sebelumnya, Cilium secara langsung mengelola tabel SNAT dalam bentuk LRU Hash Map (BPF_MAP_TYPE_LRU_HASH).
1# Tabel SNAT Cilium
2# !Contoh untuk penjelasan yang mudah. Definisi sebenarnya: 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 | protokol, conntrack, dll. metadata lainnya
4----------------------------------------------
5| | | | |
Dan karena ini adalah Hash Table, untuk pencarian cepat, ada nilai kunci yang menggunakan kombinasi src ip
, src port
, dst ip
, dst port
sebagai nilai kunci.
Identifikasi Masalah
Fenomena - 1: Pencarian
Oleh karena itu, ada satu masalah yang muncul: untuk memverifikasi apakah paket yang melewati eBPF perlu melakukan proses SNAT atau DNAT, tabel Hash di atas perlu dicari. Seperti yang telah kita lihat sebelumnya, ada dua jenis paket dalam proses SNAT: 1. Paket yang keluar dari internal ke eksternal, dan 2. Paket yang masuk dari eksternal ke internal sebagai respons. Kedua paket ini memerlukan transformasi dalam proses NAT, dan memiliki karakteristik di mana nilai src ip, port, dan dst ip, port dibalik.
Jadi, untuk pencarian cepat, baik dengan menambahkan satu nilai lagi ke Hash Table dengan kunci yang nilainya dibalik src dan dst, atau perlu melakukan pencarian yang sama dua kali pada Hash Table yang sama untuk semua paket, bahkan jika itu bukan paket yang terkait dengan SNAT. Tentu saja, Cilium mengadopsi metode memasukkan data yang sama dua kali dengan nama RevSNAT untuk kinerja yang lebih baik.
Fenomena - 2: LRU
Terlepas dari masalah di atas, tidak ada sumber daya tak terbatas di semua perangkat keras, dan terutama dalam logika perangkat keras yang membutuhkan kinerja cepat, di mana struktur data dinamis juga tidak dapat digunakan, data yang ada perlu diusir ketika sumber daya tidak cukup. Cilium mengatasi ini dengan menggunakan LRU Hash Map, struktur data dasar yang disediakan secara default di Linux.
Fenomena 1 + Fenomena 2 = Kehilangan Koneksi
https://github.com/cilium/cilium/issues/31643
Artinya, untuk satu koneksi TCP (atau UDP) yang di-SNAT:
- Data yang sama dicatat dua kali dalam satu Hash Table untuk paket keluar dan paket masuk, dan
- Dalam situasi di mana salah satu data dapat hilang kapan saja sesuai dengan logika LRU,
Jika salah satu informasi NAT (selanjutnya disebut entri) untuk paket keluar atau masuk hilang karena LRU, NAT tidak dapat dilakukan dengan benar, yang dapat menyebabkan hilangnya seluruh koneksi.
Solusi
Di sinilah PR yang disebutkan sebelumnya muncul:
bpf:nat: restore a NAT entry if its REV NAT is not found
bpf:nat: Restore ORG NAT entry if it's not found
Sebelumnya, ketika paket melewati eBPF, ia mencoba mencari di tabel SNAT menggunakan kombinasi src ip, src port, dst ip, dst port sebagai kunci. Jika kunci tidak ada, informasi NAT baru akan dibuat sesuai dengan aturan SNAT dan dicatat dalam tabel. Jika ini adalah koneksi baru, ini akan mengarah pada komunikasi yang normal, dan jika kunci dihapus secara tidak sengaja oleh LRU, NAT akan dilakukan ulang dengan port yang berbeda dari port yang digunakan untuk komunikasi yang ada, dan pihak penerima akan menolak penerimaan, dan koneksi akan dihentikan dengan paket RST.
Pendekatan yang diambil oleh PR di atas sangat sederhana.
Jika paket teramati dalam satu arah, perbarui juga entri untuk arah sebaliknya.
Jika komunikasi teramati dalam salah satu arah, kedua entri akan diperbarui, menjauhkan mereka dari target prioritas Eviction logika LRU, dan dengan demikian mengurangi kemungkinan skenario di mana hanya satu entri yang dihapus dan seluruh komunikasi runtuh.
Ini mungkin terlihat seperti pendekatan yang sangat sederhana dan ide yang mudah, tetapi melalui pendekatan ini, masalah terputusnya koneksi karena informasi NAT untuk paket respons kedaluwarsa lebih dulu dapat diselesaikan secara efektif, dan stabilitas sistem dapat sangat ditingkatkan. Ini juga merupakan peningkatan penting yang telah mencapai hasil berikut dalam hal stabilitas jaringan.
Kesimpulan
Saya percaya PR ini adalah contoh yang sangat baik yang menunjukkan bagaimana satu ide sederhana dapat membawa perubahan besar, dimulai dari pengetahuan dasar CS tentang cara kerja NAT hingga implementasinya di dalam sistem yang kompleks.
Ah, tentu saja, saya tidak secara langsung menunjukkan contoh sistem yang kompleks dalam artikel ini. Namun, untuk memahami PR ini dengan benar, saya memohon kepada DeepSeek V3 0324
selama hampir 3 jam, bahkan menambahkan kata Please
, dan sebagai hasilnya, saya memperoleh pengetahuan tentang Cilium +1 dan sebuah gambar seperti di bawah ini. 😇
Dan setelah membaca isu-isu dan PR, saya menulis artikel ini sebagai kompensasi atas firasat buruk bahwa masalah mungkin muncul karena sesuatu yang saya buat sebelumnya.
Catatan Tambahan - 1
Sebagai catatan, ada cara yang sangat efektif untuk menghindari masalah ini. Karena akar penyebab masalah ini adalah kurangnya ruang tabel NAT, yang perlu dilakukan hanyalah memperbesar ukuran tabel NAT. :-D
Saya mengagumi dan menghormati dedikasi gyutaeb yang telah menganalisis dan memahami masalah ini secara menyeluruh, serta berkontribusi pada ekosistem Cilium dengan data yang objektif, meskipun masalah ini tidak secara langsung terkait dengannya, sementara orang lain mungkin telah memperbesar ukuran tabel NAT dan melarikan diri tanpa meninggalkan isu ketika menghadapi masalah yang sama.
Ini adalah alasan mengapa saya memutuskan untuk menulis artikel ini.
Catatan Tambahan - 2
Sebenarnya, cerita ini bukan topik yang secara langsung cocok dengan Gosuda yang berurusan secara profesional dengan bahasa Go. Namun, karena bahasa Go dan ekosistem cloud memiliki hubungan yang erat dan kontributor Cilium memiliki sedikit pengetahuan tentang bahasa Go, saya mencoba membawa konten yang bisa diunggah ke blog pribadi saya ke Gosuda.
Karena salah satu administrator (saya sendiri) telah memberikan izin, saya kira ini akan baik-baik saja.
Jika Anda berpikir tidak baik-baik saja, segera simpan sebagai PDF karena mungkin akan dihapus kapan saja. ;)
Catatan Tambahan - 3
Penulisan artikel ini sangat dibantu oleh Cline dan Llama 4 Maveric. Meskipun saya memulai analisis dengan Gemini dan memohon kepada DeepSeek, bantuan sebenarnya datang dari Llama 4. Llama 4 bagus. Pastikan untuk mencobanya.