分布式系统面临的问题-----服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。因此需要有一种熔断机制来保护微服务的链路。
熔断机制概述
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解@HystrixCommand.
涉及到断路器的三个重要参数:快照时间窗、请求总数阀值、错误百分比阀值。
快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。
什么是Hystrix
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
Hystrix的设计目的如下:
通过第三方客户端库访问依赖项(通常通过网络),从而保护和控制延迟和故障。
停止复杂分布式系统中的级联故障。
快速故障并快速恢复。
如果可能,后退并优雅地降级。
实现近实时监控、警报和操作控制。
复杂分布式体系结构中的应用程序有几十个依赖项,每一个依赖项都不可避免地会在某一点发生故障。如果主机应用程序没有与这些外部故障隔离开来,那么它就有可能被这些外部故障带走。
例如,对于一个依赖于30个服务的应用程序,其中每个服务都有99.99%的正常运行时间,下面是我们可以期望的:
99.9930 = 99.7% uptime
0.3% of 1 billion requests = 3,000,000 failures
即使所有依赖项都有很好的正常运行时间,每月也会有2小时以上的停机时间。而实际情况通常可能会更糟。
即使所有依赖项都表现良好,如果我们不设计整个系统的恢复能力,数十项服务中每项服务的总停机率甚至为0.01%,也可能导致每月数小时的停机。
当一切正常时,请求流可以如下所示:
当许多后端系统中的一个变得潜在时,它会阻止整个用户请求:
在高流量的情况下,单个后端依赖性变得潜在可能会导致所有服务器上的所有资源都处于饱和状态。
应用程序中通过网络或进入客户端库的每一点都可能导致网络请求,这是潜在故障的根源。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加
备份队列、线程和其他系统资源,从而在整个系统中造成更多级联故障。
当通过第三方客户端执行网络访问时,这些问题会更加严重——每个服务都像一个“黑匣子”,实现细节是隐藏的,可以随时更改,每个客户端库的网络或资源配置都不同,通常很难监控和更改。
更糟糕的是传递依赖关系,它们在应用程序未明确调用的情况下执行可能昂贵或容易出错的网络调用。
网络连接失败或降级。服务和服务器出现故障或速度变慢。新库或服务部署会改变行为或性能特征。客户端库存在错误。
所有这些都代表了需要隔离和管理的故障和延迟,以使单个故障依赖项不会影响整个应用程序或系统。
Hystrix的设计原则是什么?
Hystrix通过以下方式工作:
防止任何单个依赖项用完所有容器(如Tomcat)用户线程。
减少负载并快速失败,而不是排队。
在可行的情况下提供回退,以保护用户不发生故障。
使用隔离技术(如隔板、泳道和断路器模式)来限制任何单一依赖的影响。
通过近实时指标、监控和警报优化发现时间
通过配置更改的低延迟传播和Hystrix大多数方面的动态属性更改支持,优化恢复时间,这允许我们使用低延迟反馈环路进行实时操作修改。
保护整个依赖关系客户端执行过程中的故障,而不仅仅是网络流量中的故障。
Hystrix如何实现目标?
Hystrix通过以下方式做到这一点:
将对外部系统(或“依赖项”)的所有调用包装在HystrixCommand或HystrixWatableCommand对象中,这些对象通常在单独的线程中执行(这是命令模式的一个示例)。
超时调用所需时间超过我们定义的阈值。有一个默认值,但对于大多数依赖项,我们可以通过“属性”自定义设置超时,使其略高于每个依赖项的99.5%性能。
为每个依赖项维护一个小的线程池(或信号量),如果它变满,则发往该依赖项的请求将立即被拒绝,而不是排队。
衡量成功、失败(客户端抛出的异常)、超时和线程拒绝。
如果服务的错误百分比超过阈值,则手动或自动跳闸断路器以在一段时间内停止对特定服务的所有请求。
当请求失败、被拒绝、超时或短路时,执行回退逻辑。
近实时监控指标和配置更改。
当我们在使用Hystix包装每个底层依赖项时,上图中所示的架构会发生变化,以类似下图。每个依赖项彼此隔离,在延迟发生时会饱和的资源受到限制,并包含在fallack逻辑中,该逻辑决定在依赖项中发生任何类型的故障时做出什么响应:
下面通过具体测试对Hytrix的降级熔断实现进行一个简单的模拟。
本次实践模拟因为采用的服务注册中心为Eureka,并通过OpenFeign实现客户端对服务端的调用,以及引用Hytrix来实现服务的降级熔断效果,因此引入的项目依赖主要如下,服务提供端的依赖不用引入OpenFeign:
SpringCloudAlibaba
com.yy
1.0.1
4.0.0
hystrix学习测试消费端-80端口
cloud-hystrix-consumer-80
1.8
1.8
org.springframework.boot
spring-boot-starter
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
2.2.10.RELEASE
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
com.yy
cloud-common
${myproject.version}
org.springframework.cloud
spring-cloud-starter-openfeign
然后配置基本的yml文件信息,指明eureka服务地址以及服务名称,因此在服务提供端8005上的配置信息为:
server:
port: 8005
spring:
application:
name: cloud-hystrix-provider
eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:7001/eureka
这里的eureka服务端为7001,不做详细构建展示。客户端作为调用服务的一端(80端口)与上面配置相似。然后在服务提供端编写量两个业务类一个正常获取数据,一个延时获取。
@Service
public class TestService {
public String ok(Integer id) {
return "时间为:" + DateUtil.format(LocalDateTime.now(), "yyyy年MM月dd日 HH:mm:ss") + "时,线程池:" + Thread.currentThread().getName() + "访问的id为:" + id;
}
public String wait(Integer id) {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "时间为:" + DateUtil.format(LocalDateTime.now(), "yyyy年MM月dd日 HH:mm:ss") + "时,等待后的线程池:" + Thread.currentThread().getName() + "访问的id为:" + id + "顺利执行";
}
}
后编写对应的controller api业务实现接口,并且在80的客户端上利用openfeign进行业务调用。
这样当我们正常调用服务时,数据获取正常。
但是当微服务在遇到高并发时,就会出现服务延迟响应的时候,或者服务异常出错,这时就像我们模拟的等待业务,当这个业务被调用时就会出现超时异常,直接在页面中抛出错误页面,用户体验极差。
这时候我们就需要使用服务降级处理,让异常信息能够被后台优雅的处理。这里利用Hytrix可以实现两种方法的处理:
服务提供方处理
消费端处理
下面接着看~
首先需要在启动类上添加@EnableHystrix注解来开启Hyrtix,使其能在项目中生效。
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableHystrix
public class HystrixProviderApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixProviderApplication.class, args);
}
然后在需要设置服务降级的业务方法上添加降级处理方法,一方面处理可能出现的延时异常,另一方面即使出现业务异常也能返回我们自定义的方法去处理。
/**
* 出现系统超时或异常过后做服务降级,单侧服务端(提供方)降级
*
* @param id
* @return
*/
@HystrixCommand(
fallbackMethod = "fallBackHandle",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
value = "5000")}) //设置超时处理,使该服务在5s内均可成功
public String wait(Integer id) {
//自定义错误,模拟业务异常
int a =10/0;
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "时间为:" + DateUtil.format(LocalDateTime.now(), "yyyy年MM月dd日 HH:mm:ss") + "时,等待后的线程池:" + Thread.currentThread().getName() + "访问的id为:" + id + "顺利执行";
}
//指定的兜底方法,方法名必须与fallbackMethod指定的一致
public String fallBackHandle(Integer id) {
return "接口异常后,走现在的回调方法" + Thread.currentThread().getName() + "系统繁忙";
}
这样我们再次调用服务后,如果出现的延时情况小于设定的超时范围,也不会直接抛出超时异常,而是正常等待后处理业务。
但是也会存在一个问题,如果仅在服务端处理延时问题的话,客户调用端不同时处理的话仍然会出现延时异常,服务端正常。
如果业务调用中确实出现异常无法处理了比如:int a =10/0,也会有我们自定的兜底方法来处理临时的问题。而不会直接出现错误页面error page。
在客户端的降级处理与服务端相似,不同点在与服务端在业务实现类上处理,客户端在controller层处理。
而且需要在yml文件中添加开启hytrix服务降级配置。
feign:
#开启hystrix服务降级
circuitbreaker:
enabled: true
因为使用openfeign实现服务调用,因此并不存在其他业务类来处理服务降级了。
/**
* @author young
* @date 2022/12/24 20:55
* @description: 单侧服务降级接口
*/
@Slf4j
@RequestMapping("consumer")
@RestController
public class ConsumerHystrixController {
@Resource
private ProviderHystrixService providerHystrixService;
@GetMapping("/test/ok/{id}")
public Result providerOk(@PathVariable("id") Integer id){
return providerHystrixService.selectOne(id);
}
//配置服务降级,单侧客户端降级
@HystrixCommand(fallbackMethod = "down",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
value = "1500")
})
@GetMapping("/test/wait/{id}")
public Result providerWait(@PathVariable("id") Integer id){
return providerHystrixService.selectOneWait(id);
}
/**
* 执行降级后的方法,返回对象Result要一致,否则报错
* Command type literal pos: unknown; Fallback type literal pos: unknown
*/
public Result down(@PathVariable("id") Integer id){
String s ="我消费者端扛不住了,要开始降级处理了,处理id为"+id;
return Result.ok(s);
}
}
同样需要在住启动类上添加Hytrix使用注解,然后编写自定义的兜底方法来避免业务直接出现error page。
需要注意的地方在于,如果兜底方法的返回值一定是与处理的业务接口api/方法一致的,如果我们在客户端有统一返回结果类处理,那么回调兜底的方法也须一致处理。否则会出现异常,这样虽然我们服务端方法执行延迟3秒,并且客户端的服务请求处理的延时范围<=1.5秒,因此按道理客户端访问也会出现延时异常,但是也会被降级处理,返回自定义的消息。
虽然问题处理完成了,但是时也会出现一个很头疼的问题:随着业务降级处理增多,每一个业务实现类,或者客户端的调用api接口都需要一个兜底的类去处理,这样就造成了大量的代码和业务逻辑耦合高,不符合代码设计原则。因此Hytrix还有另外的两种降级处理可供使用:
全局服务降级
通配服务降级
顾名思义就是在整个服务api层面进行降级维护处理。这样大部分服务就能够节省大量冗余的服务降级类,部分热点api接口另作其他独立的降级处理即可。这里我们针对客户端来实现全局的降级处理效果。
主要通过@DefaultProperties注解来实现
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DefaultProperties {
String groupKey() default "";
String threadPoolKey() default "";
HystrixProperty[] commandProperties() default {};
HystrixProperty[] threadPoolProperties() default {};
Class extends Throwable>[] ignoreExceptions() default {};
HystrixException[] raiseHystrixExceptions() default {};
String defaultFallback() default "";
}
重新构建一个controller类后,分别创建三个业务实现的api,分别是数据正常获取,延时获取,以及异常。然后定义全局的处理方法,并且在类上添加@DefaultProperties(defaultFallback = "方法名")注解来实现回调方法对全局的服务处理。
/**
* @author young
* @date 2022/12/25 17:07
* @description: 全局配置服务降级
*/
@Slf4j
@RestController
@RequestMapping("global")
@DefaultProperties(defaultFallback = "global")
public class GlobalHystrixController {
@Resource
private ProviderHystrixService providerHystrixService;
@GetMapping("/test/ok/{id}")
public Result providerOk(@PathVariable("id") Integer id){
return providerHystrixService.selectOne(id);
}
//全局配置服务降级
@HystrixCommand
@GetMapping("/test/wait/{id}")
public Result providerWait(@PathVariable("id") Integer id){
return providerHystrixService.selectOneWait(id);
}
/**
* 出现异常可以进行全局处理,成功跳转到全局处理方法
* @return
*/
@HystrixCommand
@GetMapping("ee")
public Result er(){
int a = 10/0;
return Result.ok("不行");
}
/**
* 返回类型必须与之前的服务接口返回类型一致Result,否则报错!
* @return
*/
public Result global(){
return Result.ok("全局服务降级配置效果,消费者端扛不住了,要开始降级处理了");
}
}
这样就能对大部分的服务异常情况做全局处理了。
通配服务降级可以让我们实现动态获取降级服务调用的方法,这样也能降低服务方法与降级兜底方法的耦合,大大提高处理效率。这种降级方式主要用于客户端,通过Hytrix与feign联合进行处理。
既然feign通过接口的方式对服务端api接口进行调用,那么,我们在feign接口进行处理即可。通过feign接口实现类来对服务端的每个接口统一进行一对一的异常降级处理。
/**
* Author young
* Date 2022/12/25 17:23
* Description: 通过新的类实现feign管理的服务提供端接口,动态获取降级服务调用的方法
*/
@Component
public class ProviderFeignClientServiceImpl implements ProviderHystrixService{
@Override
public Result selectOne(Integer id) {
return Result.ok("数据不会说谎,降级正常获取一个id的业务类,id为"+id);
}
@Override
public Result selectOneWait(Integer id) {
return Result.ok("降级线程等待后获取id的业务接口,id为"+id);
}
}
同时在feign的接口类上指明将包含降级处理的这个实现类作为用于处理所有回调的类。
/**
* @author young
* @date 2022/12/24 20:21
* @description:
*/
/*添加fallback通配的服务降级方法类*/
@FeignClient(value = "CLOUD-HYSTRIX-PROVIDER",fallback = ProviderFeignClientServiceImpl.class)
public interface ProviderHystrixService {
@GetMapping("/hystrix/use/{id}")
Result selectOne(@PathVariable("id") Integer id);
@GetMapping("/hystrix/wait/{id}")
Result selectOneWait(@PathVariable("id") Integer id);
}
这个时候如果服务提供端直接宕机了,也能对相关服务进行对应的降级处理。
但是并不代表它都能处理,如果客户端出现异常,或者服务端延时未降级处理仍然会报错!写在接口里只能处理服务提供端异常 ,对于客户端没有用,服务端报错:全局降级 > 服务端指定方法降级 > 客户端实现FeignFallback方法降级。客户端内方法报错如果没有指定方法降级或全局降级会直接抛出异常。因此在笔者看来,这个降价方法更像是单独处理宕机的,如果要避免客户端,服务提供段报错还需要和其他降级方法联合使用才行,大体感觉属实有些鸡肋……也有可能是笔者了解不够深入吧。
关于服务熔断主要已经在前言上详细阐述了,这里主要补充几点:
Hytrix的熔断类型:
熔断打开:
在固定时间内(Hystrix默认是10秒),接口调用出错比率达到一个阈值(Hystrix默认为50%),会进入熔断开启状态。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法。
熔断关闭:
服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制。
半熔断状态:
在进入熔断开启状态一段时间之后(Hystrix默认是5秒),熔断器会进入半熔断状态。部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
断路器开启关闭的条件:
当满足一定阈值的时候(默认10s内超过20个请求次数)
当失败率达到一定的时候(默认10s内超过50%请求失败)
到达以上阈值,断路器将会开启,开启后所有请求都不会进行转发,一段时间后(默认5s),这个时候断路器是半开状态,会让其中一个请求进行转发,成功:断路器关闭,失败:继续开启。
断路器打开后:
再有请求调用的时候,不会调用主逻辑,直接调用降级fallback,通过断路器,实现自动发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
断路器打开对主逻辑进行熔断后,hystrix会启动一个休眠时间窗,在这个时间窗内降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器进入半开状态,释放一次请求到原来的主逻辑,如果此次请求正常返回,断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时。
它的实现与服务降级差不多,同样需要在熔断后使用一个兜底的回调方法来处理业务熔断后的处理逻辑。
/**
* 用于测试服务熔断的模拟业务
*
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "circuitFallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),//是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),//请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),//时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),//失败率达到多少后跳闸
})
public String getCircuitBreaker(Integer id) {
if (id < 0) {
throw new RuntimeException("id不能为负数");
}
String uuid = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "编号为:" + uuid;
}
/**
* 回调方法
*
* @param id
* @return
*/
public String circuitFallback(Integer id) {
return "id不能为负数,请稍后再试" + id;
}
这种情况下我们已经对该方法设定了断路器,并且设定10次请求内如果失败请求(回调)率超过6成,那么会发生熔断,需要隔10s的窗口期后才能恢复正常。
正常情况下正确的访问返回的是:
失败的情况下返会回调方法的返回信息:
但是如果多次错误后失败率到达设定后就会发生熔断,此时就算我们请求正确信息也会导致请求失败,触发回调方法,需要过了时间间隔期后才能再次正确请求。
Hrtrix的学习总结告一段落,虽然Hrtrix已经停止维护了,但是它的设计理念还是很值得学习的,包括后续阿里的Sentinel。边学习边总结吧~