Kubernetes-based Event Driven Autoscaling with KEDA, RabbitMQ and .NET Core

Bildiğiniz gibi Microsoft, son dönemlerde open-source dünyası için çok fazla atılım ve yatırım yapmaktadır. Bu atılımlardan birtanesi ise Red Hat partnership’liği ile birlikte geliştirdikleri Kubernetes-based Event Driven Autoscaling yapabilmemizi sağlayan KEDA adında bir component.

KEDA‘nın duyurulmasından bu yana hemen kendi ortamlarımızda test etmeye ve kurcalamaya başladım. Bu makale ile ise KEDA hakkında edinebildiğim bilgileri paylaşmak istedim. Ayrıca .NET Core ve RabbitMQ kullanarak, event-driven bir şekilde sıfırdan n adet sayıya kadar uygulamamızı KEDA ile nasıl scale edebileceğimize bir bakacağız.

Makale için tatilin bitmesini bekleyemedim.

Kubernetes Horizontal Pod Autoscaler (HPA)

KEDA‘ya değinmeden önce, Kubernetes HPA‘i bir hatırlayalım. HPA, bildiğiniz gibi production-ready bir kubernetes ortamı için olmazsa olmaz otomatik bir pod scaler. HPA podlar’ı, gözlemlenen CPU utilization’ı veya custom metric’lere göre otomatik olarak horizontal bir şekilde scale etmektedir.

HPA‘nın basit olarak scaling flow’u aşağıdaki gibidir:

  • HPA default olarak 30 saniye bir interval ile metricleri devamlı gözlemlemektedir.
  • Gözlemleme sırasında ise daha önceden tanımlanan threshold değerleri aşılırsa, pod sayısını arttırmaya/azaltmaya başlamaktadır.

Biz bir çok uygulamalarımızda HPA‘i, CPU-based olarak yapılandırıyoruz. Örneğin bizim kullandığımız Helm chart içerisindeki basit bir HPA ayarı:

hpa:
 enabled: true
 minReplicas: 1
 maxReplicas: 3
 targetCPUUtilizationPercentage: 70

resources: 
 limits:
   cpu: 300m
   memory: 300Mi
 requests:
   cpu: 100m
   memory: 100Mi

Bu ayarlara göre eğer bir pod, %70 oranından fazla CPU consume etmeye başladığında, HPA imdadımıza yetişiyor.

NOT: Elbette bu işlemi sadece CPU-based değil, Prometheus gibi tool’lar kullanarak custom metric’lere göre gerçekleştirebilmekte mümkün.

HPA güzel bir enabler:

  • Performansa ihtiyaç duyduğumuz bazı veya beklenmedik zamanlarda, otomatik olarak performansı arttırabiliyoruz.
  • Hardware resource’larını ise gereksiz yere allocate etmiyoruz.

Peki KEDA?

KEDA ise Microsoft ve Red Hat partnership’liği ile geliştirilmiş (hala geliştirilmekte olan), event-driven autoscaling yapabilmemizi sağlayan native bir kubernetes component’idir.

KEDA ile bir container’ı, metric’lere göre sıfırdan istediğimiz adet instance’a kadar otomatik olarak scale edebiliriz. Buradaki güzel olan şey ise, bu metric’leri gözlemleyebilmek için KEDA‘nın herhangi bir dependency’si bulunmamaktadır. Mimarisi hakkında daha detaylı bilgiye ise, buradan erişebilirsiniz.

Metric’leri gözlemleyebileceği event-source’lar ise, aşağıdaki gibidir:

  • Kafka
  • RabbitMQ
  • Azure Storage
  • Azure Service Bus Queues and Topics

.NET Core ve RabbitMQ ile Scaling

Öncelikle KEDA‘nın kurulum işlemini, aşağıdaki gibi helm chart ile gerçekleştirelim.

helm repo add kedacore https://kedacore.azureedge.net/helm
helm repo update
helm install kedacore/keda-edge --devel --set logLevel=debug --namespace keda --name keda

NOT: İsterseniz GitHub üzerinden repository’i indirip, kendi image’inizi de oluşturabilirsiniz. Çünkü son yapılan değişiklikler, master image’e henüz push’lanmamış olabilir.

KEDA‘nın kurulumu ardından, aşağıdaki komut satırı ile “Todo.Contracts” adında bir class library oluşturalım.

dotnet new classlib -n Todo.Contracts

Bu library içerisinde, “Publisher” ve “Consumer” içerisinde share edeceğimiz event’i tanımlayacağız. Şimdi aşağıdaki gibi “TodoEvent” adında bir class tanımlayalım.

using System;

namespace Todo.Contracts
{
    public class TodoEvent
    {
        public string Message { get; set; }
    }
}

Ardından aşağıdaki komut satırı ile “Todo.Publisher” adında bir .NET Core console application oluşturalım ve “Todo.Contracts” projesini referans olarak ekleyelim.

Oluşturduktan sonra RabbitMQ üzerine event publish edebilmek için NuGet üzerinden projeye “MetroBus” paketini dahil edelim.

