Spring/Spring Cloud
spring open feign retry (circuit breaker)
아이티.파머
2022. 1. 14. 11:41
반응형
2022.01.14 - [Spring/Spring Cloud] - Spring Open Feign
2022.01.14 - [Spring/Spring Cloud] - open feign log 설정
2022.01.14 - [Spring/Spring Cloud] - spring open feign 예제
2022.01.14 - [Spring/Spring Cloud] - open feign error decoder custom
2022.01.14 - [Spring/Spring Cloud] - spring open feign retry (circuit breaker)
Open Feign circuit breaker 테스트 시나리오는 다음과 같다. (fallback & retry) 기능 수행
전제 조건
- service client api 와 product-service-api 는 eureka server에 등록되어 있다. → discovery pattern을 사용하지 않고 @FeginClient(url=”{host}”)를 사용하는경우엔 eureka server가 빠져도 된다.
- client는 service-client-api 로 요청을 한다.
- service-client-api 는 product-service-api 에게 상품정보 목록을 요청한다.
- 어떤이유로 인해 product-service-api는 타임아웃 혹은 서버오류로 인해 HTTP Status 코드가 정상(200~300)이 아닌 다른 코드를 반환한다.
- service-client-api 는 circuit breaker 정책에 의해 최대 5번의 재호출후(재호출 모두실패) client 에게 fallback 메세지를 전달한다.
build.gradle
plugins {
id 'org.springframework.boot' version '2.6.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2021.0.0")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
test {
useJUnitPlatform()
}
service-client-api module
spring boot application
@SpringBootApplication
@EnableFeignClients
public class ClientApplication {
public static void main(String[] args) throws Exception{
SpringApplication.run(ClientApplication.class, args);
}
}
- @EnableFeignClients 를 선언하여, 페인 클라이언트를 활성화 시킨다.
Custom FeignConfig
@Configuration
public class GlobalCustomFeignConfig {
@Bean
public Retryer retryer(){
return new Retryer.Default();
}
}
- Retry.Default() :
this(100, *SECONDS*.toMillis(1), 5);
기본 밀리세컨드 단위로 되어 있다.- period, ducation, max attempts 로 되어 있으며, 기본 5회 호출 대기시간 100 * 1 mil sec 로 구성되어 있다.
ClientController.
@Slf4j
@Controller
@AllArgsConstructor
public class ClientController {
private final FallbackClient fallbackClient;
@RequestMapping(method = RequestMethod.GET, value = "/client/feign/fallback/lazy/getAll")
public @ResponseBody
String fallbackClientGetAll() {
String responseStr = fallbackClient.productLazyGetAll();
log.debug("responseStr = {}", responseStr);
return responseStr;
}
@RequestMapping(method = RequestMethod.GET, value = "/client/feign/test/eureka/findone")
public @ResponseBody
String fallbackClientNone() {
String responseStr = fallbackClient.product();
log.debug("responseStr = {}", responseStr);
return responseStr;
}
}
Feign FallbackClient
@FeignClient(
name="MOS-SERVICE-PRODUCT"
,fallback =FallbackClient.Fallback.class
,decode404 = true
)
public interface FallbackClient {
@RequestMapping("/product/lazy/get_all")
String productLazyGetAll();
@RequestMapping("/external/product")
String product();
@Component
public class Fallback implements FallbackClient{
@Override
public String productLazyGetAll() {
return "fixed response getall";
}
@Override
public String none() {
return "fixed response none";
}
}
}
@FeignClient를 선언하고, 필요한 속성 값들을 입력한다.
- 여기서
fallback
값은 FallBackClient를 구현한한다. @Compoment로 Bean 등록을 꼭 해준다. name
은 필수 값으로써 페인 클라이언트의 이름 혹은 eureka 를 사용 하고 있다면 eureka 에서 discovery된 이름을 적어준다.url
예제에서는 없지만 eureka를 사용하지 않고 직접 url을 입력해서사용할때 디폴트 기본 도메인이 된다.
application.yml
server:
port: ${PORT:80}
spring:
application:
name: mos-server-client
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
feign:
circuitbreaker:
enabled: true
autoconfiguration:
jackson:
enabled: true
client:
config:
default:
errorDecoder: com.client.config.GlobalCustomErrorDecoder
connectionTimeout: 3000
readTimeout: 3000
default-to-properties: false
feign.circuitbreaker.enable:true
기본 false 임으로 true 로 변경해 주어야 한다.- connectionTimeout : feigin client 가 맺는 타임아웃 (milisecond)
- readTimeout : feigin client 읽는 시간에 대한 타임 아웃 (milisecond)
service-product-api module
- 상품 서비스 모듈
@Slf4j
@Component
public class ProductRouters {
@Bean
public RouterFunction<ServerResponse> routerFunction(ProductHandler productHandler) {
return
RouterFunctionsroute(GET("/product/lazy/get_all")
.and(RequestPredicates.accept(MediaType.ALL)), request -> {
try {
// 테스트를 위한 지연시간 4 초 , 게이트웨이 설정 시간 3초
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
return ServerResponse.ok().body(Mono.just("hello user get all"), String.class);
}).and(
.route(GET("/external/product"), request -> {
try {
String productId = request.queryParam("productId").orElseThrow(IllegalArgumentException::new);
APIResponse apiResponse = new APIResponse("SUCCESS", "data", "message");
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(
Mono.just(apiResponse), APIResponse.class
);
} catch (Exception e) {
log.error("product find error",e);
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
Mono.just(new APIResponse("FAIL", HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e.getMessage())), APIResponse.class
);
}
}))
;
}
}
- /product/lazy/get_all : readTimeout
- 응답 시간이 3초 임으로 , 응답 지연을 4초로 셋팅하여 fallback 이 동작되는것을 확인
- /external/product : Exception
- productId 값이 파리미터 값으로 없으면 ServerResponse status 값을 INTERAL_SERVER_ERROR 로 설정 한다.
Logs
open feign client 에서 ‘/external/product’ 를 호출 하였을때 productId 값을 보내지 않으면 server error 가 발생하고 retry 정책에 의해 5번을 호출하고 fallback 이 실행된 것을 확인 할 수 있다.
2022-01-10 17:44:56.555 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignDefaultClient : [ProductApiFeignDefaultClient#findOne] ---> GET http://localhost:8083/external/product HTTP/1.1
2022-01-10 17:44:56.747 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignDefaultClient : [ProductApiFeignDefaultClient#findOne] <--- HTTP/1.1 500 Internal Server Error (191ms)
2022-01-10 17:44:56.747 WARN 80320 --- [pool-1-thread-2] c.c.config.GlobalCustomErrorDecoder : global error code 500
2022-01-10 17:45:11.939 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] ---> GET http://MOS-SERVICE-PRODUCT/external/product HTTP/1.1
2022-01-10 17:45:11.955 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] <--- HTTP/1.1 500 Internal Server Error (16ms)
2022-01-10 17:45:11.955 WARN 80320 --- [pool-1-thread-2] com.client.config.ProductErrorDecoder : product error code 500
2022-01-10 17:45:12.112 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] ---> RETRYING
2022-01-10 17:45:12.112 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] ---> GET http://MOS-SERVICE-PRODUCT/external/product HTTP/1.1
2022-01-10 17:45:12.118 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] <--- HTTP/1.1 500 Internal Server Error (5ms)
2022-01-10 17:45:12.118 WARN 80320 --- [pool-1-thread-2] com.client.config.ProductErrorDecoder : product error code 500
2022-01-10 17:45:12.345 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] ---> RETRYING
2022-01-10 17:45:12.346 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] ---> GET http://MOS-SERVICE-PRODUCT/external/product HTTP/1.1
2022-01-10 17:45:12.352 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] <--- HTTP/1.1 500 Internal Server Error (6ms)
2022-01-10 17:45:12.352 WARN 80320 --- [pool-1-thread-2] com.client.config.ProductErrorDecoder : product error code 500
2022-01-10 17:45:12.691 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] ---> RETRYING
2022-01-10 17:45:12.691 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] ---> GET http://MOS-SERVICE-PRODUCT/external/product HTTP/1.1
2022-01-10 17:45:12.695 DEBUG 80320 --- [pool-1-thread-2] c.c.e.ProductApiFeignEurekaClient : [ProductApiFeignEurekaClient#findOne] <--- HTTP/1.1 500 Internal Server Error (4ms)
2022-01-10 17:45:12.696 WARN 80320 --- [pool-1-thread-2] com.client.config.ProductErrorDecoder : product error code 500
반응형