1 puan yazan GN⁺ 3 시간 전 | 1 yorum | WhatsApp'ta paylaş
  • TinyGate reverse proxy'si worker tabanlı yapıdan epoll'e geçerek performansı artırdı, ancak daha sonra sınırlarına ulaşıp io_uring ile yeniden yazıldı
  • epoll, I/O'nun mümkün olduğu anı bildiren bir hazır olma durumu modelidir; bu yüzden epoll_wait sonrasında read()/write() çağrılarının ayrıca yapılması gerekir
  • io_uring, I/O tamamlanmasına göre çalışan bir tamamlanma modelidir ve uygulama ile kernel, paylaşımlı bir halka tampon üzerinden gönderim kuyruğu ile tamamlanma kuyruğunu paylaşır
  • io_uring_enter() temelde gereklidir, ancak birden fazla işi tek seferde gönderip toplayabilir; IORING_SETUP_SQPOLL ise syscall sayısını azaltırken bunun karşılığında CPU kullanımı maliyeti getirir
  • kernel v5.1+ kullanan modern Linux sunucularında yeni bir projeye başlanacaksa, io_uring'in epoll'den daha uygun bir seçenek olduğu değerlendiriliyor

TinyGate'in ortaya çıkardığı epoll sınırları

  • TinyGate, öğrencilerle birlikte yapılan bir reverse proxy sunucusuydu ve ilk sürümü basit bir worker tabanlı yapıya sahipti
  • Eğitim amaçlı bir proje olarak çalışıyordu, ancak nginx ya da haproxy gibi araçlarla karşılaştırıldığında mimari sınırlamaları büyüktü
  • İkinci sürüm epoll tabanlı hale getirilince ilk sürüme göre performans ciddi biçimde iyileşti
    • Yine de benchmark'larda nginx/haproxy'yi geçemedi
  • Daha sonra epoll'ün sınırları nedeniyle io_uring'e geçildi ve proje baştan yazıldı

epoll: hazır olma bildirimi ve tekrarlayan syscall'lar

  • epoll, Linux'ta uzun süredir kullanılan asenkron I/O yönetim yöntemidir ve 2002'de Linux kernel'ine girdi
  • Temel fikir, I/O'nun yapılabileceği anı bildiren hazır olma bildirimidir
    • epoll, “okunabilir ya da yazılabilir” olduğunu bildirir
    • Gerçek veri okuma ve yazma işlemleri daha sonra uygulama tarafından read() veya write() syscall'larıyla yapılır
  • Tipik akışta her olay için syscall maliyeti tekrar eder
    • epoll_ctl, dosya tanıtıcısını kaydeden tek seferlik bir syscall'dır
    • Gerçek I/O olaylarının her biri için epoll_wait ve read()/write() gerekir
    • Sonuç olarak olay işleme sırasında ek syscall'lar sürekli birikir
  • syscall'lar, kullanıcı modu ile kernel modu arasında bağlam değişimi yaratır ve bağlantı sayısı arttıkça ek yük büyür

io_uring: tamamlanma modeli ve paylaşımlı halka tampon

  • io_uring, epoll'ün Linux kernel'ine girişinden yaklaşık 17 yıl sonra, 2019'da ortaya çıktı ve kernel v5.1+ ile destekleniyor
  • epoll'den farklı olarak I/O'nun mümkün olup olmadığına değil, I/O'nun tamamlanıp tamamlanmadığına göre çalışır
  • Uygulama ile kernel, paylaşımlı bellekteki bir halka tamponu birlikte kullanır
    • Gönderim kuyruğuna uygulama, kernel'e iletilecek işleri koyar
    • Tamamlanma kuyruğuna ise kernel, tamamlanan işlerin sonuçlarını geri yazar
  • Varsayılan yapılandırmada kernel'in gönderim kuyruğunu kontrol etmesi için io_uring_enter() çağrılmalıdır
    • Tek çağrıyla birden fazla iş gönderilebilir ve birden fazla tamamlanma alınabilir
    • epoll ile read() birleşimindeki gibi her iş için syscall çifti tekrar eden bir yapı değildir
  • IORING_SETUP_SQPOLL kullanılırsa kernel thread'i gönderim kuyruğunu polling ile izler
    • Normal çalışma durumunda syscall'lar neredeyse tamamen ortadan kaldırılabilir
    • Kuyruk boş olsa bile kernel thread'i çalışmaya devam ettiği için CPU tüketir
    • sq_thread_idle sonrasında sleep durumuna geçse de maliyet tamamen kaybolmaz

