paint-brush
파리에서 베를린까지: Kotlin에서 Circuit-Breaker를 만드는 방법~에 의해@jesperancinha
420 판독값
420 판독값

파리에서 베를린까지: Kotlin에서 Circuit-Breaker를 만드는 방법

~에 의해 João Esperancinha26m2025/01/23
Read on Terminal Reader

너무 오래; 읽다

회로 차단기는 요즘 하나 또는 다른 비응답 서비스에 과도한 양의 요청이 수행되는 것을 방지하기 위해 사용됩니다. 예를 들어 어떤 이유로든 서비스가 종료되면 회로 차단기는 이름에서 알 수 있듯이 회로를 끊어야 합니다. 이 글에서는 AOP 기반 구현을 사용하여 회로 차단기를 구현하는 세 가지 프로그래밍 방식을 살펴보겠습니다.
featured image - 파리에서 베를린까지: Kotlin에서 Circuit-Breaker를 만드는 방법
João Esperancinha HackerNoon profile picture
0-item

1. 서론

Circuit-breakers 요즘 하나 또는 다른 비응답 서비스에 과도한 양의 요청이 수행되는 것을 방지하기 위해 사용됩니다. 예를 들어, 어떤 이유로든 서비스가 종료되면 회로 차단기는 이름에서 알 수 있듯이 회로를 끊어야 합니다. 다시 말해, 경마 결과를 얻기 위해 동시에 100만 개의 요청이 수행되는 상황에서 이러한 요청을 처리할 수 있는 다른 서비스로 리디렉션하려고 합니다.


이 다른 서비스는 그것의 복제본일 수도 있고, 원래 서비스 실패와 관련된 다른 작업을 수행하는 데 순수하게 사용될 수도 있습니다. 최종 목표는 항상 불필요한 호출을 중단하고 다른 곳에서 흐름을 수행하는 것입니다. 2017 년, Michael Nygard Circuit Breaker 디자인 패턴을 소프트웨어 개발 디자인의 최전선으로 가져왔습니다. 이는 그의 출판물 Release It!: Design and Deploy Production-Ready Software (Pragmatic Programmers) 1st Edition에서 이루어졌습니다.


circuit breaker 설계 패턴은 실제 전자 및 전기 회로에서 영감을 얻었습니다.그러나 일반적인 개념의 관점에서 회로 차단기 아이디어는 실제로 1879 Thomas Edison 이 발명했습니다.그 당시와 마찬가지로 넘치는 전류를 처리해야 합니다.매우, 매우 간단하게 말해서, 이것이 이 경우 소프트웨어 아키텍처에 적용하는 것입니다.주요 목표는 시스템이 충분히 회복력이 있는지 확인하는 것입니다.얼마나 회복력이 있어야 하고 얼마나 fault-tolerant 있어야 하는지는 실제로 이 패턴의 구현을 용이하게 하는 책임이 있는 엔지니어의 눈에 달려 있습니다.이것의 이면에 있는 아이디어는 특정 조건에서 특정 요청의 흐름을 동일한 endpoint 뒤에 있는 더 사용 가능한 다른 흐름으로 원활하게 리디렉션할 수 있다는 것입니다.


A 에서 B 로 요청을 수행하고 싶다고 가정해 보겠습니다. 때때로 B가 실패하고 C 는 항상 사용 가능합니다. B 무작위로 실패하면 서비스를 완전히 사용할 수 있도록 C 에 도달하려고 합니다. 그러나 B 로 다시 요청을 하려면 B 다시는 그렇게 많이 실패하지 않도록 해야 합니다. 그런 다음 시스템을 구성하여 B로 무작위 요청을 하고 실패율이 특정 수준으로 낮아진 후에야 B 로 완전히 돌아갈 수 있습니다.


오류 시뿐만 아니라 지연 시에도 요청 C를 만들고 싶을 수 있습니다. B 가 매우 느리면 모든 요청을 C 로 다시 보내고 싶을 수 있습니다. 정의된 시도 횟수, 요청 유형, 동시 스레드 및 기타 여러 옵션 후에 C 도달하려고 시도하는 것과 같이 다른 많은 가능한 구성이 있습니다. 이를 short-circuiting 라고도 하며 대부분 일시적인 움직임입니다.

상태 머신


회로 차단기가 실제로 무엇인지에 대한 지식을 더 잘 이해하려면 회로 차단기가 애플리케이션에서 엔티티로 작동한다는 것을 이해해야 합니다. 회로 차단기에는 세 가지 주요 상태가 있습니다. 닫힘, 열림 또는 반열림일 수 있습니다. 닫힘 상태는 애플리케이션 흐름이 정상적으로 실행됨을 의미합니다. 모든 요청이 서비스 B로 전송된다는 것을 알면서 서비스 A에 안전하게 요청을 할 수 있습니다. 열림 상태는 서비스 B에 대한 모든 요청이 실패함을 의미합니다. 실패를 나타내는 데 정의된 규칙이 발생하여 더 이상 서비스 B에 도달하지 못합니다. 이 경우 항상 예외가 반환됩니다. half-open 상태는 회로 차단기가 서비스 B에서 다시 작동하는지 확인하기 위해 테스트를 수행하라는 지시를 받은 경우입니다.


