Asenkron Mikro Servisler

Mikro servislerin kullanımı arttıkça, bloklanmış bir threadin etkilerini de daha fazla hissetmeye başladık. Fakat bu etkiyi azaltmanın yolları mevcut ve bu yollardan biri de asenkron ve baştan sonra bloklanmayan servisler yazmak. Bu yazıda elimden geldiğince asenkron servislerin yararlarından ve baştan sona asenkron bir servisin nasıl geliştirilebileceğinden (kısaca) bahsedeceğim.

Asenkron vs Senkron

Öncelikle asenkton ya da senkron derken neyi kastettiğimi bir örnek üzerinden anlatalım. Mesela, socket üzerinden byte array olarak gönderilen cümleleri okuyup String’e çeviren ve sonrasında da ekrana yazdıran bir uygulama geliştirdiğimizi varsayalım. Tabiki bunu bir çok şekilde yapabilirsiniz ama ben iki türüne odaklanmak istiyorum.

İlk uygulamamızda, uygulamamızı çalıştıran ana thread gider socketi açar ve veriyi beklemeye başlar. Veri alışını tamamladıktan sonra veriyi String’e çevirir ve ekrana yazdırır. Burada ana threadimiz veri gelene kadar bloklanmış bir şekilde bekleyecektir. İkinci yöntem ise, uygulamanızı çalıştırırsınız, uygulamanız gider soketi açar fakat socketi açarken, veri geldiğinde çalışmasını istediği fonksiyonu da belirtir. Bundan sonra ana threadiniz veri gelene kadar özgürdür ve başka işler için kullanılabilir, yani bloklanmaz.

Peki asenkron ve senkron kavramları nerede devreye giriyor? Tahmin edebileceğiniz gibi ilk örnek senkron olarak tasarlanabilir. Yani tüm işlemler aynı thread üzerinde yapılabilir. İkinci örnek ise kolaylıkla asenkron hale getirilebilir ve socketten bilgiyi alacak fonksiyon ayrı bir thread üzerinde, String’e çeviren ayrı bir threadte ve ekrana basma işlemi yine ayrı bir thread üzerinde tanımlanabilir. İsterseniz tam tersini, ilk örneği asenkron ikinci örneği senkron yapmayı da deneyebilirsiniz ama bu sadece işinizi zorlaştıracaktır.

Aynı örnek üzerinden devam edecek olursak, asenkron ya da senkron olmasının ne gibi farkları olabilir biraz bunlara bakalım. İki farklı uygulamamıza socket üzerinden kimi daha uzun kimi daha kısa şekilde farklı farklı cümleler geliyor. Senkron olan uygulamamızda, tüm cümleler sockete yazıldıkları şekilde ekrana basılacaktır fakat asenkron olan uygulamamızda, kısa bir cümle kendinden önce gönderilmiş uzun bir cümleden önce ekrana yazılabilir. (Tabi String’e çevirme işleminin cümle uzunluğuna bağlı olarak çok daha uzun süreceğini varsayıyorum.) Yani asenkron uygulamamız cümle uzunluğunun doğurabileceği sorunlardan/gecikmelerden daha az etkilenecektir. Doğal olarak eğer ekranı okuyan bir kullanıcılarınız varsa, sadece uzun cümleyi bekleyen kullanıcınız etkilenecektir. Tabi eğer cümlelerin sırasının korunması gibi bir gereksiniminiz varsa asenkron uygulamanızın başka işlemler de yapması gerekecektir.

Performans açısından baktığımızda, bu durum genelde asenkron olan uygulamanın cümleleri daha hızlı çevirdiği gibi bir algı oluşturuyor. Fakat yapacağınız işlem değişmediğinden asenkron bir yapı birim çevirme hızınızı değiştirmeyecektir. Örneğin senkron uygulamanız bir veriyi okuyup, String’e çevirip ekrana ortalama 5 ms’de basıyorsa, asenkron uygulamanızda da bu 5 ms’e sürecektir. Fakat asenkron uygulamanız CPU’yu daha verimli kullanacağından ekrana daha fazla cümle (throughput) yazdırabilirsiniz. Örneğin senkron uygulamanız saniyede ekrana 200 cümle yazdırabiliyorsa, asenkron uygulamanız saniyede 1800 cümle yazdırabilir.

Asenkron yapıların diğer bir yararı da uygulamızın kabul edebileceği bağlantı sayısını arttırmasıdır. Bağlantıyı kabul eden threadlerin de aynı mantıkla çalışacağını düşünürseniz bu sizi şaşırtmayacaktır. Bağlantıyı kabul eden thread, talebi içerideki başka bir thread havuzuna iletecek ve sıradaki bağlantıyı kabul etmeye başlayabilecektir.

Peki herşey bu kadar güzelse neden tüm uygulamalar asenkron olarak yazılmıyor? Çünkü yazdığınız uygulamanızın gereklilikleri asenkron uygulamanızdan alacağınız yararı arttırıp azaltabilir. Az önceki örnekteki gibi IO yoğun işlemler yapan bir uygulama geliştiriyorsanız, bloklanmayan yapılardan daha fazla yarar sağlayacaksınızdır fakat bunun tersi olarak CPU yoğun bir uygulamanız varsa asenkron yapıdan sağlayacağınız yarar azalacaktır. Bunun yanı sıra, asenkron yapının geliştirmesi ve testi de senkron bir yapıya göre daha zor olduğundan, senkron bir yapı daha çok tercih edilebiliyor.

Tüm bu avantajlarını ve dezavantajlarını göz önüne aldığımızda, bloklanmayan yapıların mikro servisler için ne kadar önemli olduğunu görüyoruz. Bir mikro servis mimarisinde, çoğu servis başka bir servisi tüketmekte (IO) ve tükettiği servis üzerine küçük bir birimlik iş yapmaktadır. Yani, gerçekten mikro servislerle oluşturulmuş bir uygulamada asenkron yapının getirdiği faydalardan sonuna kadar yararlanabiliriz.

Bu arada, büyük şirketler de yavaş yavaş bu yapıya geçiyorlar. Örneği Netflix’in bununla ilgili çok güzel bir yazısına şurdan ulaşabilirsiniz. Canlı ortamlarını bloklanmayan yapıya geçirdiklerinde deneyimledikleri yararlardan bahsediyorlar.

Örnek Proje

Spring Boot, Spring MVC ve Java 8 kullanarak, nasıl bir bloklanmayan servis geliştireceğimize bakalım. Ben örnek olması açısından HttpBin servisini tüketeceğim. HttpBin bize bir ip servisi sunuyor. Bu ip servisinden dış ip’nizi öğrenebiliyorsunuz. Yazacağımız servis ise sadece bu servisi çağırıp sonucu bize dönecek.

Projenin Yaratılması

Intellij’de Spring Initializer ya da start.spring.io kullanarak Spring Boot projemizi yaratıyoruz. Ben aşağıdaki bilgilerle yarattım, siz istediğiniz şekilde yaratabilirsiniz.

  • Proje Adı: jdk8-async-mvc
  • Group Id: com.bahadirakin
  • Artifact Id: jdk8-async-mvc
  • Package Name: com.bahadirakin

HttpBin Entegrasyonu

Öncelikle HttpBin ile entegre olalım. Ben istemci için Retrofit kullanacağım. Bunun bir kaç sebebi var:

  1. Java 8 ile gelen CompletableFuture’ı destekliyor.
  2. Bir çok Java kütüphanesiyle güzel bir şekilde entegre olabiliyor. Mesela JSON dönüşümü için ister GSON kullanın ister Jackson kullanın. Spring MVC ile birlikte Jackson geldiğinden ben Jackson kullanacağım.
  3. Interface üzerinden istemci oluşturabiliyorsunuz. Eğer Spring’in RestTemplate’ini kullanacak olsam çoğru şeyi kendim halletmem gerekecek. Retrofit’te ise interface geliştirip doğruca istemciye dönüştürebiliyorum.

Öncelikle maven bağımlılıklarımızı ekleyelim:

pom.xml

HttpBin’in Ip servisi bize şu şekilde JSON’lar dönüyor.

Şimdi bunu dönüştüreceğimiz modelimizi/DTOmuzu oluşturalım. Bunun için com.bahadirakin.model paketinin altına Ip isminde bir sınıf yaratıyoruz. İçeriği aşağıdaki gibi olacak.

Ip.java

Modelimizi yarattığımıza göre, şimdi de Interface’imizi yaratalım. Bunun için com.bahadirakin.service paketinin altına HttpBinService isminde bir interface yaratıyoruz.

HttpBinService.java

Son olarak, entegrasyonumuzun son halkasını yapıyoruz. Bu interface sınıfından istemcimizi oluşturacağız. Bunun için bir com.bahadirakin paketinin altına ClientConfiguration sınıfını oluşturuyoruz.

ClientConfiguration.java

Burada dikkat etmenizi istediğim bir kaç nokta var.

  • Sadece bir istemci yaratıyor olabiliriz ama birden fazla thread pool ayarlaması yapıyoruz.
  • Bunlardan biri bağlantılar için.
  • Diğeri ise asenkron çalıştıracağımız metodlar için. Socket örneğinde olduğu gibi, istemcimize fonksiyon vereceğiz. İstemcimiz ise bu fonksiyonu “clientCallbackExecutor” içerisinde çalıştıracak

IpController

Entegrasyonu tamamladığımıza göre kendi Controller’ımızı yazabiliriz. Bunun için bir com.bahadirakin.controller paketinin altına IpController sınıfını oluşturuyoruz.

IpController.java

Burada da dikkat etmenizi istediğim bir kaç nokta var:

  1. Ben özellikle CompletableFuture kullanmayı tercih ettim. Amacım olabildiğince Java 8 özelliklerini kullanmak. Spring MVC başka tipte Asenkron dönüşleri de (mesela RxJava) destekliyor.
  2. Uygulamanın genelinde hiç bir Exception Handling yok. Bu tabi canlıya çıkacak bir uygulama için kabul edilebilecek bişey değil. Örneği basitleştirmek adına böyle bir yolu tercih ettim. Aynı şekilde SpringMVC annotasyonlarını da olabildiğince sade tuttum.

Şimdi uygulamanızı, benim için bu Jdk8AsyncMvcApplication oluyor, çalıştırıp deneyebilirsiniz. Uygulamanız çalıştığında aşağıdaki komut ile uygulamanızı test edebilirsiniz.

Burada dikkat etmenizi istediğim kısım ise istemci tarafında bir değişiklik yapmadığımız. Yani gidip senkron bir serviste yazsaydık istemcimiz bu şekilde çalışacaktı.

Son

Özetleyecek olursak, asenkron olarak geliştirilen bir servis CPU’yu daha verimli kullanmanızı ve daha fazla sayıda istemciyi kaldırabilmenize olanak sağlıyor. CPU’yu ne kadar verimli kullanabileceğinizi ise uygulamanızın IO bağımlı mı CPU bağımlı mı olması belirliyor. Eğer uygulamanız IO bağımlıysa, yani veriyi işlemekten çok veri alışverişinde bulunuyorsa, CPU’dan alacağınız verim artabiliyor.

Projenin tam haline aşağıdaki linkten ulaşabilirsiniz. Projenin içerisinde birim testleri ve entegrasyon testleri de bulunuyor. İlginizi çeker diye düşünüyorum.