在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,消费者调用这个服务就会出现问题,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪,接下来,我们来模拟一个高并发的场景。
server:
port: 9090
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.209.129:8848
application:
name: sentinel-provider
package com.bjpowernode.service;
import com.bjpowernode.pojo.User;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public User getUserById(Integer id) {
//模拟一次网络延时
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new User(id,"王粪堆-provider",18);
}
}
拷贝feign_interface工程:
package com.bjpowernode.feign;
import com.bjpowernode.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient("sentinel-provider")
public interface UserFeign {
@RequestMapping(value = "/provider/getUserById/{id}")
public User getUserById(@PathVariable Integer id);
}
<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">
<parent>
<artifactId>springcloud_parentartifactId>
<groupId>com.bjpowernodegroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>sentinel_consumerartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.bjpowernodegroupId>
<artifactId>springcloud_commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.bjpowernodegroupId>
<artifactId>sentinel_feignartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
dependencies>
project>
server:
tomcat:
max-threads: 10 #tomcat的最大并发值修改为10,默认是200
@RequestMapping(value = "/hello")
public String hello() {
return "Hello Sentienl!!!";
}
5. 配置http请求并启动线程:http://127.0.0.1:8080/consumer/getUserById/1
6. 访问:http://127.0.0.1:8080/consumer/hello
结论:
此时会发现, 由于sentinel_consumer囤积了大量请求, 导致msg方法的访问出现了线程阻塞等待问题,这就是服务雪崩的雏形。
雪崩效应:微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
雪要防止雪崩的扩散,我们就要做好服务的容错,容错说白了就是保护自己不被猪队友拖垮的一些措施。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
Hystrix
Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。
Resilience4J
Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和prometheus等多款主流产品进行整合。
Sentinel
Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的流量控制框架。它以流量为切入点, 从流量控制、熔断降级等多个维度来保护服务的稳定性。
资源
资源就是Sentinel要保护的东西,它可以是Java应用程序中的任何内容,可以是一个服务,也可以是一个方法;我们入门案例中的hello()方法就可以认为是一个资源。
规则
规则就是用来定义如何进行保护资源的,主要包括流量控制规则、熔断降级规等。
Sentinel的主要功能就是容错,主要体现为下面这两个方面:
流量控制(保证应用不被上游服务压垮)
任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
熔断降级(保证应用不被下游服务拖垮)
当检测到调用链路中某个资源某个资源出现慢调用比例或异常比例超出阈值的时候,则暂时切断对下游服务的调用,避免级联故障
总之一句话,我们需要做的事情,就是在Sentinel的资源上配置各种各样的规则,来实现各种容错的功能:
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-coreartifactId>
dependency>
@RequestMapping(value = "/hello")
public String hello() {
Entry entry = null;
try {
//SphU:执行规则检查,获取资源失败时会抛BlockException异常
entry = SphU.entry("/consumer/hello");
return "Hello Sentienl!!!";//被保护的逻辑
} catch (BlockException e) {
//资源访问被阻止
e.printStackTrace();
return "接口被限流了, exception: " + e;
}finally {
// SphU.entry(xxx) 需要与 entry.exit() 成对出现,否则会导致调用链记录异常
if (entry != null) {
entry.exit();
}
}
}
/**
* 定义限流/流控规则
*/
@PostConstruct//当前类的构造函数执行之后执行
public void initFlowQpsRule() {
//1.创建存放限流规则的集合
List<FlowRule> rules = new ArrayList<FlowRule>();
//2.创建限流规则
FlowRule rule1 = new FlowRule();
rule1.setResource("/consumer/hello");//定义资源,名称唯一
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);//定义限流规则类型
rule1.setCount(2); // QPS控制在2以内
//3.将限流规则存放到集合中
rules.add(rule1);
//4.加载限流规则
FlowRuleManager.loadRules(rules);
}
上述通过try-catch
风格的API可以实现限流,但是对代码侵入性太高,推荐使用注解的方式来实现。
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-annotation-aspectjartifactId>
dependency>
@RequestMapping(value = "/hello2")
@SentinelResource(value="/consumer/hello2",blockHandler = "blockHandlerMethod")
public String hello2() {
return "Hello Sentienl2!!!";//被保护的逻辑
}
/**
*资源访问被阻止的兜底方法
*/
public String blockHandlerMethod(BlockException e){
return "接口被限流了, exception: " + e;
}
@PostConstruct
public void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource("/consumer/hello2");//注意修改资源名称
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setCount(2);
rules.add(rule1);
FlowRuleManager.loadRules(rules);
}
//开启sentinel注解扫描
@Bean
public SentinelResourceAspect sentinelResourceAspect(){
return new SentinelResourceAspect();
}
Sentinel 提供一个轻量级的控制台, 它提供资源实时监控以及规则管理等功能。
java -jar sentinel-dashboard-1.8.1.jar
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8080 #指定sentinel的地址
/**
*被保护的逻辑
*/
@RequestMapping(value = "/hello3")
public String hello3() {
return "Hello Sentienl3!!!";
}
流控规则说明:
资源名:唯一名称,默认请求路径
针对来源:Sentinel可以针对调用者进行限流,填写微服务名,指定对哪个微服务进行限流 ,默认default(不区分来源,全部限制)
阈值类型:
QPS:当调用该接口的QPS达到了阈值的时候,进行限流;
线程数:当调用该接口的线程数达到阈值时,进行限流
单机阈值:次数
是否集群:不需要集群
流控模式:
直接:接口达到限流条件时,直接限流
关联:当关联的资源达到阈值时限流自己[适合做应用让步]
链路:当从某个接口过来的资源达到限流条件时,开启限流
流控效果
说明
QPS:代表每秒的访问次数,只要访问次数到达一定的阈值,则进行限流操作
案例
说明
线程数:代表的是每秒内访问该api接口的线程数,如果该接口的操作比较长,当排队的线程数到达阈值的时候,进行限流操作
案例
3.浏览器访问:http://127.0.0.1/consumer/getUserById/1
说明
对当前资源的流量控制
说明
当关联的资源达到阈值时,限流自己。如支付接口达到阈值时限流订单接口。
案例
@RequestMapping(value="/test")
public String test(){
return "test";
}
说明
链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对 来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。
案例
略…
说明
限流的时候直接提示
案例
略…
说明
它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的 1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。
案例
说明
即/consumer/getUserById/1每秒1次请求,超过的话就排队等待,等待的超时时间为10ms,目的是为了匀速处理请求,保证服务的均匀性,而不是一会处理大量的请求,一会有没有请求可处理。
案例
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
//打印请求时间
System.out.println("排队等待效果:"+new Date());
return userFeign.getUserById(id);
}
热点即经常访问的数据,比如:
热点参数流控规则是一种更细粒度的流控规则, 它允许将规则具体到参数上。
@RequestMapping(value = "/getUserById/{id}")
//不加@SentinelResource注解注解则不会触发热点参数流控
@SentinelResource(value = "getUserById", blockHandler = "blockHandlerMethod")
public User getUserById(@PathVariable Integer id) {
return userFeign.getUserById(id);
}
//资源访问被阻止的备选逻辑
public User blockHandlerMethod(Integer id, BlockException e){
return new User(id,"this is blockHandlerMethod",0);
}
系统规则和流控规则不一样,流控规则是针对方法设定的,系统规则是针对一个应用设定的;发生系统规则中配置的情况的时候,会把整个应用都断掉,所有的接口对不能对外提供服务了,这个设计很少用,因为粒度太大了,用 Sentinel 一般都是做细粒度的维护,如果设置了系统规则,可能自己都不知道怎么回事,系统就用不了了;
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel的来源访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:
若配置白名单,则只有请求来源位于白名单内时才可通过;
若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过。
package com.bjpowernode.config;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
//sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求头资源的
@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
/**
* Sentinel保护的接口资源被访问,Sentinel就会调用 RequestOriginParser 的实现类去解析访问来源。
* @param request
* @return
*/
@Override
public String parseOrigin(HttpServletRequest request) {
//当前 来源标识 放在了请求参数里面,可以放到的地方有很多,比如参数/请求头/session/等等
String origin = request.getParameter("origin");
System.out.println("origin:"+origin);
return origin;
}
}
熔断规则就是设置当满足什么条件的时候,暂时切断对下游服务的调用。那么怎么去判断资源是否处于稳定状态呢?
属性 | 说明 |
---|---|
最大RT | 需要设置的阈值,超过该值则为慢调用 |
比例阈值 | 慢调用占所有的调用的比率,范围:[0~1] |
熔断时长 | 在这段时间内发生熔断、拒绝所有请求 |
最小请求数 | 即允许通过的最小请求数,在该数量内不发生熔断 |
属性 | 说明 |
---|---|
异常比例阈值 | 异常比例=发生异常的请求数÷请求总数取值范围:[0~1] |
熔断时长 | 在这段时间内发生熔断、拒绝所有请求 |
最小请求数 | 即允许通过的最小请求数,在该数量内不发生熔断 |
属性 | 说明 |
---|---|
异常数 | 请求发生异常的数量 |
熔断时长 | 在这段时间内发生熔断、拒绝所有请求 |
最小请求数 | 即允许通过的最小请求数,在该数量内不发生熔断 |
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
System.out.println("熔断降级效果:"+new Date());
//控制请求时间
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
return userFeign.getUserById(id);
}
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
System.out.println("熔断降级效果:"+new Date());
//请求报异常
int a = 6/0;
return userFeign.getUserById(id);
}
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable Integer id) {
System.out.println("熔断降级效果:"+new Date());
//请求报异常
int a = 6/0;
return userFeign.getUserById(id);
}
当服务经过sentinel流控或熔断的时候,使用的是Sentinel默认的兜底方法,但是系统默认的方法并没有体现我们的业务要求。
所以Sentinel的@SentinelResource
提供了blockHandler
属性,让我们可以另外定义一个方法来替服务被限制时的返回数据。
属性 | 作用 |
---|---|
value | 资源名称 |
blockHandler | 处理BlockException(Sentinel的配置)的兜底方法,要求: 1.返回类型与原方法一致 2.参数类型与原方法一致,并在最后加BlockException类型的参数 |
blockHandlerClass | 存放blockHandler方法的类,要求: 1.对应的方法必须static修饰 |
@RestController
@RequestMapping(value = "/consumer")
public class ConsumerController {
@Autowired
private UserFeign userFeign;
@RequestMapping(value = "/getUserById/{id}")
@SentinelResource(value ="getUserById",
blockHandler="blockHandlerMethod")
public User getUserById(@PathVariable Integer id) {
int a = 6/0;
return userFeign.getUserById(id);
}
//BlockException异常时调用
public static User blockHandlerMethod(Integer id, BlockException e) {
return new User(0, "接口被流控或者熔断了:"+e, 0);
}
}
package com.bjpowernode.exception;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.bjpowernode.pojo.User;
public class BlockHandlerClass {
//BlockException异常时调用
public static User blockHandlerMethod(Integer id, BlockException e) {
return new User(0, "接口被流控或者熔断了:"+e, 0);
}
}
@RestController
@RequestMapping(value = "/consumer")
public class ConsumerController {
@Autowired
private UserFeign userFeign;
@RequestMapping(value = "/getUserById/{id}")
@SentinelResource(value ="getUserById",
blockHandler="blockHandlerMethod",blockHandlerClass = BlockHandlerClass.class)
public User getUserById(@PathVariable Integer id) {
int a = 6/0;
return userFeign.getUserById(id);
}
//BlockException异常时调用
//public static User blockHandlerMethod(Integer id, BlockException e) {
// return new User(0, "接口被流控或者熔断了:"+e, 0);
//}
}
默认情况下,BlockExceptionHandler 有一个默认的 DefaultBlockExceptionHandler 实现类,返回 Block 字符串提示。这个类是默认处理异常阻塞的,代码如下:
我们可以实现该接口对返回数据进行重写, 从而符合我们的业务要求
BlockException 是一个异常抽象基类,其有 5 个实现类,刚好对应 Sentinel 的 5 种流量控制异常,如下图所示:
package com.bjpowernode.exception;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class GloabBlockExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
response.setContentType("application/json;charset=utf-8");
BaseResult data = null;
if (e instanceof FlowException) {
data = new BaseResult(-1, "限流异常...");
} else if (e instanceof DegradeException) {
data = new BaseResult(-2, "降级异常...");
}else if (e instanceof ParamFlowException) {
data = new BaseResult(-3, "参数限流异常...");
}else if (e instanceof AuthorityException) {
data = new BaseResult(-4, "授权异常...");
}else if (e instanceof SystemBlockException) {
data = new BaseResult(-5, "系统负载异常...");
}
response.getWriter().write(JSON.toJSONString(data));
}
}
class BaseResult {
private int status;
private String msg;
private Object data;
public BaseResult() {
}
public BaseResult(int status, String msg) {
this.status = status;
this.msg = msg;
}
public BaseResult(int status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
前文的熔断降级是对客户端(调用方)的保护,而我们的微服务远程调用都是基于Feign来完成的,因此我们可以将Feign与Sentinel整合,在Feign里面实现服务熔断。
feign:
sentinel:
enabled: true #开启Feign对Sentinel的支持
package com.bjpowernode.exception;
import com.bjpowernode.feign.UserFeign;
import com.bjpowernode.pojo.User;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
*一旦Feign远程调用服务失败,就会进入当前类同名方法,执行容错逻辑
*/
@Component
public class UserFeignFallback implements FallbackFactory<UserFeign> {
@Override
public UserFeign create(Throwable t) {
return new UserFeign() {
@Override
public User getUserById(Integer id) {
return new User(id,"feign调用失败:"+t,0);
}
};
}
}
package com.bjpowernode.feign;
import com.bjpowernode.exception.UserFeignFallback;
import com.bjpowernode.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient(value="sentinel-provider",fallbackFactory = UserFeignFallback.class)
@RequestMapping(value = "/provider")
public interface UserFeign {
@RequestMapping(value = "/getUserById/{id}")
public User getUserById(@PathVariable("id") Integer id);
}
在上一篇,我们讲解了Sentinel的整合与使用,但是有个很明显的问题,就是一旦服务重启,当前配置的针对某个接口的规则就丢掉了,然后就需要重新再配一遍,这就很坑爹了,如果开发中需要配置的接口太多,这样岂不是让人疯掉。
因此需要一个地方来保存dashboard中配置的规则,Sentinel提供了多种持久化的方案,可以集成redis、mysql、nacos等。
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
spring:
application:
name: sentinel-consumer
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr} #nacos连接地址
namespace: sentinel
groupId: SENTINEL_GROUP #nacos连接的分组名称
dataId: ${spring.application.name}-flow-rules #读取配置文件的名称
rule-type: flow #配置文件内容为flow
ds2:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr} #nacos连接地址
namespace: sentinel
groupId: SENTINEL_GROUP #nacos连接的分组名称
dataId: ${spring.application.name}-degrade-rules #读取配置文件的名称
rule-type: degrade #配置文件内容为degrade
下载地址:https://github.com/alibaba/Sentinel/tree/master/sentinel-dashboard
nacos.address=192.168.209.129:8848
大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。这样的架构,会存在着诸多的问题:
客户端请求不同的微服务,就要维护不同的ip
客户端无法实现负载均衡
上面的这些问题可以借助API网关来解决:
在业界比较流行的网关,有下面这些:
Ngnix+lua
使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用 lua是一种脚本语言,可以来编写一些简单的逻辑,nginx支持lua脚本
Spring Cloud Gateway
Spring公司为了替换Zuul而开发的网关服务,将在下面具体介绍。
注意:SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway来做网关。
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 WebFlux等技术开发的网关,目标是替代 Netflflix ZUUL,并且基于 Filter 链的方式提供了路由,过滤,和限流等功能。
组件 | RPS(request per second) |
---|---|
Spring Cloud Gateway | Requests/sec: 32213.38 |
Zuul1X | Requests/sec: 20800.13 |
上表为Spring Cloud Gateway与Zuul的性能对比,从结果可知,Spring Cloud Gateway的RPS是Zuul的1.6倍
Gateway Client向Gateway Server发送请求
HandlerMapping负责路由查找,并根据路由断言判断路由是否可用
WebHandler创建过滤器链并调用
<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">
<parent>
<artifactId>springcloud_parentartifactId>
<groupId>com.bjpowernodegroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>api_gatewayartifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
dependencies>
project>
注意:不要添加spring-boot-starter-web启动器
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.209.129:8848
server:
port: 9527
package com.bjpowernode;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApp {
public static void main(String[] args) {
SpringApplication.run(GatewayApp.class, args);
}
}
spring:
cloud:
gateway:
routes:
- id: sentinel-consumer #自定义的路由ID,保持唯一
uri: http://localhost:80 # 请求要转发到的地址
predicates: #断言
- Path=/consumer/** #只有断言条件返回true(请求路径包含“/consumer”)时,才进行路由转发
1.开启服务和网关:
2.浏览器访问:http://127.0.0.1:9527/consumer/getUserById/1
spring:
cloud:
gateway:
routes:
- id: sentinel-consumer #自定义的路由ID,保持唯一
uri: lb://sentinel-consumer #lb代表从注册中心获取服务
predicates: #断言
- Path=/consumer/** #只有断言条件返回true(请求路径包含“/consumer”)时,才进行路由转发
Spring Cloud Gateway 的功能很强大,前面我们只是使用了 predicates 进行了简单的条件匹配,其实Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。
可参考spring官网:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
此类型的断言根据时间做判断,主要有三个:
AfterRoutePredicateFactory:接收一个日期参数,判断请求日期是否晚于指定日期。
BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期。
BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否在指定时间段内。
- After=2022-04-09T17:20:54.957+08:00[Asia/Shanghai]
RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中。
- RemoteAddr=192.168.1.1/24
CookieRoutePredicateFactory:接收两个参数,cookie名字和一个正则表达式。判断请求cookie是否具有给定名称且值与正则表达式匹配。
- Cookie=chocolate, ch.
HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否 具有给定名称且值与正则表达式匹配。
- Header=X-Request-Id, \d+
HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。
- Host=**.testhost.org
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
- Method=GET
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
- Path=/foo/{segment}
QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。
- Query=baz, ba.
我们来设定一个场景: 假设我们的应用仅仅让age>18的人来访问,在自定义断言工厂之前,我们先看内置断言工厂的实现原理,打开AfterRoutePredicateFactory这个内置断言工厂:
package com.bjpowernode.PredicateFactory;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
* 自定义断言工厂
* Config 是一个类 需要我们自己去定义
*/
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//读取配置文件的中参数值 给他赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
//断言逻辑
@Override
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//1 接收前台传入的age参数
String ageStr =
serverWebExchange.getRequest().getQueryParams().getFirst("age");
//2 先判断是否为空
if (StringUtils.isNotEmpty(ageStr)) {
//3 如果不为空,再进行路由逻辑判断
int age = Integer.parseInt(ageStr);
if (age < config.getMaxAge() && age > config.getMinAge()) {
return true;
} else {
return false;
}
}
return false;
}
};
}
//自定义一个配置类, 用于接收配置文件中的参数
@Data
@NoArgsConstructor
public static class Config {
private int minAge;//18
private int maxAge;//60
}
}
spring:
cloud:
gateway:
routes:
- id: sentinel-consumer
uri: lb://sentinel-consumer
predicates:
- Path=/consumer/**
- Age=18,60 #配置断言必须使用断言工厂类名的前缀
Spring Cloud Gateway提供了过滤器的功能,可以对进入网关的请求
和响应
做处理:
Spring Cloud Gateway 的 Filter 从作用范围可分为另外两种GatewayFilter 与 GlobalFilter。
GatewayFilter:应用到单个路由或者一个分组的路由上。
GlobalFilter:应用到所有的路由上。
Spring Cloud Gateway中通过GatewayFilter的形式内置了很多不同类型的局部过滤器,可参考spring官网:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
过滤器工厂 | 作用 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
AddRequestParameter | 为原始请求添加请求参数 |
AddResponseHeader | 给响应结果中添加一个响应头 |
DedupeResponseHeader | 去掉重复请求头 |
Spring Cloud CircuitBreaker | 断路器 |
FallbackHeaders | 添加熔断后的异常信息到请求头 |
MapRequestHeader | 将上游请求头的值赋值到下游请求头 |
PrefixPath | 匹配的路由添加前缀 |
PreserveHostHeader | 保留原请求头 |
RequestRateLimiter | 限制请求的流量 |
RedirectTo | 重定向 |
RemoveRequestHeader | 移除请求中的一个请求头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RemoveRequestParameter | 移除请求参数 |
RewritePath | 重写路径 |
RewriteLocationResponseHeader | 重写响应头中Location的值 |
RewriteResponseHeader | 重写响应头 |
SaveSession | 向下游转发请求前前置执行WebSession::save的操作 |
SecureHeaders | 禁用默认值 |
SetPath | 设置路径 |
SetRequestHeader | 重置请求头 |
SetResponseHeader | 修改响应头 |
SetStatus | 修改响应的状态码 |
StripPrefix | 对指定数量的路径前缀进行去除 |
Retry | 重试 |
RequestSize | 请求大小大于限制时,限制请求到达下游服务 |
SetRequestHostHeader | 重置请求头值 |
Modify a Request Body | 修改请求体内容 |
Modify a Response Body | 修改响应体内容 |
Relay | 将 OAuth2 访问令牌向下游转发到它所代理的服务 |
CacheRequestBody | 在请求正文发送到下游之前缓存请求正文并从 exchagne 属性获取正文 |
需求:记录调用远程服务所需要的时间
package com.bjpowernode.FilterFactory;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@Component
public class LogGatewayFilterFactory
extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
public LogGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
if (config.paramValue) {
long beginTime = System.currentTimeMillis();
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
@Override
public void run() {
long endTime = System.currentTimeMillis();
System.out.println("time of call service :" +
(endTime - beginTime));
}
}));
}
return chain.filter(exchange);
}
};
}
//读取配置文件中的参数 赋值到配置类中
@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("paramValue");
}
//自定义一个配置类, 用于接收配置文件中的参数
@Data
@NoArgsConstructor
public static class Config {
private boolean paramValue;
}
}
spring:
cloud:
routes:
- id: sentinel-consumer
uri: lb://sentinel-consumer
predicates:
- Path=/consumer/**
filters: #过滤
- Log=true
在网关过滤器中通过Token 判断用户是否登录
作用于所有路由,Spring Cloud Gateway 定义了Global Filter接口,用户可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是程序员使用比较多的过滤器。Spring Cloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
在服务网关中定义过滤器只需要实现 GlobalFilter, Ordered接口就可对请求进行拦截与过滤。
package com.bjpowernode.FilterFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
@Component
public class LoginFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String token = request.getQueryParams().getFirst("token");
if (token == null) {
BaseResult data = new BaseResult(401, "未登录");
return response(response,data);
}
//放行
return chain.filter(exchange);
}
private Mono<Void> response(ServerHttpResponse response, BaseResult data) {
String jsonData = null;
try {
jsonData = new ObjectMapper().writeValueAsString(data);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
DataBuffer buffer =
response.bufferFactory().wrap(jsonData.getBytes(StandardCharsets.UTF_8));
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
/**
* 过滤器的执行顺序:通过整数表示顺序,数值越小,优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
class BaseResult {
private int status;
private String msg;
private Object data;
public BaseResult() {
}
public BaseResult(int status, String msg) {
this.status = status;
this.msg = msg;
}
public BaseResult(int status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前 面学过的Sentinel组件来实现网关的限流。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
dependency>
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8080 #指定sentinel的地址
package com.bjpowernode.exception;
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.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.alibaba.fastjson.JSON;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class GatewayConfig {
@PostConstruct
public void init() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange,
Throwable t) {
// 自定义异常信息
Map map = new HashMap<>();
map.put("status", 200);
map.put("msg", "接口被限流了");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
};
//自定义异常处理
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}