On saniye boşta kalınca ölen proxy tünelleri
Boşta kalınca kapanan tünel, tek bir uzun oturumu binlerce yeniden bağlanmaya çevirir. Sayaçlı bant genişliğinde el sıkışma trafiği faturanın kendisi olur.
Saatlerce açık kalması gereken bir entegrasyon, bunun yerine birkaç saniyede bir yeni oturum açıyordu. Yüksek sesle bozulan hiçbir şey yoktu: istekler başarılıydı, log'lar sıradan görünüyordu ve tek belirti, gerçekten taşıdığımız verinin birkaç katı büyüklükte bir bant genişliği faturasıydı. Karşı taraf yaklaşık on saniyelik sessizlikten sonra tüneli kapatıyor, bizim client da günde binlerce kez, kibarca, yeniden bağlanıyordu.
Gerçekte ne oluyor
Proxy tüneli, bir host ve porta bağlanma isteğiyle kurulur; ardından içinden ham bayt akar. Proxy o tüneli yalnızca kullanıldığı sürece tutar. Ayarlanan boşta kalma süresi boyunca hiçbir şey geçmezse tünel kapatılır ve kaynak geri alınır. On saniye kısa ama alışılmadık değil ve çoğu zaman dokümanda da yazmıyor.
Bunu pahalı yapan şey, client'ların bu durumu nasıl ele aldığı. Kapanan soket çoğu HTTP client için hata durumu değil, yeni soket açma sebebidir. Kütüphane yeniden bağlanır, el sıkışmayı tekrarlar ve devam eder; üstteki uygulama katmanı bir şey olduğunu hiç öğrenmez.
Bu yeniden bağlanmaların her biri göründüğünden pahalıdır:
- Tünel isteği ve yanıtı.
- Hedefe yapılan tam bir TLS el sıkışması, sertifika zinciri dahil; tipik bir sunucuda birkaç kilobayt, uzun zincirli bir sunucuda daha fazla.
- Hedefin yeni bağlantıda yaptığı kimlik doğrulama veya oturum kurulumu; bir giriş turu ya da çerez alışverişi gibi.
Bunların hiçbiri taşınan veri değil. Bunu idle penceresi başına bir yeniden bağlanmayla, kaç worker koşturuyorsanız o sayıyla ve bir günle çarpın; ek yük yuvarlama hatası olmaktan çıkar. Sayaçlı bant genişliğinde gigabayt başına el sıkışma satın alıyorsunuz demektir.
Paradan daha pahalıya gelen ikinci bir etki de var. Karşı taraf oturum başına çıkış adresi atıyorsa, her yeniden bağlanma yeni bir kimliktir. Sürekliliği varsayan ne varsa tuhaf davranmaya başlar: açık bir oturum düşer, sayfalama imleci başa sarar, saygı gösterilen bir hız sınırına aynı anda bir düzine yönden vurulur. Hedef, tek bir client görmesi gereken yerde bir sürü görür ve herkesin bir sürüye vereceği tepkiyi verir. Bu da bizi hız sınırını kalıcı değil geçici saymaya geri götürür.
Nasıl görülür
Hikâyenin tamamını iki sayı anlatıyor: saatlik oturum sayısı ve oturum başına bayt.
Client'ınız bağlantı yaşam döngüsünü log'lamıyorsa çekirdek söyler. Kurulu bağlantı sayısını saniyede bir örnekleyin ve sayının çalkalanıp çalkalanmadığına bakın:
for i in $(seq 1 60); do
printf '%s %s\n' "$(date +%T)" "$(ss -tn state established "dport = :8080" | wc -l)"
sleep 1
doneSabit bir sayı, yeniden kullanılan bir pool demektir. Birkaç saniyede bir yükselip düşen sayı churn'dür ve o salınımın periyodu genelde aradığınız idle timeout'un kendisidir.
Bayt tarafı için oturum başına açılış, kapanış ve aktarılan bayt bilgisini log'layın, sonra özetleyin:
awk -F'\t' '{n++; d+=$2; b+=$3} END {
printf "oturum=%d ort_saniye=%.1f ort_bayt=%.0f\n", n, d/n, b/n
}' sessions.tsv
# oturum=41780 ort_saniye=9.8 ort_bayt=4120Yuvarlak bir sayının hemen altına düşen ortalama oturum ömrü, idle timeout'un itirafıdır. El sıkışmanın da birkaç kilobayt tuttuğu yerde birkaç kilobaytlık ortalama veri ise, ödediğiniz her şeyin kabaca yarısının kurulum olduğu anlamına gelir. Bu oturum log'larını bir haftayı diğeriyle karşılaştıracak kadar uzun tutun; yani düzgün rotate edin ki diski dolduran şey haline gelmesinler.
Timeout'u doğrudan ölçmek de bir dakikadan kısa sürüyor:
import socket, time
s = socket.create_connection(("proxy.host", 8080), timeout=5)
s.sendall(b"CONNECT ornek.com:443 HTTP/1.1\r\nHost: ornek.com:443\r\n\r\n")
print(s.recv(200).split(b"\r\n")[0])
s.settimeout(1)
for i in range(1, 90):
time.sleep(1)
try:
if s.recv(1) == b"":
print("kapandi, bosta gecen saniye:", i)
break
except TimeoutError:
continue
else:
print("90 saniye bostan sonra hala acik")Bunu iki kez çalıştırın. Bir kez olduğu gibi, bir kez de her üç saniyede bir tek bayt göndererek. Böylece etrafına bir şey inşa etmeden önce keep alive'ın farkını görmüş olursunuz.
Çözüm
Dört değişiklik, ne kadar tasarruf ettirdiklerine göre sıralı.
Tüneli sıcak tutun. Idle penceresinin içinde bir şey gönderin, protokolün kendi ping'i varsa onu kullanın. Aralığı timeout'un rahatça altında seçin: on saniyelik pencere için dokuz değil üç. Dokuzda yaşanacak bir zamanlama gecikmesi size tüneli kaybettirir. Keep alive baytları el sıkışmanın yanında yok denecek kadar küçüktür. Soket seviyesindeki TCP keepalive burada genelde işe yaramaz, çünkü proxy tünelden geçeni sayar ve varsayılan prob aralığı saat mertebesindedir.
Oturumu sticky yapın. Sağlayıcı oturum kimliğini destekliyorsa, yeniden bağlanmalarda aynı kimliği geçirin ki tünel aynı çıkışa düşsün ve hedef süreklilik görsün. Bu el sıkışma sayısını azaltmaz, her birinin verdiği zararı azaltır.
Pool kurun ve yeniden kullanın. Worker başına sıcak tutulan bir tünel, istek başına açılan tünele göre açık ara kazanır. Pool boyutunu istek hızına değil eşzamanlı worker sayısına göre ayarlayın ve pool'un kendi boşta düşürme süresinin karşı tarafın timeout'undan kısa olmasına dikkat edin; yoksa pool size zaten ölmüş soketler dağıtır.
Yeniden bağlanmada backoff uygulayın. Beklemesiz bir yeniden bağlanma döngüsü, karşı taraftaki kısa bir sorunu sizin ürettiğiniz bir sele, sayaçlı bant genişliğinde de bir faturaya çevirir.
POOL_SIZE = 24
PING_SECONDS = 3 # karsi taraf ~10 saniyede kapatiyor
SESSION_TTL_SECONDS = 600 # rotasyon kazara degil bizim takvimimizle
def session_id(worker):
window = int(time.time() // SESSION_TTL_SECONDS)
return f"w{worker}s{window}"
def acquire(worker):
sid = session_id(worker)
return pool.get(proxy_user=f"{USER}-session-{sid}", ping=PING_SECONDS)Taşımadığı protokolü kabul eden port
En çok zamana mal olan tuzak timeout değildi. Aynı port SOCKS5 selamlamasını kabul etti, metot pazarlığına başarı baytı döndü, connect isteğine başarı baytı döndü ve sonra hiçbir şey taşımadı. Client'taki her katman sağlıklı bir bağlantı raporluyordu. Hiçbir yönde tek bayt gelmedi ve arıza, hedefin yavaş olması gibi göründü.
Gerçekte yalnızca HTTP CONNECT uygulanmıştı. Çıkarılacak ders şu: başarılı el sıkışma, çalışan taşıma demek değildir. Yolu bilinen içerik dönen bir istekle kanıtlayın:
curl -s -x http://user:pass@proxy.host:8080 https://ornek.com/status \
-o /dev/null -w 'kod=%{http_code} bayt=%{size_download} sure=%{time_total}\n'
# kod=200 bayt=612 sure=0.412Bu içerik dönüyor ama aynı portta SOCKS varyantı asılı kalıyorsa, doküman ne derse desin o port yalnızca HTTP CONNECT taşıyordur.
Çalıştığını nasıl doğrularsınız
Aynı iki sayıyı öncesi ve sonrasıyla, aynı uzunlukta süre ve aynı miktarda gerçek iş üzerinden karşılaştırın. İstediğiniz şekil, taşınan veri sabit kalırken oturum sayısının bir mertebe düşmesi:
once: saatlik oturum ~1.700 ort_bayt 4.100 gunluk transfer ~6 GB
sonra: saatlik oturum ~45 ort_bayt 160.000 gunluk transfer ~1 GBSonra idle probunu tekrar çalıştırın ve doksan saniyeyi geçtiğini görün. Tünel boşta kalmayı atlatıyor ama oturum sayısı düşmediyse pool yeniden kullanılmıyordur; bu da proxy'de değil client'ta ayrı bir hatadır.
Nelere dikkat etmeli
- Timeout'a yakın bir ping aralığı, er geç kaybedeceğiniz bir yarıştır. En az üçte ikilik bir pay bırakın.
- Sıcak tünelin boştayken de bir maliyeti var. Üç saniyede bir ping atan yirmi dört tünel sürekli trafiktir; iş yokken pool'un küçülmesini sağlayın, yoksa hiçbir şeyi ayakta tutmak için ödeme yaparsınız.
- Sticky oturumların sağlayıcı tarafında bir üst ömrü olur. Rotasyonu kendi takviminizle, onlarınkinden önce yapın ki değişim başınıza gelen bir şey değil yönettiğiniz bir şey olsun.
- Kapanan tüneli başarısız istek sayan retry mantığı işi çiftler. Retry'ı taşıma katmanında tutun ya da istek seviyesinde izin vermeden önce işlemi idempotent yapın.
- Eşzamanlılık ile maliyet düz bir çizgide birlikte büyümez. Daha çok worker, daha çok sıcak tünel ve daha çok ping demektir; kapasite eklemeden önce oturum başına maliyeti ölçün. Tıpkı gönderim kapasitesinin nerede kaybolduğunu ölçmek gibi, varsaymak yerine bakmak gerekir.
Idle timeout'u olan her taşıma, uzun ömürlü kurguyu bir yeniden bağlanma döngüsüne çevirir ve bu döngü, biri faturayı ya da bağlantı sayısı grafiğini okuyana kadar görünmez. İzlenecek birim istek veya hata değil, oturum ve oturum başına bayttır; sessiz bozulmanın maliyeti ilk orada görünür. Sessizce bozulan bir taşıma, gürültüyle bozulanla aynı ölçümü hak eder, hatta ona genelde daha çok ihtiyaç duyar.
Sorular ve cevaplar
- Hiçbir sorun yokken proxy bağlantım neden kopuyor?
- Çoğu proxy, üzerinden bayt geçmeyen tüneli bir süre sonra kapatır; on ile otuz saniye arası yaygın bir ayardır. Bağlantı hata vermiyor, geri alınıyor. Client kapanan soketi görüp yenisini açar ve devam eder, uygulamanın hiç hata görmemesinin sebebi de budur.
- TCP keepalive proxy'nin boştaki tüneli kapatmasını engeller mi?
- Genelde engellemez. Proxy, tünelin içinde uygulama katmanında geçen baytı sayar, TCP keepalive paketleri ise onun altındadır. Ayrıca varsayılan keepalive aralıkları saat mertebesindedir, hiçbir proxy idle timeout'una yetişmez. Onun yerine tünelin içinden bir şey gönderin, örneğin protokolün kendi ping'ini.
- Yeniden bağlanmaların bana kaça mal olduğunu nasıl hesaplarım?
- Toplam aktarılan baytı oturum sayısına bölüp oturum başına baytı bulun, sonra bunu el sıkışmanın sabit maliyetiyle karşılaştırın; tipik bir sertifika zinciri için bu birkaç kilobayttır. Ortalama oturum birkaç kilobayt veri taşıyorsa trafiğinizin kabaca yarısı, iki kere ödediğiniz kurulum demektir.
- Port SOCKS5'i kabul ediyor ama hiçbir şey çalışmıyor, neden?
- Bazı uçlar SOCKS5 selamlamasına ve connect isteğine başarı baytı döner ama tüneli hiç taşımaz, çünkü gerçekte yalnızca HTTP CONNECT uygulanmıştır. Client'ın her katmanı bağlandı der, hiçbir veri gelmez. Taşımayı hata dönmeyen bir bağlantıyla değil, bilinen içerik dönen bir istekle doğrulayın.