loading...

DDD ve Mikroservis Kavramları Üzerine— Bounded Context Mikroservis İlişkisi ve Tutarlılık

canerpatir profile image Caner Patır Originally published at Medium on ・7 min read

DDD ve Mikroservis Kavramları Üzerine— Bounded Context Mikroservis İlişkisi ve Tutarlılık

Bir önceki yazıda bounded contextlerin domaine ait iş kurallarının mantıksal sınırları olduğundan bahsetmiştik (bkz: DDD ve Mikroservis Kavramları üzerine — Bounded Context). Yani bounded contextleri modellerken business kuralları üzerinden konuşuruz. Mikroservis için ise altyapısal gereksinimler gözetilerek oluşturulmuş teknik sınırlardır diyebiliriz. Yani, ölçeklenebilirlik, erişilebilir, yönetilebilirlik gibi mimari konular önemlidir. Buradan yola çıkarak bounded contextler ve mikroservisler arasında bire bir ilişki kurmanın anlamlı olduğunu söyleyemeyiz. Yani bir bounded context bir servistir gibi bir kural yoktur. Bir servis bir bounded context olabileceği gibi bir bounded context birden fazla servisi kapsayabilir ya da bir servis birden fazla bounded contexti kapsayabilir.

Bounded contextin ürettiği asıl değer domain in farklı mantıksal alt birimleri içerisinde ortak bir dil oluşturmak ve bu sınırlar arasındaki ilişkiyi tanımlamaktır. Teknik ihtiyaçların öncelenmesi şartı ile servis sınırlarının belirlenmesi konusunda da etkili rol oynayabilir.

Ubiquitous Language

Bir kavramın farklı bounded contextler için sağladığı anlam farklı olabilir. E-ticaret domaininden örnek verecek olursak. Ürün kavramı, katalog bilgisi yöneten ekip için müşterinin satın alma deneyimini etkileyen, renk, beden, görsel gibi temel niteliklerin yönetimine dayalı iş kurallarını barındırır. Teslimat ekibi için ise sadece teslimatı sağlanacak bir paket içeriğidir ve renk, beden gibi özellik değerleri bu ekip için bir anlam ifade etmeyebilir. Burada, paketin en hızlı ve az maliyetli şekilde müşteriye ulaştırılması için gerekli iş kuralları önceliklidir.

Peki bu farklılık teknik anlamda geleneksel bir uygulamada ne ifade eder ?

Yukarıdaki görselden de faydalanarak, ilişkisel bir veri modeli üzerinde, product ve delivery nesnelerinin birbiri ile ilişkisel cebir (relational algebra) normalizasyon kuralları ile bağıntılı olduğunu gözlemlemekteyiz. DeliveryProduct tablosunda Product ve Delivery nesneleri arasındaki many-to-many ilişki olduğunu anlayabiliriz. Peki ya Product ve Delivery biribirinden bağımsız datastorelara sahip ayrı bounded context’ler içerisinde yer alsaydı modelleri nasıl ilişkilendirecektik?

Normalizasyon, ilişkisel veri modelinde bir düzenleme tekniğidir. Tablolardaki veri tekrarlarından ve insert, update, delete işlemleri esnasındaki tutarsızlıklardan kaçınabilmek için tabloları belli sistematiklerle ayrıştıran bir yaklaşımdır.

Madem product iki farklı bounded context için farklı anlam ifade ediyor öyleyse product nesnesini iki farklı context için tekrarlamakta bir kusur yok diyebilir miyiz?

Aşağıdaki görsele göre; P_roduct_ nesnesi iki contextte de yer almaktadır. Ancak içerisinde bulundukları context için farklı iş mantığı kurallarını barındırıyor olabilirler. Örneğin; delivery context için product, kilo, adet gibi sadece teslimat kurallarına hizmet eden nitelikleri bünyesinde barındıran bir değer tipi(value object) iken, product context için kategori bazlı özellikler barındıran, göresel içerik gibi konularda kendine has iş kuralları olan, katalog içeriğini oluşturan bir bileşen olabilir.

