这篇文章总结的是Redis的benchmark源码修改中增加一些性能测试指标所应思考的问题,以及解决问题的方法,权当本渣渣一篇笔记,如果您发现有疏漏错误之处,烦请留言指出!
如不清楚吞吐率(RPS)、平均响应时间、99%响应时间的概念请参考:性能测试的几个指标(并发数、吞吐率、响应时间、平均响应时间、99%响应时间)
在原生的Redis benchmark性能测试中,只提供了两种主要的测试指标:
一是RPS(requests per second),即每秒完成的请求数量,反映的是服务器处理请求的吞吐率,这是redis最主要的性能测试指标。
二是,当你不使用quiet模式输出(不使用-q参数)时,redis会展示出响应时间低于1ms、2ms、3ms...等的请求数占总请求数的百分比。如下图所示。
下面我们分别说明以上两种测试指标的实现方式:
首先我们需要知道benchmark.c文件的一个重要的数据结构static struct config。
本文所讲的性能测试指标的最红显示和实现都是在redis-benchmark.c文件中的showLatencyReport函数中实现,所有修改也都在此函数中进行。
static struct config {
aeEventLoop *el;
const char *hostip;
int hostport;
const char *hostsocket;
int numclients;
int liveclients;
int requests;//总请求数
int requests_issued;
int requests_finished;//已完成的请求数
int keysize;
int datasize;
int randomkeys;//1表示key值随机化
int randomkeys_keyspacelen;//key值取0~该值中的随机数
int keepalive;
int pipeline;
int showerrors;
long long start;//性能测试前记录开始时间
long long totlatency;//记录性能测试所用的总延时
long long *latency;//算是一个数组指针,在堆空间开辟,存放每一笔请求的延时,一共有config.requests个
const char *title;
list *clients;
int quiet;
int csv;
int loop;
int idlemode;
int dbnum;
sds dbnumstr;
char *tests;
char *auth;
} config;
主要关注requests、 start、totlatency、*latency!
(1)RPS的实现:总请求数 / 性能测试的总延时, 即
reqpersec = (float)config.requests_finished/((float)config.totlatency/1000);
(2)响应时间低于1ms、2ms、3ms...等的请求数占总请求数的百分比实现:先对latency数组中的所有请求的响应时间进行从小到大排序,然后遍历,当遍历到响应时间低于1ms时计算响应百分比即可,以此类推。
原生Redis实现的代码如下(以上两种指标的实现)
//定义比较函数
static int compareLatency(const void *a, const void *b) {
return (*(long long*)a)-(*(long long*)b);
}
static void showLatencyReport(void) {
int i, curlat = 0;
float perc, reqpersec;
reqpersec = (float)config.requests_finished/((float)config.totlatency/1000);
if (!config.quiet && !config.csv) {
printf("====== %s ======\n", config.title);
printf(" %d requests completed in %.2f seconds\n", config.requests_finished,
(float)config.totlatency/1000);
printf(" %d parallel clients\n", config.numclients);
printf(" %d bytes payload\n", config.datasize);
printf(" keep alive: %d\n", config.keepalive);
printf("\n");
qsort(config.latency,config.requests,sizeof(long long),compareLatency);//对每条请求的响应时间进行升序排序
for (i = 0; i < config.requests; i++) {//打印出低于1ms、2ms...等的请求数与总请求数的百分比
if (config.latency[i]/1000 != curlat || i == (config.requests-1)) {
curlat = config.latency[i]/1000;
perc = ((float)(i+1)*100)/config.requests;
printf("%.2f%% <= %d milliseconds\n", perc, curlat);
}
}
printf("%.2f requests per second\n\n", reqpersec);//打印rps
} else if (config.csv) {
printf("\"%s\",\"%.2f\"\n", config.title, reqpersec);
} else {
printf("%s: %.2f requests per second\n", config.title, reqpersec);
}
平均响应时间=总响应时间÷总请求数, 公式很简单,但是我们深入思考会发现一些问题:刚开始时我把总响应时间误以为就是上面的totlatency,后来发现不对,这里说清楚区别:
totlatency是在benchmark测试前后做的一个时间记录差值,即相当于服务器处理完requests条请求所花费的时间,这里面也包含了一部分客户端的函数执行的时间开销(我测试时使用单个客户端连接(即只有一个并发连接)、不使用管道pipelining的时候,测试出来的totlatency是大于【把latency数组中所有请求的响应时间相加所得sum】)。
所以把latency数组中所有请求的响应时间相加所得sum(我们姑且称之为响应时间之和),跟延迟差值totlatency是不一样的!
响应时间之和是所有请求的响应时间加到一块,而totlatency只是测试开始前后的一个时间差!但是你千万别以为响应时间之和就应该小于或者等于totlatency(因为你在测试过程中也有客户端的一些函数时间执行开销),我刚开始就是这样认为的!这种情况仅仅是当只有一个并发连接,并且不使用pipeline的时候,它确实是这样的!
当不止一个并发连接的时候,客户端怎么操作的呢? 假如说有10个并发连接客户端,那么这十个客户端就会逐个发送请求,哪个客户端收到请求回复了就再接着发请求,一直到请求数全部得到回复。这里模拟的是10个客户端并发连接请求的情况。所以这样你会发现:最后算的响应时间之和是远远大于totlatency的!
当使用pipelining的时候呢?假如-P的参数是5,即一条管道可以一次放5条请求。使用管道其实就是每次客户端把5个请求打包到一起然后一块发给服务器进行处理,服务器处理完之后把五个请求的结果再打包到一起返回给客户端。所以五条请求的响应时间是一样的!那这个时候五条请求的响应时间之和怎么算呢?是五个请求的响应时间全加起来,还是只算一个响应时间呢? 自习想一样,应该是把5个请求看做一个整体,他们的响应时间之和就是其中一个的响应时间。为什么这样算呢?很明显人家五个一起花了5ms,你不能说人家五个一共花了25ms吧! 比如说5个人团购一张券吃饭花了5块,那么每个人实质上是只用了1块,五个人一共用5块,而不是25块。所以当我们使用管道的时候,求平均响应时间和99%响应时间的时候需要整体除以管道大小pipeline。
先排序,然后找到99%所在的位置,把对应位置的响应时间记录即可。
修改后的实现代码
static void showLatencyReport(void) {
int i, curlat = 0;
float perc, reqpersec;
reqpersec = (float)config.requests_finished/((float)config.totlatency/1000);
if (!config.quiet && !config.csv) {
printf("====== %s ======\n", config.title);
printf(" %d requests completed in %.2f seconds\n", config.requests_finished,
(float)config.totlatency/1000);
printf(" %d parallel clients\n", config.numclients);
printf(" %d bytes payload\n", config.datasize);
printf(" keep alive: %d\n", config.keepalive);
printf("\n");
qsort(config.latency,config.requests,sizeof(long long),compareLatency);//对每条请求的响应时间进行升序排序
long long sumLatency = 0;//响应时间之和
float avg, percentile99 = (float)config.latency[config.requests - 1]/1000;//平均响应时间、99%响应时间(初始化为最后一个请求的延迟),单位ms
int flag = 0;//临时辅助标志位
for (i = 0; i < config.requests; i++) {
sumLatency += config.latency[i];//requests条命令的响应时间总和
if(flag == 0 && (float)(i + 1)/config.requests >= (float)0.99) {
percentile99 = (float)config.latency[i]/1000;//99%响应时间
flag = 1;
}
if (config.latency[i]/1000 != curlat || i == (config.requests-1)) {//打印出低于1ms、2ms...等的请求数与总请求数的百分比
curlat = config.latency[i]/1000;
perc = ((float)(i+1)*100)/config.requests;
printf("%.2f%% <= %d milliseconds\n", perc, curlat);
}
}
avg = (float)sumLatency/((float)config.requests*1000);//requests条请求的平均响应时间,单位是ms
if(config.pipeline > 1) {//考虑使用管道的特殊情况
avg /= config.pipeline;
percentile99 /= config.pipeline;
}
//printf("总延迟: %lldms 响应时间之和: %lldms\n", config.totlatency, sumLatency/1000);
printf("%.2f requests per second | average response time: %.2fms | 99%%response time: %.2fms \n\n", reqpersec, avg, percentile99);
} else if (config.csv) {
printf("\"%s\",\"%.2f\"\n", config.title, reqpersec);
} else {
printf("%s: %.2f requests per second\n", config.title, reqpersec);
}
}