← blog · 17 Ekim 2026

Ara Sıra Gelen 502'lerin Sessiz Sebebi: Boşta Bağlantı Zaman Aşımları ve Keepalive Sıralaması

Yük dengeleyici, vekil ve uygulama sunucusu boşta bağlantıyı farklı sürelerde kapatır. Sıra yanlışsa sakin saatlerden sonra rastgele 502'ler gelir. Kural, varsayılanların tuzağı, tekrar denemenin sınırı ve ağ cihazları.

Trafik yokken her şey yolunda görünür, yük gelince de sorun çıkmaz. Ama günde birkaç kez, rastgele bir istek 502 döner ya da istemci "connection reset by peer" görür. Tekrar denendiğinde aynı istek başarılı olur. Uygulama logunda o isteğe dair tek satır yoktur, çünkü istek uygulamaya hiç ulaşmamıştır. Bu belirtilerin arkasında çoğu zaman aynı sebep yatar: zincirdeki iki bileşenin boşta bağlantı zaman aşımları yanlış sıralanmıştır.

Sorun nereden çıkıyor

Yük dengeleyici, ters vekil ya da bir HTTP istemcisi, arkadaki sunucuya açtığı TCP bağlantısını her istekten sonra kapatmaz. Bağlantıyı havuzda tutar ve bir sonraki istek için yeniden kullanır. Bu, her istekte TCP ve TLS el sıkışmasının maliyetini ödememek için doğru bir davranıştır.

Havuzda bekleyen bağlantının iki ucu vardır ve iki ucun da kendi boşta kalma süresi vardır. Sunucu "bu bağlantı şu kadar süredir boşta, kapatıyorum" der. İstemci de "bu bağlantı şu kadar süredir boşta, atıyorum" der. Sunucunun süresi istemcininkinden kısaysa şu yarış kaçınılmazdır: sunucu bağlantıyı kapatmaya karar verir, kapatma paketi yoldayken istemci aynı bağlantıyı havuzdan alıp isteği yazar. Sunucu artık o bağlantıda istek kabul etmediği için sıfırlama (RST) gönderir. İstemci tarafında bu, cevap başlığı okunamadan kopan bağlantı olarak görünür. Vekil bunu kendi istemcisine 502 olarak çevirir. nginx bu durumu "upstream prematurely closed connection while reading response header from upstream" satırıyla loglar.

Hata zamanlamaya bağlı olduğu için seyrek ve rastgele görünür. Yoğun trafikte bağlantılar hiç boşta kalmadığından sorun gizlenir; en çok sakin saatlerden sonra gelen ilk isteklerde görülür.

Kural: isteği başlatan taraf önce bırakır

Çözümün özü tek cümle: her bağlantıda, isteği gönderen tarafın boşta zaman aşımı, isteği karşılayan tarafınkinden kısa olmalı. Böylece bağlantıyı her zaman istemci bırakır ve sunucunun "ben kapattım ama sen yazdın" yarışı oluşmaz.

Zincir uzadıkça bu kuralı her halka için ayrı ayrı uygulamak gerekir. İstemci, yük dengeleyici, ters vekil ve uygulama sunucusundan oluşan bir yolda üç ayrı bağlantı vardır ve her birinde sunucu tarafı daha sabırlı olmalıdır. Pratikte bu, içe doğru gittikçe zaman aşımlarının büyümesi demektir: yük dengeleyicinin boşta süresi 60 saniyeyse arkasındaki vekil en az 65 saniye bekler, vekilin arka uç havuzu 60 saniyede bağlantı bırakıyorsa uygulama sunucusu 65 saniye bekler.

Birkaç saniyelik pay bırakmak önemlidir. İki değer birbirine eşitse yarış tamamen ortadan kalkmaz, çünkü iki tarafın saati aynı anda başlamaz.

Varsayılanlar neden tuzak

Sorunun bu kadar yaygın olmasının sebebi, popüler araçların varsayılan değerlerinin bu kuralın tersine dizilmiş olması. Uygulama sunucuları kaynak tasarrufu için kısa boşta süresi seçer, yük dengeleyiciler ise uzun.

