これは、なにをしたくて書いたもの?
というわけで、Spring Cloud Gatewayをお試しということで、遊んでみましたというエントリです。
Spring Cloud Gateway?
オフィシャルサイトに
This project provides a library for building an API Gateway on top of Spring MVC.
とあるように、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 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が大半だよ、という話をしました。
ソースコードを載せる前に、これについてかいておきましょう。
Predicateは、ルーティング先を振り分けるための条件を構築するためのものです。
全部のPredicateはドキュメントを参照、となりますが
例えば以下のようなものがあったりします。
- あるヘッダーが、特定の値や正規表現にマッチした場合にルーティングを決定する
- Hostヘッダーの値が、特定の値や正規表現にマッチした場合にルーティングを決定する
- リクエストのパスが、特定のパス条件にマッチした場合にルーティングを決定する
- QueryStringのあるパラメータが、特定の値や正規表現にマッチした場合にルーティングを決定する
Filterの方は、バックエンドに転送する際のリクエストや、バックエンドから戻ってきたレスポンスの内容を変更するのに
使用します。Filterには、GatewayFilterとGlobal Filterがあるようです。
Predicateと同じく、全部のFilterはドキュメントを参照、となりますが、
例えば、以下のようなものがあります。
- バックエンドに転送する際に、リクエストヘッダー、リクエストパラメーターを追加する
- バックエンドからのレスポンスをクライアントに戻す際に、レスポンスヘッダーを追加する
- 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を使います。
パスの先頭に「/express」か「/springboot」を加えることで見分けるようにして、ルーティングします。
バックエンドに転送する時には、このパスが不要になるのでこれを削除します。SetPath GatewayFilterを使います。
設定ファイルは、このようになります。
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.
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で定義したりすることも可能になります。
その他、サンプルはこちら。
Hystrix GatewayFilterを使う
ここからは、ちょっと毛色を変えていきましょう。
Hystrix Circuit Breakerを使った、GatewayFilterを適用してみます。
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を使った場合の話になります。
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が作成されています。
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