性能测试工具 基于线上请求的性能测试系统 CPC

胡刚 · May 06, 2016 · Last by jacexh replied at June 15, 2016 · 3823 hits
本帖已被设为精华帖!

1.背景

测试人员在设计性能测试脚本时,HTTP请求中的参数往往根据个人经验设置,而测试人员水平参差不齐,设计往往具有局限性,不够全面,不能涵盖全线上真实的请求,故得到的性能测试结果不能够真实反映线上真实的情况。

使用线上环境下的HTTP请求检查软件性能的问题,通过Gor记录线上真实的请求,作为性能测试脚本的请求池,用请求池物料进行性能测试,能真实的反映软件系统在线上环境下的性能指标和问题。

2.概念

2-1.架构图

这里写图片描述

2-2.技术栈

请求池:

Gor:

HTTP 录制工具 https://github.com/buger/gor

Webdis:

A very simple web server providing an HTTP interface to Redis https://github.com/nicolasff/webdis

redis:

持久化缓存

性能测试工具:

nGrinder二次开发:

http://blog.csdn.net/neven7/article/details/50740018

Spring MVC

链路可视化:

watchman(微博APM)

influxDB:

时序化DB https://github.com/influxdata/influxdb

Grafana:

可视化工具 https://github.com/grafana/grafana

3.实现

3-1.请求池

使用Gor录制线上请求,根据线上请求,序列化成Json String, 持久化到redis;性能测试脚本根据key,获取到线上请求数据,进行压测。

为了方便部署请求池,将请求池docker化,使用如下命名,启动docker容器:

docker run -i -t --net=host gor-request-parser /bin/bash

开始录制线上数据:

sh gor_request_parser.sh 8080 60 GET your_api_name NONBASIC ip port

参数介绍:
8080:监听端口
60:监听时间(秒)
GETHTTP METHOD
your_api_name:过滤其他url,只保留your_api_name请求
NONBASIC:非BASIC认证接口,参数BASICBASIC接口
ip portwebdis服务

60s后, 请求数据持久化到redis中

***************************************************
gor http请求 序列化
http请求记录中,60 秒后,终止记录
***************************************************
Version:

***************************************************
记录结束, http请求序列化到请求池
@author hugang
***************************************************

redis的key为$date_$hostname_$url

value为Json Array,形如:

这里写图片描述

3-2.性能测试脚本

性能测试工具使用nGrinder,进行二次开发,请参考:http://blog.csdn.net/neven7/article/details/50740018
性能测试脚本使用redis获取线上请求数据,依赖jedis、fastJson,在脚本lib中导入这2个jar包。

范例:

package org.ngrinder;

import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.plugin.http.HTTPRequest
import net.grinder.plugin.http.HTTPPluginControl;
import net.grinder.script.GTest
import net.grinder.script.Grinder
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
// import static net.grinder.util.GrinderUtils.* // You can use this if you're using nGrinder after 3.2.3
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith

import HTTPClient.HTTPResponse
import HTTPClient.NVPair


import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import groovy.json.JsonBuilder
import groovy.json.JsonSlurper

import com.alibaba.fastjson.JSONArray;
import redis.clients.jedis.Jedis;

/**
* A simple example using the HTTP plugin that shows the retrieval of a
* single page via HTTP.
*
* This script is automatically generated by ngrinder.
*
* @author hugang
*/

@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
public static File file
public static JSONArray jsonArray

@BeforeProcess
public static void beforeProcess() {
HTTPPluginControl.getConnectionDefaults().timeout = 6000
test = new GTest(1, "压测ip")
request = new HTTPRequest()
test.record(request);

// 读取请求池数据
Jedis jedis = new Jedis("redis ip", redis port);
// redis key
String key = "your key";
String jsonStr = jedis.get(key);

jsonArray = JSONArray.parseArray(jsonStr);
// grinder.logger.info(jsonArray.getString(0));
// grinder.logger.info("before process.");
}

@BeforeThread
public void beforeThread() {
grinder.statistics.delayReports=true;
}


@Test
public void test(){
// 随机获取
int index = (int) (Math.random() * jsonArray.size());
String httpInfo = jsonArray.getString(index);


def json = new JsonSlurper().parseText(httpInfo)

String api = json.api
Map param = json.param

def nvs = []
param.each{
key, value -> nvs.add(new NVPair(key, value))
}


// GET请求,wiki http://grinder.sourceforge.net/g3/script-javadoc/net/grinder/plugin/http/HTTPRequest.html
// param1: uri, param2: queryData
// HTTPResponse GET(java.lang.String uri, NVPair[] queryData) Makes an HTTP GET request.
HTTPResponse result = request.GET("http://压测ip" + api, nvs as NVPair[])
if (result.statusCode == 301 || result.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
} else {
assertThat(result.statusCode, is(200));
// 请求返回的数据
// println(result.text);
// 定义一个事务,接口返回数据校验,是否包含
assertThat(result.text, containsString("\"code\""));
}
}
}


3-3. 链路时间分布可视化

链路时间展示,启动web服务指定-javaagent为watchman agent,使用字节码增强,获取某一段时间内代码链路的分布时间。
请参考:http://blog.csdn.net/neven7/article/details/50980726

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 14 条回复 时间 点赞

其实这思路就是将线上的真实请求在测试环境回放吧

我们的做法是将线上请求日志回放,支持回放倍率调节,这样可以较准确的预测业务增长对服务器的影响

胡刚 #3 · May 07, 2016 作者

#2楼 @jacexh 角度不一样,直接流量回放是粗粒度的,性能数据不好统计,我们这边也有,是使用TCPCopy直接放大线上流量(开发和运维操作),一般通过监控系统观察错误和响应时间或者通过代码打点统计日志数据;CPC是细粒度的,性能测试脚本直接通过线上的请求数据压测,通过不同的qps模拟正常流量和峰值流量下各项性能指标。

#3楼 @neven7 TCPCopy是直接的流量复制,的确像你所说的,是粗粒度的,而且较难统计,我所提到的基于日志回放的性能测试框架不是这种简单的回放。
CPC依赖Gor,但据我以前的经验,Gor无法录制https,你们怎么处理?
另外你给出的例子基本都是Get请求,那对于非幂等的接口,不能重用请求参数,该怎么处理?

胡刚 #5 · May 07, 2016 作者

#4楼 @jacexh 1.https的问题,Gor确实是不能录制https,一种折中的方式是进行压测时,临时将服务https改成http;CPC现在不支持https协议,业务暂时不需要支持https。

  1. 上行接口(POST),如果压测的系统资源是线上资源,不会录制上行接口,因为会写花数据,这种只能用测试数据模拟请求;如果压测的系统资源是测试资源,可以录制上行接口; 你说的非匿等接口,不能重用请求参数,这种接口只能临时生成满足的参数。

#5楼 @neven7 比较关注,线上的数据隔离,读写数据如何处理的,环境是全应用,还是切实例或者分组?压力源与被压是否同机房?
针对 主从或者多个从环境是否不变,依赖接口有写操作 如何处理?

帮转一个公众号文章上的留言:

这个工具性能怎么样?如果要支持百万级的下单接口,需要多少负载机啊?

胡刚 #8 · May 11, 2016 作者

#6楼 @taki 问题1.数据隔离,其实可以通过接口参数标识,微博接口都有个来源参数,压测时用压测source,不会影响线上的一些策略统计(忽略压测source);读数据可以用线上录制的参数;写数据,环境如果是线上,只能用测试数据模拟调用; 问题2.同机房最好了,但是在同一个内网内就可以了; 问题3.“主从或者多个从环境是否不变” 没理解意思

胡刚 #9 · May 11, 2016 作者

#7楼 @chenhengjie123 性能还是可以的,当然nGrinder工具本身是用java写的,并发量很高时是很耗内存,这就要求安装的机器配置要很高;启web容器时,堆的大小也要尽量大点,单个nGrinder controller万级是可以支持的(实践已经到了万级),如果搭建controller集群几十万级的量还是可以(理论,没校验);百万级的,我所了解,业内还没有这种工具;你如果只是想得出接口性能数据,其实不一定要模拟到百万级,你算出均摊到单台前端机的qps,直接压测单台。接口日均pv * (80%/20%)/(24 * 3600)/前端机总数=单台前端qps峰值

胡刚 #10 · May 12, 2016 作者

#9楼 @neven7 更正一下9楼,百万级并发的数人云做到了,是用tsung,他们用数千个docker容器模拟并发,可参考http://m.it168.com/article_2532243.html,但是并发量达到了,他们的性能数据怎么产生和搜集,文章中没有细节,其实也最关注结果,并发量那么大收集数据也是个瓶颈。

#8楼 @neven7 线上都有应急预案,一般数据库 可能是 一主一从,或者一主多从,读写分离,我想问的是,在压测的过程中,是否还是用正式环境的持久化,还是 摘出了一个数据库或者是另外搭建一套

胡刚 #12 · May 12, 2016 作者

#11楼 @taki 还是保持一致,线上真实场景;如果单独拆,成本高,又不真实;当然前提是有完善的监控,随时暂停压测。

#2楼 @jacexh 请问一下,流浪放大针对唯一性的入参数据这块是怎么处理的呢?

#13楼 @taki 在采集参数、拷贝参数、回放阶段都预设了hook,利用这些hook将被复制的参数再做处理,使其能够唯一

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up