MSA with SPRING CLOUD(2) -Discovery Service_Spring Cloud Netflix Eureka

InSeok·2023년 3월 21일
1

외부의 다른 서비스들이 마이크로 서비스를 검색하기 위한 개념

Key, Value로 저장된다고 가정할 때

KeyValue
서비스 명들...서비스의 위치들...

각각의 마이크로서비스가 어디에 누가 저장되어있는지, 요청 정보에 따라 서비스의 위치를 알려주는 역할이다.

스프링에서는 Spring Cloud Netflix Eureka가 있다.

클라이언트가 요청한 것을 load balancer나 api gateway로 전달되고, 요청 정보가 서비스 디스커버리에 전달되어 필요한 서비스가 어디있는지 확인한 후, 해당 마이크로 서비스에 전달한다.

@EnableEurekaServer

  • @SpringBootApplication을 실행시킴
  • 서버의 자격으로 등록을 해주는 @EnableEurekaServer.
  • 메인 메서드 실행 시 이 애플리케이션은 서비스 디스커버리 역할을 해준다고 메모리에 등록을 하는 역할

해당 애플리케이션의 이름설정

spring:
    application:
        name: xxxservice

유레카 클라이언트 설정

서비스 디스커버리는 마이크로서비스가 아닌, 요청이 왔을 경우 어느 마이크로서비스로 가야하는지 알려주는 역할이다. register-with-eureka와 fetch-registry가 디폴트로 true로 설정이 되어있는데, 이걸 false로 선언해야한다. 만약 true로 설정할 경우 이 서비스 디스커버리도 마이크로서비스로 인식이 되기 때문에 굳이 할 이유가 없는 작업이어서 false로 설정해두자.

eureka:
    client:
        register-with-eureka: false
        fetch-registry: false

@EnableDiscoveryClient vs @EnableEurekaClient

EnableDiscoveryClient는 유레카 이외에 consul, zookeeper들이 구현되어있으며, spring-cloud-commons에 기반을 두고 있다. EnableEurekaClient는 유레카 관련만 의존해 있으며, spring-cloud-netflix에 기반을 두고 있다. 만약 msa를 Eureka 기반으로 구성한다면 EnableEurekaClient를 사용하면 되고, 그 외에는 EnableDiscoveryClient를 사용하면 된다.(EnableDiscoveryClient가 좀 더 많은 것들을 구현하고 있으니깐)

@EnableDiscoveryClient 로 내 서비스 등록하기

eureka:
    client:
# 유레카 서비스로 등록register-with-eureka: true
# 해당 서비스가 검색 되도록 true 설정fetch-registry: true
# 서비스 디스커버리에 내가 만든 서비스 등록하기service-url:
            defaultZone: http://127.0.0.1:8761/eureka

서비스를 여러 개의 인스턴스로 실행하는 방법

인텔리제이에서 실행하기

  • 인텔리제이 우측 상단에 Run/Debug Configuration에서 가능
  • 내가 만든 서비스를 카피하여
    • 밑의 Environment를 열어 VM Options에 -Dserver.port=XXXX로 설정해서 여러 포트에 같은 서버를 실행시킬 수 있다.

이렇게 확인이 가능하다.

인텔리제이 내의 터미널에서 실행하기

  • 프로젝트의 최상단으로 이동하기(src, target, pom.xml이 있는 위치)
  • mvn spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=XXXX'
    • D 옵션을 통해 스프링 부트를 실행할 때 인자를 전달해주는 명령어를 실행해야한다.

마찬가지로 확인하기!

외부 터미널로 실행하기

  • 프로젝트의 최상단으로 이동하기(src, target, pom.xml이 있는 위치)
  • mvn clean
  • mvn compile package 입력
    • 해당 프로젝트를 컴파일, 빌드하는 과정이며 끝나면 target 폴더에 jar파일 생성
  • java -jar -Dserver.port=9004 ./target/'jar 파일 명'

랜덤포트 사용하기 (Load Balancer)

  • yml파일에 port를 0으로 설정하면 스프링부트가 알아서 랜덤하게 포트를 배정해준다.
  • 이 때, 실행한 인스턴스 설정들을 다 지워줘야 한다.

  • 대시보드에는 포트가 0으로 써있지만, 서비스를 실행한 콘솔에는 포트 번호가 명시되어있는 것을 확인

랜덤포트로 여러 서버 인스턴스를 실행했을 때, 대시보드에는 하나의 인스턴스만 표시되는 이유

  • 대시보드에는 host ip + service name(application name) + port로 표기되어있다.
  • application.yml 파일에 표기된 정보에 기반하여 출력되기 때문에 0번 포트로 여러 인스턴스를 돌려도 하나밖에 표기되지 않는다.
  • 이 때, 랜덤포트로 생성된 인스턴스를 대시보드에 따로 표기하는 방법
    eureka:
        instance:
            instance-id: ${spring.cloud.client.hostname}:${spring.application.instance_id:${random.value}}

  • 이전과는 다르게 랜덤포트로 설정되어도 서로 다른 인스턴스로 표시된다.

