第 3 章 开发性能测试引擎

经过对 Java 多线程编程基础和常用的功能的学习,我们已经具备了在性能测试之海自由翱翔的条件。但在出发之前,我们需要一款超级引擎,让我们更快更丝滑拓展航海范围。

本章我们要开发一款基于 Java 的性能测试引擎,通过性能测试常见测试需求的拆解、设计、编程实现,逐步完成这个目标。

在这个过程当中,我们会逐个实践前两章中所学到的知识点和技能,增强我们的熟练程度,为后面更具挑战性的项目做好准备。

3.1 性能测试模型

对于新手性能测试工程师来讲,性能测试模型可能比较陌生。因为常用的测试工具通常会将一部分封装隐藏起来,暴露给使用者的就是线程数、用户数、并发数此类概念。如果想从测试工具直接切换成为 Java 性能测试方式,一时间不太好适应。所以我们先从一个小故事开始本章的内容。

3.1.1 超市小故事

在开发性能测试引擎之前,先看一个小故事:

回到第 1 章中提到的 “小八” 超市,假设超市老板 “小八” 扩大了规模,增加了收银台的人手,现在总计 8 条结账通道,每个通道 1 个收银员。

在设计新的收银台的时候,小八手上有两种方案可供选择。第一种是收银台占地比较小,每个结账通道都有单独的排队区域,有护栏与其他空间隔开,前面顾客结账完成,后面顾客才可以进入收银台。这里称为排队方案;二是收银台占地比较大,将整个收银台和其他区域隔开,顾客可以先进入收银台,后选择结账通道。假设两种设计,进入收银台的顾客只有两种选择:要么将手头的商品结账带走,要么放弃购买这些商品,直接出超市。这里称为限流方案。

在业务高峰期,结账的人依旧会很多。加入每个结账通道结账能力稳定,每位顾客结账流程预计花费 2 分钟,那么每分钟预计可以为 4 位顾客结账。当每分钟要结账的顾客大于 4 位,我们来看看两种收银台设计会发生哪些有趣的事情。

排队方案:每个结账通道都要排队,新来的待结账的顾客自然会选择人数最少的结账通道。如果顾客太多,等待的队伍会越来越长。

限流方案:每分钟会允许 4 位顾客会进入到收银台区域,新来的待结账顾客自然会选择空闲的结账通道。如果顾客太多,大多数都会在收银台区域外等待进入收银台。

当收银台处理能力稳定,且与限流方案设定值一致,两个方案设计除了收银台占地面积以外没啥特别大的区别,都是理想的状态。

下面我们向这个稳定系统增加一个变量:打雷下雨了,一位收银员要去回家收衣服,只剩下 7 位收银员。

若是排队方案,没有收银员的结账通道会关闭,排队的人会自行分散到其他队伍中,此时收银台的结账压力依旧是每个收银台 2 分钟完成一次结账。若是限流方案,依旧按照 4 位每分钟的速率让顾客进入收银台,那么收银台会逐渐无法承载压力,最终会导致顾客在收银台发生严重拥堵。有顾客就会因为各种原因放弃此次购物,直接从无购物通道离开超市。

除了收银员外,顾客也是不稳定因素,下面我们向稳定系统注入另外一个变量:加入顾客中有两位老年顾客,他们相约来了一次大采购,东西多种多样,自带小推车。由于种种原因,实际处理这两位顾客需要每人 10 分钟。

若是排队方案,那么这两人所在的队伍结账时间会明显变长,不明所以的其他顾客依旧会按照队伍长短选择队伍排队。在这两位顾客后面的排队顾客,等待的时间就会变很长。若是限流方案,两个人结账时,两条结账通道就会被占据较长时间,收银台会积累几位顾客,最终当客流量减少到 4 位每分钟之后几分钟内,收银台就不存在积累的顾客。

除此以外,两种方案在应对多位顾客时存在差异。假如小八有两位好朋友:小七和小九,两人想约逛 “小八” 超市。两个人各买了 120 块钱的商品,但是小七只带了一张 100 元的纸币,小九带了两张 100 元的纸币。两人商量了一下,决定采取这样的策略:先让小九结账,然后小九把找零的钱借给小七 20 元。

这种场景如果是排队方案,只需要让小九排在小七前面即可。在限流方案中,两人分配到的结账通道不一样,结账顺序也是无序的,所以无法保证他们策略中根本条件:小九在小七前结账。那么最坏的情况,小七先结账,但是要一直等到小九结账之后把钱送过来。这样,小七所在通道在这段时间内都是无法给其他顾客结账的。