모든 성공적인 요청은 정상적으로 처리되지만, C에 대한 요청은 계속됩니다. B가 우리가 정한 검증 규칙에 따라 예상대로 동작하는 경우, 회로 차단기는 닫힌 상태로 돌아가고, 서비스 A는 서비스 B에만 요청을 하기 시작합니다. 대부분의 애플리케이션에서 회로 차단기는 데코레이터 디자인 패턴을 따릅니다. 그러나 수동으로 구현할 수 있으며, 회로 차단기를 구현하는 세 가지 프로그래밍 방식과 마지막으로 AOP 기반 구현을 살펴보겠습니다. 코드는 GitHub 에서 사용할 수 있습니다.

도표

2. 자동차 검사

이 글의 마지막 부분에서는 자동차 경주 게임을 살펴보겠습니다. 하지만 그 전에 circuit breaker 로 실행되는 애플리케이션을 빌드하는 몇 가지 측면을 안내해 드리고자 합니다.

2.1. Kystrix(파리에서 베를린까지-kystrix-runnable-app)

Kystrixs 는 작은 DSL 로서 Johan Haleby 가 발명하고 만든 놀라운 라이브러리입니다. 이 라이브러리는 Spring 및 Spring WebFlux와의 통합을 포함하여 많은 가능성을 제공합니다. 살펴보고 약간 놀아보는 것은 흥미롭습니다.

 <dependency> <groupId>se.haleby.kystrix</groupId> <artifactId>kystrix-core</artifactId> </dependency> <dependency> <groupId>se.haleby.kystrix</groupId> <artifactId>kystrix-spring</artifactId> </dependency>


저는 예제를 만들었고, 그것은 GitHubfrom-paris-to-berlin-kystrix-runnable-app 모듈에 있습니다. 먼저, 코드를 살펴보겠습니다.

 @GetMapping("/{id}") private fun getCars(@PathVariable id: Int): Mono<Car> { return if (id == 1) Mono.just(Car("Jaguar")) else { hystrixObservableCommand<Car> { groupKey("Test2") commandKey("Test-Command2") monoCommand { webClient.get().uri("/cars/carros/1").retrieve().bodyToMono<Car>() .delayElement(Duration.ofSeconds(1)) } commandProperties { withRequestLogEnabled(true) withExecutionTimeoutInMilliseconds(5000) withExecutionTimeoutEnabled(true) withFallbackEnabled(true) withCircuitBreakerEnabled(false) withCircuitBreakerForceClosed(true) } fallback { Observable.just(Car("Tank1")) } }.toMono() } }

이 코드는 예제의 명령 2를 나타냅니다. 명령 1의 코드를 확인하세요. 여기서 일어나는 일은 monoCommand 로 원하는 명령을 정의한다는 것입니다. 여기서 호출해야 하는 메서드를 정의합니다. commandProperties 에서 circuit-breaker 상태를 열림으로 변경하는 규칙을 정의합니다. 호출을 명시적으로 지연하여 정확히 1초 동안 지속되도록 합니다.


동시에 5000 밀리초의 타임아웃을 정의합니다. 즉, 타임아웃에 도달하지 않습니다. 이 예에서 Id 로 호출할 수 있습니다. 이는 테스트일 뿐이므로 circuit-breaker 가 필요 없는 Jaguar인 자동차의 IdId=1 이라고 가정합니다. 이는 또한 폴백 메서드에 정의된 Tank1을 결코 얻지 못할 것임을 의미합니다. 아직 알아차리지 못했다면 폴백 메서드를 자세히 살펴보세요. 이 메서드는 Observable 을 사용합니다. WebFlux Observable 디자인 패턴에 따라 구현되었지만 Flux 정확히 Observable이 아닙니다.


하지만 hystrix는 둘 다 지원합니다. 애플리케이션을 실행하고 http://localhost:8080/cars/2 에서 브라우저를 열어 이를 확인하세요. Spring Boot 시작 초기에 호출을 시작하면 결국 Tank1 메시지를 받을 수 있다는 점을 이해하는 것이 중요합니다. 이는 시작 지연이 이 프로세스를 실행하는 방법에 따라 매우 쉽게 5초를 초과할 수 있기 때문입니다. 두 번째 예에서 우리는 Tank 2로 예제를 단락시킬 것입니다.

 @GetMapping("/timeout/{id}") private fun getCarsTimeout(@PathVariable id: Int): Mono<Car> { return if (id == 1) Mono.just(Car("Jaguar")) else { hystrixObservableCommand<Car> { groupKey("Test3") commandKey("Test-Command3") monoCommand { webClient.get().uri("/cars/carros/1").retrieve().bodyToMono<Car>() .delayElement(Duration.ofSeconds(1)) } commandProperties { withRequestLogEnabled(true) withExecutionIsolationThreadInterruptOnTimeout(true) withExecutionTimeoutInMilliseconds(500) withExecutionTimeoutEnabled(true) withFallbackEnabled(true) withCircuitBreakerEnabled(false) withCircuitBreakerForceClosed(true) } fallback { Observable.just(Car("Tank2")) } }.toMono() } }

이 예에서, circuit-breaker 응답으로 개방 상태 종료 리턴 Tank 2로 전환됩니다. 이는 여기에서도 1초 지연을 발생시키기 때문이지만, 회로 차단 조건이 500ms 마크 이후에 트리거되도록 지정합니다. hystrix 어떻게 작동하는지 알고 있다면 kystrix 앞으로 나아갈 때 별반 다르지 않다는 것을 알게 될 것입니다. 이 시점에서 Hystrix가 제공하지 못한 것은 게임을 만드는 데 필요한 것을 제공하는 매끄럽고 간편한 방법이었습니다. Kystrix 클라이언트 기반으로 작동하는 것 같습니다. 즉, 메인 서비스 뒤에 있는 서비스에 요청을 하기 전에 코드를 선언해야 합니다.

2.2. 회복성4J

Resilience4J 많은 사람이 서킷 브레이커의 매우 완전한 구현으로 언급하는 것 같습니다. 저의 첫 시도는 서킷 브레이커의 몇 가지 중요한 측면을 탐색하는 것이었습니다. 즉, 타임아웃과 성공적인 요청의 빈도를 기반으로 작동할 수 있는 서킷 브레이커를 보고 싶었습니다. Resilience4J 사용하면 다양한 유형의 short-circuiting 모듈을 구성할 수 있습니다. 이들은 CircuitBreaker , Bulkhead , Ratelimiter , RetryTimelimiter6 가지 범주로 구분됩니다. 이러한 모든 것은 또한 디자인 패턴의 이름입니다. CircuitBreaker 모듈은 이 패턴의 완전한 구현을 제공합니다.


우리는 구성할 수 있는 많은 매개변수를 가지고 있지만, 근본적으로 CircuitBreaker 모듈은 우리가 무엇을 실패로 인식하는지, 반쯤 열린 상태에서 얼마나 많은 요청을 허용하는지, 그리고 시간이나 카운트로 구성할 수 있는 슬라이딩 윈도우를 구성할 수 있게 해줍니다. 여기서 우리는 닫힌 상태에서 발생하는 요청 카운트를 유지합니다. 이것은 오류 빈도를 계산하는 데 중요합니다. 근본적으로, 우리는 이 CircuitBreaker 모듈이 요청 속도를 도울 것이라고 말할 수 있지만, 반드시 그런 것은 아닙니다.


어떻게 해석하느냐에 따라 다릅니다. 오류를 처리하는 단순한 방법으로 생각하는 것이 더 나은 방법인 듯합니다. 시간 초과나 예외에서 비롯된 오류이든, 여기서 오류를 처리하고 요청을 다른 곳으로 원활하게 리디렉션할 수 있습니다. Bulkhead 모듈은 동시 요청을 처리하도록 설계되었습니다. 속도 제한기가 아닙니다.


대신, 단일 엔드포인트에서 너무 많은 처리가 발생하는 것을 방지하는 데 사용되는 Bulkhead 디자인 패턴을 구현합니다. 이 경우 Bulkhead 하면 요청을 모든 사용 가능한 엔드포인트에 분산하는 방식으로 처리할 수 있습니다. Bulkhead 라는 이름은 대형 선박이 일반적으로 침몰하는 것을 피하기 위해 가지고 있는 여러 개의 밀폐된 구획에서 유래되었으며, 사고가 발생했을 때 선박의 경우와 마찬가지로 스레드 풀에서 사용할 수 있는 스레드 수와 임대 시간을 정의해야 합니다.


RateLimiter 모듈은 요청 속도를 처리하도록 설계되었습니다. 이 모듈과 Bulkhead 모듈의 차이점은 특정 지점까지 속도를 허용해야 한다는 것입니다. 즉, 이를 위해 실패를 일으킬 필요가 없습니다. 설계에서 특정 값 이상의 속도를 허용하지 않는다고만 말합니다. 또한 요청을 리디렉션하거나 요청을 수행할 수 있는 권한이 부여될 때까지 보류할 수 있습니다. Retry 모듈은 다른 모듈과 공통점이 많지 않기 때문에 이해하기 가장 쉽습니다.


우리는 기본적으로 정의된 임계값에 도달할 때까지 특정 엔드포인트에 대한 재시도 횟수를 명시적으로 선언합니다. Timelimiter 모듈은 둘 다 타임아웃을 구성할 수 있는 가능성을 공유한다는 점에서 CircuitBreaker 모듈의 단순화로 볼 수 있습니다. 그러나 Timelimiter 슬라이딩 윈도우와 같은 다른 매개변수에 의존하지 않으며, 내장 실패 임계값 계산도 없습니다.


따라서 특정 서비스를 호출할 때 타임아웃을 처리하는 것에만 관심이 있고 다른 가능한 오류를 고려하지 않는다면 Timelimiter 사용하는 것이 더 나을 것입니다.

2.2.1. Kotlin과 Spring 프레임워크 없이 Resilience4J(from-paris-to-berlin-resilience4j-runnable-app)

