《深入理解 Spring Cloud 与微服务构建》第十一章 服务网关

文章目录

  • 《深入理解 Spring Cloud 与微服务构建》第十一章 服务网关
  • 一、服务网关简介
  • 二、服务网关的实现原理
  • 三、断言工厂
    • 1.After 路由断言工厂
    • 2.Header 断言工厂
    • 3.Cookie 路由断言工厂
    • 4.Host 路由断言工厂
    • 5.Method 路由断言工厂
    • 6.Path 路由断言工厂
    • 7.Query 路由断言工厂
  • 四、过滤器
    • 1.过滤器的作用
    • 2.过滤器的生命周期
    • 3.网关过滤器
    • 4.全局过滤器
  • 五、限流
    • 1.常见的限流算法
    • 2.服务网关的限流
  • 六、服务化
    • 1.工程介绍
    • 2.service-gateway 工程详细介绍

一、服务网关简介

服务网关(Spring Cloud Gateway)是 Spring Cloud 官方推出的第二代网关框架,用于替代第一代网关 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关的基本功能。服务网关建立在 Spring Framework 5 之上,使用非阻塞模式,并且支持长连接 Websocket。Netflix Zuul 是基于 Servlet 的,采用 HttpClient 进行请求转发,使用阻塞模式。在性能上服务网关优于 Netflix Zuul,并且服务网关几乎实现了 Netflix Zuul 的全部功能。在使用和功能上,用服务网关替换掉 Netflix Zuul 的成本上是非常低的,几乎可以实现无缝切换

服务网关作为整个分布式系统的流量入口,有着举足轻重的作用:

  • 协议转换,路由转发
  • 流量聚合,对流量进行监控,日志输出
  • 作为整个系统的前端工程,对流量进行控制,有限流的作用
  • 作为系统的前端边界,外部流量只能通过网关才能访问系统
  • 可以在网关层做权限判断
  • 可以在网关层做缓存

二、服务网关的实现原理

Spring Cloud 的第一代网关 Netflix Zuul 有两大核心组件,分别为路由(Router)和过滤器(Filter)。和 Netflix Zuul 一样,服务网关的核心组件也有路由和过滤器,不同之处在于多了一个断言(Predicate),用来判断请求到底交给哪一个 Gateway Web Handler 处理。当客户端向服务网关发出请求时,首先将请求交给 Gateway Handler Mapping 处理,如果请求与路由匹配(这时就会用到断言),则将其发送到相应的 Gateway Web Handler 处理。Gateway Web Handler 处理请求时会经过一系列的过滤器链

三、断言工厂

断言(Predicate)来自于 Java 8 的接口。该接口接受一个输入参数,返回一个布尔值结果,包含多种默认方法将断言组合成其它复杂的逻辑(比如:与、或、非)

当一个请求到来时,需要首先将其交给断言工厂去处理。根据配置的断言规则进行,如果匹配成功,则进行下一步处理;如果没有匹配成功,则返回错误信息。服务网关内置了许多断言工厂(Predicate Factory),能够满足大部分的业务场景,当然用户也可以自己实现断言工厂

1.After 路由断言工厂

After 路由断言工厂可配置一个时间,只有请求的时间在配置时间之后,才交给路由去处理;否则报错,不通过路由

新建一个 Spring Boot 工程,在工程的 pom 文件引入服务网关的起步依赖 spring-cloud-starter-gateway。该工程使用的 Spring Cloud 版本为 Greenwich,Spring Boot 版本为 2.1.0,代码如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>Predicate</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.0.RELEASE</version></parent><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Greenwich.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency></dependencies></project>

在工程的配置文件 application.yml 中添加以下配置:

server:port: 8081spring:profiles:active: after_route
---
spring:cloud:#将此服务设置为网关gateway:routes:#路由名称- id: after_route#跳转路由uri: http://httpbin.org:80#断言,设置拦截条件predicates:- After=2017-01-20T17:42:47.789-07:00[America/Denver]profiles: after_route

在上述配置文件中,配置服务的端口为 8081,配置 spring.profiles.active:after_route 制定了程序的 Spring 启动文件为 after_route 文件。在 application.yml 中再建一个配置文件,语法是 3 个横线,在此配置文件中通过 spring.profiles 来配置文件名,这里与 spring.profiles.active 一致。然后开始服务网关的相关配置,id 标签配置的是路由的 ID,每个路由都需要一个唯一的 ID,uri 配置的是将请求路由转发的地址,本案例请求全部路由到 http://httpbin.org:80 这个 Url

配置 predicates:After=2017-01-20T17:42:47.789-07:00[America/Denver] 会被解析成 PredicateDefinition 对象(name=After,args=2017-01-20T17:42:47.789-07:00[America/Denver])。这里需要注意,在 predicates 的 After 配置中,遵循 “契约大于配置” 的规则,它实际被 AfterRoutePredicateFactory 这个类处理,这里的 “After” 制定了它的 Gateway Web Handler 类为 After 路由断言工厂,同理,其它类型的断言也遵循这个规则

当请求的时间在这个配置的时间之后,请求会被路由转发到 http://httpbin.org:80

启动工程,在浏览器中访问 http://localhost:8081/get,会显示 http://httpbin.org:80/get 返回结果,此时网关路由到了配置的 Url。如果我们将配置的时间设置到当前时间之后,浏览器会显示错误信息 404,此时请求没有路由到配置的 Url

与时间相关的断言工厂还有 Before 路由断言工厂和 Between 路由断言工厂

2.Header 断言工厂

Header 路由断言工厂需要 2 个参数,分别是 Header 的键和 Header 的值,Header 的值可以是一个正则表达式。当此请求头匹配了断言的 Header 的键和 Header 的值时,断言通过,进入路由的逻辑中去;否则,返回错误信息

在工程的配置文件 application 中添加以下配置:

---
spring:cloud:gateway:routes:- id: header_routeuri: http://httpbin.org:80predicates:- Header=X-Request-Id, \d+profiles: header_route

修改 spring.profiles:active 为 header_route:

spring:profiles:active: header_route

该配置指定了断言为 Header 断言工厂,请求需要传 Header 的键为 Request-Id 才能匹配该断言

重新启动工程,使用 curl 执行以下命令:

curl --header "X-Request-Id:1" localhost:8081/get

命令执行成功后会得到正确的返回结果。如果在请求中没有带上 X-Request-Id 的键,或者 Header 的值不为数字时,路由请求不会被正确执行,并报 404 错误信息

3.Cookie 路由断言工厂

Cookie 路由断言工厂需要 2 个参数,分别是 Cookie 名和 Cookie 值。Cookie 值可以为正则表达式。当请求头带有 Cookie 信息,并且请求的 Cookie 和断言配置的 Cookie 相匹配时,请求能够被正确路由,否则报 404 错误信息

在工程的配置文件 application.yml 中添加以下配置:

---
spring:cloud:gateway:routes:- id: cookie_routeuri: http://httpbin.org:80predicates:- Cookie=name, sisyphusprofiles: cookie_route

修改 spring.profiles:active 为 cookie_route

该配置指定了断言为 Cookie 断言工厂,请求需要 Cookie 名为 name,Cookie 值为 sisyphus,才能匹配该断言

重新启动工程,待启动成功后,使用 curl 执行以下命令:

curl --header "Cookie:name=sisyphus" localhost:8081/get

使用 curl 命令进行请求,在请求中带上 Cookie,并且和断言配置的 Cookie 相匹配,会返回正确结果,否则请求会报 404 错误

4.Host 路由断言工厂

Host 路由断言工厂需要一个参数——Hostname,它它可以进行模糊匹配。这个参数会匹配请求头中的 Host 的值,如果匹配成功,则请求正确转发;否则,报 404 错误信息

在工程的配置文件 application.yml 中添加配置并修改 spring.profiles.active:host_route:

---
spring:cloud:gateway:routes:- id: host_routeuri: http://httpbin.org:80predicates:- Host=**.sisyphus.comprofiles: host_route

在上面的配置中,请求头中含有 Host 后缀为 sisyphus.com 的请求都会被路由转发到配置的 Url。启动工程,执行以下的 curl 命令,会返回正确的请求结果

curl --header "Host:www.sisyphus.com" localhost:8081/get

5.Method 路由断言工厂

Method 路由断言工厂即方法路由断言工厂,只允许配置请求类型的请求路由通过。该路由断言工厂需要一个参数,即请求的类型,比如 GET、POST、PUT 和 DELETED 等。在工程的配置文件 application.yml 中添加配置并修改 spring.profiles.active:method_route:

---
spring:cloud:gateway:routes:- id: method_routeuri: http://httpbin.org:80predicates:- Method-GETprofiles: method_route

在上面的配置中,所有 GET 类型的请求都会路由转发到配置的 Url。使用以下的 curl 命令模拟 GET 类型的请求,会得到正确的返回结果

curl localhost:8081/get

使用以下的 curl 命令模拟 POST 请求,则会返回 404 错误信息

curl -XPOST localhost:8081/get

6.Path 路由断言工厂

Path 路由断言工厂即路径路由断言工厂,当请求的路径和配置的请求路径相匹配时,则路由通过。该断言工厂需要配置一个参数——应用匹配路径,可以是一个 spel 表达式

