1. 序章

Spring Cloud Netflix Zuul は、 NetflixZuulをラップするオープンソースゲートウェイです。 SpringBootアプリケーションにいくつかの特定の機能を追加します。 残念ながら、レート制限はそのままでは提供されていません。

このチュートリアルでは、レート制限リクエストのサポートを追加する Spring Cloud ZuulRateLimitについて説明します。

2. Maven構成

Spring Cloud Netflix Zuulの依存関係に加えて、 Spring Cloud ZuulRateLimitをアプリケーションのpom.xmlに追加する必要があります。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
    <groupId>com.marcosbarbero.cloud</groupId>
    <artifactId>spring-cloud-zuul-ratelimit</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>

3. コントローラの例

まず、レート制限を適用するRESTエンドポイントをいくつか作成しましょう。

以下は、2つのエンドポイントを持つ単純なSpringControllerクラスです。

@Controller
@RequestMapping("/greeting")
public class GreetingController {

    @GetMapping("/simple")
    public ResponseEntity<String> getSimple() {
        return ResponseEntity.ok("Hi!");
    }

    @GetMapping("/advanced")
    public ResponseEntity<String> 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<String> 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));
    assertThat(
      parseInt(headers.getFirst(HEADER_RESET + key)),
      is(both(greaterThanOrEqualTo(0)).and(lessThanOrEqualTo(60000)))
    );
}

ここでは、エンドポイント /greeting /simpleを1回呼び出します。 レート制限内であるため、リクエストは成功します。

もう1つの重要なポイントは、応答ごとに、レート制限に関する詳細情報を提供するヘッダーが返されることです。上記のリクエストの場合、次のヘッダーが返されます。

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<String> 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つのリクエストとして設定したため、2番目の呼び出しは失敗します。 その結果、エラーコード 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. カスタムキージェネレータ

カスタムキージェネレーターを使用して、応答ヘッダーで送信されるキーをカスタマイズできます。これは、アプリケーションが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つの重要なポイントは、 RateLimitKeyGeneratorBeanがspring-cloud-zuul-ratelimitによって自動的に構成されることです。

7. カスタムエラー処理

フレームワークは、レート制限データストレージのさまざまな実装をサポートします。 たとえば、SpringDataJPAとRedisが提供されています。 デフォルトでは、の失敗は、DefaultRateLimiterErrorHandlerクラスを使用してエラーとしてログに記録されます。

エラーを別の方法で処理する必要がある場合は、カスタム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 CloudNetflixZuulとSpringCloudZuulRateLimitを使用してAPIをレート制限する方法を説明しました。

いつものように、この記事の完全なコードはGitHubにあります。