Spring Cloud Gateway 마이크로서비스 구현

image-20220603191530117

1. Eureka Server

Eureka(유레카)는 넷플릭스에서 공개한 Discovery Pattern을 사용한 미들웨어 서버입니다. Eureka는 Eureka Client로 설정된 서비스를 일정 간격으로 폴링하여 서비스의 IP, Port 등 정보를 자동으로 등록하고 관리합니다. 게이트웨이 또는 마이크로 서비스는 유레카의 등록된 정보를 참조하여 다른 서비스들을 참조할 수 있기 때문에 서비스의 IP, Port 등 변경에 유연하고 편리합니다.

각 마이크로서비스는 등록된 모든 마이크로서비스의 정보를 주기적으로 갖고 와서 캐싱할 수 있습니다.

kubernetes의 k8s service가 service discovery역할을 하기 때문에 모든 서비스가 kubernetes위에서 서비스 된다면 eureka는 필요없습니다. 다만 kubernetes 외 다른 runtime환경과 함께 마이크로서비스가 운영된다면 모든 서비스를 discovery할 수 있는 eureka가 필요합니다.

  • 환경 구성

image-20220530203308535

application.yml

server:
  port: 8761

spring:
  application:
    name: discoverservice

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

Application에 @EnableEurekaServer 추가

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

서버 시작 후 http://localhost:8761/ 으로 접속하면 Eureka 화면이 보입니다.

image-20220530212659098

2. Service 설정

build.gradle

implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.2'

application.yml

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka
  instance:
    prefer-ip-address: true # eureka에 호스트네임이 아닌 ip로 등록

spring:
  config:
    activate:
      on-profile: local
  application:
    name: receipt-api
  cloud:
    inetutils:
      ignored-interfaces: ens192     # 제외할 이더넷
      preferred-networks: 211.xxx.xxx  # 선호하는 ip대역

Application에 @EnableEurekaServer 추가

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

3. Spring Cloud Gateway

  • Routing
  • Load balancing
  • 인증/인가
  • 유량제어, 트래픽 통제

image-20220523211427197

localhost EmbeddedRedisServer 값 확인

C:\Windows\system32>cd C:\Program Files\Redis

C:\Program Files\Redis>redis-cli -h localhost -p 6380
localhost:6380> AUTH
(error) ERR wrong number of arguments for 'auth' command

localhost:6380> keys *
1) "refreshToken:test"
2) "refreshToken"
localhost:6380> type refreshToken
set
localhost:6380> type refreshToken:test
hash

localhost:6380> get "refreshToken:test"
(error) WRONGTYPE Operation against a key holding the wrong kind of value

localhost:6380> HGETALL refreshToken:test
 1) "email"
 2) "test@gmail.com"
 3) "id"
 4) "test"
 5) "_class"
 6) "api.manager.global.config.redis.RefreshToken"
 7) "expirationTime"
 8) "600"
 9) "name"
10) "\xea\xb9\x80\xeb\x8f\x99\xed\x98\x81"
11) "token"
12) "eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJwcmMyMDAiLCJhdXRob3JpdGllcy..."

사용자 인증(Authentication) JWT Token 검증 및 만료 accessToken 자동 갱신: Spring Cloud Gateway

로그인, 로그아웃, accessToken 갱신(새탭) 등으로 JWT accessToken 및 refreshToken 생성, 파기 역할: manager/auth

사용자 역할 및 작업 수행의 권한 여부는 각 서비스에서 정의한다.

MicroService Feign client

timeout

logging

retry

header

  • 분산 시스템, 특히 클라우드 환경에선 실패(Failure)는 일반적인 표준이다 (Failure as a First Class Citizen)
  • 모놀리틱엔 너어어어무 확률이 적어서 신경 안썼던 장애 유형
  • 한 마이크로 서비스의 가동률(uptime) 최대 99.99%이라고 할 때 ✔ 30개의 마이크로 서비스가 있다면, 99.99^30 = 99.7% uptime ✔ 10억개의 요청 중 0.3% 실패 -> 300만 요청이 실패 ✔ 모든 서비스들이 이상적인 uptime을 가지고 있어도 매 달 2시간 이상의 downtime이 발생한다는 수학적 예측

MSA에서 서비스간 장애가 전파 : 하나의 컴포넌트가 느려지거나 장애가 나면 그 장애가 난 컴포넌트를 호출하는 종속된 컴포넌트까지 장애가 전파된다.