在工程的配置文件 application.yml 中添加配置并修改 spring.profiles.active:path_route:

---
spring:cloud:gateway:routes:- id: path_routeuri: https://sisyphus.blog.csdn.netpredicates:- Path=/article/details/{segment}profiles: path_method

在上面的配置中,所有满足 /article/details/{segment} 路径的请求都会和配置的路径相匹配,并被路由。比如路径为 /article/details/121428288的请求,将会命中匹配,并成功转发

使用 curl 命令模拟一个请求,命令如下,执行后会返回正确的请求结果

curl localhost:8081/article/details/121428288

7.Query 路由断言工厂

Query 路由断言工厂,即请求参数断言工厂,当请求携带的参数和配置的参数匹配时,路由被正确转发;否则,报 404 错误信息。该路由断言工厂需要配置 2 个参数,分别是参数名和参数值。其中,参数值可以是正则表达式

在工程的配置文件 application.yml 中添加配置并修改 spring.profiles.active:query_route:

---
spring:cloud:gateway:routes:- id: query_routeuri: http://httpbin.orgpredicates:- Query=foo, ba.profiles: query_route

在上述配置文件中,如果请求参数有 foo,并且 foo 参数的值与 ba.相匹配,则请求命中路由。比如一个请求中含有参数名为 foo,值为 bar,则能够被正确路由转发

使用以下的 curl 命令模拟请求,能够返回正确的请求信息:

curl localhost:8081/get?foo=bar

Query 路由断言工厂也可以只填一个参数,这时只匹配参数名,即请求的参数中含有配置的参数,则命中路由

在工程的配置文件 application.yml 中添加配置并修改 spring.profiles.active:query_route2:

---
spring:cloud:gateway:routes:- id: query_routeuri: http://httpbin.org:80/getpredicates:- Query=fooprofiles: query_route2

比如在以下的配置中,请求参数中含有参数名为 foo 的参数,将会被请求转发到 http://httpbin.org:80 的 Uri

四、过滤器

断言决定了请求会被路由到配置的 Router 中,在断言之后,请求会进入过滤器链(Filter Chain)的逻辑中。在路由处理之前,需要经过 “pre” 类型的过滤器处理,处理返回响应后,可以由 “post” 类型的过滤器处理

1.过滤器的作用

过滤器过滤器有着非常重要的作用,在 “pre” 类型的过滤器可以实现参数校验、权限校验、流量监控、日志输出、协议转换等功能,在 “post” 类型的过滤器中可以做响应内容、响应头的修改、日志输出、流量监控等功能。要弄清楚为什么需要网关这一层,就不得不提到过滤器的作用了

当我们有很多个服务时,客户端在请求各个服务的 API 时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。对于这样的重复工作,有没有办法做到更好呢?答案是肯定的。在微服务的上一层加一个全局的权限控制、限流、日志输出的 API 网关服务,然后将请求转发到具体的业务服务层。这个 API 网关服务起到服务边界的作用,外界的请求访问系统,必须先通过网关层

2.过滤器的生命周期

服务网关与 Zuul 类似,有 “pre” 和 “post” 两种方式的过滤器。客户端的请求先经过 “pre” 类型的过滤器,然后将请求转发到具体的业务服务,收到业务服务的响应之后,再经过 “post” 类型的过滤器处理,最后返回响应到客户端

与 Zuul 不同的是,过滤器除了分为 “pre” 和 “post” 两种方式外,在服务网关中,从作用范围可将过滤器分为另外两种,一种是针对单个路由的网关过滤器(Gateway Filter),它在配置文件中的写法与断言类似;另一种是针对所有路由的全局网关过滤器(Global Gateway Filter)

3.网关过滤器

网关过滤器允许以某种方式修改传入的 HTTP 请求报文或传出的 HTTP 响应报文。过滤器可以限定作用在某些特定请求路径上。服务网关包含许多内置的网关过滤器工厂(Gateway Filter Factory)

网关过滤器工厂同断言工厂类似,都是在配置文件 application.yml 中配置,遵循 “约定大于配置” 的规则,只需要在配置文件中配置网关过滤器工厂的类名的前缀,而不需要写全类名。在配置文件中配置的网关过滤器工厂会由它对应的过滤器工厂类处理

AddRequestHeader 过滤器工厂

创建工程,在工厂的 pom 文件引入相关的依赖,包括 Spring Boot 的起步依赖,版本为 2.1.0;Spring Cloud 的依赖版本为 Greenwich,Gateway 的起步依赖为 spring-cloud-starter-gateway

在工程的配置文件 application.yml 中添加以下配置:

server:port: 8081spring:profiles:active: add_request_header_route---
spring:cloud:gateway:routes:- id: add_request_header_routeuri: http://httpbin.orgfilters:- AddRequestHeader=X-Request-Foo, Barpredicates:- After=2017-01-20T17:42:47.789-07:00[America/Denver]profiles: add_request_header_route

在上述的配置中,工程的启动端口为 8081,配置文件为 add_request_header_route,在 add_request_header_route 配置文件中,配置了路由的 ID 为 add_request_header_route,路由地址为 http://httpbin.org:80。该路由有一个 After 断言工厂类配置,有一个过滤器为 AddRequestHeaderGatewayFilterFactory(约定写为 AddRequestHeader),AddRequestHeader 过滤器工程会在请求头加上一对请求头,名称为 X-Request-Foo,值为 Bar。其源码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.cloud.gateway.filter.factory;import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory.NameValueConfig;
import org.springframework.http.server.reactive.ServerHttpRequest;public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {public AddRequestHeaderGatewayFilterFactory() {}public GatewayFilter apply(NameValueConfig config) {return (exchange, chain) -> {ServerHttpRequest request = exchange.getRequest().mutate().header(config.getName(), config.getValue()).build();return chain.filter(exchange.mutate().request(request).build());};}
}

由上述代码可知,这里根据旧的 ServerHttpRequest 创建新的 ServerHttpRequest,在新的 ServerHttpRequest 加了一个请求头,然后根据 ServerHttpRequest 创建了一个新的 ServerWebExchange,提交过滤器链继续过滤

工程启动完成后,通过如下的 curl 命令来模拟请求:

curl localhost:8081/get

最终显示从 http://httpbin.org:80/get 得到了请求,响应如下:

从上面的响应可知,确实在请求头中加入了 X-Request-Foo 请求头,这就说明在配置文件 application.yml 中配置的添加请求头过滤器工厂生效了

RewritePath 过滤器工厂

Nginx 的强大功能之一就是重写路径,服务网关默认也提供了这样的功能,而 Zuul 没有这个功能。在工程的配置文件 application.yml 中添加配置并修改 spring.profiles.active:rewritepath_route:

---
spring:cloud:gateway:routes:- id: rewritepath_routeuri: https://blog.csdn.netpredicates:- Path=/foo/**filters:- RewritePath=/foo/(?<segment>.*), /$\{segment}profiles: rewritepath_route

在上述配置中,所有以 /foo/** 开始的路径都会命中配置的路由,并执行过滤器的逻辑。在本案例中配置了 RewritePath 过滤器工厂,此工厂将 /foo(?<segment>.*) 重写为 {segment},然后转发到 https://blog.csdn.net。比如在网页上请求 localhost:8081/foo/sisyphus/goo,页面显示 404 错误,就是因为不存在 https://blog.csdn.net/fsisyphus/goo 这个页面

自定义过滤器

服务网关内置了 19 种强大的过滤器工厂,能够满足很多场景的需求。在 Spring Cloud Gateway 种,过滤器需要实现 GatewayFilter 和 Ordered 这两个接口。现在写一个打印请求耗时的过滤器 RequestTimeFilter,代码如下:

package com.sisyphus.filter;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;public class RequestTimeFilter implements GatewayFilter, Ordered{private static final Log log = LogFactory.getLog(GatewayFilter.class);private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());return chain.filter(exchange).then(Mono.fromRunnable(() -> {Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);if (startTime != null){log.info(exchange.getRequest().getURI().getRawPath() + ": " + (System.currentTimeMillis() - startTime) + "ms");}}));}@Overridepublic int getOrder() {return 0;}
}

在上述代码中,getOrder() 方法用来给过滤器设定优先级别,值越大,则优先级越低。另有一个 filter(exchange, chain) 方法,该方法种先记录了请求的开始时间,并保存在 ServerWebExchange 中,此处是一个 “pre” 类型的过滤器。chain.filter(exchange) 的内部类中的 run() 方法相当于 “post” 类型的过滤器,在此处打印了请求所消耗的时间.

然后将该过滤器注册到路由中,代码如下:

package com.sisyphus.config;import com.sisyphus.filter.RequestTimeFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class FilterConfiguration {@Beanpublic RouteLocator customerRouteLocator(RouteLocatorBuilder builder){//@formatter:offreturn builder.routes().route(r -> r.path("/get/**").filters(f -> f.filter(new RequestTimeFilter()).addRequestHeader("X-Response-Default-Foo", "Default-Bar")).uri("http://httpbin.org:80").order(0).id("customer_filter_router")).build();}
}

重启应用,通过如下的 curl 命令模拟请求:

curl localhost:8081/get

在程序的控制台输出以下请求信息的日志,打印出来请求的耗时,证明自定义的过滤器已生效

自定义过滤工厂

上述的是自定义过滤器,下面来看如何自定义过滤器工厂。在实现一个过滤器工厂时,可以在打印时设置在配置文件中是否打印的参数

过滤器工厂的顶级接口是 GatewayFilterFactory,有两个较接近具体实现的抽象类,分别为 AbstractGatewayFilterFactory 和 AbstarctNameValueGatewayFilterFactory。在这两个类中,前者接收一个参数,比如它的实现类 AddRequestHeaderGatewayFilterFactory。现在需要将请求的日志打印出来,要使用到一个参数,这时可以参照 RedirectToGatewayFilterFactory 的写法,代码如下:

package com.sisyphus.filterFactory;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import reactor.core.publisher.Mono;import java.util.Arrays;
import java.util.List;public class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> {private static final Log log = LogFactory.getLog(GatewayFilter.class);private static final String REQUEST_TIME_BEGIN = "requestTimeBegin";private static final String KEY = "withParams";@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList(KEY);}public RequestTimeGatewayFilterFactory(){super(Config.class);}@Overridepublic GatewayFilter apply(Config config) {return (exchange, chain) -> {exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());return chain.filter(exchange).then(Mono.fromRunnable(() -> {Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);if(startTime != null){StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath()).append(": ").append(System.currentTimeMillis() - startTime).append("ms");if (config.isWithParams()){sb.append(" params:").append(exchange.getRequest().getQueryParams());}log.info(sb.toString());}}));};}public static class Config{private boolean withParams;public boolean isWithParams(){return withParams;}public void setWithParams(boolean withParams){this.withParams = withParams;}}
}

在上述代码中,apply(Config config) 方法中创建了一个 GatewayFilter 的匿名类,具体的实现逻辑和之前一样,但额外增加了是否打印请求参数的逻辑,这个逻辑的开关是 config.isWithParams()。静态内部类 Config 是为了接收 Boolean 类型的参数服务的,类的变量名可以随意写,但是要重写 List shortcutFieldOrder() 方法

需要注意的是,在类的构造器中一定要调用父类的构造器把 Config 类型传过去,否则会报 ClassCastException

最后,需要在工程的启动文件 Application 类中向 Spring IoC 容器注册 RequestTimeGatewayFilterFactory 类型的 Bean

@Bean
public RequestTimeGatewayFilterFactory elapsedGatewayFilterFactory(){return new RequestTimeGatewayFilterFactory();
}

然后在工程的配置文件 application.yml 中添加配置并修改 spring.profiles.active:elapse_route:

---
spring:cloud:gateway:routes:- id: elapse_routeuri: http://httpbin.org:80filters:- RequestTime=falsepredicates:- After=2017-01-20T17:42:47.789-07:00[America/Denver]profiles: elapse_route

记得将之前注册到路由上的其它过滤器取消

启动工程,在浏览器上访问 localhost:8081/get?name=sisyphus,可以在控制台上看到日志输出了请求消耗的时间和请求参数

4.全局过滤器

服务网关根据作用范围划分为网关过滤器(GatewayFilter)和全局过滤器(GlobalFilter),二者区别如下:

  • GatewayFilter:需要通过 spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上;或通过 spring.cloud.default-filters 配置在全局中,作用在所有路由上
  • GlobalFilter:不需要在配置文件中配置,作用在所有路由上,最终通过 GatewayFilterAdapter 包装成 GatewayFilterChain 可识别的过滤器。它是将请求业务以及路由的 Url 转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上

服务网关内置的全局过滤器如下:

  • 负载均衡客户端过滤器:LoadBalancerClientFilter
  • Http 客户端相关的过滤器:NettyRouteFilter、NettyWriteResponseFilter
  • Websocket 相关的过滤器:WebsocketRoutingFilter
  • 路径转发过滤器:ForwardPathFilter
  • 转发路由 Url 过滤器:RouteToRequestUrlFilter
  • WebClient 相关过滤器:WebClientHttpRoutingFilter、WebClientWriteResponseFilter

这些内置的全局过滤器能够满足大多数需求,如果遇到定制业务,可以编写满足特定需求的 GlobalFilter。在下面这个自定义的 TokenFilter 中会校验请求中是否包含请求参数 “token”,如果不包含请求参数 “token”,则不转发路由;否则,执行正常的逻辑。代码如下:

package com.sisyphus.globalFilter;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;public class TokenFilter implements GlobalFilter, Ordered {Logger logger = LoggerFactory.getLogger(TokenFilter.class);@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String token = exchange.getRequest().getQueryParams().getFirst("token");if (token == null || token.isEmpty()){logger.info("token is empty...");exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}return chain.filter(exchange);}@Overridepublic int getOrder() {return -100;}
}

上述的 TokenFilter 需要实现 GlobalFilter 接口和 Ordered 接口,这和实现 GatewayFilter 很相似。根据 ServerWebExchange 获取 ServerHttpRequest,然后判断 ServerHttpRequest 中是否含有参数 token,如果没有,则完成请求,终止转发;否则,执行正常逻辑

接着需要将 TokenFilter 以 Bean 的形式注入 Spring IoC 容器中,代码如下:

@Bean
public TokenFilter tokenFilter(){return new TokenFilter();
}

启动工程,使用如下的 curl 命令请求:

curl localhost:8081/get

可以看到请求没有被转发,而是被终止了,并在控制台打印了如下日志:

上述日志显示了请求进入了没有传递 “token” 的逻辑

五、限流

高并发系统中往往需要做限流,一方面是为了防止流量突发使服务器过载,另一方面是为了防止流量攻击

常见的限流方式有 Hystrix 适用线程池隔离,当超过线程池的负载时,走熔断的逻辑;在一般应用服务器中,比如 Tomcat 容器是通过限制它的线程数来控制并发的;也可以通过时间窗口的平均速度来控制流量。常见的限流维度有通过 IP 限流、通过请求的 Url 限流、通过用户访问频次限流

一般限流都发生在网关层,比如 Nginx、Openresty、Kong、Zuul 和服务网关等,也可以在应用层通过 AOP 的方式去做限流

1.常见的限流算法

计数器算法

计数器算法是使用计数器实现的限流算法,实现简单。比如,限流策略为在 1 秒内只允许有 100 个请求通过,算法的实现思路是第一个请求进来时计数为 1,后面每通过一个请求计数加 1.当计数满 100 后,后面的请求全部被拒绝。这种计数器算法非常简单,当流量突发时,它只允许前面的请求通过,一旦计数满了,拒绝所有后续请求,这种现象称为 “突刺现象”

漏桶算法

漏桶算法可以消除 “突刺现象”,其内部有一个容器,类似漏斗,当请求进来时,相当于水倒入漏斗,然后从容器中均匀地取出请求进行处理,处理速率是固定的。不管上面流量多大,都全部装进容器,下面流出的速率始终保持不变。当容器中的请求数装满了,就直接拒绝请求

令牌桶算法

令牌桶算法是对漏桶算法的改进,漏桶算法只能均匀地处理请求。令牌桶算法需要一个容器来存储令牌,令牌以一定的速率均匀地向桶中存放,当超过桶地容量时,桶会丢弃多余的令牌。当一个请求进来时,需要从令牌桶获取令牌,获取令牌成功,则请求通过;如果令牌桶中的令牌消耗完了,则获取令牌失败,拒绝请求

2.服务网关的限流

服务网关中可以配置过滤器,因此可以在 “pre” 类型的过滤器中自行实现上述 3 中限流算法。但限流作为网关最基本的功能,服务网关官方只提供了 RequestRateLimiterGatewayFilterFactory 这个类,使用 Redis 和 lua 脚本实现令牌桶算法进行限流。具体实现逻辑在 RequestTateLimiterGatewayFilterFactory 类中

下面以案例的形式讲解如何在服务网关中使用内置的限流过滤工厂来实现限流的功能

首先在工程的 pom 文件中引入 Gateway 的起步依赖和 Redis 的reactive 依赖,代码如下:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

在工厂的配置文件 application.yml 中添加以下配置:

server:port: 8081spring:cloud:gateway:routes:- id: limit_routeuri: http://httpbin.org:80predicates:- After=2017-01-20T17:42:47.789-07:00[America/Denver]filters:- name:RequestRateLimiterargs:key-resolver: '#{@hostAddKeyResolver}'redis-rate-limiter.replenishRate: 1redis-rate-limiter.burstCapacity: 3application:name: gateway-limiterredis:host: localhostport: 6379database: 0

在上述配置文件,指定应用的端口为 8081,配置 Redis 的连接信息,并配置了 RequestRateLimiter 的限流过滤器,该过滤器需要配置如下 3 个参数:

  • burstCapacity:令牌桶总容量
  • replenishRate:令牌桶每秒的平均填充速率
  • key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式,根据 #{@beanName} 从 Spring 容器中获取 Bean 对象

KeyResolver 需要实现 resolve 方法,比如在根据 Hostname 进行限流时,需要用 hostAddress 去判断。实现了 KeyResolver 之后,需要将这个类的 Bean 注册到 IoC 容器中,具体代码如下:

package com.sisyphus.keyResolver;import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;@Component
public class HostAddrKeyResolver implements KeyResolver {@Overridepublic Mono<String> resolve(ServerWebExchange exchange) {return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());}@Beanpublic HostAddrKeyResolver hostAddrKeyResolver(){return new HostAddrKeyResolver();}
}

可以根据 Url 去限流,这时的 KeyResolver 代码如下:

package com.sisyphus.keyResolver;import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;public class UriKeyResolver implements KeyResolver {@Overridepublic Mono<String> resolve(ServerWebExchange exchange) {return Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));}
}

也可以以用户的维度去限流,这时的 KeyResolver 代码如下:

@BeanKeyResolver userKeyResolver(){return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));}

用 Jmeter 进行压测,配置 10thread 去循环请求 localhost:8081/get,循环间隔时间为 1 秒。从压测的结果看到,有的部分请求通过,而部分请求失败

六、服务化

1.工程介绍

本案例中使用 Spring Boot 的版本为 2.1.0,Spring Cloud 版本为 Greenwich。案例中涉及 3 个工程,分别为注册中心 eureka-server、服务提供者 service-hi 和服务网关 service-gateway,具体如下表所示:

工程名 端口 作用
eureka-server 8761 注册中心、eureka server
service-hi 8762 服务提供者、eureka client
service-gateway 8801 路由网关、eureka client

在这 3 个工程中,service-hi 和 service-gateway 向注册中心 eureka-server 注册。用户的请求首先经过服务 service-gateway,服务 service-gateway 根据请求路径由网关的 Predicate 去断言进入哪一个路由,Router 经过各种过滤器处理后,最后路由到具体的业务服务,比如 service-hi

eureka-server 和 service-hi 这两个工程请参考 Eureka 章节的 eureka-server 和 eureka-client。其中 service-hi 服务对外暴露了一个 RESTFUL 接口 “/hi”

package com.sisyphus.controller;import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("service")
public class HiController {@Value("${server.port}")String port;@GetMapping("/hi")public String hi(String name){return "hi " + name + ", i am from port:" + port;}
}

2.service-gateway 工程详细介绍

在 service-gateway 工程中引入项目所需的依赖,包括 eureka-client 的起步依赖和 gateway 的起步依赖,代码如下:

<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency>
</dependencies>

在工程的配置文件 application.yml 中,配置应用的启动端口为 8081,并配置注册地址和 gateway 等信息,具体如下:

server:port: 8081spring:application:name: sc-gateway-servicecloud:gateway:discovery:locator:enabled: truelower-case-service-id: trueeureka:client:service-url:defaultZone: http://localhost:8761/eureka/

其中,配置 spring.cloud.gateway.discovery.locator.enabled 为 true,表明服务网关开启服务注册和发现的功能,并且服务网关自动根据服务发现为每一个服务创建了一个路由,这个路由将以服务名开头的请求路径转发到对应的服务中。配置 spring.cloud.gateway.discovery.locator.lowerCasServiceId 是将请求路径上的服务名配置为小写(因为在服务注册过程中,向注册中心注册时将服务名转成大写了),比如 /service-hi/* 的请求路径被路由转发到服务名为 service-hi 的服务上

启动所有工程,在浏览器上输入 localhost:8081/service-hi/service/hi?name=sisyphus,网页显示如下信息:

hi sisyphus, i am from port:8762

在上述例子中,向 gateway-service 发送请求时,url 必须带上服务名 service-hi 这个前缀,才能转发到 service-hi 上,转发之前会将 service-hi 去掉

有时根据服务名转发会显得路径太长,或者由于历史原因不能根据服务名去路由,需要自定义路径。那么是否自定义请求路径呢?答案是肯定的,只需要修改工程的配置文件 application.yml,具体配置如下:

spring:application:name: sc-gateway-servicecloud:gateway:discovery:locator:enabled: falselower-case-service-id: trueroutes:- id: service-hiuri: lb://SERVICE-HIpredicates:- Path=/demo/**filters:- StripPrefix=1

在上面的配置中,配置了一个 path 的断言,将以 /demo/** 开头的请求都转发到 uri 为 lb://SERVICE-HI 的地址上,lb://SERVICE-HI 即 service-hi 服务的负载均衡地址,并用 StripPrefix 的过滤器在转发之前将 /demo 去掉。同时将 spring.cloud.gateway.discovery.locator.enabled 改为false,如果不改,那么之前 localhost:8081/service-hi/service/hi?name=sisyphus 这样的请求地址也能正常访问,这时为每个服务创建了两个路由

在浏览器上请求 localhost:8081/demo/service/hi?name=sisyphus,浏览器返回如下的响应,证明请求能够正确转发到 service-hi 服务上

hi sisyphus, i am from port:8762

《深入理解 Spring Cloud 与微服务构建》第十一章 服务网关相关推荐

  1. 《深入理解Spring Cloud与微服务构建》出版啦!

    作者简介 方志朋,毕业于武汉理工大学,CSDN博客专家,专注于微服务.大数据等领域,乐于分享,爱好开源,活跃于各大开源社区.著有<史上最简单的Spring Cloud教程>,累计访问量超过 ...

  2. 《深入理解 Spring Cloud 与微服务构建》第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统

    <深入理解 Spring Cloud 与微服务构建>第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统 文章目录 <深入理解 Spring Cl ...

  3. 《深入理解 Spring Cloud 与微服务构建》第十七章 使用 Spring Cloud OAuth2 保护微服务系统

    <深入理解 Spring Cloud 与微服务构建>第十七章 使用 Spring Cloud OAuth2 保护微服务系统 文章目录 <深入理解 Spring Cloud 与微服务构 ...

  4. 《深入理解 Spring Cloud 与微服务构建》第十六章 Spring Boot Security 详解

    <深入理解 Spring Cloud 与微服务构建>第十六章 Spring Boot Security 详解 文章目录 <深入理解 Spring Cloud 与微服务构建>第十 ...

  5. 《深入理解 Spring Cloud 与微服务构建》第十五章 微服务监控 Spring Boot Admin

    <深入理解 Spring Cloud 与微服务构建>第十五章 微服务监控 Spring Boot Admin 文章目录 <深入理解 Spring Cloud 与微服务构建>第十 ...

  6. 《深入理解 Spring Cloud 与微服务构建》第十四章 服务链路追踪 Spring Cloud Sleuth

    <深入理解 Spring Cloud 与微服务构建>第十四章 服务链路追踪 Spring Cloud Sleuth 文章目录 <深入理解 Spring Cloud 与微服务构建> ...

  7. 《深入理解 Spring Cloud 与微服务构建》第十三章 配置中心 Spring Cloud Config

    <深入理解 Spring Cloud 与微服务构建>第十三章 配置中心 Spring Cloud Config 文章目录 <深入理解 Spring Cloud 与微服务构建>第 ...

  8. 《深入理解 Spring Cloud 与微服务构建》第十二章 服务注册和发现 Consul

    <深入理解 Spring Cloud 与微服务构建>第十二章 服务注册和发现 Consul 文章目录 <深入理解 Spring Cloud 与微服务构建>第十二章 服务注册和发 ...

  9. 《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul

    <深入理解 Spring Cloud 与微服务构建>第十章 路由网关 Spring Cloud Zuul 文章目录 <深入理解 Spring Cloud 与微服务构建>第十章 ...

最新文章

  1. 互联网金融售前心得数据脱敏分析 | PMCAFF微分享
  2. source tree 递归子模块_多模块 Spring Boot 项目
  3. [Done]Spring @Pointcut 切点调用不到(SpringAOP嵌套方法不起作用) 注意事项
  4. java parseexception_Java ParseException类代码示例
  5. python与机械教育初探_Python公开课-机械学习之手写识别
  6. SK海力士宣布业界首次提供24Gb DDR5样品
  7. sed修炼系列(三):sed高级应用之实现窗口滑动技术
  8. Eclipse-阶段1-配置问题解决
  9. 用几何(解析几何)方法求解概率问题
  10. 在布局空间标注的尺寸量不对_CAD解决布局标注尺寸不对问题 及快捷键混乱问题...
  11. python面板数据模型操作步骤_面板数据模型估计一般要做哪些步骤?
  12. 祝大家2019新春快乐
  13. 132 django模版文件的使用
  14. 来了,掏心窝的最重要3条建议
  15. 产品经理就业喜报:沉舟侧畔终迎万木春
  16. macOS修复系统默认文件夹显示为英文的问题
  17. Trove系列(七)——Trove的Mysql的复制功能介绍
  18. Yii:zii.widgets.CMenu使用方法
  19. matlab画线的形状颜色
  20. 5月10日12点,看雪.深信服2021 KCTF春季赛正式开赛!

热门文章

  1. python 字节码操作_从操作码和参数列表创建Python字节码?
  2. app服务器不运行了,springmvc app URL在本地运行,但不在服务器上运行
  3. linux c 网络事件 通知,深入理解Linux网络技术内幕—通知链
  4. mysql触发器的基本操作_MySQL基本操作-触发器
  5. 解决:无法将文件“obj\x86\Debug\Windows123.exe”复制到“bin\Debug\Windows123.exe”。
  6. 思卡乐科技发布SR3系列RFID产品
  7. HTML5为输入框添加语音输入功能
  8. 【bzoj1036】 ZJOI2008—树的统计Count
  9. telegram定时消息_ActiveMQ(18):Message之延迟和定时消息投递
  10. python经纬度获取县名_利用 Python 批量获取县镇运输距离