全4193文字
PR

 複数のコンポーネントがネットワークで相互に通信しながらシステムが稼働する分散システム環境では、ハードウエア障害、ソフトウエアの不具合、トラフィックの急増といった様々なトラブルが発生する。こうした現実の環境を前提としてサービスの可用性を高めるためのソフトウエア設計パターンが「レジリエンスプログラミング」である。

 今回は、Java実行環境でレジリエンスプログラミングを実現するライブラリー「Resilience4j」を利用し、分散システムにおけるタイムアウトやリトライなどのデザインパターンを実際のコードで体験していこう。

JSONの取得コードにレジリエンスを導入する

 Javaは、マルチスレッドベースの逐次実行型のプログラミングモデルを採用している。分散システム環境においてHTTP(Hypertext Transfer Protocol)やgRPC(gRPC Remote Procedure Calls)といったプロトコルを使い、別のコンポーネントのAPIを呼び出すとしよう。通常は、あるスレッドがリクエストを発行した後、レスポンスが戻るまでの間、そのスレッドは応答待ちでブロッキングされることになる。

 Javaで広く使われているHTTPクライアントライブラリー「Apache HttpClient」を利用し、HTTPのエンドポイントからJSONデータをGETメソッドで取得する標準的なコードは次のようになる。

public String get(String url) throws IOException {
  try (CloseableHttpClient client = HttpClients.createDefault()) {
    return client.execute(new HttpGet(url), response -> {
      int status = response.getCode();
      if (status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_REDIRECTION) {
        HttpEntity entity = response.getEntity();
        try {
          return entity != null ? EntityUtils.toString(entity) : null;
        } catch (ParseException ex) {
          throw new ClientProtocolException(ex);
        }
      } else {
        throw new ClientProtocolException("unexpected response status: " + status);
      }
    });
  }
}

 このコードはレスポンスが返るまでブロッキングされる。このため、HTTPのエンドポイント側で遅延などの障害が発生した場合、その障害の影響が次々に波及してしまう。これを「カスケード障害」と呼ぶ。

 カスケード障害を防ぐには、通常は次のようにネットワークレベルでタイムアウト時間を明示的に設定する。

RequestConfig config = RequestConfig.custom()
  // TLS接続が確立するまでのタイムアウト
  .setConnectTimeout(Timeout.ofMilliseconds(3000))
  // HttpClientの接続プールから接続を取得する際のタイムアウト
  .setConnectionRequestTimeout(Timeout.ofMilliseconds(1000))
  // エンドポイントからレスポンスが送信され始めるまでのタイムアウト
  .setResponseTimeout(Timeout.ofMilliseconds(10000))
  .build();
CloseableHttpClient client = HttpClients.custom().setDefaultRequestConfig(config).build();

 「TLS接続が確立するまでのタイムアウト」「HttpClientの接続プールから接続を取得する際のタイムアウト」「エンドポイントからレスポンスが送信され始めるまでのタイムアウト」は、いずれもデフォルトでは3分になっている。この状態だと、何らかの障害が発生した場合、それぞれの箇所で3分間のブロッキングが発生する可能性がある。

 そこでここでは、それぞれのタイムアウトを3000ミリ秒、1000ミリ秒、1万ミリ秒に設定している。こうしたタイムアウトの設定は煩雑なため、書籍やブログのサンプルコードでは省略されているケースが多い。実際には対応が必要なのはタイムアウトだけではなく、呼び出し先がエラーのステータスコードを返すケースもある。