最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

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

  • 2020-10-19
  • 本文字数:5015 字

    阅读完需:约 16 分钟

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

在假期某个夜黑风高的晚上,商家正在直播间如火如荼的做着直播,突然间屏幕卡顿,随后屏幕上出现大大的“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:002067

评论 1 条评论

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

推荐一个GitHub的开源电子书仓库,值得收藏

C语言与CPP编程

Java c++ Python C语言 电子书

品牌认同感与鄙视链

ES_her0

5月日更

打破思维定式(四)

Changing Lin

5月日更

架构实战营模块 3 作业

阿体

架构实战营 模块三作业

Dylan

架构实战营

架构实战营模块三作业

hunk

架构实战营

消息队列的架构设计文档

Geek_bded54

架构实战营 模块3作业

夏日

架构训练营

模块三 - 消息队列架构设计文档

华仔架构训练营

消息队列设计文档

青春不可负,生活不可欺

ARTS - week 9

steve_lee

架构实战营 模块三作业

fazinter

架构实战营

架构训练营模块三作业

Geek_e0c25c

架构训练营

架构实战营 模块3 作业

CR

学妹问我:OpenJDK是什么?作为师哥,必须万字详解屁颠屁颠奉上

牛哄哄的java大师

Java

智能家居控制原理

lenka

5月日更

Go 语言垃圾回收

escray

学习 极客时间 Go 语言 5月日更

Ansible Inventory

耳东@Erdong

ansible 5月日更

腾讯T3大佬亲自讲解!我的腾讯安卓面试经历分享,分分钟搞定!

欢喜学安卓

android 程序员 面试 移动开发

模块三:课后作业

黄先生

架构实战营

WebRTC下摄像头的采集分析

小辣条

ios WebRTC

音频变速变调-sonic源码分析

floer rivor

音视频

五月学习心得(二)

攻城先森

学习 音视频 5月日更

聊聊传统企业如何做好数字化转型

长沙造纸农

中台 数字化转型 企业 数字化 中台战略

模块三作业

梦寐凯旋

架构实战营

Dubbo 泛化引用

青年IT男

dubbo

已收藏!2021年最新腾讯Android面经,超详细

欢喜学安卓

android 程序员 面试 移动开发

架构训练营模块3作业-消息队列架构设计

最好的关系,是成就彼此

小天同学

爱情 日常感悟 个人思考 5月日更 相处之道

架构实战营 模块三:课后作业

Ahu

#架构实战营

模块3作业-架构设计文档

yu

架构实战营

一次假期故障引发的性能优化思考_软件工程_程英杰_InfoQ精选文章