Hystrixの紹介

Hystrixの概要

1. 概要

典型的な分散システムは、多くのサービスが連携して構成されています。

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

もちろん、アプリケーションを回復力とフォールトトレラントにするのに役立つソリューションが利用可能です。そのようなフレームワークの1つがHystrixです。

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

この一連の投稿では、サービスまたはシステムに障害が発生した場合にHystrixがどのように対処するか、およびこれらの状況でHystrixが何を達成できるかを見ていきます。

2. 簡単な例

Hystrixがフォールトトレランスとレイテンシトレランスを提供する方法は、リモートサービスへの呼び出しを分離してラップすることです。

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

class CommandHelloWorld extends HystrixCommand {

    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依存関係が必要です。


    com.netflix.hystrix
    hystrix-core
    1.5.4

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


    com.netflix.rxjava
    rxjava-core
    0.20.7

このライブラリの最新バージョンは常にhereで見つけることができます。

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

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

In the example below、クラスRemoteServiceTestSimulatorは、リモートサーバー上のサービスを表します。 指定された時間が経過するとメッセージで応答するメソッドがあります。 この待機は、リモートシステムでの時間のかかるプロセスのシミュレーションであり、呼び出しサービスへの応答が遅れることを想像できます。

class RemoteServiceTestSimulator {

    private long wait;

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

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

RemoteServiceTestSimulatorを呼び出すAnd here is our sample client

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

class RemoteServiceTestCommand extends HystrixCommand {

    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. Defensive Programming with Timeout

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

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. ショート・ブレーカー・パターンを使ったディフェンシブ・プログラミング

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

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

リクエストを出し続けてリソースを浪費したくありません。 リクエストを再開する前にサービスが回復する時間を確保するために、一定時間リクエストの作成を停止することが理想的です。 これは、Short Circuit Breakerパターンと呼ばれるものです。

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。 これにより、サーキットブレーカーウィンドウが設定され、リモートサービスへの要求が再開されるまでの時間間隔が定義されます。

  • 1に設定され、失敗率が考慮される前に必要な要求の最小数を定義するCircuitBreakerRequestVolumeThreshold

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

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

6. 結論

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

  1. ネットワーク経由で通常アクセスされるサービスからの障害と遅延に対する保護と制御を提供します

  2. 一部のサービスがダウンしているために発生する障害の連鎖を停止します

  3. 迅速に失敗し、迅速に回復する

  4. 可能な限り優雅に分解する

  5. 障害に関するコマンドセンターのリアルタイム監視とアラート

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

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