稳定性之面向失败设计【过载保护】
在互联网架构中,高可用架构设计很重要的一个抓手就是过载保护,所谓的过载保护,是指当系统负载超过该系统或服务的承载能力时,系统会自动采取保护措施,确保自身不被压垮,从而提供有损服务,如果系统不能进行自我保护,很可能会导致雪崩现象。
下面通过12306网站的案例,详细解释下过载保护的重要性,12306初期开始网络订票时,每次春运抢票,12306网站都会卡顿,瘫痪,网站打不开等各种情况,原因是流量突发剧增,系统未做过载保护,才出现以上的情况,那么后来它是如何改进的呢,改进措施如下:
- 用户登录时添加图片验证,防止抢票软件自动登录
- 当用户请求比较频繁的时候,系统出现提示“您的操作频率过快请稍后重试”
- 当流量过大时,系统会提示“系统繁忙,请稍后重试”
当系统负载过高时,系统资源不够,不足以应对大量请求,即系统资源与访问量出现矛盾的时候,我们为了保证有限的资源能够正常服务,通常会采用三种措施来保护系统,分别是限流、降级和熔断。
限流
限流是通过对在单位时间内流量速率进行限制,保证请求流量在合理范围内,避免因为超出预期的大流量导致服务整体性能下降、响应缓慢或不可用。有点类似于,在帝都早上我们坐地铁一样,如果人多上不去,只能排队,等待下一趟地铁,系统也是如此。至于限流触发机制可以是提前预设阈值,负反馈或下游通知。
限流场景
- 限制总并发数(比如数据库连接池、线程池、服务并发数、某个接口)
- 限制瞬时并发数(如Nginx的limit_conn模块,用来限制瞬时并发连接数)
- 限制时间窗口内的平均速率(如Guava的RateLimiter、Nginx的limit_req模块,用来限制每秒的平均速率)
- 限制远程接口调用速率、限制MQ的消费速率等。
另外,还可以根据网络连接数、网络流量、CPU或内存负载等来限流。
限流阈值
可以通过压测的方式,获取系统能力的上限,一般情况下,压测是为了获得两个结果,如下:
第一是速率,表示单位时间内能够处理的请求数据量,例如xxx次请求/秒。
第二是并发数,表示同一时刻能够处理的最大并发数,例如xxx次并发。
值得考虑的是,还需要获取最大值、平均值、中位值,限流阈值是需要根据这些指标中得来的,另外一个是考虑的是被拒绝的流量该如何处理?是否直接丢弃?不能该作何处理,可参考:
- 拒绝服务(友好提示用户)
- 排队或等待(比如秒杀、评论、下单)重新发起
- 延迟处理(可以将处理不过来的请求,先放到池子中,不处理,后端程序从池子中依次取出,匀速消费处理,常见的可以用队列模式来实现)
- 特权处理(可以根据用户标签或者级别进行特权处理,例如VIP用户优先处理,访客用户延迟处理或者不处理)
限流模型
限流算法模型,基于时间模型、桶模型两种方式,分别是
第一是时间模型:固定时间窗口、滑动时间窗口
第二是桶模型:漏桶、令牌桶
业界比较常用的框架或者工具:比如 Redis 配合lua脚本使用、Sentinel、Guava中的Ratelimiter来实现控制速率
限流实践
限流实践是对上面几种限流模型的一些分析,包括应用场合,每一种模型都有独特的特点,只有对这几个模型有一定的了解之后,结合应用场合选择。
首先是固定时间窗口,粗暴的方式,一般来说,如非时间紧迫,不建议选择该方案,但是为了能够快速止损眼前的问题,也可以作为临时应急的方案。
其次是滑动时间窗口,相对友好些,该方案适用于对异常结果高容忍的场景,原因是相比两个窗口少了一个缓存区。
然后是漏桶,个人觉得该方案适合作为通用方案,漏桶算法不能够有效地使用网络资源,因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。但宽进演出的思路在系统保护的同时还留有一些余地,使得他的使用场景更广。
最后是令牌桶,当你需要尽可能的压榨程序的性能(此时桶的最大容量必然会大于等于程序的最大并发能力),并且所处的场景流量进入波动不是很大(不至于一瞬间取完令牌,压垮后端系统)。
降级
降级是将系统的所有功能服务进行一个分级,当系统出现问题,需要紧急降级时,可将不是那么重要的功能进行降级处理,停止服务,防止该问题,影响主流程的稳定性,另一方面降级是为了主流程提供更多资源,让有限的资源发挥更大的价值,从而将非核心的功能进行降级,停止服务,这样可以释放出更多的资源供给核心功能的去用。
例如某电商平台中,在双十一促销活动中,可临时将商品评论、积分、退款等非核心功能进行降级,停止这些服务,释放出机器和CPU等资源来保障用户正常下单,而这些降级的功能服务可以等整个系统恢复正常后,再来启动,进行补单/补偿处理。除了功能降级以外,还可以采用不直接操作数据库,而全部读缓存、写缓存的方式作为临时降级方案。
可降级的点
在一个系统中可以降级的点,可以分为以牺牲用户体验、牺牲功能完整性、牺牲时效性这三大类,主流程或核心功能点是不能够降级的,所有的降级都是为主流程或核心功能点提供更多资源,或者为了保证主流程的稳定性。
牺牲用户体验。
第一以牺牲用户体验降级,来保证稳定性,例如:
- 评价列表禁止10页之后的翻页
- 使用通用内容代替个性化推荐内容
- 或是提供降级友好界面提示用户,该功能因什么原因,预计什么时间恢复之类
第二以牺牲功能完整性,来保证稳定性,例如:
- 适当关闭风控的行为,通过裸奔的形式,来节约资源
- 适当关闭条件校验,如:积分商品添加到购物车时判断积分够不够
第三以牺牲时效性,来保证稳定性,例如:
- 消息通知这类业务延迟处理
- 优惠补贴这类业务延迟处理
- 库存、售卖数量、这类调整静态文案展示
降级前置条件
降级前置条件是首先对我们的业务功能,进行分级,确定每个功能的「重要程度」,它决定了在什么情况下可以抛弃它以保证剩下的功能可用,有点类似于给日志定义级别一样,比如我们可以定义P0~5 五个级别,P0的级别最高,要拼死保护,P5的级别最低最先可以被降级掉。
对业务功能分级之后,针对不同级别的业务功能,进行上下游依赖分析,明确上下游依赖关系、SLA,降级方案设计包括降级开关、降级策略、降级之后上下游依赖容错处理机制。
降级策略
降级策略可以是人工、自动降级两种方式,自动降级实现起来相对比较复杂,例如平均响应时间、异常数、异常比例等多种方式,这种方式需要实时对降级指标计算与降级规则匹配,另外一种方式人工降级,相对容易,通过开关设计即可,当需要时打开开关,不需要时关闭开关即可。
降级原则
- 明确降级成本收益比,选择收益最高的的方案
- 降级方案需要定期Review,确保方案是最新的,随着业务变化
- 程序所依赖的下游程序的级别不能低于该程序的级别
- 弱依赖降级、事件解耦
降级案例分析
案例一、写服务降级、降级思路:同步写转异步写,例如扣减库存的操作,正常情况下的设计一般是:
方案1:数据库中扣减,成功后更新 Redis 缓存。
方案2:先扣减 Redis 缓存,同步扣减数据库,如果失败则回滚 Redis 缓存。
当数据库性能跟不上时,就需要采用异步方式了,先扣减 Redis 缓存,同时向队列中发送一条扣减数据库库存的消息,异步进行数据库扣减,实现最终一致性。
案例二、读服务降级、降级思路:缓存、静态化 ,从读取数据的角度考虑降级,读取缓存,例如商品详情页,其中有非常多的内容,比如商家信息、推荐信息、配送至信息、相关分类、热销榜等等,这些都不是核心数据,所以在出现异常时可以进行降级处理,还可将整个页面切换为静态化,最大程度的降级读服务。
案例三、默认数据、降级思路:默认值、或临近缓存数据、比如远程服务挂掉了,就自动降级,可以使用默认值、提前准备的内容、之前的缓存数据。
熔断
熔断机制这个词对你来说肯定不陌生,它的灵感来源于我们电闸上的“保险丝”,当电压有问题时(比如短路),自动跳闸,此时电路就会断开,我们的电器就会受到保护。不然,会导致电器被烧坏,如果人没在家或是人在熟睡中,还会导致火灾。所以,在电路世界通常都会有这样的自我保护装置。
在分布式系统设计中,熔断指的是在发起服务调用的时候,如果被调用方返回的错误率超过一定的阈值或触发某些特定策略,那么后续的请求将不会真正发起请求,而是在调用方直接返回错误或者兜底策略。一句话总结就是发现下游不能正常提供服务,不再对下游进行调用,而是主动直接返回错误或者兜底策略。
为什么要熔断
假定服务A依赖服务B,当服务B处于正常状态,整个调用是健康的,服务A可以得到服务B的正常响应。当服务B出现故障时,比如响应缓慢或者响应超时,如果服务A继续请求服务B,那么服务A的响应时间也会增加,进而导致服务A响应缓慢。如果服务A不进行熔断处理,服务B的故障会传导至服务A,最终导致服务A也不可用。
熔断三种状态
熔断器模式就是像是哪些容易导致错误操作的一种代理,代理能够记录最近调用发生错误的次出手,然后决定使用允许操作继续,或者立即返回错误,熔断器也能够诊断错误是否已经修正,如果已经修复,应用程序会再次尝试调用操作,因此熔断状态有关闭,打开,半开三种状态。
关闭状态通俗说是“识别系统不可用状态策略”,在这种情况下默认是关闭的,此时不做任何拦截,对全部流量进行放行,该状态下,计算并统计不正常状态的策略,一旦到达不可用状态,则进行打开状态,对所有的流量进行拦截,使用预案或者兜底机制。
半开状态通俗说是“尝试恢复正常”,在这种情况是一个中间状态,尝试从打开状态到关闭的状态的中间态,一般来说会放一些请求,具体多少流量是可以进行案例进行配置,尝试探测,在一定的时间窗口内,符合预设规则,如果满足可用状态,则状态设置成关闭状态,对全部流量进行放行。
打开状态通俗说是“识别系统是可用的状态策略”,在这种情况对所有流量进行拦截切断,影响和损失肯定还是有的,所以还是需要尽快恢复回来,以提供完整的服务能力,在这这种情况下,对部分流量流量进行下放,进行探测,故状态转移到半开状态。
熔断策略
熔断策略,是定义出可以判断服务可用或者不可用的指标,该指标判断形式,可以是阈值或者百分比,例如:
- 在10秒内出现100次“无法连接”或者出现100次大于5秒的请求。
- 在10秒内有30%请求“无法连接”或者30%的请求大于5秒。
常见的策略模式
- 秒级 RT 模式:若持续进入 5 个请求,它们资源的平均响应时间都超过阈值(秒级平均 RT,以 ms 为单位),资源调用会被熔断。
- 秒级异常比例模式:当资源的每秒异常数占通过量的比值超过阈值之后,资源进入降级状态,即在接下的降级时间窗口(在降级规则中配置,以 s 为单位))之内,对这个方法的调用都会自动地返回。
- 分钟级异常数模式:当资源最近 1 分钟的异常数目超过阈值之后会进行熔断。
熔断策略,一般情况下还需要增加人为降级开关,便于紧急情况下使用,优先级要高于规则之上。
熔断原则
熔断很重要的一点是要区分对待,原因是一般服务都是集群模式,要确认单节点不可用还是整个服务集群都不可用。另外一点是熔断是大多数情况是被动,熔断往往是最后的选择,一旦熔断可能代表整个服务集群都不用,那么损失是可想而知,在架构设计中尽可能是选择限流或者降级的方案,因为“部分胜于无”,虽然无法提供完整的服务,但是还可以提供有损服务,尽可能往降低影响的方向去努力。
服务雪崩
服务雪崩是多个服务之间调用的时候,假设服务A调用服务B和服务C,服务B和服务C有调用其他的微服务,这就是所谓的”扇出”,如扇出的链路上某个微服务的调用响应式过长或者不可用,对服务A的调用就会占用越来越多的系统资源,进而引起系统雪崩,所谓的”雪崩效应”,熔断机制是应对雪崩效应的一种服务链路保护机制,
当扇出链路的某个服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回”错误”的响应信息。
限流、降级、熔断概述
不同点
- 触发时机不同,服务熔断一般是下游某个服务不可用而引起的,而服务降级则一般是从整体负荷考虑。
- 行为不同,熔断一般情况被动的,处于无奈的,降级一般是可主动或者被动,具备更多主动性。
- 管理目标不同,熔断是每个微服务都需要的,是一个框架级的处理;而服务降级一般是关注业务,对业务进行考虑,抓住业务的层级,从而决定在哪一层上进行处理:比如在IO层,业务逻辑层,还是在外围进行处理。
- 限流是对外部流量的行为,服务端根据其自身能力设置的一个过载保护,目标是对外。
- 熔断是对内的流量的行为,是调用端对自身的一个降级保护,目标是对内。
- 熔断和限流都可以认为是降级的一种方式。
共同点
- 都是从可用性、可靠性出发,提高系统的容错能力。
- 因不可控因素,使某一些应用不可达或不可用,来保证整体系统稳定。
- 对其自治性要求很高。都要求具有较高的自动处理机制。
解决方案
业界流行的解决方案分别是Sentinel、Hystrix,两种解决方案各有各的优势,个人觉得Sentinel更优秀,功能丰富,有更完善的控制台,下面是两者区别:
# | Sentinel | Hystrix |
隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于响应时间或失败比率 | 基于失败比率 |
实时指标实现 | 滑动窗口 | 滑动窗口(基于 RxJava) |
规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 不支持 |
流量整形 | 支持慢启动、匀速器模式 | 不支持 |
系统负载保护 | 支持 | 不支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC | Servlet、Spring Cloud Netflix |
总结
总体来看,限流偏向于管理入口流量,熔断偏向于保护下游服务,降级是为了在系统触发负载保护时,尽可能提供弹性服务,一般来说,熔断比限流严重,而限流比降级严重,目标都是从可用性、可靠性出发,提高系统的容错能力和稳定性。