Azure Üzerinde Redis Pub/Sub İle Microservis Mimarisinde Data Consistency Sağlamak

Selamlar,

Bu makalede, günlük hayatta daha çok Cache yönetimi için kullanılan Redis’in, Azure üzerinde bir başka özelliği olan Publisher/Subscriber platform’u sayesinde haberleşerek, Azure Function Microservice üzerinden değişen kur bilgilerini asenkron olarak yani Net Core WebApi servisinden bağımsız olarak nasıl MsSQL bir DB’de güncellediğini ve Redis Cache Memory üzerinde data tutarlılığını sağlandığını hep beraber inceleyeceğiz. Bu işlemleri yaparken, .Net Core 3.1 ile beraber kullanılan çeşitli kütüphanelerden ve katmanlı mimaride kullanılan bir kaç yöntemden bahsedeceğiz. Okurken, adım adım bir iş sürecinin halkalar halinde nasıl bir araya gelip birlikte çalıştığını inceleyeceğiz.

“Tutarlı olmak değişmez olmak değil, kararlı olmak inatçı olmak değildir, disiplinli olmak sert olmak değildir, kar yavaş ve devamlı yağarsa tutar.” ―Nevzat Tarhan

Azure Üzerinde Redis Cache Yaratma:

Öncelikle gelin Azure üzerinde Redis Cache yaratalım.

1-)Azure üzerinde “Cache Redis” şeklinde bir arama yapıldıktan sonra, gelen arama sonucundan aşağıdaki seçenek seçilir.

2-)Create buttonuna basıldıktan sonra, aşağıdaki gibi bir ekran ile karşılaşılır. Resource Group, Redis DNS adı, Lokasyonu ve cache type yani makina tipi bütçeye göre seçilir :)

3-) En son onay ekranı geçildikten sonra ilgili Redis, Azure üzerinde aşağıdaki gibi oluşturulur: Console buttonuna basılır ise Redis Console ekranına ulaşılır.
Not: Non-SSL port disabled iken default port 6380’dir. Ama ben bu şekilde ilgili Azure Redis’e uzaktan erişemediğim için Nob-SSL port(6379)’u enabled hale getirdim.

Aşağıdaki gibi Redis Console’a ping yazılınca, “PONG” cevabı alınıyor ise herşey yolunda demektir.

Azure üzerinde kurulu Redis Cache’e, kendi local makinanızda “Redis Desktop Manager” ile bağlanabilirsiniz. Redis Desktop Manager’ı buradaki linkden indirilebilirsiniz.

Redis Desktop Manager üzerinden, Azure Redis Cache’e bağlanılırken, aşağıdaki gibi bir ekran ile karşılaşılır.

.Net Core WebApi Servis Yaratma:

Şimdi gelin .Net Core 3.1 ile, güncel kur bilgilerini gösteren ve güncelleyen bir WebApi servisi yazalım.

Bu makalenin yazıldığı an itibari ile geçerli .Net Core (3.1.403) versiyonu aşağıdaki gibidir.

.Net Core ExchangeService WebApi servisi aşağıdaki gibi yaratılır.

.Net Core üzerinde Redis’in kullanılabilmesi için ServiceStack, kullanılabilecek kütüphanelerden biridir. İlgili kütüphane aşağıdaki komut satırı ile projeye eklenir.

Model klasörü oluşturulup, aşağıdaki gibi Exchange sınıfı yaratılır.

ExchangeService/Models/Exchange: Sayfaya basılacak Exchange(Kur) modeli.

Bu makalede ayrıca bir arayüz yazılmayacağı için, ilgili methodlara dışardan erişilebilmesi amacı ile Swagger .Net Core ortamına aşağıdaki komut ile eklenir. Siz isterseniz Postman de kullanabilirsiniz.

Swagger için, Actionların üstünde açıklama girilebilmesi için, aşağıdaki kütüphanenin eklenmesi gerekmektedir.

Sıra geldi, .Net Core Ortamında Swagger’ın “Startup.cs’de tanımlanmasına.

ExchangeService/Startup.cs/ConfigureServices(1):

  • “services.AddSwaggerGen(c =>” : Swagger .Net Core ortamında belli configurasyonlar ile tanımlanır.
  • “c.EnableAnnotations()” ile Actionlar üzerinde swagger açıklaması tanımlanabilir.
  • “c.SwaggerDoc(“CoreSwagger”, new OpenApiInfo” : CoreSwagger adı ile Başlık, versiyon, açıklama ve iletişim özellikleri ile service’e eklenir. Burada “CoreSwagger” tanımlaması çok önemlidir. Dinamik oluşacak döküman ismi burada tanımlanır.

ExchangeService/Startup.cs/Configure: 

  • app.UseSwagger(): .Net Core projesine swagger eklenmiştir.
  • .UseSwaggerUI(c => {c.SwaggerEndpoint(“/swagger/CoreSwagger/swagger.json”, “Swagger Test .Net Core”);}): Burada swagger için kullanılacak “json” dosyasının kaydedileceği yer tanımlanmaktadır.

NOT: “/CoreSwagger” olarak yazılan kısım ==> yukarıda ConfigureServices’de tanımlanan  “c.SwaggerDoc(“CoreSwagger“, new OpenApiInfo” ile aynı olmak zorundadır. Swagger’ın isim parametre olarak kullanılmaktadır.

ExchangeService/Controllers/ExchangeController.cs(1): Aşağıda görüldüğü gibi, kayıtlı kur listesi çekilecek method şimdilik tanımlanmıştır. Swagger açıklaması olarak ilgili text, [SwaggerOperation] attribute’ü ile tanımlanır.

Örnek ekran görüntüsü aşağıdaki gibidir.

“Yükü paylaşmanın en iyi yolu, organize olmaktır. Onu da yönetmek için bir orchestrator’a ihtiyaç vardır. Tek özelliği, herkesi ile ortak dilde buluşmasıdır.” ―

Azure Üzerinde SQL DB Yaratma:

Azure üzerinde Sql Database aşağıdaki gibi aranarak yaratılır:

Aşağıda görüldüğü gibi ilgili alanlar doldurulup, istenen donanım seçildikten sonra SqlDB Azure üzerinde yaratılır.

Azure üzerindeki SQL DB Connection aşağıdaki gibi ilgili alan tıklanarak alınır:

Exchange tablosu, aşağıdaki Sql Scriptin çalıştırılması ile oluşturulur.

Şimdi gelin, manuel birkaç kur datasını Insert edelim.

Project Entity:

Şimdi gelin önce DBContext katmanını yaratalım. Aşağıdaki komut ile yeni bir .Net Core Console Entity projesi oluşturulur.

Bu yazıda Database First kullanılarak, Azure üzerindeki Exchange Tablosu ve BlackJack DBContext(Database) projeye dahil edilir.

Sıra, var olan bir SQL DB’den, DBContext ve Model sınıflarını oluşturmaya geldi. Açılan Entity projesine, EntityFrameworkCore’a ait SqlServer, SqlServer.Design ve Tools kütüphaneleri aşağıdaki gibi eklenir.

Bu uygulamada, Azure üzerinde BlackJack Sql DB’ye bağlanılıp, BlackJackContext sınıfı ve Exchange Pocosu aşağıdaki komut ile otomatik olarak oluşturulur. .Net Core projesinde bu işlemin yapılabileceği bir arayüz malesef henüz yoktur. Belki 3th party toollar olabilir. Ama bu makalede, aşağıdaki komut satırılı ile yetinilmiştir. İlgili komut satırı çalıştırıldıktan sonra, proje içinde “Models/DB” şeklinde klasörler oluşturulmuş ve ilgili DBContext ve Poco dosyaları burada yaratılmıştır.

ExchangeService:

Şimdi gelin, Kur Datasını çektiğimiz ve güncellediğimiz servisi yazalım. Aşağıdaki komut ile ilgili webapi servisi yaratılır.

Öncelikle aşağıdaki kod çalıştırılıp, yukarıda yazılan Entity.dll’i ExchangeService projesine dahil edilir.

ExchangeService/Startup.cs/ConfigureServices(2):

Aşağıdaki komut ile EntityFrameworkCore.SqlServer kütüphanesi projeye dahil edilir.

Aşağıdaki “AddDbContext()” methodu ile Azure Üzerindeki SQL DB’ye bağlı dbContext sınıfı Dependency Injection ile ayağa kaldırılır.

ExchangeService/appsettings.json: Ayrıca Azure üzerindeki Sql server’ın connection string’i, appsettings’e aşağıdaki gibi yazılması gerekmektedir.

Şimdi sıra geldi ExchangeService’in yazılmasına.

Not: Database işlemleri gibi tüm bussines işler, doğrudan servis arkasında yazılmamalıdır. Bunun için ayrı bir bussines katmanı yapılmalıdır. Bu projede, Database ve Redis cache ile alakalı, Exchange() Action’ın arkasında hiçbir kod bulunmamaktadır. Bussines katmanı olan ExchangeServices sınıfında, tüm bu bussines işleri çözülmektedir.

Öncelikle Servisden dönecek Global Response Modeli yazalım: Böyle bir genel model tipine gidilmesindeki amaç, projenin tamamında yani hem backend hem de front-end tarafda aynı model tipinin beklenmesinin sağlanması ile işlerin kolaylaşması ve ihtiyaç duyulan ortak propertylerin tekbir model altında toplanmasıdır.

ExchangeService/Service/IServiceResponse :

ExchangeService/Service/ServiceResponse : Aşağıda görüldüğü gibi, geri dönülecek model tek bir kayıt ise Entity<T>, List ise List<T> propertysine atanarak geri dönülmüştür. Count paging için, IsSuccessful da işin başarılıp başarılmadığını göstermek için kullanılmaktadır.

Öncelikle AutoMapper kütüphanesi aşağıdaki komut ile yüklenir: Amaç DBModel ile ViewModelleri birbiri ile otomatik eşleştirmektir.

AutoMapper’ın amacı : Uygulamada client’a gönderilen ViewModel’den Entity’e veya Entity’den ViewModel’e eşleme amacı ile kullanılan kolonların, belirtilen konfigürasyonlar ile otomatik olarak atanması sağlayan kütüphanedir.

ExchangeService/Startup.cs/ConfigureServices(3): Automapper sınıfı, birazdan ilgili Entity ve ViewModeller ile eşlenecek olan MappingProfile sınıfı ile, .Net Core tarafında aşağıdaki gibi ayağa kaldırılır.

ExchangeService/MappingProfile.cs: Automapper sınıfı içinde Exchange ve ExchangeModel sınıflarını birbiri ile eşleyen, tanımlama Profile sayfası, MapingProfile sınıfıdır.

ExchangeService/Service/IExchangeServices(1): Tüm kurların liste halinde çekileceği method tanımlanır.

ExchangeService/Service/ExchangeServices(1): Dependency injection ile Automapper ve DBContext constructor’da eklenir.

GetAllExchange() : Methodunda Azure üzerindeki SQL DB üzerindeki tüm kur kayıtları çekilir.

  • “new ServiceResponse<ExchangeModel>(null)”: Geriye global ServicesResponse tipinde bir model dönülecektir. Baştan boş olarak yaratılır. Amaç oluşabilecek null hatasını önceden önlemek.
  • “_context.Exchange.ToList()”: DBContext ile Azure üzerinden, “List of Exchange” tipinde kayıtlar çekilir.
  • “_mapper.Map<IList<ExchangeModel>>(exchangeResult)”: Çekilen Exchange listesi,  “ExchangeModel” tipine, AutoMapper yardımı ile dönüştürülür.
  • “response.List = model”: Response modelin List property’sine, ilgili result atanır ve geri dönülür.

ExchangeService/Startup.cs/ConfigureServices(3): Yazılan ExchangeServices, Dependency Injection ile.Net Core ortamında RunTime’da ayağa kaldırılır.

ExchangeService/Controllers/ExchangeController.cs(2): Şimdi gelin ilgili ExchangeService’i kullanarak, Client’dan istenen tüm kur listesini geriye dönelim. Aşağıda görüldüğü gibi “IExchangeServices service“, constructor’da Dependency Injection ile projeye dahil edilmiştir.

  • “public ServiceResponse<ExchangeModel> Get()”: Get() methodunda, yukarıda yazılan services sınıfının “GetAllExchange()” methodu çağrılıp ==> “ServiceResponse<ExchangeModel>” tipinde model geri dönülür.

Gelin şimdi benzer işlemleri belirlenen bir Kur’un çekilmesi için yapalım. Yani seçilen bir kurun detayına gidelim.

ExchangeService/Model/ExchangeEnum.cs: Öncelikle istenen kur tipi için, ExchnageEnum sınıfı aşağıdaki gibi yaratılır. Amaç, ne olduğu bilinmeyen ID ler ile çalışmaktan kaçınmaktır.

ExchangeService/Service/IExchangeServices(2): “GetExchangeByName(ExchangeType exchangeType)” methodu aşağıdaki gibi eklenir.

ExchangeService/Service/ExchangeServices(2): “GetExchangeByName(ExchangeType exchangeType)” methodunda, belirlenen Kur tipine göre detay bilgisi Azure üzerindeki SqlDb’den çekilip geri dönülür.

  • “var exchangeResult = _context.Exchange.Where(ex => ex.Name == exchangeType.ToString()).FirstOrDefault()”: Azure SQL server üzerinden, Linq ile “ExchangeType“‘a göre filitreleme yapılarak ilgili kur detayı çekilir.
  • “var model = _mapper.Map<ExchangeModel>(exchangeResult)”: Çekilen Exchange Entity, AutoMapper ile ExchangeModel tipine cast edilir.
  • “response.Entity = model”: geriye tek bir değer döndüğü için, “ServiceResponse” modelinin Entity property’si set edilir. Ve geri dönülür.

ExchangeService/Controllers/ExchangeController.cs(3): İstenen kur detayını dönen Action’ı, gelin hep beraber yazalım.

  • “[HttpGet(“GetExchangeByName/{exchangeType}”)]” : İlgili attribute ile routing tanımlaması yapılmıştır.
  • [SwaggerOperation(Summary ve Description): methodunun ne işe yaradığı ve parametre olarak beklenen ExchangeType Enum’unun sayısal değer karşılığı açıklanmıştır.
  • “return _service.GetExchangeByName(exchangeType)”: Yukarıda yazılan “_service.GetExchangeByName()“, servis çağrılarak “ServiceResponse<ExchangeModel>” tipi geri dönülmüştür.

Şimdi Gelin Çekilen Kur Listesi ve Kur Detay Bilgisini, Performansı Arttırmak Amacı ile Azure Üzerindeki Redis’den Alalım:

ExchangeService/Service/IRedisCacheService: Aşağıda görüldüğü gibi redis işlemleri için ayrı bir servis yazılmıştır. Get<>(), Set<>() generic methodları tanımlanmıştır.

ExchangeService/Service/RedisCacheService: Generic olarak tanımlı methodlar, istenen tipde datayı redis’de saklamak ve redis’den almak için kullanılır. Azure üzerindeki Redis connection, burada config’den almak yerine string değişkenler ile tanımlanmıştır.

  • “public T Get<T>(string key)”: Belirlenen key’e göre, Redisdeki data model çekilir.
  • “public void Set(string key, object data, DateTime time)”: Belirlenen Expire suresi kadar, tanımlanan string key’e ilgili model Azure üzerindeki Redis’e atılır.

“Mühendislik, yüz durumdan sadece biri bile kıritik duruma sebebiyet veriyorsa, o durum için hazırlanmaktır.”  ―Bora Kaşmer

Custom Redis Exception:

ExchangeService/Model/RedisNotAvailableException: Redise bağlanılamaması durumunda, fırlatılacak Custom Redis hata mesajı aşağıda görüldüğü gibi tanımlanmıştır.

ExchangeService/Startup.cs/ConfigureServices(4): İlgili Redis Servis, .Net Core Projede aşağıdaki gibi tanımlanır.

ExchangeService/Service/ExchangeServices(3): Aşağıda görüldüğü gibi, GetAllExchange() ve GetExchangeByName() methodlarında doğrudan DB’ye gitmek yerine öncelikle Redis’e bakılmış yok ise, DB’ye gidilip çekilen data, 1 dakika expire süreli Redis Cache atılmıştır.

  • _redisCacheManager = redisCacheManager“: RedisCacheManager, sınıfa Dependency Injection ile dahil edilir.
  • GetAllExchange() => var cacheKey = “AllExchange”; var result = _redisCacheManager.Get<List<ExchangeModel>>(cacheKey)“: Tüm kur bilgisi, Redis’de var mı diye bakılır?
  • GetAllExchange() => if (result != null)“: Varsa Redis’den alınan List of ExchangeModel => ServiceResponse modelinin, “List” propertysine atanarak geri dönülür. Redis’de yok ise, Azure üzerindeki SqlDB’den tüm kur bilgileri çekilerek Redise’e 1 dakikalık Expire süresi ile atanıp, geri dönülür.
  • GetExchangeByName() => var cacheKey = “Exchange:” + (int)exchangeType; var result = _redisCacheManager.Get<ExchangeModel>(cacheKey)“: İstenen bir kur detay bilgisi alınması sırasında da, önce Redis’e bakılır. Redis key olarak “Exchange:”+ “Enum ExchangeType”‘ın int değeri alınır. Yani mesela Dolar için => “Exchange:1” key’i oluşturulur. İlgili data Redisde varsa çekilir.
  • GetExchangeByName() =>if (exchangeResult != null)” Redis’de Kur detay bilgisi var ise ExchangeModel  => ServiceResponse modelinin, “Entity” propertysine atanarak geri dönülür. Redis’de yok ise, Azure üzerindeki SqlDB’den Kur Detay bilgisi çekilerek Redise’e 1 dakikalık Expire süresi ile atanıp, geri dönülür.

Şimdi Sıra Geldi Bir Kur Bilgisi Güncellenirken, Redis Pub/Sub ile hem SqlDB hem de Redis Cache’in Asenkron Olarak Güncellenmesine:

Burada esas amaç, performansı arttırmak amacı ile, SQL DB ve RedisCache güncelleme işlemlerinin arka tarafda yani WebApi servisinden bağımsız asenkron bir şekilde, hatta başka sunucularda yapılmasını sağlamaktır.

ExchangeService/Service/IRedisCacheService(2): Öncelikle gelin RedisCacheManager’a, Publish yapabilme özelliğini katalım. Aşağıda görüdüğü gibi Publish methodu interface’e eklenmiştir.

ExchangeService/Service/RedisCacheService(2): Aşağıda görüldüğü gibi publish() methodunda, parametre olarak gelen object data(bu bizim örnekte “ExchangeModel” olacak), ilgili channel’a Serialize edilip, string olarak gönderilmektedir.

ExchangeService/Service/IExchangeServices: Aşağıdaki method, ilgili Exchange interface’e eklenir.

ExchangeService/Service/ExchangeServices(4): Aşağıda bir kur güncelleneceği zaman, çağrılan servis tanımlanmaktadır. Güncellenecek Kur Datası, yani exchangeData ==> “Exchange” channel’ına, işlenmek üzere gönderilir. Burada yapılan tek iş budur. Herhangi bir DB veya Redis güncellemesi yapılmamaktadır.

ExchangeService/Controllers/ExchangeController.cs(4): Gönderilen Kur’un, Sql DB ve Redis’de güncellenmesi için, burada bir işlem yapılmayıp doğrudan servise gönderilir.

Azure Functions:

Gelin yeni bir Azure Functions Oluşturalım: Amaç Redisdeki “Exchange” channel’ının dinlenip, gelen ExchangeModel’in hem Sql DB’de hem de Redis’de güncellenmesinin sağlanması.

Redis Pub/Sub:

RedisWorker:

1-)Öncelikle aşağıdaki komut ile Worker Service projesi oluşturulur.

2-)Sonra aşağıdaki paketlerin kurulumu yapılır: Redis için ServiceStack, DB işlemleri için Entity Projesindeki BlackJackContext ve Sql işlemleri için EntityFrameworkCore kütüphaneleri eklenir.

3-RedisWorker/Worker.cs(1): Redisden alınacak Kur bilgisi, aşağıda tanımlı model’e dönüştürülür.

Resimin Kaynağı: https://miro.medium.com/max/1400/1*Mc3xijLnyBbiAPEGonlqtg.gif

4-RedisWorker/Worker.cs(2): Aşağıda görüldüğü gibi Azure üzerindeki Redis’deki “Exchange” channel’ını dinleyen Subscriber, kendisine güncelleme amaçlı gönderilen son güncel “ExchangeModel“‘i, Azure üzerindeki Sql ve Redis’de günceller.

  • Worker proje template ile default olarak kodda gelen “while(){ }” döngüsü,bu örnekte gerek olmadığı için kaldırılmıştır. Zaten Redis Subscriber her an ilgili channel’ı dinlemektedir. Bu neden ile tekrar eden bir döngüye ihtiyaç yoktur.
  • “using (IRedisClient client = new RedisClient(conf))”: Azure üzerindeki Redis’e, belirtilen config ile bağlanılır.
  • “using (sub = client.CreateSubscription())”: İlgili Redis’i dinleyen, subscriber yaratılır.
  • “sub.OnMessage += (channel, exchange) =>” : Exchange channel’ının dinlendiği ve gönderilen ExchangeModel’in yakalandığı method, burasıdır.
  • “ExchangeModel _exchange = JsonConvert.DeserializeObject<ExchangeModel>(exchange)”: Yakalanan string data => yukarıda tanımlanan ExchangeModel’e deserialize edilir.
  • “using (IRedisClient clientServer = new RedisClient(conf))” : Redis’e güncelleme yapılabilmesi için yeni bir Redis Client’ın yaratılması gerekmektedir. Eğer Subscription için hemen yukarısında yaratılmış olan Redis Client kullanılmaya çalışılır ise, aşağıdaki gibi bir hata aile karşılaşılır.
    • only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context
  • “string redisKey = “Exchange:” + _exchange.ID”: Gelen Exchange modele göre, Redis key oluşturulur.
  • “clientServer.Set<ExchangeModel>(redisKey, _exchange)”: Redis üzerindeki ExchangeModel, yeni gelen ExchangeModel ile ezilir. Yok ise yeni yaratılır.
  • “using (BlackJackContext context = new BlackJackContext())”: Azure üzerindeki Sql DB’ye ait BlackJackContext, yukarıda yazılan Entity projesinin import edilmesi ile yaratılır.
  • “var exchangeModel = context.Exchange.FirstOrDefault(ex => ex.Id == _exchange.ID)”: SqlDb üzerindeki güncellenecek, eski kur bilgisi çekilir.
  • “exchangeModel.Value = (decimal)_exchange.Value; exchangeModel.UpdateDate = DateTime.Now”: Kur değeri ve son güncellenme tarihi, update edilir.
  • “context.SaveChanges()”: Entity üzerinde yapılan değişiklikler, kaydedilir.
  • “sub.SubscribeToChannels(new string[] { “Exchange” })”: Redis’in dinlediği channel, “Exchange” olarak tanımlanır.

Geldik bir makalenin daha sonuna. Bu makalede, baştan sona ufak bir .Net Core Projenin ayağa kaldırılması sırasında nelere dikkat edimesi gerektiğine, bazı pratik ip uçlarına, ve Azure üzerinde yaratılan SqlDB ve Redis Cache gibi yapıların entegrasyonuna değindik. Performans amaçlı Redis sadece bir Memory Cache Tool’u değil, aynı zamanda hem bir DB hem de Socket desteği olan tam bir isviçre çakısıdır. Biz bu makalede, yüksek trafik anında, WebServisi üzerindeki yükü, SqlDB ve Redis güncellemelerini başka bir sunucu üzerindeki Worker Servis üzerinden yaparak azalttık. Worker Servis üzerindeki Redis Subscriber, ilgili “Exchange” channel’ını sürekli dinleyerek kendisine gelen güncel kur paketlerini asenkron olarak SqlDB ve Redis’de güncellemiştir. Microservice mimarisinde bu şekilde dağıtık çalışmak, yükün dağıtmasına, farklı teknolojilerin bir arada kullanılmasına, teste ve debug işlemlerine olanak sağlasa da, ciddi bir maintenance getirmektedir. Publish ve paketlerin dağıtılması için Devops, Monitor için de çeşitli toolara mutlak ihtiyaç vardır.

Yeni bir makalede görüşmek üzere hepinize hoşçakalın.

Source Code: https://github.com/borakasmer/AzureWorker-RedisPub-Sub

Source:

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir