분산 시스템에서의 장애에 대응하는 애플리케이션 구축 방법은 중요한 일
- 원격 서비스 문제는 탐지하기 어렵고 비정상적인 서비스 하나가 여러 애플리케이션을 짧은 시간에 다운시킬 수 있기 때문
클라이언트 회복성 패턴
- 클라이언트 측 부하 분산
- 회로 차단기(circuit breaker)
- 폴백(fallback)
- 벌크헤드(bulkhead)
1. 클라이언트 측 부하 분산
- 클라이언트가 서비스 디스커버리 에이전트(넷플릭스 유레카)를 이용해 서비스의 모든 인스턴스를 검색한 후 해당 서비스 인스턴스의 실제 위치를 캐싱하는 것. (넷플릭스 리본라이브러리)
- 문제가 된 서비스 인스턴스를 제거해 서비스 호출이 그 인스턴스로 전달되는 것을 막는다
2. 회로 차단기
- 원격 자원에 대한 모든 호출을 모니터링하고, 호출이 필요한 만큼 실패하면 회로 차단기가 활성화되어 빨리 실패하게 만들며, 고장 난 원격 자원은 더 이상 호출되지 않도록 차단한다.
- 회로 차단 패턴이 제공하는 핵심 기능
1. 빠른 실패 (fail fast)
빨리 실패함으로써 애플리케이션 전체를 다운시킬 수 있는 자원 고갈 이슈 방지
2. 원만한 실패 (fail gracefully)
타임아웃과 폴백을 사용하여 원만하게 실패하거나 대체 매커니즘을 찾을 수 있음
3. 원만한 회복(recover seamlessly)
회로 차단기는 요청 자원이 온라인 상태인지 주기적으로 확인하고, 사람의 개입 없이 자원 접근을 다시 허용 가능
3. 폴백 처리
- 원격 서비스에 대한 호출이 실패할 때 예외를 발생시키지 않고 서비스 소비자가 대체 코드 경로를 실행해 다른 방법으로 작업을 수행할 수 있다
4. 벌크헤드
- 원격 자원에 대한 호출을 자원별 스레드 풀로 분리하여 특정 원격 자원의 호출이 느려져 전체 애플리케이션이 다운될 수 있는 위험을 줄일 수 있다
스프링 클라우드와 히스트릭스를 위한 라이선싱 서버 설정
스프링 히스트릭스 메이븐 의존성 추가
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
서비스 부트스트랩 클래스에 히스트릭스 애너테이션 추가
licensing-service/src/main/java/com/hmg/licenses/LicensingServiceApplication.java
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker // 스프링 클라우드에 이 서비스에서 히스트릭스를 사용할 것이라고 지정
public class Application {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
히스트릭스를 사용한 회로 차단기 구현
히스트릭스 구현 방법 두가지 범주
- 라이선싱 및 조직 서비스 모두 자기 데이터베이스에 대한 호출을 히스트릭스 회로 차단기에 연결
- 두 서비스 사이의 호출을 히스트릭스에 연결
→ 두 범주의 호출이 다르지만 히스트릭스 사용 방법은 완전히 동일함
@HystrixCommand
- 이 애너테이션을 사용해 히스트릭스 회로 차단기가 관리하는 자바 클래스 메서드라고 표시함
- 스프링 프레임워크가 @HystrixCommand를 만나면 메서드를 감싸는 프록시를 동적으로 생성하고 원격 호출을 처리하기 위해 확보한 스레드가 있는 스레드 풀로 해당 메서드에 대한 모든 호출을 관리한다.
licensing-service/src/main/java/com/hmg/licenses/services/LicenseService.java
@HystrixCommand // 회로차단기로 getLicensesByOrg() 메서드를 연결
public List<License> getLicensesByOrg(String organizationId){
logger.debug("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
- @HystrixCommand를 사용하면 getLicensesByOrg() 메서드 호출 시마다 히스트릭스 회로 차단기와 해당 호출이 연결되어 메서드 호출이 1,000밀리초(기본값)보다 오래 걸릴 때마다 호출을 중단한다.
3회 호출 중 1회정도 1초이상 지연되는 데이터베이스 질의문을 수행하는 상황 테스트
private void randomlyRunLong(){ // 데이터 베이스 3호출 3회중 1회 지연
Random rand = new Random();
int randomNum = rand.nextInt((3 - 1) + 1) + 1;
if (randomNum==3) sleep();
}
private void sleep(){
try {
Thread.sleep(11000); // 11,000밀리초 (11초) 동안 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
해당 메서드 엔드포인트 여러번 호출 시 타임아웃 에러 메시지 응답
GET http://localhost/v1/organizations/e254f8c-c442-4ebea82a-e2fc1d1ff78a/licenses/

히스트릭스가 감싼 코드(getLicensesByOrg)를 실행하는 데 1,000밀리초(기본값) 이상 걸린다면 서비스 호출은 HystrixRuntimeException 예외를 던진다.
회로 차단기의 타임아웃 사용자 정의
호출이 타임아웃되기 전까지 히스트릭스가 기다릴 시간을 사용자 정의
@HystrixCommand(
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="12000")
}
)
public List<License> getLicensesByOrg(String organizationId){
logger.debug("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
폴백 프로세싱
회로 차단기 패턴의 장점은 원격 자원의 소비자와 리소스 사이에 ‘중간자’를 두어 개발자에게 서비스 실패를 가로채고 다른 대안을 선택할 기회를 준다는 것. 이를 폴백 전략이라 함.
@HystrixCommand(fallbackMethod = "buildFallbackLicenseList")
public List<License> getLicensesByOrg(String organizationId){
logger.debug("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
private List<License> buildFallbackLicenseList(String organizationId){
List<License> fallbackList = new ArrayList<>();
License license = new License()
.withId("0000000-00-00000")
.withOrganizationId( organizationId )
.withProductName("Sorry no licensing information currently available");
fallbackList.add(license);
return fallbackList;
}
- @HystrixCommand에 fallbackMethod 속성을 추가.
- 이 폴백 메서드는 @HystrixCommand가 보호하려는 메서드와 같은 클래스에 있어야함
- @HystrixCommand가 보호하는 메서드에 전달되는 모든 매개변수를 폴백이 받으므로 폴백 메서드는 이전 메서드와 서식이 완전히 동일해야 함
- buildFallbackLicenseList()는 가짜 정보를 담고 있는 License 객체 하나를 만들어 반환
엔드포인트 호출 테스트
타임아웃 에러 발생 시 예외가 발생하지 않는 대신 가짜 라이선스 값이 결과로 반환

벌크헤드 패턴 구현
- 벌크헤드 패턴을 적용하지 않는다면 기본적인 호출은 전체 자바 컨테이너에 대한 요청을 처리하는 스레드에서 이루어진다. 대규모 상황에서 한 서비스에서 발생한 성능 문제로 자바 컨테이너의 모든 스레드가 최대치에 도달해 비정상 종료될 수 있다.
- 벌크헤드 패턴은 원격 자원 호출을 자신의 스레드 풀에 격리하므로 오작동 서비스를 억제하고 컨테이너의 비정상 종료를 방지한다.
- 서로 다른 원격 자원 호출 간에 벌크헤드 생성 : 각 원격 자원 호출이 자기 스레드 풀 이용하여 성능 나쁜 서비스가 다른 서비스 호출에 영향을 주는 것을 방지
벌크헤드 구현
- 메서드 호출을 위한 별도 스레드 풀 설정하기
- 스레드 풀의 스레드 숫자 설정하기
- 스레드가 분주할 때 큐에 들어갈 요청 수에 해당하는 큐의 크기 설정하기
@HystrixCommand(fallbackMethod = "buildFallbackLicenseList",
threadPoolKey = "licenseByOrgThreadPool", // 스레드 풀의 고유 이름 정의
threadPoolProperties = // 스레드 풀 동작을 정의하고 설정
{@HystrixProperty(name="coreSize",value="30"), // 스레드 풀의 스레드 개수 정의 (기본값 10)
@HystrixProperty(name="maxQueueSize", value="10")} // 스레드 풀 앞에 배치할 큐와 큐에 넣을 요청수 정의 (기본값 -1)
)
public List<License> getLicensesByOrg(String organizationId){
logger.debug("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
히스트릭스 세부 설정
- 히스트릭스는 오래 수행되는 호출의 타임아웃 기능 외에도 호출 실패 횟수를 모니터링해 호출이 필요 이상으로 실패할 때 원격 자원에 도달하기 전에 호출을 실패시켜 서비스로 들어오는 이후 호출을 자동으로 차단함
- 원격 자원에 성능 문제가 있는 경우 빨리 실패하여 자원 고갈 문제와 비정상 종료 위험 낮춤
- 빠른 실패로 호출을 막으면 문제있던 시스템이 회복할 수 있는 시간을 얻는다.
히스트릭스 회로차단기 차단 결정 과정
- 서비스 에러 포착 시 서비스 호출 빈도 검사용 10초 타이머를 시작 (기본값 10초)
- 10초 시간대 동안 원격 자원 호출이 최소 호출 횟수를 넘으면 전체 실패 비율을 조사하기 시작 (기본 임계치는 50%)
전체 실패 비율이 임계치를 초과하면 회로를 차단하고 원격 자원에 대한 추가 호출을 막음. - 히스트릭스가 원격 호출에 대해 회로 차단기 차단 시 새로운 활동 시간대를 시작
5초마다 (시간대 설정 가능) 히스트릭스는 호출을 허용하여 호출이 성공하면 회로 차단기를 초기화 하고 다시 호출을 허용함.
회로 차단기 행동 양식 구현 (commandProperties)
@HystrixCommand(
fallbackMethod = "buildFallbackLicenseList",
threadPoolKey = "licenseByOrgThreadPool", // 스레드 풀의 고유 이름 정의
threadPoolProperties = { // 스레드 풀 동작을 정의하고 설정
@HystrixProperty(name="coreSize",value="30"), // 스레드 풀의 스레드 개수 정의 (기본값 10)
@HystrixProperty(name="maxQueueSize", value="10")}, // 스레드 풀 앞에 배치할 큐와 큐에 넣을 요청수 정의 (기본값 -1)
commandProperties={ // 회로 차단기 동작 사용자 정의
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="10"), // 최소 연속 호출 횟수 (기본값 20)
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="75"), // 실패해야 하는 호출 비율 (기본값 50%)
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="7000"),// 차단된 후 서비스 회복 상태 확인할 때 까지 대기할 시간 간격 (기본값 5,000ms)
@HystrixProperty(name="metrics.rollingStats.timeInMilliseconds", value="15000"),// 서비스 호출 문제를 모니터할 시간 간격 (기본값 10,000 ms)
@HystrixProperty(name="metrics.rollingStats.numBuckets", value="5")} // 설정한 시간 간격 동안 통계를 수집할 횟수 (기본값 10)
)
public List<License> getLicensesByOrg(String organizationId){
logger.debug("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
randomlyRunLong();
return licenseRepository.findByOrganizationId(organizationId);
}
클래스 레벨 프로퍼티 구성 방법
예시) 특정 클래스 안의 모든 자원에 10초 타임아웃 설정
@DefaultProperties(
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMiliseconds", value="10000")})
class MyService { ... }
스레드 컨텍스트와 히스트릭스
@HystrixCommand 격리 전략
1. THREAD
- 기본 히스트릭스 격리 전략
- 모든 히스트릭스 명령은 호출을 시도한 부모 스레드와 컨텍스트를 공유하지 않는 격리된 스레드 풀에서 수행
2. SEMAPHORE
- 히스트릭스는 새로운 스레드를 시작하지 않고 @HystrixCommand가 보호하는 분산 호출을 관리하며 타임아웃 발생 시 부모 스레드를 중단시킨다
격리 전략 설정
@HystrixCommand(
commandProperties={
@HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")})
기본적으로 THREAD 방식 권장. THREAD 격리 방식은 히스트릭스 명령 스레드와 부모 스레드 사이의 격리 수준을 높이며, SEMAPORE 방식보다 무겁다. SEMAPORE 격리 모델은 경량이며, 서비스에서 대용량을 처리하고 비동기 I/O 프로그래밍 모델(예, Netty 같은 비동기 I/O 컨테이너)을 적용할 때 사용해야 함
ThreadLocal과 히스트릭스
- 기본적으로 히스트릭스는 부모 스레드의 컨텍스트를 히스트릭스 명령이 관리하는 스레드에 전파하지 않는다.
예시
- REST 서비스에 대한 호출을 가로채 컨텍스트 정보를 추출해 UserContext객체로 변환 후 ThreadLocal에 저장하여 필요시마다 읽어옴.
- 그러나 히스트릭스 보호 메서드 실행 시 이 ThreadLocal 값 (UserContext)이 전파되지 않음.
licensing-service/src/main/java/com/hmg/licenses/utils/UserContextFilter.java
@Component
public class UserContextFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(UserContextFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
// HTTP 호출 헤더에서 검색한 값을 UserContextHolder의 UserContext에 저장한다.
UserContextHolder.getContext().setCorrelationId(httpServletRequest.getHeader(UserContext.CORRELATION_ID));
UserContextHolder.getContext().setUserId(httpServletRequest.getHeader(UserContext.USER_ID));
UserContextHolder.getContext().setAuthToken(httpServletRequest.getHeader(UserContext.AUTH_TOKEN));
UserContextHolder.getContext().setOrgId(httpServletRequest.getHeader(UserContext.ORG_ID));
logger.debug("UserContextFilter Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
filterChain.doFilter(httpServletRequest, servletResponse);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}
- UserContextHolder는 ThreadLocal 클래스에 UserContext를 저장하는데 사용.
licensing-service/src/main/java/com/hmg/licenses/utils/UserContextHolder.java
public class UserContextHolder {
// 정적 ThreadLocal 변수에 저장되는 UserContext
private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>();
// UserContext 객체를 사용하기 위해 가져오는 getContext 메서드
public static final UserContext getContext(){
UserContext context = userContext.get();
if (context == null) {
context = createEmptyContext();
userContext.set(context);
}
return userContext.get();
}
public static final void setContext(UserContext context) {
Assert.notNull(context, "Only non-null UserContext instances are permitted");
userContext.set(context);
}
public static final UserContext createEmptyContext(){
return new UserContext();
}
}
라이선싱 서비스에 로깅 추가
logger.info("LicenseService.getLicensesByOrg Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
- src/main/java/com/thoughtmechanix/licenses/utils/UserContextFilter.java의 doFilter() 메서드
- src/main/java/com/thoughtmechanix/licenses/controllers/LicenseServiceController.java의 getLicenses() 메서드
- src/main/java/com/thoughtmechanix/licenses/services/LicenseService.java의 getLicensesByOrg() 메서드.
이 메서드는 @HystrixCommand 애너테이션 존재
서비스 호출 테스트
GET http://localhost:8080/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/

로그 메시지 결과
UserContext Correlation id: TEST-CORRELATION-ID
LicenseServiceController Correlation id: TEST-CORRELATION-ID
LicenseService.getLicenseByOrg Correlation:
- 히스트릭스가 보호하는 LicenseService.getLicenseByOrg() 메서드에서는 상관관계 ID가 출력되지 않는다. (부모 컨텍스트를 공유 받지 못함)
HystrixConcurrencyStrategy 동작
- 히스트릭스 사용하면 히스트릭스 호출을 감싸는 병행성 전략을 사용자 정의하고 부모 스레드의 컨텍스트를 히스트릭스 명령이 관리하는 스레드에 주입할 수 있다
사용자 정의 HystrixConcurrencyStrategy 구현 작업
- 히스트릭스 병행성 전략 클래스 사용자 정의
- 히스트릭스 명령에 UserContext를 주입하도록 자바 Callable 클래스 정의
- 히스트릭스 병행성 전략을 사용자 정의하기 위해 스프링 클라우드 구성
히스트릭스 병행성 전략 클래스 사용자 정의
src/main/java/com/hmg/licenses/hystrix/ThreadLocalAwareStrategy.java
// Hystrix 병행성 젼략을 위한 클래스
public class ThreadLocalAwareStrategy extends HystrixConcurrencyStrategy{ // HystrixConcurrencyStrategy 클래스를 상속한다.
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
// 스프링 클라우드가 미리 정의한 병행성 클래스를 이 클래스의 생성자에 전달한다.
public ThreadLocalAwareStrategy(
HystrixConcurrencyStrategy existingConcurrencyStrategy) {
this.existingConcurrencyStrategy = existingConcurrencyStrategy;
}
// 일부 메서드의 재정의 필요.
// existingCouncurrencyStrategy 메서드 구현을 호출하거나 부모 HystrixConcurrencyStrategy 메서드를 호출한다.
@Override
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy.getBlockingQueue(maxQueueSize)
: super.getBlockingQueue(maxQueueSize);
}
...
// 히스트릭스로 보호된 메서드(callable) 호출 전 추가 작업 처리를 위해 새로운 Callable(DelegatingUserContextCallable)로 감싼다
// 부모 스레드의 ThreadLocal 값을 히스트릭스로 보호된 메서드의 스레드로 전달한다.
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
return existingConcurrencyStrategy != null
? existingConcurrencyStrategy
.wrapCallable(new DelegatingUserContextCallable<T>(callable, UserContextHolder.getContext()))
: super.wrapCallable(new DelegatingUserContextCallable<T>(callable, UserContextHolder.getContext()));
}
}
히스트릭스 명령에 UserContext를 주입하도록 자바 Callable 클래스 정의
src/main/java/com/hmg/licenses/hystrix/DelegatingUserContextCallable.java
public final class DelegatingUserContextCallable<V> implements Callable<V> {
private final Callable<V> delegate;
private UserContext originalUserContext;
// 히스트릭스로 보호된 코드를 호출하는 원본 Callable과 부모 스레드에서 받은 UserContext 전달
public DelegatingUserContextCallable(Callable<V> delegate,
UserContext userContext) {
this.delegate = delegate;
this.originalUserContext = userContext;
}
// @HystrixCommand 애너테이션이 메서드를 보호하기 전에 호출되는 Call() 함수
public V call() throws Exception {
// UserContext 설정. UserContext를 저장하는 ThreadLocal 변수는 히스트릭스가 보호하는 메서드를 실행하는 스레드에 연결된다.
UserContextHolder.setContext( originalUserContext );
try {
// UserContext 설정 후 히스트릭스가 보호하는 메서드 실행
return delegate.call();
}
finally {
this.originalUserContext = null;
}
}
public static <V> Callable<V> create(Callable<V> delegate,
UserContext userContext) {
return new DelegatingUserContextCallable<V>(delegate, userContext);
}
}
히스트릭스 병행성 전략을 사용자 정의하기 위해 스프링 클라우드 구성
src/main/java/com/hmg/licenses/hystrix/ThreadLocalConfiguration.java
@Configuration
public class ThreadLocalConfiguration {
// 구성 객체가 생성될 때 기존 HystrixConcurrencyStrategy와 자동 연결한다.
@Autowired(required = false)
private HystrixConcurrencyStrategy existingConcurrencyStrategy;
@PostConstruct
public void init() {
// 기존 히스트릭스 플러그인의 레퍼런스 유지
// 새로운 병행성 전략을 등록하기 때문에 모든 히스트릭스 컴포넌트를 가져와 히스트릭스 플러그인을 재설정한다.
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance()
.getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance()
.getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy = HystrixPlugins.getInstance()
.getPropertiesStrategy();
HystrixCommandExecutionHook commandExecutionHook = HystrixPlugins.getInstance()
.getCommandExecutionHook();
HystrixPlugins.reset();
// HystrixConcurrencyStrategy(ThreadLocalAwareStrategy)를 히스트릭스 플러그인에 등록한다.
HystrixPlugins.getInstance().registerConcurrencyStrategy(new ThreadLocalAwareStrategy(existingConcurrencyStrategy));
// 히스트릭스 플러그인이 사용하는 모든 히스트릭스 컴포넌트를 재등록한다.
HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
}
}
서비스 호출 재 테스트
GET http://localhost:8080/v1/organizations/e254f8c-c442-4ebe-a82ae2fc1d1ff78a/licenses/

- 이제 히스트릭스로 보호된 메서드에서도 부모 스레드의 컨텍스트를 공유 받음
참고
- 2018년부터 Netflix 에서 hystrix를 유지관리 모드로 (maintenance mode, 새로운 기능을 추가하지 않고 버그 및 보안 문제만 수정) 전환하여 hystrix 대체로 Resilience4j를 권장함.
'MSA > Spring Microservice In Action' 카테고리의 다른 글
부록. Dockerfile과 Docker-compose를 이용한 프로젝트 빌드 및 실행 (0) | 2022.01.20 |
---|---|
4. 서비스 디스커버리 (0) | 2022.01.09 |
3. 스프링 클라우드 컨피그 서버로 구성 관리 (0) | 2022.01.02 |
2. 스프링 부트 마이크로 서비스 구축 (0) | 2021.12.28 |
1. Spring Microservice 개요 (0) | 2021.12.28 |
댓글