TSM - (Micro)Service Discovery cu Netflix Eureka

Adrian Ivan - Software architect @ SDL


Articolul prezintă o soluție tehnică pentru conceptul Descoperirii Serviciilor folosind Netflix Eureka, o tehnologie dezvoltată în Java de către Netflix, expusă open-source în cadrul Netflix Open Source Software (OSS).

Exemplele de cod și configurări prezentate nu folosesc Eureka direct. Folosim Spring Cloud, o inițiativă de la Spring care reunește cele mai bune soluții tehnice pentru a construi un sistem distribuit modern. Spring Cloud este construit peste Spring Boot și are suport pentru integrarea Netflix OSS.

Descoperirea Serviciilor este unul dintre conceptele cheie în construirea sistemelor distribuite orientate pe servicii.

Un Serviciu A pentru a apela un Serviciu B, trebuie să găsească o instanță fizică a Serviciului B.

Configurările statice nu mai sunt o soluție viabilă într-un sistem elastic, dinamic, unde instanțele de servicii sunt pornite și oprite frecvent (planificat și neplanificat) sau unde problemele de rețea pot fi dese (Cloud). Găsirea unei instanțe a Serviciului B nu mai este un lucru banal.

Descoperirea Serviciilor implică un mecanism unde:

Eureka - Prezentare generală

Arhitectura Netflix Eureka constă în două componente, Serverul și Clientul. Serverul este o aplicație de sine stătătoare și este responsabilă de:

Clientul face parte din ecosistemul Serviciului propriu zis și are următoarele responsabilități:

Unități de Descoperire

Eureka lucrează cu Applications și Instances. Căutarea se face după id-ul serviciului, iar rezultatele constau în informații despre instanțele Serviciului care se regăsesc în registru de instanțe.

High Availability

Netflix Eureka este construit pentru High Availability. În termenii teoremei CAP, Eureka favorizează Availability în defavoarea Consistenței.

Accentul e pus pe faptul ca serviciile să se găsească în cazul partiționărilor de rețea sau problemele de Eureka Server.

High Availability este obținut la două niveluri:

Terminologie

Eureka a fost construită pentru a fi funcțională în Cloud-ul Amazon (AWS). În consecință, folosește o terminologie specifică Amazon: regiuni, zone, etc. .Exemplele prezentate în acest articol folosesc setări implicite, regiunea us-east-1 și zona defaultZone.  

Eureka Server

Serverul este ceea ce numim Serviciul de Descoperire într-un sistem SOA.

Începeți prin a clona repo-ul Spring Cloud Eureka de pe github.com.  

Configurare Server standalone

Este simplu de pornit o aplicație Eureka Server folosind Spring Cloud. Orice aplicație Spring Boot devine un Server Eureka folosind adnotarea \@EnableEurekaServer. Folosiți exemplele următoare pentru development pe mediul local.

Exemplu aplicație Java:

@SpringBootApplication
@EnableEurekaServer 
@EnableDiscoveryClient
public class EurekaApplication {

public static void main(String[] args) {
  SpringApplication.run(EurekaApplication.class, args);
    }
}  

Configurare yml:

server:
  port: 8761
security:
  user:
    password: ${eureka.password} 
eureka:   
  password: ${SECURITY_USER_PASSWORD:password}
  server:
    waitTimeInMsWhenSyncEmpty: 0
    enableSelfPreservation: false
  client:    
    preferSameZoneEureka: false 
---
spring:
  profiles: devlocal
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false 
    serviceUrl:
      defaultZone: http://user:${eureka.password:${SECURITY_USER_PASSWORD:password}}@localhost:8761/eureka/

Porniți aplicația folosind comanda: 

mvn spring-boot:run -Drun.jvmArguments="-Dspring.profiles.active=devlocal"

Setările registerWithEureka și fetchRegistry au valoarea false, ceea ce înseamnă ca Serverul nu face parte dintr-un cluster.

Configurare Cluster

Serverele Eureka sunt configurate în mod cluster pentru a asigura disponibilitatea în cazul în care unele instanțe au probleme.

Clienții eureka nu au nevoie de afinitate pentru Server și se pot conecta transparent la oricare Server din Cluster.

Serverele au nevoie de referințe către alte instanțe de Server.

Exista diferite modalități pentru a obține aceste referințe, descrise mai jos.

DNS

Netflix folosește DNS pentru a gestiona lista de referințe Eureka Server într-un mod dinamic. Clienții Eureka nu au nevoie de repornire pentru a obține noile configurări, ci le obțin printr-o nouă interogare DNS.

Aceasta este configurarea recomandată de producție.

Să vedem un exemplu cu un cluster de două servere Eureka, dsc01 și dsc02.

