Spring BootでCircuit Breaker(Spring Cloud Netflix Hystrix)を試す

Spring BootでCircuit Breaker(Spring Cloud Netflix Hystrix)を試してみたメモです。


(2017/1/12 dependencyを修正)
---

Spring BootでCircuit Breakerを試してみました。

マイクロサービスでAPI通信しているときに、一部で通信エラーが発生した場合に
アクセスを遮断して切り離す必要があります。
その際に用いられるのがサーキットブレイカーです。

サーキットブレイカーパターンの詳しい説明はこちらにあります。
martinfowler.com


Netflix Hystrixはフォールトトレランス(障害が起きてもサービスを継続する)のためのライブラリで、
サーキットブレイカーの実装が含まれています。
github.com

Spring BootでSpring Cloud NetflixのHystrixを試してみました。
Spring Cloud Netflix

dependency

dependencyにはspring-cloud-starter-hystrixを追加しました。

dependencyManagementを設定し、release trainに現時点の安定版のCamden.SR3を指定しました。
これでspring cloudの依存関係は適切なバージョンで解決されます。

pom.xml

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>

確認用アプリケーション

自分のサービス(MyApp)から外部サービス(Supplier)にAPIで文字列を取得するサンプルコードを書きました。


自分のサービス(MyApp)

MyAppController.java

@RestController
public class MyAppController {
    @RequestMapping(value = "myapp", method = RequestMethod.GET)
    public String myApp() {
        RestTemplate restTemplate = new RestTemplate();
        URI uri = URI.create("http://localhost:8888/supplier/text");

        return restTemplate.getForObject(uri, String.class);
    }
}

port:8080で起動。

application.properties

server.port=8080


外部サービス(Supplier)

SupplierController.java

@RestController
public class SupplierController {
    @RequestMapping(value = "supplier/text", method = RequestMethod.GET)
    public String replyTest() {
        return "Supplier reply text";
    }
}

port:8888で起動。

application.properties

server.port=8888

両方起動して、curlでMyAppにアクセスしてみます。

$ curl http://localhost:8080/myapp
Reply supplier text

きちんとSupplierから文字列が返ってきてます。


Supplierの方を停止してみます。
この状態でcurlでMyAppにアクセスしてみます。

$ curl http://localhost:8080/myapp
{"timestamp":1483609186065,"status":500,"error":"Internal Server Error","exception":"org.springframework.web.client.ResourceAccessException","message":"I/O error on GET request for \"http://localhost:8888/supplier/text\": Connection refused: connect; nested exception is java.net.ConnectException: Connection refused: connect","path":"/myapp"}

500エラーが返って来ました。

Fallback

Hystrixではフォールバックメソッドを設定可能です。
メソッド呼び出しが失敗して閾値に達した場合、フォールバックメソッドを呼び出します。

フォールバックメソッドを設定するには@HystrixCommandを設定し、フォールバックするメソッドを文字列で指定します。

@HystrixCommand(fallbackMethod = "fallbackText")
public String getSupplierText() {
    URI uri = URI.create("http://localhost:8888/supplier/text");

    return restTemplate.getForObject(uri, String.class);
}

public String fallbackText() {
    return "Reply fallback text";
}

ただし、@HystrixCommandは@Componentまたは@Serviceが設定されたクラスでのみ動作するため、
下記のように、APIコールのメソッドをサービスクラスに切り出します。

MyAppService.java

@Service
public class MyAppService {
    private final RestTemplate restTemplate;

    public MyAppService(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    @HystrixCommand(fallbackMethod = "fallbackText")
    public String getSupplierText() {
        URI uri = URI.create("http://localhost:8888/supplier/text");

        return restTemplate.getForObject(uri, String.class);
    }

    public String fallbackText() {
        return "Reply fallback text";
    }
}

それにともなって、コントローラー側もサービスクラスを呼ぶように変更。
さらに@EnableCircuitBreakerを付与することで、サーキットブレイカーを有効にします。

MyAppController.java

@EnableCircuitBreaker
@RestController
public class MyAppController {
    private final MyAppService myAppService;

    public MyAppController(MyAppService myAppService) {
        this.myAppService = myAppService;
    }

    @RequestMapping(value = "myapp", method = RequestMethod.GET)
    public String myApp() {
        return myAppService.getSupplierText();
    }
}

先程と同じように両方起動して、curlでMyAppにアクセスしてみます。

$ curl http://localhost:8080/myapp
Reply supplier text

きちんとSupplierから文字列が返ってきてます。


Supplierの方を停止してみます。
この状態でcurlでMyAppにアクセスしてみます。

$ curl http://localhost:8080/myapp
Reply fallback text

そうすると設定したフォールバックメソッドが実行され、フォールバックメソッドで返している文字列が返って来ていることがわかります。

Circuit Breaker

フォールバックされることが確認できたので、
APIコールが遮断されること(サーキットがオープンになること)を確認してみます。

@HystrixCommandのcommandPropertiesで各設定が出来るので、設定値について確認してみます。

circuitBreaker.enabled

サーキットブレイカーを有効にするかどうかの設定。デフォルトは有効。

circuitBreaker.requestVolumeThreshold

サーキットがオープンになるエラー回数。デフォルトは20。
(Hystrixではローリングウィンドウで状態を管理しており、そのローリングウィンドウ内でのエラー回数)

@HystrixCommand(
        fallbackMethod = "fallbackText",
        commandProperties = {
                @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
                @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5")
        }
)
public String getSupplierTextWithThreshold() {
    URI uri = URI.create("http://localhost:8888/supplier/text");
    LocalDateTime now = LocalDateTime.now();
    System.out.println(now + ": api called");
    return restTemplate.getForObject(uri, String.class);
}

APIを6回コールすると、5回めまではsupplierのAPIをコールしてますが、6回目からはしていません。

2017-01-04T18:12:27.738: api called
2017-01-04T18:12:29.815: api called
2017-01-04T18:12:31.833: api called
2017-01-04T18:12:33.861: api called
2017-01-04T18:12:35.888: api called

テストコード

@Test
public void withThreshold() throws InterruptedException {
    restTemplate = new TestRestTemplate();
    URI uri = URI.create("http://localhost:8080/myapp_with_threshold");

    for(int i = 0; i < 6; i++) {
        Thread.sleep(1000);
        restTemplate.getForObject(uri, String.class);
    }
}
circuitBreaker.sleepWindowInMilliseconds

サーキットがオープンした後、sleepWindowInMilliseconds待って再度リクエストを受け付けます。デフォルトは5,000msec。
再度受け付けたリクエストが失敗した場合オープンの状態のままとなり、
リクエストが成功した場合はサーキットがクローズします。

5000msecを指定して確認してみます。

@HystrixCommand(
        fallbackMethod = "fallbackText",
        commandProperties = {
                @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
                @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"),
                @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000")
        }
)
public String getSupplierTextWithSleepWindow() {
    URI uri = URI.create("http://localhost:8888/supplier/text");
    LocalDateTime now = LocalDateTime.now();
    System.out.println(now + ": api called");
    return restTemplate.getForObject(uri, String.class);
}

連続でリクエストしてみます。
五回目のリクエスト失敗でサーキットがオープンになり、その後は5秒経過ごとに再度api callしています。
5秒ごとのapi callの結果が失敗の場合、サーキットはオープンのままです。
途中から(21:07:51から)はapi callが成功したためサーキットがクローズとなり、再び連続でリクエストを受け付けています。

2017-01-04T21:07:17.318: api called
2017-01-04T21:07:18.884: api called
2017-01-04T21:07:20.413: api called
2017-01-04T21:07:21.938: api called
2017-01-04T21:07:23.486: api called
2017-01-04T21:07:30.153: api called
2017-01-04T21:07:35.273: api called
2017-01-04T21:07:40.489: api called
2017-01-04T21:07:45.798: api called
2017-01-04T21:07:51.207: api called
2017-01-04T21:07:51.920: api called
2017-01-04T21:07:52.450: api called
2017-01-04T21:07:52.975: api called
2017-01-04T21:07:53.507: api called

テストコード

@Test
public void withSleepWindow() throws InterruptedException {
    restTemplate = new TestRestTemplate();
    URI uri = URI.create("http://localhost:8080/myapp_with_sleep_window");

    for(int i = 0; i < 300; i++) {
        Thread.sleep(500);
        restTemplate.getForObject(uri, String.class);
    }
}
circuitBreaker.errorThresholdPercentage

サーキットがオープンになるエラーパーセンテージ。デフォルトは50%。

Hystrix Dashboardで50%以上になった時にサーキットがオープンになるのを見てみます。
(Hystrix Dashboardを有効にする方法は記事下部を参照)

@HystrixCommand(
        fallbackMethod = "fallbackText",
        commandProperties = {
                @HystrixProperty(name = "execution.timeout.enabled", value = "true"),
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "50"),
                @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
                @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
        }
)
public String getSupplierTextWithPercentage() {
    URI uri = URI.create("http://localhost:8888/supplier/text");
    LocalDateTime now = LocalDateTime.now();
    System.out.println(now + ": api called");
    return restTemplate.getForObject(uri, String.class);
}

両方起動しておいて、途中でSupplierの方を停止してみます。
Hystrix Dashboardを見てみると、エラー率が50%になった時にサーキットがオープンになっているのがわかります。
f:id:pppurple:20170111212022g:plain

テストコード

@Test
public void withPercentage() throws InterruptedException {
    restTemplate = new TestRestTemplate();
    URI uri = URI.create("http://localhost:8080/myapp_with_percentage");

    for(int i = 0; i < 10000000; i++) {
        Thread.sleep(100);
        String text = restTemplate.getForObject(uri, String.class);
        System.out.println(i + " : " + text);
    }
}

Hystrix Dashboard

Hystrix DashboardはリアルタイムでHystrixメトリックを監視できるダッシュボードです。

Hystrix Dashboardを利用するには、spring-cloud-starter-hystrix-dashboarddependencyに追加して、
spring-boot-starter-actuatorを有効にします。

pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

SpringBootのmainクラスに@EnableHystrixDashboardをつけます。

CircuitBreakerExampleApplication.java

@EnableHystrixDashboard
@SpringBootApplication
public class CircuitBreakerExampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(CircuitBreakerExampleApplication.class, args);
	}
}

あとは、アプリケーションを起動して、http://localhost:8080/hystrix にアクセスします。
すると、下記の様な画面が出てきます。

f:id:pppurple:20170111214134p:plain

hystrix clientのアプリケーションの場合、http://hystrix-app:port/hystrix.stream がエンドポイントになっているので、
http://localhost:8080/hystrix.stream を指定して「Monitor Stream」をクリックします。

すると下記のようなダッシュボードが表示されます。

f:id:pppurple:20170111214147p:plain

各表示項目の説明は下記に載っています。
https://github.com/Netflix/Hystrix/wiki/Dashboard

f:id:pppurple:20170111214155p:plain


ソースは下記にあげときました。
spring_examples/circuit-breaker-example at master · pppurple/spring_examples · GitHub
spring_examples/circuit-breaker-supplier-example at master · pppurple/spring_examples · GitHub

終わり。

----
追記
@makingさんに指摘をもらったのでdependency修正しました。