İstek zaman aşımına düşüyor: arka plan işleri, kuyruk ve yeniden deneme
Bir HTTP isteğinin içinde 30 saniyeden uzun süren iş yapıyorsanız o iş yanlış yerde duruyor. Excel dışa aktarma, 40 bin satırlık içe aktarma, PDF üretimi, toplu e-posta, üçüncü taraf API'ye seri çağrı: hepsi kullanıcının tarayıcısı bağlantıyı açık tutmaya çalışırken değil, arka planda ayrı bir işçi süreçte çalışmalı. Doğru kurgu üç parçadır: isteği alıp işi kuyruğa yaz, kullanıcıya bir iş kimliği dön, durumu sorgulanabilir yap. İşin zor kısmı kuyruk yazılımını seçmek değil. Aynı işin iki kez çalışması, kalıcı bir hatada sonsuz yeniden deneme ve kimsenin bakmadığı bir ölü mektup kuyruğu, bu mimarinin gerçek maliyetini oluşturan üç şey.
Zaman aşımını yükseltmek çözüm değil, çünkü sınır sizde değil
Uzun isteğin nerede kesildiğini bilmek işin yarısıdır. Heroku router'ı 30 saniyede vazgeçer ve H12 hatası döner; ilginç olan kısmı, arkadaki dyno isteği işlemeye devam eder, router bunu bilmediği için o meşgul dyno'ya yeni istekler yönlendirir ve sorun kartopuna dönüşür. AWS Application Load Balancer'ın varsayılan boşta kalma zaman aşımı 60 saniyedir. Nginx'te proxy_read_timeout varsayılanı da 60 saniye. Cloudflare arkasındaysanız origin'in yanıt vermesi için 100 saniyeniz var, aşarsanız kullanıcı 524 görür ve bu süre yalnızca Enterprise planda değiştirilebilir.
Bu sayıların hepsi ayrı katmanlarda duruyor. Nginx'in zaman aşımını 120 saniyeye çekmek, önündeki yük dengeleyici 60'ta kestiği için hiçbir işe yaramaz; sadece hatanın hangi bileşenden döndüğünü değiştirir. Bir sınırı yükseltmek, isteği bekleyen kullanıcının 90 saniye boyunca dönen bir spinner'a bakmasını da düzeltmez. Uzun iş, isteğin dışına çıkarılmadıkça bir yerde kesilir.
Önce istek/yanıt sözleşmesini değiştirin
Kuyruk eklemeden önce API'nin davranışı değişmeli. Uzun bir işi başlatan uç nokta işi hemen yapmaz: kaydı oluşturur, kuyruğa yazar ve 202 Accepted ile birlikte bir iş kimliği döner. Kullanıcı arayüzü o kimlikle durum uç noktasını yoklar, ya da sunucu tarafından gönderilen olaylar (SSE) üzerinden ilerlemeyi dinler. Rapor gibi dakikalar süren işlerde en dürüst çözüm genelde en basiti: "hazır olduğunda e-posta ile bildireceğiz".
Bu değişikliğin arayüz tarafında bedeli var, çünkü artık üç durum göstermeniz gerekiyor: kuyrukta bekliyor, çalışıyor, bitti ya da başarısız. Ekipler bunu genelde küçümser. Kullanıcının "raporum nerede" sorusunun cevabını hiçbir yerde göremediği bir kuyruk mimarisi, senkron zaman aşımından daha çok destek talebi üretir.
Kuyruk için yeni bir altyapı şart değil
Varsayılan refleks Redis ya da SQS kurmak oluyor, oysa çoğu ekip için Postgres yeterli. SELECT ... FOR UPDATE SKIP LOCKED, PostgreSQL 9.5 ile (Ocak 2016) geldi ve bir işçinin kilitli satırı beklemek yerine atlayıp sıradaki işi almasını sağlıyor. Ruby'deki Que ve Elixir'deki Oban tam olarak bu deseni kullanıyor. Saniyede yüzlerce iş seviyesine kadar bu yaklaşım gayet iyi çalışır.
Postgres'in burada asıl avantajı hız değil, işlem bütünlüğü. İşi, kaydı oluşturan transaction'ın içinde kuyruğa yazarsanız "iş kuyruğa girdi ama kayıt geri alındı" ya da tersi vakası hiç oluşmaz. Redis veya SQS kullanıyorsanız bu atomikliği kaybedersiniz; çözüm, işi bir outbox tablosuna yazıp ayrı bir süreçle kuyruğa aktarmak. Sidekiq, BullMQ ya da SQS'e geçmenin doğru gerekçeleri var (yüksek hacim, izole işçi filoları, hazır izleme arayüzü), ama "kuyruk deyince akla o geliyor" bunlardan biri değil. Yeni bir altyapı bileşeni, yedek alma, sürüm yükseltme ve nöbet yükü olarak geri döner.
İş iki kez çalışacak, buna göre yazın
Neredeyse tüm kuyruklar en az bir kez teslim garantisi verir, en fazla bir kez değil. Mekanizma şöyle: işçi mesajı alır ve bir kiralama süresi boyunca o mesaj diğer işçilere görünmez olur. SQS'te bu görünürlük zaman aşımının varsayılanı 30 saniye, aralığı 0 saniye ile 12 saat arası. Google Pub/Sub'da onay süresinin varsayılanı 10 saniye, en fazla 600 saniyeye çıkarılabiliyor ve istemci kütüphaneleri işlem sürdükçe bu süreyi uzatıyor.
Sonuç net: iş kiralama süresinden uzun sürerse mesaj yeniden teslim edilir ve aynı iş, birincisi hâlâ çalışırken ikinci kez başlar. İşçi süreç ölürse aynı şey olur. Dolayısıyla her işin tekrar çalıştığında zarar vermemesi gerekiyor. Pratikte bu, işin sonucunu bir iş kuralı kimliğiyle tekilleştirmek anlamına geliyor: veritabanında tekil kısıt, ya da işin başında alınan bir danışma kilidi (advisory lock). "E-posta iki kez gitti" ile "fatura iki kez kesildi" arasındaki fark, bu kısıtı koymuş olup olmamak. Aynı problemin ödeme tarafındaki hali için ödeme entegrasyonu yazısına bakabilirsiniz.
Yeniden denemede iki soru: hangi hata ve hangi aralık
Her hata yeniden denenmez. Geçici hatalar (bağlantı zaman aşımı, 502, 429, veritabanı deadlock) denenmeli. Kalıcı hatalar (doğrulama hatası, olmayan kayıt, "kart reddedildi") ilk seferde bitmiştir; onları 20 kez denemek log şişirir ve gerçek sorunu gizler. Kodda bu ayrımı açıkça yapın, çünkü kütüphanelerin varsayılanı genelde "her istisnayı yeniden dene" oluyor.
Aralık için sabit gecikme kullanmayın. Üstel geri çekilmenin üzerine rastgele bir sapma (jitter) eklemek standart yaklaşım; AWS'nin mimari blogunda karşılaştırılan full jitter yöntemi en az toplam yük üretiyor ve çoğu servis için doğru varsayılan. Jitter olmadan, bir kesintinin ardından tüm işçiler aynı saniyede geri döner ve servisi yeniden devirir. Sidekiq'in varsayılanına bakmak faydalı: 25 deneme, yaklaşık 21 gün boyunca, (deneme ** 4) + 15 + rastgele formülüyle. Bir arşivleme işi için makul. "Sipariş onay e-postası" için 21 gün sonra gelen bir deneme, kullanıcı açısından hatanın kendisinden kötü. Deneme sayısını iş tipine göre ayarlayın.
Dış servise vuran işlerde eşzamanlılık sınırı koyun
İşçi sayısını artırmak kuyruğu hızlı boşaltır, ta ki işler dışarıya çıkana kadar. 50 işçinin paralel çalıştığı bir havuz, saniyede 10 istek kabul eden bir kargo ya da e-fatura servisine dakikalar içinde 429 yığını üretir. Yeniden deneme mekanizması bunun üstüne binince yük artar, servis sizi geçici olarak engeller ve kuyruk temizlenmek yerine büyür.
Çözüm işçi sayısını düşürmek değil, o iş tipine özel bir eşzamanlılık sınırı ya da jeton kovası (token bucket) koymak. Sağlayıcı Retry-After başlığı gönderiyorsa kendi geri çekilme formülünüz yerine o değere uyun; sağlayıcının söylediği süre tahmininizden her zaman iyidir. Kotayı aşan işler kuyrukta bekler, bu normaldir. Ekranda bekleyen bir kullanıcı yoksa 20 saniye gecikmenin maliyeti sıfır.
Ölü mektup kuyruğu, kimse bakmıyorsa sessiz veri kaybıdır
Deneme hakkını tüketen iş bir yere gitmeli. SQS'te maxReceiveCount aşıldığında mesaj ölü mektup kuyruğuna (DLQ) taşınır; Sidekiq'te 25 denemeden sonra iş "dead" kümesine düşer ve orada altı ay bekler. Buradaki amaç zehirli mesajın kuyruğu tıkamasını engellemek: tek bir bozuk kayıt yüzünden yüzlerce sağlam işin sırasını bekletmemek.
Bu yapının işe yaraması bir şeye bağlı. DLQ'da mesaj birikmesi alarm üretmeli ve o alarmın sahibi olmalı. Aksi halde ay sonunda "şu 300 sipariş bildirimi hiç gitmemiş" cümlesini duyarsınız. Mesajı geri gönderme (redrive) yolunun da hazır olması gerekir; hatayı düzelttikten sonra bekleyen 300 işi elle yeniden oluşturmak istemezsiniz.
Tek kuyruk her şeye yetmez
Her işi aynı kuyruğa atmak ilk günlerde sorun çıkarmaz. Sonra bir kullanıcı 40 bin satırlık içe aktarma başlatır ve parola sıfırlama e-postası o işin arkasında 20 dakika bekler. Kuyrukları alan adına göre değil, gecikme beklentisine göre ayırın: kullanıcının ekranda beklediği işler, dakikalar içinde bitmesi gerekenler, gece çalışan toplu işler. Her grubun kendi işçi havuzu olsun ki biri diğerini açlığa mahkûm etmesin.
Toplu işleri de tek parça yazmayın. 40 bin satırı tek işte işlemek yerine bir üst iş satırları bölüp 400 alt iş oluşturur. Kazanç üç yerde: kiralama süresini aşmıyorsunuz, kısmi ilerleme görünür oluyor, hata durumunda 40 binin tamamı değil yalnızca başarısız parça tekrar deneniyor.
İşin yükünü değil kimliğini taşıyın
İş payload'ına nesnenin tamamını koymak yaygın bir hata. İş sıraya girdikten beş dakika sonra çalıştığında elindeki veri bayattır; sipariş iptal edilmiş, fiyat değişmiş olabilir. Payload'a kimliği yazın, işçi güncel kaydı veritabanından okusun.
Boyut da bir gerekçe. Amazon SQS azami mesaj boyutunu Ağustos 2025'te 256 KiB'den 1 MiB'ye çıkardı, ama faturalama 64 KB'lik birimler üzerinden yapılıyor: 1 MB'lık tek bir mesaj 16 istek olarak ücretlendiriliyor. Payload'a serbestçe veri doldurmanın maliyeti sessizce büyür. Güvenlik tarafı da var: iş argümanları Redis'te durur, log'lara düşer, Sidekiq Web gibi arayüzlerde okunabilir. Sır ve kişisel veri buraya girmemeli, kimlik referansı girmeli. Sırların nerede tutulacağı sır yönetimi yazısında ayrıntılı.
Zamanlanmış işler ayrı bir tuzak
Cron ifadeleri sunucu sayısı bir olduğu sürece çalışır. İki instance'a çıktığınız gün gece 03:00'te iki fatura toplu işi birlikte başlar. Zamanlayıcıyı tek bir yerde çalıştırın (lider seçimi ya da dağıtık kilit) ve her zamanlanmış işin başına "bu iş şu anda çalışıyor mu" kontrolü koyun, çünkü önceki çalıştırma bitmeden yenisi tetiklenebilir.
Saat dilimi ikinci tuzak. Yerel saate göre kurulmuş bir cron, yaz saati uygulamasının olduğu bölgelerde yılda bir kez işi iki defa, bir kez de hiç çalıştırmaz. Zamanlayıcıyı UTC üzerinden kurun, kullanıcıya gösterirken yerel saate çevirin.
Ölçmediğiniz kuyruk sessizce birikir
İzlenecek metrikler kısa bir liste: kuyruk derinliği, en eski bekleyen mesajın yaşı, iş süresinin p95 değeri, başarısızlık oranı, DLQ boyutu ve işçi doygunluğu. Alarm kurmaya en uygun olan derinlik değil, en eski mesajın yaşı. Derinlik dalgalanır ve bu normaldir; "en eski iş 15 dakikadır bekliyor" ise her zaman anlamlı bir sinyal.
Bu ölçümleri bir hedefe bağlamadan izlemek yetmiyor. "Dışa aktarma isteklerinin %95'i 2 dakika içinde biter" gibi bir söz verin ve alarmı ona göre kurun; hedef belirleme ve hata bütçesi mekaniği için SLO ve izlenebilirlik yazısına bakın. İşçi süreçlerin dağıtım sırasında nasıl kapatıldığı da bu başlığın içinde: yayına alırken çalışan işlere önce sinyal gönderip bitmelerini beklemezseniz her dağıtımda yarım kalmış iş biriktirirsiniz. Sıfır kesintili dağıtım yazısında bu tarafı ayrıntılandırdık.
Nereden başlanır
Log'lardan uç nokta bazında p95 süreyi çıkarın ve 3 saniyeyi geçen her şeyi listeleyin. Listenin en üstündeki üç uç noktayı arka plana taşımak genelde birkaç günlük iş, karşılığında zaman aşımı şikayetlerinin büyük kısmı biter. Zaten kuyruğu olan bir sisteminiz varsa üç soru yeterli: bir işi iki kez çalıştırmak veriyi bozar mı, DLQ'ya mesaj düştüğünde kim haber alıyor, deneme sayıları iş tipine göre mi ayarlanmış yoksa hepsi kütüphane varsayılanında mı. Üçüncü sorunun cevabı çoğu projede "hepsi varsayılanda" oluyor.
Bu konuda yardıma mı ihtiyacınız var?