微服务架构由很多个微小的服务(微服务)组成系统,每个微服务有自己的主机和端口号。如果直接让客户端与各个微服务通信,带来的问题有:

在客户端和各个微服务之间添加微服务网关可以解决如上问题,客户端的请求都先经过微服务网关,客户端就只需要知道网关的请求地址即可,添加网关的优点有:

本文介绍的微服务网关技术是Spring Cloud GateWay。该项目提供了一个用于在 Spring WebFlux 或 Spring WebMVC 之上构建 API 网关的库。 Spring Cloud Gateway 旨在提供一种简单而有效的方法来路由到 API 并为其提供横切关注点,例如:安全性、监控。
本文的操作是在服务熔断保护实践--Sentinal的基础上进行。
jdk1.8
maven3.6.3
mysql8
spring cloud2021.0.8
spring boot2.7.12
idea2022
在父工程下,创建子模块:api_gateway_server
添加依赖
- <dependencies>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-gateway</artifactId>
- </dependency>
- </dependencies>
启动类
- package org.example.gateway;
-
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
-
- @SpringBootApplication
- public class GatewayServerApplication {
- public static void main(String[] args) {
- SpringApplication.run(GatewayServerApplication.class, args);
- }
- }
配置文件application.yml
- server:
- port: 8080
- spring:
- application:
- name: api-gateway-server
- cloud:
- gateway:
- routes:
- - id: product-service
- uri: http://127.0.0.1:9001
- predicates:
- - Path=/product/**
配置的作用:通过网关访问路径符合/product/开头时,网关会将请求转发到uri(http://127.0.0.1:9001)的对应的路径下。例如:访问localhost:8080/product/1,网关会将请求转发到http://127.0.0.1:9001/product/1,其中127.0.0.1与localhost可以互相替换。
启动网关服务报错如下
Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway.
Action:
Please set spring.main.web-application-type=reactive or remove spring-boot-starter-web dependency.
启动报错原因:
SpringCloud Gateway使用的web框架为webflux,和SpringMVC不兼容,所以gateway不需要引入web依赖,否则造成依赖冲突。

解决启动报错:
父工程不全局引入web依赖,需要的子工程单独引入web依赖,不需要则不引入(例如:gateway工程不需要则不引入)
父工程,删除web依赖

在需要的子工程(order-service、order-service-feign_hystrix、product-service)里添加Web依赖
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-webartifactId>
- dependency>
刷新依赖
测试
启动eureka服务、启动product服务(确认product服务的端口号为9001)
浏览器访问
直接访问微服务
http://localhost:9001/product/1
通过网关访问
http://localhost:8080/product/1

可以看到通过网关访问和直接访问微服务效果一样。
Spring Cloud Gateway 的功能很强大,前面只是使用了 Predicates(断言) 进行了简单的条件匹配,其实Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。在 Spring Cloud Gateway 中 Spring 利用Predicate 的特性实现了各种路由匹配规则,如:通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。
路由匹配示例
- spring:
- cloud:
- gateway:
- routes:
- - id: product-service
- uri: https://xxxx.com
- predicates:
- # 路由断言之前匹配
- - Before=xxx
- # 路由断言之后匹配
- - After=xxx
- # 路由断言之间匹配
- - Between=xxx,xxx
- # 路由断言Cookie匹配,此predicate匹配给定名称(chocolate)和正则表达式(ch.p)
- - Cookie=chocolate, ch.p
- # 路由断言Header匹配,header名称匹配X-Request-Id,且正则表达式匹配\d+
- - Header=X-Request-Id, \d+
- # 路由断言匹配Host匹配,匹配下面Host主机列表,**代表可变参数
- - Host=**.somehost.org, **.anotherhost.org
- # 路由断言Method匹配,匹配的是请求的HTTP方法
- - Methos=GET
- # 路由断言访问路径匹配,{segment}为可变参数
- - Path=/foo/{segment},/bar/{segment}
- # 路由断言请求参数匹配,将请求的参数param(baz)进行匹配
- - Query=baz
- # 路由断言请求参数匹配,可以进行regexp正则表达式匹配 (参数包含foo,并且foo的值匹配ba.)
- - Query=foo,ba.
- # 路由断言RemoteAddr匹配,将匹配192.168.1.1~192.168.1.254之间的ip地址,其中24为子网掩码位 数即255.255.255.0
- - RemoteAddr=192.168.1.1/24
之前我们配置路由uri是直接配置了微服务的主机和端口号:uri: http://127.0.0.1:9001。
如果微服务地址有变化,需要修改网关配置uri,耦合度高。
Spring Cloud GateWay支持动态路由:即自动的从注册中心中获取服务列表并访问。
在api_gateway_server服务(网关服务)引入eureka依赖
- <dependency>
- <groupId>org.springframework.cloudgroupId>
- <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
- dependency>
修改网关服务配置文件application.yml
lb代表从注册中心获取服务,同时具备赋值均衡功能,//后面接的是服务名称
- server:
- port: 8080
- spring:
- application:
- name: api-gateway-server
- cloud:
- gateway:
- routes:
- - id: product-service
- uri: lb://service-product
- predicates:
- - Path=/product/**
- # 配置eureka
- eureka:
- client:
- service-url:
- defaultZone: http://localhost:9000/eureka/
- instance:
- prefer-ip-address: true
配置的作用:通过网关访问,如果路径包含/product开头,会转发到uri(lb://service-product) 对应的微服务接口。例如:访问localhost:8080/product/1符合断言路径的条件,网关将请求会转发到service-product微服务,微服务名称映射的主机和端口号从eureka注册中心得到,转发对应的接口为localhost:9001/product/1
测试
重启gateway服务
查看eureka服务

访问正常
http://localhost:8080/product/1

为了让更好识别访问路径将转发到哪个服务,在网关访问路径中带有具有识别作用的特定路径,例如:用带有product-service的路径表示请求将转发到product服务。
完整的访问路径如下
响应错误页面

要访问成功,可以使用过滤器重写请求路径,
application.yml配置如下:
- predicates:
- #- Path=/product/**
- - Path=/product-service/**
- filters:
- - RewritePath=/product-service/(?<segment>),/$\{segment} #路径重写的过滤器,把/product-service/xxx的路径重写为/xxx

重启网关服务
再次访问
http://localhost:8080/product-service/product/1
成功访问到数据
过滤器类型:局部过滤器(GatewayFilter)和全局过滤器(GlobalFilter)。
局部过滤器(GatewayFilter):应用到单个路由或者一个分组的路由上
全局过滤器(GlobalFilter):应用到所有路由上。
部分局部过滤器如下
| 过滤器工厂 | 作用 | 参数 |
|---|---|---|
| AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
| AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
| AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
| DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 |
| Hystrix | 为路由引入Hystrix的断路器保护 | HystrixCommand 的名称 |
| FallbackHeaders | 为fallbackUri的请求头中添加具体的异常信息 | Header的名称 |
| PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
| PreserveHostHeader | 为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 |
| RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
| RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的url |
| RemoveHopByHopHeadersFilter | 为原始请求删除IETF组织规定的一系列Header | 默认就会启用,可以通过配置指定仅删除哪些Header |
| RemoveRequestHeader | 为原始请求删除某个Header | Header名称 |
| RemoveResponseHeader | 为原始响应删除某个Header | Header名称 |
| RewritePath | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则表达式 |
| RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正则表达式,重写后的值 |
| SaveSession | 在转发请求之前,强制执行WebSession::save 操作 | 无 |
| secureHeaders | 为原始响应添加一系列起安全作用的响应头 | 无,支持修改这些安全响应头的值 |
| SetPath | 修改原始的请求路径 | 修改后的路径 |
| SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后的值 |
| SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是数字,也可以是字符串 |
| StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的路径的数量 |
| Retry | 针对不同的响应进行重试 | retries、statuses、methods、series |
| RequestSize | 设置允许接收最大请求包的大 小。如果请求包大小超过设置的值,则返回 413 Payload TooLarge | 请求包大小,单位为字节,默认值为5M |
| ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
| ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
每个过滤器工厂都对应一个实现类,并且这些类的名称必须以GatewayFilterFactory 结尾,这是Spring Cloud Gateway的一个约定,例如 AddRequestHeader 对应的实现类为AddRequestHeaderGatewayFilterFactory。

全局过滤器(GlobalFilter)作用于所有路由。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
GloabalFilter接口源码如下:

编写一个全局过滤器
- package org.example.gateway.filter;
-
- import org.springframework.cloud.gateway.filter.GatewayFilterChain;
- import org.springframework.cloud.gateway.filter.GlobalFilter;
- import org.springframework.core.Ordered;
- import org.springframework.stereotype.Component;
- import org.springframework.web.server.ServerWebExchange;
- import reactor.core.publisher.Mono;
-
- /**
- * 自定义全局过滤器
- */
- @Component
- public class LoginFilter implements GlobalFilter, Ordered {
- /**
- * 过滤器逻辑
- */
- @Override
- public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) { - System.out.println("执行了自定义的全局过滤器");
- return chain.filter(exchange);//放行,继续向下执行
- }
-
- /**
- * 指定过滤器执行顺序
- * 返回值越小,优先级越高
- */
- @Override
- public int getOrder() {
- return 0;
- }
- }
重启网关服务
访问
http://localhost:8080/product-service/product/1
看到数据如下
查看网关服务IDEA控制台的输出,看到执行了自定义的全局过滤器。

