Microservice Mimarisinde Servisler Arası İletişim ve Event-Driven Mimari
Microservice mimarisi dağıtık servisler bütününü ifade ettiği için bu servisler arasındaki iletişim yapısı yavaşlık oluşturmayacak ve birbirinden olabildiğince bağımsız olacak şekilde konumlandırılmalıdır.
Her zaman microservice mimarisindeki servisler olabildiğince bağımsız olmalı tezi üzerinde durmuştuk. Bu tez her zaman geçerli ancak microservice mimarisinde bağımsızlıktan kasıt hiçbir microservice diğerinden “bilgi talep etmeden ya da bilgi almadan hayatına devam etmelidir” anlamına gelmiyor ve bunun pratikte karşılığı yok. Ancak bu bilgi alışverişinde microservicelerin birbirine olan bağımlılığının olabildiğince az tutulması servisler arası iletişimin senkron yapıdan olabildiğince asenkron yapıya evrilmesi de oldukça önemlidir.
Bu yazıda uygulamamıza kullanıcılar tarafından gelen istekleri değil microservicelerin kendi aralarındaki iletişimi çözümleyeceğiz. Microservice mimarisi uygulanan bir projede yazılım geliştirmenin en önemli yönlerinden biri aralarındaki iletişimdir; servisler çoğaldıkça süreç oldukça karmaşık hale gelebilir.
Monolitik mimaride direkt modüller arası bir iletişim söz konusuyken microservice mimarisinde farklı servisler arası iletişim bulunduğu için çözülmesi gereken zorluklar ve sorulanlarla karşılaşırız.
- Kodumuzda olduğu gibi microserviceler arasında da loosely coupled bir yapı elde etmemiz gerekiyor. Yani bir servisin diğerlerine olan bağımlılığı olabildiğince az olmalıdır.
- Deployment için diğer servislere ihtiyaç olmamalıdır.
- Bağımsızca test edilebilir olmalıdır.
Microservice mimarisinin amacı “loosely coupled” microserviceler oluşturmaktır ve microserviceler arası iletişim bu amaç için en önemli yapıtaşlarından biridir.
Microservice’den microservice’e olan iletişim için kabul edilmiş ve yoğun olarak kullanılan senkron ve asenkron iletişim olmak üzere iki tane temel yöntem bulunmaktadır. Tüm iletişim yöntemleri bu iki yapı üzerinden oluşturulmuştur. Farklı kullanım amaçları olduğu gibi farklı avantajları ve dezavantajları da mevcuttur. Yazılımda asla ve asla “kesin bu yöntem kullanılmalı” diye bir şey söz konusu olamadığı gibi her farklı ihtiyacın farklı bir çözümü mevcuttur. Bu iki yöntemden daha çok asenkron operasyonlar üzerine projeyi konumlandırmak önerilir ve senkron işlemlerden bazen kaçınamayacağımız da çok olasıdır. Microservice mimarilerinde bu iki yöntem genellikle birlikte kullanılmaktadır. Buna da hibrit mimari adı verilmektedir.
Senkron İletişim
Monolitik uygulamaların konsepti oldukça açık ve kolaydır. Client uygulamaya istekte bulunur, uygulama tek bir parçadan oluştuğu için işlemlerini tek bir koldan yaparak cevap döner. Microservice mimarisinde ise her zaman birbiriyle iletişim kuran birçok farklı servis vardır. Senkron iletişim ise microserviceler arası iletişimi sağlamak içi kullanılan yöntemlerinden biridir.
Senkron iletişimde bir microservice işlemlerini yaparken farklı bir servisten bir bilgi ihtiyacı olduğunda o servise istek (request) atar ve cevabını (response) bekler. Gelen cevap sonucunda ise işlemlerine devam eder.
Bir servis diğerine istekte bulunduğunda ve servis yanıtı geciktiğinde ya da hiç cevap veremediğinde istekte bulunan servis cevap beklediği için işlem başarısız olabilir. Böyle isteklerin artması da sistemde büyük bir yığılmaya ve dolayısıyla bozulmaya neden olacaktır. İşlemler başarılı bir şekilde hızlı bir yanıt alsa bile isteğin cevabını alana kadar gerekli diğer işlemler bekler ve engellenmiş olur. Elbette bu tarz durumlara karşı circuit breakers gibi farklı çözümler mevcuttur. Bütün bunlar iyi uygulanması için zaman ve efor maliyeti oluşturur.
Senkron iletişimdeki bloklanma, hata oluşma ve bağımlılık gibi nedenlerden dolayı mimariniz içindeki tüm senkron iletişimden olabildiğince sisteminizi arındırmamız gerekiyor. Ancak bu durum pratikte her zaman mümkün olmuyor. Bu yüzden senkron iletişimi olabildiğince azaltmak sistem için oldukça faydalı hale gelecektir.
Senkron işlemlerin, cevabın kesinlikle zorunlu olduğu durumlarda kullanılması büyük bir avantaj sağlar. Herhangi bir ödeme işlemi ve authentication işleminin farklı serviste konumlandığında kesinlikle senkron bir işlem olması gerekir, aksi takdirde işlemlerin doğruluğu kesinleşemez. Böyle ortamlarda diğer servislerin de sadece senkron iletişim yöntemini kullanması, sisteme farklı zorluklar getireceği kesindir. Sürekli ve sadece senkron iletişime ihtiyaç duyuyorsanız microservice mimarisinin size uygun olup olmadığını tekrar sorgulamalısınız.
Microserviceler arası senkron iletişimi uygulamak için en uygun protokol HTTP’dir. HTTP, REST veya SOAP ile uygulanabilir. Senkron işlemler için en yaygın kullanılan benim de tercihim olan Restful ile HTTP protokolü üzerinden haberleşmedir.
Senkron iletişimde, başka servislere rest çağrıları yapıyor olmak o microservice’in işlemlerine devam edebilmesi için istek attığı servise olan bağımlılığını gösterir. Bir rest çağrısı yapılırken cevap beklenir ve asıl olan işlemi gerçekleştirmek için cevap gelmesini beklemek işlemi bloklamaktadır. Bütüne baktığımızda bu durumda istemediğimiz bir yola girilmiş olur.
Microserviceler aralarında iletişimi kurarken, istek atılacak servisin ayakta olup olmadığı ve IP gibi bilgilere ihtiyaç duyar. Microserviceler, Service Discovery sayesinde iletişim kuracakları servislerin iletişim bilgilerine ulaşabilir ve servislerin durumlarını da izleyebilirler.
Senkron iletişimi daha iyi anlamak için, sipariş verilen bir platformun küçük bir parçasını canlandıracak basit bir mimari ile senkron iletişimi örneklendirelim.
- Servisler ilk olarak çalıştığında Service Discovery servisine kendilerini kaydeder.
- Bir ödeme gerçekleştiğinde Payment servisine bu request iletilir.
- Sonrasında bu bilgilere göre payment işlemi banka ile iletişime geçmek üzere banking servisine gönderilir ve bu gönderim yapılırken sadece servisin ismi bilinerek bu gerçekleştirir. Servis Discovery sayesinde ip ve port bilgileri gibi bilgiler gönderici servisi tarafından bilinmesine gerek kalmaz.
- Banking servisine gelen bilgiler ile ödeme gerçekleştiğinde banking servisi bir response üretir.
- Üretilen response’u hali hazırda cevabı bekleyen payment servisi alarak gerekli işlemleri kendi içinde gerçekleştirir ve kullanıcıya cevabını döner.
Payment servisinden request gönderilirken ya da banking servisinde bir hata oluşma durumunda fallback çalıştırılır ve böylece hata senaryoları da çözümlenmiş olur. Ancak payment servisi cevap alana kadar işlemde olan thread bloke olduğu unutulmamalıdır. Bu örneğin kodlarıyla birlikte gerçekleştirimine github üzerinden erişebilirsiniz.
Asenkron İletişim
Asenkron işlemlerde istekte bulunan servis, hizmet aldığı servisten yanıt beklemez. Böylece istekte bulunan servis hizmet aldığı serviste bağımlı hale gelmez ve bu da microservice mimarisinde tam olarak istediğimiz bir durumdur (Loosely coupled servisler). Asenkron istek başarısız olduğunda bile servis sorunsuzca çalışmaya devam eder. Başarısız olma durumu ise farklı tekniklerle çözümlenir ama bu durum herhangi bir işlemin devam etmesine engel olmaz. Yani istekte bulunan servis threadleri bloke olmamıştır.
Asenkron iletişim ayrıca bire çok (one-to-many) iletişim yapılabilmesine de olanak sağlar, yani bir mesaj atıldığında birden fazla servise ulaşması sağlanabilir. Senkron iletişimde ise client her bir farklı servise tek tek istekte bulunmak durumundadır.
Notification ve Request/Non-blocking Response
Notification asenkron iletişimin en basit formudur. Başka bir servise istek gönderilir ancak cevap beklemeksizin işlemlere devam edilir ve non-blocking olarak gönderilen işlem başarılı sayılır. Uygulaması ise diğer asenkron yöntemlere göre oldukça basittir. Direkt olarak diğer servise yapılan bir asenkron istek başarılı sayılarak bloklanmadan devam edilir ve dolayısıyla bağımlılık aza indirgenmiş olur yani loosely coupled yapıda olduğunu gösterir.
Notification örneğimizi senkron örneğimize benzer bir biçimde betimleyebiliriz. Burada gelen sipariş isteğini aldığımızda servis içi işlemleri yürütürken asenkron olarak paralelde Inventory ve Shop servislerine istekte bulunuruz. Ancak onların cevaplarını beklemeden, başarılı sayıp Response’u dönmüş oluruz. Bu durumda “ben işlemin gerçekleştiği haberini verdim, Inventory ve Shop servislerim bu haberi alıp doğru bir şekilde kendi işlemlerini gerçekleştirebilir” demenin yazılımcasıdır. Hata payı yüksektir ve diğer servislerin hata vermeden çalışacağını garanti etmemiz gerekir.
Request/Non-blocking Response örneğinde ise istek önderilir ve gönderici cevabı anında beklemeksizin işlemlerine devam eder ve sonrasındaki bir zaman diliminde isteği alan servis, isteğin cevabını gönderir. Diğer servis ne zaman cevap verirse çağrı o zaman biter, bu da kısmide olsa bağımlılığı getirir. Ancak cevap anında beklenmediği için yapılması gereken bağımsız diğer işlemlerin yürütülmesine olanak sağlanır. Yani bir nevi senkron iletişimde HTTP örneğinin asenkron hale getirilmesidir. Bunun avantajı ise servisin diğer servise istekte bulunup eninde sonunda cevabını alacak olmasıdır. Hem notification’da oluşabilecek risklere karşı bir kalkan görevi görmüş oluruz hem de asenkron bir istek olduğu için de asıl işlem bloklanmamış olur. Ancak cevap beklendiği için bağımlılıktan yine de kaçamamış oluruz.
Message-Driven Mimari ile Asenkron İletişim
Message-Driven mimarisinde HTTP ile iletişimin aksine, ilgili servisler birbirleriyle doğrudan iletişim kurmaz. Mesaj tabanlı mimaride publish/subscriber ilkesi üzerine konumlandırılmış bir yapı mevcuttur.
Mesaj tabanlı iletişim birebir iletişimde kullanıldığı gibi bire çok (one-to-many) iletişimde de kullanılabilir. Sistem içerisinde bir gönderici için bir ya da birden fazla alıcı bulunabilir. Sadece microservice mimarisinde değil genel kullanılmada hitap eden bir mimaridir. Bu yöntemde mesaj alışverişi için kullanılabilecek kanallar mevcuttur. Bir kanal üzerinden publisher, göndermek istediği mesajı yayımlar ve (subscriber(s)) alıcı ya da alıcılar bu mesajın hangi kanal aracılığıyla geldiğini ve hangi aksiyonu almaları gerektiğini bildiği için sürekli dinleyici konumdadırlar. Böylece kanaldan gelen mesaj subscriber tarafından alınarak gerekli işlemleri kendi üzerinde yapabilecek konuma gelir.
Message-Driven mimari ile birlikte microservice haberleşmesi anlamında bağımsız bir iletişim söz konusu olmuş olur. Bu yöntemin en büyük avantajı ise servisler arasında çok yüksek düzeyde bir ayrışma (highly decoupled) oluşturması ve mesajlaşma için message-bus ya da message queue’ların kullanılmasıdır. En çok rağbet gören ve kullanılanlar ise Kafka ve Rabbit-MQ’dur. Benim izlenimlerime göre özellikle microserviceler arası iletişimde Kafka daha fazla kullanılmaktadır.
Mesaj tabanlı iletişimde; bir servis sadece bir mesaj gönderir ve o mesajı hangi servislerin alacağından ve nasıl işleneceğinden tamamen habersizdir. Microservicelerde tam olarak istediğimiz şey de budur. Bir servis diğer servislerden ve mesajlarıyla ne yaptıklarından haberdar olmamalıdır. Kanalın abonesi herhangi bir nedenle ayakta değilse o servis bu mesajları tekrar alana kadar mesajlar bekletilir ve sonrasında alıcı aktif duruma geldiğinde mesajları alarak işleyebilir.
Event-Driven Mimari ile Asenkron İletişim
Event-Driven mimari microservice ekosisteminde servislerin birbirine olan bağını tamamen birbirinden bağımsız hale getirerek ortadan kaldırmayı amaçlar. Yani amaç, bağımsız microservicelerden oluşmuş bir cumhuriyettir. Servislerin ortak bir mesaj yapısını bilmesi gereken mesajlaşma modelinin aksine, event odaklı bir yaklaşımda mesajın tam olarak ne olduğu ve mesaj sonucunda her servis ne yapmalı gibi bir duruma ihtiyaç yoktur. Servisler arasındaki iletişim, servislerde oluşan olaylar sonucunda gerçekleşir.
Event-Driven mimari asenkron HTTP isteklerle birlikte kullanılabiliyor olsada bu yöntem oluşacak riskler ve bağımlılıklar açısından pek tercih edilmez. Event oluştuğunda request atılacak servisler bilinmelidir ve hata durumlarına karşı retry mekanizmaları geliştirmek gereklidir.
Event-Driven mimari yine message-driven mimarideki tüm olumlu yönleri barındırmaktadır. İşlemler genelde Message bus’lar aracılığıyla yürütülür. Ana konsept aslında her bir oluşan durumun message bus’a iletilmesidir.
Event-Driven mimaride her bir olay bir haber niteliği taşır. Bir serviste bir durum değişikliği veya kayda değer bir işlem gerçekleştiğinde diğer servislerin haberdar olabilmesi için cevap beklemeksizin asenkron biçimde bu olayın bildirilmesi anlamına gelir.
Event-Driven mimarisinin microservice mimarisiyle birlikte yoğun kullanımı sonucunda Event Driven Microservice (EDM) kavramı ortaya çıkmıştır.
Sistemdeki her servis alacağı mesajın hangi olaya karşılık geldiğini bilmesi dışında başka bir model bilgisine ihtiyaç duymaz, böylece olacak olaya karşı hangi işlemleri yapacağına kendi karar verir ve işlemlerini tamamlar.
Even-Driven’ı Banabi, Getir gibi market uygulamaları üzerinden, siparişin tamamlandı bilgisinden sonra oluşabilecek durumlara ilişkin örnekleyebiliriz. Bir sipariş tamamlandığında işlem tamamlandı bilgisi diğer servislerden bilgi beklemeksizin kullanıcıya bildirilir. Ama sipariş oluştuğunda neler yapılmalıdır kısaca bir göz atalım.
Sipariş oluştuğunda;
- Mağaza (Market,Depo) bilgilendirilmeli,
- Kullanıcıya siparişin oluştuğuna dair e-mail atılmalı,
- Kurye siparişi teslim edebilmek üzere bilgilenmeli ve bu sırada başka sipariş almamalı,
- İlgili ürünler mağaza stoğundan düşürülmeli,
- …
Event-Driven mimarisinin izinden gittiğimizde burada siparişin oluştuğu bilgisi message bus’a iletilir ve Order servisi siparişin oluştuğu bilgisi dışında bir şeyle ilgilenmez ve buna gerekte kalmaz.
- Order service’inden Order Created event şeklinde Message Bus’a bildirilir.
İletilen mesaj sonrası order olaylarıyla ilgilenen servisler hali hazırda dinleyici konumunda oldukları için bu mesajı message Bus’tan almış olur.
- Email servisi aldığı event’ı kendi içerisindeki bilgiler ile birleştirerek ilgili kullanıcıya email göndermektedir.
- Paralelde ise Courier servisi aracılığıyla kurye atama işlemleri gerçekleştirilir
Böylece başka bir bilgiye ihtiyaç duyulmadan kurye işlemleri de tamamlanmış olur.
- Yine paralelde siparişin oluştuğu bilgisi Shop servisine düşmüştür, oluşan bilgi ile birlikte shop servisi üzerinden işlemler yapılır ve siparişin içeriklerine uygun şekilde sipariş hazırlanmış olur.
Böylece tek bir olaydan sonra her bir servis ilgili işlemi kendi üzerinde gerçekleştirir ve bu işlemlerin her biri birbirinden bağımsız ve asenkron olarak yapılmaktadır. Catalog servisi Order ile ilgili herhangi bir bilgiye ihtiyaç duymadığı için dinleyici konumunda değildir, dolayısıyla order’ın oluşma süreciyle ilgilenmez.
Örnek üzerinden de anlaşılabileceği gibi asenkron olarak tüm işlemler bir olaya uygun şekilde bağımsız olarak gerçekleştirilebilmektedir. Servislerde oluşabilecek her durum yeni bir olay olarak kabul edilir ve message bus’a iletilir. İlgili servisler o olayı dinler ve paralel bir biçimde kendi işlemlerini tamamlarlar. Örneğe kod örnekleriyle birlikte github üzerinden ulaşabilirsiniz.
TL, DR;
Microservice mimarisinde asenkron ve senkron olmak üzere iki iletişim yöntemi kullanılmaktadır. Senkron iletişim HTTP üzerinden genellikle Rest ile yürütülmektedir. Senkron iletişim servisler arasındaki iletişimin kesinlikle tamamlanması gerektiği ve cevap beklendiği durumlarda kullanılmaktadır. Geri kalan tüm durumlarda Asenkron iletişim yöntemi tavsiye edilir. Asenkron iletişim için en iyi yol Event-Driven mimarisiyle birlikte message bus üzerinden iletişimin konumlandırılmasıdır. Event-Driven mimari ile her bir olay message bus’a iletilir ve o event ile ilgili servisler bu event’ı dinleyerek gerekli işlemleri kendi üzerlerinde başka bir bilgiye ihtiyaç duymadan, bağımsız ve paralel olarak yürütebilir.