Microservice Ekosisteminde Yolculuk: Stratejik Veri tabanı Seçiminden Dockerize Entegrasyona

Journey through the Microservice Ecosystem: From Strategic Database Selection to Dockerized Integration

Cihat Solak
Intertech
10 min readFeb 15, 2024

--

Microservices mimarisi, bir uygulamayı bağımsız hizmetlere bölen ve her bir hizmetin belirli bir işlevselliği gerçekleştiren bir yaklaşımdır. Scale, Scale up ve Scale-out terimleri, bu mimarideki büyüme ve performans optimizasyonuyla ilgilidir.

Mikroservis mimarisi, yazılım uygulamalarını modüler hizmetlere bölen bir yaklaşımdır.

Scale: Ölçeklendirmek.

Scale-up, servislerin çalıştığı sanal donanımın özelliklerini artırmayı ifade eder. Örneğin, bir makineye 8 gigabyte yerine 16 gigabyte bellek ekleyebilirsiniz.

Projenin çalıştığı sunucunun belleğini ve işlemci çekirdek sayısını artırmamız birer yöntemdir. Örneğin, 16 çekirdeği 32 çekirdeğe çıkarmak veya 4 çekirdeği 8 çekirdek gibi arttırmak gibi konfigürasyon ayarlarını yükseltmek de bu kapsamda değerlendirilebilir.

Scale-out ise, artan ihtiyaca cevap vermek amacıyla paralel olarak yeni örnekler veya konteynerlar oluşturmayı içerir. Dolayısıyla uygulamanın aynısından bir instance daha ayağa kaldırmaktır.

Her bir mikroservis, bağımsız bir hizmettir ve bu mikroservisler bağımsız olarak dağıtılabilir.

Microservice Veri tabanlarında Nasıl Veri Tutulmalı? Sence?

Görüldüğü gibi, her iki farklı veri tabanında da CustomerName ve CustomerSurname alanları bulunmaktadır. Veri tekrarı gibi görünse de, her bir mikroservisin kendi kendine yetebilmesi ve diğer mikroservislere bağlı olmaması önem arz etmektedir.

Eğer bank microservice’inde CustomerName ve CustomerSurname alanları yerine CustomerId alanını tutsaydım, müşteri bilgileri için mutlaka customer microservice’ine ihtiyaç olacaktı. Bu istenmeyen bir durumdur. Mikroservisler, görevlerini yerine getirebilmek için ihtiyaçları olan verileri kendi içerisinde tutabilmelidir. Aynı veri, birden fazla mikroserviste bulunabilir. Eğer bir mikroservis, görevini yerine getirmek için gerekli olan verileri tutarsa, diğer bir mikroservisin ayakta olup olmamasıyla ilgilenmez.

Örneğin, mimari içerisinde yer alan 100 adet mikroservisin 99 tanesi down duruma geçse bile kalan 1 adet mikroservisin yaşamına devam etmesi gereklidir. HERHANGİ BİR MİKROSERVİS, DİĞER BİR MİKROSERVİSİN VERİ TABANINA KESİNLİKLE DİREKT OLARAK ERİŞMEMELİDİR. Eğer bir mikroservis illa ki bir veri alması gerekiyorsa, Bank servisi, Customer servisinin endpointlerini kullanmalıdır. Yani, Customer doğrudan Bank’ın veritabanına erişmemelidir; API aracılığıyla haberleşmelidir.

Microservisler Arasında Distributed Transaction’nı Nasıl Yönetebiliriz?

Distributed transaction’ları mikroservisler arasında nasıl yönetebileceğimizi düşündüğümüzde, her mikroservisin kendi veri tabanına sahip olduğu ve bu veri tabanlarının bazı alanlarının aynı olabileceği bir senaryo ortaya çıkar. Örneğin, A mikroservisi veri tabanında bir değişiklik yapıldığında, bu değişikliğin B mikroservisi veri tabanında nasıl yansıtılacağı önemlidir. Bu tür durumları ele alırken distributed transaction’ları nasıl gerçekleştirebileceğimizi düşünmek önemlidir.