Reverse Proxy

리버스 프록시는 요청을 대신해서 보내는 것이다. 클라이언트의 요청을 받아 적절한 웹 서버로 요청을 전송한다. 요청을 받은 서버는 응답을 전송할 떄, 다시 이 리버스 프록시에게 전달하여 그 응답을 클라이언트에게 제공한다.

대표적으로는 nginx가 우리가 잘 알고있는 리버스 프록시 서버이다.

Api Gateway?

  • 사용자가 설정한 라우팅 설정에 따라서 각 엔드 포인트로 클라이언트 대신해서 요청을 하고 응답받으면 다시 클라이언트에게 전송하는 reverse proxy 역할
  • 클라이언트가 마이크로서비스를 직접 호출하는 구조는 별로 좋지 않음
  • 시스템의 내부 구조는 숨기고 외부의 요청에 대해 적절한 형태로 가공하여 응답을 클라이언트에게 전달하는 장점을 갖고 있다.
  • 클라이언트와 각 마이크로서비스 사이에 위치하여 단일 진입점 형태로 개발하며, 각각의 마이크로서비스 로 요청되는 모든 정보에 대해 모든 것들을 처리한다.
  • 즉, 클라이언트는 각각의 마이크로서비스들과 통신하는 것이 아닌 api gateway랑 통신하게 된다.

API Gateway가 하게 될 것

  • 인증과 권한 부여
  • 마이크로서비스들의 검색 통합
  • 속도 제한과 부하 분산(클라이언트의 원하는 서비스를 찾아줌)
  • 로깅
  • 헤더, 쿼리 문자열 및 청구 변환
  • IP 허용 목록에 추가

Spring cloud에서의 MSA간 통신

  • RestTemplate
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.getForObject("http://localhost:8080/", User.class, 200);
  • Feign Client
    @FeignClient("stores")// 호출하고 싶은 마이크로 서비스 명 작성public interface StoreClient {
        @GetMapping("/stores")
        List<Store> getStores();
    }

Ribbon

  • Client side Load Balncer
    • 클라이언트가 직접 서비스의 이름으로 호출한다.
  • 비동기와 호환이 잘 되지 않는다.
  • Health Check : 해당 서비스가 잘 작동하는지 확인
  • 현재 부트 2.4에서는 Maintenance 상태

Nexflix Zuul

  • Routing, Api Gateway 역할
  • Netflix Zuul도 부트 2.4에서 Maintenance 상태

Spring Cloud Gateway

  • Gateway Handler Mapping으로 들어오는 요청들을 적절한 대상으로 라우팅하는 간단하고 효과적인 방법 제공
  • 기존의 내장 서버는 톰캣이었다. 하지만 Gateway의 내장 톰캣은 Netty인 것을 알 수 있다.
  • Netty는 Non-blocking, Asynchronous 방식이다. 때문에 서블릿 컨테이너나 war로 빌드하면 동작하지 않는다.
  • Spring Cloud Gateway는 기본적으로 세 가지의 핵심 단위로 구성되어 있다.
    • Route : 목적지 URI, 충족해야 될 여러 조건들(Predicates), 각종 필터들로 이루어진 요청을 라우팅할 대상들이라고 가정
    • Predicate : 자바 8의 Function Predicate로 이루어져 있다. path 혹은 리퀘스트 헤더에 포함된 조건들을 의미한다. 즉, 라우팅에 필요한 조건!
    • Filter : 스프링 프레임워크의 WebFilter의 인스턴스이다. 사용자가 보내는 request, 응답 받는 response를 수정하거나 정제하는 것.

Spring Cloud Gateway의 동작 방식

  • 클라이언트는 스프링 클라우드 게이트웨이에 요청을 보낸다.
  • Gateway Handler Mapping이 요청 정보가 게이트웨이에 설정된 Route로 전달하는 것을 결정하고, Gateway Web Handler에게 전송한다.
  • Gateway Web Handler는 요청에 따른 필터체인을 통해 요청을 보낸다.
  • 그림에서 필터들이 점선으로 구분된 이유는, Proxied Service에 요청을 보내기 전에 해당 게이트웨이 서버에서 필터를 통한 로직을 수행해야하기 때문이다. 위에서 말했던 API Gateway가 하게 될 것들이라 생각하면 되겠다.
  • 사전에 필터들에 대한 로직들을 수행한 다음, proxied server에 원하는 정보를 얻고 사후 필터들에 대한 로직이 동작하여 클라이언트에게 응답이 간다.

Filter 설정

Java code로 설정

@Configuration
public class FilterConfig {

