2 puan yazan GN⁺ 2025-08-23 | 1 yorum | WhatsApp'ta paylaş
  • 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_files kullanılarak yalnızca io_uring içinde geçerli olan ayrı bir “tamsayı” dosya numarası kullanılır ve bu değer /proc/pid/fd altında görünmez
  • Sistemin ulimit sı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. setsockopt desteğ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

 
GN⁺ 2025-08-23
Hacker News görüşleri
  • io_uring kullanarak yazma işlemleri gönderirken, bellek konumunun serbest bırakılmaması veya üzerine yazılmaması gerekiyor, ancak io-uring crate API'sinde Rust'ın borrow checker'ı bu konuda yardımcı olmuyor ve çalışma zamanı kontrolü de yok gibi görünüyor
    Bu durum hakkında yazılmış yazıları ve yorumları gördüm; sonuç olarak io_uring etrafında güvenli bir Rust asenkron kütüphanesi yapmak gerçekten zor izlenimi veriyor
    Tokio 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, epoll standartken tasarlandı ve IOCP neredeyse 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 uyuyor
      Ama 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_blocking yöntemiyle uygulanıyor ve io_uring ile aynı tampon sorunlarını yaşıyor; bu yüzden io_uring'e taşımak çok zor değil
      Ancak tokio::net'in mevcut API'si, io_uring tabanlı tampon API'siyle uyumlu değil; readiness kontrolü yapılabilse de tam destek zor

    • Güvenli bir io_uring arayü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üyor

    • Her şeyi borrow'larla ifade etmek zorunda değilsiniz
      Slab benzeri veri yapıları kullanırsanız bunu cancel-safe hale getirebilirsiniz
      Referans: 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-bin ile tanışmıştım; bunun her istek için yeni bir süreç başlatan yapı olduğunu ancak şimdi fark ediyorum
    sendfile, 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şıyorum
      90'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 olarak io_uring'i devre dışı bırakıyor
    Bu durum düzelmezse, io_uring bir 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ı
    kTLS ya da io_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/Sync bound'ları, async trait object'ler gibi pek çok kavramı anlamak gerekiyor
    C++20: coroutines
    Go: goroutines
    Java21+: virtual threads

    • C++ coroutine'leri, Pin'in çözdüğü problemden kaçınmak için heap allocation kullanıyor
      Bu, 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/Sync ile 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_uring tabanlı 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

  • kTLS kesinlikle bir ilerleme
    Ben 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_uring son birkaç yılda gerçekten etkileyici bir hızla gelişti

  • Bu 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ız Aya'yı tavsiye ederim
    Aya projesi Github

  • kTLS'in mevcut durumunu merak ediyorum
    Kı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 kTLS var ve sysctl (kern.ipc.tls.enable=1) ile çalışma zamanında açılıp kapatılabiliyor
      FreeBSD-15'te varsayılanın etkin olması planlanıyor ve Netflix yaklaşık 10 yıldır trafik şifrelemesinde kTLS kullanıyor

    • kTLS genel 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 nproc thread ile sınırlamanın riskli olabileceğini unutmamak gerekir

    • io_uring sö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

  • DPDK gibi çekirdeği tamamen baypas eden bir yaklaşımı da görmek isterdim