LoginSignup
0
0

Spring Boot x Resilience4j CircuitBreaker、Retryを試してみる

Last updated at Posted at 2023-12-16

実現したい事

  • Spring Boot、Resilience4j、RestTemplate を使用する
  • 連携先のサービスから通信エラー、500エラーが返ってきた場合、リトライする
  • リトライも失敗した場合、代わりの処理を実行する
  • リクエストの失敗率が閾値を超えた場合、連携先のサービスへはリクエストを投げずに代わりの処理を実行する
  • 連携先のサービスが復旧した場合、連携先のサービスへリクエストを投げるように自動で復旧する

参考にした記事

構成

distributed-tracing.drawio.png

Spring Boot アプリケーションの作成

api-service の実装

構成図の api-service に該当するアプリケーションを作成します。

build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
	id("org.springframework.boot") version "3.2.0"
	id("io.spring.dependency-management") version "1.1.4"
	kotlin("jvm") version "1.9.20"
	kotlin("plugin.spring") version "1.9.20"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"

java {
	sourceCompatibility = JavaVersion.VERSION_17
}

repositories {
	mavenCentral()
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter")
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("org.springframework.boot:spring-boot-starter-aop")
	implementation("org.springframework.boot:spring-boot-starter-actuator")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	//	Micrometer Observation APIをOpenTelemetryにブリッジするライブラリ
	implementation("io.micrometer:micrometer-tracing-bridge-otel:1.2.0")
	// トレース情報を Zipkin に送信するライブラリ
	implementation("io.opentelemetry:opentelemetry-exporter-zipkin:1.32.0")
	implementation("io.github.resilience4j:resilience4j-spring-boot3:2.1.0")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
	kotlinOptions {
		freeCompilerArgs += "-Xjsr305=strict"
		jvmTarget = "17"
	}
}

tasks.withType<Test> {
	useJUnitPlatform()
}
application.yml
spring:
  application:
    name: api-service customer-service # Zipkin に送信されるサービス名。同一のサービスと認識されてしまう為、一意である必要がある

management:
  tracing:
    sampling:
      probability: 1.0 # トレース情報の送信割合。1.0 で全てのトレース情報が送信される
  health:
    circuitbreakers.enabled: true # CircuitBreakerのヘルスインジケーターを有効にする
  endpoints:
    web:
      exposure:
        include: "health,circuitbreakers,metrics"

resilience4j.circuitbreaker:
  circuitBreakerAspectOrder: 1 # 優先順位の変更。retry より後に実行されるようにする
  instances:
    customer-service: # 設定したい接続先システムを指定
      registerHealthIndicator: true # ヘルスチェックエンドでサーキットブレーカーのステータスが確認可能になる
      slidingWindowType: COUNT_BASED # slidingWindowTypeをカウントベースにする
      slidingWindowSize: 3 # ここで指定した数のコール数を保持してエラーレートの計算に利用する
      minimumNumberOfCalls: 3 # エラーレートを計算するのに必要な最小のコール数
      permittedNumberOfCallsInHalfOpenState: 2 # HALF_OPENの時に許可される呼び出しの数。こちらに指定した回数を実行して、failureRateThreshold を満たさない場合は、OPEN に移行する
      automaticTransitionFromOpenToHalfOpenEnabled: true # trueだと自動でHALF_OPENに移行する
      waitDurationInOpenState: 10s # OPENからHALF_OPENに移行する前に待機する時間
      failureRateThreshold: 100 # 失敗率の閾値。この数値を超えて失敗しているとOPENに移行する
      # ここで指定したExceptionが発生すると失敗としてカウントする
      recordExceptions:
        - org.springframework.web.client.HttpServerErrorException
        - org.springframework.web.client.RestClientException

resilience4j.retry:
  retryAspectOrder: 2 # 優先順位の変更。circuitbreaker より先に実行されるようにする
  instances:
    customer-service:
      maxAttempts: 3 # 最大試行回数
      waitDuration: 1s
      retryExceptions:
        - org.springframework.web.client.HttpServerErrorException
        - org.springframework.web.client.RestClientException

logging:
  level:
    root: INFO
CustomerController.kt
import com.example.sleuthdemoclient.service.CustomerService
import org.apache.commons.logging.LogFactory
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class CustomerController(
    private val customerService: CustomerService
) {
    private val logger = LogFactory.getLog(CustomerController::class.java)

    @RequestMapping("/customer")
    fun get(): HelloRes {
        logger.info("get() has been called")
        val result = customerService.get()
        return HelloRes(
            result.value
        )
    }

    class HelloRes(
        val value: String
    )
}
CustomerService.kt
import io.github.resilience4j.circuitbreaker.CallNotPermittedException
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
import io.github.resilience4j.retry.annotation.Retry
import org.apache.commons.logging.LogFactory
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.client.HttpServerErrorException
import org.springframework.web.client.RestClientException
import org.springframework.web.client.RestTemplate
import org.springframework.web.client.getForEntity
import org.springframework.web.util.UriComponentsBuilder

@Service
class CustomerService(
    private val restTemplate: RestTemplate
) {
    private val logger = LogFactory.getLog(CustomerService::class.java)

    @Retry(name = "customer-service")
    @CircuitBreaker(name = "customer-service", fallbackMethod = "fallback")
    fun get(): CustomerRes {
        logger.info("get()")
        val uri = UriComponentsBuilder.fromUriString("http://localhost:8081")
            .path("customer")
            .toUriString()

        val response: ResponseEntity<CustomerRes> = restTemplate.getForEntity(
            uri,
            CustomerRes::class
        )

        return response.body!!
    }

    private fun fallback(exception: HttpServerErrorException): CustomerRes {
        return CustomerRes("state が CLOSE または HALF_OPEN の状態で失敗")
    }

    private fun fallback(exception: RestClientException): CustomerRes {
        return CustomerRes("state が CLOSE または HALF_OPEN の状態で通信エラー")
    }

    private fun fallback(exception: CallNotPermittedException): CustomerRes {
        return CustomerRes("state が OPEN")
    }
}

data class CustomerRes(
    val value: String
)

動作確認

Retry の実行が CircuitBreaker の failure rate に反映されるか確認、fallback が動作することの確認

結果としては最後のリクエストのみが反映されるようです。

{
	"name": "resilience4j.circuitbreaker.failure.rate",
	"description": "The failure rate of the circuit breaker",
	"measurements": [
		{
			"statistic": "VALUE",
			"value": -1
		}
	],
	"availableTags": [
		{
			"tag": "name",
			"values": [
				"customer-service"
			]
		}
	]
}
  • customer-service を停止して、必ずリトライされるようにする
  • http://localhost:8080/customer にアクセス
  • 2回リトライが発生し、合計3回リクエストを投げましたが、failure rate は -1のままでした。minimumNumberOfCallsを3で設定しているので、Retry が failure rate に反映されている場合は-1以外になります。
  • api-service からのレスポンスは以下の結果が返ってきた為、fallback も期待通りに動いているようです。
{"value":"state が CLOSE または HALF_OPEN の状態で通信エラー"}

failureRateThreshold に指定した失敗率に達した場合に、OPEN になることの確認

{
	"name": "resilience4j.circuitbreaker.failure.rate",
	"description": "The failure rate of the circuit breaker",
	"measurements": [
		{
			"statistic": "VALUE",
			"value": 100
		}
	],
	"availableTags": [
		{
			"tag": "name",
			"values": [
				"customer-service"
			]
		}
	]
}
{
	"circuitBreakers": {
		"customer-service": {
			"failureRate": "100.0%",
			"slowCallRate": "0.0%",
			"failureRateThreshold": "100.0%",
			"slowCallRateThreshold": "100.0%",
			"bufferedCalls": 3,
			"failedCalls": 3,
			"slowCalls": 0,
			"slowFailedCalls": 0,
			"notPermittedCalls": 0,
			"state": "OPEN"
		}
	}
}

CircuitBreaker の state が OPEN の時に、Retry が実行されるか確認。fallback が動作することの確認

結果としては state が OPEN の場合は、Retry は実行されないようです。
state が CLOSE の場合のlog
Zipkin_close.png

state が OPEN の場合のlog
customer-service へはリクエストは投げずに即座にレスポンスを返しています。
Zipkin_open.png

api-service からのレスポンスは以下の結果が返ってきた為、fallback も期待通りに動いているようです。

{"value":"state が OPEN"}

OPEN から HALF_OPEN への移行することの確認

  • OPEN の状態から、10秒経過すると、HALF_OPENへ移行しました。
  • HALF_OPEN の状態で、permittedNumberOfCallsInHalfOpenStateに指定されている回数アクセスした場合に、failureRateThresholdを満たすと CLOSE へ移行し、満たさない場合は OPEN へ移行するようです。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0