Redis2.8源码之性能压测工具

333 阅读8分钟

性能测试工具源码

本小节将全面解读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: 10.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_inlineping_mbulkset/getincrlpush/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: 10.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: 10.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: 10.00% <= 1 milliseconds
94.05% <= 2 milliseconds
99.70% <= 3 milliseconds
100.00% <= 3 milliseconds
44033.47 requests per second
​
​

可以看到,上面的命令中通过-t选项控制只压测Redissetgetincr命令。对于该选项值的接下在前面提到的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->randptrc->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服务端每秒能处理的平均请求数。