开发高并发系统时保护系统的三大利器:缓存、降级、限流。之前有文章介绍过缓存和限流。本文将详细讨论降级。当访问量激增,服务出现问题(如响应慢或无响应),或非核心服务影响核心进程性能时,仍然需要保证服务仍然可用,即使如果是损坏的服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现手动降级。本文将介绍笔者在实际工作中遇到或看到的一些降级解决方案,供大家参考。降级的最终目标是保持核心服务可用,即使有损。并且有些服务是不能降级的(比如加入购物车,结账)。降级方案降级前,需要对系统进行梳理,看系统能否保持安全;从而分清哪些必须死守,哪些可以降级;例如,可以参考日志级别设置方案:一般:比如某些服务偶尔由于网络波动或者服务在线超时,可以自动降级;警告:部分服务在一段时间内成功率有波动(如95~100%之间),可自动降级或手动降级,并发出告警;错误:比如可用率低于90%,或者数据库连接池炸毁,或者流量突然飙升到系统可以承受的最大阈值。这时候可以根据情况自动降级或者手动降级;严重错误:比如由于特殊原因导致数据错误,此时需要紧急手动降级。降级根据是否自动分为自动开关降级和手动开关降级。降级按功能分为:读服务降级和写服务降级。降级可分为:按系统级别进行多级降级。降级的功能主要是从服务端链接考虑,即根据用户访问的服务调用链接来梳理需要降级的地方:占用一些稀缺的服务资源,在紧急情况下可以整体降级,让小兵保持帅气;降级页面碎片:比如商品详情页的商户部分因为数据错误需要降级;页面异步请求降级:比如商品详情页面有推荐信息/配送至等异步加载请求。如果这些信息响应慢或者后台服务有问题,可以降级;服务功能降级:比如在渲染商品详情页时需要调用一些不太重要的服务:相关分类、畅销榜等,这些服务在异常情况下不会直接获取,即可以降级;读降级:比如多级缓存模式,如果后端服务出现问题,可以降级为只读缓存,适用于对读一致性要求不高的场景;写降级:比如闪购,我们只能更新Cache,然后异步和同步扣库存到DB,保证最终一致性。这时候DB就可以降级为Cache了。爬虫降级:在大促期间,可以将爬虫流量导向静态页面或返回空数据,降级保护后端稀缺资源。自动切换降级自动降级是根据系统负载、资源使用、SLA等指标进行的。超时降级当访问的数据库/http服务/远程调用响应慢或响应时间长,且该服务不是核心服务时,可超时自动降级;例如,商品详情页有推荐内容/评论,但推荐内容/评论暂时不展示,不会对用户的购物过程产生很大影响;对于这种服务,可以加班降级。如果是调用别人的远程服务,和对方定义一个服务响应时间,如果超时,会自动降级。一些文章《使用httpclient必须知道的参数设置及代码写法、存在的风险》和《dbcp配置及jdbc超时设置总结》之前总结过。在实际场景中,需要配置超时时间、超时重试次数和机制。统计降级失败次数有时会依赖一些不稳定的API,比如调用外部工单服务。当调用失败次数达到一定阈值时,会自动降级;然后使用异步线程检测服务是否恢复,然后取消降级。故障降级比如被调用的远程服务挂了(网络故障、DNS故障、http服务返回错误状态码、rpc服务抛出异常),可以直接降级。降级方案包括:默认值(比如库存服务宕机,返回默认点),口袋数据(比如广告宕机,返回一些事先准备好的静态页面),缓存(一些之前暂存的缓存数据)。限流降级当我们去秒杀或者抢购一些受限商品时,这时候可能会因为访问过多导致系统崩溃。这时候开发者会使用限流来限制访问量。当达到限流阈值时,后续请求将被降级;降级后的处理方案可以是:排队页面(将用户引导到排队页面,稍等片刻重试),缺货(直接告知用户缺货),错误页面(如果活动太火爆),稍后再试)。手动切换降级大促期间,通过监控发现部分线上服务出现问题。这时需要暂时移除这些服务;有时通过任务系统调用一些服务,但是服务依赖的数据库可能存在:网卡开满,挂掉,或者有很多慢查询,需要暂停任务系统让服务器处理它;而如果突然发现调用量过大,可能需要改变处理方式(比如同步转异步);这时,你可以使用一个开关来降级。开关可以存储在配置文件、数据库和Redis/ZooKeeper中;如果不保存在本地,可以周期性同步交换机数据(比如每秒一次)。然后通过判断一个KEY的值来决定是否降级。另外,对于新开发的服务,想要上线灰度测试;但您不确定服务的逻辑是否正确。这时候就需要设置开关了。当新服务出现问题时,可以通过交换机切换回旧服务。还有多机房服务。如果某个机房宕机,需要将一个机房的服务切换到另一个机房。这时候也可以通过一个switch来完成切换。还有一些功能因为功能问题需要暂时封禁。比如产品规格参数数据有问题,回滚无法解决数据问题。这时需要开关控制降级。读服务降级读服务降级一般采用的策略有:暂时切换读(降级为读缓存,降级为静态),暂时屏蔽读(阻塞读入口,阻塞某一个读服务)。《应用多级缓存模式支撑海量读服务》中引入了读服务,即接入层缓存-->应用层本地缓存-->分布式缓存-->RPC服务/DB,我们会在接入层和应用层设置开关,当分布式cache,RPC服务/DB出现问题自动降级为不调用。当然,这种情况适用于对读一致性要求不高的场景。页面降级、页面分片降级、页面异步请求降级都是读服务降级,目的是为了挽救棋子(比如因为这些服务也占用核心资源,或者占用带宽影响核心服务)或者暂时阻塞由于数据问题。还有一种静态页面场景:动态降为静态:比如网站平时可以动态渲染商品详情页,但是当大促来临的时候,可以切换为静态,减少核心资源的占用。并且可以提高性能;其他如列表页、首页、频道页等都可以这样播放;静态页面可以通过程序定时推送到缓存或者生成到磁盘,出现问题直接切掉;静态降级为动态:比如在使用静态实现商品详情页的结构时,通常使用静态来提供服务,但是由于特殊原因,静态页面出现了问题,需要暂时切换回动态以确保服务的正确性。以上均保证问题有预案,用户仍可正常使用网站,不影响用户购物。写服务降级大多数场景下写服务是不能降级的,但是可以采用一些迂回的策略来解决问题。比如将同步操作转换为异步操作,或者限制写入的数量/比例。比如扣除库存,通常是这样进行的:方案一:1,扣除DB库存,2,扣除成功后,更新Redis中的库存;方案二:1、扣除Redis库存,2、同步扣除DB库存,如果扣除失败,则回滚Redis库存;前两个方案非常依赖于数据库。如果此时DB性能跟不上,就会出现库存扣除的问题;所以我们可以想到方案三:1.Redis库存扣减,2.正常同步扣DB库存,降级为在性能无法承受的情况下发送扣DB库存消息,再异步扣DB库存,实现最终一致性;这种发送扣减数据库库存消息的方法也可能成为瓶颈;这种情况下,可以考虑方案4:1、Redis库存扣减,2、DB库存正常同步扣减。当性能无法承受时,降级为将扣除DBinventory消息写入本机,由本机异步进行DBinventory。减去以实现最终一致性。也就是说,正常情况下,可以同步扣库存,性能受不了的时候可以降级为异步;另外,如果是秒杀场景,可以直接降级为异步,保护系统。还有下面的订单操作,大促时可以暂时降级将订单数据写入Redis,过了峰值后再同步回DB。当然还有更好的解决方案,但是比较复杂,不是本文的重点。另一个例子是用户评价。如果评估量过大,也可以将评估从同步写入降级为异步写入。当然评价按钮也可以按比例打开(比如有些人看不到评价操作按钮)。例如,评估成功后,会发放一些奖励,必要时,会从同步降级为异步。多级降级缓存离用户最近,效率更高;而降级就是离用户越近,对系统的保护就越好。因为业务复杂,QPS/TPS越低越到后端。页面JS降级开关:主要控制页面功能的降级,通过页面JS脚本部署功能降级开关,并在适当的时候打开/关闭开关;接入层降级开关:主要控制请求入口的降级,请求进入后会优先进入接入层,可以在接入层配置功能降级开关,可以根据需要进行自动/手动降级实际情况;这个可以参考《京东商品详情页服务闭环实践》,尤其是当后端应用服务出现问题时,应用服务可以通过接入层降级,有足够的时间恢复服务;应用层降级开关:主要控制业务的降级,在应用中配置相应的功能开关,根据实际业务情况进行自动/手动降级。【本文为专栏作者张凯涛原创文章,作者微信公众号:凯涛的博客(kaitao-1234567)】
