Linux'ta epoll ve io_uring karşılaştırması
(sibexi.co)- 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_waitsonrasındaread()/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_SQPOLLise 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()veyawrite()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_waitveread()/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_SQPOLLkullanı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_idlesonrasında sleep durumuna geçse de maliyet tamamen kaybolmaz
Kod örnekleriyle fark
-
epoll örneği
stdindosya tanıtıcısı kaydedilir ve olay geldiğinde ayrıcaread()çağrılırepoll_create1ile bir epoll instance'ı oluşturulurepoll_ctlileSTDIN_FILENOkaydedilirepoll_waitile 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_waitvereadgerekir
-
io_uring örneği
liburingkullanılırio_uring_queue_initile halka başlatılırio_uring_get_sqeile gönderim kuyruğu girdisi alınırio_uring_prep_readilestdinokuma işi hazırlanırio_uring_submitile gönderilir veio_uring_wait_cqeile 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()'ninNULLdö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
resalanı üzerinden asenkron olarak gelir- Hata işleme
cqe->resüzerinden yapılmalıdır
- Hata işleme
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
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_CPUkullanırsanız performansı biraz daha artırabilirsinizGiden 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
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
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 vario_uringtabanlı web sunucusunda henüz paylaşımlı buffer'ları test etmedim. Çünkü dosyadan okuyup yazmak yerine doğrudanmmap'lenmiş alandan gönderiyorumAslında
io_uringilesendfilekullanmak isterdim ama henüz desteklenmiyorRust 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
splice(2)implemente edildiği içinuringile sendfile benzeri bir yöntem kullanılabiliyor.sendfilekadar rahat değil ama neredeyse aynı şekilde çalışacaktırDPDK 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_waitbusy 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ıyorO 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
epollcontext'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ılabilirBenim 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
epollolay 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 gerekiyorepollarka ucunuio_uringile 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 muhtemel2050 civarında Linux’ta soketleri poll etmenin 20 kadar yolu olacak gibi görünüyor
io_uringiçinde bile durum böyle. Daha hızlı olmak içinio_uringtek atımlı modu çıktı, ardından çoklu atım modu da geldiEvet,
io_uringkesin olarakepoll’dan daha hızlı. Benim durumumdaio_uringsaniye 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 olduBu 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ığıepollyerinepollile yaptığım POSIX tarzıio_uringemülasyonu bazenio_uringden daha hızlıydı. Ama büyük sıfır kopya tamponlar söz konusu olduğunda en iyisiio_uringio_uring, asenkron I/O olmasa bile faydalı. Örneğinmkdirsonrasında o dizini açmak gibi işlem zincirleri tek bir atomik iş gibi uygulanabiliyorAğ 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
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ı vermedihttps://access.redhat.com/solutions/4723221
Go da desteği yeniden değerlendirmeli. Denemeye değer
io_uringözellik algılama yapmak da bir seçenek olamaz mı? Exploitler yalnızcaio_uringkullanmayı seçen programların sorunu değil, tüm işletim sisteminin sorunu değil mi?io_uring—sonuçta bellek yalıtımının sorumluluğunu kullanıcıya bırakma eğilimindeAncak
io_uringdurumunda halka çekirdeğin içinde olduğundan kullanıcının yapabileceği çok şey yokLLM 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