Competing Consumers: İş Yükü Paylaşımı

Aynı kuyruğu dinleyen birden çok consumer ile iş yükünü dağıtma deseni, ölçekleme ve mesaj sırası garantisi gibi konular ele alınır.

Competing Consumers: İş Yükü Paylaşımı

Competing Consumers Pattern: Yüksek Yük Altında İş Yükünü Paylaştırma

Dağıtık sistemlerde, bir uygulama tek başına gelen tüm mesajları işleyemez hale geldiğinde, iş yükünü birden fazla tüketici (consumer) arasında paylaştırmak gerekir. Competing Consumers (Rakip Tüketiciler) deseni, aynı kuyruktan (queue) veya topic'ten gelen mesajları, birden fazla tüketici instance'ının eşzamanlı olarak tüketmesini sağlayan bir ölçeklenebilirlik modelidir. Bu sayede, mesaj işleme hızı artar ve yüksek trafik altında sistemin dayanıklılığı (resilience) korunur.

Bu yazıda, Competing Consumers deseninin nasıl çalıştığını, RabbitMQ, Azure Service Bus ve Kafka'daki uygulamalarını, mesaj sırası (ordering) ve partition key kavramlarını, .NET ve MassTransit ile nasıl yapılandırılacağını detaylıca ele alacağız.


1. Competing Consumers Deseni Nedir?

Temel Fikir: Bir kuyrukta bekleyen mesajlar, o kuyruğu dinleyen birden fazla tüketici (consumer) tarafından eşzamanlı olarak işlenir. Her mesaj, tüketicilerden yalnızca birine gönderilir (tek bir kez işlenir). Bu, iş yükünün tüketiciler arasında dengeli bir şekilde dağıtılmasını sağlar.

Klasik Senaryo:

  • Bir sipariş kuyruğu (order-queue) 10.000 sipariş mesajı içeriyor.

  • Bu kuyruğu dinleyen 5 adet OrderConsumer instance'ı çalışıyor.

  • Her consumer, kuyruktan mesajları alır, işler (siparişi veritabanına kaydeder, ödeme işlemini başlatır) ve onaylar (ack).

  • Kuyruk boşalana kadar bu şekilde devam eder.

Bu desen, iş yükünü yatayda ölçeklendirmenin (horizontal scaling) en yaygın yoludur.


2. Nasıl Çalışır? (Farklı Broker'larda)

A. RabbitMQ (Competing Consumers)
RabbitMQ'da birden fazla consumer aynı kuyruğa bağlanabilir. RabbitMQ, mesajları tüketicilere round-robin (sırayla) dağıtır. Varsayılan olarak, bir mesaj bir tüketiciye gönderilir ve onaylanana (ack) kadar kuyrukta kalmaz.

csharp

// RabbitMQ'da 3 consumer aynı kuyruğu dinliyor
channel.BasicConsume(queue: "order-queue",
                     autoAck: false,
                     consumer: consumer1);

channel.BasicConsume(queue: "order-queue",
                     autoAck: false,
                     consumer: consumer2);

channel.BasicConsume(queue: "order-queue",
                     autoAck: false,
                     consumer: consumer3);

Önemli: RabbitMQ, mesaj sırasını (order) garanti etmez. Consumer'lar farklı hızlarda çalışabilir, bu nedenle mesajlar işlenme sırasına göre değil, tüketilme sırasına göre işlenir.

B. Azure Service Bus (Competing Consumers)
Azure Service Bus, varsayılan olarak bir kuyruğu birden fazla consumer'ın dinlemesine izin verir. Mesajlar, PeekLock modunda tüketicilere dağıtılır. Her mesajın bir DeliveryCount'u vardır; eğer bir consumer mesajı işleyemez ve Abandon ederse, mesaj tekrar kuyruğa döner ve başka bir consumer tarafından alınabilir.

csharp

// Azure Service Bus'ta birden fazla consumer
var receiver1 = client.CreateReceiver(queueName);
var receiver2 = client.CreateReceiver(queueName);

C. Kafka (Consumer Groups)
Kafka'da Consumer Group kavramı, Competing Consumers deseninin ta kendisidir. Aynı group.id'ye sahip tüketiciler, bir topic'in partition'larını paylaşırlar. Her partition, aynı anda yalnızca bir tüketici tarafından okunabilir. Kafka, mesaj sırasını partition bazında garanti eder.

csharp

// Kafka'da aynı group.id ile 3 consumer
var config = new ConsumerConfig { GroupId = "order-group", BootstrapServers = "..." };
var consumer1 = new ConsumerBuilder<string, string>(config).Build();
var consumer2 = new ConsumerBuilder<string, string>(config).Build();
var consumer3 = new ConsumerBuilder<string, string>(config).Build();

3. Ölçekleme (Scaling) Stratejileri

A. Yatay Ölçekleme (Horizontal Scaling)
En yaygın yöntemdir. Tüketici sayısını artırarak işlem kapasitesini yükseltirsiniz.

  • RabbitMQ: Consumer sayısını artırın. RabbitMQ, mesajları otomatik olarak tüketicilere dağıtır.

  • Azure Service Bus: Consumer sayısını artırın. Service Bus, mesajları tüketicilere dengeli dağıtır.

  • Kafka: Consumer group içindeki tüketici sayısını, partition sayısına kadar artırabilirsiniz. Partition sayısından fazla tüketici olursa, fazla tüketiciler boşta kalır.

B. Dikey Ölçekleme (Vertical Scaling)
Tüketici instance'larını daha güçlü donanımlarda çalıştırmak (daha fazla CPU, RAM). Ancak, bu yöntem genellikle daha pahalıdır ve yatay ölçekleme kadar esnek değildir.

C. Autoscaling (Otomatik Ölçeklendirme)
Kuyruk uzunluğuna (queue length) göre tüketici sayısını otomatik olarak artırıp azaltmak.

  • Azure Service Bus: Kuyruk uzunluğuna göre tüketici instance'larının sayısını ayarlamak için Azure Functions veya Kubernetes HPA (Horizontal Pod Autoscaler) kullanılabilir.

  • Kafka: Kafka, consumer group içindeki tüketici sayısını dinamik olarak ayarlamak için Kafka Consumer Group Rebalance mekanizmasına sahiptir.


4. Mesaj Sırası (Ordering) Garantisi

Competing Consumers deseninin en büyük zorluklarından biri mesaj sırasının (ordering) korunmasıdır.

  • RabbitMQ / Azure Service Bus: Birden fazla consumer aynı kuyruktan okuduğunda, mesajların işlenme sırası garanti edilmez. Consumer'lar farklı hızlarda çalışabilir, bu nedenle mesajlar işlenme sırasına göre değil, tüketilme sırasına göre işlenir.

  • Kafka: Kafka, partition bazında sıralama garantisi verir. Aynı partition'daki mesajlar, işlenme sırasına göre tüketilir. Bu nedenle, sıralama önemliyse, mesajları aynı anahtarla (key) aynı partition'a göndermeniz gerekir.

Çözümler:

  1. Partition Key / Message Key: Kafka'da mesajları aynı anahtarla (ör. OrderId) göndererek aynı partition'a gitmesini sağlayın. Bu sayede aynı siparişe ait tüm mesajlar sırayla işlenir.

  2. Sıralama Numarası (Sequence Number): Her mesaja bir sıralama numarası ekleyin ve tüketicide bu numaraya göre sıralamayı kontrol edin.

  3. Tek Bir Consumer: Sıralama kesinlikle korunmalıysa, tek bir consumer kullanın. Ancak bu, ölçeklenebilirliği sınırlar.


5. .NET / MassTransit ile Competing Consumers

MassTransit, Competing Consumers desenini destekler. Birden fazla consumer instance'ı aynı endpoint'i (kuyruk) dinleyebilir.

A. RabbitMQ ile

csharp

builder.Services.AddMassTransit(x =>
{
    x.AddConsumer<OrderConsumer>(typeof(OrderConsumerDefinition));
    
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("localhost", "/", h => { ... });

        cfg.ReceiveEndpoint("order-queue", e =>
        {
            // Max Concurrent Consumers: Aynı anda kaç mesaj işlenecek
            e.PrefetchCount = 20; // Her consumer'a 20 mesaj önceden getir
            e.ConcurrentMessageLimit = 10; // Her consumer aynı anda 10 mesaj işleyebilir
            
            e.ConfigureConsumer<OrderConsumer>(context);
        });
    });
});

