码迷,mamicode.com
首页 > 其他好文 > 详细

漫谈雪崩

时间:2017-07-30 19:56:35      阅读:160      评论:0      收藏:0      [点我收藏+]

标签:check   ==   没有   比例   攻击   失效   美的   本地   流控   

雪崩是指平时正常调用和被调用的A系统和B系统,突然A系统对B系统的訪问超出B系统的承受能力。造成B系统崩溃。

注意将雪崩效应与拒绝服务攻击相差别:两者都是由于B系统过载导致崩溃,但后者是人为的蓄意攻击。

注意将雪崩效应与訪问量激增差别:假设A系统直接面对用户。那么激增的用户将直接带来A系统和B系统的流量激增,只是这样的case能够通过预估而对A系统和B系统做扩容应对。

一种雪崩case

假设A系统的訪问量非常平稳,那么是什么造成B的訪问量激增呢?造成雪崩的原因非常多,本文给出一个比較典型的case。首先先来看一个被称为火柴棍的体系结构:

技术分享

A系统依赖B系统读服务,A系统是60台机器组成的集群,B系统是6台机器组成的集群,之所以6台机器可以扛住60台机器的訪问,是由于A系统并非每次都訪问B。而是首先请求缓存,仅仅有缓存的对应数据失效时才会请求B。

这正是cache存在的意义,它让B系统节省了大量机器,假设没有cache。B系统不得不组成60台机器的集群,假设A也同一时候依赖C系统呢?C系统也要60台机器,放大的流量将非常快耗尽公司的资源。

然而这也不是十全十美,这个结构就像一个火柴棍顶住了上面的一辆大卡车。原因并非火柴棍有多牛逼。而是由于大卡车上绑满了氢气球,而氢气球就是cache。我想你应该看出问题来了,假设气球破了不就傻逼了?没错,这就是雪崩的一种方式!

回到A和B的架构。造成雪崩的原因至少有以下三种:

1、B系统的前置代理发生问题或者其它原因造成B系统临时不可用,等B系统系统服务恢复时,A系统流量将铺天盖地打过来。

2、Cache系统故障,A系统的訪问将铺天盖地的打到B系统。

3、Cache故障恢复。但这时Cache为空。Cache瞬间命中率为0,相当于Cache被击穿

第一个原因不太好理解,为什么B系统恢复后流量会猛增呢?主要原因就是缓存的超时时间。当有数据超时的时候,A系统会訪问B系统,可是这时候B系统偏偏故障不可用,那么这个数据仅仅好超时,等发现B系统恢复时,B系统发现缓存里的数据已经都超时了,都成了旧数据,这时当然全部的请求就打到了A。

或许你非常快就给出解决方式:既然引入Cache会有这么多雪崩隐患。那么去掉Cache。让B系统服务能力大于或等于A系统不就得了。

这样的方案确实能消除雪崩。但却不是一条正确路子。这句话解释起来有点麻烦,而是举一个栗子说明:众所周知资本主义对于封建社会是一种制度进步。可是资本主义本身也会有各种各样的新问题,这些问题在封建社会中可能并不存在,假设想解决资本主义的这些问题难道要退回到封建社会么?

雪崩的预防

Client端的方案

所谓Client端指的就是上文结构中的A系统。相对于B系统,A系统就是B系统的Client。

针对造成雪崩的三个原因:B系统故障恢复、Cache故障、Cache故障恢复,看看A系统有哪些方案能够应对。

合理使用Cache应对B系统宕机

cache的每一个Key除了相应Value,还相应一个过期时间T,在T内,get操作直接在Cache中拿到Key相应Value并返回。可是在T到达时。get操作主要有五种模式:

  1、基于超时的简单(stupid)模式

在T到达时,不论什么线程get操作发现Cache中的Key和相应Value将被清除或标记为不可用,get操作将发起调用远程服务获取key相应的Value。并更新写回Cache;

  2、基于超时的常规模式

在T到达时。Cache中的Key和相应Value将被清除或标记为不可用,get操作将调用远程服务获取key相应的Value,并更新写回Cache;

此时,假设还有一个线程发现key和Value已经不可用。get操作还须要推断有没有其它线程发起了远程调用,假设有。那么自己就等待,直到那个线程远程获取操作成功。Cache中得key变得可用。自己直接从Cache中获取就完事了。

为了便于理解。打个例如:5个工人(线程)去港口取相同key的货(get),发现货已经过期被扔掉了,那么仅仅需派出一个人去对岸取货,其它四个人在港口等待就可以,而不用5个人全去。而基于超时的简单模式中。就相当于5个工人去港口取相同key的货,发现没有,则5个工人都去远方港口取货,5个工人彼此全然无沟通,视彼此不存在。

实现基于超时的常规模式就须要用到经典的Java Synchronized-double-check惯使用方法了。

  3、基于刷新的简单(stupid)模式

在T到达时,Cache中的key和对应Value不动。可是假设有线程调用get操作,将触发refresh操作,依据get和refresh的同步关系。又分为两种模式:

  • 同步模式:不论什么线程发现key过期,都触发一次refresh操作,get操作等待refresh操作结束,refresh结束后,get操作返回当前Cache中key相应的Value。注意refresh操作结束并不意味着refresh成功,还可能抛了异常,没有更新Cache,可是get操作无论,get操作返回的值可能是旧值。
  • 异步模式:不论什么线程发现key过期。都触发一次refresh操作,get操作触发refresh操作,不等refresh完毕,直接返回Cache中的旧值。

  4、基于刷新的常规模式

在T到达时。Cache中的key和对应Value不动,可是假设有线程调用get操作。将触发refresh操作。依据get和refresh的同步关系,又分为两种模式:

  • 同步模式:get操作等待refresh操作结束。refresh结束后。get操作返回当前Cache中key相应的Value,注意refresh操作结束并不意味着refresh成功,还可能抛了异常,没有更新Cache。可是get操作无论,get操作返回的值可能是旧值。假设其它线程进行get操作,key已经过期,而且发现有线程触发了refresh操作,则自己不等refresh完毕直接返回旧值。

  • 异步模式:get操作触发refresh操作,不等refresh完毕,直接返回Cache中的旧值。

    那么显然,假设其它线程进行get操作,key已经过期,而且发现有线程触发了refresh操作,则自己不等refresh完毕直接返回旧值。

再举上面码头工人的样例说明基于刷新:这次还是5工人去港口取货,这时货都在,尽管已经旧了。这时5个工人有两种选择:

派一个人去远方港口取新货。其余哥四个拿着旧货先回(同步模式);

通知一个雇佣工去远方取新货。哥五个都拿着旧货先回(异步模式);

基于刷新的简单模式和基于刷新的常规模式的差别能够參考基于超时的简单模式和基于超时的常规模式的差别,不再赘述。

  5、基于刷新的续费模式

该模式和基于刷新的常规模式唯一的差别在于refresh操作超时或失败的处理上。在基于刷新的常规模式中,refresh操作超时或失败时抛出异常,Cache中的对应key-Value还是旧值。这样下一个get操作到来时又会触发一次refresh操作。

在基于刷新的续费模式中,假设refresh操作失败,那么refresh将把旧值当成新值返回,这样就相当于旧值又被续费了T时间,兴许T时间内get操作将取到这个续费的旧值而不会触发refresh操作。

基于刷新的续费模式也像常规模式那样分为同步模式和异步模式。不再赘述。

以下讨论这5种cache get模式在雪崩发生时的表现。首先如果例如以下:

  • 如果A系统的訪问量为每分钟M次
  • 如果cache能存Key为C个,而且key空间有N个
  • 如果正常状态下,B系统訪问量为每分钟w次,显然w=未命中数+过期数<C+M*(N-C)/N。

这时由于某种原因,比方B长时间故障,造成cache中得key所有过期,B系统这时从故障中恢复,五种get模式分析表现分析例如以下:

1、在基于超时和刷新的简单模式中,B系统的瞬间流量将达到和A的瞬时流量M大体等同,相当于cache被击穿。

这就发生了雪崩。这时刚刚恢复的B系统将肯定会被拍死。

2、在基于超时和刷新的常规模式中。B系统的瞬间流量将和cache中key空间N大体等同。这时是否发生雪崩。B系统是否能扛得住就要看key空间N是否超过B系统的流量上限了。

3、在基于刷新的续费模式中,B系统的瞬间流量为w,和正常情况同样而不会发生雪崩!实际上。在基于刷新的续费模式中。不存在cache key所有过期的情况。就算把B系统永久性的干掉。A系统的cache也会基于旧值长久的平稳执行!

从B系统的角度看,可以抵抗雪崩的基于刷新的续费模式完胜。

从A系统的角度看,因为普通情况下A系统是一个高訪问量的在线web应用,这样的应用最讨厌的一个词就是“线程等待”,因此基于刷新的各种异步模式较优。

综合考虑。基于刷新的异步续费模式是首选

然而凡是有利就有弊,有两点须要注意的地方:

1、基于刷新的模式最大的缺点是key-Value一旦放入cache就不会被清除。每次更新也是新值覆盖旧值。GC永远无法对其进行垃圾收集。

而基于超时的模式中。key-Value超时后假设新的訪问没有到来,内存是能够被GC垃圾回收的。所以假设你使用的是寸土寸金的本地内存做cache就要小心了。

2、基于刷新的续费模式须要做好监控,不然有可能发生cache中得值已经和真实的值相差非常远了,应用还以为是新值,而你也不知道。

关于详细的cache。来自Google的Guava本地缓存库支持上文的另外一种、第四种和第五种get操作模式。

可是对于Redis等分布式缓存。只提供原始的get、set方法,而提供的get不过获取。与上文提到的五种get操作模式不是一个概念。开发人员想用这五种get操作模式的话不得不自己封装和实现。

五种get操作模式中,基于超时和刷新的简单模式是实现起来最简单的模式,但遗憾的是这两种模式对雪崩全然无免疫力,这可能也是雪崩在大量依赖缓存的系统中频繁发生的一个重要原因吧。

应对分布式cache宕机

假设是Cache直接挂了,那么上面的基于刷新的异步续费模式也就然并卵了。这时A系统铁定无法对Cache进行存取操作,仅仅能将流量全然打到B系统,B系统面对雪崩在劫难逃...

本节讨论的预防Cache宕机仅限于分布式Cache,由于本地Cache一般和A系统应用共享内存和进程。本地Cache挂了A系统也挂了。不会出现本地Cache挂了而A系统应用正常的情况。

首先。A系统请求线程检查分布式cache状态,假设无应答等说明分布式Cache挂了,则转向请求B系统。这样一来雪崩将压垮B系统。

这时可选的方案例如以下:

1、A系统的当前线程不请求B系统。而是打个日志并设置一个默认值。

2、A系统的当前线程依照一定概率决定是否请求B系统。

3、A系统的当前线程检查B系统执行情况,假设良好则请求B系统。

方案1最简单。A系统知道假设没有Cache。B系统可能扛不住自己的所有流量,索性不请求B系统,等待Cache恢复。但这时B系统利用率为0,显然不是最优方案,并且当请求不easy设置默认值时,这个方法就不行了。

方案2能够让一部分线程请求B系统,这部分请求肯定能被B系统hold住。

能够保守的设置这个概率u=B系统的平均流量/A系统的峰值流量

方案3是一种更为智能的方案,假设B系统执行良好,当前线程请求。假设B系统过载,则不请求。这样A系统将让B系统处于一种宕机与不宕机的临界状态,最大限度挖掘B系统性能。

这样的方案要求B系统提供一个性能评估接口返回yes和no,yes表示B系统良好,能够请求。no表示B系统情况不妙,不要请求。这个接口将被频繁调用,必须高效。

方案3的关键在于怎样评估一个系统的执行状况。

一个系统中当前主机的性能參数有cpu负载、内存使用率、swap使用率、gc频率和gc时间、各个接口平均响应时间等。性能评估接口须要依据这些參数返回yes或者no,是不是机器学习里的二分类问题?关于这个问题已经能够单独写篇文章讨论了。再这里就不展开了,你能够想一个比較简单傻瓜的保守策略,结果无非是让A系统无法非常好的逼近B系统的性能极限。

综合以上分析,方案2比較靠谱。

假设选择方案3。建议由专门团队负责研究并提供统一的系统性能实时评估方案和工具。

应对分布式cache宕机后的恢复

不要以为成功hold住分布式cache宕机就万事大吉了,真正的考验是分布式Cache从宕机过程恢复之后,这时分布式Cache中什么都没有。

即使是上文中提到了基于刷新的异步续费策略这时也没有什么卵用,由于分布式Cache为空,不管怎样都要请求B系统。这时B系统的最大流量是Key的空间取值数量。

假设Key的取值空间数量非常少,则相安无事;假设Key的取值空间数量大于B系统的流量上限。雪崩依旧在所难免。

这样的情况A系统非常难处理,关键原因是A系统请求cache返回key相应value为空,A系统无法知道是由于当前Cache是刚刚初始化,全部内容都为空;还是由于不过自己请求的那个key没在Cache里

假设是前者。那么当前线程就要像处理cache宕机那样进行某种策略的回避;假设是后者。直接请求B系统就可以,由于这是正常的cache使用流程。

对于cache宕机的恢复,A系统真的无能为力,仅仅能寄希望于B系统的方案了。

Server端的方案

相当于client端须要应对各种复杂问题,Server端须要应对的问题很easy。就是怎样从容应对过载的问题。

不管是雪崩也要。还是拒绝服务攻击也罢,对于Server端来说都是过载保护的问题。对于过载保护,主要有两种现实方案和一种超现实方案。

流量控制

流量控制就是B系统实时监控当前流量,假设超过预设的值或者系统承受能力。则直接拒绝掉一部分请求,以实现对系统的保护。

流量控制依据基于的数据不同,可分为两种:

1、基于流量阈值的流控:流量阈值是每一个主机的流量上限,流量超过该阈值主机将进入不稳定状态。阈值提前进行设定,假设主机当前流量超过阈值,则拒绝掉一部分流量,使得实际被处理流量始终低于阈值。

