Actor Model - Microsoft Orleans

17.09.2023 | dakika okuma
ChatGPT sayesinde bu yazıdaki cümleler dil bilgisi kurallarına uygun hale getirilmiş ve daha akıcı, anlaşılır cümleler oluşturulmuştur.

OOP dört temel kavram üzerine kuruludur. Bunlar; Encapsulation, Abstraction, Inheritance ve Polymorphism. Bunlar arasından en önemlisi encapsulation'dır. Encapsulation nesnenin internal verisine dışarıdan erişilememesini ifade eder.

Stok özelliğine sahip bir product nesnesi düşünüyoruz. Stoğu düşürmek için "DecreaseStock" metoduna ihtiyacımız olacak. Stoğa dışarıdan müdahale edilemeyecek ve business kurallarına sahip "DecreaseStock" metodu ile stok düşürülecektir. "DecreaseStock" metoduna aynı anda iki veya daha fazla thread girerse ne olur? Ne kadar stok düşeceğini sadece Allah bilir.

Bunun nedeni, nesne yönelimli programlamanın en büyük dezavantajı olan "shared memory" sorunudur. Tabii ki bu sorunu aşmak için birçok yöntem ve yaklaşım var. En klasik yöntem, "locking" mekanizmasıdır, ancak bu oldukça maliyetli bir işlemdir, bu yüzden bundan kaçınmak gerekir. Actor Model, hem locking maliyetinden kurtarır hem de dağıtık bir ortamda dağıtık bir ortamda değilmiş gibi geliştirme yapmamıza imkan verir. Peki bunu nasıl yapar?

Actor Model

Actor Model’in temel yapı taşı actor'dür. Actor Model’de her şey actor'dür. OOP'de ise her şey birer nesnedir. Product'ı bir actor olarak düşünebiliriz.

Her actor'ün kendine "internal state"i vardır. OOP'deki nesnenin "property"leri ve "field"ları gibi düşünebiliriz. Fakat actor'ler başka actor'lerin state'lerine erişemezler ve değiştiremezler. OOP'deki gibi "shared memory" sorunu yoktur.

Actor'lerin behaviour'ları vardır. OOP'deki metotlar gibi düşünebiliriz. Fakat actor'ler ile immutable mesajlar aracılığıyla iletişim kurulur. OOP'deki gibi nesne üzerinden metot çağrısı yapılmaz.

Actor'lerin mailbox'ları vardır. Immutable mesajlar actor'lerin mailbox’ına gönderilir. Actor'ler bu mesajları FIFO olarak işlerler. Bu sayede concurreny sorunu da tamamen çözülmüş olur.

Actor

.NET dünyasında Actor Model için en çok kullanılan provider'lar Akka.NET ve Microsoft Orleans'dır. Bu yazıda Microsoft Orleans üzerinde çalışmalar yapacağız.

Microsoft Orleans

Microsoft Orleans'da Grain ve Silo kavramları önemli yer tutar.

Grain aslında sanal bir actor. Yani actor diyebiliriz.

Bir grain'e mesaj geldiğinde, memory'de aktive olur. Daha sonraki mesajlarda memory'de aktif olan grain'ler üzerinde işlemler yapılır. Kullanılmayan grain'ler belli bir süre sonra deaktive olur ve memory'den silinir.

Grain Cycle

Silo, grain'lerin barındırıldığı host'lar olarak tanımlanır.

Cluster

Yukarıdaki görsele dikkat ederseniz, bir cluster'da iki silo bulunmaktadır. Silo'lar içerisinde de grain'ler mevcuttur. Yüke göre kolay bir şekilde silo eklenebilir veya çıkarılabilir. Ayrıca bu silo'lar farklı lokasyonlarda da çalışabilir. Bu sayede Actor Model dağıtık bir sistemi destekler.

