Spring Cloud mikroservis arhitektura

Uvod

Klasičan razvoj aplikacija podrazumeva monolitnu arhitekturu – aplikacija je izgrađena kao jedna celina. Tipičnu troslojnu aplikaciju čine klijentski deo (UI), serverski deo i baza podataka, pri čemu serverski deo predstavlja jedinstvenu logičku izvršnu celinu tj. monolit. Serverska aplikacija obrađuje HTTP zahteve, izvršava poslovnu logiku, čita i ažurira podatke iz baze i generiše HTML i šalje ga klijentu. Monolitni server predstavlja prirodan način razvoja aplikacije. Problemi se javljaju prilikom promene bilo kog dela aplikacije. Tada se zahteva ponovno prevođenje i postavljanje nove verzije. Ukoliko dođe do potrebe za skaliranjem, vrši se skaliranje cele aplikacije umesto samo delova koji zahtevaju više resursa.

Navedene mane monolitne arhitekture otklonjene su prelaskom na mikroservisnu arhitekturu. Radi boljeg upravljanja resursima delovi aplikacija se izdvajaju iz monolita. Dobijaju se manje i nezavisne komponente. Svaka komponenta se izvršava kao zasebna aplikacija.

Mikroservisna arhitektura je princip razvoja aplikacija u obliku malih, izdvojenih i nezavisnih servisa koji komuniciraju putem jednostavnih mehanizama kao što je HTTP API.

Monolitna vs. mikroservis arhitektura

Mnoge poznate kompanije uveliko koriste mikroservisnu arhitekturu, koja i predstavlja jedan od faktora njihovog uspeha danas. Jedan od najboljih primera je Netflix. Smatra se pionirom u prelasku s monolitne na mikroservisnu arhitekturu. Još 2009. je doneo odluku da svoj monolit rastavi na manje servise. Trenutno ima preko 600 mikroservisa. Nije na odmet pomenuti i Amazon – za generisanje jedne strane se koristi približno 120 mikroservisa.

U nastavku će biti dat jednostavan primer koji korišćenjem komponenti Spring Cloud-a koje je razvio Netflix ilustruje rad s mikroservisima. Platforma otvorenog koda Spring Cloud je odabrana zbog toga što je pogodna za upravljanje konfiguracijom distribuiranih aplikacija, što mikroservisi i jesu. Za samo kreiranje mikroservisa koristi se Spring Boot.

Kreiranje dva jednostavna Spring Boot mikroservisa

U ovom delu kreiramo dva Spring Boot mikroservisa: movie-producer i movie-consumer. Kako se i na osnovu naziva zaključuje, movie-producer pruža REST API na korišćenje klijentu movie-consumer. Oba projekta možete naći ovde u direktorijumu simple-microservices.

Kreiranje mikroservisa movie-producer

Prvo kreiramo maven projekat movie-producer.

U fajlu pom.xml su opisane zavisnosti Spring okruženja.

Osnovni model definisan je klasom Movie.

Movie.java

package springcloud.netflix.models;

public class Movie { 
    private int movieID; 
    private String title; 
    private int rating; 
    private String genre; 
    private int runtime; 
    private int year;

    public Movie() {}

    public int getMovieID() {
        return movieID;
    }

    public void setMovieID(int movieID) {
        this.movieID = movieID;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public int getRating() {
        return rating;
    }

    public void setRating(int rating) {
        this.rating = rating;
    }

    public String getGenre() {
        return genre;
    }

    public void setGenre(String genre) {
        this.genre = genre;
    }

    public int getRuntime() {
        return runtime;
    }

    public void setRuntime(int runtime) {
        this.runtime = runtime;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }


}

Da bi servis zaista bio dostupan neophodan je kontroler.

MovieController.java

package springcloud.netflix.controllers;

import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController;

import springcloud.netflix.models.Movie;

@RestController public class MovieController { @RequestMapping(value = "/movie", method = RequestMethod.GET) public Movie home() {

        Movie movie = new Movie();
        movie.setTitle("V for Vendetta");
        movie.setGenre("Sci-fi");
        movie.setRating(73);
        movie.setRuntime(132);
        movie.setYear(2005);
        movie.setMovieID(1);

