본문 바로가기

개발(합니다)/Java&Spring

[spring boot 설정하기-23] spring cloud loadbalancer 설정 및 테스트 소스

반응형

1. 개요

많은 트래픽을 분산해서 처리하기 위해서는 서버를 여러대를 두고 처리량을 나누서 부하를 낮추는 방법을 사용합니다.

OSI 7 계층에서 보면 L4, L7 계층에서 Switch라는 비싼 하드웨어 장비를 두고 사용하는 방식으로 서버사이드 로드밸런싱을 합니다.

하드웨어 서버사이드 방식

서버사이드는 서버를 증설하면 되기때문에 간단히 조치할 수 있지만 아래와 같은 문제점을 가지고 있습니다.

1. Switch가 처리할 수 있는 요청수에는 한계가 있습니다.

2. Switch를 증설, 셋팅 설정이 어렵습니다.

3. 하드웨어 비용이 비쌉니다.

4. Switch가 문제가 생길걸 대비해 이중화 해야합니다.

5. 수동으로 등록해주고 사람이 상황에 따라 조치해야 해서 유연성이 떨어집니다.

소프트웨어 클라이언트 서버사이드 방식

서버사이드의 단점을 보완하고자 나온 방식으로 클라이언트사이드 로르밸런서입니다.

위 그림에서처럼 중간에 있는 switch를 클라이언트로 옮기면서 서비스를 직접 선택하도록 소프트웨어로 구성합니다.

Ribbon과 LoadBalancer의 차이

  • Ribbon은 Blocking방식의 HttpClient RestTemplate만 지원하고 SCL은 RestTemplate과 WebClient도 지원합니다.
  • Ribbon은 RoundRobin, AvailabilityFilteringRule, WeightedResponseTimeRule를 지원하고 SCL은 RoundRobin과 Random을 지원합니다.

SCL(Spring Cloud LoadBalancer) 동작 방식

  1. Client의 요청을 EventLoop Job에 등록
  2. ServiceInstanceListSupplier로 생성된 Bean이 있는지 체크
  3. WebClient의 filter로 주입한 ReactorLoadBalancerExchangeFilterFunction으로 Service Instance 목록을 조회하고 없을 시 Service Registry Server에서 목록을 조회
  4. 조회 된 Service Instance 목록에서 LoadBalance 정책에 따라 Instance를 연결
  5. 연결 된 instance의 주소를 WebClient로 전달
  6. WebClient는 SCL에 제공 된 Service instance의 주소를 호출
  7. 응답을 받아 EventLoop는 Client에 리턴

2. Server LoadBalancer 설정

2-1. 의존성 추가

cloud-config와 eureka에 연동한 의존성도 함께 추가합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.cloud:spring-cloud-bus'
    implementation 'org.springframework.cloud:spring-cloud-starter'
    implementation 'org.springframework.cloud:spring-cloud-starter-config'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'

    implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp'
    implementation 'org.springframework.security:spring-security-rsa'
    implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'

}

2-2. bootstrap.yml

spring:
  application:
    name: member-service
  profiles:
    active: default
  config:
    import: optional:configserver:http://localhost:8888


encrypt:
  key: my_config_key

2-3. application.yml

server:
  port: 9191

2-4. LoadbalanceServerApplication.java

@EnableDiscoveryClient 를 등록합니다.

package com.otrodevym.loadbalanceserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class LoadbalanceServerApplication {

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

}

2-5. LoadBalanceContoller.java

package com.otrodevym.loadbalanceserver;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;


@RestController
@RequestMapping("/loadbalance/server")
@Slf4j
public class LoadBalanceContoller {
    @Value("${server.port}")
    private int port;


    @GetMapping("/webclient/{param}")
    public String testWebClient(
            @PathVariable(value = "param") String param,
            @RequestHeader HttpHeaders httpHeaders,
            @CookieValue(name = "httpclient-type", required = false, defaultValue = "undefined") String httpClientType
    ) {
        log.info(" >>> cookie httpclient-type {} ", httpClientType);
        httpHeaders.forEach((s, strings) -> {
            log.info(String.format(" >>>  header '%s' => %s", s, strings));
        });

        String msg = "(" + httpHeaders.get("host") + ":" + port + ")" + param + " => Working";
        log.info(" send: {}", msg);
        return msg;
    }

}

3. Client LoadBalancer 설정

3-1. 의존성 추가

Client도 cloud-config와 eureka 함께 의존성을 추가합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.cloud:spring-cloud-starter'
    implementation 'org.springframework.cloud:spring-cloud-starter-config'
    implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.projectreactor:reactor-test'

    implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp'
    implementation 'org.springframework.security:spring-security-rsa'
    implementation 'org.springframework.cloud:spring-cloud-starter-bootstrap'

}

3-2. bootstrap.yml

spring:
  application:
    name: client-service
  profiles:
    active: default
  config:
    import: optional:configserver:http://localhost:8888
  cloud:
    loadbalancer:
      ribbon:
        enabled: false

encrypt:
  key: my_config_key

3-3. application.yml

아주 중요한 부분은 spring.cloud.loadbalancer.ribbon.enabled: false ribbon을 사용하지 않는 설정을 해주어야 합니다.

server:
  port: 9292
spring:
  cloud:
    loadbalancer:
      ribbon:
        enabled: false

3-4. LoadBalanceController.java

client-service에서 member-service를 호출하기 위한 WebClient를 설정합니다.

package com.otrodevym.loadbalanceclient;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;


@RestController
@RequestMapping("/loadbalance/client")
@Slf4j
@RequiredArgsConstructor
public class LoadBalanceController {
    private final ReactorLoadBalancerExchangeFilterFunction lbFunction;

    @GetMapping("/test1")
    public Mono<String> test1() {
        WebClient client = WebClient.builder()
                .filter(this.lbFunction)
                .baseUrl("http://member-service")
                .build();
        return client.get()
                .uri("/loadbalance/server/webclient/test1")
                .retrieve()
                .bodyToMono(String.class);
    }



}

4. Cloud-Config Server Repo 설정(skip 가능) : gatewayserver-yml

본 설정은 gateway와 loadbalancer를 동시에 설정하고 다르게 호출했을 때 찾을 수 있는지를 확인하기 위한 설정으로
서버사이드와 클라이언트 사이드에서 lb 하는 방식에 대한 차이를 보기 위한 설정입니다.


        - id: loadbalance-client
          uri: http://localhost:9292
          predicates:
            - Path=/loadbalance/client/**
            # - After=2021-05-15T20:20:20.126+09:00[Asia/Seoul]
            # - Before=2021-05-15T20:20:20.126+09:00[Asia/Seoul]
            # - Between=2021-05-15T20:20:20.126+09:00[Asia/Seoul], 2021-05-15T21:21:21.126+09:00[Asia/Seoul]
            # - Weight=group-user, 9
          filters:
            - name: GlobalFilter
              ages:
                baseMessage: Spring Cloud Gateway GlobalFilter
                preLogger: true
                postLogger: true
#            - RewritePath=/order/(?<path>.*),/$\{path}

        - id: loadbalance-server-high
          uri: lb://member-service
          predicates:
            - Path=/loadbalance/server/**
            # - After=2021-05-15T20:20:20.126+09:00[Asia/Seoul]
            # - Before=2021-05-15T20:20:20.126+09:00[Asia/Seoul]
            # - Between=2021-05-15T20:20:20.126+09:00[Asia/Seoul], 2021-05-15T21:21:21.126+09:00[Asia/Seoul]
            - Weight=group-loadbalance, 9
          filters:
            - name: GlobalFilter
              ages:
                baseMessage: Spring Cloud Gateway GlobalFilter
                preLogger: true
                postLogger: true
#            - RewritePath=/order/(?<path>.*),/$\{path}

        - id: loadbalance-server-low
          uri: lb://member-service
          predicates:
            - Path=/loadbalance/server/**
            # - After=2021-05-15T20:20:20.126+09:00[Asia/Seoul]
            # - Before=2021-05-15T20:20:20.126+09:00[Asia/Seoul]
            # - Between=2021-05-15T20:20:20.126+09:00[Asia/Seoul], 2021-05-15T21:21:21.126+09:00[Asia/Seoul]
            - Weight=group-loadbalance, 1
          filters:
            - name: GlobalFilter
              ages:
                baseMessage: Spring Cloud Gateway GlobalFilter
                preLogger: true
                postLogger: true
#            - RewritePath=/order/(?<path>.*),/$\{path}

5. 테스트

member-service(Server-LoadBalancer)를 2개를 각각 9191, 9192로실행시킵니다.

Cloud-Gateway에 의한 호출

호출하면 9191과 9292를 번갈아가면서 호출합니다.

Cloud-LoadBalancer에 의한 호출

 

반응형