我们微服务的出现满足了高内聚低耦合的设计思想。这样固然是好的,但随之而来的是微服务的不断增多,经常会出现80端口调用8001,8001再去调用8002,8002再去调用8006…这样的情况,链路过长
,一旦中间有一个服务挂掉了就会全体连坐
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”(像一把扇子一样打开,而影响一个面)。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常
,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
一句话:即便我这个服务不可用了,你也要给我一个兜底的东西让我好返回数据让调用链路继续下去,不能停在我这里。不能空手回去。
服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback(当前客服全忙,请等待~)
类比保险丝达到最大服务访问后,直接拒绝访问,服务器为了自保拉闸限电,然后调用服务降级的方法并返回友好提示
服务的降级->进而熔断->调整一下微服务再恢复调用链路
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行,也是保护我们的服务器,怕崩了
新建一个provider(Hystrix版)
废话不多说,先上pom依赖
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7002 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7002.class, args);
}
}
service层
@Service
public class PaymentService
{
/**
* 正常访问,一切OK
* @param id
* @return
*/
public String paymentInfo_OK(Integer id)
{
return "线程池:"+Thread.currentThread().getName()+"paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O";
}
/**
* 超时访问,模拟超时导致降级
* @param id
* @return
*/
public String paymentInfo_TimeOut(Integer id)
{//自定义超时时间
int time=3;
try { TimeUnit.SECONDS.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); }
return "线程池:"+Thread.currentThread().getName()+"paymentInfo_TimeOut,id: "+id+"\t"+"O(∩_∩)O,耗费3秒";
}
}
controller层
@RestController
@Slf4j
public class PaymentController
{
@Autowired
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id)
{
String result = paymentService.paymentInfo_OK(id);
log.info("****result: "+result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException
{
String result = paymentService.paymentInfo_TimeOut(id);
log.info("****result: "+result);
return result;
}
}
启动测试,启动Eureka7001注册中心(单机版)、8001Hystrix
上述module均正常运行
2w并发压测进timeout的api,把他搞崩了,模拟微服务报错了的场景
启动测试,测试结果就是除了这个目标测试的服务变慢了之外,另外的一个端口也变慢了。两个接口在一个微服务下,大量的资源都给了被高频访问的接口,没有额外的资源给其他端口,所以就会被拖慢,最直接反映到用户身上的就是体验变差了。
为什么会卡死?
tomcat的默认的工作线程数被打满 了,没有多余的线程来分解压力和处理。
上面还是服务提供者8001自己测试,假如此时外部的消费者80也来访问,那消费者只能干等,最终导致消费端80不满意,服务端8001直接被拖死
新建工程consumer(Hystrix版),feign+hystrix,来解决上面的问题
引入依赖和yml
yml:
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
主启动类
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
service接口
controller
启动服务测试
正常访问没问题,开始压测
压测timeout接口的同时,对ok接口进行访问,结果发现ok接口反应也慢,说明资源被占用了。
正因为有上述故障或不佳表现
才有我们的降级/容错/限流等技术诞生
首先加入注解:@HystrixCommand
,这个是替代继承HystrixCommand
如图所示,是代码级别的处理
加入兜底的方法,服务降级fallback
设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作服务降级fallback
先在controller接口上加入 @HystrixCommand
注解标示,图上规定时间为3s,一旦当前服务运行时间超过了3s,系统就会认为这个服务出现了异常,就要去“服务降级”,也就是运行兜底的服务(paymentInfo_TimeOutHandler)。
改完了业务接口,还得改造主启动类。添加新注解@EnableCircuitBreaker
启动业务类,将睡眠时间time修改为5s
这个时候就触发了服务降级,走兜底的方法。
先改yml文件
主启动类加入 @EnableHystrix
修改controller层接口,加入了降级服务(兜底),设置了超时时间
业务类内容
业务类是利用feign去调用上面的8001的timeout超时接口,因为调用的8001的接口的耗时时长为3s,而80端口设置的等待时间只有3s,所以这里会有自动触发服务降级
启动测试,降级成功
不光是超时,这里如果说80端也出现了运行时异常,那么也会自动服务降级。
不难发现,有兜底的方法虽然好,但是出现的关键问题是这种写法的代码量会急剧膨胀,且降级兜底的方法和正常的业务方法杂糅在一起,耦合度极高。我们要把统一和自定义的分开。
global fallback 统一的,通用的服务降级方法放在这里。
一些特殊的方法特殊写在代码里。
加在类上面,通用的服务降级触发。在controller类上加入注解@DefaultProperties(defaultFallback = "")
目的是通用的降级配置
说明:
@DefaultProperties(defaultFallback = “”)
1对1 每个方法配置一个服务降级方法,技术上可以,实际上傻X
1对N 除了个别重要核心业务有专属,其它普通的可以通过@DefaultProperties(defaultFallback = “”) 统一跳转到统一处理结果页面
通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量。
之前的注解修改,因为配置了默认的降级方法,所以这里不需要再特别复杂的配置了
当然,如果有特殊需要的降级处理,那再另行配一个就好。
重启服务进行测试,走的是全局异常处理
为服务接口提供一个服务降级,这里的服务降级与被调用者8001的服务无关,专指80端口调用者保护自己的异常处理
为了实现解耦而产生的方法,可以看到,即便是有了全局默认的降级处理方法,在controller量很大的情况下也会变得难以维护。
回到OpenFeign的服务调用接口
异常处理类
再次回到OpenFeign的服务调用接口,把刚刚写好的降级处理类加入到注解配置中,到时候服务调用出现问题的时候就自动到这个降级处理类里面找方法。
注释掉80端口controller类中的服务降级,只保持开启service接口以及服务降级处理类的正常运行
测试,8001为80端口正常提供服务
因为我们写的是80侧的自我保护,所以这个时候把8001的服务停掉,模拟8001微服务挂掉了80触发自我保护自动降级,避免阻塞时间过长而导致等待。
停掉8001服务
重新测试80端口刚刚的API,可以发现已经触发了自我保护,进行了降级处理。
一句话就是家里的保险丝
熔断机制概述
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,
会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当检测到该节点微服务调用响应正常后,恢复调用链路。
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,
当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。
引入一个很好用的工具类Hutool
依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.0.M1</version>
</dependency>
service层的新增熔断配置
controller层对service调用也要进行更改
新增熔断方法调用
API功能,ID若为负就抛异常,正数就正常
API功能测试正常。
这个时候,我们快速反复访问错误情况,传值进-1。目的是为了造成大量的错误满足之前的配置条件,触发断路器。
多次访问http://localhost:8001/payment/circuit/-1。造成大量降级,触发熔断器。
此时这个时间点,我突然给了一个正常的访问。可以看到依旧走的是服务降级的方法
如何恢复正常让他重新“拉闸上电”?再次发出几次正常的访问,慢慢开始恢复正常
多次错误,然后慢慢正确,发现刚开始不满足条件,就算是正确的访问地址也不能进行,也就是触发了熔断器,他需要慢慢地开启,此时处于半开状态(尝试),需要一些正常的访问通过,才会恢复正常
涉及到断路器的三个重要参数:快照时间窗、请求总数阀值、错误百分比阀值。
1:快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
2:请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
3:错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。
1:再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
2:原来的主逻辑要如何恢复呢?
对于这一问题,hystrix也为我们实现了自动恢复功能。
当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,
当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,
主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
//========================All
@HystrixCommand(fallbackMethod = "str_fallbackMethod",
groupKey = "strGroupCommand",
commandKey = "strCommand",
threadPoolKey = "strThreadPool",
commandProperties = {
// 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
// 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数)
@HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
// 配置命令执行的超时时间
@HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
// 是否启用超时时间
@HystrixProperty(name = "execution.timeout.enabled", value = "true"),
// 执行超时的时候是否中断
@HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
// 执行被取消的时候是否中断
@HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"),
// 允许回调方法执行的最大并发数
@HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
// 服务降级是否启用,是否执行回调函数
@HystrixProperty(name = "fallback.enabled", value = "true"),
// 是否启用断路器
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
// 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,
// 如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
// 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过
// circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50,
// 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
// 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,
// 会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态,
// 如果成功就设置为 "关闭" 状态。
@HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
// 断路器强制打开
@HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
// 断路器强制关闭
@HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),
// 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间
@HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"),
// 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据
// 设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。
// 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
// 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。
@HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
// 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
@HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
// 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。
@HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
// 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,
// 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行,
// 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
@HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
// 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。
@HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),
// 是否开启请求缓存
@HystrixProperty(name = "requestCache.enabled", value = "true"),
// HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中
@HystrixProperty(name = "requestLog.enabled", value = "true"),
},
threadPoolProperties = {
// 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量
@HystrixProperty(name = "coreSize", value = "10"),
// 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列,
// 否则将使用 LinkedBlockingQueue 实现的队列。
@HystrixProperty(name = "maxQueueSize", value = "-1"),
// 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。
// 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue
// 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
}
)
public String strConsumer() {
return "hello 2020";
}
public String str_fallbackMethod()
{
return "*****fall back str_fallbackMethod";
}
见高级篇alibaba的Sentinel说明
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。
新引入的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
启动类上加入新注解 @EnableHystrixDashboard
注意,被监控端也是要加上依赖的
所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
启动cloud-consumer-hystrix-dashboard9001该微服务后续将监控微服务8001,访问豪猪哥地址:http://localhost:9001/hystrix
看到监控图形化界面
所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置,步骤如下
加入主启动类的main方法之后加入配置信息,不然会报错404
/**
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
*只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
启动测试,测试内容为9001来监控8001
测试正常端口
http://localhost:8001/payment/circuit/31,运行正常。
有流量进来的监控画面
并发模拟降级
怎么看这个监控?
整图说明以及标注
曲线含义:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。
实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色<黄色<橙色<红色递减。
该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压力实例。
Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关;
但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul,
那就是SpringCloud Gateway一句话:gateway是原zuul1.x版的替代
网关就是挡在所有服务的前面,做分流处理
能干的事情
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
参考的是Java8的java.util.function.Predicate
开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
说白了就是,前面的路由转发把服务转发到微服务门口,由断言来看是否你符合进入微服务的条件,符合条件了才会放请求进入微服务,不符合条件的就给拒绝了。
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
在断言条件成立后,进入服务还有一次条件的判断。类似查处有没有一些不好的记录,对其进行拦截
web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件;
而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。
Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。
Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,
在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
路由转发+执行过滤器链
(新建9527工程)
新引入的gateway依赖
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
注意,网关作为一种服务也是要注册进入Eureka的注册中心的,所以要引入Eureka的依赖
yml文件
测试流程
访问Eureka注册中心,可以看到已经入驻的服务
此时访问测试8001,正常访问
换网关9527接口访问,注意,这里网关只能访问到yml配置文件中已经配好的路由url+断言
没配的访问不到
可以看到,这里返回了8001端口,说明找到了8001
这样做目的是为了淡化真实地址和端口号,不让其暴露在外。
yml文件的配置方式可以看上面的配置方法,可以预见的是,在接口数量庞大的情况下,后面路由配的数量会越来越多,不过这种方法仍然是用的比较多的,因为比较直观。
之前的yml配置都是静态的。通过以上配置文件配置路由,地址、端口 都是写死的,就没办法做负载均衡了。所以这一节,就配置动态路由来实现负载均衡。
修改yml配置文件
只修改yml文件即可,启动服务测试,可以看到,访问同一个接口实现了负载均衡的功能
启动9527之后可以看到的一些现象
Route Predicate Factories这个是什么东东?
官方解释,巴拉巴拉一大堆
Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。
Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与HTTP请求的不同属性匹配。多个Route Predicate工厂可以进行组合
Spring Cloud Gateway 创建 Route 对象时, 使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。 Spring Cloud Gateway 包含许多内置的Route Predicate Factories。
所有这些谓词都匹配HTTP请求的不同属性。多种谓词工厂可以组合,并通过逻辑and。
时间控制,在xxx时间之后(after)才开放端口访问
启动测试,因为上面的配置是经过9527的路由接口localhost:9527/payment/lib/xxx要上海时间2022-4-04 15:10:03分之后才能访问,这里先测试一下,由于未开启访问,直接404
但是,我直接访问被代理的8001端口服务是没问题的
由此可以看出,路由配置的时间可以控制接口开放的时间。
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
- After=2022-04-04T15:10:03.685+08:00[Asia/Shanghai] # 断言,路径相匹配的进行路由
Cookie Route Predicate需要两个参数,一个是 Cookie name ,一个是正则表达式。
路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行
要求带Cookie访问
测试,带Cookie访问(curl就是postman的cmd命令版)
curl http://localhost:9527/payment/lb --cookie "username=zzyy"
curl http://localhost:9588/paymentInfo
curl http://localhost:9588/paymentInfo -H "X-Request-Id:123"
主机访问规定
predicates:
- Host=**.atguigu.com
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符。
它通过参数中的主机地址作为匹配规则。
规定请求方法
predicates:
- Method=GET
规定路径匹配
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
规定特定参数名称
predicates:
- Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
支持传入两个参数,一个是属性名,一个为属性值,属性值可以是正则表达式。
说白了,Predicate就是为了实现一组匹配规则,
让请求过来找到对应的Route进行处理。,不满足要求的可以直接打回拒绝掉(404)
全部的断言配置
Spring:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
- After=2020-02-05T15:10:03.685+08:00[Asia/Shanghai] # 断言,路径相匹配的进行路由
- Before=2020-02-05T15:10:03.685+08:00[Asia/Shanghai] # 断言,路径相匹配的进行路由
- Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2020-03-25T18:59:06.206+08:00[Asia/Shanghai]
- Cookie=username,zzyy
- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
- Host=**.atguigu.com
- Method=GET
- Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。
Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生
filters:
- AddRequestParameter=X-Request-Id,1024 #过滤器工厂会在匹配的请求头加上一对请求头,名称为X-Request-Id值为1024
新建一个类实现两个接口(GlobalFilter、Ordered)以及对应方法
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。
SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,上百个配置文件的管理......
随着工程的不断庞大,服务数量暴增,而且yml文件的数量也会增多,yml文件中大量的配置例如数据库都是重复的,一旦想修改起来就比较麻烦(运维都得骂街了)。为了实现一个理念“一处修改处处生效”的解决方案,Config出现了。
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
怎么玩
SpringCloud Config分为服务端和客户端两部分。
服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口
只有一个文件,后续还会上传(这里只做示范,实际上我用了码云,速度快很多)
把仓库克隆到本地(地址和仓库要换成自己的,同时还要公开访问)
git clone 仓库地址
新引入的配置依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
yml配置,关联GitHub,这里的url也可以改成http的那种,都可以
老生常谈老几样
这里新加入的一个注解@EnableConfigServer
标示作为配置中心
启动服务测试,走3344端口(http://localhost:3344/master/config-dev.yml)
可以查看到Gitee上的yml文件,运维就可以把仓库pull到本地,针对yml进行修改并上传
applicaiton.yml是用户级的资源配置项
bootstrap.yml是系统级的,优先级更加高
Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的Application Context
的父上下文。初始化的时候,Bootstrap Context
负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment
。
Bootstrap
属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap context
和Application Context
有着不同的约定,所以新增了一个bootstrap.yml
文件,保证Bootstrap Context
和Application Context
配置的分离。
要将Client模块下的application.yml文件改为bootstrap.yml,这是很关键的,
因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml
可以看到图标和之前的application.yml也不太一样
其他业务controller层
@RestController
public class ConfigClientController
{
@Value("${config.info}")#读yml里的配置信息
private String configInfo;
@GetMapping("/configInfo")#Get请求
public String getConfigInfo()
{
return configInfo;
}
}
启动服务测试
连接的流程就是3344连到3355再连接到Git上
访问http://localhost:3355/configInfo就可以读取到文件信息
成功实现了客户端3355访问SpringCloud Config3344通过GitHub获取配置信息
这里想要解决就需要引入对工程的状态监控
这里又要引入新的配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
3355 bootstrap中加入新的配置
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
在controller层加入刷新业务的注解标签
启动测试,需要运维人员发出POST请求刺激一下3355刷新一下,不发直接重新请求是获取不到新数据的
但是这样要手动重启还是很麻烦,这里就引出了一堆新问题
这些问题留给下一章Bus(消息总线来完成)
书接上文,Config的git上更新文件后就要手动去重启。每次手动去刷新配置,就会很繁琐,能不能配一个消息发布的组件,让需要订阅消息的服务来订阅新消息发布。大白话解释:每个微服务都是个微信号,每个微信号(微服务)都去按照自己的需求来订阅一些公众号(消息总线),依照发出的消息来做出一定的处理,类似自动刷新的功能就可以在接收到消息之后自动做出处理。
Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,
它整合了Java的事件处理机制和消息中间件的功能。
Spring Clud Bus目前支持RabbitMQ和Kafka。
Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。
官方解读
什么是总线
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。
安装Erlang(不装这玩意装不了Rabbit MQ)
安装Rabbit MQ
进入RabbitMQ安装目录下的sbin目录,cmd启动命令窗口运行指令
rabbitmq-plugins enable rabbitmq_management
这样就添加好了可视化的插件
访问http://localhost:15672/就可以看到了
输入账号密码并登录:guest guest
成功进入
新建工程都是老一套,参考3355复制一份出来,不多赘述了,只把一些特殊的部分拿出来
yml也是只有端口号不一样,其他的都一样
这种设计方案用的比较少,一般来说都是触发Config Server(通用)拉取配置,带动三个微服务模块来触发,这里刚好相反。
这种方案用的少的原因如下:
加入Rabbit MQ依赖
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
新增的Rabbit MQ依赖参照上面的3344
yml新增
注意:rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口
yml和3355一毛一样,我懒,不截图了
注意一个点,这里的yml是bootstrap.yml 而不是 application.yml
在gitee端进行修改一下
在gitee提交一下
重新获取一下,这里是3366,3344也是一个效果
此时,广播一下更新配置,直接打到中心上,通知所有下属的服务都更新配置
此时再次或许配置,就可以读到了
3344也是一样,成功更新了数值
最终效果:一次修改,广播通知,处处生效
上面的都是全体通知,那我想只通知一个,做到单个点对点的通知(微信订阅一个单独的微信公众号专门接收他通知)
这种的实现方式就是定点通知,该通知变更的就变更,不该通知的就不用动。精确打击。
定点刷新命令:http://localhost:配置中心的端口号/actuator/bus-refresh/{destination}
/bus/refresh请求不再发送到具体的服务实例上,而是发给config server并
通过destination(目的地的意思)参数类指定需要更新配置的服务或实例
3344下有3355和3366,此时我只通知3355而不通知3366的写法
举例:curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"
这样就可以做到单点专门通知刷新
什么是SpringCloudStream
官方定义 Spring Cloud Stream 是一个构建消息驱动微服务的框架。
应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream中binder对象交互。
通过我们配置来binding(绑定) ,而 Spring Cloud Stream 的 binder对象负责与消息中间件交互。
所以,我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。
Spring Cloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。
目前仅支持RabbitMQ、Kafka。
一句话概括:
出现的原因:中间件太多,各种中间件要对接。用一个binder对象来屏蔽底层差异,让各种中间件能够打通
屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
Spring Cloud Stream是用于构建与共享消息传递系统连接的高度可伸缩的事件驱动微服务框架,该框架提供了一个灵活的编程模型,它建立在已经建立和熟悉的Spring熟语和最佳实践上,包括支持持久化的发布/订阅、消费组以及消息分区这三个核心概念
只针对Binder对象进行操作,不需要管中间件之间的差异
官网介绍
Spring Cloud Stream中文指导手册点这里
实际工程中的情况,系统中有两种中间件,要适配就很烦
比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,
像RabbitMQ有exchange,kafka有Topic和Partitions分区,这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候springcloud Stream给我们提供了一种解耦合的方式。
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,
由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性
通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Binder可以生成Binding,Binding用来绑定消息容器的生产者和消费者,它有两种类型,INPUT和OUTPUT,INPUT对应于消费者,OUTPUT对应于生产者。
INPUT对应于消费者(消息接受者,消息订阅者)
OUTPUT对应于生产者(消息生产者,消息发布者)
Source和Sink是什么?
简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入
Channel是什么?
通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
Binder是什么?
很方便的连接中间件,屏蔽差异
新引入的pom依赖,这里是rabbit的,要是其他中间件就换成对应的就可以了
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
新yml配置(一脸懵逼,cv工程师)
这里层级关系错了,修改后是这样的
缩进有错误,参考上一张
主启动类也就那些东西,不多赘述
重头戏!Service接口及其实现类,原来的实现类都是专门与Dao层打交道,各种注入Dao对象,现在的Service实现类不再需要注入这些东西,因为这里是与消息中间件打交道
service接口
public interface IMessageProvider{
public String send() ;
}
impl实现类
@EnableBinding(Source.class)
public class IMessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; // 消息的发送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
this.output.send(MessageBuilder.withPayload(serial).build()); // 创建并发送消息
System.out.println("***serial: "+serial);
return serial;
}
}
Controller层
测试接口,启动Rabbit MQ进入可视化监控模块,发送Get请求查看消息中间件负载状态
RabbitMQ监控页面
之前yml起的名字这里已经注册进来了
有了消息的生产者,就应该有消息的订阅者接收消息
新建模块
pom,主启动类和上面的基本一致
只有yml有点不太一样,和生产端的区别
再强调一下层级关系
controller业务类
启动测试http://localhost:8801/sendMessage
此时8802就可以接收消息了
什么是重复消费?
8802
8803
默认8802和8803各位一组,不同组之间就会重复消费消息
解决方案:分组和持久化属性group
微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。
不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。
RabbitMQ默认分组
这里就需要自定义分组配置
修改yml
重启服务测试,再次访问控制中心
可以看到已经分组成功了
已经不是原来随机生成的长长的流水号了
重新广播发送消息,还是能看到被重复消费了
也就是不同组会被重复消费
多数情况,生产者发送消息给某个具体微服务时只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但是这个消息出现了被重复消费两次的情况。为了解决这个问题,在Spring Cloud Stream中提供了消费组的概念。
所以到底怎么去解决重复消费
把两个分组改成一个分组,group名字相同就行,到时候两个消费者就轮流来接收广播的消息
修改yml
此时重启服务,查看MQ管理界面
此时再发起广播命令。就会两个微服务进行轮询
同一个组的多个微服务实例,每次只会有一个拿到
假设8801疯狂发消息,8802(没有分组)、8803(有分组)这个时候两个服务挂掉了,那这个时候8801的消息8802、8803就收不到了但是,重新上线后8803有分组,就可以重新上线把消息捡起来重新消费,8802就不行,没法接收到这些消息也就无法消费
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。说白了就是,计算一个调用链路上,各个微服务所耗费的时间、延时、总消耗时间的问题
监控链路状态,一般使用zipkin来监控
SpringCloud从F版起已不需要自己构建Zipkin Server了,只需调用jar包即可
也就是那个java -jar jar包
运行一下就可以启动
访问图形化界面http://localhost:9411/zipkin/
zipkin完整的调用链路
表示一请求链路,一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来
一条链路通过Trace Id唯一标识,Span标识发起的请求信息,各span通过parent id 关联起来
引入新pom
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
改造cloud-consumer-order80
引入新pom
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
调用80消费者的接口来间接调用了8001,实现调用链路
完成了服务链路调用,此时访问zipkin的图形化界面,看一下服务关系
查看具体消息
查看依赖之间的关系
这样就可以对服务进行很好的观测