장애 전파에 대한 해결책: Service 간 Circuit Breaker를 설치한다. 만약에 특정 Service 가 문제가 생겼음을 Circuit breaker가 감지한 경우에는 특정 Service 로의 호출을 강제적으로 끊어서 요청 Service에서 쓰레드들이 더 이상 요청을 기다리지 않도록 해서 장애가 전파하는 것을 방지 한다.

  • Service A는 상품 목록을 화면에 뿌려주는 서비스이다.
  • Service B는 사용자에 대해서 머신러닝을 이용하여 상품을 추천해주는 서비스이다. 그리고 Service B가 장애가 나면 상품 추천을 해줄 수 없다.
  • 이때 상품 진열자 (MD)등이 미리 추천 상품 목록을 설정해놓고, Service B가 장애가 났다.
  • Circuit breaker에서 이 목록을 리턴해주게 하면 머신러닝 알고리즘 기반의 상품 추천보다는 정확도는 낮아지지만 최소한 시스템이 장애가 나는 것을 방지 할 수 있고 다소 낮은 확률로라도 상품을 추천하여 꾸준하게 구매를 유도할 수 있다.

  • : 장애를 유발하는 (외부) 시스템에 대한 연동을 조기에 차단 (Fail Fast) 시킴으로서 나의 시스템을 보호함
  • 기본 설정,
    10초동안 20개 이상의 호출이 발생했을 때, 50% 이상의 호출에서 에러가 발생하면 Circuit을 open함.

✔ Circuit이 open된 경우의 에러처리? -> Fallback ✔ Fallback method는 Circuit이 오픈된 경우 혹은 Exception이 발생한 경우 대신, 호출될 Method. ✔ 장애 발생시 Exception대신 응답할 Default구현을 넣는다

circuit breaker를 더 발전 시킨것이 Fall-back messaging, Circuit breaker에서 Service B가 정상적인 응답을 할 수 없을 때, Circuit breaker가 룰에 따라서 다른 메세지를 리턴하게 하는 방법

정상적 응답이 오면 Circuit breaker는 계속 ‘CLOSED’상태임

- 일정횟수 이상 비정상적 응답이 오면 Circuit Breaker는 ‘OPEN’상태가 됨. 더 이상 Producer마이크로서비스를 호출하지 않고, 빠른 실패(Fast failing)를 위한 Fallback method를 호출함. Fallback method는 에러메시지나 캐싱된 결과를 리턴함.

Circuit breaker는 OPEN된 상태에서 일정시간이 지나면 Producer마이크로서비스를 1번 호출함. 정상적 결과가 오면, Circuit Breaker의 상태를 ‘CLOSED’로 바꾸고, Producer마이크로서비스를 호출하기 시작함. 비정상적 결과가 오면, OPEN된 상태에서 일정시간이 경과했는지 계산하는 타이머를 0으로 초기화함.

Circuit Breaker 의 상태값에는 3가지 상태 값이 있다.

CLOSED : 정상 OPEN : circuit 이 열려 있는 상태 , 제공자 서버 불안정 HALF_OPEN : 오류 상태에서 정상 상태 여부 판단을 하기 위한 반 열림 상태

추가로 Hystrix의 커스터마이징을 알아보도록 하겠습니다. Hystrix의 기본ㅇ느 서킷브레이크를 오픈하여 장애 전파 방지를 가능하게 합니다. hystrix는 기준시간 동안 요청값을 검사하여 그중 에러 비율이 어느정도를 넘으면 서킷을 오픈하여 일정 시간동안 fallback 처리한다

2018년에 Netflix가 ribbon, hytrix를 유지관리 모드 (maintenance mode, 새로운 기능을 추가하지 않고 버그 및 보안 문제만 수정)로 더 이상 개발하지 않는다고 발표하면서 Spring은 다음과 같은 대안을 권장하였다.

Hystrix -> Resilience4j

Hystrix Dashboard / Turbine -> Micrometer + Monitoring System

Ribbon -> Spring Cloud Loadbalancer

Zuul 1 -> Spring Cloud Gateway

Archaius1 -> Spring Boot external config + Spring Cloud Config

Resilience4j 라이브러리 구성

image-20220603194925855

오류 시나리오

시나리오 1 - 요청 서비스에서 Exception이 발생한 경우.

  • 서비스A에서 서비스B의 API(/service-b/test)를 FeignClient로 호출하였을 때 서비스B의 API(/service-b/test)에서 RuntimeException이 발생한다.

  • 서비스B의 /service-b/test에 대한 fallback method로 응답한다.

  • circuitbreaker가 OPNE되면 /service-b/test는 더 이상 실행되지 않고 fallback method가 바로 실행된다. 또한 fallback method로 넘어오는 Throwable 매개변수 class가 io.github.resilience4j.circuitbreaker.CallNotPermittedException로 넘어온다.

    DEBUG i.g.r.retry.configure.RetryAspect - Created or retrieved retry 'userApi' with max attempts rate '3'  for method:
    DEBUG i.g.r.c.c.CircuitBreakerAspect - Created or retrieved circuit breaker 'userApi' with failure rate '30.0' for method:
    DEBUG i.g.r.c.i.CircuitBreakerStateMachine - Event NOT_PERMITTED published: 2022-06-07T14:29:42.691232500+09:00[Asia/Seoul]: CircuitBreaker 'userApi' recorded a call which was not permitted.
    findUserByIdFallback call!  exception: class io.github.resilience4j.circuitbreaker.CallNotPermittedException
    
  • circuitbreaker가 CLOSE되면 /service-b/test가 다시 실행된다.

