İki hafta önce kripto sinyal API'm neredeyse 22 saat boyunca sessizce çalışmayı bıraktı.
Hata mesajları yoktu. Kapanmalar olmadı. Servis çalışmaya devam etti, loglar akmaya devam etti, deployment paneli her şeyin yolunda olduğunu gösteriyordu. Sadece veritabanını kontrol ettiğimde, neredeyse bir gün boyunca hiç yeni veri yazılmadığını fark ettim.
Sorun neydi? Binance'e bağlanan WebSocket bağlantım. "Bağlıydı" — ama saatlerdir tek bir mesaj almıyordu.
Bu, sessiz donukluk problemi. Ve TCP keepalive bunu yakalayamaz.
Fiyat verisi, sohbet mesajları, IoT telemetri veya log akışı gibi uzun süreli WebSocket beslemeleriyle çalışan herhangi bir sistemde bu sorundan etkilenme riski taşıyorsunuz. İşte neler olduğunu ve nasıl çözüleceğini anlatıyoruz.
"Bağlı" algısının ardındaki gerçek
WebSocket bağlantısı açıldığında, arka planda TCP soketi el sıkışması gerçekleştirir. Bundan sonra "bağlı" demek, sizinle sunucu arasında açık bir TCP bağlantısının olduğu ve TCP’nin yolun yaşayan olduğunu düşündüğü anlamına gelir.
Hepsi bu.
TCP keepalive (etkinleştirildiğinde) yolun canlı olup olmadığını doğrulamak için periyodik boş paketler gönderir. İşletim sistemi bunu otomatik olarak yapar. Eğer yol koparsa, sonunda bağlantı kapatma hatası alırsınız.
Ancak TCP’nin göremediği şeyler var:
- Uygulamanın diğer tarafında mesaj gönderilmeye devam edilip edilmediği
- Sizinle sunucu arasında bulunan proxy veya yük dengeleyicinin aboneliğinizi düşürüp düşürmediği
- Arka uçtaki bir hata olay akışını durdururken bağlantıyı açık bıraktığında
WebSocket’iniz TCP katmanında mükemmel görünürken uygulama verisi akışı tamamen durmuş olabilir.
Benim durumumda, Binance’in WebSocket ağı bağlantımı kabul etti, aboneliklerimi kaydetti — ardından ticker güncellemelerini göndermeyi kesti. TCP soketi sorunsuzdu. İşletim sistemi sorunsuzdu. Kodum sorunsuzdu. Veriler ise kaybolmuştu.
Naif çözümlerin neden işe yaramadığı
İlk içgüdü: "Hata oluşunca yeniden bağlanacağım." Ama uygulama asla hata vermedi. Hiçbir istisna tetiklenmedi. Bağlantı mükemmel şekilde canlıydı — sadece içinden veri geçmiyordu.
İkinci içgüdü: "Sunucuya ping atan bir bekçi saati ekleyeceğim." Bu daha doğru bir yaklaşım gibi görünüyor ama bir kusuru var — çoğu hizmet (buna borsa beslemeleri de dahil) veri WebSocket’lerinde istemci ping’lerine yanıt vermez. Ping’iniz gider, sessizlikle geri döner ve "sunucu yanıt vermiyor" ile "sunucu bozuk" arasında ayrım yapamazsınız.
Üçüncü içgüdü: "Abone mesajı gönderip onay alacağım." Bu başlangıçtaki başarısızlıkları yakalar ama akışın ortasındaki arızaları yakalamaz.
Gerçekten işe yarayan çok daha basit bir şey:
Son alınan mesajın zamanını takip edin. Eşik süresini aşarsa akış donuk demektir — TCP ne düşünürse düşünsün.
Mesaj düzeyinde donukluk algılama uygulaması
İşte Python’da bu deseni uygulayan kod örneği:
import asyncio
import json
import time
import websockets
STALENESS_TIMEOUT_SECONDS = 60 # Beslemenizin beklenen sıklığına göre ayarlayın
class StaleStreamError(Exception):
pass
async def consume_stream(url, subscribe_message):
while True:
try:
async with websockets.connect(url) as ws:
await ws.send(json.dumps(subscribe_message))
last_message_at = time.time()
async def monitor_staleness():
while True:
await asyncio.sleep(STALENESS_TIMEOUT_SECONDS)
age = time.time() - last_message_at
if age > STALENESS_TIMEOUT_SECONDS:
await ws.close()
raise StaleStreamError(
f"{age:.1f} saniyedir mesaj yok "
f"(eşik: {STALENESS_TIMEOUT_SECONDS} saniye)"
)
staleness_task = asyncio.create_task(monitor_staleness())
try:
async for message in ws:
last_message_at = time.time()
await handle_message(message)
finally:
staleness_task.cancel()
except websockets.exceptions.ConnectionClosed:
print("Bağlantı kapandı, yeniden bağlanılıyor...")
except StaleStreamError as e:
print(f"Donukluk algılandı: {e}, yeniden bağlanılıyor...")
await asyncio.sleep(1) # Yeniden denemeden önce beklemeAnahtar fikir: "Canlı" tanımını işletim sistemi seviyesinde değil, uygulama seviyesinde yapın.
Beslemeniz doğal sessizlik dönemlerine sahip olabilir (piyasaların kapalı olduğu saatler, düşük trafikli dönemler), bu yüzden eşiği ayarlayın. 60 saniyelik bir zaman aşımı IoT telemetrisi için çok agresif olabilir; 5 dakikalık bir zaman aşımı yüksek frekanslı bir fiyat takibi için çok yumuşak kalabilir.
İyi bir kural: Zaman aşımını en yavaş dönemlerdeki mesaj aralıklarının 3-5 katı olarak ayarlayın.
Borsa tarafından sağlanan kalp atışları hakkında ne düşünmeli?
Bazı WebSocket protokolleri uygulama katmanında açık kalp atışları içerir — her iki tarafın da canlı olduğunu doğrulayan küçük periyodik mesajlar. Örneğin Binance Vadeli İşlemler her birkaç dakikada bir ping gönderir; siz de pong ile yanıt verirsiniz.
Bunlar yardımcı olur. Ama tek başlarına donukluk sorununu çözmezler, çünkü:
- Kalp atışları veri aboneliğinin öldüğü sırada çalışmaya devam edebilir (sunucudaki farklı kod yolları)
- Bazı beslemelerde kalp atışı hiç yer almaz
- Kalp atışlarıyla birlikte bile, veri akışınız için hâlâ donukluk mantığına ihtiyacınız var
Kalp atışlarını bir girdi olarak görün, tek gerçek olarak değil. Gerçek sinyaliniz şudur: "Abone olduğum türde mesajları alıyor muyum?"
Durumu daha da kötüleştirmeyen yeniden bağlanma mantığı
Donukluk algıladığınızda ve yeniden bağlandığınızda şunları göz önünde bulundurun:
- Üstel geri çekilme: Sunucu gerçekten kapalıysa, yeniden bağlanma girişimleriyle sunucuyu boğmayın
- Titreme (jitter): 1000 istemci aynı anda donukluk algılarsa (sunucu arızası sonrası), rastgele yeniden deneme aralıkları birikme etkisini önler
- Durum kurtarma: Sipariş defteri, abonelik kanalları gibi durumlu beslemeler için yeniden bağlanma sonrası durumu yeniden senkronize etmeniz gerekebilir
- Uyarılar: M dakika içinde N’den fazla yeniden bağlanma yaşadıysanız, daha derin bir sorun var demektir — sizi uyarın
22 saatlik ders
Bana gelen hata gizli değildi — uzun süreli akış sistemlerinde bilinen bir başarısızlık moduydu. Ama servisimi "WebSocket bağlı = veri akıyor" varsayımı üzerine kurmuştum ve bu varsayım sessizce bozulunca sistemimin sessizce durmasına izin vermiş oldum.
Bunu kalıcı olarak düzeltmek için yaptıklarım:
- Mesaj düzeyinde donukluk algılama (yukarıdaki desen)
- Dış sağlık izleme —
last_signal_age_secondsdöndüren küçük bir uç nokta, böylece UptimeRobot eşiği aştığında beni uyarabilir
Unutmayın: TCP sizinle sunucu arasında bir tünel inşa eder. Ama o tünelin diğer ucundan size gelen şeylerin canlı olup olmadığını anlamanın tek yolu, ne aldığınızı izlemektir.
Yapay zeka özeti
WebSocket 'bağlandı' diyor ama veri gelmiyorsa sorun TCP keepalive'de değil. Uygulama katmanında donukluk algılama nasıl yapılır? Pratik Python örneğiyle açıklanıyor.