Javaにおけるスロットリングパターン:高負荷アプリケーションでのリソース使用の最適化
別名
- レート制限
スロットリングデザインパターンの目的
スロットリングパターン(レート制限とも呼ばれる)は、過負荷を防ぎ安定性を確保するために、システムが特定の時間枠内で処理できるリクエスト数を制限します。これは、Javaアプリケーションにおけるリソース管理にとって非常に重要です。
スロットリングパターンの詳細な説明と実世界の例
実世界の例
人気のある遊園地が、過密状態を避けるために1時間あたりに入場できる訪問者数を制限することを想像してください。これにより、すべての訪問者が長い待ち時間なしに公園を楽しむことができ、快適な体験を維持できます。同様に、ソフトウェアにおけるスロットリングデザインパターンは、システムへのリクエストのレートを制御し、圧倒されるのを防ぎ、すべてのユーザーに一貫したパフォーマンスを保証します。
平易な言葉で
スロットリングパターンは、リソースへのアクセスをレート制限するために使用されます。
Microsoftのドキュメントによると
アプリケーションのインスタンス、個々のテナント、またはサービス全体で使用されるリソースの消費を制御します。これにより、需要の増加によってリソースに極端な負荷がかかった場合でも、システムが引き続き機能し、サービスレベルアグリーメントを満たすことができます。
Javaにおけるスロットリングパターンのプログラム例
このJavaの例では、スロットリングを実証します。若い人間と年老いたドワーフがバーに入ります。彼らはバーテンダーにビールを注文し始めます。バーテンダーはすぐに、若い人間があまりにも早く飲みすぎるべきではないと判断し、十分な時間が経過していない場合は提供を拒否します。年老いたドワーフの場合、提供レートはより高くすることができます。
BarCustomer
クラスは、Bartender
APIのクライアントを表します。CallsCount
は、BarCustomer
ごとの呼び出し数を追跡します。
@Getter
public class BarCustomer {
private final String name;
private final int allowedCallsPerSecond;
public BarCustomer(String name, int allowedCallsPerSecond, CallsCount callsCount) {
if (allowedCallsPerSecond < 0) {
throw new InvalidParameterException("Number of calls less than 0 not allowed");
}
this.name = name;
this.allowedCallsPerSecond = allowedCallsPerSecond;
callsCount.addTenant(name);
}
}
@Slf4j
public final class CallsCount {
private final Map<String, AtomicLong> tenantCallsCount = new ConcurrentHashMap<>();
public void addTenant(String tenantName) {
tenantCallsCount.putIfAbsent(tenantName, new AtomicLong(0));
}
public void incrementCount(String tenantName) {
tenantCallsCount.get(tenantName).incrementAndGet();
}
public long getCount(String tenantName) {
return tenantCallsCount.get(tenantName).get();
}
public void reset() {
tenantCallsCount.replaceAll((k, v) -> new AtomicLong(0));
LOGGER.info("reset counters");
}
}
次に、テナントが呼び出しているサービスが導入されます。呼び出し数を追跡するために、スロットラータイマーが使用されます。
public interface Throttler {
void start();
}
public class ThrottleTimerImpl implements Throttler {
private final int throttlePeriod;
private final CallsCount callsCount;
public ThrottleTimerImpl(int throttlePeriod, CallsCount callsCount) {
this.throttlePeriod = throttlePeriod;
this.callsCount = callsCount;
}
@Override
public void start() {
new Timer(true).schedule(new TimerTask() {
@Override
public void run() {
callsCount.reset();
}
}, 0, throttlePeriod);
}
}
Bartender
は、BarCustomer
にorderDrink
サービスを提供します。顧客は、ビールの提供レートが外見によって制限されていることをおそらく知らないでしょう。
class Bartender {
private static final Logger LOGGER = LoggerFactory.getLogger(Bartender.class);
private final CallsCount callsCount;
public Bartender(Throttler timer, CallsCount callsCount) {
this.callsCount = callsCount;
timer.start();
}
public int orderDrink(BarCustomer barCustomer) {
var tenantName = barCustomer.getName();
var count = callsCount.getCount(tenantName);
if (count >= barCustomer.getAllowedCallsPerSecond()) {
LOGGER.error("I'm sorry {}, you've had enough for today!", tenantName);
return -1;
}
callsCount.incrementCount(tenantName);
LOGGER.debug("Serving beer to {} : [{} consumed] ", barCustomer.getName(), count + 1);
return getRandomCustomerId();
}
private int getRandomCustomerId() {
return ThreadLocalRandom.current().nextInt(1, 10000);
}
}
これで、完全な例の動作を見ることができます。BarCustomer
の若い人間は、1秒あたり2回の呼び出しにレート制限され、年老いたドワーフは4回にレート制限されます。
@Slf4j
public class App {
public static void main(String[] args) {
var callsCount = new CallsCount();
var human = new BarCustomer("young human", 2, callsCount);
var dwarf = new BarCustomer("dwarf soldier", 4, callsCount);
var executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> makeServiceCalls(human, callsCount));
executorService.execute(() -> makeServiceCalls(dwarf, callsCount));
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
}
private static void makeServiceCalls(BarCustomer barCustomer, CallsCount callsCount) {
var timer = new ThrottleTimerImpl(1000, callsCount);
var service = new Bartender(timer, callsCount);
// Sleep is introduced to keep the output in check and easy to view and analyze the results.
IntStream.range(0, 50).forEach(i -> {
service.orderDrink(barCustomer);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
LOGGER.error("Thread interrupted: {}", e.getMessage());
}
});
}
}
例のコンソール出力からの抜粋
18:46:36.218 [Timer-0] INFO com.iluwatar.throttling.CallsCount - reset counters
18:46:36.218 [Timer-1] INFO com.iluwatar.throttling.CallsCount - reset counters
18:46:36.242 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [1 consumed]
18:46:36.242 [pool-1-thread-1] DEBUG com.iluwatar.throttling.Bartender - Serving beer to young human : [1 consumed]
18:46:36.342 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [2 consumed]
18:46:36.342 [pool-1-thread-1] DEBUG com.iluwatar.throttling.Bartender - Serving beer to young human : [2 consumed]
18:46:36.443 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.443 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [3 consumed]
18:46:36.544 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.544 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [4 consumed]
18:46:36.645 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.645 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.745 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.745 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.846 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.846 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.947 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.947 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:37.048 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:37.048 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:37.148 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:37.148 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
Javaでスロットリングパターンを使用する場合
- リソースが過剰なリクエストによって圧倒されるのを防ぐ必要があります。
- 複数のユーザー間でサービスを公平に利用できるようにする必要があります。
- 高負荷条件下でサービスの品質を維持する必要があります。
Javaにおけるスロットリングパターンの実世界での応用
- AWS、Google Cloud、Azureなどの主要なクラウドプロバイダーのAPIは、リソース使用を管理するためにスロットリングを使用しています。
- 単一のIPアドレスからのリクエスト数を制限することにより、サービス拒否(DoS)攻撃を防ぐためのWebサービス。
- ソーシャルメディアサイトやeコマースWebサイトなどのオンラインプラットフォームで、サーバー負荷の均等な分散を確保します。
スロットリングパターンの利点とトレードオフ
利点
- リソースの枯渇を防ぎ、システムの安定性を確保します。
- 一貫したパフォーマンスとサービスの品質を維持するのに役立ちます。
- 高負荷時のシステムクラッシュを回避することにより、フォールトトレランスを向上させます。
トレードオフ
- リクエスト処理のレイテンシーが増加したり、遅延が発生したりする可能性があります。
- リソース保護とユーザーエクスペリエンスのバランスを調整するために、慎重なチューニングが必要です。
- 正しく構成されていない場合、正当なユーザーへのサービス拒否につながる可能性があります。
関連するJavaデザインパターン
- サーキットブレーカー:過負荷のサービスへのアクセスを繰り返す試みを防ぐために、スロットリングと連携して動作します。
- バルクヘッド:システム内のさまざまな部分を分離し、スロットリングが他のコンポーネントに与える影響を制限します。