Yazılım Mimarileri ve Tasarım Desenleri Üzerine

YARP Load Balancing İle Yatay Ölçeklendirme(Horizontally Scaling-Scaling Out)

Load Balancing(Yük Dengeleme) ve Load Balancer(Yük Dengeleyici) Nedir?

Merhaba,

Biliyorsunuz ki, bir yazılımdan beklenen mühim davranışlardan biri sürdürülebilirliktir. Tek bir sunucuda çalıştırılan yazılımlar artan sayıda kullanıcılara hizmet verirken kaynakların yetenek sınırlarına ulaşıldığında istemsizce performans düşmesi, yavaş yanıt süreleri, trafikte dalgalanmalar ve hatta tamamen kesinti durumları söz konusu olabilmektedir. İşte bu durumlara istinaden load balancing davranışı ile uygulamalar ölçeklendirilerek bu zorlukların üstesinden gelinebilmektedir. Bizler bu içeriğimizde YARP(Yet Another Reverse Proxy) kütüphanesi ile load balancing davranışının nasıl sağlanacağını ve bunun yanında yük testi aracı olan K6’nın nasıl kullanılacağını inceleyecek ve bu temel hat üzerinden genel anlamda konuyu detaylandırıyor olacağız. O halde buyurun başlayalım…

Öncelikle yazılımdaki ölçeklendirmenin temellerini ele alarak, yani ölçeklendirme türlerini hatırlayarak başlayalım. Malumunuz, ölçeklendirmede Scale-Up ve Scale-Out olmak üzere iki yaklaşım söz konusudur. Kısaca bu yaklaşımları izah etmemiz gerekirse eğer; Scale-Up, donanımsal kaynak artırımıyken Scale-Out ise sistemdeki sunucu sayısının niceliksel olarak artırılmasıdır.Konuya dair daha teferruatlı bilgiyi önceden klavyeye almış olduğumuz Load Balancing(Yük Dengeleme) ve Load Balancer(Yük Dengeleyici) Nedir? başlıklı makalemizden inceleyebilirsiniz.
YARP kütüphanesi ile Scale-Out yaklaşımında ölçeklendirmeyi gayet efektif bir şekilde uygulayabilmekteyiz. Bunun için Yarp.ReverseProxy kütüphanesi eşliğinde önceki YARP İle Microservice’ler de API Gateway Implementasyonu başlıklı makalemizde anlatılan temel yapılandırmaların sağlanması gerekmektedir.

Ayrıca load balancing için aşağıdaki gibi bir yapılandırma yeterlidir.

appsettings.json yapılandırması;

{
  .
  .
  .
  "ReverseProxy": {
    "Routes": {
      "API1-Route": {
        "ClusterId": "API1-Cluster",
        "Match": {
          "Path": "{**catch-all}"
        },
        "Transforms": [
          {
            "PathPattern": "{**catch-all}"
          }
        ]
      }
    },
    "Clusters": {
      "API1-Cluster": {
        "LoadBalancingPolicy": "RoundRobin",
        "Destinations": {
          "destination1": {
            "Address": "https://localhost:7177"
          },
          "destination2": {
            "Address": "https://localhost:7178"
          },
          "destination3": {
            "Address": "https://localhost:7179"
          }
        }
      }
    }
  }
}

In-Memory yapılandırması;

.
.
.
builder.Services.AddReverseProxy()
    .LoadFromMemory(new List<RouteConfig>()
    {
        new()
        {
            RouteId = "API1-Route",
            ClusterId = "API1-Cluster",
            Match = new(){ Path = "{**catch-all}" },
            Transforms = new List<Dictionary<string, string>>()
            {
                new()
                {
                    { "PathPattern", "{**catch-all}" }
                }
            }
        }
    },
    new List<ClusterConfig>
    {
        new()
        {
            ClusterId = "API1-Cluster",
            LoadBalancingPolicy = "RoundRobin",
            Destinations = new Dictionary<string, DestinationConfig>()
            {
                { "destination1", new(){  Address = "https://localhost:7177" } },
                { "destination2", new(){  Address = "https://localhost:7178" } },
                { "destination3", new(){  Address = "https://localhost:7179" } },
            }
        }
    });
.
.
.

Şimdi burada yapılan konfigürasyonun izahına gelirsek eğer API1-Cluster isimli cluster’da üç adet destination tanımlanmıştır. İşte bu destination’larda ki adreslere LoadBalancingPolicy özelliğinde belirtilen RoundRobin algoritmasında load balancing davranışı uygulanacaktır.

Yukarıdaki satırlarda referans edilen(bknz: Load Balancing(Yük Dengeleme) ve Load Balancer(Yük Dengeleyici) Nedir?) makalemizden de biliyorsunuz ki load balancing’in request’leri dağıtabilme algoritması birden fazladır. YARP’da, bu algoritmaların birçoğu ortak olsa da aşağıdaki gibi kendine özel olarak davranış sergileyenleri de mevcuttur;

Test Edelim

Şimdi yaptığımız bu çalışmayı test edebilmek için öncelikle local’de bir API uygulamasını farklı instance’larla ayağa kaldırarak başlayalım. Bunun için dileyenler Docker’dan istifade edebilirler. Lakin ben, farklı port’lara göre ayarlanmış uygun profiller oluşturarak uygulamayı terminal üzerinden dotnet cli aracılığıyla ayağa kaldırmaya çalışacağım. Dolayısıyla oluşturulan herhangi bir Asp.NET Core uygulamasının ‘launchSettings.json’ dosyasında aşağıdaki gibi profilleri kullanabiliriz;

