背景
网络抖动,依赖服务&中间件故障都会对我们的业务服务带来影响,严重时会直接导致产品功能不可用。作为业务研发,我们需要梳理错综复杂的强弱依赖关系,并针对性制定有效降级策略,使得产品功能在故障期间也可以以无损或部分有损状态继续提供服务。极端情况下,也可通过兜底页(或挂公告)来告知用户当前系统状态以及预期恢复时长,避免完全裸奔。
降级目标
整个业务链路中,部分依赖节点故障时产品功能尽量可用。
概念解释
- 无损降级:部分依赖节点故障时,产品功能完整性、用户体验与没有故障时表现一致
- 有损降级:部分依赖节点故障时,产品功能完整性、用户体验受到部分影响,但不至于完全不可用
- 弱依赖:依赖组件或服务不可用时,对整个业务核心逻辑或流程无影响。比如微博热搜标题后有一个热度值,获取热度值逻辑异常但不影响整个热搜列表的展示,则热度值是弱依赖,获取热度值对应的服务的对应接口是弱依赖(只是举个例子,实际底层数据结构可能不一样)
(热度值是自己yy的,不代表微博实际含义)
- 强依赖:依赖组件或服务不可用时,整个业务核心逻辑或流程失败。假设微博热搜列表是从数据库中获取的,如果数据库不可用,那么整个微博热搜列表数据就为空,这时数据库就是强依赖。
案例分析
一个简单的业务流程大致链路如下(以微博热搜列表为例):
用户通过浏览器(或APP)等方式访问产品功能承载页,前端页面调用后端服务接口请求数据渲染页面,最终呈现给用户,前端页面和后端业务服务形成依赖关系。同时后端业务服务又依赖一堆基础中间件和其他服务进行数据交互和处理,形成更为复杂的依赖关系。
在上述链路中,哪些依赖关系可能会出问题?以及出现问题使用用何种降级手段来降低损失?下面我们将逐一进行分析。
前端页面依赖后端接口降级处理策略
后端接口故障(如5xx、404、请求超时、连接中断)时会导致前端页面无法正确获取或变更数据,产品功能不可用。此时除了依赖后端服务恢复外,前端还可进行降级兜底处理,从而避免功能完全不可用。下面我们针对不同的业务场景,列举几种常见降级措施:
可接受短暂数据延迟的信息展示类业务,如微博热搜列表(数据实时性要求极高的业务如股票实时报价不适用)
降级方案1:本地数据快照,通过一定的频率或规则在本地缓存一份正常后端接口返回数据,当后端接口异常时,取本地缓存数据使用(也适用于存在用户差异化数据场景)
(后端接口正常时,通过一定采样策略缓存数据快照)
(后端接口异常时,通过本地数据快照获取数据)
该方案存在以下劣势:
- 冷启动没有缓存数据时,没有降级效果
- 用户清理浏览器缓存时,快照数据会丢失
- 大量业务数据快照缓存,占用用户存储空间
- 数据存在滞后性
降级方案2:远程数据快照,通过一定规则缓存一份数据到 OSS(云对象存储)中,后端接口异常时,前端通过读取OSS数据兜底
(后端通过一定采样策略缓存数据快照)
(后端接口异常时,前端通过快照数据获取)
劣势:不适用于存在用户差异化的场景
降级方案3:兜底页,如果无法通过快照数据恢复,重定向到一个友好的兜底页(或公告页)。如谷歌浏览遇到网络问题时,会提示用户去玩小游戏,虽然并没有解决问题,但能给用户一个很好的体验。
数据写入类场景,如发表评论(数据操作反馈敏感型场景不适用,如秒杀、创建订单)
降级方案4:延迟更新,请求更新失败接口时,前端执行重试策略,交互状态为更新中,最终根据重试结果刷新交互状态。该措施适用于不需要及时反馈且对数据一致性要求较低的场景,比如更换头像。
(注意接口超时场景是一个状态模糊的场景,前端无法确认后端接口逻辑是否执行成功,后端需要做好幂等处理)
降级方案5:表单数据缓存,复杂表单数据可以本地进行缓存,后端接口恢复后,用户重新填写表单时可以载入缓存,大幅提升用户体验。
(请求后端接口失败,表单数据缓存到本地)
(后端故障恢复,用户重新进入填写表单时,根据缓存自动回填,注意数据安全性问题)
服务端依赖降级策略
服务端降级的本质是让 暴露给前端的接口尽量不出现异常,后端异常的情况有很多种,一类是代码质量问题,比如 NPE、OOM、死循环等,这类编码问题可以通过代码审计、单元测试、CR 把关、接口自动化等方式来发现,也可通过编码规范约束以及最佳实践学习积累来避免。另一类是依赖服务或中间件(如 redis、es、mq 等)出现问题,则可以根据梳理出来的强弱依赖关系,进行针对性降级。
降级方案1:弱依赖组件或服务故障时接口返回值部分字段使用默认值替代(返回数据缺失部分无关紧要数据)
(业务服务接口正常时,使用业务服务返回值1000000填充hotNum字段)
(业务服务接口异常,使用默认值或空值填充hotNum字段,前端空值可以不展示)
降级方案2:弱依赖组件或服务可被剔除,该策略需要结合具体场景具体分析。
- cese1:如使用 redis 分布式锁来防并发或恶意流量,redis 故障时当作无锁化处理。该场景 redis 本质是弱依赖(防小人),如果因为 redis 故障导致拿锁失败,整个业务不可用得不偿失。如果整个业务逻辑仅通过该 redis 锁来唯一做幂等 block 逻辑,上面方案是否适用需要结合业务损失、数据恢复成本等综合考量。
- case2:如果 redis 是用来缓解 mysql 查询、更新压力,此时需要评估流量降级到 mysql 时数据库能否扛住压力。
降级方案3:重试策略,应对依赖中间件或服务瞬时抖动时有效。瞬时抖动的场景很多,比如云数据库升配切换、服务扩缩容(现在很多公司都做了平滑上下线,但无法完全避免)、网络波动等。重试策略需要注意以下几点:
- 写逻辑一定要支持幂等
- 合理评估重试次数和间隔时间
- 支持熔断(可以配合 sentinel)
- 推荐使用成熟框架(比如 spring retry)
降级方案4:本地数据快照,类似于前面提到的前端本地数据快照,后端也可采用。如鉴权服务,判断某个用户是否拥有某些功能的权限,依赖服务接口正常时使用SOA接口返回结果并 异步线程池更新缓存(尽量这个操作不要对主流程造成影响,更新失败就失败) ,异常时用缓存结果(正常一个用户前几分钟有权限,此刻大概率有权限 --- 局部性原理,且大部分微服务故障回复时长 mttr 相对较小)。但该方案也有如下缺陷:
- 对于数据高频变化场景或对数据实时性要求强一致场景不适用,比如股票交易实时报价
- 冷启动时(业务流量从低峰期到高峰期的过渡也是冷启动)该方案无效
- 引入了其他依赖(如 redis,但可以用内存缓存,需要关注内存占用),增加的架构复杂性
- 如果依赖服务和缓存组件同时故障,该方案无效(根据概率论中的乘法原则,两个同时失效的概率远低于其中一个失效的概率)
降级方案5:接口分级限流,优先保障高价值用户的可用性。在突发流量导致系统出现性能瓶颈时可采取该方式,需要对应接口感知用户登录态。
总结
任何有效降级措施都有落地成本(研发、维护),是否所有产品功能都需要降级?显然不是,我们应该优先保障核心链路的稳定性(是否核心链路需要与产品一起梳理),这块的 ROI 显然最高。其次在时间、成本满足条件下优化其他。如果降级措施涉及到产品功能交互变更、或者功能退化,一定要与交互、产品沟通对齐(有时研发对交互的理解不够全面)。
其次我们需要将降级意识(也可以理解为稳定性保障意识或者防御性编程)融入到每一行代码里,我们不相信用户的任何输入,也不要轻易相信队友提供的服务时刻可用,毕竟连云厂商都无法提供SLA100的产品。但也不要太过于极端,降级本身是一个边际价值递减的过程,前期可以用少量成本去得到一个高价值的回报,越往后投入越大价值越低。
本次分享偏向于经验总结,后面会再出一期实战篇(基于 sentinel 封装的业务降级工具类以及 pring retry 在业务代码中的应用),敬请期待!当然,作者水平有限,如果大家有其他的专业见解或对作者观点有不同意见,欢迎评论交流。
关注微信公众号可第一时间看到最新文章哟,搜索:码农丁丁 ,一起探索世界的本质!