        return movie;
    }


}

Da bi kreirani projekat bio Spring Boot, potrebno je definisati klasu sa anotacijom @SpringBootApplication. To je ujedno i klasa koja se izvršava.

SpringBootMovieProducerApplication.java

package springcloud.netflix;

import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;

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

Nakon pokretanja aplikacije posetimo u pretraživaču . U kontroleru smo izabrali rutu movie i metodu GET. Port se definiše u konfiguracionom fajlu application.properties. Ukoliko se ne navede port, aplikacija se startuje na portu definisanom za Tomcat server, podrazumevani 8080.

application.properties

server.port=9090

Kreiranje mikroservisa movie-consumer

Radi razvijanja mikroservisa koji će koristiti uslugu mikroservisa movie-producer , kreiramo maven projekat movie-consumer.

Zavisnosti Spring platforme definiše pom.xml.

Zatim, definišemo kontroler koji će da koristi uslugu mikroservisa iznad. Umesto da u pretraživač kucamo adresu, pristupamo joj korišćenjem klase RestTemplate.

MovieConsumerController.java

package springcloud.netflix.controllers;

import java.io.IOException;

import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate;

public class MovieConsumerController { public void getMovie() throws RestClientException, IOException {

        String baseUrl = "http://localhost:9090/movie";
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity response=null;
        try{
        response=restTemplate.exchange(baseUrl,
                HttpMethod.GET, getHeaders(),String.class);
        }catch (Exception ex)
        {
            System.out.println(ex);
        }
        System.out.println(response.getBody());
    }

    private static HttpEntity getHeaders() throws IOException {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Accept", MediaType.APPLICATION_JSON_VALUE);
        return new HttpEntity(headers);
    }


}

Spring Boot aplikacija u ovom slučaju će pozivati metodu getMovie() definisanu u kontroleru.

SpringBootMovieConsumerApplication.java

package springcloud.netflix;

import java.io.IOException;

import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestClientException;

import springcloud.netflix.controllers.MovieConsumerController;

@SpringBootApplication public class SpringBootMovieConsumerApplication {  
public static void main(String[] args) throws RestClientException, IOException { ApplicationContext ctx = SpringApplication.run( SpringBootMovieConsumerApplication.class, args);

        MovieConsumerController movieConsController=ctx.getBean(MovieConsumerController.class);
        System.out.println(movieConsController);
        movieConsController.getMovie();

    }

    @Bean
    public  MovieConsumerController  movieConsController()
    {
        return  new MovieConsumerController();
    }


}

U application.properties definišemo slobodan port za mikroservis movie-consumer.

application.properties

server.port=9091

Nakon pokretanja aplikacije, u konzoli se ispisuju podaci o filmu.

Kreiranje Eureka servisa i registracija mikroservisa

Videli smo da se pri kreiranju projekata sva konfiguracija nalazi u properties fajlovima. Kako se sve više servisa razvija, dodavanje i izmena konfiguracionih parametara postaje sve komplikovanija. Neki mikroservisi mogu da se pokvare, da postanu nedostupni ili da promene adresu. Ručna izmena konfiguracije bi mogla da stvori dodatne probleme. Setimo se metode getMovie() u kojoj smo hardkodovali URL za pristup. Zamislimo da imamo više takvih mikroservisa koji koriste movie-producer i da u jednom trenutku producer promeni npr. port. U takvim situacijama pomaže sledeća Netfliksova komponenta – Eureka Service Registration and Discovery. Mikroservisi se registruju na Eurekin server i postaju Eurekini klijenti. Pretraga se obavlja pomoću Eureke. Možemo da posmatramo Eurekin server kao telefonski imenik za mikroservise – prilikom registracije mikroservis govori servisu za registrovanje svoju adresu (host, port, čvor). Moguće je dostaviti serveru i neke dodatne metapodatke koji mogu biti od koristi drugim mikroservisima.

Prvo kreiramo maven projekat eureka-server nakon čega registrujemo movie-producer na njega.

U pom.xml fajlu definišemo pored dosadašnjih zavisnosti, zavisnost za spring-cloud-starter-eureka-server. Novina je i upravljanje uvedenom zavisnošću regulisano unutar taga dependencyManagement.

Spring Boot klasa sada sadrži i anotaciju @EnableEurekaServer.

EurekaServerApplication.java

package springcloud.netflix;

import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication {

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


}

Dva reda nakon porta sprečavaju da Eureka registruje sama sebe. Poslednjim redom se obezbeđuje da Eureka ne pravi replikacije mikroservisa – što je loše za uravnotežavanje opterećenja (load balancing). Ovde je dato radi jednostavnosti primera.

application.properties

server.port=8090
eureka.client.register-with-eureka=false 
eureka.client.fetch-registry=false
eureka.server.maxThreadsForPeerReplication=0

Kada posetimo adresu http://localhost:8090, vidimo da nema registrovanih aplikacija.

Da bismo registrovali prethodno kreiranu aplikaciju movie-producer, potrebno je da napravimo tri manje izmene:

  1. Modifikujemo pom.xml dodajući zavisnost za spring-cloud-starter-eureka-server sa sve definisanim upravljanjem, na isti način kao u projektu eureka-server.
  2. Spring Boot klasi dodamo anotaciju @EnableDiscoveryClient.
  3. Na kraju, u application.properties dodamo: eureka.client.serviceUrl.defaultZone=http://localhost:8090/eureka.

Nakon pokretanja aplikacije movie-producer, primećujemo da se tabela registrovanih aplikacija izmenila.

Da ne bi aplikacija bila registrovana pod nazivom UNKNOWN, u application.properties se doda: spring.application.name=movie-producer.

Upotreba Eureka servera za otkrivanje servisa

Čitava analogija Eureke sa telefonskim imenikom postaje jasnija ako se podsetimo problema sa hardkodovanjem rute. Sada ćemo modifikovati aplikaciju movie-consumer tako da preko Eureke pronađe servis koji koristi – movie-producer, kao što bismo u imeniku pronašli željenog pretplatnika na osnovu imena bez obzira da li je skorije menjao broj telefona.

Vrši se izmena tri fajla – pom.xml, MovieConsumerController.java i application.properties.

  1. Dodaje se zavisnost za Eurekin servis (pogledati pom.xml projekta eureka-server).
  2. MovieConsumerController se menja na sledeći način – dodat je DiscoveryClient, te baseUrl više nije hardkodovan:
    List instances=discoveryClient.getInstances("MOVIE-PRODUCER");
    ServiceInstance serviceInstance=instances.get(0);

    String baseUrl=serviceInstance.getUri().toString();

    baseUrl=baseUrl+“/movie“;

  3. U application.properties dodati:

  • eureka.client.serviceUrl.defaultZone=http://localhost:8090/eureka
  • spring.application.name=employee-consumer
    Pokrenuti movie-consumer. Rezultat u konzoli je identičan kao i bez korišćenja Eureke.

Kompletan kod za izmenjene mikroservise iz prvog dela i eureka-server se nalazi ovde.

Implementacija komponente Netflix Zuul

Kada govorimo o komponentama mikroservisne arhitekture, jako je bitno naglasiti da one prema dizajnu moraju biti otporne na poteškoće i kvarove. Pravovremena detekcija grešaka i automatski oporavak, ukoliko je moguć, vrlo su važni. Takođe, mikroservisne aplikacije pridaju veliki značaj nadgledanju rada komponenata i sistema kao celine. Zuul predstavlja gateway za sve zahteve upućene Netfliksovoj aplikaciji. Izgrađen je sa idejom da obezbedi dinamičko rutiranje, monitoring, otpornost na greške i bezbednost. Obezbeđuje korišćenje servisa s različitih servera bez upotrebe CORS -a i autentifikacije za svaki servis pojedinačno. Dijagram pokazuje kako izgleda komunikacija s mikroservisom movie-producer kada se uključi Zuul-ov servis u ceo sistem. Naime, Zuul je registrovan na Eureku, consumer dobija instancu Zuul-a od Eureke i svaku rutu koja sadrži /producer, Zuul će proslediti producer-u.

Zuul implementiramo kreiranjem maven projekta movie-netflix-zuul.

Fajl pom.xml mora da sadrži definisanu zavisnost za spring-cloud-starter-zuul pored onih koje smo definisali pri kreiranju Eureke.

Prvom dodelom se označava da svaka ruta koja sadrži /producer treba da se usmeri na adresu movie-producer. Zuul se ovde ponaša kao ruter. Slično može da se doda za bilo koji drugi servis.

application.properties

zuul.routes.producer.url=http://localhost:9090
eureka.client.serviceUrl.defaultZone=http://localhost:8090/eureka
server.port=8079
spring.application.name=movie-netflix-zuul

Zuul podržava četiri tipa filtera: pre, post, route i error. Implementacija svakog se svodi na nasleđivanje ZuulFilter-a i prepisivanje postojećih metoda – filterType(), filterOrder(), shouldFilter() i run(). Zbog sličnosti samih filtera, u nastavku je dat samo PreFilter. Primećujemo da bi se razlikovale samo metode filterType() i run().

PreFilter.java

package springcloud.netflix.filters;

import javax.servlet.http.HttpServletRequest;

import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext;

public class PreFilter extends ZuulFilter{ @Override public String filterType() { return "pre"; }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        System.out.println(
                "Request Method : " + request.getMethod() + " Request URL : " + request.getRequestURL().toString());

