背景
Azure CosmosDBであったり、AWS S3であったり、FAXを送信するサービスであったり、外部のサービスを呼び出すSDKを使うシーンが多くなってきた。
これまで普通に生きてきた世界は、自分で管理するデータベースサーバーの生死だけを気にすればよくて、ネットワーク的な距離もごく近傍。すべてが想定の範囲内の出来事だったが、こうした外部のサービスを利用するときには単純にはいかない。
ネットワークの遅延は安定せず、時には秒単位での遅延も起こりうる。多くのサービスはホスト名でエンドポイントが提供され、IPアドレスは予告なく変更になる。
こうした不安定なイベントが発生したとき、各サービスをHTTPなりで呼び出してブロッキングしているスレッドが急激にたまってしまい終いにはサービス全体の停止を引き起こすようなことがある。
つまりブロッキングするような呼び出しは、関数呼び出しにタイムアウトをつけてメインのHTTPスレッドを埋めないような注意が必要だ。
Rubyならばわりと簡単に書ける。
require 'timeout'
def callAnotherWorld
sleep 3
return "何か外部のサービスを呼び出した結果"
end
begin
result = Timeout.timeout(1) do # 1秒でタイムアウト
callAnotherWorld()
end
rescue Timeout::Error
result = ""
end
p result # タイムアウトするのでブランクになる
Javaでもだいたい同じようなコードになる。
private final ExecutorService threadPool = Executors.newCachedThreadPool();
private String callAnotherWorld() {
Future<String> f = threadPool.submit(() -> innerCallAnotherWorld());
while (true) {
try {
f.get(1, TimeUnit.SECONDS); // 1秒でタイムアウト
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException retry) {
}
}
}
private String innerCallAnotherWorld() {
return "何か外部のサービスを呼び出した結果";
}
RubyもJavaもバックグラウンドのスレッドでメソッドが実行され、呼び出し側スレッドはその完了を待つという動作である。
Javaの場合、スレッドプールの存在と、Futureの存在が見えてしまっており、少し残念な気持ちになる。
Resilience4J
そこで Resilience4J である。
Resilience4J は最近のマイブームで、分散アーキテクチャにおけるシステムの安定性を向上させるべく、以下のパターンが簡単に利用できるようになる。
- CircuitBreaker
- RateLimitter
- BulkHead
- Retry
- TimeLimiter
メソッド呼び出しに対するタイムアウトを実現するには、TimeLimiterを利用すればよい。
CircuitBreakerの使い方などはすぐにヒットするが、TimeLimiterについてはあまり情報がないので、自分用の備忘として書いておく。
private final ExecutorService threadPool = Executors.newCachedThreadPool();
private final TimeLimiterConfig config = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(1000))
.cancelRunningFuture(true)
.build();
private String callAnotherWorld() {
Callable<String> callable = TimeLimiter
.decorateFutureSupplier(
TimeLimiter.of(config),
() -> threadPool.submit(() -> innerCallAnotherWorld())
);
return Try.of(callable::call)
.getOrElse("");
}
あんまり短くならないが、catchとか例外処理がなくなり、ビジネスロジックにフォーカスできるコードになる。
Resilience4Jが提供する機能は、いまどきの分散環境において、必須となるデザインパターンで、非常に勉強になる。