当前位置: 首页 > 科技观察

如何使用k6进行性能测试

时间:2023-03-16 17:08:44 科技观察

作者|屈迅、涂嘉瑶项目背景项目目标是向客户交付一个ToCapp,其后端基于RESTful微服务架构,后端同样使用Protobuf协议以提高传输效率。在最终上线之前,我们需要进行性能测试,以确定系统在正常和预期的峰值负载条件下的性能,从而识别应用程序的最大运行能力和存在的瓶颈,优化性能问题以改善用户体验。性能测试是一项相对复杂的工作,包括确定性能测试目标、工具选择、脚本开发、CI集成、结果分析、性能调优等过程,需要QA、Dev、DevOps的配合。本文将详细描述这一系列过程。为什么选择k6在了解到我们需要做性能测试之后,我们开始对性能测试做一些研究。在看了一些比较性能测试工具的文章后,我们最终选择了k6,并对locust和Gatling做了进一步的比较。下面是比较的结果。对于我们来说,k6的优势在于:k6支持TypeScript。由于项目已经有使用TypeScript的经验,所以这个工具的学习成本相对要少一些;k6本身支持metrics的输出,可以满足大部分metrics的需求。可定制;k6官方支持与各种CI工具和数据可视化系统集成,开箱即用;Gatling支持Scala/Java/Kotlin,项目没有用到相关技术栈,需要跟客户申请,成本高于k6。手写第一个案例有了上面的基础,我们开始尝试在项目中集成k6。当我们选择一个简单的API来编写第一个案例时,我们发现需要解决以下挑战:挑战1-获取AccessToken并保证token的时效性由于当前项目的API集成了OAuth,所以任何操作都必须有一个有效的用户和Accesstoken,所以需要提前生成token和测试数据。这部分会因为项目不同而有些差异,需要具体分析。本次测试具体包括以下内容:用户账号准备,比如生成200个用户,进行一系列的预处理,使其成为正常的测试账号,需要根据项目安全规范保存到合适的时间.可以重复使用帐户的地方,例如AWSSecretsManager或AWSParameterStore。token生成,在运行测试之前,生成最新的有效token,执行测试时只需要读取token数据。token刷新,因为token基本上是时效性的,如果有效期很短,就需要考虑renewtoken,这里我们使用refreshtoken来获取新的accesstoken。需要注意的是,测试时的refreshtoken会包含在request中,对性能测试数据会有轻微影响,需要考虑refresh机制。挑战2-Protobuf数据编解码下图简要说明了前后端的架构。Mobile和BFF以Protobuf格式交换数据,BFF和Backend以Json格式交换数据。我们的性能测试是针对BFF的,所以需要将请求数据按照项目中定义的Protobuf格式进行编码,然后发送给BFF。在收到BFF的响应数据时,我们还需要根据Protobuf定义的响应格式进行解码,从而解析出想要的数据。你想要的数据。另外,由于性能测试使用的是TypeScript语言,所以我们还需要将Protobuf文件编译成TS版本。这是Protobuf官方文档给出的解决方案,可以轻松生成TS版代码。由于每个API的编解码结构都是一个单独的proto,所以也涉及到代码复用的问题。需要设计一种合适的方法,使得不同的API只需要提供相应的编码和解码模式即可。解决了前两个挑战后,就可以初步得到一个符合项目需求的测试框架。├──protobuffile/---protobuf文件├──dist/---ts转js测试文件└──src/├──command/---一些脚本文件├──config/---配置文件├──httpClient/---http客户端├──ProtobufSchema/---编译后的protobuf文件├──test/---测试用例└──testAccount/---测试账号优化项目&集成CI&可视化报表测试用例设计时,当测试用例的数量逐渐增多时,我们对测试用例进行了多次调整,比如对API进行分类,以不同的方式对其进行性能测试。独立API独立API是指不依赖其他接口提供参数输入就可以完成请求的API,比如一些GetAPI。Non-independentAPI非独立API是指依赖于其他API的结果作为参数输入来完成请求的API,比如一些Put和DeleteAPI。由于此类API依赖于其他API的结果数据,因此无法独立进行性能测试。在本次性能测试中,将这些非独立的API以整体旅程的形式进行测试,将上一步的结果传递给测试用例中的下一步,从而完成整体行程测试。下面用一个例子来说明我们的测试用例目录结构如下:└──test├──orderService│├──createOrder││├──createOrderRequestBuilder.ts││├──createOrderRequestClient.ts││└──createOrderTest.ts│├──getOrders││├──getOrdersRequestClient.ts││└──getOrdersTest.ts│├──orderJourney││└──orderJourneyTest.ts│──└──updateOderRequest.Order│└──updateOrderRequestClient.ts├──payService└──userService其中:对于createOrder,getOrders是一个独立的API,可以方便的单独调用API直接测试;对于updateOder,它取决于createOrder的结果,所以我们将它们组合起来在Journey中测试,createOrder->getOrder->updateOrder可以组合在orderJourneyTest中。K6的执行器选择k6提供了多个执行器,不同的执行器会以不同的方式执行测试。我们可以根据项目的需要选择不同的executor来执行测试。让性能测试跑在CI-Integrated上TeamCityk6官方提供了目前主流CI工具的Howto文档,非常好用。唯一需要注意的是需要手动设置阈值。当性能结果不达标时,k6会返回非零值,让CI知道测试失败。展示报表——集成NewRelic(一)数据采集k6支持多种数据可视化工具,如Datadog、NewRelic、Grafana等,添加一个参数即可轻松搞定。我们使用NewRelic,通过配置K6_STATSD_ENABLE_TAGS=true,可以方便的通过k6提供的标签对数据进行分类,对不同API和Journey的性能数据进行分类统计。(2)指标的展示指标的展示主要是在数据可视化平台上,通过自定义各种图表来展示性能指标(3)指标的校验这里其实就是对上面的指标进行校验,确保我们设置的指标是准确的,以为后续的性能分析做准备测试执行&结果分析和调优测试执行在执行一个测试的时候,我们需要分析影响性能的因素,并尽量控制变量,以便对多个执行结果进行比较分析,例如在Executeonpipeline减少网络影响,定期检查数据库数据量,关注K8spod数量等。结合我们项目的特点,总结出以下几个因素:(1)数据库数据量我们的系统在架构上比较简单明了,后端使用AWSDynamoDB,所以数据量会比较大对性能的影响,尤其是查询类,计算类API,这里需要了解用户每个维度的数据量,比如每个月,每天等。(2)请求体的大小主要是为了发布和放置接口。因为涉及到文件上传,所以文件大小也会对性能产生很大的影响。需要了解正常用户使用场景下附件的大小范围(3)K8spod数量,开启HPA会触发AutoScaling测试,发现性能不稳定。后来发现UAT环境启用了HPA,会触发AutoScaling。因此,在进行测试时,需要考虑不同的场景:测试固定pod下的性能,方便优化和比较AutoScaling策略的有效性。(4)网络影响这是一个比较普遍的问题。测试过程中要注意网络变化对性能指标的影响,防止变量过多,性能数据分析不准确。(5)不同API的性能存在较大差距。这主要是设计用例时需要考虑的。K6会统计所有的请求数据,导致API之间相互影响,数据失真:比如通过token获得的数据也会被统计,导致实际的业务接口数据。另外像delete这样的接口依赖create。如果两个API一起测试,createAPI的性能数据与deleteAPI相差甚远,导致delete接口的数据严重失真。可以通过tag过滤,获取单个API的一些数据,比如响应时间,还是有意义的,比如rps数据,如果两者一起跑,主要靠create,所以采集到的rps是不使删除很有意义。(6)多个后端API之间的交互,比如文件上传对性能的影响由于我们有BFF和BE,BFF会结合多个BE,所以需要识别多个BE之间的交互,尽量保证准确测试目标并减少其他API的影响。比如准备单独测试一个服务时,可以考虑不添加文件,以免干扰文件服务。结果分析和优化对于结果分析,k6本身提供了丰富的指标供查看,我们也集成了NewRelic。因此,两者可以结合起来进行数据收集、分析和调优。原图链接:https://k6.io/docs/static/f9df206f5a86e9b4c59d2bdb6a9e351f/485a2/new-relic-dashboard.webp如上图所示,NewRelic可以将收集到的数据以图表的形式展示出来,我们可以根据您的需要定制报告。这里不仅可以使用k6采集的数据,还可以叠加一些APM数据,比如CPU、Memory、Pod数量等。用鼠标在横坐标上定位一个点,就可以清楚的看到那一刻对应的并发数、总请求数、响应时间、失败率等数据。另外,在进行测试的时候,我们在控制变量的前提下进行横向比较,比较相同配置下类似API的性能数据。如果数据差异很大,可以进行进一步调查。也可以利用工具对请求进行深入的排查,拆解请求中各个模块的耗时,找到最终的原因。这里有两个例子来说明这个过程。案例一——一个获取配置信息的API这个API的逻辑比较简单,主要是读取一些配置信息,然后做一些简单的处理返回。运行测试后,http_req_duration的平均值约为1s,平均rps约为108,最高VU为300,说明此时已满用户,还有0.7%的误差。其他需要查询数据库的API在相同设置下,http_req_duration仅为23ms,rps为204,最高VU为76。这个API只是拉取一些配置信息,没有其他太复杂的操作,也不需要访问数据库。很明显,这个性能数据是不正常的,所以我先带着Dev查了一下逻辑,发现是配置文件内容的缓存逻辑有问题。每次请求都会读取配置文件,导致性能数据异常。修改后,相同配置下,http_req_duration为12ms,平均rps为145,最高VU为50,错误率为0。显然,这个数据表明我们可以继续提高Rate。当Rate加到500时,平均http_req_duration还是12ms,最大VU也只有80,依然没有达到瓶颈。由此可见修改后的性能提升非常明显。Case2-某个getAPI这个API是一个get类型的API,它的职责是从数据库中获取一个值,不需要任何其他额外的操作。运行测试后,http_req_duration的平均值约为320ms。把duration的结果和其他getAPI横向比较是非常不合理的。但是k6只是给出了最终的运行结果,我们无法从这些结果中得知具体问题出在哪里。幸运的是,newrelic提供了一些具体的API信息,其中一个提供了详细的API调用流程以及每个流程的具体耗时。由于项目的安全需求,这里以newrelic提供的图为例。原图链接:https://docs.newrelic.com/static/distributed-tracing-trace-details-page-1c064ef6a7607f95be583786b6af9251.png从图中可以清楚的看到API的服务调用流程图和不同的services相互调用的次数。并且可以清楚的看到每个步骤所花费的时间,从而找到最耗时的步骤调用。最后根据这张图,我们发现我们只是去数据库取回了一个值,但是由于错误的实现方式,导致数据库与数据库之间调用了200多次。这使得响应时间可达320毫秒。重新编码后,API响应时间缩短至20ms,性能提升15倍。最后写到这个性能测试很复杂,不是一两个人就能完成的。作为QA,我们可以主导事情的发生,成为主要承办者。要及时提出问题和寻求帮助。通过团队的协作尽快解决问题,最终顺利完成性能测试任务。