假如小七和小九就是这两位采购的老人,造成的情况会更加严重。在排队方案,他俩排一对,但是在限流方案,最坏的情况,小七会阻塞某个结账通道很长时间,导致收银台积累的用户中着急回家收衣服的,就会选择放弃购物。

我们再更加极端一点,假如小七和小九共享了 8 折购物卡。小九结账时发现购物卡在小七哪里。而此时小七也正收银台另外端翘首期盼小九送钱来付款。那么两条购物通道就会被堵住,再也无法给其他顾客结账了。

排队方案还有一个管理方面的优势,可以针对结账通道进行区分管理,提升整体的顾客结账速率。例如可以设置电子支付快捷通道;设置自助结账通道;设置优先结账通道等等。通过流程优化,设备专一化,降低平均购物结账时间。而且排队方案由于增加了护栏,可以有效降低有些投机顾客经常串对的麻烦。而限流方案的顾客进入收银台之后,再进行管理的难度和成本就会升高很多。

在这个故事中,收银台就是我们所测的服务,结账的顾客就是客户端的请求,顾客从进入收银台到离开超市的时间,就是请求的延迟或者说响应耗时。

如果顾客在进入收银台前拥堵排队,则不会影响收银台处理单个顾客的耗时。若顾客在收银台内拥堵,则会增加等待耗时,但不会增加处理单个顾客的耗时,但在客户端角度看,这两个时间之和,即为响应耗时,会增加。

在小七和小九场景中,借钱的过程可以理解为两个异步任务,在进行多线程通信或者说多线程协调之后,按照我们预期执行。最后的极端场景,两个人都卡在了收银台,相互需要对方哪里执行完结账流程,导致谁也无法完成结账流程,这就是 Java 多线程编程常会遇到的死锁。

排队方案和限流方式对应的就是我们本节的主题,分别对应线程模式和 TPS 模型。这里的 TPS 指的 Task Per Second,跟常见 QPS(Queries Per Second)、TPS(Transactions Per Second)以及 RPS(Requests Per Second)同根同源,描述单位时间内执行的任务数量,在绝大多数场景可以相互替代。为了防止混淆,下面内容统一称为 TPS。

3.1.2 线程模型和 TPS 模型

线程模型用于模拟并发访问的一种测试设计方法,最常遇到的就是线程模型。它最小管理单元是线程,通常线程会循环执行某一段任务。性能测试设计者需要设计这段任务内容,交给线程去循环执行。最后通过对运行线程数的调整,来模拟不同性能测试场景的压力。

用伪代码演示如下:

public static void main(String[] args) {
    for (int i = 0; i < N; i++) {
        new Thread() {
            @Override
            public void run() {
                for (int j = 0; j < M; j++) {
                    executeTask();
                }
            }
        }.start();
    }
}

TPS 模型用于模拟不同请求速率的一种测试设计方法,也是性能测试常见的模型。他最小的管理单元就是任务,通常会有一个执行器,以设定的速率多次执行任务。性能设计者需要设计这个任务的内容,交给执行器按预期速率执行。最后通过对执行速率的调整,来模拟不同性能测试场景的压力。

伪代码演示如下:

public static void main(String[] args) {
    while (true) {
        Thread.sleep(1000);
        for (int i = 0; i < N; i++) {
            executor.executeTask(task);
        }
    }
}

两种测试模型在使用中各有所长,就如前文所说,线程模型更加容易实现,而且更方便进行管理。限于篇幅,笔者将基于线程模型进行后续内容的讲解。对于 TPS 模型,将会在本章的末尾展示一个相对基础版本的开发流程。希望各位能在完成本书学习之后,可以根据实际测试需求,丰富这个 TPS 模型压测框架。

3.1.3 性能测试引擎展望

在最小管理单元的维度以外,还有针对性能测试用例执行过程中的静态模型和动态模型。静态模型指的是用例的压力策略在执行前设置,例如递增策略,最大压力等,用例一旦运行起来,就无法改变压力策略。而动态模型是可以在执行过程中根据服务压力、监控指标等反馈信息,灵活调整压力值。设计这样的性能测试引擎,可以当作完成本书内容实战后,下一步的目标。实现这个目标,我们要拥有更强的多线程管理能力。如果思路再打开一点,当单机无法满足我们的性能测试需求,就会用到分布式性能测试。又是一座需要攀登的高峰。咱们山顶见!

书的名字:从 Java 开始做性能测试

如果本书内容对你有所帮助,希望各位不吝赞赏,让我可以贴补家用。赞赏两位数可以提前阅读未公开章节。我也会尝试制作本书的视频教程,包括必要的答疑。

FunTester 原创精华

【连载】从 Java 开始性能测试


↙↙↙阅读原文可查看相关链接,并与作者交流