找到了性能瓶颈,然后呢?

开发
性能问题根据场景不同而千变万化,不同场景下其对应的性能优化模式不同,付出的代价也不同,其归根结底还是“看碟下菜”四字。

作者 | 田博文

前言

本文直接从性能优化开始谈起,并非意味着寻找性能瓶颈无关紧要,性能优化一般都存在于发现性能瓶颈之后。找到性能瓶颈自然是优化的第一步,毕竟所谓有的放矢。我们今天主要讨论的是找到了性能问题之后,到底该怎么办?

为什么要进行性能优化?

相信大家对这些句子都不陌生:“通过解决了XX提升了30%吞吐量”、“优化了XXX后降低了40%的延迟”等等,可能会有人觉得这么大的性能提升是不是在开玩笑。其实不然,性能优化的效果在早期是相当显著的,我们先从一个简单的小例子一窥端倪:

小寅是一位Java开发程序员,他今天的工作是把花果山小卖部的每一笔订单金额都存到数据库中并统计合计值。为了尽快完成工作,他把所有订单都写入到一个数组中并循环插入到数据库,他的程序逻辑是这个样子的:

图片

很明显,这个逻辑下完成任务总时长等于“几毫秒×数组数量”,由于平日里花果山没什么人,小寅的程序提交后一直运行良好,直到有一天,花果山变成了5A级景区……

小寅的代码在花果山游客大量增加之后延迟过高,已经影响到了其他的业务,而要解决上面的性能问题很简单,只需要将循环内的insert语句移到循环之外即可。虽然看上去仅改了一行代码,但这一行的改变中就包含了批处理的解决方案,它减少了与数据库的交互,与原代码之间的时间成本天差地别,这就是性能优化带来的的好处。

什么是性能优化模式?

聊完了性能优化的好处,我们接下来就讨论一下什么是性能优化模式,这个说法也是最近看到的一篇博客中提到的:

性能优化模式是一个模型对模型的方式,我们把性能问题想象(抽象)成模型,再把解决它的办法也抽象成模型,这样一来就成了恶化模型对应优化模型,而这种组合拳就是性能优化模式,也可以说是解决方案。当然,性能问题千奇百怪,解决方案自然也并非是一成不变,具体问题还得具体分析,我们话不多说,直接开始。

请求泛洪与数据局部性模式

问题来了!假设我们自己开发了一个外卖平台“饿了美”,为了保证客户用尽可能少的时间去找商家以提高客户留存率,我们推出了推荐列表用于推荐用户可能喜欢的商家或者团购,每一个商家都是由多个标签或属性构成的,这些商家根据不同的标签或属性分布在不同的微服务甚至不同服务器上。此时,要展示推荐列表将会发生下图这样多次请求的情况,即请求泛洪:

图片

我们可以看到,虽然在用户端只是展现了平台最推荐的十几个商家或团购的信息,但是可能背后存在数百甚至数千次的底层服务调用。对于这种存在多次请求问题的分布式系统,请求泛洪所导致的性能恶化几乎是随流量呈指数关系增长的。那么可以想像,随着流量高峰的到来,其需要临时增加的服务器数量也是指数级的。当然这里有人可能会说,我这个服务是云服务,可以通过一些自动缩放策略来满足峰时设备需求,比如AWS的Auto Scaling Groups。但是,这只是治标不治本的方案,而且云设备按需付费,设备越多开销越大,随着应用越做越大,流量越来越多,这方面的成本将会无限扩大。

这里,我们就要隆重介绍一番针对此问题的解决方案:数据局部性模式。

图片

数据局部性模式拥有一个中心,两个基本点:一个中心是合理组织数据业务,以减少服务调用次数为中心,两个基本点是从服务端和客户端两点进行优化。在服务端,数据应当由尽可能少的服务器来提供,且常在一起被消费的数据应尽可能放在同一服务器上,如上图所示。当我们返回给客户的响应中每一条数据块都是从数百个服务器上取来拼成的时候,通过木桶效应理论可知,客户需要等待的时间取决于最慢的那个服务响应的时间。

不仅如此,该模式还解决了依赖的服务器过多造成的系统可用性下降问题,毕竟只要任意一个服务器宕机都会导致整个任务的失败。在客户端,我们可以采用本地缓存,批处理请求,一致性哈希左移等方案。一致性哈希左移指的是将需要通过哈希进行负载均衡的服务中的哈希放到客户端去做,减少服务端的接口调用。说到缓存和批处理,它们带来的好处是显而易见的,不只是在各大应用上发光发热,CPU、操作系统等我们日常接触的很多东西的设计理念里也少不了它们。当然,事物都有两面性,这个模式的最大弊端在于它需要进行重构,重构带来的改变就意味着庞大的工作量以及可能引入的BUG、可靠性问题等。

