deniz.in

Piyasalar

Hava durumu

Hava durumu yükleniyor

· kaynak Hacker News – Front Page (native)

Go 1.27, HTTP/1 bağlantı tekrar kullanımını düzeltti; tekrarlayan istekleri 710 ms'den 150 ms'ye düşürdü

Go blogundaki bir yazı, Go 1.27'nin Go 1.26'nın terk ettiği HTTP/1 bağlantılarını canlı tuttuğunu gösteriyor; 100 isteklik bir benchmark, hiçbir kod değişikliği olmadan yaklaşık beş kat daha hızlı çalışıyor.

Go 1.27, HTTP/1 bağlantı tekrar kullanımını düzeltti; tekrarlayan istekleri 710 ms'den 150 ms'ye düşürdü

Go 1.27, standart kütüphanedeki HTTP istemcisinin HTTP/1 bağlantılarını tekrar kullanma şeklini yeniden düzenliyor ve bunun getirisi çok yaygın bir kodlama kalıbı için çarpıcı. Go blogunda yayımlanan bir yazıya göre, index.golang.org'a 100 kez http.Get çağıran — her yanıttan yalnızca status code'u okuyan — küçük bir program, yazarın makinesinde GODEBUG=http2client=0 ile HTTP/2 devre dışı bırakıldığında Go 1.26'da yaklaşık 710 milisaniye, Go 1.27'de ise yaklaşık 150 milisaniye sürdü. Bu, yalnızca runtime yükseltmesiyle elde edilen neredeyse beş kat bir iyileşme. Hacker News ana sayfasına kadar yükselen yazı, farkın neden bu kadar büyük olduğunu açıklıyor.

Yeni bir bağlantının maliyeti

Yazı, bir bağlantı açmanın neden pahalı olduğunu adım adım anlatıyor. Bir TCP handshake bir round trip maliyetine sahiptir: SYN, SYN-ACK, ACK; istemci ilk TLS mesajını bu son ACK ile birlikte gönderebilir. TLS handshake ise TLS 1.2 altında iki round trip daha maliyettir; böylece ilk HTTP isteği ancak üç tam round trip sonrasında, ayrıca kriptografik handshake için harcanan CPU süresi eklenerek gönderilebilir.

Gecikme yüksekken bu round trip'ler belirleyici olur. Yazı, bölgeler arası bağlantıların tek yönde sık sık 50 milisaniyeye ulaştığını veya aştığını, yani round trip başına 100 milisaniye demek olduğunu belirtiyor; isteğin kendisi zar zor bir round trip'e ihtiyaç duyarken kuruluma yaklaşık 300 milisaniye harcamak kötü bir takastır. Küresel olarak dağıtık olan ve TLS 1.3 destekleyen (TLS handshake'i tek round trip'e indiren) index.golang.org bile, sürekli yeni bağlantılar açıp kapatmanın cezasını çok net biçimde gösteriyor.

Tekrar kullanım standart cevaptır ve mekanizmalar protokol sürümüne göre farklıdır. HTTP/1.0 istemci ve sunucuları Connection: keep-alive header'ı ile gayri resmi olarak katılır. HTTP/1.1 bağlantıları, taraf göndermedikçe varsayılan olarak kalıcıdır. HTTP/2 daha da ileri gider ve tek bir TCP bağlantısı üzerinden birçok istek ve yanıtı çoğullar (multiplexing).

HTTP/1 tekrar kullanımı nasıl bozuluyor

HTTP/1'de bir bağlantıyı paylaşan istekler ve yanıtlar hat üzerinde kesinlikle sıralıdır ve tek sürekli bir byte akışı olarak gelir. net/http, status line ve header'ları ayrıştırdığında http.Get'ten döner; body, çağıran ondan okudukça tembel biçimde tüketilir. Sonraki yanıt hemen mevcut yanıtın bitiminde başladığı için, istemci bu sınırı yalnızca önceki body Content-Length veya Transfer-Encoding header'larıyla belirlenen şekilde sonuna kadar okunduğunda bulabilir.

Örnek tam burada ısırıyor. Body'yi hiç okumadan kapatıyor ve Go 1.26'ya kadar bu, net/http'nin bağlantıdan vazgeçmesini zorunlu kılıyor: sonraki yanıtın başlangıcını bulamayan istemci bağlantıyı çöpe atıyor ve her sonraki istek için tam kuruluma ödüyor. HTTP/2 bağışıktır, çünkü çoğullanmış akışları sayesinde bir akışta okunmamış bir body, aynı bağlantıdaki diğer istekleri engellemez.

Bu hata modu istemeden tetiklenmesi kolaydır. Yalnızca status code'u kontrol eden bir istemci, endpoint'in gönderdiği body'yi yine de alır; örneğin başarı bayrağı ve mesaj içeren kısa bir JSON onayı. Body'yi okuyan kod bile yetersiz kalabilir: encoding/.Decoder kapanış parantezini görür görmez durur, bu yüzden JSON'unu yeni satırla bitiren bir sunucu tam bir byte okunmamış bırakır — sağlıklı bir bağlantının çöpe atılmasına yetecek kadar.

Geliştiricilerin kullandığı geçici çözümler

Kabul edilen çözüm, body'yi kapatmadan önce boşaltmaktı; byte'ları io.Discard'a kopyalamak. Küçük yanıtlar için bu sorun değildir, ancak büyük bir okunmamış body için yeni bir bağlantı açmak, kalanını indirip atmaktan daha hızlı olabilir; bu yüzden boşaltma işlemi bir boyut sınırına ihtiyaç duyar — yazı io.CopyN ile 256 KB kullanıyor. Byte'ları yavaş yavaş damlatan bir sunucu ikinci bir kısıt ekler, yaklaşık 50 milisaniyelik bir süre sınırı; ve close üzerinde bloklanmamak için boşaltmanın arka planda asenkron çalışması, sonraki isteklerle yarışması gerekir. Yazı bu diziyi adım adım kuruyor ve doğru bir elle yapılmış çözümün giderek artan karmaşıklığı, sorunun neden bu kadar uzun sürdüğünün iyi bir örneği.

Neden önemli

Go 1.27 bu tuzağı çözüyor: benchmark'taki aynı okunmamış-body kalıbı artık beşte bir sürede tamamlanıyor ve hiçbir kaynak değişikliği gerekmiyor. Bu önemli, çünkü performans düşüşü sessizdi — her istekte yeni bir TCP ve TLS handshake ödeyen servisler doğru görünüyor ve yalnızca yavaş oluyordu; maliyet tam da round trip'lerin en pahalı olduğu yerde yoğunlaşıyordu, örneğin bölgeler arası trafikte. Dahili API'lara HTTP/1 istemcisi çalıştıran ya da HTTP/2'nin devre dışı bırakıldığı ortamlarda bulunan herkes, yalnızca yükseltme yaparak anlamlı bir gecikme ve CPU kazanımı elde ediyor. Yanıt body'lerini kapatmadan önce boşaltmak iyi bir alışkanlık olmayı sürdürüyor, ama artık havuzlanmış bir bağlantı ile her seferinde yeni bir handshake arasındaki farkı belirlemiyor.

  • #golang
  • #http
  • #networking
  • #performance
  • #standard-library

İlgili yazılar