提前锁票 InfoQ 最具价值感的视频栏目 | InfoQ 大咖说 了解详情
写点什么

一次假期故障引发的性能优化思考

2020 年 10 月 19 日

一次假期故障引发的性能优化思考

在假期某个夜黑风高的晚上,商家正在直播间如火如荼的做着直播,突然间屏幕卡顿,随后屏幕上出现大大的“404”,紧接着大量的客诉、告警扑面而来。好在有赞教育的技术团队响应及时,再经过很短时间的问题分析后,迅速的恢复了系统,保障了商家直播顺利进行。这故障到底是怎么产生的呢?经排查是因为在流量高峰时,系统在性能、可用性方面存在不足导致的。那当时你们是怎么处理的呢?接下来,我会重点从性能优化这块出发,先普及下性能优化的基本概念,然后再简述下常用的性能优化手段,最后给出这个故障我们当时的应对之道。


一、什么是性能优化

正如熵增定律描述的那样,在一个孤立的系统里,如果没有外力做工,其总混乱度(即熵)会不断增大,直至系统彻底变得无序。在软件服务领域亦是如此:从应用系统上线那一刻开始,随着用户量的增加、业务功能的持续迭代,系统会面临各种不同程度的挑战,如果不及时采取优化措施,我们会发现诸多问题,比如:系统怎么越来越慢了,流量一高系统就卡顿、甚至宕机等等。可以说,性能优化是贯穿在整个软件生命周期之中的。


1.1 性能衡量指标

在衡量系统性能基线时,一般会从接口“响应时间”和“并发能力”两个维度考虑。


(1)响应时间(RT)


所谓响应时间,是指完成某一功能所需要的时间,一般可以通过“平均响应时间”、“百分位数”等指标来考量。


  • 平均响应时间(AVG)


该指标反映的是接口的平均处理能力,计算方式是:将接口请求所有的响应时间叠加起来,然后除以总的请求次数。举个例子:接口 A 总共发起了 8 次请求,其中,有 1 次 3ms,3 次 5ms,4 次 6ms,那么此接口的平均响应时间就是 (1 * 3 + 3 * 5 + 4 * 6) / 8 = 5.25ms 。


  • 百分位数(Top Percentile)


一种统计学术语,反映的是超过 n%的请求都在 m 时间内返回,一般用 TPn=m 来描述,比如:TP99=5,表示超过 99%的请求都能在 5ms 内返回。它的计算方式是:将接口的响应时间按从小到大的顺序进行排列,取特定百分位的耗时,即为该接口的百分位数。举个例子:接口 A 总共发起了 100 次请求,响应时间依次是 1、2、3、…、100,那么,TP95 就是 95ms。


一般而言,百分位数更能反映接口的整体响应情况,因为在高并发场景中,常常会出现一些长尾请求,如果采用平均响应时间去衡量,由于长尾请求会被大量低 RT 平均掉(此时很多用户的请求已经很慢了),进而无法及时感知真实业务状况。举个例子:接口 A 有 100 次请求,其中 97 次 1ms,3 次 100ms,平均响应时间为 (1 * 97 + 3 * 100) / 100 = 3.97ms,此时 3.97ms 并不能真实反映接口的整体性能,因为其中 97 次请求 RT 才 1ms。


(2)并发能力


并发能力一般用 QPS 或 TPS 来衡量。QPS 指的是每秒请求数,TPS 是指每秒事务数。一般在做性能评估时,TPS 用的比较多。


1.2 性能优化本质

在算法领域,评价一个算法的效率如何,主要会看它的时间复杂度和空间复杂度情况。同理,如果将“响应时间”比作时间维度的话,“并发能力”可类比为空间维度。那么,在做性能优化时,本质上也是从“优化时间”、“优化空间”、“时空互换(用时间换空间或用空间换时间)”三个方向去思考,然后在空间、时间上不停地做取舍。


举一个生活中的例子来说明下。图 1-1 是一条长度为 5km 的道路,道路的限速是 50km/h,同时,规定在任何时刻,车道上有且仅有一辆汽车。那么,在 1h 内,从 A 点出发到达 B 点的汽车最多只有 10 辆。假设上级部门想提升这块路段的车流量,我们该怎么办?



图 1-1 单车道限速 50km/h


第一种方式,可以增加车道数(空间维度):将道路从单车道变为多车道,比如增加到 6 车道,那么,在 1h 内,从 A 点出发到达 B 点的汽车数可提量到 60 辆,见图 1-2。



图 1-2 六车道限速 50km/h


第二种方式,道路提速(时间维度):将道路限速从 50km/h 提升到 100km/h,那么,在 1h 内,从 A 点出发到达 B 点的汽车数可提量到 20 辆,见图 1-3。



图 1-3 单车道限速 100km/h


二、怎么做性能优化

2.1 系统性思考性能优化点

我们先来看下性能优化的落地过程,性能优化是由人来执行,然后服务于产品的,人和产品共同参与性能优化的落地,见图 2-1。



图 2-1 性能优化落地过程


从人维度出发,性能优化是属于技术团队的,技术团队包括开发、测试和运维,其中,运维负责提供一些监控数据,测试负责提供一些压测数据,开发基于压测、监控数据,明确具体的优化点以及优化手段;从产品维度出发,性能优化是业务功能的一部分,是为了满足某些业务场景。于是,在做性能优化时,一般会考虑以下几点:


(1)本次性能优化的业务场景是什么,有哪些场景需要优化;


(2)这些场景的运维监控数据、测试压测数据是什么,要优化哪里;


(3)这些数据里面反映的系统瓶颈在哪里,如何去优化;


(4)重复(2)、(3)过程,直至满足优化目标。


结合性能优化的本质,整个优化过程其实就是:先从业务需求角度出发,思考待优化场景是否值得投入,比如:一个任务每次需要跑半小时,从技术层面,可以做下优化,但结合业务情况却发现,此任务的执行频次是每周一次,如果优化此场景需要耗费较大人力,那么,这个投入就是不值得的;然后再从技术实现角度出发,不停地去思考怎么优化时间、怎么优化空间、怎么牺牲空间换时间、怎么牺牲时间换空间等问题。总之,我们在做性能优化时,需要以一个更全面的视角去看待它,避免进入头痛医头脚痛医脚的误区。


2.2 常见性能优化方式

在实际业务场景中,一个外部请求进入系统后,会先后经历多个软硬件节点,所有节点的处理时间加起来才是用户请求的处理时间,如果其中任意一个节点性能有问题,系统整体的性能就会上不去。而且,由于节点自身差异性,其性能提升的方法也会不一样,但总体概括起来,可以分为两大类:提升单个请求处理效率;并行处理多个请求。


2.2.1 提升单个请求处理效率

这种方式,简单来说,就是一个外部请求进来后,让其在尽可能短的时间内处理完成。常见的方法有以下几种:


(1)提升调用链上各节点的处理速度


从技术角度考虑:在数据库层面,可以考虑加索引、读写分离、分库分表等;在应用层层面,可以考虑加缓存(本地缓存,分布式缓存,或两者叠加)、复杂查询走 ES 索引;在代码编写时,可以考虑更高效的算法和数据结构,比如:读多写少用数组、写多读少用链表、取余采用位运算等。


从业务角度考虑:尽量避免重复查询;对于一些查询类操作,尽可能采用批量查询;上游调用方尽可能使用更合适的下游接口,比如:下游服务方有分别返回 A、B、AB 的三类接口,如果上游使用方仅需要 A 信息,应使用 A 接口;如果同时需要 AB 信息,应使用 AB 接口,而不是依次调用 A、B 接口,再在内存中做聚合。


(2)请求内部做并行化处理


这种思想,就是将单个请求拆分为多个子请求,各子请求并行处理,最后对子请求结果合并后返回。在实践中,我们基于 CompletableFuture 实现了一套并行处理框架,并成功运用到了商品详情页加载场景中。


(3)请求处理异步化


此思想,最典型的方法是采用消息队列,比如:下单操作时,除了扣减库存、生成订单外,还会给用户发送支付成功消息、赠送积分等后置操作。对于这些非核心的后置流程,可以采用消息队列做异步化处理,以此提升下单接口的性能。其他一些方法还有:在进程内,另开一个线程执行这些非核心流程;或者先将非核心操作数据暂存在某种介质(DB 表、redis 等)中,然后采用定时任务定期扫描并执行这些操作。


2.2.2 并行处理多个请求

字面意思来看,就是当有多个外部请求进来时,可以让系统内部多个节点分别处理这些请求,或者节点内部做并行处理。比如:节点采用集群部署,并通过负载均衡策略,将用户请求分摊到不同的节点进行处理;节点内部采用线程池,通过另开线程来实现。


三、我们是怎么做的

在具体讲述之前,先带大家一起熟悉下当时的业务场景:用户首先访问直播商品详情页,然后购买此商品,紧接着再次访问详情页面时,会出现直播间入口,在进入直播间之前,会做一次权限校验,校验通过后,方才可以进入直播间与讲师进行互动。详细的流程可见图 3-1 。



图 3-1 直播间进入流程


从上述流程中,可清晰看出,主要涉及到三种外部请求:查询直播商品详情;商品下单;进入直播间前做用户权限校验。当时通过流量监控数据以及日志分析发现,性能瓶颈主要在“直播商品详情加载”这一环节。


直播商详这块,主要是因为上游服务的请求量超过了下游服务能承受的吞吐量,导致大量 RPC 调用超时。具体反应的问题点有:


(1)依赖的部分非核心接口没有加缓存、做降级,导致整个请求失败;


(2)依赖的部分核心接口性能较差,导致后续请求一直被阻塞,直至超时异常返回;


(3)下游服务提供的查询接口比较重量级,但上游服务仅需要返参中的部分字段,导致单次查询 RT 一直下不去;


(4)上游调用方使用了错误的下游接口,比如上游调用方本来可以调用一次详细信息查询接口,便能获取所有需要的信息,可实际中,却先后调用了两种查信息的接口,才拿到完整的信息;


(5)无状态查询接口没有加缓存,导致了频繁的 RPC 调用。


针对上述这些问题点,我们当时主要从以下几点去做了优化:


优化前,我们重新梳理了整个调用链上,接口的强弱依赖关系,以及每个接口的 RT 情况


(1) 针对弱依赖接口,从超时时间、缓存策略、降级策略三个层面进行了优化


  • RPC 调用超时时间设置策略


统计出弱依赖接口 TP99(RT 较稳定的接口)/ TP95 (RT 波动较大接口)的 RT,设置它们的超时时间为 (1 + 50%) (TP99 或 TP95)


这里讲下为什么要这样设置超时时间:一般我们会设置超时时间为 2s 或 3s,但每个接口的 RT 是不一样的,比如:接口 A 的 RT 稳定在 100ms 内,那么,如果超时时间是 2s,假若接口 A 超时了,本次 RT 至少是 2s,但如果超时时间设置为 100ms,且我们加了 1 次重试,那么,本次请求的 RT 不会超过 200ms,同时,重试时接口很大概率会正常返回结果。


  • 缓存策略


给接口添加前置缓存。我们采用了公司自研的分布式缓存 zanKV,缓存的更新策略是:采用了两个缓存,缓存 A 和缓存 B(缓存 A 的失效时间为 m 分钟,缓存 B 为 n 分钟,且 n>2m),首先从缓存 A 读数据,有则直接返回,没有则从 B 读数据,并在返回之前,异步启动一个更新线程,同时更新缓存 A 和缓存 B。


  • 降级策略


接口接入熔断降级机制,并对异常做捕获,返回默认值。


(2)针对强依赖接口,从超时时间、重试策略、缓存策略三个层面做了优化


  • RPC 调用超时时间设置策略


统计出强依赖接口 TP99 的 RT,设置它们的超时时间为 (1 + 50%) (TP99)


  • 重试策略


根据接口 RT 波动性,基于 dubbo 的重试机制,设置重试次数为 2 或 3 次。


  • 缓存策略


对于商品基础信息,考虑到“缓存预热”、“热点访问”等问题,接入了公司 TMC(透明多级缓存),具体说明可见文档 https://mp.weixin.qq.com/s/BnWtbetNq076iRRZfnGRrw ;对于其他一些无状态查询信息,采用了本地缓存 Guava。


(3)商品详情信息聚合操作并行化


商品详情页面是一个聚合类信息展示窗口,它除了商品基础信息外,还包括 A、B、C 等内容(出于商业保密性,这里泛化内容名称),且这里的 A、B、C 和商品基础信息四者间是没有任何前后依赖关系的。当时我们将商品详情加载拆分为了 4 个子任务,并采用教育后端团队自研的并行处理框架,对子任务做了并行化处理,并聚合返回,较大提升了接口 RT 性能。