Bir grain'e mesaj geldiğinde, hangi silo'da aktive olacağını framework belirler. Biz geliştiriciler olarak grain'in nerede aktive olduğuna dikkat etmiyor ve grain'e doğrudan erişim de sağlayamıyoruz. Çünkü bir grain B silosunda aktifken deaktif olup, A silosunda tekrar aktif olabilir. Bu nedenle mesajı grain'e grain'in referansı (proxy'si) aracılığıyla gönderiyoruz. Framework gerisiyle ilgilenir. Bu duruma location transparency denir.

Grain'ler stateless veya stateful olabilir. Eğer grain stateless ise yüke göre birden fazla aktive olabilir ve framework yükü eşit bir şekilde dağıtır. Ancak stateful ise yalnızca bir kez aktive olur ve mailbox aracılığıyla mesajlar işlenir.

Grain'ler ne yapabilir?

Kendi state'ini güncelleyebilir. Örneğin, 001 id'li ProductStockGrain'ine decrease mesajı geldiğinde, stoğunu düşürebilir.

Başka bir grain'e mesaj göndererek iletişime geçebilir. Örneğin, 001 id'li ProductStockGrain'ine decrease mesajı geldiğinde stok miktarı yetersizse bu durumu ayrı bir grain'e bildirebilir. Bu, kendi yükünü başka grain'lerle paylaşarak dağıtımını sağlar.

Grain Oluşturma

Biraz kod yazalım. Solution'a bir class library ekleyelim ve adına ProductActor.Contracts verelim.

Nuget package'dan aşağıdaki kütüphaneleri kuralım.

    dotnet add package Microsoft.Orleans.CodeGenerator.MSBuild -v 3.6.5
dotnet add package Microsoft.Orleans.Core.Abstractions -v 3.6.5

Her grain'in davranışları bir interface aracılığıyla tanımlanır. ProductStockGrain'i için bir interface tanımlıyoruz. IGrainWithIntegerKey interface'ini uyguluyoruz. Bu interface sayesinde ProductStockGrain'inin identity tipini integer olarak belirliyoruz. String için IGrainWithStringKey veya guid için IGrainWithGuidKey kullanılabilir.

Daha sonra grain implementasyonu yapılır. Solution'a bir class library daha ekleyelim ve adına ProductActor.Grains verelim.

5. satır : OnActivate metodu, grain memory'de aktif olduğunda tetiklenir. Burada çeşitli işler yapılabilir. Örneğin; bir HTTP isteği ile ürünün stok bilgisi alınabilir.

12. satır : OnDeactivate metodu, grain memory'den silindiğinde tetiklenir. Örneğin; bir HTTP isteği ile ürünün stok bilgisi gönderilebilir.

26. ve 35. satır : Increase ve Decrease metotlarında concurrency sorunları ile karşılaşmayacağımızı biliyoruz, bu yüzden rahatlıkla stoklarımızı artırıp azaltabiliyoruz. Biliyoruz ki, mailbox sayesinde mesajlar sıra ile işlenecek, bu da bize dağıtık bir ortamda, in-memory'de işlemler yapmanın kolaylığını sağlıyor. Bu müthiş bir özellik.

Silo Oluşturma

Artık silo oluşturabiliriz. Silo, console uygulaması veya web uygulaması olarak oluşturulabilir. Biz console uygulaması oluşturalım. Solution'a ProductActor.Host adında bir console uygulaması ekleyelim.

Nuget'den aşağıdaki kütüphaneleri yükleyelim ve ProductActor.Grains projesini referans olarak ekleyelim.

    dotnet add package Microsoft.Orleans.Core -v 3.6.5
dotnet add package Microsoft.Orleans.OrleansRuntime -v 3.6.5
dotnet add package OrleansDashboard -v 3.6.2

Program.cs dosyasını aşağıdaki gibi düzenleyelim.

7. satır : Silo'yu localhost cluster'ında çalışacak şekilde ayarlayarak hızlıca bir test ortamı hazırlıyoruz. Canlı ortamlarda ise SQL Server, DynamoDb, MongoDB gibi veritabanı sistemlerini kullanabiliriz.

8. satır : Silo için bir IP tanımı yapıyoruz. Silo'nın IP'sini 11111 ve gateway IP'sini 30000 olarak veriyoruz. Bu ip'leri console'dan parametre olarak alarak istediğimiz kadar silo ayağa kaldırabiliriz.

11. satır : ClusterId için dev tanımını yaparak silo'yu dev cluster'ında ayağa kaldırıyoruz. Eğer birden fazla silo ayağa kalkarsa, dev cluster'ında çalışan silo'lar birbirleriyle haberleşebilir. Bu özellik, blue-green deployment gibi senaryolarda çok yararlıdır. ClusterId değişebilirken, ServiceId ise değişmezdir.

14. satır : Dashboard sayesinde silo'nun CPU, Memory, Grain Activation gibi metriklerine erişebilir ve bu sayede silo'nun performansını anlık olarak takip edebiliriz.

Client Oluşturma

Grain'lere mesaj gönderebilmek için client tarafına da ihtiyacımız var. Bu client'ı API olarak tasarlayalım. Solution'a bir API projesi ekleyelim ve adına ProductActor.Api verelim.

Nuget'den aşağıdaki kütüphaneleri yükleyelim ve ProductActor.Contracts projesini referans olarak ekleyelim.

    dotnet add package Microsoft.Orleans.Core -v 3.6.5

Program.cs dosyasını aşağıdaki gibi düzenleyelim.

4. satır : Silo'da tanımladığımız gateway port, cluster id ve service id değerlerini aynı şekilde API projesinde de belirtmemiz gerekir.

10. satır : Dependency container'a ClusterClient'ı register ettikten sonra IClusterClient aracılığıyla grain'lere mesaj gönderebiliriz.

17. 25. ve 32. satır : ClusterClient.GetGrain ile grain'in referansına (proxy'sine) erişiyoruz. Bu proxy aracılığıyla Get, Increase ve Decrease metotlarını çağırarak grain'e mesaj gönderebiliriz.

