Perbandingan epoll dan io_uring di Linux
(sibexi.co)- Reverse proxy TinyGate meningkatkan performa dengan beralih ke epoll pada arsitektur berbasis worker, tetapi kemudian menemui batasannya dan ditulis ulang menggunakan io_uring
- epoll adalah model kesiapan yang memberi tahu kapan I/O siap dilakukan, sehingga setelah
epoll_waitmasih perlu memanggilread()/write()secara terpisah - io_uring adalah model penyelesaian yang bergerak berdasarkan selesainya I/O, dengan aplikasi dan kernel saling bertukar antrean pengajuan dan antrean penyelesaian melalui ring buffer bersama
io_uring_enter()pada dasarnya tetap diperlukan, tetapi dapat mengajukan dan mengambil kembali banyak operasi sekaligus, sementaraIORING_SETUP_SQPOLLmengurangi syscall dengan konsekuensi penggunaan CPU- Jika memulai proyek baru di server Linux modern yang memakai kernel v5.1+, io_uring dinilai sebagai pilihan yang lebih tepat daripada epoll
Batasan epoll yang terungkap lewat TinyGate
- TinyGate adalah server reverse proxy yang dibuat bersama para mahasiswa, dan versi pertamanya memakai struktur sederhana berbasis worker
- Sebagai proyek pembelajaran, ini berjalan dengan baik, tetapi dibandingkan alat seperti nginx atau haproxy, keterbatasan arsitekturnya cukup besar
- Versi kedua diubah menjadi berbasis epoll dan performanya meningkat jauh dibanding versi pertama
- Namun dalam benchmark, tetap belum bisa melampaui nginx/haproxy
- Setelah itu, karena batasan epoll, proyek ini beralih ke io_uring dan akhirnya ditulis ulang dari awal
epoll: notifikasi kesiapan dan syscall berulang
- epoll adalah cara pengelolaan I/O asinkron yang telah lama digunakan di Linux, dan masuk ke Linux kernel pada 2002
- Intinya adalah notifikasi status siap yang memberi tahu kapan I/O dapat dilakukan
- epoll memberi tahu bahwa sesuatu “bisa dibaca atau ditulis”
- Pembacaan dan penulisan data yang sebenarnya kemudian dilakukan oleh aplikasi melalui syscall
read()atauwrite()
- Dalam alur umum, biaya syscall berulang pada setiap event
epoll_ctladalah syscall satu kali untuk mendaftarkan file descriptor- Untuk setiap event I/O nyata,
epoll_waitsertaread()/write()tetap diperlukan - Akibatnya, syscall tambahan terus menempel pada proses penanganan event
- Syscall menyebabkan perpindahan konteks antara mode pengguna dan mode kernel, dan overhead ini makin besar saat jumlah koneksi meningkat
io_uring: model penyelesaian dan ring buffer bersama
- io_uring muncul pada 2019, sekitar 17 tahun setelah epoll masuk ke Linux kernel, dan didukung mulai kernel v5.1+
- Berbeda dari epoll, mekanismenya tidak bertumpu pada apakah I/O sudah bisa dilakukan, melainkan pada apakah I/O sudah selesai
- Aplikasi dan kernel memakai ring buffer di memori bersama
- Pada submission queue, aplikasi menaruh pekerjaan yang ingin diminta ke kernel
- Pada completion queue, kernel mengembalikan hasil yang telah selesai
- Dalam konfigurasi dasar,
io_uring_enter()perlu dipanggil agar kernel memeriksa submission queue- Satu pemanggilan dapat mengajukan banyak operasi sekaligus dan mengambil banyak completion sekaligus
- Ini bukan struktur yang mengulang pasangan syscall per operasi seperti kombinasi epoll dan
read()
- Jika memakai
IORING_SETUP_SQPOLL, thread kernel akan melakukan polling pada submission queue- Dalam kondisi operasi normal, syscall hampir bisa dihilangkan
- Karena thread kernel tetap berjalan walau queue kosong, CPU tetap terpakai
- Setelah
sq_thread_idle, ia bisa tidur, tetapi biayanya tidak benar-benar hilang
Perbedaan lewat contoh kode
-
Contoh epoll
- Mendaftarkan file descriptor
stdin, lalu memanggilread()terpisah saat event datang - Membuat instance epoll dengan
epoll_create1 - Mendaftarkan
STDIN_FILENOdenganepoll_ctl - Melakukan blok dengan
epoll_waitsampai bisa dibaca - Saat event datang, membaca data dengan syscall
read() - Dalam alur ini, setiap event I/O nyata memerlukan
epoll_waitdanread
- Mendaftarkan file descriptor
-
Contoh io_uring
- Menggunakan
liburing - Menginisialisasi ring dengan
io_uring_queue_init - Mengambil submission queue entry dengan
io_uring_get_sqe - Menyiapkan operasi baca
stdindenganio_uring_prep_read - Mengajukannya dengan
io_uring_submitdan menunggu completion denganio_uring_wait_cqe - Dalam contoh io_uring, tidak ada pemeriksaan status siap terpisah, dan tidak ada pemanggilan
read()lagi saat completion datang - Untuk penyederhanaan, kedua contoh menghilangkan penanganan exception penting
- Jika tidak ada data pada
stdin, keduanya bisa terblokir selamanya - Contoh io_uring tidak memeriksa kasus ketika
io_uring_get_sqe()mengembalikanNULLkarena submission queue penuh
- Menggunakan
Syarat tambahan saat memakai io_uring
- Untuk memakai zero-copy I/O, buffer perlu didaftarkan lebih dulu dengan
io_uring_register_buffers()- Ini dapat menghindari kernel memetakan ulang memori pada setiap operasi
- Untuk pengiriman jaringan,
IORING_OP_SEND_ZCpada kernel 6.0+ menyediakan pengiriman tanpa menyalin buffer ke kernel
IORING_SETUP_SQPOLLdapat mengurangi syscall, tetapi biayanya adalah penggunaan CPU- Thread kernel tetap melakukan polling meski queue kosong
- Setelah idle timeout, ia bisa beralih ke mode tidur, tetapi biaya itu tidak hilang sepenuhnya
- Error pada io_uring tidak dikembalikan langsung seperti syscall sinkron, melainkan secara asinkron melalui field
respada completion queue entry- Penanganan error harus dilakukan lewat
cqe->res
- Penanganan error harus dilakukan lewat
Pilihan di server Linux modern
- epoll adalah metode I/O asinkron Linux lama yang bertumpu pada notifikasi saat I/O bisa dilakukan dan pemanggilan syscall terpisah
- io_uring di Linux modern menyediakan model berbasis penyelesaian serta pengajuan batch dan pemrosesan completion batch
- Jika membangun proyek baru dari awal di server Linux modern, memilih io_uring terasa lebih alami
- Jika dukungan untuk sistem lama bisa dihentikan pada waktu yang masuk akal, maka di lingkungan kernel v5.1+ tidak banyak alasan untuk memilih epoll
1 komentar
Opini Hacker News
Saya sempat melihat sangat singkat repositori GitHub https://github.com/sibexico/TinyGate, dan sepertinya CPU pinning masih belum digunakan
Dengan menambatkan thread dan socket listen ke CPU, lalu menggunakan
sockopt SO_INCOMING_CPU, performa masih bisa didorong sedikit lebih jauhJika socket keluar juga disejajarkan ke CPU, peningkatannya bisa cukup besar, tetapi setahu saya belum ada API yang bagus untuk itu. Linux punya API traffic steering/flow steering untuk NIC yang kompatibel, dan jika kita tahu hash yang dipakai NIC—kemungkinan besar Toeplitz—kita bisa memilih source port ke backend dengan tepat agar hashnya cocok
Tujuannya adalah membuat proxy memproses paket tanpa komunikasi antar-CPU
Mungkin bagus untuk melihat https://github.com/concurrencykit/ck dan https://github.com/microsoft/mimalloc. Keduanya akan cocok untuk reverse proxy zero-copy yang selaras memori
Jika ingin menambahkan perlindungan DDoS dan fitur L4 yang lebih canggih, https://docs.ebpf.io/ebpf-library/libxdp/libxdp/ juga layak diperiksa
Tulisan yang sangat bagus
Gara-gara tulisan ini saya jatuh ke lubang kelinci
uring, pengembangan kernel, dan C. Saya sudah cukup lama mengembangkan dengan Rust dan C++, tetapi pada program C yang kecil dan berukuran pas, ada kesederhanaan dan bahkan nuansa artistikPada web server berbasis
io_uring, saya belum menguji shared buffer. Sebab saya mengirim langsung dari area yang di-mmap, bukan membaca dari file lalu menulisnyaSebenarnya saya ingin menggunakan
sendfilelewatio_uring, tetapi itu masih belum didukungArtikel dengan bumbu kata-kata populer seperti Rust dan kTLS: https://blog.habets.se/2025/04/io-uring-ktls-and-rust-for-ze...
Juga pernah muncul di HN: https://news.ycombinator.com/item?id=44980865
splice(2)sudah diimplementasikan, jadi denganuringkita bisa memakai cara mirip sendfile. Memang tidak senyamansendfile, tetapi seharusnya bekerja hampir samaKalau dibuat dengan DPDK, semuanya akan jadi jauh lebih rumit, tetapi dari sisi performa ada peluang untuk mengalahkan nginx
Kalau dibuat berjalan di FPGA, akan jadi lebih rumit lagi
Pelajarannya adalah demi performa, kadang kita perlu menerobos abstraksi seperti pisau panas membelah mentega, tetapi sebagai gantinya semua hal menjadi lebih sulit. Pendekatan socket dan satu thread per koneksi adalah pendekatan yang baik pada masa ketika jaringan jauh lebih lambat daripada CPU, dan bahkan hari ini pun sering kali tetap menjadi pendekatan yang paling sederhana
Saya juga selalu penasaran soal ini, jadi baru-baru ini saya menulis beberapa implementasi HTTP file server untuk memahami perbedaan intinya
https://theconsensus.dev/p/2026/05/18/serving-files-three-wa...
Dalam konteks proxy, busy polling
epoll_waitjuga perlu disebutkan. Saya sempat meninjaunya saat mengevaluasi opsi latensi rendah baru-baru ini, dan tampaknya dengan socket biasa saja kita bisa mendekati busy polling di user space tanpa DPDK/VMA/io_uring, dan Fastly berkontribusi pada ini serta memakainyaIni sangat low-level sehingga saya tidak bisa bilang benar-benar memahami semuanya, saya hanya menangkap konsepnya saja, jadi saya tinggalkan tautannya. Fitur ini hanya bekerja per konteks NAPI
epoll, dan NAPI ID tidak mudah dikendalikan, tetapi jika seluruh mesin dipakai khusus untuk proxy, mungkin ada trik sederhana untuk menetapkan socket per NAPI ID ke poller khususKasus penggunaan saya bukan proxy, melainkan mem-poll N buah socket di satu mesin lalu memproses data yang diterima. Untuk kasus seperti itu tampaknya tidak realistis, meski mungkin bisa jika satu thread melakukan polling konteks NAPI secara round-robin. Akan bagus jika suatu hari kita bisa dengan mudah memberi tahu kernel, “percayalah, socket tunggal ini pada akhirnya akan saya poll, jadi jangan pernah pakai jalur IRQ”
Diskusi HN sebelumnya tentang fitur kernel ini: https://news.ycombinator.com/item?id=43749271
Materi presentasi yang bagus dari kontributor Fastly, dengan diagram yang memudahkan memahami gambaran besarnya: https://netdevconf.info/0x18/docs/netdev-0x18-paper10-talk-s...
Artikel LWN: https://lwn.net/Articles/1008399/, https://lwn.net/Articles/997491/, https://lwn.net/Articles/959462/
Dokumentasi kernel: https://docs.kernel.org/networking/napi.html#irq-mitigation
Jika suka C++ dan jaringan asinkron, ada Boost.Asio
epollbuatan sendiri, RPS meningkat sekitar 16%. Ini hasil dari server SQL berukuran sedang, jadi perlu hati-hati saat memakai library yang sudah dikemas rapiepollAsio diganti keio_uring, penggunaan CPU justru naik tajam. Kemungkinan besar hasilnya sangat bergantung pada cara pemakaian dan bagaimana integrasinya ke kode eventSekitar tahun 2050 nanti, sepertinya akan ada 20 cara untuk melakukan polling socket di Linux
io_uringsendiri juga begitu. Demi jadi lebih cepat, muncul mode one-shotio_uring, lalu setelah itu bahkan ada mode multi-shotYa,
io_uringmemang jelas lebih cepat daripadaepoll. Dalam kasus saya,io_uringtampaknya sekitar 20% lebih cepat dalam requests per secondMasalahnya, ini harus diaktifkan secara eksplisit di kernel, dan karena alasan keamanan dinonaktifkan hampir di mana-mana. Sepertinya ada pembagian memori langsung antara kernel dan user space, yang terasa cukup mengkhawatirkan. Belakangan ini juga beberapa kali ada eksploit yang menargetkan
io_uringKarena itu, bahkan proyek engineering seperti Go yang mengejar performa setinggi mungkin pun tidak memasukkan
io_uringsecara mendalam sebagai default yang masuk akal. Jika mau mengambil risikonya, tetap bisa menjalankannya langsung di bahasa favorit Anda. Memang lebih cepat, tetapi harganya adalah potensi kemungkinan eksploitio_uringgaya POSIX saya yang dibuat denganpoll, bukanepoll, justru lebih cepat daripadaio_uring. Namun untuk buffer zero-copy besar,io_uringadalah yang terbaikio_uringjuga berguna meski bukan untuk I/O asinkron. Misalnya, rangkaian operasi sepertimkdirlalu membuka direktori itu bisa diimplementasikan seperti satu operasi atomik tunggalDalam jaringan, kalau mencoba memaksimalkan jumlah paket per detik, Anda akan sangat cepat menabrak batas kernel[1], dan pada akhirnya harus memanfaatkan fitur seperti GSO/GRO atau melewati network stack sepenuhnya
1: https://github.com/axboe/liburing/discussions/1346
io_uringsepenuhnya secara default. Ini memang sangat baru, tetapi berarti banyak instalasi Linux perusahaan kini tercakup. Gemini “mengatakan” Ubuntu dan SuSE juga mendukungnya, tetapi tidak memberi tautan untuk membuktikannyahttps://access.redhat.com/solutions/4723221
Go juga perlu meninjau ulang dukungannya. Rasanya layak dicoba sekali
io_uringhanya sekali saat runtime dimulai? Bukankah eksploit bukan masalah khusus program yang memilih memakaiio_uring, melainkan masalah seluruh OS?io_uring—pada akhirnya cenderung membuat pengguna bertanggung jawab atas isolasi memoriNamun dalam kasus
io_uring, ring-nya ada di dalam kernel sehingga tidak banyak yang bisa dilakukan penggunaSaya berharap keadaan akan membaik berkat LLM, tetapi ini masalah yang sulit diselesaikan. Menanganinya di level kernel sendiri juga sangat sulit, dan sering kali orang pun tidak benar-benar paham cara men-tuning hal ini