Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

Netflix Eureka Server İle Service Discovery

Merhaba,

Microservice mimarisi ile geliştirilen projelerde birbirlerinden bağımsız onlarca servis arasında iletişim esastır. Haliyle bu iletişim sürecini mümkün mertebe basit ve etkili bir şekilde dizayn etmemiz gerekmektedir. Genellikle servisler arası iletişim kompleksliğine, cephe görevi gören API Gateway yazılımlarıyla çözüm getirmeye çalışırız lakin API Gateway’ler uç düzeyde clienttan servise(client-to-service) trafiği yönetiklerinden dolayı servisler arası iletişim için yeterli bir model sunamadıklarından dolayı yetersiz gelmektedirler. Çünkü microservice mimarilerde yoğun olarak servisten servise(service-to-service) haberleşme söz konusudur. Dolayısıyla bu davranışı karşılayabilecek bir çözümsel modele ihtiyacımız olacaktır. O yüzden bu içeriğimizde, API Gateway yerine bu ihtiyaca istinaden daha elverişli olan Service Discovery tool’u olan Eureka ile çözüm getirmeye çalışacak ve servisler arası iletişim sürecini olabildiğince yönetilebilir hale getirmeye odaklanıyor olacağız.

Nedir Bu Service Discovery?

Service Discovery, farklı adreslerde host edilen servislerin(ya da API’lerin) birbirleri arasındaki iletişim/haberleşme için geliştiriciler tarafından adres bilgilerinin bilinmesi zorunluluğunu ortadan kaldırarak, bu servislerin ortak bir store’a register olmasını sağlayan ve böylece erişim tarifini kolaylaştırarak iletişimi daha sade hale getiren bir modeldir. Bu model sayesinde ilgili proje ekosisteminde bulunan tüm servisler birbirleriyle ‘service name‘ değerleri aracılığıyla rahatlıkla iletişim kurabilmekte ve geliştiricileri adres, port vs. gibi ufak tefek bilgilere boğmaksızın iletişimi sürdürebilmektedirler.

Service Discovery mimarisini daha iyi anlayabilmek için aşağıdaki görseli incelersek eğer;Netflix Eureka Server İle Service Discoveryservislerin kendi aralarındaki iletişimi herhangi bir host bilgisine gerek kalmaksızın gerçekleştirdiğini görmekteyiz. Misal olarak, sol taraftaki ‘A Service’, sağ taraftaki ‘B Service’e istek gönderecekse eğer ilgili servisin ip’sini, hostunu, portunu vs. bilmeye gerek duymaksızın direkt olarak ‘Service Registry‘ servisinden istek yönlendirmede bulunabilmektedir. Bu işlem nasıl gerçekleşiyor? diye sorarsanız eğer dikkat ederseniz tüm servisler service registry’e kayıt olmuş vaziyettedir. Haliyle istek gönderecek servisin, hedef servisin tag(service name) bilgisini bilmesi ve bu bilgi doğrultusunda ‘ben n isimli servise şu isteği göndermek istiyorum‘ şeklinde service registry üzerinden hareket etmesi yeterli olacaktır 🙂

Uygulama ekosisteminde kullanılan servislerin, service registry’e kayıt olması teoride oldukça basittir. Bunun için her bir servis ayağa kalkarken(ilk anda) service discovery’e request atarak kendisi hakkında belirli tanımlamalarda bulunarak kayıt işlemini gerçekleştirmektedir. Böylece service discovery üzerinde tanımlanan servis diğer servislerle tag üzerinden iletişim kurabilir vaziyette konumunu almış demektir.

Bu arada service registry’in service discovery ile olan ilişkisini kısaca izah etmemiz gerekirse eğer, tüm (mikro)servislerin host edildikleri network lokasyonlarının register edilerek tutulduğu ve servisler arası iletişimde bu lokasyonların tagler(service name) eşliğinde erişilebilir kılındığı bir yapılanmadır. Ayrıca birbirleri arasındaki iletişim sürecini, bu yapılanma üzerinden gerçekleştirmek isteyen tüm servislere Service Registry Client ismi verildiğini söylemekte fayda görmekteyim.

Service discovery, service-to-service haberleşme süreçlerinde uygulamaların birbirlerinin network adreslerine metinsel değerler karşılığında ulaşmasını sağlayan basit bir model ortaya koymaktadır.

Service discovery ile API Gateway arasındaki fark nedir?
İçeriğimize tam olarak giriş yapmadan önce belki service discovery’nin API Gateway’den farkını üst satırlarda ele almış olsak bile teoride pek yaratamamış olabiliriz. Bundan dolayı iki kavram arasındaki farkı aşağıdaki tablo üzerinden somutlaştırarak içeriğe devam etmek daha verimli olacaktır kanaatindeyim.

Service Discovery API Gateway
Servisten servise yapılacak istekleri yönetir. Clienttan servise yapılacak olan istekleri yönetir. Biraz daha nihaidir.
Yatay düzlemde(servisler arası – east/west traffic) istek trafiğini işler. Dikey düzlemde(client ile servis arası – north/south traffic) istek trafiğini işler.
İç kaynaklardaki trafiklere/işlere odaklanır. Harici/dış trafikleri iç kaynaklara yönlendirir. Burada dış trafikten kasıt clientlardır. Clientların tükettiği iç kaynaklardan mevzu bahis ise servislerdir.
Uygulamada kullanılan servislerin iletişimini ön plana çıkarır. Uygulamada kullanılan servislerin yönetimini ve kontrolünü ortaya çıkarır.

Şimdi gelin uygulamalı olarak service discovery implementasyonunu gerçekleştirmeyi inceleyelim. Tabi üzerine çalıştığınız projede service discovery implementasyonunu gerçekleştirebilmek için ZooKeeper, Istio, Consul by HashiCorp, Netflix Eureka vs. gibi türlü türlü tool’lar mevcuttur. Bizler bu içeriğimizde malumunuz giriş paragrafında da bildirildiği gibi Netflix Eureka üzerinden service discovery operasyonlarını .NET Core eşliğinde yürütüyor olacak ve implementasyon açısından pratikte incelemede bulunacağız.

Eureka Kurulumu

İlk olarak Eureka sunucusunun kurulumuyla başlayalım. Eureka’yı kullanabilmek için steeltoe ekibinin sunduğu docker image’inden istifade edebiliriz. Bunun için aşağıdaki docker talimatının çalıştırılması yeterlidir.

docker run --name Eureka --publish 8761:8761 steeltoeoss/eureka-server

Bu talimat neticesinde Eureka Server’ı barındıran bir docker container http://localhost:8761 portunda ayağa kaldırılmış olacaktır.Netflix Eureka Server İle Service DiscoveryEvet, görüldüğü üzere service discovery sunucumuz hali hazır ayakta. Bundan sonra bu sunucuya register olacak servisleri oluşturmaya geçebiliriz.

Eureka – Service Registry

Netflix Eureka Server İle Service DiscoveryŞimdi örneklendirmemizi iki API bir console uygulaması üzerinden sergiliyor olacağız. Bunun için blank solution açıp içerisine yandaki görseldeki gibi ‘AService’ ve ‘BService’ adında iki Web API ve ‘CService’ isminde de bir console projesi oluşturalım.

Tabi bu projeler bizim farazimizi tatbik edeceğimiz örnek uygulamalardır. Sizler gerçek operasyonlarda farklı solutionlarda bulunan projeleri bu çalışmaya dahil edebilir ve hatta farklı bir framework veya dil/platform’da geliştirilen uygulamayı da kendi fıtratına uygun sisteme implemente edebilirsiniz. Velhasıl bizler şimdiki ahvalde devam edelim.

Şimdi Eureka üzerinden service discovery ile call edilecek tüm uygulamalar ilgili sunucuya registry olmak mecburiyetindedirler. Bunun için .NET Core ortamında Steeltoe.Discovery.Eureka kütüphanesinden istifade etmekteyiz. Sisteme hem register olacak hem de register olmuş servislere istek gönderecek tüm uygulamalar bu kütüphaneyi entegre etmek mecburiyetindedir.

Tüm servislere ilgili kütüphaneyi entegre ettikten sonra öncelikle Web API’lar üzerinden gerekli konfigürasyonlara başlayabiliriz. Bunun için her iki API servisindeki ‘Program.cs’ dosyasında aşağıdaki gibi AddDiscoveryClient metodu eşliğinde Eureka implementasyonunu gerçekleştirelim.

using Steeltoe.Discovery.Client;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDiscoveryClient(builder.Configuration);

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

Bu AddDiscoveryClient metodu neticesinde uygulamalarımız ilk ayağa kalktığında service discovery’e client olarak talepte bulunacak ve içeriği aşağıda bildirildiği gibi appsettings.json dosyasındaki konfigürasyonel değerler eşliğinde kendilerini register edecektirler.
‘AService’ :

.
.
.
  "spring": {
    "application": {
      "name": "AService"
    }
  },
  "eureka": {
    "client": {
      "serviceUrl": "http://localhost:8761/eureka/",
      "shouldFetchRegistry": "true",
      "shouldRegisterWithEureka": true,
      "validateCertificates": false
    },
    "instance": {
      "port": "7175",
      "ipAddress": "localhost",
      "preferIpAddress": true
    }
  }
.
.
.

‘BService’ :

.
.
.
  "spring": {
    "application": {
      "name": "BService"
    }
  },
  "eureka": {
    "client": {
      "serviceUrl": "http://localhost:8761/eureka/",
      "shouldFetchRegistry": "true",
      "shouldRegisterWithEureka": true,
      "validateCertificates": false
    },
    "instance": {
      "port": "7253",
      "ipAddress": "localhost",
      "preferIpAddress": true
    }
  }
.
.
.

Evet, service discovery sürecinde olacak tüm servisler bu konfigürasyonel değerler eşliğinde bildiride bulunmak mecburiyetindedirler. Peki bu değerler ne anlama gelmektedir? diye sorduğunuzu duyar gibiyim… Gelin kısaca açıklayalım;

  • spring:application:name
    İlgili servisin kaydı neticesinde kullanacağı adını temsil eder.
  • eureka:client
    Servisin Eureka ile olan davranışının temel ayarlarını barındırır.
  • eureka:client:serviceUrl
    Bağlanılacak olan Eureka sunucu adresini temsil eder.
  • eureka:client:shouldFetchRegistry
    İlgili servisin, başka bir servis tarafından discovery/keşifini temsil eder. “true” ise erişilebilir. “false” ise erişilemez!
  • eureka:client:shouldRegisterWithEureka
    İlgili servisin Eureka’ya kayıt olup olmamasını ifade eder. Tabi ki de servis diğer servisler tarafından call edilecekse ‘true’ olması gerekmektedir.
  • eureka:client:validateCertificates
    Sertifika doğrulamasını etkinleştirir.
  • eureka:instance
    Uygulamaya dair temel bilgileri barındırır.
  • eureka:instance:port
    Uygulamanın port bilgisini ifade eder.
  • eureka:instance:ipAddress
    Uygulamanın ip adresini/host bilgisini ifade eder.
  • eureka:instance:preferIpAddress
    Uygulama Eureka’ya kaydedilirken hostname yerine ip adresin kullanılarak kayıt edilip edilmemesini belirler.
  • Tüm bunların dışında geri kalan konfigürasyonel değerlerin default değerleriyle birlikte neler olduğunu incelemek için şuradaki içeriğe göz atmanızı tavsiye ederim.

Velhasıl, bu ayarlar eşliğinde uygulamaları ayağa kaldırıp Eureka server’a http://localhost:8761 adresinden giriş yaparsanız eğer ilgili uygulamaların instance olarak eklendiğini göreceksiniz.
Netflix Eureka Server İle Service DiscoveryBurada görüldüğü üzere ‘Application’ kolonunda yazan değerler ilgili servisin host ve port bilgisine gerek kalmaksızın direkt olarak istek atmamızı sağlayacak olan ‘service name‘ değerleridir. Bu ‘service name‘ değerini kullanarak https://BService/****** şeklinde isteklerde bulunabileceğiz. Şimdi gelin bunu test edelim.

Calling Service/API

Service discovery’e register olan tüm servisler birbirlerini çağırabileceklerinden dolayı burada örnek amaçlı ‘AService’ üzerinden ‘BService’e bir istek gönderelim. Bunun için her iki serviste de ‘ServiceController’ isminde örnek bir controller sınıfı oluşturulması kafidir.

Önce ‘BService’ içerisinde controller’ı oluşturalım.

namespace BService.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ServiceController : ControllerBase
    {
        public string Get()
            => "B Service is working";
    }
}

Ardından ‘AService’ içerisinde ‘BService’e istek gönderecek şekilde controller’ı inşa edelim.

namespace AService.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ServiceController : ControllerBase
    {
        readonly DiscoveryHttpClientHandler _discoveryHttpClientHandler;
        public ServiceController(IDiscoveryClient discoveryClient)
        {
            _discoveryHttpClientHandler = new(discoveryClient);
        }
        public async Task<string> GetBService()
        {
            using HttpClient httpClient = new(_discoveryHttpClientHandler, false);
            return await httpClient.GetStringAsync("https://BService/api/service");
        }
    }
}

Yukarıdaki kod bloğuna dikkat ederseniz eğer IoC Container’dan IDiscoveryClient referansıyla inject edilen nesne DiscoveryHttpClientHandler türünden handler sınıfının constructor’ına verilmekte ve ‘HttpClient’ nesnesinin bu handler sınıfını kullanarak istek yapması sağlanmaktadır. Tüm bu çalışmalardan sonra ‘AService’ine https://localhost:7175/api/service adresi üzerinden get isteğinde bulunalım ve sonucu hep birlikte gözlemleyelim.Netflix Eureka Server İle Service DiscoveryEvet, görüldüğü üzere ‘AService’ üzerinden ‘BService’e https://BService/api/service endpoint’i aracılığıyla başarıyla istek göndermiş bulunuyoruz. Burada iki servis üzerinden olayı simüle ettiğimiz için kıymeti pek anlaşılmasa da esasında onlarca servisin söz konusu olduğu durumlarda Eureka server üzerinden application adını alıp, portsuz, hostsuz direkt istek yapabilmenin tadı paha biçilemez olacaktır 🙂

Ayrıca örneklendirme için oluşturduğumuz console uygulaması olan ‘CService’i unuttuğumuzu sanmanızı istemem 🙂 Şimdi gelin service discovery’e register edilmiş servisleri sadece call edecek bir client uygulamasının örneklendirmesini bu console uygulaması üzerinden gerçekleştirelim.

Şimdi ilk olarak console uygulaması içerisinde ‘appsettings.json’ isimli dosya oluşturalım ve içeriğini aşağıdaki gibi dolduralım.

{
  "spring": {
    "application": {
      "name": "CService"
    }
  },
  "eureka": {
    "client": {
      "serviceUrl": "http://localhost:8761/eureka/",
      "shouldFetchRegistry": "true",
      "shouldRegisterWithEureka": true,
      "validateCertificates": false
    },
    "instance": {
    }
  }
}

Dikkat ederseniz eğer bu uygulama sadece Eureka’da ki servisleri call edeceğinden dolayı instance kısmını boş geçmekteyiz. Bu dosyaya console uygulaması tarafından runtime’da erişilebilmesi için ‘CService.csproj’ dosyasında aşağıdaki gibi include etmemiz gerekmektedir.

<Project Sdk="Microsoft.NET.Sdk">
	.
	.
	.
	<ItemGroup>
		<Content Include="./appsettings.json">
			<CopyToOutputDirectory>Always</CopyToOutputDirectory>
		</Content>
	</ItemGroup>
	.
	.
	.
</Project>

Ve son olarak aşağıdaki gibi service discovery üzerinden isteği gönderelim.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Steeltoe.Common.Discovery;
using Steeltoe.Discovery;
using Steeltoe.Discovery.Client;

ConfigurationManager configurationManager = new();
configurationManager.AddJsonFile("./appsettings.json");

IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddConfigurationDiscoveryClient(configurationManager);
serviceCollection.AddSingleton<IConfiguration>(configurationManager);
serviceCollection.AddDiscoveryClient(configurationManager);

ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();

IDiscoveryClient discoveryClient = serviceProvider.GetService<IDiscoveryClient>();
DiscoveryHttpClientHandler discoveryHttpClientHandler = new(discoveryClient);
using HttpClient httpClient = new(discoveryHttpClientHandler, false);
Console.WriteLine(await httpClient.GetStringAsync("https://BService/api/service"));

Burada yaptığımız çalışmaya göz atarsanız eğer ‘appsettings.json’ dosyasındaki konfigürasyonel değerleri okuyabilmek için ConfigurationManager nesnesinden istifade etmekteyiz. Ardından build-in gelen IServiceCollection IoC container’ına discovery client servisi yüklenmekte ve devamında ise IDiscoveryClient türünden nesne inject edilip, DiscoveryHttpClientHandler nesnesine verildikten sonra ‘HttpClient’ üzerinden istek gerçekleştirilmektedir.

Netflix Eureka Server İle Service Discovery

Console uygulamasında sonuç 🙂

Nihai olarak,
Servisler arası iletişim süreçlerinde host ve port bilgilerine boğulmaksızın service discovery üzerinden ‘service name‘ değerleri eşliğinde call operasyonlarını yürütebilmenin nasıl olduğunu Netflix Eureka tool’u üzerinden incelemiş ve bu yaklaşımın biz geliştiriciler açısından süreci oldukça verimli ve yönetilebilir kıldığına hep beraber şahit olmuş bulunmaktayız.

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek çalışmaya aşağıdaki github adresinden erişebilirsiniz.
https://github.com/gncyyldz/Eureka-Service-Discovery-Example

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.