2、基于主机状态的流控:每一个接受每一个请求之前先推断当前主机状态。假设主机状况不佳,则拒绝当前请求。

基于阈值的流控实现简单,可是最大的问题是须要提前设置阈值,并且随着业务逻辑越来越复杂,接口越来越多,主机的服务能力实际应该是下降的。这样就须要不断下调阈值,添加了维护成本。并且万一忘记调整的话。呵呵……

主机的阈值能够通过压力測试确定,选择的时候能够保守些。

基于主机状态的流控免去了人为控制。可是其最大的确定上文已经提到:怎样依据当前主机各个參数推断主机状态呢?想较完美的回答这个问题目測并不easy。因此在没有太好答案之前,我推荐基于阈值的流控。

流量控制基于实现位置的不同,又能够分为两种:

1、反向代理实现流控:在反向代理如Nginx上基于各种策略进行流量控制。

这样的一般针对Http服务。

2、借助服务治理系统:假设Server端是RMI、RPC等服务,能够构建专门的服务治理系统进行负载均衡、流控等服务。

3、服务容器实现流控:在业务逻辑之前实现流量控制。

第三种在server的容器(如Java容器)中实现流控并不推荐,由于流控和业务代码混在一起easy乱;其次实际上流量已经全量进入到了业务代码里,这时的流控仅仅是阻止其进入真正的业务逻辑。所以流控效果将打折;再次,假设流量策略常常变动,系统将不得不为此常常更改。

因此。推荐前两种方式。

最后提一个注意点:当由于流控而拒绝请求时,务必在返回的数据中带上相关信息(比方“当前请求由于超出流量而被禁止訪问”),假设返回值什么都没有将是一个大坑。由于造成调用方请求没有被响应的原因非常多,可能是调用方Bug。也可能是服务方Bug,还可能是网络不稳定。这样一来非常可能在排查一整天后发现是流控搞的鬼。。。

服务降级

服务降级一般由人为触发,属于雪崩恢复时的策略,但为了和流控对照,将其放到这里。

流量控制本质上是减小訪问量,而服务处理能力不变。而服务降级本质上是增强服务处理能力,而訪问量不变。

服务降级是指在服务过载时关闭不重要的接口(直接拒绝处理请求)。而保留重要的接口。

比方服务由10个接口,服务降级时关闭了当中五个。保留五个,这时这个主机的服务处理能力将增强到二倍左右。

然而。雪崩到来时动辄就超出系统处理能力10倍,而服务降级能使主机服务处理能力提高10倍么?显然非常困难。看来服务降级并不能取代流控成为过载保护的主要策略。

动态扩展

动态扩展指的是在流量超过系统服务能力时。自己主动触发集群扩容。自己主动部署并上线执行。当流量过去后又自己主动回收多余机器,全然弹性。

这个方法是不是感觉非常不错。

可是就算是前两年云计算泡沫吹的最天花乱坠的时候也没见哪个国内大公司真正用起来。

据说亚马逊、谷歌能够,可是天朝不让用,自研的话还是任重道远呐!

雪崩的恢复

雪崩发生时须要运维控制流量,等后台系统启动完成后循序渐进的放开流量,主要目的是让Cache慢慢预热。流量控制刚開始能够为10%,然后20%,然后50%。然后80%,最后全量。当然详细的比例,尤其是初始比例,还要看后端承受能力和前端流量的比例,各个系统并不同样。

假设后端系统有专门的工具进行Cache预热,则省去了运维的工作,等Cache热起来再公布后台系统就可以。可是假设Cache中的key空间非常大,开发预热工具将比較困难。

结论

“防患于未然”放在雪崩的应对上也适合,预防为主。补救为辅。综合上文分析。详细的预防要点例如以下:

1、调用方(A系统)採用基于刷新的异步续费模式使用Cache,或者至少不能使用基于超时或刷新的简单(stupid)模式。

2、调用方(A系统)每次请求Cache时检查Cache是否可用(available),假设不可用则依照一个保守的概率訪问后端。而不是无所顾忌的直接訪问后端。

3、服务方(B系统)在反向代理处设置流量控制进行过载保护。阈值须要通过压測获得。

假设有精力的话能够研究下主机应用健康推断问题和动态弹性运维问题。

至于雪崩的补救主要还是靠运维和研发控流量公布了。

雪崩研究了两周多,就到这了。下一步可能给图灵出版社翻译一些开源书籍。大家想看什么踊跃提出喔!

漫谈雪崩

标签:check   ==   没有   比例   攻击   失效   美的   本地   流控   

原文地址:http://www.cnblogs.com/gccbuaa/p/7260004.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!