시나리오 2 - 요청 서비스가 Down된 경우

  • 서비스A에서 서비스B의 API(/service-b/test)를 FeignClient로 호출하였을 때 서비스B가 down되어 응답이 없다.

Spring resilience4j 설정

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-actuator:2.6.7'
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j:2.1.2'

// implementation 'org.springframework.boot:spring-boot-starter-aop:2.6.7'
// implementation "io.github.resilience4j:resilience4j-spring-boot2:${resilience4jVersion}"
// implementation "io.github.resilience4j:resilience4j-metrics:${resilience4jVersion}"
// implementation "io.github.resilience4j:resilience4j-micrometer:${resilience4jVersion}"

application.yml

resilience4j.circuitbreaker:
  configs:
    default:
      minimumNumberOfCalls: 10 # 서킷 실패율 또는 지연응답을 계산하기 전에 유구되는 최소 요청 수
      slidingWindowType: COUNT_BASED # 횟수의 실패률을 이용하여 circuit의 상태를 확정, TIME_BASED(시간 기반)
      slidingWindowSize: 5 # COUNT_BASED라면 array 크기, TIME_BASED라면 시간(5s)
      failureRateThreshold: 30 # 지정된 실패율(%)보다 커지면  CircuitBreaker open된다.(default: 50)
      waitDurationInOpenState: 10s # CircuitBreaker open 상태에서 half-open으로 변경되기 전에 대기하는 시간
      registerHealthIndicator: true # actuator를 통해 circuitbreaker 상태를 확인하기 위해 설정 http://localhost:9091/actuator
  instances:
    userApi:
      baseConfig: default
resilience4j.retry:
  configs:
    default:
      maxAttempts: 3 #최대 재시도 수
      waitDuration: 500ms # 재시도 사이에 고정된 시간
  instances:
    userApi:
      baseConfig: default
resilience4j.timelimiter:
  configs:
    default:
      cancelRunningFutrue: false
      timeoutDuration: 1s
  instances:
    userApi:
      baseConfig: default

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    shutdown:
      enabled: true
    health:
      show-details: always
  health:
    circuitbreakers:
      enabled: true

Service.java

@Transactional(readOnly = true)
@CircuitBreaker(name = "userApi", fallbackMethod = "findUserByIdFallback")
@Retry(name = "userApi")
public UserModel findUserById(String id) {
    log.info("findUserById call!");
    if(id != null)
        throw new RuntimeException("CircuitBreaker Test!");

    return this.userModelAssembler.toModelDetail(userRepository.findById(id)
                                                  .orElseThrow(() -> new NotFoundDataException()));
}

public UserModel findUserByIdFallback(Throwable t) {
    log.info("findUserByIdFallback call!  exception: " + t.getClass());

    return UserModel.builder().build();
}

Actuator로 circuitBreakers 상태 확인 해보기

chrome json viewer 설치

https://chrome.google.com/webstore/detail/json-viewer/gbmdgpbipfallnflgajpaliibnhdgobh?hl=ko

http://localhost:9091/actuator/health

circuitBreakers CLOSE상태

"status": "UP",
  "components": {
    "circuitBreakers": {
      "status": "UP",
      "details": {
        "managerApi": {
          "status": "UP",
          "details": {
            "failureRate": "-1.0%",
            "failureRateThreshold": "30.0%",
            "slowCallRate": "-1.0%",
            "slowCallRateThreshold": "100.0%",
            "bufferedCalls": 1,
            "slowCalls": 0,
            "slowFailedCalls": 0,
            "failedCalls": 1,
            "notPermittedCalls": 0,
            "state": "CLOSED"
          }
        }
      }
    },

circuitBreakers OPEN상태

{
  "status": "UP",
  "components": {
    "circuitBreakers": {
      "status": "UNKNOWN",
      "details": {
        "managerApi": {
          "status": "CIRCUIT_OPEN",
          "details": {
            "failureRate": "100.0%",
            "failureRateThreshold": "30.0%",
            "slowCallRate": "0.0%",
            "slowCallRateThreshold": "100.0%",
            "bufferedCalls": 10,
            "slowCalls": 0,
            "slowFailedCalls": 0,
            "failedCalls": 10,
            "notPermittedCalls": 1,
            "state": "OPEN"
          }
        }
      }
    },

java Non Blocking 을 지원하는 DB Driver

https://r2dbc.io/

참조:

https://sabarada.tistory.com/205?category=822738

https://javachoi.tistory.com/402

https://luvstudy.tistory.com/150

https://mycup.tistory.com/387