dotnet add package MetroBus

Şimdi “Program” class’ını aşağıdaki gibi kodlayalım.

using System;
using MetroBus;
using Todo.Contracts;

namespace Todo.Publisher
{
    class Program
    {
        static void Main(string[] args)
        {
            string rabbitMqUri = "rabbitmq://127.0.0.1:5672";
            string rabbitMqUserName = "user";
            string rabbitMqPassword = "123456";

            var bus = MetroBusInitializer.Instance
                            .UseRabbitMq(rabbitMqUri, rabbitMqUserName, rabbitMqPassword)
                            .InitializeEventProducer();

            int messageCount = int.Parse(Console.ReadLine());

            for (int i = 0; i < messageCount; i++)
            {
                bus.Publish(new TodoEvent
                {
                    Message = "Hello!"
                }).Wait();

                Console.WriteLine(i);
            }
        }
    }
}

Burada basitçe “MetroBus” paketini kullanarak, RabbitMQ üzerine bir “TodoEvent” publish ediyoruz.

Hızlıca bu event’i consume edecek olan projeyi oluşturalım. Bunun için “Todo.Consumer” adında yeni bir .NET Core console application’ı daha oluşturalım ve “Todo.Contracts” projesini referans olarak ekleyelim.

Daha sonra NuGet üzerinden “MetroBus” paketini de projeye dahil edelim. Consume işlemini gerçekleştirebilmek için, “Program” class’ını aşağıdaki gibi kodlayalım.

using System;
using MetroBus;

namespace Todo.Consumer
{
    class Program
    {
        static void Main(string[] args)
        {
            string rabbitMqUri = "rabbitmq://my-rabbit-rabbitmq.default.svc.cluster.local:5672";
            string rabbitMqUserName = "user";
            string rabbitMqPassword = "123456";
            string queue = "todo.queue";

            var bus = MetroBusInitializer.Instance
                            .UseRabbitMq(rabbitMqUri, rabbitMqUserName, rabbitMqPassword)
                                .SetPrefetchCount(1)
                                .RegisterConsumer<TodoConsumer>(queue)
                            .Build();

            bus.StartAsync().Wait();
        }
    }
}

Burada ise basit olarak, “TodoEvent” ine “todo.queue” üzerinden subscribe olacağımızı belirttik. Ardından KEDA ile scaling’i simulate edebilmek için ise, consumer’ın “prefetch” count’ını “1” olarak set ettik.

Consumer olarak ise, “TodoConsumer” class’ını register ettik. Son olarak aşağıdaki gibi “TodoConsumer” adında bir class oluşturalım.

using System.Threading.Tasks;
using MassTransit;
using Todo.Contracts;

namespace Todo.Consumer
{
    public class TodoConsumer : IConsumer<TodoEvent>
    {
        public async Task Consume(ConsumeContext<TodoEvent> context)
        {
            await Task.Delay(1000);
            
            await System.Console.Out.WriteLineAsync(context.Message.Message);
        }
    }
}

Bu class içerisinde ise event ile gelecek olan “Message” property’sini, console üzerine yazdıracağız.

Evet, dummy olarak pub/sub uygulamamız hazır. Şimdi sıra ise KEDA ile deployment yapmakta!

Öncelikle projelerin root’unda, “Todo.Consumer” projesi için aşağıdaki gibi bir Dockerfile oluşturalım.

#Build Stage
FROM microsoft/dotnet:2.2-sdk AS build-env

WORKDIR /workdir

COPY ./Todo.Contracts ./Todo.Contracts/
COPY ./Todo.Consumer ./Todo.Consumer/

RUN dotnet restore ./Todo.Consumer/Todo.Consumer.csproj
RUN dotnet publish ./Todo.Consumer/Todo.Consumer.csproj -c Release -o /publish

FROM microsoft/dotnet:2.2-aspnetcore-runtime
COPY --from=build-env /publish /publish
WORKDIR /publish
ENTRYPOINT ["dotnet", "Todo.Consumer.dll"]

Sonrasında ise KEDA ile birlikte gerçekleştireceğimiz deployment file’ını, aşağıdaki gibi oluşturalum.

todo.consomer.deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: todo-consumer
  namespace: default
  labels:
    app: todo-consumer
spec:
  selector:
    matchLabels:
      app: todo-consumer
  template:
    metadata:
      labels:
        app: todo-consumer
    spec:
      containers:
      - name: todo-consumer
        image: ggplayground.azurecr.io/todo-consumer:dev-04
        imagePullPolicy: Always
---
apiVersion: keda.k8s.io/v1alpha1
kind: ScaledObject
metadata:
  name: todo-consumer
  namespace: default
  labels:
    deploymentName: todo-consumer
spec:
  scaleTargetRef:
    deploymentName: todo-consumer
  pollingInterval: 5   # Optional. Default: 30 seconds
  cooldownPeriod: 30   # Optional. Default: 300 seconds
  maxReplicaCount: 10  # Optional. Default: 100
  triggers:
  - type: rabbitmq
    metadata:
      queueName: todo.queue
      host: 'amqp://user:123456@my-rabbit-rabbitmq.default.svc.cluster.local:5672'
      queueLength  : '5'
