1 poin oleh GN⁺ 3 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • 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_wait masih perlu memanggil read()/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, sementara IORING_SETUP_SQPOLL mengurangi 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() atau write()
  • Dalam alur umum, biaya syscall berulang pada setiap event
    • epoll_ctl adalah syscall satu kali untuk mendaftarkan file descriptor
    • Untuk setiap event I/O nyata, epoll_wait serta read()/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 memanggil read() terpisah saat event datang
    • Membuat instance epoll dengan epoll_create1
    • Mendaftarkan STDIN_FILENO dengan epoll_ctl
    • Melakukan blok dengan epoll_wait sampai bisa dibaca
    • Saat event datang, membaca data dengan syscall read()
    • Dalam alur ini, setiap event I/O nyata memerlukan epoll_wait dan read
  • Contoh io_uring

    • Menggunakan liburing
    • Menginisialisasi ring dengan io_uring_queue_init
    • Mengambil submission queue entry dengan io_uring_get_sqe
    • Menyiapkan operasi baca stdin dengan io_uring_prep_read
    • Mengajukannya dengan io_uring_submit dan menunggu completion dengan io_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() mengembalikan NULL karena submission queue penuh

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_ZC pada kernel 6.0+ menyediakan pengiriman tanpa menyalin buffer ke kernel
  • IORING_SETUP_SQPOLL dapat 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 res pada completion queue entry
    • Penanganan error harus dilakukan lewat cqe->res

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

 
GN⁺ 3 jam lalu
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 jauh
    Jika 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

    • v0 dan v1 di repositori itu adalah implementasi yang sepenuhnya berbeda yang hampir ditulis ulang dari nol, dan sekarang sedang dikerjakan implementasi ketiga yang mungkin akan jadi yang terakhir. Pilihan arsitekturnya juga benar-benar berbeda
    • Saya ingin melihat benchmark dari patch itu
  • 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

    • Rencananya adalah menerapkan optimisasi di lapisan lain terlebih dulu lalu beralih ke allocator. Saat ini saya sedang mempelajari allocator bersama para mahasiswa, dan tulisan blog sebelumnya membahas custom allocator yang dibuat dengan bahasa Zig
  • 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 artistik

  • Pada web server berbasis io_uring, saya belum menguji shared buffer. Sebab saya mengirim langsung dari area yang di-mmap, bukan membaca dari file lalu menulisnya
    Sebenarnya saya ingin menggunakan sendfile lewat io_uring, tetapi itu masih belum didukung
    Artikel 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

    • Sebagai catatan, splice(2) sudah diimplementasikan, jadi dengan uring kita bisa memakai cara mirip sendfile. Memang tidak senyaman sendfile, tetapi seharusnya bekerja hampir sama
  • Kalau 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_wait juga 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 memakainya
    Ini 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 khusus
    Kasus 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

    • Baru-baru ini, setelah mengganti Asio dengan event loop epoll buatan sendiri, RPS meningkat sekitar 16%. Ini hasil dari server SQL berukuran sedang, jadi perlu hati-hati saat memakai library yang sudah dikemas rapi
    • Di server database, saat backend epoll Asio diganti ke io_uring, penggunaan CPU justru naik tajam. Kemungkinan besar hasilnya sangat bergantung pada cara pemakaian dan bagaimana integrasinya ke kode event
    • Boost terlalu merepotkan. Kumpulan dynamic library besar yang sulit dibangun dan digunakan. Padahal sudah memakai CMake, tapi proses memasang Boost agar bisa terdeteksi sangat merepotkan. Namun ini pengalaman di Mac
  • Sekitar tahun 2050 nanti, sepertinya akan ada 20 cara untuk melakukan polling socket di Linux

    • Benar, bahkan di dalam io_uring sendiri juga begitu. Demi jadi lebih cepat, muncul mode one-shot io_uring, lalu setelah itu bahkan ada mode multi-shot
  • Ya, io_uring memang jelas lebih cepat daripada epoll. Dalam kasus saya, io_uring tampaknya sekitar 20% lebih cepat dalam requests per second
    Masalahnya, 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_uring
    Karena itu, bahkan proyek engineering seperti Go yang mengejar performa setinggi mungkin pun tidak memasukkan io_uring secara 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 eksploit

    • Alasan utama penonaktifan itu sekarang sudah teratasi. Di RC terbaru sudah masuk dukungan cBPF, sehingga alih-alih mematikan semuanya, sekarang bisa membatasi operasi yang boleh dijalankan
    • Tergantung kasusnya. Ada kalanya emulasi io_uring gaya POSIX saya yang dibuat dengan poll, bukan epoll, justru lebih cepat daripada io_uring. Namun untuk buffer zero-copy besar, io_uring adalah yang terbaik
      io_uring juga berguna meski bukan untuk I/O asinkron. Misalnya, rangkaian operasi seperti mkdir lalu membuka direktori itu bisa diimplementasikan seperti satu operasi atomik tunggal
      Dalam 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
    • RHEL 9 dan 10 kini mendukung io_uring sepenuhnya 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 membuktikannya
      https://access.redhat.com/solutions/4723221
      Go juga perlu meninjau ulang dukungannya. Rasanya layak dicoba sekali
    • Untuk proyek seperti Go, bukankah ada opsi untuk melakukan deteksi kemampuan io_uring hanya sekali saat runtime dimulai? Bukankah eksploit bukan masalah khusus program yang memilih memakai io_uring, melainkan masalah seluruh OS?
    • Semua jenis networking mode polling—RDMA, DPDK, io_uring—pada akhirnya cenderung membuat pengguna bertanggung jawab atas isolasi memori
      Namun dalam kasus io_uring, ring-nya ada di dalam kernel sehingga tidak banyak yang bisa dilakukan pengguna
      Saya 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