Eventual Consistency (Nihai Tutarlılık) prensibi, eğer kullanıcıların farklı verilere anında erişimi bir sorun oluşturmuyorsa devreye girebilir. Örneğin, A ve B mikro servisleri arasında bir senaryo düşünelim. A mikroservisinde ürün adı “cihat” olarak güncellendiğinde, B mikroservisinde bu güncelleme bir süre sonra gerçekleşebilir. Bu durumda, kullanıcılar belirli bir süre boyunca farklı verileri görebilirler. Ancak belirli bir zaman sonra senkronizasyon sağlandığında, veri bütünlüğü geri kazanılır.

Bu yaklaşım, mikroservis mimarisi içinde veri tutarlılığı sağlamak için kullanılan bir stratejidir. Her iki durumda da, zaman içinde senkronizasyonun gerçekleşmesi, kullanıcıların tutarlı verilere erişimini sağlar.

Neden NoSQL (MongoDB, Redis vb.) Tercih Etmeliyiz?

Eğer yukarıdaki tabloya benzer bir tabloya sahipseniz ve bu tablonun sütunları dinamik bir şekilde artacaksa, yani bugün 5 sütunla başlıyorsanız yarın 10 veya 20 sütuna doğru genişleyecekse ve ayrıca ilişkisel bir yapıya ihtiyacınız yoksa, NoSQL veritabanı kullanmak daha avantajlı olabilir.

Eğer dinamik olarak artacak sütunlara sahip bir tabloyu PostgreSQL veya MSSQL gibi ilişkisel bir veri tabanında tutarsanız, ilerleyen zamanlarda her eklediğiniz sütun için ekstra bir migration yapmak durumunda kalabilirsiniz. Yani yeni bir sütun eklediğinizde, sınıfa bir property ekleyip migration ile veri tabanına yansıtmak zorunda kalacaksınız.

Örneğin, bir ‘Bluetooth’ kolonu tutmak istiyorsunuz ve 50.000 satırlı bir tabloda sadece 4 satırda bu özelliğe ihtiyacınız var. Bu durumda, boolean bir değer olarak Bluetooth sütunu ekleyeceksiniz. Ancak bu, her eklenen kayıt için default değeri yönetmek zorunda olduğunuz anlamına gelir. Bu özellikle sürekli artan sütun sayısında ciddi bir yük getirebilir.

Bunun yerine, MongoDB gibi bir NoSQL veri tabanında bu özelliği tutarsanız, koleksiyonlarınız (tablolarınız) içindeki satırlar JSON belgesi olarak saklanır ve dinamiktir. Yani, bugün 5 özelliğe sahip başladıysanız, yarın 6. özelliği kodlama tarafında eklediğinizde veri tabanı tarafında herhangi bir işlem yapmanıza gerek kalmaz. Default bir değer belirtmeniz veya migration yapmanız gerekmez. Kodda ekleyip kaydetmeye devam edebilirsiniz.

Koleksiyonun bir satırında 10 özelliği, 8. satırında 50 özelliği olabilir, diğer bir satırında ise 3 özelliği tutabilir. Burada herhangi bir sınırlama olmadığı için rahatlıkla bu özelliklerin sayısını ilgili satırlarda artırabilir ya da azaltabilirsiniz.

Dapper ve Contrib

Hafif, hızlı, kullanımı basit ve performansı yüksek olan bir ara yüze sahiptir, ayrıca herhangi bir veri tabanıyla kullanılabilir.

Dapper ile Entity Framework (EF) arasındaki farkları düşündüğümüzde, Dapper’da SQL cümlelerini kendiniz yazmanız gerekirken, EF’de LINQ sorguları sayesinde SQL cümlelerine otomatik çevrim yapılabilmektedir. Dapper, SQL cümlelerini manuel olarak yazmak gerektiği için daha düşük seviyeli bir kütüphanedir.

Dapper’da, bellekte izlenen (track edilen) veri bulunmaz, yani bellek üzerinde çalışmaz. Ancak EF Core’da, bir entity üzerinde create, update, delete gibi işlemler gerçekleştirildiğinde ve izleme özelliği kapatılmamışsa, bu işlemler bellekte takip edilir. Ardından, SaveChanges metodunu çağırdığınızda bellekte izlenen veriler veri tabanına yansıtılır. Elbette, EF Core’da izleme özelliği kapatılabilir veya SQL cümleleriyle işlem yapılabilir, ancak Dapper, bu konuda daha hafif olması nedeniyle tercih edilebilir bir alternatif olarak öne çıkar.

