CLOVER🍀

That was when it all began.

Spring Cloud Gatewayで遊ぶ

これは、なにをしたくて書いたもの?

  • Spring Cloud Gatewayという、API Gatewayを構築できるライブラリがあるらしい
  • けっこう面白そうな機能を持っている雰囲気なので、1度触っておこうかと

というわけで、Spring Cloud Gatewayをお試しということで、遊んでみましたというエントリです。

Spring Cloud Gateway?

オフィシャルサイトに

This project provides a library for building an API Gateway on top of Spring MVC.

Spring Cloud Gateway

とあるように、Spring Web MVCでAPI Gatewayを作るためのライブラリのようです。

主要な機能としては

  • Spring Framework 5およびReactor、Spring Boot 2.0で作られている
  • リクエストに対して他のサービスへのルーティングと、フィルターの適用が可能
  • Circuit Breaker(Hystrix)を統合できる
  • Spring Cloud DiscoveryClientと統合できる
  • リクエストに対するRate Limitが設定できる
  • パスのRewriteが可能

といったところ。

ドキュメントとしても、PredicateとFilterの説明が大半で、とてもシンプルにまとめられています。

Spring Cloud Gateway

日本語情報としては、こちら。

Spring I/O 2018 報告会 - Spring Cloud Gateway / Spring Cloud Pipelines

あとは、サンプルを見ながら進めていく感じですね。

https://github.com/spring-cloud/spring-cloud-gateway/tree/v2.0.2.RELEASE/spring-cloud-gateway-sample

https://github.com/spring-cloud-samples/spring-cloud-gateway-sample

では、これらの情報を参照しつつ、試していってみましょう。

サンプルアプリケーション(バックエンド)

API Gatewayということで、バックエンドになんらかのAPI(じゃなくてもいいですが…)が必要ですよね。
というわけで、Express(Node.js)とSpring Web MVCでひとつずつ作っておきました。

まずは、Expressの方から。

環境。

$ node -v 
v10.13.0


$ npm -v
6.4.1

Expressのインストール。

$ npm i -S express

Expressのバージョン。

  "dependencies": {
    "express": "^4.16.4"
  }

ソースコード。
server.js

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser());

app.get('/echo', (req, res) => {
    res.send(`★${req.query.message}★ from Express`);
});

app.post('/echo', (req, res) => {
    res.send(`★${req.body.message}★ from Express`);
});

app.listen(3000);

2つエンドポイントを持ち、どちらもEcho的なものですが、GETの方はQueryStringで受け取った値を、POSTの場合は
JSONで受け取った値を「★」を付けて返す感じにしています。

あと、Expressからレスポンスが返却されたこともわかるようにしています。

package.jsonで、起動の設定をして

  "scripts": {
    "start": "node server.js"
  },

起動。

$ npm start

続いて、Spring Web MVCの方。

こちらは、Spring CLI+sdkmanでいくことにします。

環境。

$ java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-1ubuntu0.18.04.1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

$ sdk version

SDKMAN 5.7.3+337

この時点でのSpring Cloudが使用しているSpring Bootのバージョンが、2.0.2.RELEASEなので、バックエンドのアプリケーションに
直接的な関係はないのですが、なんとなく合わせておきました。

$ sdk install springboot 2.0.2.RELEASE

スクリプトを作成。
server.groovy

@RestController
class Server {
    @GetMapping('/echo')
    String echo(@RequestParam('message') String message) {
        "★${message}★ from Spring Boot"
    }

    @PostMapping('/echo')
    String echo(@RequestBody Map request) {
        "★${request.message}★ from Spring Boot"
    }
}

動作は、Expressの方と同じです。返ってくる文字列は、Spring Bootであることがわかるようにしました。

実行。

$ spring run server.groovy -- --server.port=9000

これで、バックエンドの準備はできました。

Spring Cloud Gatewayを使ったアプリケーションを作成する

では、いよいよSpring Cloud Gatewayを使ったアプリケーションを作っていきます。

ルーティングの条件やフィルターなど、いくつか試していきましょう。

環境

今回の環境は、こちら。

$ java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-1ubuntu0.18.04.1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)


