服务治理的解决方案:
Eureka:Netflix公司
Consul:Spring开源组织直接贡献
Nacos:阿里服务治理中间件
Eureka | Consul | Nacos | |
---|---|---|---|
一致性 | 弱一致性(AP) | 弱一致性(AP) | AP/CP |
性能 | 快 | 慢(RAFT协议Leader选举) | 快 |
网络协议 | HTTP | HTTP&DNS | HTTP/DNS/UDP |
应用广度 | 主流 | 小众一些 | 发展中 |
spring-cloud-learn父工程的POM
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.icodingedu.springcloudgroupId>
<artifactId>spring-cloud-learnartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>pompackaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Hoxton.SR3version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.5.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.12version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.6.0version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
plugins>
build>
project>
创建子项目eureka-server的POM文件
<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>spring-cloud-learnartifactId>
<groupId>com.icodingedu.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>eureka-serverartifactId>
<name>eureka-servername>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
dependencies>
project>
application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
//注册中心的服务
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(EurekaServerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
properties的设置
# 切记一定要加
spring.application.name=eureka-server
server.port=20001
eureka.instance.hostname=localhost
# 是否发起服务器注册,服务端关闭
eureka.client.register-with-eureka=false
# 是否拉取服务注册表,服务端是生成端不用拉取
eureka.client.fetch-registry=false
创建Eureka-Client的项目模块
引入POM依赖
<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>spring-cloud-learnartifactId>
<groupId>com.icodingedu.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>eureka-clientartifactId>
<name>eureka-clientname>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
project>
application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class EurekaClientApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(EurekaClientApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
controller服务提供内容
package com.icodingedu.springcloud.controller;
import com.icodingedu.springcloud.pojo.PortInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@RestController
@Slf4j
public class EurekaClientController {
@Value("${server.port}")
private String port;
@GetMapping("/sayhello")
public String sayHello(){
return "my port is "+port;
}
@PostMapping("/sayhello")
public PortInfo sayPortInfo(@RequestBody PortInfo portInfo){
log.info("you are "+portInfo.getName()+" is "+portInfo.getPort() );
return portInfo;
}
}
pojo
package com.icodingedu.springcloud.pojo;
import lombok.Data;
@Data
public class PortInfo {
private String name;
private String port;
}
application配置
spring.application.name=eureka-client
server.port=20002
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
创建一个Eureka-Consumer的模块
引入POM依赖
<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>spring-cloud-learnartifactId>
<groupId>com.icodingedu.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>eureka-consumerartifactId>
<name>eureka-consumername>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
project>
启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableDiscoveryClient
public class EurekaConsumerApplication {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
new SpringApplicationBuilder(EurekaConsumerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
controller实现调用
package com.icodingedu.springcloud.controller;
import com.icodingedu.springcloud.pojo.PortInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@Slf4j
public class ConsumerController {
//Consumenr+RestTemplate
@Autowired
private RestTemplate restTemplate;
//Eureka+Ribbon
@Autowired
private LoadBalancerClient client;
@GetMapping("/hello")
public String hello(){
ServiceInstance instance = client.choose("eureka-client");
if(instance==null){
return "No available instance";
}
String target = String.format("http://%s:%s/sayhello",instance.getHost(),instance.getPort());
log.info("url is {}",target);
return restTemplate.getForObject(target,String.class);
}
@PostMapping("/hello")
public PortInfo portInfo(){
ServiceInstance instance = client.choose("eureka-client");
if(instance==null){
return null;
}
String target = String.format("http://%s:%s/sayhello",instance.getHost(),instance.getPort());
log.info("url is {}",target);
PortInfo portInfo = new PortInfo();
portInfo.setName("gavin");
portInfo.setPort("8888");
return restTemplate.postForObject(target,portInfo,PortInfo.class);
}
}
pojo
package com.icodingedu.springcloud.pojo;
import lombok.Data;
@Data
public class PortInfo {
private String name;
private String port;
}
application的配置文件
spring.application.name=eureka-consumer
server.port=20003
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
启动进行consumer测试:server、client、consumer
# 两个核心的Eureka-Client配置
# 每隔10秒向Eureka-Server发送一次心跳包
eureka.instance.lease-renewal-interval-in-seconds=10
# 如果Eureka-Server在这里配置的20秒没有心跳接收,就代表这个节点挂了
eureka.instance.lease-expiration-duration-in-seconds=20
# 这两个参数是配置在服务提供节点
1、启动定时任务来轮询节点是否正常,默认60秒触发一次剔除任务,可以修改间隔
# 配置在Eureka-Server上
eureka.server.eviction-interval-timer-in-ms=30000
2、调用evict方法来进行服务剔除
如果自保开启,注册中心就会中断服务剔除操作
3、遍历过期服务,如和判断服务是否过期,以下任意一点满足即可
4、计算可剔除的服务总数,所有的服务是否能被全部剔除?当然不能!设定了一个稳定系数(默认0.85),这个稳定系数就是只在注册的服务总数里只能剔除:总数*85%个,比如当前100个服务,99个已经over了,只能剔除85个over,剩下的14个over的不会剔除
eureka.server.renewal-percent-threshold=0.85
5、乱序剔除服务:随机到哪个过期服务就把他踢下线
**同步时间:**心跳、续约、剔除
我们先来说说续约和心跳的关系,服务续约分为两步
接下来,服务剔除并不会和心跳以及续约直接打交道,而是通过查验服务节点在注册中心记录的同步时间,来决定是否剔除这个节点。
所以说心跳,续约和剔除是一套相互协同,共同作用的一套机制
接下来,就是服务节点向注册中心发送续约请求的时候了
服务续约请求 在前面的章节里我们讲到过,客户端有一个DiscoverClient类,它是所有操作的门面入口。所以续约服务就从这个类的renew方法开始
发送心跳
服务续约借助心跳来实现,因此发给注册中心的参数和上一小节的心跳部分写到的一样,两个重要参数分别是服务的状态(UP)和lastDirtyTimeStamp
在重新注册之前,客户端会做下面两个小操作,然后再主动调用服务册流程。
当注册成功的时候,清除脏节点标记,但是lastDirtyTimeStamp不会清除,因为这个属性将会在后面的服务续约中作为参数发给注册中心,以便服务中心判断节点的同步状态。
注册中心开放了一系列的HTTP接口,来接受四面八方的各种请求,他们都放在com.netflix.eureka.resources这个包下。只要客户端路径找对了,注册中心什么都能帮你办到
接受请求 InstanceResource下的renewLease方法接到了服务节点的续约请求。
尝试续约
服务节点发起续约请求。注册中心进行校验,从现在算到下一次心跳间隔时间,如果你没来renew,就当你已经死掉了。注册中心此时会做几样简单的例行检查,如果没有通过,则返回404,不接受反驳
脏数据校验 如果续约校验没问题,接下来就要进行脏数据检查。到了服务续约最难的地方了,脏数据校验逻辑之复杂,如同这皇冠上的明珠。往细了说,就是当客户端发来的lastDirtyTimeStamp,晚于注册中心保存的lastDirtyTimeStamp时(每个节点在中心都有一个脏数据时间),说明在从服务节点上次注册到这次续约之间,发生了注册中心不知道的事儿(数据不同步)。这可不行,这搞得我注册中心的工作不好有序开展,回去重新注册吧。续约不通过,返回404。
服务自保:把当前系统所有的节点保留,一个都不能少,即便服务节点挂了也不剔除
服务自保的触发机关
手动关闭服务自保
配置强行关闭服务自保,即便上面的自动开关被触发,也不能开启自保功能了
eureka.server.enable-self-preservation=false
在eureka-client里配置
# 测试设置,生产环境第一个值要比第二个小
eureka.instance.lease-renewal-interval-in-seconds=30
eureka.instance.lease-expiration-duration-in-seconds=5
在eureka-server配置
# 测试设置
eureka.server.enable-self-preservation=false
eureka.server.eviction-interval-timer-in-ms=10000
微服务架构中每一个较大业务领域都有自己的注册中心
eureka-server如果挂了,consumer依然可以使用server挂掉前的服务列表进行访问,但新的服务无法进行治理了
如何确保服务中心的高可用呢
如果要实现HA,就是通过镜像节点,我们再copy一个eureka-server
# eureka-server配置互相注册的节点即可
eureka.client.service-url.defaultZone=http://eurekaserver2:20011/eureka/
# eureka-client和eureka-consumer调用可以用csv确保调用的HA
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/,http://localhost:20002/eureka/
负载均衡(Load Balance)
Ribbon的体系结构分析
创建一个带ribbon的eureka-consumer应用,可以直接复制之前的eureka-consumer项目
POM里加入ribbon的依赖功能
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
在Application里给RestTemplate增加LoadBalance的注解
@SpringBootApplication
@EnableDiscoveryClient
public class RibbonConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
new SpringApplicationBuilder(RibbonConsumerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
Controller进行一下修改,直接调用client服务即可
package com.icodingedu.springcloud.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@Slf4j
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/hello")
public String hello(){
return restTemplate.getForObject("http://eureka-client/sayhello",String.class);
}
}
Properties的配置
spring.application.name=ribbon-consumer
server.port=30099
# 服务提供者连接的注册中心
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
Ribbon是在第一次加载的时候才会去初始化LoadBanlancer,第一次不仅包好HTTP连接和业务请求还包含LoadBanlancer的创建耗时,假如你的方法本身就比较耗时,并且你设置的超时时间不是很长,就很有可能导致第一次HTTP调用失败,这是ribbon的懒加载模式导致的,默认就是懒加载的
# ribbon开启饥饿加载模式,在启动时就加载LoadBanlancer配置
ribbon.eager-load.enabled=true
# 指定饥饿加载的服务名称
ribbon.eager-load.clents=ribbon-consumer
全局的负载均衡策略
package com.icodingedu.springcloud.config;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RibbonConfiguration {
@Bean
public IRule defaultLBStrategy(){
return new RoundRobinRule();
}
}
指定服务的负载均衡配置
方法一:properites里指定服务名对应的负载均衡策略
eureka-client.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
方法二:在configuration上注解实现
@Configuration
@RibbonClient(name = "eureka-client",configuration = com.netflix.loadbalancer.RandomRule.class)
public class RibbonConfiguration {
}
在Ribbon里有两个时间和空间密切相关的负载均衡策略:BestAvailableRule(BA)、WeightedResponseTimeRule(WRT)共同目标都是需要负载选择压力小的服务节点,BA选择并发量最小的机器也就是空间选择,WRT根据时间选择响应最快的服务
对于连接敏感型的服务模型,使用BestAvailableRule策略最合适
对于响应时间敏感的服务模型,使用WeightedResponseTimeRule策略最合适
如果使用了熔断器,用AvailabilityFilteringRule进行负载均衡
Eureka:http://ip:port/path
Ribbon:http://serviceName/path
引入Fegin组件来进行远程调用,这两个组件也一并被引入
Ribbon:利用负载均衡策略进行目标机器选择
Hystrix:根据熔断状态的开启状态,决定是否发起远程调用
发送请求时有两个核心的点
建立一个feign的文件夹并创建一个feign-consumer的应用
添加POM依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>feign-consumerartifactId>
<name>feign-consumername>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
project>
启动类实现
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeignConsumerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FeignConsumerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
创建调用的Service接口引用实现
package com.icodingedu.springcloud.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
//eureka服务提供者的service-name
//这个注解的意思是IService这个接口的调用都发到eureka-client这个服务提供者上
@FeignClient("eureka-client")
public interface IService {
@GetMapping("/sayhello")
String sayHello();
}
创建一个controller实现
package com.icodingedu.springcloud.controller;
import com.icodingedu.springcloud.service.IService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FeignController {
@Autowired
private IService service;
@GetMapping("/sayhi")
public String sayHi(){
return service.sayHello();
}
}
创建配置properties
spring.application.name=feigon-consumer
server.port=40001
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
在feigon目录下创建项目feigon-client-intf
POM里仅保持最低限度依赖,不要添加过多依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>feign-client-intfartifactId>
<name>feign-client-intfname>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
project>
建立接口层,并将实体对象放进来
package com.icodingedu.springcloud.service;
import com.icodingedu.springcloud.pojo.PortInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
//这里不能再使用eureka-client的了,需要使用自己的
//如果提供给的下游应用没有使用feign就不加注解@FeignClient,@GetMapping,@PostMapping,就是一个简单的接口,让下游自己实现即可
@FeignClient("feign-client")
public interface IService {
@GetMapping("/sayhello")
String sayHello();
@PostMapping("/sayhello")
PortInfo sayHello(@RequestBody PortInfo portInfo);
}
pojo实体对象
package com.icodingedu.springcloud.pojo;
import lombok.Data;
@Data
public class PortInfo {
private String name;
private String port;
}
在feign目录中创建项目feign-client-advanced
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>feign-client-advancedartifactId>
<name>feign-client-advancedname>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.icodingedugroupId>
<artifactId>feign-client-intfartifactId>
<version>${project.version}version>
dependency>
dependencies>
project>
创建application的启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class FeignClientAdvancedApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FeignClientAdvancedApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
在Controller里实现feign-client-intf里的IService
package com.icodingedu.springcloud.controller;
import com.icodingedu.springcloud.pojo.PortInfo;
import com.icodingedu.springcloud.service.IService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class FeignController implements IService {
@Value("${server.port}")
private String port;
@Override
public String sayHello() {
return "my port is "+port;
}
@Override
public PortInfo sayHello(@RequestBody PortInfo portInfo) {
log.info("you are "+portInfo.getName());
portInfo.setName(portInfo.getName());
portInfo.setPort(portInfo.getPort());
return portInfo;
}
}
配置Properties
spring.application.name=feign-client
server.port=40002
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
创建feign-consumer-advanced
设置POM文件,可以从feign-client-advanced里取
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>feign-consumer-advancedartifactId>
<name>feign-consumer-advancedname>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.icodingedugroupId>
<artifactId>feign-client-intfartifactId>
<version>${project.version}version>
dependency>
dependencies>
project>
创建启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
//这个地方要注意IService所在包路径,默认是扫当前包com.icodingedu.springcloud
//如果接口不在同一个包下就需要把包路径扫进来
//@EnableFeignClients(basePackages = "com.icodingedu.*")
@EnableFeignClients
public class FeignConsumerAdvancedApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(FeignConsumerAdvancedApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
controller实现
package com.icodingedu.springcloud.controller;
import com.icodingedu.springcloud.pojo.PortInfo;
import com.icodingedu.springcloud.service.IService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class FeignController {
@Autowired
private IService service;
@GetMapping("/sayhi")
public String sayHi(){
return service.sayHello();
}
@PostMapping("/sayhi")
public PortInfo sayHello(@RequestBody PortInfo portInfo){
return service.sayHello(portInfo);
}
}
properties的实现配置
spring.application.name=feign-consumer-advanced
server.port=40003
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
# feign的调用超时重试机制是由Ribbon提供的
# feign-server-proivder是指你的服务名
feign-server-proivder.ribbon.OkToRetryOnAllOperations=true
feign-server-proivder.ribbon.ConnectTimeout=1000
feign-server-proivder.ribbon.ReadTimeout=2000
feign-server-proivder.ribbon.MaxAutoRetries=2
feign-server-proivder.ribbon.MaxAutoRetriesNextServer=2
上面的参数设置了feign服务的超时重试策略
OkToRetryOnAllOperations:比如POST、GET、DELETE这些HTTP METHOD哪些可以Retry,设置为true表示都可以,这个参数是为幂等性设计的,默认是GET可以重试
ConnectTimeout:单位ms,创建会话的连接时间
ReadTimeout:单位ms,服务的响应时间
MaxAutoRetries:当前节点重试次数,访问次数等于首次访问+这里配置的重试次数
MaxAutoRetriesNextServer:当前机器重试超时后Feign将连接新的机器节点访问的次数
按照上面配置的参数,最大超时时间是?
(1000+2000) x (1+2) x (1+2) = 27000ms
总结一下极值函数
MAX(Response Time)=(ConnectTimeout+ReadTimeout)*(MaxAutoRetries+1)*(MaxAutoRetriesNextServer+1)
在feign-consumer-advanced里进行配置即可
# feign-client:这个是自己定义的服务名
# 每台机器最大的重试次数
feign-client.ribbon.MaxAutoRetries=2
# 可以重试的机器数量
feign-client.ribbon.MaxAutoRetriesNextServer=2
# 连接请求超时的时间限制ms
feign-client.ribbon.ConnectTimeout=1000
# 业务处理的超时时间ms
feign-client.ribbon.ReadTimeout=2000
# 默认是false,默认是在get上允许重试
# 这里是在所有HTTP Method进行重试,这里要谨慎开启,因为POST,PUT,DELETE如果涉及重试就会出现幂等问题
feign-client.ribbon.OkToRetryOnAllOperations=true
配置完毕后进行测试验证
在feign-client-intf里增加接口retry接口
@GetMapping("/retry")
String retry(@RequestParam(name = "timeout") int timeout);
在feign-client-advanced里实现这个接口
@Override
public String retry(@RequestParam(name="timeout") int timeout) {
try {
while (timeout-- > 0) {
Thread.sleep(1000);
}
}catch (Exception ex){
ex.printStackTrace();
}
log.info("retry is "+port);
return port;
}
在feign-consumer-advanced的controller里实现
@GetMapping("/retry")
public String retry(@RequestParam(name = "timeout") Integer timeout){
return service.retry(timeout);
}
启动三个feign-client-advanced
进行超时验证并看控制台数据是是每个节点输出三次,重试三个机器
缓存雪崩我们在之前的课程中都已经掌握过了,我们看一下另一个雪崩,那就服务雪崩
先来看一下这个场景:
目前异常场景出现了:
场景分析:
这个时候如何处理,就需要通过线程隔离来解决
在生产环境中系统故障发生的严重状态一般有以下几步
这里就要再次提到主链路的概念了,如果商品详情页的评价模块挂了几分钟,这没有问题,但如果商品下单流程挂了几分钟,那就会导致资金损失了
熔断和降级的核心目的就是为了保障系统的主链路稳定
如何降低故障影响
《断舍离》,是日本作家山下英子的著作,这本书传达了一种生活理念。断=不买、不收取不需要的东西。舍=处理掉堆放在家里没用的东西。离=舍弃对物质的迷恋,让自己处于宽敞舒适,自由自在的空间。
对过往不迷恋,拿得起放得下,这样的生活哲学确实可以帮助人们度过一些困难时期。我们知道Hystrix也是为了帮服务节点度过他们的困难时期(缓解异常、雪崩带来的影响),它也有同样一套佛系的设计理念,分别对应Hystrix中三个特色功能
微服务架构强调高可用,但并非高一致性,在一致性方面远比不上银行的大型机系统。也就是说,在日常服务调用阶段会出现一系列的调用异常,最常见的就是服务下线。举个例子:重启服务节点的时候,服务下线指令发送到注册中心后,这时还没来得及通过服务发现机制同步到客户端,因此某些服务调用请求依然会发送到这台机器,但由于服务已经下线,最终调用方只能无功而返,404 Not Found。
再举一个破坏力更大的例子。前面我们讲到了服务的雪崩效应,大家可能只听说过缓存雪崩,其实雪崩效应不仅仅针对缓存,它更大的危害是在大规模分布式应用上。我们举一个真实的案例,电商系统很多模块都依赖营销优惠服务,比如商品详情页、搜索列表页、购物车页和下单页面都依赖营销服务来计算优惠价格,因此这个服务承载的负载压力可谓非常之高。我们设想,假如这个服务出现了异常,导致响应超时,那么所有依赖它的下游系统的响应时间都会被拉长,这就引发了一个滚雪球的雪崩效应,由最上游的系统问题,引发了一系列下游系统响应超时,最终导致整个系统被拖垮。
服务降级用来应对上面的几种情况再合适不过了,假如HystrixClient调用目标请求的时候发生异常(exception),这时Hystrix会自动把这个请求转发到降级逻辑中,由服务调用方来编写异常处理逻辑。对响应超时的场景来说,我们可以通过配置Hystrix的超时等待时间(和Ribbon的timeout是两个不同配置),把超时响应的服务调用也当做是异常情况,转发到fallback逻辑中进行处理。
服务熔断是建立在服务降级之上的一个异常处理措施,你可以将它看做是服务降级的升级版。服务降级需要等待HTTP请求从服务节点返回异常或超时,再转向fallback逻辑,但是服务熔断引入了一种叫“断路器/熔断器”的机制,当断路器打开的时候,对服务的调用请求不会发送到目标服务节点,直接转向fallback逻辑。
断路器的打开/关闭有很多的判断标准,我们在服务熔断小节里再深入探讨。(好吧,这里我先剧透一点好了,比如我们可以这样设置:每10个请求,失败数量达到8个的时候打开断路器)。
同学可能会问了,假如断路器打开之后,就这么一直开着吗?当然不是了,一直开着多浪费电啊。服务一时失败,不代表一直失败,Hystrix也有一些配置规则,会主动去判断断路器关闭的时机。在后续章节,我们再来深入学习断路器的状态流转过程,我会带大家通过Turbine监控大盘,查看Hystrix断路器的开启关闭。
断路器可以显著缓解由QPS(Query Per Second,每秒访问请求,用来衡量系统当前压力)激增导致的雪崩效应,由于断路器打开后,请求直接转向fallback而不会发起服务调用,因此会大幅降低承压服务的系统压力。
大家知道Web容器通常有一个线程池来接待来访请求,如果并发量过高,线程池被打满了就会影响后面请求的响应。在我们应用内部,假如我们提供了3个微服务,分别是A,B,C。如果请求A服务的调用量过多,我们会发现所有可用线程都会逐渐被Service A占用,接下来的现象就是服务B和服务C没有系统资源可供调用。
Hystrix通过线程隔离的方案,将执行服务调用的代码与容器本身的线程池(比如tomcat thread pool)进行隔离,我们可以配置每个服务所需线程的最大数量,这样一来,即便一个服务的线程池被吃满,也不会影响其他服务。
与线程隔离相类似的还有“信号量”技术,稍后的小节我们会对两个技术做一番对比,看看这两个技术方案适合在哪些业务场景里应用。
所谓的静默处理,就是什么也不干,在fallback逻辑中直接返回一个空值Null。
同学们可能会问,那我用try-catch捕捉异常不也是能达到一样的效果吗?其实不然,首先try-catch只能处理异常抛出的情况,并不能做超时判定。其次,使用try-catch就要在代码里包含异常处理块,我们在程序设计时讲究单一职责和开闭原则。既然有了专门的fallback处理类,这个工作还是交给fallback来吧,这样你的业务代码也会很清爽。
默认值处理实际上就是说个谎话,在并不确定真实结果的情况下返回一个默认值
假如在商品详情页调用营销优惠接口时发生了故障,无法返回正确的计算结果,这里我们就可以在fallback逻辑中返回商品原价,作为打折后的价格,这样就相当于返回了一个没有打折优惠的计算结果。
这种方式下接口的返回值并不是真实的,因此不能应用在某些核心主链路中。举个例子,比如下单页面就是核心主链路,是最终确定订单价格的关键步骤。假如我们对订单优惠计算采用了默认值的方式,那么就会实际造成用户损失。因此,这里面的优惠计算决不能返回默认值,一定要得出真实结果,如果无法获取那么宁可返回异常中断下单操作。
同学们可能会问,那为什么商品详情页可以用默认值降级,而下单页面不能呢?这就要讲到主链路的规划,简单来说,电商平台的用户购物行为是一个漏斗模型,上宽下窄,用户流量在漏斗口最多,在尾部最少,越接近尾部的流量被转化为购物行为的比例就越高,因此越到后面对降级的容忍度就越低。商品搜索和商品详情页处于漏斗的上部,主要是导流端,在没有发生金钱往来的情况下我们可以容忍一定程度的降级误差。但对于下单页,这是整个漏斗模型的尾部,直接发生交易的环节,绝不能容忍任何金钱上的误差。老师在实际工作里设计商品详情页服务的时候,计算优惠访问返回的上限是1000ms,超过这个数字则自动降级为0优惠进行返回。
这才称得上是正经的积极措施,fallback会尝试用各种方法获取正确的返回值,有这么几个常用场景。
在某种情况下,fallback里由于各种问题又出现一个异常来。这时我们可以做二次降级,也就是在fallback中再引入一个fallback。当然,你也可以引入三四五六七八更多层的降级,对应一些复杂的大型应用,比如淘系很多核心系统,多级降级是很常见的,根据系统故障的严重程度采取更精细粒度的降级方案。
那假如这一连串降级全部都失败了,难道要牢底坐穿不成?对这种一错再错无药可救的顽固分子,Hystrix也没有办法,只好放你走了,将异常抛到最外层。
Request Cache并不是让你在fallback里访问缓存,它是Hystrix的一个特殊功能。我们可以通过@CacheResult和@CacheKey两个注解实现,配置如下
@CacheResult
@HystrixCommand
public Friend requestCache(@CacheKey Integer id) {
}
@CacheResult注解的意思是该方法的结果可以被Hystrix缓存起来,@CacheKey指定了这个缓存结果的业务ID是什么。在一个Hystrix上下文范围内,如果使用相同的参数对@CacheResult修饰的方法发起了多次调用,Hystrix只会在首次调用时向服务节点发送请求,后面的几次调用实际上是从Hystrix的本地缓存里读取数据。
Request Cache并不是由调用异常或超时导致的,而是一种主动的可预知的降级手段,严格的说,这更像是一种性能优化而非降级措施。
创建一个新的目录hystrix,在该目录下创建hystrix-fallback项目模块
我们这个新的模块还是一个服务调用者,参考前面的feign-consumer-advanced的引入POM依赖,注意要添加hystrix的依赖包了
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>hystrix-fallbackartifactId>
<name>hystrix-fallbackname>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.icodingedugroupId>
<artifactId>feign-client-intfartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
dependencies>
project>
我们需要定义一个可以抛出异常的Feign接口调用
先去Feign-client-intf里新建立一个error接口
@GetMapping("/error")
String error();
再去feign-client-advanced里实现这个方法
@Override
public String error() {
throw new RuntimeException("mouse droppings");
}
再回到hystrix-fallback里进行applicaiton代码的编写
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
//断路器
@EnableCircuitBreaker
public class HystrixFallbackApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(HystrixFallbackApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
在hystrix-fallback中新建一个service包,在里面创建一个MyService的Interface
package com.icodingedu.springcloud.service;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient(name = "feign-client")
public interface MyService extends IServiceAdvanced {
}
在hystrix-fallback中新建一个业务包hystrix,在里面创建一个业务类Fallback
package com.icodingedu.springcloud.hystrix;
import com.icodingedu.springcloud.pojo.PortInfo;
import com.icodingedu.springcloud.service.MyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
//Fallback其实就是我们的容错类
@Slf4j
@Component
public class Fallback implements MyService {
@Override
public String error() {
log.info("********* sorry ********");
return "I am so sorry!";
}
//下面的方法都暂时不管,只针对error方法做实现
@Override
public String sayHello() {
return null;
}
@Override
public PortInfo sayHello(PortInfo portInfo) {
return null;
}
@Override
public String retry(int timeout) {
return null;
}
}
再回到MyService接口里加上降级处理的实现类
package com.icodingedu.springcloud.service;
import com.icodingedu.springcloud.hystrix.Fallback;
import org.springframework.cloud.openfeign.FeignClient;
//这是一个整体容错方案,接口里的所有方法都进行了容错管理都需要在上面的Fallback类里实现容错
@FeignClient(name = "feign-client",fallback = Fallback.class)
public interface MyService extends IServiceAdvanced {
}
创建controller调用实现
package com.icodingedu.springcloud.controller;
import com.icodingedu.springcloud.service.MyService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class HystrixController {
@Resource
private MyService myService;
@GetMapping("/fallback")
public String fallback(){
return myService.error();
}
}
配置properties文件
spring.application.name=hystrix-consumer
server.port=50001
# 允许bean的注解重载
spring.main.allow-bean-definition-overriding=true
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
# 开启feign下的hystrix功能
feign.hystrix.enabled=true
# 是服务开启服务降级
hystrix.command.default.fallback.enabled=true
测试启动顺序
eureka-server、feign-client-advanced、hystrix-fallback
测试结果是feign-client-advanced出现:java.lang.RuntimeException: mouse droppings 异常
而hystrix-fallback则返回降级后的返回 I am so sorry !
测试结果:调用产生异常后就会返回降级的实现内容
在controller里加入一个timeout的方法,还是用之前的retry方法
@GetMapping("/timeout")
public String timeout(int second){
return myService.retry(second);
}
在Fallback类里将降级方法实现了
@Override
public String retry(int timeout) {
return "Yout are late !";
}
properties里配置超时降级机制
# 配置全局超时
hystrix.command.default.execution.timeout.enabled=true
# 全局超时时间,默认是1000ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=2000
# 超时以后终止线程
hystrix.command.default.execution.isolation.thread.interruptOnTimeout=true
# 取消的时候终止线程
hystrix.command.default.execution.isolation.thread.interruptOnFutureCancel=true
# 这里把ribbon的超时重试机制也拿进来
# 每台机器最大的重试次数
feign-client.ribbon.MaxAutoRetries=0
# 可以重试的机器数量
feign-client.ribbon.MaxAutoRetriesNextServer=0
# 连接请求超时的时间限制ms
feign-client.ribbon.ConnectTimeout=1000
# 业务处理的超时时间ms
feign-client.ribbon.ReadTimeout=5000
# 默认是false,默认是在get上允许重试
# 这里是在所有HTTP Method进行重试,这里要谨慎开启,因为POST,PUT,DELETE如果涉及重试就会出现幂等问题
feign-client.ribbon.OkToRetryOnAllOperations=false
重启hystrix-fallback进行测试,发现超时没有返回达到配置的2秒就直接降级了
刚刚是对全局进行的超时配置,如果想要对具体方法实现如下
# 将default替换成MyService
# 具体方法的超时时间
hystrix.command.MyService#retry(int).execution.isolation.thread.timeoutInMilliseconds=4000
这块如果不知到这个参数怎么写的可以在main里输出一下:MyService#retry(int)
public static void main(String[] args) throws NoSuchMethodException {
System.out.println(Feign.configKey(MyService.class,
MyService.class.getMethod("retry",int.class)
));
}
也可以通过注解的方式实现具体方法的超时降级,下面会讲到
Request Cache并不是由调用异常或超时导致的,而是一种主动的可预知的降级手段,严格的说,这更像是一种性能优化而非降级措施
代码直接开撸,在hytrix-fallback里创建一个RequestCacheService
package com.icodingedu.springcloud.service;
import com.icodingedu.springcloud.pojo.PortInfo;
import com.netflix.hystrix.contrib.javanica.cache.annotation.CacheKey;
import com.netflix.hystrix.contrib.javanica.cache.annotation.CacheResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@Service
@Slf4j
public class RequestCacheService {
@Resource
private MyService service;
//这里就是将结果缓存并根据参数值来进行k-v的缓存对应
@CacheResult
@HystrixCommand(commandKey = "cacheKey")
public PortInfo requestCache(@CacheKey String name){
log.info("request cache name "+ name);
PortInfo portInfo = new PortInfo();
portInfo.setName(name);
portInfo.setPort("2020");
portInfo = service.sayHello(portInfo);
log.info("after request cache name "+name);
return portInfo;
}
}
超时降级的方法key另一种配置方式
注意这里的@HystrixCommand(commandKey = "cacheKey")
这个cacheKey就可以用在上面的具体方法超时时间上,但还要配套fallbackMethod的方法
hystrix.command.cacheKey.execution.isolation.thread.timeoutInMilliseconds=2000
编写controller的实现
@Autowired
private RequestCacheService requestCacheService;
@GetMapping("/cache")
public PortInfo requestCache(String name){
//缓存存放在hystrix的上下文中,需要初始化上下文,上下文打开后执行完还要关闭context.close();
//使用try-catch-finally里去context.close();掉
//或者使用lombok的@Cleanup注解,默认调用close方法,如果默认不是close方法而是其他方法关闭
//可以这样来设置@Cleanup("shutup")
@Cleanup HystrixRequestContext context = HystrixRequestContext.initializeContext();
//我们在这里调用两次看看执行过程
PortInfo portInfo = requestCacheService.requestCache(name);
portInfo = requestCacheService.requestCache(name);
return portInfo;
}
properties里可以配置也可以不配置
# 默认requestCache是打开状态
hystrix.command.default.requestCache.enabled=true
重启后进行测试发现,feign-client-advanced的控制台只被调用了一次,这样就可以将远程调用需要K-V缓存的内容放到一个hystrix上下文中进行调用,只要调用参数值一样,无论调用多少次返回值都是从缓存中取这样就能提升一定的性能不用每次都进行远程调用了
去到Fallback类里再创建两个降级的实现方法
@Override
//降级方法的参数要保持一致
@HystrixCommand(fallbackMethod = "fallback2")
public String error() {
log.info("********* sorry ********");
throw new RuntimeException("first fallback");
}
@HystrixCommand(fallbackMethod = "fallback3")
public String fallback2(){
log.info("********* sorry again ********");
throw new RuntimeException("first fallback again");
}
public String fallback3(){
log.info("********* sorry again 2 ********");
return "success sorry again 2!";
}
通过注解配置实现timeout超时降级
去到controller里添加一个方法
@GetMapping("/timeout2")
@HystrixCommand(
fallbackMethod = "timeoutfallback",
//可以忽略不进行降级的异常
ignoreExceptions = {IllegalArgumentException.class},
//这个commandProperties是一个数组,所以可以配置多个HystrixProperty
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "6000")
}
)
public String timeout2(int second){
return myService.retry(second);
}
//这个是降级的实现方法
public String timeoutfallback(int second){
return "timeout success "+second;
}
这里需要注意的是在注解上添加的超时和配置文件里配置的全局超时设置之间的时间关系
Feign集成了Ribbon和Hystrix两个组件,它俩都各自有一套超时配置,那到底哪个超时配置是最终生效的那个呢
我们先来复习一下Ribbon的超时时间计算公式:
最大超时时间=(连接超时时间+接口超时时间)*(当前节点重试次数+1)*(换节点重试次数+1)
假如经过上述计算,Ribbon的超时时间是2000ms,那么Hystrix的超时时间应该设置成多少才合理呢?我们先来看看Hystrix的默认全局配置
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000
以上全局配置设置了Hystrix的熔断时间为1000ms。这里Hystrix的超时时间设置比Ribbon配置的时间短,那么不等Ribbon重试结束,Hystrix判定超时后就会直接执行熔断逻辑。因此,Hystrix和Ribbon是一个共同作用的关系,谁先到达超时指标就会率先起作用。
通常来讲,Hystrix的熔断时间要比Ribbon的最长超时时间设置的略长一些,这样就可以让Ribbon的重试机制充分发挥作用,以免出现还没来得及重试就进入fallback逻辑的情况发生。
那如果我们有一些接口对响应时间的要求特别高,比如说商品详情页接口,元数据必须在2s以内加载返回,那我们怎么针对方法设置更细粒度的Hystrix超时限制?
这个时候我们就需要以方法为维度来设置服务降级时间而不是直接应用全局配置了
方法级别的降级时间优先级是高于全局应用级别的,即便是方法超时时长>全局超时时长,也是走方法级别的超时时间
# 具体方法的超时时间
hystrix.command.MyService#retry(int).execution.isolation.thread.timeoutInMilliseconds=6000
# 超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000
方法上实现超时时间配置有三种方法
服务熔断是建立在降级之上的强力手段,是进击的降级,对于调用后进入降级处理的业务反复fallback就需要启用熔断机制了,不再进行远程调用,待什么时候服务恢复了再恢复远程调用访问
以上流程中省略了服务降级部分的业务,我们只关注熔断部分。
熔断器有三个状态阶段:
熔断器的判断阀值:
主要从两个维度判断熔断器是否开启:
其中时间窗口的大小也是可以配置的,而且我们还可以指定half-open判定的时间间隔,比如说熔断开启10秒以后进入half-open状态,此时就会让一个请求发起调用,如果成功就关闭熔断器。
这一节比较轻松,只需要通过配置来进行熔断设置即可
# 熔断的前提条件(请求的数量),在一定的时间窗口内,请求达到5个以后,才开始进行熔断判断
hystrix.command.default.circuitBreaker.requestVolumeThreshold=5
# 失败请求数达到50%则熔断开关开启
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
# 当熔断开启后经过多少秒再进入半开状态,放出一个请求进行远程调用验证,通过则关闭熔断不通过则继续熔断
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=15000
# 配置时间窗口
hystrix.command.default.metrics.rollingStats.timeInMilliseconds=20000
# 开启或关闭熔断的功能
hystrix.command.default.circuitBreaker.enabled=true
# 强制开启熔断开关
hystrix.command.default.circuitBreaker.forceOpen=false
# 强制关闭熔断开关
hystrix.command.default.circuitBreaker.forceClosed=false
要把retry的全局延时降级时间调整成2秒进行测试
1秒正常,2秒降级进行测试,验证阈值的百分比触发条件
作业:
上面配置的是基于全局的熔断器,如何配置基于某个方法的熔断机制
可以参考超时降级的配置方法,将配置文件中default换成具体key,通过注解实现的方式
线程池拒绝
这一步是线程隔离机制直接负责的,假如当前商品服务分配了10个线程,那么当线程池已经饱和的时候就可以拒绝服务,调用请求会收到Thread Pool Rejects,然后将被转到对应的fallback逻辑中。其实控制线程池中线程数量是由多个参数共同作用的,我们分别看一下
线程Timeout:我们通常情况下认为延迟只会发生在网络请求上,其实不然,在Netflix设计Hystrix的时候,就有一个设计理念:调用失败和延迟也可能发生在远程调用之前(比如说一次超长的Full GC导致的超时,或者方法只是一个本地业务计算,并不会调用外部方法),这个设计理念也可以在Hystrix的Github文档里也有提到。因此在方法调用过程中,如果同样发生了超时,则会产生Thread Timeout,调用请求被流转到fallback
服务异常/超时:这就是我们前面学习的的服务降级,在调用远程方法后发生异常或者连接超时等情况,直接进入fallback
**代码实现:**在service包下创建一个业务类来做测试
package com.icodingedu.springcloud.service;
import com.icodingedu.springcloud.pojo.PortInfo;
import com.netflix.hystrix.*;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class GetPortInfoCommand extends HystrixCommand<PortInfo> {
private String name;
public GetPortInfoCommand(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetPortInfoCommandPool"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GetPortInfoCommandKey"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(name))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
.withExecutionTimeoutInMilliseconds(25000)
//.withExecutionIsolationSemaphoreMaxConcurrentRequests(3)
)
.andThreadPoolPropertiesDefaults(
HystrixThreadPoolProperties.Setter()
.withCoreSize(3)
.withQueueSizeRejectionThreshold(1)
)
);
this.name = name;
}
@Override
protected PortInfo run() throws Exception {
log.info("********进入线程池******");
Thread.sleep(20000);
PortInfo portInfo = new PortInfo();
portInfo.setName(name);
portInfo.setPort("99999");
log.info("********执行完毕******");
return portInfo;
}
}
controller层进行验证,创建一个方法
@GetMapping("/command")
public String portInfoCommand(){
com.netflix.hystrix.HystrixCommand<PortInfo> hystrixCommand = new GetPortInfoCommand("gavin.huang");
PortInfo portInfo = hystrixCommand.execute();
return "success "+portInfo.getName()+" --- "+portInfo.getPort();
}
线程隔离原理
从性能角度看
信号量实现只需要修改service 的两个地方
//在service的构造方法里将THREAD改成SEMAPHORE
//增加信号量控制.withExecutionIsolationSemaphoreMaxConcurrentRequests(3)
//下面线程池的内容注释掉即可
public GetPortInfoCommand(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetPortInfoCommandPool"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GetPortInfoCommandKey"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(name))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionTimeoutInMilliseconds(25000)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(3)
)
// .andThreadPoolPropertiesDefaults(
// HystrixThreadPoolProperties.Setter()
// .withCoreSize(3)
// .withQueueSizeRejectionThreshold(1)
// )
);
this.name = name;
}
Turbine需要连接了服务注册中心获取服务提供者列表以便进行相应信息聚合
在hystrix目录下创建一个hystrix-turbine的module
导入POM依赖,和hystrix-fallback基本一样,只需要把feign-client-intf依赖去掉并加入turbine的依赖即可
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>hystrix-turbineartifactId>
<name>hystrix-turbinename>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-turbineartifactId>
dependency>
dependencies>
project>
编写application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.netflix.turbine.EnableTurbine;
@EnableDiscoveryClient
@EnableHystrix
@EnableTurbine
@EnableAutoConfiguration
public class HystrixTurbineApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(HystrixTurbineApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
配置propertie的配置文件
spring.application.name=hystrix-turbine
server.port=50002
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
# 指定需要监控的服务名
turbine.app-config=hystrix-consumer
turbine.cluster-name-expression="default"
# 将端口和hostname作为区分不同服务的条件,默认只用hostname,默认方式在本地一个IP下就区分不开了
turbine.combine-host-port=true
# turbine通过这个路径获取监控数据,所以监控的服务也要开放这个路径监控
turbine.instanceUrlSuffix.default=actuator/hystrix.stream
turbine.aggregtor.clusterConfig=default
去到hystrix-fallback项目中打开actuator的配置
# actuator暴露接口
management.security.enabled=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
去到浏览打开项目hystrix-fallback的actuator路径进行查看
http://localhost:50001/actuator 可以看到所有监控的信息
http://localhost:50001/actuator/hystrix.stream 可以看到一个长连接的ping在不断返回结果
现在启动hystrix-turbine项目
http://localhost:50002/turbine.stream 访问这个路径
从这个页面发现turbine自己并不生产数据,只是将数据进行收集
创建一个hystrix-dashboard的module
导入POM依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>hystrix-dashboardartifactId>
<name>hystrix-dashboardname>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
dependencies>
project>
创建application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@EnableHystrixDashboard
@SpringBootApplication
public class HystrixDashboardApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(HystrixDashboardApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
创建properties配置文件
spring.application.name=hystrix-dashboard
server.port=50003
# hystrix监控路径:这个是一台服务的监控检查
# http://localhost:50001/actuator/hystrix.stream
# turbine监控路径:这是整合多台服务的监控检查
# http://localhost:50002/turbine.stream
分别启动eureka-server、feign-client-advanced、hystrix-fallback、hystrix-turbine、hystrix-dashboard
按照顺序启动完毕后登录:http://localhost:50003/hystrix
频繁出错的应用,应该快速失败,不要占用系统,快速失败后不再进行远程调用了,直接进行本地访问,待远程恢复后再进行访问
以上流程中省略了服务降级部分的业务,我们只关注熔断部分。
熔断器有三个状态阶段:
熔断器的判断阀值:
主要从两个维度判断熔断器是否开启:
其中时间窗口的大小也是可以配置的,而且我们还可以指定half-open判定的时间间隔,比如说熔断开启10秒以后进入half-open状态,此时就会让一个请求发起调用,如果成功就关闭熔断器。
纯粹的properties配置,在hystrix-fallback里配置
# 熔断的前提条件(请求数量),在一定时间窗口内,请求达到5个以后,才开始熔断判读
hystrix.command.default.circuitBreaker.requestVolumeThreshold=5
# 失败请求数达到50%熔断开关开启
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
# 当熔断开启后经过多少秒进入半开判断,单位ms
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=15000
# 发生异常的时间窗口,单位ms
hystrix.command.default.metrics.rollingStats.timeInMilliseconds=20000
# 开启熔断功能
hystrix.command.default.circuitBreaker.enabled=true
# 关闭强制开启和强制关闭熔断器开关
hystrix.command.default.circuitBreaker.forceOpen=false
hystrix.command.default.circuitBreaker.forceClosed=false
Turbine需要连接了服务注册中心获取服务提供者列表以便进行相应信息聚合
在hystrix目录下创建一个hystrix-turbine的module
导入POM依赖,和hystrix-fallback基本一样,只需要把feign-client-intf依赖去掉并加入turbine的依赖即可
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>hystrix-turbineartifactId>
<name>hystrix-turbinename>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-turbineartifactId>
dependency>
dependencies>
project>
编写application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.netflix.turbine.EnableTurbine;
@EnableDiscoveryClient
@EnableHystrix
@EnableTurbine
@EnableAutoConfiguration
public class HystrixTurbineApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(HystrixTurbineApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
配置propertie的配置文件
spring.application.name=hystrix-turbine
server.port=50002
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
# 指定需要监控的服务名
turbine.app-config=hystrix-consumer
turbine.cluster-name-expression="default"
# 将端口和hostname作为区分不同服务的条件,默认只用hostname,默认方式在本地一个IP下就区分不开了
turbine.combine-host-port=true
# turbine通过这个路径获取监控数据,所以监控的服务也要开放这个路径监控
turbine.instanceUrlSuffix.default=actuator/hystrix.stream
turbine.aggregtor.clusterConfig=default
去到hystrix-fallback项目中打开actuator的配置
# actuator暴露接口
management.security.enabled=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
去到浏览打开项目hystrix-fallback的actuator路径进行查看
http://localhost:50001/actuator 可以看到所有监控的信息
http://localhost:50001/actuator/hystrix.stream 可以看到一个长连接的ping在不断返回结果
现在启动hystrix-turbine项目
http://localhost:50002/turbine.stream 访问这个路径
从这个页面发现turbine自己并不生产数据,只是将数据进行收集
创建一个hystrix-dashboard的module
导入POM依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>hystrix-dashboardartifactId>
<name>hystrix-dashboardname>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
dependencies>
project>
创建application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@EnableHystrixDashboard
@SpringBootApplication
public class HystrixDashboardApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(HystrixDashboardApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
创建properties配置文件
spring.application.name=hystrix-dashboard
server.port=50003
# hystrix监控路径:这个是一台服务的监控检查
# http://localhost:50001/actuator/hystrix.stream
# turbine监控路径:这是整合多台服务的监控检查
# http://localhost:50002/turbine.stream
分别启动eureka-server、feign-client-advanced、hystrix-fallback、hystrix-turbine、hystrix-dashboard
按照顺序启动完毕后登录:http://localhost:50003/hystrix
程序中硬编码
配置文件
环境变量
数据库/缓存存储
格式不统一
没有版本控制
基于静态配置
分布零散
直连配置中心的工作模式非常简单,Config Server直接从配置库(GitHub)中拉取配置同步到各个Server中来进行直连式配置
首先在GitHub上创建repo然后创建两个配置文件
命名规则:
应用名-环境名.yaml / 应用名-环境名.properties
我们创建开发环境的文件名为:config-consumer-dev.yaml
info:
profile: dev
name: config-dev
words: this is a development environment
创建生产环境的文件名:config-consumer-prod.yaml
info:
profile: prod
name: config-prod
words: this is a production environment
创建一个config目录,在目录中创建一个config-sever的module
指定POM导入config-server
config-server和eureka-server很像就是一个中心化配置服务所以加入的依赖就一个
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>config-serverartifactId>
<name>config-servername>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
dependencies>
project>
创建application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigServerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
这次我们设置yaml配置文件
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://github.com/hjtbfx/config-repo.git
force-pull: true # 强制拉取资源文件
# search-paths: dev,prod* 支持csv格式配置多个路径同时获取,还支持*作为通配符
# username:
# password:
server:
port: 60001
启动config-server使用PostMan拉取配置文件
# GET
# config-consumer/dev 就是我们的命名规范application-profile
http://localhost:60001/config-consumer/dev
返回数据内容
{
"name": "config-consumer",
"profiles": [
"dev"
],
"label": null,
"version": "598ccaf69010b4ebbf13558fc3cb21f0212ceb3a",
"state": null,
"propertySources": [
{
"name": "https://github.com/hjtbfx/config-repo.git/config-consumer-dev.yaml",
"source": {
"info.profile": "dev",
"name": "config-dev",
"words": "this is a development environment"
}
}
]
}
并且在config-server的控制台还会输出一个本地资源的路径
2020-04-06 23:56:56.262 INFO 4610 --- [io-60001-exec-2] o.s.c.c.s.e.NativeEnvironmentRepository : Adding property source: file:/var/folders/bs/t5hqbzl52r18nfyx3v0dc0k80000gn/T/config-repo-6888460700345550079/config-consumer-dev.yaml
如果要获取repo仓库中不同路径下的文件,可以这样访问
http://localhost:60001/config-consumer/dev/master
如果只想获得文件内容
# 其实这也是一个通配符方式,返回yaml格式数据
http://localhost:60001/config-consumer-dev.yaml
# 同理可得,返回properties格式数据
http://localhost:60001/config-consumer-dev.properties
# 返回json格式数据
http://localhost:60001/config-consumer-dev.json
# 这里就说明config并不要求你在github要把所有格式保存了,他会按照你的要求进行统一
如果我们的文件不在repo的根目录下,可以在前面加上路径
http://localhost:60001/master/config-consumer-dev.yaml
通配的格式如下
http://localhost:60001/{application}/{profile}/{label}
http://localhost:60001/{application}-{profile}.json(.yaml .properties)
在config目录下创建config-client的module
引入POM依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>config-clientartifactId>
<name>config-clientname>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
dependencies>
project>
创建application启动类,只需要springboot启动类即可
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
public class ConfigClientApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigClientApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
创建controller实现配置获取的两种方式
package com.icodingedu.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ConfigController {
//直接从外部github上的配置文件加载
@Value("${name}")
private String name;
//从外部将配置注入到本地配置文件,再从本地加载
@Value("${myWords}")
private String words;
@GetMapping("/name")
public String getName(){
return name;
}
@GetMapping("/words")
public String getWords(){
return words;
}
}
定义配置文件:bootstrap.yaml
为什么是bootstrap文件,而不是application,因为bootstrap的加载早于application,而且application在这里是要被写入属性的,所以需要在之前加载配置,因此要使用bootstrap配置
spring:
application:
name: config-client
cloud:
config:
uri: http://localhost:60001
profile: prod # 这个应该是从外部注入的,比如启动tomcat是传入参数来确定是什么环境
label: master
server:
port: 60002
myWords: ${words}
这样配置是启动不起来的原因是这里直接使用了spring.application.name作为applicationName了,要么和github上一致要么在下面spring.cloud.config里重写一下name
spring:
application:
name: config-client
cloud:
config:
uri: http://localhost:60001
profile: prod # 这个应该是从外部注入的,比如启动tomcat是传入参数来确定是什么环境
label: master
name: config-consumer
server:
port: 60002
myWords: ${words}
还在原来的config-client项目中进行修改
在POM中增加依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
项目中创建一个新的controller
package com.icodingedu.springcloud.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("refresh")
//允许bean在运行期更新值,进入源码看下注释
@RefreshScope
public class RefreshController {
@Value("${words}")
private String words;
@GetMapping("/words")
public String getWords(){
return words;
}
}
修改配置文件yaml配置开放Actuator的内容
spring:
application:
name: config-client
cloud:
config:
uri: http://localhost:60001
profile: prod # 这个应该是从外部注入的,比如启动tomcat是传入参数来确定是什么环境
label: master
name: config-consumer
server:
port: 60002
myWords: ${words}
management:
security:
enabled: false
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
测试配置变更情况
1、我们到github里将words数据进行修改,添加一些内容
2、通过controller访问:http://localhost:60002/refresh/words 获得的数据还是历史未修改的值
3、需要通过actuator来进行刷新,在config-client端进行刷行
# POST 请求
http://localhost:60002/actuator/refresh
# 请求后会在控制台输出更新信息
4、再次访问就会产生更新信息了:http://localhost:60002/refresh/words
5、这种方式就可以进行不同服务器的功能开关了:对多台服务器中的某几台进行刷行,功能就只在这几台服务上打开,这样就做到手工灰度发布了
方案1
使用Eureka注册中心来实现配置中心的高可用,将所有的配置中心注册到Eureka中,利用Eureka的服务发现,服务续约服务剔除来维护配置中心,配置调用方通过Eureka拿到配置中心列表再通过Ribbon负载均衡访问具体的配置中心即可
方案2
如果我们单独使用配置中心,系统中没有使用Eureka,可以在网关层做负载均衡,搭建多个配置中心接入到负载均衡的网关,可以是Nginx/HAProxy/Lvs,对于网关层HA可以配套keepalived的VIP进行HA,也可以直接使用LB的云服务进行使用
既然我们是在springcloud环境中,就来看下如何借助Eureka实现配置中心高可用的
创建一个新的module用来实现配置中心的高可用:config-server-eureka,证明这个配置中行集成了eureka
在POM中导入两个依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>config-server-eurekaartifactId>
<name>config-server-eurekaname>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
project>
创建application实现类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
@EnableDiscoveryClient
public class ConfigServerEurekaApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigServerEurekaApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
创建application.yaml,需要配置eureka的client加入到eureka-server中
spring:
application:
name: config-server-eureka
cloud:
config:
server:
git:
uri: https://github.com/hjtbfx/config-repo.git
force-pull: true # 强制拉取资源文件
# search-paths: dev,prod* 支持csv格式配置多个路径同时获取,还支持*作为通配符
# username:
# password:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:20001/eureka/
server:
port: 60003
可以按顺序启动eureka-server、config-server-eureka了
我们来修改前面创建的config-client项目
先修改POM,加入eureka的服务发现依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
修改启动类增加eureka的服务发现注解
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ConfigClientApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigClientApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
修改config-client的bootstrap.yaml的配置文件
spring:
application:
name: config-client
cloud:
config:
# uri: http://localhost:60001
discovery:
enabled: true
service-id: config-server-eureka
profile: prod # 这个应该是从外部注入的,比如启动tomcat是传入参数来确定是什么环境
label: master
name: config-consumer
eureka:
client:
serviceUrl:
defaultZone: http://localhost:20001/eureka/
server:
port: 60002
myWords: ${words}
management:
security:
enabled: false
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
这样就可进行测试了,启动服务然后进行验证即可
之前都是明文保存,比如用户名,密码(社会工程学)
config是如何做密码加密和解密,通过config的encrypt的功能加密明文生成密文,然后把密文放到github上,需要在密文前加一个标识{cipher},config 发现有这样的标识,就会启动解密
加密的密钥的方式:对称密钥、非对称密钥
我们是用对称加密来进行config的加解密
在config-server-eureka中增加bootstrap.yaml
# 在bootstrap中增加对称加密的key
encrypt:
key: 19491001
通过encrypt进行加密
# POST 明文:icodingedu is very well!
http://localhost:60003/encrypt
# 获得密文
289765ddfd0a7aaf747f59c549cf81a76dce270067a22b91b7ed3259d41a7c550b9de1a2c913b5a3f99ecf779a66f327
# 如何解密
# POST 密文
http://localhost:60003/decrypt
如果在系统中自动解密,则需要将密文加上{cipher}进行密文标识
# Github里
introduce: '{cipher}289765ddfd0a7aaf747f59c549cf81a76dce270067a22b91b7ed3259d41a7c550b9de1a2c913b5a3f99ecf779a66f327'
在config-client获取introduce和没有加密的配置一样,正常获取即可
如果我们的config-client是对外提供负载均衡服务的,有60002,60012,60022三台服务,这时我们配置更新了,如果需要三台机器都更新配置,是否需要在每台服务上都执行一遍actuator/refresh?如果需要全部更新则需要在三台机器上都执行
在软件工程领域有这样一句名言:计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决
自己如果做:程序订阅一个更新调用actuator/refresh
SpringCloud bus + config + github搭建一套总线式的配置中心
BUS-消息总线:代理了将消息变更发送给所有服务节点的角色
Stream组件代理了大部分消息中间件的通信服务,BUS在实际应用中大多是为了应对消息广播的场景
前面我们了解了Bus的工作方式,在动手改造配置中心之前,我们先来了解一下Bus有哪些接入方式。
Spring的组件一向是以一种插件式的方式提供功能,将组件自身和我们项目中的业务代码隔离,使得我们更换组件的成本可以降到最低。Spring Cloud Bus也不例外,它的底层消息组件非常容易替换,替换过程不需要对业务代码引入任何变更。Bus就像一道隔离了底层消息组件和业务应用的中间层,比如我们从RabbitMQ切换为Kafka的时候,只需要做两件事就好了:
RabbitMQ是实现了AMQP(Advanced Message Queue Protocal)的开源消息代理软件,也是大家平时项目中应用最广泛的消息分发组件之一。同学们在分布式章节应该已经深入了解了消息队列的使用,这里我们就不再赘述了。
接入RabbitMQ的方式很简单,我们只要在项目中引入以下依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
点进去查看这个依赖的pom,你会发现它依赖了spring-cloud-starter-stream-rabbit
,也就是说Stream组件才是真正被用来发送广播消息到RabbitMQ的,Bus这里只是帮我们封装了整个消息的发布和监听动作。
接下来我们看下项目中所需的具体配置:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
上面配置分别指定了RabbitMQ的地址、端口、用户名和密码,以上均采用RabbitMQ中的默认配置。
要使用Kafka来实现消息代理,只需要把上一步中引入的spring-cloud-starter-bus-amqp依赖替换成spring-cloud-starter-bus-kafka依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-kafkaartifactId>
dependency>
如果大家的Kafka和ZooKeeper都运行在本地,并且采用了默认配置,那么不需要做任何额外的配置,就可以直接使用。但是在生产环境中往往Kafka和ZooKeeper会部署在不同的环境,所以就需要做一些额外配置:
属性 | 含义 |
---|---|
spring.cloud.stream.kafka.binder.brokers | Kafka服务节点(默认localhost) |
spring.cloud.stream.kafka.binder.defaultBrokerPort | Kafka端口(默认9092) |
spring.cloud.stream.kafka.binder.zkNodes | ZooKeeper服务节点(默认localhost) |
zspring.cloud.stream.kafka.binder.defaultZkPort | ZooKeeper端口(默认2181) |
创建bus目录,然后创建一个config-bus-server
添加POM文件依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>config-bus-serverartifactId>
<name>config-bus-servername>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
project>
我们看一下bus-amqp这个依赖有什么玄机,进去后看到他里面还藏着一个stream-rabbit的组件,所有bus其实就是一个空壳子,他在通信的时候引入的是stream-rabbit这个适配层
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
<version>3.0.3.RELEASEversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-busartifactId>
<version>2.2.1.RELEASEversion>
<scope>compilescope>
dependency>
dependencies>
创建启动类application
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
@EnableDiscoveryClient
public class ConfigBusServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigBusServerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
配置application.yaml和bootstrap.yaml,可以从config-server-eureka项目中copy过来
在application.yaml里修改application name并添加rabbitmq的连接字符串,开放actuator的所有endpoint
可以从config-client里复制actuator的内容
spring:
application:
name: config-bus-server
rabbitmq:
host: 39.98.81.253
port: 5672
username: guest
password: guest
cloud:
config:
server:
git:
uri: https://github.com/hjtbfx/config-repo.git
force-pull: true # 强制拉取资源文件
# search-paths: dev,prod* 支持csv格式配置多个路径同时获取,还支持*作为通配符
# username:
# password:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:20001/eureka/
server:
port: 60011
management:
security:
enabled: false
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
启动测试一下:RabbitMQ要提前启动好
eureka-server、config-server-bus
测试一下配置是否能拿到:http://localhost:60011/config-consumer/prod
测试一下actuator:http://localhost:60011/actuator
# 下面我们将使用actuator的这个更新来进行批量通知更新
# 如果只想更新部分节点则可以使用{destination}路径参数
http://localhost:60011/actuator/bus-refresh/{destination}
在bus目录下创建一个config-bus-client
创建POM依赖,可以从config-client里复制过来
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>config-bus-clientartifactId>
<name>config-bus-clientname>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
dependencies>
project>
启动类和实现类可以直接从config-client里复制过来
application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ConfigBusClientApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ConfigBusClientApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
把config-client里的controller都copy过来,这个bus推送在代码上是没有什么感知,只会在yaml配置上有所不同,我们来进行一下yaml的配置,可以从config-client里把yaml复制过来进行修改
spring:
application:
name: config-bus-client
rabbitmq:
host: 39.98.81.253
port: 5672
username: guest
password: guest
cloud:
stream:
default-binder: rabbit
config:
# uri: http://localhost:60001
discovery:
enabled: true
service-id: config-bus-server
profile: prod # 这个应该是从外部注入的,比如启动tomcat是传入参数来确定是什么环境
label: master
name: config-consumer
eureka:
client:
serviceUrl:
defaultZone: http://localhost:20001/eureka/
server:
port: 60012
myWords: ${words}
management:
security:
enabled: false
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
启动两个config-bus-client进行测试
先看一下现有的words值
http://localhost:60012/refresh/words
http://localhost:60013/refresh/words
然后去github上修改words值,修改后再访问并没有改变
这是时候我们去config-bus-server上进行更新
# POST
# http://localhost:60011/actuator/bus-refresh
# 返回204就说明全部更新完成了
现在去访问多个config-bus-client看看效果,更新完成没,已全部更新
也可以发送到某个client节点,在具体的config-bus-client节点上执行
# POST
# http://localhost:60012/actuator/bus-refresh
# 这样也会触发所有节点更新
可以指定某个具体服务的具体端口进行更新,这个就要使用{destination}参数了
# POST 将请求发送到confi-bus-server上
# http://localhost:60011/actuator/bus-refresh/config-bus-client:60014
# {destination}参数格式:serverName:serverPort
# 也可以在端口上使用通配符,让这个服务的所有的端口都更新
# http://localhost:60011/actuator/bus-refresh/config-bus-client:*
注意:bus会在rabbitmq上自动创建exchagne、queue、routingkey
通过github的webhook机制(就是第三方的回调接口,当配置参数修改后github会主动调用你预留的接口)
通过github触发更新需要以下几步
1、在config-server上设置encrypt.key
2、将上一步的key添加到Github仓库中
3、配置webhook url
我们需要填写以下两个内容
Payload URL:http://45.89.90.12:60011/actuator/bus-refresh
这个地址的IP一定是公网上可以访问的IP或域名
Secret:encrypt.key里设置的值,我们项目中是19491001
很多webhook都要求回调的地址是https的,确保安全
自动推送需要注意的点:
如果借助github的webhook的实现自动更新并能实时修改目标节点
微服务的应用系统体系很庞大,光是需要独立部署的基础组件就有注册中心、配置中、服务总线、Turbine和监控大盘dashboard、调用链追踪和链路聚合,还有kafka和MQ之类的中间件,再加上拆分后的零散微服务,一个系统轻松就有20多个左右部署包
都微服务了,所有的业务对外都是实现单一原则,这就导致服务节点和服务数增多,一个整体的链路需要整合很多服务进行组合使用
还有一个问题就是安全性,如果让所有服务都引入安全验证,把所有的接口都加上安全验证,要更换成OAuth2.0,这个时候让所有的服务提供者都变更?
我们就给微服务引入一层专事专办的中间层-传达室
1、访问控制:看你是否有权进入,拒绝无权来访者
2、引导指路:问你做什么,给你指路,就是路由
网关层作为唯一的对外服务,外部请求不直接访问服务层,由网关层承接所有HTTP请求,我们会将gateway和nginx一同使用
Gateway的标签
Gateway可以做什么
Gateway VS zuul(第一代网关是Netflix出品)
Gateway | zuul 1.x | zuul 2.x | |
---|---|---|---|
靠谱性 | 官方背书指出 | 开创者,曾经靠谱 | 一直跳票,千呼万唤始出来 |
性能 | Netty | 同步阻塞,性能慢 | Netty |
QPS | 超30000 | 20000左右 | 20000-30000 |
SpringCloud | 已整合 | 已整合 | 暂无整合到组件库计划,但可以引用 |
长连接keepalive | 支持 | 不支持 | 支持 |
编程体验 | 略复杂 | 同步模型,比较简单 | 略复杂 |
调试&链路追踪 | 异步模型,略复杂 | 同步方式,比较容易 | 异步模型,略复杂 |
新的项目果断选择Gateway
导入POM依赖
<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>spring-cloud-learnartifactId>
<groupId>com.icodingedu.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>gateway-serverartifactId>
<name>gateway-servername>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
dependency>
dependencies>
project>
application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayServerApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class,args);
}
}
application.yaml配置
spring:
application:
name: gateway-server
cloud:
gateway:
discovery:
locator:
enabled: true
server:
port: 65000
eureka:
client:
serviceUrl:
defaultZone: http://localhost:20001/eureka/
management:
security:
enabled: false
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
启动服务:eureka-server、feign-client-advanced(启动三个)、gateway-server
启动后访问:http://localhost:65000/actuator/gateway/routes
可以得到动态加载的eureka路由规则
通过自动路由规则负载均衡实现:http://localhost:65000/FEIGN-CLIENT/sayhello
访问服务的路径希望是小写的
spring:
application:
name: gateway-server
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true # 增加这个
gateway动态路由规则配置
# POST
# http://localhost:65000/actuator/gateway/routes/myrouter
{
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/myrouter-path/**"
}
}
],
"filters": [
{
"name": "StripPrefix",
"args": {
"_genkey_0": "1"
}
}
],
"uri": "lb://FEIGN-CLIENT",
"order": 0
}
# DELETE删除路由规则
# http://localhost:65000/actuator/gateway/routes/myrouter
先删除路由表,再删除服务
Gateway中可以有多个Route,一个Route就是一套包含完整转发规则的路由,主要由三部分组成
对最后一步寻址来说,如果采用基于Eureka的服务发现机制,那么在Gateway的转发过程中可以采用服务注册名的方式来调用,后台会借助Ribbon实现负载均衡(可以为某个服务指定具体的负载均衡策略),其配置方式如:lb://FEIGN-SERVICE-PROVIDER/
,前面的lb就是指代Ribbon作为LoadBalancer。
Predicate接受一个判断条件,返回true或false的布尔值,告知调用方判断结果,也可以通过and、or、negative(非)三个操作符来将多个Predicate,对所有来的Request进行条件判断
只要网关接收到请求立即触发断言,满足所有的断言后才进入Filter阶段
Gateway给我们提供了十几种内置断言,常用的就下面几种
.router(r -> r.path("/gateway/**"))
.uri("lb://FEIGN-CLIENT")
)
.router(r -> r.path("/baidu"))
.uri("https://www.baidu.com")
)
.router(r -> r.path("/gateway/**"))
.and().method(HttpMethod.GET)
.uri("lb://FEIGN-CLIENT")
)
.router(r -> r.path("/gateway/**"))
.and().method(HttpMethod.GET)
.and().query("name","icodingedu")
.and().query("age")
.uri("lb://FEIGN-CLIENT")
)
//这里的age仅需要有age这个参数即可,至于值是什么不关心,但name的值必须是icodingedu
.router(r -> r.path("/gateway/**"))
.and().header("Authorization")
.uri("lb://FEIGN-CLIENT")
)
//header中必须包含一个Authorization属性,也可以传入两个参数,锁定值
.router(r -> r.path("/gateway/**"))
.and().cookie("name","icodingedu")
.uri("lb://FEIGN-CLIENT")
)
//cookie是几个参数断言中唯一一个必须指定值的断言
时间片匹配有三种模式:Before、After、Between,这个指定了在什么时间范围内容路由才生效
.router(r -> r.path("/gateway/**"))
.and().after("具体时间")
.uri("lb://FEIGN-CLIENT")
)
断言配置可以在yaml和java代码里都能够实现
在yaml里配置一个,rotues这部分
spring:
application:
name: gateway-server
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: feignclient
uri: lb://FEIGN-CLIENT
predicates:
- Path=/gavinyaml/**
filters:
- StripPrefix=1
配置完成后:http://localhost:65000/actuator/gateway/routes
在Java程序里进行配置
创建一个config包,建立一个配置类
package com.icodingedu.springcloud.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
@Configuration
public class GatewayConfiguration {
@Bean
@Order
public RouteLocator customerRoutes(RouteLocatorBuilder builder){
return builder.routes()
.route(r -> r.path("/gavinjava/**")
.and().method(HttpMethod.GET)
.and().header("name")
.filters(f -> f.stripPrefix(1)
.addResponseHeader("java-param","gateway-config")
)
.uri("lb://FEIGN-CLIENT")
).build();
}
}
geteway调用的是feign-client的业务,我们就到feign-client-advanced里创建一个controller实现
这里面要使用到的product需要提前在feign-client-intf中定义好
package com.icodingedu.springcloud.pojo;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class Product {
private Long productId;
private String description;
private Long stock;
}
feign-client-advanced中创建GatewayController
package com.icodingedu.springcloud.controller;
import com.icodingedu.springcloud.pojo.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@Slf4j
@RequestMapping("gateway")
public class GatewayController {
//我们就构建一个简易的数据存储,Product需要在feign-client-intf中定义
public static final Map<Long, Product> items = new ConcurrentHashMap<>();
@GetMapping("detail")
public Product getProduct(Long pid){
//如果第一次没有先创建一个
if(!items.containsKey(pid)){
Product product = Product.builder().productId(pid)
.description("very well!")
.stock(100L).build();
//没有才插入数据
items.putIfAbsent(pid,product);
}
return items.get(pid);
}
@GetMapping("placeOrder")
public String buy(Long pid){
Product product = items.get(pid);
if(product==null){
return "Product Not Found";
}else if(product.getStock()<=0L){
return "Sold Out";
}
synchronized (product){
if(product.getStock()<=0L){
return "Sold Out";
}
product.setStock(product.getStock()-1);
}
return "Order Placed";
}
}
回到Gateway-sever项目,按照时间顺延方式定义
package com.icodingedu.springcloud.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import java.time.ZonedDateTime;
@Configuration
public class GatewayConfiguration {
@Bean
@Order
public RouteLocator cutomerRoutes(RouteLocatorBuilder builder){
return builder.routes()
.route(r -> r.path("/gavinjava/**")
.and().method(HttpMethod.GET)
.and().header("name")
.filters(f -> f.stripPrefix(1)
.addResponseHeader("java-param","gateway-config")
)
.uri("lb://FEIGN-CLIENT")
)
.route(r -> r.path("/secondkill/**")
.and().after(ZonedDateTime.now().plusSeconds(20))
.filters(f -> f.stripPrefix(1))
.uri("lb://FEIGN-CLIENT")
)
.build();
}
}
也可以定义精确的时间节点值
@Bean
@Order
public RouteLocator cutomerRoutes(RouteLocatorBuilder builder){
LocalDateTime ldt = LocalDateTime.of(2020, 4, 11, 16, 11, 10);
return builder.routes()
.route(r -> r.path("/gavinjava/**")
.and().method(HttpMethod.GET)
.and().header("name")
.filters(f -> f.stripPrefix(1)
.addResponseHeader("java-param","gateway-config")
)
.uri("lb://FEIGN-CLIENT")
)
.route(r -> r.path("/secondkill/**")
.and().after(ZonedDateTime.of(ldt,ZoneId.of("Asia/Shanghai")))
.filters(f -> f.stripPrefix(1))
.uri("lb://FEIGN-CLIENT")
)
.build();
}
过滤器的实现方式
只需要实现两个接口:GatewayFilter、Ordered
过滤器类型
Header过滤器:可以增加和减少header里的值
StripPrefix过滤器:
.router(r -> r.path("/gateway/**"))
.filters(f -> f.stripPrefix(1))
.uri("lb://FEIGN-CLIENT")
)
假如请求的路径:/gateway/sample/update,如果没有stripPrefix过滤器,http://FEIGN-CLIENT/gateway/sample/update,他的作用就是将第一个路由路径截取掉
PrefixPath过滤器:它和StripPrefix作用相反
.router(r -> r.path("/gateway/**"))
.filters(f -> f.prefixPath("go"))
.uri("lb://FEIGN-CLIENT")
)
/gateway/sample/update 变成 /go/gateway/sample/update
RedirectTo过滤器:
.filters(f -> f.redirect(303,"https://www.icodingedu.com"))
// 遇到错误是30x的直接过滤跳转
去到gateway-server项目中进行修改,创建一个filter的package
package com.icodingedu.springcloud.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//Ordered是指定执行顺序的接口
@Slf4j
@Component
public class TimerFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//给接口计时并能打出很漂亮的log
StopWatch timer = new StopWatch();
timer.start(exchange.getRequest().getURI().getRawPath());//开始计时
//我们还可以对调用链进行加工,手工放入请求参数
exchange.getAttributes().put("requestTimeBegin",System.currentTimeMillis());
return chain.filter(exchange).then(
//这里就是执行完过滤进行调用的地方
Mono.fromRunnable(() -> {
timer.stop();
log.info(timer.prettyPrint());
})
);
}
@Override
public int getOrder() {
return 0;
}
}
拿上TimerFilter去到GatewayConfiguration里设置自定义filter
package com.icodingedu.springcloud.config;
import com.icodingedu.springcloud.filter.TimerFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@Configuration
public class GatewayConfiguration {
// @Autowired
// private TimerFilter timerFilter;
@Bean
@Order
public RouteLocator customerRoutes(RouteLocatorBuilder builder){
LocalDateTime ldt1 = LocalDateTime.of(2020,4,12,22,6,30);
LocalDateTime ldt2 = LocalDateTime.of(2020,4,12,23,6,35);
return builder.routes()
.route(r -> r.path("/gavinjava/**")
.and().method(HttpMethod.GET)
.and().header("name")
.filters(f -> f.stripPrefix(1)
.addResponseHeader("java-param","gateway-config")
// .filter(timerFilter)
)
.uri("lb://FEIGN-CLIENT")
)
.route(r -> r.path("/secondkill/**")
//.and().after(ZonedDateTime.of(ldt, ZoneId.of("Asia/Shanghai")))
.and().between(ZonedDateTime.of(ldt1, ZoneId.of("Asia/Shanghai")),ZonedDateTime.of(ldt2, ZoneId.of("Asia/Shanghai")))
.filters(f -> f.stripPrefix(1))
.uri("lb://FEIGN-CLIENT")
)
.build();
}
}
全局Filter就是把filter的继承从GatewayFilter换成GlobalFilter
从我们开始学JavaEE的时候,就被洗脑式灌输了一种权限验证的标准做法,那就是将用户的登录状态保存到HttpSession中,比如在登录成功后保存一对key-value值到session,key是userId而value是用户后台的真实ID。接着创建一个ServletFilter过滤器,用来拦截需要登录才能访问的资源,假如这个请求对应的服务端session里找不到userId这个key,那么就代表用户尚未登录,这时候可以直接拒绝服务然后重定向到用户登录页面。
大家应该都对session机制比较熟悉,它和cookie是相互依赖的,cookie是存放在用户浏览器中的信息,而session则是存放在服务器端的。当浏览器发起服务请求的时候就会带上cookie,服务器端接到Request后根据cookie中的jsessionid拿到对应的session。
由于我们只启动一台服务器,所以在登录后保存的session始终都在这台服务器中,可以很方便的获取到session中的所有信息。用这野路子,我们一路搞定了各种课程作业和毕业设计。结果一到工作岗位发现行不通了,因为所有应用都是集群部署,在一台机器保存了的session无法同步到其他机器上。那我们有什么成熟的解决方案吗?
Session复制是最容易先想到的解决方案,我们可以把一台机器中的session复制到集群中的其他机器。比如Tomcat中也有内置的session同步方案,但是这并不是一个很优雅的解决方案,它会带来以下两个问题:
这个方案可以放在Nignx网关层做的,我们可以指定某些IP段的请求落在某个指定机器上,这样一来session始终只存在一台机器上。不过相比前一种session复制的方法来说,绑定IP的方式有更明显的缺陷:
为了解决第二个问题,可以通过一致性Hash的路由方案来做路由,比如根据用户ID做Hash,不同的Hash值落在不同的机器上,保证足够均匀的分配,这样也就避免了IP切换的问题,但依然无法解决第一点里提到的负载均衡问题
这个方案解决了前面提到的大部分问题,session不再保存在服务器上,取而代之的是保存在redis中,所有的服务器都向redis写入/读取缓存信息。
在Tomcat层面,我们可以直接引入tomcat-redis-session-manager组件,将容器层面的session组件替换为基于redis的组件,但是这种方案和容器绑定的比较紧密。另一个更优雅的方案是借助spring-session管理redis中的session,尽管这个方案脱离了具体容器,但依然是基于Session的用户鉴权方案,这类Session方案已经在微服务应用中被淘汰了。
OAuth 2.0是一个开放授权标准协议,它允许用户让第三方应用访问该用户在某服务的特定私有资源,但是不提供账号密码信息给第三方应用
拿微信登录第三方应用的例子来说:
我们可以借助Spring Cloud中内置的spring-cloud-starter-oauth2
组件搭建OAuth 2.0的鉴权服务,OAuth 2.0的协议还涉及到很多复杂的规范,比如角色、客户端类型、授权模式等。
JWT也是一种基于Token的鉴权机制,它的基本思想就是通过用户名+密码换取一个Access Token
鉴权流程
相比OAuth 2.0来说,它的鉴权过程更加简单,其基本流程是这样的:
Access Token中的内容
JWT的Access Token由三个部分构成,分别是Header、Payload和Signature,我们分别看下这三个部分都包含了哪些信息:
{
'typ': 'JWT',
'alg': 'HS256'
}
目前实现JWT的开源组件非常多,如果决定使用这个方案,只要添加任意一个开源JWT实现的依赖项到项目的pom文件中,然后在加解密时调用该组件来完成
目前来说应用比较广泛的三种方案就是JWT、OAuth和spring-session+redis
通过以下几步完成鉴权操作
在gateway里创建一个auth-service-api
添加POM依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>auth-service-apiartifactId>
<name>auth-service-apiname>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
project>
创建一个entity包,创建一个账户实体对象
package com.icodingedu.springcloud.entity;
import com.sun.tracing.dtrace.ArgsAttributes;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Account implements Serializable {
private String username;
private String token;
//当token接近失效的时候可以用refreshToken生成一个新的token
private String refreshToken;
}
在entity包下,创建一个AuthResponse类
package com.icodingedu.springcloud.entity;
public class AuthResponse {
public static final Long SUCCESS = 1L;
public static final Long INCORRECT_PWD = 1000L;
public static final Long USER_NOT_FOUND = 1001L;
public static final Long INVALID_TOKEN = 1002L;
}
在entity包下创建一个AuthResponse处理结果类
package com.icodingedu.springcloud.tools;
import com.icodingedu.springcloud.pojo.Account;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {
private Account account;
private Long code;
}
创建一个service包在里面创建接口AuthService
package com.icodingedu.springcloud.service;
import com.icodingedu.springcloud.entity.AuthResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@FeignClient("auth-service")
public interface AuthService {
@PostMapping("/login")
@ResponseBody
public AuthResponse login(@RequestParam("username") String username,
@RequestParam("password") String password);
@GetMapping("/verify")
@ResponseBody
public AuthResponse verify(@RequestParam("token") String token,
@RequestParam("username") String username);
@PostMapping("/refresh")
@ResponseBody
public AuthResponse refresh(@RequestParam("refresh") String refreshToken);
}
创建服务实现的auth-service的module,还是放在gateway目录下
导入POM依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>auth-serviceartifactId>
<name>auth-servicename>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.7.0version>
dependency>
<dependency>
<groupId>com.icodingedugroupId>
<artifactId>auth-service-apiartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
project>
创建启动类application
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class AuthServiceApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(AuthServiceApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
创建一个service包,建立JwtService类
package com.icodingedu.springcloud.service;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.icodingedu.springcloud.entity.Account;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Date;
@Slf4j
@Service
public class JwtService {
//生产环境中应该从外部加密后传入
private static final String KEY = "you must change it";
//生产环境中应该从外部加密后传入
private static final String ISSUER = "icodingedu";
//定义个过期时间
private static final long TOKEN_EXP_TIME = 60000;
//定义传入的参数名
private static final String USERNAME = "username";
/**
* 生成token
* @param account 账户信息
* @return token
*/
public String token(Account account){
//生成token的时间
Date now = new Date();
//生成token所要用到的算法
Algorithm algorithm = Algorithm.HMAC256(KEY);
String token = JWT.create()
.withIssuer(ISSUER) //发行方,解密的时候依然要验证,即便拿到了key不知道发行方也无法解密
.withIssuedAt(now) //这个key是在什么时间点生成的
.withExpiresAt(new Date(now.getTime() + TOKEN_EXP_TIME)) //过期时间
.withClaim(USERNAME,account.getUsername()) //传入username
//.withClaim(ROLE,"roleName") 还可以传入其他内容
.sign(algorithm); //用前面的算法签发
log.info("jwt generated user={}",account.getUsername());
return token;
}
/**
* 验证token
* @param token
* @param username
* @return
*/
public boolean verify(String token,String username){
log.info("verify jwt - user={}",username);
try{
//加密和解密要一样
Algorithm algorithm = Algorithm.HMAC256(KEY);
//构建一个验证器:验证JWT的内容,是个接口
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER) //前面加密的内容都可以验证
.withClaim(USERNAME,username)
.build();
//这里有任何错误就直接异常了
verifier.verify(token);
return true;
}catch (Exception ex){
log.error("auth failed",ex);
return false;
}
}
}
创建controller包,建立JwtController类
package com.icodingedu.springcloud.controller;
import com.icodingedu.springcloud.entity.Account;
import com.icodingedu.springcloud.entity.AuthResponse;
import com.icodingedu.springcloud.entity.AuthResponseCode;
import com.icodingedu.springcloud.service.AuthService;
import com.icodingedu.springcloud.service.JwtService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@Slf4j
public class JwtController implements AuthService{
@Autowired
private JwtService jwtService;
@Autowired
private RedisTemplate redisTemplate;
@Override
public AuthResponse login(String username, String password) {
Account account = Account.builder()
.username(username)
.build();
//TODO 0-这一步需要验证用户名和密码,一般是在数据库里,假定验证通过了
//1-生成token
String token = jwtService.token(account);
account.setToken(token);
//这里保存拿到新token的key
account.setRefreshToken(UUID.randomUUID().toString());
//3-保存token,把token保存起来在refresh时才知道更新关联哪个token
redisTemplate.opsForValue().set(account.getRefreshToken(),account);
//2-返回token
return AuthResponse.builder()
.account(account)
.code(AuthResponseCode.SUCCESS)
.build();
}
@Override
public AuthResponse verify(String token, String username) {
boolean flag = jwtService.verify(token, username);
return AuthResponse.builder()
.code(flag?AuthResponseCode.SUCCESS:AuthResponseCode.INVALID_TOKEN)
.build();
}
@Override
public AuthResponse refresh(String refreshToken) {
//当使用redisTemplate保存对象时,对象必须是一个可被序列化的对象
Account account = (Account) redisTemplate.opsForValue().get(refreshToken);
if(account == null){
return AuthResponse.builder()
.code(AuthResponseCode.USER_NOT_FOUND)
.build();
}
String token = jwtService.token(account);
account.setToken(token);
//更新新的refreshToke
account.setRefreshToken(UUID.randomUUID().toString());
//将原来的删除
redisTemplate.delete(refreshToken);
//添加新的token
redisTemplate.opsForValue().set(account.getRefreshToken(),account);
return AuthResponse.builder()
.account(account)
.code(AuthResponseCode.SUCCESS)
.build();
}
}
设置application配置文件
spring.application.name=auth-service
server.port=65100
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
spring.redis.host=localhost
spring.redis.database=0
spring.redis.port=6379
info.app.name=auth-service
info.app.description=test
management.security.enabled=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
可以启动验证一下:先启动eureka-server,再启动auth-server
在PostMan里进行验证:login,verify,refresh都测试一下
开始改造gateway-sever
POM里引入依赖,增加下面三个依赖
<dependency>
<groupId>com.icodingedugroupId>
<artifactId>auth-service-apiartifactId>
<version>${project.version}version>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.5version>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.7.0version>
dependency>
创建一个新的类:AuthFilter
package com.icodingedu.springcloud.filter;
import com.icodingedu.springcloud.entity.AuthResponse;
import com.icodingedu.springcloud.service.AuthService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class AuthFilter implements GatewayFilter, Ordered {
private static final String AUTH = "Authorization";
private static final String USERNAME = "icodingedu-username";
@Autowired
private RestTemplate restTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Auth Start");
ServerHttpRequest request = exchange.getRequest();
HttpHeaders header = request.getHeaders();
String token = header.getFirst(AUTH);
String username = header.getFirst(USERNAME);
ServerHttpResponse response = exchange.getResponse();
if(StringUtils.isBlank(token)){
log.error("token not found");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
String path = String.format("http://auth-service/verify?token=%s&username=%s",token,username);
AuthResponse resp = restTemplate.getForObject(path,AuthResponse.class);
if(resp.getCode() != 1L){
log.error("invalid token");
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
//将用户信息再次存放在请求的header中传递给下游
ServerHttpRequest.Builder mutate = request.mutate();
mutate.header(USERNAME,username);
ServerHttpRequest buildRequest = mutate.build();
//如果响应中需要放数据,可以放在response的header中
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add("icoding-user",username);
return chain.filter(exchange.mutate()
.request(buildRequest)
.response(response)
.build());
}
@Override
public int getOrder() {
return 0;
}
}
给gateway-server的application启动类加上RestTemplate实现
package com.icodingedu.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayServerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class,args);
}
}
AuthFilter注入到configuration中的,只需要注入并加入过滤器即可
网关层的异常分为以下两种:
调用请求异常 通常由调用请求直接抛出的异常,比如在订单服务中直接报错
throw new RuntimeException("error")
网关层异常 由网关层触发的异常,比如Gateway通过服务发现找不到可用节点,或者任何网关层内部的问题。这部分异常通常是在实际调用请求发起之前发生的。
在以上两种问题中,网关层只应该关注第二个点,也就是自身异常。在实际应用中我们应该尽量保持网关层的“纯洁性”并且做好职责划分,Gateway只要做好路由的事情,不要牵扯到具体业务层的事儿,最好也不要替调用请求的异常操心。对于业务调用中的异常情况,如果需要采用统一格式封装调用异常,那就交给每个具体服务去定义结构,让各自业务方和前端页面协调好异常消息的结构。
但是在实际项目中,不能保证每个接口都实现了异常封装,如果想给前台页面一个统一风格的JSON格式异常结构,那就需要让Gateway做一些分外的事儿,比如拦截Response并修改返回值。(还是强烈建议让服务端自己定义异常结构,因为Gateway本身不应该对这些异常做额外封装只是原封不动的返回)
Gateway已经将网关层直接抛出的异常(没有调用远程服务之前的异常)做了结构化封装,对于POST的调用来说其本身也会返回结构化的异常信息,但是对于GET接口的异常来说,则是直接返回一个HTML页面,前端根本无法抓取具体的异常信息。所以我们这里主要聚焦在如何处理调用请求异常。
装饰器编程模式+代理模式,给Gateway加一层处理,改变ResponseBody中的数据结构
代理模式 - BodyHackerFunction接口
在最开始我们先定义一个代理模式的接口
package com.icodingedu.springcloud.tools;
import org.reactivestreams.Publisher;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import reactor.core.publisher.Mono;
import java.util.function.BiFunction;
public interface BodyHackerFunction extends
BiFunction<ServerHttpResponse, Publisher<? extends DataBuffer>, Mono<Void>> {
}
这里引入代理模式是为了将装饰器和具体业务代理逻辑拆分开来,在装饰器中只需要依赖一个代理接口,而不需要和具体的代理逻辑绑定起来
装饰器模式 - BodyHackerDecrator
接下来我们定义一个装饰器类,这个装饰器继承自ServerHttpResponseDecorator类,我们这里就用装饰器模式给Response Body的构造过程加上一层特效
package com.icodingedu.springcloud.tools;
import org.reactivestreams.Publisher;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import reactor.core.publisher.Mono;
public class BodyHackerHttpResponseDecorator extends ServerHttpResponseDecorator {
/**
* 负责具体写入Body内容的代理类
*/
private BodyHackerFunction delegate = null;
public BodyHackerHttpResponseDecorator(BodyHackerFunction bodyHandler, ServerHttpResponse delegate) {
super(delegate);
this.delegate = bodyHandler;
}
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
return delegate.apply(getDelegate(), body);
}
}
这个装饰器的构造方法接收一个BodyHancker代理类,其中的关键方法writeWith就是用来向Response Body中写入内容的。这里我们覆盖了该方法,使用代理类来托管方法的执行,而在整个装饰器类中看不到一点业务逻辑,这就是我们常说的单一职责。
创建Filter
package com.icodingedu.springcloud.filter;
import com.icodingedu.springcloud.tools.BodyHackerFunction;
import com.icodingedu.springcloud.tools.BodyHackerHttpResponseDecorator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class ErrorFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
final ServerHttpRequest request = exchange.getRequest();
// TODO 这里定义写入Body的逻辑
BodyHackerFunction delegate = (resp, body) -> Flux.from(body)
.flatMap(orgBody -> {
// 原始的response body
byte[] orgContent = new byte[orgBody.readableByteCount()];
orgBody.read(orgContent);
String content = new String(orgContent);
log.info("original content {}", content);
// 如果500错误,则替换
if (resp.getStatusCode().value() == 500) {
content = String.format("{\"status\":%d,\"path\":\"%s\"}",
resp.getStatusCode().value(),
request.getPath().value());
}
// 告知客户端Body的长度,如果不设置的话客户端会一直处于等待状态不结束
HttpHeaders headers = resp.getHeaders();
headers.setContentLength(content.length());
return resp.writeWith(Flux.just(content)
.map(bx -> resp.bufferFactory().wrap(bx.getBytes())));
}).then();
// 将装饰器当做Response返回
BodyHackerHttpResponseDecorator responseDecorator = new BodyHackerHttpResponseDecorator(delegate, exchange.getResponse());
return chain.filter(exchange.mutate().response(responseDecorator).build());
}
@Override
public int getOrder() {
// WRITE_RESPONSE_FILTER的执行顺序是-1,我们的Hacker在它之前执行
return -2;
}
}
在这个Filter中,我们定义了一个装饰器类BodyHackerHttpResponseDecorator,同时声明了一个匿名内部类(代码TODO部分),实现了BodyHackerFunction代理类的Body替换逻辑,并且将这个代理类传入了装饰器。这个装饰器将直接参与构造Response Body。
我们还覆盖了getOrder方法,是为了确保我们的filter在默认的Response构造器之前执行
我们对500的HTTP Status做了特殊定制,使用我们自己的JSON内容替换了原始内容,同学们可以根据需要向JSON中加入其它参数。对于其他非500 Status的Response来说,我们还是返回初始的Body。
我们在feign-client-advanced的GatewayController中定一个500的错误方法进行测试
@GetMapping("/valid")
public String valid(){
int i = 1/0;
return "Page Test Success";
}
ErrorFilter的注入方式同之前的过滤器一样
创建一个限流配置类RedisLimiterConfiguration
package com.icodingedu.springcloud.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;
@Configuration
public class RedisLimiterConfiguration {
// ID: KEY 限流的业务标识
// 我们这里根据用户请求IP地址进行限流
@Bean
@Primary //一个系统不止一个KeyResolver
public KeyResolver remoteAddressKeyResolver(){
return exchange -> Mono.just(
exchange.getRequest()
.getRemoteAddress()
.getAddress()
.getHostAddress()
);
}
@Bean("redisLimiterUser")
@Primary
public RedisRateLimiter redisRateLimiterUser(){
//这里可以自己创建一个限流脚本,也可以使用默认的令牌桶
//defaultReplenishRate:限流桶速率,每秒10个
//defaultBurstCapacity:桶的容量
return new RedisRateLimiter(10,60);
}
@Bean("redisLimiterProduct")
public RedisRateLimiter redisRateLimiterProduct(){
//这里可以自己创建一个限流脚本,也可以使用默认的令牌桶
//defaultReplenishRate:限流桶速率,每秒10个
//defaultBurstCapacity:桶的容量
return new RedisRateLimiter(20,100);
}
}
配置application.yaml 中的redis信息
spring:
application:
name: gateway-server
redis:
host: localhost
port: 6379
database: 0
main:
allow-bean-definition-overriding: true
在GatewayConfiguration中进行配置加入RedisLimiter的配置
package com.icodingedu.springcloud.config;
import com.icodingedu.springcloud.filter.AuthFilter;
import com.icodingedu.springcloud.filter.ErrorFilter;
import com.icodingedu.springcloud.filter.TimerFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@Configuration
public class GatewayConfiguration {
// @Autowired
// private TimerFilter timerFilter;
@Autowired
private AuthFilter authFilter;
@Autowired
private ErrorFilter errorFilter;
@Autowired
private KeyResolver hostNameResolver;
@Autowired
@Qualifier("redisLimiterUser")
private RateLimiter rateLimiterUser;
@Bean
@Order
public RouteLocator customerRoutes(RouteLocatorBuilder builder){
LocalDateTime ldt1 = LocalDateTime.of(2020,4,12,22,6,30);
LocalDateTime ldt2 = LocalDateTime.of(2020,4,12,23,6,35);
return builder.routes()
.route(r -> r.path("/gavinjava/**")
.and().method(HttpMethod.GET)
// .and().header("name")
.filters(f -> f.stripPrefix(1)
.addResponseHeader("java-param","gateway-config")
// .filter(timerFilter)
// .filter(authFilter)
.filter(errorFilter)
.requestRateLimiter(
c -> {
c.setKeyResolver(hostNameResolver);
c.setRateLimiter(rateLimiterUser);
})
)
.uri("lb://FEIGN-CLIENT")
)
.route(r -> r.path("/secondkill/**")
//.and().after(ZonedDateTime.of(ldt, ZoneId.of("Asia/Shanghai")))
.and().between(ZonedDateTime.of(ldt1, ZoneId.of("Asia/Shanghai")),ZonedDateTime.of(ldt2, ZoneId.of("Asia/Shanghai")))
.filters(f -> f.stripPrefix(1))
.uri("lb://FEIGN-CLIENT")
)
.build();
}
}
微服务之间的调用关系是网状的
根本无法用人力梳理出上线游调用关系,通过调用链追踪技术进行调用关系梳理,得处调用拓扑图,理清上下游关系以及更细粒度的时间报表
HTTP–>服务A–>服务D–>服务C–>服务F
用户请求访问了服务A,接着服务A又在内部先后调用了服务D,C和F,在这里Sleuth的工作就是通过一种“打标”的机制,将这个链路上的所有被访问到的服务打上一个相同的标记,这样我们只要拿到这个标记,就很容易可以追溯到链路上下游所有的调用
借助Sleuth的链路追踪能力,我们还可以完成一些其他的任务,比如说:
Sleuth的设计理念
哪些数据需要埋点
每一个微服务都有自己的Log组件(slf4j,logback等各不相同),当我们集成了Sleuth之后,它便会将链路信息传递给底层Log组件,同时Log组件会在每行Log的头部输出这些数据,这个埋点动作主要会记录两个关键信息:
比如这里服务A是起始节点,所以它的Event ID(单元ID)和Trace ID(链路ID)相同,而服务B的前置节点就是A节点,所以B的Parent Event就指向A的Event ID。而C在B的下游,所以C的Parent就指向B。A、B和C三个服务都有同一个链路ID,但是各自有不同的单元ID。
数据埋点之前要解决的问题
看起来创建埋点数据是件很容易的事儿,但是想让这套方案在微服务集群环境下生效,我们还需要先解决两个核心问题:
MDC
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能
MDC是通过InheritableThreadLocal来实现的,它可以携带当前线程的上下文信息。它的底层是一个Map结构,存储了一系列Key-Value的值。Sleuth就是借助Spring的AOP机制,在方法调用的时候配置了切面,将链路追踪数据加入到了MDC中,这样在打印Log的时候,就能从MDC中获取这些值,填入到Log Format中的占位符里。
Sleuth从一个调用请求开始直到结束,不管中途又调用了多少外部服务,从头到尾一直贯穿一个ID。
Span不单单只是一个ID,它还包含一些其他信息,比如时间戳,它标识了一个事件从开始到结束经过的时间,我们可以用这个信息来统计接口的执行时间。
我们知道了Trace ID和Span ID,问题就是如何在不同服务节点之间传递这些ID。在Eureka的服务治理下所有调用请求都是基于HTTP的,那我们的链路追踪ID也一定是HTTP请求中的一部分。把ID加在HTTP哪里呢,一来GET请求压根就没有Body,二来加入Body还有可能影响后台服务的反序列化。那加在URL后面呢?似乎也不妥,因为某些服务组件对URL的长度可能做了限制(比如Nginx可以设置最大URL长度)。
那剩下的只有Header了!Sleuth正是通过Filter向Header中添加追踪信息,我们来看下面表格中Header Name和Trace Data的对应关系:
HTTP Header Name | Trace Data | 说明 |
---|---|---|
X-B3-TraceId | Trace ID | 链路全局唯一ID |
X-B3-SpanId | Span ID | 当前Span的ID |
X-B3-ParentSpanId | Parent Span ID | 前一个Span的ID |
X-Span-Export | Can be exported for sampling or not | 是否可以被采样 |
在调用下一个服务的时候,Sleuth会在当前的Request Header中写入上面的信息,这样下游系统就很容易识别出当前Trace ID以及它的前置Span ID是什么
创建sleuth-trace-a、sleuth-trace-b、sleuth-trace-c模块,先创建一个sleuth-trace-a
导入POM依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>sleuth-trace-aartifactId>
<name>sleuth-trace-aname>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
dependencies>
project>
创建application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@SpringBootApplication
public class SleuthTraceAApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
new SpringApplicationBuilder(SleuthTraceAApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
创建controller实现集成sleuth
package com.icodingedu.springcloud.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@Slf4j
public class SleuthController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/traceA")
public String traceA(){
log.info("--------------TraceA");
return restTemplate.getForEntity("http://sleuth-traceB/traceB",String.class).getBody();
}
}
创建配置文件,可以从auth-service中复制一部分
spring.application.name=sleuth-traceA
server.port=65501
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
logging.file=${spring.application.name}.log
# 采样率,1就表示100%,0.8表示80%
spring.sleuth.sampler.probability=1
management.security.enabled=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
添加logback-spring.xml配置文件
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<springProperty scope="context" name="springAppName"
source="spring.application.name" />
<property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}" />
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFOlevel>
filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}pattern>
<charset>utf8charset>
encoder>
appender>
<root level="INFO">
<appender-ref ref="console" />
root>
configuration>
sleuth-trace-b和sleuth-trace-c和sleuth-trace-a类似,修改controller和properties文件即可
Why Zipkin
我们先思考一个问题:Sleuth空有一身本领,可是没个页面可以show出来,而且Sleuth似乎只是自娱自乐在log里埋点,却没有一个汇聚信息的能力,不方便对整个集群的调用链路进行分析。Sleuth目前的情形就像Hystrix一样,也需要一个类似Turbine的组件做信息聚合+展示的功能。在这个背景下,Zipkin就是一个不错的选择。
Zipkin是一套分布式实时数据追踪系统,它主要关注的是时间维度的监控数据,比如某个调用链路下各个阶段所花费的时间,同时还可以从可视化的角度帮我们梳理上下游系统之间的依赖关系。
Zipkin由来
Zipkin也是来源于Google发布的一篇有关分布式监控系统论文(论文名称《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》),Twitter基于该论文研发了一套开源实现-Zipkin
Zipkin的核心功能
Zipkin的主要作用是收集Timing维度的数据,以供查找调用延迟等线上问题。所谓Timing其实就是开始时间+结束时间的标记,有了这两个时间信息,我们就能计算得出调用链路每个步骤的耗时。Zipkin的核心功能有以下两点
Zipkin分为服务端和客户端,服务端是一个专门负责收集数据、查找数据的中心Portal,而每个客户端负责把结构化的Timing数据发送到服务端,供服务端做索引和分析。这里我们重点关注一下“Timing数据”到底用来做什么,前面我们说过Zipkin主要解决调用延迟情况的线上排查,它通过收集一个调用链上下游所有工作单元的独立用时,Zipkin就能知道每个环节在服务总用时中所占的比重,再通过图形化界面的形式,让开发人员知道性能瓶颈出在哪里。
Zipkin提供了多种维度的查找功能用来检索Span的耗时,最直观的是通过Trace ID查找整个Trace链路上所有Span的前后调用关系和每阶段的用时,还可以根据Service Name或者访问路径等维度进行查找。
创建一个zipkin-server的module
导入POM依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>zipkin-serverartifactId>
<name>zipkin-servername>
<dependencies>
<dependency>
<groupId>io.zipkin.javagroupId>
<artifactId>zipkin-serverartifactId>
<version>2.8.4version>
dependency>
<dependency>
<groupId>io.zipkin.javagroupId>
<artifactId>zipkin-autoconfigure-uiartifactId>
<version>2.8.4version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<mainClass>com.icodingedu.springcloud.ZipkinServerApplicationmainClass>
configuration>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
创建application启动类
package com.icodingedu.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import zipkin.server.internal.EnableZipkinServer;
@SpringBootApplication
@EnableZipkinServer
public class ZipkinServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ZipkinServerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
设置application.properties配置文件
spring.application.name=zipkin-server
server.port=65503
spring.main.allow-bean-definition-overriding=true
# 关闭后台窗口输出的一些无效错误日志
management.metrics.web.server.auto-time-requests=false
SpringCloud F版以后都可以通过jar包的形式直接启动了,下载地址如下
https://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/
下载最新版的:zipkin-server-2.12.9-exec.jar
然后 java -jar zipkin-server-2.12.9-exec.jar 运行即可
访问地址:http://localhsot:9411 默认端口9411
在sleuth-trace-a、sleuth-trace-b、sleuth-trace-c模块集成zipkin服务
增加POM依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
增加properties的属性
# zipkin地址
spring.zipkin.base-url=http://localhost:65503
# 采样率,1就表示100%,0.8表示80% 采样率和zipkin是一对同时生效的
# spring.sleuth.sampler.probability=1
给zipkin的POM中加入eureka-client的依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<configuration>
<mainClass>com.icodingedu.springcloud.ZipkinServerApplicationmainClass>
configuration>
因为要做高可用,所以在Application启动类上要加入Eureka的注解
@SpringBootApplication
@EnableZipkinServer
@EnableDiscoveryClient
public class ZipkinServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ZipkinServerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
application配置文件里增加注册中心配置
eureka.client.serviceUrl.defaultZone=http://localhost:20001/eureka/
zipkin已经注册到eureka中了
这时就需要将traceA、traceB之前连接的ip地址更换成服务名并开启自动发现服务
# zipkin地址
spring.zipkin.sender.type=web
spring.zipkin.discovery-client-enabled=true
spring.zipkin.locator.discovery.enabled=true
spring.zipkin.base-url=http://ZIPKIN-SERVER/
# 1-安装Elasticsearch
# 2-安装kibana
# 3-配置Logstash
# 解压Logstash在根目录创建新的配置文件目录
mkdir sync
vi logstash-log-sync.conf
input {
tcp {
port => 5044
codec => json_lines
}
}
output {
elasticsearch {
hosts => ["192.168.0.200:9200"]
}
}
去到Kibanan创建日志查询工具
创建完毕后,还是通过discover点击进入查询页面
将Sleuth的数据对接进ELK
引入POM依赖
<dependency>
<groupId>net.logstash.logbackgroupId>
<artifactId>logstash-logback-encoderartifactId>
<version>5.2version>
dependency>
将traceA和traceB、traceC项目中日志模版增加Logstash日志输出部分,Logstash日志输出级别
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<springProperty scope="context" name="springAppName"
source="spring.application.name" />
<property name="LOG_FILE" value="${BUILD_FOLDER:-build}/${springAppName}" />
<property name="CONSOLE_LOG_PATTERN"
value="%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFOlevel>
filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}pattern>
<charset>utf8charset>
encoder>
appender>
<appender name="logstash"
class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>39.99.216.16:5044destination>
<encoder
class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTCtimeZone>
timestamp>
<pattern>
<pattern>
{
"severity": "%level",
"service": "${springAppName:-}",
"trace": "%X{X-B3-TraceId:-}",
"span": "%X{X-B3-SpanId:-}",
"exportable": "%X{X-Span-Export:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{40}",
"rest": "%message"
}
pattern>
pattern>
providers>
encoder>
appender>
<root level="INFO">
<appender-ref ref="console" />
<appender-ref ref="logstash" />
root>
configuration>
配置完数据源去启动Logstash并指定刚刚创建的配置文件
# 进入logstash/bin目录下
./logstash -f /usr/local/logstash/sync/logstash-log-sync.conf
springcloud就是想通过这些全家桶的组件来屏蔽底层差异,使用springcloud的使用者只关注业务
跨系统异步通信
系统应用解耦
流量削峰
SpringCloud Stream是基于SpringBoot构建的,专门为构建消息驱动服务设计的应用框架,它的底层是使用Spring Integration来为消息代理层提供网络连接支持的
创建一个新的stream目录,建立stream-server的module
创建POM依赖
<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>spring-cloud-projectartifactId>
<groupId>com.icodingedugroupId>
<version>1.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>stream-serverartifactId>
<name>stream-servername>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
dependencies>
project>
创建启动类application
package com.icodingeud.springcloud;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@SpringBootApplication
public class StreamServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(StreamServerApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
创建消息体对象,新建一个entity包
package com.icodingeud.springcloud.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MessageBean {
//生产者产生的消息体
private String payload;
}
创建接收消息的业务对象,创建一个service包
package com.icodingeud.springcloud.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
@Slf4j
@EnableBinding(value = {
Sink.class
})
public class StreamConsumer {
//这里先使用stream给的默认topic
@StreamListener(Sink.INPUT)
public void consumer(Object payload){
log.info("message consumed successfully, payload={}",payload);
}
}
配置文件properties
spring.application.name=stream-server
server.port=63000
# RabbitMQ连接字符串
spring.rabbitmq.host=39.98.53.94
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
management.security.enabled=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
启动项目后在rabbitMQ的queue中去查看创建了一个新的队列
创建一个topic的包,建立自己的topic类
package com.icodingeud.springcloud.topic;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface MyTopic {
String INPUT = "myTopic";
//可以被订阅的通道
//stream中input指接收消息端
//output是生产发送消息端
@Input(INPUT)
SubscribableChannel input();
}
在consumer类里增加自己的topic接收,一个自定义的消息消费者就创建好了
package com.icodingeud.springcloud.service;
import com.icodingeud.springcloud.topic.MyTopic;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
@Slf4j
@EnableBinding(value = {
Sink.class,
MyTopic.class
})
public class StreamConsumer {
//这里先使用stream给的默认topic
@StreamListener(Sink.INPUT)
public void consumer(Object payload){
log.info("message consumed successfully, payload={}",payload);
}
@StreamListener(MyTopic.INPUT)
public void consumerMyMessage(Object payload){
log.info("My Message consumed successfully, payload={}",payload);
}
}
接下来我们创建生产者,在MyTopic里定义消息生产者
package com.icodingeud.springcloud.topic;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface MyTopic {
String INPUT = "myTopic";
//可以被订阅的通道
//stream中input指接收消息端
//output是生产发送消息端
@Input(INPUT)
SubscribableChannel input();
//TODO 按照正常理解这里应该可以一样,我们先测试一下
@Output(INPUT)
MessageChannel output();
}
定义一个controller调用生产者来发送一条消息,创建一个controller的包
package com.icodingeud.springcloud.controller;
import com.icodingeud.springcloud.topic.MyTopic;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class StreamController {
//这就是由stream给我完成注入和绑定了
@Autowired
private MyTopic producer;
@PostMapping("send")
public void sendMessage(@RequestParam("body") String body){
producer.output().send(MessageBuilder.withPayload(body).build());
}
}
再次启动项目,这个时候会出现错误
org.springframework.beans.factory.BeanDefinitionStoreException: Invalid bean definition with name 'myTopic' defined in com.icodingeud.springcloud.topic.MyTopic: bean definition with this name already exists
告诉你之前定义的myTopic重复了,虽然我们前面认为input和output应该用一个topic名,但这里相当于声明了两个一样名字的bean,spring启动会报错
两步处理
1、将input和output分开定义:但这里发送和接收就不在一个topic里了,就会导致各自发送和接收
package com.icodingeud.springcloud.topic;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface MyTopic {
String INPUT = "myTopic-consumer";
String OUTPUT = "myTopic-producer";
//可以被订阅的通道
//stream中input指接收消息端
//output是生产发送消息端
@Input(INPUT)
SubscribableChannel input();
//TODO 按照正常理解这里应该可以一样,我们先测试一下
@Output(OUTPUT)
MessageChannel output();
}
2、通过配置文件将两个topic绑定到一起
# 将两个channel绑定到同一个topic上
spring.cloud.stream.bindings.myTopic-consumer.destination=mybroadcast
spring.cloud.stream.bindings.myTopic-producer.destination=mybroadcast
启动两个项目,去RMQ中看一下,发现mybroadcast其实就是exchange并绑定了两个queue
在上面这个例子中,“商品发布”就是一个消息,它被放到了对应的消息队列中,有两拨人马同时盯着这个Topic,这两拨人马各自组成了一个Group,每次有新消息到来的时候,每个Group就派出一个代表去响应,而且是从这个Group中轮流挑选代表(负载均衡),这里的Group也就是我们说的消费者。
消费组相当于是每组派一个代表去办事儿,而消费分区相当于是专事专办,也就是说,所有消息都会根据分区Key进行划分,带有相同Key的消息只能被同一个消费者处理。
消息分区有一个预定义的分区Key,它是一个SpEL表达式。我们需要在配置文件中指定分区的总个数N,Stream就会为我们创建N个分区,这里面每个分区就是一个Queue(可以在RabbitMQ管理界面中看到所有的分区队列)。
当商品发布的消息被生产者发布时,Stream会计算得出分区Key,从而决定这个消息应该加入到哪个Queue里面。在这个过程中,每个消费组/消费者仅会连接到一个Queue,这个Queue中对应的消息只能被特定的消费组/消费者来处理。
先创建一个GroupTopic接口
package com.icodingeud.springcloud.topic;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface GroupTopic {
String INPUT = "group-consumer";
String OUTPUT = "group-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
在controller里进行注入并加入消息生产者
package com.icodingeud.springcloud.controller;
import com.icodingeud.springcloud.topic.GroupTopic;
import com.icodingeud.springcloud.topic.MyTopic;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class StreamController {
//这就是由stream给我完成注入和绑定了
@Autowired
private MyTopic producer;
@Autowired
private GroupTopic groupProducer;
@PostMapping("send")
public void sendMessage(@RequestParam("body") String body){
producer.output().send(MessageBuilder.withPayload(body).build());
}
@PostMapping("sendgroup")
public void sendGroupMessage(@RequestParam("body") String body){
groupProducer.output().send(MessageBuilder.withPayload(body).build());
}
}
创建消息消费者在consumer里进行添加
package com.icodingeud.springcloud.service;
import com.icodingeud.springcloud.topic.GroupTopic;
import com.icodingeud.springcloud.topic.MyTopic;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
@Slf4j
@EnableBinding(value = {
Sink.class,
MyTopic.class,
GroupTopic.class
})
public class StreamConsumer {
//这里先使用stream给的默认topic
@StreamListener(Sink.INPUT)
public void consumer(Object payload){
log.info("message consumed successfully, payload={}",payload);
}
@StreamListener(MyTopic.INPUT)
public void consumerMyMessage(Object payload){
log.info("My Message consumed successfully, payload={}",payload);
}
@StreamListener(GroupTopic.INPUT)
public void consumerGroupMessage(Object payload){
log.info("Group Message consumed successfully, payload={}",payload);
}
}
最后设置配置文件
spring.cloud.stream.bindings.group-consumer.destination=group-exchange
spring.cloud.stream.bindings.group-producer.destination=group-exchange
# 消费分组是对于消息的消费者来说的
spring.cloud.stream.bindings.group-consumer.group=Group-A
Group-A启动两个服务进行一下测试,看是否一次只接收一个消息并且是轮询接收
可以看一下这个在RMQ中的形式,其实是只创建了一个消息队列:group-exchange.Group-A
只有一个队列接收消息然后由stream转给其中一个消费者
可以把分组名改成Group-B后再创建两个端口实例测试一下,每个消息被所有消费组中的一个消费者消费
前提:先要把RabbitMQ的延时插件:rabbitmq_delayed_message_exchange 安装好
已安装插件查看命令:rabbitmq-plugins list
安装完成后在RMQ管理界面创建exchage那里可以看到type中增加了x-delayed-message类型就ok了
创建一个DelayedTopic
package com.icodingeud.springcloud.topic;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface DelayedTopic {
String INPUT = "delayed-consumer";
String OUTPUT = "delayed-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
修改controller,加入DelayedTopic
package com.icodingeud.springcloud.controller;
import com.icodingeud.springcloud.entity.MessageBean;
import com.icodingeud.springcloud.topic.DelayedTopic;
import com.icodingeud.springcloud.topic.GroupTopic;
import com.icodingeud.springcloud.topic.MyTopic;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class StreamController {
//这就是由stream给我们完成注入和绑定了
@Autowired
private MyTopic producer;
@Autowired
private GroupTopic groupProducer;
@Autowired
private DelayedTopic delayedProducer;
@PostMapping("send")
public void sendMessage(@RequestParam("body") String body){
producer.output().send(MessageBuilder.withPayload(body).build());
}
@PostMapping("sendgroup")
public void sendGroupMessage(@RequestParam("body") String body){
groupProducer.output().send(MessageBuilder.withPayload(body).build());
}
@PostMapping("senddm")
public void sendDelayedMessage(@RequestParam("body") String body,
@RequestParam("second") Integer second){
MessageBean messageBean = new MessageBean();
messageBean.setPayload(body);
log.info("***** 准备进入延迟发送队列.....");
delayedProducer.output().send(MessageBuilder.withPayload(messageBean)
.setHeader("x-delay",1000 * second)
.build());
}
}
修改consumer实现,增加DelayedTopic实现
package com.icodingeud.springcloud.service;
import com.icodingeud.springcloud.entity.MessageBean;
import com.icodingeud.springcloud.topic.DelayedTopic;
import com.icodingeud.springcloud.topic.GroupTopic;
import com.icodingeud.springcloud.topic.MyTopic;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
@Slf4j
@EnableBinding(value = {
Sink.class,
MyTopic.class,
GroupTopic.class,
DelayedTopic.class
})
public class StreamConsumer {
//这里先使用stream给的默认topic
@StreamListener(Sink.INPUT)
public void consumer(Object payload){
log.info("message consumed successfully, payload={}",payload);
}
@StreamListener(MyTopic.INPUT)
public void consumerMyMessage(Object payload){
log.info("My Message consumed successfully, payload={}",payload);
}
@StreamListener(GroupTopic.INPUT)
public void consumerGroupMessage(Object payload){
log.info("Group Message consumed successfully, payload={}",payload);
}
@StreamListener(DelayedTopic.INPUT)
public void consumerDelayedMessage(MessageBean bean){
log.info("Delayed Message consumed successfully, payload={}",bean.getPayload());
}
}
增加配置文件
# 延迟消息配置
spring.cloud.stream.bindings.delayed-consumer.destination=delayed-exchange
spring.cloud.stream.bindings.delayed-producer.destination=delayed-exchange
# 声明exchange类型
spring.cloud.stream.rabbit.bindings.delayed-producer.producer.delayed-exchange=true
创建重试的Topic
package com.icodingeud.springcloud.topic;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface ErrorTopic {
String INPUT = "error-consumer";
String OUTPUT = "error-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
修改controller进行消息发送
@Autowired
private ErrorTopic errorProducer;
//单机版错误重试
@PostMapping("senderror")
public void sendErrorMessage(@RequestParam("body") String body){
errorProducer.output().send(MessageBuilder.withPayload(body).build());
}
修改consumer
package com.icodingeud.springcloud.service;
import com.icodingeud.springcloud.entity.MessageBean;
import com.icodingeud.springcloud.topic.DelayedTopic;
import com.icodingeud.springcloud.topic.ErrorTopic;
import com.icodingeud.springcloud.topic.GroupTopic;
import com.icodingeud.springcloud.topic.MyTopic;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@EnableBinding(value = {
Sink.class,
MyTopic.class,
GroupTopic.class,
DelayedTopic.class,
ErrorTopic.class
})
public class StreamConsumer {
//定一个一个线程安全的变量
private AtomicInteger count = new AtomicInteger(1);
//这里先使用stream给的默认topic
@StreamListener(Sink.INPUT)
public void consumer(Object payload){
log.info("message consumed successfully, payload={}",payload);
}
@StreamListener(MyTopic.INPUT)
public void consumerMyMessage(Object payload){
log.info("My Message consumed successfully, payload={}",payload);
}
@StreamListener(GroupTopic.INPUT)
public void consumerGroupMessage(Object payload){
log.info("Group Message consumed successfully, payload={}",payload);
}
@StreamListener(DelayedTopic.INPUT)
public void consumerDelayedMessage(MessageBean bean){
log.info("Delayed Message consumed successfully, payload={}",bean.getPayload());
}
//异常重试单机版
@StreamListener(ErrorTopic.INPUT)
public void consumerErrorMessage(Object payload){
log.info("****** 进入异常处理 ******");
//计数器进来就自增1
if(count.incrementAndGet() % 3 == 0){
log.info("====== 完全没有问题! ======");
count.set(0);
}else{
log.info("----- what's your problem? -----");
throw new RuntimeException("****** 整个人都不行了 ******");
}
}
}
配置properties文件
# 单机错误重试消息配置
spring.cloud.stream.bindings.error-consumer.destination=error-exchange
spring.cloud.stream.bindings.error-producer.destination=error-exchange
# 重试次数(本机重试,是在客户端这里不断重试而不会发回给RabbitMQ)
# 次数=1相当于不重试
spring.cloud.stream.bindings.error-consumer.consumer.max-attempts=2
需要注意的点:
首先要注意一点:Re-queue和前面的本地重试Retry是有冲突的,配置了Retry的消息就不会触发Re-queue
创建RequeueTopic
package com.icodingeud.springcloud.topic;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface RequeueTopic {
String INPUT = "requeue-consumer";
String OUTPUT = "requeue-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
修改controller
@Autowired
private RequeueTopic requeueProducer;
@PostMapping("requeue")
public void sendRequeueMessage(@RequestParam("body") String body){
requeueProducer.output().send(MessageBuilder.withPayload(body).build());
}
修改consumer
@EnableBinding(value = {
Sink.class,
MyTopic.class,
GroupTopic.class,
DelayedTopic.class,
ErrorTopic.class,
RequeueTopic.class
})
@StreamListener(RequeueTopic.INPUT)
public void consumerRequeueMessage(Object payload){
log.info("****** 进入入队异常处理 ******");
try{
Thread.sleep(3000);
}catch (Exception ex){
log.error("**** 延迟等待错误{} ****",ex);
}
//让这个消息一直抛错
throw new RuntimeException("****** 整个人都不行了 ******");
}
修改配置文件properties
# 联机Requeue错误重试消息配置
spring.cloud.stream.bindings.requeue-consumer.destination=requeue-exchange
spring.cloud.stream.bindings.requeue-producer.destination=requeue-exchange
# 仅对当前consumer开启重新入队
spring.cloud.stream.rabbit.bindings.requeue-consumer.consumer.requeue-rejected=true
# 还要将本地重试次数设置为1,让其不要本地重试
spring.cloud.stream.bindings.requeue-consumer.consumer.max-attempts=1
# 增加一个消费者组,让其在一个组内进行消费
spring.cloud.stream.bindings.requeue-consumer.group=Group-Requeue
测试启动两个服务外,看是否入队后能被其他消费者消费
顽固异常分为以下几类
对于架构师处理问题的态度和意识:一个都不能少
创建DlqTopic
package com.icodingeud.springcloud.topic;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface DlqTopic {
String INPUT = "dlq-consumer";
String OUTPUT = "dlq-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
controller中引入DlqTopic
@Autowired
private DlqTopic dlqProducer;
//死信队列
@PostMapping("dlq")
public void sendDlqMessage(@RequestParam("body") String body){
dlqProducer.output().send(MessageBuilder.withPayload(body).build());
}
修改consumer中的内容
@EnableBinding(value = {
Sink.class,
MyTopic.class,
GroupTopic.class,
DelayedTopic.class,
ErrorTopic.class,
RequeueTopic.class,
DlqTopic.class
})
//死信队列
@StreamListener(DlqTopic.INPUT)
public void consumerDlqMessage(Object payload){
log.info("****** DLK 进入异常处理 ******");
//计数器进来就自增1
if(count.incrementAndGet() % 3 == 0){
log.info("====== DLK 完全没有问题! ======");
}else{
log.info("----- DLK what's your problem? -----");
throw new RuntimeException("****** DLK 整个人都不行了 ******");
}
}
在配置文件中进行设置properties
# 死信队列配置
spring.cloud.stream.bindings.dlq-consumer.destination=dlq-exchange
spring.cloud.stream.bindings.dlq-producer.destination=dlq-exchange
spring.cloud.stream.bindings.dlq-consumer.consumer.max-attempts=2
spring.cloud.stream.bindings.dlq-consumer.group=Group-DLQ
# 默认创建一个exchange.dlq死信队列
spring.cloud.stream.rabbit.bindings.dlq-consumer.consumer.auto-bind-dlq=true
测试一下错误重试后的消息发送到死信队列中了,我们可以移动这个队列里的内容到另一个queue
但Move messages里提示需要安装两个插件
# rabbitmq_shovel
# rabbitmq_shovel_management
rabbitmq-plugins enable rabbitmq_shovel
rabbitmq-plugins enable rabbitmq_shovel_management
安装好后就是这样的内容显示了
可以移动到指定的queue中再次消费:dlq-exchange.Group-DLQ
创建一个FallbackTopic
package com.icodingeud.springcloud.topic;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.SubscribableChannel;
public interface FallbackTopic {
String INPUT = "fallback-consumer";
String OUTPUT = "fallback-producer";
@Input(INPUT)
SubscribableChannel input();
@Output(OUTPUT)
MessageChannel output();
}
在controller里进行设置
@Autowired
private FallbackTopic fallbackProducer;
//fallbackTopic
@PostMapping("fallback")
public void sendFallbackMessage(@RequestParam("body") String body,
@RequestParam(value = "version",defaultValue = "1.0") String version){
//假定我们调用接口的版本
//生成订单placeOrder,placeOrderV2,placeOrderV3
//可以在上游通过不同的queue来调用区分版本
//也可以不改动上游只需要在调用时加上verison
fallbackProducer.output().send(MessageBuilder
.withPayload(body)
.setHeader("version",version)
.build());
}
进入consumer进行设置
@EnableBinding(value = {
Sink.class,
MyTopic.class,
GroupTopic.class,
DelayedTopic.class,
ErrorTopic.class,
RequeueTopic.class,
DlqTopic.class,
FallbackTopic.class
})
//fallback + 升级版本
@StreamListener(FallbackTopic.INPUT)
public void consumerFallbackMessage(Object payload, @Header("version") String version){
log.info("****** Fallback Are you ok? ******");
//可以通过这样不同的版本走不同的业务逻辑
if("1.0".equalsIgnoreCase(version)){
log.info("====== Fallback 完全没有问题! ======");
}else if("2.0".equalsIgnoreCase(version)){
log.info("----- unsupported version -----");
throw new RuntimeException("****** fallback version ******");
}else{
log.info("---- Fallback version={} ----",version);
}
}
修改配置文件properties
# fallback队列配置
spring.cloud.stream.bindings.fallback-consumer.destination=fallback-exchange
spring.cloud.stream.bindings.fallback-producer.destination=fallback-exchange
spring.cloud.stream.bindings.fallback-consumer.consumer.max-attempts=2
spring.cloud.stream.bindings.fallback-consumer.group=Group-Fallback
回到consumer里增加fallback的逻辑
package com.icodingeud.springcloud.service;
import com.icodingeud.springcloud.entity.MessageBean;
import com.icodingeud.springcloud.topic.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Header;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@EnableBinding(value = {
Sink.class,
MyTopic.class,
GroupTopic.class,
DelayedTopic.class,
ErrorTopic.class,
RequeueTopic.class,
DlqTopic.class,
FallbackTopic.class
})
public class StreamConsumer {
//定一个一个线程安全的变量
private AtomicInteger count = new AtomicInteger(1);
//这里先使用stream给的默认topic
@StreamListener(Sink.INPUT)
public void consumer(Object payload){
log.info("message consumed successfully, payload={}",payload);
}
@StreamListener(MyTopic.INPUT)
public void consumerMyMessage(Object payload){
log.info("My Message consumed successfully, payload={}",payload);
}
@StreamListener(GroupTopic.INPUT)
public void consumerGroupMessage(Object payload){
log.info("Group Message consumed successfully, payload={}",payload);
}
@StreamListener(DelayedTopic.INPUT)
public void consumerDelayedMessage(MessageBean bean){
log.info("Delayed Message consumed successfully, payload={}",bean.getPayload());
}
//异常重试单机版
@StreamListener(ErrorTopic.INPUT)
public void consumerErrorMessage(Object payload){
log.info("****** 进入异常处理 ******");
//计数器进来就自增1
if(count.incrementAndGet() % 3 == 0){
log.info("====== 完全没有问题! ======");
count.set(0);
}else{
log.info("----- what's your problem? -----");
throw new RuntimeException("****** 整个人都不行了 ******");
}
}
@StreamListener(RequeueTopic.INPUT)
public void consumerRequeueMessage(Object payload){
log.info("****** 进入入队异常处理 ******");
try{
Thread.sleep(3000);
}catch (Exception ex){
log.error("**** 延迟等待错误{} ****",ex);
}
throw new RuntimeException("****** 整个人都不行了 ******");
}
//死信队列
@StreamListener(DlqTopic.INPUT)
public void consumerDlqMessage(Object payload){
log.info("****** DLK 进入异常处理 ******");
//计数器进来就自增1
if(count.incrementAndGet() % 3 == 0){
log.info("====== DLK 完全没有问题! ======");
}else{
log.info("----- DLK what's your problem? -----");
throw new RuntimeException("****** DLK 整个人都不行了 ******");
}
}
//fallback + 升级版本
@StreamListener(FallbackTopic.INPUT)
public void consumerFallbackMessage(Object payload, @Header("version") String version){
log.info("****** Fallback Are you ok? ******");
//可以通过这样不同的版本走不同的业务逻辑
if("1.0".equalsIgnoreCase(version)){
log.info("====== Fallback 完全没有问题! ======");
}else if("2.0".equalsIgnoreCase(version)){
log.info("----- unsupported version -----");
throw new RuntimeException("****** fallback version ******");
}else{
log.info("---- Fallback version={} ----",version);
}
}
//exchange.group.errors
//配置一定要设置组名否则就找不到queue了,不设置组名是随机生成queue后缀
@ServiceActivator(inputChannel = "fallback-exchange.Group-Fallback.errors")
public void fallback(Message<?> message){
log.info("**** Enter Fallback, Payload={}",message.getPayload());
}
}