Dapper: .NET platformu için hafif ve hızlı bir ORM (Object-Relational Mapping) kütüphanesidir, basit SQL sorguları kullanarak veritabanı işlemlerini gerçekleştirmek için tasarlanmıştır.

Dapper.Contrib: Dapper’ı genişleten bir pakettir ve CRUD işlemlerini (Create, Read, Update, Delete) kolaylaştırarak veri tabanı tablolarındaki kayıtları sınıflar arasında eşlemeyi sağlar.

Domain Driven Desing Nedir? (Business Kurallar)

Bir projenin karmaşıklığını çözmek ve sürdürülebilirliğini sağlamak için kullanılan bir yaklaşımdır. Bu metodoloji, projede bulunan iş kurallarını etkili bir şekilde uygulamak, yönetmek ve uzun vadeli olarak sürdürebilmek amacıyla geliştirilmiştir.

Bir projede DDD kullanmak her zaman gerekli olmayabilir. Örneğin, bir blog sitesi gibi iş kurallarının çok karmaşık olmadığı projelerde DDD yaklaşımına ihtiyaç duyulmayabilir.

Ancak, iş süreçleri karmaşık ve çok sayıda iş kuralı içeriyorsa, DDD yaklaşımı bu karmaşıklığı çözmek ve projenin başarılı bir şekilde yönetilebilmesini sağlamak için ideal bir seçenek olabilir.

DDD, projenin temelinde yer alan iş alanını anlamak ve bu iş alanına odaklanmak üzerine kuruludur. Bu sayede, iş süreçleri ve kuralları daha açık bir şekilde tanımlanabilir, uygulanabilir ve sürdürülebilir hale getirilebilir.

Özetle, DDD, iş kurallarının yoğun olduğu ve karmaşıklığın çözümlenmesi gereken projelerde etkili bir yaklaşım olarak öne çıkar. Ancak, her projede bu yaklaşımın kullanılması gerekli olmayabilir, basit projelerde ise daha az gereklilik gösterebilir.

Ubiquitous Language Nedir? (Her Yerde Bulunan Ortak Dil)

Ubiquitous Language, herkesin ortak bir dili konuştuğu bir yaklaşım olarak, Domain Driven Design (DDD) metodolojisinin temel prensiplerinden biridir. Bu, bir proje ekibinde yer alan geliştiricilerin ve alan uzmanlarının, özellikle domain expert olarak adlandırılan konu uzmanlarının, aynı terimleri aynı anlamda kullanmalarını sağlayan bir iletişim stratejisidir.

Örneğin, bir fatura oluşturma servisi geliştirdiğimizi düşünelim. Sol tarafta geliştirici ekibimiz var, sağ tarafta ise faturayla ilgili derin bilgiye sahip olan domain expert’ler bulunuyor. Domain expert’ler genellikle yazılım bilgisine sahip olmayan, ancak belirli bir alanda uzmanlaşmış kişilerdir.

DDD prensiplerine göre, bu iki taraf arasında etkili iletişim kurabilmek için ortak bir dil oluşturmak önemlidir. Yani, domain expert “fiş” dediyse, geliştirici ekibi de aynı kavram için “makbuz” dememelidir. Bu, karşılıklı anlaşılmazlıkları önlemek ve projenin tutarlı ve doğru bir şekilde hayata geçirilmesine olanak tanımak adına kritiktir. Ortak dil, benzer anlamlara gelen farklı terimlerin kullanılmamasını sağlar, bu da projenin başarılı bir şekilde ilerlemesine katkı sağlar.

Bounded Context Nedir?

Bounded Context, Domain Driven Design’ın bir kavramıdır ve projedeki dilin ve modellerin belirli sınırlar içinde tanımlandığı bir bağlamı ifade eder. Her bağlam, kendi içinde geçerli olan özel terimlere ve kurallara sahiptir. Bu, farklı bağlamlar arasındaki dil çeşitliliğini ele alarak, her bir bağlamın tutarlı bir şekilde anlaşılmasını ve iletişim kurulmasını sağlar. Bounded Context, projenin karmaşıklığını azaltmaya ve anlaşıla bilirliği artırmaya yönelik bir araçtır.

Gateway

Client isteklerini ilgili mikroservise yönlendiren ve genellikle bir API uygulaması olarak tasarlanan bir yapıdır. Herhangi bir client, mikroservislere doğrudan erişmek yerine bu işlemi bir gateway üzerinden gerçekleştirir. Bu ara katman, bir dizi avantaj elde etmemizi sağlar.

Örneğin; yetkilendirme (Authorization), önbellekleme (Caching), günlük kaydı (Logging), sorgu hızını sınırlama (Rate Limiting), yük dengeleme (Load Balancing) gibi işlemler gateway üzerinden yönetilebilir. Ocelot, bu tür bir gateway kurulumunu sağlamak için kullanılabilen açık kaynaklı bir kütüphanedir.

{
"DownstreamPathTemplate": "/api/{everything}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 7006
}
],
"UpstreamPathTemplate": "/user-service/{everything}",
"UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ],
"UpstreamScheme": "https",
"AuthenticationOptions": {
"AuthenticationProviderKey": "Bearer",
"AllowedScopes": []
},
"SwaggerKey": "User"
}

DownstreamPathTemplate: Gateway’den ilgili microservise gidecek URL.

UpstreamPathTemplate: Gateway’e gelen URL. (Client’dan gelen istek)

MassTransit (RabbitMQ) Framework

Masstransit, distributed sistemler için tasarlanmış bir framework’tir. Hata yönetimi, yeniden deneme, bekleme ve işlem yönetimi gibi bir dizi özelliği kendi bünyesinde barındırarak, kullanıcıların bu süreçlerle uğraşma yükünü azaltır. Bildirimleri framework’e ilettiğimizde, Masstransit otomatik olarak bu işlemleri gerçekleştirir. Ayrıca, framework birçok mesaj kuyruk sistemiyle uyumlu çalışır; örneğin, Amazon, Azure (Service Bus), RabbitMQ gibi birçok kuyruk sistemiyle entegrasyon sağlanabilirsiniz.

Masstransit, mesaj kuyruğuna gönderilecek mesajlar için iki temel tür önerir: command ve event. Eğer RabbitMQ gibi bir kuyruk sistemine gönderilecek mesajı sadece tek bir servis işleyecekse, command tipinde mesaj göndermek uygun olacaktır. Eğer gönderilen mesajı birden fazla farklı servis işleyecekse, o zaman event tipinde mesaj göndermek gerekebilir.

Event’ler genellikle geçmiş zamanla ilişkilendirilir ve belirli bir durumun gerçekleştiği bir olayı temsil eder. Örneğin, UserCreated isimli bir event, yeni bir kullanıcının oluşturulduğu anı ifade eder. Bu event’i dinleyen farklı servisler, kendilerine düşen görevleri yerine getirebilirler. Örneğin, A servisi kullanıcıya e-posta gönderebilir, B servisi bir kupon oluşturabilir, C servisi bir rapor hazırlayabilir. Bu sayede aynı mesaj, birden fazla servis tarafından işlenir.

Eğer aynı mesajı birden fazla servis işleyecekse, genellikle event tipi kullanılır. Ancak, aynı servisin birden fazla örneği olsa bile yine command kullanılabilir. Örneğin, bir sipariş servisinden üç farklı konteynerda üç farklı örneğini ayağa kaldırabilirsiniz. Kuyrukta ise, sipariş detaylarıyla ilgili mesajlar varsa, bu mesajlar birer birer işlenir. Aynı mesaj iki kez işlenmez, böylece sipariş sadece bir kere işlenir. Bu yaklaşım, özellikle ödeme gibi hassas işlemlerle ilgili mesajlar gönderirken, istenmeyen durumları önlemek ve bir olayın yalnızca bir kez işlenmesini sağlamak açısından önemlidir.

Mesaj iletim tipleri arasında iki farklı kavram bulunmaktadır. Send ifadesi, bir komutu (command) mesaj kuyruğuna iletmek anlamına gelir. Publish ifadesi ise bir olayı (event) birden fazla servisin işlemesi için mesaj kuyruğuna göndermek anlamına gelir.

