서킷 브레이커 패턴(Circuit Breaker Pattern)
RECOMMEND POSTS BEFORE THIS
1. MicroService Architecture
마이크로서비스 아키텍처(MSA, MicroService Architecture)는 한 가지 일만 잘하는 서비스들이 협업하는 아키텍처이다. 서비스들은 서로 협업을 위해 이벤트 혹은 REST API 통신을 수행한다. REST API 통신은 동기식 처리이기 때문에 한 서비스에서 에러가 발생하거나 느려지면 다른 서비스들로 장애가 전파된다.
서비스F에서 발생한 예외가 이를 의존하는 서비스들로 전파된다.- 장애 전파를 막기 위해 다음과 같은 것들을 고려해야 한다.
- 마이크로서비스 아키텍처는 스스로 회복성(resilience)을 가지도록 설계되어야 한다.
- 장애가 다른 서비스로 전파되지 않도록 장애를 격리해야 한다.
2. Circuit Breaker Pattern
한 서비스가 느려지면 응답을 받지 못한 서비스의 스레드가 대기하게 되면서 사용 가능한 스레드가 줄기 때문에 장애가 전파된다. 혹은 예외(exception) 처리가 미흡하면 예외가 다른 서비스로 전달되면서 장애가 전파된다. 마이크로서비스 아키텍처는 장애 전파를 막기 위해 회로 차단기(circuit breaker) 패턴을 사용한다. 회로 차단기 패턴은 이름처럼 회로 차단기 역할을 수행하는 모듈(module)이 예외가 발생하는 경로를 차단한다.
2.1. How to work circuit breaker?
클라이언트(client), 공급자(supplier) 모두 서비스이다. 다른 서비스의 기능이 필요해 요청을 하는 서비스가 클라이언트, 클라이언트 서비스의 요청을 처리하는 서비스가 공급자이다.
- 클라이언트가 공급자로 요청을 수행한다.
- 장애가 없다면 회로 차단기는 요청을 그대로 전달한다.
- 회로가 닫혀 있다고 표현한다.(circuit closed)
- 공급자 서비스에 문제가 발생하면 회로 차단기는 공급자 서비스로 요청하는 경로를 차단한다.
- 회로가 열렸다고 표현한다.(circuit open)
- 회로가 열린 경우 대체 계획(fallback plan)으로 지정한 응답을 클라이언트 서비스에게 대신 전달한다.
3. Practice
netflix-hystrix 의존성을 사용해 회로 차단기 패턴을 적용시켜 보자. 다음과 같은 실습 환경을 구축했다.
- JUnit 프레임워크로 작성한 테스트 코드를 통해 API 요청을 수행한다.
/post/{id}경로로 API 요청을 수행하며id의 범위에 따라서비스A는 다음과 같은 응답을 전달한다.0 ~ 24범위의 경우 정상 응답을 전달한다.25 ~ 49범위의 경우 임의로 1초 대기 후 정상 응답을 전달한다.50 ~ 74범위의 경우 임의로 런타임 예외(runtime exception)를 던진다.75 ~ 99범위의 경우 다시 정상 응답을 전달한다.
pom.xml 파일에 netflix-hystrix 관련 의존성을 추가한다.
<properties>
<spring-cloud.version>Hoxton.SR10</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
PostController 클래스에 회로 차단기 테스트를 위한 로직을 추가한다. id 범위에 따른 정상/에러 응답을 반환한다.
package cloud.in.action.controller;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;
@Log4j2
@RestController
public class PostController {
private void sleep(int milli) {
try {
Thread.sleep(milli);
} catch (Exception e) {
log.error(e.getMessage());
}
}
@GetMapping(value = "/post/{id}")
public String getPost(@PathVariable(name = "id") Integer id) {
boolean execution = new Random().nextBoolean();
String result = String.format("POST(id: %s)", id);
if (25 <= id && id < 50 && execution) {
sleep(1000);
} else if (50 <= id && id < 75 && execution) {
throw new RuntimeException("occur intentional exception");
}
return result;
}
}
4. Test
테스트 코드를 실행하기 전에 이전 단계에 준비한 서비스를 실행한다. 다음 아래와 같은 회로 차단기가 적용된 서비스 객체를 준비한다. 적절한 회로 차단을 위해 다음과 같이 설정한다.
- 회로 차단기를 설정할 메서드 위에
@HystrixCommand애너테이션을 추가한다. - fallbackMethod 속성
- 정상적인 응답을 받지 못하는 경우 대체 계획을 지정한다.
- commandProperties 속성
- 회로 차단기를 위한 설정들을 추가한다.
@HystrixProperty애너테이션을 통해 다음과 같은 설정들을 지정할 수 있다.execution.isolation.thread.timeoutInMilliseconds- 메서드 호출 이후 모니터링하는 시간이다.
- 해당 시간이 지나면
fallbackMethod로 지정한 메서드를 실행한다. - 기본값 1000ms
metrics.rollingStats.timeInMilliseconds- 요청을 시작한 시점부터 요청에 대한 오류 감지를 수행하는 시간이다.
- 측정되는 시간 동안 오류가 발생한 비율에 따라 회로의 개폐 여부가 결정된다.
- 기본값 10000ms
circuitBreaker.requestVolumeThreshold- 오류 감지 시간 동안 최소 요청 횟수를 설정할 수 있다.
- 최소 요청 횟수를 달성하면 요청 실패에 대한 통계를 내어 설정 값보다 높으면 회로를 차단한다.
- 이후 요청은 모두 실패로 간주하고
fallbackMethod로 지정한 메서드를 실행한다. - 기본값 20회
circuitBreaker.errorThresholdPercentage- 오류 감지 시간, 최소 요청 횟수를 모두 만족할 때 요청 실패에 대한 통계를 낸다.
- 이 설정 값보다 실패 확률이 높은 경우 회로를 차단한다.
- 이후 요청은 모두 실패로 간주하고
fallbackMethod로 지정한 메서드를 실행한다. - 기본값 50%
circuitBreaker.sleepWindowInMilliseconds- 회로 차단기가 다른 서비스의 회복 상태를 확인하기까지 대기하는 시간이다.
- 해당 설정 시간만큼 기다린 후에 재요청을 해보고 서비스 정상 여부를 확인한다.
- 기본값 5000ms
- 기타 설정들
@Service
class CircuitBreakerService {
private final RestTemplate restTemplate = new RestTemplate();
private String fallbackPlan(int index) {
return "fallback plan";
}
@HystrixCommand(
fallbackMethod = "fallbackPlan",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "20"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "3000")
}
)
public String getPost(int index) {
String url = String.format("http://localhost:8000/post/%s", index);
return restTemplate.getForObject(url, String.class);
}
}
이제 위에서 정의한 서킷 브레이커 패턴이 적용된 CircuitBreakerService 객체를 사용해 API 요청을 수행해보자. 아래 테스트를 실행한다.
/post/{id}경로로 API 요청을 수행한다.- 0.1초 간격으로 100회 반복 요청하며 각 인덱스가 호출 시 사용하는
id값이다.
- 0.1초 간격으로 100회 반복 요청하며 각 인덱스가 호출 시 사용하는
@SpringBootTest애너테이션을 추가하여 통합 테스트를 수행한다.@EnableCircuitBreaker애너테이션을 추가하여 회로 차단기를 활성화한다.
package cloud.in.action.hystrix;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
// ... CircuitBreakerService Class
@Log4j2
@EnableCircuitBreaker
@SpringBootTest
public class CircuitBreakerTest {
@Autowired
private CircuitBreakerService circuitBreakerService;
void sleep(int milli) {
try {
Thread.sleep(milli);
} catch (Exception e) {
log.error(e.getMessage());
}
}
@Test
public void circuit_close_open() {
for (int index = 0; index < 100; index++) {
sleep(100);
String result = circuitBreakerService.getPost(index);
log.info(result);
}
}
}
테스트 결과를 로그를 통해 확인해보자. 로그를 id 범위에 맞춰 나눠 살펴보겠다. 요청을 받은 서비스는 0 ~ 24 범위 동안 정상적인 응답을 한다.
2023-02-05 23:35:40.610 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 0)(response time: 252)
2023-02-05 23:35:40.720 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 1)(response time: 3)
2023-02-05 23:35:40.829 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 2)(response time: 3)
2023-02-05 23:35:40.938 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 3)(response time: 2)
2023-02-05 23:35:41.047 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 4)(response time: 2)
...
2023-02-05 23:35:42.794 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 20)(response time: 1)
2023-02-05 23:35:42.903 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 21)(response time: 2)
2023-02-05 23:35:43.013 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 22)(response time: 2)
2023-02-05 23:35:43.122 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 23)(response time: 2)
2023-02-05 23:35:43.230 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 24)(response time: 2)
요청을 받은 서비스는 25 ~ 49 범위 동안 임의로 1초 대기 후 응답한다.
- 현재 타임아웃(timeout) 에러 기준은 500ms이므로 요청을 받은 서비스의 스레드가 1초 멈추는 경우 에러이다.
- 응답 대기를 약 500ms 수행 후 에러로 판단하여
fallback plan문자열을 반환한다.
2023-02-05 23:35:43.339 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 25)(response time: 2)
2023-02-05 23:35:43.447 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 26)(response time: 2)
2023-02-05 23:35:44.072 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 519)
2023-02-05 23:35:44.179 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 28)(response time: 2)
2023-02-05 23:35:44.288 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 29)(response time: 2)
...
2023-02-05 23:35:49.605 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 511)
2023-02-05 23:35:49.715 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 46)(response time: 1)
2023-02-05 23:35:49.824 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 47)(response time: 2)
2023-02-05 23:35:49.930 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 48)(response time: 1)
2023-02-05 23:35:50.549 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 510)
요청을 받은 서비스는 50 ~ 75 범위 동안 임의로 런타임 예외를 던진다.
- 지정한 10초 동안 5회 이상 에러가 발생하면 발생 확률에 따라 회로를 차단한다.
- 회로가 차단된 경우 응답 대기 시간 없이
fallback plan문자열을 반환한다. - 상대 서비스가 정상화됐는지 확인하기 위해 1초마다 회로를 잠시 연결하여 실제로 API 요청을 수행한다.
2023-02-05 23:35:50.661 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 50)(response time: 1)
2023-02-05 23:35:50.770 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 51)(response time: 1)
2023-02-05 23:35:50.879 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 52)(response time: 2)
2023-02-05 23:35:50.991 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 8)
2023-02-05 23:35:51.092 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
...
2023-02-05 23:35:51.853 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
2023-02-05 23:35:51.961 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
2023-02-05 23:35:52.070 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
2023-02-05 23:35:52.183 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 64)(response time: 4)
2023-02-05 23:35:52.289 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 65)(response time: 2)
2023-02-05 23:35:52.402 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 5)
...
2023-02-05 23:35:53.046 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
2023-02-05 23:35:53.153 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
2023-02-05 23:35:53.261 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
요청을 받은 서비스는 75 ~ 99 범위 동안 정상 응답한다.
- 상대 서비스가 정상화됐는지 확인하기 위해 1초마다 회로를 잠시 연결하여 실제로 API 요청을 수행한다.
- 응답이 정상적으로 오는 것을 확인하면 회로를 다시 연결한다.
2023-02-05 23:35:53.370 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
2023-02-05 23:35:53.481 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
2023-02-05 23:35:53.588 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
2023-02-05 23:35:53.697 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : fallback plan(response time: 0)
2023-02-05 23:35:53.808 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 79)(response time: 3)
2023-02-05 23:35:53.917 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 80)(response time: 2)
...
2023-02-05 23:35:55.645 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 96)(response time: 1)
2023-02-05 23:35:55.755 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 97)(response time: 2)
2023-02-05 23:35:55.862 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 98)(response time: 1)
2023-02-05 23:35:55.972 INFO 20692 --- [ main] c.in.action.hystrix.CircuitBreakerTest : POST(id: 99)(response time: 2)
CLOSING
회로 차단기를 활성화하는 애너테이션은 두 개 존재한다. 각 애너테이션의 차이점은 다음과 같다.
@EnableHystrixhystrix를 사용하겠다는 의미로 내부에@EnableCircuitBreaker애너테이션이 추가되어 있다.hystrix를 사용한 회로 차단기 패턴이 적용된다.
@EnableCircuitBreaker- 회로 차단기 패턴을 구현한 라이브러리를 활성화한다.
hystrix이 외에 다른 의존성을 사용할 수 있다.
netflix-hystrix 의존성은 간단한 모니터링 기능도 함께 제공한다. 다음과 같은 의존성을 pom.xml 파일에 추가한다.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
다음과 같은 애너테이션을 추가하여 대시보드 기능을 활성화할 수 있다.
package cloud.in.action;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@EnableHystrixDashboard
@SpringBootApplication
public class ActionInBlogApplication {
public static void main(String[] args) {
SpringApplication.run(ActionInBlogApplication.class, args);
}
}
서비스의 /hystrix 경로로 접근한다.
댓글남기기