Veri tekrarından kaçınmak için karmaşık normalizasyon kurallarına ihtiyaç duyuyorken, neden iki farklı databasede aynı veriyi tutuyoruz? Veri tekrarı tutarlılıkla ilgili problemlerle başımızı ağrıtmaz mı? Herhangi bir product nesnesinin “name’’i değiştiğinde delivery servisindeki product bu durumdan nasıl etkilenir? Örneğin sitemizin ürün detay sayfasında gördüğümüz ürün başlığı ile sipariş detay sayfasında gördüğümüz başlık arasında farklılık oluşmayacak mı? gibi bir sürü soruya gelin birlikte cevap bulmaya çalışalım.

Product ve delivery farklı servisler olarak tasarladığımızı düşünelim. Bu servisler birbirinden bağımsız databaselere sahip olabilirler. Bu durumda, iki farklı servis içerisinde yer alan product nesnelerinde yapılacak değişiklikler bir transaction olmaksızın gerçekleşecek ve veri tutarlılığı ile ilgili çözülmesi gereken yeni problemleri beraberinde gelecektir.

Tutarlılık (Consistency): Tutarlılık, sistemi bir durumdan başka bir duruma geçirirken farklı sistem birleşenlerinin o durumla ilgili referans bütünlüğü sağlamasıdır. Örneğin, bir e-ticaret sisteminde kullanıcı bir ürün için başarılı bir sipariş kaydı oluşturdu ise aynı stoğu başka bir kullanıcıya da satmamak adına, ilgili ürüne ait stok bilgisinin sipariş miktarı kadar azaltılması beklenir.

Ancak, bu iki contextin business gereksinimleri açısından birbirinden ayrı olduğuna kanaat getirdiysek, product ve delivery birbirinden servis seviyesinde ayrılmalı mıdır? Doğru cevap yine yok :) Bu durum mikroservis dünyasının getirdiği problemlerden birisidir. Ancak şunu bilmeliyiz ki bu iki contexti business ihtiyaçları doğrultusunda ayırmamız, servis seviyesinde ayırmamız zorunluluğunu doğurmaz. Eğer servis seviyesinde bir ayrıştırma düşünüyorsak önceliklendirmemiz gereken konu ölçekleme gibi teknik ihtiyaçlardır. Örneğin; müşterilerin satın almadan çok kataloğunuzu gezme eğilimi gösterdiği zamanlarda product servisinizin daha fazla trafik aldığını gözlemleriz. Bu durumda product servisini ölçekleme zorunluluğu doğar. Kampanya gibi siparişin çok daha yoğun olduğu dönemlerde teslimat servislerinin ölçeklenmesi gerekir. Bu tarzda daha esnek bir ölçeklendirilebilirlik altyapısı istersek bu iki bileşeni servis olarak ayırabiliriz. Bu durum, ACID transactionlardan vazgeçmek zorunluluğu doğurabilir. Dolayısı ile tutarlılıkla ilgili yeni yaklaşımlara açık olmak gerekir. Önce tutarlılık neydi bir hatırlayalım.

Strong Consistency?

Geleneksel yaklaşımlarda tutarlılığı (consistency) sağlamak için atomic transactionlardan bolca faydalanırız. Transaction kapsamı içerisinde yapacağımız operasyonlar farklı nesneler üzerinde olsa bile atomik bir bütünlük içerisinde ele alınır. Böylelikle, sipariş kaydı oluşturuldu -> stock bilgisi güncellendi gibi bir operasyonda, “sipariş kaydı oluşturulamazsa stock bilgisi güncellenemez, stock bilgisi güncellenemezse sipariş kaydı oluşturulamaz” önermesini sağlamış oluruz. Operasyonlar topyekün olarak gerçekleşmiş olur (commit) ya da iptal edilir (rollback). Atomic transactionlar genelde database engine içerisinde ele alındığından, transaction kapsamı doğru tanımlandığı sürece bize bütünlük garantisi verir ve developerdan soyuttur. Veri bütünlüğünü düşünmek zorunda kalmayız. Database storage engine bunu bizim yerimize halleder.

Peki ama modelimizde stock ve siparişler ayrı bounded contextlerse ve bunları ayrı servisler olarak dizayn edersek, tutarlılık konusunu nasıl ele alırdık? Aslında dağıtık sistemlerin uzun yıllardır baş etmeye çalıştığı problemlerin, mikroservislerle birlikte uygulama seviyesine kadar çıktığını görmüş olduk. Ancak, yeni olmayan bu problemlerin -bütün düşünce tarzımızı değiştirmemiz gerekse de- çözümlerinin de var olduğunu biliyoruz. Burada eventual consistency kavramı hayatımıza giriyor.

