高并发高可用复杂系统中的缓存架构(二十三) 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 的时候,一些设计原则是什么?
-
对依赖服务调用时出现的调用延迟和调用失败进行控制和容错保护
-
在复杂的分布式系统中,阻止某一个依赖服务的故障在整个系统中蔓延
服务 A - 服务 B -> 服务 C,服务 C 故障了,服务 B 也故障了,服务 A 故障了,整套分布式系统全部故障,整体宕机
-
提供 fail-fast(快速失败)和快速恢复的支持
-
提供 fallback 优雅降级的支持
-
支持近实时的监控、报警以及运维操作
关键词总结:
-
调用延迟 + 失败,提供容错
-
阻止故障蔓延
-
快速失败 + 快速恢复
-
降级
-
监控 + 报警 + 运维
在高可用缓存系统中我们用Hystrix 要解决的问题是什么?
在复杂的分布式系统架构中,每个服务都有很多的依赖服务,而每个依赖服务都可能会故障, 如果服务没有和自己的依赖服务进行隔离,那么可能某一个依赖服务的故障就会拖垮当前这个服务
举例来说:某个服务有 30 个依赖服务,每个依赖服务的可用性非常高,已经达到了 99.99% 的高可用性
那么该服务的可用性就是 99.99% - (100% - 99.99% * 30 = 0.3%)= 99.69%, 意味着 3% 的请求可能会失败,因为 3% 的时间内系统可能出现了故障不可用了
对于 1 亿次访问来说,3% 的请求失败也就意味着 300万 次请求会失败,也意味着每个月有 2个 小时的时间系统是不可用的, 在真实生产环境中,可能更加糟糕
上面的描述想表达的意思是:即使你每个依赖服务都是 99.99% 高可用性,但是一旦你有几十个依赖服务, 还是会导致你每个月都有几个小时是不可用的
下面画图分析说,当某一个依赖服务出现了调用延迟或者调用失败时,为什么会拖垮当前这个服务? 以及在分布式系统中,故障是如何快速蔓延的?
简而言之:
-
假设只有系统承受并发能力是 100个线程,
-
C 出问题的时候,耗时增加,将导致当前进入的 40 个线程得不到释放
-
后续大量的请求涌进来,也是先调用 c,然后又在这里了
-
最后 100 个线程都被卡在 c 了,资源耗尽,导致整个服务不能提供服务
-
那么其他依赖的服务也会出现上述问题,导致整个系统全盘崩溃
当时这个只能是在 高并发高流量的场景下会出现这种情况,其实工作中也遇到过一次真实的案例, quartz 默认线程只有 25 个,当时定时任务接近 150 个左右,平时每个定时任务触发时间基本上上分散的, 而且基本上在 10 分钟左右会结束任务,当我们调用其他第三方服务时,没有加超时功能, 第三方服务可能出问题了,导致我们的请求被卡主,进而导致任务线程不能结束,最后整个任务调度系统完全崩溃, 完全不能提供服务。
这个场景在我所工作生涯中可能是记忆最深的一次了,因为当时在线上,根据日志打印完全看不出来问题, 就像系统假死一样,后来通过 jconsole 查看线程挂起情况,发现所有线程调用第三方服务后都被卡主了。 才顺藤摸瓜找到 quartz 的默认线程只有 25 个。最后加大了线程,也只是治标不治本,长时间运行还是会出问题
再看 hystrix 的更加细节的设计原则是什么?
-
阻止任何一个依赖服务耗尽所有的资源,比如 tomcat 中的所有线程资源
-
避免请求排队和积压,采用限流和 fail fast 来控制故障
-
提供 fallback 降级机制来应对故障
-
使用资源隔离技术
隔离技术是为了实现第一条的功能
比如 bulkhead(舱壁隔离技术),swimlane(泳道技术),circuit breaker(短路技术), 来限制任何一个依赖服务的故障的影响
-
通过近实时的统计/监控/报警功能,来提高故障发现的速度
-
通过近实时的属性和配置热修改功能,来提高故障处理和恢复的速度
-
保护依赖服务调用的所有故障情况,而不仅仅只是网络故障情况
调用这个依赖服务的时候,client 调用包有 bug、阻塞,等等
依赖服务的各种各样的 调用的故障,都可以处理
Hystrix 是如何实现它的目标的?
-
通过 HystrixCommand 或者 HystrixObservableCommand 来封装对外部依赖的访问请求 d 这个访问请求一般会运行在独立的线程中,资源隔离
-
对于超出我们设定阈值的服务调用,直接进行超时,不允许其耗费过长时间阻塞住。
这个超时时间默认是 99.5% 的访问时间,但是一般我们可以自己设置一下
-
为每一个依赖服务维护一个独立的线程池,或者是 semaphore(信号量),当线程池已满时,直接拒绝对这个服务的调用
-
对依赖服务的调用的成功次数、失败次数、拒绝次数、超时次数,进行统计
-
如果对一个依赖服务的调用失败次数超过了一定的阈值,自动进行熔断
在一定时间内对该服务的调用直接降级,一段时间后再自动尝试恢复
-
当一个服务调用出现失败、被拒绝、超时、短路(熔断)等异常情况时,自动调用 fallback 降级机制
-
对属性和配置的修改提供近实时的支持
搭建两个服务,一个缓存服务,一个商品服务
配置文件
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 来通知缓存是否有修改,缓存服务调用商品服务完成更新
最基本的商品服务接口调用故障,导致缓存服务资源耗尽的场景
这里总结下上图的信息:
-
我们的缓存架构大体上上面这样,缓存架构简介
-
nginx 本地缓存,过期,过期之后服务去请求 redis 缓存
-
redis 集群,高可用,大数据量,高并发
-
nginx 在 redis 获取不到的时候,就去缓存服务获取
-
缓存服务会在本地缓存中获取,如果获取不到则去商品服务获取,并返回 nginx,同时更新 redis 缓存信息(保证数据不会并发冲突覆盖)
-
商品信息有更新,则通过消息队列通知缓存服务更新 redis 相关缓存
-
-
缓存故障的产生
当所有缓存都失效的时候,大量获取商品详情的请求会到达商品服务, 商品服务会去数据库获取信息, 这时当获取商品服务接口比平时耗时更长时,大量的请求会被阻塞
缓存服务的线程资源也被阻塞,nginx 的线程资源也被阻塞,这个时候就会出现, 大量的商品详情页请求失败,一个服务还有其他的接口,比如店铺接口,当线程资源被耗尽的时候,其他服务也不能正常提供服务了
这样一来所有服务不能对外提供服务,大量流量进来,系统崩溃