io_uring, kTLS ve Rust ile sıfır sistem çağrılı HTTPS sunucusu
(blog.habets.se)- Yüksek performanslı web sunucuları oluşturmak için geçmişte select(), poll(), epoll gibi çeşitli olay tabanlı modeller kullanıldı
- Ancak bu sistem çağrılarının performans sınırları nedeniyle io_uring ortaya çıktı ve isteklerin kuyruğa eklenip çekirdek tarafından asenkron işlenmesi yaklaşımını getirdi
- kTLS ile TLS şifreleme işlemlerini çekirdek üstleniyor; bu da sendfile() kullanımı ve donanım offload gibi ek optimizasyonları mümkün kılıyor
- Descriptorless files yaklaşımı, dosya tanıtıcılarını doğrudan iletmeden io_uring için optimize edilmiş bir erişim yöntemi sağlıyor
- Rust, io_uring ve kTLS'yi birleştiren tarweb açık kaynak projesi, istek başına ek sistem çağrısı olmadan HTTPS sunmayı hedefliyor; ayrıca güvenlik ve bellek yönetimi konuları da ele alınıyor
Yüksek performanslı web sunucusu mimarisinin evrimi
- 2000'lerin başından itibaren yüksek kapasiteli web sunucularına olan ihtiyaç arttı
- İlk dönemde her istek için yeni bir süreç oluşturma yaklaşımı yaygındı; ancak bunun yüksek maliyeti nedeniyle preforking tekniği ortaya çıktı
- Sonrasında thread kullanımının ve select(), poll() mekanizmalarının devreye girmesiyle, bağlam değiştirme maliyetini azaltan bir yapıya doğru evrildi
- Ancak select() ve poll() yöntemleri de bağlantı sayısı arttıkça çekirdeğe büyük dizilerin sık sık iletilmesini gerektirdiği için ölçeklenebilirlik sınırlarına sahipti
epoll'un ortaya çıkışı
- Linux ortamında epoll kullanıma girdi ve önceki yöntemlere göre çoklu bağlantıları daha verimli işlemek mümkün hale geldi
- epoll yalnızca değişiklikleri (delta) işleyerek gereksiz kaynak tüketimini azaltır
- Tüm sistem çağrıları tamamen ortadan kalkmasa da, maliyet önemli ölçüde düşer
io_uring'e genel bakış
- io_uring, her istek için sistem çağrısı yapmak yerine, çekirdeğin asenkron işleyebilmesi için isteklerin bellekteki bir kuyruğa eklenmesini sağlar
- Örneğin accept() kuyruğa konulduğunda, çekirdek işlemi tamamladıktan sonra sonucu tamamlanma kuyruğuna döndürür
- Web sunucusu isteği kuyruğa ekler, sonuçları ise ayrı bir bellek alanından kontrol eder
- Yoğun döngüden (busy loop) kaçınmak için, kuyrukta değişiklik yoksa hem web sunucusu hem de çekirdek yalnızca gerektiğinde sistem çağrısı yaparak enerji tasarrufu sağlar
- Uygun kütüphaneler kullanıldığında, aktif bir sunucu istekleri işlerken ek sistem çağrısı olmadan çalışabilir
Çok çekirdekli ve NUMA ortamları
- Modern CPU'ların çok çekirdekli yapısı göz önüne alındığında, çekirdek başına tek thread çalıştırmak ve veri yapılarının paylaşımını en aza indirmek etkili bir stratejidir
- NUMA ortamında her thread'in yalnızca kendi yerel düğüm belleğine erişmesi optimizasyon sağlar
- İstek dağıtımında kusursuz denge ise ek araştırma gerektirir
Bellek tahsisi
- Hem çekirdekte hem de web sunucusunda bellek tahsisi devam eder; kullanıcı alanındaki tahsisler de sonunda sistem çağrılarına bağlanır
- Web sunucusu tarafında bağlantı başına sabit boyutlu bellek blokları önceden ayırarak parçalanma ve yetersizlik sorunları önlenebilir
- Çekirdek tarafında da bağlantı başına G/Ç tamponları gerekir ve bunlar soket seçenekleriyle kısmen ayarlanabilir
- Bellek yetersizliği oluştuğunda ciddi arızalara yol açabilir
kTLS (çekirdek TLS) tanıtımı
- kTLS, Linux çekirdeğinde şifreleme ve şifre çözme işlemlerini üstlenen bir özelliktir
- Handshake uygulama tarafından yapılır; sonrasında çekirdek veriyi düz metinmiş gibi aktarır
sendfile()kullanılabildiği için kullanıcı alanı ile çekirdek alanı arasındaki bellek kopyaları azaltılabilir- Ağ kartı destekliyorsa, şifreleme işlemleri donanıma da offload edilebilir
Descriptorless Files
- Kullanıcı alanından çekirdek alanına dosya tanıtıcısı doğrudan aktarılırken oluşan ek yükü azaltmak için ortaya çıkan bir yaklaşımdır
register_fileskullanılarak yalnızca io_uring içinde geçerli olan ayrı bir “tamsayı” dosya numarası kullanılır ve bu değer/proc/pid/fdaltında görünmez- Sistemin
ulimitsınırı yine de geçerlidir
tarweb projesine giriş
- tarweb, yukarıdaki tüm teknolojileri uygulayan örnek bir açık kaynak web sunucusu projesidir
- Tek bir tar dosyasının içeriğini sunan bir yapıya sahiptir ve Rust, io_uring, kTLS gibi modern yüksek performans teknolojilerini bir araya getirir
- Gerçek kullanım sırasında io_uring ile kTLS arasında uyumluluk sorunları (ör.
setsockoptdesteğinin olmaması) yaşanmış ve bazı problemler Pull Request ile çözülmüştür - Proje hâlâ tamamlanmış değildir; ayrıca Rust'ın rustls kütüphanesi handshake sürecinde bellek tahsisi yapabilir
- Temel nokta, her istek için ek sistem çağrısı olmadan HTTPS hizmeti sunmanın mümkün olmasıdır
Benchmark ve performans ölçümü
- Yazar henüz yeterli benchmark yapmadı; kod düzenlendikten sonra performans testleri planlanıyor
io_uring ve Rust'ta güvenlik sorunları
- Eşzamanlı sistem çağrılarından farklı olarak, io_uring'de tamamlanma olayı gelene kadar bellek tamponları serbest bırakılmamalıdır
- io-uring crate'i, Rust'ın derleme zamanındaki güvenliğini garanti etmez ve çalışma zamanındaki kontroller de yetersizdir
- Yanlış kullanım, C++'takine benzer şekilde ciddi sorunlara yol açabilir; bu da Rust'ın doğal güvenliğini zayıflatır
- Pinning ve borrow checker'ı aktif kullanan ayrı bir safer-ring crate'ine ihtiyaç vardır
- Bu konu topluluk içinde zaten tartışılmaktadır
Referanslar ve ek bağlantılar
- Bu içerik, 2025-08-22 itibarıyla HackerNews'te tartışılan bir gönderiye dayanmaktadır
1 yorum
Hacker News görüşleri
io_uringkullanarak yazma işlemleri gönderirken, bellek konumunun serbest bırakılmaması veya üzerine yazılmaması gerekiyor, ancakio-uringcrate API'sinde Rust'ın borrow checker'ı bu konuda yardımcı olmuyor ve çalışma zamanı kontrolü de yok gibi görünüyorBu durum hakkında yazılmış yazıları ve yorumları gördüm; sonuç olarak
io_uringetrafında güvenli bir Rust asenkron kütüphanesi yapmak gerçekten zor izlenimi veriyorTokio ekibinden Alice'in de yakın zamanda bu sorunu aşmaya yönelik ilginin çok yüksek olmadığını söylediğini hatırlıyorum
Bunun sebebi şu an performansın "yeterince iyi" olması
Referans: https://boats.gitlab.io/blog/post/io-uring/
Rust async hakkında pek çok hayal kırıklığım var; bunlardan biri de bu
Rust async,
epollstandartken tasarlandı veIOCPneredeyse hiç dikkate alınmadıSenkron syscall'larda bu sorun yok, çünkü
readçağrısında tamponun değiştirilebilir referansını çekirdeğe veriyorsunuz ve bu, Rust'ın yerel sahiplik/borrow modeliyle iyi uyuyorAma completion tabanlı I/O'nun sahiplik modeline gerçekten uyması için, iş tamamlanana kadar kullanıcı kodunun çalışmaya devam etmediğinin garanti edilmesi gerekir; bunu state machine polling yapısıyla yapamazsınız
Burada thread modeli ya da green thread yapısı tam oturuyor
Rust bir "async'e özel hedef" ekleseydi daha iyi olabilirdi
Rust geliştiricileri stackless polling tabanlı asenkron modele çok umut bağladı; şimdi bunun nereye vardığını izliyoruz
Rust'ın borrow checker'ının düzgün destekleyemediği bir sahiplik modeli olduğunu düşünüyorum
Buna geçici olarak "hot potato ownership" diyorum; tamponu kısa süreliğine verip sonra geri alma yapısı
Rust'ta böyle bir deseni güvenli biçimde kodlamak çok zor ve kodu da epey dağınık hale getiriyor
Tokio ekibinden Alice'in söylediğinin aksine, dosya I/O tarafında ilgi var
Dosya I/O zaten
spawn_blockingyöntemiyle uygulanıyor veio_uringile aynı tampon sorunlarını yaşıyor; bu yüzdenio_uring'e taşımak çok zor değilAncak
tokio::net'in mevcut API'si,io_uringtabanlı tampon API'siyle uyumlu değil; readiness kontrolü yapılabilse de tam destek zorGüvenli bir
io_uringarayüzü yapmak için, ring'in sahip olduğu tamponları alıp kullanmak ve yazmayı başlatırken bunları geri vermek en uygun yöntem gibi görünüyorHer şeyi borrow'larla ifade etmek zorunda değilsiniz
Slabbenzeri veri yapıları kullanırsanız bunu cancel-safe hale getirebilirsinizReferans: https://github.com/steelcake/io2
Bu yazıyı okumak gerçekten çok keyifliydi
Performans testlerini merak ediyorum ama yazarın benchmark'lardan önce kodu temizleyip düzenlemek istemesi özellikle etkileyiciydi
Her şeyin benchmark odaklı olduğu bu dönemde birinin böyle düşünmesi ferahlatıcı
Yaklaşık 11 yaşımdayken veritabanı kurmaya çalışırken
cgi-binile tanışmıştım; bunun her istek için yeni bir süreç başlatan yapı olduğunu ancak şimdi fark ediyorumsendfile, büyük oyun forumlarında demo indirmelerini eşzamanlı işlerken oyunun kurallarını değiştirmişti; Netflix'in 40ms düşüş örneği ya da GTA 5'in yükleme süresini %70 kısaltan örnek gibi sonuçları görünce, daha etkileyici mühendisliğin perde arkasında saklı olduğunu hissediyorumİlgili bağlantılar: Common Gateway Interface, Netflix 40ms örneği, GTA Online yükleme kısaltması
Sadece CGI değil, eski CERN ve Apache türevi HTTP oturumları da tüm sunucuyu fork'layarak çalışıyordu
Zamanla durum iyileşti, ancak Apache'nin yapılandırma tarzı nedeniyle, baştan itibaren olay tabanlı I/O ile tasarlanmış hafif sunucuların, örneğin nginx'in, büyük popülerlik kazanmasına yol açtı
sendfile'ın verimliliğine şüpheyle yaklaşıyorum90'ların sonunda modaydı ama pratikte performans kazancının sınırlı olduğunu düşünüyorum
Çoğu bulut iş yükü orkestratörü (
CloudRun,GKE,EKS, yerel Docker vb.) varsayılan olarakio_uring'i devre dışı bırakıyorBu durum düzelmezse,
io_uringbir süre daha çok sınırlı bir teknoloji olarak kalacak gibi görünüyorİnsan merak ediyor: neden
io_uring'i devre dışı bırakıyorlar?Böyleyse yeniden self-hosting'e dönmek gerekir
Gerçekten çok keyifle okudum
Benchmark'ları bekleyeceğim, o yüzden acele etmene gerek yok; yazarın benchmark'lardan önce kod düzenini önemsemesi beni gerçekten etkiledi
Bu aralar benchmark puanlarına kafayı takan çok proje var; bu düşünce tarzı gerçekten ferahlatıcı ve saygı uyandırıcı
kTLSya daio_uring'in bu kadar farklı biçimlerde kullanılabildiğini bilmiyordumŞu anda asenkron işleme dünyasının durumu kabaca şöyle
Rust:
Futures,Pin,Waker, async runtime,Send/Syncbound'ları, async trait object'ler gibi pek çok kavramı anlamak gerekiyorC++20: coroutines
Go: goroutines
Java21+: virtual threads
C++ coroutine'leri,
Pin'in çözdüğü problemden kaçınmak için heap allocation kullanıyorBu, C++'ın savunduğu "zero-overhead" ilkesinden ciddi bir sapma
Rust'ın gelecekte de async trait'leri yerleştirmesinin uzun sürmesinin nedeni de futures'ı heap üzerinde ayırmaması
Performans/taşınabilirlik ile karmaşıklık arasındaki ödünleşimin değeri, her projede farklı olabilir
Send/Syncile ilgili kısıtlar diğer dillerde de hâlâ anlamlı; bu kısıtlar olmazsa ince hatalı kod yazmak çok daha kolay olur"Yeterince iyi" seviyede Rust kodu yazıyorsanız ve başkalarının hazırladığı orta seviye primitive'leri kullanıyorsanız, bu kavramların hepsini bilmeniz şart değil
Rust, bu kavramları anlamadıysanız kodun derlenmesine bile izin vermiyor
Go'da goroutine asenkronlukla aynı şey değil ve kanalları anlamadan goroutine'leri de gerçekten anlayamazsınız
Go'nun kanal uygulaması kendine özgü olduğu için sınır durumlarındaki davranışı sezgisel olarak öngörmek zor olabiliyor
Go'da derinlemesine anlamadan da kod yazılabildiği için bunun artıları ve eksileri var
"Ucuz thread" asenkronlukla aynı şey değil
tarweb(blogdaki sunucu),io_uringtabanlı olay döngüsüne sahip tek thread'li bir yapı; fikir, CPU çekirdeği başına bir thread koymak"Büyük ölçekli eşzamanlılığın bugünkü durumu" demektense "ucuz thread'lerin bugünkü durumu" demek daha doğru olabilir
Ucuz thread ile async loop arasındaki en büyük fark, muhakeme etmesinin daha kolay olması
Bunun da bir bedeli var; her thread hafif olsa da bir stack boyutuna ihtiyaç duyar
kTLSkesinlikle bir ilerlemeBen de birkaç yıl önce gerçekten istek başına syscall sayısı 0 olan bir sunucu yapıp bununla ilgili bir blog yazısı yazmıştım (https://wjwh.eu/posts/2021-10-01-no-syscall-server-iouring.html)
Ama bunun dezavantajı, sürekli busy-loop yapmak zorunda olmanız
io_uringson birkaç yılda gerçekten etkileyici bir hızla geliştiBu proje gerçekten harika ve uzun süredir benzer bir şey tasarlıyordum; birinin bunu hayata geçirmiş olmasına sevindim
BPF'yi Rust ile yazacaksanızAya'yı tavsiye ederimAya projesi Github
kTLS'in mevcut durumunu merak ediyorumKısa süre önce bir Cilium geliştiricisine sordum; Thomas Graf umutlu olduğunu söyledi ama pratikte birçok Linux dağıtımında çekirdek desteği yetersiz olduğu için varsayılan olarak etkinleştirilmesi hâlâ uzak görünüyor
Üzücü ama etkinleştirmenin ne kadar zor olduğunu da merak ediyorum
Özel çekirdek derlemek mi gerekiyor, yoksa çalışma zamanında doğrudan açılabiliyor mu?
FreeBSD'de 13. sürümden beri çekirdek/OpenSSL içinde
kTLSvar vesysctl(kern.ipc.tls.enable=1) ile çalışma zamanında açılıp kapatılabiliyorFreeBSD-15'te varsayılanın etkin olması planlanıyor ve Netflix yaklaşık 10 yıldır trafik şifrelemesinde
kTLSkullanıyorkTLSgenel olarak kötü bir fikir gibi geliyorÇekirdek başına bir thread yapısının zaman dilimli sistemlerde doğru olup olmadığından emin değilim
Benim deneyimimde "oversubscribing" yaklaşımı (çekirdek sayısından fazla thread kullanmak), gerçek wall-clock süre açısından fayda sağlıyor
Preemptive scheduling yoksa ya da çekirdek başına bir thread modeli varsa daha mantıklı olabilir
Tabii o zaman Unix'ten bahsetmiyoruzdur
Düşük gecikme ve yüksek throughput istiyorsanız, çekirdekleri izole edip thread'leri sabitlemek etkili olabilir
Bu yaklaşım Linux'ta iyi çalışıyor ve trading sistemleri gibi alanlarda verimsizlik pahasına da olsa sık kullanılıyor
Çekirdekler çoğu zaman boşta spin atıyor ve gerçekte iş yapmıyor, ama gecikme ve throughput açısından en iyi sonucu veriyor
Thread-per-core yapısının tuzağı, "rahat kısmını alıp kullanalım" sanmak
Aslında ya tamamen benimsersiniz ya da hiç kullanmazsınız
Yarım yamalak uygulamalar hiç verimli olmaz
Ama doğru tasarlanırsa neredeyse her durumda verimlidir
TPC tasarım bilgisini (çekirdekler arası yük dengeleme gibi) gerçekten bilen geliştirici azdır
Thread-per-core, yalnızca "CPU-bound" olduğunuzda verimli değildir
Bu sunucu projesindeki gibi işlerin çoğu asenkron ve olay tabanlı olduğunda, sunucu neredeyse I/O veya syscall beklemeden bir sonraki isteğe geçer; teoride bu yüzden çekirdek başına bir thread tam doğru yapı olur
Ama gerçek dünyada bu kadar ideal durum nadirdir; o yüzden kendinizi koşulsuz biçimde
nprocthread ile sınırlamanın riskli olabileceğini unutmamak gerekirio_uringsöz konusu olduğunda, çekirdek başına bir kullanıcı thread'i bulundurmak çok da kötü bir tercih sayılmazÇünkü çekirdek tarafında bir thread havuzu gibi çalışıyor
DPDKgibi çekirdeği tamamen baypas eden bir yaklaşımı da görmek isterdimLUNAbunu zaten yapıyorMakale bağlantısı: https://www.usenix.org/system/files/atc23-zhu-lingjun.pdf