이 모듈에서는 resilience4j kotlin 라이브러리만 사용하기로 결정했습니다.

 <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-kotlin</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-retry</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-circuitbreaker</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-ratelimiter</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-timelimiter</artifactId> </dependency>


이 구현은 GitHub의 repo에서 사용할 수 있습니다. 먼저 TimeLimiter 패턴을 살펴보겠습니다.

 var timeLimiterConfig: TimeLimiterConfig = TimeLimiterConfig.custom() .timeoutDuration(Duration.ofMillis(100)) .build() var timeLimiter: TimeLimiter = TimeLimiter.of("backendName", timeLimiterConfig) private suspend fun getPublicCar(): Car { return timeLimiter.decorateSuspendFunction { getPrivateCar() }.let { suspendFunction -> try { suspendFunction() } catch (exception: Exception) { Car("Opel Corsa") } } } private suspend fun getPrivateCar(): Car { delay(10000) return Car("Lancya") }

이 경우, 우리는 decorateSuspendFunction 함수를 사용하여 getPrivateCar function TimeLimiter 함수로 장식합니다. 이렇게 하면 시간 초과가 발생하고, 호출하는 함수가 너무 오래 걸리면 Lancya 대신 Opel Corsa를 받게 됩니다. 이를 시도하려면 애플리케이션을 실행하고 http://localhost:8080/cars/timelimiter/normal/1을 열면 됩니다.


구현을 살펴보면 Lancya 를 얻을 수 없다는 것을 알 수 있습니다. 이는 반환하기 전에 의도적으로 10s 기다리기 때문입니다. TimeLimiter 는 훨씬 더 짧은 시간 제한을 가지고 있으므로 이것은 결코 작동하지 않습니다. TimeLimiter 는 이해하기가 매우 간단합니다. 반면 CircuitBreaker 다른 이야기일 수 있습니다. 다음은 이를 수행하는 방법의 예입니다.

 val circuitBreakerConfig = CircuitBreakerConfig.custom() .failureRateThreshold(20f) .slowCallRateThreshold(50f) .slowCallDurationThreshold(Duration.ofMillis(1000)) .waitDurationInOpenState(Duration.ofMillis(1000)) .maxWaitDurationInHalfOpenState(Duration.ofMillis(1000)) .permittedNumberOfCallsInHalfOpenState(500) .minimumNumberOfCalls(2) .slidingWindowSize(2) .slidingWindowType(COUNT_BASED) .build() val circuitBreaker = CircuitBreakerRegistry.of(circuitBreakerConfig).circuitBreaker("TEST") private suspend fun getPublicCar(id: Long): Car { return circuitBreaker.decorateSuspendFunction { getPrivateCar(id) }.let { suspendFunction -> try { suspendFunction() } catch (exception: Exception) { Car("Opel Corsa") } } } private fun getPrivateCar(id: Long): Car { if (id == 2L) { throw RuntimeException() } return Car("Lancya") }

이 경우, 우리는 속성에서 실패율이 20% 미만이 되면 회로 차단기가 회로를 닫도록 하기를 원한다고 말하고 있습니다. 느린 호출에도 임계값이 있지만, 이 경우 50% 미만이 됩니다. 느린 호출은 1초 이상 지속되어야 하나로 간주된다고 말합니다. 또한 반개방 상태의 지속 시간이 1초여야 한다고 명시하고 있습니다. 이는 실제로 개방 상태, 반개방 상태 또는 폐쇄 상태가 된다는 것을 의미합니다.


또한 최대 500개의 반개방 상태 요청을 허용한다고 말합니다. 오류 계산의 경우 회로 차단기는 어느 마크에서 그렇게 할지 알아야 합니다. 이는 회로를 닫을 시점을 결정하는 데 중요합니다. 이 계산에는 최소한 2개의 요청이 필요하며, minimumNumberOfCalls 속성이 있습니다. 반개방은 요청이 안전한 실패 임계값에 도달하면 회로를 닫으려고 계속 시도하는 시점이라는 것을 기억하세요?


이 구성에서는 오류 빈도를 계산하고 닫힌 상태로 돌아갈지 여부를 결정하기 위해 슬라이딩 윈도우 내에서 최소 2 개의 요청을 해야 함을 의미합니다. 이는 우리가 구성한 모든 변수의 정확한 판독값입니다. 일반적으로 이는 애플리케이션이 대체 서비스에 대한 호출을 여러 번 할 가능성이 높으며, 대체 서비스가 있다면, 반열린 상태에서 이를 수행하는 성공률이 80%여야 하고 열린 상태에 대한 시간 초과가 발생해야 하기 때문에 열린 상태에서 닫힌 상태로 쉽게 전환되지 않을 것임을 의미합니다.


이러한 시간 초과를 지정하는 방법은 여러 가지가 있습니다. 우리의 예에서 maxDurationInHalfOpenState 는 1초라고 말합니다. 즉, 우리의 CircuitBreaker 우리의 체크가 닫힌 상태 조건을 충족하지 않거나 이 시간 초과가 아직 발생하지 않은 경우에만 열린 상태를 유지합니다. 이 CircuitBreaker 에 정의된 동작은 특정 다운타임, 비율 및 요청의 다른 기능을 정확히 복제할 수 없기 때문에 따르고 예측하기 어려울 수 있지만, 이 엔드포인트에 여러 요청을 수행하면 위에서 설명한 동작이 우리의 경험과 일치한다는 것을 알 수 있습니다.


따라서 엔드포인트 http://localhost:8080/cars/circuit/1http://localhost:8080/cars/circuit/2 에 대한 여러 요청을 시도해 보겠습니다. 1로 끝나는 것은 성공적인 자동차 검색의 엔드포인트이고, 2로 끝나는 것은 지정된 자동차를 가져오는 데 실패한 엔드포인트입니다. 코드를 살펴보면 2가 아닌 다른 것은 Lancya 를 응답으로 받는다는 것을 의미합니다. 2 는 즉시 런타임 예외를 throw한다는 것을 의미하며, 이는 결국 Opel Corsa 응답으로 받는다는 것을 의미합니다.


우리가 엔드포인트 1 에 요청만 하면, Lancya 응답으로 계속 보게 됩니다. 시스템이 실패하기 시작하면, 즉 2에 요청을 하면, 잠시 후에 Lancya 로 돌아가는 것이 일정하지 않다는 것을 알게 될 것입니다. System Open 상태이며 더 이상 요청이 허용되지 않는다고 알립니다.

 2021-10-20 09:56:50.492 ERROR 34064 --- [ctor-http-nio-2] .fcbrrcCarControllerCircuitBreaker : io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'TEST' is OPEN and does not permit further calls


성공적인 요청 후 회로 차단기가 반개방 상태로 전환되고, 이는 정상화되기 전에 1로 돌아가기 위해 몇 가지 요청을 수행해야 함을 의미합니다. Lancya 에서 Opel Corsa 로 몇 번 전환한 후에야 다시 Lancya 얻을 수 있습니다. 이 숫자를 2로 정의했습니다. 이는 오류 계산을 위한 최소값입니다. 실패를 하나만 발생시키고 실패하지 않은 엔드포인트를 계속 호출하면 무슨 일이 일어나고 있는지 더 명확하게 파악할 수 있습니다.

 2021-10-20 11:53:29.058 ERROR 34090 --- [ctor-http-nio-4] .fcbrrcCarControllerCircuitBreaker : java.lang.RuntimeException 2021-10-20 11:53:41.102 ERROR 34090 --- [ctor-http-nio-4] .fcbrrcCarControllerCircuitBreaker : io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'TEST' is OPEN and does not permit further calls

이 개방 상태 메시지는 사실이지만, 실패하지 않는 엔드포인트에 2개의 요청을 한 후에 발생했습니다. 이것이 상태가 반 개방이라고 하는 이유입니다.

2.2.2. Spring Boot와 AOP가 없는 Resilience4J(from-paris-to-berlin-resilience4j-spring-app)

이전 세그먼트에서 우리는 Spring 기술을 사용하지 않고 매우 프로그래밍적인 방식으로 구현하는 방법을 살펴보았습니다. 우리는 Spring을 사용했지만 WebFlux MVC 유형의 서비스를 제공하기 위해서만 사용했습니다. 또한 서비스 자체에 대해서는 아무것도 변경하지 않았습니다. 다음 애플리케이션에서 다음 라이브러리를 살펴보겠습니다.

 <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot2</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-all</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-reactor</artifactId> </dependency>


코드가 어떻게 작성되었는지 살펴보면 꽤 큰 차이를 알 수 있습니다.

 @RestController @RequestMapping("/cars") class CarController( private val carService: CarService, timeLimiterRegistry: TimeLimiterRegistry, circuitBreakerRegistry: CircuitBreakerRegistry, bulkheadRegistry: BulkheadRegistry ) { private var timeLimiter: TimeLimiter = timeLimiterRegistry.timeLimiter(CARS) private var circuitBreaker = circuitBreakerRegistry.circuitBreaker(CARS) private var bulkhead = bulkheadRegistry.bulkhead(CARS) @GetMapping("/{id}") private fun getCars(@PathVariable id: Int): Mono<Car> { return carService.getCar() .transform(TimeLimiterOperator.of(timeLimiter)) .transform(CircuitBreakerOperator.of(circuitBreaker)) .transform(BulkheadOperator.of(bulkhead)) .onErrorResume(TimeoutException::class.java, ::fallback) } @GetMapping("/test/{id}") private fun getCarsTest(@PathVariable id: Int): Mono<Car> { return carService.getCar() .transform(TimeLimiterOperator.of(timeLimiter)) .transform(CircuitBreakerOperator.of(circuitBreaker)) .transform(BulkheadOperator.of(bulkhead)) .onErrorResume(TimeoutException::class.java, ::fallback) } @GetMapping("/carros/{id}") private fun getCarros(@PathVariable id: Long): Mono<Car> { return Mono.just(Car("Laborghini")) } private fun fallback(ex: Throwable): Mono<Car> { return Mono.just(Car("Rolls Royce")) } }

이 예제와 다음 예제에서는 주로 타임아웃 속성을 살펴보겠습니다. CircuitBreaker 자체의 복잡성은 소개 문서이기 때문에 그다지 중요하지 않습니다. 여기서 깨달아야 할 중요한 점은 Resilience4J 에서 Spring에 제공하는 데코레이터를 사용하여 이를 얼마나 쉽게 구현할 수 있는가입니다. 여전히 programmatical 방식이지만 carService.getCar() 에서 얻은 초기 게시자를 원하는 short-circuit 유형으로 쉽게 장식할 수 있습니다.


이 예제에서 TimeLiter , BulkHead , CircuitBreaker 등록합니다. 마지막으로 TimeoutException 이 발생하면 트리거되는 폴백 함수를 정의합니다. 아직 확인해야 할 것은 이 모든 것이 어떻게 구성되어 있는가입니다. 다른 구성 가능한 모듈과 마찬가지로 Spring에서 Resilience4J를 구성합니다. application.yml 사용합니다.

 resilience4j.circuitbreaker: configs: default: registerHealthIndicator: true slidingWindowSize: 10 minimumNumberOfCalls: 5 permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 5s failureRateThreshold: 50 eventConsumerBufferSize: 10 recordExceptions: - org.springframework.web.client.HttpServerErrorException - java.util.concurrent.TimeoutException - java.io.IOException ignoreExceptions: # - io.github.robwin.exception.BusinessException shared: slidingWindowSize: 100 permittedNumberOfCallsInHalfOpenState: 30 waitDurationInOpenState: 1s failureRateThreshold: 50 eventConsumerBufferSize: 10 ignoreExceptions: # - io.github.robwin.exception.BusinessException instances: cars: baseConfig: default roads: registerHealthIndicator: true slidingWindowSize: 10 minimumNumberOfCalls: 10 permittedNumberOfCallsInHalfOpenState: 3 waitDurationInOpenState: 5s failureRateThreshold: 50 eventConsumerBufferSize: 10 # recordFailurePredicate: io.github.robwin.exception.RecordFailurePredicate resilience4j.retry: configs: default: maxAttempts: 3 waitDuration: 100 retryExceptions: - org.springframework.web.client.HttpServerErrorException - java.util.concurrent.TimeoutException - java.io.IOException ignoreExceptions: # - io.github.robwin.exception.BusinessException instances: cars: baseConfig: default roads: baseConfig: default resilience4j.bulkhead: configs: default: maxConcurrentCalls: 100 instances: cars: maxConcurrentCalls: 10 roads: maxWaitDuration: 10ms maxConcurrentCalls: 20 resilience4j.thread-pool-bulkhead: configs: default: maxThreadPoolSize: 4 coreThreadPoolSize: 2 queueCapacity: 2 instances: cars: baseConfig: default roads: maxThreadPoolSize: 1 coreThreadPoolSize: 1 queueCapacity: 1 resilience4j.ratelimiter: configs: default: registerHealthIndicator: false limitForPeriod: 10 limitRefreshPeriod: 1s timeoutDuration: 0 eventConsumerBufferSize: 100 instances: cars: baseConfig: default roads: limitForPeriod: 6 limitRefreshPeriod: 500ms timeoutDuration: 3s resilience4j.timelimiter: configs: default: cancelRunningFuture: false timeoutDuration: 2s instances: cars: baseConfig: default roads: baseConfig: default

이 파일은 그들의 repo에서 가져온 예시 파일이며, 제 예시에 맞게 수정되었습니다. 앞서 살펴본 것처럼, 다양한 유형의 limiters/short-circuit 인스턴스에는 이름이 있습니다. 여러 개의 다른 레지스트리와 다른 리미터가 있는 경우 이름은 매우 중요합니다. 우리의 예시에서는 앞서 언급한 것처럼 타임리미터에 관심이 있습니다. 2초로 제한되어 있음을 알 수 있습니다. 서비스를 구현한 방식을 살펴보면 타임아웃이 발생하도록 강제하고 있음을 알 수 있습니다.

 @Component open class CarService { open fun getCar(): Mono<Car> { return Mono.just(Car("Fiat")).delayElement(Duration.ofSeconds(10)); } }

애플리케이션을 시작하고 브라우저에서 http://localhost:8080/cars/test/2 로 이동합니다. Fiat 대신 Rolls Royce 받게 됩니다. 이렇게 해서 타임아웃을 정의했습니다. 같은 방식으로 CircuitBreaker 쉽게 만들 수 있습니다.

3. 사례

지금까지 우리는 CircuitBreakers 와 관련 리미터를 구현하는 세 가지 필수적인 방법을 살펴보았습니다. 나아가, 제가 만든 애플리케이션을 통해 제가 가장 좋아하는 Circuit Breaker 구현 방법을 살펴보겠습니다. 이 애플리케이션은 파리에서 베를린까지 가기 위해 사각형을 클릭하기만 하면 되는 매우 간단한 게임입니다. 이 게임은 구현 방법을 이해하도록 만들어졌습니다. 어디에 구현해야 하는지에 대해서는 많은 언급이 없습니다. 그저 제가 여러분에게 노하우를 공유하기 위해 디자인한 사례일 뿐입니다.


언제를 알아야 할지는 나중에 결정하도록 맡기겠습니다. 기본적으로 우리는 여러 대의 차를 만들고 베를린으로 가는 경로를 설정하고 싶습니다. 이 경로의 다른 위치에서 우리는 무작위로 문제를 일으킬 도시에 도착할 것입니다. 우리의 circuit breakers 우리가 계속 이동할 수 있기 전에 얼마나 기다려야 할지 결정할 것입니다. 다른 차들은 문제가 없고, 우리는 올바른 경로를 선택하기만 하면 됩니다.


우리는 특정 문제가 특정 분에 도시에서 발생할 때 등록된 시간표를 확인할 수 있습니다. 분 표시는 0 인덱스 위치에서 유효합니다. 즉, 2는 시계의 every 2, 12, 22, 32, 42, 52분 표시가 이 문제를 생성하는 데 유효함을 의미합니다. 문제는 ERRORTIMEOUT 의 두 가지 종류가 있습니다. 오류 실패는 20초의 지연을 제공합니다.


타임아웃은 50초의 지연을 제공합니다. 도시가 바뀔 때마다 모든 사람이 10초를 기다려야 합니다. 그러나 대기하기 전에, 폴백 메서드에서 이것이 수행될 때 자동차는 이미 다음 도시의 입구에 있습니다. 이 경우, 다음 도시는 무작위로 선택됩니다.

게임 페이지

4. 구현

우리는 이전에 application.yml 사용하여 resilience4j 레지스트리를 구성하는 방법을 보았습니다. 이 작업을 마쳤으니 함수를 데코레이트하는 방법에 대한 몇 가지 예를 살펴보겠습니다.

 @TimeLimiter(name = CarService.CARS, fallbackMethod = "reportTimeout") @CircuitBreaker(name = CarService.CARS, fallbackMethod = "reportError") @Bulkhead(name = CarService.CARS) open fun moveToCity(id: Long): Mono<RoadRace> { val myCar = roadRace.getMyCar() if (!myCar.isWaiting()) { val destination = myCar.location.forward.find { it.id == id } val blockage = destination?.blockageTimeTable?.find { it.minute.toString().last() == LocalDateTime.now().minute.toString().last() } blockage?.let { roadBlockTime -> when (roadBlockTime.blockageType) { BlockageType.TIMEOUT -> return Mono.just(roadRace).delayElement(Duration.ofSeconds(10)) BlockageType.ERROR -> return Mono.create { it.error(BlockageException()) } BlockageType.UNKNOWN -> return listOf(Mono.create { it.error(BlockageException()) }, Mono.just(roadRace).delayElement(Duration.ofSeconds(10))).random() else -> print("Nothing to do here!") } } destination?.let { myCar.delay(10) myCar.location = it myCar.formerLocations.add(myCar.location) } } return Mono.just(roadRace) } private fun reportError(exception: Exception): Mono<RoadRace> { logger.info("---- **** error reported") roadRace.getMyCar().delay(20L) roadRace.getMyCar().randomFw() roadRace.errorReports.add("Error reported! at ${LocalDateTime.now()}") return Mono.create { it.error(BlockageException()) } } private fun reportTimeout(exception: TimeoutException): Mono<RoadRace> { logger.info("---- **** timeout reported!") roadRace.getMyCar().delay(50L) roadRace.getMyCar().randomFw() roadRace.errorReports.add("Timeout reported! at ${LocalDateTime.now()}") return Mono.just(roadRace) }


보시다시피, 원래 서비스 호출은 주석을 사용하여 직접 장식됩니다! 이는 패키지에 AOP 모듈이 있기 때문에만 가능합니다.

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>

AOP 또는 Aspect Oriented Programming OOP 에 기반한 또 다른 프로그래밍 패러다임입니다. OOP 를 보완하는 것으로 간주되며 정확히 많은 주석이 작동하는 방식입니다. 이를 통해 정확한 컷포인트에서 원래 메서드 주변, 이전 또는 이후에 다른 함수를 트리거할 수 있습니다. 예에서 볼 수 있듯이 시간 초과 또는 오류를 생성합니다. BlockageException 도 폴백 메서드 내부에서 생성됩니다. 이는 문제를 나타내지 않습니다. 응답을 제외하고요.


그러나 애플리케이션은 WebSockets, 에서 실행 중이므로 이 오류는 애플리케이션에서 나타나지 않습니다.지금까지는 이것이 게임이었습니다.저는 복원력 있는 애플리케이션을 구현할 때 주석을 사용하면 어떻게 우리의 삶을 훨씬 더 쉽게 만들 수 있는지 보여주는 데 중점을 두고 이것을 구현했습니다.우리는 CircuitBreakers를 구현했을 뿐만 아니라 WebSockets , Spring WebFlux , Docker , NGINX , typescript 및 기타 여러 기술과 같은 다른 기술도 구현했습니다.이 모든 것은 CircuitBreakers가 애플리케이션에서 어떻게 작동하는지 보기 위해 만들어졌습니다.이 애플리케이션을 가지고 놀고 싶다면 프로젝트의 루트로 이동하여 다음을 실행하세요.

 make docker-clean-build-start


그런 다음 다음 명령을 실행합니다.

 curl -X POST http://localhost:8080/api/fptb/blockage -H "Content-Type: application/json" --data '{"id":1,"name":"Paris","forward":[{"id":2,"name":"Soissons","forward":[{"id":5,"name":"Aken","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":6,"name":"Heerlen","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":7,"name":"Düren","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":3,"name":"Compiègne","forward":[{"id":5,"name":"Aken","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":6,"name":"Heerlen","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":7,"name":"Düren","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":4,"name":"Reims","forward":[{"id":5,"name":"Aken","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":6,"name":"Heerlen","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]},{"id":7,"name":"Düren","forward":[{"id":8,"name":"Berlin","forward":[],"blockageTimeTable":[]}],"blockageTimeTable":[]}],"blockageTimeTable":[]}],"blockageTimeTable":[]}'


이 요청의 페이로드는 from-paris-to-berlin-city-generator 모듈을 사용하여 생성됩니다. 이 모듈을 살펴보면 이해하기 매우 간단하고 게임에 대한 자체 맵을 생성할 수 있다는 것을 알 수 있습니다! 마지막으로 http://localhost:9000 으로 이동하면 애플리케이션이 실행될 것입니다! 이제 게임을 하려면 올바른 사각형을 클릭하기만 하면 됩니다. 이기고 싶다면 빨간색 사각형은 클릭하지 마세요. 하지만 circuit-breaker가 작동하는 것을 보고 싶다면 애플리케이션 로그를 실행하세요.

 docker logs from_paris_to_berlin_web -f

그리고 실패를 일으키려면 빨간색 사각형을 명확하게 클릭하세요.

5. Kystrix와 Resilience4J의 차이점

Kystrix 애플리케이션이 작고 DSL 사용량을 정말 낮게 유지하려는 경우에 이상적입니다. 유일한 단점은 회로 차단기의 영향을 받는 메서드를 쉽게 장식할 수 있는 방법을 제공하지 않는다는 것입니다. Resilience4J 회로 차단기를 사용하는 엔터프라이즈 작업에 좋은 옵션인 듯합니다. 주석 기반 프로그래밍을 제공하고 AOP의 모든 이점을 활용하며 모듈이 분리되어 있습니다. 어떤 면에서는 애플리케이션의 중요한 지점에 전략적으로 사용할 수도 있습니다. 애플리케이션의 여러 측면을 포괄하는 완전한 프레임워크로 사용할 수도 있습니다.

6. 결론

어떤 브랜드를 선택하든 목표는 항상 회복성 있는 애플리케이션을 갖는 것입니다. 이 글에서 저는 개인적으로 회로 차단기를 조사하고 매우 높은 수준에서 발견한 내용에 대한 몇 가지 예를 보여주었습니다. 즉, 이 글은 회로 차단기가 무엇이고 리미터가 무엇을 할 수 있는지 알고 싶어 하는 사람들을 위해 쓰여졌다는 의미입니다.


circuit breakers 와 같은 회복성 메커니즘으로 애플리케이션을 개선하는 것에 대해 생각할 때 가능성은 솔직히 무한합니다. 이 패턴은 우리가 가진 사용 가능한 리소스를 더 잘 활용하기 위해 애플리케이션을 미세 조정할 수 있게 합니다. 대부분 클라우드에서 비용을 최적화하고 실제로 할당해야 하는 리소스의 양을 최적화하는 것이 여전히 매우 중요합니다.


CircuitBreakers 구성하는 것은 Limiters의 경우처럼 간단한 작업이 아니며, 최적의 성능과 회복성 수준에 도달하려면 모든 구성 가능성을 이해해야 합니다. 이것이 제가 이 소개 기사에서 circuit breakers에 대한 자세한 내용을 다루고 싶지 않은 이유입니다.


Circuit-breakers 다양한 유형의 애플리케이션에 적용할 수 있습니다. 대부분의 메시징 및 스트리밍 유형의 애플리케이션에 이것이 필요합니다. 고가용성이 필요한 대량의 데이터를 처리하는 애플리케이션의 경우, 우리는 어떤 형태의 회로 차단기를 구현할 수 있고 구현해야 합니다. 대형 온라인 리테일 매장은 매일 엄청난 양의 데이터를 처리해야 하며, 과거에는 Hystrix 널리 사용되었습니다. 현재는 이보다 훨씬 더 많은 것을 포함하는 Resilience4J 방향으로 이동하고 있는 것 같습니다.

6. 참고문헌

감사합니다!

이 글을 제가 만든 것만큼 즐기셨으면 좋겠습니다! 아래 링크의 소셜에 리뷰, 댓글 또는 피드백을 남겨주세요. 이 글을 더 좋게 만드는 데 도움을 주시면 정말 감사하겠습니다. 이 애플리케이션의 모든 소스 코드를 GitHub에 올렸습니다. 읽어주셔서 감사합니다!