これは、なにをしたくて書いたもの?
- Spring Cloud Gatewayという、API Gatewayを構築できるライブラリがあるらしい
- けっこう面白そうな機能を持っている雰囲気なので、1度触っておこうかと
というわけで、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