(4)查询类接口能力收拢,下游服务方提供稳定的原子化接口


在问题点(3)、(4)中有提到,上游调用方使用了下游不太合适的接口。由于历史原因,当前下游服务方中有特别多的查询类接口,且很多查询类接口在功能上都是重叠的。本次我们针对查询类接口,按照其返参字段使用场景的不同,提供了三种不同粒度的通用类原子化接口,之后所有的查询类需求,都会强制要求上游调用方从这三类接口中选择。这三类接口如下:


  • 粗粒度:返回最基本字段

  • 中粒度:返回经常使用的字段

  • 细粒度:返回详细信息


四、总结

产品功能是持续迭代的,性能优化也不是一蹴而就的事,大家在遇到性能问题时,可以参考本文提到的一些方法,做一些针对性的优化。同时,针对同一个节点,在不同的时刻,其优化点也可能不一样,比如:新功能刚上线时,查询性能的提升可能仅仅通过加索引的方式便能解决,但随着功能的不断叠加,后续的优化方向可能是“尽量走批量查询”、“加缓存”等方向。所以,性能优化还是要遵循“具体案例具体分析”这一基本原则。鉴于作者经验有限,我对性能优化的理解难免会有不足之处,欢迎大家共同探讨,共同提高。


本文转载自公众号有赞 coder(ID:youzan_coder)。


原文链接


一次假期故障引发的性能优化思考


2020 年 10 月 19 日 14:001434

评论 1 条评论

发布
用户头像
666 , TMC设计还是很牛的
2021 年 01 月 21 日 18:10
回复
没有更多了
发现更多内容

智能对话系统产品经理岗位拆解

元二

产品经理训练营第一课作业

Jobs

产品经理训练营

第一周作业

戎帅

产品训练营第一周作业

万顷湖天碧

产品经理训练营

产品经理训练营第一章作业-G20210639010157

苏格图德

产品经理训练营

作业:项目经理岗位备忘录

顾庆隆

项目经理

产品经理训练营-第一周作业

玖玖

极客大学产品经理训练营

产品经理第一课作业

产品经理训练营

产品经理训练营Week1作业

Mai

Worktile 王涛的经营之道:7 年牵引式破局 | TGO 科技领袖三十人

李忠良

28天写作

阿里技术官甩出百亿级并发系统设计高阶手册,原来这才叫高并发!

Java架构之路

Java 程序员 架构 面试 编程语言

终结代孕乱象,一场科技与黑产的赛跑

脑极体

产品岗位观察小结

庞玉坤

产品经理 产品经理训练营 极客大学认识产品经理 极客大学产品经理训练营

产品经理训练营第一周学习总结

月亮 😝

【作业-01】认识产品经理

西西里奇

产品经理 产品经理训练营

Alibaba开发十年,写出这本“MQ技术手册”,看完我愣住了

互联网架构师小马

Java MQ 消息中间件

产品经理岗位需求分布

jpcr987i

高情商与低情商:有效的沟通

北风

交流 情商

产品经理第0期训练营第一周作业提交

Krystal

第一章 总结

青葵

学习笔记2

超赞!肝完这份阿里微服务高阶笔记,我构建出了自己的“微”服务

Java架构之路

Java 程序员 架构 面试 编程语言

第一章 作业

青葵

学习笔记

极客产品训练营第一课作业

HaoJi

产品经理训练营

Dreamed Job?

顾远山

作业 极客大学产品经理训练营 Job Model

「年度总结」在字节跳动混了两年,然后呢?

Java架构师迁哥

产品岗位对比&自身岗位

novaln🍉

产品经理训练营第一章作业

黑小白白白

极客大学产品经理训练营

类别型特征

Tango

机器学习 日更挑战

认识产品经理

夏天的风

产品经理

认识产品经理-通过岗位模型了解自己的发展方向与空间20210119

WooBeyna

两行代码修复了解析MySQL8.x binlog错位的问题!!

冰河

MySQL 中间件 Binlog 二进制日志 数据异构

打造 VUCA 时代的 10 倍速 IT 团队

打造 VUCA 时代的 10 倍速 IT 团队

一次假期故障引发的性能优化思考-InfoQ