请求拥塞与水平分割模式

除了可以解决数据泛洪的数据局部性模式,现在让我们来想象这样一个场景:我们为某某银行开发了一个节日福利活动业务,它需要依赖多个服务并依次执行:认证登录、查询账户过去一个月平均余额、查询历史交易信息、查询用户福利等级、查询客户是否为新用户等,业务上线后活动极其火爆,参与者众多,没过一会儿我们就收到了这样的反馈:“不行,业务卡死了”、“太慢了”。此时,我们遇到的就是单次请求时延变长导致的系统性能恶化甚至崩溃的情况,即请求拥塞:

图片

用户想参与福利活动薅点羊毛,但是众多参与者的加入却造成了其中某个环节的响应时间变长,在这个系统下,该服务的响应时间上升势必造成系统整体的响应时间上升,进而出现CPU、内存、Swap报警等一系列问题。自然地,可能很多人就会想到增加服务器的解决方案,既然用户激增且这样的状态并不长久,那么临时增加服务器分流并进行负载均衡不就行了?这自然也是一个好的解决方案,但我想要介绍的是不需要额外购置服务器的另一个方案:水平分割模式。

水平分割模式就是将整个请求流程切分为必须要相互依赖的多个阶段,每个阶段包含相互独立的多种业务处理。完成切分后,该模式按阶段内部并行,各阶段间串行的方式运作,此时的请求总耗时将下降为各阶段间耗时的总和。用上面银行活动的案例来看,是否使用水平分割模式的效果对比如下(图中各模块延迟均为胡诌的):

图片

并行处理的思路并非万能,比如在数据更新的时候,并行处理就会出现数据不一致的问题(如下图),这也是为什么DB只允许同时只有一个人对一条数据进行更新的原因。另外,还有一种情况,当负载不大时,并行也不会得到很大的性能提升,举个例子:银行窗口排队,如果大厅中没人等待,那么就算再怎么增加窗口数量,排队时间也不会有什么优化的余地(因为本就为0)。

图片

降级模式

除了针对性的性能优化模式外,不论任何问题都有一种万金油式的解决方案:降级模式。降级模式即发生性能恶化时系统进行自我阉割以保证其运转,常用的降级策略为:流量降级(主动拒绝访问)、效果降级(使用低质量低延迟服务保证可用性)和功能性降级(取消部分服务保证系统可用性),用一个情景来说明其区别:计划公司全体员工去巴厘岛度假,但预算不够。流量降级策略下,只有一半员工去度假了,其余员工仅放假;效果降级策略下,全体员工去北戴河玩;功能性降级策略则是全体员工都去巴厘岛,但不提供机票。

其他模式

除上面说到的模式外,在实际场景中还有反贪心模式、缓存思维模式、绑定模式等等。其中,反贪心模式指局部最优并非导向结果最优,比如在频繁请求的情境下,可能全盘扫描这个看起来并非最优解的方案在整体上会比循环+索引的方案更好;缓存思维模式指频繁访问的数据尽可能放在离用户侧较近的位置上,LRU算法、边缘计算等场景都是应用缓存思维模式的解决方案;绑定模式即在优先考虑局部性的时候,将任务与处理器绑定会更迅速(减少频繁上下文切换消耗),此模式典型应用就是多核CPU采用的NUMA(Non-Uniform Memory Access)技术。

小结

性能问题根据场景不同而千变万化,不同场景下其对应的性能优化模式不同,付出的代价也不同,其归根结底还是“看碟下菜”四字。

责任编辑:赵宁宁 来源: Thoughtworks洞见
相关推荐

2015-11-06 09:36:20

云计算网络中断应急计划

2019-09-11 10:23:58

Redis性能存储

2024-03-11 15:21:42

2020-08-11 11:12:56

微软TikTok收购

2010-07-26 09:07:32

Windows SerWindows SerLinux

2019-12-27 13:31:33

Talking DatAI人工智能

2009-01-17 15:26:05

2013-02-22 18:28:18

容错服务器、

2019-10-31 11:50:19

MySQL数据库Windows

2022-12-07 19:12:46

移动开发性能优化

2020-03-02 19:51:40

戴尔

2023-11-07 14:58:26

2015-07-01 15:32:39

前端前后端分离

2020-11-11 10:00:13

NAT性能内核

2022-04-29 15:24:53

Redis存储慢查询

2021-01-26 10:06:14

创新首席信息官CIO

2013-10-09 09:53:41

AMD微型服务器Opteron 630

2022-07-11 08:48:52

业务转型CIO

2015-09-01 09:27:25

2010-07-21 09:33:09

VMware View
点赞
收藏

51CTO技术栈公众号