如果使用Zuul作为网关,Spring Cloud Zuul 访问后端服务时,服务发现的默认路由是:http://zuul_host:zuul_post/微服务在Eureka上的/servicesId/**
,Spring Cloud Gateway 在不同的注册中心下的基于服务发现的路由规则如下所示:
注册中心 | 描述 |
---|---|
Eureka | 通过网关转发服务调用,访问网关URL是http://Gateway_HOST:Gateway_PORT/大些ServiceId/*,服务名默认必须大写,否则会抛404错误 ,如果服务名要用小写,可在属性配置文件中添加spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true 配置解决 |
Zookeeper | 通过网关转发服务调用,服务名默认小写,无需特殊处理 |
Consul | 通过网关转发服务调用,服务名默认小写,无需特殊处理 |
通过Eureka作为注册中心实现Gateway服务发现路由规则。
包 | 工程 | 端口 | 描述 |
---|---|---|---|
cloud-gty-eureka-case | 父包 | ||
case-consumer | 8000 | 服务消费者 | |
case-eureka | 8761 | Eureka注册中心 | |
case-gateway | 9000 | 基于Spring Cloud Gateway的网关server | |
case-provider | 8001 | 服务提供者 |
访问地址:localhost:9000/case-consumer/hello/frank
浏览器返回:
Hello ' frank
Thu Jan 24 16:35:58 CST 2019
application.yml
).....
locator:
# 是否与服务发现组件进行结合,通过serviceId转发到具体的服务实例。默认为false,为true代表开启基于服务发现的路由规则。
enabled: true
# 配置之后访问时无需大写
lower-case-service-id: true
......
程序启动时将服务都注册到EurekaServer中,Gateway作为入口,访问consumer,consumer通过feign向eureka-server发现对应的provider
Spring Cloud Gateway Filter从接口实现上分为两种:Gateway Filter
和Global Filter
。
Gateway Filter
Gateway Filter通过复制Web Filter实现,是一个Filter过滤器,可以对访问的URL过滤,进行横切处理(切面处理),应用场景:超时、安全等。
Global Filter
Spring Cloud Gateway 定义了Global Filter接口,用户可以自定义实现自己的Global Filter。Global Filter是一个全局的Filter,作用于所有路由。
应用场景:
全局系统统计,例如请求服务、请求时长、请求过滤等
应用在有针对性服务(例如用户服务、订单服务、商品服务),单业务自定义过滤时
public class CustomGatewayFilter implements GatewayFilter, Ordered {
private static final Log log = LogFactory.getLog(GatewayFilter.class);
private static final String COUNT_Start_TIME = "countStartTime";
/**
* 对路由转发的耗时进行统计
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(COUNT_Start_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(COUNT_Start_TIME);
Long endTime=(System.currentTimeMillis() - startTime);
if (startTime != null) {
log.info(exchange.getRequest().getURI().getRawPath() + ": " + endTime + "ms");
}
})
);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
Gateway Filter配置到路由断言
)@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/test")
.filters(f -> f.filter(new CustomGatewayFilter()))
.uri("http://localhost:8001/customFilter?name=xujin")
.order(0)
.id("custom_filter")
)
.build();
}
2019-01-25 16:01:17.274 INFO 32023 --- [ctor-http-nio-5] o.s.cloud.gateway.filter.GatewayFilter : /test: 5633ms
定义一个全局过滤器
,对请求到网关的URL进行权限校验,对请求到网关的URL进行权限校验,判断请求的URL是否是合法请求。例子 AuthSignatureFilter.java 中全局过滤器处理逻辑是通过从Gateway上下文ServerWebExchange对象中获取authToken对应的值进行判Null处理,实际投产时要按实际生产环境逻辑编写。
@Component
public class AuthSignatureFilter implements GlobalFilter, Ordered {
/**
* 拦截请求,获取authToken,并校验
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("authToken");
if (null==token || token.isEmpty()) {
//当请求不携带Token或者token为空时,直接设置请求状态码为401,返回
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -400;
}
}
包 | 工程 | 端口 | 描述 |
---|---|---|---|
cloud-gty-gateway-global-filter | 父包 | ||
filter | 9000 | 过滤器程序,代码在package com.sc.gty.gg.filter 下 |
|
filter-provider | 8001 | 路由断言成功匹配后跳转的项目 |
WeightRoutePredicateFactory 是一个路由断言工厂,下面通过使用WeightRoutePredicateFactory对URL进行权重路由。
权重最常见的情况就如下图所示,在做金丝雀灰度测试时,旧系统接收95%的流量,新系统接收5%的流量,需要通过网关动态实时推送路由权重信息。
包 | 工程 | 端口 | 描述 |
---|---|---|---|
cloud-gty-wcmp | 父包 | 权重多路径路由 | |
gateway | 8080 | 配置权重路由的应用程序 | |
provider | 8081 | 路由到的程序,为了演示只有一个,可以设置多端口或多应用体现效果 |
@SpringBootApplication
public class WcmpGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(WcmpGatewayApplication.class, args);
}
}
server:
port: 8080
spring:
application:
name: gty_wcmp
cloud:
gateway:
routes:
- id: service1_v1
# 这里可以配置为其它要路由的地址
uri: http://localhost:8081/v1
predicates:
- Path=/test
# 设置权重 为95%
- Weight=service1, 95
- id: service1_v2
# 这里可以配置为其它要路由的地址
uri: http://localhost:8081/v2
predicates:
- Path=/test
# 设置权重 为5%
- Weight=service1, 5
logging:
level:
org.springframework.cloud.gateway: TRACE
org.springframework.http.server.reactive: DEBUG
org.springframework.web.reactive: DEBUG
reactor.ipc.netty: DEBUG
@RestController
public class ServiceController {
@GetMapping(value = "/v1")
public String v1() {
return "v1";
}
@GetMapping(value = "/v2")
public String v2() {
return "v2";
}
}
Spring Cloud Gateway还可以将https证书配置在网关中
,以前我们都是配置在Nginx中,如果我们要用网关来作为所有API和程序的入口的话,便可使用这种技术来实现此目的
。
包 | 工程 | 端口 | 描述 |
---|---|---|---|
cloud-gty-https | 父包 | gateway https协议实例 | |
cloud-gty-eureka | 8761 | 注册中心 | |
cloud-gty-gateway-https | 8080 | 含https证书的网关,需要用https访问。 |
|
cloud-gty-controller-sourece | 8071 | 目标客户端程序 | |
cloud-gty-controller-ectype | 8072 | source的副本,用于负载均衡 |
Java keytool生成https证书
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
<spring-cloud.version>Finchley.RELEASEspring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
<version>2.0.0.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-expressionartifactId>
<version>4.3.17.RELEASEversion>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
<repositories>
<repository>
<id>spring-milestonesid>
<name>Spring Milestonesname>
<url>https://repo.spring.io/milestoneurl>
<snapshots>
<enabled>falseenabled>
snapshots>
repository>
repositories>
spring:
application:
name: gateway-https
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
server:
ssl:
# 在生成证书时键入的别名
key-alias: certificatekey
enabled: true
# 证书密码,我设置的相同的
key-password: 12979613
# 证书,与application.yml统计目录下
key-store: classpath:shfqkeystore.jks
key-store-type: JKS
key-store-provider: SUN
# 证书存储密码,我设置的相同的
key-store-password: 12979613
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
logging:
level:
org.springframework.cloud.gateway: TRACE
org.springframework.http.server.reactive: DEBUG
org.springframework.web.reactive: DEBUG
reactor.ipc.netty: DEBUG
【请注意阅读yml的注释】
常规Eureka-Server,将Gateway与Controller注册到Eureka,这样通过Gateway端口即可访问到Controller,详细请查看代码。
这两个controller是除端口不同以外,其它都相同,都注册到Eureka-Server当中。
路由到的Controller并不是https,https转发调用http了,所以会出现此问题
,详细代码我就不粘贴来,可以把源码中的两种解决方案Filter,Gateway Filter删除,再访问即可体现
。
URI uri = exchange.getRequest().getURI();
String overrideScheme = null;
if (schemePrefix != null) {
//这里获取访问策略
overrideScheme = url.getScheme();
}
URI requestUrl = this.loadBalancer.reconstructURI(new LoadBalancerClientFilter.DelegatingServiceInstance(instance, overrideScheme), uri);
//直接使用策略
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
/**
* 在LoadBalancerClientFilter执行之后将Https修改为http
*/
@Component
public class HttpSchemeFilter implements GlobalFilter, Ordered {
private static final int HTTPS_TO_HTTP_FILTER_ORDER = 10101;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Object uriObj = exchange.getAttributes().get(GATEWAY_REQUEST_URL_ATTR);
if (uriObj != null) {
URI uri = (URI) uriObj;
uri = this.upgradeConnection(uri, "http");
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri);
}
return chain.filter(exchange);
}
private URI upgradeConnection(URI uri, String scheme) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uri).scheme(scheme);
if (uri.getRawQuery() != null) {
// When building the URI, UriComponentsBuilder verify the allowed characters and does not
// support the '+' so we replace it for its equivalent '%20'.
// See issue https://jira.spring.io/browse/SPR-10172
uriComponentsBuilder.replaceQuery(uri.getRawQuery().replace("+", "%20"));
}
return uriComponentsBuilder.build(true).toUri();
}
/**
* 由于LoadBalancerClientFilter的order是10100,
* 所以设置HttpSchemeFilter的的order是10101,
* 在LoadBalancerClientFilter之后将https修改为http
* @return
*/
@Override
public int getOrder() {
return HTTPS_TO_HTTP_FILTER_ORDER;
}
}
/**
* 在LoadBalancerClientFilter执行之前将Https修改为Http
* https://github.com/spring-cloud/spring-cloud-gateway/issues/378
*/
@Component
public class HttpsToHttpFilter implements GlobalFilter, Ordered {
private static final int HTTPS_TO_HTTP_FILTER_ORDER = 10099;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI originalUri = exchange.getRequest().getURI();
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String forwardedUri = request.getURI().toString();
if (forwardedUri != null && forwardedUri.startsWith("https")) {
try {
URI mutatedUri = new URI("http",
originalUri.getUserInfo(),
originalUri.getHost(),
originalUri.getPort(),
originalUri.getPath(),
originalUri.getQuery(),
originalUri.getFragment());
mutate.uri(mutatedUri);
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
ServerHttpRequest build = mutate.build();
return chain.filter(exchange.mutate().request(build).build());
}
/**
* 由于LoadBalancerClientFilter的order是10100,
* 要在LoadBalancerClientFilter执行之前将Https修改为Http,需要设置
* order为10099
* @return
*/
@Override
public int getOrder() {
return HTTPS_TO_HTTP_FILTER_ORDER;
}
}
Swagger可视化API测试工具。
我用paw和postman,所以这里就先空着,未来补全。
名称 | 描述 |
---|---|
缓存 | 提升系统访问速度和增大系统处理容量,解决高并发流量所带来的缺陷。 |
降级 | 当服务出现问题或者影响到核心流程时,需要暂时将其屏蔽掉,待高峰过去之后或者问题解决后再打。 |
限流 | 应用在不便于使用缓存和降级技术时,如秒杀 ,抢购 ,评论 ,下单 ,频繁复杂查询 ,等稀缺资源时 |
兜底数据
或默认数据
)常见的限流场景如天猫双11,双12高并发场景
Spring Cloud Gateway实现限流只需实现一个过滤器Filter即可。
【Google Guava】中的RateLimiter、Bucket4j、RateLimitJ都是基于令牌桶算法实现的限流工具。
包 | 包 | 工程 | 端口 | 描述 |
---|---|---|---|---|
cloud-gty-limiting | gateway 限流实例 | |||
cloud-gty-custom-filter-limiting | 自定义过滤器实现限流(令牌桶) | |||
custom-filter | 8080 | 自定义过滤器实现限流实现 |
||
cloud-gty-redis-lua-limiting | 通过内置过滤器实现限流(令牌桶) | |||
redis-lua-request-rate | 8081 | 内置过滤器(redis + lua),通过yml配置 |
||
cloud-gty-cpu-limiting | 判断CPU使用情况,限流 | |||
cpu-limiting | 8082 | 通过spring-boot-starter-actuator获取CPU信息,并设置CPU最大阀值,做到限流 |
||
limiting-provider | 8000 | 用于过滤路由断言转发节点(公用provider ) |
cloud-gty-custom-filter-limiting
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.github.vladimir-bukhtoyarovgroupId>
<artifactId>bucket4j-coreartifactId>
<version>4.0.0version>
dependency>
dependencies>
......
/**
* 通过流式API配置路由规则,当访问/test/rateLimit时,自动转发到http://localhost:8000/hello/rateLimit
* @param builder
* @return
*/
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/test/rateLimit")
.filters(f -> f.filter(new GatewayRateLimitFilterByIp(10,1, Duration.ofSeconds(1))))
.uri("http://localhost:8000/hello/rateLimit")
.id("rateLimit_route")
).build();
}
......
核心过滤器,实现接口GatewayFilter, Ordered,通过过滤器的方式实现令牌桶限流
public class GatewayRateLimitFilterByIp implements GatewayFilter, Ordered {
private final Logger log = LoggerFactory.getLogger(GatewayRateLimitFilterByIp.class);
/**
* 单机网关限流用一个ConcurrentHashMap来存储 bucket,
* 如果是分布式集群限流的话,可以采用 Redis等分布式解决方案
*/
private static final Map<String, Bucket> LOCAL_CACHE = new ConcurrentHashMap<>();
/**
* 桶的最大容量,即能装载 Token 的最大数量
*/
int capacity;
/**
* 每次 Token 补充量
*/
int refillTokens;
/**
*补充 Token 的时间间隔
*/
Duration refillDuration;
public GatewayRateLimitFilterByIp() {
}
public GatewayRateLimitFilterByIp(int capacity, int refillTokens, Duration refillDuration) {
this.capacity = capacity;
this.refillTokens = refillTokens;
this.refillDuration = refillDuration;
}
private Bucket createNewBucket() {
Refill refill = Refill.of(refillTokens, refillDuration);
Bandwidth limit = Bandwidth.classic(capacity, refill);
return Bucket4j.builder().addLimit(limit).build();
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
Bucket bucket = LOCAL_CACHE.computeIfAbsent(ip, k -> createNewBucket());
System.out.println("IP:{} ,令牌桶可用的Token数量:{} " + "ip -" +ip +
"tokens - " + bucket.getAvailableTokens());
if (bucket.tryConsume(1)) {
return chain.filter(exchange);
} else {
//当可用的令牌书为0是,进行限流返回429状态码
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}
}
@Override
public int getOrder() {
return -1000;
}
public static Map<String, Bucket> getLocalCache() {
return LOCAL_CACHE;
}
public int getCapacity() {
return capacity;
}
public void setCapacity(int capacity) {
this.capacity = capacity;
}
public int getRefillTokens() {
return refillTokens;
}
public void setRefillTokens(int refillTokens) {
this.refillTokens = refillTokens;
}
public Duration getRefillDuration() {
return refillDuration;
}
public void setRefillDuration(Duration refillDuration) {
this.refillDuration = refillDuration;
}
}
访问
limiting-provider,custom-filter
,访问地址 http://localhost:8080/test/rateLimit,我通过jmeter压测1秒并发的方式访问的
基于RequestRateLimiterGatewayFilterFactory
做到限流,要结合Redis,运用Lua脚本。
当项目实际投产时,利用网关限流要考虑多方面情况,例如通过CPU使用率来进行限流,Spring Boot Actuator
提供的
Metrics
获取当前CPU的使用情况
,当CPU使用率高于某个阀值就开启限流,反之不开启。
spring:
application:
name: case-gateway-server
cloud:
gateway:
# 设置路由:所有的请求都会路由到http://127.0.0.1:8081/**
routes:
- id: cloud-client-app
uri: http://127.0.0.1:8081
order: 0
predicates:
- Path=/**
所有的请求都会路由到8081端口到API应用上
概念 | 描述 |
---|---|
路由配置 | 配置某请求路径路由到指定的目的地址。 |
路由规则 | 匹配到路由配置之后,再根据路由规则进行转发处理。 |