Backend

Mikroserwisowa architektura na przykładzie Spring Cloud Netflix

programiści pracujący w biurze

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.

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:

Service Discovery na przykładzie Eureka Server

Najprostsze sposoby na uruchomienie i zaczęcie pracy z Eureka:

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.

spring cloud

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.

spring cloud

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.

Posiada ponad dziesięcioletnie doświadczenie w pisaniu aplikacji, głównie w języku Java. Zafascynowany kulturą DevOps i architekturą mikroserwisową, a co za tym idzie rozwiązaniami konteneryzacji Docker, Kubernetes i rozwiązań chmurowych. Szkoli, dzieli się wiedzą i mentoruje w szkołach IT. Miłośnik dobrych meetup’ów.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/mikroserwisowa-architektura-na-przykladzie-spring-cloud-netflix/" order_type="social" width="100%" count_of_comments="8" ]