在开发高并发系统时,有很多手段来保护系统,如缓存、降级和限流等。
当访问量剧增、服务出现问题(如响应时间长或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。
系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。降级也需要根据系统的吞吐量、响应时间、可用率等条件进行手工降级或自动降级。
降级预案
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅,从而梳理出哪些必须誓死保护,哪些可降级。比如,可以参考日志级别设置预案。
- 一般:比如,有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级。
- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警。
- 错误:比如,可用率低于90%,或者数据库连接池用完了,或者访问量突然猛增到系统能承受的最大阈值,此时,可以根据情况自动降级或者人工降级。
- 严重错误:比如,因为特殊原因数据出现错误,此时,需要紧急人工降级。
降级按照是否自动化可分为:自动开关降级和人工开关降级。
降级按照功能可分为:读服务降级和写服务降级。
降级按照处于的系统层次可分为:多级降级。
降级的功能点主要从服务器端链路考虑,即根据用户访问的服务调用链路来梳理哪里需要降级。
- 页面降级:在大促或者某些特殊情况下,某些页面占用了一些稀缺服务资源,在紧急情况下可以对其整个降级,以达到丢卒保帅的目的。
- 页面片段降级:比如,商品详情页中的商家部分因为数据错误,此时,需要对其进行降级。
- 页面异步请求降级:比如,商品详情页上有推荐信息/配送至等异步加载的请求,如果这些信息响应慢或者后端服务有问题,则可以进行降级。
- 服务功能降级:比如,渲染商品详情页时,需要调用一些不太重要的服务(相关分类、热销榜等),而这些服务在异常情况下直接不获取,即降级即可。
- 读降级:比如,多级缓存模式,如果后端服务有问题,则可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。
- 写降级:比如,秒杀抢购,我们可以只进行Cache的更新,然后异步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。
- 爬虫降级:在大促活动时,可以将爬虫流量导向静态页或者返回空数据,从而保护后端稀缺资源。
- 风控降级:如抢购/秒杀等业务,完全可以识别机器人、用户画像或者根据用户风控级别进行降级处理,直接拒绝高风险用户。
自动开关降级
自动降级是根据系统负载、资源使用情况、SLA等指标进行降级。
超时降级
当访问的数据库/HTTP服务/远程调用响应慢或者长时间响应慢,且该服务不是核心服务的话,可以在超时后自动降级。
比如,商品详情页上有推荐内容/评价,但是,推荐内容/评价暂时不展示,对用户购物流程不会产生很大影响。
对于这种服务是可以超时降级的。如果是调用别人的远程服务,则可以和对方定义一个服务响应最大时间,如果超时了,则自动降级。
在实际场景中一定要配置好超时时间和超时重试次数及机制。
统计失败次数降级
有时依赖一些不稳定的API,比如,调用外部机票服务,当失败调用次数达到一定阈值自动降级(熔断器)。然后通过异步线程去探测服务是否恢复了,恢复则取消降级。
故障降级
比如,要调用的远程服务挂掉了(网络故障、DNS故障、HTTP服务返回错误的状态码、RPC服务抛出异常),则可以直接降级。降级后的处理方案有:默认值(比如库存服务挂了,返回默认现货)、兜底数据(比如广告挂了,返回提前准备好的一些静态页面)、缓存(之前暂存的一些缓存数据)。
限流降级
当我们去秒杀或者抢购一些限购商品时,可能会因为访问量太大而导致系统崩溃,此时,开发者会使用限流来限制访问量,当达到限流阈值时,后续请求会被降级。
降级后的处理方案可以是:排队页面(将用户导流到排队页面等一会儿重试)、无货(直接告知用户没货了)、错误页(如活动太火爆了,稍后重试)。
人工开关降级
在大促期间通过监控发现线上的一些服务存在问题,这个时候需要暂时将这些服务摘掉。还有,有时通过任务系统调用一些服务,但是,服务依赖的数据库可能存在:网卡打满了、数据库挂掉了或者很多慢查询,此时,需要暂停任务系统让服务方进行处理。
还有发现突然调用量太大,可能需要改变处理方式(比如同步转换为异步)。此时可以使用开关来完成降级。开关可以存放到配置文件、数据库、Redis/ZooKeeper。如果不是存放在本地,则可以定期同步开关数据(比如1秒同步一次)。然后,通过判断某个key的值来决定是否降级。
另外,对于新开发的服务如果想上线进行灰度测试,但是,不太确定该服务的逻辑是否正确,此时,就需要设置开关,当新服务有问题时可以通过开关切换回老服务。还有多机房服务,如果某个机房挂掉了,则需要将一个机房的服务切到另一个机房,此时,也可以通过开关完成切换。
还有一些是因为功能问题需要暂时屏蔽掉某些功能,比如,商品规格参数数据有问题,数据问题不能用回滚解决,此时需要开关控制降级。
读服务降级
对于读服务降级一般采用的策略有:暂时切换读(降级到读缓存、降级到走静态化)、暂时屏蔽读(屏蔽读入口、屏蔽某个读服务)。
读服务,即接入层缓存→应用层本地缓存→分布式缓存→RPC服务/DB
我们会在接入层、应用层设置开关,当分布式缓存、RPC服务/DB有问题时自动降级为不调用。当然,这种情况适用于对读一致性要求不高的场景。
页面降级、页面片段降级、页面异步请求降级都是读服务降级,目的是丢卒保帅(比如,因为这些服务也要使用核心资源,或者占了带宽影响到核心服务),或者因数据问题暂时屏蔽。
还有一种是页面静态化场景。
动态化降级为静态化:比如,平时网站可以走动态化渲染商品详情页,但是,到了大促来临之际可以将其切换为静态化来减少对核心资源的占用,而且可以提升性能。其他还有如列表页、首页、频道页都可以这么处理。
可以通过一个程序定期推送静态页到缓存或者生成到磁盘,出问题时直接切过去。
静态化降级为动态化:比如,当使用静态化来实现商品详情页架构时,平时使用静态化来提供服务,但是,因为特殊原因静态化页面有问题了,需要暂时切换回动态化来保证服务正确性。
以上都保证了出问题时有预案,用户可以继续使用网站,不影响用户购物。
写服务降级
写服务在大多数场景下是不可降级的,不过,可以通过一些迂回战术来解决问题。比如,将同步操作转换为异步操作,或者限制写的量/比例。
比如,扣减库存一般这样操作。
- 方案1:扣减DB库存,扣减成功后,更新Redis中的库存。
- 方案2:扣减Redis库存,同步扣减DB库存,如果扣减失败,则回滚Redis库存。
- 方案3:扣减Redis库存,正常同步扣减DB库存,性能扛不住时,降级为发送一条扣减DB库存的消息,然后异步进行DB库存扣减实现最终一致即可。
- 方案4:扣减Redis库存,正常同步扣减DB库存,性能扛不住时降级为写扣减DB库存消息到本机,然后本机通过异步进行DB库存扣减来实现最终一致性。
前两种方案非常依赖DB,假设此时DB性能跟不上,则扣减库存就会遇到问题。
方案3发送扣减DB库存消息也可能成为瓶颈。
方案4正常情况下可以同步扣减库存,在性能扛不住时,降级为异步
如果是秒杀场景可以直接降级为异步,从而保护系统。
还有,如下单操作可以在大促时暂时降级,将下单数据写入Redis,然后等峰值过去了再同步回DB。
还有如用户评价,如果评价量太大,那么也可以把评价从同步写降级为异步写。当然也可以对评价按钮进行按比例开放(比如,一些人看不到评价操作按钮)。比如,评价成功后会发一些奖励,在必要的时候降级同步到异步。
多级降级
缓存是离用户越近越高效,而降级是离用户越近越对系统保护得好。因为业务的复杂性导致越到后端QPS/TPS越低。
- 页面JS降级开关:主要控制页面功能的降级,在页面中,通过JS脚本部署功能降级开关,在适当时机开启/关闭开关。
- 接入层降级开关:主要控制请求入口的降级,请求进入后,会首先进入接入层,在接入层可以配置功能降级开关,可以根据实际情况进行自动/人工降级。通过接入层降级从而给应用服务有足够的时间恢复服务。
- 应用层降级开关:主要控制业务的降级,在应用中配置相应的功能开关,根据实际业务情况进行自动/人工降级。
在下图的订单履约工作流中,整个工作流可以进行多级降级。
- 如果恶意订单校验出现不可用的情况,则可以降级,不再同步进行恶意校验,可以直接绕过,也可以改成异步。
- 如果订单计划出现性能下降,但还可以处理,则在这里优先处理高优级订单、处理逻辑较简单的数据(例单品单件)。
- 分发订单时,如果仓库负载饱和,则可以降低向京东库房的输送量,增大其他目标地的输送量。
在工作流中的每一个流程中都可以进行相应的降级:优先处理高优先级数据、只处理某些特征的数据、合理分配流量到最需要的场合。
配置中心
我们需要通过配置方式来动态开启/关闭降级开关,在应用时,首先要封装一套应用层API方便业务逻辑使用。
对于开关数据的存储,如果涉及的服务器/系统较少,则初期可以考虑使用配置文件进行配置。
如果涉及的服务器/系统较多,则应该使用配置中心进行配置。实现时要做到不需要修改代码,不需要重启应用即可动态配置开关。
使用配置中心实现开关配置
使用统一配置中心,或者叫分布式配置中心,目的是实现配置开关的集中管理,要有配置后台方便开关的配置,对于一般公司来说配置中心的维护要简单,不需要投入过多的人力来做这件事情。
配置中心不管是采用拉取模式还是推送模式,要考虑到连接数和网络带宽可能带来的风险和问题。
目前有一些开源方案可以选择,如ZooKeeper、Diamond、Disconf、Eted 3、Consul。
本文选择使用Consul,其支持多数据中心、服务发现、KV存储等特性,而且使用简单,提供了简单的Web UI方便管理,更多介绍可以参考Nginx负载均衡部分。我们借助Consul的KV存储特性来实现配置管理。
使用Hystrix实现降级
通过配置中心可以进行人工降级,而我们也需要根据服务的超时时间进行自动降级。
使用Hystrix实现熔断
Hystrix提供了熔断实现,熔断后会自动降级处理。
- 闭合(Closed):如果配置了熔断开关强制闭合,或者当前请求失败率没有超过失败率阈值,则熔断开关处于闭合状态,不启动熔断机制,即不进行降级处理。
- 打开(Open):如果配置了熔断开关强制打开,或者当前失败率超过失败率阈值,则熔断开关打开,启动熔断机制,根据配置调用降级处理方法getFallback进行降级处理。
- 半打开(Half-Open):当熔断处于打开状态后,不能一直熔断下去,需要在一个时间窗口后进行重试,这种状态就是半打开。Hystrix允许在circuitBreakerSleepWindowInMilliseconds窗口内进行一次重试,重试成功则闭合熔断开关,否则熔断开关还是处于打开状态。