性能的重要性

Posted by comeyke on 06-04,2023

代码性能的重要性

怎么定义“性能”和“性能好”
我们以代码性能为例:首先我们需要弄清楚什么样的代码算是性能好?怎么样算是性能不好?
代码性能表现在很多方面和指标,比较常见的几个指标有吞吐量(Throughput)、服务延迟(Service latency)、扩展性(Scalability)和资源使用效率(Resource Utilization)。

  • 吞吐量:单位时间处理请求的数量。
  • 服务延迟:客户请求的处理时间。
  • 扩展性:系统在高压的情况下能不能正常处理请求。
  • 资源使用效率:单位请求处理所需要的资源量(比如 CPU,内存等)。

性能好的代码,可以用四个字来概括:“多快好省”。
看到这四个字,你可能想起了咱们国家当年制定的大跃进总路线,那就是:“鼓足干劲、力争上游、多快好省地建设社会主义”。没错,高性能代码的要求和这个“社会主义建设总路线”相当一致。这里的“多”,就是吞吐量大;“快”,就是服务延迟低;“好”,就是扩展性好;“省”,就是资源使用量低(也即是资源使用效率高)。
用这样的四个指标来衡量,那么性能不好的代码的表现就是:吞吐量小、延迟大、扩展性差、资源使用高(资源使用效率低)。

性能的层级关系

应用程序的性能(标示 1)
image-1698655197894
我来简单解释一下这张图:

  • 首先,红色模块是我们负责的模块(标示 0),它和其他模块一起构成了整个应用程序(标示 1);
  • 这个应用程序运行在服务器和 OS 上面,构成了一个单机系统(标示 2);
  • 几个单机系统一起组成一个互联网服务(标示 3),来面向客户;
  • 这个服务和其他服务一起,需要公司的硬件容量支持,从而占用公司的商业成本(标示 4);

应用程序的性能(标示 1)
我们先从标示 0 和 1 开始,也就是模块和应用程序。
每个模块一般不是孤立存在的,都会要和其他模块交互。如果一个模块性能不好,一定会在某种情况下影响到其他的模块。
具体来说,假如端到端的服务延迟有最大的延迟允许,比如不能超过 2 秒钟,那么这个服务所需要的应用程序或者微服务,一般也都会有自己的最大延迟预算。假设这个端到端服务需要三个微服务或应用程序来串联,那么,每个应用程序都会分到一定的延迟预算,比如最大 1 秒。
同理,我们所负责的模块也会根据程序的逻辑设计分到相应的延迟预算,比如 300 毫秒,如下图红色模块所示。
image-1698656578350
这种情况下,如果我们的模块在流量适度变大时,处理时间超过 300 毫秒的预算,那这个模块的延展性显然就不够了,很可能会导致整个端到端服务的延迟超标。
单机系统的性能(标示 2)
讨论完了模块和程序,我们再看看单机系统
我们的模块所在的应用程序(或者微服务),是运行在服务器的硬件和操作系统上面的。对这台服务器而言,这是个单机系统,包括软件和硬件的整个垂直全栈。现在的系统都非常复杂,软硬件之间的交互也复杂而微妙,并且随着各个构件的升级而经常变化。
单机系统的软硬件构件包括操作系统、程序库、存储系统、CPU、内存、还有网络等等。这些构件都会或多或少地影响上层程序的性能。
互联网服务的性能(标示 3)
如今的系统复杂度越来越高,一个复杂的服务给拆分成了很多的单一功能的微服务,各服务为之间的交互过程,一般通过nginx-API网关-微服务。上游服务对我们产生请求,向下游服务发送请求。
比如下面的图示,我们所在的服务模块用红色标识,上游服务模块用绿色标识,下游服务模块用黄色标识。
image-1698657131472
模块性能设计的不错,也可能对上下游模块造成不好的影响,进而影响整个大的服务性能。一个真实的案例如下:
从一个生产环境下的服务问题中发现。某个下游模块出现延展性问题,服务的延迟变大;上游模块发出的请求排了很长的队。这个时候上游模块已经感觉到下游的性能问题,因为对下游请求的处理延迟已经大幅度增加了。
此时上游模块本应该怎么做呢?
它应该降低对下游模块的请求速度,从而减轻下游模块的负担。但是案例中的上游模块设计没有考虑到这一点。不但没有降低请求速度,反而发送了更多的请求,以求得更快的回答。这样无异于火上浇油,最后导致下游模块彻底挂掉,引发了整个服务的瘫痪。
后来我们学到的教训就是,串联的服务模块中,上游模块必须摒弃这样雪上加霜的服务异常尝试,应该采用指数退避机制(Exponential Backoff ),通过快速地降低请求速度来帮助下游模块恢复(上游模块对下游资源进行重试请求的时间间隔,要随着失败次数的增加而指数加长)。
公司的成本(标示 4)
我们所负责的互联网服务的性能直接影响公司的成本。
一个高性能的服务,在服务同等数量的客户时,需要的成本会比较小。具体来说,如果我们的服务是计算密集型,那么就应该尽量优化算法和数据结构等方面来降低 CPU 的使用量,这样就可以用尽量少的服务器来完成同样的需求,从而降低公司的成本。

性能测试的概念

性能测试
针对系统的性能指标,建立性能测试模型,制定性能测试方案,制定监控策略,在场景条件之下执行性能场景,分析判断性能瓶颈并调优,最终得出性能结果来评估系统的性能指标是否满足既定值。

对性能团队的职责定位有如下几种。

  • 性能验证:针对给定的指标,只做性能验证。第三方测试机构基本上都是这样做的。
  • 性能测试:针对给定的系统,做全面的性能测试,可以得到系统最大容量,但不涉及到调优。
  • 性能测试 + 分析调优:针对给定的系统,做全面的性能测试,同时将系统调优到最优状态。

但是测试人员接触应用最直接的方式是接口测试,可以完整的覆盖后端逻辑的验证。功能测试完成后,我们会进行单接口的性能测试,属于代码性能测试的范畴。在实际的工作中性能你会感觉离我们很近又好像很远。因为测试人员做的性能测试中更多的是进行的性能验证+简单的分析定位,而非性能测试+分析+定位+调优。
其实最主要的原因是性能测试涉及的范围大,需要具备的知识面广且深,还要有足够三方支持。

  • 在技术细节上,要达到架构师的基础技能;
  • 工作范围上是要扩大到业务、架构、研发、测试、运维过程等;
  • 工作权限上需要同时拥有技术权限和指挥权限,技术权限很容易理解,无非就是主机登录 root、数据库 DBA 等权限。而指挥权限就是,我们在需要什么人做什么事情的时候,一定要能叫得动。比如你让运维查个生产数据,要是运维只给你一个白眼,这活就没法干了。所以,我们需要什么数据,会产生什么样的结果,一定要环环相扣,缺少了一个环节,那就走不下去。

以上可见性能测试的成长之路还有一段距离。

性能测试的分类

  1. 基准性能场景:这里要做的是单交易的容量,为混合容量做准备(不要跟我说上几个线程跑三五遍脚本叫基准测试,在我看来,那只是场景执行之前的预执行,用来确定有没有基本的脚本和场景设计问题,不能称之为一个分类)。
  2. 容量性能场景:这一环节必然是最核心的性能执行部分。根据业务复杂度的不同,这部分的场景会设计出很多个,在概念部分就不细展开了,我会在后面的文章中详细说明。
  3. 稳定性性能场景:稳定性测试必然是性能场景的一个分类。只是现在在实际的项目中,稳定性测试基本没和生产一致过。在稳定性测试中,显然最核心的元素是时间(业务模型已经在容量场景中确定了),而时间的设置应该来自于运维周期,而不是来自于老板、产品和架构等这些人的心理安全感。
  4. 异常性能场景:要做异常性能场景,前提就是要有压力。在压力流量之下,模拟异常。

指标关系:并发用户数应该怎么算?

