1. 概要

一般的な分散システムは、連携する多くのサービスで構成されています。

これらのサービスは、失敗したり、応答が遅れたりする傾向があります。 サービスに障害が発生すると、他のサービスに影響を与えてパフォーマンスに影響を与え、アプリケーションの他の部分にアクセスできなくなるか、最悪の場合、アプリケーション全体がダウンする可能性があります。

もちろん、アプリケーションの復元力とフォールトトレラント性を高めるのに役立つソリューションもあります。そのようなフレームワークの1つがHystrixです。

Hystrixフレームワークライブラリは、フォールトトレランスとレイテンシトレランスを提供することにより、サービス間の相互作用を制御するのに役立ちます。 障害のあるサービスを分離し、障害のカスケード効果を停止することにより、システムの全体的な復元力を向上させます。

この一連の投稿では、サービスまたはシステムに障害が発生したときにHystrixがどのように救済されるか、およびこれらの状況でHystrixが何を達成できるかを確認することから始めます。

2. 簡単な例

Hystrixが障害と遅延の許容範囲を提供する方法は、リモートサービスへの呼び出しを分離してラップすることです。

この簡単な例では、 HystrixCommand: run()メソッドで呼び出しをラップします。

class CommandHelloWorld extends HystrixCommand<String> {

    private String name;

    CommandHelloWorld(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    @Override
    protected String run() {
        return "Hello " + name + "!";
    }
}

そして、次のように呼び出しを実行します。

@Test
public void givenInputBobAndDefaultSettings_whenCommandExecuted_thenReturnHelloBob(){
    assertThat(new CommandHelloWorld("Bob").execute(), equalTo("Hello Bob!"));
}

3. Mavenのセットアップ

MavenプロジェクトでHystrixを使用するには、プロジェクトpom.xmlのNetflixからhystrix-coreおよびrxjava-coreの依存関係が必要です。

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-core</artifactId>
    <version>1.5.4</version>
</dependency>

最新バージョンはいつでもここで見つけることができます。

<dependency>
    <groupId>com.netflix.rxjava</groupId>
    <artifactId>rxjava-core</artifactId>
    <version>0.20.7</version>
</dependency>

このライブラリの最新バージョンは、常にここにあります。

4. リモートサービスの設定

実世界の例をシミュレートすることから始めましょう。

以下の例では、クラスRemoteServiceTestSimulatorはリモートサーバー上のサービスを表します。 一定時間後にメッセージで応答する方式があります。 この待機は、リモートシステムでの時間のかかるプロセスのシミュレーションであり、呼び出し元のサービスへの応答が遅延することを想像できます。

class RemoteServiceTestSimulator {

    private long wait;

    RemoteServiceTestSimulator(long wait) throws InterruptedException {
        this.wait = wait;
    }

    String execute() throws InterruptedException {
        Thread.sleep(wait);
        return "Success";
    }
}

これが、RemoteServiceTestSimulatorを呼び出すサンプルクライアントです。

サービスへの呼び出しは分離され、HystrixCommandのrun()メソッドでラップされます。このラッピングは、上記で触れた復元力を提供します。

class RemoteServiceTestCommand extends HystrixCommand<String> {

    private RemoteServiceTestSimulator remoteService;

    RemoteServiceTestCommand(Setter config, RemoteServiceTestSimulator remoteService) {
        super(config);
        this.remoteService = remoteService;
    }

    @Override
    protected String run() throws Exception {
        return remoteService.execute();
    }
}

呼び出しは、 RemoteServiceTestCommandオブジェクトのインスタンスでexecute()メソッドを呼び出すことによって実行されます。

次のテストは、これがどのように行われるかを示しています。

@Test
public void givenSvcTimeoutOf100AndDefaultSettings_whenRemoteSvcExecuted_thenReturnSuccess()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroup2"));
    
    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(100)).execute(),
      equalTo("Success"));
}

これまで、HystrixCommandオブジェクトでリモートサービス呼び出しをラップする方法を見てきました。 以下のセクションでは、リモートサービスが劣化し始めた場合の対処方法を見ていきましょう。

5. リモートサービスと防御プログラミングの操作

5.1. タイムアウトのある防御プログラミング

リモートサービスへの呼び出しのタイムアウトを設定することは、一般的なプログラミング手法です。

HystrixCommand でタイムアウトを設定する方法と、それが短絡によってどのように役立つかを見てみましょう。

@Test
public void givenSvcTimeoutOf5000AndExecTimeoutOf10000_whenRemoteSvcExecuted_thenReturnSuccess()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest4"));

    HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
    commandProperties.withExecutionTimeoutInMilliseconds(10_000);
    config.andCommandPropertiesDefaults(commandProperties);

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));
}

上記のテストでは、タイムアウトを500ミリ秒に設定して、サービスの応答を遅らせています。 また、 HystrixCommand の実行タイムアウトを10,000ミリ秒に設定しているため、リモートサービスが応答するのに十分な時間があります。

次に、実行タイムアウトがサービスタイムアウト呼び出しよりも短い場合に何が起こるかを見てみましょう。

@Test(expected = HystrixRuntimeException.class)
public void givenSvcTimeoutOf15000AndExecTimeoutOf5000_whenRemoteSvcExecuted_thenExpectHre()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest5"));

    HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
    commandProperties.withExecutionTimeoutInMilliseconds(5_000);
    config.andCommandPropertiesDefaults(commandProperties);

    new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(15_000)).execute();
}

バーを下げて、実行タイムアウトを5,000ミリ秒に設定したことに注目してください。