    @Bean
    public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/first-service/**")
                        .filters(f -> f.addRequestHeader("first-request", "first-request-header")
                                .addResponseHeader("first-response", "first-response-header"))
                        .uri("http://localhost:8081"))
                .route(r -> r.path("/second-service/**")
                        .filters(f -> f.addRequestHeader("second-request", "second-request-header")
                                .addResponseHeader("second-response", "second-response-header"))
                        .uri("http://localhost:8082"))
                .build();
    }
}
  • 람다 표현식을 통해 라우팅 설정
  • route 메서드 내에 마이크로서비스 path를 설정한다.
  • filters 메서드를 통해 request, response header를 각각 추가
  • 마지막에 마이크로서비스의 uri로 매핑시켜주기

application.yml로 라우팅 설정

server:
  port: 8080
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka
spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
            - AddRequestHeader=first-request, first-request-header-withyml
            - AddResponseHeader=first-response, first-response-header-withyml
        - id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
            - AddRequestHeader=second-request, second-request-header-withyml
            - AddResponseHeader=second-response, second-response-header-withyml
  • spring.cloud.gateway.routes : 프로젝트 각각의 마이크로서비스에 대한 라우팅 설정
  • id : 마이크로서비스 고유 id
  • uri : 실행 중인 마이크로서비스의 uri(포트까지!)
  • predicates : 위에서 설명한 조건절
    • 만약 'localhost:8080/first-service/hihi' url을 요청하면 yml 파일 설정에 적힌 것 처럼 first-service path는 8081포트를 사용하는 마이크로서비스인 id : first-service로 라우팅되어 있으므로 8081/first-service/hihi로 라우팅된다.
  • filters : 위의 자바 코드로 설정하는 방법처럼, 요청이나 응답에 대한 값들을 정제할 수 있다.

개인적으로는 자바코드로 설정하는 것이 훨씬 나아보인다. 컴파일로 내가 잘못 작성했을 경우 에러가 발생하기 때문에!

Custom Filter application.yml

@Component
@Slf4j
public class CustomFilter extends AbstractGatewayFilterFactory<CustomFilter.Config> {

    public CustomFilter() {
        super(Config.class);
    }

    public static class Config {
// Put the configuration properties
    }

    @Override
    public GatewayFilter apply(Config config) {

// Custom Prefilterreturn (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            log.info("Custom Pre filter : request id -> {}", request.getId());

// Custom Postfilterreturn chain.filter(exchange)
                    .then(Mono.fromRunnable(() -> {
                        log.info("Custom Post filter : response statusCode -> {}", response.getStatusCode());
                    }));
        };
    }
}

Global Filter와 Custom Filter의 차이

각 라우팅 정보마다 커스터마이징한 필터를 등록해주어야 한다. 앞에서 설정했던 Custom Filter가 그렇다. 하지만 Global Filter의 경우 라우팅 정보들 마다 공통적으로 필요한 처리들을 한번의 설정으로 해결해준다.

둘 다 적용되었을 경우의 순서 : Global Filter PRE -> Custom Filter PRE -> Custom Filter POST -> Global Filter POST

application.yml 설정

server:
  port: 8080
eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka
spring:
  application:
    name: apigateway-service
  cloud:
    gateway:
# Global Filter 설정하기default-filters:
        - name: GlobalFilter# 클래스명args:
            baseMessage: Spring Cloud Gateway Global Filter
            preLogger: true
            postLogger: true
      routes:
        - id: first-service
          uri: http://localhost:8081/
          predicates:
            - Path=/first-service/**
          filters:
            - CustomFilter
#            - AddRequestHeader=first-request, first-request-header-withyml#            - AddResponseHeader=first-response, first-response-header-withyml- id: second-service
          uri: http://localhost:8082/
          predicates:
            - Path=/second-service/**
          filters:
            - CustomFilter
#            - AddRequestHeader=second-request, second-request-header-withyml#            - AddResponseHeader=second-response, second-response-header-withyml

LoggingFilter

Api Gateway에서는 로깅을 위한 필터도 존재하는데, 여기서 로깅 필터도 Custom Filter라서 마이크로 서비스 마다 등록을 해주어야 한다. 강의 내용 처럼 second-service에 등록하자.

이 Logging Filter를 등록하게 되면 요청에 대한 흐름은 다음과 같다.

Load Balancer

기본적으로 라운드 로빈 방식으로 각 마이크로서비스를 한번씩 호출한다.

API Gateway에서uri에 lb://서비스명 이라고 설정해두었는데, 이 때 랜덤포트로 설정한 경우는 서비스 명으로 게이트웨이에서 인스턴스를 찾아 각 서비스를 한번씩 호출한다.

설정에 따라 랜덤하게 서버 인스턴스를 호출할 수 있다.

출처

profile
백엔드 개발자

0개의 댓글