Sendismi genellikle ISendEndpoint interface’i üzerinden kullanılarak komutları göndermek için kullanılır. Publish ise IPublishEndpoint arayüzü üzerinden olayları göndermek için kullanılır. Publish metodu bir event beklerken, Send metodu bir command bekler. Bu noktada dikkat edilmesi gereken önemli bir konu, gönderilen komutlar ve olaylar için Publisher ve Receiver’ın aynı namespace’e sahip olmasıdır.

Command’lar farklı namespace’lere sahip olmamalıdır. Yani, Publisher kendi komutunu oluşturmak için kendi namespace’ini kullanırken, Receiver kuyruktan mesaj almak için kendi namespace’ini kullanmamalıdır. Genellikle, bir ‘Shared Library’ içinde bir komut tanımlanır ve hem Publisher hem de Receiver aynı komutu kullanır. Bu sayede her ikisi de aynı namespace üzerinden komuta erişebilir, çünkü Masstransit, Publisher ve Receiver’ın aynı namespace üzerinden komut veya olay almayı bekler.

TOKEN EXCHANGE

Örneğin iki mikro servis, A ve B, ile birlikte bir müşteri (client) bulunmaktadır. Müşteri, bir token ile birlikte A mikro servisine istek göndermektedir. A mikro servisi, bu isteği aldığında, B mikro servisine yönlendirilen bir istek başlatır. Yani, müşteri doğrudan B mikro servisine erişim sağlamaz; bu durumda A ve B mikro servisleri arasında bir token takası gerçekleşebilir.

Bank Service, dışarıdan gelen isteklere kapalı olacak ve sadece Customer Service den istek kabul edecek bir yapıda oluşturulmuştur. Bu durumda, client tarafından gönderilen token, client’ın storage’dan alınacak ve bu token kullanılarak Customer service istek yapılacaktır. Eğer Customer service Bank service’e bir istek yapmak isterse, mevcut token’ı Identity Service’a gönderecek, yeni bir token alacak ve ardından Bank service’e istekte bulunacaktır.

Token Exchange, mevcut bir token’ı kullanarak, bu token’ın sahibi tarafından belirlenen diğer mikroservislere istek yapabilmek için yeni bir token almayı amaçlar.

Bu yaklaşımın avantajı nedir? İlk seferde kullanıcı adı + şifre ya da e-posta + şifre gibi bilgilerle dört farklı mikroservise istek yapacak bir token almak yerine, sadece iki mikroservise istek yapabilecek bir token alırız. Sonuç olarak, bu token ele geçirildiğinde sadece iki mikroservise istek yapılabilir. Oysa eski yöntemde token ele geçirildiğinde dört farklı mikroservise istek yapılabilirdi. Bu, güvenlik açısından önemlidir.

DOCKERIZE

Docker Compose, birden fazla konteynerle çalışma ihtiyacımız olduğunda işleri kolaylaştıran bir araçtır. Docker kurulumuyla birlikte gelir ve birden fazla konteyneri docker compose komutlarıyla rahatça yönetmemize olanak tanır. Bu aracın sunduğu avantajlardan biri, tüm ayarları tek bir dosya üzerinden yapabilme olanağımızdır. Bu dosya, her bir konteynerın hangi porttan mapleneceği, volume’lerin nasıl olacağı gibi detayları belirleme konusunda bize esneklik sağlar.

Docker Compose, genellikle docker-compose.yml ve docker-compose.override.yml olmak üzere iki dosyayı varsayılan olarak okur. Örneğin, docker up gibi docker compose komutları kullanıldığında, Docker Compose aracı bu iki dosyayı varsayılan olarak göz önüne alır. docker-compose.yml dosyasında genel ayarlamalar yapılırken, docker-compose.override.yml dosyasında daha detaylı ayarlamalar gerçekleştirilir. Örneğin, MongoDB kullanılacaksa, MongoDB imajı docker-compose.yml dosyasında belirtilir. Override dosyasında ise ayağa kalkacak konteynerın hangi volumeye ve hangi porta mapleneceği gibi daha detaylı ayarlamalar yapılır. Yani, Docker Compose’da temel komutlar genellikle docker-compose.yml dosyasında, daha detaylı komutlar ise docker-compose.override.yml dosyasında yer alır. Bu, yönetimi kolaylaştırmak için kullanılır.

docker-compose up -- containerları ayağa kaldırır.
docker-compose down -- her şeyi siler.

--

--