GoSuda

Kisah Cilium: Peningkatan Stabilitas Jaringan yang Luar Biasa dari Perubahan Kode Kecil

By iwanhae views ...

Pendahuluan

Beberapa waktu lalu saya meninjau PR proyek Cilium dari rekan kerja saya di tempat kerja sebelumnya.

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

Perubahannya sendiri (tidak termasuk kode pengujian) sangat kecil, hanya berupa penambahan satu blok if. Namun, dampak yang ditimbulkan perubahan ini sangat besar. Saya merasa menarik bahwa sebuah ide sederhana dapat memberikan kontribusi yang sangat besar terhadap stabilitas sistem. Oleh karena itu, saya ingin menuliskan kasus ini agar orang yang tidak memiliki keahlian khusus di bidang jaringan pun dapat memahaminya dengan mudah.

Latar Belakang

Jika ada perangkat yang sama pentingnya dengan ponsel pintar bagi masyarakat modern, mungkin itu adalah router Wi-Fi. Router Wi-Fi berkomunikasi dengan perangkat melalui standar komunikasi nirkabel Wi-Fi dan berperan membagikan alamat IP publik yang dimilikinya agar dapat digunakan oleh banyak perangkat. Pertanyaan teknis yang menarik di sini adalah bagaimana router tersebut "membagikan" alamat IP tersebut.

Teknologi yang digunakan di sini adalah Network Address Translation (NAT). Karena komunikasi TCP atau UDP dilakukan melalui kombinasi alamat IP dan informasi port, NAT adalah teknologi yang memetakan komunikasi internal dalam bentuk IP privat:Port ke IP publik:Port yang sedang tidak digunakan, sehingga komunikasi dengan pihak eksternal dapat dilakukan.

NAT

Ketika perangkat internal mencoba terhubung ke internet eksternal, perangkat NAT mengubah kombinasi alamat IP privat dan nomor port perangkat tersebut menjadi alamat IP publik miliknya sendiri beserta nomor port yang tidak digunakan secara acak. Informasi konversi ini dicatat di dalam sebuah tabel yang disebut tabel NAT di dalam perangkat NAT.

Sebagai contoh, misalkan ponsel pintar di rumah (IP privat: a.a.a.a, port: 50000) akan terhubung ke server web (IP publik: c.c.c.c, port: 80).

1Ponsel (a.a.a.a:50000) ==> Router (b.b.b.b) ==> Server Web (c.c.c.c:80)

Setelah menerima permintaan dari ponsel, router akan melihat paket TCP seperti berikut.

1# Paket TCP yang diterima router, ponsel => 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 ponsel dengan IP privat (a.a.a.a). Oleh karena itu, router terlebih dahulu mencari port acak yang tidak terlibat dalam komunikasi saat ini (misalnya 60000) dan mencatatnya di 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 di tabel NAT, router mengubah alamat IP dan nomor port sumber pada paket TCP yang diterima dari ponsel menjadi alamat IP publik miliknya (b.b.b.b) dan port yang baru dialokasikan (60000), lalu meneruskannya 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) menganggap permintaan tersebut berasal dari port 60000 pada router (b.b.b.b), dan mengirimkan paket respons kembali ke router seperti 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 menerima paket respons ini, router mencari di tabel NAT alamat IP privat asli (a.a.a.a) dan port (50000) yang berpasangan dengan alamat IP tujuan (b.b.b.b) dan port (60000), lalu mengubah tujuan paket menjadi ponsel.

1# Paket TCP yang dikirim router, router => ponsel
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, ponsel merasa seolah-olah ia berkomunikasi langsung dengan server web menggunakan alamat IP publiknya sendiri. Berkat NAT, banyak perangkat internal dapat menggunakan internet secara bersamaan dengan satu alamat IP publik.

Kubernetes

Kubernetes memiliki struktur jaringan yang paling canggih sekaligus kompleks di antara teknologi yang muncul belakangan ini. Tentu saja, NAT yang telah disebutkan sebelumnya juga digunakan di berbagai tempat. Dua contoh yang paling representatif adalah sebagai berikut.

Ketika Pod berkomunikasi dengan luar klaster

Pod di dalam klaster Kubernetes umumnya diberi alamat IP privat yang hanya dapat berkomunikasi di dalam jaringan klaster. Oleh karena itu, agar Pod dapat berkomunikasi dengan internet eksternal, diperlukan NAT untuk lalu lintas yang keluar dari klaster. NAT ini umumnya dilakukan pada node Kubernetes (setiap server dalam klaster) tempat Pod tersebut berjalan. Ketika Pod mengirim paket ke luar, paket tersebut pertama-tama diteruskan ke node tempat Pod berada. Node kemudian mengganti alamat IP sumber paket (IP privat Pod) dengan alamat IP publik miliknya, serta mengubah port sumber secara tepat sebelum meneruskannya ke luar. Proses ini serupa dengan proses NAT yang dijelaskan sebelumnya pada router Wi-Fi.

Sebagai contoh, misalkan Pod di dalam klaster Kubernetes (10.0.1.10, port: 40000) terhubung ke API server 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 dan mengirim paket ke luar.

1# Paket TCP yang dikirim node, node => API server
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 tahap ini, prosesnya mengikuti alur yang sama dengan kasus router Wi-Fi pada ponsel.

Ketika komunikasi dari luar klaster ke Pod dilakukan melalui NodePort

Salah satu cara untuk mengekspos layanan ke luar pada Kubernetes adalah dengan menggunakan layanan NodePort. Layanan NodePort membuka port tertentu (NodePort) pada semua node di dalam klaster, lalu meneruskan lalu lintas yang masuk melalui port tersebut ke Pod yang tergabung dalam layanan tersebut. Pengguna eksternal dapat mengakses layanan melalui alamat IP node di klaster dan NodePort.

Dalam skenario ini, NAT memainkan peran penting, khususnya DNAT (Destination NAT) dan SNAT (Source NAT) terjadi secara bersamaan. Ketika lalu lintas dari luar masuk ke NodePort pada node tertentu, jaringan Kubernetes harus meneruskan lalu lintas tersebut ke Pod yang menyediakan layanan. Dalam proses ini, DNAT terjadi terlebih dahulu sehingga alamat IP dan nomor port tujuan paket diubah menjadi alamat IP dan nomor port Pod.

Sebagai contoh, misalkan pengguna di luar klaster (203.0.113.10, port: 30000) mengakses layanan melalui NodePort (30001) pada salah satu node klaster Kubernetes (192.168.1.5). Anggap layanan ini secara internal menunjuk ke Pod dengan 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 hal ini, node Kubernetes memiliki dua alamat: alamat IP 192.168.1.5 yang dapat diakses dari luar, dan alamat IP 10.0.1.1 yang valid di dalam jaringan Kubernetes internal. (Kebijakan terkait hal ini dapat bervariasi tergantung pada jenis CNI yang digunakan, tetapi dalam artikel ini penjelasan dilakukan dengan berfokus pada Cilium.)

Ketika permintaan pengguna eksternal sampai ke node, node harus meneruskannya ke Pod yang akan memprosesnya. Pada tahap ini, node menerapkan aturan DNAT berikut untuk mengubah alamat IP dan nomor port tujuan 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    |

Hal penting di sini adalah ketika Pod mengirim respons atas permintaan ini, alamat IP sumbernya adalah alamat IP Pod itu sendiri (10.0.2.15), sedangkan alamat IP tujuannya adalah alamat IP pengguna eksternal yang mengirim permintaan (203.0.113.10). Dalam kondisi ini, pengguna eksternal akan menerima respons dari alamat IP yang tidak pernah ia minta dan tidak dikenalnya, sehingga paket tersebut akan dibuang begitu saja. Oleh karena itu, node Kubernetes melakukan SNAT tambahan ketika Pod mengirim paket respons ke luar, dengan mengubah alamat IP sumber paket menjadi alamat IP node (192.168.1.5 atau alamat IP jaringan internal 10.0.1.1; dalam kasus ini, digunakan 10.0.1.1).

1# Paket TCP yang sedang disiapkan node untuk dikirim ke Pod
2# Setelah menerapkan DNAT dan 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 mengirim respons kembali ke node yang sebelumnya menerima permintaan NodePort. Node kemudian menerapkan kebalikan dari proses DNAT dan SNAT yang sama, lalu mengembalikan informasi kepada pengguna eksternal. Selama proses ini, setiap node menyimpan informasi 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            |

