2 poin oleh GN⁺ 2025-08-23 | 1 komentar | Bagikan ke WhatsApp
  • Untuk membuat server web berperforma tinggi, sebelumnya digunakan berbagai model berbasis event seperti select(), poll(), epoll
  • Namun karena batas performa dari system call tersebut, muncullah io_uring, yang memperkenalkan cara memasukkan permintaan ke antrean agar kernel memprosesnya secara asinkron
  • kTLS membuat kernel menangani pemrosesan enkripsi TLS, sehingga dimungkinkan penggunaan sendfile() dan offloading perangkat keras untuk optimasi tambahan
  • Pengenalan Descriptorless files menyediakan pendekatan yang dioptimalkan untuk io_uring tanpa perlu meneruskan file descriptor secara langsung
  • Melalui proyek open source tarweb yang menggabungkan Rust, io_uring, dan kTLS, HTTPS dapat disediakan tanpa system call tambahan per permintaan, sekaligus membahas isu keamanan dan manajemen memori

Evolusi arsitektur server web berperforma tinggi

  • Sejak awal 2000-an, kebutuhan akan server web berkapasitas tinggi terus meningkat
  • Pada awalnya, pendekatan yang umum adalah membuat proses baru untuk setiap permintaan, tetapi karena biayanya tinggi, muncullah teknik preforking
  • Setelah itu, arsitektur berkembang melalui penggunaan thread serta aktivasi select(), poll() untuk mengurangi biaya context switching
  • Namun, pendekatan select() dan poll() juga memiliki batas skalabilitas karena semakin banyak koneksi, semakin sering array besar harus dikirim ke kernel

Munculnya epoll

  • Di lingkungan Linux, epoll diperkenalkan sehingga pemrosesan banyak koneksi menjadi lebih efisien dibanding pendekatan sebelumnya
  • epoll hanya menangani perubahan (delta), sehingga mengurangi konsumsi resource yang tidak perlu
  • Meski tidak menghilangkan semua system call sepenuhnya, biayanya berkurang secara signifikan

Gambaran umum io_uring

  • io_uring menambahkan permintaan ke antrean di memori agar kernel dapat memprosesnya secara asinkron, alih-alih memanggil system call untuk setiap permintaan
  • Misalnya, jika accept() dimasukkan ke antrean, kernel akan memprosesnya lalu mengembalikan hasil ke completion queue
  • Server web bekerja dengan menambahkan permintaan ke antrean, lalu memeriksa hasilnya di area memori terpisah
  • Untuk menghindari busy loop, jika tidak ada perubahan pada antrean maka server web dan kernel hanya memanggil system call saat benar-benar diperlukan, sehingga ada efek penghematan daya
  • Dengan library yang tepat, server yang sedang aktif dapat berjalan tanpa system call tambahan selama pemrosesan permintaan

Multi-core dan lingkungan NUMA

  • Dengan mempertimbangkan lingkungan CPU multi-core modern, strategi single thread per core dan meminimalkan berbagi struktur data merupakan pendekatan yang efektif
  • Di lingkungan NUMA, optimasi dilakukan dengan membuat setiap thread hanya mengakses memori node lokalnya sendiri
  • Penyeimbangan distribusi permintaan secara sempurna masih memerlukan riset tambahan

Alokasi memori

  • Baik di kernel maupun di server web, alokasi memori tetap ada, dan alokasi di user space pada akhirnya juga terhubung ke system call
  • Di sisi server web, blok memori berukuran tetap dialokasikan terlebih dahulu untuk tiap koneksi guna mencegah fragmentasi dan kekurangan memori
  • Di sisi kernel, buffer I/O per koneksi juga diperlukan, dan sebagian dapat disesuaikan melalui opsi socket
  • Jika terjadi kekurangan memori, hal ini dapat berujung pada gangguan serius

Pengenalan kTLS (kernel TLS)

  • kTLS adalah fitur di kernel Linux yang menangani operasi enkripsi dan dekripsi
  • Handshake ditangani oleh aplikasi, tetapi setelah itu kernel memproses transmisi data seolah-olah berupa teks biasa
  • Penggunaan sendfile() menjadi mungkin, sehingga salinan memori antara user space dan kernel space dapat dikurangi
  • Jika didukung oleh kartu jaringan, ada keuntungan tambahan berupa offloading operasi enkripsi ke perangkat keras

Descriptorless Files

  • Ini adalah pendekatan yang muncul untuk mengurangi overhead saat meneruskan file descriptor secara langsung dari user space ke kernel space
  • Dengan register_files, digunakan nomor file berupa 'integer' terpisah yang hanya valid di io_uring, dan tidak muncul di /proc/pid/fd
  • Batas ulimit sistem tetap berlaku