Se poate folosi Bind sau alt server DNS. Aici găsiți instrucțiuni pentru a instala și configura Bind.

  Configurație DNS:

$TTL 604800
@ IN  SOA ns.eureka.local. hostmaster.eureka.local. (
             1024       ; Serial
             604800     ; Refresh
              86400     ; Retry
            2419200     ; Expire
             604800 )   ; Negative Cache TTL
;
@ IN  NS  ns.eureka.local.
ns IN A 10.111.42.10
txt.us-east-1 IN TXT "defaultZone.eureka.local"
txt.defaultZone IN TXT "dsc01" "dsc02"
;

Configurația aplicației:

eureka:
  client:
    registerWithEureka: true
    fetchRegistry: true
    useDnsForFetchingServiceUrls: true
    eurekaServerDNSName: eureka.local
    eurekaServerPort: 8761
    eurekaServerURLContext: eureka

Lista statică de Servere

Configurarea clusterului prezentat anterior:

eureka:
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://dsc01:8761/eureka/,http://dsc02:8762/eureka/ Orice schimbare a listei necesită o repornire a Clientului Eureka pentru a obține lista actualizată.

Instanțe multiple de Server pe aceeași mașină

În cazul în care se dorește pornirea mai multor instanțe de Server pe aceeași mașină, trebuie folosite hostname-uri diferite pentru fiecare instanță. Eureka identifică pe baza combinației hostname și port. Hostname-urile trebuie să rezolve către localhost. în Windows OS se folosește fișierul hosts.

eureka:
instance:
hostname: server1

[Eureka Dashboard] Eureka Dashboard

Interfața grafică

Eureka Server oferă în interfața grafică pentru a observa statusul și detalii despre Instantele înregistrate.

Noi avem un mediu în care evaluăm tehnologii. Inițial am pornit cu exemplele Spring Cloud și le-am modificat pentru nevoile noastre.

Exemplul de mai sus prezintă un Server Eureka din mediul nostru. Sunt prezentate următoarele:

Interfata XML/Text

Această interfața prezintă mai multe detalii, http://dsc02:8761/eureka/apps (copy&paste din browser):

<?xml version="1.0"?>

-<applications>
  <versions__delta>1
  </versions__delta>
  <apps__hashcode>UP_11_
  </apps__hashcode>
   +<application>
   +<application>
   +<application>
   -<application>                   
     <name>BESTPRICES</name>            
     +<instance>            
     -<instance>                
     <hostName>clj-lcpdevsrv10
     </hostName>                
      <app>BESTPRICES</app>                
      <ipAddr>10.111.42.71</ipAddr>                
      <status>UP</status>                
      <overriddenstatus>UNKNOWN
      </overriddenstatus>                     
 <port enabled="true">8000</port>                
<securePort enabled="true">443
</securePort>                
<countryId>1</countryId>                                           
<dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo">                
 <name>MyOwn</name>                
 </dataCenterInfo>                                
 -<leaseInfo>                
  <renewalIntervalInSecs>30
  </renewalIntervalInSecs>                

<durationInSecs>90</durationInSecs>                
<registrationTimestamp> 
  1426271956166 
</registrationTimestamp>                
<lastRenewalTimestamp>  
  1427199350993
</lastRenewalTimestamp>                
<evictionTimestamp>0</evictionTimestamp>                
<serviceUpTimestamp>1426157045251
</serviceUpTimestamp>                
</leaseInfo>                

<metadata class="java.util.Collections$EmptyMap"/>                
  <appGroupName> MYSIDECARGROUP</appGroupName>                
  <homePageUrl>http://clj-lcpdevsrv10:8000/
  </homePageUrl>                

<statusPageUrl>http://clj-lcpdevsrv10:8001/info
</statusPageUrl>                
<healthCheckUrl>http://clj-lcpdevsrv10:8001/health
</healthCheckUrl>                
<vipAddress>bestprices</vipAddress>                
<isCoordinatingDiscoveryServer>false
</isCoordinatingDiscoveryServer>                
<lastUpdatedTimestamp>1426271956166
</lastUpdatedTimestamp>                
<lastDirtyTimestamp>1426271939882
</lastDirtyTimestamp>                
<actionType>ADDED</actionType>            
 </instance>        
</application>                
+<application>
+<application>
+<application>
</applications>

Informații despre Instanță

Elementul "instance" prezintă informații complete despre o instanță înregistrată.

Majoritatea detaliilor sunt clare și prezintă informații precum locația fizica a instanței, momentul ultimei înnoiri, etc. .

URL-urile de healthcheck poți fi folosite de către tool-uri externe de monitorizare.

Informații proprii pot fi adăugate cu rolul de metadata.