{
  .
  .
  .
  "profiles": {
    .
    .
    .
    "https1": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "applicationUrl": "https://localhost:7177;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "https2": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "applicationUrl": "https://localhost:7178;http://localhost:5001",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "https3": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "applicationUrl": "https://localhost:7179;http://localhost:5002",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    .
    .
    .
  }
}

Buradaki adreslere dikkat ederseniz load balancing yapılandırmasında kullandığımız adreslerle birebir uyuşmaktadırlar. Uygulamanın içeriği için de aşağıdaki gibi bir çalışma kâfidir diyebiliriz;

internal class Program
{
    private static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        var app = builder.Build();

        app.MapGet("/", () => args[0]);

        app.Run();
    }
}

Evet, burada da görüldüğü üzere uygulama ayağa kaldırılırken ‘Main’ metodunda aldığı parametre değerini direkt olarak ‘/’ endpoint’inde geriye döndürmektedir. Haliyle bu değeri dotnet cli talimatı sürecinde vermemiz gerekmektedir. Dolayısıyla bu uygulamanın yukarıda oluşturulan profiller ile birlikte aşağıdaki talimatlar eşliğinde üç farklı instance’ını ayağa kaldıralım;

dotnet run '1. server' --launch-profile https1
dotnet run '2. server' --launch-profile https2
dotnet run '3. server' --launch-profile https3

Görüldüğü üzere her bir talimat farklı bir ‘Main’ değeri ile belirtilen profil ayarlarında uygulama instance’ını ayağa kaldırmaktadır.

Artık bizler direkt YARP ile geliştirdiğimiz load balancer uygulamasını çalıştırarak yukarıdaki konfigürasyona göre RoundRobin algoritmasında gelen istekleri bu instance’lara dağıtabiliriz.

K6 İle Performans Testi Gerçekleştirelim

Bizler, yazılım süreçlerinde yapılandırdığımız load balancing davranışlarını test edebilmek için yukarıdaki gibi manuel ve basit seviyeli bir süreçten ziyade K6 gibi modern, geliştirici dostu ve kompleks test araçlarından istifade etmeyi tercih ederiz. K6, web uygulamalarının performans testlerinde kullanılan open source bir test aracıdır. Basit ve esnek bir kullanıcı arayüzüne sahip olsa da karmaşık senaryoları simüle edebilmek için kullanılabilmektedir. Bu araç sayesinde uygulamamızdaki kullanıcı trafiğini belirlediğimiz maksimum şartlara göre simüle edecek ve uygulamanın bu şartlardaki davranışlarını ölçümleyerek, değerlendirmede bulunacağız.

Tabi öncelikle K6’nın kurulumunu gerçekleştirmek gerekmektedir. K6’yı kurabilmek için birkaç yöntem olsa da bizler Windows Package Manager üzerinden kurulumu gerçekleştireceğiz. Bunun için PowerShell üzerinden
winget install k6 --source winget talimatının verilmesi yeterlidir.Bu kurulumdan sonra herhangi bir terminal ya da PowerShell ekranında k6 komutunu yazdığımızda bir sonuç alamıyorsanız eğer yükleyici üzerinden de K6’yı kurmayı deneyebilirsiniz.

Kurulumdan sonra aşağıdaki gibi basit bir K6 script’i ile yük testini gerçekleştirebiliriz;

import http from 'k6/http';

export const options = {
    stages: [
        { duration: '10s', target: 20 }, // İlk 10 saniye boyunca 20 istek
        { duration: '10s', target: 50 }, // Sonraki 10 saniye boyunca 50 istek
        { duration: '1m5s', target: 1000 }, // 1 dakika 5 saniye boyunca 1000 istek
    ],
};

export default function () {

    // GET isteği yapalım
    const response = http.get('https://localhost:7180');

    // Yanıtı kontrol edelim
    //if (response.status === 200) {
    //    console.log(`İstek başarılı👍 Yanıt:`, response.body);
    //} else {
    //    console.log('İstek başarısız👎 Durum kodu:', response.status);
    //}
}

Bu script’i k6 run --out json=result.json .\k6-test.js talimatıyla çalıştırdığımızda aşağıdaki ekran görüntüsünde olduğu gibi test süreci başlayacak ve netice raporlanacaktır. Tabi burada test sürecinde örnekte de görüldüğü üzere console’a çıktılarda bulunabilir ya da sadece testin raporlanmasını bekleyebiliriz.Evet, yapılan test neticesinde elde edilen sonuçları incelersek eğer tarafımıza aşağıdaki gibi bir rapor sunulmuştur.Bu raporda bizler için önemli metrikleri değerlendirirsek eğer;

şeklinde bir yorumda bulunulabilir.

Nihai olarak;
Bu içeriğimizde YARP ile web uygulamalarında performans artışı ve etkili bir ölçeklenebilirlik sağlayacak olan load balancing davranışını incelemiş ve özelliklerini değerlendirmiş bulunuyoruz.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek çalışmaya aşağıdaki github adresinden erişebilirsiniz.
https://github.com/gncyyldz/Load_Balancing_Scale_Out_With_YARP

Mobil sürümden çık