牛骨文教育服务平台(让学习变的简单)

     今天讲的这个是用来给redis数据库做性能测试的,说到性能测试,感觉这必然是高大上的操作了,redis性能测试,测的到底是哪方面的性能,如何测试,通过什么指标反映此次测试的性能好坏呢,下面我通过源码给大家做一一解答。

   redis做的性能测试时对立面的基本操作做的检测,比如Client客户端执行set,get,lpush等数据操作的性能,可以从他的测试程序可以看出:

 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);
        }

那么通过什么指标反映测试性能的好坏之分呢,在这里我们使用的就是延时性来判断,最简单的想法,就是在测试到额最开始,记录一个时间,中间执行测试操作,在操作结束在记录一个时间,中间的时间差就是执行的时间,时间越短说明性能越好。这也正是redis性能测试的做法。

/* 对指定的CMD命令做性能测试 */
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();
}

因为这样的操作要求时间精度比较高,用秒做单位肯定不行了,所以这里用的是ms毫秒,在这里添加个知识点,在这里用到了时间相关的结构体,在Linux里也存在:

/* 介绍一下struct timeval结构体
struct timeval结构体在time.h中的定义为:
struct timeval
{
  __time_t tv_sec;        // Seconds. 
  __suseconds_t tv_usec;    // Microseconds. (微秒)
}; */

那么下面是最关键的问题了,如何测,测试肯定要通过一个模拟客户端,按照配置文件中的设置去测试,所以必须存在一个配置信息,然后我们通过我们想要的配置再去逐步测试,亮出config,配置信息:

/* config配置信息结构体,static静态变量,使得全局只有一个 */
static struct config {
	//消息事件
    aeEventLoop *el;
    const char *hostip;
    int hostport;
    //据此判断是否是本地测试
    const char *hostsocket;
    //Client总数量
    int numclients;
    int liveclients;
    //请求的总数
    int requests;
    int requests_issued;
    //请求完成的总数
    int requests_finished;
    int keysize;
    int datasize;
    int randomkeys;
    int randomkeys_keyspacelen;
    int keepalive;
    int pipeline;
    long long start;
    long long totlatency;
    long long *latency;
    const char *title;
    //Client列表,这个在下面会经常提起
    list *clients;
    int quiet;
    int csv;
    //判断是否loop循环处理
    int loop;
    int idlemode;
    int dbnum;
    sds dbnumstr;
    char *tests;
    char *auth;
} config;

typedef struct _client {
	//redis上下文
    redisContext *context;
    //此缓冲区将用于后面的读写handler
    sds obuf;
    //rand指针数组
    char **randptr;         /* Pointers to :rand: strings inside the command buf */
    //randptr中指针个数
    size_t randlen;         /* Number of pointers in client->randptr */
    //randptr中没有被使用的指针个数
    size_t randfree;        /* Number of unused pointers in client->randptr */
    unsigned int written;   /* Bytes of "obuf" already written */
    //请求的发起时间
    long long start;        /* Start time of a request */
    //请求的延时
    long long latency;      /* Request latency */
    //请求的等待个数
    int pending;            /* Number of pending requests (replies to consume) */
    int selectlen;  /* If non-zero, a SELECT of "selectlen" bytes is currently
                       used as a prefix of the pipline of commands. This gets
                       discarded the first time it"s sent. */
} *client;

上面还附带了client的一些信息,这里的Client和之前的RedisClient还不是一个东西,也就是说,这里的Client会根据config结构体中的配置做相应的操作。client根据获得到命令无非2种操作,read和Write,所以在后面的事件处理中也是针对这2种事件的处理,这里给出read的方法:

/* 读事件的处理方法 */
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);

    /* Calculate latency only for the first read event. This means that the
     * server already sent the reply and we need to parse it. Parsing overhead
     * is not part of the latency, so calculate it only once, here. */
    //计算延时,然后比较延时,取得第一个read 的event事件
    if (c->latency < 0) c->latency = ustime()-(c->start);

    if (redisBufferRead(c->context) != REDIS_OK) {
    	//首先判断能否读
        fprintf(stderr,"Error: %s
",c->context->errstr);
        exit(1);
    } else {
        while(c->pending) {
            if (redisGetReply(c->context,&reply) != REDIS_OK) {
                fprintf(stderr,"Error: %s
",c->context->errstr);
                exit(1);
            }
            if (reply != NULL) {
            	//获取reply回复,如果这里出错,也会直接退出
                if (reply == (void*)REDIS_REPLY_ERROR) {
                    fprintf(stderr,"Unexpected error reply, exiting...
");
                    exit(1);
                }

                freeReplyObject(reply);
					
                if (c->selectlen) {
                    size_t j;

                    /* This is the OK from SELECT. Just discard the SELECT
                     * from the buffer. */
                    //执行到这里,请求已经执行成功,等待的请求数减1
                    c->pending--;
                    sdsrange(c->obuf,c->selectlen,-1);
                    /* We also need to fix the pointers to the strings
                     * we need to randomize. */
                    for (j = 0; j < c->randlen; j++)
                        c->randptr[j] -= c->selectlen;
                    c->selectlen = 0;
                    continue;
                }

                if (config.requests_finished < config.requests)
                    config.latency[config.requests_finished++] = c->latency;
                c->pending--;
                if (c->pending == 0) {
                	//调用客户端done完成后的方法
                    clientDone(c);
                    break;
                }
            } else {
                break;
            }
        }
    }
}

这个方法其实是一个回调方法,会作为参数传入另一个函数中,redis的函数式编程的思想又再次体现了,在这些操作都执行好了之后,会有个延时报告,针对各种操作的延时统计,这时我们就能知道,性能之间的比较了:

/* 输出请求延时 */
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 ======
", config.title);
        printf("  %d requests completed in %.2f seconds
", config.requests_finished,
            (float)config.totlatency/1000);
        printf("  %d parallel clients
", config.numclients);
        printf("  %d bytes payload
", config.datasize);
        printf("  keep alive: %d
", config.keepalive);
        printf("
");

		//将请求按延时排序
        qsort(config.latency,config.requests,sizeof(long long),compareLatency);
        for (i = 0; i < config.requests; i++) {
            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
", perc, curlat);
            }
        }
        printf("%.2f requests per second

", reqpersec);
    } else if (config.csv) {
        printf(""%s","%.2f"
", config.title, reqpersec);
    } else {
        printf("%s: %.2f requests per second
", config.title, reqpersec);
    }
}

当然你能更改配置文件,做你想做的性能测试,不过这里我想吐槽一点,这么多个if判断语句不是很好吧,换成switch也比这简洁啊:

/* Returns number of consumed options. */
/* 根据读入的参数,设置config配置文件 */
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));
		
		//通过多重if判断,但个人感觉不够优美,稳定性略差
        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],"-a") ) {
            if (lastarg) goto invalid;
            config.auth = 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]);
//......省略

redis的性能测试还能支持本地测试和指定Ip,端口测试,挺方便的。下面列出全部的API:

/* Prototypes */
/* 方法原型 */
static void createMissingClients(client c); /* 创建没有Command命令要求的clint */
static long long ustime(void) /* 返回当期时间的单位为微秒的格式 */
static long long mstime(void) /* 返回当期时间的单位为毫秒的格式 */
static void freeClient(client c) /* 释放Client */
static void freeAllClients(void) /* 释放所有的Client */
static void resetClient(client c) /* 重置Client */
static void randomizeClientKey(client c) /* 随机填充client里的randptr中的key值 */
static void clientDone(client c) /* Client完成后的调用方法 */
static void readHandler(aeEventLoop *el, int fd, void *privdata, int mask) /* 读事件的处理方法 */
static void writeHandler(aeEventLoop *el, int fd, void *privdata, int mask) /* 写事件方法处理 */
static client createClient(char *cmd, size_t len, client from) /* 创建一个基准的Client */
static int compareLatency(const void *a, const void *b) /* 比较延时 */
static void showLatencyReport(void) /* 输出请求延时 */
static void benchmark(char *title, char *cmd, int len) /* 对指定的CMD命令做性能测试 */
int parseOptions(int argc, const char **argv) /* 根据读入的参数,设置config配置文件 */
int showThroughput(struct aeEventLoop *eventLoop, long long id, void *clientData) /* 显示Request执行的速度,简称RPS */
int test_is_selected(char *name) /* 检测config中的命令是否被选中 */