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

Microservice – Saga – Events/Choreography Implemantasyonu İle Transaction Yönetimi

Microservice Mimarilerde Saga Pattern İle Transaction Yönetimi

Merhaba,

Bir önceki Microservice Mimarilerde Saga Pattern İle Transaction Yönetimi başlıklı makalemizde Saga pattern üzerine detaylıca teorik incelemede bulunmuştuk. Bu içeriğimizde ise Saga pattern’ını Events/Choreography implemantasyonu çerçevesinde ele alacak ve pratikte nasıl bir inşanın söz konusu olduğunu hep beraber adım adım inceliyor olacağız.

Senaryo

İncelememizde senaryo olarak basit bir e-ticaret sürecini ele alıyor olacağız. Bu süreçte Order.API, Stock.API ve Payment.API olmak üzere üç temel microservice bizlere eşlik ediyor olacak. İşlevsel olarak kullanıcıdan gelen sipariş talebi üzerine Order.API bu talebi karşılayacak ve state’i Suspend olan bir sipariş oluşturulacaktır. Ardından sipariş edilen ürünlerin stok verilerinin düşürülmesi için Stock.API servisinde gerekli güncelleme gerçekleştirilecek ve bu işlem başarılı olduktan sonra nihai olarak ödeme işlemi için devreye Payment.API girecek ve gerekli tahsilat gerçekleştirilecektir. Eğer süreçte herhangi bir hata meydana gelirse tüm transactionları compensable transaction ile telafi edeceğiz.

Bu servislerin kendi aralarında haberleşebilmeleri için publish edecekleri ve subscribe olacakları event şeması aşağıdaki gibi olacaktır.

Service Publish Subscribe
Order Service OrderCreatedEvent : Sipariş oluşturulduğunu ifade eden event’tir. Stock Service tarafından dinlenmektedir. PaymentCompletedEvent
PaymentFailedEvent
StockNotReservedEvent
Stock Service StockReservedEvent : Stok güncellemesi başarıyla gerçekleştirildiğinde yayılan event’tir. Payment Service tarafından dinlenmektedir.
StockNotReservedEvent : Stok güncellemesinde bir hata ya da tutarsızlık meydana geldiğinde yayılan event’tir. Order Service tarafından dinlenmektedir ve fırlatıldığı taktirde sipariş Fail durumuna çekilir.
OrderCreatedEvent
PaymentFailedEvent
Payment Service PaymentCompletedEvent : Ödeme işleminin başarıyla gerçekleştirildiğini ifade eden event’tir. Order Service tarafından dinlenmektedir ve sipariş durumunun Completed olmasını sağlar.
PaymentFailEvent : Ödeme sürecinde bir hata meydana geldiğini ifade eden event’tir. Order Service ve Stock Service tarafından dinlenmektedir. Sipariş durumunun Fail olmasını sağlar.
StockReservedEvent

İşte böyle 🙂 Pratik yapacağımız senaryo ve event şemasını masaya yatırdıktan sonra sıra servislerimizi oluşturmaya gelmiştir.

Servislerin Oluşturulması

Yukarıdaki paragraflarda bahsedildiği gibi Order.API, Stock.API ve Payment.API olmak üzere üç adet servis oluşturacağız. Bunu herhangi bir yöntemle gerçekleştirebileceğiniz gibi aşağıdaki dotnet cli komutlarını kullanarak da gerçekleştirebilirsiniz.

(Service) dotnet new webapi --name Order.API
(Service) dotnet new webapi --name Stock.API
(Service) dotnet new webapi --name Payment.API
(Class Library) dotnet new classlib --name Shared : Servisler arası mesajlaşma için event’leri vs. bu class library’de oluşturacağız.

Servislere MassTransit Kurulumu ve Temel Konfigürasyonlar

Saga pattern ile servisler arası distributed transaction’ı sağlayabilmek için asenkron bir iletişim modeli kullanacağımızı makalemizin ilk paragrafında referans edilen içeriğimizde konuşmuştuk. Haliyle bizler asenkron iletişim için RabbitMQ sistemini tercih edeceğiz. Lakin uygulama içerisinde salt bir şekilde RabbitMQ’yu kodlamak bizler için bir nebze fazla maliyetli olacağından dolayı hızlı hareket edebilmek ve güvenilir bir şekilde kuyruklara mesajlarımızı iletebilmek için Enterprise Service Bus(ESB) olan MassTransit kütüphanesini kullanacağız.

Dolayısıyla tüm servislerimize MassTransit’i aşağıdaki dotnet cli komutları eşliğinde yükleyelim.

Ardından tüm servislerin ‘Startup.cs’ dosyasında aşağıdaki konfigürasyonu sağlayarak, ilgili kütüphaneyi ve RabbitMQ servislerini uygulamalara entegre edelim.

    public class Startup
    {
        .
        .
        .
        public void ConfigureServices(IServiceCollection services)
        {
            .
            .
            .
            services.AddMassTransit(configure =>
            {
                configure.UsingRabbitMq((context, configurator) =>
                {
                    configurator.Host(Configuration.GetConnectionString("RabbitMQ"));
                });
            });

            services.AddMassTransitHostedService();
            .
            .
            .
        }
        .
        .
        .
    }

Dikkat ederseniz tüm servislerde bağlantı kurulacak RabbitMQ sunucusunun bilgileri yapılandırma noktasından talep edilmektedir. Bu bilgiyi ister environment‘tan isterseniz de ‘appsettings.json’ dosyasından verebilirsiniz. Misal bizler şimdilik tüm servislerimizin ‘appsettings.json’ dosyasında sunucu bilgilerini aşağıdaki gibi tutalım.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RabbitMQ": "amqps://pg*******w6iBqJ92iNG_0@elk.r******p.com/pgjnreme"
  }
}

Bizler örneklendirmemizde RabbitMQ’yu cloud servis olarak kullanacağımızdan dolayı gerekli bağlantıyı cloudamqp.com adresindeki instance’a uygun olarak yazmış bulunmaktayız. Eğer ki, RabbitMQ instance’ının cloud ortamda nasıl ayağa kaldırıldığına dair herhangi bir fikriniz yoksa RabbitMQ – Cloud Ortamda(CloudAMQP) Kurulumu başlıklı makaleme göz atmanızı tavsiye ederim.

Velhasıl, tüm servislerde yapılan bu konfigürasyon neticesinde artık microservice yapılanmamızın tüm parçaları asenkron bir iletişim kurabilir niteliğe kavuşmuş demektir. Artık servislerimizi tek tek amaca uygun bir şekilde geliştirebilir ve Saga pattern’ın Choreography implemantasyonunu kullanarak distributed transaction’ı sağlayabiliriz. Hadi buyrun başlayalım.

Servislerin Geliştirilmesi

Şimdi yukarılarda bahsedilen senaryoya uygun bir şekilde e-ticaret yapılanmamızı adım adım kodlamaya geçebiliriz. Burada makalenin hizasına göre kodlanan herhangi bir servis süreçte tekrardan düzeltmek yahut yeni bir event’e göre tekrar kodlanmak gerektirebileceğinden dolayı sayısal olarak başlıklandırılmış adımlarla parça parça ilerleyeceğiz. Dolayısıyla iyi takip etmenizi, yapılan işlemleri yakalayabilmek ve bütünsel olarak yorumlayabilmek için gerekirse sizlerin de bu pratiğe eşlik etmenizi tavsiye ederim. Hadi başlayalım…

Test Edelim

Evet, artık yaptığımız microservice çalışmasını ve Saga pattern ile distributed transaction uygulamasını baştan ayağa test edebiliriz. Bunun için öncelik olarak servislerin portlarını ve MongoDB’de bir kaç dummy data yapılanmasını ayarlamamız gerekmektedir.

Order.API https://localhost:5001
Stock.API https://localhost:5003
Payment.API https://localhost:5005

Stock.API‘ın ‘Program.cs’ dosyasına girelim ve aşağıdaki dummy dataları MongoDB’ye yükleyecek olan konfigürasyonu gerçekleştirelim.

    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using IServiceScope scope = host.Services.CreateScope();
            MongoDbService mongoDbService = scope.ServiceProvider.GetRequiredService<MongoDbService>();
            if (!mongoDbService.GetCollection<Models.Stock>().FindSync(x => true).Any())
            {
                mongoDbService.GetCollection<Stock.API.Models.Stock>().InsertOne(new()
                {
                    ProductId = 21,
                    Count = 200
                });
                mongoDbService.GetCollection<Stock.API.Models.Stock>().InsertOne(new()
                {
                    ProductId = 22,
                    Count = 100
                });
                mongoDbService.GetCollection<Stock.API.Models.Stock>().InsertOne(new()
                {
                    ProductId = 23,
                    Count = 50
                });
                mongoDbService.GetCollection<Stock.API.Models.Stock>().InsertOne(new()
                {
                    ProductId = 24,
                    Count = 10
                });
                mongoDbService.GetCollection<Stock.API.Models.Stock>().InsertOne(new()
                {
                    ProductId = 25,
                    Count = 30
                });
            }

            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }

Artık bu ayarlardan sonra uygulamamız teste hazır. Şimdi testimizi gerçekleştirebiliriz.
Görüldüğü üzere MonboDB’ye girilen dummy datalara uygun verilerde bir sipariş post edildiği taktirde başarılı bir sipariş söz konusu olabilmektedir.Veritabanına bakıldığında da verilerin stok miktarlarının tutarlı bir şekilde düştüğünü gözlemlemekteyiz.

Benzer mantıkla stok miktarı tutarsız olan ürünler sipariş edildiğindesipariş durumu Fail olarak düzenlenmektedir. Ve hatta Order.API‘ın console’una göz atarsak bu durumun nedeni mesaj olarak karşımıza çıkmaktadır.MongoDB’ye göz atarsak eğer verilerin en sonuncu halinde olduğunu görmekteyiz.

Ve son olarak Payment.API‘da farazi olarak yaptığımız ödeme işlemini ‘false’ yapıp başarısız duruma getirerek son durumu test edersek eğer;ekran görüntüsünde de görüldüğü üzere ‘Bakiye yetersiz!’ mesajıyla karşılaşılmakta ve stok bilgileri compensable transaction ile telafi edilerek geri alınmakta ve sipariş iptal edilmektedir.

Ayrıca bu servislerin RabbitMQ’da ki connection durumlarına da göz atarsak eğer şeklinde olduğunu gözlemlemekteyiz.

Nihai olarak;
Distributed transaction senaryolarında microservisler arasında tutarlılığı sağlayabilmek için Saga pattern’ın choreography implemantasyonunu adım adım hep beraber tecrübe etmiş olduk. Dikkat ederseniz eğer her bir aksiyona karşılık bir event yayınlanmakta ve bu event’e subscribe olan consumer’lar aracılığıyla ilgili servisin alacağı aksiyon belirlenmektedir. Haliyle bu yaklaşımda servis sayısı arttıkça ister istemez kuyrukların yönetimi ve koordinasyonu zorlaşacak ve bir yerden sonra gelişimsel açıdan son raddeye varılmış olunacaktır. Haliyle servis sayısı 2 ile 4 arasında adil bir sayıya tekabül eden çalışmalar da choreography implemantasyonunun tercih edilmesi makul görülürken daha fazla servisin söz konusu olduğu durumlarda bir sonraki makalemizde pratiksel olarak yine benzer bir senaryo üzerinden ele alacağımız Orchestrator implemantasyonunu uygulamamız daha ideal bir çözüm getirmiş olacaktır. O halde sizler bu makaleyi sindirirken, ben de biraz dinlendikten sonra diğer makaleyi klavyeye almaya başlıyor olacağım 🙂

Bu noktaya kadar üşenmeden okuyup, eşlik ettiğiniz için teşekkür ederim…..

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

Not : Örnek projeyi indirmek için buraya tıklayınız.

Exit mobile version