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/
Great article thanks for sharing.
Teşekkürler makale için
Teşekkürler makale için