高并发高可用复杂系统中的缓存架构(二十三) hystrix 与高可用系统架构

怎么保证高可用性:在各种系统的各个地方有乱七八糟的异常和故障的情况下,整套缓存系统还能继续健康的 run 着

有一些将高可用的知识:HA、HAProxy 组件,主备服务间的切换,这就做到了高可用性, 主备实例,多冗余实例只是高可用最最基础的东西

接下来会讲解在什么样的情况下,可能会导致系统的崩溃?以及系统不可用,针对各种各样的一些情况, 然后我们用什么技术,去保护整个系统处于高可用的一个情况下

高可用有很多方式,在这里使用 hystrix

hystrix 提供了高可用相关的各种各样的功能,确保在 hystrix 的保护下,整个系统可以长期处于 高可用的状态,如 99.99%;

最理想的状态下,软件故障不应该导致整个系统的崩溃,服务器硬件故障可用通过服务的冗余来保证, 唯一有可能导致系统彻底崩溃,就是类似于机房停电,自然灾害等状况

不可用和产生的一些故障或者 bug 的区别:

  • 不可用:

    是完全不可用,整个系统完全崩溃

  • 部分故障或 bug:

    只是一小部分服务出问题

资源隔离、限流、熔断、降级、运维监控 而这些也是 hystrix 提供的功能

  • 资源隔离:让某一刻东西在故障的情况下,不会耗尽系统所有资源,如线程资源

    一个真实的遭遇,线上某块代码 bug,导致大量线程死循环,又创建大量线程, 最后系统资源被耗尽。崩溃

    资源隔离的话,比如限制只能使用 10 个线程,那么这一块出问题,也不会影响整个系统

  • 限流

    高并发流量涌入,比如突然间一秒钟 100 万 QPS,系统完全承受不住,直接崩溃。 限流可以只对 10 万 QPS 进行服务,其他的都拒绝。这种情况下就是在你双 11 抢东西付款 的时候,老是告诉你系统繁忙的情况,但是偶尔又可以刷出来

  • 熔断:连续故障,则在一段时间内直接拒绝服务

    当某一个服务连续转发失败(如那个服务根本没有启动), 则在短时间内直接返回异常信息,而不是继续转发,继续等待异常

  • 降级:

    如 mysql 挂了,系统发现了,自动降级,从内存里存的少量数据中,去提取一些数据出来。 但注意,这样的数据在什么场景下可以使用

  • 运维监控:

    监控 + 报警 + 优化,各种异常的情况,有问题就及时报警,然后对症下药

简而言之,系统在 hystrix 的保护下,不会完全崩溃,就算所有依赖都失效了,那么也还能提供一些最最基础的简单服务;

 

在分布式系统中,每个服务都可能会调用很多其他服务,被调用的那些服务就是依赖服务,有的时候某些依赖服务出现故障也是很正常的。

Hystrix 可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟或者依赖故障的容错机制。 Hystrix 通过将依赖服务进行资源隔离,进而阻止某个依赖服务出现故障的时候,这种故障在整个系统所有的依赖服务调用中进行蔓延, 同时 Hystrix 还提供故障时的 fallback 降级机制

总而言之,Hystrix 通过这些方法帮助我们提升分布式系统的可用性和稳定性

上面一段文字用下图示意

高并发高可用复杂系统中的缓存架构(二十三) hystrix 与高可用系统架构

初步看一看 Hystrix 的设计原则是什么?

hystrix 为了实现高可用性的架构,设计 hystrix 的时候,一些设计原则是什么?

  1. 对依赖服务调用时出现的调用延迟和调用失败进行控制和容错保护

  2. 在复杂的分布式系统中,阻止某一个依赖服务的故障在整个系统中蔓延

    服务 A - 服务 B -> 服务 C,服务 C 故障了,服务 B 也故障了,服务 A 故障了,整套分布式系统全部故障,整体宕机

  3. 提供 fail-fast(快速失败)和快速恢复的支持

  4. 提供 fallback 优雅降级的支持

  5. 支持近实时的监控、报警以及运维操作

关键词总结:

  • 调用延迟 + 失败,提供容错

  • 阻止故障蔓延

  • 快速失败 + 快速恢复

  • 降级

  • 监控 + 报警 + 运维

在高可用缓存系统中我们用Hystrix 要解决的问题是什么?

在复杂的分布式系统架构中,每个服务都有很多的依赖服务,而每个依赖服务都可能会故障, 如果服务没有和自己的依赖服务进行隔离,那么可能某一个依赖服务的故障就会拖垮当前这个服务

举例来说:某个服务有 30 个依赖服务,每个依赖服务的可用性非常高,已经达到了 99.99% 的高可用性

那么该服务的可用性就是 99.99% - (100% - 99.99% * 30 = 0.3%)= 99.69%, 意味着 3% 的请求可能会失败,因为 3% 的时间内系统可能出现了故障不可用了

对于 1 亿次访问来说,3% 的请求失败也就意味着 300万 次请求会失败,也意味着每个月有 2个 小时的时间系统是不可用的, 在真实生产环境中,可能更加糟糕

上面的描述想表达的意思是:即使你每个依赖服务都是 99.99% 高可用性,但是一旦你有几十个依赖服务, 还是会导致你每个月都有几个小时是不可用的

下面画图分析说,当某一个依赖服务出现了调用延迟或者调用失败时,为什么会拖垮当前这个服务? 以及在分布式系统中,故障是如何快速蔓延的?

 高并发高可用复杂系统中的缓存架构(二十三) hystrix 与高可用系统架构

简而言之:

  1. 假设只有系统承受并发能力是 100个线程,

  2. C 出问题的时候,耗时增加,将导致当前进入的 40 个线程得不到释放

  3. 后续大量的请求涌进来,也是先调用 c,然后又在这里了

  4. 最后 100 个线程都被卡在 c 了,资源耗尽,导致整个服务不能提供服务

  5. 那么其他依赖的服务也会出现上述问题,导致整个系统全盘崩溃

当时这个只能是在 高并发高流量的场景下会出现这种情况,其实工作中也遇到过一次真实的案例, quartz 默认线程只有 25 个,当时定时任务接近 150 个左右,平时每个定时任务触发时间基本上上分散的, 而且基本上在 10 分钟左右会结束任务,当我们调用其他第三方服务时,没有加超时功能, 第三方服务可能出问题了,导致我们的请求被卡主,进而导致任务线程不能结束,最后整个任务调度系统完全崩溃, 完全不能提供服务。

这个场景在我所工作生涯中可能是记忆最深的一次了,因为当时在线上,根据日志打印完全看不出来问题, 就像系统假死一样,后来通过 jconsole 查看线程挂起情况,发现所有线程调用第三方服务后都被卡主了。 才顺藤摸瓜找到 quartz 的默认线程只有 25 个。最后加大了线程,也只是治标不治本,长时间运行还是会出问题

再看 hystrix 的更加细节的设计原则是什么?

  1. 阻止任何一个依赖服务耗尽所有的资源,比如 tomcat 中的所有线程资源

  2. 避免请求排队和积压,采用限流和 fail fast 来控制故障

  3. 提供 fallback 降级机制来应对故障

  4. 使用资源隔离技术

    隔离技术是为了实现第一条的功能

    比如 bulkhead(舱壁隔离技术),swimlane(泳道技术),circuit breaker(短路技术), 来限制任何一个依赖服务的故障的影响

  5. 通过近实时的统计/监控/报警功能,来提高故障发现的速度

  6. 通过近实时的属性和配置热修改功能,来提高故障处理和恢复的速度

  7. 保护依赖服务调用的所有故障情况,而不仅仅只是网络故障情况

    调用这个依赖服务的时候,client 调用包有 bug、阻塞,等等

    依赖服务的各种各样的 调用的故障,都可以处理

Hystrix 是如何实现它的目标的?

  1. 通过 HystrixCommand 或者 HystrixObservableCommand 来封装对外部依赖的访问请求 d 这个访问请求一般会运行在独立的线程中,资源隔离

  2. 对于超出我们设定阈值的服务调用,直接进行超时,不允许其耗费过长时间阻塞住。

    这个超时时间默认是 99.5% 的访问时间,但是一般我们可以自己设置一下

  3. 为每一个依赖服务维护一个独立的线程池,或者是 semaphore(信号量),当线程池已满时,直接拒绝对这个服务的调用

  4. 对依赖服务的调用的成功次数、失败次数、拒绝次数、超时次数,进行统计

  5. 如果对一个依赖服务的调用失败次数超过了一定的阈值,自动进行熔断

    在一定时间内对该服务的调用直接降级,一段时间后再自动尝试恢复

  6. 当一个服务调用出现失败、被拒绝、超时、短路(熔断)等异常情况时,自动调用 fallback 降级机制

  7. 对属性和配置的修改提供近实时的支持

高并发高可用复杂系统中的缓存架构(二十三) hystrix 与高可用系统架构

 

搭建两个服务,一个缓存服务,一个商品服务

配置文件

server:
  port: 7000
logging:
  level:
    root: info
    # 可以打印 sql
    com.liu.shop.product: info
    org.springframework.web: TRACE
#  path: ./
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
#    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.99.173:3306/shop?useUnicode=yes&characterEncoding=UTF-8&useSSL=false
 
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

mybatis:

  mapper-locations: classpath*:mapper/*.xml

 

服务之间使用 mq 来通知缓存是否有修改,缓存服务调用商品服务完成更新

最基本的商品服务接口调用故障,导致缓存服务资源耗尽的场景

高并发高可用复杂系统中的缓存架构(二十三) hystrix 与高可用系统架构

这里总结下上图的信息:

  1. 我们的缓存架构大体上上面这样,缓存架构简介

    1. nginx 本地缓存,过期,过期之后服务去请求 redis 缓存

    2. redis 集群,高可用,大数据量,高并发

    3. nginx 在 redis 获取不到的时候,就去缓存服务获取

    4. 缓存服务会在本地缓存中获取,如果获取不到则去商品服务获取,并返回 nginx,同时更新 redis 缓存信息(保证数据不会并发冲突覆盖)

    5. 商品信息有更新,则通过消息队列通知缓存服务更新 redis 相关缓存

  2. 缓存故障的产生

    当所有缓存都失效的时候,大量获取商品详情的请求会到达商品服务, 商品服务会去数据库获取信息, 这时当获取商品服务接口比平时耗时更长时,大量的请求会被阻塞

    缓存服务的线程资源也被阻塞,nginx 的线程资源也被阻塞,这个时候就会出现, 大量的商品详情页请求失败,一个服务还有其他的接口,比如店铺接口,当线程资源被耗尽的时候,其他服务也不能正常提供服务了

    这样一来所有服务不能对外提供服务,大量流量进来,系统崩溃