Kod örnekleriyle fark

  • epoll örneği

    • stdin dosya tanıtıcısı kaydedilir ve olay geldiğinde ayrıca read() çağrılır
    • epoll_create1 ile bir epoll instance'ı oluşturulur
    • epoll_ctl ile STDIN_FILENO kaydedilir
    • epoll_wait ile okunabilir hale gelene kadar bloklanır
    • Olay geldiğinde veriler read() syscall'ı ile okunur
    • Bu akışta gerçek I/O olaylarının her biri için epoll_wait ve read gerekir
  • io_uring örneği

    • liburing kullanılır
    • io_uring_queue_init ile halka başlatılır
    • io_uring_get_sqe ile gönderim kuyruğu girdisi alınır
    • io_uring_prep_read ile stdin okuma işi hazırlanır
    • io_uring_submit ile gönderilir ve io_uring_wait_cqe ile tamamlanma beklenir
    • io_uring örneğinde ayrıca bir hazır olma durumu kontrolü yoktur ve tamamlanma anında ayrıca read() çağrılmaz
    • Basitleştirmek için her iki örnekte de önemli istisna işleme adımları çıkarılmıştır
    • stdin'de veri yoksa sonsuza kadar bloklanabilir
    • io_uring örneği, gönderim kuyruğu dolduğunda io_uring_get_sqe()'nin NULL döndürmesi durumunu kontrol etmez

io_uring kullanırken ek koşullar

  • zero-copy I/O kullanmak için tamponların io_uring_register_buffers() ile önceden kaydedilmesi gerekir
    • Böylece kernel'in her işte belleği yeniden eşlemesi önlenebilir
    • Ağ iletiminde kernel 6.0+ içindeki IORING_OP_SEND_ZC, tamponun kernel'e kopyalanmadığı bir gönderim sağlar
  • IORING_SETUP_SQPOLL, syscall sayısını azaltabilir ama bunun bedeli CPU kullanımıdır
    • Kuyruk boş olsa bile kernel thread'i polling yapmayı sürdürür
    • Boşta kalma zaman aşımından sonra sleep durumuna geçilebilir, ancak maliyet tamamen ortadan kalkmaz
  • io_uring hataları, senkron syscall'lardaki doğrudan dönüş değeri olarak değil, tamamlanma kuyruğu girdisinin res alanı üzerinden asenkron olarak gelir
    • Hata işleme cqe->res üzerinden yapılmalıdır

Modern Linux sunucularında seçim

  • epoll, I/O'nun mümkün olduğu anın bildirilmesine ve ayrıca syscall çağrılmasına dayanan eski bir Linux asenkron I/O yöntemidir
  • io_uring, modern Linux'ta tamamlanma tabanlı model ile toplu gönderim ve tamamlanma işleme sunar
  • Modern Linux sunucularında sıfırdan yeni bir proje geliştirilecekse io_uring seçmek daha doğal görünür
  • Eski sistem desteği makul bir noktada sonlandırılabiliyorsa, kernel v5.1+ ortamında epoll'ü seçmek için çok fazla neden yoktur