Pembahasan

Pada umumnya di Linux, proses NAT ini dikelola dan dijalankan melalui iptables oleh subsistem bernama conntrack. Faktanya, proyek CNI lain seperti flannel dan calico memanfaatkan mekanisme ini untuk menangani masalah seperti yang dijelaskan di atas. Namun, masalahnya adalah Cilium menggunakan teknologi bernama eBPF dan mengabaikan sepenuhnya tumpukan jaringan Linux tradisional tersebut. 🤣

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

Akibatnya, Cilium memilih jalur dengan mengimplementasikan langsung hanya fungsi-fungsi yang dibutuhkan dalam konteks Kubernetes dari sekian banyak tugas yang dilakukan oleh tumpukan jaringan Linux konvensional, sebagaimana ditunjukkan pada gambar di atas. Karena itu, untuk SNAT yang telah dibahas sebelumnya, Cilium mengelola tabel SNAT secara langsung dalam bentuk LRU Hash Map (BPF_MAP_TYPE_LRU_HASH).

1# Tabel SNAT Cilium
2# !Contoh untuk penjelasan yang mudah. Definisi sebenarnya ada di: 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, dan metadata lainnya
4----------------------------------------------
5|            |          |         |          |

Karena merupakan Hash Table, untuk mempercepat pencarian digunakan sebuah kunci, dan kombinasi src ip, src port, dst ip, dst port digunakan sebagai kunci.

Identifikasi Masalah

Fenomena 1: Pencarian

Masalah yang muncul dari desain ini adalah sebagai berikut. Untuk memastikan apakah paket yang melewati eBPF memerlukan proses SNAT atau DNAT, Hash Table tersebut harus dicari. Seperti yang telah dilihat sebelumnya, terdapat dua jenis paket dalam proses SNAT: 1. paket yang keluar dari internal ke eksternal, dan 2. paket respons yang masuk dari eksternal ke internal. Kedua paket ini memerlukan konversi NAT, tetapi nilai src ip, port, dan dst ip, port pada keduanya saling terbalik.

Oleh karena itu, untuk mempercepat pencarian, perlu dilakukan salah satu dari dua hal: menambahkan satu entri lagi ke Hash Table dengan kunci berupa nilai src dan dst yang dibalik, atau mencari Hash Table yang sama dua kali untuk setiap paket, termasuk paket yang tidak terkait dengan SNAT. Cilium tentu saja memilih cara kedua untuk performa yang lebih baik, yaitu dengan memasukkan data yang sama dua kali, yang dikenal dengan nama RevSNAT.

Fenomena 2: LRU

Terlepas dari masalah di atas, perangkat keras apa pun tidak mungkin memiliki sumber daya yang tak terbatas. Terutama untuk logika pada tingkat perangkat keras yang membutuhkan performa tinggi, struktur data dinamis tidak dapat digunakan, sehingga ketika sumber daya tidak mencukupi, data yang sudah ada perlu dihapus (eviction). Cilium menyelesaikan hal ini dengan menggunakan LRU Hash Map, yaitu struktur data dasar yang disediakan Linux secara bawaan.

Fenomena 1 + Fenomena 2 = Hilangnya Koneksi

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

Artinya, untuk satu koneksi TCP (atau UDP) yang telah di-SNAT:

  1. Dua data yang sama dicatat dua kali dalam satu Hash Table, untuk paket keluar dan paket masuk, dan
  2. Berdasarkan logika LRU, salah satu dari kedua data tersebut dapat hilang kapan saja.

Jika salah satu informasi NAT (selanjutnya disebut entri) untuk paket keluar maupun paket masuk dihapus oleh LRU, NAT tidak dapat dilakukan dengan benar, yang dapat berujung pada terputusnya seluruh koneksi.

Solusi

Di sinilah dua PR yang telah disebutkan sebelumnya berperan.

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, sistem membuat kunci dari kombinasi src ip, src port, dst ip, dan dst port, lalu mencarinya di tabel SNAT. Jika kunci tidak ditemukan, sistem membuat informasi NAT baru sesuai aturan SNAT dan mencatatnya di tabel. Jika ini adalah koneksi baru, proses ini akan menghasilkan komunikasi yang normal. Namun, jika kunci terhapus secara tidak sengaja oleh LRU, NAT akan dilakukan menggunakan port yang berbeda dari port yang digunakan dalam komunikasi sebelumnya. Akibatnya, pihak penerima akan menolak paket tersebut, dan koneksi akan diakhiri dengan paket RST.

