【☁Spring Cloud】第一代Spring Cloud核心组件详解
服务注册中心组件 Eureka
Eureka遵守的是AP原则。
CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得。
Eureka 交互流程
Eureka 包含两个组件:Eureka Server 和 Eureka Client,Eureka Client是一个 Java客户端,用于简化与Eureka Server的交互;Eureka Server提供服务发现的能力,各个微服务启动时,会通过Eureka Client向Eureka Server 进行注册自己的信息(例如网络信息),Eureka Server会存储该服务的信息。
- 图中us-east-1c、us-east-1d,us-east-1e代表不同的区也就是不同的机房
- 图中每一个Eureka Server都是一个集群。
- 图中Application Service作为服务提供者向Eureka Server中注册服务, Eureka Server接收到注册事件会在集群和分区中进行数据同步,Application Client作为消费端(服务消费者)可以从Eureka Server中获取到服务注册信息,进行服务调用。
- 微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30秒) 以续约自己的信息
- Eureka Server在一定时间内没有接收到某个微服务节点的心跳,Eureka Server将会注销该微服务节点(默认90秒)
- 每个Eureka Server同时也是Eureka Client,多个Eureka Server之间通过复制的方式完成服务注册列表的同步
- Eureka Client会缓存Eureka Server中的信息,即使所有的Eureka Server节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者
Eureka通过心跳检测、健康检查和客户端缓存等机制,提高系统的灵活性、可伸缩性和可用性。
Eureka元数据
Eureka的 元数据 有两种:标准元数据和自定义元数据。
- 标准元数据:主机名、IP地址、端口号等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
- 自定义元数据:可以使用eureka.instance.metadata-map配置,符合KEY/VALUE的 存储格式。这 些元数据可以在远程客户端中访问。
Eureka代码演示
环境:① Spring Boot:2.3.3.RELEASE ② Spring Cloud: Hoxton.SR12
通用的父pom.xml
<!-- 因为是总项目 所以用dependencyManagement来管理 因为其他的子项目就不会来管理版本了了 可以直接引用 -->
<dependencyManagement>
<dependencies>
<!-- spring cloud的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR12</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring boot的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!--单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- lombok, idea需要安装插件Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
eureka server
依赖配置
<!--web 应用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
application.yml配置文件
server:
port: 8300
spring:
application:
name: springcloud-eureka-server
eureka:
server:
# 是否允许开启自我保护模式,默认是true,当Eureka服务器在短时间内丢失过多客户端时,自我保护模式可使服务端不再删除失去连接的客户端
enable-self-preservation: true
instance:
hostname: localhost
instance-id: ${spring.application.name}-${server.port} # 唯一
client:
#通过设置fetch-registry与register-with-eureka 表明自己是一个eureka服务
register-with-eureka: false
fetch-registry: false
# 指定注册中心的地址
service-url:
# defaultZone不能写成default-zone
# 这里特别注意:浏览器访问监控页面是http://${eureka.instance.hostname}:${server.port}
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
- fetch-registry:检索服务选项,当设置为True(默认值)时,会进行服务检索,注册中心不负责检索服务。
- register-with-eureka:服务注册中心也会将自己作为客户端来尝试注册自己,为true(默认)时自动生效
- eureka.client.serviceUrl.defaultZone是一个默认的注册中心地址。配置该选项后,可以在服务中心进行注册。
启动类
@EnableEurekaServer //标记为eureka服务端
@SpringBootApplication
public class EurekaServerApp {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApp.class);
}
}
eureka client
依赖配置
<!--web 应用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.yml配置文件
server:
port: 8400
spring:
application:
name: app-service
eureka:
instance:
hostname: localhost
instance-id: ${spring.application.name}-${server.port} # 唯一
client:
#通过设置fetch-registry与register-with-eureka 表明自己是一个eureka服务
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8300/eureka/
eureka client的fetch-registry和register-with-eureka必须设置为true,否则服务注册不上。
启动类
@EnableDiscoveryClient //客户端, 不推荐使用@EnableEurekaClient,因为注册中心不仅仅是eureka
@SpringBootApplication
public class AppService {
public static void main(String[] args) {
SpringApplication.run(AppService.class);
}
}
eureka server集群
创建两个eureka server 应用,可以参考单点配置,但是有以下几点需要注意:
- 需要保证 eureka.instance.hostname eureka服务的实例名称在集群中保证唯一。
- 需要保证 eureka.client.service-url.defaultZone 的主机名(或IP)要唯一。如果是 Eureka 集群在同一个主机上时(学习和开发时),通过配置 hosts ,保证主机名的唯一。
win10 环境修改,在 C:\Windows\System32\drivers\etc 的 hosts 文件修改:
127.0.0.1 hostServer01.com
127.0.0.1 hostServer02.com
第一个eureka server配置
server:
port: 8300
spring:
application:
name: springcloud-eureka-server
eureka:
server:
# 是否允许开启自我保护模式,默认是true,当Eureka服务器在短时间内丢失过多客户端时,自我保护模式可使服务端不再删除失去连接的客户端
enable-self-preservation: true
instance:
hostname: hostServer01.com
instance-id: ${spring.application.name}-${server.port} # 唯一
client:
#通过设置fetch-registry与register-with-eureka 表明自己是一个eureka服务
register-with-eureka: false
fetch-registry: false
# 指定注册中心的地址
service-url:
# defaultZone不能写成default-zone
# 这里特别注意:浏览器访问监控页面是http://${eureka.instance.hostname}:${server.port}
# 如果eureka是集群,则需要列举其他注册中心eureka server地址,用逗号分隔
defaultZone: http://hostServer02.com:8350/eureka/
第二个eureka server配置
server:
port: 8350
spring:
application:
name: springcloud-eureka-server8350
eureka:
server:
# 是否允许开启自我保护模式,默认是true,当Eureka服务器在短时间内丢失过多客户端时,自我保护模式可使服务端不再删除失去连接的客户端
enable-self-preservation: true
instance:
hostname: hostServer02.com
instance-id: ${spring.application.name}-${server.port} # 唯一
client:
#通过设置fetch-registry与register-with-eureka 表明自己是一个eureka服务
register-with-eureka: false
fetch-registry: false
# 指定注册中心的地址
service-url:
# defaultZone不能写成default-zone
# 这里特别注意:浏览器访问监控页面是http://${eureka.instance.hostname}:${server.port}
# 如果eureka是集群,则需要列举其他注册中心eureka server地址
defaultZone: http://hostServer01.com:8300/eureka/
注意:集群中,服务名spring.application.name要保持一致,eureka.instance.hostname和eureka.instance.instance-id要唯一
单机时:是本注册中心的地址,如http://localhost:8300/eureka/
集群时:是集群中其他注册中的地址集合,用逗号分开
eureka client的application.yml配置文件需要修改:
eureka:
client:
# eureka server是集群,这里将所有的地址都列出
defaultZone: http://hostServer01.com:8300/eureka/,http://hostServer02.com:8350/eureka/
分别启动两个eureka server,再启动eureka client。
访问hostServer01.com:8300,页面如下:
访问hostServer02.com:8350,页面如下:
Ribbon负载均衡
负载均衡一般分为服务器端负载均衡和客户端负载均衡。
所谓服务器端负载均衡,比如Nginx、F5这些,请求到达服务器之后由这些负载均衡器根据一定的算法将请求路由到目标服务器处理。
所谓客户端负载均衡,比如我们要说的Ribbon,服务消费者客户端会有一个服务器 地址列表,调用方在请求前通过一定的负载均衡算法选择一个服务器进行访问,负载均衡算法的执行是在请求客户端进行。
Ribbon是Netflix发布的负载均衡器。Eureka一般配合Ribbon进行使用,Ribbon利用从Eureka中读取到服务信息,在调用服务提供者提供的服务时,会根据一定的算法进行负载。
上图中service1调用service2的服务时,需要从service2的服务列表中选择出一个节点,拿到其对应的ip和端口,然后再发起对应的请求,这便是所谓的负载均衡算法,Ribbon便是一个帮我们实现了这个功能的组件。
负载均衡策略
Ribbon调用流程
Ribbon代码演示
创建两个service2提供服务
注意:两个服务提供者的服务名(spring.application.name)相同
第一个service2服务提供者:
application.yml配置:
server:
port: 8501
spring:
application:
name: app-service-consumer
eureka:
instance:
instance-id: ${spring.application.name}-${server.port} # 唯一
client:
#通过设置fetch-registry与register-with-eureka 表明自己是一个eureka服务
register-with-eureka: true
fetch-registry: true
registry-fetch-interval-seconds: 30000
service-url:
# eureka server是集群,这里将所有的地址都列出
defaultZone: http://hostServer01.com:8300/eureka,http://hostServer02.com:8350/eureka
提供的服务方法:
@RequestMapping("/app")
@RestController
public class App8501Controller {
@GetMapping("/getInfo")
public Map<String, Object> getInfo() {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("port", 8501);
return resultMap;
}
@PostMapping("/saveInfo")
public Map<String, Object> saveInfo(@RequestBody Map<String, Object> requestInfo) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("requestInfo", requestInfo);
resultMap.put("port", 8501);
return resultMap;
}
}
第二个service2服务提供者:
server:
port: 8502
spring:
application:
name: app-service-consumer
eureka:
instance:
instance-id: ${spring.application.name}-${server.port} # 唯一
client:
#通过设置fetch-registry与register-with-eureka 表明自己是一个eureka服务
register-with-eureka: true
fetch-registry: true
registry-fetch-interval-seconds: 30000
service-url:
# eureka server是集群,这里将所有的地址都列出
defaultZone: http://hostServer01.com:8300/eureka,http://hostServer02.com:8350/eureka
提供的服务方法:
@RequestMapping("/app")
@RestController
public class App8502Controller {
@GetMapping("/getInfo")
public Map<String, Object> getInfo() {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("port", 8502);
return resultMap;
}
@PostMapping("/saveInfo")
public Map<String, Object> saveInfo(@RequestBody Map<String, Object> requestInfo) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("requestInfo", requestInfo);
resultMap.put("port", 8502);
return resultMap;
}
}
创建 service1调用服务
依赖配置
<dependencies>
<!--web 应用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka-client, 默认集成了ribbon,所以不需要再引入ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
application.yml配置文件
server:
port: 8400
spring:
application:
name: app-service
eureka:
instance:
instance-id: ${spring.application.name}-${server.port} # 唯一
client:
#通过设置fetch-registry与register-with-eureka 表明自己是一个eureka服务
register-with-eureka: true
fetch-registry: true
registry-fetch-interval-seconds: 30000
service-url:
# eureka server是集群,这里将所有的地址都列出
defaultZone: http://hostServer01.com:8300/eureka,http://hostServer02.com:8350/eureka
服务调用
@EnableDiscoveryClient //客户端, 不推荐使用@EnableEurekaClient,因为注册中心不仅仅是eureka
@SpringBootApplication
public class AppService {
public static void main(String[] args) {
SpringApplication.run(AppService.class);
}
@LoadBalanced //添加注解,实现客户端负载均衡
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
}
以上必须添加@LoadBalanced注解才能实现客户端的负载均衡。
@RequestMapping("/demo")
@RestController
public class DemoController {
@Resource
private RestTemplate restTemplate;
@GetMapping("testGetRibbon")
public Object testGetRibbon() {
//app-service-consumer为服务名称
Map result = restTemplate.getForObject("http://app-service-consumer/app/getInfo", Map.class);
return result;
}
@PostMapping("testPostRibbon")
public Object testPostRibbon() {
Map<String, Object> requestInfo = new HashMap<>();
requestInfo.put("ribbon", "测试");
requestInfo.put("method", "POST");
Map result = restTemplate.postForObject("http://app-service-consumer/app/saveInfo", requestInfo, Map.class);
return result;
}
}
启动eureka集群,启动service1和service2, 打开eureka监控页面:
服务都注册成功,调用接口测试:
- GET http://localhost:8400/demo/testGetRibbon
- POST http://localhost:8400/demo/testPostRibbon
自定义负载均衡策略
1)自定义一个继承AbstractLoadBalancerRule的类,重写choose方法,实现自己的负载均衡算法
- AbstractLoadBalancerRule已经包含了getLoadBalancer()方法来获取负载均衡器,通过ILoadBalancer我们获取到所有的服务列表
2)建立自定义配置类(@Configuration注解),自定义配置类不能放在@ComponentScan当前包和和子包下(一般在启动类所在的包建立同级的包即可),否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了
3)在主启动类上添加@RibbonClient或@RibbonClients注解
//针对全局修改
@RibbonClients(defaultConfiguration = MyRuleConfig.class)
//针对某个服务修改
@RibbonClient(name = "cloud-payment-service",configuration = MyRuleConfig.class)
//针对多个服务定制化
@RibbonClients({
@RibbonClient(value = "cloud-payment-service", configuration = MyRuleConfig.class),
@RibbonClient(value = "cloud-order-service", configuration = MyRuleConfig2.class)
})
Hystrix熔断器
Hystrix(豪猪),宣言“defend your app”是由Netflix开源的一个 延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而 提升系统的可用性与容错性。Hystrix主要通过以下几点实现延迟和容错:
- 包裹请求:使用HystrixCommand包裹对依赖的调用逻辑。
- 跳闸机制:当某服务的错误率超过一定的阈值时,Hystrix可以跳闸,停止请求 该服务一段时间。
- 资源隔离:Hystrix为每个依赖都维护了一个小型的线程池(舱壁模式)(或者信号 量)。如果该线程池已满, 发往该依赖的请求就被立即拒绝,而不是排队等待,从而加速失败判定。
- 监控:Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、 超时、以及被拒绝 的请求等。
- 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑由开发人员自行提供,例如返回一个缺省值。
- 自我修复:断路器打开一段时间后,会自动进入“半开”状态。
扇入、扇出概念:
- 扇入:代表着该微服务被调用的次数,扇入大,说明该模块复用性好。
- 扇出:该微服务调用其他微服务的个数,扇出大,说明业务逻辑复杂。
扇入大是一个好事,扇出大不一定是好事。
微服务架构中,一个应用可能会有多个微服务组成,微服务之间的数据交互通过远程过程调用完成。这就带来一个问题,假设微服务A调用微服务B和微服务C,微 服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某 个微服务的调用响应时间过⻓或者不可用,对微服务A的调用就会占用越来越多的系 统资源,进而引起系统崩溃,所谓的“雪崩效应”。
Hystrix舱壁模式
Hystrix工作流程
Hystrix代码演示
依赖配置
一般是在服务消费者添加依赖
<!--hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
启动类上添加@EnableCircuitBreaker注解
对应的接口方法上添加@HystrixCommand注解
@HystrixCommand(fallbackMethod = "fallback") //fallback就是熔断的方法名
@GetMapping("testHystrix")
public Object testHystrix() {
//app-service-consumer为服务名称
Map result = restTemplate.getForObject("http://app-service-consumer/app/getInfo", Map.class);
return result;
}
public String fallback() {
return "触发降级";
}
更多的配置属性如下,我们也可以使用@DefaultProperties配置全局的熔断方法。
@HystrixCommand(commandProperties = {
// 配置请求隔离的方式,有 threadPool(线程池,默认)和 semaphore(信号量)两种,一般我们也都是默认即可
@HystrixProperty(name = "execution.isolation.strategy", value = "threadPool"),
// 是否给方法执行设置超时,默认为 true
@HystrixProperty(name = "execution.timeout.enabled", value = "true"),
// 方法执行超时时间,默认值是 1000,即 1秒,此值根据业务场景配置
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"),
//指定了窗口的大小,单位是 ms,默认值是 1000,即一个滑动窗口默认统计的是 1s 内的请求数据。
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000"),
//请求阈值
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
//指定了熔断器打开后经过多长时间允许一次请求尝试执行,默认值是 5000
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000"),
//窗口时间内超过 50% 的请求失败后会打开熔断器将后续请求快速失败。
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
}, threadPoolProperties = { //线程池配置
//核心线程数
@HystrixProperty(name = "coreSize", value = "4"),
//最大线程数
@HystrixProperty(name = "maximumSize", value = "10"),
//作业队列的最大值
@HystrixProperty(name = "maxQueueSize", value = "100"),
//指定了线程空闲时被回收前的存活时间, 默认是2分钟
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2")
}, fallbackMethod = "fallback")
@GetMapping("/{num}")
public Object testHystrix(@PathVariable("num") int num) {
if (num % 2 == 0) {
return "正常访问。";
}
//app-service-consumer为服务名称
Map result = restTemplate.getForObject("http://app-service-consumer/app/getInfo", Map.class);
return result;
}
public String fallback(int num) {
return "触发降级";
}
这里要注意:被熔断的方法有什么入参,熔断方法就要有什么入参,否则会报错。
Feign远程调用组件
Feign是Netflix开发的一个轻量级RESTful的HTTP服务客户端(用它来发起请求, 远程调用的),是以Java接口注解的方式调用Http请求,而不用像Java中通过封装 HTTP请求报文的方式直接调用,Feign被广泛应用在Spring Cloud 的解决方案中。
类似于Dubbo,服务消费者拿到服务提供者的接口,然后像调用本地接口方法一样 去调用,实际发出的是远程的请求。
- Feign可帮助我们更加便捷,优雅的调用HTTP API:不需要我们去拼接url调用restTemplate的api,在SpringCloud中,使用Feign非常简单,创建一个接口(在消费者服务调用方这一端),并在接口上添加一些注解,代码就完成了。
- SpringCloud对Feign进行了增强,使Feign支持了SpringMVC注解 (OpenFeign)
本质:封装了Http调用流程,更符合面向接口化的编程习惯,类似于Dubbo的服务调用。
Feign和OpenFeign的联系与区别
- 底层都是内置了Ribbon,去调用注册中心的服务。
- Feign是Netflix公司写的,是SpringCloud组件中的一个轻量级RESTful的HTTP服务客户端,是SpringCloud中的第一代负载均衡客户端。
- OpenFeign是SpringCloud自己研发的,在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。是SpringCloud中的第二代负载均衡客户端。
- Feign本身不支持Spring MVC的注解,使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务
- OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
Feign是在2019就已经不再更新了,通过maven网站就可以看出来,随之取代的是OpenFeign,从名字上就可以知道,它是Feign的升级版。
Feign的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
OpenFeign的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
OpenFeign代码演示
在实际开发中,Hystrix都是和OpenFeign组件一起结合使用的,OpenFeign组件中已经包含了Hystrix,但是默认情况下,OpenFeign是没有开启Hystrix的功能,我们需要在application.yml配置文件中手动的开启Hystrix的功能。
<!-- 引入 OpenFeign 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
需要注意的是,OpenFeign虽然集成了Hystrix,但是如果你要使用Hystrix中的 @HystrixCommand等注解,那还是需要引入下面的依赖:
hystrix的依赖根据需要自行引入,如果你项目中要使用hystrix的注解,那就引入这个依赖。
<!-- 引入 hystrix 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
openfeign启用hystrix
在application.yml配置文件中,添加如下配置:
# openfeign 开启 hystrix 的支持
feign:
hystrix:
enabled: true
编写FeignClient接口
在@FeignClient注解中,使用 fallback 属性指定服务降级之后的处理类,当然也可以不用启用。
普通的服务调用
@FeignClient(name = "app-service-consumer") //name是服务名
@Component
public interface HystrixFeignClient {
@GetMapping("/app/getInfo")
Map<String, Object> getInfo();
@PostMapping("/app/saveInfo")
Map<String, Object> saveInfo(@RequestBody Map<String, Object> requestInfo);
}
带熔断的服务调用
@FeignClient(name = "app-service-consumer", fallback = HystrixFallback.class) //name是服务名, fallback指定用于 hystrix 实现服务降级的类
public interface HystrixFeignClient {
@GetMapping("/app/getInfo")
Map<String, Object> getInfo();
@PostMapping("/app/saveInfo")
Map<String, Object> saveInfo(@RequestBody Map<String, Object> requestInfo);
}
FeignClient实现类:
//这个实现类必须注入到IOC容器里面。
@Component
public class HystrixFallback implements HystrixFeignClient {
@Override
public Map<String, Object> getInfo() {
Map<String, Object> map = new HashMap<>();
map.put("fallback", "getInfo");
map.put("pass", "000");
return map;
}
@Override
public Map<String, Object> saveInfo(Map<String, Object> requestInfo) {
Map<String, Object> map = new HashMap<>();
map.put("fallback", "saveInfo");
map.put("pass", "000");
return map;
}
}
编写Controller控制器
@Autowired
private HystrixFeignClient hystrixFeignClient;
@GetMapping("testOpenFeignGet")
public Object testOpenFeignGet() {
Map result = hystrixFeignClient.getInfo();
return result;
}
@PostMapping("testOpenFeignPost")
public Object testOpenFeignPost() {
Map<String, Object> requestInfo = new HashMap<>();
requestInfo.put("ribbon", "测试");
requestInfo.put("method", "POST");
Map result = hystrixFeignClient.saveInfo(requestInfo);
return result;
}
启动类启动OpenFeign
启动类上添加注解
// 启动类启动 OpenFeign 扫描
@EnableFeignClients
捕获异常信息
采用上面那种方式,当接口调用出现异常的时候,控制台是不会抛出异常的,所以为了能够获取到异常信息,OpenFeign中的@FeignClient注解提供了一个fallbackFactory属性。fallbackFactory属性需要指定一个服务降级工厂类,自定义的工厂类需要实现【FallbackFactory】接口,并且重写其中的【create()】方法,这个方法中会有一个Throwable类型的参数,这个参数就是运行过程中出现的一些异常信息。
自定义的服务降级工厂类
//注意:这里是org.springframework.cloud.openfeign.FallbackFactory,而不是feign.hystrix.FallbackFactory
@Component
public class HystrixFallbackFactory implements FallbackFactory<HystrixFeignClient> {
@Override
public HystrixFeignClient create(Throwable ex) {
return new HystrixFeignClient() {
@Override
public Map<String, Object> getInfo() {
System.out.println("[HystrixFallbackFactory]服务降级,请稍后重试!getInfo出现的异常是: " + ex.getMessage());
Map<String, Object> map = new HashMap<>();
map.put("fallbackFactory", "getInfo");
map.put("pass", "000");
return map;
}
@Override
public Map<String, Object> saveInfo(Map<String, Object> requestInfo) {
System.out.println("[HystrixFallbackFactory]服务降级,请稍后重试!saveInfo出现的异常是: " + ex.getMessage());
Map<String, Object> map = new HashMap<>();
map.put("fallbackFactory", "saveInfo");
map.put("pass", "000");
return map;
}
};
}
}
fallbackFactory指定服务降级工厂类
@FeignClient(name = "app-service-consumer", fallbackFactory = HystrixFallbackFactory.class) //name是服务名
public interface HystrixFeignClient {
@GetMapping("/app/getInfo")
Map<String, Object> getInfo();
@PostMapping("/app/saveInfo")
Map<String, Object> saveInfo(@RequestBody Map<String, Object> requestInfo);
}
Zuul网关组件
Zuul是Netflix开源的微服务网关,可以和Eureka、Ribbon、Hystrix等组件配合使用,Spring Cloud对Zuul进行了整合与增强,Zuul默认使用的HTTP客户端是Apache HTTPClient,也可以使用RestClient或okhttp3.OkHttpClient。 Zuul的主要功能是路由转发和过滤器。路由功能是微服务的一部分,比如/demo/test转发到到demo服务。zuul默认和Ribbon结合实现了负载均衡的功能。
zuul的核心是一系列的filters, 其作用类比Servlet框架的Filter,或者AOP。zuul把请求路由到用户处理逻辑的过程中,这些filter参与一些过滤处理。
Zuul使用一系列不同类型的过滤器,使我们能够快速灵活地将功能应用于我们的边缘服务。这些过滤器可帮助我们执行以下功能:
- 身份验证和安全性 - 确定每个资源的身份验证要求并拒绝不满足这些要求的请求
- 洞察和监控 - 在边缘跟踪有意义的数据和统计数据,以便为我们提供准确的生产视图
- 动态路由 - 根据需要动态地将请求路由到不同的后端群集
- 压力测试 - 逐渐增加群集的流量以衡量性能。
- Load Shedding - 为每种类型的请求分配容量并删除超过限制的请求
- 静态响应处理 - 直接在边缘构建一些响应,而不是将它们转发到内部集群
过滤器的生命周期
Zuul 默认定义了 4 种不同生命周期,分别为 pre(请求被路由前调用)、routing(在路由请求时被调用)、post(在 routing 和 error 过滤器之后被调用) 和 error(发生错误时被调用)。
Zuul过滤器
pre过滤器
ServletDetectionFilter
它的执行顺序为-3,是最先被执行的过滤器。该过滤器总是会被执行,主要用来检测当前请求是通过Spring的DispatcherServlet处理运行的,还是通过ZuulServlet来处理运行的。它的检测结果会以布尔类型保存在当前请求上下文的isDispatcherServletRequest参数中,这样后续的过滤器中,我们就可以通过RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest()方法来判断请求处理的源头,以实现后续不同的处理机制。
Servlet30WrapperFilter
它的执行顺序为-2,是第二个执行的过滤器,目前的实现会对所有请求生效,主要为了将原始的HttpServletRequest包装成Servlet30RequestWrapper对象。
FormBodyWrapperFilter
它的执行顺序为-1,是第三个执行的过滤器。该过滤器仅对两类请求生效,第一类是Context-Type为application/x-www-form-urlencoded的请求,第二类是Context-Type为multipart/form-data并且是由Spring的DispatcherServlet处理的请求(用到了ServletDetectionFilter的处理结果)。而该过滤器的主要目的是将符合要求的请求体包装成FormBodyRequestWrapper对象
DebugFilter
它的执行顺序为1,是第四个执行的过滤器,该过滤器会根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作。而它的具体操作内容是将当前请求上下文中的debugRouting和debugRequest参数设置为true。由于在同一个请求的不同生命周期都可以访问到这两个值,所以我们在后续的各个过滤器中可以利用这两个值来定义一些debug信息,这样当线上环境出现问题的时候,可以通过参数的方式来激活这些debug信息以帮助分析问题,另外,对于请求参数中的debug参数,我们可以通过zuul.debug.parameter来进行自定义
PreDecorationFilter
执行顺序是5,是pre阶段最后被执行的过滤器,该过滤器会判断当前请求上下文中是否存在forward.to和serviceId参数,如果都不存在,那么它就会执行具体过滤器的操作(如果有一个存在的话,说明当前请求已经被处理过了,因为这两个信息就是根据当前请求的路由信息加载进来的)。而它的具体操作内容就是为当前请求做一些预处理,比如说,进行路由规则的匹配,在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过RequestContext.getCurrentContext()来访问这些信息。另外,我们还可以在该实现中找到对HTTP头请求进行处理的逻辑,其中包含了一些耳熟能详的头域。另外,对于这些头域是通过zuul.addProxyHeaders参数进行控制的,而这个参数默认值是true,所以zuul在请求跳转时默认会为请求增加X-Forwarded-*头域,包括X-Forwarded-Host,X-Forwarded-Port,X-Forwarded-For,X-Forwarded-Prefix,X-Forwarded-Proto。也可以通过设置zuul.addProxyHeaders=false关闭对这些头域的添加动作。
route过滤器
RibbonRoutingFilter
它的执行顺序为10,是route阶段的第一个执行的过滤器。该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即只对通过serviceId配置路由规则的请求生效。而该过滤器的执行逻辑就是面向服务路由的核心,它通过使用ribbon和hystrix来向服务实例发起请求,并将服务实例的请求结果返回。
SimpleHostRoutingFilter
它的执行顺序为100,是route阶段的第二个执行的过滤器。该过滤器只对请求上下文存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效。而该过滤器的执行逻辑就是直接向routeHost参数的物理地址发起请求,从源码中我们可以知道该请求是直接通过httpclient包实现的,而没有使用Hystrix命令进行包装,所以这类请求并没有线程隔离和断路器的保护。
SendForwardFilter
它的执行顺序是500,是route阶段第三个执行的过滤器。该过滤器只对请求上下文中存在的forward.do参数进行处理请求,即用来处理路由规则中的forward本地跳转装配。
post过滤器
SendErrorFilter
SendResponseFilter
Zuul代码样式
依赖配置
<!--web 应用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka-client, 默认集成了ribbon,所以不需要再引入ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--zuul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
application.yml配置
server:
port: 8600
spring:
application:
name: zuul-server
eureka:
instance:
instance-id: ${spring.application.name}-${server.port} # 唯一
client:
#通过设置fetch-registry与register-with-eureka 表明自己是一个eureka服务
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://hostServer01.com:8300/eureka,http://hostServer02.com:8350/eureka
# 构建路由地址
zuul:
routes:
# 路由名,随意定义
demo2:
# 匹配的路由规则
path: /gateway/** # app-service有个服务/demo/testOpenFeignGet, 我们只需要调用/gateway/demo/testOpenFeignGet即可
# 路由的目标服务名
url: app-service
demo3:
path: /consumer/**
url: app-service-consumer
# 关闭使用eureka负载路由
#ribbon:
# eureka:
# enabled: false
# 如果不使用eureka的话,需要自己定义路由的那个服务的其他负载服务
#demo2:
# ribbon:
# 这里写你要路由的demo服务的所有负载服务请求地址,本项目只启动一个,因此只写一个
# listOfServers: http://localhost:8400/
#demo3:
# ribbon:
# listOfServers: http://localhost:8501/,http://localhost:8502/
启动类添加注解
@EnableZuulProxy //开启zuul
@EnableDiscoveryClient
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
GateWay网关组件
Spring Cloud GateWay是Spring Cloud的一个全新项目,目标是取代Netflix Zuul,它基于Spring5.0+SpringBoot2.0+WebFlux(基于高性能的Reactor模式响应式通信 框架Netty,异步非阻塞模型)等技术开发,性能高于Zuul,官方测试,GateWay是 Zuul的1.6倍,旨在为微服务架构提供一种简单有效的统一的API路由管理方式。
Spring Cloud GateWay不仅提供统一的路由方式(反向代理)并且基于Filter(定义过滤器对请求过滤,完成一些功能) 链的方式提供了网关基本的功能,例如:鉴权、 流量控制、熔断、路径重写、日志监控等。
Spring Cloud GateWay天生就是异步非阻塞的,基于Reactor模型。
一个请求到达网关根据一定的条件匹配,匹配成功之后可以将请求转发到指定的服务地址,而在这个过程中,我们可以进行一些比较具体的控制(限流、日志、黑白名单)
- 路由(route):网关最基础的部分,也是网关比较基础的工作单元。路由由一 个ID、一个目标URL(最终路由到的地址)、一系列的断言(匹配条件判断)和 Filter过滤器(精细化控制)组成。如果断言为true,则匹配该路由。
- 断言(predicates):参考了Java8中的断言java.util.function.Predicate,开发 人员可以匹配Http请求中的所有内容(包括请求头、请求参数等)(类似于 nginx中的location匹配一样),如果断言与请求相匹配则路由。
- 过滤器(filter):一个标准的Spring Web Filter。 Spring Cloud GateWay 中的Filter分为两种类型,分别是Gateway Filter和Global Filter。过滤器将会对请求和响应进行处理。
其中,Predicates断言就是我们的匹配条件,而Filter就可以理解为一个无所不能的拦截器,有了这两个元素,结合目标URL,就可以实现一个具体的路由转发。
GateWay工作过程
客户端向 Spring Cloud Gateway 发出请求。如果Gateway Handler Mapping确定请求与路由匹配,则将其发送到Gateway Web Handler 处理程序。此处理程序通过特定于请求的Fliter链运行请求。Fliter被虚线分隔的原因是Fliter可以在发送代理请求之前(pre)和之后(post)运行逻辑。执行所有pre过滤器逻辑。然后进行代理请求。发出代理请求后,将运行“post”过滤器逻辑。
过滤器作用:
- Filter在pre类型的过滤器可以做参数效验、权限效验、流量监控、日志输出、协议转换等。
- Filter在post类型的过滤器可以做响应内容、响应头的修改、日志输出、流量监控等
GateWay核心的流程就是:路由转发+执行过滤器链
GateWay代码演示
依赖配置
<!--eureka-client, 默认集成了ribbon,所以不需要再引入ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--gateway网关, 切记不可再引入spring-boot-starter-web, 已引入了spring-boot-starter-webflux-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
application.yml配置
server:
port: 8700
spring:
application:
name: gateway-server
cloud:
gateway:
locator:
enabled: true #开启注册中心路由功能
routes: # 路由
- id: app-service #路由ID,没有固定要求,但是要保证唯一,建议配合服务名
uri: lb://app-service # 匹配提供服务的路由地址
predicates: # 断言
- Path=/demo/** # 断言,路径相匹配进行路由
eureka:
instance:
instance-id: ${spring.application.name}-${server.port} # 唯一
client:
#通过设置fetch-registry与register-with-eureka 表明自己是一个eureka服务
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://hostServer01.com:8300/eureka,http://hostServer02.com:8350/eureka
启动类
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
启动报错:
***************************
APPLICATION FAILED TO START
***************************
Description:
An attempt was made to call a method that does not exist. The attempt was made from the following location:
org.springframework.cloud.gateway.config.GatewayAutoConfiguration$NettyConfiguration.buildConnectionProvider(GatewayAutoConfiguration.java:798)
The following method did not exist:
reactor.netty.resources.ConnectionProvider$Builder.evictInBackground(Ljava/time/Duration;)Lreactor/netty/resources/ConnectionProvider$ConnectionPoolSpec;
The method's class, reactor.netty.resources.ConnectionProvider$Builder, is available from the following locations:
jar:file:/E:/englishPath/respority/io/projectreactor/netty/reactor-netty/0.9.11.RELEASE/reactor-netty-0.9.11.RELEASE.jar!/reactor/netty/resources/ConnectionProvider$Builder.class
The class hierarchy was loaded from the following locations:
reactor.netty.resources.ConnectionProvider.Builder: file:/E:/englishPath/respority/io/projectreactor/netty/reactor-netty/0.9.11.RELEASE/reactor-netty-0.9.11.RELEASE.jar
reactor.netty.resources.ConnectionProvider.ConnectionPoolSpec: file:/E:/englishPath/respority/io/projectreactor/netty/reactor-netty/0.9.11.RELEASE/reactor-netty-0.9.11.RELEASE.jar
Action:
Correct the classpath of your application so that it contains a single, compatible version of reactor.netty.resources.ConnectionProvider$Builder
Disconnected from the target VM, address: '127.0.0.1:52402', transport: 'socket'
Process finished with exit code 1
这块主要是版本兼容的问题,本人修改reactor-netty的版本后解决问题:
<!-- reactor-netty -->
<dependency>
<groupId>io.projectreactor.netty</groupId>
<artifactId>reactor-netty</artifactId>
<version>0.9.14.RELEASE</version>
</dependency>
重新启动后,访问http://localhost:8700/demo/testOpenFeignGet 即可访问app-service下的/demo/testOpenFeignGet请求。
配置路由规则
path路由匹配规则
spring:
application:
name: gateway-server
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 路由ID,唯一,一般为各个服务名称
uri: http://localhost:7070/ #目标URI,路由到微服务的地址
predicates:
- Path=/product/** # 当请求的路径为product开头的时,转发到http://localhost:7070服务器上,即目标服务也要是以/product开头的
http://localhost:8700/product/getInfoById 会转发到 http://localhost:7070/product/getInfoById
query路由匹配规则
spring:
application:
name: gateway-server
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 路由ID,唯一,一般为各个服务名称
uri: http://localhost:7070/ #目标URI,路由到微服务的地址
predicates:
#- Query=token # 匹配请求参数中包含 token 的请求
- Query=token, abc. # 匹配请求参数中包含 token 并且参数值满足正则表达式 abc.(abc开头,后面匹配任意字符) 的请求
- http://localhost:8700/product/getInfoById?token=123 不能匹配到
- http://localhost:8700/product/getInfoById?token=abcd 可以匹配到
method路由匹配规则
spring:
application:
name: gateway-server
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 路由ID,唯一,一般为各个服务名称
uri: http://localhost:7070/ #目标URI,路由到微服务的地址
predicates:
- Method=GET # 匹配任意 GET 请求
Datetime路由匹配规则
spring:
application:
name: gateway-server
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 路由ID,唯一,一般为各个服务名称
uri: http://localhost:7070/ #目标URI,路由到微服务的地址
predicates:
# 匹配中国上海时间 2021-02-02 20:20:20 之后的请求
- After=2021-02-02T20:20:20.000+08:00[Asia/Shanghai]
#- Before 在某某时间之前的请求 -Between 介于某某时间之间
RemoteAddr路由匹配规则
spring:
application:
name: gateway-server
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 路由ID,唯一,一般为各个服务名称
uri: http://localhost:7070/ #目标URI,路由到微服务的地址
predicates:
# 匹配远程地址请求RemoteAddr的请求,0表示子网掩码
- RemoteAddr=192.168.10.1/0
Header路由匹配规则
spring:
application:
name: gateway-server
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 路由ID,唯一,一般为各个服务名称
uri: http://localhost:7070/ #目标URI,路由到微服务的地址
predicates:
# 匹配请求头包含X-Request-Id 并且其值匹配正则表达式 \d+(匹配任意数字) 的请求
- Header=X-Request-Id, \d+
uri配置的三种方式
- ws(websocket)方式 ----> uri: ws://localhost:9000
- http方式 ----> uri: http://localhost:8130/
- lb(注册中心中服务名字)方式 ----> uri: lb://brilliance-consumer
使用第三种方式时,要注意当服务名称有英文下划线("_")则不能被识别,具体的可以查看org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter下的规则,如下图:
过滤器规则
列举几个过滤器
PrefixPath
对所有的请求路径添加前缀。
spring:
application:
name: gateway-server
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 路由ID,唯一,一般为各个服务名称
uri: http://localhost:7070/ #目标URI,路由到微服务的地址
predicates:
- Path=/product/** # 当请求的路径为product开头的时,转发到http://localhost:7070服务器上,即目标服务也要是以/product开头的
filters:
- PrefixPath=/payment # 访问/product/123请求会被发送到http://127.0.0.1:7070/payment/product/123。
StripPrefix
跳过指定的路径。
spring:
application:
name: gateway-server
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 路由ID,唯一,一般为各个服务名称
uri: http://localhost:7070/ #目标URI,路由到微服务的地址
predicates:
- Path=/product/** # 当请求的路径为product开头的时,转发到http://localhost:7070服务器上,即目标服务也要是以/product开头的
filters:
- StripPrefix=1
- PrefixPath=/payment
此时访问http://localhost:8700/api/123,⾸先StripPrefix过滤器去掉⼀个/api,然后PrefixPath 过滤器加上⼀个/payment,能够正确访问到微服务。
RewritePath
spring:
application:
name: gateway-server
cloud:
gateway:
# 路由规则
routes:
- id: product-service # 路由ID,唯一,一般为各个服务名称
uri: http://localhost:7070/ #目标URI,路由到微服务的地址
predicates:
- Path=/product/**
filters:
- RewritePath=/product/test,/product/testGet
先判断请求是否服务predicates配置的规则,若符合规则,则将/product/test重写为/product/testGet。
Spring Cloud Config 分布式配置中心
Spring Cloud Config是一个分布式配置管理方案,包含了 Server端和 Client端两个部分。
- Server 端:提供配置文件的存储、以接口的形式将配置文件的内容提供出去, 通过使用@EnableConfigServer注解在 Spring boot 应用中非常简单的嵌入
- Client 端:通过接口获取配置数据并初始化自己的应用
Spring Cloud Bus(基于MQ的,支持RabbitMq/Kafka)是Spring Cloud中的消息总线方案,Spring Cloud Config + Spring Cloud Bus 结合可以实现配置信息的自动更新。
Spring Cloud Stream消息驱动组件
Spring Cloud Stream 消息驱动组件帮助我们更快速,更方便,更友好的去构建消息驱动微服务的。
本质:屏蔽掉了底层不同MQ消息中间件之间的差异,统一了MQ的编程模型,降低了学习、开发、维护MQ的成本。
Spring Cloud Stream 是一个构建消息驱动微服务的框架。应用程序通过inputs(相 当于消息消费者consumer)或者outputs(相当于消息生产者producer)来与 Spring Cloud Stream中的binder对象交互,而Binder对象是用来屏蔽底层MQ细节 的,它负责与具体的消息中间件交互。
Binder绑定器是Spring Cloud Stream 中非常核心的概念,就是通过它来屏蔽底层 不同MQ消息中间件的细节差异,当需要更换为其他消息中间件时,我们需要做的就 是更换对应的Binder绑定器而不需要修改任何应用逻辑(Binder绑定器的实现是框架内置的,Spring Cloud Stream目前支持Rabbit、Kafka两种消息队列)。
分布式链路追踪技术 Sleuth + Zipkin
分布式链路追踪技术核心思想
记录日志,作为一个完整的技术,分布式链路追踪也有自己的理论和概念微服务架构中,针对请求处理的调用链可以展现为一棵树,示意如下:
上图标识一个请求链路,一条链路通过TraceId唯一标识,SpanId标识发起的请求信息,各Span通过ParentId关联起来。
一个Trace由一个或者多个Span组成,每一个Span都有一个SpanId,Span中会记录TraceId,同时还有一个叫做ParentId,指向了另外一个Span的SpanId,表明父子关系,其实本质表达了依赖关系。
Span ID:为了统计各处理单元的时间延迟,当请求到达各个服务组件时,也是通过一个唯一标识Span ID来标记它的开始,具体过程以及结束。对每一个Span来说,它必须有开始和结束两个节点,通过记录开始Span和结束Span的时间戳,就能统计出该Span的时间延迟,除了时间戳记录之外,它还可以包含⼀些其他元数据,比如时间名称、请求信息等。
每一个Span都会有一个唯一跟踪标识 Span ID,若干个有序的 Span就组成了一个trace。
Span可以认为是一个日志数据结构,在一些特殊的时机点会记录了一些日志信息,比如有时间戳、SpanId、TraceId,ParentId等,Span中也抽象出了另外一个概念,叫做事件,核心事件如下:
- CS:client send/start 客户端/消费者发出一个请求,描述的是一个Span开始
- SR:server received/start 服务端/生产者接收请求 SR-CS属于请求发送的网络延迟
- SS:server send/finish 服务端/生产者发送应答 SS-SR属于服务端消耗时间
- CR:client received/finished 客户端/消费者接收应答 CR-SS表示回复需要的时间(响应的网络延迟)
Spring Cloud Sleuth (追踪服务框架)可以追踪服务之间的调用,Sleuth可以记录一个服务请求经过哪些服务、服务处理时长等,根据这些,我们能够理清各微服务间的调用关系及进行问题追踪分析。
注意:我们往往把Spring Cloud Sleuth 和 Zipkin 一起使用,把 Sleuth 的数据信息发送给Zipkin 进行聚合,利用 Zipkin 存储并展示数据。
Sleuth + Zipkin代码演示
搭建zipkin
下载:Central Repository: io/zipkin/zipkin-server
zipkin-server-2.24.0-exec.jar
运行:java -jar zipkin-server-2.24.0-exec.jar
打开控制台:http://localhost:9411/zipkin/
服务提供者
依赖配置
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
application.yml配置
spring:
application:
name: app-service
zipkin:
base-url: http://localhost:9411 # zipkin server地址
sleuth:
sampler:
probability: 1 #采样率值介于 0 到 1 之间,1 则表示全部采集
服务消费者
依赖配置
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
application.yml配置
spring:
application:
name: app-service-consumer
zipkin:
base-url: http://localhost:9411 # zipkin server地址
sleuth:
sampler:
probability: 1 #采样率值介于 0 到 1 之间,1 则表示全部采集
接口调用后,打开zipkin监控页面:
统一认证方案 Spring Cloud OAuth2 + JWT
微服务架构下统一认证思路
- 基于Session的认证方式:在分布式的环境下,基于session的认证会出现一个问题,每个应用服务都需要 在session中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将session信息带过去,否则会重新认证。我们可以使用Session共享、 Session黏贴等方案。Session方案也有缺点,比如基于cookie,移动端不能有效使用等
- 基于token的认证方式:服务端不用存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地方,并且可以实现web和app统一认证机制。其缺点也很明显,token由于自包含信息,因此一般数据量较大,而且每次请求 都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理 负担。
OAuth2开放授权协议/标准
OAuth(开放授权)是一个开放协议/标准,OAuth 2.0 的标准是 RFC 6749 文件。允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享 他们数据的所有内容。
- 资源所有者(Resource Owner):可以理解为用户自己
- 客户端(Client):我们想登陆的网站或应用
- 认证服务器(Authorization Server):可以理解为微信或者QQ
- 资源服务器(Resource Server):可以理解为微信或者QQ
Resource Owner在Authorization Server注册Client信息,通过Client去访问Authorization Server拿到token凭证,通过token凭证去Resource Server获取自己的资源。
OAuth2的颁发Token授权方式
- 授权码(authorization-code) :授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
- 密码式(password):如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
- 隐藏式(implicit):有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)。
- 客户端凭证(client credentials):最后一种方式是凭证式(client credentials),适用于没有前端的命令行应用,即在命令行下请求令牌。
JWT令牌
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名 可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改。
JWT令牌结构
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
Header:头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或 RSA),例如:{ "alg" : "HS256" , "typ" : "JWT" }
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
第二部分是负载(Payload),内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳),sub(面向的用户)等,也可自定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。 最后将第二部分负载使用Base64Url编码,得到一个字符串 就是JWT令牌的第二部分。 一个例子:{ "sub" : "1234567890" , "name" : "John Doe" , "iat" : 1516239022 }
第三部分是签名(Signature),此部分用于防止jwt内容被篡改。 这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明签名算法进行签名。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
- base64UrlEncode(header):jwt令牌的第一部分。
- base64UrlEncode(payload):jwt令牌的第二部分。
- secret:签名所使用的密钥。
Spring Cloud OAuth2+JWT 实现
Spring Cloud OAuth2是Spring Cloud体系对OAuth2协议的实现,可以用来做多个微服务的统一认证(验证身份合法性)授权(验证权限)。通过向OAuth2服务(统一认证授权服务)发送某个类型的grant_type进行集中认证和授权,从而获得access_token(访问令牌),而这个token是受其他微服务信任的。
注意:使用OAuth2解决问题的本质是,引入了一个认证授权层,认证授权层连接了资源的拥有者,在授权层里面,资源的拥有者可以给第三方应用授权去访问我们的某些受保护资源。
Spring Cloud OAuth2构建微服务统一认证服务思路:
在我们统一认证的场景中,Resoure Server其实就是我们的各种受保护的微服务,微服务中的各种API访问接口就是资源,发起http请求的浏览器就是Client客户端(对应为第三方应用)。
常见问题及解决方案
Eureka 服务发现慢的原因
Eureka 服务发现慢的原因主要有两个,一部分是因为服务缓存导致的,另一部分是因为客户端缓存导致的。
服务端缓存
- 服务注册到注册中心后,服务实例信息是存储在注册表中的,也就是内存中。但Eureka为了提高响应速度,在内部做了优化,加入了两层的缓存结构,将Client需要的实例信息,直接缓存起来,获取的时候直接从缓存中拿数据然后响应给 Client。 第一层缓存是readOnlyCacheMap,readOnlyCacheMap是采用ConcurrentHashMap来存储数据的,主要负责定时与readWriteCacheMap进行数据同步,默认同步时间为 30 秒一次。
- 第二层缓存是readWriteCacheMap,readWriteCacheMap采用Guava来实现缓存。缓存过期时间默认为180秒,当服务下线、过期、注册、状态变更等操作都会清除此缓存中的数据。
- Client获取服务实例数据时,会先从一级缓存中获取,如果一级缓存中不存在,再从二级缓存中获取,如果二级缓存也不存在,会触发缓存的加载,从存储层拉取数据到缓存中,然后再返回给 Client。
- Eureka 之所以设计二级缓存机制,也是为了提高 Eureka Server 的响应速度,缺点是缓存会导致 Client 获取不到最新的服务实例信息,然后导致无法快速发现新的服务和已下线的服务。
- 了解了服务端的实现后,想要解决这个问题就变得很简单了,我们可以缩短只读缓存的更新时间(eureka.server.response-cache-update-interval-ms)让服务发现变得更加及时,或者直接将只读缓存关闭(eureka.server.use-read-only- response-cache=false),多级缓存也导致C层面(数据一致性)很薄弱。Eureka Server 中会有定时任务去检测失效的服务,将服务实例信息从注册表中移除,也可以将这个失效检测的时间缩短,这样服务下线后就能够及时从注册表中清除。
客户端缓存
客户端缓存主要分为两块内容,一块是 Eureka Client 缓存,一块是 Ribbon 缓存。
Eureka Client 缓存
EurekaClient负责跟EurekaServer进行交互,在EurekaClient中的 com.netflix.discovery.DiscoveryClient.initScheduledTasks() 方法中,初始化了一 个 CacheRefreshThread 定时任务专⻔用来拉取 Eureka Server 的实例信息到本地。
所以我们需要缩短这个定时拉取服务信息的时间间隔(eureka.client.registryFetchIntervalSeconds)来快速发现新的服务。
Ribbon 缓存
Ribbon会从EurekaClient中获取服务信息,ServerListUpdater是Ribbon中负责服务实例更新的组件,默认的实现是PollingServerListUpdater,通过线程定时去更新实例信息。定时刷新的时间间隔默认是30秒,当服务停止或者上线后,这边最快也需要30秒才能将实例信息更新成最新的。我们可以将这个时间调短一点,比如3秒。
刷新间隔的参数是通过 getRefreshIntervalMs 方法来获取的,方法中的逻辑也是从 Ribbon 的配置中进行取值的。
将这些服务端缓存和客户端缓存的时间全部缩短后,跟默认的配置时间相比,快了很多。我们通过调整参数的方式来尽量加快服务发现的速度,但是还是不能完全解决报错的问题,间隔时间设置为3秒,也还是会有间隔。所以我们一般都会开启重试功能,当路由的服务出现问题时,可以重试到另一个服务来保证这次请求的成功。
Spring Cloud 各组件超时
Ribbon
如果采用的是服务发现方式,就可以通过服务名去进行转发,需要配置 Ribbon的超时。Rbbon的超时可以配置全局的ribbon.ReadTimeout和 ribbon.ConnectTimeout。也可以在前面指定服务名,为每个服务单独配置,比如 user-service.ribbon.ReadTimeout。其次是Hystrix的超时配置,Hystrix的超时时间要大于Ribbon的超时时间,因为 Hystrix将请求包装了起来,特别需要注意的是,如果Ribbon开启了重试机制,比如重试3 次,Ribbon 的超时为 1 秒,那么 Hystrix 的超时时间应该大于 3 秒,否则就会出现 Ribbon 还在重试中,而 Hystrix 已经超时的现象。
Hystrix
Hystrix全局超时配置就可以用default来代替具体的command名称。
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000 如果想对具体的 command 进行配置,那么就需要知道 command 名称的生成规则,才能准确的配置。
如果我们使用 @HystrixCommand 的话,可以自定义 commandKey。如果使用 FeignClient的话,可以为FeignClient来指定超时时间:hystrix.command.UserRemoteClient.execution.isolation.thread.timeoutInMilliseconds = 3000
如果想对FeignClient中的某个接口设置单独的超时,可以在FeignClient名称后加上 具体的方法: hystrix.command.UserRemoteClient#getUser(Long).execution.isolation.thread. timeoutInMilliseconds = 3000
Feign
Feign本身也有超时时间的设置,如果此时设置了Ribbon的时间就以Ribbon的时间为准,如果没设置Ribbon的时间但配置了Feign的时间,就以Feign的时间为准。Feign的时间同样也配置了连接超时时间(feign.client.config.服务名称.connectTimeout)和读取超时时间(feign.client.config.服务名称.readTimeout)。
建议,我们配置Ribbon超时时间和Hystrix超时时间即可。