spring cloud项目07:网关(Gateway)(2)

JAVA 8

spring boot 2.5.2

spring cloud 2020.0.3

---

 

授人以渔:

1、Spring Cloud PDF版本

最新版本,下载下来,以便查阅。

更多版本的官方文档:

https://docs.spring.io/spring-cloud/docs/

2、Spring Cloud Gateway

没有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

 

目录

1、网关服务化

2、限流

使用RequestRateLimiterGatewayFilterFactory限流

3、编程配置路由

参考文档

 

1、网关服务化

前文 中,路由配置中的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

检查注册中心:网关服务已注册成功

 spring cloud项目07:网关(Gateway)(2)

 

检查 /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.* 下还有很多配置(可以在 官文 查到):

spring cloud项目07:网关(Gateway)(2)

 

负载均衡配置:

开启3个adapter.web服务,端口分别为:21001、21002、21003。来自博客园

spring cloud项目07:网关(Gateway)(2)

去掉前面的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.* 的最佳实践还有待探索,比如,根据前缀只允许访问服务中的部分请求,这就需要开发不同的断言、过滤器了吧。

 

2、限流

限流,限制进入系统的流量。

限流的作用: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脚本位置:

spring cloud项目07:网关(Gateway)(2)

注,官文中的 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,此时触发了限流规则。

spring cloud项目07:网关(Gateway)(2)

2)Apache JMeter:配置多个线程快速访问

spring cloud项目07:网关(Gateway)(2)

 

在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脚本是怎么写的?都需要继续探索的。

先读一遍官文才好。

 

3、编程配置路由

在前面的示例中,网关的配置都是在 配置文件中完成的。

是否可以通过编程来实现路由配置呢?

配置文件配置 和 编程配置,两种方式的优缺点分别是什么?来自博客园

 

编程配置路由方式:使用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、

 

上一篇:nginx+gateway+nacos集群手记


下一篇:go grpc gateway 事例