İçeriğe geç
Muhammet Şafak
en
Güncelleme 2 dk okuma

Laravel Queue Production'da Neden Yavaşlar?

Laravel queue worker'ları yerelde mükemmel, production'da yavaş. Geliştiricilerin gözden kaçırdığı en yaygın beş neden.

Kısaca

Queue'nun production'da yavaşlaması genelde Laravel'in içindeki değil, altındaki beş şeyden gelir: job başına açılan bağlantı, job içinde timeout'suz I/O, çekirdek sayısını yok sayan worker sayısı, memory leak'e karşı periyodik restart'ın olmayışı ve tek kuyruğa yığılmış öncelikler. Beşi de ölçülebilir; asıl mesele de bu: queue yavaş, üzerine iş yapacağınız bir hipotez olmamalı.

Laravel Queue Production'da Neden Yavaşlar? — kapak görseli

“Bu queue yerelde çalışıyordu” cümlesi senelerdir aynı tonla söyleniyor. Production’da çalışıyor — sadece beklediğiniz hızda değil. Gerçek nedenler genelde Laravel’in dışında.

1. Queue Storage için yeterli connection pool yok

Queue Storage olarak Redis kullandığınızı varsayalım.

Default phpredis ya da predis bağlantıyı job başına değil worker süreci başına kuruyor — queue:work uzun ömürlü tek bir süreçtir ve Laravel’in RedisManager’ı çözümlediği bağlantıyı önbelleğe alır. Her job için yeni süreç doğan kurulumlarda (queue:listen, PHP-FPM istekleri) bağlantı gerçekten job başına açılır: her birinde bir TCP handshake’lik gecikme + arada bir ECONNRESET.

Çözüm: config/database.php içinde persistent => true (phpredis için) — Laravel aynı seçeneği RedisCluster’a da geçirir, yani cluster kurulumu da kapsanır. Worker başlattığınızda bağlantıyı açık bırakın:

'redis' => [
    'options' => [
        'cluster' => env('REDIS_CLUSTER', 'redis'),
        'prefix' => env('REDIS_PREFIX', ''),
        'persistent' => true,
    ],
],

Not: RabbitMQ tarafında optimizasyonun ana noktası persistent socket flag’i değil, connection lifecycle yönetimidir. Her job’da connection açmak yerine worker process başına uzun ömürlü AMQP connection kullanılmalı, channel reuse yapılmalı ve heartbeat/read-write timeout değerleri job sürelerine göre ayarlanmalıdır.

2. Job içinde sync I/O bekletmesi

Http::get() çağrısı 30 saniye timeout ile bekleyebiliyor. O job o worker’ı tutar. 10 worker, 10 yavaş upstream — kuyruğun gerisi durur.

İki kural:

  • Her HTTP/SQL çağrısının explicit timeout’u olsun. Default 30 saniye değil 5.
  • Beklenmesi gereken işler delay’li ayrı bir kuyrukta tutulsun (örn. webhook retry’leri için slow kuyruğu, hızlı işler default kuyruğunda).

3. Supervisor numprocs yanlış

Tek bir queue worker tek bir PHP process’tir. Tek bir PHP process tek bir CPU core’unu kullanır. 4 core’lu sunucuda 1 worker çalıştırmak nproc * 0.75’i atıl bırakıyor demektir.

Tipik kural: numprocs = nproc (CPU-bound) veya numprocs = 2 * nproc (I/O-bound). Her uygulamayı ölçün.

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --queue=default --sleep=1 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=4
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/laravel-worker.log
stopwaitsecs=3600

stopwaitsecs=3600 önemli — Supervisor restart’ı sırasında uzun bir job’u yarıda kesmesin.

4. Memory leak’e karşı yeniden başlatmıyorsunuz

Long-running PHP process’ler memory’i biriktirir. --max-time=3600 veya --max-jobs=1000 ile worker’lar periyodik olarak kendilerini sonlandırmalı ve Supervisor tarafından yeniden başlatılmalı. Aksi takdirde:

  • Worker 8 GB RAM tüketir.
  • OOM killer onu öldürür.
  • Hangi job’u yarıda kestiği belirsiz.

5. Job batch’leri için tek bir kuyruk kullanmak

Yüksek hacimli “send notification” job’larıyla az sayıda “process payment” job’unu aynı kuyruğa atınca:

  • 50.000 notification 30 dakika kuyruğu tıkar.
  • Payment job’u bekler.
  • Müşteri “ödemem geçmedi” diye yazar.

Prioritize: önemli işleri ayrı kuyruklara koyun ve worker --queue=payments,default,low şeklinde sıralı dinlesin.

Görmeniz gereken metrikler

Üç şey yeterli:

  • Kuyruk uzunluğu (Redis: kuyruk listesi için LLEN — delayed ve reserved job’lar ayrı sorted set’lerde durur, onların ZCARD’ını da ekleyin).
  • Job süresi p95 (kendi instrumentation’ınız — Horizon’un metrik panosu throughput, bekleme süresi ve ortalama runtime verir, yüzdelik dilim vermez).
  • Failed job sayısı son 5 dakikada.

Bu üçü dashboard’da olsun ve eşik geçince alarm üretsin.


Queue yavaşlığının %90’ı bu listedeki bir şeydir. Geri kalan %10 ise gerçek bottleneck’ler (database, external API) — onları bulmak için doğru ölçümünüz olmalı. Production’da “queue yavaş” hipotez değil, ölçülmüş bir şey olmalı.

Sık sorulanlar

3 soru

  • Supervisor kaç queue worker çalıştırmalı?

    Bir worker tek bir PHP process, o da tek bir CPU core kullanıyor; dolayısıyla 4 core'lu bir makinede tek worker makinenin çoğunu atıl bırakıyor. Tipik kural CPU-bound işte numprocs = core sayısı, I/O-bound işte iki katı — sonra kurala güvenmek yerine o uygulamayı ölçün.

  • Supervisor yapılandırmasında stopwaitsecs neden önemli?

    Supervisor bir süreci öldürmeden önce stopwaitsecs saniye bekliyor ve varsayılan 10. Uzun bir job'un ortasındaki worker bu sürede yarıda kesilir. Değeri en uzun job sürenizin üzerine çekin ki restart, job'un bitmesine izin versin.

  • Kuyruğun gerçekten yavaş olduğunu hangi ölçümler söyler?

    Üç tanesi: kuyruk uzunluğu, job süresinin p95'i ve son beş dakikadaki failed job sayısı. Redis'te kuyruk uzunluğu, liste için LLEN artı delayed ve reserved sorted set'lerin ZCARD'ı demek; p95 ise kendi instrumentation'ınızı gerektiriyor — Horizon throughput ve bekleme süresi veriyor, yüzdelik dilim değil.

Kaynaklar

İlgili

Yorumlar

Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi