细说Java性能测试第一课 Jmeter导读

564 篇文章 136 订阅

细说java性能测试

课前导读

作为一个测试从业者,如何在有限的测试时间里保证交付物的质量一直是绕不开的话题,性能测试作为质量保障的一部分,自然也有着重要的地位。这一讲作为本课程的导读,我想带你相对全面地了解一下性能测试的整个过程,以及在这个过程中需要落地的事情。在后面的学习中,我们将一步步展开。

历史访问数据

历史访问数据,指的是什么类型的用户通过何种终端访问服务的接口次数。

为什么我要把访问数据记录放在第一个呢?线上作为“案发”的第一现场,保留现场的证据是非常重要的。性能测试说白了是模拟案发现场来寻找破案线索,访问数据记录用户轨迹、作为衡量性能的重要手段,自然是不可或缺的。绝大多数公司都会封装平台来采集历史访问数据,如果要看原始的访问日志,Nginx 日志也是一种方式(如下所示),不过原始的日志都需要加工处理来提取我们需要的信息。

120.204.101.238 - - [29/Nov/2020:14:09:22 +0800] "GET /v1/register HTv1TP/1.1" 200 150 "-
120.204.101.238 - - [29/Nov/2020:14:09:22 +0800] "POST /v1/login HTTP/1.1" 200 36 "-
120.204.101.238 - - [29/Nov/2020:14:09:22 +0800] "GET /hello/map HTTP/1.1" 200 202 

可能你能理解为什么要通过终端类型统计服务的接口次数,但却对为什么要统计用户的类型有些困惑?在绝大多数电商场景下,电商用户等级对应不同的权益、优惠券类型和数量,这些业务规则都会影响到性能测试的结果。很多人在做性能测试的时候会忽略这一点 。

需求管理

有了参考数据,我们就可以来看需求了,对需求接入和充分的分析能帮助你在测试之前获得更多的信息,也能制定出较为完善的性能测试方案。业务测试和性能测试都是从需求入手的,但业务测试会去了解相关的业务背景和产品方案;对于性能测试而言,则在需求来源、分析方面提出了更多的要求。

需求来源

需求来源其实就是你这次性能测试的目的,调研清楚这个问题能帮助你更有针对性地获取数据,从而制定更为准确的性能目标。例如,我们这次的性能测试是为了应对“黑色星期五”的活动,那么就要考虑有没有以往的性能测试数据沉淀、当前有多少活跃用户数、网站交易数和活跃人数有没有相应的递增比例等和该活动有关的数据。

需求分析

弄清楚了性能测试的目的,我们就要来做需求分析和梳理了。

需求分析是在原始数据中提炼出有效的性能参考数据,通过这些数据构建性能测试的模型,再通过模型形成测试步骤。性能测试模型和性能测试执行步骤也是性能测试方案的核心内容,它决定了你做性能测试是否准确,是否更符合真实场景。

分析方案

在需求分析完成之后,就需要将你分析的内容提交一份性能测试方案了。性能测试方案的目的不仅仅在于让自己知道这次性能测试如何执行,也要让你的项目成员知道这次性能方案,它的执行周期、涉及的成员等,然后再一起评审这次方案中有没有不合理的地方。

性能测试环境管理

从目前的趋势来看,线上的全链路性能测试非常热门,但并不意味着就只做线上的性能测试了。关于性能测试环境,一般情况下我们会独立搭建一套,与业务测试环境相隔离,同时也能够在上线之前尽可能暴露一些代码中的问题。

我曾看到过这样一个观点:线下性能环境与生产环境机器配置相差甚大,我们直接在生产上做性能测试就可以了,没有必要在测试环境中做

这个观点引起了一部分人的赞成,但我认为这个说法不够全面。环境的配置高低是决定性能结果的一个影响因素,但不是全部因素。能够提前测试、提前暴露 bug,修复 bug 的成本也就越低,所以在线下必须有专门的性能环境,它可以帮助你提前发现内存泄漏、死锁等问题。更何况,这些问题的发现和修复与服务器硬件配置并没有直接联系,如果能够在线下提早用更低的成本解决是一种更优的选择。

如果没有做线下性能测试的情况下直接在生产上测试,对性能中的异常测试、高可用测试可能无法充分执行;同时,修复性能 bug 也需要功能上的回归,这些都增加了过程管理的复杂度。

监控管理

监控是发现性能问题的眼睛,没有监控,性能定位分析也就无从谈起了。监控的核心在于全面和深入,因此,我将监控管理分为了客户端数据监控、硬件资源监控、链路监控和业务规则监控,通过这几个层次的监控可以让你最大限度地避免监控死角,也为你调优分析提供充足的依据。

客户端数据监控

性能测试中说的客户端一般是指测试机,测试机输出的数据是观察性能好差的关键指标。我推荐 JMeter+InfluxDB+Grafana 的框架,它具备展现直观、数据实时的特点,可以全面地展示监控的数据。

硬件资源监控

基础硬件资源监控一般包含 CPU、内存、磁盘、网络等,常用的监控方式可以分为命令行监控和可视化监控。

  • 命令行监控

通过命令的监控我们能够以最直接的方式获取服务器的实时状态。以 Linux 服务器举例,top、vmstat、iostat、iftop 等都是性能监控常用的命令。

  • 可视化监控

可视化监控相对于命令行监控提供了更为丰富的图表展示,这样的话看起来更直观易懂,适合监控大屏的展示,能够将监控信息传递给项目组成员,但它需要提取数据之后计算,然后再展示,有一定的延迟,不如命令行监控直接。

Zabbix、Prometheus+Grafana 等都是可视化监控常用的手段,它们可以把数据持久化,能够调取过往时间轴的历史数据,一般在回溯、汇报、复盘时使用比较多。

不管采用何种方式,在进行硬件监控时,都应该涵盖测试过程中所有的服务器,包括压测机、应用服务器、中间件服务器数据库服务器等。

链路监控

链路监控是对代码本身的追踪,代码问题常常是问题产生的根因,所以关于代码的监控不可忽视。目前常用的代码链路追踪工具有 SkyWalking、PinPoint、Arthas,在后续的学习中我会向你介绍其中的一些,帮助你定位代码问题。

业务规则监控

业务逻辑报错和用户息息相关并且用户是可以直接感受到的,比如商品库存不足、用户余额不足,它们会直接影响用户的体验。线上出现问题并不少见,重要的是如何第一时间得知并且解决这些问题。所以当出现问题即时发送报警邮件或者短信也是十分必要的,对于业务的监控同样不能忽视。

数据模型建设

为什么有数据模型这样的概念呢?数据模型的意义在于沉淀以往的历史数据,通过不同的维度去发现一些规律,我认为这也是性能测试领域中的一种探索方向。通过数据模型的建设,我们可以尝试在不同纬度建立数据之间的联系,从而发现数据间的规律,对未来的数据进行预测。这些纬度分为时间纬度和机器纬度。

时间纬度

一般的电商每年至少有两次大促,618 和双 11。它们一般会详细记录每年总的成交额、网关访问次数、各个服务访问次数等,通过每年的活动力度、广告投放,以及数据团队来预测下次大促的成交金额和网站访问量等,这些数据也会间接帮助性能测试制定目标。

机器纬度

机器纬度是一个什么概念呢?你可能会认为在线下两台机器测出来接口的处理能力是 100,线上有 10 台等配置的机器,就不用测试了,处理能力直接按照 5 倍去推算。

这其实是默认只要扩充机器系统的处理能力就会倍数增加,事实上是毫无道理的。不过你可以长期记录接口或者服务在性能测试环境的数据和生产环境中,相同场景下的压测数据,再进行长时间地跟踪对比,尝试发现其中是否能够存在一些规律。

技术建设

技术建设基于你的技术视野。关于技术的重要性,你可能了解,但理解得不够全面。无论是你写的测试脚本,还是在做的代码调优,其实它们都只是技术的一部分。我认为对于一名优秀的性能测试来说,需要具备以下 3 个方面的能力。

  • 熟练掌握一门编程语言。测试很难说一定要掌握哪一种语言,但是熟练地使用一门语言可以帮助你迅速上手其他编程语言。

  • 能够读懂服务端基本架构。如果你不懂服务端的架构,那基本只能根据你的性能测试工具去编写报告,了解不到更深层次内容。

  • 能够根据性能测试需求,提出系统改造建议。性能测试与业务测试不太一样:业务测试基本是通过构造测试场景去满足业务规则,而性能测试,尤其是线上全链路性能测试,为了避免造成线上数据污染和影响真实用户访问,往往会改造系统去进行流量隔离和清理。

技术有个重要作用:改善测试效率。测试讲究质效合一,质代表质量,效则是效率。

好的测试平台可以管理测试资料,固化测试过程,自动化测试执行,可视化测试结果。它可以增加团队成员之间的协作性,不要重复造轮子,提升团队能效;对于脚本管理和监督,测试结果的回溯也有重要作用。

总结

这一讲我带你了解了性能测试全过程中的要点,你可以对性能测试有一个大概的认识。在后面的学习中,我会将上述的知识结合我的经验来讲解。你可以从这一讲中看一下自己还有哪些需要夯实的知识,也可以看看公司的性能测试开展到什么阶段了,看在发展上还有哪些自己力所能及的地方。

对于全过程中需要注意的事情,除了我写的这些,你还有什么要补充的吗?欢迎在评论区留言。

下一讲,我将带你了解 JMeter 的核心概念,它是我们现在最流行的性能测试工具。

你好,我是周辰晨,欢迎来到《说透性能测试》。

我从事测试工作 8 年,曾就职于京东、平安、易果生鲜等公司,目前在某大型数据科技公司担任测试专家,主导全链路性能测试及测试平台开发相关工作。其间,我通过问题总结出的种种单点经验逐渐转变为自己的方法论,我也渐渐跳出了原来对性能测试的局部认知,开始从全局的视角来看待性能测试。

像京东这样的头部互联网公司,网站承载着数以亿计的客户群,自然对性能有着很高的要求。在京东的工作也让我对性能测试有了更深的理解,性能测试并不只是要一个结果,更多的是要从部署结构、代码链路、业务上下游等多角度来综合考量

后来,我又经历了从 0 到 1 搭建性能测试体系的全过程,保证了双十一峰值期间 500 万下单量下 App 的正常运转;同时,这套测试体系也能够有效承接每分钟 150 万的访问量。

为什么要学性能测试

目前,最成熟的性能测试从业者一般都分布在各大互联网公司,这些公司对性能有着切实的需求,也具备深耕性能测试技术的土壤,所以往往能培养和聚集一批优秀的性能测试从业者。

那是不是说,其他公司就没有性能测试需求了呢?并不是的。

这两年,测试开发这个职位火了起来,许多公司招测试时都是在招测试开发。虽然招聘的不是专职的性能测试人员,但任职要求水涨船高,往往都需要你能够进行非功能测试,如性能测试、自动化测试。

我们来看一则快手的招聘信息,其中就明确要求求职者有性能测试的相关经验。

Drawing 0.png

招聘信息来源拉勾网

我也看到很多测试同学会在简历上写:“熟悉 JMeter 的基本使用和性能测试。”

但当我在面试时问:“性能测试的基本过程是什么?”很多人说“我就是用 JMeter 做了脚本”,至于“如何监控数据?”“需要监控哪些数据?”这样的问题,回答就更是模糊不清了

下面我列举了几个其他的常见问题,你也可以对照自检:

  • 只会使用 JMeter 但执行却不规范。在性能测试过程中,工具使用不恰当会影响到性能测试的结果。我见过很多因为工具使用不当导致的客户端瓶颈,让处理能力未达到预期的情况。很多测试没能及时发现是工具的原因,导致自己的专业能力备受质疑。

  • 不会制定有效的性能测试目标。如果不会制定有效的性能测试目标,那测出来的数据也没有什么参考价值,因为你不知道能不能满足上线需求,也不能准确地评估线上风险,做完了性能测试依然留有一大堆问题。

  • 不会定位和分析性能测试结果。测试脚本得到的数据并不能直接用来分析系统瓶颈,你只有通过监控去观察系统存在的异常点,然后根据异常点来重点监控相关组件,由表及里、层层深入才能找到根本原因。

事实上,现在很多人做性能测试只是在用工具写脚本、跑压测,最后出来一个结果,至于什么是性能测试,性能测试的过程是什么样的,性能测试目的是什么,缺少系统性的认知。

性能测试真正的价值,并不在于你用工具完成了一份报告,而是通过对过程和结果的分析找到症结,帮助团队有效提升产品性能,比如提升了多少 TPS,降低了多少响应时间,节约了多少硬件成本,等等。

因此,我希望用这门课把性能测试的全过程讲给你听,不只带你玩转工具,学会制定一个有效的性能测试方案,更在把工具做到极致的基础上,和你分享如何监控数据才能迅速定位问题,如何做性能调优,攻克性能测试的重难点。

课程设计

性能测试中的很多标准其实都是非常主观的,你在网上看到的很多推导公式、二八原则之类的概念,如果不结合业务实际,盲目地学习,然后把这些作为性能测试的标准打开方式,很可能是有害无益的。

因此,本课程注重实战,我将以真实的互联网使用场景为导向,帮助你建立一个体系化的性能测试认知,分工具使用、场景分析、监控搭建和问题定位分析实践 4 个模块,为你全面展示性能测试的整个过程。

模块一:性能测试的工具原理与使用。JMeter 是目前最流行的性能测试工具之一,它具备较为完善的基础功能,还具备丰富的可拓展性,因此这一模块我将带你玩转 JMeter。

你在这里不仅仅能学到如何使用 JMeter,还能学到 JMeter 的二次开发和调用 JMeter 的 API 完成性能平台开发的基础步骤。二次开发可以让你了解如何通过 JMeter 提供的接口进行拓展,实现自己的定制化需求,而掌握平台化的操作可以极大地提高团队协作效率。

模块二:性能测试目标与场景分析。这个模块可以开启你从使用工具做性能测试到专业化性能测试的进阶之路。

我会聚焦正式开始性能测试之前应当明确的事情:如何制定性能测试指标;参考数据有哪些,怎么获取;常见的性能测试场景有哪些,如何通过这些场景来提高性能测试的覆盖率,等等。这些都是性能测试方案的组成部分,只有制定了正确的性能测试方案才能做出有效的性能测试。通过这一模块的学习,你可以理解性能测试的每一步,而不只是机械地执行上级派给你的任务。

模块三:分层监控体系建设。这一模块的重点是监控和问题定位,包括如何做硬件监控、系统链路监控,如何打造可视化的监控报表。监控是性能测试必要的步骤,是你发现性能问题的“眼睛”。

模块四:性能分析优化实践。我在前面提到,性能测试的标准常常是主观的,过往经验有时候不能照搬。因此,我会从服务端、中间件、数据层三个角度带你了解如何定位和优化问题,希望你看完以后可以结合自身工作场景进行性能调优。

很多公司担心直接在生产环境进行性能测试会影响用户体验、污染线上数据,其实这些都不是问题。我会从线上全链路性能测试的开展、组织和注意事项等多个维度来展开介绍,为你更好地实践提供思路。

讲师寄语

测试需要掌握越来越多的技能。对你来说,能多学会一门技能就可以胜任更多的工作,更可以“去同质化”,拥有更强的竞争力。

而且,性能测试作为非功能测试,其实是一个非常有价值、有成就感的工作,当你遇到性能瓶颈时,不是简单地说“去硬件扩容”。如果你的建议不只是简单地增加服务器成本,而是能够通过自己的定位和分析,以及一轮轮的调优和测试提升系统处理能力,一定更能够彰显你的技术视野,体现你工作的价值。

希望这门课可以帮助你成为一名更优秀的测试工程师,将工具的使用、性能需求的梳理、监控的搭建、问题的定位灵活应用到你的工作中去,也欢迎你在留言区和大家分享你成功的喜悦。

Jmeter

从今天开始,我们将进入模块一的学习,在学习的过程中,希望你能够明白为什么 JMeter 要这么用并掌握 JMeter 的一些进阶用法。这一讲作为我们学习的第一讲,我将带你了解 JMeter 的核心概念,完善你对测试工具的认识。

为什么是 JMeter

性能测试有很多工具,JMeter、Loadrunner、Locust、nGrinder 都不乏粉丝。有人认为做性能测试重要的不是工具,是思想。但从学习实践的角度讲,工具在一定程度上决定了工作效率及协作模式。要成为一名测试专家,对工具一定是要精通的。

JMeter 原生的方式只支持单点工作,团队成员并不能很方便地互相检查脚本和查看报告。如果我们想改变这样的协作方式,就要对 JMeter 进行改造。如果不了解工具,改造也就无从谈起。

说了这么多,那我为什么会选择介绍 JMeter 呢?总的来说,它有以下 3 点优势。

  • 开源免费、安装简易、多系统兼容。相对于 Loadrunner,JMeter 没有版权的困扰,脚本可以在 Windows、Linux、Mac 任意系统间切换,非常简单方便。

  • 丰富的基础插件。相对于 Locust,JMeter 提供了较多的插件,可以减少重复造轮子的工作。Locust 的基础功能需要写代码实现,更适合定制性较强的测试场景,如游戏类测试,在敏捷化的测试团队中需要考虑到这部分的时间成本问题。

  • 良好的拓展性。虽然 JMeter 已经有了丰富的基础插件,它本身还是提供了入口进行二次开发,以满足团队定制化的需求。同样,你也可以将 JMeter 平台化,通过平台化的操作来管理 JMeter,增强测试团队的协作性。

我们虽然是从 JMeter 工具开始的,但网上其实已经提供了很多实例来教你 JMeter 的基础使用,所以这一讲的重点是帮你厘清 JMeter 设计上的一些核心理念。我将从 3 个方面来介绍,分别是:线程、循环、Ramp-Up,组件和元件,以及分布式压测。

我们先来看线程、循环、Ramp-Up。

线程、循环、Ramp-Up

这是你在 JMeter 的线程组元件中的线程属性,线程组建立是你使用 JMeter 进行性能测试最基础的步骤,压力发起策略几乎都依赖于这个元件。

线程与循环

我们先来看两张图,看看它们之间有什么区别。

Drawing 0.png

图 1:设置图 A

Drawing 1.png

图 2:设置图 B

从两张图的对比中,我们可以看到图 1 和图 2 的区别在于线程数和循环次数,一个是 1 和 10,一个则是 10 和 1。从结果来看,图 1 和图 2 都是发送了 10 个请求,那它们的核心区别是什么呢? 我们不妨来看两段代码演示。

先来看图 1 的代码演示:

for(int j=0;j<10;j++) {

System.out.println(Thread.currentThread().getName());//打印线程名字
}

这段代码我使用线程循环的方式打印运行线程的名字,运行后的内容如下:

Thread-0
......
Thread-0
Thread-0 //可以看到是基于同一个线程

再来看图 2 的代码演示:

for(int i=0;i<10;i++){
    new Thread(new Runnable() {
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    }).start();
} //示意代码

这段代码我是使用多线程的方式打印正在运行的线程,运行后效果如下:

Thread-0
......
Thread-8
Thread-9 //不同的线程

以上代码内容主要是打印线程的名字。不难看出,循环的方式是基于同一个线程反复进行 10 次操作,而多线程则启动了 10 个不一样的线程,虽然都是向服务器发送了 10 次请求,但这两种方式完成的时间和对系统的压力也完全不一样。

打个比方,我们需要掰 100 斤玉米,一组是 10 个人一起掰,一组只有 1 个人掰,每个人的速度如果是一致的,不用想就知道哪个组更快。这样的场景经常发生在使用 JMeter 利用接口造数据时,同样是造 1 万条数据,如果你觉得速度很慢,那你就可以考虑一下多线程了。但掰玉米用 10 个人的成本当然要比用 1 个人来得多,我们的压力场景也是这样的。通常压力场景都是多线程的,线程的多少也直接决定了对被测系统压力的大小。

Ramp-Up

Ramp-Up 其实是一个可选项,如果没有特殊要求,保持默认配置脚本即可。如果填 1,代表在 1 秒内所有设置线程数全部启动。不过这个是理论上的,实际启动时间也依赖于硬件的接受程度。如果硬件跟不上,启动时间自然也会增加。

在有的性能测试场景中,如果你不想在性能测试一开始让服务器的压力过大,希望按照一定的速度增加线程到既定数值,你就可以使用这个选项。比如我想用 10 个线程进行测试,启动速度是每秒 2 个线程,就可以在这里填 5,如下所示:

Drawing 2.png

图 3:设置图 C

我们来通过运行展示一下。

Drawing 3.png

图 4:生成线程数

我使用了监听器中的用表格查看结果插件。通过这组数据可以看到,每秒产生了 2 个新的线程,合计在 5 秒内完成。

组件和元件

了解了线程、循环和 Ramp-Up,接着来聊聊组件和元件。

组件和元件的关系

要解释组件首先就要说元件。我们看图 4 中的 HTTP 请求,其实这就是一个实际的元件。同样作为元件的还可以是 JDBC 请求、Java 请求等,这一类元件我们统一称为取样器,也就是组件。我用一个示意图来表示组件和元件的关系:

图片2.png

图 5:组件和元件关系图

如图所示,HTTP 请求、JDBC 请求等元件都从属于取样器。

组件的作用

JMeter 有多种组件,我们重点看下这七类: 配置元件、取样器、定时器、前置处理器、后置处理器、断言、监听器。我们来看下它们各自的作用。

  • 配置元件:用于初始化变量,以便采样器使用。类似于框架的配置文件,参数化需要的配置都在配置元件中。

  • 取样器:承担 JMeter 发送请求的核心功能,支持多种请求类型,如 HTTP、FTP、JDBC 等,也可以使用 Java 类型的请求进行自定义编写。

  • 定时器:一般用来指定请求发送的延时策略。在没有定时器的情况下,JMeter 发送请求是不会暂停的。

  • 前置处理器:在进行取样器请求之前执行一些操作,比如生成入参数据。

  • 后置处理器:在取样器请求完成后执行一些操作,通常用于处理响应数据,从中提取需要的值。

  • 断言:主要用于判断取样器请求或对应的响应是否返回了期望的结果。

  • 监听器:监听器可以在 JMeter 执行测试的过程中搜集相关的数据,然后将这些数据在 JMeter 界面上以树、图、报告等形式呈现出来。不过图形化的呈现非常消耗客户端性能,在正式性能测试中并不推荐使用。

组件的顺序

了解正确的组件执行顺序可以帮助你明白在什么情况下应该添加什么组件,而不会添加错误的组件造成不必要的麻烦。我将它们做了一个排序,如下图所示:

图片1.png

图 6:组件顺序

搞懂了组件顺序,你在测试前准备脚本生成参数化数据时,就可以在前置处理器中寻找相关元件;在要提取接口返回的数据,就可以在后置处理器中寻找相关插件,而不是在其他地方寻找数据,浪费时间。

我经常看到有的测试人员在需要在后置处理器中使用 BeanShell PostProcesor 的时候,错误地用了前置处理器中的 Beanshell PreProcessor,导致系统报错,无法实现预期的功能,甚至是测试无法进行下去。

元件作用域

以上说的都是组件相关的东西,这里就来看看元件作用域。我们先来看一张图:

Drawing 5.png

图 7:结果树 1、2、3

在图中可以看到,我在不同位置放了 3 个一样的元件“查看结果树”(为了方便区分,我分别标记了 1、2、3)。运行后发现,查看结果树 1(图 8)里面显示了 HTTP1 和 HTTP2,而插件结果树 2 里只有 HTTP1,查看结果树 3 里面只有 HTTP2。

Drawing 6.png

图 8:查看结果树 1 的显示图

这是为什么呢?这就要说到元件作用域了。

通过截图可以发现 JMeter 元件除了从上到下的顺序外,有还具备一定的层次结构,比如图 5 中的响应断言和查看结果树,它相对于取样器存在父子组件的关系,说白了就是 HTTP 元件对取样器有效的区域,比如查看结果树 2 是 HTTP1 请求的子节点,那它就只对 HTTP1 生效;如果父节点是测试计划,那就会对测试计划下的 HTTP1 和 HTTP2 都生效。

分布式压测

压测就是 JMeter 通过产生大量线程对服务器进行访问产生负载,监听服务器返回结果并进行校验。在大部分情况下,用单台 JMeter 进行性能测试或者自动化测试是可行的,但在多线程运行过程中可能存在性能瓶颈,很多人在排查定位问题时经常会漏掉这一点。

从我的工作经验出发,单机的 JMeter 最好将线程数控制在 1000 以内;如果超过了 1000 线程,则建议使用 JMeter 分布式压测,这在一定程度上可以解决 JMeter 客户端自身形成的瓶颈问题。

在分布式 JMeter 架构下,JMeter 使用的是 Master 和 Slave。

Master

Master 负责远程控制 Slave(负载机)。分布式通常有多个 JMeter 节点,其中一个节点承担 Master 的作用。Master 通过发送信号控制节点机的启动和停止,并进行收集节点机的数据等操作。

Slave

Slave 一般也叫负载机,主要是发起线程来访问 target 服务器。一般在 Slave 节点机上先启动代理 jar 包,控制机远程连接,负载机运行脚本后对 Master 回传数据。流程示意图如下:

图片3.png

图 9:Slave 流程示意图

JMeter 的 Master 和 Slave 配置也比较简单。将 JMeter 的 bin 目录下的 jmeter.properties 文件配置:IP 和 Port 是 Slave 机的 IP 以及默认的 1099 端口。如下所示:

remote_hosts=ip:1099,ip:1099

Slave 启动 jar 包之后,默认会启动 1099 端口。Master 配置完成启动后便可以建立和 Slave 连接,从而进行控制和收集等操作。

一般来说,JMeter 分布式压测都是作为缓减客户端瓶颈的重要方式。这里我要强调“缓减”,因为在性能测试领域中不存在一种技术手段能够保证永远没有问题。随着公司的体量发展,对性能的要求也是水涨船高。JMeter 自带的分布式压测作为一种缓解客户端性能问题的方式,并不是万能法则。

总结

本讲我主要讲解了 JMeter 的核心设计理念,希望能够让你能对 JMeter 的核心概念有一定的理解。JMeter 作为目前最流行的性能测试工具,它本身提供的插件可以满足绝大多测试场景的使用,并且它也提供了二次开发的接口和 API,使用起来非常灵活。同时它分布式的使用方式也能够让你在较大程度上缓减客户端瓶颈。

不过 JMeter 自身的分布式机制只是缓减客户端瓶颈其中的一种方式,如果你有什么不一样的方法,欢迎在评论区留言分享你的心得~

下一讲我会介绍 JMeter 执行参数化的几种策略,这些策略分别使用于何种测试场景,以及参数化不合理带来的问题。


Jmeter参数化策略

上一讲我梳理了 JMeter 的核心概念,希望你能够通过课程去理解并灵活的应用到实际工作中。这一讲我会带你学习一个重要的知识点:参数化。无论是从使用频率还是从参数化对性能测试结果的影响,它都是你做性能测试必须要掌握的。

参数化是什么

简单来说,参数化就是选取不同的参数作为请求内容输入。使用 JMeter 测试时,测试数据的准备是一项重要的 工作。若要求每次传入的数据不一样,就需要进行参数化了。

为什么要进行参数化

刚才说到,若要求每次传入的数据不一样,就需要进行参数化。那为什么会有这种要求呢?我们来看两个场景。

  • 数据被缓存导致测试结果不准确

缓存原本是为了让数据访问的速度接近 CPU 的处理速度而设置的临时存储区域,比如 cache。如今缓存的概念变得更广了,很多空间都可以设置客户端缓存、CDN 缓存等等。

当你频繁地请求某一条固定的数据时,这条数据就很容易被缓存,而不是每次都从数据库中去获取,这就可能导致和真实的场景有差别。

比如大促有 10w 用户会从获取不同的商品信息,而你的压测中并没有进行充分的参数化,此时用大量线程反复请求同一件商品,极有可能访问的是缓存数据。从业务逻辑看接口返回并没有问题,但这样的场景几乎不会发生,这就会导致无效压测,测试的结果并没有多少意义。

  • 流程不能正常执行

数据被缓存可能会导致测试结果不准确,但至少业务能够走通。还有的情况是,在没有参数化的情况下,会产生大量的业务报错。

打个比方,你在测试限购商品抢购,如果用多线程模拟同一个用户操作可能会直接报错,因为在生成订单接口(支付等)都会判断是否是同一个用户。

在要求每次传入的数据不一样时,如果不进行参数化会造成很多问题,以上我列举的两个场景基本概括了没有参数化时会发生的问题,希望你能在性能测试时多加注意。下面我们就来看如何实现 JMeter 参数化。

JMeter 参数化的实现方式

我列举了 3 种比较常见的 JMeter 参数化的实现方式,你可以根据自己的需要进行选择。

  • CSV Data Set Config:将参数化的数据放入文件中,参数化读取依赖于文件操作。这样的参数化方式很常用,尤其适用于参数化数据量较多的场景,而且维护比较简单灵活。

  • User Defined Variables:一般来说可以配置脚本中的公共参数,如域名,端口号,不需要随着压测进行动态改变,比较方便环境切换

  • Function Helper 中的函数:使用函数的方式生成参数,如果你需要随机数、uuid 等都可以使用函数生成。JMeter 还提供了相应的接口给你二次开发,自定义需要的功能。

CSV Data Set Config

CSV Data Set Config 的可配置选项较多,也是目前性能测试参数化时使用最多的插件,这里我就重点介绍一下 CSV Data Set Config。

在配置组件中添加元件 CSV Data Set Config,如下图所示:

Drawing 0.png

图 1:CSV Data Set Config

我们来看一下 CSV Data Set Config 各项的含义。

文件名:顾名思义,这里填写文件的名字即可。

保存参数化数据的文件目录,我这边是将 user.csv 和脚本放置在同一路径下。在这里我要推荐一个小技巧,就是“相对路径”。使用绝对路径,在做脚本迁移时大部分情况下都需要修改。如果你是先在 Windows 或 Mac 机器上修改脚本,再将脚本上传到 Linux 服务器上执行测试的,那你就可以用相对路径,这样就不用频繁修改该选项了。

文件编码:指定文件的编码格式,设置的格式一般需要和文件编码格式一致,大部分情况下保存编码格式为 UTF-8 即可。

变量名称:对应参数文件每列的变量名,类似 Excel 文件的文件头,主要是作为后续引用的标识符,一般使用英文。如下图所示:

Drawing 1.png

图 2:user.csv

图中我标示了 name 和 password,相对应 user.csv 中的第一列和第二列。

那如何引用需要的文件数据?通过“${变量名称}”就可以了,如下图所示:

Drawing 2.png

图 3:引用演示图

忽略首行: 第一行不读取。比如图 2 的第一行我只是标示这一列是什么类型的数据,实际上并不是需要读取的业务数据,此时就可以采用忽略首行。

分隔符:用来标示参数文件中的分隔符号,与参数文件中的分隔符保持一致即可。

遇到文件结束符再次循环:是否循环读取参数文件内容。因为 CSV Data Set Config 一次读入一行,如果设置的循环次数超过文本的行数,标示为 True 就是继续再从头开始读入。

遇到文件结束符停止线程:读取到参数文件末尾时,是否停止读取线程,默认为 False。如果“遇到文件结束符再次循环”已经设置为 True 了,这个选项就没有意义了。

线程共享模式:这个适用范围是一个脚本里多线程组的情况。所有线程是默认选项,代表当前测试计划中的所有线程中的所有的线程都有效;当前线程组代表当前线程组中的线程有效;当前线程则表示当前线程有效。一般情况下,我们选择默认选项“所有线程”就好,“当前线程组”和“当前线程”很少会用到。

上面我介绍了参数化的意义以及常见用法,参数化对于性能测试是基础且必需的,因为在性能场景中,很多时候不进行参数化,脚本也是可以跑通的,所以有一些测试同学在这方面就会“偷工减料”,但这会造成性能数据不准确。下面,我们就来看一种特殊的参数化:关联。

特殊的参数化:关联

关联是将上个请求的数据提取需要的部分放到下个请求中,通过关联我们可以获取到满足业务特性的不同数据,因此可以认为是一种特殊的参数化。

关联的使用场景

我们来看一个例子,从例子中了解关联解决了什么问题。

我编写了一个查看订单接口,运行完成没有问题,并且返回正确的结果,如下所示:

{"data":{"code":0,"count":16,"items":[{"actualPrice":8900,"gmtCreate":1601448530000,"id":357,"orderNo":"1012020091448501002","skuList":[{"barCode":"150004","gmtCreate":1601448530000,"gmtUpdate":1601448530000,"id":389,"img":"https://demo.oss-cn-shenzhen.aliyuncs.com/bg/86338c9e576342baa0d079bc1caef9cc.jpg","num":1,"orderId":357,"orderNo":"1012020091448501002","originalPrice":10690,"price":8900,"skuId":2777,"spuId":1236771,"spuTitle":"昵趣 NaTruse 山羊奶配方狗狗洁齿骨 盒装 20g*40 支","title":"山羊奶","unit":"盒"}],"status":90},"msg":"第 1 页,共 1 条","pageNo":1,"pageSize":1"total":1"totalPageNo":2},"errmsg":"成功","errno":200,"timestamp":1609219480400}

一个小时之后,我再来运行这个接口时,却发现返回用户未登录:

 {"errmsg":"用户尚未登录","errno":10001,"timestamp":1609220170295}

在所有入参都没有修改的情况下为什么会出现这样的情况呢?因为你看到返回的信息是用户未登录,也就是说用户信息是无效的。

这个接口使用 Token 验证用户,Token 有效期为 15 分钟,刚刚问题产生的原因就是 Token 过期了。

那如何保证查看订单接口信息中需要的 Token 都是有效的呢?其中一个方法就是查看订单接口之前调用登录接口获取 Token,把登录接口的 Token 传入查看订单接口中。这个过程其实就是“关联”。

JMeter 如何实现关联

JMeter实现关联有 3 种方式:边界提取器,通过左右边界的方式关联需要的数据;Json Extractor提取器,针对返回的 json 数据类型;正则表达式提取器,通过正则表达式去提取数据,实现关联。

正则表达式提取器是最为常用,也是这里我要向你介绍的关联方式。我们来看下面的例子:

Drawing 3.png

图 4:正则表达式提取器

我们来看一下正则表达式提取器中每一项的含义。

  • 引用名称:自己定义的变量名称以及后续请求将要引用到的变量名。在图中我填写的是“token”,则引用方式是“${token}”。

  • 正则表达式:提取内容的正则表达式。“( )”括起来的部分就是需要提取的,“.”点号表示匹配任何字符串,“+”表示一次或多次,“?”表示找到第一个匹配项后停止。

  • 模板:用“$ $”引用,表示解析到的第几个值给 token,图 4 中的正则表达式如下:

"accesstoken":(.+?),"gender":(.+?)

$1$ 表示匹配的第一个值,即 accesstoken 后匹配后的值,模板 $2$ 则匹配 gender 后的值。图 4 演示的实例中只有 1 个 token 值,所以使用的 $1$。

  • 匹配数字:0 代表随机取值,1 代表第一个值。假设我返回数据的结构是:

[{"accesstoken":"ABDS88WDWHJEHJSHWJW","gender":null},{{"accesstoken":"NDJNW3U98SJWKISXIWN","gender":null}]

此时提取结果是一个数组,accesstoken 对应了多个值相当于数组,1 代表匹配第一个 accsstoken 的值“ABDS88WDWHJEHJSHWJW”。

  • 缺省值:正则匹配失败时的取值。比如这里我设置的是 null(token 值取不到时就会用 null 代替)。上面我们已经匹配了 token 值,在被测接口传参处直接用“${token}”就可以了。

Drawing 4.png

图 5:关联 Token

关联后就可以看到,每次都能进行正常的业务返回了。

Drawing 5.png

图 6:关联后正常业务返回

总结

这一讲我介绍了参数化策略以及使用场景。作为性能测试中最常用到的操作,你不仅要学会基本操作,还需要思考参数化策略适合的场景以及参数化数据对性能测试的影响。说到这里,我就要问一个问题了:不同的参数对性能结果会不会有影响?

举个例子,在电商系统中,你准备了不同的用户数据,用户又分为不同的等级,不同的等级可能有不一样的优惠规则和对应的优惠券,每个会员的优惠券数量可能也不一样,那这些不同的会员信息分布会对性能测试的结果有什么样的的影响?欢迎在评论区给出你的思考。

下一讲,我将带你了解如何执行有效的性能测试脚本,和你聊聊在脚本构建与执行过程当中可能存在的不规范的地方以及它们的解决方案。

构建并执行Jmeter脚本的正确姿势

通过上两讲的学习,相信你已经掌握了 JMeter 的组件结构、关联、参数化等知识,这些是你使用性能测试工具的基础,那如何才能有效地执行这些脚本呢?

说到这个话题,我回想起一些找我咨询的同学。

有些团队在组建之初往往并没有配置性能测试人员,后来随着公司业务体量的上升,开始有了性能测试的需求,很多公司为了节约成本会在业务测试团队里选一些技术能力不错的同学进行性能测试,但这些同学也是摸着石头过河。他们会去网上寻找一些做性能的方案,通常的步骤是写脚本,出结果然后交给开发。这虽然能够依葫芦画瓢地完成一些性能测试的内容,但整个过程中会存在不少值得商榷之处。

这一讲我就以脚本为切入点,和你聊聊在脚本构建与执行过程中可能存在不规范的地方有哪些,以及如何去解决。

脚本构建

脚本构建就是编写脚本,是你正式开始执行性能测试的第一步,对于常规的请求来说只需要通过界面的指引就可以完成,这个是非常容易的,但是上手容易不代表你使用方法科学,下面我带你看看常见的误区。

一个线程组、一条链路走到底

先来看下这样一张线程组的图:

Drawing 0.png

图 1:一个线程组

图中包含了注册、登录、浏览商品、查看订单等,它们在同一个线程组,基于同一线程依次进行业务。这样的做法其实和自动化非常相似。

比如张三先注册一个网站,然后进行登录、添加购物车等操作。但仔细想一想,对于一个网站的性能而言,这么考虑是有些问题的。

在正常情况下,基于同一个时间节点,一部分人在浏览商品,而另一部分人在下单。这两部分之间没有先后关系,人数占比也不一定就是 1:1。脚本中的设计思路实际上也是你对性能测试模型的理解,能够反馈出模型中的用户访问比例分布,这块内容我会在第二模块重点描述,不仅会讲述满足脚本的跑通,还会通过脚本构建基于性能模型的场景。

未提取公共部分,增加脚本管理难度

我在平时的工作中发现,有的测试会基于同一类型的 HTTP 请求,配置相同的 host、端口等,并没有很好地利用JMeter 中作用域的思想

一般全链路级别的测试脚本里可能会包含上百个接口,对于一些 host 和端口号,并不需要每一个接口都去配置,我们可以使用一个 HTTP 请求默认值去做公共部分。如果说不提取这些公共部分,每改动一个配置,所有接口可能都要改动,这样脚本维护成本工作量也会比较大,有可能会造成“牵一发而动全身”的情况。

查看结果树使用频率高

在脚本调试过程中,我们通常会添加结果树来实时查看返回数据的正确性。这个插件本身是比较消耗性能的,在正式压测中应当禁止使用。一般来说,在脚本调试中通过作用域的思想去配置一个查看结果树就可以了,不要过度使用,不然等到正式压测的时候,一个个地禁用结果树不仅会消耗时间,还容易遗漏。

脚本逻辑复杂

有的测试在编写脚本的过程中为了区分业务逻辑,会使用很多插件,比如 if 判断、循环, 这些插件虽然可以让你进入不同的业务场景,但会增加脚本的复杂度,影响发起压力的效率。你可以自己做一些对比测试,看使用该插件和去除该插件实际的处理能力相差多少,不要因为自己的脚本结构而影响实际的性能测试结果。

以上是在脚本构建时,一些普遍存在的误区,而规范的脚本构建,我认为要做到真实和精简

  • 真实在于你的脚本可以体现出真实的用户访问场景;

  • 精简在于少使用周边的插件,比如通过 JMeter 去监控服务器资源,这样的监控不仅简单粗糙,而且较大地影响 JMeter 的压力发起的效率。

脚本执行

在正确构建了脚本之后,我们就要来看如何执行脚本了。脚本执行就是你怎么去运行脚本,可能有的同学会一头雾水,我直接点击界面上的运行按钮不就行了吗?事实上真正的压测可不是这个样子的。

界面化执行性能测试

一些测试人员在 Windows 或 Mac 环境编写完脚本后,会直接用界面化的方式进行性能测试,这样的做法是非常不规范的。打开 JMeter 界面之后就会弹出提示,如图 2 所示:

Drawing 1.png

图 2:界面化性能测试提示

很多人会选择直接忽略掉,但图中的第一段是这样的:

Don't use GUI mode for load testing!only for Test creation and Test debugging。For load testing,use NON GUI Mode。

中文意思就是图形化模式只让你调试,不要进行压测

图形化的压测方式会消耗较多的客户端性能,在压测过程中容易因为客户端问题导致内存溢出。既然官方不推荐我们使用图形化界面,那我们应当如何执行测试脚本呢?

我们来看图 2 中的第三行内容:

jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]

官方早已给出答案:通过命令行执行。命令行执行的方式同样适用于 Windows、Mac 和 Linux 系统,不需要纠结系统兼容性的问题。

那既然命令行执行的方式不会造成这样的问题,为什么还要用界面化的方式呢?

相对于命令行执行,界面化的方式更为简单、方便,但命令行执行也并不是完美无缺的。

我们来回顾这段文字中的含义:

jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder]

  • -n 表示在非 GUI 模式下运行 JMeter;

  • -t 表示要运行的 JMeter 测试脚本文件,一般是 jmx 结尾的文件;

  • -l 表示记录结果的文件,默认以 jtl 结尾;

  • -e 表示测试完成后生成测试报表;

  • -o 表示指定的生成结果文件夹位置。

我们来看一下执行了上面的脚本后的实际效果,如图 3 所示:

Drawing 2.png

图 3:脚本执行后的效果

从图中可以看到,命令行的方式直接产生了总的 TPS、报错和一些时间层级的指标。命令行的执行方式规避了一些图形化界面存在的问题,但这样的结果输出方式存在 2 个问题:

  • 看不到实时的接口返回报错的具体信息;

  • 看不到混合场景下的每个接口的实时处理能力。

这 2 个问题都有个关键词是“实时”,是在压测过程中容易遗漏的点。虽然压测之后我们有很多方式可以回溯,但在性能测试过程中,发现、排查、诊断问题是必不可少的环节,它能够帮助我们以最快的速度发现问题的线索,让问题迅速得到解决。

先来看第一点:看不到实时的接口返回报错的具体信息

jmeter.log 刚刚执行过程中记录了哪些内容呢?如图 4 所示:

Drawing 3.png

图 4:jmeter.log 执行时记录的内容

你可以看到只能显示报错率,但看不到具体的报错内容,那如何去解决呢?一般我会使用 beanshell,把判定为报错的内容增加到 log 里。beanshell 的示意代码如下所示,你可以根据自己的需求改进。

String response = prev.getResponseDataAsString();
//获取接口响应信息
String code = prev.getResponseCode();
//获取接口响应状态码
if (code.equals("200")){//根据返回状态码判断
	log.info("Respnse is " + response);
    //打印正确的返回信息,建议调试使用避免无谓的性能消耗
}else {
	log.error("Error Response is"+response);
    //打印错误的返回信息
	}

这样就会自动在 jmeter.log 中打印出具体的报错信息,如图 5 所示:

Drawing 4.png

图 5:报错信息

客户端的日志只是我们需要关注的点之一,排查错误的根因还需要结合服务端的报错日志,一般来说服务端的报错日志都有相关的平台记录和查询,比较原始的方式也可以根据服务器的路径找相关日志。

我们再来看第二个问题:看不到综合场景下的每个接口的实时处理能力

我个人认为原生的实时查看结果是有些鸡肋的,如果想实时且直观地看到每个接口的处理能力,我比较推荐 JMeter+InfluxDB+Grafana 的方式,基本流程如下图所示:

Lark20210113-183606.png

图 6:JMeter+InfluxDB+Grafana

JMeter 的二次开发可以满足很多定制化的需求,但也比较考验开发的能力(关于二次开发,我会在《04 | JMeter 二次开发其实并不难 》中介绍)。如果不想进行二次开发,JMeter+InfluxDB+Grafana 也是一种比较好的实现方式了。

下面,我会对 InfluxDB 和 Grafana 做一个简单的介绍,包括特点、安装等。

InfluxDB

InfluxDB 是 Go 语言编写的时间序列数据库,用于处理海量写入与负载查询。涉及大量时间戳数据的任何用例(包括 DevOps 监控、应用程序指标等)。我认为 InfluxDB 最大的特点在于可以按照时间序列面对海量数据时候的高性能读写能力,非常适合在性能测试场景下用作数据存储。

安装

首先带你来看下 InfluxDB 具体的安装步骤(基于 CentOS 7.0),直接输入以下命令行即可:

#wget https://dl.influxdata.com/influxdb/releases/influxdb-1.1.0.x86_64.rpm
#rpm -ivh Influxdb-1.1.0.x86_64.rpm
#systemctl enable Influxdb
#systemctl start Influxdb
#systemctl status Influxdb  (查看 Influxdb 状态)
基本操作

当你已经安装完成之后,我带你了解下如何操作 InfluxDB:

#influx 
linux 命令行模式下进入数据库
#show databases
查看库
create database jmeter;
建库
use jmeter
使用该库
show measurements;
查看库下面的表

InfluxDB 成功安装并且建库之后,我们就可以来配置 JMeter 脚本了。配置过程可以分为以下 3 步。

(1)添加核心插件,在 listener 组件中选择 Backend Listener(如下图所示)。

Drawing 6.png

图 7:添加 Backkend Listenner

(2)Backend Listener implementation 中选择第二项(如下图所示)。

Drawing 7.png

图 8:Backend Listener implementation

(3)配置 InfluxDB URL,示例“http://127.0.0.1:8086/write?db=jmeter”;IP 为实际 InfluxDB 地址的 IP,DB 的值是 InfluxDB 中创建的库名字(如下图所示)。

Drawing 8.png

图 9:配置连接 influxdb 库的具体信息

配置完后运行 JMeter 脚本,再去 InfluxDB 的 JMeter 数据库中查看是否有数据,有数据则代表如上链路配置没有问题。

我们再来了解一下 Grafana。

Grafana

Grafana 是一个跨平台的开源的度量分析和可视化工具,纯 JavaScript 开发的前端工具,通过访问库(如 InfluxDB),展示自定义报表、显示图表等。大多时候用在时序数据的监控上。Grafana 功能强大、UI 灵活,并且提供了丰富的插件。

安装步骤

在 linux 命令行下直接输入以下内容即可:

#wget https://dl.grafana.com/oss/release/grafana-6.4.4-1.x86_64.rpm
#下载 granafa
#yum install  Grafana-6.4.4-1.x86_64.rpm
#安装,遇到需要输入的直接 y 就 ok;
#systemctl start Grafana-server
#systemctl enable Grafana-server
#启动 Grafana
#/etc/Grafana/Grafana.ini
配置文件路径,一般保持默认配置即可。
#systemctl  status   firewalld.service
查看防火墙状态,防止出现其他干扰问题,最好关闭
登录访问 Grafana 访问:http://127.0.1.1:3000(ip 自行替换,3000 为默认端口)
默认账号/密码:admin/admin

输入密码后如果出现了如下界面,说明 Grafana 安装成功了。

Drawing 9.png

图 10:Grafana 界面

数据源配置

为什么要配置数据源呢,简单来说就是 Grafana 需要获取数据去展示,数据源的配置就是告诉你去哪里找数据,配置安装的 InfluxDB 地址和端口号,如下图所示:

Drawing 10.png

图 11:配置地址和端口号

然后输入 InfluxDB 中写入的数据库名字,如下图所示:

Drawing 11.png

图 12:数据库名字

输入完成之后可以 Save & Test,如出现以下示意图即配置成功:

Drawing 12.png

图 13:配置成功

导入 JMeter 模板

为了达到更好的展示效果,Grafana 官网提供了针对性的展示模版。先下载 JMeter 模板,然后再导入 Grafana。

Drawing 13.png

图 14:导入 JMeter 模板

配置完成后,运行 JMeter 脚本。如果在界面右上方下拉选择 5s,则每 5s 更新一次:

Drawing 14.png

图 15:运行 JMeter 脚本

如上图便是完成了实时压测情况下运行结果的实时展示图,你可以以此为基础,进行多接口的数据采集,相应增加脚本里的 Backend Listener 插件,区分不同的 application name 名称,你会看到不同的接口数据都进入 influxdb 数据库中。并且 Grafana 从 Edit 中进入, 你可以根据不同的 application name 修改 SQL 来区分展示。

Drawing 15.png

图 16:编辑 Grafana

总结

这一讲我们主要介绍了构建和执行性能测试脚本时的一些注意事项,总结了目前业内使用 JMeter 常见的方法。你不仅需要知道这些常见的手段,也需要知道为什么要这么做,这么做有什么好处,同样随着实际采集数据指标的增高,这些做法可能还会存在哪些缺陷或者注意点,如果上述内容你都能考虑清楚了,相信你也就掌握工具了。

以上我讲到的内容,希望你可以动手实践,也许你在实践的过程中会踩一些坑,不过没关系,欢迎来评论区交流,我会和你分享我的经验和见解。

下一讲我带你走进 JMeter 的二次开发,它其实并没有你想象中那么难,到时见!


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

办公模板库 素材蛙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值