---

İlk kısm “Deployment” controller kısmı. Ben container registry olarak Azure Container Registry kullandığım için, consumer image’ini oraya push’ladım.

Asıl önemli kısım ise “ScaledObject” kısmı. Burada scale işleminin gerçekleşebilmesi için custom resource tanımlamalarını yapıyoruz.

NOT: “metadata” ve “scaleTargetRef” içerisindeki name değerlerinin, deployment ile aynı name’de olması gerekmektedir.

Message broker olarak RabbitMQ seçtiğimiz için ise, “triggers” altındaki “type” değerinin “rabbitmq” olması gerekmektedir. Event metric’lerinin KEDA tarafından gözlemlenebilmesi için ise, ilgili “queueName” ve “host” bilgilerini de set ettik. Ayrıca “queueLength” variable’ı ile de, HPA için bir nevi threshold değeri tanımlamış olduk.

Yani bu spec’lere göre KEDA, “5” saniyede bir “queueLength” değerinin “5” e ulaşıp ulaşmadığını kontrol edecektir ve buna göre scaling e karar verecektir. Scaling’e ihtiyaç olmadığında ise, “cooldownPeriod” süresini boyunca bekleyerek, deployment’ı tekrardan 0’a düşürecektir.

Özellikle performansa ihtiyaç duyduğumuz ve queue’daki event’ler birikmeye başladığında otomatik olarak consumer’ları scale edebilmek, cool bir hareket değil mi?

Şimdi aşağıdaki komut satırı ile oluşturduğumuz deployment file’ını kubernetes üzerinde apply edelim.

kubectl apply -f todo.consomer.deploy.yaml

Eğer deployment işlemi başarılı ise, HPA aşağıdaki gibi listeye gelecektir.

Artık test işlemine hazırız.

Demo’yu gerçekleştirebilmek için ben publisher’ı local’den çalıştıracağım ve “100” adet event’i, RabbitMQ üzerine publish edeceğim. Bakalım KEDA nasıl davranacak.

Oluşturmuş olduğumuz “ScaledObject” e göre, KEDA, consumer’ı “0” dan başlayıp maximum “10” pod’a kadar scale etmesi gerekiyor. Test için event’leri publish etmeye başlamadan önce, RabbitMQ UI üzerinden herhangi aktif bir consumer olmadığından emin olalım.

Gördüğümüz gibi henüz herhangi bir message ve herhangi bir consumer bulunmamaktadır. Şimdi “Todo.Publisher” projesini çalıştıralım ve “100” adet event publish etmesini sağlayalım. Ardından aşağıdaki komut satırı ile, “todo-consumer” ın deployment’larını izleyelim.

kubectl get deploy -w

Todo.Consumer” için available kısmına dikkat edersek, event’leri publish etmeye başladıktan sonra available consumer sayısı “0” dan başlayarak “4” e kadar scale up oldu. Event’ler consume edildikten sonra ise consumer sayısı tekrardan “0” a scale down oldu.

Sonuç

Artık günümüz çağında teknoloji kullanımının oldukça artması ile beraber, bu artışa ayak uydurabilmek için geliştiriyor olduğumuz uygulamaları da reactive manifesto‘nun söylediği gibi “Elastic” ve “Message-Driven” olarak geliştirmeliyiz. Böylece geliştiriyor olduğumuz uygulamalar, yüksek yükler karşısında responsive olabilirler.

Bu bağlamda KEDA, reactive bir sistem tasarlayabilmemiz için “Elastic” başlığını bizim için handle ediyor. Queue length’ine göre ilgili kaynağın increase veya decrease işlemini gerçekleştiriyor.

KEDA hala geliştirilmekte olan harika bir component. Eğer sizde katkıda bulunmak istiyorsanız, buradan yardım istenilen bazı başlıklara erişebilirsiniz.

Demo: https://github.com/GokGokalp/RabbitMQPubSubDemoWithKeda

References

https://github.com/kedacore/keda/wiki
https://cloudblogs.microsoft.com/opensource/2019/05/06/announcing-keda-kubernetes-event-driven-autoscaling-containers/

Gökhan Gökalp

View Comments

Recent Posts

Containerized Uygulamaların Supply Chain’ini Güvence Altına Alarak Güvenlik Risklerini Azaltma (Güvenlik Taraması, SBOM’lar, Artifact’lerin İmzalanması ve Doğrulanması) – Bölüm 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

8 ay ago

Identity & Access Management İşlemlerini Azure AD B2C ile .NET Ortamında Gerçekleştirmek

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 yıl ago

Azure Service Bus Kullanarak Microservice’lerde Event’ler Nasıl Sıralanır (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 yıl ago

.NET Microservice’lerinde Outbox Pattern’ı ile Eventual Consistency için Atomicity Sağlama

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 yıl ago

Dapr ve .NET Kullanarak Minimum Efor ile Microservice’ler Geliştirmek – 02 (Azure Container Apps)

{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…

2 yıl ago