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