性能测试工具源码
本小节将全面解读redis-benchmark.c中的源码,掌握一个性能测试工具的实现。
1. 从main()开始
对于redis-benchmark命令而言,同样是从main()函数开始,内容如下:
// ...
int main(int argc, const char **argv) {
int i;
char *data, *cmd;
int len;
client c;
srandom(time(NULL));
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
config.numclients = 50;
config.requests = 10000;
config.liveclients = 0;
// 1. 创建事件循环实例,同时创建超时事件,每隔250ms显示压测结果
config.el = aeCreateEventLoop(1024*10);
aeCreateTimeEvent(config.el,1,showThroughput,NULL,NULL);
config.keepalive = 1;
config.datasize = 3;
config.pipeline = 1;
config.randomkeys = 0;
config.randomkeys_keyspacelen = 0;
config.quiet = 0;
config.csv = 0;
config.loop = 0;
config.idlemode = 0;
config.latency = NULL;
config.clients = listCreate();
config.hostip = "127.0.0.1";
config.hostport = 6379;
config.hostsocket = NULL;
config.tests = NULL;
config.dbnum = 0;
i = parseOptions(argc,argv); // 2. 解析参数
argc -= i;
argv += i;
// 给所有的请求预留记录延迟事件的空间
config.latency = zmalloc(sizeof(long long)*config.requests);
if (config.keepalive == 0) {
// 打印告警
// ...
}
if (config.idlemode) {
// 打印告警
// ...
c = createClient("",0,NULL); /* will never receive a reply */
createMissingClients(c);
aeMain(config.el);
/* and will wait for every */
}
// 3. 经过parseOptions()函数处理后,如果还有多余参数,会将后面的参数值作为性能测试的参数
if (argc) {
sds title = sdsnew(argv[0]);
for (i = 1; i < argc; i++) {
title = sdscatlen(title, " ", 1);
title = sdscatlen(title, (char*)argv[i], strlen(argv[i]));
}
do {
// 同样会将剩余参数作为压测命令执行,cmd是得到要发送的命令
len = redisFormatCommandArgv(&cmd,argc,argv,NULL);
benchmark(title,cmd,len); // 进行压测
free(cmd);
} while(config.loop);
return 0;
}
// 4. 调用性能测试案例
do {
data = zmalloc(config.datasize+1);
memset(data,'x',config.datasize);
data[config.datasize] = '\0';
if (test_is_selected("ping_inline") || test_is_selected("ping"))
benchmark("PING_INLINE","PING\r\n",6);
if (test_is_selected("ping_mbulk") || test_is_selected("ping")) {
len = redisFormatCommand(&cmd,"PING");
benchmark("PING_BULK",cmd,len);
free(cmd);
}
if (test_is_selected("set")) {
len = redisFormatCommand(&cmd,"SET key:__rand_int__ %s",data);
benchmark("SET",cmd,len);
free(cmd);
}
if (test_is_selected("get")) {
len = redisFormatCommand(&cmd,"GET key:__rand_int__");
benchmark("GET",cmd,len);
free(cmd);
}
if (test_is_selected("incr")) {
len = redisFormatCommand(&cmd,"INCR counter:__rand_int__");
benchmark("INCR",cmd,len);
free(cmd);
}
if (test_is_selected("lpush")) {
len = redisFormatCommand(&cmd,"LPUSH mylist %s",data);
benchmark("LPUSH",cmd,len);
free(cmd);
}
if (test_is_selected("lpop")) {
len = redisFormatCommand(&cmd,"LPOP mylist");
benchmark("LPOP",cmd,len);
free(cmd);
}
if (test_is_selected("sadd")) {
len = redisFormatCommand(&cmd,
"SADD myset element:__rand_int__");
benchmark("SADD",cmd,len);
free(cmd);
}
if (test_is_selected("spop")) {
len = redisFormatCommand(&cmd,"SPOP myset");
benchmark("SPOP",cmd,len);
free(cmd);
}
if (test_is_selected("lrange") ||
test_is_selected("lrange_100") ||
test_is_selected("lrange_300") ||
test_is_selected("lrange_500") ||
test_is_selected("lrange_600"))
{
len = redisFormatCommand(&cmd,"LPUSH mylist %s",data);
benchmark("LPUSH (needed to benchmark LRANGE)",cmd,len);
free(cmd);
}
if (test_is_selected("lrange") || test_is_selected("lrange_100")) {
len = redisFormatCommand(&cmd,"LRANGE mylist 0 99");
benchmark("LRANGE_100 (first 100 elements)",cmd,len);
free(cmd);
}
if (test_is_selected("lrange") || test_is_selected("lrange_300")) {
len = redisFormatCommand(&cmd,"LRANGE mylist 0 299");
benchmark("LRANGE_300 (first 300 elements)",cmd,len);
free(cmd);
}
if (test_is_selected("lrange") || test_is_selected("lrange_500")) {
len = redisFormatCommand(&cmd,"LRANGE mylist 0 449");
benchmark("LRANGE_500 (first 450 elements)",cmd,len);
free(cmd);
}
if (test_is_selected("lrange") || test_is_selected("lrange_600")) {
len = redisFormatCommand(&cmd,"LRANGE mylist 0 599");
benchmark("LRANGE_600 (first 600 elements)",cmd,len);
free(cmd);
}
if (test_is_selected("mset")) {
const char *argv[21];
argv[0] = "MSET";
for (i = 1; i < 21; i += 2) {
argv[i] = "key:__rand_int__";
argv[i+1] = data;
}
len = redisFormatCommandArgv(&cmd,21,argv,NULL);
benchmark("MSET (10 keys)",cmd,len);
free(cmd);
}
if (!config.csv) printf("\n");
} while(config.loop);
return 0;
}
上述第1处源码主要是创建Redis的事件循环实例el,然后创建了超时事件并加入到该循环实例el中。该超时事件的回调函数showThroughput()用于打印本次压测结果,其实现源码如下:
// ...
int showThroughput(struct aeEventLoop *eventLoop, long long id, void *clientData) {
REDIS_NOTUSED(eventLoop);
REDIS_NOTUSED(id);
REDIS_NOTUSED(clientData);
if (config.csv) return 250;
float dt = (float)(mstime()-config.start)/1000.0; // 计算整个请求的处理时间
float rps = (float)config.requests_finished/dt; // 计算Redis每秒处理的请求数
printf("%s: %.2f\r", config.title, rps);
fflush(stdout); // 刷新保持打印结果
return 250; /* every 250ms */
}
// ...
从showThroughput()函数的返回值可知该函数每250ms会被调用一次,而该函数主要是根据压测结果计算本次对Redis的压测性能,即每秒处理的请求数。
第2处代码和前面介绍redis-cli命令一样,通过调用parseOptions()函数解析redis-benchmark的相关参数。该函数的实现源码如下:
// ...
int parseOptions(int argc, const char **argv) {
int i;
int lastarg;
int exit_status = 1;
for (i = 1; i < argc; i++) {
lastarg = (i == (argc-1));
// 从这里可以看到redis-benchmark支持的所有选项
if (!strcmp(argv[i],"-c")) {
if (lastarg) goto invalid;
config.numclients = atoi(argv[++i]);
} else if (!strcmp(argv[i],"-n")) {
if (lastarg) goto invalid;
config.requests = atoi(argv[++i]);
} else if (!strcmp(argv[i],"-k")) {
if (lastarg) goto invalid;
config.keepalive = atoi(argv[++i]);
} else if (!strcmp(argv[i],"-h")) {
if (lastarg) goto invalid;
config.hostip = strdup(argv[++i]);
} else if (!strcmp(argv[i],"-p")) {
if (lastarg) goto invalid;
config.hostport = atoi(argv[++i]);
} else if (!strcmp(argv[i],"-s")) {
if (lastarg) goto invalid;
config.hostsocket = strdup(argv[++i]);
} else if (!strcmp(argv[i],"-d")) {
if (lastarg) goto invalid;
config.datasize = atoi(argv[++i]);
if (config.datasize < 1) config.datasize=1;
if (config.datasize > 1024*1024*1024) config.datasize = 1024*1024*1024;
} else if (!strcmp(argv[i],"-P")) {
if (lastarg) goto invalid;
config.pipeline = atoi(argv[++i]);
if (config.pipeline <= 0) config.pipeline=1;
} else if (!strcmp(argv[i],"-r")) {
if (lastarg) goto invalid;
config.randomkeys = 1;
config.randomkeys_keyspacelen = atoi(argv[++i]);
if (config.randomkeys_keyspacelen < 0)
config.randomkeys_keyspacelen = 0;
} else if (!strcmp(argv[i],"-q")) {
config.quiet = 1;
} else if (!strcmp(argv[i],"--csv")) {
config.csv = 1;
} else if (!strcmp(argv[i],"-l")) {
config.loop = 1;
} else if (!strcmp(argv[i],"-I")) {
config.idlemode = 1;
} else if (!strcmp(argv[i],"-t")) {
if (lastarg) goto invalid;
config.tests = sdsnew(",");
config.tests = sdscat(config.tests,(char*)argv[++i]);
config.tests = sdscat(config.tests,",");
sdstolower(config.tests);
} else if (!strcmp(argv[i],"--dbnum")) {
if (lastarg) goto invalid;
config.dbnum = atoi(argv[++i]);
config.dbnumstr = sdsfromlonglong(config.dbnum);
} else if (!strcmp(argv[i],"--help")) {
exit_status = 0;
goto usage;
} else {
if (argv[i][0] == '-') goto invalid;
return i;
}
}
return i;
invalid:
printf("Invalid option "%s" or option argument missing\n\n",argv[i]);
usage:
// 打印帮助信息
// ...
exit(exit_status);
}
// ...
从parseOptions()函数的源码中可以看到redis-benchmark命令所支持的所有选项,这些选项的值会更新到config结构体变量中的各个成员变量中,比如指定模拟压测的客户端数、总请求数等。
main()中的第3处代码主要是处理redis-benchmark的多余参数,即在经过parseOptions()函数处理后还剩下相关参数,那剩余参数的处理将直接被当作压测命令执行。请看下面的示例:
[root@server2 redis2.8]# ./redis-benchmark -c 100 -n 100000 set hello world
====== test set hello world ======
100000 requests completed in 2.43 seconds
100 parallel clients
3 bytes payload
keep alive: 1
0.09% <= 1 milliseconds
87.62% <= 2 milliseconds
99.26% <= 3 milliseconds
99.81% <= 4 milliseconds
99.90% <= 20 milliseconds
99.92% <= 22 milliseconds
100.00% <= 22 milliseconds
41186.16 requests per second
[root@server2 redis2.8]# ./redis-cli get hello
"world"
可以看到,上述命令在将执行10万次set hello world命令后打印统计结果并退出。而这其中压测的关键函数正是benchmark()。
后续第4处代码的核心同样是调用benchmark()执行压测操作,其中默认压测多种命令,包括ping_inline、ping_mbulk、set/get、incr、lpush/lpop等。这里的代码中还出现了一个test_is_selected()函数,该函数通过命令外部输入的选项-t来指定要测试的命令。例如下面的操作:
[root@server2 redis2.8]# ./redis-benchmark -c 100 -n 100000 -t set,get,incr
====== SET ======
100000 requests completed in 2.63 seconds
100 parallel clients
3 bytes payload
keep alive: 1
0.07% <= 1 milliseconds
83.18% <= 2 milliseconds
91.89% <= 3 milliseconds
93.09% <= 4 milliseconds
97.86% <= 5 milliseconds
99.78% <= 6 milliseconds
99.82% <= 7 milliseconds
99.93% <= 8 milliseconds
100.00% <= 8 milliseconds
38022.81 requests per second
====== GET ======
100000 requests completed in 2.48 seconds
100 parallel clients
3 bytes payload
keep alive: 1
0.06% <= 1 milliseconds
84.50% <= 2 milliseconds
99.74% <= 3 milliseconds
99.95% <= 4 milliseconds
99.97% <= 5 milliseconds
99.97% <= 28 milliseconds
100.00% <= 28 milliseconds
40371.42 requests per second
====== INCR ======
100000 requests completed in 2.27 seconds
100 parallel clients
3 bytes payload
keep alive: 1
0.00% <= 1 milliseconds
94.05% <= 2 milliseconds
99.70% <= 3 milliseconds
100.00% <= 3 milliseconds
44033.47 requests per second
可以看到,上面的命令中通过-t选项控制只压测Redis的set、get和incr命令。对于该选项值的接下在前面提到的parseOptions()函数中:
} else if (!strcmp(argv[i],"-t")) {
if (lastarg) goto invalid; // -t选项后面必须有值
config.tests = sdsnew(","); //
config.tests = sdscat(config.tests,(char*)argv[++i]); // 在一前以后追加逗号
config.tests = sdscat(config.tests,",");
sdstolower(config.tests); // 全部转成小写
}
接着test_is_selected()函数的实现源码入选:
// ...
int test_is_selected(char *name) {
char buf[256];
int l = strlen(name);
if (config.tests == NULL) return 1; // 如果没有设置-t选项及其值,永远返回True
buf[0] = ',';
memcpy(buf+1,name,l); // 拷贝name的字符串内容到buf指定位置
buf[l+1] = ','; // 在要搜索的name字符串前后加上逗号
buf[l+2] = '\0'; // 下面是调用strstr()函数判断config.tests中是否出现了buf
return strstr(config.tests,buf) != NULL;
}
// ...
注意到对于前面示例命令中-t选项的值来说,最终得到的 config.tests值为sds(",set,get,incr,")。接着在test_is_selected()函数中,如果传入的name = "set",那么在test_is_selected()函数内部将调用strstr()函数在字符串",set,get,incr,"中搜索字符串buf = ",set,",这将返回非0值。因此,对应该set命令的压测工作将被执行。
2. benchmark()函数
benchmark()函数的实现源码如下:
// ...
static void benchmark(char *title, char *cmd, int len) {
client c;
config.title = title;
config.requests_issued = 0;
config.requests_finished = 0;
c = createClient(cmd,len,NULL); // 创建客户端
createMissingClients(c);
config.start = mstime();
aeMain(config.el);
config.totlatency = mstime()-config.start; // 计算总时间
showLatencyReport(); // 显示延迟结果
freeAllClients(); // 释放空间
}
// ...
上面的源码是不是非常简单?那为何如此简单的代码就能实现模拟多个客户端对Redis进行压测呢?这里有几个非常关键的函数,它们分别是:createClient()、createMissingClients()以及aeMain()函数。aeMain()函数内部会开始执行事件循环,借助IO多路复用机制等待事件发送以及处理超时事件。那文件事件的监听是在哪里完成的呢?没错,正是在前面两个函数中。先看createClient()函数的实现源码,内容如下:
// ...
static client createClient(char *cmd, size_t len, client from) {
int j;
client c = zmalloc(sizeof(struct _client)); // 分配空间
// 1. 建立与Redis服务端连接,然后得到连接套接字并保存在c->context->fd中
if (config.hostsocket == NULL) {
c->context = redisConnectNonBlock(config.hostip,config.hostport);
} else {
c->context = redisConnectUnixNonBlock(config.hostsocket);
}
if (c->context->err) {
// 错误请看处理
// ...
}
c->context->reader->maxbuf = 0;
c->obuf = sdsempty(); // 保存要发送Redis服务端的字符串
if (config.dbnum != 0) {
// 2. 如果选择数据库非默认,需要发送SELECT命令选择使用数据库
c->obuf = sdscatprintf(c->obuf,"*2\r\n$6\r\nSELECT\r\n$%d\r\n%s\r\n",
(int)sdslen(config.dbnumstr),config.dbnumstr);
c->selectlen = sdslen(c->obuf); // select命令字符串的长度
} else {
c->selectlen = 0; // 否则selectlen成员变量设置为0,表明c->obuf中只有命令字符串
}
if (from) { // 3. 是否从其他客户端复制过来,主要是从c->obuf中获取要发送的命令字符串
c->obuf = sdscatlen(c->obuf,
from->obuf+from->selectlen,
sdslen(from->obuf)-from->selectlen); // 先要去掉原来的select命令字符串
} else {
for (j = 0; j < config.pipeline; j++)
c->obuf = sdscatlen(c->obuf,cmd,len); // 这里直接给c->obuf追加命令字符串
}
c->written = 0; // 已向服务端写入的字节数
c->pending = config.pipeline; // 是否开启流水线方式
c->randptr = NULL; // 记录随机字符串位置
c->randlen = 0; // 随机字符串个数
if (c->selectlen) c->pending++; // 记录命令中多了select命令
// 4. 如果设置了-r选项,则需要记录c->obuf中出现的随机key,记录结果保存在c->randptr中
if (config.randomkeys) {
if (from) {
c->randlen = from->randlen;
c->randfree = 0;
c->randptr = zmalloc(sizeof(char*)*c->randlen);
/* copy the offsets. */
for (j = 0; j < c->randlen; j++) {
c->randptr[j] = c->obuf + (from->randptr[j]-from->obuf);
/* Adjust for the different select prefix length. */
c->randptr[j] += c->selectlen - from->selectlen;
}
} else {
char *p = c->obuf;
c->randlen = 0;
c->randfree = RANDPTR_INITIAL_SIZE;
c->randptr = zmalloc(sizeof(char*)*c->randfree);
// 记录c->obuf中包含__rand_int__的个数及位置
while ((p = strstr(p,"__rand_int__")) != NULL) {
if (c->randfree == 0) {
c->randptr = zrealloc(c->randptr,sizeof(char*)*c->randlen*2);
c->randfree += c->randlen;
}
c->randptr[c->randlen++] = p; // 记录__rand__int__的位置
c->randfree--;
p += 12; /* 12 is strlen("__rand_int__). */
}
}
}
// 5. 添加对套接字c->context->fd的读事件的监听
aeCreateFileEvent(config.el,c->context->fd,AE_WRITABLE,writeHandler,c);
// 6. 将该客户端c加入到config.clients链表中
listAddNodeTail(config.clients,c);
config.liveclients++; // 记录客户端数的成员变量加1
return c;
}
// ...
整体上看createClient()的逻辑并不复杂,主要有6处代码需要说明。其中最重要的当属第5处代码,这里会调用了aeCreateFileEvent()函数将第1处代码中与Redis服务端建立的套接字c->context->fd的写事件加入epoll中进行监听。当有数据向Redis服务端写入时,将回调这里的writeHandler()函数。
第2处代码主要是处理请求数据库不是默认数据库时(即使用--dbnum选项指定数据库),需要在客户端的输入缓冲区(c->obuf)中先添加SELECT的命令字符串,同时会对c->selectlen设置为该SELECT命令字符串的长度。如果没有选择数据库的选项,则c->selectlen设置为0。
第3处代码中会继续对c->obuf追加传入的命令字符串cmd,该命令字符串是最终要发送给Redis端执行的命令。而该命令字符串通常是普通的Redis命令调用redisFormatCommand()函数转换而来,它最终会得到该Redis命令真正发送给Redis服务端的字符串形式(带上\r\n以及命令和参数的长度信息)。
第4处代码是当redis-bencnmark命令带上-r选项后需要进行的一些列操作,主要是扫描前面得到的要发送给Redis服务端的命令字符串,记录随机字符串__rand__int__在c->obuf中的位置信息,相关结果将保存在c->randptr和c->randlen中。
第6处代码是将本次生成的请求客户端变量c记录到config.clients中并将config中的成员变量liveclients的值加1,以此记录活跃的客户端数。
继续看createMissingClients()函数的实现源码,内容如下:
// ...
static void createMissingClients(client c) {
int n = 0;
char *buf = c->obuf;
size_t buflen = sdslen(c->obuf); // 从原来的客户端中获取要发送的缓冲区数据
if (c->selectlen) { // 跳过select命令字符串,因为在下面的createClient()中会添加该select
buf += c->selectlen;
buflen -= c->selectlen;
}
while(config.liveclients < config.numclients) {
createClient(NULL,0,c); // 创建模拟创建多个客户端
if (++n > 64) {
usleep(50000); // 50ms
n = 0;
}
}
}
// ...
createMissingClients()函数的最核心是根据指定的客户端数调用createClient()函数模拟创建客户端,而创建客户端则是调用前面介绍的createClient()方法。在创建客户端之前,需要先清除原c的缓冲区的select命令字符串,其原因在于调用createClient()函数中会根据config.dbnum来添加select命令字符串,所以为了避免重复,需要在调用createClient()之前先将原来存在的select命令字符串去掉。
3. writeHandler()函数
从前面的分析可知,在createClient()或者createMissingClients()函数中,在创建了与Redis服务端连接的套接字后,最核心的就是将这些连接后的套接字的写事件加入监听。相关写事件的回调函数为writeHandler()函数。以下是该函数的实现源码:
// ...
static void writeHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
client c = privdata;
REDIS_NOTUSED(el);
REDIS_NOTUSED(fd);
REDIS_NOTUSED(mask);
/* Initialize request when nothing was written. */
if (c->written == 0) {
/* Enforce upper bound to number of requests. */
if (config.requests_issued++ >= config.requests) {
freeClient(c);
return;
}
if (config.randomkeys) randomizeClientKey(c); // 给命令字符串中的随机字符串添加随机值
c->start = ustime();
c->latency = -1;
}
if (sdslen(c->obuf) > c->written) {
void *ptr = c->obuf+c->written;
// 发送客户端数据
int nwritten = write(c->context->fd,ptr,sdslen(c->obuf)-c->written);
if (nwritten == -1) {
if (errno != EPIPE)
fprintf(stderr, "Writing to socket: %s\n", strerror(errno));
freeClient(c);
return;
}
c->written += nwritten;
if (sdslen(c->obuf) == c->written) {
// 如果数据全部写完,删除写事件而添加读事件
aeDeleteFileEvent(config.el,c->context->fd,AE_WRITABLE);
aeCreateFileEvent(config.el,c->context->fd,AE_READABLE,readHandler,c);
}
}
}
// ...
从writeHandler()函数中可以看到,最终保存在缓冲区c->obuf内的命令字符串将通过write()系统调用发送给建立连接的Redis服务端。在数据发送完毕后除了删除本次fd的写事件外,还要将该fd的读事件加入监听以接收Redis服务对该客户端的响应。因此,接下来的学习重点就是readHandler()函数。
4. readHandler()函数
由上分析可知,redis-benchmark命令中在模拟了多个客户端的多路发送数据后,将由readHandler()函数处理服务端的响应数据。以下是该函数的实现源码:
// ...
static void readHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
client c = privdata; // 保存关联的客户端信息
void *reply = NULL;
REDIS_NOTUSED(el);
REDIS_NOTUSED(fd);
REDIS_NOTUSED(mask);
if (c->latency < 0) c->latency = ustime()-(c->start); // 计算本次请求的延迟时间
if (redisBufferRead(c->context) != REDIS_OK) {
fprintf(stderr,"Error: %s\n",c->context->errstr);
exit(1);
} else {
// 处理Redis服务的响应数据
while(c->pending) {
if (redisGetReply(c->context,&reply) != REDIS_OK) {
fprintf(stderr,"Error: %s\n",c->context->errstr);
exit(1);
}
if (reply != NULL) {
if (reply == (void*)REDIS_REPLY_ERROR) {
fprintf(stderr,"Unexpected error reply, exiting...\n");
exit(1);
}
freeReplyObject(reply);
if (c->selectlen) { // 如果发送的请求有select命令字符串
int j;
// 返回结果中先取出select命令的结果
c->pending--;
sdsrange(c->obuf,c->selectlen,-1);
for (j = 0; j < c->randlen; j++)
c->randptr[j] -= c->selectlen;
c->selectlen = 0;
continue;
}
// 如果本次客户端请求成功数小于onfig.requests,将记录该请求的延迟
if (config.requests_finished < config.requests)
config.latency[config.requests_finished++] = c->latency;
c->pending--;
if (c->pending == 0) {
clientDone(c); // 关键处理,这里会继续打开fd的写事件而关闭fd的读事件
break;
}
} else {
break;
}
}
}
}
// ...
readHandler()函数的逻辑也不复杂,主要是通过redisGetReply()函数来处理响应结果,而该函数同样是通过read()系统调用接收数据并根据Redis的传输协议解析响应数据。要注意的是由于请求的命令字符串中可能包含多个命令,如select命令等,因此会有一处专门处理该命令结果的代码段。
最后对于本次请求会记录其属于当前客户端完成的第几次请求(config.requests_finished),然后更新请求的响应延迟数据到config.latency数组的对应位置上。那redis-benchmark命令的-n选项设置的控制每个客户端总请求数的代码在哪呢?答案在clientDone()函数中,该函数的实现源码如下:
// ...
static void clientDone(client c) {
// 当所有客户端完成的请求数等于配置中设置的总请求数时,会停止事件循环
if (config.requests_finished == config.requests) {
freeClient(c);
aeStop(config.el);
return;
}
if (config.keepalive) {
resetClient(c);
} else {
config.liveclients--; // 结束活跃的客户端数
createMissingClients(c); // 再次继续创建剩余客户端
config.liveclients++;
freeClient(c);
}
}
// ...
从clientDone()函数的开头代码可以看到,当所有客户端完成的请求数等于配置中设置的总请求数时,会停止事件循环。否则会继续创建客户端,继续连接Redis服务端并监听该连接套接字的读事件,如此往复。
5. 小结
跟踪完redis-benchmark.c中的源码后,读者将对该工具的理解会进一步加深。从该工具的源码中,我们可以看到redis-benchmark.c内部是如何借助实现模拟多个客户端请求以及如何限定总的请求数。最后,从源码中还可以看到该工具是如何统计每个请求的延迟以及计算出Redis服务端每秒能处理的平均请求数。