TCP_NODELAY seçeneğinin sürekli kullanımı
(brooker.co.za)- Dağıtık sistemlerdeki gecikme sorunları, yalnızca
TCP_NODELAYetkinleş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_NODELAYetkin 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
- Küçük paketler çoğunlukla shell gibi insan etkileşimli uygulamalarda ve veriyi birden çok
- 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_NODELAYetkinleş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ılanTCP_NODELAYaltı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_QUICKACKbir 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çektenwrite()işleminin gerçekleşmesi beklenir
1 yorum
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ı.
Ş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ı.
writedavranışı 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.Sistem çağrısı maliyetini telafi eden
io_uringgibi bir şey yoksa kullanıcı alanı tarafı daha iyi çalışır.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.
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.
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_QUICKACKsoket seçeneği olduğunu gördüm, ama her alımda yeniden ayarlanması gerekiyor./proc/sys/net/ipv4/tcp_delack_minve/proc/sys/net/ipv4/tcp_ato_minde var.FreeBSD’de
net.inet.tcp.delayed_ackvenet.inet.tcp.delacktimevar.TCP_QUICKACKen 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.
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?
quickack 1ekleyerek 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_NODELAYdeğil, sadece TCP.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.
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.
Bunu hem kodu okuyarak hem de
stracedavranışını gözlemleyerek doğruladım.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.
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 1koyarak 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
setsockoptgibi bir şey yapıp değiştirilmiş soketi döndürmek gibi.Normalde
your_app —> serverise bunuyour_app -> localhost_socat -> serverolacak hâle getirmek.socat’ta
tcp_nodelayayarlamak 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/hostsgirdisiyle 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_nodelayetkili olmaz.ptraceilesetsockoptçağırmak olmaz mı?/proc//fd/açıp soket seçeneklerini ayarlamak işe yarayabilir. Denemedim.LD_PRELOADYaklaşı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ü.
İ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
Go gibi varsayılan olarak TCP_NODELAY’i etkinleştiren modern bir dil kullanıyorsanız bu geçerli değil :-)
https://github.com/golang/go/issues/57530
Bunu bilmiyordum
Sadece “modern” bir ağ kütüphanesi kullanılamaz mı?
Her zaman öyle değil. Bazen sorun DNS’tir
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ı
Yine de genelde bir yama ile etrafından dolaşılabildiği için büyük mesele değildir
TCP_NODELAYya da stream buffering’dirGerçekten karmaşık bir sistem olan Web, önbellek yüzünden de başarısız olur