Eureka Client

Clientul Eureka face parte din ecosistemul unei Instanțe de Serviciu business.

Poate fi folosit în mod embedded sau ca și process atasat (logic).

Netflix sugerează ca modul embedded să fie folosit pentru servicii Java și modul atașat pentru servicii scrise în limbaje non-JVM.

Clientul este configurat cu o listă de Servere Eureka. Configurațiile anterioare din cazul Eureka Server se aplică și la Client.

Orice aplicație Spring Boot devine un Client Eureka folosind adnotarea @EnableDiscoveryClient și având Eureka în Classpath:

@SpringBootApplication
@EnableDiscoveryClient
public class SampleEurekaClientApp extends RepositoryRestMvcConfiguration {

    public static void main(String[] args) {
        SpringApplication.run(CustomerApp.class, args);
    }
}  

Heartbeat-uri

Clientul și Serverul implementează un protocol de heartbeat-uri. Clientul trebuie să trimită mesaje de heartbeat regulat către Server. Serverul așteaptă aceste mesaje pentru a menține instanța de serviciu în registru sau pentru a reînnoi informații despre aceasta. În cazul în care aceste mesaje nu sunt primite, instanța este scoasă din registru.

Mesajele heartbeat pot specifica și un status pentru instanța: UP, DOWN, OUT_OF_SERVICE, cu consecințe imediate pentru rezultatele de descoperire returnate.

Mod protecție Server

Eureka Server are un mod protecție: în cazul în care un anumit număr de instanțe nu mai trimit mesaje heartbeat într-un anumit interval de timp, serverul blochează registrul și nu va șterge automat aceste instanțe. Consideră că a avut loc o partiționare a rețelei fizice și așteaptă ca aceste instanțe să revină când problema fizică se rezolvă. Această capabilitate este utilă în cazul Cloud și poate fi oprită în cazul data center-elor private.

Cache local Client

Una dintre capacitățile speciale ale Eureka este Cach-ul local Client. Clientul obține regulat informații din registru Eureka Server și le stochează local. La un moment dat poate avea aceleași informații ca și Server-ul. În cazul în care toate serverele devin indisponibile sau clientul este izolat de servere printr-o problemă fizică de rețea, acest Client poate funcționa în continuare cu rezultate bune până când acest cache local devine învechit.

Cache-ul local îmbunătățește performanța comunicării între servicii pentru că nu mai este nevoie de a trece peste încă un hop intermediar cu scopul de a face balansarea apelurilor.

Folosire

Clientul Eureka poate fi folosit în mod direct sau cu ajutorul altor librării ce se integrează cu Eureka.

În continuare sunt prezentate două opțiuni de folosire a clientului Eureka pentru a apela un serviciu, în cazul acesta serviciu bestprices din mediul nostru de evaluare:

Direct

 ...
@Autowired
private DiscoveryClient discoveryClient;
...

private BestPrice findBestPriceWithEurekaclient(final String productSku) {

 BestPrice bestPrice = null;
 // get hold of a service instance from Eureka
 ServiceInstance instance = null;
 // "bestprices" is the name of the service in Eureka
 List instances = discoveryClient
    .getInstances("bestprices");
 if (instances != null && instances.size() > 0) {
 instance = instances.get(0); //could be random
 // Invoke server based on host and port. 
 // Example using RestTemplate.    
 URI productUri = URI.create(String
   .format("http://%s:%s/bestprices/" + productSku,
    instance.getHost(), instance.getPort()));

 bestPrice = restTemplate.getForObject(productUri,
    BestPrice.class);            
 }
  return bestPrice;        
}   

Ribbon Load Balancer

Ribbon este un client HTTP și un load balancer software de la Netflix și care se integrează cu Eureka:

... 
@Autowired  
private LoadBalancerClient loadBalancerClient;
...

private BestPrice findBestPriceWithLoadBalancerClient(final String productSku) {
 BestPrice bestPrice = null;

 // "bestprices" is the name of the service in 
 // Eureka, as well as of the Ribbon LoadBalancer 
 // which gets created automatically.

ServiceInstance instance = loadBalancerClient
  .choose("bestprices");

  if (instance != null) {
  // Invoke server, based on host and port. 
  // Example using RestTemplate.
  URI productUri = URI.create(String
    .format("http://%s:%s/bestprices/" + productSku, 
     instance.getHost(), instance.getPort()));

   bestPrice = restTemplate.getForObject(productUri,
   BestPrice.class);
   }

    return bestPrice;
}

Servicii Non-JVM

Acest aspect nu va fi prezentat extensiv în acest articol și poate fi subiectul altuia.

Exista două abordări:

Concluzii