<

PHP'de Kuyruk Sistemleri: Basit Cron'dan Dağıtık Kuyruğa

Yıllar önce, ajans dönemimde yürüttüğümüz bir muhasebe entegrasyonunda gelen ilk şikâyet masumdu: "Fatura kes butonuna basınca sayfa 20 saniye dönüyor." Koda baktık; buton, HTTP isteğinin içinde PDF üretiyor, e-fatura servisine bağlanıyor, iki e-posta gönderiyor ve bir de muhasebe yazılımına webhook atıyordu. Hepsi senkron, hepsi aynı istekte. E-fatura servisi yavaşladığı gün istekler 30 saniyelik PHP limitine takılıyor, kullanıcı butona tekrar basıyor, mükerrer fatura oluşuyordu. Ayda ortalama 60 mükerrer kayıt, muhasebe ekibinin iki günü temizlikle geçiyordu. Teşhis basitti: bu projenin kuyruğa ihtiyacı vardı. Asıl ilginç soru şuydu — ne kadar kuyruğa?

Herkesin küçümsediği kahraman: cron ve bir veritabanı tablosu

PHP dünyasında kuyruk lafı geçince akıllar hemen RabbitMQ'ya, Kafka'ya gidiyor. Açık konuşayım: yıllar içinde teslim ettiğim projelerin çoğunda ilk kuyruk, bir MySQL tablosu ve dakikada bir çalışan cron'dan ibaretti. jobs tablosu; iş tipi, payload, durum, deneme sayısı, müsait olma zamanı. Cron uyanır, bekleyen işleri sırayla işler, biter.

Bu mimarinin küçümsenmeyi hak etmeyen erdemleri var. Sıfır yeni altyapı — elinizde zaten olan veritabanı ve cron ile çalışıyor. İşler SQL ile sorgulanabilir; "dün başarısız olan işler hangileri" sorusunun cevabı bir SELECT. Yedekleme, izleme, yetkilendirme — hepsi mevcut veritabanı pratiğinizin içinde. O muhasebe projesinde tam bunu yaptık: buton artık sadece işi tabloya yazıp "faturanız hazırlanıyor" diyor, cron gerisini hallediyor. Buton süresi 20 saniyeden 300 milisaniyeye indi, mükerrer fatura sıfırlandı.

Bu katmanın gözden kaçan bir avantajı daha var: zamanlanmış işler bedavaya geliyor. "Bu faturayı yarın sabah 09:00'da kes" demek, müsait olma zamanı sütununa bir tarih yazmaktan ibaret. Ertelenmiş bildirimler, hatırlatma e-postaları, deneme süresi biten hesapların kapatılması — hepsi aynı tablo, aynı worker. Ayrı bir zamanlayıcı altyapısı kurmadan, kuyruk ve takvimi tek mekanizmada birleştirmiş oluyorsunuz.

Dakikada bir uyanan cron'un gecikmesi kabul edilemez hâle geldiğinde bir sonraki adım da ucuz: cron yerine supervisor altında sürekli çalışan bir-iki worker prosesi. Aynı tablo, aynı kod, saniye altı gecikme.

Veritabanı kuyruğunun duvara çarptığı an

Peki bu mütevazı düzen nereye kadar? Sınırları da yaşayarak öğrendim. Geçmişte bir e-ticaret projesinde kampanya günü, dakikada 4.000'e yaklaşan iş hacminde jobs tablosu kilitlenme yarışlarına sahne olmaya başladı: worker'lar aynı satırları kapmaya çalışıyor, SELECT ... FOR UPDATE SKIP LOCKED ile idare ediyorduk ama tablo büyüdükçe hem iş alma sorgusu yavaşlıyor hem de asıl uygulama veritabanıyla aynı kaynağı yiyorduk. Kuyruğun yoğunluğu, korumaya çalıştığı uygulamayı yavaşlatır hâle gelmişti — kendi hastasını ezen ambulans.

O noktada Redis tabanlı kuyruğa geçtik. Laravel projelerinde bu geçişin güzelliği şu: iş sınıflarının tek satırı değişmiyor, sadece bağlantı konfigürasyonu değişiyor. Horizon ile birlikte worker'ların anlık durumu, iş başına süre dağılımları, kuyruk bazlı yoğunluk grafikleri hazır geliyor. Aynı kampanya senaryosunu ertesi ay tekrar yaşadık; dakikada 4.000 iş, Redis için ısınma turu bile değil.

RabbitMQ'ya ise bugüne kadar yalnızca birkaç projede gerçekten ihtiyaç duyduk — hepsinde ortak özellik, PHP dışındaki sistemlerle (Java tarafında yazılmış bir depo yönetimi, Go ile yazılmış bir telemetri toplayıcı) aynı kuyruk üzerinden konuşma zorunluluğuydu. Tek dilli, tek uygulamalı bir projede RabbitMQ kurmak, mahalle bakkalına forklift almak gibi; gücü gerçek, ihtiyacı hayal.

