2 puan yazan GN⁺ 2024-05-10 | 1 yorum | WhatsApp'ta paylaş
  • Dağıtık sistemlerdeki gecikme sorunları, yalnızca TCP_NODELAY etkinleştirilerek tekrar tekrar çözülebiliyor; bu da TCP’nin varsayılan davranışının modern iş yükleriyle örtüşmeyebileceğini gösteriyor
  • Nagle algoritması, küçük TCP paketlerinin başlık maliyetini azaltmak için 1984 tarihli RFC896’da tanımlandı ve ACK alınmadan yeni segment gönderimini engelliyor
  • delayed ACK ile birlikte kullanıldığında bir taraf ACK beklerken diğer taraf yanıt verisini ya da zamanlayıcıyı bekliyor; bu da gecikmeye duyarlı boru hattı uygulamaları için elverişsiz
  • Veri merkezi içi RTT yaklaşık 500μs olsa bile modern sunucular bu sürede çok iş yapabildiğinden, iletimi bir RTT kadar geciktirmenin faydası net değil
  • Modern dağıtık sistemlerde TLS, encoding, serialization ve uygulama mesaj boyutları nedeniyle tek baytlık paket sorunu azaldı; bu yüzden gecikmeye duyarlı ortamlarda Nagle algoritmasını devre dışı bırakmak daha doğal

Gecikme ayıklarken ilk bakılan ayar

  • Dağıtık sistemlerde bir gecikme sorunu çıktığında ilk olarak TCP_NODELAY etkin mi diye bakılır
  • Pek çok dağıtık sistem geliştiricisi, bu tek soket seçeneğiyle çözülen sorunlar yüzünden zaman kaybetti
  • Bu tekrar, TCP’nin varsayılan davranışının günümüz dağıtık sistemlerine uymadığını ya da Nagle algoritmasının artık eskidiğini düşündürüyor

Nagle algoritmasının çözmeye çalıştığı sorun

  • RFC896, 1984’te küçük paket sorununu ele alan bir belgedir
  • O dönemde klavye girdisi gibi veriler TCP üzerinden karakter karakter gönderildiğinde, 1 bayt veri için 40 bayt başlık eklenmesi gibi bir verimsizlik oluşuyordu
    • 1 bayt faydalı veri başına 40 bayt başlık gelerek %4000 ek yük oluşuyordu
    • Hafif yükte tolere edilebilse de ağ verimi açısından elverişsizdi
  • Nagle algoritmasının amacı, TCP başlık maliyetini daha iyi amorti ederek throughput’u artırmaktı
    • Küçük paketler çoğunlukla shell gibi insan etkileşimli uygulamalarda ve veriyi birden çok write çağrısıyla çekirdeğe parça parça veren uygulamalarda ortaya çıkıyordu
  • Temel davranış şudur: Önceden gönderilen veri henüz ACK almamışsa, yeni gönderim verisi ayrı bir TCP segmenti olarak hemen yollanmaz
  • Nagle algoritması çoğu zaman bir zamanlayıcıyla açıklansa da RFC896’nın kendisi, ağ gidiş-dönüş süresi (RTT) dışında ayrı bir zamanlayıcı kullanmaz

delayed ACK ile birleştiğinde oluşan gecikme

  • delayed ACK, paket alındısını hemen göndermek yerine, geri gönderilecek veri oluşana ya da zamanlayıcı dolana kadar bekleme yaklaşımıdır
  • RFC813, 1982’de ACK geciktirmeyi öneren erken bir belgedir ve belirli durumlarda alıcının ACK gönderimini erteleyip daha sonra göndermek üzere zamanlayıcı kurabileceğini anlatır
  • RFC1122, delayed ACK’yi daha resmî hale getirir
  • Bu iki özellik ayrı ayrı mantıklı olsa da birlikte kullanıldıklarında gecikme yaratabilirler
    • Nagle algoritması, daha fazla veri göndermeden önce ACK alınmasını bekler
    • delayed ACK ise yanıt verisi hazır olana ya da zamanlayıcı dolana kadar ACK göndermeyi erteler
    • Paketleri daha dolu göndermeye yardımcı olsa da gecikmeye duyarlı boru hattı uygulamaları için iyi değildir
  • John Nagle’ın Hacker News yorumunda da sorun, tinygram önlemesinden çok ACK gecikmesi ile sabit zamanlayıcının birleşimi olarak görülüyor
  • Bu, tek tek makul iki protokol özelliğinin birleşip istenmeyen davranış üretmesine bir örnek; bu tür etkileşimler de protokol tasarımını zorlaştırır