        return null;
    }


}

Ostalo je još da definišemo Spring Boot izvršnu klasu sa anotacijom @EnableZuulProxy.

SpringBootZuulApplication.java

package springcloud.netflix;

import org.springframework.boot.SpringApplication; import org.springframework.context.annotation.Bean;

import springcloud.netflix.filters.ErrorFilter; import springcloud.netflix.filters.PostFilter; import springcloud.netflix.filters.PreFilter; import springcloud.netflix.filters.RouteFilter;

@SpringBootApplication @EnableDiscoveryClient @EnableZuulProxy public class SpringBootZuulApplication { public static void main(String[] args) { SpringApplication.run(SpringBootZuulApplication.class, args); }

    @Bean
    public PreFilter preFilter() {
        return new PreFilter();
    }

    @Bean
    public PostFilter postFilter() {
        return new PostFilter();
    }

    @Bean
    public ErrorFilter errorFilter() {
        return new ErrorFilter();
    }

    @Bean
    public RouteFilter routeFilter() {
        return new RouteFilter();
    }
} 

Ideja na početku je bila da se movie-consumer ne obraća movie-producer-u, već Zuul-u. Stoga, potrebno je napraviti dve izmene u klasi MovieConsumerController. Prvo, umesto discoveryClient.getInstances(„MOVIE-PRODUCER“), staviti discoveryClient.getInstances(„MOVIE-NETFLIX-ZUUL“), jer je to ime koje smo odabrali u application.properties. Drugo, baseUrl=baseUrl+“/producer/movie“, takođe jer smo tako definisali u application.properties. Kao i do sada, pokrenemo sledeće Spring Boot aplikacije eureka-server, movie-producer, movie-netflix-zuul i movie-consumer, respektivno.

Dakle, definisani filteri su se izvršili.

I na ovaj način, movie-consumer je proizveo željeni rezultat. Konačan projekat koji koristi Zuul nalazi se ovde.

Zaključak

Nakon kreiranja mikroservisnih aplikacija movie-consumer i movie-producer u prvom delu, istakle su se neke od prednosti mikroservisne arhitekture:

  • Aplikacije se razvijaju relativno nezavisno jedne od drugih. Mogu se i isporučivati nezavisno.
  • Centralizovano upravljanje je minimalno.
  • Iz navedenih prednosti proizilazi i to da se aplikacije mogu razvijati i u različitim tehnologijama i korišćenjem različitih programskih jezika. Moguće je i korišćenje različitih baza podataka.

Međutim, došle su do izražaja i mane koje proizilaze iz složenosti međuservisne komunikacije. Netfliksovom komponentom Eureka Servise Registry and Discovery smo uspešno rešili problem manuealnog menjanja konfiguracionih parametara korišćenih servisa.

Korišćenjem Zuul-a ilustrovali smo rutiranje u mikroservisnoj arhitekturi. Jasno je da je zbog jednostavnosti primera korišćen samo movie-producer, ali na isti način bi se dodalo još aplikacija kojima bi se pristupalo na identičan način.

Izvori i korisni linkovi

Autor: Sara Vujicic

Studentkinja četvrte godine informatike na Prirodno-matematičkom fakultetu u Kragujevcu.

Sara Vujicic

Studentkinja četvrte godine informatike na Prirodno-matematičkom fakultetu u Kragujevcu.