GoSuda

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

By iwanhae
views ...

Pendahuluan

Beberapa waktu lalu, saya melihat PR tentang proyek Cilium dari mantan rekan kerja saya.

bpf:nat: Pulihkan entri NAT ORG jika tidak ditemukan

(Tidak termasuk kode pengujian) Jumlah modifikasi itu sendiri kecil, hanya menambahkan satu blok pernyataan if. Namun, dampak dari modifikasi ini sangat besar, dan saya secara pribadi merasa menarik bahwa 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.

Pengetahuan Latar Belakang

Jika ada kebutuhan pokok modern yang sama pentingnya dengan ponsel pintar, mungkin itu adalah router Wi-Fi. Router Wi-Fi berkomunikasi dengan perangkat melalui standar komunikasi Wi-Fi dan berfungsi untuk berbagi alamat IP publik yang dimilikinya agar dapat digunakan oleh beberapa perangkat. Keunikan teknis yang muncul di sini adalah bagaimana cara "berbagi" tersebut.

Teknologi yang digunakan di sini adalah Network Address Translation (NAT). NAT adalah teknologi yang memungkinkan komunikasi dengan dunia luar 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, anggaplah ponsel pintar di rumah (IP pribadi: a.a.a.a, port: 50000) mencoba mengakses server web (IP publik: c.c.c.c, port: 80).

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

Ketika router menerima permintaan dari ponsel pintar, ia akan melihat Paket TCP berikut:

1# Paket TCP yang diterima router, dari ponsel pintar => 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 pintar (a.a.a.a) yang memiliki alamat IP pribadi. Oleh karena itu, router terlebih dahulu 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 paket TCP yang diterima dari ponsel pintar 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, dari router => server web
2# SNAT dilakukan
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 permintaan tersebut berasal dari port 60000 router (b.b.b.b), dan mengirimkan paket respons ke router sebagai berikut:

1# Paket TCP yang diterima router, dari 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) di tabel NAT, kemudian mengubah tujuan paket ke ponsel pintar.

1# Paket TCP yang dikirim router, dari router => ponsel pintar
2# DNAT dilakukan
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 pintar merasa seolah-olah ia berkomunikasi langsung dengan server web menggunakan alamat IP publiknya sendiri. 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 terkini. Dan tentu saja, NAT yang disebutkan sebelumnya juga digunakan di berbagai tempat. Berikut adalah dua contoh representatif:

Ketika Pod melakukan komunikasi dengan dunia luar klaster dari dalam Pod

Pod di dalam klaster Kubernetes umumnya diberi alamat IP pribadi yang hanya dapat berkomunikasi di dalam jaringan klaster. Oleh karena itu, jika Pod ingin berkomunikasi dengan internet eksternal, NAT diperlukan untuk lalu lintas keluar dari klaster. Dalam hal ini, NAT terutama dilakukan pada node Kubernetes (setiap server di klaster) tempat Pod berjalan. Ketika Pod mengirim 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 yang sesuai, lalu meneruskannya ke luar. Proses ini mirip dengan proses NAT yang dijelaskan sebelumnya pada router Wi-Fi.

Sebagai contoh, misalkan sebuah Pod di dalam klaster Kubernetes (10.0.1.10, port: 40000) mencoba mengakses server API eksternal (203.0.113.45, port: 443). Node Kubernetes akan menerima paket berikut dari Pod:

1# Paket TCP yang diterima node, dari Pod => node
2| src ip    | src port | dst ip        | dst port |
3---------------------------------------------------
4| 10.0.1.10 | 40000    | 203.0.113.45  | 443      |

Node akan 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       |

Kemudian, node akan melakukan SNAT sebagai berikut dan mengirimkan paket ke luar:

1# Paket TCP yang dikirim node, dari node => server API
2# SNAT dilakukan
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 mengikuti kasus router ponsel pintar yang disebutkan sebelumnya.

Ketika berkomunikasi dengan Pod melalui NodePort dari luar klaster