Node.js'in yerleşik HTTP sunucusunda keepAliveTimeout varsayılanı 5 saniyedir. Gunicorn'un --keep-alive varsayılanı 2 saniyedir. Apache'nin KeepAliveTimeout varsayılanı 5 saniyedir. Öte yandan bulut sağlayıcıların uygulama yük dengeleyicileri genellikle 60 saniyelik boşta süresiyle gelir. Bu bileşenleri varsayılanlarla yan yana koyduğunuzda, tam olarak yanlış sırayı elde edersiniz.

Node.js için tipik düzeltme şöyle görünür:

const server = http.createServer(app);
server.keepAliveTimeout = 65_000;
server.headersTimeout = 66_000;
server.listen(port);

headersTimeout değerini keepAliveTimeout üzerinde tutmak gerekir; aksi halde sunucu, yeniden kullanılan bağlantıda başlıkları beklerken kendi zaman aşımına takılabilir.

Gunicorn için aynı ayar --keep-alive 65 ile yapılır. Go'da http.Server üzerindeki IdleTimeout alanı belirleyicidir; boş bırakılırsa ReadTimeout kullanılır, ikisi de sıfırsa boşta bağlantı hiç kapatılmaz.

Ters vekilde arka uç havuzu

nginx'te arka uç bağlantılarının yeniden kullanılması ayrıca açılması gereken bir özelliktir. Açıldığında, nginx'in havuzu kaç saniye tuttuğu da bu zincirin bir halkası olur:

upstream app_backend {
    server app.example.com;
    keepalive 32;
    keepalive_timeout 60s;
}