说明过滤器正常工作了。
鉴权逻辑:
当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
以后每次请求,客户端都携带认证的token
服务端对token进行解密,判断是否有效
修改LoginFilter的filter方法
- /**
- * 过滤器逻辑
- * 对请求参数中的access-token进行判断
- * 如果存在此参数:代表已经认证成功
- * 如果不存在此参数:代表认证失败
- * ServerWebExchange:相当于请求和响应的上下文(类似于zuul中的RequestContext)
- */
- @Override
- public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) { - System.out.println("执行了自定义的全局过滤器");
- //1.获取请求参数,例如:access-token
- String token = exchange.getRequest().getQueryParams().getFirst("access-token");
- //2.判断是否存在,不存在则认证失败,存在则放行
- if(token == null){
- System.out.println("没有登录");
- exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
- return exchange.getResponse().setComplete();//请求结束
- }
- return chain.filter(exchange);//放行,继续向下执行
- }
重启网关服务
测试
未携带access-token测试
http://localhost:8080/product-service/product/1

IDEA控制台提示没有登录

携带access-token测试
http://localhost:8080/product-service/product/1?access-token=1

可以在网关阶段对服务调用进行限流,达到保护服务的作用。
常见的限流算法有:计数器限流算法、滑动窗口算法、漏桶算法、令牌桶算法。
每种算法原理、优缺点、使用场景也各不一样,具体可参考 常见限流算法
拒绝服务
排队等待
服务降级
Spring Cloud Gateway官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂RequestRateLimiterGatewayFilterFactory 实现。在过滤器工厂中是通过redis和lua脚本结合的方式进行流量控制。
安装好redis
启动redis服务端
启动redis客户端,使用monitor命令监控redis数据变化信息
127.0.0.1:6379> monitor