B. Kafka ile

csharp

builder.Services.AddMassTransit(x =>
{
    x.AddRider(rider =>
    {
        rider.AddConsumer<OrderConsumer>();
        
        rider.UsingKafka((context, kcfg) =>
        {
            kcfg.Host("localhost:9092");
            
            // Consumer Group ile birden fazla consumer aynı topic'i dinler
            kcfg.TopicEndpoint<string, string>("order-topic", "order-group", e =>
            {
                e.ConcurrentMessageLimit = 10; // Aynı anda 10 mesaj
                e.ConfigureConsumer<OrderConsumer>(context);
            });
        });
    });
});

MassTransit'de ConcurrentMessageLimit ve PrefetchCount ayarları, tüketici performansını optimize etmek için kritik öneme sahiptir.


6. Ölçekleme İpuçları ve Riskler

İpucu / Risk Açıklama / Çözüm
Prefetch Count RabbitMQ'da PrefetchCount, her consumer'a önceden kaç mesaj gönderileceğini belirler. Çok yüksek değer, consumer'ların aşırı yüklenmesine neden olabilir. Çok düşük değer, ağ gecikmesini artırır.
Concurrent Message Limit Her consumer'ın aynı anda işleyebileceği maksimum mesaj sayısı. Bu, consumer'ın thread havuzunu aşmaması için önemlidir.
Idempotency (Yinelenebilirlik) Competing Consumers deseninde, aynı mesajın farklı consumer'lar tarafından işlenme riski vardır. Tüketicilerin idempotent olması şarttır.
Poison Message (Zehirli Mesaj) Hatalı bir mesaj, tüm consumer'ları tıkayabilir. Bu durumda Dead Letter Queue (DLQ) kullanarak mesajı izole edin.
Kafka Partition Sayısı Kafka'da partition sayısı, aynı anda çalışabilecek maksimum consumer sayısını belirler. Partition sayısı, tüketici sayısından az olmamalıdır.
Monitoring (İzleme) Kuyruk uzunluğunu, tüketici gecikmesini (lag) ve hata oranlarını sürekli izleyin.

7. Sık Yapılan Hatalar

Hata Çözüm
Çok az consumer (darboğaz) Consumer sayısını, kuyruk uzunluğuna göre artırın.
Çok fazla consumer (gereksiz maliyet) Kuyruk boşsa, consumer sayısını azaltın.
Sıralama garantisi beklentisi Sıralama önemliyse Kafka partition key veya sequence number kullanın.
İdempotency ihmal etmek Tüketicileri idempotent yapın, aksi halde aynı mesaj iki kez işlenir.
DLQ (Dead Letter Queue) kullanmamak İşlenemeyen mesajlar kuyruğu tıkar, DLQ kullanarak izole edin.

Sonuç:

Competing Consumers deseni, dağıtık sistemlerde iş yükünü yatay olarak ölçeklendirmenin en etkili ve en yaygın yoludur. RabbitMQ, Azure Service Bus ve Kafka gibi farklı mesajlaşma sistemlerinde farklı şekillerde uygulanır, ancak temel prensip aynıdır: birden fazla tüketici aynı kuyruğu veya topic'i dinler ve mesajlar bu tüketiciler arasında dengeli bir şekilde dağıtılır.

MassTransit gibi kütüphaneler, bu deseni uygulamayı oldukça kolaylaştırır. Ancak, mesaj sırası (ordering) garantisi, idempotency ve poison message yönetimi gibi konulara dikkat edilmelidir. Doğru yapılandırıldığında, Competing Consumers deseni, sisteminizin yüksek yük altında bile kararlı ve hızlı çalışmasını sağlar.

Tüm yazılar