Pendekatan yang digunakan PR ini sangat sederhana:

Setiap kali sebuah paket teramati pada salah satu arah, perbarui juga entri untuk arah sebaliknya.

Jika komunikasi teramati pada salah satu arah, kedua entri akan diperbarui sehingga keduanya menjauh dari prioritas eviction LRU. Dengan demikian, kemungkinan terjadinya skenario di mana hanya salah satu entri yang terhapus dan seluruh komunikasi runtuh dapat dikurangi.

Pendekatan ini memang sangat sederhana dan ide yang digunakan mungkin tampak remeh. Namun, melalui pendekatan ini, masalah entri NAT untuk paket respons yang kedaluwarsa lebih dahulu dan menyebabkan koneksi terputus dapat diatasi secara efektif, serta stabilitas sistem dapat ditingkatkan secara signifikan. Selain itu, dari sisi stabilitas jaringan, perubahan ini dapat dikatakan sebagai perbaikan penting yang menghasilkan pencapaian berikut.

benchmark

Kesimpulan

Menurut saya, PR ini adalah contoh yang sangat baik untuk menunjukkan bagaimana cara kerja NAT, mulai dari pengetahuan dasar ilmu komputer, hingga bagaimana sebuah ide sederhana dapat membawa perubahan besar di dalam sistem yang kompleks.

Oh, tentu saja, dalam artikel ini saya tidak secara langsung menunjukkan contoh sistem yang kompleks. Namun, untuk memahami PR ini dengan benar, saya sampai memohon-mohon kepada DeepSeek V3 0324 hampir tiga jam, bahkan dengan menambahkan kata Please, dan hasilnya saya mendapatkan tambahan satu pengetahuan tentang Cilium serta sebuah diagram seperti di bawah ini. 😇

diagram

Selain itu, sambil membaca berbagai issue dan PR, saya menulis artikel ini sebagai bentuk kompensasi psikologis atas firasat buruk bahwa issue tersebut mungkin disebabkan oleh sesuatu yang pernah saya buat.

Catatan 1

Omong-omong, masalah ini memiliki cara penghindaran yang sangat efektif. Karena akar penyebab masalah ini adalah kurangnya kapasitas tabel NAT, Anda cukup memperbesar ukuran tabel NAT. :-D

Dalam situasi di mana seseorang menghadapi issue yang sama, mungkin ada yang hanya memperbesar ukuran tabel NAT lalu pergi tanpa meninggalkan issue. Namun, gyutaeb menganalisis dan memahami masalah ini secara menyeluruh, lalu berkontribusi bagi ekosistem Cilium dengan data bukti yang objektif, padahal issue ini tidak berhubungan langsung dengan dirinya. Saya sangat mengagumi dan menghormati dedikasi beliau.

Hal inilah yang mendorong saya untuk menulis artikel ini.

Catatan 2

Topik ini sebenarnya tidak sepenuhnya sesuai dengan Gosuda, yang berfokus pada bahasa Go. Namun, bahasa Go dan ekosistem cloud memiliki hubungan yang erat, dan kontributor Cilium memiliki pemahaman tertentu tentang bahasa Go. Karena itu, saya membawa konten yang sebenarnya dapat dimuat di blog pribadi ini ke Gosuda.

Saya berpikir bahwa tulisan ini mungkin tidak bermasalah karena telah mendapat izin dari salah satu pengelola (yaitu saya sendiri).

Jika Anda merasa tidak nyaman dengan hal ini, silakan simpan artikel ini dalam format PDF secepatnya, karena artikel ini bisa saja dihapus sewaktu-waktu. ;)

Catatan 3

Dalam penulisan artikel ini, saya sangat terbantu oleh Cline dan Llama 4 Maverick. Meskipun analisis awal saya dimulai dengan Gemini dan saya sempat memohon-mohon kepada DeepSeek, bantuan yang sebenarnya justru saya dapatkan dari Llama 4. Llama 4 bagus. Silakan coba sendiri.