在api_gateway_server服务中引入redis相关依赖(基于reactive的redis依赖)
-
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-actuatorartifactId>
- dependency>
-
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-data-redis-reactiveartifactId>
- dependency>
修改application.yml,配置redis和网关过滤器
- server:
- port: 8080 #端口
- spring:
- application:
- name: api-gateway-server #服务名称
- redis:
- host: localhost
- port: 6379
- database: 0
- cloud: #配置SpringCloudGateway的路由
- gateway:
- routes:
- - id: product-service
- uri: lb://service-product
- predicates:
- - Path=/product-service/**
- filters:
- - name: RequestRateLimiter
- args:
- # 使用SpEL从bean容器中获取对象
- key-resolver: '#{@pathKeyResolver}'
- # 令牌桶每秒填充平均速率
- redis-rate-limiter.replenishRate: 1
- # 令牌桶的上限
- redis-rate-limiter.burstCapacity: 3
- - RewritePath=/product-service/(?<segment>.*), /$\{segment}
- #eureka注册中心
- eureka:
- client:
- service-url:
- defaultZone: http://localhost:9000/eureka/
- instance:
- prefer-ip-address: true #使用ip地址注册
虽然args相关配置存在报错如下,但是经验证,不影响运行。

在org.example.gateway包下,新建config包,并在config包下编写KeyResolverConfiguration配置
- package org.example.gateway.config;
-
- import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import reactor.core.publisher.Mono;
-
- @Configuration
- public class KeyResolverConfiguration {
- /**
- * 基于请求路径的限流
- */
- @Bean
- public KeyResolver pathKeyResolver() {
- return exchange -> Mono.just(
- exchange.getRequest().getPath().toString()
- );
- }
- }
启动eureka、product、gateway服务
浏览器访问
http://localhost:8080/product-service/product/1?access-token=1