Salah satu cara untuk mengekspos layanan ke dunia luar di Kubernetes adalah dengan menggunakan layanan NodePort. Layanan NodePort membuka port tertentu (NodePort) pada semua node di dalam klaster dan meneruskan lalu lintas yang masuk melalui port ini ke Pod yang termasuk dalam 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 akhirnya meneruskan lalu lintas ini ke Pod yang menyediakan layanan tersebut. Dalam proses ini, DNAT terjadi terlebih dahulu untuk mengubah alamat IP tujuan dan nomor port paket menjadi alamat IP dan nomor port Pod.

Sebagai contoh, misalkan pengguna eksternal (203.0.113.10, port: 30000) mengakses layanan melalui NodePort (30001) dari sebuah node Kubernetes (192.168.1.5). Anggaplah 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)

Di sini, node Kubernetes memiliki alamat IP 192.168.1.5 yang dapat diakses dari luar dan alamat IP 10.0.1.1 yang valid dalam jaringan Kubernetes internal. (Kebijakan terkait ini bervariasi tergantung pada jenis CNI yang digunakan, tetapi dalam artikel ini, kami akan menjelaskan berdasarkan Cilium.)

Ketika permintaan 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 DNAT diterapkan
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 bahwa 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 mengirimkan permintaan (203.0.113.10). Dalam kasus ini, pengguna eksternal akan menerima respons dari alamat IP yang tidak ada yang tidak pernah mereka minta dan akan langsung menghapus paket tersebut. Oleh karena itu, node Kubernetes melakukan SNAT tambahan ketika Pod mengirimkan paket respons ke luar, 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 DNAT, SNAT diterapkan
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 NodePort, dan node akan mengembalikan informasi ke pengguna eksternal dengan membalikkan proses DNAT dan SNAT yang sama. Dalam proses ini, setiap node akan 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 Utama

Secara umum, di Linux, proses NAT ini dikelola dan dioperasikan oleh subsistem conntrack melalui iptables. Faktanya, proyek CNI lainnya seperti flannel atau calico memanfaatkan ini untuk mengatasi masalah di atas. Namun, masalahnya adalah Cilium menggunakan teknologi eBPF dan sama sekali mengabaikan tumpukan jaringan Linux tradisional ini. 🤣

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

Akibatnya, Cilium memilih untuk mengimplementasikan secara langsung hanya fungsi-fungsi yang diperlukan dalam situasi Kubernetes dari tugas-tugas yang sebelumnya dilakukan oleh tumpukan jaringan Linux tradisional, seperti yang ditunjukkan pada gambar di atas. Oleh karena itu, untuk 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 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, dll. metadata
4----------------------------------------------
5|            |          |         |          |

Dan karena ini adalah Hash Table, untuk pencarian cepat, ada nilai kunci yang digunakan, yaitu kombinasi src ip, src port, dst ip, dst port sebagai nilai kunci (lihat di sini).

Identifikasi Masalah

Fenomena - 1: Pencarian

Akibatnya, ada satu masalah. Paket yang melewati eBPF harus melakukan pencarian pada Hash Table di atas untuk memverifikasi apakah paket tersebut perlu melakukan proses SNAT atau DNAT. 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 proses NAT, dan memiliki karakteristik di mana nilai src ip, port, dan dst ip, port dibalik.

Oleh karena itu, untuk pencarian cepat, harus ada penambahan nilai lain ke Hash Table dengan kunci yang nilainya dibalik src dan dst, atau harus dilakukan dua kali pencarian pada Hash Table yang sama untuk semua paket, bahkan jika paket tersebut tidak 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

Dan terlepas dari masalah di atas, tidak ada sumber daya tak terbatas pada setiap perangkat keras, dan karena logika tingkat perangkat keras membutuhkan kinerja cepat, struktur data dinamis juga tidak dapat digunakan. Dalam situasi di mana sumber daya terbatas, data yang ada perlu dikeluarkan (evict). Cilium menyelesaikannya dengan menggunakan LRU Hash Map, struktur data dasar yang disediakan secara default di Linux.

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. Data yang sama dicatat dua kali dalam satu Hash Table untuk paket keluar dan paket masuk.
  2. Dalam situasi di mana salah satu dari dua 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, maka NAT tidak dapat dilakukan dengan benar, yang dapat menyebabkan hilangnya koneksi secara keseluruhan.

Solusi

Di sinilah PR yang disebutkan sebelumnya muncul.

bpf:nat: memulihkan entri NAT jika REV NAT-nya tidak ditemukan

bpf:nat: Pulihkan entri NAT ORG jika tidak ditemukan

Sebelumnya, ketika sebuah paket melewati eBPF, ia mencoba mencari di tabel SNAT dengan membuat kunci dari kombinasi src ip, src port, dst ip, dst port. Jika kunci tidak ada, informasi NAT baru dibuat sesuai dengan aturan SNAT dan dicatat dalam tabel. Untuk koneksi baru, ini akan menghasilkan komunikasi yang normal, dan jika kunci dihapus secara tidak sengaja oleh LRU, NAT akan dilakukan ulang dengan port yang berbeda dari yang digunakan dalam komunikasi sebelumnya, menyebabkan penerima paket menolak penerimaan dan koneksi akan diakhiri dengan paket RST.

Pendekatan PR di atas sederhana:

Kapan pun paket diamati dalam arah mana pun, perbarui entri untuk arah sebaliknya juga.

Setiap kali komunikasi teramati dari arah mana pun, kedua entri akan diperbarui, menjauhkan mereka dari prioritas pengeluaran (eviction) dalam logika LRU. Hal ini mengurangi kemungkinan skenario di mana hanya satu entri yang dihapus, menyebabkan seluruh komunikasi terganggu.

Ini adalah pendekatan yang sangat sederhana dan mungkin terlihat seperti ide yang mudah, tetapi melalui pendekatan ini, masalah terputusnya koneksi karena kedaluwarsa informasi NAT untuk paket respons telah berhasil diatasi, dan stabilitas sistem telah meningkat secara signifikan. Hal ini juga merupakan peningkatan penting yang telah mencapai hasil berikut dalam hal stabilitas jaringan:

benchmark

Kesimpulan

Saya berpendapat bahwa PR ini adalah contoh yang sangat baik yang menunjukkan bagaimana satu ide sederhana dapat membawa perubahan besar dalam sistem yang kompleks, dimulai dari pengetahuan CS dasar tentang bagaimana NAT bekerja.

Ah, tentu saja, saya tidak menunjukkan secara langsung 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 dengan kata "Please", dan hasilnya adalah pengetahuan Cilium +1 dan gambar di bawah ini. 😇

diagram

Dan dengan membaca isu dan PR, saya menulis artikel ini sebagai kompensasi atas firasat buruk bahwa isu-isu mungkin muncul karena sesuatu yang saya buat di masa lalu.

Ulasan - 1

Sebagai referensi, ada cara yang sangat efektif untuk menghindari masalah ini. Karena akar penyebab masalah adalah kurangnya ruang tabel NAT, kita hanya perlu meningkatkan ukuran tabel NAT. :-D

Ada seseorang yang, ketika menghadapi masalah yang sama, tidak meninggalkan isu dan malah meningkatkan ukuran tabel NAT lalu menghilang. Saya mengagumi dan menghormati semangat Bapak gyutaeb yang telah menganalisis, memahami, dan berkontribusi pada ekosistem Cilium dengan data objektif, meskipun isu tersebut tidak secara langsung terkait dengannya.

Ini adalah alasan mengapa saya memutuskan untuk menulis artikel ini.

Ulasan - 2

Kisah ini sebenarnya tidak secara langsung berkaitan dengan Gosuda, yang secara profesional berurusan dengan bahasa Go. Namun, karena bahasa Go dan ekosistem cloud memiliki hubungan yang erat, dan kontributor Cilium memiliki tingkat kemahiran tertentu dalam bahasa Go, saya mencoba membawa konten yang bisa diunggah ke blog pribadi ke Gosuda.

Karena salah satu administrator (saya sendiri) telah memberikan izin, saya rasa ini tidak akan menjadi masalah.

Jika Anda merasa ini tidak baik, simpanlah ke PDF sekarang juga sebelum dihapus kapan saja. ;)

Ulasan - 3

Penulisan artikel ini sangat terbantu oleh Cline dan Llama 4 Maveric. Meskipun saya memulai analisis dengan Gemini dan memohon kepada DeepSeek, bantuan sebenarnya saya dapatkan dari Llama 4. Llama 4 sangat bagus. Pastikan untuk mencobanya.