Spring Cloud Netflix Zuulのレート制限
1. 前書き
Spring Cloud Netflix Zuulは、Netflix Zuulをラップするオープンソースゲートウェイです。 Spring Bootアプリケーション用の特定の機能を追加します。 残念ながら、すぐにレート制限は提供されません。
このチュートリアルでは、レート制限リクエストのサポートを追加するSpring Cloud Zuul RateLimitについて説明します。
2. Mavenの構成
Spring Cloud Netflix Zuulの依存関係に加えて、アプリケーションのpom.xmlにSpring Cloud Zuul RateLimitを追加する必要があります。
org.springframework.cloud
spring-cloud-starter-netflix-zuul
com.marcosbarbero.cloud
spring-cloud-zuul-ratelimit
2.2.0.RELEASE
3. コントローラ例
まず、レート制限を適用するRESTエンドポイントをいくつか作成しましょう。
以下は、2つのエンドポイントを持つ単純なSpring Controllerクラスです。
@Controller
@RequestMapping("/greeting")
public class GreetingController {
@GetMapping("/simple")
public ResponseEntity getSimple() {
return ResponseEntity.ok("Hi!");
}
@GetMapping("/advanced")
public ResponseEntity getAdvanced() {
return ResponseEntity.ok("Hello, how you doing?");
}
}
ご覧のとおり、エンドポイントのレート制限に固有のコードはありません。 これは、application.ymlファイル内のZuulプロパティで構成するためです。 したがって、コードを分離したままにします。
4. ズールの特性
次に、application.ymlファイルに次のZuulプロパティを追加しましょう。
zuul:
routes:
serviceSimple:
path: /greeting/simple
url: forward:/
serviceAdvanced:
path: /greeting/advanced
url: forward:/
ratelimit:
enabled: true
repository: JPA
policy-list:
serviceSimple:
- limit: 5
refresh-interval: 60
type:
- origin
serviceAdvanced:
- limit: 1
refresh-interval: 2
type:
- origin
strip-prefix: true
zuul.routesの下に、エンドポイントの詳細を提供します。 また、zuul.ratelimit.policy-list,の下で、エンドポイントのレート制限構成を提供します。 limitプロパティは、refresh-interval内でエンドポイントを呼び出すことができる回数を指定します。
ご覧のとおり、serviceSimpleエンドポイントに60秒あたり5リクエストのレート制限を追加しました。 対照的に、serviceAdvancedには、2秒あたり1リクエストのレート制限があります。
type構成は、どのレート制限アプローチに従うかを指定します。 可能な値は次のとおりです。
-
origin –ユーザーオリジンリクエストに基づくレート制限
-
url –ダウンストリームサービスのリクエストパスに基づくレート制限
-
user –認証されたユーザー名または「匿名」に基づくレート制限
-
値なし-サービスごとにグローバル構成として機能します。 このアプローチを使用するには、param 'type'を設定しないでください。
5. レート制限のテスト
5.1. レート制限内のリクエスト
次に、レート制限をテストしましょう。
@Test
public void whenRequestNotExceedingCapacity_thenReturnOkResponse() {
ResponseEntity response = restTemplate.getForEntity(SIMPLE_GREETING, String.class);
assertEquals(OK, response.getStatusCode());
HttpHeaders headers = response.getHeaders();
String key = "rate-limit-application_serviceSimple_127.0.0.1";
assertEquals("5", headers.getFirst(HEADER_LIMIT + key));
assertEquals("4", headers.getFirst(HEADER_REMAINING + key));
assertEquals("60000", headers.getFirst(HEADER_RESET + key));
}
ここでは、エンドポイント/greeting/simpleを1回呼び出します。 リクエストはレート制限内であるため成功します。
もう1つの重要なポイントは、with each response we get back headers providing us with further information on the rate limit.上記のリクエストの場合、次のヘッダーを取得することです。
X-RateLimit-Limit-rate-limit-application_serviceSimple_127.0.0.1: 5
X-RateLimit-Remaining-rate-limit-application_serviceSimple_127.0.0.1: 4
X-RateLimit-Reset-rate-limit-application_serviceSimple_127.0.0.1: 60000
言い換えると:
-
X-RateLimit-Limit-[key]:エンドポイント用に構成されたlimit
-
X-RateLimit-Remaining-[key]:エンドポイントの呼び出しの残りの試行回数
-
X-RateLimit-Reset-[key]:エンドポイント用に構成されたrefresh-intervalの残りのミリ秒数
さらに、同じエンドポイントをすぐに再度起動すると、次の結果が得られます。
X-RateLimit-Limit-rate-limit-application_serviceSimple_127.0.0.1: 5
X-RateLimit-Remaining-rate-limit-application_serviceSimple_127.0.0.1: 3
X-RateLimit-Reset-rate-limit-application_serviceSimple_127.0.0.1: 57031
残りの試行回数と残りのミリ秒数が減少していることに注意してください。
5.2. レート制限を超えるリクエスト
レート制限を超えるとどうなるか見てみましょう。
@Test
public void whenRequestExceedingCapacity_thenReturnTooManyRequestsResponse() throws InterruptedException {
ResponseEntity response = this.restTemplate.getForEntity(ADVANCED_GREETING, String.class);
assertEquals(OK, response.getStatusCode());
for (int i = 0; i < 2; i++) {
response = this.restTemplate.getForEntity(ADVANCED_GREETING, String.class);
}
assertEquals(TOO_MANY_REQUESTS, response.getStatusCode());
HttpHeaders headers = response.getHeaders();
String key = "rate-limit-application_serviceAdvanced_127.0.0.1";
assertEquals("1", headers.getFirst(HEADER_LIMIT + key));
assertEquals("0", headers.getFirst(HEADER_REMAINING + key));
assertNotEquals("2000", headers.getFirst(HEADER_RESET + key));
TimeUnit.SECONDS.sleep(2);
response = this.restTemplate.getForEntity(ADVANCED_GREETING, String.class);
assertEquals(OK, response.getStatusCode());
}
ここでは、エンドポイント/greeting/advancedを2回続けて呼び出します。 レート制限を2秒ごとに1つのリクエストとして構成しているため、the second call will fail。 その結果、エラーコード429 (Too Many Requests)がクライアントに返されます。
以下は、レート制限に達したときに返されるヘッダーです。
X-RateLimit-Limit-rate-limit-application_serviceAdvanced_127.0.0.1: 1
X-RateLimit-Remaining-rate-limit-application_serviceAdvanced_127.0.0.1: 0
X-RateLimit-Reset-rate-limit-application_serviceAdvanced_127.0.0.1: 268
その後、2秒間寝ます。 これは、エンドポイント用に構成されたrefresh-intervalです。 最後に、エンドポイントを再度起動し、成功した応答を取得します。
6. カスタムキージェネレータ
We can customize the keys sent in the response header using a custom key generator.これは、アプリケーションがtypeプロパティによって提供されるオプションを超えてキー戦略を制御する必要がある場合があるため便利です。
たとえば、これはカスタムRateLimitKeyGenerator実装を作成することで実行できます。 修飾子またはまったく異なるものを追加できます。
@Bean
public RateLimitKeyGenerator rateLimitKeyGenerator(RateLimitProperties properties,
RateLimitUtils rateLimitUtils) {
return new DefaultRateLimitKeyGenerator(properties, rateLimitUtils) {
@Override
public String key(HttpServletRequest request, Route route,
RateLimitProperties.Policy policy) {
return super.key(request, route, policy) + "_" + request.getMethod();
}
};
}
上記のコードは、RESTメソッド名をキーに追加します。 例えば:
X-RateLimit-Limit-rate-limit-application_serviceSimple_127.0.0.1_GET: 5
もう1つの重要な点は、RateLimitKeyGenerator Beanがspring-cloud-zuul-ratelimitによって自動的に構成されることです。
7. カスタムエラー処理
フレームワークは、レート制限データストレージのさまざまな実装をサポートしています。 たとえば、Spring Data JPAとRedisが提供されています。 デフォルトでは、DefaultRateLimiterErrorHandlerクラスを使用するfailures are just logged as errors。
エラーを別の方法で処理する必要がある場合は、カスタムRateLimiterErrorHandlerBeanを定義できます。
@Bean
public RateLimiterErrorHandler rateLimitErrorHandler() {
return new DefaultRateLimiterErrorHandler() {
@Override
public void handleSaveError(String key, Exception e) {
// implementation
}
@Override
public void handleFetchError(String key, Exception e) {
// implementation
}
@Override
public void handleError(String msg, Exception e) {
// implementation
}
};
}
RateLimitKeyGenerator Beanと同様に、RateLimiterErrorHandlerBeanも自動的に構成されます。
8. 結論
この記事では、Spring Cloud Netflix ZuulおよびSpring Cloud Zuul RateLimitを使用して制限APIを評価する方法を説明しました。
いつものように、この記事の完全なコードはover on GitHubにあります。