1 yorum

 
GN⁺ 3 시간 전
Hacker News yorumları
  • GitHub deposu https://github.com/sibexico/TinyGate'a çok kısa bir göz attım; CPU sabitleme henüz kullanılmıyor gibi görünüyor
    Thread'leri ve dinleme soketlerini CPU'ya sabitleyip sockopt SO_INCOMING_CPU kullanırsanız performansı biraz daha artırabilirsiniz
    Giden soketleri de CPU'ya hizalamak oldukça büyük bir iyileştirme sağlayabilir ama bildiğim kadarıyla bunun için iyi bir API yok. Linux'ta uyumlu NIC'ler için traffic steering/flow steering API'leri var ve NIC'in kullandığı hash'i biliyorsanız—muhtemelen Toeplitz'dir—backend'e giden kaynak portlarını dikkatli seçerek hash'in tutmasını sağlayabilirsiniz
    Amaç, proxy'nin CPU'lar arası iletişim olmadan paketleri işlemesini sağlamak

    • Depodaki v0 ve v1, neredeyse sıfırdan yeniden yazılmış tamamen farklı implementasyonlar ve şu anda üçüncü implementasyon üzerinde çalışılıyor; muhtemelen sonuncusu bu olacak. Mimari tercihleri de tamamen değişti
    • O patch'in benchmark sonuçlarını görmek isterim
  • https://github.com/concurrencykit/ck ve https://github.com/microsoft/mimalloc iyi olabilir. Zero-copy ve belleğe hizalanmış bir reverse proxy için uygun olacaklardır
    DDoS koruması ve daha gelişmiş L4 özellikleri eklemek isterseniz https://docs.ebpf.io/ebpf-library/libxdp/libxdp/ de bakmaya değer

    • Plan, diğer katmanlarda optimizasyonları uyguladıktan sonra allocator tarafına geçmekti. Şu anda öğrencilerle allocator'ları inceliyorum ve blogdaki önceki yazı Zig diliyle yazılmış özel bir allocator hakkındaydı
  • Gerçekten çok iyi bir yazı
    Bu yazı yüzünden uring, kernel geliştirme ve C dünyasında derin bir tavşan deliğine düştüm. Uzun süredir Rust ve C++ geliştiriyorum ama küçük ve orta ölçekli C programlarında hem sadelik hem de sanatsal bir hava var

  • io_uring tabanlı web sunucusunda henüz paylaşımlı buffer'ları test etmedim. Çünkü dosyadan okuyup yazmak yerine doğrudan mmap'lenmiş alandan gönderiyorum
    Aslında io_uring ile sendfile kullanmak isterdim ama henüz desteklenmiyor
    Rust ve kTLS gibi moda sözcüklerle dolu bir yazı: https://blog.habets.se/2025/04/io-uring-ktls-and-rust-for-ze...
    HN'de de paylaşılmıştı: https://news.ycombinator.com/item?id=44980865

    • Bilginize, splice(2) implemente edildiği için uring ile sendfile benzeri bir yöntem kullanılabiliyor. sendfile kadar rahat değil ama neredeyse aynı şekilde çalışacaktır
  • DPDK ile yapılırsa çok daha karmaşık olur ama performans açısından nginx'i geçme şansı doğar
    FPGA üzerinde çalıştırırsanız daha da karmaşıklaşır
    Çıkarılacak ders şu: performans için bazen soyutlamaları kızgın bıçakla tereyağını keser gibi delip geçmek gerekiyor, ama bunun bedeli her şeyin zorlaşması. Soketler ve bağlantı başına thread yaklaşımı, ağın CPU'ya kıyasla çok yavaş olduğu dönemlerde iyi bir yaklaşımdı; bugün de çoğu durumda hâlâ en basit yaklaşım olabiliyor

  • Ben de bunu hep merak ettiğim için temel farkları öğrenmek amacıyla yakın zamanda birkaç HTTP dosya sunucusu implementasyonu yazdım
    https://theconsensus.dev/p/2026/05/18/serving-files-three-wa...

  • Proxy bağlamında epoll_wait busy polling de anılmalı. Yakın zamanda düşük gecikme seçeneklerine bakarken bunu inceledim; DPDK/VMA/io_uring olmadan da yalnızca basit soketlerle kullanıcı alanı busy polling'e yakın bir şeyin mümkün göründüğünü fark ettim ve Fastly buna katkı sağlayıp kullanıyor
    O kadar düşük seviyeli ki tamamını anladığımı söyleyemem; yalnızca kavramı anladım, o yüzden bağlantıları bırakıyorum. NAPI başına epoll context'inde çalışıyor ve NAPI ID'sini kolayca kontrol etmek mümkün değil ama makinenin tamamını proxy'ye ayırırsanız NAPI ID'ye göre soketleri özel poller'lara atayan basit bir hile yapılabilir
    Benim kullanım durumum proxy değildi; tek bir makinede N adet soketi poll edip gelen veriyi işlemekti. O durumda pek uygulanabilir görünmedi ama belki tek thread içinde NAPI context'lerini round-robin poll ederek mümkün olabilir. Bir gün kernel'e “bana güven, bu tek soketi sonunda ben poll edeceğim, bu yüzden asla IRQ yolunu kullanma” demenin kolay bir yolu olmasını isterdim
    Bu kernel özelliğiyle ilgili önceki HN tartışması: https://news.ycombinator.com/item?id=43749271
    Fastly katkıcısının büyük resmi anlamayı kolaylaştıran diyagramlar içeren iyi sunum slaytları: https://netdevconf.info/0x18/docs/netdev-0x18-paper10-talk-s...
    LWN yazıları: https://lwn.net/Articles/1008399/, https://lwn.net/Articles/997491/, https://lwn.net/Articles/959462/
    Kernel belgeleri: https://docs.kernel.org/networking/napi.html#irq-mitigation

  • C++ ve asenkron ağ iletişimini seviyorsanız Boost.Asio var

    • Kısa süre önce Asio’yu kendi yazdığım epoll olay döngüsüyle değiştirdiğimde RPS yaklaşık %16 arttı. Sonuçlar orta ölçekli bir SQL sunucusundan çıktı; bu yüzden iyi paketlenmiş kütüphaneler kullanırken dikkatli olmak gerekiyor
    • Veritabanı sunucusunda Asio’nun epoll arka ucunu io_uring ile değiştirdiğimde CPU kullanımı belirgin şekilde arttı. Bunun kullanım biçimine ve olay koduna nasıl entegre edildiğine göre çok değişmesi muhtemel
    • Boost fazla kullanışsız. Derlemesi ve kullanması zor, devasa dinamik kütüphanelerden oluşuyor. Zaten CMake kullanıyor olmama rağmen, Boost’u kurup keşfedilebilir hale getirme süreci aşırı can sıkıcıydı. Yine de bunu Mac’te yaşadım
  • 2050 civarında Linux’ta soketleri poll etmenin 20 kadar yolu olacak gibi görünüyor

    • Evet, io_uring içinde bile durum böyle. Daha hızlı olmak için io_uring tek atımlı modu çıktı, ardından çoklu atım modu da geldi
  • Evet, io_uring kesin olarak epoll’dan daha hızlı. Benim durumumda io_uring saniye başına istek sayısında yaklaşık %20 daha hızlıydı
    Sorun şu ki çekirdekte açıkça etkinleştirilmesi gerekiyor ve güvenlik gerekçeleriyle neredeyse her yerde devre dışı bırakılmış durumda. Çekirdek ile kullanıcı alanı arasında doğrudan bellek paylaşımı var gibi ve bu oldukça tedirgin edici. Son dönemde io_uringi hedef alan birkaç exploit de oldu
    Bu yüzden Go gibi mümkün olan en yüksek performansı hedefleyen mühendislik projeleri bile io_uringi makul bir varsayılan olarak derinlemesine yerleştirmiyor. Riski almak istiyorsanız sevdiğiniz dilde doğrudan kullanabilirsiniz. Daha hızlı ama bedeli potansiyel exploit olasılığı

    • Devre dışı bırakılmasının ana sebebi artık çözüldü. En yeni RC’de cBPF desteği var; böylece her şeyi kapatmak yerine çalıştırılabilecek işleri sınırlamak mümkün
    • Duruma göre değişir. epoll yerine poll ile yaptığım POSIX tarzı io_uring emülasyonu bazen io_uringden daha hızlıydı. Ama büyük sıfır kopya tamponlar söz konusu olduğunda en iyisi io_uring
      io_uring, asenkron I/O olmasa bile faydalı. Örneğin mkdir sonrasında o dizini açmak gibi işlem zincirleri tek bir atomik iş gibi uygulanabiliyor
      Ağ iletişiminde saniye başına paket sayısını en üst düzeye çıkarmaya çalışırsanız çekirdek sınırlarına[1] çok hızlı çarparsınız ve sonunda GSO/GRO gibi özelliklerden yararlanmanız ya da ağ yığınını tamamen baypas etmeniz gerekir
      1: https://github.com/axboe/liburing/discussions/1346
    • RHEL 9 ve 10 artık varsayılan olarak io_uringi tamamen destekliyor. Bu çok yeni bir gelişme ama böylece çok sayıda kurumsal Linux kurulumu kapsama giriyor. Gemini, Ubuntu ve SuSE’nin de desteklediğini “söyledi”, ancak bunu kanıtlayan bir bağlantı vermedi
      https://access.redhat.com/solutions/4723221
      Go da desteği yeniden değerlendirmeli. Denemeye değer
    • Go gibi projelerde çalışma zamanı başlarken bir kez io_uring özellik algılama yapmak da bir seçenek olamaz mı? Exploitler yalnızca io_uring kullanmayı seçen programların sorunu değil, tüm işletim sisteminin sorunu değil mi?
    • Her tür poll modlu ağ iletişimi—RDMA, DPDK, io_uring—sonuçta bellek yalıtımının sorumluluğunu kullanıcıya bırakma eğiliminde
      Ancak io_uring durumunda halka çekirdeğin içinde olduğundan kullanıcının yapabileceği çok şey yok
      LLM sayesinde ileride bunun iyileşmesini umuyorum, ama çözmesi zor bir problem. Çekirdeğin kendi içinde ele alması da çok zor ve insanlar da bunu nasıl ayarlayacaklarını çoğu zaman gerçekten anlamıyor