多位微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他微服务,这就是所谓的"扇处"。如果扇处的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃—所谓的"雪崩效应"。
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上面的所有资源在几秒钟内饱和比失败更糟糕的是这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
“Hystrix”是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等。Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应"FallBack",而不是长时间的等待或者抛出调用方无法解决的异常。这样就保证了服务调用方的线程不会被长时间、不必要的占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
【1】服务熔断
熔断机制是应对雪崩效应的一种微服务链路保护机制
当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复链路。在SpringCloud框架里熔断机制通过Hystrix实现。Hystrix会监控微服务调用的状况,当失败的调用到一定阈值就会启动熔断机制(默认是5秒内20次调用失败就会启动熔断机制)。
熔断机制的注解:@HystrixCommand。
① 参考/microservicecloud-provider-dept-8001创建/microservicecloud-provider-dept-hystrix-8001
基础代码延用上篇博文Feign负载均衡与Ribbon代码
② 添加Hystrix依赖
<dependencies> <!-- 引入自己定义的api通用包,可以使用Dept部门Entity --> <dependency> <groupId>com.web.springcloud</groupId> <artifactId>microservicecloud-api</artifactId> <version>${project.version}</version> </dependency> <!-- actuator监控信息完善 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- Hystrix --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency> <!-- 将微服务provider侧注册进eureka --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <!-- 修改后立即生效,热部署 --> <dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> </dependencies>
③ yml文件配置
server: port: 8001 mybatis: config-location: classpath:mybatis/mybatis.cfg.xml # mybatis配置文件所在路径 type-aliases-package: com.web.springcloud.entities # 所有Entity别名类所在包 mapper-locations: - classpath:mybatis/mapper/**/*.xml # mapper映射文件 spring: application: name: microservicecloud-dept datasource: type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型 driver-class-name: com.mysql.jdbc.Driver # mysql驱动包 # driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包 url: jdbc:mysql://localhost:3306/clouddb01 # 数据库名称 username: root password: 123456 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: true testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 # dbcp2: # min-idle: 5 # 数据库连接池的最小维持连接数 # initial-size: 5 # 初始化连接数 # max-total: 5 # 最大连接数 # max-wait-millis: 200 # 等待连接获取的最大超时时间 eureka: client: #客户端注册进eureka服务列表内 service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ # defaultZone: http://localhost:7001/eureka/ instance: instance-id: microservicecloud-dept8001-hystrix # 自定义服务实例Id prefer-ip-address: true #访问路径可以显示IP地址 # http://192.168.2.100:8001/info优化显示 info: app.name: web-microservicecloud company.name: www.web.com build.artifactId: $project.artifactId$ build.version: $project.version$
项目结构如下图:
④ 修改DeptController
@RestController public class DeptController { @Autowired private DeptService service = null; @RequestMapping(value = "/dept/get/{id}", method = RequestMethod.GET) //一旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand标注好的fallbackMethod中的指定方法 @HystrixCommand(fallbackMethod = "processHystrix_Get") public Dept get(@PathVariable("id") Long id) { Dept dept = this.service.get(id); if (null == dept) { throw new RuntimeException("该ID:" + id + "没有没有对应的信息"); } return dept; } public Dept processHystrix_Get(@PathVariable("id") Long id) { Dept dept = new Dept(); dept.setDeptno(id); dept.setDname("该ID:" + id + "没有没有对应的信息,null--@HystrixCommand"); dept.setDb_source("no this database in MySQL"); return dept; } }
⑤ 主程序类添加熔断支持
@SpringBootApplication @EnableEurekaClient //本服务启动后会自动注册进eureka服务中 @EnableDiscoveryClient//服务发现 @EnableCircuitBreaker //对Hystrix熔断机制支持 public class DeptProvider8001_Hystrix_App { public static void main(String[] args) { SpringApplication.run(DeptProvider8001_Hystrix_App.class, args); } }
⑥ 测试
首先启动三个服务中心7001 7002 7003然后启动/microservicecloud-provider-dept-hystrix-8001,接着启动/microservicecloud-consumer-dept-80,浏览器使用消费者请求一个数据库中不存在的记录。
【2】服务降级
一句话简要描述:整体资源快不够了,忍痛将某些服务先关掉,待渡过难关,再开启回来。
服务降级处理是在客户端(消费者)完成的与服务端没有关系。
参考【1】服务熔断的处理方式,会发现这种方式是很可怕的。第一如果有多个方法,对应的FallBack方法也会随之膨胀;第二异常处理与业务逻辑高耦合,完全不符合Spring AOP面向切面的思想。
上面说了,服务降级是在客户端(消费者)处理的,又需要与业务解耦,使用面向切面的思想。再次参考@FeignClient注解源码:
/** * Annotation for interfaces declaring that a REST client with that interface should be * created (e.g. for autowiring into another component). If ribbon is available it will be * used to load balance the backend requests, and the load balancer can be configured * using a <code>@RibbonClient</code> with the same name (i.e. value) as the feign client. * * @author Spencer Gibb * @author Venil Noronha */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FeignClient { /** * The name of the service with optional protocol prefix. Synonym for {@link #name() * name}. A name must be specified for all clients, whether or not a url is provided. * Can be specified as property key, eg: ${propertyKey}. */ @AliasFor("name") String value() default ""; /** * The service id with optional protocol prefix. Synonym for {@link #value() value}. * * @deprecated use {@link #name() name} instead */ @Deprecated String serviceId() default ""; /** * The service id with optional protocol prefix. Synonym for {@link #value() value}. */ @AliasFor("value") String name() default ""; /** * Sets the <code>@Qualifier</code> value for the feign client. */ String qualifier() default ""; /** * An absolute URL or resolvable hostname (the protocol is optional). */ String url() default ""; /** * Whether 404s should be decoded instead of throwing FeignExceptions */ boolean decode404() default false; /** * A custom <code>@Configuration</code> for the feign client. Can contain override * <code>@Bean</code> definition for the pieces that make up the client, for instance * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. * * @see FeignClientsConfiguration for the defaults */ Class<?>[] configuration() default {}; /** * Fallback class for the specified Feign client interface. The fallback class must * implement the interface annotated by this annotation and be a valid spring bean. */ Class<?> fallback() default void.class; /**注意这里注释!!!!*/ /** * Define a fallback factory for the specified Feign client interface. * //为指定的 Feign客户端接口定义一个fallback factory。 * The fallback factory must produce instances of fallback classes that implement the interface * annotated by {@link FeignClient}. * // 该工程必须产生一个标注了@FeignClient注解接口的实现类的实例。 * The fallback factory must be a valid spring bean. *// 该工厂必须是一个可用的Spring bean--要注册进容器 * @see feign.hystrix.FallbackFactory for details. */ Class<?> fallbackFactory() default void.class; /** * Path prefix to be used by all method-level mappings. Can be used with or without * <code>@RibbonClient</code>. */ String path() default ""; /** * Whether to mark the feign proxy as a primary bean. Defaults to true. */ boolean primary() default true; }
解决方案如下:
① 修改/microservicecloud-api工程,根据已有的DeptClientService新建一个实现了FallbackFactory接口的类DeptClientServiceFallbackFactory
DeptClientServiceFallbackFactory实例如下:
@Component // 不要忘记添加,不要忘记添加 public class DeptClientServiceFallbackFactory implements FallbackFactory<DeptClientService> { @Override public DeptClientService create(Throwable throwable) { return new DeptClientService() { @Override public Dept get(long id) { Dept dept = new Dept(); dept.setDeptno(id); dept.setDname("该ID:" + id + "没有没有对应的信息,Consumer客户端提供的降级信息,此刻服务Provider已经关闭"); dept.setDb_source("no this database in MySQL"); return dept; } @Override public List<Dept> list() { return null; } @Override public boolean add(Dept dept) { return false; } }; } }
② 修改DeptClientService上的@FeignClient
@FeignClient(value = "MICROSERVICECLOUD-DEPT",fallbackFactory=DeptClientServiceFallbackFactory.class) public interface DeptClientService { @RequestMapping(value = "/dept/get/{id}", method = RequestMethod.GET) public Dept get(@PathVariable("id") long id); @RequestMapping(value = "/dept/list", method = RequestMethod.GET) public List<Dept> list(); @RequestMapping(value = "/dept/add", method = RequestMethod.POST) public boolean add(Dept dept); }
③ 修改/microservicecloud-consumer-dept-feign工程的yml
server: port: 80 spring: application: name: microservicecloud-consumer feign: hystrix: enabled: true eureka: instance: prefer-ip-address: true # 注册服务的时候使用服务的ip地址 client: register-with-eureka: false # 不向服务注册中心注册自己 service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ # defaultZone: http://localhost:7001/eureka/
④ 测试
首先启动三个服务中心7001 7002 7003,然后启动服务提供者/microservicecloud-provider-dept-8001,最后启动服务消费者/microservicecloud-consumer-dept-feign。
正常访问:http://localhost/consumer/dept/get/1
将服务提供者关掉,然后再测试消费者:
可以看到,此时我的服务提供者provider已经down了,但是我们做了服务降级处理,让客户端在服务不可用的时候也会获得信息提示而不会挂起耗死服务器。
【3】服务监控Hystrix Dashboard
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续的记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户。其中包括每秒执行多少请求,多少成功,多少失败等。
Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。SpringCloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。
① 新建module microservicecloud-consumer-hystrix-dashboard
pom文件如下:
<dependencies> <!-- 自己定义的api --> <dependency> <groupId>com.web.springcloud</groupId> <artifactId>microservicecloud-api</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 修改后立即生效,热部署 --> <dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <!-- Ribbon相关 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <!-- feign相关 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency> <!-- hystrix和 hystrix-dashboard相关 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId> </dependency> </dependencies>
② yml文件如下
server: port: 9001
③ 主启动类如下:
@SpringBootApplication @EnableHystrixDashboard public class DeptConsumer_DashBoard_App { public static void main(String[] args) { SpringApplication.run(DeptConsumer_DashBoard_App.class, args); } }
④ 服务提供者8001 8002 8003添加监控依赖
<!-- actuator监控信息完善 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
⑤ 启动/microservicecloud-consumer-hystrix-dashboard
别乱猜,这货不是熊,这是豪猪!!!
⑥ 启动三个服务中心和/microservicecloud-provider-dept-hystrix-8001
访问http://localhost:8001/hystrix.stream:
可以看到该页面在不断获取数据!!!
⑦ 使用Hystrix Dashboard监控
页面如下填写,分别是监控URL,延迟时间和自定义名字。
⑧ 如何查看?
一圈–实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色<黄色<橙色<红色递减。
实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化。流量越大该实心圆也就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压力实例。
曲线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。
多次刷新http://localhost:8001/dept/get/1请求,然后查看Hystrix Dashboard如下:
连续发出六次请求http://localhost:8001/dept/get/1,如下图:
参数详细说明如下: