Circuit-breakers
応答しないサービスに過剰なリクエストが送信されるのを避けるために、現在使用されています。たとえば、何らかの理由でサービスが停止した場合、サーキット ブレーカーはその名のとおり、サーキットを遮断します。つまり、競馬の結果を取得するために 100 万件のリクエストが同時に送信されている場合、これらのリクエストを処理できる別のサービスにリダイレクトする必要があります。
この他のサービスは、元のサービスのレプリカにすることも、元のサービスの障害に関する他の操作を実行するためにのみ使用することもできます。最終目標は常に、不要な呼び出しを中断し、フローを別の場所で実行することです。2017 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
とも呼ばれ、主に一時的な動きです。
サーキット ブレーカーが実際どのようなものであるかについての知識をさらに深めるには、サーキット ブレークがアプリケーション内のエンティティとして機能することを理解する必要があります。サーキット ブレーカーには、主に 3 つのステータスがあります。クローズ、オープン、ハーフ オープンです。クローズ ステータスは、アプリケーション フローが正常に実行されていることを意味します。すべてのリクエストがサービス B に送信されることがわかっているので、サービス A にリクエストを安全に送信できます。オープン状態は、サービス B へのすべてのリクエストが失敗することを意味します。失敗を表すために定義したルールが発生し、サービス B に到達しなくなりました。この場合、常に例外が返されます。 half-open
状態は、サーキット ブレーカーがサービス B でテストを実行し、サービス B が再び動作するかどうかを確認するように指示されている状態です。
成功したリクエストはすべて通常どおり処理されますが、C へのリクエストは継続されます。設定した検証ルールに従って B が期待どおりに動作した場合、サーキット ブレーカーはクローズ状態に戻り、サービス A はサービス B のみにリクエストを送信し始めます。ほとんどのアプリケーションでは、サーキット ブレーカーはデコレータ デザイン パターンに従います。ただし、手動で実装することもできます。ここでは、サーキット ブレーカーを実装する 3 つのプログラム的な方法と、最後に AOP ベースの実装について説明します。コードはGitHubで入手できます。
この記事の最後のポイントでは、カーレース ゲームを見ていきます。ただし、そこに行く前に、 circuit breaker
で実行されるアプリケーションを構築するいくつかの側面について説明したいと思います。
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>
私は例を作成しました。これはGitHubのモジュールfrom-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
を使用して呼び出しを行うことができます。これは単なるテストなので、 Id=1
、 circuit-breaker
を必要としない Jaguar という車のId
であると想定します。これは、フォールバック メソッドで定義されている Tank1 を取得することもないことも意味します。まだ気づいていない場合は、フォールバック メソッドをよく見てください。このメソッドはObservable
を使用します。WebFlux WebFlux
Observable
デザイン パターンに従って実装されていますが、 Flux
は厳密には Observable ではありません。
ただし、hystrix は両方をサポートしています。アプリケーションを実行し、ブラウザでhttp://localhost:8080/cars/2を開いて、これを確認してください。Spring Boot の起動の非常に早い段階で呼び出しを開始すると、最終的に Tank1 メッセージが表示される可能性があることを理解することが重要です。これは、このプロセスの実行方法に応じて、起動の遅延が 5 秒を簡単に超える可能性があるためです。2 番目の例では、例を 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 秒の遅延が発生しているためですが、回路ブレーク条件は 500 ミリ秒後にトリガーするように指定しています。hystrix の動作を知っていれば、 kystrix
今後何も変わらないことがわかります。この時点でhystrix
が提供してくれなかったのは、ゲームを作成するために必要なものをシームレスかつ簡単に提供してくれる方法でした。Kystrix Kystrix
クライアント ベースで動作するようです。つまり、メイン サービスの背後にあるサービスにリクエストする前に、コードを宣言する必要があります。
Resilience4J
、サーキット ブレーカーの非常に完全な実装として多くの人に参照されているようです。私の最初の試みは、サーキット ブレーカーのいくつかの重要な側面を調査することでした。つまり、タイムアウトと成功したリクエストの頻度に基づいて機能するサーキット ブレーカーを見てみたかったのです。Resilience4J では、さまざまな種類のshort-circuiting
モジュールを構成できます。これらはCircuitBreaker
Resilience4J
Bulkhead
、 Ratelimiter
、 Retry
、およびTimelimiter
の6
の異なるカテゴリに分かれています。これらはすべて、設計パターンの名前でもあります。CircuitBreaker モジュールはCircuitBreaker
このパターンの完全な実装を提供します。
設定できるパラメータは多数ありますが、基本的にCircuitBreaker
モジュールでは、何を失敗と認識するか、半開状態で許可するリクエストの数、および時間またはカウントで設定できるスライディング ウィンドウを設定でき、閉じた状態で発生するリクエストの数を保持します。これは、エラー頻度を計算するために重要です。基本的に、このCircuitBreaker
モジュールはリクエストのレートに役立つと言えますが、必ずしもそうとは限りません。
どのように解釈するかによって異なります。単に障害に対処する方法として考える方がよいようです。タイムアウトまたは例外のどちらから発生したかに関係なく、ここで障害が処理され、リクエストをシームレスに別の場所にリダイレクトできます。 Bulkhead
モジュールは同時リクエストを処理するように設計されています。レート リミッターではありません。
代わりに、 Bulkhead
設計パターンを実装します。これは、1 つのエンドポイントで過度の処理が行われないようにするために使用されます。この場合、 Bulkhead
使用すると、リクエストをすべての利用可能なエンドポイントに分散して処理できます。 Bulkhead
という名前は、事故が発生した場合に沈没を回避するために大型船に通常備わっているさまざまな密閉された区画に由来しています。船の場合と同様に、スレッド プールで使用できるスレッドの数とリース時間を定義する必要があります。
RateLimiter
モジュールは、リクエストのレートを処理するように設計されています。このモジュールとBulkhead
モジュールの重要な違いは、一定のレートまで許容したいという点です。つまり、そのために障害を起こす必要はありません。設計では、一定の値を超えるレートは許容しないと決めるだけです。さらに、リクエストをリダイレクトするか、リクエストを実行する許可が与えられるまでRetry
にしておくことができます。Retry モジュールは、他のモジュールとあまり共通点がないため、おそらく最も理解しやすいでしょう。
基本的に、定義したしきい値に達するまで、特定のエンドポイントへの再試行回数を明示的に宣言します。Timelimiter モジュールは、両方ともCircuitBreaker
Timelimiter
の簡略化と見なすことができます。ただし、 Timelimiter
スライディング ウィンドウなどの他のパラメーターに依存せず、組み込みの障害しきい値計算もありません。
したがって、特定のサービスを呼び出すときにタイムアウトを処理することにのみ興味があり、他の可能性のある障害を考慮しない場合は、 Timelimiter
を使用する方がよいでしょう。
このモジュールでは、 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 のリポジトリで入手できます。まずは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
を使用して、 function
getPrivateCar
TimeLimiter
機能で装飾しています。これにより、タイムアウトが発生し、呼び出した関数に時間がかかりすぎると、Lancya ではなく Opel Corsa が表示されます。これを試すには、アプリケーションを実行してhttp://localhost:8080/cars/timelimiter/normal/1を開きます。
実装を調べてみると、 Lancya
取得できないことがわかります。これは、わざと10s
待ってから返しているからです。TimeLimiter のTimeLimiter
はこれよりずっと短いため、これは機能しません。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 件の半開状態のリクエストを許可することも言います。エラー計算では、回路ブレーカーはどのマークでそれを実行するかを知る必要があります。これは、回路をいつ閉じるかを決定するために重要です。minimumNumberOfCalls プロパティを使用して、この計算には 2 件のリクエストがminimumNumberOfCalls
必要であると言います。半開とは、リクエストが安全な失敗しきい値に達した場合に回路を閉じるように試行し続けることを覚えておいてください。
この構成では、エラー頻度を計算し、クローズ状態に戻るかどうかを判断するために、スライディング ウィンドウ内で少なくとも2
つのリクエストを行う必要があります。これは、構成したすべての変数の正確な読み取りです。一般的に、これは、アプリケーションが代替サービスに対して複数の呼び出しを行う可能性があり、代替サービスが存在する場合、半オープン状態中に 80% の成功率が必要であり、オープン状態のタイムアウトが発生している必要があるため、オープン状態からクローズ状態に簡単に切り替わらないことを意味します。
このようなタイムアウトを指定する方法は多数あります。この例では、 maxDurationInHalfOpenState
は 1 秒としています。つまり、チェックがクローズ状態条件を満たさない場合、またはこのタイムアウトがまだ発生していない場合にのみ、 CircuitBreaker
ステータスをオープンのままにします。このCircuitBreaker
で定義された動作は、特定のダウンタイム、レート、およびリクエストのその他の機能を正確に再現することが不可能なため、追跡および予測が困難な場合がありますが、このエンドポイントに対して複数のリクエストを実行すると、上記の動作が経験と一致することがわかります。
それでは、エンドポイントhttp://localhost:8080/cars/circuit/1とhttp://localhost:8080/cars/circuit/2へのリクエストをいくつか実行してみましょう。 1 で終わるのは車の取得に成功したエンドポイントで、 2 で終わるのは指定された車の取得に失敗したエンドポイントです。コードを見ると、 2 以外の値はすべてLancya
応答として取得することを意味します。 2
は、すぐにランタイム例外をスローすることを意味し、応答としてOpel Corsa
取得することになります。
エンドポイント1
にリクエストを送信すると、応答としてLancya
返され続けます。システムがダウンし始めた場合、つまりエンドポイント 2 にリクエストを送信した場合、しばらくするとLancya
への復帰が一定ではなくなることがわかります。 System
、オープン状態にあり、これ以上のリクエストは許可されないことを通知します。
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
リクエストが成功すると、サーキット ブレーカーは半開きの状態になります。つまり、正常化する前に、 Lancya
に戻るリクエストを数回実行する必要があります。Lancya からOpel Corsa
に数回切り替えてから、再びLancya
に戻ります。この数値は 2 と定義しました。これは、エラー計算の最小値です。1 つの失敗のみを引き起こし、失敗していないエンドポイントを呼び出し続けると、何が起こっているかをより明確に把握できます。
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 つのリクエストを送信した後に発生しました。これが、状態が半オープンであると言われる理由です。
前のセクションでは、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
このファイルは、リポジトリから取得したサンプル ファイルで、私の例に合わせて変更されています。前に説明したように、さまざまなタイプのlimiters/short-circuit
のインスタンスには名前があります。さまざまなレジストリとさまざまなリミッターがある場合、名前は非常に重要です。この例では、前に説明したように、timelimiter に注目します。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
Fiat
されます。これがタイムアウトを定義した方法です。同様に、 CircuitBreaker
簡単に作成できます。
これまで、 CircuitBreakers
と関連するリミッターを実装する 3 つの基本的な方法を見てきました。さらに、私が作成したアプリケーションを使用して、サーキット ブレーカーを実装する私のお気に入りの方法を見ていきます。このアプリケーションは、パリからベルリンに移動するために四角形をクリックするだけの非常にシンプルなゲームです。このゲームは、実装方法を理解できるように作られています。これをどこに実装するかについてはあまり説明されていません。これは、ノウハウを共有するために私が設計したケースにすぎません。
いつ知るかは、後で決めてください。基本的には、いくつかの車を作成し、ベルリンへのルートを確立します。このルートのさまざまな場所で、ランダムに問題が発生する都市に到着します。 circuit breakers
によって、移動が許可されるまでにどれくらいの時間を待つ必要があるかが決まります。他の車には問題はありません。正しいルートを選択するだけです。
ある都市で特定の分に特定の問題が発生すると登録されている時刻表を確認できます。分マークは、インデックス位置 0 で有効です。つまり、2 は、時計の 2、12、22、32、42、52 分マークevery
この問題が発生することを意味します。問題には、 ERROR
とTIMEOUT
の 2 種類があります。エラーが発生すると、20 秒の遅延が発生します。
タイムアウトにより 50 秒の遅延が発生します。都市が変わるたびに、全員が 10 秒待つ必要があります。ただし、フォールバック方式でこれを行うと、待つ前に車はすでに次の都市の入り口にいます。この場合、次の都市はランダムに選択されます。
以前、 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,
上で実行されているため、このエラーはアプリケーションでは表示されません。ここまでがゲームです。私は、復元力のあるアプリケーションを実装する際にアノテーションを使用すると、作業がはるかに簡単になることを示すことに重点を置いてこれを実装しました。CircuitBreaker だけでなく、 WebSockets
、 Spring WebFlux
、 Docker
、 NGINX
、 typescript
などの他のテクノロジも実装しました。これはすべて、CircuitBreaker がアプリケーションでどのように機能するかを確認するために作成されました。このアプリケーションで遊んでみたい場合は、プロジェクトのルートに移動して、以下を実行してください。
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に移動すると、アプリケーションが実行されます。これで、正しい四角形をクリックしてゲームをプレイできます。ただし、勝ちたい場合は赤い四角形をクリックしないでください。ただし、サーキットブレーカーの動作を確認したい場合は、アプリケーションログを実行してください。
docker logs from_paris_to_berlin_web -f
失敗を引き起こすには、赤い四角を明示的にクリックします。
Kystrix
アプリケーションが小さく、DSL の使用を極力抑えたい場合に最適です。唯一の欠点は、サーキット ブレーカーの影響を受けるメソッドを簡単に装飾できないことです。Resilience4J Resilience4J
サーキット ブレーカーを使用するエンタープライズ作業に最適なオプションのようです。アノテーション ベースのプログラミングを提供し、AOP のすべての利点を活用し、モジュールが分離されています。ある意味では、アプリケーションの重要なポイントに戦略的に使用することもできます。また、アプリケーションの多くの側面をカバーする完全なフレームワークとして使用することもできます。
選択するブランドに関係なく、目標は常に回復力のあるアプリケーションを持つことです。この記事では、サーキット ブレーカーの調査を個人的に体験した例と、非常に高いレベルでの調査結果をいくつか示しました。つまり、この記事はサーキット ブレーカーとは何か、リミッターで何ができるのかを知りたい人向けに書かれています。
circuit breakers
などの回復力メカニズムを使用してアプリケーションを改善することを考えた場合、可能性は率直に言って無限です。このパターンでは、利用可能なリソースをより有効に活用するためにアプリケーションを微調整できます。主にクラウドでは、コストと実際に割り当てる必要があるリソースの数を最適化することが依然として非常に重要です。
CircuitBreakers
の構成は、Limiters の場合のように簡単な作業ではありません。パフォーマンスと回復力を最適なレベルにするには、すべての構成の可能性を理解する必要があります。これが、この CircuitBreaker の紹介記事で詳細に説明しなかった理由です。
Circuit-breakers
さまざまなタイプのアプリケーションに適用できます。ほとんどのメッセージングおよびストリーミング タイプのアプリケーションでは、これが必要になります。大量のデータを処理し、高い可用性も必要とするアプリケーションでは、何らかの形のサーキット ブレーカーを実装できますし、実装する必要があります。大規模なオンライン リテール ストアでは、毎日大量のデータを処理する必要があり、過去にはHystrix
広く使用されていました。現在、私たちはこれよりも多くの機能を備えたResilience4J
の方向に進んでいるようです。
この記事を私が作成するのと同じくらい楽しんでいただければ幸いです。以下のリンクのいずれかのソーシャル メディアに、レビュー、コメント、フィードバックなどを残してください。この記事をより良いものにするためにご協力いただけると大変ありがたく思います。このアプリケーションのソース コードはすべてGitHub に置きました。お読みいただきありがとうございました。