Pengenalan proyek tarweb

  • tarweb adalah proyek open source server web contoh yang menerapkan semua teknologi di atas
  • Strukturnya menyajikan isi dari satu file tar, dan menggabungkan teknologi berperforma tinggi modern seperti Rust, io_uring, kTLS
  • Dalam penggunaan nyata, ada masalah kompatibilitas antara io_uring dan kTLS (seperti setsockopt yang belum didukung), dan beberapa isu diselesaikan melalui Pull Request
  • Proyek ini masih belum selesai, dan library rustls milik Rust dapat melakukan alokasi memori selama proses handshake
  • Poin utamanya adalah layanan HTTPS dimungkinkan tanpa system call tambahan untuk tiap permintaan

Benchmark dan pengukuran performa

  • Penulis belum melakukan benchmark yang cukup, dan berencana menguji performa setelah perapian kode selesai

Masalah keamanan io_uring dan Rust

  • Berbeda dari system call sinkron, pada io_uring buffer memori tidak boleh dibebaskan sebelum event penyelesaian diterima
  • crate io-uring tidak menjamin keamanan compile-time ala Rust, dan juga minim pemeriksaan saat runtime
  • Jika digunakan secara keliru, hal ini dapat menyebabkan masalah serius seperti pada C++, sehingga keamanan bawaan Rust menjadi melemah
  • Diperlukan crate safer-ring terpisah yang secara aktif memanfaatkan pinning dan borrow checker
  • Masalah ini sudah sedang dibahas di komunitas

Referensi dan tautan tambahan

  • Konten ini adalah postingan yang dibahas di HackerNews per 2025-08-22