Host projesi de Client projesi de hazır. Önce Host projesini, sonra da Api projesini çalıştırarak test edebilirsiniz. http://localhost:8080 üzerinden dashboard'a erişerek silo'nun performansını takip edebilirsiniz.

Dashboard - Silos
Dashboad - Grains

Stateless Worker Grain'ler

Grain'ler bir kez aktive olur ve concurrency sorunlarını ortadan kaldırır. Ancak bazen Microsoft'un deyimiyle "functional stateless operations" gerektiren durumlar da vardır. Örneğin ProductStockGrain'ine decrease mesajı geldiğinde stok miktarı yetersiz ise bildirimde bulunmak isteyebiliriz. Bu durumda bildirim işini ayrı bir grain olarak tasarlayabiliriz. Böylece grain'leri olabildiğince küçük parçalara böler ve bu grain yüke göre birden fazla aktive olabilir.

Contracts projesine dönelim ve yeni bir class ekleyelim, ismine de "IInsufficientStockNotification" verelim.

Gördüğünüz gibi interface tanımlamada herhangi bir farklılık yok. Şimdi Grains projesine dönelim ve orada yeni bir class ekleyelim, ismine de "InsufficientStockNotificationGrain" verelim.

Dikkat ederseniz StatelessWorker attribute'u var. Sadece bunu eklememiz yeterli. Bu attribute sayesinde bu grain birden fazla aktive olabilir ve hiçbir state'i yoktur.

Grain'ler Arası İletişim

ProductStockGrain'ine dönüp Decrease metoduna yetersiz stok kontrolünü ekleyebiliriz. Eğer stok yetersizse, ProductNotificationGrain'e mesaj gönderebiliriz.

Dikkat ederseniz; bir grain'den farklı bir grain'e mesaj göndermek için GrainFactory class'ını kullanıyoruz.

Persisted Grain'ler

Grain'ler varsayılan olarak state'lerini bir kaynakta saklamazlar, sadece InMemory'de tutarlar. Bu durumda grain deaktif olursa state kaybolur. State'i saklamak için öncelikle storage provider tanımlamamız gerekli. SQL Server, MongoDb gibi provider'ları nuget'den yükleyerek kullanabiliriz. Örneğimizde MongoDB kullanacağız. Host projesine aşağıdaki nuget package'ı yükleyerek başlayabiliriz.

    dotnet add package Orleans.Providers.MongoDB -v 3.8.0

Program.cs dosyasını aşağıdaki gibi düzenleyelim.

4. satır : "ProductStockStorage" isimli bir storage oluşturuyoruz.

Grain'imizin state'i için serileştirilebilir bir class oluşturuyoruz.

ProductStockGrain class'ına dönüyoruz ve storage tanımını yapıyoruz.

