Kuyruk ve Asenkron İş Yönetimi: Nerede Gerekir, Nerede Zarar Verir
Her yavaş işlemi kuyruğa atmak çözüm değil, yeni bir hata sınıfı demek. Kuyruğun ne zaman gerçekten gerektiğini, hangi teslim garantilerinin var olduğunu ve idempotency olmadan neden patladığını anlatıyoruz.
Sorun: her şey senkron olamaz
Bir HTTP isteği geldiğinde sunucunun elinde sınırlı bir süre vardır. Kullanıcı birkaç yüz milisaniye bekler, birkaç saniyeye tahammül eder, on saniyeyi geçince sayfayı yeniler ya da uygulamayı kapatır. Ama yapılması gereken iş her zaman bu pencereye sığmaz: bir video dönüştürme, bir toplu e-posta gönderimi, üçüncü taraf bir API'ye yapılan ve yavaş olabilen bir çağrı, büyük bir raporun üretilmesi.
Buradaki doğal çözüm işi arka plana almak: isteği hemen kabul et, kullanıcıya "alındı" de, gerçek işi bir kuyruğa koy ve ayrı bir süreç (worker) o kuyruğu tüket. Sorun şu ki kuyruk eklemek karmaşıklığı azaltmaz, taşır. Senkron kodda bir hata olduğunda kullanıcı anında görür ve tekrar dener. Asenkron kodda hata bir yerlerde sessizce birikir, kimse fark etmeyebilir, ve iş üç gün sonra fark edildiğinde neyin ne zaman başarısız olduğunu anlamak ayrı bir uğraş haline gelir.
Ne zaman kuyruk gerçekten gerekir
Kuyruğun kazandırdığı şey gecikme değil, ayrıştırmadır (decoupling). İşin kabul edilmesiyle işlenmesi birbirinden bağımsızlaşır. Bunun anlamlı olduğu durumlar:
- İşlem süresi kullanıcı bekleyişinin çok üzerinde (dakikalar süren dönüştürme, toplu içe aktarma).
- İş, isteği yapan servisten bağımsız olarak yeniden denenebilmeli (webhook teslimatı, e-posta gönderimi, ödeme sağlayıcısına bildirim).
- Yük dengesiz geliyor ve anlık patlamaları emmek gerekiyor (kampanya döneminde binlerce bildirim aynı anda tetikleniyor ama worker sayısı sabit kalabilir).
- Farklı bileşenler farklı hızlarda ölçeklenmeli. Web katmanı hızlı yanıt vermeli, işleme katmanı yavaş ama güvenilir çalışmalı; ikisini aynı sürece hapsetmek ikisini de kötü yapar.
Bu durumların ortak noktası şu: iş, çağıranın canlı bağlantısına ihtiyaç duymuyor. Çağıran taraf sonucu beklemeden devam edebiliyor ve sonucu sonradan (bildirimle, polling ile, webhook ile) öğrenmesi kabul edilebilir.
Ne zaman zarar verir
En sık görülen hata, her yavaşça çalışan şeyi refleks olarak kuyruğa atmak. Birkaç somut örnek:
Kullanıcı sonucu hemen görmek zorundaysa kuyruk yanlış araçtır. Bir ödeme onayı, bir giriş denemesi, bir form doğrulaması senkron kalmalı; bunları asenkron yapıp sonucu polling ile sormak, hem kullanıcı deneyimini kötüleştirir hem de arayüzde gereksiz bir durum makinesi (bekleniyor / tamamlandı / başarısız) zorunlu kılar.
İşlem zaten hızlıysa kuyruk sadece gecikme ve operasyonel yük ekler. Bir veritabanı güncellemesi 10 milisaniye sürüyorsa onu kuyruğa koymanın kazandırdığı hiçbir şey yoktur, kaybettirdiği ise bir worker süreci daha izlemek, bir kuyruk daha yönetmek ve hata ayıklarken bir zıplama noktası daha eklemektir.
Sıra (ordering) önemliyse ve kuyruk bunu garanti etmiyorsa ortaya sinsi hatalar çıkar. Örneğin bir kullanıcının profil güncelleme olayları farklı worker'larda paralel işlenirse, geç gelen ama önce kuyruğa giren bir güncelleme yanlışlıkla üstteki daha yeni veriyi ezebilir. Çoğu genel amaçlı kuyruk sistemi (Kafka bölüm içi hariç) global sıra garantisi vermez; sıra gerekiyorsa bunu ya partition anahtarıyla ya da uygulama seviyesinde versiyon kontrolüyle sağlamak gerekir.
Debugging maliyeti göz ardı edilirse kuyruk sisteminin kendisi bir kara kutuya dönüşür. Senkron bir hata stack trace ile gelir; asenkron bir hata genelde "bu iş bir daha görünmedi" ya da "aynı iş beş kere işlendi" şeklinde kendini gösterir, ve kaynağını bulmak log korelasyonu gerektirir.
Teslim garantileri: en az bir kez neredeyse her zaman kazanır
Kuyruk sistemleri üç teorik teslim modeli sunar: en fazla bir kez (mesaj kaybolabilir ama tekrar işlenmez), en az bir kez (mesaj kaybolmaz ama birden fazla işlenebilir) ve tam olarak bir kez (ikisi de garanti, pratikte çok pahalı ve çoğu sistemde tam anlamıyla yok). Neredeyse tüm üretim sistemleri en az bir kez modelini seçer, çünkü mesaj kaybı genelde tekrar işlemekten daha kötü bir sonuçtur.
Bunun bedeli açık: worker'ınız aynı işi iki kez alabilir. Bir worker mesajı işler, sonucu commit etmeden önce çöker, kuyruk mesajı görünür hâle getirir (visibility timeout dolar), başka bir worker aynı işi tekrar alır. Bu senaryo teoride değil, her üretim sisteminde düzenli olarak gerçekleşir.
Idempotency: opsiyonel değil, ön koşul
En az bir kez teslimi kabul ettiğiniz an, her worker fonksiyonunun idempotent olması gerekir: aynı iş iki kez çalıştırıldığında sonuç, bir kez çalıştırılmışla aynı olmalı. Bunu sağlamanın standart yolu bir idempotency key kullanmak: işin kendine özgü bir kimliği olur (örneğin "kullanıcı 42 için 2026-09 faturası oluştur"), worker işi işlemeden önce bu kimliğin daha önce işlenip işlenmediğini kontrol eder (genelde tekil bir kısıt taşıyan bir tabloda ya da Redis'te). İşlenmişse worker sessizce çıkar.
Bunu atlayan sistemlerde klasik belirtiler şunlardır: aynı e-postanın kullanıcıya iki kez gitmesi, bir ödemenin iki kez tahsil edilmesi, bir sayacın çift artması. Hepsi aynı kök nedene bağlanır: worker'ın yan etkisi (side effect) tekilleştirilmemiş.
Retry, backoff ve ölü mesaj kuyruğu
Bir iş başarısız olduğunda üç seçenek vardır: hemen tekrar dene, bekleyip tekrar dene, ya da vazgeç. Hemen tekrar denemek, geçici olmayan bir hatada (örneğin bozuk veri) kuyruğu anlamsız bir döngüye sokar ve downstream servisi daha da yorar. Standart pratik üstel geri çekilmedir (exponential backoff): ilk tekrar 1 saniye sonra, sonraki 2, sonraki 4 saniye gibi artan aralıklarla, üstüne rastgele bir jitter eklenir ki binlerce iş aynı anda tekrar denenip yeni bir yük patlaması yaratmasın.
Bir iş belirli bir deneme sayısını (örneğin 5-10) geçtiğinde, sonsuza kadar denemek yerine ölü mesaj kuyruğuna (dead-letter queue, DLQ) taşınmalı. DLQ'nun amacı işi silmek değil, otomatik döngüden çıkarıp insan incelemesine açmaktır. DLQ'yu izlemeyen ekipler, orada aylarca birikmiş, hiç fark edilmemiş başarısız işlerle karşılaşır; bu yüzden DLQ'ya düşen her mesaj için ayrı bir uyarı (alert) kurmak, kuyruğun kendisini kurmak kadar önemlidir.
Araç seçimi
Büyük bir mesajlaşma sistemi kurmadan önce basit seçenek genelde gözden kaçar: eğer zaten bir ilişkisel veritabanı kullanıyorsanız, PostgreSQL'in SELECT ... FOR UPDATE SKIP LOCKED özelliğiyle bir tabloyu kuyruk gibi kullanmak, düşük-orta ölçekli işler için gayet yeterlidir. Ayrı bir mesajlaşma altyapısı işletmeden, mevcut transaction garantilerinizden faydalanırsınız.
İş hacmi büyüdüğünde ya da farklı servisler arasında mesaj yayınlamanız gerektiğinde RabbitMQ gibi klasik bir mesaj kuyruğu (yönlendirme, DLQ, önceliklendirme gibi özellikler hazır gelir) ya da Redis tabanlı hafif kuyruk kütüphaneleri makul bir orta nokta olur. Olay akışı çok yüksek hacimli, birden fazla tüketicinin aynı veriyi farklı hızlarda okuması gereken ve geçmişe dönük yeniden oynatma (replay) gerektiren bir senaryoysa Kafka gibi log tabanlı bir sisteme geçmek mantıklıdır; ama bu, çoğu uygulamanın ihtiyaç duymadığı bir operasyonel yük getirir.
Kuyruk derinliğini izlemek
Kuyruk sistemlerinin sinsi tarafı, sorunun uzun süre görünmez kalabilmesidir. Worker sayısı iş üretim hızının gerisinde kaldığında kuyruk sessizce büyür, hiçbir hata fırlatmaz, sadece işlerin sonuçlanma süresi (yaş, age) yavaşça uzar. Bu yüzden izlenmesi gereken temel metrik kuyruk uzunluğu değil, kuyrukta bekleyen en eski işin yaşıdır. Kuyrukta 10.000 iş birikmiş olması, hepsi 2 saniye içinde işleniyorsa sorun değildir; kuyrukta 50 iş varsa ama en eskisi 6 saattir bekliyor ise gerçek bir sorun vardır.
Ne zaman bu yaklaşımdan kaçınmalı
Ekip küçükse ve tek bir monolitik uygulama işletiyorsanız, ayrı bir worker filosu ve mesajlaşma altyapısı işletmenin operasyonel maliyeti çoğu zaman kazandırdığından fazladır. Aynı süreç içinde çalışan, veritabanı tarafından yönetilen basit bir zamanlanmış görev (cron job) çoğu "arka planda çalışsın" ihtiyacını, ayrı bir kuyruk sistemi kurmadan karşılar. Kuyruk sistemine geçişi, gerçekten ölçek ya da bağımsız yeniden deneme ihtiyacı ortaya çıktığında yapmak, baştan kurup bakımını üstlenmekten daha ucuza gelir.