1 komentar

 
GN⁺ 2025-08-23
Komentar Hacker News
  • Saat mengirim operasi tulis dengan menggunakan io_uring, kita harus memastikan lokasi memori tidak dibebaskan atau tertimpa, tetapi API crate io-uring tampaknya tidak dibantu oleh borrow checker Rust untuk bagian ini dan juga tidak memiliki pemeriksaan runtime.
    Saya sudah membaca tulisan dan komentar tentang situasi seperti ini, dan kesimpulannya saya mendapat kesan bahwa membuat library async Rust yang aman yang membungkus io_uring itu benar-benar sulit.
    Saya juga ingat Alice dari tim tokio baru-baru ini menyebutkan bahwa minat untuk mengatasi masalah ini sekarang tidak begitu besar.
    Alasannya karena performanya saat ini sudah "cukup bagus".
    Referensi: https://boats.gitlab.io/blog/post/io-uring/

    • Ada banyak hal yang saya sesalkan dari Rust async, dan ini salah satunya.
      Rust async dirancang pada masa epoll menjadi standar dan hampir tidak memedulikan IOCP.
      Syscall sinkron tidak punya masalah seperti ini karena saat memanggil read kita menyerahkan referensi mutable buffer ke kernel, dan itu cocok dengan model ownership/borrow bawaan Rust.
      Namun untuk completion-based IO, agar benar-benar cocok dengan model ownership, harus ada jaminan bahwa kode pengguna tidak terus berjalan sampai pekerjaannya selesai, dan ini tidak bisa dilakukan dengan struktur state machine polling.
      Model threading atau struktur green thread sangat pas untuk kasus ini.
      Seandainya Rust menambahkan "target khusus async", hasilnya mungkin akan lebih baik.
      Tim pengembang Rust menaruh banyak harapan pada model polling stackless async, dan sekarang kita sedang melihat bagaimana akhirnya.

    • Saya rasa ada model ownership yang tidak bisa didukung dengan baik oleh borrow checker Rust.
      Saya menyebutnya sementara sebagai "hot potato ownership", yaitu struktur di mana buffer diserahkan sebentar lalu diterima kembali.
      Menulis pola seperti ini secara aman di Rust terasa sangat sulit dan membuat kodenya berantakan.

    • Berbeda dengan ucapan Alice dari tim tokio, di sisi file IO justru ada minat.
      File IO sudah diimplementasikan dengan pendekatan spawn_blocking sehingga sekarang pun sudah menghadapi isu buffer yang sama seperti io_uring, dan memindahkannya ke io_uring tidak terlalu sulit.
      Namun API tokio::net yang ada sekarang tidak kompatibel dengan API buffer berbasis io_uring, jadi pemeriksaan readiness memang bisa dilakukan, tetapi dukungan penuh akan sulit.

    • Untuk membuat antarmuka io_uring yang aman, menurut saya pendekatan yang paling tepat adalah menerima buffer yang dimiliki oleh ring untuk digunakan, lalu mengembalikannya lagi saat memulai penulisan.

    • Tidak harus mengekspresikan semuanya dengan borrows.
      Dengan menggunakan struktur data seperti Slab, ini bisa dibuat cancel safe.
      Referensi: https://github.com/steelcake/io2

  • Saya sangat menikmati membaca tulisan ini.
    Saya menantikan pengujian performanya, tetapi saya terkesan karena penulis mengatakan akan merapikan kode terlebih dahulu sebelum benchmark.
    Di zaman ketika benchmark sering dianggap segalanya, segar rasanya melihat ada orang yang berpikir seperti ini.
    Saat berusia sekitar 11 tahun saya pernah mencoba membangun database dan berkenalan dengan cgi-bin, dan baru sekarang saya sadar bahwa itu bekerja dengan meluncurkan proses baru untuk setiap request.
    sendfile dulu menjadi game changer saat forum game besar harus menangani unduhan demo secara bersamaan, dan melihat hasil seperti pengurangan 40ms di Netflix atau pemangkasan waktu loading 70% di GTA 5 membuat saya merasa ada engineering yang jauh lebih berdampak tersembunyi di baliknya.
    Tautan terkait: Common Gateway Interface, kasus Netflix 40ms, pemangkasan loading GTA Online

    • Bukan hanya CGI, sesi HTTP lama dari lini CERN dan Apache juga berjalan dengan melakukan fork pada seluruh server.
      Seiring waktu hal itu membaik, tetapi karena cara konfigurasi Apache, server ringan yang sejak awal dirancang dengan event-based I/O seperti nginx akhirnya menjadi sangat populer.

    • Saya skeptis terhadap efisiensi sendfile.
      Memang sempat populer pada akhir 90-an, tetapi menurut saya keuntungan performa nyatanya kecil.

  • Sebagian besar orkestrator workload cloud (CloudRun, GKE, EKS, Docker lokal, dan sebagainya) menonaktifkan io_uring secara default.
    Jika bagian ini tidak membaik, tampaknya untuk sementara io_uring akan tetap menjadi teknologi yang sangat terbatas.

    • Saya jadi bertanya-tanya kenapa mereka menonaktifkan io_uring.

    • Kalau situasinya seperti ini, kita harus kembali ke self-hosting.

  • Sangat menyenangkan untuk dibaca.
    Saya akan menunggu benchmark-nya, jadi santai saja, dan saya sangat terkesan dengan pola pikir penulis yang lebih mementingkan perapian kode daripada benchmark.
    Belakangan ini banyak proyek yang all-in pada skor benchmark, jadi cara berpikir seperti ini benar-benar segar dan patut dihormati.
    Saya tidak tahu kTLS atau io_uring bisa dipakai dengan cara yang sedemikian beragam.

  • Kondisi pemrosesan asinkron saat ini kurang lebih seperti berikut.
    Rust: perlu memahami berbagai konsep seperti Futures, Pin, Waker, async runtime, batasan Send/Sync, async trait object, dan lain-lain
    C++20: coroutines
    Go: goroutines
    Java21+: virtual threads

    • Coroutine C++ menggunakan alokasi heap untuk menghindari masalah yang diselesaikan oleh Pin.
      Ini cukup jauh menyimpang dari prinsip "zero overhead" yang dikejar C++.
      Alasan Rust juga butuh waktu lama untuk memasukkan async trait di masa depan adalah karena Rust tidak mengalokasikan futures di heap.
      Trade-off antara performa/portabilitas dan kompleksitas bisa memiliki nilai yang berbeda tergantung proyek masing-masing.

    • Batasan terkait Send/Sync tetap bermakna dalam bahasa lain juga, dan tanpa batasan itu akan lebih mudah menulis kode yang salah secara halus.

    • Jika kita menulis kode Rust yang "cukup oke", dan memakai primitive level menengah yang dibuat orang lain, sebenarnya tidak perlu memahami semua konsep itu.

    • Rust memaksa agar tanpa memahami konsep-konsep itu, kodenya memang tidak akan bisa dikompilasi.
      Di Go, goroutine itu bukan async, dan kalau tidak memahami channel, kita juga tidak bisa memahami goroutine.
      Implementasi channel di Go unik sehingga perilaku pada kasus batas tidak mudah diprediksi secara intuitif.
      Di Go kita tetap bisa menulis kode tanpa pemahaman mendalam, jadi ada plus minusnya.
      "Thread murah" tidak sama dengan async.
      tarweb (server yang muncul di blog) adalah struktur single-thread berbasis event loop io_uring, dengan gagasan satu thread per inti CPU.
      Daripada "keadaan terkini konkurensi skala besar", mungkin lebih tepat disebut "keadaan terkini thread murah".
      Pembeda terbesar antara cheap thread dan async loop adalah kemudahan untuk melakukan reasoning.
      Ada juga kekurangannya, yaitu tiap thread walaupun ringan tetap membutuhkan ukuran stack.

  • kTLS jelas merupakan sebuah kemajuan.
    Saya sendiri beberapa tahun lalu benar-benar membuat server dengan 0 syscall per request dan menulis posting blog tentang itu (https://wjwh.eu/posts/2021-10-01-no-syscall-server-iouring.html).
    Hanya saja ada kekurangannya: kita harus selalu melakukan busy-looping.
    io_uring berkembang dengan kecepatan yang benar-benar mengesankan dalam beberapa tahun terakhir.

  • Proyek ini benar-benar keren dan saya sudah lama membayangkan sesuatu yang mirip, jadi saya senang ada yang mewujudkannya.
    Jika ingin menulis BPF dengan Rust, saya merekomendasikan Aya.
    Github proyek Aya

  • Saya penasaran dengan status kTLS saat ini.
    Belum lama ini saya bertanya kepada seorang pengembang Cilium, dan Thomas Graf mengatakan dia berharap banyak, tetapi kenyataannya dukungan kernel masih kurang di banyak distribusi Linux sehingga jalan menuju aktivasi default masih panjang.

    • Memang disayangkan, tetapi saya juga penasaran seberapa sulit mengaktifkannya.
      Apakah harus memakai kernel kustom, atau bisa langsung dinyalakan saat runtime.
      Di FreeBSD, sejak versi 13 kTLS sudah masuk ke kernel/openssl, dan bisa ditoggle saat runtime dengan sysctl (kern.ipc.tls.enable=1).
      Di FreeBSD-15 nilai default-nya akan berubah menjadi aktif, dan di Netflix kTLS sudah dipakai selama hampir 10 tahun untuk enkripsi trafik.

    • kTLS secara keseluruhan terasa seperti ide yang buruk.

  • Saya ragu apakah struktur satu thread per inti CPU cocok untuk sistem berbasis time slice.
    Menurut pengalaman saya, pendekatan "oversubscribing" (menaruh lebih banyak thread daripada jumlah inti) memberi keuntungan nyata pada wall-clock time.
    Mungkin satu per inti lebih cocok ketika tidak ada preemptive scheduling atau semacamnya.
    Tentu saja kalau begitu, kita tidak sedang membicarakan Unix.

    • Jika menginginkan latensi rendah dan throughput tinggi, metode mengisolasi inti dan melakukan pinning thread cukup efektif.
      Pendekatan seperti ini bekerja baik di Linux, dan di sistem trading serta sejenisnya sering dipakai walaupun harus menerima inefisiensi.
      Sebagian besar inti akan diam melakukan spin dan pada praktiknya tidak ada pekerjaan, tetapi dari sisi latency dan throughput hasilnya optimal.

    • Jebakan struktur thread-per-core adalah salah mengira bahwa kita bisa "mengambil bagian yang enaknya saja".
      Pada dasarnya ini pilihan all-in atau tidak dipakai sama sekali.
      Implementasi setengah-setengah sama sekali tidak efisien.
      Namun jika dirancang dengan benar, ini sangat efisien di hampir semua situasi.
      Hanya sedikit pengembang yang benar-benar memahami know-how desain TPC, seperti load balancing antar inti.

    • thread-per-core hanya efisien saat "CPU bound".
      Ketika seperti proyek server ini sebagian besar pekerjaan bersifat asinkron dan event-based, server hampir selalu langsung berpindah ke request berikutnya tanpa menunggu I/O atau syscall, sehingga secara teoretis satu thread per inti memang struktur yang tepat.
      Namun di dunia nyata situasi ideal seperti ini hampir tidak pernah ada, jadi perlu diingat bahwa membatasi tanpa syarat ke nproc thread itu berisiko.

    • Dalam io_uring, memiliki hanya satu thread pengguna per inti juga tampaknya bukan pilihan yang buruk.
      Karena di kernel ia bekerja lewat thread pool.

  • Saya juga ingin melihat gaya seperti DPDK yang sepenuhnya melewati kernel.