Modern dağıtık sistemlerle uyuşmayan noktalar

  • delayed ACK olmasa bile Nagle algoritmasının davranışı, modern dağıtık sistemlerin istediği çalışma biçimiyle örtüşmeyebilir
  • Günümüz ortamında RTT’nin kendisi göz ardı edilemeyecek bir maliyettir
    • Veri merkezi içi tek RTT genelde yaklaşık 500μs
    • Aynı bölgedeki veri merkezleri arası RTT birkaç ms
    • Küresel rotalarda bu süre yüzlerce ms’ye çıkabilir
  • Modern sunucular yüzlerce μs içinde çok sayıda iş yapabildiğinden, veriyi bir RTT kadar geciktirerek göndermenin açık bir kazanç sağladığını söylemek zor
  • Nagle algoritmasının ilk gerekçesi, tek baytlık paketlerde oluşan 40 kat başlık ek yükünü azaltmaktı
  • Modern dağıtık veritabanları ve dağıtık sistemler genel olarak tek baytlık paket göndermez
    • Uygulamanın gönderdiği veri zaten daha büyüktür
    • TLS gibi protokollerin ek yükü vardır
    • Encoding ve serialization ek yükleri de eklenir
  • Küçük mesajlardan kaçınma sorunu hâlâ önemlidir, ancak bunun sorumluluğu fiilen uygulama katmanına kaymıştır
  • JSON’a sarılmış veriyi tek tek baytlar halinde göndermek, Nagle algoritmasından bağımsız olarak da verimli değildir

TCP_NODELAY’in varsayılan tercih olarak görülmesinin nedeni

  • Gecikmeye duyarlı bir dağıtık sistemi modern veri merkezi sınıfı donanım üzerinde kuruyorsanız, TCP_NODELAY etkinleştirilerek Nagle algoritması devre dışı bırakılabilir
  • Modern sistem trafiği, uygulama yapısı ve donanım performansı düşünüldüğünde Nagle algoritmasına artık ihtiyaç olmayabilir
  • Hatta TCP_NODELAY’in varsayılan olması gerektiği de savunulabilir
  • Her bayt için write çağıran kod, varsayılan TCP_NODELAY altında daha yavaş çalışabilir
  • Verimlilik önemliyse, böyle bir kodun Nagle algoritmasına güvenmek yerine uygulama düzeyinde düzeltilmesi gerekir

TCP_QUICKACK daha çok yardımcı bir seçenek

  • TCP_QUICKACK bir alternatif olarak anılabilir, ancak taşınabilirliğinin düşük olması ve alışılmadık anlamı nedeniyle ilk tercih olmak zordur
  • Anlamını doğrudan Linux tcp man page üzerinden kontrol etmek gerekir
  • Daha büyük sorun ise TCP_QUICKACK’in, çekirdeğin veriyi programın niyetinden daha uzun süre tutması gibi temel problemi çözmemesidir
  • Program write() çağırdıysa, gerçekten write() işleminin gerçekleşmesi beklenir

