微服务架构 基础(三)
持续更新…
服务降级、服务熔断和服务限流
基本概念理解
服务雪崩
多个服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的"扇出"。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,也就是所谓的"雪崩效应"。
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源在几秒内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加、备份队列、线程和其它系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统运行。
因此,通常发现一个模块下的运行某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样会发生级联故障,或者叫雪崩。
- 服务降级
当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理(比如向调用方返回一个符合预期的、可处理的备选响应… ),从而释放服务器资源以保证核心交易正常运作或高效运作。
使用场景:
当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运行,我们可以将一些不重要或不紧急的服务或任务进行服务的延迟使用或暂停使用。
- 服务熔断
熔断这一概念来源于电子工程中的断路器。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。但发生服务熔断后,一般调用服务降级的方法来处理,最后慢慢恢复调用链路。
- 服务限流
限流即限制并发量,限制某一段时间只有指定数量的请求进入后台服务器,遇到流量高峰期或者流量突增时,把流量速率限制在系统所能接受的合理范围之内,不至于让系统被高流量击垮。
什么是Hystrix?
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
所谓的"断路器"本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应,而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要的占用,从而避免了故障在分布式系统的蔓延,乃至雪崩。
Hystrix能干什么?
- 服务降级
- 服务熔断
- 接近实时的监控
- 限流、隔离
- …
服务降级
为了方便学习测试,所以需要构建一个基本的测试子模块模块来作为基本框架
子模块(micro-service-8003)
Hystrix依赖引入:
<!-- Hystrix依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
application.yml:
server:
port: 8003
spring:
application:
name: micro-service-8003
eureka:
client:
register-with-eureka: true # 注册
fetch-registry: true # 获取Eureka服务端的注册信息
service-url:
# 集群版本的服务端口地址,直接填写所有的服务端地址即可
defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka
主启动类:
package cn.wu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
public class Application8003 {
public static void main(String[] args) {
SpringApplication.run(Application8003.class,args);
}
}
业务层:
package cn.wu.service;
import org.springframework.stereotype.Service;
@Service("testServiceBean")
public class TestService {
public String testNormal(){
return "当前的线程名称为:"+Thread.currentThread().getName()
+",模拟处理正常业务过程… ";
}
public String testTimeOut(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "当前的线程名称:"+Thread.currentThread().getName()
+",模拟处理复杂业务过程… ";
}
}
控制层:
package cn.wu.cotroller;
import cn.wu.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
private TestService testService;
@Autowired
@Qualifier("testServiceBean")
public void setTestService(TestService testService) {
this.testService = testService;
}
// 模拟正常业务过程
@GetMapping("/test/normal")
public String testNormal(){
return testService.testNormal();
}
// 模拟复杂业务过程
@GetMapping("/test/timeout")
public String testTimeOut(){
return testService.testTimeOut();
}
}
最后测试一遍,不再做过多赘述…
同时为了测试高并发,使用JMeter工具来做简单测试,如果读者有疑问,可以查看该篇文章
模拟400000级流量压力(读者慎重配置,有可能电脑会死机… )
此时可以看到浏览器已经出现加载图标了…
我们进一步扩展,再建立服务消费者子模块(main-service-8082)
主要依赖配置文件pom.xml:
<!-- 服务注册依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- openfeign依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Hystrix依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
application.yml(注意此时未设置feign的最大等待时间… ):
server:
port: 8082
eureka:
client:
register-with-eureka: true # 将自己注册
fetch-registry: true
service-url:
defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka
spring:
application:
name: main-service-8082
主启动类:
package cn.wu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端
@EnableFeignClients // 开启Feign
public class MainApplication8082 {
public static void main(String[] args) {
SpringApplication.run(MainApplication8082.class,args);
}
}
业务层接口:
package cn.wu.service.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(value = "micro-service-8003")
public interface TestService {
@GetMapping("/test/normal")
String testNormal();
@GetMapping("/test/timeout")
String testTimeOut();
}
控制层:
package cn.wu.controller;
import cn.wu.service.feign.TestService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class TestController {
@Resource
private TestService testService;
@GetMapping("/test/normal")
public String testNormal(){
return testService.testNormal();
}
@GetMapping("/test/timeout")
public String testTimeOut(){
return testService.testTimeOut();
}
}
测试相应URL,结果为:
以上结果在我们的预期当中,因为我们未配置相应的Feign的等待参数,默认就会超时…
此时,继续使用JMeter来进行压力测试,记得URL中的端口修改一下(测试服务消费者的端口… ):
并发量还是400000级流量:
最好不要尝试这种并发量,这里笔者的内存是16GB的实测数据,瞬间拉满,感觉可能随时会宕机…
可以从后台控制台发现,有一些访问的线程已经出现了超时情况,也就是说这些用户访问失败了:
面对如此高的并发量,上面出现了两个严重问题:
- 超时导致服务器变慢
- 宕机或者程序出错
解决方案
从两个角色来分析问题
- 服务提供者超时,服务消费者不能一直等待,此时必须服务降级
- 服务提供者已经宕机,服务消费者不能一直等待,此时必须服务降级
- 服务消费者本身出现故障,但是服务提供者无故障(如服务消费者的等待时间小于服务提供者处理业务时间),此时服务消费者自我服务降级
首先,先解决服务提供者的问题(micro-service-8003)
修改业务层:
package cn.wu.service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service("testServiceBean")
public class TestService {
public String testNormal(){
log.info("正常业务… ");
return "当前的线程名称为:"+Thread.currentThread().getName()
+",模拟处理正常业务过程… " ;
}
// 设置自身处理时间峰值为3秒,峰值内可以正常运行,以及服务降级方法
@HystrixCommand(fallbackMethod = "testTimeOutHandler",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String testTimeOut(){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "当前的线程名称:"+Thread.currentThread().getName()
+",模拟处理复杂业务过程… ";
}
public String testTimeOutHandler(){
return "我是服务降级的复杂业务备选响应… ";
}
}
修改主启动类,添加注解:
package cn.wu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class Application8003 {
public static void main(String[] args) {
SpringApplication.run(Application8003.class,args);
}
}
重启8003端口服务,测试:
简单分析:上述业务处理时间为5秒,但是由于设置了处理时间峰值3秒,所以会服务降级…
同时,也可以用于对一些其它异常做服务降级:
// 设置自身处理时间峰值为3秒,峰值内可以正常运行,以及服务降级方法
@HystrixCommand(fallbackMethod = "testHandler",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String testTimeOut(){
int temp = 11/0; // 异常出现
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "当前的线程名称:"+Thread.currentThread().getName()
+",模拟处理复杂业务过程… ";
}
public String testHandler(){
return Thread.currentThread().getName()+": 我是服务降级的复杂业务备选响应… ";
}
重启,测试结果为:
然后,再解决服务消费者侧(main-service-8082)的问题
application.yml:
server:
port: 8082
eureka:
client:
register-with-eureka: true # 将自己注册
fetch-registry: true
service-url:
defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka
spring:
application:
name: main-service-8082
feign:
hystrix:
enabled: true # 开启Feign对Hystrix的支持
主启动类:
package cn.wu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableHystrix // 开启Hystrix
public class MainApplication8082 {
public static void main(String[] args) {
SpringApplication.run(MainApplication8082.class,args);
}
}
修改控制层:
package cn.wu.controller;
import cn.wu.service.feign.TestService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class TestController {
@Resource
private TestService testService;
@GetMapping("/test/normal")
public String testNormal(){
return testService.testNormal();
}
@HystrixCommand(fallbackMethod = "testHandler" , commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
@GetMapping("/test/timeout")
public String testTimeOut(){
return testService.testTimeOut();
}
public String testHandler(){
return "请求繁忙,请稍后再试… ";
}
}
结果:
以上出现了几个问题
- 业务代码和降级方法写在同一个类中,代码会显得冗杂
- 对于每一个控制层方法,都需要一个降级方法与之对应,如果有时多个控制层方法对应同一个降级方法,代码就会冗余
修改服务消费者控制层:
@RestController
@DefaultProperties(defaultFallback = "testHandler")
public class TestController {
@Resource
private TestService testService;
@GetMapping("/test/normal")
public String testNormal(){
return testService.testNormal();
}
// @HystrixCommand(fallbackMethod = "testHandler" , commandProperties = {
// @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "10000")
// })
@HystrixCommand
@GetMapping("/test/timeout")
public String testTimeOut(){
return testService.testTimeOut();
}
public String testHandler(){
return "请求繁忙,请稍后再试… ";
}
}
以上就是默认采用全局配置中的降级方法,对于特殊的服务降级,就只需要使用以上已经注释掉的代码
进一步优化…
实现@ClientFeign注解所在接口:
package cn.wu.service.feign;
import org.springframework.stereotype.Component;
@Component
public class TestServiceFallback implements TestService {
@Override
public String testNormal() {
return "服务繁忙,请稍后再试!";
}
@Override
public String testTimeOut() {
return "服务繁忙,请稍后再试!";
}
}
修改接口:
package cn.wu.service.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
@Component
@FeignClient(value = "micro-service-8003",fallback = TestServiceFallback.class)
public interface TestService {
@GetMapping("/test/normal")
String testNormal();
@GetMapping("/test/timeout")
String testTimeOut();
}
以上就是为每个接口实现一个服务降级方法
验证服务提供者宕机结果:
服务熔断
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当检测到该节点微服务调用响应正常时,恢复调用链路。
Hystrix可以实现熔断机制,Hystrix可以监控微服务之间的调用状况,当失败的调用到达一定的阈值时,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand
- 熔断打开: 请求不在进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态
- 熔断关闭: 熔断关闭不会进行熔断
- 熔断半开: 部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
修改服务提供者(micro-service-8003)业务层:
// 服务熔断测试学习
@HystrixCommand(fallbackMethod = "circuitBreakerFallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"), // 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"), // 一个时间窗口内的最大请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期(ms为单位,统计数据范围为10秒)
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "20"), // 一个时间窗口内的失败率达到多少触发熔断机制
})
public String testCircuitBreaker(@PathVariable("id") String id){
if(Integer.parseInt(id) < 0){
throw new RuntimeException("ID 不能为负数!");
}else{
return "Id为: "+id+",服务正常… ";
}
}
public String circuitBreakerFallback(@PathVariable("id") String id){
return "ID为: "+id+",数据异常,请重新尝试!";
}
修改服务提供者(micro-service-8003)的控制层:
// 服务熔断测试学习
@GetMapping("/test/circuitBreaker/id/{id}")
public String testCircuitBreaker(@PathVariable("id") String id){
return testService.testCircuitBreaker(id);
}
测试结果:
但是,当我们快速连续重复发送触发熔断机制的URL请求时,然后当我们提交正确的URL请求时,此时会因为超过服务的失败率或者超过最大请求上限而一样的会触发熔断机制,但是,这不是主要的,核心是处于半开状态,可以逐渐恢复服务
:
略过几秒,再次尝试正确的URL请求,此时,恢复正常的访问:
除了隔离依赖服务的调用之外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。
全程搭建简单,首先新建子模块(hystrix-server-7003):
主要依赖配置:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<!-- 服务监控依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
application.yml:
server:
port: 7003
主启动类:
package cn.wu;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
@SpringBootApplication
@EnableHystrixDashboard
public class MainHystrixApplication7003 {
public static void main(String[] args) {
SpringApplication.run(MainHystrixApplication7003.class,args);
}
}
测试:
为了能够监控服务提供者(micro-service-8003),首先需要修改该服务提供者配置信息:
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class Application8003 {
public static void main(String[] args) {
SpringApplication.run(Application8003.class,args);
}
@Bean
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean<>(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
最后添加子模块(hystrix-server-7003)的application.yml:
hystrix:
dashboard:
proxy-stream-allow-list: "localhost"
结果: