kagamihogeの日記

kagamihogeの日記です。

Spring Cloud Circuit Breakerさわる

Spring Cloud Circuit Breaker https://spring.io/projects/spring-cloud-circuitbreaker#overviewチュートリアルレベルのことをやる。

Spring Cloud Circuit Breaker自身は抽象APIを提供するだけで、使う際には具体的な実装を選択する、というタイプ。以前のJSUG勉強会でResilience4Jが良いとかなんとか聞いた記憶があるので、今回はこれを使う。

とりあえず使ってみる

plugins {
  id 'org.springframework.boot' version '2.2.7.RELEASE'
  id 'io.spring.dependency-management' version '1.0.9.RELEASE'
  id 'java'
}


sourceCompatibility = '11'
configurations {
  developmentOnly
  runtimeClasspath {
    extendsFrom developmentOnly
  }
}
repositories {
  mavenCentral()
}
ext {
  set('springCloudVersion', "Hoxton.SR4")
}
dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
  developmentOnly 'org.springframework.boot:spring-boot-devtools'
  testImplementation('org.springframework.boot:spring-boot-starter-test') {
    exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
  }
}
dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  }
}
test {
  useJUnitPlatform()
}
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
public class Application {

  @Bean
  public RestTemplate restTemplate() {
    return new RestTemplate();
  }

  public static void main(String[] args) {
    new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
  }
}
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class DemoController {
  private RestTemplate rest;
  private CircuitBreakerFactory cbFactory;

  public DemoController(RestTemplate rest, CircuitBreakerFactory cbFactory) {
    this.rest = rest;
    this.cbFactory = cbFactory;
  }

  @GetMapping("/sample")
  public String sample() {
    return cbFactory.create("sample").run(
        () -> rest.getForObject("http://localhost:8080/hoge", String.class), throwable -> "fallback");
  }

  @GetMapping("/hoge")
  public String slow() {
    return "hoge";
  }
}

これでhttp://localhost:8080/sampleにアクセスするとhogeと返ってくる。

で、以下のように意図的に実行時例外を発生させてみると、fallbackと返ってくる。

    return cbFactory.create("slow").run(
        () -> rest.getForObject("invalid-url", String.class), throwable -> "fallback"); //不正なURL文字列

設定変更

デフォルト設定変更

特に何も設定しない状態だと1秒でタイムアウトする。ので、デフォルトのタイムアウト設定を変更してみる。以下はほぼチュートリアルからコピペしてきたもの。5秒タイムアウトにしてある。

  @Bean
  public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
      return factory -> {factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
              .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(5)).build())
              .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
              .build());
      };
  }

デモ用に6秒スリープするエンドポイントを用意し、そこへアクセスする。こうすると5秒でタイムアウト、fallbackしてfallbackが返ってくる。

  @GetMapping("/sample")
  public String sample() {
    return cbFactory.create("sample").run(() -> rest.getForObject("http://localhost:8080/slow", String.class),
        throwable -> "fallback");
  }

  @GetMapping("/slow")
  public String slow() throws InterruptedException {
    TimeUnit.SECONDS.sleep(6L);
    return "slow";
  }

指定

次に、それぞれのCircuitBreakerごとに異なる設定をしてみる。

以下のように、デフォルトは10秒、idがtimeout-3secは3秒でタイムアウトに設定する。

  @Bean
  public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
    return factory -> {
      factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
          .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(10)).build())
          .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()).build());

      factory.configure(
          c -> c.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(3)).build()).build(), "timeout-3sec");
    };
  }

動作確認用のcontrollerをつくる。/sample2のところはidをtimeout-3secにするのがポイント。

  @GetMapping("/sample")
  public String sample() {
    return cbFactory.create("sample").run(() -> rest.getForObject("http://localhost:8080/slow", String.class),
        throwable -> "fallback");
  }
  
  @GetMapping("/sample2")
  public String sample2() {
    return cbFactory.create("timeout-3sec").run(() -> rest.getForObject("http://localhost:8080/slow", String.class),
        throwable -> "fallback");
  }

  @GetMapping("/slow")
  public String slow() throws InterruptedException {
    TimeUnit.SECONDS.sleep(6L);
    return "slow";
  }

この状態で、

  • http://localhost:8080/sampleは、デフォルト10秒タイムアウトなので、6秒経過してslowが返る。
  • http://localhost:8080/sample2は、3秒タイムアウトなので、3秒経過するとfallbackが返る。