서킷 브레이커란?
서킷 브레이커는 쉽게 말하면 집에있는 두꺼비집이다. 설정 이상의 전류가 흐르면 전기를 사용하는 회로를 보호하기 위해서 차단기가 작동된다. 이를 웹개발로 비유하면 과한 전류는 클라이언트의 요청이고 회로는 우리의 서비스다.
어떠한 이유에서 서비스가 장애를 겪으면 클라이언트에게 유효한 응답을 줄 수 없다. 이때 조치를 취하지 않으면 응답이 없는 서비스에 계속해서 요청을 보내게되고 우리의 한정된 자원(쓰레드, 메모리 등등)은 낭비되게 된다. 이같은 자원과 시간의 낭비를 막기위한 조치가 필요한데, 서킷 브레이커를 활용하면 위의 문제를 해결 할 수 있다.
서킷 브레이커 구성
![]()
서킷 브레이커는 크게 세 가지로 구성되어 있다. Closed, Open, Half Open이 있다. 어떤 흐름으로 서킷 브레이커가 작동하는지 알아보자.
Closed
아무런 문제가 없는 경우에는 closed 상태이다. 전기회로를 생각해보면, 전기가 흐르기 위해서는 회로가 닫혀있어야 한다. 그래서 아무런 문제가 없는 경우는 closed 상태이고 기존에 설정한 서비스가 정상적으로 작동하고 있는 상황이다.
그러다가 서비스에 문제가 생겨서 응답을 주지 못하는 상황이 발생한다. 요청된 응답 중 예외같은 오류코드가 발생한다. 이때 서킷 브레이커가 open되기 위한 기준치가 존재한다. 요청된 응답 중 설정된 비율 이상의 실패가 발생하면 서킷 브레이커는 open되게 된다.
Open
과한 전류가 흘러서 두꺼비집이 내려가듯이, 실패한 응답 비율이 설정 값보다 높아지면 서킷 브레이커가 open되어 기존의 서비스로의 흐름을 막아버린다. 그래서 일정 기간동안 요청이 들어오면 open으로 설정된 대체 서비스로 요청이 넘어간다.
그렇게 설정한 시간이 지나고나면 바로 closed 상태로 복귀해서 기존의 서비스로 복귀하면 될까? 서비스를 장애로부터 복구하기 위해서 요청을 대신 처리하긴 했지만 기존의 서비스가 아직 정삭 작동하는지는 알 수가 없다. 그래서 Half open을 이용해서 기존의 서비스가 복구되었는지 확인한다.
Half open
Half open은 open에게 요청을 받으면 기존의 서비스가 복구되었는지 확인하는 역할을 수행한다. Half open이 테스트를 하여 설정된 값을 기준으로 성공/실패 여부를 판단한다. 서비스가 복구되었다고 판단하면 서킷 브레이커는 closed상태로 변경되고 서비스는 기존의 로직으로 돌아온다. 만약 Half open이 실패라고 판단하면 다시 Open상태로 돌아가게 되고 일정시간이 지나면 다시 Open이 Half open에게 요청을 보내는 방식으로 반복된다.
도입 배경
우선 현재 우리 서비스의 메인 검색엔진은 엘라스틱 서치다. 처음에는 Mysql의 fulltext엔진을 사용하다가 여러가지 한계(검색 정확도, 필터 적용시 속도 저하 등)를 겪고 전문 검색엔진인 엘라스틱 서치를 도입하였다. 엘라스틱 서치의 성능은 비교가 안될정도로 좋았지만 아직 설정에 익숙치 않아서 그런지 서버가 불안정했다.
물론 엘라스틱 서버가 죽지 않는 것이 최선이기는 하지만 현재 서버가 불안정하기도 하고 이미 작성한 SQL쿼리를 보조 검색수단으로 사용하면 좀 더 나은 서비스를 제공할 수 있겠다고 판단하여 서킷 브레이커를 도입하였다.
Resilience4j 라이브러리
Java 진영에서는 서킷 브레이커 라이브러리로는 Hystrix와 Resilience4j가 있다. Hystrix는 넷플릭스에서 만든 오픈 소스인데 현재는 deprecated 되었으므로 Resilience4j가 대표적인 Java 진영의 서킷 브레이커 라이브러리라고 할 수 있다.
Resilience4j의 구성 요소는 다음과 같다.
- Circuit Breaker
- 위에서 언급한 서킷 브레이커를 구현한 모듈
- Bulkhead
- 동시 실행 횟수 제한 모듈 (쓰레드)
semaphore방식
semaphore
깃발이란 뜻으로, 옛날 기찻길의 공유되는 지점에서 semaphore의 색깔을 보고 지나갈 수 있는지 확인했다고 함.
FixedThreadPool 방식
둘 다 쓰레드 수를 제한하는데 무슨 차이가 있을까?
모르겠다. 추측해보자면 Fixed는 쓰레드 풀이 고정적이고 semaphore는 가변적인가?
- RateLimiter
- RateLimit에 대한 설정값 지정 가능
Rate Limit
서버가 임계치까지만 클라이언트의 요청을 허용하는 정책. Rate Limit를 설정함으로써 서비스보호할 수 있다.
- Retry
- 재시도. Retry 인스턴스를 관리할 수 있는 RetryRegistry 제공
- TimeLImiter
- TimeLimiter 인스턴스를 관리할 수 있는 인 메모리 TImeLimiterRegistry 제공
설정 파일
![]()
구현 상황
엘라스틱 서치를 Closed 환경에서, 서킷 브레이커가 Open되면 Mysql 환경으로 검색을 구현하도록 설정하였다.
실제 구현은 properties로 되어있지만 가시성을 위해 yml로 설정
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 10
failureRateThreshold: 70
waitDurationInOpenState: 4s
instances:
ElasticError:
base-config: default
어노테이션 방식으로 서킷 브레이커 설정
// ES 검색
@Transactional(readOnly = true)
@CircuitBreaker(name = "ElasticError", fallbackMethod = "keywordSearchBySql")
public BookListDto keywordSearchByElastic(FilterDto filter) {
SearchHits<BookDto> search = elasticRepository.keywordSearchByElastic(filter);
BookListDto result = resultToDto(search, filter);
return result;
}
// Cursor 기반 페이징
@Transactional(readOnly = true)
public BookListDto keywordSearchBySql(FilterDto filter, Throwable t) {
try {
log.warn("keyword Elastic Down : " + t.getMessage());
List<BookDto> books = bookDSLRepository.keywordSearchByCursor(filter);
List<Object> cursors = getCursor(books, filter.getTotalRow(), filter.getSort());
return new BookListDto(books, (String)cursors.get(0), (Long)cursors.get(1), filter.getPage(), false);
} catch (Exception e) {
log.warn("keyword Mysql SQLException : " + e.getMessage());
return new BookListDto(new ArrayList<>(), null, null, null, false);
}
}
핵심은 @CircuitBreaker(name = "ElasticError", fallbackMethod = "keywordSearchBySql")이다. yml에서 설정한 ElasticError라는 인스턴스는 default의 config를 가진다. 이 ErrorElastic 인스턴스를 keywordSearchByElastic()메서드에서 문제가 생겼을 때 기준값으로 사용한다는 말이다. fallbackMethod는 open시 사용할 메서드를 지정하는 곳이다.
트러블 슈팅
페이징 적용
현재 서비스에서 엘라스틱 서치가 쓰이는 요청은 크게 세 가지다.
- 검색 버튼을 눌러서 카테고리 + 키워드를 검색하는 경우
- 검색 페이지에서 필터 적용 버튼을 눌러서 필터 검색을 하는 경우
- 페이지 버튼을 눌러 페이지를 이동하는 경우
검색과 필터는 요청당시 서킷이 열리면 바로 Mysql로 전환해서 결과를 조회하면 된다. 하지만 페이징 문제점이 있었다. 현재 페이징의 방식은 커서 페이지네이션으로 진행하고 있는데 Mysql과 엘라스틱 서치의 커서 값이 다르다는 점이다. 같은 결과를 조회해서 키워드를 자르는 방식과 점수를 측정하는 알고리즘이 다르기 때문에 검색 결과가 일치하지 않아서 기존의 커서를 사용할 수 없다.
- 커서가 다르기 때문에 페이지 이동시에는 추가적인 작업이 필요
그래서 가능한 페이지 이동 시 서킷 브레이커가 열리는 상황에서 대처가능한 시나리오는 총 두 가지가 나왔다.
- 어떤 페이지에 있던지 간에 1페이지로 리다이렉트 한다.
- 요청한 페이지 정보를 가지고 커서 값을 모두 찾아와서 커서 리스트를 교체한다.
두 방안 중에서 두 번째 시나리오가 적절하다고 판단했다. 저 시나리오를 구현하면 페이지 이동 중간에 서킷이 열리더라도 이어서 하던 작업이 가능하다. 하지만 우리 서비스에는 첫 번째 방법으로 구현했다. 그 이유는 다음과 같다.
- Mysql의 속도
- 애초에 엘라스틱 서치로 넘어간 이유가 Mysql의 속도였다.많은 필터를 적용하면 한 페이지를 불러오는데도 시간이 오래 걸려서 문제였다. 그런데 한 페이지가 아닌 여러 페이지의 커서를 모두 찾아오는 로직을 Mysql이 하면, 결과는 안봐도 타임아웃이다. 그리고 가져온다고 한들 클라이언트는 하염없이 로딩창을 보며 기다려야 한다.
그렇다고해서 첫 번째 방법이 문제가 없는 것은 아니다. 바로 1페이지로 리다이렉트 하므로 속도는 빠르지만 기존에 보고있던 페이지를 잃어버리는 문제가 발생한다. 결국 속도와 서비스의 질의 싸움인데 나는 속도의 손을 들었다. 만약 내가 소비자라고 생각헀을 때 페이지가 1페이지로 돌아가면 그런갑다 하고 다시 이동하겠지만 몇 십초 동안 로딩이 계속되면 화면을 꺼버릴거 같아서 서비스의 질을 포기하더라도 속도를 선택했다.
'개발 > 이것저것' 카테고리의 다른 글
| fulltext vs elastic search 점수 계산 알고리즘 (0) | 2023.04.26 |
|---|---|
| Transaction이란? (0) | 2023.04.23 |
| 도커 (Docker) (0) | 2023.03.10 |
| 웹소켓 (0) | 2023.03.01 |
| SQL 개념 (0) | 2023.02.02 |