Redis Data Consistency Sağlayan Bir Yapının Performansını, RabbitMQ ve SignalR İle Arttırma

Selamlar,

Bu yazı, Redis’de Data Consistency’yi .Net Core Ortamında Mikroservisler İle Sağlama makalesinin devamı olarak, tamamen performans amaçlı yazılmış bir yazıdır. Önceki makalede anlatılan yapıda, anlık kullanıcı sayısı 20.000 veya yukarısına çıkartılsa idi, ne yapardık? Tabi ki bu yükü microservislerin üzerine yıkabilirdik. Peki bu işi bir kuyruğa atıp, tek tek asenkron işlemeye ne dersiniz? Bir de gelin hazır performansa girişmişken, ilgili data değişikliğini sayfayı manuel yenileyerek göstermek yerine, Real-Time signalR ile gösterelim.

1-) Makinanızda RabbitMQ yok ise, ilk önce HomeBrew’ün kurulmasının ardından, aşağıdaki link ile ilgili yükleme yapılır.

2-) Bash Profile Güncellenir: Mac ortamında, RabbitMQ ilk kurulduğu zaman, “/usr/local/sbin” altına kurulmaktadır. RabbitMQ server’ı kendi profilimiz altında ayağa kaldırmak için, ilgili yolun “.bash_profile” altında tanımlanması gerekmektedir. Bu makalede ilgili yetki işlem için, “nano” uygulaması aşağıdaki gibi kullanılmıştır.

3-)RabbitMQ Server’ın çalıştırılması: Aşağıda görüldüğü gibi “rabbitmq-server” komutu ile, RabbitMQ ilgili client tarafında ayağa kaldırılır.

4-) Browser’dan http://localhost:15672 adresine gidilip default oluşturulan Username :”guest”, Password :”guest” yazılması durumunda, aşağıdaki Monitör ekranına ulaşılır.

Şimdi gelelim asıl soruna ve çözümüne : Diyelim ki anlık girilen ya da güncellenen haber sayısı 10bin olsun. Bu durumda microservislerin üzerine anlık çok fazla yük binecek ve muhtemelen bir süre sonra cevap veremeyecek bir hale geliceklerdir. Hatta data eşleniği yapılacak ilgili remote redis makina sayısı çok ise, ve eşitlenecek data miktarı büyük ise, işler daha da karmaşık bir hal alabilecektir.

Bir de şöyle bir senaryo düşünelim: Eşleniği alınamayan makinaların tekrardan denenmesi ve belli bir denemeden sonra başarısız olunur ise yöneticiye mail veya sms atılması istensin. İşte bunun gibi zaman alan, yüksek trafikli durumda bir Queue mekanizması bizim sorunlarımızı çözebilir. Bu makalede örnek amaçlı RabbitMQ kullanılacaktır. Ama siz Kafka, ActiveMQ veya MsMq gibi başka teknolojiler de kullanabilirsiniz. Hepsinin kendine göre avantaj ve dezavantajları vardır. Bir önceki makalede, “News” kanalını dinleyen her bir sunucu üzerinde kurulu microservices’e, değişen News datasını Serialize edip string olarak gönderiyorduk. Şimdi bunun yerine ilgili datayı, aşağıdaki gibi tek bir RabbitMQ sunucusuna göndereceğiz.

Öncelikle RabbitMQ’nun .Net Core bir projede kullanılabilmesi için, ilgili kütüphanelerin aşağıdaki gibi yüklenmesi gerekmektedir.

Not: Eğer RabbitMQ ile alakalı projede versiyon uyuşması sorununu yaşarsanız,  ilgili .csproj üzerinde RabbitMQ’ya ait versiyon numarasını silin ==> “<PackageReference Include=”RabbitMQ.Client” />” Sonra tekrar “dotnet restore” edin.

Home/Updatenews: Aşağıda görüldüğü gibi localde redisde güncellenen data, Redis”News” kanalı yerine, RabbitMQ’ya gönderilir. Ve böylece işlenmek amacı ile sıraya alınır.

AddRabbitMQ(): Girilen veya güncellenen herbir haber, bir Queue mekanizmasına yani bu makalede RabbitMQ’ya eklenir.

  • ConnectionFactory :RabbitMQ sunucusuna bağlanmak için kullanılır. Bulunulan sunucudaki host name (localhost) ve eğer tanımlanır ise credentials (password) girilir.
  • CreateModel() :RabbitMQ üzerinde yeni bir kanal oluşturulur. Bu kanal aşağıda görüldüğü gibi haberler için “News” kanalıdır.
  • QueueDeclare() : Bu method ile oluşturulacak olan queue‘nin ismi tanımlanır. Bu uygulamada ilgili “queue” ismi “News”‘dır.
    • “durable” ile datanın in-memory mi yoksa fiziksel olarak mı saklanacağı belirlenir. Fiziksel olarak saklanır ise, performansdan büyük ödün verilir.
    • exclusive” parametresi ile diğer connectionlar ile kullanılmasına izin verilir.
    • autoDelete” Eğer queue deleted olarak işaretlenmiş ve silme işlemi gerçekleştirilememiş ise, işte bu gibi durumlarda “autoDelete” ile  delete method’u normal olarak çalıştırılır. Böylece ilgili queueler silinir.
    • arguments” Belirlenen haber ile alakalı tanımlanacak parametrelerdir. Bu uygulamada news için herhangi bir parametre tanımlanmamıştır.
  • Güncellenen “Stoc” modeli, JsonConvert ile Serialize edilip string bir ifadeye dönüştürülür. Daha sonra “byte[ ]” dizisine çevrilip post edilecek “body“ parametresine atanır.
  • BasicPublish() : Bu methodda “exchange”, aslında “news” datasının alınıp bir veya daha fazla queue’ya yani sıraya konmasını sağlar. routingKey ile konucak olan kuyruğun adı(“News”) tanımlanır.

Aşağıda, ilgili haber 2 kere güncellendiğinde, RabbitMQ’de Görülen Channel Sayısı 1 ==> “News” ve  Queue’de bekleyen toplam iş sayısı 2‘dir.

Şimdi sıra geldi RabbitMQ’ya atılan bu paketlerin, bir Microservices yardımı ile tek tek alınıp, tanımlı tüm redis sunucularında güncellenmesine:

Project/RedisNewsServices: Aşağıdaki microservices’de localdeki rabbitMQ server’a bağlanılmış ve ilgili “News” channel’ı dinlenmeye başlanmıştır. Gelen herbir News paketi, tanımlı tüm Redis sunucularında güncellenmiştir.

  • var factory = new ConnectionFactory() { HostName = “localhost” }” : Localdeki RabbitMQ Queue’ya bağlanılır.
  • “using (var connection = factory.CreateConnection())” : Connection açılır.
  • “using (var channel = connection.CreateModel())” : İlgili kanal oluşturulur.
  • channel.QueueDeclare(queue: “News”” : RabbitMQ’da dinlecek channel’ın “News” olduğu tanımlanır.
  • “var consumer = new EventingBasicConsumer(channel)”  RabbitMQ’da dinleyici, ya da alıcı da diyebileceğimiz tanımlama yapılır.
  • “consumer.Received += (model, ea) =>” : RabbitMQ’da yeni bir “News” paketi yakalandığında, Received event’i trigger edilir. İlgili method altında “RefreshRedisServers()” methodu çağrılarak, tanımlı tüm redis sunucularında gelen “News” datası güncellenir.
  • “channel.BasicConsume()” : Methodu ile ilgili Queue’den güncellenen haberleri çekme işlemine başlanır. Burada autoAckparametresi true olarak atanır ise, ilgili mesaj alındıktan sonra Queue’den otomatik olarak silinir.
  • “Console.ReadLine()” : Bu methodu ile kod bu noktada bekletilir. RabbitMQ’da ilgili “News” channel’ı dinlenmeye devam edilir. Ve yeni bir paket yakalandığında “Received” tekrardan tetiklenip, yukarıda yapılan işlemler tekrardan çalıştırılır.

Şimdi sıra geldi diğer remote sunucularda çalıştırılan AddRabbitMQ() methoduna:

Not: Tek bir yerde bulunan ana sunucudaki “RabbitMQ”‘ya erişim “guest” kullanıcısı üzerinden yapılamamaktadır. Bunun için RabbitMQ’da, aşağıdaki adımlar izlenerek yeni bir kullanıcı oluşturulması gerekmektedir. İlgili komutlar Mac’de Bash, Windows’da Command Prompt’da çalıştırılmalıdır.

  • rabbitmqctl add_user test test : UserName : “test”, password : “test” bir kullanıcı yaratılır.
  • rabbitmqctl set_user_tags test administrator : “test” user’ına admin tag’i atanır.
  • rabbitmqctl set_permissions -p / test “.*” “.*” “.*” : “test” user’ına yetki verilir.

Önemli Not: RabbitMQ’da 5672 portuna uzaktan erişim için,  “/usr/local/etc/rabbitmq-env.conf” dosyasından “NODE_IP_ADDRESS=127.0.0.1” kısmı silinir. Böylece erişim her IP’den yapılabilecek bir hale gelir.

  1. “var factory = new ConnectionFactory() { HostName = “192.168.1.234” }” : Remote server’ın IP adresi yazılır.
  2. factory.UserName = “test”; factory.Password = “test”; factory.Port = 5672 : Bağlanılacak RabbitMQ’nun credential bilgileri ve portu tanımlanır.

Diğer adımlar Ana sunucuda bulunun diğer “AddRabbitMQ()” methodu ile aynıdır.

Son olarak bu yeni yapıda, ana makina dışındaki diğer makinalarda, herhangi bir microservices’in kurulmasına ihtiyaç yoktur. Güncellenen her bir haber tek biryerde bulunan sunucudaki RabbitMQ’ya konmakta, ve yine ana makinada çalışan bir microservice RabbitMQ’dan aldığı her haberi kendine tanımlı tüm redis sunucularında güncellemektedir. Böylece anlık 100bin haber bile güncellense, her bir haber belirli bir Queue mekanizmasına atılıp, daha sonra var olan çalışan yapıdan bağımsız olarak, ilgili redis sunucularında güncellenecek, bu da bize büyük bir performance sağlayacaktır.

Son olarak güncellenen yani değişen haberleri, clientlarda Real Time Web Socket (signalR) kullanarak değiştirelim. Böylece ilgili sayfanın yenilenme ihtiyacı duymadan, en güncel haberi göstermesini, signalR optimizasyonu ile sağlayalım.

Öncelikle Mvc uygulamamızın “redis.csproj” dosyasına, alttaki signalR kütüphaneleri eklenir. Ve “dotnet restore” komutu çalıştırılarak ilgili kütüphaneler projeye eklenir.

Aşağıdaki komutu ile signalR javascript kütüphaneleri ==> “*node_modules\ @aspnet\signalr\dist\browser*” altına indirilir.

NOT :signalr.js ve signalr.js.map” dosyaları, “wwwroot/lib/signalr/” altına taşınması gerekmektedir.

Startup.cs: Bir Mvc projede, signalR’ın kullanılabilmesi için aşağıdaki tanımlamaların yapılması gerekmektedir.

ConfigureServices() methodunda Mvc tanımlamadan sonra SignalR’ın tanımlanması gerekmektedir.

  • Cross domain probleminin yaşanmaması için aşağıda görülen “UseCors()” methodu tanımlanmıştır.
  • SignalR ilk connect olurken doğru yolu bulabilmesi için, routing amaçlı “routes.MapHub()” methodu tanımlanmıştır.

NewsRefresh: SignalR Hub sınıfı aşağıda görüldüğü gibi oluşturulur:

  • OnConnectedAsync() : Connect olan client’ın signalR ConnectionID’si alınır. Ve client side taraftaki “GetConnectionId()” function’ına gönderilir.
  • RefreshNews() : Güncelleme yapılan haber, ilgili SignalR Hub sunucusuna bağlı olan tüm clientlarIın “RefreshNews()” function’ına gönderilir.

Index.cshtml : Index sayfasına SignalR socket kullanımı için aşağıdaki script kodları eklenir. Burada amaç, yeni bir haber güncellemesi durumunda sayfanın yenilenmeden güncel haberin socket teknolojisi kullanarak göstermesidir.

  • “const connection = new signalR.HubConnectionBuilder() .withUrl(“/newsrefresh”) .build()” : “NewRefresh : Hub” signalR Hub sınıfına bağlanılır.
  • “connection.on(“GetConnectionId”, (connectionID) =>” : Bağlanan client’ın ilerde kullanılabilmesi amacı ile , signalR ConnectionID’si sayfada tanımlı “connectionID” hidden field’ına basılmaktadır.
  • RefreshNews() : Bu method ile güncellenen “news” değeri parametre olarak alınmıştır. Haber bilgisinin basıldığı her bir html elemente, “id” tanımlaması yapılmıştır. “title”, “date” ve “detail” id’li alanların “innerHTML“‘ine gelen news kolonları basılarak son güncel haber bilgisi, ekrana yenileme işleme olmaksızın basılmıştır.

Not : Sayfaya gelen tüm new’s datasına ait kolonlar küçük harfle geldiği görülmektedir. Bir tek 2 kelimeli değişkenlerde, ortadaki harf büyüktür. Örnek : “news.updatedDate“. Bu neden ile öncelikle gelen datanın “consoloe.log(news)” komutu ile ekrana basılması, şiddet ile tavsiye ettiğim adımlardan biridir.

Son olarak RabbitMQ’dan güncellenen haberleri çekip, tüm redis sunucularını güncelleyen microservices projesine, signalR aşağıdaki gibi eklenir:

Öncelikle ilgili console application’a alttaki kütüphane eklenir.

program.cs: Aşağıdaki tanımlamalarda, örnek amaçlı 2 sunucu için ip değerleri ile ayrı ayrı bağlanan, 2 static HubConnectionBuilder methodu() oluşturulmuştur. Elimizdeki sunucu sayısının çok daha fazla olması durumunda, bu yapı daha dinamik bir hale getirilmelidir. Örneğin sunucu listeleri bir config dosyadan alınıp tek tek gezilerek, ilgili socket connectionlar açılabilir.

RefreshRedisServer(): İlgili methodda herhangi bir haberin güncellenmesi durumunda, ilgili tüm redis sunucuları güncellenir ve tüm clientlara signalR-Socket kullanılarak notify fırlatılır.”:

  • clientServer.Set<News>(“RedisNews”, _news) : Tanımlı Redis Sunucularında ilgili haber güncellenir.
  • ConnectServer1().Wait(), ConnectServer2().Wait() : Her bir signalR server’ına, ait oldukları IP’leri ile bağlanılır.
  • await connectionSignalR2.InvokeAsync(“RefreshNews”, _news) : SignalR Hub sınıfına ait, serverSide taraftaki “RefreshNews()” methodu güncellenen “_news” haber datası ile tetiklenir.

Not: Remote makinalara ipleri ile erişilebilmesi için .Net Core kestrel’de yayım yapılması gerekmektedir. Bunun için aşağıda görüldüğü gibi Program.cs’de “.UseUrls(“http://0.0.0.0:5000”)” tanımlaması yapılması gerekmektedir.

Program.cs:

Geldik bir makalenin daha sonuna. Bu makalede zaman alacak ya da anlık olarak çok fazla yükün oluşabileceği yapılarda, tüm işlerin bir Queue’ya (RabbitMQ) alınıp, çalışan yapıdan bağımsız olarak mikroservices mantığı ile asenkron olarak arkada nasıl işlenebileceğini ve işlenen bu datanın socket(signalR) kullanılarak tüm clientlara nasıl bildirilebileceğini gördük.

Bir sonraki makalede görüşmek üzere hoşçakalın.

Source Code : http://www.borakasmer.com/projects/RedisPerformanceWithRabbitMQAndSignalR.zip

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

3 Cevaplar

  1. ahmet dedi ki:

    merhaba hocam,
    iis server üzerinde hem signalr haberleşme portu hem de web sitesinin aynı port numarası(:80) üzerinden haberleşmesinin performansa etkisi olur mu ?
    Bir de sql server procedure üzerinden tetikleme mekanizması yazdım yaptığınız örneklerden,9 tetikleme sonrası haber vermeyi kesiyor. localhost bekleniyor da kalıyor. bunu sebebi ne olabilir.

    • borsoft dedi ki:

      Selam,
      Portların aynı olmasında bir sorun olmaz. Belli bir push işleminden sonra muhtemelen bir sorun oluyor ve Rem Cpu değerlerine bakarmısın makinanın. Ya da IIS üzerinden Socket Pool değerlerine bakarmısın?

Bir cevap yazın

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