$ mvn -version
Apache Maven 3.6.0 (97c98ec64a1fdfee7767ce5ffb20918da4f719f3; 2018-10-25T03:41:47+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_181, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-38-generic", arch: "amd64", family: "unix"
準備

BOMのimport。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.SR2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.0.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

Spring Bootのバージョンは、Spring Cloudが依存しているSpring Bootのバージョンに合わせています。

依存関係としては、まずは以下が必要です。

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

このあと、必要に応じて追加していきます。

PredicateとFilter?

ところで、最初の方にドキュメントはPredicateとFilterが大半だよ、という話をしました。

Spring Cloud Gateway

ソースコードを載せる前に、これについてかいておきましょう。

Predicateは、ルーティング先を振り分けるための条件を構築するためのものです。

全部のPredicateはドキュメントを参照、となりますが

Route Predicate Factories

例えば以下のようなものがあったりします。

  • あるヘッダーが、特定の値や正規表現にマッチした場合にルーティングを決定する
  • Hostヘッダーの値が、特定の値や正規表現にマッチした場合にルーティングを決定する
  • リクエストのパスが、特定のパス条件にマッチした場合にルーティングを決定する
  • QueryStringのあるパラメータが、特定の値や正規表現にマッチした場合にルーティングを決定する

Filterの方は、バックエンドに転送する際のリクエストや、バックエンドから戻ってきたレスポンスの内容を変更するのに
使用します。Filterには、GatewayFilterとGlobal Filterがあるようです。

Predicateと同じく、全部のFilterはドキュメントを参照、となりますが、

GatewayFilter Factories

Global Filters

例えば、以下のようなものがあります。

  • バックエンドに転送する際に、リクエストヘッダー、リクエストパラメーターを追加する
  • バックエンドからのレスポンスをクライアントに戻す際に、レスポンスヘッダーを追加する
  • Hystrix Circuit Breakerの機能を追加する
  • バックエンドに対するRate Limitを設定する
  • バックエンドに転送する際に、URL Rewriteを行う
  • バックエンドに転送する際に、リクエストパスの変更を行う

などなど。

これらのPredicateやFilterを使って、バックエンドへのルーティングや、変換処理を設定していきます。

サンプルプログラム

作成したサンプルプログラムは、これだけ。
src/main/java/org/littlewings/spring/cloud/gateway/App.java

package org.littlewings.spring.cloud.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

Spring Bootの起動クラスのみです。とても小さいですが、これだけ。

基本的には、application.propertiesもしくはapplication.ymlで多くの設定を行うことができます。

配列などを使うことが多そうなので、今回はapplication.ymlを使用します。

パスでルーティングする

最初は、パスでルーティングしてみます。Path Route Predicateを使います。

Path Route Predicate Factory

パスの先頭に「/express」か「/springboot」を加えることで見分けるようにして、ルーティングします。

バックエンドに転送する時には、このパスが不要になるのでこれを削除します。SetPath GatewayFilterを使います。

SetPath GatewayFilter Factory

設定ファイルは、このようになります。
src/main/resources/application.yml

spring:
  cloud:
    gateway:
      routes:
      - id: express_setpath_route
        uri: http://localhost:3000
        predicates:
        - Path=/express/{segment}
        filters:
        - SetPath=/{segment}
      - id: springboot_setpath_route
        uri: http://localhost:9000
        predicates:
        - Path=/springboot/{segment}
        filters:
        - SetPath=/{segment}

「id」からの指定が、個々のルーティングの設定ですが、例えば以下をクローズアップ。

      - id: express_setpath_route
        uri: http://localhost:3000
        predicates:
        - Path=/express/{segment}
        filters:
        - SetPath=/{segment}

「id」は任意の値ですが、ルーティング名を設定します。「uri」は、転送先のURLです。
「predicates」はこのルーティングを選択するための条件、「filters」には変換処理などを適用するFilterを定義します。

起動。

$ mvn spring-boot:run

確認

### from Express
$ curl localhost:8080/express/echo?message=Hello
★Hello★ from Express

$ curl -XPOST -H 'Content-Type: application/json' localhost:8080/express/echo -d '{"message": "Hello/POST"}'
★Hello/POST★ from Express


### from Spring Boot
$ curl localhost:8080/springboot/echo?message=Hello
★Hello★ from Spring Boot

$ curl -XPOST -H 'Content-Type: application/json' localhost:8080/springboot/echo -d '{"message": "Hello/POST"}'
★Hello/POST★ from Spring Boot

OKですね。

また、別解としてはSetPathではなくて、URL Rewriteするという方法もあるでしょう。

RewritePath GatewayFilter Factory

というわけで、Predicateはそのままに、FilterをRewritePath GatewayFilterを使うように書き直してみます。
src/main/resources/application.yml

spring:
  cloud:
    gateway:
      routes:
      - id: express_rewritepath_route
        uri: http://localhost:3000
        predicates:
        - Path=/express/**
        filters:
        - RewritePath=/express/(?<segment>.*), /$\{segment}
      - id: springboot_rewritepath_route
        uri: http://localhost:9000
        predicates:
        - Path=/springboot/**
        filters:
        - RewritePath=/springboot/(?<segment>.*), /$\{segment}

結果は、SetPathを使った時と同じなので割愛。

HTTPヘッダーでルーティングする

続いては、HTTPヘッダーでルーティングしてみます。

HTTPヘッダーでルーティングするには、Header Route Predicateを使用します。

Header Route Predicate Factory

HTTPヘッダー「X-Request-App」の値が、「express」か「springboot」かで見分けるようにします。

設定ファイルは、このようになりました。
src/main/resources/application.yml

spring:
  cloud:
    gateway:
      routes:
      - id: express_header_route
        uri: http://localhost:3000
        predicates:
        - Header=X-Request-App, express
      - id: springboot_header_route
        uri: http://localhost:9000
        predicates:
        - Header=X-Request-App, springboot

アプリケーションを起動して、確認してみます。

### from Express
$ curl -H 'X-Request-App: express' localhost:8080/echo?message=Hello
★Hello★ from Express

$ curl -XPOST -H 'X-Request-App: express' -H 'Content-Type: application/json' localhost:8080/echo -d '{"message": "Hello/POST"}'
★Hello/POST★ from Express


### from Spring Boot
$ curl -H 'X-Request-App: springboot' localhost:8080/echo?message=Hello
★Hello★ from Spring Boot

$ curl -XPOST -H 'X-Request-App: springboot' -H 'Content-Type: application/json' localhost:8080/echo -d '{"message": "Hello/POST"}'
★Hello/POST★ from Spring Boot

OKそうです。

Predicateにマッチしなかった場合は?

こういうルーティング設定の時、

spring:
  cloud:
    gateway:
      routes:
      - id: express_setpath_route
        uri: http://localhost:3000
        predicates:
        - Path=/express/{segment}
        filters:
        - SetPath=/{segment}
      - id: springboot_setpath_route
        uri: http://localhost:9000
        predicates:
        - Path=/springboot/{segment}
        filters:
        - SetPath=/{segment}

マッチしなかった部分はNot Foundになります。

### from Express
$ curl localhost:8080/express/echo?message=Hello
★Hello★ from Express


### from Spring Boot
$ curl localhost:8080/springboot/echo?message=Hello
★Hello★ from Spring Boot


### ???
$ curl -i localhost:8080/echo?message=Hello
HTTP/1.1 404 Not Found
transfer-encoding: chunked
Content-Type: application/json;charset=UTF-8

{"timestamp":"2018-11-12T13:57:18.925+0000","path":"/echo","status":404,"error":"Not Found","message":null}

以下のように、ハズれてもマッチするようなPredicateを仕掛けておくと拾えます。

spring:
  cloud:
    gateway:
      routes:
      - id: express_setpath_route
        uri: http://localhost:3000
        predicates:
        - Path=/express/{segment}
        filters:
        - SetPath=/{segment}
      - id: springboot_setpath_route
        uri: http://localhost:9000
        predicates:
        - Path=/springboot/{segment}
        filters:
        - SetPath=/{segment}
      - id: default_route
        uri: http://localhost:9000
        predicates:
        - Path=/**

まあ、ケースバイケースですね。

$ curl localhost:8080/echo?message=Hello
★Hello★ from Spring Boot
Predicateを複数書いた場合は?

Predicateを複数書いた場合はどうなるか?ですが、これはand結合となります。

ドキュメントにも、そのように書かれています。

Multiple Route Predicate Factories can be combined and are combined via logical and.

Route Predicate Factories

Java側でPredicateやFilterを設定する

ここまで、ずっとYAMLでルーティングなどを定義してきましたが、Java側でも書くことができます。

例えば、こちらのYAMLに対して

spring:
  cloud:
    gateway:
      routes:
      - id: express_setpath_route
        uri: http://localhost:3000
        predicates:
        - Path=/express/{segment}
        filters:
        - SetPath=/{segment}
      - id: springboot_setpath_route
        uri: http://localhost:9000
        predicates:
        - Path=/springboot/{segment}
        filters:
        - SetPath=/{segment}

等価なJavaコードは、このようになります。
src/main/java/org/littlewings/spring/cloud/gateway/App.java

package org.littlewings.spring.cloud.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder builder) {
        return builder
                .routes()
                .route(
                        "custom_express_setpath_route",
                        r -> r.path("/express/{segment}")
                                .filters(f -> f.setPath("/{segment}"))
                                .uri("http://localhost:3000"))
                .route(
                        "custom_springboot_setpath_route",
                        r -> r.path("/springboot/{segment}")
                                .filters(f -> f.setPath("/{segment}"))
                                .uri("http://localhost:9000"))
                .build();
    }
}

Java側で定義すると、Predicateをandではなくorで定義したりすることも可能になります。

その他、サンプルはこちら。

spring-cloud-gateway/GatewaySampleApplication.java at v2.0.2.RELEASE · spring-cloud/spring-cloud-gateway · GitHub

Hystrix GatewayFilterを使う

ここからは、ちょっと毛色を変えていきましょう。

Hystrix Circuit Breakerを使った、GatewayFilterを適用してみます。

Hystrix GatewayFilter Factory

Hystrix GatewayFilterを使うことで、ルーティング先のサービスがダウンしていた場合に、接続をしないようにして代替の
レスポンスを返したりできるようになります。

Hystrix GatewayFilterを使うには、「spring-cloud-starter-netflix-hystrix」を依存関係に追加します。

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

フォールバック先は、フォワードのリクエストを受け付けるControllerとして作成します。
src/main/java/org/littlewings/spring/cloud/gateway/MyHystrixController.java

package org.littlewings.spring.cloud.gateway;

import java.util.HashMap;
import java.util.Map;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyHystrixController {
    @GetMapping("fallback-dummy-response")
    public Map<String, Object> dummyResponse() {
        Map<String, Object> response = new HashMap<>();
        response.put("message", "response from HystrixCommand");
        return response;
    }
}

このControllerにフォワードするように、Hystrix GatewayFilterの設定を行います。「fallbackUri」というやつですね。 src/main/resources/application.yml

spring:
  cloud:
    gateway:
      routes:
      - id: express_setpath_hystrix_route
        uri: http://localhost:3000
        predicates:
        - Path=/express/{segment}
        filters:
        - SetPath=/{segment}
        - name: Hystrix
          args:
            name: fallbackcmd
            fallbackUri: forward:/fallback-dummy-response
      - id: springboot_setpath_hystrix_route
        uri: http://localhost:9000
        predicates:
        - Path=/springboot/{segment}
        filters:
        - SetPath=/{segment}
        - name: Hystrix
          args:
            name: fallbackcmd
            fallbackUri: forward:/fallback-dummy-response

これで、例えばExpressで作成したサーバーを停止させてアクセスすると、こんな感じになります。

$ curl localhost:8080/express/echo?message=Hello
{"message":"response from HystrixCommand"}

フォールバック先のControllerから、レスポンスが返っていますね。

なお、停止させたExpressのサーバーを復帰させると、そのうち回復します。

$ curl localhost:8080/express/echo?message=Hello
★Hello★ from Express

ところで、ドキュメントを読んでいるとfallbackUri以外の設定(HystrixCommandを使う)とか書いているのですが、
ソースコードを見ているとむしろ、fallbackUriを使わないとうまく動かないのでは?という気もするのですが…。
https://github.com/spring-cloud/spring-cloud-gateway/blob/v2.0.2.RELEASE/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java#L150-L172

Rate Limit

Rate Limit(流量制御)も試してみましょう。

Rate Limitを使うには、RequestRateLimiter GatewayFilterを使用します。

RequestRateLimiter GatewayFilter Factory

RequestRateLimiter GatewayFilterを使用すると、流量制御ができ、許容値を超えると「HTTP 429 - Too Many Requests」(デフォルト)を
返すようになります。

「なにに対して流量制御するのか」は、KeyResolverというもので決定します。

RequestRateLimiter GatewayFilterの実装としては、Redisを使ったものが提供されています。

Redis RateLimiter

ですので、ここから先はRedisを使った場合の話になります。

Redis RateLimiterを使うには、依存関係に「spring-boot-starter-data-redis-reactive」を追加します。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

設定は、こんな感じで。
src/main/resources/application.yml

spring:
  cloud:
    gateway:
      routes:
      - id: express_setpath_ratelimit_route
        uri: http://localhost:3000
        predicates:
        - Path=/express/{segment}
        filters:
        - SetPath=/{segment}
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 2
            redis-rate-limiter.burstCapacity: 4
            key-resolver: "#{@userKeyResolver}"
      - id: springboot_setpath_ratelimit_route
        uri: http://localhost:9000
        predicates:
        - Path=/springboot/{segment}
        filters:
        - SetPath=/{segment}
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 2
            redis-rate-limiter.burstCapacity: 4
            key-resolver: "#{@userKeyResolver}"

replenishRateというのは、「ある単位に対して」1秒あたりの許容するリクエスト数を表します。

burstCapacityは、「ある単位に対して」実行できる最大リクエスト数です。burstCapacityがreplenishRateを超えている場合、
それは一時的なバースト値を許可することを表します。つまり、replenishRateを超えても大丈夫な時があるということです。

そういう揺らぎが必要ないのであれば、replenishRateとburstCapacityには同じ値を設定しましょう。

今回は、replenishRateを2、burstCapacityを4としています。

で、ここまで言っている「ある単位」というのは、KeyResolverで決定します。

KeyResolverはデフォルトでjava.securiyt.Principal#getNameの結果を単位とするPrincipalNameKeyResolverが提供されて
いますが、今回は自前でいきます(といっても、ドキュメントの例のまんまですが)。
src/main/java/org/littlewings/spring/cloud/gateway/App.java

package org.littlewings.spring.cloud.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import reactor.core.publisher.Mono;

@SpringBootApplication
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @Bean
    KeyResolver userKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    }
}

QueryStringの「user」を単位にする、KeyResolverです。

    @Bean
    KeyResolver userKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    }

通常は、ここをAPIキーとかにするのでしょう。

これが、先ほどのYAMLで指定していたKeyResolverです。

            key-resolver: "#{@userKeyResolver}"

で、こちらのサンプルを実行するのにはRedisが必要になるので、Redisを用意して接続先も定義します。Redisは、4.0.11を
使用しました。

設定ファイルは、結果このようになりました。

spring:
  redis:
    host: 172.17.0.2
    port: 6379
    password: redispass
  cloud:
    gateway:
      routes:
      - id: express_setpath_ratelimit_route
        uri: http://localhost:3000
        predicates:
        - Path=/express/{segment}
        filters:
        - SetPath=/{segment}
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 2
            redis-rate-limiter.burstCapacity: 4
            key-resolver: "#{@userKeyResolver}"
      - id: springboot_setpath_ratelimit_route
        uri: http://localhost:9000
        predicates:
        - Path=/springboot/{segment}
        filters:
        - SetPath=/{segment}
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 2
            redis-rate-limiter.burstCapacity: 4
            key-resolver: "#{@userKeyResolver}"

これでアクセスすると、ヘッダーに現在の「replenishRate」と「burstCapacity」、残りのキャパシティ(X-RateLimit-Remaining)が
入るようになります。

$ curl -i 'localhost:8080/express/echo?message=Hello&user=abc'
HTTP/1.1 200 OK
X-RateLimit-Remaining: 3
X-RateLimit-Burst-Capacity: 4
X-RateLimit-Replenish-Rate: 2
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 24
ETag: W/"18-GI8qRvU/O6RpZUoTp0txNT1Ttao"
Date: Mon, 12 Nov 2018 14:35:51 GMT

頻度を上げてアクセスすると減っていき

$ curl -i 'localhost:8080/express/echo?message=Hello&user=abc'
HTTP/1.1 200 OK
X-RateLimit-Remaining: 2
X-RateLimit-Burst-Capacity: 4
X-RateLimit-Replenish-Rate: 2
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 24
ETag: W/"18-GI8qRvU/O6RpZUoTp0txNT1Ttao"
Date: Mon, 12 Nov 2018 14:35:50 GMT

使い切ると「429 Too Many Requests」となります。

$ curl -i 'localhost:8080/express/echo?message=Hello&user=abc'
HTTP/1.1 429 Too Many Requests
X-RateLimit-Remaining: 0
X-RateLimit-Burst-Capacity: 4
X-RateLimit-Replenish-Rate: 2
content-length: 0

時間を空けると、また戻ります。

$ curl -i 'localhost:8080/express/echo?message=Hello&user=abc'
HTTP/1.1 200 OK
X-RateLimit-Remaining: 3
X-RateLimit-Burst-Capacity: 4
X-RateLimit-Replenish-Rate: 2
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 24
ETag: W/"18-GI8qRvU/O6RpZUoTp0txNT1Ttao"
Date: Mon, 12 Nov 2018 14:41:02 GMT

戻った値は、burstCapacityから1引いた値ですね。

これを連続してアクセスしていると、replenishRateまでしか戻らなくなったりします。

このロジックですが、Luaで書かれたスクリプトをRedisで実行することで実現しています。
https://github.com/spring-cloud/spring-cloud-gateway/blob/v2.0.2.RELEASE/spring-cloud-gateway-core/src/main/resources/META-INF/scripts/request_rate_limiter.lua

Luaスクリプトを実行する時のキーは、KeyResolverの結果を元に決まり、
https://github.com/spring-cloud/spring-cloud-gateway/blob/v2.0.2.RELEASE/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/ratelimit/RedisRateLimiter.java#L199-L210

制限した流量を超えた場合は、後続のFilterを実行しなくなります。
https://github.com/spring-cloud/spring-cloud-gateway/blob/v2.0.2.RELEASE/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestRateLimiterGatewayFilterFactory.java#L71-L73

ちなみに、指定したKeyResolverを使うようにしている箇所は、ここですね。
https://github.com/spring-cloud/spring-cloud-gateway/blob/v2.0.2.RELEASE/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RequestRateLimiterGatewayFilterFactory.java#L57

で、許容するキャパシティですが、「burstCapacity」か、「replenishRate×経過秒数+残キャパシティ」の少ない方で決まります。
https://github.com/spring-cloud/spring-cloud-gateway/blob/v2.0.2.RELEASE/spring-cloud-gateway-core/src/main/resources/META-INF/scripts/request_rate_limiter.lua#L33

連続してアクセスしていると、少ない方が選択されるので「replenishRate×経過秒数+残キャパシティ」が選択され、
1秒あたりreplenishRateしかキャパシティが増えないことになります。

どういう時にキャパシティがburstCapacityとなるか(バースト許容期間)の割合は、replenishRateとburstCapacityの割合で
決まります。
https://github.com/spring-cloud/spring-cloud-gateway/blob/v2.0.2.RELEASE/spring-cloud-gateway-core/src/main/resources/META-INF/scripts/request_rate_limiter.lua#L10-L11 https://github.com/spring-cloud/spring-cloud-gateway/blob/v2.0.2.RELEASE/spring-cloud-gateway-core/src/main/resources/META-INF/scripts/request_rate_limiter.lua#L47-L48

この割合がTTLとして使われます。また、初回は前回の結果がないので、burstCapacityがキャパシティとして選択される
わけですね。

ちなみに、この説明したロジック、「Redisを使ったRateLimiterの場合」の話なので、そうでない場合はこれに沿った
ロジックを実装する必要があるということです。

Spring Cloud Gatewayのサンプルでも、Redisを使わないRateLimiterが作成されています。

https://github.com/spring-cloud/spring-cloud-gateway/blob/v2.0.2.RELEASE/spring-cloud-gateway-sample/src/main/java/org/springframework/cloud/gateway/sample/ThrottleGatewayFilter.java

Ribbonとの統合や、リトライについて

今回は、パス…。

まとめ

Spring Cloud Gatewayを使って、基礎的なことをいろいろと試してみました。

最初、PredicateやFilterの扱いがわからなくてちょっと困りましたが、慣れると簡単に使えそうな感じで良いですね。

オマケ

デバッグの際のログレベルは、こんな感じにしておくとよいかも?

logging:
  level:
    org.springframework.cloud.gateway: TRACE
    org.springframework.http.server.reactive: DEBUG
    org.springframework.web.reactive: DEBUG
    reactor.ipc.netty: DEBUG
    reactor.netty: DEBUG

こんな感じで、定義したルーティングとマッチしたルーティングがわかったり

2018-11-13 01:04:18.334 DEBUG 18322 --- [server-epoll-10] o.s.web.reactive.DispatcherHandler       : Processing GET request for [http://localhost:8080/express/echo?message=Hello]
2018-11-13 01:04:18.340 DEBUG 18322 --- [server-epoll-10] s.w.r.r.m.a.RequestMappingHandlerMapping : Looking up handler method for path /express/echo
2018-11-13 01:04:18.343 DEBUG 18322 --- [server-epoll-10] s.w.r.r.m.a.RequestMappingHandlerMapping : Did not find handler method for [/express/echo]
2018-11-13 01:04:18.356 DEBUG 18322 --- [server-epoll-10] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition express_setpath_route applying {_genkey_0=/express/{segment}} to Path
2018-11-13 01:04:18.364 DEBUG 18322 --- [server-epoll-10] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition express_setpath_route applying filter {_genkey_0=/{segment}} to SetPath
2018-11-13 01:04:18.372 DEBUG 18322 --- [server-epoll-10] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: express_setpath_route
2018-11-13 01:04:18.372 DEBUG 18322 --- [server-epoll-10] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition springboot_setpath_route applying {_genkey_0=/springboot/{segment}} to Path
2018-11-13 01:04:18.374 DEBUG 18322 --- [server-epoll-10] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition springboot_setpath_route applying filter {_genkey_0=/{segment}} to SetPath
2018-11-13 01:04:18.375 DEBUG 18322 --- [server-epoll-10] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: springboot_setpath_route
2018-11-13 01:04:18.375 DEBUG 18322 --- [server-epoll-10] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition default_route applying {_genkey_0=/**} to Path
2018-11-13 01:04:18.377 DEBUG 18322 --- [server-epoll-10] o.s.c.g.r.RouteDefinitionRouteLocator    : RouteDefinition matched: default_route
2018-11-13 01:04:18.381 TRACE 18322 --- [server-epoll-10] o.s.c.g.h.p.RoutePredicateFactory        : Pattern "/express/{segment}" matches against value "[path='/express/echo']"
2018-11-13 01:04:18.382 DEBUG 18322 --- [server-epoll-10] o.s.c.g.h.RoutePredicateHandlerMapping   : Route matched: express_setpath_rou

バックエンドへ転送したリクエストの内容や

2018-11-13 01:04:18.474 DEBUG 18322 --- [client-epoll-14] r.i.n.channel.ChannelOperationsHandler   : [id: 0x31cae6ce, L:/127.0.0.1:59272 - R:localhost/127.0.0.1:3000] Writing object DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
GET /echo?message=Hello HTTP/1.1
User-Agent: curl/7.58.0
Accept: */*
Forwarded: proto=http;host="localhost:8080";for="127.0.0.1:39500"
X-Forwarded-For: 127.0.0.1
X-Forwarded-Proto: http
X-Forwarded-Prefix: /express
X-Forwarded-Port: 8080
X-Forwarded-Host: localhost:8080
host: localhost:3000

レスポンスの内容が確認できたりします。

2018-11-13 01:04:18.506 DEBUG 18322 --- [server-epoll-10] r.i.n.channel.ChannelOperationsHandler   : [id: 0xe34b930c, L:/127.0.0.1:8080 - R:/127.0.0.1:39500] Writing object DefaultHttpResponse(decodeResult: success, version: HTTP/1.1)
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 24
ETag: W/"18-GI8qRvU/O6RpZUoTp0txNT1Ttao"
Date: Mon, 12 Nov 2018 16:04:18 GMT