什么是并发
下面我们就来说一下“并发”这个概念。
image-1698659268068
我们假设上图中的这些小人是严格按照这个逻辑到达系统的,那显然,系统的绝对并发用户数是 4。如果描述 1 秒内的并发用户数,那就是 16。是不是显而易见?

在线用户数、并发用户数怎么计算
image-1698659547574
如上图所示,总共有 32 个用户进入了系统,但是绿色的用户并没有任何动作,那么显然,在线用户数是 32 个,并发用户数是 16 个,这时的并发度就是 50%。

但在一个系统中,通常都是下面这个样子的。
image-1698659573569
为了能 hold 住更多的用户,我们通常都会把一些数据放到 Redis 这样的缓存服务器中。所以在线用户数怎么算呢,如果仅从上面这种简单的图来看的话,其实就是缓存服务器能有多大,能 hold 住多少用户需要的数据。
最多再加上在超时路上的用户数。如下所示:
image-1698659595580
所以我们要是想知道在线的最大的用户数是多少,对于一个设计逻辑清晰的系统来说,不用测试就可以知道,直接拿缓存的内存来算就可以了。
假设一个用户进入系统之后,需要用 10k 内存来维护一个用户的信息,那么 10G 的内存就能 hold 住 1,048,576 个用户的数据,这就是最大在线用户数了。在实际的项目中,我们还会将超时放在一起来考虑。
但并发用户数不同,他们需要在系统中执行某个动作。我们要测试的重中之重,就是统计这些正在执行动作的并发用户数。
当我们统计生产环境中的在线用户数时,并发用户数也是要同时统计的。这里会涉及到一个概念:并发度。
要想计算并发用户和在线用户数之间的关系,都需要有并发度。
做性能的人都知道,我们有时会接到一个需求,那就是一定要测试出来系统最大在线用户数是多少。这个需求怎么做呢?
这里有一个比较严重的理解误区,那就是压力工具中的线程或用户数到底是不是用来描述性能表现的?我们通过一个示意图来说明:
image-1698659621755
通过这个图,我们可以看到一个简单的计算逻辑:

  1. 如果有 10000 个在线用户数,同时并发度是 1%,那显然并发用户数就是 100。
  2. 如果每个线程的 20TPS,显然只需要 5 个线程就够了(请注意,这里说的线程指的是压力机的线程数)。
  3. 这时对 Server 来说,它处理的就是 100TPS,平均响应时间是 50ms。50ms 就是根据 1000ms/20TPS 得来的(请注意,这里说的平均响应时间会在一个区间内浮动,但只要 TPS 不变,这个平均响应时间就不会变)。
  4. 如果我们有两个 Server 线程来处理,那么一个线程就是 50TPS,这个很直接吧。
    请大家注意,这里要转换的一个细节,那就是并发用户数到压力机的并发线程数。这一步,我们通常怎么做呢?就是基准测试的第一步。关于这一点,我们在后续的场景中交待。而我们通常说的“并发”这个词,依赖 TPS 来承载的时候,指的都是 Server 端的处理能力,并不是压力工具上的并发线程数。在上面的例子中,我们说的并发就是指服务器上 100TPS 的处理能力,而不是指 5 个压力机的并发线程数。请你切记这一点,以免沟通障碍。

通过示意图和示例,我描述了在线用户数、并发用户数、TPS(这里我们假设了一个用户只对应一个事务)、响应时间之间的关系。有几点需要强调:

  1. 通常所说的并发都是指服务端的并发,而不是指压力机上的并发线程数,因为服务端的并发才是服务器的处理能力。
  2. 性能中常说的并发,是用 TPS 这样的概念来承载具体数值的。
  3. 压力工具中的线程数、响应时间和 TPS 之间是有对应关系的。

总结

性能包含多个层次,从代码模块,到整个系统,到服务架构之间,到公司运营。在降本增效的大背景下,性能测试的必要性和重要性在凸显,希望可以得到更多的重视,更重要的是要体现性能测试的价值。