连续多次访问,每秒超过3次访问后,响应如下:

查看redis客户端监控的输出如下:

以上实现了基于请求路径的限流,其他方式限流,参考代码如下:
- package org.example.gateway.config;
-
- import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import reactor.core.publisher.Mono;
-
- @Configuration
- public class KeyResolverConfiguration {
- /**
- * 基于请求路径的限流
- * 方法名称与yml中key-resolver的值对应
- */
- @Bean
- public KeyResolver pathKeyResolver() {
- return exchange -> Mono.just(
- exchange.getRequest().getPath().toString()
- );
- }
-
- // 如果是其他的方式限流,代码如下,同时注意修改yml的key-resolver的值。 注意只能有一个@Bean生效,否则启动出错
-
- /**
- * 基于请求ip地址的限流
- */
- // @Bean
- // public KeyResolver ipKeyResolver() {
- // return exchange -> Mono.just(
- // exchange.getRequest().getHeaders().getFirst("X-Forwarded-For")
- // );
- // }
-
- /**
- * 基于用户的限流(参数)
- */
- // @Bean
- // public KeyResolver userKeyResolver() {
- // return exchange -> Mono.just(
- // exchange.getRequest().getQueryParams().getFirst("user")
- // );
- // }
- }
测试其他的限流,例如:基于用户参数的限流
注释其他Bean代码,让如下Bean生效
- /**
- * 基于请求参数的限流
- * 请求 ?userId=
- * @return
- */
- @Bean
- public KeyResolver userKeyResolver(){
- return exchange -> Mono.just(
- exchange.getRequest().getQueryParams().getFirst("userId")
- );
- }
修改application.yml
key-resolver: '#{@userKeyResolver}'
测试
http://localhost:8080/product-service/product/1?userId=1&access-token=1
访问达不到限流条件时

访问达到限流条件时

在api_gateway_server服务添加如下依赖
-
- <dependency>
- <groupId>com.alibaba.cspgroupId>
- <artifactId>sentinel-spring-cloud-gateway-adapterartifactId>
- dependency>
添加配置类
- package org.example.gateway.config;
-
- import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
- import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
- import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
- import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
- import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
- import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
- import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem;
- import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
- import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
- import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
- import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
- import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
- import org.springframework.beans.factory.ObjectProvider;
- import org.springframework.cloud.gateway.filter.GlobalFilter;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.core.Ordered;
- import org.springframework.core.annotation.Order;
- import org.springframework.core.io.buffer.DataBuffer;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.MediaType;
- import org.springframework.http.codec.ServerCodecConfigurer;
- import org.springframework.http.server.reactive.ServerHttpResponse;
- import org.springframework.web.reactive.function.BodyInserters;
- import org.springframework.web.reactive.function.server.ServerResponse;
- import org.springframework.web.reactive.result.view.ViewResolver;
- import org.springframework.web.server.ServerWebExchange;
- import reactor.core.publisher.Mono;
-
- import javax.annotation.PostConstruct;
- import java.nio.charset.StandardCharsets;
- import java.util.*;
-
- @Configuration
- public class GatewayConfiguration {
-
- private final List
viewResolvers; -
- private final ServerCodecConfigurer serverCodecConfigurer;
-
- public GatewayConfiguration(ObjectProvider
> viewResolversProvider,
- ServerCodecConfigurer serverCodecConfigurer) {
- this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
- this.serverCodecConfigurer = serverCodecConfigurer;
- }
-
- /**
- * 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
- */
- @Bean
- @Order(Ordered.HIGHEST_PRECEDENCE)
- public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
- return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
- }
-
- /**
- * 配置限流过滤器
- */
- @Bean
- @Order(Ordered.HIGHEST_PRECEDENCE)
- public GlobalFilter sentinelGatewayFilter() {
- return new SentinelGatewayFilter();
- }
-
- /**
- * 配置初始化的限流参数
- */
- @PostConstruct
- public void initGatewayRules() {
- Set
rules = new HashSet<>(); - // rules.add(
- // new GatewayFlowRule("order-service") //资源名称
- // .setCount(1) // 限流阈值
- // .setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
- // );
- // rules.add(new GatewayFlowRule("order-service")
- // .setCount(1)
- // .setIntervalSec(1)
- // .setParamItem(new GatewayParamFlowItem()
- // .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM).setFieldName("id")
- // )
- // );
- //限流规则对访问路径匹配product-service开头的路径生效
- rules.add(new GatewayFlowRule("product-service")
- .setCount(1)
- .setIntervalSec(1)
- );
- // rules.add(new GatewayFlowRule("product_api")
- // .setCount(1)
- // .setIntervalSec(1)
- // );
- // rules.add(new GatewayFlowRule("order_api")
- // .setCount(1)
- // .setIntervalSec(1)
- // );
- GatewayRuleManager.loadRules(rules);
- }
-
- // @PostConstruct
- // public void initBlockHandlers() {
- // BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
- // public Mono
handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { - // Map map = new HashMap<>();
- // map.put("code", 001);
- // map.put("message", "对不起,接口限流了");
- // return ServerResponse.status(HttpStatus.OK).
- // contentType(MediaType.APPLICATION_JSON_UTF8).
- // body(BodyInserters.fromObject(map));
- // }
- // };
- // GatewayCallbackManager.setBlockHandler(blockRequestHandler);
- // }
-
- // @PostConstruct
- // private void initCustomizedApis() {
- // Set
definitions = new HashSet<>(); - // ApiDefinition api1 = new ApiDefinition("product_api")
- // .setPredicateItems(new HashSet
() {{ - // add(new ApiPathPredicateItem().setPattern("/product-service/product/**").
- // setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
- // }});
- // ApiDefinition api2 = new ApiDefinition("order_api")
- // .setPredicateItems(new HashSet
() {{ - // add(new ApiPathPredicateItem().setPattern("/order-service/order"));
- // }});
- // definitions.add(api1);
- // definitions.add(api2);
- // GatewayApiDefinitionManager.loadApiDefinitions(definitions);
- // }
- }
以上配置,根据路由(访问路径)进行限流,限流规则对访问路径匹配product-service开头的路径生效。
修改application.yml
- filters:
- # - name: RequestRateLimiter
- # args:
- # # 以下上个配置项目报错,不能解析,但不影响使用
- # # 使用SpEL从容器中获取对象
- # #key-resolver: '#{@pathKeyResolver}'
- # key-resolver: '#{@userKeyResolver}'
- # # 令牌桶每秒填充平均速率
- # redis-rate-limiter.replenishRate: 1
- # # 令牌桶的上限
- # redis-rate-limiter.burstCapacity: 3
- - RewritePath=/product-service/(?
.*), /$\{segment}
- # redis:
- # host: localhost
- # port: 6379
- # database: 0
为了防止启动服务报redis连接拒绝,注释掉redis依赖,同时注意刷新依赖生效
-
测试
启动eureka、product、gateway服务
访问
http://localhost:8080/product-service/product/1?access-token=1

访问超过阈值后,响应
Blocked by Sentinel: ParamFlowException

因为路径中包含product-service开头,符合限流规则,所以当访问超出哦阈值后,执行限流操作,看到如上默认的响应信息。
为了更加友好的限流提示,可以自定义响应信息,如下:
取消initBlockHandlers方法的注释
- /**
- * 自定义限流提示
- */
- @PostConstruct
- public void initBlockHandlers() {
- BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
- public Mono
handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { - Map map = new HashMap<>();
- map.put("code", 001);
- map.put("message", "对不起,接口限流了");
- return ServerResponse.status(HttpStatus.OK).
- contentType(MediaType.APPLICATION_JSON_UTF8).
- body(BodyInserters.fromObject(map));
- }
- };
- GatewayCallbackManager.setBlockHandler(blockRequestHandler);
- }
重启gateway服务
测试,访问超过限流阈值时,响应如下

以上是基于路由的限流规则,这个路由下的所有资源都受到这个限流规则限制,sentinel也支持自定义API限流规则,去掉initCustomizedApis()方法注释
- /**
- * 自定义API限流规则
- * 1.定义分组,例如:product_api和order_api
- * 2.在initGatewayRules方法对小组配置限流规则
- */
- @PostConstruct
- private void initCustomizedApis() {
- Set
definitions = new HashSet<>(); - ApiDefinition api1 = new ApiDefinition("product_api")
- .setPredicateItems(new HashSet
() {{ - add(new ApiPathPredicateItem().setPattern("/product-service/product/**").
- setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
- }});
- ApiDefinition api2 = new ApiDefinition("order_api")
- .setPredicateItems(new HashSet
() {{ - add(new ApiPathPredicateItem().setPattern("/order-service/order"));
- }});
- definitions.add(api1);
- definitions.add(api2);
- GatewayApiDefinitionManager.loadApiDefinitions(definitions);
- }
在initGatewayRules方法对小组配置限流规则
- @PostConstruct
- public void initGatewayRules() {
- Set
rules = new HashSet<>(); -
- rules.add(new GatewayFlowRule("product_api")
- .setCount(1)
- .setIntervalSec(1)
- );
- // rules.add(new GatewayFlowRule("order_api")
- // .setCount(1)
- // .setIntervalSec(1)
- // );
- GatewayRuleManager.loadRules(rules);
- }
重启gateway服务
测试
访问超过阈值后,同样能够起到限流效果

所有请求都需要先经过网关才到具体的微服务,网关只有一个节点会存在单点故障,解决办法是使用多个网关服务构成网关集群,实现网关高可用,当一个网关服务不可用,还有其他网关服务可用。实现架构如下图所示:

为了方便测试,取消网关限流配置,注释@Configuration注解
- //@Configuration
- public class GatewayConfiguration {
重启网关服务(8080端口)
复制配置得到另一个网关服务
修改api_gateway_server网关服务的配置文件 application.yml,将端口号改为8081,通过选中运行中的GatewayServerApplication,右键-->Copy Configuration复制配置得到另一个网关服务运行实例

运行复制得到的网关服务(8081端口)
访问8080和8081均能访问到服务
http://localhost:8080/product-service/product/1?access-token=1

http://localhost:8081/product-service/product/1?access-token=1

官网下载并解压nginx安装包

修改nginx配置,进入conf目录,修改nginx.conf文件

注释掉原来location /的配置,添加如下配置
- http {
- upstream gateway{
- server 127.0.0.1:8080;
- server 127.0.0.1:8081;
- }
- server {
- ...
- location / {
- proxy_pass http://gateway;
- }
- ...
- }
- }
配置后,如图

启动nginx

浏览器访问

如果需要改策略,可以修改nginx配置,配置案例如下
IP哈希(IP Hash)策略
upstream gateway {
ip_hash;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
重启nginx,同一机器访问服务三次,均是8080网关提供服务


最少连接(Least Connections)策略
upstream gateway {
least_conn;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
加权轮询(Weighted Round Robin)策略
upstream gateway {
server 127.0.0.1:8080 weight=3;
server 127.0.0.1:8081 weight=1;
}
模拟故障,停掉一个网关服务

依然能访问,实现了网关的高可用。

完成!enjoy it!