1 yorum

 
GN⁺ 2024-05-10
Hacker News yorumları
  • Kariyerim boyunca Nagle algoritması yüzünden oluşan gecikme sorunlarını birkaç kez düzelttim; artık ilk şüphelendiğim şeylerden biri oldu.
    Mantığın kendisi geçerli, ama bazı iş yüklerine uymuyor; bence soket oluşturulurken mühendisin bunu açıkça seçmesi gerekir, işletim sisteminin varsayılanına bırakılmamalı.
    Sorun bunun iyi/kötü bir seçenek olup olmaması değil; veri gönderme biçimini epey agresif şekilde değiştiren bir ayarın var olması ve birçok kişinin bunun varlığından haberdar olmaması.

    • Ben de benzer durumdayım; yeni bir RPC framework'ü gördüğümde GitHub issue'su açıp “TCP_NODELAY düşündünüz mü, yoksa bu framework saniyede yalnızca 20 çağrı mı yapabiliyor?” diye sormak gibi bir hobim var.
      Şimdiye kadar her seferinde bir bug buldum.
      Örnek: https://cloud-haskell.atlassian.net/browse/DP-108 veya https://github.com/agentm/curryer/issues/3
      Ancak “iyi/kötü seçenek değildir” kısmına katılmıyorum.
      Bu, hatalı yazılmış uygulamaları “sihirli şekilde düzeltmek” için kernel tarafında bir sezgisel yöntem; makalenin dediği gibi, düzgün uygulamalar 1 baytlık ağ write() sistem çağrıları yapmaz.
      Böyle yazılımlar düzeltilmeli.
      Bu özelliğin anlamlı olduğu durum bence, kernel sistem yöneticisi olup ekip içi politika gibi nedenlerle makinede çalışan yazılımı düzeltemediğiniz nadir durumlardır.
      Onun dışında düzgün yazılımı daha karmaşık hale getirir.
      Hatalı yazılmış yazılımın throughput'unu biraz artırmak için eklenmiş tuhaf bir sihri açıkça kapatmanız gerektiği ve düzgün yazılmış yazılımda büyük, şaşırtıcı gecikmeler yarattığı anlamına gelir.
      John Nagle burada bağlantısı verilen thread'de delayed ACK'lerin daha kötü olduğunu söylüyor; buna katılıyorum.
      Ama Nagle algoritmasının kötüleştirdiği Send/Send/Receive deseni tamamen geçerli ve yaygın bir kullanım senaryosu; TCP üzerinde pipeline'lı RPC yapan her şey buna girer.
      Bence delayed ACK ve Nagle algoritması ikisi de varsayılan olarak kapalı olmalı.
      Adı da TCP_DELAY gibi bir şey olmalı ve yalnızca temel kullanıcı alanı tamponlamasını uygulamak istemediğinizde açılmalı.
      İnsanların böyle şeyleri bilmek zorunda kalmaması gerekir; varsayılan davranış şaşırtıcı olmayan yönde olmalı.
    • Amaç ağırlıklı olarak kötü write davranışı sergileyen uygulamaları düzeltmekse, TCP_DELAY'i açan bir seçenek epey tuhaf hale gelir.
      Bu, o seçeneği bilecek kadar akıllı ama write çağrılarını doğru bölmek ya da kendi uygulamasına uygun daha iyi bir Nagle tarzı tamponlama yapmak için yeterince akıllı olmayan bir yazılım mühendisine ihtiyaç var demektir.
    • Katılıyorum. Yüksek frekanslı/düşük gecikmeli trading tarafında Nagle algoritmasını kapatmak epey uzun zamandır, muhtemelen 15 yılı aşkın süredir iyi bilinen bir şey ve benim de ilk kontrol ettiğim şeylerden biri.
    • Aslında istenen şey gecikmeyi n mikrosaniye yapmak; ama sistem çağrısının önüne doğrudan kullanıcı alanı tamponlaması koymak dışında iyi bir yol yok.
      Sistem çağrısı maliyetini telafi eden io_uring gibi bir şey yoksa kullanıcı alanı tarafı daha iyi çalışır.
    • Bu mantık aslında Telnet oturumları gibi şeylere yönelikti.
      Hatırladığım kadarıyla asıl motivasyon buydu.
  • Sonuç biraz garip. Nagle algoritması açıkça yazmaları gruplama girişimiydi ve donanım, ağ, uygulama ya da kullanım senaryosundan bağımsız olarak bazı durumlarda yazmaları gruplamak daha iyidir.
    Bugün bile bilgi işlemin büyük kısmı gruplu yazma kullanıyor ve ağ uygulamaları da bundan fayda görüyor.
    QUIC gibi daha yeni üst seviye protokoller yazmaları grupluyor; TCP'nin bağımsız bağlantı ve hata işleme mantığını fiilen kullanıcı alanına taşıyarak protokolün veriyi uygulamaya mümkün olduğunca hızlı itmesini, tekil stream'lerin bağlantı ve hata işlemesini ise host TCP/IP stack'i ya da router yerine uygulamanın üstlenmesini sağlıyor.
    Ağlar eskiden olduğu gibi yeniden doygunluğa ulaşırsa Nagle algoritması QUIC'e uyarlanmış bir biçimde geri dönecek; muhtemelen uygulama kodunun daha derinlerinde, belirli eşiklere ulaşılana kadar QUIC paketlerinin gönderimini bekletme şeklinde olacak.
    Teknolojide her şey, donanım ya da yazılım bir darboğaza ulaştığında yeniden icat edilir. İkisinin performansı aynı hızda büyümediği için sonunda hep böyle olur.
    Bant genişliği dışında, küçük paketler yüzünden saniye başına paket sayısının doygunluğa ulaştığı durumlarda da Nagle algoritması yararlıdır.

    • QUIC ile TCP arasındaki fark, TCP'nin ve öncüllerinin asli günahında yatıyor: mesaj katmanının görünmediği asenkron seri port bağlantısını taklit etmeleri.
      Bu sayede fiziksel bir teletypewriter ile bir servise bağlanabiliyordunuz; ama TCP mesaj sınırlarını bilemez hale geldi ve bugün bu bilgiyi bir ölçüde içeri itebiliyor olsak da erken dönem yazılımlar bunu yapamıyordu.
      Buna karşılık QUIC, SCTP ve TP4 gibi birçok TCP dışı protokol mesaj sınırlarını açıkça sağlar.
      Sistemle arayüz, emüle edilmiş bir seri port değil; en fazla yeniden birleştirilen mesajlara dayalıdır.
    • Doğru, ama bu belirli uygulama gruplamanın nasıl yapılacağını belirleyen bir sezgisel yönteme dayanıyordu ve bu varsayımın tutmadığı görülüyor.
    • Gruplamayı protokol değil, uygulama kontrol etmeli.
      Protokol doğru şekilde gruplama yapacak kadar bağlama sahip değildir.
  • Tersine, gecikmeli ACK’i kapatmaya ne dersiniz?
    Sorun, küçük paketleri önleme ile gecikmeli ACK etkileşime girdiğinde ortaya çıkan patolojik davranış.
    Küçük paketleri önlemeyi kapatan açık seçenek TCP_NODELAY; peki gecikmeli ACK nasıl kapatılabilir?
    Dört kombinasyonun hepsini benchmark edip hangisinin en iyi uyduğunu görmek istediğinizde yani.
    Biraz bakınca Linux’ta TCP_QUICKACK soket seçeneği olduğunu gördüm, ama her alımda yeniden ayarlanması gerekiyor.
    /proc/sys/net/ipv4/tcp_delack_min ve /proc/sys/net/ipv4/tcp_ato_min de var.
    FreeBSD’de net.inet.tcp.delayed_ack ve net.inet.tcp.delacktime var.

    • TCP_QUICKACK en kötü biçimi düzeltir, ama sorunun tamamını çözmez.
      Nagle algoritması yine de veri göndermeden önce en fazla bir gidiş-dönüş süresi kadar bekleyebilir ve RFC’ye göre bakıldığında, neredeyse hiç kazanç sağlamadan yalnızca gecikme ekler.
    • Doğru. TCP_QUICKACK’in her alımda yeniden ayarlanması gerekiyormuş; ne düşünüyorlardı acaba?
      İnsan neden sadece zamanın bir bölümünde kapalı tutmak istesin ki?
    • CentOS/RedHat’te rotanın sonuna quickack 1 ekleyerek o yol için gecikmeli ACK’i kapatabilirsiniz.
  • Bant genişliğinin sınırlı olduğu, paket minimum boyutunun 64 bayt olduğu ve üstüne çerçeveler arası boşluk gerektiği bir dünyada, her bir bayt için TCP paketi göndermek muazzam bir bant genişliği israfıydı.
    Çoğu Ethernet ağında minimum boyut hâlâ böyle; boş ACK göndermek için de durum aynı.
    Ama benim varsayılan duruşum şu: TCP_NODELAY değil, sadece TCP.

    • Karşı taraftaki borunun herhangi bir nedenle koptuğunu fark eden yerleşik bir mekanizması olan bir protokol olsa iyi olurdu.
    • QUIC’in (https://en.wikipedia.org/wiki/QUIC) gecikme gibi TCP sorunlarını çözmesi gerekmiyor mu?
  • Nagle’a artık ihtiyaç olmadığı argümanını pek ikna edici bulmuyorum.
    Telnet bugün önemli değil, ama hâlâ şu tarz uygulamalar çok gibi geliyor:
    write(fd, "Host: "), write(fd, hostname), write(fd, "\r\n"), write(fd, "Content-type: ") vb.
    Bu 40 kat overhead olmasa bile yaklaşık 5 kat olabilir.

    • Uygulamayı düzeltmek yeterli.
      Dosyaya yazarken böyle yapıp sihirli bir performans beklemeyiz; işletim sisteminin kendi tamponu olsa bile.
      Sokete yazarken farklı bir beklentiye girmek için neden yok ve Nagle zaten sistem çağrısı overhead’inden de kurtarmaz.
    • Telnet lafını görünce OpenSSH’nin ne yaptığını merak ettim; etkileşimli oturumlar dâhil tüm bağlantılarda TCP_NODELAY ayarlıyor.
      Bunu hem kodu okuyarak hem de strace davranışını gözlemleyerek doğruladım.
    • Asenkron giriş/çıkış yaptığınızı varsayarsak, her küçük write(2) çağrısında bloklanmak yerine tampona koymak tek mantıklı yöntem; bu yüzden bu desenlerin artık o kadar yaygın olmadığını düşünüyorum.
      Sunucularda iyi ölçeklenmek için genelde asenkron giriş/çıkış gerekir; istemcilerde de ağ çağrısında bloklanmak kötü bir deneyimdir.
      Özellikle ağ değişikliklerinin sık olduğu ve kapsama alanı dışına çıkmanın çok yaşandığı günümüz ortamında bu daha da geçerli.
    • Bazı geliştiriciler berbat kod yazıyor diye tüm internet cezalandırılmamalı.
    • En başta böyle yapılmamalı.
      Ağ tarafını bir kenara bıraksak bile sistem çağrıları epey pahalıdır, bu yüzden performans için kötüdür.
  • Uygulama kaynağına erişim olmadığında sokette TCP_NODELAY’i açmanın iyi bir yolunu bilen var mı merak ediyorum.
    Kalıcı olarak uygulayan bir kernel ayarı ya da sonradan değiştiren bir komut bulamadım.
    Yönlendirme tablosuna quickack 1 koyarak gecikmeli ACK’i kapatabildim, ama uygulamanın dışından TCP_NODELAY’i açmak özellikle zor görünüyor.
    Son zamanlarda, sahip olduğum bir uygulama ile onun etkileştiği kapalı kaynaklı bir uygulama arasında burada anlatılan sorunun aynısını yaşıyorum.

    • socket(2) için LD_PRELOAD ile araya girme gibi bir şey işe yaramaz mı?
      Gerçek fonksiyonu çağırdıktan sonra setsockopt gibi bir şey yapıp değiştirilmiş soketi döndürmek gibi.
    • Somut duruma göre araya socat koyabilirsiniz belki.
      Normalde your_app —> server ise bunu your_app -> localhost_socat -> server olacak hâle getirmek.
      socat’ta tcp_nodelay ayarlamak için komut satırı seçeneği var.
      Tabii kapalı kaynak uygulamayı localhost’a bağlanmaya ikna etmeniz gerekir.
      DNS sorgusu yapıyorsa bir /etc/hosts girdisiyle localhost’a bağlanmasını sağlamak mümkün olabilir.
      Uygulama socat ile yerel soket üzerinden iletişim kuracağı için uygulama tarafındaki tcp_nodelay etkili olmaz.
    • Bir debugger bağlayıp ptrace ile setsockopt çağırmak olmaz mı?
    • /proc//fd/ açıp soket seçeneklerini ayarlamak işe yarayabilir. Denemedim.
    • LD_PRELOAD
  • Yaklaşık 15 yıl önce çok gerçek zamanlı bir MMO oynuyordum; tüm iletişim TCP üzerindeydi.
    Bir düğmeye tıkladığımda, yanıt paketi geri dönene kadar eylemim ekranda bile görünmüyordu.
    Sonunda bu oyunu oynayan çocukların hepsi, ben dâhil, TCP_NODELAY’i açınca oyunun çok daha akıcı hâle geldiğini keşfetti.
    Özellikle oyun sunucusuna yakın California tarafındaki oyuncularda etkisi büyüktü.

    • WoW’dan mı bahsediyorsunuz bilmiyorum, ama o dönemlerde bir oyun güncellemesi tam olarak bu değişikliği yaptı ve muhtemelen başka şeyleri de değiştirdi.
      İlginç bir yan etki şuydu: Değişiklikten önce TCP akışı duraksarsa oyun kısa süreliğine donar, sonra kaçırılan alma olaylarını çok hızlı şekilde yeniden oynatırdı.
      Genelde o olay benim ölme sahnem olurdu.
      Değişiklikten sonra ise bunun yerine bağlantı doğrudan kopuyordu.
  • İlgili Oxide and Friends podcast bölümü: https://www.youtube.com/watch?v=mqvVmYhclAg

    • Harika bir bölümdü ve görselleştirmenin önemini gerçekten çok güçlü biçimde gösterdi.
  • Go gibi varsayılan olarak TCP_NODELAY’i etkinleştiren modern bir dil kullanıyorsanız bu geçerli değil :-)

  • Her zaman öyle değil. Bazen sorun DNS’tir

    • Bir keresinde yönlendiricideki arızalı bir hat kartı, IPv4 adresinin son bitini 0’a çeviriyordu; bu da “yalnızca çift IPv4 adreslerine erişilebiliyor” diye bir destek kaydına dönüştü
    • Benim başıma gelen bir olayda ise cam kirliydi
      Bir şantiye yakınındaki yönlendiricide toz, lazer ile fiber optik arasındaki boşluğa çökmüş, sinyali yeterince zayıflatmıştı; %40~50 paket kaybı görülüyordu
      Kayıp noktasını bulunca NOC ilgili taşıyıcı operatöre e-posta gönderdi; bir gün sonra gönderilen teknisyen bu hikâyeyi yanıt olarak anlattı
    • 50 yılda bir, 2 milyar km uzakta, arızalı bir bellek çipi de olabilir
      Yine de genelde bir yama ile etrafından dolaşılabildiği için büyük mesele değildir
    • BGP’yi ya da uyarı vermeden diskin dolmasını da unutmamak gerek
    • Başarısız oluyorsa DNS’tir; sadece hareket duruyorsa TCP_NODELAY ya da stream buffering’dir
      Gerçekten karmaşık bir sistem olan Web, önbellek yüzünden de başarısız olur