1. satır : ProductStockGrain'e StorageProvider attribute'u ile storage'ın adını (ProductStockStorage) veriyoruz.

2. satır : ProductStockGrain'i, Grain yerine GenericGrain'den türetiyoruz. Generic tip olarak ProductStockState'i veriyoruz.

5. 10. 19. ve 27. satır : Artık grain'in state'ine field yerine State property'si üzerinden erişiyoruz.

14. ve 31. satır : Increase ve Decrease işlemlerinden sonra WriteStateAsync metotu ile state'i storage'a kaydediyoruz. Artık grain aktive olduğunda state MongoDBStore'dan yüklenecek.

Grain'in state'i saklandığında veritabanında örnek görüntü şu şekilde olacak.

Persisted Grains

Cluster Oluşturma

Hızlı bir şekilde test yapabilmek için cluster'ı localhost üzerinde oluşturmuştuk. Cluster'ı ve silo'ları da bir storage'a kaydedebiliriz.

Host projesine dönüyoruz ve UseMongoDBClustering tanımını ekliyoruz.

Silo'yu ayağa kaldırdığımızda veritabanında örnek görüntü şu şekilde olacak.

Persisted Cluster

İki tane silo ayağa kaldırdım. İkisininde status'unun Active olduğu görebiliriz. Eğer silo'yu durdurursanız status'un Died olduğunu görebilirsiniz.

Api projesine dönüyoruz ve aşağıdaki nuget paketini yüklüyoruz.

    dotnet add package Orleans.Providers.MongoDB -v 3.8.0

UseMongoDBClustering tanımını Api projesine de eklememiz gerekli. Api'ye istek gönderdiğimizde cluster'ın bilgilerine "ProductStockCluster" veritabanından erişilecek.

Grain Versiyonlama

Bu kısımda şöyle bir senaryo ortaya koyuyorum.

Senaryoya göre ProductStockGrain’ine Remove metotu ekliyoruz ve değiştirilen bu kodu canlıya gönderiyoruz.

Canlıda ise InMemory'de aktif olan ProductStockGrain'ler vardır. Peki bu aktif olan grain'lere remove isteği geldiğinde ne olur? Bir hata oluşur. Çünkü aktive olan bu grain'ler henüz Remove metoduna sahip değiller. Bu durumda ne yapacağız?

Neyse ki Orleans grain'leri versiyonlamaya imkan veriyor. Fakat bunun için bir kaç değişiklik yapmak gerekli.

Service Worker Grain'ler versiyonlamayı desteklemiyor.

IProductStock'a dönüyoruz ve Version attribute'u ekliyoruz. Bunun default'u 0'dır.

Interface'e eklediğimiz her metot ve bu metotların parametrelerinde yaptığımız her değişiklik için versiyonu artırmalıyız. Host projesine dönüyoruz ve "GrainVersioningOptions" tanımını yapıyoruz.

Microsoft tarafından önerilen konfigürasyon bu.

İki tane silomuz olduğunu varsayıyoruz.

Silo1'de ProductStockGrain'in V1 sürümü var. Grain'in bu sürümünde Remove metodu yok. Id'si 1 olan ProductStockGrain'i InMemory'de aktif halde.
Silo2'de ProductStockGrain'in V2 sürümü var. Grain'in bu sürümünde Remove metodu var.

1 id'li ProductStockGrain'ine Remove mesajı gelirse; bu grain Silo1'de deaktif olur ve Silo2'de aktif olur.
Aktif olmayan tüm ProductStockGrain'leri için mesaj gelirse bu grain'ler Silo2'de aktif olur.

Benim anlatacaklarım bu kadar. Daha fazla bilgi için video'yu izleyebilirsiniz. Örnek projeye github'dan erişebilirsiniz.

ahmetkucukoglu/actor-model-product-app

Product App with Actor Model

C#
5
0

Vesselam.

Yazıyı Paylaş

Yorum bırak

Yanıtla

Yanıtlamayı iptal et
Bu site reCAPTCHA tarafından korunmaktadır ve Google Gizlilik Politikası ve Hizmet Şartları geçerlidir. Yorumunuz başarılı şekilde gönderildi reCaptcha doğrulanamadı
Muhabbetle ASP.NET Core ile geliştirildi.