サービスは5,000ミリ秒以内に応答することを期待していますが、サービスは15,000ミリ秒後に応答するように設定しています。 テストの実行時に気付いた場合、テストは15,000ミリ秒待つのではなく、5,000ミリ秒後に終了し、HystrixRuntimeExceptionをスローします。

これは、Hystrixが設定された応答のタイムアウトより長く待機しないことを示しています。 これにより、Hystrixによって保護されているシステムの応答性が向上します。

以下のセクションでは、スレッドが使い果たされるのを防ぐスレッドプールサイズの設定について検討し、その利点について説明します。

5.2. 制限されたスレッドプールを使用した防御プログラミング

サービス呼び出しのタイムアウトを設定しても、リモートサービスに関連するすべての問題が解決されるわけではありません。

リモートサービスの応答が遅くなると、通常のアプリケーションはそのリモートサービスを呼び出し続けます。

アプリケーションはリモートサービスが正常であるかどうかを認識せず、リクエストが着信するたびに新しいスレッドが生成されます。 これにより、すでに苦労しているサーバー上のスレッドが使用されます。

サーバーで実行されている他のリモート呼び出しまたはプロセスにこれらのスレッドが必要であり、CPU使用率が急上昇しないようにするため、これが発生することは望ましくありません。

HystrixCommandでスレッドプールサイズを設定する方法を見てみましょう。

@Test
public void givenSvcTimeoutOf500AndExecTimeoutOf10000AndThreadPool_whenRemoteSvcExecuted
  _thenReturnSuccess() throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupThreadPool"));

    HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
    commandProperties.withExecutionTimeoutInMilliseconds(10_000);
    config.andCommandPropertiesDefaults(commandProperties);
    config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
      .withMaxQueueSize(10)
      .withCoreSize(3)
      .withQueueSizeRejectionThreshold(10));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));
}

上記のテストでは、最大キューサイズ、コアキューサイズ、およびキュー拒否サイズを設定しています。 Hystrix は、スレッドの最大数が10に達し、タスクキューのサイズが10に達すると、リクエストの拒否を開始します。

コアサイズは、スレッドプールで常に存続するスレッドの数です。

5.3. 短絡ブレーカーパターンを使用した防御プログラミング

ただし、リモートサービス呼び出しに対して行うことができる改善はまだあります。

リモートサービスが失敗し始めた場合を考えてみましょう。

リクエストを出し続けてリソースを浪費したくありません。 理想的には、要求を再開する前にサービスが回復する時間を与えるために、一定時間要求を停止することをお勧めします。 これは、いわゆるショートサーキットブレーカーパターンです。

Hystrixがこのパターンをどのように実装するかを見てみましょう。

@Test
public void givenCircuitBreakerSetup_whenRemoteSvcCmdExecuted_thenReturnSuccess()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupCircuitBreaker"));

    HystrixCommandProperties.Setter properties = HystrixCommandProperties.Setter();
    properties.withExecutionTimeoutInMilliseconds(1000);
    properties.withCircuitBreakerSleepWindowInMilliseconds(4000);
    properties.withExecutionIsolationStrategy
     (HystrixCommandProperties.ExecutionIsolationStrategy.THREAD);
    properties.withCircuitBreakerEnabled(true);
    properties.withCircuitBreakerRequestVolumeThreshold(1);

    config.andCommandPropertiesDefaults(properties);
    config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
      .withMaxQueueSize(1)
      .withCoreSize(1)
      .withQueueSizeRejectionThreshold(1));

    assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
    assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
    assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));

    Thread.sleep(5000);

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));
}
public String invokeRemoteService(HystrixCommand.Setter config, int timeout)
  throws InterruptedException {

    String response = null;

    try {
        response = new RemoteServiceTestCommand(config,
          new RemoteServiceTestSimulator(timeout)).execute();
    } catch (HystrixRuntimeException ex) {
        System.out.println("ex = " + ex);
    }

    return response;
}

上記のテストでは、さまざまな回路ブレーカーのプロパティを設定しました。 最も重要なものは次のとおりです。

  • 4,000ミリ秒に設定されているCircuitBreakerSleepWindow。 これにより、回路ブレーカーウィンドウが構成され、リモートサービスへの要求が再開されるまでの時間間隔が定義されます。
  • CircuitBreakerRequestVolumeThreshold は1に設定され、失敗率が考慮される前に必要な要求の最小数を定義します

上記の設定を行うと、 HystrixCommand は、2つのリクエストが失敗した後、トリップして開きます。 サービス遅延を500ミリ秒に設定しても、3番目の要求はリモートサービスにヒットしません。 Hystrix が短絡し、メソッドは応答としてnullを返します。

続いて、設定したスリープウィンドウの制限を超えるために、 Thread.sleep(5000)を追加します。 これにより、 Hystrix が回路を閉じ、後続の要求が正常に流れます。

6. 結論

要約すると、Hystrixは次のように設計されています。

  1. ネットワークを介して通常アクセスされるサービスからの障害と遅延に対する保護と制御を提供します
  2. 一部のサービスのダウンに起因する障害のカスケードを停止します
  3. 迅速に失敗し、迅速に回復する
  4. 可能な場合は優雅に劣化させる
  5. 障害に関するコマンドセンターのリアルタイム監視とアラート

次の投稿では、Hystrixの利点をSpringフレームワークと組み合わせる方法を説明します。

完全なプロジェクトコードとすべての例は、githubプロジェクトにあります。