server {
    location / {
        proxy_pass http://app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Burada keepalive 32 her çalışan süreç başına havuzda tutulacak boşta bağlantı sayısıdır, toplam bağlantı sınırı değildir. keepalive_timeout ise havuzdaki bağlantının ne kadar boşta kalabileceğini belirler. Arkadaki uygulamanın boşta süresi bu değerden uzun olmalıdır. Connection başlığını boşaltmak, istemciden gelen close değerinin arka uca taşınıp havuzu bozmasını engeller.

Arka uç havuzunu hiç açmazsanız bu yarış oluşmaz, çünkü her istek yeni bağlantıyla gider ve cevaptan sonra kapanır. Bedeli, yoğun trafikte her istekte el sıkışma ve vekil makinede çok sayıda TIME_WAIT durumunda soket birikmesidir.

Tekrar denemek neden yetmiyor

İlk akla gelen çözüm, bu hatayı alan isteği bir kez daha göndermek. Bazı HTTP istemcileri bunu zaten sessizce yapar: yeniden kullanılan bir bağlantı hiç cevap gelmeden koparsa, idempotent isteği yeni bir bağlantıyla tekrarlar.

Sorun, idempotent olmayan isteklerde başlar. Bağlantının hangi noktada koptuğunu istemci kesin bilemez; sunucu isteği okuyup işlemeye başlamış da olabilir. Bu yüzden nginx proxy_next_upstream ile hata durumunda isteği başka sunucuya aktarırken, POST, LOCK ve PATCH gibi idempotent olmayan istekleri varsayılan olarak tekrarlamaz; bunu açmak için non_idempotent değerini açıkça eklemek gerekir. Açmak, ödeme ya da sipariş gibi işlemlerde çift kayıt riskini kabul etmek demektir.

Tekrar deneme, zaman aşımı sıralaması doğru yapıldıktan sonra kalan nadir durumlar için bir güvenlik ağıdır. Asıl düzeltmenin yerine konursa, hata ortadan kalkmaz, yalnızca POST isteklerinde görünür olarak kalır.

Uzun süre boşta duran bağlantılar ve ağ cihazları

Aynı problemin HTTP dışındaki bir biçimi veritabanı ve mesaj kuyruğu bağlantılarında görülür. Uygulama havuzundaki bir bağlantı uzun süre boşta kalırsa, aradaki NAT ağ geçidi ya da güvenlik duvarı bağlantı izleme kaydını kendi zaman aşımıyla siler. Bunu iki uca haber vermez. Uygulama bir sonraki sorguyu yazdığında paket ya düşer ya da sıfırlama döner; ilki, sorgunun uzun bir TCP yeniden iletim süresinin sonunda hata vermesine yol açar.

Burada iki araç işe yarar. Birincisi TCP keepalive: işletim sistemi boşta bağlantıda belirli aralıklarla boş paket göndererek aradaki cihazın kaydı canlı tutmasını sağlar. Linux'ta net.ipv4.tcp_keepalive_time varsayılanı 7200 saniyedir, yani çoğu NAT cihazının zaman aşımından çok uzundur. Bu yüzden değeri uygulama ya da sürücü düzeyinde kısaltmak gerekir; PostgreSQL sunucu tarafında tcp_keepalives_idle, istemci kütüphanesi tarafında keepalives_idle parametreleri bunun içindir. İkincisi bağlantı havuzunun en uzun ömür ayarı: havuz, bağlantıları ağ cihazlarının zaman aşımından önce kendisi emekli eder. İkisi birlikte kullanıldığında havuzdan alınan bağlantının ölü olma ihtimali pratikte ortadan kalkar.

HTTP/2 ve gRPC

HTTP/2 bu problemin bir kısmını çözer. Sunucu bağlantıyı kapatmadan önce GOAWAY çerçevesiyle hangi akışları işleyeceğini bildirir ve istemci yeni istekleri yeni bağlantıya yönlendirir. Bu kapanışı düzenli hale getirir ama aradaki ağ cihazı sorununu çözmez.

gRPC'de istemci tarafı keepalive ping'leri bunun için vardır. Tuzak şudur: istemci sunucunun izin verdiğinden sık ping atarsa, sunucu bağlantıyı "too_many_pings" sebebiyle kapatır. İstemcinin ping aralığı, sunucunun izin verdiği en kısa aralıktan uzun ayarlanmalıdır. Bu da yine aynı kuralın bir biçimidir: iki uç birbirinin beklentisini bilmeden kendi değerini seçmemeli.

Sorunu doğrulamak

Teşhis için en hızlı yol, hatanın zaman dağılımına bakmaktır. 502'ler yükle orantılı değil de sakin dönemlerden sonra kümeleniyorsa, boşta zaman aşımı yarışından şüphelenin. Vekil logunda arka ucun bağlantıyı erken kapattığını söyleyen satırları, uygulama logunda ise karşılığı olmayan istekleri arayın.

Yeniden üretmek için havuzlu bir istemciyle tek istek gönderin, uygulama sunucusunun boşta süresinden birkaç milisaniye uzun bekleyin ve aynı bağlantıdan ikinci isteği gönderin. Bekleme süresini sunucunun zaman aşımı etrafında küçük adımlarla kaydırdığınızda hata belli bir pencerede tekrar edilebilir hale gelir. Düzeltmeden sonra aynı test, değeri ne olursa olsun temiz geçmelidir.

Ne zaman bu kadar uğraşmaya değmez

Trafiği çok düşük, gecikmeye duyarsız iç servislerde arka uç bağlantı yeniden kullanımını tamamen kapatmak meşru bir seçimdir. Her istek yeni bağlantı açar, el sıkışma maliyeti ödenir ama yarış hiç oluşmaz ve yapılandırılacak bir zincir de kalmaz. Saniyede birkaç isteğin altında bu maliyet ölçülemeyecek kadar küçüktür.

Kalan her durumda, zincirdeki her halkanın boşta süresini bir tabloya yazıp içe doğru büyüdüğünü kontrol etmek yarım saatlik bir iştir. Bu tabloyu altyapı değiştiğinde güncellemek de işin parçasıdır: araya yeni bir vekil ya da yük dengeleyici girdiğinde, en sık bozulan şey tam olarak bu sıralamadır.