2026-08-17altyapi4 dk

Kuyruk sistemi seçimi: Redis, Postgres ve gerçek mesaj kuyrukları

Ayrı bir kuyruk altyapısı kurmak gerekiyor mu, yoksa elinizdeki Postgres yetiyor mu? Kararı hacim değil, kayıp toleransı ve topoloji belirliyor. SKIP LOCKED ve XAUTOCLAIM örnekleriyle.

Arka plan işi çalıştırması gereken her projede aynı karar bir kez veriliyor ve genelde yanlış gerekçeyle veriliyor: kuyruk için ne kullanacağız? Cevap çoğu zaman "herkes RabbitMQ kullanıyor" ya da "Redis zaten var" oluyor. İkisi de gerekçe değil.

Bu yazı tek bir soruya bakıyor: bu iş için ayrı bir kuyruk altyapısı kurmanız gerekiyor mu, yoksa elinizdeki veritabanı yetiyor mu? Karar, kuyruk teknolojisinin özellik listesine değil, sizin kayıp toleransınıza ve iş hacminize bağlı.

Kuyruktan gerçekten ne istiyorsunuz

Bir kuyruk seçmeden önce şu beş şeyin her birine cevap verin. Bunlar cevaplanmadan yapılan seçim, ilk üretim arızasında geri alınıyor.

  • Bir iş kaybolursa ne olur? Fatura üretiliyorsa felaket, önbellek ısıtılıyorsa hiçbir şey.
  • Aynı iş iki kez çalışırsa ne olur? Neredeyse hiçbir kuyruk exactly-once vermiyor. İşleyiciniz idempotent değilse teknoloji seçiminiz bu sorunu çözmüyor.
  • Saniyede kaç iş? 5 mi, 5.000 mi? Aradaki fark bir mimari farkı.
  • Bir iş ne kadar sürüyor? 20 ms ile 20 dakika farklı görünürlük zaman aşımı stratejisi ister.
  • Sıra önemli mi? Aynı müşterinin işlerinin sırayla çalışması gerekiyorsa bu bir bölümleme (partition) gereksinimi.

Postgres zaten oradaysa, kuyruk da orada olabilir

SELECT ... FOR UPDATE SKIP LOCKED sayesinde Postgres, birden çok işçinin aynı satırı almasını engelleyerek çalışan bir kuyruk verir. Tablo şu kadar basit olabilir:

create table jobs (
  id           bigserial primary key,
  payload      jsonb        not null,
  status       text         not null default 'pending',
  run_after    timestamptz  not null default now(),
  attempts     int          not null default 0,
  locked_until timestamptz
);

create index jobs_pending_idx on jobs (run_after)
  where status = 'pending';

İş çekme tek ifadede yapılıyor:

update jobs
   set status = 'running',
       locked_until = now() + interval '5 minutes',
       attempts = attempts + 1
 where id = (
   select id from jobs
    where status = 'pending'
      and run_after <= now()
    order by run_after
      for update skip locked
    limit 1
 )
returning id, payload;

Bu yaklaşımın kazandırdıkları küçük değil. İşi kuyruğa koymak ile veriyi yazmak aynı transaction içinde olabiliyor, yani "kayıt oluştu ama iş kuyruğa girmedi" durumu ortadan kalkıyor. Yeniden deneme, gecikmeli çalıştırma ve ölü mektup kutusu birer status değeri. Kuyruğun içine SQL ile bakabiliyorsunuz, ki üretimde bunun değeri sanılandan yüksek.

Sınırları da net: her çekme işlemi bir yazma. Yüksek hacimde tablo şişer, autovacuum yükü artar ve WAL trafiği veritabanınızın asıl işini etkilemeye başlar. Süresi dolan kilitleri geri alacak bir süpürücü işi de kendiniz yazmak zorundasınız:

update jobs set status = 'pending', locked_until = null
 where status = 'running' and locked_until < now();

Bu tasarım, işlerin çoğu için yeterli. Sınırın nerede olduğunu tahmin etmeyin; kendi donanımınızda bir yük testi çalıştırıp pg_stat_activity ve WAL üretim hızına bakın.

Redis: LIST hızlı, Streams doğru

Redis tarafında en sık görülen hata LPUSH + BRPOP ikilisi. Bu kombinasyon işi kuyruktan çıkarır ve o andan sonra iş yalnızca işçinin belleğindedir. İşçi ölürse iş yok olur. Bunu kabul edebileceğiniz tek durum, işin kaybının önemsiz olması.

Kayıpsızlık istiyorsanız Streams tüketici gruplarını kullanın:

XADD jobs * type resize path /uploads/a.jpg
XGROUP CREATE jobs workers $ MKSTREAM

# işçi tarafı
XREADGROUP GROUP workers worker-1 COUNT 1 BLOCK 5000 STREAMS jobs >
XACK jobs workers 1723890000000-0

Burada XACK gelene kadar mesaj "bekleyen" listesinde durur. Ölen bir işçinin işlerini devralmak için:

XAUTOCLAIM jobs workers worker-2 60000 0 COUNT 10

Yani 60 saniyedir onaylanmamış mesajları worker-2 üstleniyor. Bu, Postgres'teki locked_until süpürücüsünün Redis karşılığı. Streams kullanacaksanız XAUTOCLAIM çağıran bir döngü yazmadan üretime çıkmayın, aksi halde bekleyen liste sessizce büyür.

Redis'in kalıcılığı da kararın parçası. AOF everysec ile çalışıyorsanız çökme anında bir saniyelik iş kaybı ihtimalini kabul etmişsiniz demektir. Bu kabul edilebilir olabilir, ama bilinçli olmalı.

Gerçek mesaj kuyruğu ne zaman gerekir

RabbitMQ, SQS, NATS JetStream ya da Kafka'ya geçmenizi gerektiren şey hacim değil, topoloji. Şu üç ihtiyaçtan biri varsa ayrı altyapı mantıklı:

Fan-out. Bir olayı birden fazla bağımsız tüketicinin, biri diğerinin ilerlemesini bilmeden işlemesi gerekiyorsa. Bunu Postgres'te taklit etmek her tüketici için ayrı offset yönetmek demek.

Uygulamalar arası sınır. Kuyruğun iki ucunda farklı takımların, farklı dillerde yazılmış ve farklı sürüm takvimine sahip servisleri varsa, paylaşılan bir veritabanı tablosu kötü bir sözleşmedir.

Yeniden oynatma. Son 7 günün olaylarını yeni bir tüketiciye baştan oynatmak istiyorsanız, aradığınız şey kuyruk değil log; Kafka ya da JetStream bu işi yapar, kuyruk yapmaz.

Bunların hiçbiri yoksa, ayrı bir broker eklemek size çalıştıracak bir servis, izlenecek bir metrik seti ve öğrenilecek bir hata modeli daha getirir.

Pratik karar

Tek uygulama, saniyede birkaç yüz işin altı, kaybı önemli işler: Postgres. Kuyruğu veriyle aynı transaction'da tutabilmek tek başına yeterli gerekçe.

Yüksek hacimli, kısa ve kaybı tolere edilebilir işler (önbellek, sayaç, bildirim tetikleme): Redis Streams. Ama XAUTOCLAIM döngüsünü yazın.

Birden çok servis, fan-out ya da yeniden oynatma: broker kurun.

Hangisini seçerseniz seçin, işleyicinizi idempotent yazın. Kuyruk teknolojisi değiştirilebilir; iki kez çalışan bir para transferi geri alınamaz.