Mikroserwisowa architektura na przykładzie Spring Cloud Netflix
W podejściu mikroserwisowym mamy wiele aspektów, o które trzeba się zatroszczyć. Mając na uwadze, że część z nich jest również używana w podejściu monolitycznym, niezbędnymi okazują się: Service Discovery, API Gateway (Routing), Server Side Load Balancing, Client Side Load Balancing, Circuit Breaker czy External Configuration. Dziś postaram się przedstawić, jak wygląda mikroserwisowa architektura na przykładzie Spring Cloud Netflix.
Spis treści
Service Discovery
W najprostszych słowach jest to serwis, który umożliwia rejestrowanie się nowych serwisów, jak i udostępnianie informacji o zarejestrowanych użytkownikach. Często są to implementacje wzorca klucz-wartosc
, od którego wymagana jest duża niezawodność i szybkość. Bazowy serwis, który umożliwia zrównoważenie obciążenia poprzez wszystkie dostępne maszyny oraz, co za tym idzie, automatyczne skalowanie naszych aplikacji.
Najpopularniejsze implementacje Service Discovery to:
- Consul by HashiCorp,
- Zookeeper,
- etcd – napisany w Go używający algorytm konsensusu Raft algorithm to manage a highly-available replicated log
- Netflix Eureka,
- AWS DNS-Based Servcie Discovery – bazujący na Amazon Route 53, AWS Lambda oraz ECS Event Stream,
- Kubernetes Service Discovery.
Service Discovery na przykładzie Eureka Server
Najprostsze sposoby na uruchomienie i zaczęcie pracy z Eureka:
- Spring Boot Cloud CLI – Spring Cloud Eureka,
- uruchomienie obrazu Dockera,
- utworzenie prostego projektu Spring Boot z zależnościami do Eureka Server.
Przejdźmy krok po kroku po źródłach niezbędnych do samodzielnego utworzenia i uruchomienia projektu Eureka Server. Kod źródłowy klasy głównej (oraz najważniejsza adnotacja w tym przykładzie @EnableEurekaServer
):
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class ServiceDiscovery { public static void main(String[] args) { SpringApplication.run(ServiceDiscovery.class, args); } }
Właściwości do zdefiniowania w application.properties
:
spring.application.name=eureka-service server.port=8761 #By default, the registry will also attempt to register itself, so youu2019ll need to disable that, as well. eureka.client.fetch-registry=false eureka.client.register-with-eureka=false eureka.client.serviceUrl.defaultZone=http://discovery-container:8761/eureka/
W tej sytuacji powinniśmy mieć dostępny serwis Eureka pod adresem http://host:8761
. Główna strona wygląda następująco.
Zawiera podstawowe informacje takie jak: aktualny czas, repliki (zarejestrowane, dostępne, niedostępne), czyli cały tak zwany status serwera oraz podstawowe informacje, czyli zużycie pamięci, dostępną pamięć, nazwę środowiska, na którym jest uruchomiona, ilość dostępnych procesorów, jak i czas pracy od uruchomienia.
Oczywiście, co najważniejsze z naszej perspektywy, to lista dostępnych serwisów.
Client Service Discovery na przykładzie Spring Boot Web
Naturalnie przejdziemy do prostych serwisów, które zarejestrują się w naszym Service Discovery. Przykładowy kod serwisu, gdzie najważniejsza z naszej perspektywy jest adnotacja @EnableDiscoveryClient
:
@EnableDiscoveryClient @RestController @SpringBootApplication public class Service { @Value("${HOSTNAME}") String hostname; @GetMapping("/info") String info() { return String.format("Hostname: %s", hostname); } public static void main(String[] args) { SpringApplication.run(Service.class, args); } }
Oraz application.properties
spring.application.name=service server.port=${port::8080} eureka.client.serviceUrl.defaultZone=${EUREKA_URL:http://discovery-container:8761/eureka} eureka.instance.prefer-ip-address=true
API Gateway
API Gateway jest punktem wejściowym naszej aplikacji, który przekierowuje żądania do odpowiednich serwisów w naszym środowisku. Podsumowując eksponuje publiczne API. Najpopularniejsze implementacje są udostępniane przez dostawców chmury. Oczywiście nic nie stoi na przeszkodzie, aby użyć innej implementacji, np.:
- Nginx,
- Zuul Netflix,
- Amazon API Gateway,
- Azure API Management.
API Gateway – na przykładzie Zuul Netflix
Zuul jest serwisem działającym na wirtualnej maszynie Javy, działający jako router, jak i również Server Side Load Balancing. Dzięki bazowaniu na filtrach: pre, route, post, error, umożliwia wiele funkcjonalności, takich jak:
- security,
- routing,
- monitoring,
- wstrzykiwanie danych (np. do nagłówków).
Client Side Load Balancing – Ribbon
Ribbona można używać bez dynamicznej informacji o dostępnych serwerach. Wtedy jesteśmy w stanie zdefiniować niezbędne właściwości w pliku application.properties
z informacją pomiędzy jakimi serwerami klient ma balansować obciążenie. Przykład konfiguracji:
`listOfServers=localhost:8081,localhost:8082,localhost:8083`
W naszym przykładzie chcemy, aby Ribbon
pracował odpowiednio w architekturze mikroserwisów, która zakłada dynamiczną ilość instancji danego serwisu. Klient powinien mieć możliwość zdobywania tej informacji w czasie pracy.
Przykład application.properties
Ribbon’a rejestrującego się w ‘Eureka Serwer’ i odświeżającego listę instancji interesującego go serwisu:
spring.application.name=client server.port=8080 eureka.instance.hostname=client eureka.client.register-with-eureka=true eureka.client.fetch-registry=true eureka.client.service-url.default-zone=http://localhost:8761/eureka hello.ribbon.eureka.enabled=true hello.ribbon.ServerListRefreshInterval:15000 hello.ribbon.NIWSServerListClassName: com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
Konfiguracja użyta w projekcie:
public class HelloConfiguration { @Autowired IClientConfig clientConfig; @Bean IPing ribbonPing(IClientConfig config) { return new PingUrl(); } @Bean IRule ribbonRule(IClientConfig config) { return new AvailabilityFilteringRule(); } }
Klasa startująca wraz z klientem:
@RibbonClient(name = "hello", configuration = HelloConfiguration.class) @EnableScheduling @EnableDiscoveryClient @SpringBootApplication public class RibbonDiscoveryClient { private static Logger log = LoggerFactory.getLogger(RibbonDiscoveryClient.class); @LoadBalanced @Bean RestTemplate restTemplate() { return new RestTemplate(); } @Autowired RestTemplate restTemplate; @Autowired DiscoveryClient discoveryClient; @Scheduled(fixedRate = 1000) void callRibbon() { final List<ServiceInstance> instances = discoveryClient.getInstances("hello"); log.info("### SERVERS START:"); for (final ServiceInstance instance : instances) { log.info("Instance: " + instance.getHost().toString()); log.info("Port: " + instance.getPort()); log.info("URI: " + instance.getUri().toString()); } log.info("### SERVERS STOP:"); ResponseEntity<String> entity = restTemplate.getForEntity("http://hello", String.class); log.info(entity.getBody()); } public static void main(String[] args) { SpringApplication.run(RibbonDiscoveryClient.class, args); } }
API Gateway – Routing
Punkt wejściowy do naszej architektury. Przekierowuje żądania do odpowiednich serwisów. W naszym przypadku będzie to projekt open-source Zuul
. Dzięki mechanizmowi filtrów jest w stanie filtrować ruch wejściowy, pozwalać na łatwy monitoring oraz zapewniać bezpieczeństwo i autentykacje. Musi zapewniać wysoką wydajność oraz skalowalność.
Przykładowo, korzystając ze środowisk chmurowych mamy odpowiedniki. I tak dla AWS jest dedykowany API Gateway zapewniający podobne funkcjonalności. Dla zobrazowania działania głównie routingu zbudujemy dwa różne proste serwisy oraz API Gateway na przykładzie Zuul
.
Kod źródłowy serwisu pierwszego oraz application.properties
:
@RestController @SpringBootApplication public class RouteBooks { @GetMapping String books() { return "books"; } public static void main(String[] args) { SpringApplication.run(BooksCalc.class, args); } }
spring.application.name=books server.port=8090
Kod źródłowy serwisu drugiego oraz application.properties
:
@RestController @SpringBootApplication public class RouteCalc { @GetMapping String books() { return "calc"; } public static void main(String[] args) { SpringApplication.run(RouteCalc.class, args); } }
spring.application.name=calc server.port=8090
Najważniejszy nasz komponent, czyli Zuul service i jego application.properties
:
@EnableZuulProxy @SpringBootApplication public class ZuulServer { @Bean ZuulFilter simpleFilter() { return new SimpleFilter(); } public static void main(String[] args) { SpringApplication.run(ZuulServer.class, args); } }
public class SimpleFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(SimpleFilter.class); @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { return null; } @Override public String filterType() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); log.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString())); return "pre"; } @Override public int filterOrder() { return 1; } }
application.properties server.port=8080 zuul.routes.books.path=/ksiazki/** zuul.routes.books.url=http://localhost:8090 zuul.routes.calc.url=http://localhost:8091
Od tej chwili nasze żądania będą przekierowywane do prawidłowego serwisu poprzez określoną część adresu URL.
Server Side Load Balancing
W celu zobrazowania posłużymy się poznanym wyżej stackiem technologicznym: Eureka, Zuul i Spring Boot Web.
Chcemy uzyskać poniższą architekturę (poniższy screen), w której serwisy wykonujące operacje rejestrują się w Service Discovery (Eureka). O dostępne instancje dopytuje i odświeża informacje API Gateway (Zuul). Dla działającej całości potrzeba stworzyć klienta, który będzie odpytywał poprzez API Gateway nasze serwisy. Jedyną różnicą do poprzedniego podejścia jest dodanie Service Discovery w naszym przypadku Eureka Service, która udostępnia informacje o zarejestrowanych serwisach.
Stworzona architektura umożliwi łatwe skalowanie, w tym przypadku jeszcze ręczne. Jednak integrując z orkiestratorem np. Kubernetesem, Docker Swarm lub chmurą w łatwy sposób umożliwić autoskalowanie w zależności od obciążenia systemu.
Podsumowanie
Na pewno istnieją lepsze i nowsze rozwiązania do zabawy z mikroserwisami. Najlepiej pracować używając rozwiązania chmurowego, np. Amazon Web Services lub Microsoft Azure. Jednak w przedstawionym stacku technologicznym wszystko jest darmowe, nie musimy obawiać się o nieprzewidziane koszty, ponieważ podłączyliśmy dane karty kredytowej.
Próg wejścia wydaje się dosyć mały dla programisty Javy. A sama zabawa nie wymaga mocnego sprzętu. W zasadzie zostało już tylko życzyć dobrej zabawy.
Zainteresował Cię Netflix i chcesz dowiedzieć się więcej? Przeczytaj o architekturze chaosu, którą stosuje Netflix.
Zdjęcie główne artykułu pochodzi z unsplash.com.