JAVA 8
spring boot 2.5.2
spring cloud 2020.0.3
---
授人以漁:
最新版本,下載下來,以便查閱。
更多版本的官方文檔:
https://docs.spring.io/spring-cloud/docs/
沒有PDF版本,把網頁保存下來。
前文:spring cloud項目06:網關(Gateway)(1)
本文使用的項目:
主要路徑:前端請求經過 external.gateway 轉發到 adapter.web。在此過程中,會做一些試驗。
external.gateway | 網關服務 | 端口 25001 |
adapter.web | web適配層應用 | 端口 21001 |
data.user | user數據層應用 | 端口 20001 |
eureka.server | Eureka注冊中心 | 端口 10001 |
目錄
使用RequestRateLimiterGatewayFilterFactory限流
前文 中,路由配置中的uri使用的是 http://localhost:21001,硬編碼,而且uri無法配置多個(試驗失敗),實現不了負載均衡(LB,Load Balance),需要改進。
在S.C.微服務系統中,所有服務都可以注冊,那么,網關服務是否可以注冊呢?當然可以!
網關服務注冊之后,即可使用注冊中心的服務信息來訪問 已注冊的服務。
添加依賴包:
<!-- 注冊到注冊中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加配置(同其它微服務):
# Eureka客戶端配置
eureka:
instance.prefer-ip-address: true
lease-renewal-interval-in-seconds: 15
lease-expiration-duration-in-seconds: 30
client:
service-url:
defaultZone: http://localhost:10001/eureka/
registry-fetch-interval-seconds: 20
# 提升日志級別,避免輸出太多注冊相關的正常日志
logging:
level:
com.netflix.discovery.DiscoveryClient: warn
spring:
application:
# 服務名
name: external.gateway
檢查注冊中心:網關服務已注冊成功
檢查 /actuator/gateway/globalfilters端點:來自博客園
和前文對比,多了一個 ReactiveLoadBalancerClientFilter,看起來是做 LB 的。
返回信息
{
"org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@68b9834c": -2147482648,
"org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@7da39774": 10000,
"org.springframework.cloud.gateway.filter.NettyRoutingFilter@7d7cac8": 2147483647,
"org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@306f6f1d": 10150,
"org.springframework.cloud.gateway.filter.ForwardRoutingFilter@441b8382": 2147483647,
"org.lib.external.gateway.filters.TokenGlobalFilter@6f76c2cc": 0,
"org.springframework.cloud.gateway.filter.ForwardPathFilter@1df1ced0": 0,
"org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@5349b246": 2147483646,
"org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@6fc6deb7": -1,
"org.springframework.cloud.gateway.filter.GatewayMetricsFilter@32b0876c": 0,
"org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@367f0121": -2147483648
}
在上面的配置后,還是只能使用之前配置的路由。來自博客園
進一步:添加下面的配置后,可以無需手動添加路由即可訪問注冊中心 所有服務(不建議 生產環境使用)
# spring.cloud.gateway.discovery.locator.enabled=true 默認是 false
# 路由配置
gateway:
discovery:
locator:
enabled: true
lowerCaseServiceId: true
配置后,可以使用下面的鏈接訪問 已注冊的 adapter.web、data.user 服務的端點:
http://localhost:25001/adapter.web/user/get?id=1
http://localhost:25001/data.user/user/get?id=1
注,紅色部分是 小寫了的服務名。
此時,/actuator/gateway/routes 也發生了很大的變化,多了很多路由:來自博客園
響應
[
{
"predicate": "Paths: [/adapter.web/**], match trailing slash: true",
"metadata": {
"jmx.port": "59178",
"management.port": "21001"
},
"route_id": "ReactiveCompositeDiscoveryClient_ADAPTER.WEB",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[RewritePath /adapter.web/?(?<remaining>.*) = '/${remaining}'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]"
],
"uri": "lb://ADAPTER.WEB",
"order": 0
},
{
"predicate": "Paths: [/data.user/**], match trailing slash: true",
"metadata": {
"jmx.port": "59158",
"management.port": "20001"
},
"route_id": "ReactiveCompositeDiscoveryClient_DATA.USER",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[RewritePath /data.user/?(?<remaining>.*) = '/${remaining}'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]"
],
"uri": "lb://DATA.USER",
"order": 0
},
{
"predicate": "Paths: [/external.gateway/**], match trailing slash: true",
"metadata": {
"jmx.port": "53359",
"management.port": "25001"
},
"route_id": "ReactiveCompositeDiscoveryClient_EXTERNAL.GATEWAY",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[RewritePath /external.gateway/?(?<remaining>.*) = '/${remaining}'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]"
],
"uri": "lb://EXTERNAL.GATEWAY",
"order": 0
},
{
"predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
"route_id": "route1",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[AddRequestHeader addHead = 'abc'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]",
"[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]"
],
"uri": "http://localhost:21001",
"order": 0
}
]
甚至通過 其自身(/external.gateway/**) 來訪問——不會死循環嗎?!
還好,spring.cloud.gateway.discovery.locator.* 下還有很多配置(可以在 官文 查到):
負載均衡配置:
開啟3個adapter.web服務,端口分別為:21001、21002、21003。來自博客園
去掉前面的spring.cloud.gateway.discovery.locator.* 的配置。
修改路由中的uri為下面的:lb://adapter.web
- id: route1
# 1)服務
# uri: http://localhost:21001
# 負載均衡訪問服務 adapter.web
uri: lb://adapter.web
訪問 /web/user/get?id=1,檢查 3個adapter.web服務 是否均衡地收到並處理了請求:成功,均衡地處理了請求。
小結,
網關服務化后,可以很方便地實現負載均衡地訪問代理服務。來自博客園
spring.cloud.gateway.discovery.locator.* 的最佳實踐還有待探索,比如,根據前綴只允許訪問服務中的部分請求,這就需要開發不同的斷言、過濾器了吧。
限流,限制進入系統的流量。
限流的作用:1)防止流量突發使服務器過載;2)防止流量攻擊。
常見限流維度:IP限流、請求URL限流、用戶訪問頻次限流。(注:在使用微信公眾平台接口時,還可以限制每個賬號每小時、每天的調用次數等)
限流發生的位置:1)網關層(Nginx、Zuul、S.C.Gateway等),2)應用層。
本文介紹在S.C.Gateway中實現限流。
搜索:常見限流算法——計數器算法、漏桶算法、令牌桶算法
自定義pre類型的過濾器,可以實現需要的限流算法。來自博客園
在S.C.Gateway中,已經提供了一個 RequestRateLimiterGatewayFilterFactory,其使用 Redis和Lua腳本實現令牌桶算法進行限流。
@ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
public class RequestRateLimiterGatewayFilterFactory
extends AbstractGatewayFilterFactory<RequestRateLimiterGatewayFilterFactory.Config> {
// ...
private final RateLimiter defaultRateLimiter;
private final KeyResolver defaultKeyResolver;
// ...
}
Lua腳本位置:
注,官文中的 The RequestRateLimiter GatewayFilter Factory 一節有它詳細的介紹。來自博客園
6.10. The RequestRateLimiter GatewayFilter Factory
The RequestRateLimiter GatewayFilter factory uses a RateLimiter implementation to determine if the
current request is allowed to proceed. If it is not, a status of HTTP 429 - Too Many Requests (by
default) is returned.
This filter takes an optional keyResolver parameter and parameters specific to the rate limiter
(described later in this section).
使用RequestRateLimiterGatewayFilterFactory限流
實現根據遠程主機地址限流。
由於S.C.Gateway基於Netty,因此,需要引入reactive版本的redis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
配置Redis:來自博客園
spring:
# Redis配置-限流使用
redis:
host: mylinux
port: 6379
建立KeyResolver類並注冊到Spring容器:
# HostAddrKeyResolver.java
public class HostAddrKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
# AppConfig.java
@Configuration
public class APPConfig {
/**
* 限流的鍵解析器
* @author ben
* @date 2021-09-13 16:48:26 CST
* @return
*/
@Bean
public HostAddrKeyResolver hostAddrKeyResolver() {
return new HostAddrKeyResolver();
}
}
配置路由使用RequestRateLimiterGatewayFilterFactory:
配置方式和其它的不太一樣,具體需要看看源碼。
配置參數已在下面的注釋中有說明(SpEL真的很重要,使用Spring時鍵值無處不在啊)!
# 過濾器配置
filters:
# 限流過濾器
- name: RequestRateLimiter
args:
# 用於限流的鍵的解析器的Bean對象的名稱——SpEL表達式
# 默認有一個 PrincipalNameKeyResolver類,下面的hostAddrKeyResolver 需要自行實現
key-resolver: '#{@hostAddrKeyResolver}'
# 令牌桶每秒的平均填充速率
redis-rate-limiter.replenishRate: 1
# 令牌桶總量
redis-rate-limiter.burstCapacity: 3
測試限流效果:來自博客園
1)Postman:快速點擊(要足夠快),以此觸發限流機制
測試期間發現響應的狀態為:429 Too Many Requests,此時觸發了限流規則。
2)Apache JMeter:配置多個線程快速訪問
在Redis中,限流的數據是怎么保存的呢?檢查下。來自博客園
redis-cli檢查
# 多了兩個key
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04set1"
2) "request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens"
3) "\xac\xed\x00\x05t\x00\x05test3"
4) "request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp"
5) "\xac\xed\x00\x05t\x00\x05test1"
127.0.0.1:6379>
# 生存期很短
127.0.0.1:6379> ttl request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens
(integer) 4
127.0.0.1:6379> get request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens
"2"
127.0.0.1:6379> get request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp
"1631535157"
127.0.0.1:6379> ttl request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp
(integer) 4
小結,
就這樣,在S.C.Gateway中把 限流 用起來了。
上面的用法很簡單,真實的限流則有各種各樣的規則,比如,服務器彈性部署時,網關怎么彈性更改配置呢?更改配置文件嗎?這個時候就需要編程來實現了。
Gateway中有限流,底層應用是否也要有限流呢?兩者如何互補?
Gateway中的令牌桶限流算法的實現原理是怎樣的?那個Lua腳本是怎么寫的?都需要繼續探索的。
先讀一遍官文才好。
在前面的示例中,網關的配置都是在 配置文件中完成的。
是否可以通過編程來實現路由配置呢?
配置文件配置 和 編程配置,兩種方式的優缺點分別是什么?來自博客園
編程配置路由方式:使用Spring容器中routeLocatorBuilder Bean生成一個RouteLocator Bean即可。
默認下,已經有 routeDefinitionRouteLocator、cachedCompositeRouteLocator 兩個Bean了,是用來做什么的呢?
示例代碼:用各種方式,配置了 3個路由
package org.lib.external.gateway.routes;
import java.util.function.Function;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.Buildable;
import org.springframework.cloud.gateway.route.builder.PredicateSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 應用路由配置(編程方式)
* @author ben
* @date 2021-09-13 20:47:31 CST
*/
@Configuration
public class AppRoutesConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder
.routes()
// 路由1
.route("routeP1", new Function<PredicateSpec, Buildable<Route>>() {
@Override
public Buildable<Route> apply(PredicateSpec t) {
return t.order(3)
.path("/user/**")
.filters(f->f.addResponseHeader("program-header", "routeP1"))
.uri("lb://adapter.web");
}
})
// 路由2
.route("routeP2", r->r.order(2)
.path("/routeP2/**")
.filters(f->f.addResponseHeader("program-header", "routeP2")
.retry(3)
.rewritePath("/routeP2/(?<segment>.*)", "/$\\{segment}"))
.uri("lb://adapter.web")
)
// 路由3
.route(r->r.order(-10)
.path("/routeP3/**")
.filters(f->f.addResponseHeader("program-header", "routeP3")
.rewritePath("/routeP3/(?<segment>.*)", "/$\\{segment}"))
.uri("lb://adapter.web")
)
.build();
}
}
訪問/actuator/gateway/routes:按優先級 從高到低 展示了系統中的路由,其中,第二的route1 是 配置文件中的,看來可以共存。
[
{
"predicate": "Paths: [/routeP3/**], match trailing slash: true",
"route_id": "c48fc06f-380c-405d-abf4-791df2008e37",
"filters": [
"[[RewritePath /routeP3/(?<segment>.*) = '/${segment}'], order = 0]"
],
"uri": "lb://adapter.web",
"order": -10
},
{
"predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
"route_id": "route1",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[AddRequestHeader addHead = 'abc'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]",
"[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]",
"[org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory$$Lambda$978/889546737@2cad0ced, order = 3]"
],
"uri": "lb://adapter.web",
"order": 0
},
{
"predicate": "Paths: [/routeP2/**], match trailing slash: true",
"route_id": "routeP2",
"filters": [
"[[AddRequestHeader program-header = '210913'], order = 0]",
"[[Retry routeId = 'routeP2', retries = 3, series = list[SERVER_ERROR], statuses = list[[empty]], methods = list[GET], exceptions = list[IOException, TimeoutException]], order = 0]",
"[[RewritePath /routeP2/(?<segment>.*) = '/${segment}'], order = 0]"
],
"uri": "lb://adapter.web",
"order": 2
},
{
"predicate": "Paths: [/user/**], match trailing slash: true",
"route_id": "routeP1",
"filters": [
"[[AddRequestHeader program-header = '210913'], order = 0]"
],
"uri": "lb://adapter.web",
"order": 3
}
]
測試使用4個路由訪問 web適配層應用:都能成功獲取數據
http://localhost:25001/user/get?id=1
http://localhost:25001/web/user/get?id=1
http://localhost:25001/routeP2/user/get?id=1
http://localhost:25001/routeP3/user/get?id=1
小結:
編程添加路由,策略有變化,需要重啟服務:確定。來自博客園
配置文件中添加路由。策略有變化,是否不需要重啟服務?更新配置即可?TODO
對了,上面說的配置文件更新,是指存放於外部的配置文件(S.C.Config)更改后,是否可以更新到 正在運行的 網關服務?
哪些路由需要使用 編程添加,哪些通過 配置文件添加?
還是再看看官文吧,最權威的。來自博客園
》》》全文完《《《
1、《深入理解Spring Cloud與微服務構建》
2019年9月第2版,作者:方志朋
2、