클라이언트 사이드 로드 밸런싱 (FeignClient와 Ribbon)

ayboori·2024년 8월 5일
0

MSA

목록 보기
4/8

클라이언트 사이드 로드 밸런싱 개요

로드 밸런싱이란?

  • 로드 밸런싱은 네트워크 트래픽을 여러 서버로 분산시켜 서버의 부하를 줄이고, 시스템의 성능과 가용성을 높이는 기술
  • 서버 간 트래픽을 고르게 분배하여 특정 서버에 부하가 집중되는 것을 방지
  • 종류: 클라이언트 사이드 로드 밸런싱, 서버 사이드 로드 밸런싱

클라이언트 사이드 로드 밸런싱이란?

  • 클라이언트 사이드 로드 밸런싱은 클라이언트가 직접 여러 서버 중 하나를 선택하여 요청을 보내는 방식
  • 클라이언트는 서버의 목록을 가지고 있으며, 이를 바탕으로 로드 밸런싱을 수행

FeignClient 개요

참고

FeignClient란?

  • FeignClient는 Spring Cloud에서 제공하는 HTTP 클라이언트로, 선언적으로 (따로 구현체 없이) RESTful 웹 서비스를 호출할 수 있음
    +) Http Client : Http 요청을 간편하게 만들어서 보낼 수 있도록 돕는 객체
  • Eureka와 같은 서비스 디스커버리와 연동하여 동적으로 서비스 인스턴스를 조회하고 로드 밸런싱을 수행
    (RestTemplate와 같은 역할)

FeignClient의 주요 특징

  • 선언적 HTTP 클라이언트: 인터페이스와 어노테이션을 사용하여 REST API를 호출할 수 있음
  • Eureka 연동: Eureka와 통합하여 서비스 인스턴스 목록을 동적으로 조회하고 로드 밸런싱을 수행
  • 자동 로드 밸런싱: Ribbon이 통합되어 있어 자동으로 로드 밸런싱을 수행
  • 어노테이션 사용 : SpringMvc에서 제공되는 어노테이션을 그대로 사용할 수 있다.
    (Spring Cloud의 starter-openfeign을 사용할 경우)
  • 동기적 동작 : 하나의 요청이 끝나야 다음 동작이 가능

Ribbon 개요

Ribbon이란?

  • 넷플릭스가 개발한 클라이언트 사이드 로드 밸런서로, 마이크로서비스 아키텍처에서 서비스 인스턴스 간의 부하를 분산
  • 다양한 로드 밸런싱 알고리즘을 지원하며, Eureka와 같은 서비스 디스커버리와 연동하여 사용

4.3.2 Ribbon의 주요 특징

  • 서버 리스트 제공자: Eureka 등으로부터 서비스 인스턴스 리스트를 제공받아 로드 밸런싱에 사용
  • 로드 밸런싱 알고리즘: 라운드 로빈, 가중치 기반 등 다양한 로드 밸런싱 알고리즘 지원
  • Failover: 요청 실패 시 다른 인스턴스로 자동 전환

FeignClient와 Ribbon 설정

기본 설정

  • FeignClient와 Ribbon을 사용하려면 Spring Boot 애플리케이션에 의존성을 추가해야 함
  • build.gradle 파일 예시:
    dependencies {
        implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
        implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    }
  • Spring Boot 애플리케이션 설정:
    @SpringBootApplication
    @EnableFeignClients // 어노테이션 추가
    public class MyServiceApplication {
        public static void main(String[] args) {
            SpringApplication.run(MyServiceApplication.class, args);
        }
    }

FeignClient 인터페이스 작성

  • FeignClient 인터페이스를 작성하여 서비스 호출을 수행
  • 예시 코드:
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @FeignClient(name = "my-service") // 호출하고자 하는 host
    
    public interface MyServiceClient {
    
    	// 내부 메소드 작성하여 api 서비스 호출
        @GetMapping("/endpoint")
        String getResponse(@RequestParam(name = "param") String param);
    }

로드 밸런싱 알고리즘

라운드 로빈

  • 라운드 로빈: 각 서버에 순차적으로 요청을 분배하는 방식
  • 간단하고 공평하게 트래픽을 분산

가중치 기반 로드 밸런싱

  • 가중치 기반 로드 밸런싱: 각 서버에 가중치를 부여하고, 가중치에 비례하여 요청을 분배하는 방식
  • 서버의 성능이나 네트워크 상태에 따라 가중치를 조절