Kuyruğun asıl zor kısmı: başarısızlıkla yüzleşme protokolü

Teknoloji seçimi, kuyruk işinin en kolay yüzde yirmisi. Zor kısım, işler başarısız olduğunda ne olacağının tasarımı — çünkü kuyruk demek, hatanın kullanıcıdan koparılıp sizin sorumluluğunuza geçmesi demek. Kullanıcı "faturanız hazırlanıyor" mesajını gördü ve gitti; e-fatura servisi o sırada çökerse bunu ona kim, nasıl söyleyecek?

Benim her kuyruklu sistemde istisnasız uyguladığım — ve şimdiki ekibimde de şart koştuğum — asgari protokol şu:

  • Her iş idempotent yazılır: iki kez çalışması tek kez çalışmasıyla aynı sonucu üretir. Retry'ın ön koşulu budur; idempotent olmayan işe retry eklemek mükerrer fatura üretmenin otomasyonudur.
  • Deneme sayısı ve artan bekleme: 3 deneme, 1-5-15 dakika aralıklarla. Karşı servis çöktüyse 10 saniyede 3 kez vurmanın âlemi yok.
  • Ölü iş kuyruğu: hakkını tüketen iş silinmez, failed_jobs'a düşer ve birinin ekranında görünür. Görünmeyen ölü kuyruk, sessizce kaybolan faturalar demektir.
  • İş bazlı zaman aşımı: tek bir asılı kalan HTTP çağrısının worker'ı sonsuza dek rehin almasına izin yok.

Bu listenin her maddesi, geçmişte bir projede yaşanmış somut bir kazanın dersidir; hangi kaza hangi maddeye denk geliyor, tahmininize bırakıyorum.

Bir de öncelik meselesi var. Tek kuyruk, kampanya günü atılan 50.000 pazarlama e-postasının arkasına sıkışmış bir şifre sıfırlama e-postası demektir — kullanıcı iki dakika içinde beklediği mailin kırk dakika gecikmesini affetmez. Bu yüzden işleri en az iki sınıfa ayırıyoruz: kullanıcının başında beklediği işler ve arka plan yükü. Her sınıfın kendi kuyruğu, kendi worker havuzu var; pazarlama seli ne kadar kabarırsa kabarsın, kritik şerit hep boş.

2026 dokunuşu: kuyruğu izleyen artık sadece Horizon değil

Yakın dönemde eklediğimiz bir katman, bu mimariye beklemediğim bir rahatlık getirdi. Kuyruk metriklerimizi MCP üzerinden operasyon ajanımıza açtık; ajan, failed_jobs'a düşen işleri düzenli tarıyor, hata mesajlarını gruplayıp "son iki saatte 34 iş aynı SSL hatasıyla düştü, karşı servisin sertifikası dün gece değişmiş" gibi teşhislerle Slack'e yazıyor. Eskiden ölü kuyruk taraması pazartesi sabahlarının angaryasıydı; şimdi çoğu vakayı, kullanıcı fark etmeden önce ajanın teşhisiyle kapatıyoruz. Retry kararlarını hâlâ kod veriyor, silme kararlarını hâlâ insan veriyor — ajan sadece gözcü. Ama iyi bir gözcünün değerini, gözcüsüz geçen yıllarınız kadar iyi hiçbir şey anlatamıyor.

Deploy tarafında da kuyruğa özgü bir dikkat gerekiyor ve bunu yeni başlayan her ekip bir kez acı yoldan öğreniyor: worker'lar uzun ömürlü prosesler olduğu için, kod güncellemesi yaptığınızda eski kodu çalıştırmaya devam ederler. Yeni sürümü yayınladınız, buglı iş sınıfını düzelttiniz ama işler hâlâ hatalı çıkıyor — çünkü worker, belleğine üç gün önce yüklediği sınıfı koşturuyor. Her deploy'da worker'ları nazikçe (eldeki işi bitirtip) yeniden başlatmak, deploy betiğinin pazarlıksız adımı olmalı.

Özetle önerim mütevazı: kuyruğa cron ve tablodan başlayın, sıkıştığınız yerde bir üst kata çıkın ve enerjinizi teknoloji seçiminden çok başarısızlık protokolüne harcayın. Projenizde "butona basınca 20 saniye dönüyor" hikâyesinin bir versiyonu yaşanıyorsa, hangi katın yeteceği genelde yarım günlük bir keşifle ortaya çıkıyor — ve cevap, tahmin ettiğinizden daha sık, "bir tablo ve cron yeter" oluyor. Kuyruk muhabbetine her zaman varım; iletişim sayfası açık.

📅 Yayınlanma:  ·  Yakup Zengin