Eventual Consistency?

Yukarıdaki sipariş adımlarını sepet durumunu da işin içerisine katarak genişletecek olursak. Örneğin: Sipariş kaydı oluşturuldu -> stock bilgisi güncellendi -> kullanıcı sepet içeriği güncellendi. Bunu daha fazla detaylandırıp sürecimize daha fazla adım ekleyebiliriz hatta ödeme sürecini de buna dahil edebiliriz. Ancak transaction sınırı (transaction boundry) genişledikçe, özünde birbirinden bağımsız hareket edebilecek contextlerin bir arada yaşama zorunluluğu ortaya çıkmış olur. Bu durumun, db lock’larından kaynaklı getirmiş olduğu performans problemlerini de beraberinde getirecektir. Üstelik, ödeme gibi harici bir sistemi (banka servisleri) dahil etmemizi gerektirecek bir süreç, aynı transaction kapsamı içerisinde ele almamız mümkün olmayan adımları da işlememizi gerektirebilir. O halde, sipariş, stok, sepet ve ödeme içerisindeki iş mantıklarını birbirinden ayırma yaklaşımına gidebiliriz. Ancak, artık db level transactionları bıraktığımıza göre tutarlılığı bir şekilde sağlamamız gerekecektir. Bu durumda eventual consistent yani nihai anlamda tutarlı bir sistem dizayn etmemiz gerekir.

Nihai tutarlılık, değişen bir veri öğesinin değerinin, yeterli süre verildiğinde, tüm birimlerde aynı olacak şekilde dağıtılmasına dayalı bir tutarlılık yaklaşımıdır. Buna göre, tüm birimlerdeki değer, en son yapılan güncellemeyle tutarlı olacaktır. Öte yandan, yeterli zaman geçmeden, verinin birden fazla noktada tutarsız olacağını göz önünde bulundurmak gerekir. Bu duruma gecikme (latency) denir ve sistemin diğer aktörlerinin de (örneğin diğer mikroservislerin) bu gecikmeyi gözetecek şekilde davranmasını sağlamak gerekir. Şunu unutmamalıyız ki nihai tutarlılık nokta atış bir çözüm değildir ve uygun durumlarda tercih edilmelidir. Yani nihai tutarlılığın tercih edildiği sistemin gecikmeleri tolere edilebilecek karakteristiğe sahip olması gerekir.

Mikroservisler gibi dağıtık bilgi işlem ortamlarında, CAP teoremi tutarlılık(consistency), erişilebilirlik(availability) ve bölünebilme toleransını(partition tolerance) aynı anda garanti etmenin imkansız olduğunu ve geliştiricinin kendisi için en önemli üç taneden ikisini seçmek zorunda olduğunu varsayar. Buna göre, nihai tutarlılıkla birlikte A ve P yi tercih etmiş oluruz.

Consistency(Tutarlılık): Dağıtık bir sistemde bir bileşene bir veri yazıldıktan sonra diğer bir bileşene bu verinin değeri sorulduğunda, bu bileşenin aynı değeri sağlayabilmesidir.

Availability(Erişebilirlik): Dağıtık bir sistemde, herhangi bir istemci bir değer sorduğunda o değerin herhangi bir versiyonuna mutlaka erişebilmelidir.

Partition Tolerance (Bölünebilme Toleransı): Sistem birbirinden bağımsız çalışabilen birimlere bölünebilmeli ve bu birimlerden birine herhangi bir sebepten erişilemese bile istemci gecikmeyle de olsa cevap alabilmelidir.

Sonuç

  • Servislerin sadece teknik ihtiyaçlar gözetilerek oluşturulması da yanlıştır. Bu tutum business açısından zayıf servisler ortaya çıkmasına neden olabilir. Business açısından hiçbir sorunu çözmeyen ama DevOps maliyeti oluşturan servisler istemeyiz.
  • Consistency ve partition tolerence mikroservis gibi dağıtık mimarilerde bir ödünleşmedir (trade-off). Consistency problemleri bazen çok pahalı olabilir dolayısı ile mimari bunu tolere edebilecek bütünlükte tasarlanmalıdır.

Discussion

pic
Editor guide