기타 알고리즘

  • 최소 연결: 현재 연결된 클라이언트 수가 가장 적은 서버로 요청을 보내는 방식
  • 응답 시간 기반: 서버의 응답 시간을 기준으로 가장 빠른 서버로 요청을 보내는 방식

FeignClient와 Eureka 연동

Eureka 설정

  • Eureka와 FeignClient를 함께 사용하면 동적으로 서비스 인스턴스를 조회하여 로드 밸런싱을 수행
    (Eureka는 MSA에서 각 서비스의 위치를 동적으로 관리하고 찾아주는 기능, 모든 서비스 인스턴스의 위치를 저장하는 중앙 저장소 역할을 함)
  • application.yml 파일 설정 예시:
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:8761/eureka

FeignClient와 Ribbon 설정

  • FeignClient에서 제공하는 서비스 인스턴스를 사용하여 로드 밸런싱을 수행
  • 예시 설정 파일:
    my-service:
      ribbon:
        eureka:
          enabled: true #ribbon 활성화

FeignClient와 Ribbon 동작 원리

  1. 서비스 이름: @FeignClient(name = "my-service") 어노테이션은 Eureka에 등록된 서비스 이름을 참조 (properties 파일의 name)
  2. 서비스 인스턴스 조회: Eureka 서버에서 my-service라는 이름으로 등록된 서비스 인스턴스 목록을 조회
  3. 로드 밸런싱: 위에서 조회된 서비스 인스턴스 목록 중 하나를 선택하여 요청을 보냅니다. 이는 기본적으로 Ribbon을 사용하여 로드 밸런싱을 수행
  4. 요청 분배: 여러 서비스 인스턴스가 있을 경우, Round Robin 또는 다른 설정된 로드 밸런싱 알고리즘을 사용하여 요청을 분배

시나리오로 생각해보기

Order 서비스는 Product 서비스를 호출하여 상품 정보를 가져옵니다. 이 과정에서 어떻게 동작하는지 설명하겠습니다.
Order 서비스 인스턴스: 1개
Product 서비스 인스턴스: 3개

  1. Order 서비스 실행: Order 서비스가 실행되면 Eureka 서버에서 Product 서비스 인스턴스 목록을 가져옵니다.
  2. Product 서비스 호출: Order 서비스에서 Product 서비스의 정보를 가져오기 위해 FeignClient를 사용하여 호출합니다.
  3. Ribbon을 통한 로드 밸런싱: FeignClient는 Ribbon을 통해 3개의 Product 인스턴스 중 하나를 선택하여 호출합니다. 이 과정에서 Round Robin 알고리즘을 사용하여 요청을 순차적으로 분배합니다.
  4. 응답 처리: 선택된 Product 인스턴스에서 응답을 받아 Order 서비스에 반환하고, 최종적으로 클라이언트에 응답을 전달합니다.

FeignClient 인터페이스 작성 예시

  • RESTful 웹 서비스를 호출

  • 예시 코드:
    http://my-service/endpoint?param=... 을 호출함

    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @FeignClient(name = "my-service")
    public interface MyServiceClient {
    
        @GetMapping("/endpoint")
        // 요청 정보, 리턴 값 아래에 명시
        String getResponse(@RequestParam(name = "param") String param);
    }
    

서비스 사용 예제

  • Controller에서 FeignClient를 사용하여 다른 서비스 호출을 수행

  • 예시 코드:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class MyController {
    
        @Autowired
        private MyServiceClient myServiceClient;
    
        @GetMapping("/call-service")
        public String callService(@RequestParam String param) {
            return myServiceClient.getResponse(param);
            // 위의 인터페이스에서 받아온 응답을 보냄
        }
    }
    

실습

9분 이후

유레카 서버 하나에 order 인스턴스 1개와 같은 기능의 포트만 다른 product 인스턴스 3개를 연결합니다. (총 4개의 클라이언트 어플리케이션 생성)
상품을 요청(http://localhost:19091/order/1) 하면 응답하는 인스턴스의 포트를 받아서 노출합니다.
이를 통해 라운드로빈으로 로드밸런싱 (돌아가면서 product 호출)이 되는것을 확인합니다.

Eureka Server

  • 서비스 디스커버리 (Eureka) 강의에서 진행했던 유레카 서버를 그대로 복사해서 실행합니다.

Product instance

Product는 요청이 오면 아래의 문자열을 반환합니다.
”Product {productId} info!!!! From port : ${serverPort}”

  • ProductApplication.java
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    
    @SpringBootApplication
    @EnableFeignClients
    public class ProductApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(ProductApplication.class, args);
    	}
    
    }
  • ProductController.java
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class ProductController {
    
    	// application.properties에서 가져옴
        @Value("${server.port}") // 애플리케이션이 실행 중인 포트를 주입받습니다.
        private String serverPort;
    
        @GetMapping("/product/{id}")
        public String getProduct(@PathVariable String id) {
            return "Product " + id + " info!!!!! From port : " + serverPort ;
        }
    
        
    }
  • resources/application.yml (application.properties 파일은 삭제)
    spring:
      application:
        name: product-service
    server:
      port: 19092
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/

여러 포트에 같은 애플리케이션 실행

19092,19093,19094 포트에 같은 애플리케이션 실행하기
1) 인텔리제이의 상단 메뉴에서 실행 > 구성 편집으로 들어갑니다.
2) ProductApplication을 복제하여 여러개 생성
3) ProductApplication 의 이름을 ProdcutApplication:19092로 변경합니다.
4) 옵션수정을 클릭하여 VM옵션 추가 > -Dserver.port=19093 을 입력

localhost:포트/product/아이디
입력하면 해당 포트에 해당하는 app이 실행된다.

Order instance

Order는 요청이 오면 product를 호출하여 상품의 정보를 가져옵니다. 그리고 아래의 문자열을 반환합니다.
”Your order {orderId} product info => ${프로덕트에서 받은 문자열}”

주문은 주문아이디가 1인 주문만 있다고 가정하겠습니다.
1 주문은 112번 상품을 호출한다고 가정합니다.

  • OrderApplication.java
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    
    @SpringBootApplication
    @EnableFeignClients
    public class OrderApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(OrderApplication.class, args);
    	}
    
    }
    
  • OrderController.java
    import org.springframework.web.bind.annotation.RestController;
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    @RestController
    @RequiredArgsConstructor
    public class OrderController {
        
        private final OrderService orderService;
    
    	// end-point 에서 입력 받아온 orderId에 해당하는 
        @GetMapping("/order/{orderId}")
        public String getOrder(@PathVariable String orderId) {
            return orderService.getOrder(orderId);
        }
        
    }
  • ProductClient.java (클래스가 아닌 인터페이스로 생성)

    Product 인스턴스와 연결

    ```java
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    // 받아올 서비스의 이름을 Eureka에서 찾아와서 연결함
    @FeignClient(name = "product-service")
    public interface ProductClient {
    
    	// OrderService에서 호출하게 된다    
    //product 어플리케이션의 Controller의 getProduct를 호출한다
    @GetMapping("/product/{id}")
    String getProduct(@PathVariable("id") String id);
    }
    ```
  • OrderService.java
    import org.springframework.stereotype.Service;
    
    import lombok.RequiredArgsConstructor;
    
    @Service
    @RequiredArgsConstructor
    public class OrderService {
    
        private final ProductClient productClient;
    
   		//product 어플리케이션의 Controller의 getProduct를 호출한다
        public String getProductInfo(String productId) {
            return productClient.getProduct(productId);
        }
    
        public String getOrder(String orderId) {
            if(orderId.equals("1") ){
                String productId = "2";
                String productInfo = getProductInfo(productId);
                return "Your order is " + orderId + " and " + productInfo;
            }
            return "Not exist order...";
            }
        }
  • order의 application.yml
    spring:
      application:
        name: order-service
    server:
      port: 19091
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/
    

실행 순서
OrderController.getOrder > orderService.getOrder(orderId) > orderService.getOrder 내에서 getProductInfo > getProductInfo에서는 FeignClient의 productClient를 통해 product app 호출 / productClient 마다 포트가 있어서 실행 시 마다 어떤 포트에서 실행되는지 확인 가능

Run

  • 유레카 서버 → order → product(3개 모두) 순으로 실행
  • http://localhost:19090/ 으로 접속하면 인스턴스를 확인할 수 있음
  • PRODUCT-SERVICE에 포트가 19092,19093,19094 3개가 떠있는것을 확인할 수 있음 Untitled

확인

  • http://localhost:19091/order/1 에 접속할때마다 텍스트의 포트가 변경되는것을 볼 수 있음
    이를 통해 요청마다 라운드로빈으로 동작함을 확인할 수 있음
profile
프로 개발자가 되기 위해 뚜벅뚜벅.. 뚜벅초

0개의 댓글