前言:本文会用到 前文SpringCloud(1)里面的微服务。
1.1.1 简介
官网:https://github.com/Netflix/ribbon/wiki/Getting-Started
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。
但是目前也进入维护模式,未来可能被Spring Cloud LoadBalacer替代
1.1.2 负载均衡LB(Load Balance)
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA (高可用)。
常见的负载均衡有软件Nginx,LVS,硬件F5等。
1.1.3 Ribbon本地负载均衡客户端VS Nginx服务端负载均衡区别
复习:Nginx负载均衡 https://blog.csdn.net/chengqingshihuishui/article/details/116902124
(1)Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
(2)Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
由上面的区别可以将负载均衡分为两种:集中式LB 与 进程内 LB
① 集中式LB
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;
② 进程内 LB
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就是一个软负载均衡的客户端组件,就是负载均衡+RestTemplate调用,与Eureka结合的架构如下:
1.1.4 Ribbon的使用
(1)安装Ribbon
① 作为高级框架的附属品自动安装(比如Eureka)
SpringCloud(1)第三节讲的Eureka就已经集成好了Ribbon
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
这个starter里面有Ribbon依赖
② 单独安装Ribbon
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
说明:在Web服务端项目里面,Ribbon肯定是集成在高级框架里面配套使用的。
(2)复习:RestTemplate
RestTemplate Java Doc
重点方法:
① getForObject() / getForEntity() - GET请求方法
getForObject():返回对象为响应体中数据转化成的对象,基本上可以理解为Json。
getForEntity():返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。
② postForObject() / postForEntity() - POST请求方法
postForObject本质上是将对象放入HttpEntity中,然后将对象POST给一个url。直接形参放java对象也行,因为底层会完成java对象到HttpEntity 的自动转换。
形参分别是传输的URL,传输的Object,和返回值的类型。这里返回类型是CommonResult
,还记得前面专门在CommonResult实体类中写了一个只有两个参数的构造器吗 Exp:
@GetMapping("/consumer/payment/getForEntity/{id}") public CommonResult
getPayment2(@PathVariable("id") Long id) { ResponseEntity entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,CommonResult.class); if(entity.getStatusCode().is2xxSuccessful()){ return entity.getBody();//getForObject() }else{ return new CommonResult<>(444,"操作失败"); } }
1.1.5 Ribbon常用负载规则
Riboon使用 IRule 接口,根据特定算法从所有服务中,选择一个服务。而IRule 接口有7个实现类。每个实现类代表一个负载均衡算法:
① RoundRobinRule 轮询
② RandomRule 随机
③ RetryRule 先按照 轮询 的策略获取服务,如果获取服务失败则在指定时间内会进行重试
④ WeightedResponseTimeRule 对轮询的扩展,响应速度越快的实例选择权重越大,越容易被选择
⑤ BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
⑥ AvailabilityFilteringRule 先过滤掉故障实例,再选择并发较小的实例
⑦ ZoneAvoidanceRule 默认规则,复合判断server所在区域的性能和server的可用性选择服务器
1.1.6 Ribbon负载规则替换
(1)新建package - com.atguigu.myrule
在com.atguigu.myrule下新建MySelfRule规则类
@Configuration
public class MySelfRule {
@Bean
public IRule myRule(){
return new RandomRule();//随机负载均衡算法
}
}
特别注意:这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类(负载均衡规则)就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。(也就是说不要将Ribbon配置类与主启动类同包)
修正:图片里的包应该是com.atguigu
小问题:既然都没扫描不到myrule这个包,你在组件上加了@Bean也没法添加进容器吧。那在这个包下的配置类myRule头上加@Bean注解还有什么意义呢,可不可以不加,毕竟都没扫描这个包了,难道加了@Bean注解还能进入spring容器吗
(2)在主启动类上加@RibbonClient注解,表示访问CLOUD-PAYMENT-SERVICE服务时,使用我们自定义的负载均衡算法
@SpringBootApplication
@EnableEurekaClient
//添加到此处
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class OrderMain80{
public static void main( String[] args ){
SpringApplication.run(OrderMain80.class, args);
}
}
1.2.1 Feign简介
Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
官网地址:https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/#spring-cloud-openfeign
Github地址:https://github.com/spring-cloud/spring-cloud-openfeign
1.2.2 Feign能干什么?
Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。
(1)Feign集成了Ribbon
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。
(2)Feign与OpenFeign
① Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。(已过期)
② OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@Feignclient可以解析SpringMVc的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
特别说明:Feign已经停止维护,过期了。OpenFeign作为他的继承者,优化升级了Feign,后文说的Feign都是OpenFeign
接口+注解:微服务调用接口 + @FeignClient
(1)新建一个order子工程,用于feign测试,名为cloud-consumer-feign-order80
(2)改POM
特别注意:openfeign依赖
LearnCloud
com.atguigu.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-consumer-feign-order80
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
com.atguigu.springcloud
cloud-api-commons
${project.version}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(3)改YML
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
(4)主启动类
@SpringBootApplication
@EnableFeignClients //开启feign
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
(5)业务类
① Service接口
新建 项目\cloud-consumer-feign-order80(Module)\src\main\java\com\atguigu\springcloud\service 包
编写PaymentFeignService.java接口并新增注解@FeignClient
业务逻辑接口+@FeignClient配置(consumer调用provider服务)
@Component @FeignClient(value = "CLOUD-PAYMENT-SERVICE") public interface PaymentFeignService{ @GetMapping(value = "/payment/get/{id}") public CommonResult
getPaymentById(@PathVariable("id") Long id); } 备注:去CLOUD-PAYMENT-SERVICE 所在的工程(负载均衡会选择去哪个具体工程调用实现类)找该调用该接口的实现类(Provider)
Feign自带负载均衡配置项
② Controller
package com.atguigu.springcloud.controller;
@RestController
@Slf4j
@RequestMapping("/consumer")
public class OrderFeginController {
@Resource
private PaymentService paymentService;
@GetMapping("/payment/get/{id}")
public CommonResult getPyamentById(@PathVariable("id")Long id){
return paymentService.getPaymentById(id);
}
}
(6)测试
先启动2个eureka集群7001/7002
再启动2个微服务8001/8002
启动OpenFeign启动
http://localhost/consumer/payment/get/1
Feign自带负载均衡配置项
OpenFeign 默认等待时间是1秒,超过1秒Provider没得反应,就会直接报错,如下图:
设置超时时间,修改配置文件
#设置feign客户端超时时间(OpenFeign默认支持ribbon)(单位:毫秒)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
(1)日志打印功能(配置好以后,自动打印)
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign 中 Http请求的细节。
说白了就是对Feign接口的调用情况进行监控和输出
(2)日志级别
(3)用spring注解创建日志Bean
@Configuration
public class FeignConfig
{
@Bean
Logger.Level feignLoggerLevel()
{
return Logger.Level.FULL; //日志级别
}
}
(4)YML文件里需要开启日志的Feign客户端
logging:
level:
# feign日志以什么级别监控哪个接口
com.atguigu.springcloud.service.PaymentFeignService: debug
(5)启动微服务即可
2.1 服务雪崩
① 分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
② 服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”.
对于高流量的应用来说,单一的后避依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
2.2.1 简介
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
"断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
① 服务降级:比如当某个服务繁忙,不能让客户端的请求一直等待,应该立刻返回给客户端一个备选方案。
② 服务熔断:当某个服务出现问题,卡死了,不能让用户一直等待,需要关闭所有对此服务的访问然后调用服务降级。
类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
服务的降级 -> 进而熔断 -> 恢复调用链路
③ 服务限流:比如秒杀场景,不能访问用户瞬间都访问服务器,限制一次只可以有多少请求。
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。
④ 接近实时的监控
比如当某个服务繁忙,不能让客户端的请求一直等待,应该立刻返回给客户端一个备选方案。
出现降级的情况:
① 程序运行导常
② 超时
③ 服务熔断触发服务降级
④ 线程池/信号量打满也会导致服务降级
2.3.1 Hystrix支付微服务构建
将上文的cloud-eureka-server7001子工程改配置成单机版
(1)创建带降级机制的子工程(Module)cloud-provider-hygtrix-payment8001
重点是Hystrix相关的依赖
cloud2020
com.atguigu.springcloud
1.0-SNAPSHOT
4.0.0
cloud-provider-hystrix-payment8001
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
com.atguigu.springcloud
cloud-api-commons
${project.version}
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(2)改YML
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka
# defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
(3)主启动类
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001{
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
(4)业务类
① Service
说明:本来Service应该写接口和接口实现类(接口实现类再调DAO),这里老师为了节约时间,就直接全部写在Service里了
@Service
public class PaymentService {
//正确的方法
public String paymentInfo_OK(Integer id){
return "线程池: "+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"O(∩_∩)O哈哈~";
}
//会超时报错的方法
public String paymentInfo_TimeOut(Integer id){
try {
TimeUnit.MILLISECONDS.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): 3";
}
}
② Controller
@RestController
@Slf4j
public class PaymentController{
@Resource
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){
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*****result: "+result);
return result;
}
}
(5)测试
① 正常测试
启动eureka7001
启动cloud-provider-hystrix-payment8001
访问
success的方法 - http://localhost:8001/payment/hystrix/ok/1
每次调用耗费5秒钟 - http://localhost:8001/payment/hystrix/timeout/1
上述module均OK
以上述为根基平台,从正确 -> 错误 -> 降级熔断 -> 恢复。
总结:上述 正常测试 在非高并发情形下,还能勉强满足
② 用JMETER进行高并发高压测试
Jmeter是一个web压力测试工具,可以模拟大型高并发场景
Jmeter官网:https://jmeter.apache.org/index.html
开启Jmeter,来20000个并发压死8001,20000个请求都去访问paymentInfo_TimeOut服务
1.测试计划中右键添加-》线程-》线程组(线程组202102,线程数:200,线程数:100,其他参数默认)
2.刚刚新建线程组202102,右键它-》添加-》取样器-》Http请求-》基本 输入http://localhost:8001/payment/hystrix/ok/1
3.点击绿色三角形图标启动。
看演示结果:拖慢,需要进行等待
原因:tomcat的默认的工作线程数被打满了,没有多余的线程来分解压力和处理。
2.3.2 创建带降级的order模块
(1)新建Module cloud-consumer-feign-hystrix-order80
(2)修改POM
LearnCloud
com.atguigu.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-consumer-feign-hystrix-order80
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
com.atguigu.springcloud
cloud-api-commons
${project.version}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(3)改YML
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
(4)主启动类
@SpringBootApplication
@EnableFeignClients
//@EnableHystrix
public class OrderHystrixMain80{
public static void main(String[] args){
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
(5)业务类
① 远程调用pay模块的接口
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" /*,fallback = PaymentFallbackService.class*/)
public interface PaymentHystrixService{
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
② controller
@RestController
@Slf4j
public class PaymentController{
@Resource
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){
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*****result: "+result);
return result;
}
}
(6)测试
① 正常测试
http://localhost/consumer/payment/hystrix/ok/1
② 高并发测试
启动order模块,2W个线程压8001消费端80微服务再去访问正常的Ok微服务8001地址
http://localhost/consumer/payment/hystrix/ok/32
消费者80被拖慢
可能原因:8001同一层次的其它接口服务被困死,因为tomcat线程池里面的工作线程已经被挤占完毕。
解决方案:正因为有上述故障或不佳表现才有我们的降级/容错/限流等技术诞生。
2.3.3 配置服务降级
(1)Provider 自身找问题
回到cloud-provider-hygtrix-payment8001 子工程
(1.1)配置Service
降级配置注解:@HystrixCommand
设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处埋,作服务降级fallback。(commandProperties 可以配置峰值)
—旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand标注好的fallbackMethod调用类中的指定方法
故意制造异常:
① int age = 10/0,计算异常
② 我们能接受3秒钟,它运行5秒钟,超时异常。
当前服务不可用了,做服务降级,兜底的方案都是paymentInfo_TimeOutHandler
@Service
public class PaymentService{
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler"/*指定善后方法名*/,commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
})//设置调用超时时间的峰值,超时则会调用设置的paymentInfo_TimeOutHandler方法
public String paymentInfo_TimeOut(Integer id)
{
//int age = 10/0;
try { TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): ";
}
//用来善后的方法
public String paymentInfo_TimeOutHandler(Integer id)
{
return "线程池: "+Thread.currentThread().getName()+" 8001系统繁忙或者运行报错,请稍后再试,id: "+id+"\t"+"o(╥﹏╥)o";
}
}
(1.2)还是用原来的Controller
(1.3)主启动类
添加新注解@EnableCircuitBreaker
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker//添加到此处
public class PaymentHystrixMain8001{
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
(1.4)测试
故意访问8001工程的timeout方法,发生服务降级
(2)consumer 订单侧找问题
一般服务降级,都是放在客户端。因此80订单微服务,也可以更好的保护自己,自己也依样画葫芦进行客户端降级保护
题外话:我们自己配置过的热部署(前文讲的热部署工具)方式对java代码的改动明显
但对@HystrixCommand内属性的修改建议重启微服务
(2.1)回到 cloud-consumer-feign-hystrix-order80 子工程
(2.2)修改YML配置文件
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
#开启
feign:
hystrix:
enabled: true
(2.3)主启动类
添加@EnableHystrix,启用hystrix
@SpringBootApplication
@EnableFeignClients
@EnableHystrix//添加到此处
public class OrderHystrixMain80{
public static void main(String[] args){
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
(2.4)修改controller【pay模块timeout改为3s,限制5s】
@RestController
@Slf4j
public class OrderHystirxController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
//int age = 10/0;
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
//善后方法
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
}
(2.5)测试
order访问pay需要3s,而自己只运行1.5s,所以会在order中降级
(3)现有降级方法的问题
① 每个业务方法都对应一个服务降级方法,重复代码多,代码膨胀
② 降级方法与业务方法写在了一块,耦合度高
解决:重复代码多
除了个别重要核心业务有专属,其它普通的可以通过@DefaultProperties(defaultFallback = “”)统一跳转到统一处理结果页面,通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量
在消费者cloud-consumer-feign-hystrix-order80 子工程中 package com.atguigu.springcloud.controller;
(1)创建一个全局降级方法paymentInfo_Global_FallbackMethod()
(2)使用@DefaultProperties注解指定其为全局降级方法(默认降级方法)
说明:业务方法不指定具体降级方法,就会使用默认降级方法统一处理。下面代码也体现了,如果用@@HystrixCommand指定了降级方法,则会走自己的降级方法
package com.atguigu.springcloud.controller;
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystirxController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
// @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties ={
// @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
// })
@HystrixCommand//用全局的fallback方法
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
//int age = 10/0;
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
//特定降级方法
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
// 下面是全局fallback方法
public String payment_Global_FallbackMethod(){
return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
}
}
解决:代码耦合度高的问题
目前业务代码和降级代码都写在一个类里。代码混乱
(1)继续修改 消费者 order模块,这里开始,8001pay模块(provider)就不服务降级了,服务降级写在order模块即可。只需要为Feign客户端定义的接口(此案例是PaymentHystrixService接口)添加一个服务降级处理的实现类即可实现解耦
① 回顾:原PaymentHystrixService接口
如果没出现异常或者超时,就通过HTTP去正常调用provider。如果异常,则直接去自己的实现类PaymentFallbackService,实现服务降级
package com.atguigu.springcloud.service;
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" ,fallback = PaymentFallbackService.class)//指定PaymentFallbackService类
public interface PaymentHystrixService{
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
② 将降级代码从Controller分离,写在Service包下
PaymentFallbackService类实现PaymentHystrixService接口,发生异常,就在自己的工程里面解决啦
package com.atguigu.springcloud.service;
@Component
public class PaymentFallbackService implements PaymentHystrixService{
@Override
public String paymentInfo_OK(Integer id){
return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o";
}
@Override
public String paymentInfo_TimeOut(Integer id){
return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o";
}
}
③ 改YML
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
#开启
feign:
hystrix:
enabled: true
(2)启动测试
Step1: 单个eureka先启动7001
Step2: PaymentHystrixMain8001启动
Step3: 正常访问测试 - http://localhost/consumer/payment/hystrix/ok/1
Step4: 故意关闭微服务8001(provider)
客户端自己调用提示 - 此时服务端provider已经down了,但是我们做了服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器。
2.4.1 概述
熔断机制是应对雪崩效应的一种微服务链路保护机制。当某个服务出现问题时
需要关闭所有对此服务的访问【断路器】,然后调用服务降级。
当检测到该服务响应正常后,恢复调用链路。
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。
熔断类型
2.4.2 修改cloud-provider-hystrix-payment8001 子工程
(1)改Service
Hutool国产工具类(paymentCircuitBreaker方法中用了Hutool工具类)
package com.atguigu.springcloud.service;
@Service
public class PaymentService{
...
//=====服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失败率达到多少后跳闸
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
if(id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID(); //hutool工具的方法
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " +id;
}
}
对于配置的参数说明:
- 如果并发超过10个或者10个并发中失败了6个,就会开启断路器
- 在时间窗口期10秒之内会尝试请求,如果请求成功就会关闭断路器
附录:
- HystrixCommandProperties 默认配置
package com.netflix.hystrix; ... public abstract class HystrixCommandProperties { private static final Logger logger = LoggerFactory.getLogger(HystrixCommandProperties.class); /* defaults */ /* package */ static final Integer default_metricsRollingStatisticalWindow = 10000;// default => statisticalWindow: 10000 = 10 seconds (and default of 10 buckets so each bucket is 1 second) private static final Integer default_metricsRollingStatisticalWindowBuckets = 10;// default => statisticalWindowBuckets: 10 = 10 buckets in a 10 second window so each bucket is 1 second private static final Integer default_circuitBreakerRequestVolumeThreshold = 20;// default => statisticalWindowVolumeThreshold: 20 requests in 10 seconds must occur before statistics matter private static final Integer default_circuitBreakerSleepWindowInMilliseconds = 5000;// default => sleepWindow: 5000 = 5 seconds that we will sleep before trying again after tripping the circuit private static final Integer default_circuitBreakerErrorThresholdPercentage = 50;// default => errorThresholdPercentage = 50 = if 50%+ of requests in 10 seconds are failures or latent then we will trip the circuit private static final Boolean default_circuitBreakerForceOpen = false;// default => forceCircuitOpen = false (we want to allow traffic) /* package */ static final Boolean default_circuitBreakerForceClosed = false;// default => ignoreErrors = false private static final Integer default_executionTimeoutInMilliseconds = 1000; // default => executionTimeoutInMilliseconds: 1000 = 1 second private static final Boolean default_executionTimeoutEnabled = true; ... }
- 涉及到断路器的三个重要参数:
① 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
② 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次7,即使所有的请求都超时或其他原因失败,断路器都不会打开。
③ 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。
(2)修改Controller
添加一个测试方法 paymentCircuitBreaker()
@RestController
@Slf4j
public class PaymentController
{
@Resource
private PaymentService paymentService;
...
//====服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id)
{
String result = paymentService.paymentCircuitBreaker(id);
log.info("****result: "+result);
return result;
}
}
(3)测试
自测cloud-provider-hystrix-payment8001
正确 - http://localhost:8001/payment/circuit/1
错误 - http://localhost:8001/payment/circuit/-1
多次错误,并且错误率超过60%,服务熔断。此时就算再次正确方案,也会报错
等待窗口期过了,自动取消熔断,再次正确访问,就能成功了
2.4.3 熔断服务总结
(1)熔断整体流程
① 请求进来,首先查询缓存,如果缓存有,直接返回;如果缓存没有,则到2.
② 查看断路器是否开启,如果开启的,Hystrix直接将请求转发到降级返回,然后返回;如果断路器是关闭的,则到3。
③ 判断线程池等资源是否已经满了,如果已经满了也会走降级方法,如果资源没有满,则到4。
④ 判断我们使用的什么类型的Hystrix,决定调用构造方法还是run方法,然后处理请求
⑤ 然后Hystrix将本次请求的结果信息汇报给断路器,断路器收到信息,判断是否符合开启或关闭断路器的条件。
⑥ 如果本次请求处理失败,又会进入降级方法;如果处理成功,判断处理是否超时,如果超时了也进入降级方法。没有超时,则本次请求处理成功,将结果返回给controller。0
(2)断路器开启或者关闭的条件
到达以下阀值,断路器将会开启:
① 当满足一定的阀值的时候(默认10秒内超过20个请求次数)
② 当失败率达到一定的时候(默认10秒内超过50%的请求失败)
③ 当开启的时候,所有请求都不会进行转发
④ 一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。
(3)All配置
都有默认配置,看不懂的参数,不写就好(自动用默认的)
@HystrixCommand(fallbackMethod = "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 doSomething() {
...
}
2.5.1 HystrixDashboard 简介
HystrixDashboard是Hystrix提供的准实时调用监控,它记录了所有Hystrix发起的请求的执行信息,并以图形化的形式显示,包括每秒执行多少请求多少成功,多少失败等。
2.5.2 HystrixDashboard的使用
(1)新建子工程cloud-consumer-hystrix-dashboard9001(Module)
(2)修改POM
LearnCloud
com.atguigu.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-consumer-hystrix-dashboard9001
//dashboard重点就是这个依赖
org.springframework.cloud
spring-cloud-starter-netflix-hystrix-dashboard
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(3)YML配置文件
server:
port: 9001
(4)主启动类HystrixDashboardMain9001
新注解@EnableHystrixDashboard
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001{
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class, args);
}
}
(5)修改想被监控的子工程(一般都是监控Provider)
① POM里面添加监控依赖actuator
org.springframework.boot
spring-boot-starter-actuator
所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置
② 主启动类
新版本Hystrix需要在主启动类PaymentHystrixMain8001中指定监控路径
修改cloud-provider-hystrix-payment8001(8002/8003和这个类似)
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001{
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
/**
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
*只要在自己的项目里配置上下面的servlet就可以了
*否则,Unable to connect to Command Metric Stream 404
*/
@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;
}
}
(6)测试
启动eureka
启动微服务7001,8001,9001,访问: http://localhost:9001/hystrix
然后在web界面,指定9001要监控8001:填写监控地址 - http://localhost:8001/hystrix.stream 到 http://localhost:9001/hystrix页面的输入框。
扩展:监控图说明:
(1)7色
(2)1圈
实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色<黄色<橙色<红色递减。
该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压力实例。
(3)1线
曲线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。
(4)整图说明
3.1 微服务中的网关位置
3.2 GateWay简介
SpringCloud Gateway作为Spring Cloud 生态系统中的网关,目标是替代Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。
Gateway 是在 Spring 生态系统之上构建的 API 网关服务,基于 Spring 5、Spring Boot 2 和 Project Reactor 等技术。它提供了一种简单且有效的方式来对 API 进行路由,并提供了一些强大的过滤器功能(熔断、限流、重试等)。
Gateway 的底层是基于 WebFlux 框架实现的,而webFlux底层使用netty通信(NIO)。
3.2.1 Gateway作用
3.2.2 GateWay的新特性:
① 基于 Spring 5、Spring Boot 2 和 Project Reactor 进行构建
② 动态路由,能够匹配任何请求属性
③ 可以对路由指定和边写 Predicate(断言)和 Filter(过滤器)
④ 集成 Hystrix 的断路器功能
⑤ 集成 Spring Cloud 服务发现功能
⑥ 请求限流功能、支持路径重写
3.2.3 GateWay 与 zuul
① Zuul 1.x 是一个基于 Servlet 2.5 的阻塞架构,性能较差。而 2.x 版本基于 Netty 非阻塞并支持长连接,但与 Spring Cloud 目前还没整合。
② Gateway 使用非阻塞 API,支持 WebSocket 且与 Spring 紧密集成。
3.3 Gateway 工作流程
3.3.1 三大核心概念
(1)Predicate(断言) - 参考的是Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由;
(2)Route(路由) - 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由;
(3)Filter(过滤) - 指的是Spring框架中Gateway Filter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件;而fliter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
3.3.2 工作过程
官网:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#gateway-how-it-works
(1)客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到GatewayWeb Handler。
Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
(2)过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post")执行业务逻辑。
核心逻辑:路由转发 + 执行过滤器链。
(1)新建 cloud-gateway-gateway9527子工程(Module)
(2)改POM(重点是新增网关依赖)
LearnCloud
com.atguigu.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-gateway-gateway9527
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
com.atguigu.springcloud
cloud-api-commons
${project.version}
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(3)改YML
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
(4)主启动类
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527{
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class, args);
}
}
(5)业务类
无
(6)新增网关配置(重点)
① 回到 cloud-provider-payment8001子工程,看看controller的访问地址
② 在YML中配置网关
server:
port: 9527
spring:
application:
name: cloud-gateway
#############################新增网关配置###########################
cloud:
gateway:
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/** # 断言,路径相匹配的进行路由
####################################################################
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
(7)测试
① 启动7001 注册中心
② 启动8001-cloud-provider-payment8001 支付微服务
③ 启动9527 网关
④访问
添加网关前 - http://localhost:8001/payment/get/1
添加网关后 - http://localhost:9527/payment/get/1 9527帮你直接路由到目标地址(http://localhost:8001/payment/get/1)
两者访问成功,返回相同结果
备注:除了YML文件方式配置网关,也可使用代码配置网关。但是有配置文件,写什么代码,这里就不讲啦
有兴趣的,可以自行研究 https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/html/#modifying-the-way-remote-addresses-are-resolved
上面的配置虽然首先了网关,但是是在配置文件中写死了要路由的地址(uri: http://localhost:8001)。
我们可以修改为,从注册中心获取某组微服务的地址,然后根据微服务名字进行动态路由
(1)动态路由使用方法
修改YML配置文件,需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能。
lb://serviceName是spring cloud gateway在微服务中自动为我们创建的负载均衡uri。
server:
port: 9527
spring:
application:
name: cloud-gateway
#############################新增网关配置###########################
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
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/** # 断言,路径相匹配的进行路由
####################################################################
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
(2)测试
① 启动7001,8001,8002,9527服务
② 浏览器输入 - http://localhost:9527/payment/lb
结果:
不停刷新页面,8001/8002两个端口切换。
回顾:我们之前在配置文件(上文3.5)中配置了断言:
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
Gateway 包括许多内置的 Route Predicate 工厂,所有这些 Predicate 都与 HTTP 请求的不同属性匹配,且多个 Route Oredicate 工厂可以进行组合并通过逻辑and。之前我们配置的Path,就是由其中一种工厂创建的。断言还有以下的类型:
(1)The After Route Predicate Factory :After
可以指定,只有在指定时间后,才可以路由到指定微服务,在指定的时间之前访问,都会报404。
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
# 这个时间后才能起效
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
特别说明:可以通过下述方法获得上述格式的时间戳字符串
public class T2{
public static void main(String[] args){
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
System.out.println(zbj);
//2021-02-22T15:51:37.485+08:00[Asia/Shanghai]
}
}
(2)The Between Route Predicate Factory :Between
需要指定两个时间,在他们之间的时间才可以访问
spring:
cloud:
gateway:
routes:
- id: between_route
uri: https://example.org
# 两个时间点之间
predicates:
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
(3)The Cookie Route Predicate Factory:cookie
只有包含某些指定cookie(key,value),的请求才可以路由
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: https://example.org
predicates:
- Cookie=chocolate, ch.p
#表示只有cookie包含username,并且value是zzyy的才可以路由
测试:
# 该命令相当于发get请求,且没带cookie
curl http://localhost:9527/payment/lb
# 带cookie的
curl http://localhost:9527/payment/lb --cookie "chocolate=chip"
扩展:Linux 命令 curl
在Linux中curl是一个利用URL规则在命令行下工作的文件传输工具,可以说是一款很强大的http命令行工具。它支持文件的上传和下载,是综合传输工具,但按传统,习惯称url为下载工具。
(4)The Header Route Predicate Factory: Header
只有包含指定请求头的请求,才可以路由
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+
测试:
# 带指定请求头的参数的CURL命令
curl http://localhost:9527/payment/lb -H "X-Request-Id:123"
(5)Host:只有指定主机的才可以访问
(6)Method:只有指定请求才可以路由,比如get请求…
(7)Path:只有访问指定路径,才进行路由,我们已经用过了
(8)Query:必须带有请求参数才可以访问
(9)其他的,举一反三
(1)简介
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,Spring Cloud Gateway 内置了多种路由过滤器,由 GatewayFilter 的工厂类生成,过滤器的类型可以在官网查看。(过滤器用来修改HTTP请求,容易让人晕)
总结:所谓过滤,就是在进入的HTTP请求 和 返回的HTTP响应之前,将框架代码 拦截下来。用自己的代码切入,做点自己想做的事情
官方文档:https://cloud.spring.io/spring-cloud-gateway/reference/html/#gatewayfilter-factories
(2)种类
GatewayFilter:单一的过滤器 有31种,具体看官方文档
GlobalFilter:全局过滤器 有10种
常用的GatewayFilter:AddRequestParameter 和 GatewayFilter
(3)生命周期
在请求进入路由之前,和处理请求完成,再次到达路由之前
(4)使用方法
(4.1)使用自带的Filter
(1)以常用的AddRequestParameter 为例(独立案例,和以上所有子工程无关)
通过名称我们可以快速的明白这个过滤器工厂的作用,就是添加请求头。
spring:
cloud:
gateway:
routes:
- id: fsh-house
uri: lb://fsh-house
predicates:
- Path=/house/**
filters:
- AddRequestHeader=NAME, yinjihuan
上面的配置,我们针对fsh-house这个服务的路由配置了AddRequestHeader,增加了一个名称为NAME,值是yinjihuan的请求头。
这样配置之后,在fsh-house服务中的所有接口,都可以通过request来获取到NAME请求头的信息,代码如下:
@GetMapping("/hello")
public String hello(HttpServletRequest request) throws Exception {
System.err.println(request.getHeader("NAME"));
return "Hello"+serverPort;
}
(2)RewritePath(重点:项目中常用)
改写http的url路径。RewritePath A,B:重写路径将A重写为B
默认有 /api/ 前缀先路由负载均衡到 renren-fast,放到最后优先级最后
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?.*),/renren-fast/$\{segment}
注意:由于yml语法规则,$\用了转义字符,$\就是$的意思
(4.2)自定义全局GlobalFilter
① 实现 GlobalFilter接口 和 Ordered接口
② GateWay9527项目package com.atguigu.springcloud.filter下添加MyLogGateWayFilter类:
package com.atguigu.springcloud.filter;
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter,Ordered{
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain){
log.info("***********come in MyLogGateWayFilter: "+new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if(uname == null)
{
log.info("*******用户名为null,非法用户,o(╥﹏╥)o");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder(){
return 0;
}
}
③测试
Step1 启动:
Step2 浏览器输入:
4.1 分布式系统面临的配置问题
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。
SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,上百个配置文件的管理.……
4.2 Config分布式配置中心简介
https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
(1)能干嘛:
① 集中管理配置文件
② 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
③ 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
④ 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
⑤ 将配置信息以REST接口的形式暴露 - post/crul访问刷新即可…
(2)怎么玩
SpringCloud Config分为服务端和客户端两部分。
① 服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。
② 客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。
(3)与GitHub整合配置
由于SpringCloud Config默认使用Git来存储配置文件(也有其它方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是http/https访问的形式。
4.3.1 在GitHub上建立远程仓库
复习:Git https://blog.csdn.net/chengqingshihuishui/article/details/110910846
(1)用你自己的账号在GitHub上新建一个名为springcloud-config的新Repository。
(2)由上一步获得刚新建的git地址 - [email protected]:abc/springcloud-config.git。
本地硬盘目录上新建git仓库并clone 远端内容到本地仓库。
① 工作目录为D:\SpringCloud2021
② git clone [email protected]:abc/springcloud-config.git
此时,工作目录会创建名为springcloud-config的文件夹
(3)在springcloud-config的文件夹中创建三个配置文件(为本次教学使用的),随后git add .
,git commit -m "sth"
等一系列上传操作上传到springcloud-config的新Repository。
① config-dev.yml
config:
info: "master branch,springcloud-config/config-dev.yml version=7"
② config-prod.yml
config:
info: "master branch,springcloud-config/config-prod.yml version=1"
③ config-test.yml
config:
info: "master branch,springcloud-config/config-test.yml version=1"
(1)建Module cloud-config-center-3344
该工程即为Cloud的配置中心模块CloudConfig Center
(2)改POM
LearnCloud
com.atguigu.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-config-center-3344
org.springframework.cloud
spring-cloud-config-server
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(3)改YML
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: [email protected]:zzyybs/springcloud-config.git #GitHub上面的git仓库名字
####搜索目录
search-paths:
- springcloud-config
####读取分支
label: master
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
(4)主启动类
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344{
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain3344.class, args);
}
}
(5)修改hosts文件
由于是模拟访问域名地址,因此需要在本机hosts文件添加映射
127.0.0.1 config-3344.com
(6)测试
测试通过Config微服务是否可以从GitHub上获取配置内容
启动ConfigCenterMain3344
浏览器防问 - http://config-3344.com:3344/master/config-dev.yml 规则:/{label}/{application}-{profile}.yml(推荐)
页面返回结果:
config:
info: "master branch,springcloud-config/config-dev.yml version=7"
4.3.3 配置读取规则:https://cloud.spring.io/spring-cloud-static/spring-cloud-config/2.2.1.RELEASE/reference/html/#_quick_start
① label:分支(branch)
② profiles:环境(dev/test/prod)
(1)官方推荐规则:/{label}/{application}-{profile}.yml(推荐)
① 读取master分支演示:
http://config-3344.com:3344/master/config-dev.yml
http://config-3344.com:3344/master/config-test.yml
http://config-3344.com:3344/master/config-prod.yml
② 读取dev分支演示
http://config-3344.com:3344/dev/config-dev.yml
http://config-3344.com:3344/dev/config-test.yml
http://config-3344.com:3344/dev/config-prod.yml
(2)规则2 /{application}-{profile}.yml
http://config-3344.com:3344/config-dev.yml
http://config-3344.com:3344/config-test.yml
http://config-3344.com:3344/config-prod.yml
http://config-3344.com:3344/config-xxxx.yml(不存在的配置)
(3)规则3 /{application}/{profile}[/{label}]
http://config-3344.com:3344/config/dev/master
http://config-3344.com:3344/config/test/master
http://config-3344.com:3344/config/test/dev
(1)新建cloud-config-client-3355 子工程
(2)改POM
LearnCloud
com.atguigu.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-config-client-3355
org.springframework.cloud
spring-cloud-starter-config
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(3)改YML——这里是bootstrap.yml
applicaiton.yml是用户级的资源配置项
bootstrap.yml是系统级的,优先级更加高
Spring Cloud会创建一个Bootstrap Context,作为Spring应用的Application Context的父上下文。初始化的时候,BootstrapContext负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的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。
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址k
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
特别注意:Config的配置中心是上一节的3344子工程
(4)主启动类
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3355{
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class, args);
}
}
(5)业务类
@RestController
public class ConfigClientController{
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo(){
return configInfo;
}
}
客户端启动后,就自动完成远端配置文件的同步。在web浏览器访问/configInfo,就已经能够读取到远端配置文件的内容了
(6)测试
Step1: 启动Config配置中心3344微服务并自测
http://config-3344.com:3344/master/config-prod.yml
http://config-3344.com:3344/master/config-dev.yml
Step2 :启动3355作为Client准备访问
http://localhost:3355/configlnfo
成功实现了客户端3355访问SpringCloud Config3344通过GitHub获取配置信息,可是会遇到新的分布式配置动态刷新问题
(7)分布式配置的动态刷新问题
① Linux运维修改GitHub上的配置文件内容做调整
② 刷新3344,发现ConfigServer配置中心立刻响应
③ 刷新3355,发现ConfigClient客户端没有任何响应
④ 3355没有变化除非自己重启或者重新加载
⑤ 难到每次运维修改配置文件,客户端都需要重启??噩梦
目的:避免每次更新配置都要重启客户端微服务3355
修改3355子工程(Module)
(1)POM新增actuator监控
org.springframework.boot
spring-boot-starter-actuator
(2)修改YML,添加暴露监控端口的配置
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
(3)业务类 Controller 新增 @RefreshScope
@RestController
@RefreshScope
public class ConfigClientController{
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo(){
return configInfo;
}
}
(4)测试
此时修改github配置文件内容 -> 访问3344 -> 访问3355
http://localhost:3355/configInfo
3355改变没有??? 没有,还需一步
需要运维人员发送Post请求刷新3355
curl -X POST "http://localhost:3355/actuator/refresh"
再次测试
http://localhost:3355/configInfo
3355改变没有??? 改了。
(5)新的问题
虽然成功实现了客户端3355刷新到最新配置内容,避免了服务重启,但是还有以下问题:
4.4.1 Bus消息总线解决的问题
一言以蔽之,分布式自动刷新配置功能。
Spring Cloud Bus 配合 Spring Cloud Config使用可以实现配置的动态刷新。
4.4.2 消息总线简介
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
ConfigClient实例都监听MQ中同一个topic(默认是Spring Cloud Bus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。
Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。Spring Clud Bus目前支持RabbitMQ和Kafka。
4.4.3 能干嘛
Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。
(1)安装Erlang,下载地址:http://erlang.org/download/otp_win64_21.3.exe
(2)安装RabbitMQ,下载地址:https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.3/rabbitmq-server-3.8.3.exe
(3)打开cmd进入RabbitMQ安装目录下的sbin目录,如:D:\devSoft\RabbitMQ Scrverk\rabbitmq_server-3.7.14\sbin
(4)输入以下命令启动管理功能
rabbitmq-plugins enable rabbitmq _management
这样就可以添加可视化插件。
(5)访问地址查看是否安装成功:http://localhost:15672/
(6)输入账号密码并登录:guest guest
4.4.5 设计思想
(1)利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置(不合理思想)
(2)利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置
(3)结论
图二的架构显然更加适合,图—不适合的原因如下:
① 打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责。
② 破坏了微服务各节点的对等性。
③ 有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修改。
4.4.6 案例演示cloud-config-client-3366
(1)新建cloud-config-client-3366 子工程
为了演示广播效果,再以3355为模板再制作一个3366子工程
(2)改POM
LearnCloud
com.lun.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-config-client-3366
org.springframework.cloud
spring-cloud-starter-bus-amqp
org.springframework.cloud
spring-cloud-starter-config
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(3)改YML
server:
port: 3366
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址
#rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
(4)主启动
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientMain3366{
public static void main(String[] args){
SpringApplication.run(ConfigClientMain3366.class,args);
}
}
(5)controller
@RestController
@RefreshScope
public class ConfigClientController{
@Value("${server.port}")
private String serverPort;
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String configInfo(){
return "serverPort: "+serverPort+"\t\n\n configInfo: "+configInfo;
}
}
给cloud-config-center-3344配置中心服务端添加消息总线支持
(1)回到cloud-config-center-3344
(1.1)改POM
新增兔子MQ支持,和监控actuator
LearnCloud
com.atguigu.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-config-center-3344
org.springframework.cloud
spring-cloud-config-server
org.springframework.cloud
spring-cloud-starter-bus-amqp
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(1.2)改YML
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: [email protected]:zzyybs/springcloud-config.git #GitHub上面的git仓库名字
####搜索目录
search-paths:
- springcloud-config
####读取分支
label: master
#rabbitmq相关配置<--------------------------
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
##rabbitmq相关配置,暴露bus刷新配置的端点<--------------------------
management:
endpoints: #暴露bus刷新配置的端点
web:
exposure:
include: 'bus-refresh'
(2)同样 回到cloud-config-client-3355子工程
(2.1)改POM
客户端添加消息总线支持
LearnCloud
com.atguigu.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-config-client-3355
org.springframework.cloud
spring-cloud-starter-bus-amqp
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-config
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(2.2)改YML
server:
port: 3366
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址
#rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口<-----------------------
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
(3)测试
(3.1)启动
EurekaMain7001
ConfigcenterMain3344
ConfigclientMain3355
ConfigclicntMain3366
(3.2)运维工程师修改Github上配置文件内容,增加版本号
发送POST请求 curl -X POST "http://localhost:3344/actuator/bus-refresh"
实现“—次发送,处处生效”
(3.3)配置中心
http://config-3344.com:3344/config-dev.yml
(3.4)客户端
http://localhost:3355/configlnfo
http://localhost:3366/configInfo
获取配置信息,发现都已经刷新了。—次修改,广播通知,处处生效
(1)需求:不想全部通知,只想定点通知
① 只通知3355
② 不通知3366
简单一句话 - 指定具体某一个实例生效而不是全部
运维工程师只需要按以下公式推送即可
公式:http://localhost:3344/actuator/bus-refresh/{destination}
/bus/refresh请求不再发送到具体的服务实例上,而是发给config server通过destination参数类指定需要更新配置的服务或实例
(2)案例
我们这里以刷新运行在3355端口上的config-client(配置文件中设定的应用名称)为例,只通知3355,不通知3366
curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355
(3)通知总结:
5.1 Stream引入背景
常见MQ(消息中间件):
① ActiveMQ
② RabbitMQ https://blog.csdn.net/chengqingshihuishui/article/details/115712828
③ RocketMQ
④ Kafka
有没有一种新的技术诞生,让我们不再关注具体MQ的细节,我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换。(类似于Hibernate)
Cloud Stream是什么?屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。
官方文档:https://spring.io/projects/spring-cloud-stream#overview
中文手册:https://m.wang1314.com/doc/webapp/topic/20971999.html
官方定义Spring Cloud Stream是一个构建消息驱动微服务的框架,应用程序通过inputs或者 outputs 来与Spring Cloud Stream中binder对象交互。
通过我们配置来binding(绑定),而Spring Cloud Stream 的binder对象负责与消息中间件交互。所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。
目前仅支持RabbitMQ、 Kafka。
5.3.1 标准MQ
① 生产者/消费者之间靠消息媒介传递信息内容
② 消息必须走特定的通道 - 消息通道 Message Channel
③ 消息通道里的消息如何被消费呢,谁负责收发处理 - 消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅。
5.3.2 为什么用Cloud Stream( 简单地说,就是有jdbc为什么要用mybatis)
比方说我们的项目不同微服务之间分别用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic和Partitions分区。
这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候Spring Cloud Stream给我们提供了—种解耦合的方式。
5.3.3 Stream凭什么可以统一底层差异?(就是靠Binder)
在没有绑定器(Binder)这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
(1)Binder
① INPUT对应于消费者
② OUTPUT对应于生产者
(2)Stream中的消息通信方式遵循了发布-订阅模式
Topic主题进行广播
① 在RabbitMQ就是Exchange
② 在Kakfa中就是Topic
5.4.1 Spring Cloud Stream标准流程套路
① Binder - 很方便的连接中间件,屏蔽差异。
② Channel - 通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置。
③ Source和Sink - 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。
组成 | 说明 |
Middleware | 中间件,目前只支持RabbitMQ和Kafka |
Binder | Binder是应用与消息中间件之间的封装,目前实行了Kafka和RabbitMQ的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应于Kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现 |
@Input | 注解标识输入通道,通过该输乎通道接收到的消息进入应用程序 |
@Output | 注解标识输出通道,发布的消息将通过该通道离开应用程序 |
@StreamListener | 监听队列,用于消费者的队列的消息接收 |
@EnableBinding | 指信道channel和exchange绑定在一起 |
5.5.1 案例准备
(1)准备RabbitMQ环境(4.4.5_Bus之RabbitMQ环境配置有提及)
(2)工程中新建三个子模块
cloud-stream-rabbitmq-provider8801,作为生产者进行发消息模块
cloud-stream-rabbitmq-consumer8802,作为消息接收模块
cloud-stream-rabbitmq-consumer8803,作为消息接收模块
(1)新建Module:cloud-stream-rabbitmq-provider8801
(2)改POM
LearnCloud
com.atguigu.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-stream-rabbitmq-provider8801
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-stream-rabbit
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(3)改YML
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: send-8801.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
(4)主启动类 StreamMQMain8801
@SpringBootApplication
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class,args);
}
}
(5)业务类
① 发送消息接口
package com.atguigu.springcloud.service;
public interface IMessageProvider {
public String send();
}
② 接口实现类
package com.atguigu.springcloud.service.impl;
@EnableBinding(Source.class) //定义消息的推送管道
public class MessageProviderImpl implements IMessageProvider{
@Resource
private MessageChannel output; // 消息发送管道
@Override
public String send(){
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("*****serial: "+serial);
return null;
}
}
@EnableBinding
注释可以将一个或多个接口类作为参数,这些接口类包含表示可绑定组件(通常是消息通道)的方法。在创建消息生产者的时候用到了@EnableBinding(Source.class)
在创建消息消费者的时候用到了@EnableBinding(Sink.class)
扩展:Source.class与Sink.class(后面消息消费者的时候用)
(1)源码
public interface Source { String OUTPUT = "output"; @Output("output") MessageChannel output(); } public interface Sink { String INPUT = "input"; @Input("input") SubscribableChannel input(); }
其实这个input、output就是对应配置文件里面的内容,其实用户也是可以自定义管道信息的。
由于在tream-provider与stream-consumer这两个模块中都需要用到自定义管道信息,所以我们在系统api模块增加stream支持。
③ Controller
@RestController
public class SendMessageController{
@Resource
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage() {
return messageProvider.send();
}
}
(6)测试
① 启动 7001eureka
② 启动 RabpitMq(79_Bus之RabbitMQ环境配置)
rabbitmq-plugins enable rabbitmq_management
http://localhost:15672/
③ 启动 8801
④ 访问 - http://localhost:8801/sendMessage
后台将打印serial: UUID字符串
(1)新建子工程:cloud-stream-rabbitmq-consumer8802(Module)
(2)改POM
LearnCloud
com.atguigu.springcloud
1.0.0-SNAPSHOT
4.0.0
cloud-stream-rabbitmq-consumer8802
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-stream-rabbit
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
(3)修改YML
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: receive-8802.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
(4)主启动类
@SpringBootApplication
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class,args);
}
}
(5)业务类
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController{
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message message){
System.out.println("消费者1号,----->接受到的消息: "+message.getPayload()+"\t port: "+serverPort);
}
}
(6)测试
启动EurekaMain7001
启动StreamMQMain8801
启动StreamMQMain8802
8801发送8802接收消息
5.6.1 问题背景
(1)依照8802,克隆出来一份运行8803 - cloud-stream-rabbitmq-consumer8803
启动:
运行后有两个问题
① 有重复消费问题
② 消息持久化问题
(2)具体生产案例
比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中的消息分组来解决。
注意:在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次。不同组是可以全面消费的(重复消费)。
5.6.2.1 原理
微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。
不同的组是可以重复消费的,同一个组内会发生竞争关系,只有其中一个可以消费。
5.6.2.2 解决上文的消息重复消费
(1)修改 8802/8803 YML
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
group: A_Group #<----------------------------------------关键
重点是:都把group改成A_Group。此时8802/8803 产生竞争关系,就避免了重复消费
通过上述,解决了重复消费问题,再看看持久化。
停止8802/8803并去除掉8802的分组group: A_Group,8803的分组group: A_Group没有去掉。
8801先发送4条消息到RabbitMq。
先启动8802,无分组属性配置,后台没有打出来消息。
再启动8803,有分组属性配置,后台打出来了MQ上的消息。(消息持久化体现)
结论:只要给微服务配置了Group分组,就自动加入了消息持久化功能
6.1.1 技术背景
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
6.1.2 简介
官方项目文档:https://github.com/spring-cloud/spring-cloud-sleuth
Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案,在分布式系统中提供追踪解决方案并且兼容支持了zipkin
6.2.1 完整链路简介
表示一请求链路,一条链路通过Trace ld唯一标识,Span标识发起的请求信息,各span通过parent id关联起来
—条链路通过Trace ld唯一标识,Span标识发起的请求信息,各span通过parent id关联起来。
整个链路的依赖关系如下:
名词解释(看图更好理解)
6.2.2 Zipkin搭建并运行(就是安装一个普通软件)
说明:SpringCloud从F版起已不需要自己构建Zipkin Server了,只需调用jar包即可
(1)下载
https://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/
(2)运行zipkin
指令:zipkin-server-2.12.9-exec.jar
java -jar zipkin-server-2.12.9-exec.jar
(3)运行控制台
http://localhost:9411/zipkin/
(6.3.1)服务提供方(Provider)
(1)回到 cloud-provider-payment8001 支付子工程
(2)改POM
org.springframework.cloud
spring-cloud-starter-zipkin
(3)改YML
spring:
application:
name: cloud-payment-service
zipkin: #<-------------------------------------关键
base-url: http://localhost:9411
sleuth: #<-------------------------------------关键
sampler:
#采样率值介于 0 到 1 之间,1 则表示全部采集
probability: 1
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
url: jdbc:mysql://localhost:3306/db2019?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 123456
(4)业务类
@RestController
@Slf4j
public class PaymentController {
...
@GetMapping("/payment/zipkin")
public String paymentZipkin() {
return "hi ,i'am paymentzipkin server fall back,welcome to here, O(∩_∩)O哈哈~";
}
}
(6.3.2)服务消费者(Consumer)
(1)回到cloud-consumer-order80 子工程
(2)改POM
org.springframework.cloud
spring-cloud-starter-zipkin
(3)YML
spring:
application:
name: cloud-order-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1
(4)业务类OrderController
// ====================> zipkin+sleuth
@GetMapping("/consumer/payment/zipkin")
public String paymentZipkin(){
String result = restTemplate.getForObject("http://localhost:8001"+"/payment/zipkin/", String.class);
return result;
}
(6.3.3)测试
(1)依次启动eureka7001/8001/80 - 80调用8001几次测试下
(2)打开浏览器访问: http://localhost:9411
监控成功