1 /* Redis benchmark utility. 2 * 3 * Copyright (c) 2009-2010, Salvatore Sanfilippo <antirez at gmail dot com> 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * * Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * * Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * * Neither the name of Redis nor the names of its contributors may be used 15 * to endorse or promote products derived from this software without 16 * specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 22 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28 * POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 #include "fmacros.h" 32 33 #include <stdio.h> 34 #include <string.h> 35 #include <stdlib.h> 36 #include <unistd.h> 37 #include <errno.h> 38 #include <sys/time.h> 39 #include <signal.h> 40 #include <assert.h> 41 42 #include "ae.h" 43 #include "hiredis.h" 44 #include "sds.h" 45 #include "adlist.h" 46 #include "zmalloc.h" 47 48 #define REDIS_NOTUSED(V) ((void) V) 49 50 static struct config { 51 aeEventLoop *el; 52 const char *hostip; 53 int hostport; 54 const char *hostsocket; 55 int numclients; 56 int requests; 57 int liveclients; 58 int donerequests; 59 int keysize; 60 int datasize; 61 int randomkeys; 62 int randomkeys_keyspacelen; 63 int keepalive; 64 long long start; 65 long long totlatency; 66 long long *latency; 67 const char *title; 68 list *clients; 69 int quiet; 70 int loop; 71 int idlemode; 72 } config; 73 74 typedef struct _client { 75 redisContext *context; 76 sds obuf; 77 char *randptr[10]; /* needed for MSET against 10 keys */ 78 size_t randlen; 79 unsigned int written; /* bytes of 'obuf' already written */ 80 long long start; /* start time of a request */ 81 long long latency; /* request latency */ 82 } *client; 83 84 /* Prototypes */ 85 static void writeHandler(aeEventLoop *el, int fd, void *privdata, int mask); 86 static void createMissingClients(client c); 87 88 /* Implementation */ 89 static long long ustime(void) { 90 struct timeval tv; 91 long long ust; 92 93 gettimeofday(&tv, NULL); 94 ust = ((long)tv.tv_sec)*1000000; 95 ust += tv.tv_usec; 96 return ust; 97 } 98 99 static long long mstime(void) { 100 struct timeval tv; 101 long long mst; 102 103 gettimeofday(&tv, NULL); 104 mst = ((long)tv.tv_sec)*1000; 105 mst += tv.tv_usec/1000; 106 return mst; 107 } 108 109 static void freeClient(client c) { 110 listNode *ln; 111 aeDeleteFileEvent(config.el,c->context->fd,AE_WRITABLE); 112 aeDeleteFileEvent(config.el,c->context->fd,AE_READABLE); 113 redisFree(c->context); 114 sdsfree(c->obuf); 115 zfree(c); 116 config.liveclients--; 117 ln = listSearchKey(config.clients,c); 118 assert(ln != NULL); 119 listDelNode(config.clients,ln); 120 } 121 122 static void freeAllClients(void) { 123 listNode *ln = config.clients->head, *next; 124 125 while(ln) { 126 next = ln->next; 127 freeClient(ln->value); 128 ln = next; 129 } 130 } 131 132 static void resetClient(client c) { 133 aeDeleteFileEvent(config.el,c->context->fd,AE_WRITABLE); 134 aeDeleteFileEvent(config.el,c->context->fd,AE_READABLE); 135 aeCreateFileEvent(config.el,c->context->fd,AE_WRITABLE,writeHandler,c); 136 c->written = 0; 137 } 138 139 static void randomizeClientKey(client c) { 140 char buf[32]; 141 size_t i, r; 142 143 for (i = 0; i < c->randlen; i++) { 144 r = random() % config.randomkeys_keyspacelen; 145 snprintf(buf,sizeof(buf),"%012zu",r); 146 memcpy(c->randptr[i],buf,12); 147 } 148 } 149 150 static void clientDone(client c) { 151 if (config.donerequests == config.requests) { 152 freeClient(c); 153 aeStop(config.el); 154 return; 155 } 156 if (config.keepalive) { 157 resetClient(c); 158 } else { 159 config.liveclients--; 160 createMissingClients(c); 161 config.liveclients++; 162 freeClient(c); 163 } 164 } 165 166 static void readHandler(aeEventLoop *el, int fd, void *privdata, int mask) { 167 client c = privdata; 168 void *reply = NULL; 169 REDIS_NOTUSED(el); 170 REDIS_NOTUSED(fd); 171 REDIS_NOTUSED(mask); 172 173 /* Calculate latency only for the first read event. This means that the 174 * server already sent the reply and we need to parse it. Parsing overhead 175 * is not part of the latency, so calculate it only once, here. */ 176 if (c->latency < 0) c->latency = ustime()-(c->start); 177 178 if (redisBufferRead(c->context) != REDIS_OK) { 179 fprintf(stderr,"Error: %s\n",c->context->errstr); 180 exit(1); 181 } else { 182 if (redisGetReply(c->context,&reply) != REDIS_OK) { 183 fprintf(stderr,"Error: %s\n",c->context->errstr); 184 exit(1); 185 } 186 if (reply != NULL) { 187 if (reply == (void*)REDIS_REPLY_ERROR) { 188 fprintf(stderr,"Unexpected error reply, exiting...\n"); 189 exit(1); 190 } 191 192 if (config.donerequests < config.requests) 193 config.latency[config.donerequests++] = c->latency; 194 clientDone(c); 195 } 196 } 197 } 198 199 static void writeHandler(aeEventLoop *el, int fd, void *privdata, int mask) { 200 client c = privdata; 201 REDIS_NOTUSED(el); 202 REDIS_NOTUSED(fd); 203 REDIS_NOTUSED(mask); 204 205 /* When nothing was written yet, randomize keys and set start time. */ 206 if (c->written == 0) { 207 if (config.randomkeys) randomizeClientKey(c); 208 c->start = ustime(); 209 c->latency = -1; 210 } 211 212 if (sdslen(c->obuf) > c->written) { 213 void *ptr = c->obuf+c->written; 214 int nwritten = write(c->context->fd,ptr,sdslen(c->obuf)-c->written); 215 if (nwritten == -1) { 216 if (errno != EPIPE) 217 fprintf(stderr, "Writing to socket: %s\n", strerror(errno)); 218 freeClient(c); 219 return; 220 } 221 c->written += nwritten; 222 if (sdslen(c->obuf) == c->written) { 223 aeDeleteFileEvent(config.el,c->context->fd,AE_WRITABLE); 224 aeCreateFileEvent(config.el,c->context->fd,AE_READABLE,readHandler,c); 225 } 226 } 227 } 228 229 static client createClient(const char *cmd, size_t len) { 230 client c = zmalloc(sizeof(struct _client)); 231 if (config.hostsocket == NULL) { 232 c->context = redisConnectNonBlock(config.hostip,config.hostport); 233 } else { 234 c->context = redisConnectUnixNonBlock(config.hostsocket); 235 } 236 if (c->context->err) { 237 fprintf(stderr,"Could not connect to Redis at "); 238 if (config.hostsocket == NULL) 239 fprintf(stderr,"%s:%d: %s\n",config.hostip,config.hostport,c->context->errstr); 240 else 241 fprintf(stderr,"%s: %s\n",config.hostsocket,c->context->errstr); 242 exit(1); 243 } 244 c->obuf = sdsnewlen(cmd,len); 245 c->randlen = 0; 246 c->written = 0; 247 248 /* Find substrings in the output buffer that need to be randomized. */ 249 if (config.randomkeys) { 250 char *p = c->obuf, *newline; 251 while ((p = strstr(p,":rand:")) != NULL) { 252 newline = strstr(p,"\r\n"); 253 assert(newline-(p+6) == 12); /* 12 chars for randomness */ 254 assert(c->randlen < (signed)(sizeof(c->randptr)/sizeof(char*))); 255 c->randptr[c->randlen++] = p+6; 256 p = newline+2; 257 } 258 } 259 260 redisSetReplyObjectFunctions(c->context,NULL); 261 aeCreateFileEvent(config.el,c->context->fd,AE_WRITABLE,writeHandler,c); 262 listAddNodeTail(config.clients,c); 263 config.liveclients++; 264 return c; 265 } 266 267 static void createMissingClients(client c) { 268 int n = 0; 269 270 while(config.liveclients < config.numclients) { 271 createClient(c->obuf,sdslen(c->obuf)); 272 273 /* Listen backlog is quite limited on most systems */ 274 if (++n > 64) { 275 usleep(50000); 276 n = 0; 277 } 278 } 279 } 280 281 static int compareLatency(const void *a, const void *b) { 282 return (*(long long*)a)-(*(long long*)b); 283 } 284 285 static void showLatencyReport(void) { 286 int i, curlat = 0; 287 float perc, reqpersec; 288 289 reqpersec = (float)config.donerequests/((float)config.totlatency/1000); 290 if (!config.quiet) { 291 printf("====== %s ======\n", config.title); 292 printf(" %d requests completed in %.2f seconds\n", config.donerequests, 293 (float)config.totlatency/1000); 294 printf(" %d parallel clients\n", config.numclients); 295 printf(" %d bytes payload\n", config.datasize); 296 printf(" keep alive: %d\n", config.keepalive); 297 printf("\n"); 298 299 qsort(config.latency,config.requests,sizeof(long long),compareLatency); 300 for (i = 0; i < config.requests; i++) { 301 if (config.latency[i]/1000 != curlat || i == (config.requests-1)) { 302 curlat = config.latency[i]/1000; 303 perc = ((float)(i+1)*100)/config.requests; 304 printf("%.2f%% <= %d milliseconds\n", perc, curlat); 305 } 306 } 307 printf("%.2f requests per second\n\n", reqpersec); 308 } else { 309 printf("%s: %.2f requests per second\n", config.title, reqpersec); 310 } 311 } 312 313 static void benchmark(const char *title, const char *cmd, int len) { 314 client c; 315 316 config.title = title; 317 config.donerequests = 0; 318 319 c = createClient(cmd,len); 320 createMissingClients(c); 321 322 config.start = mstime(); 323 aeMain(config.el); 324 config.totlatency = mstime()-config.start; 325 326 showLatencyReport(); 327 freeAllClients(); 328 } 329 330 /* Returns number of consumed options. */ 331 int parseOptions(int argc, const char **argv) { 332 int i; 333 int lastarg; 334 int exit_status = 1; 335 336 for (i = 1; i < argc; i++) { 337 lastarg = (i == (argc-1)); 338 339 if (!strcmp(argv[i],"-c")) { 340 if (lastarg) goto invalid; 341 config.numclients = atoi(argv[++i]); 342 } else if (!strcmp(argv[i],"-n")) { 343 if (lastarg) goto invalid; 344 config.requests = atoi(argv[++i]); 345 } else if (!strcmp(argv[i],"-k")) { 346 if (lastarg) goto invalid; 347 config.keepalive = atoi(argv[++i]); 348 } else if (!strcmp(argv[i],"-h")) { 349 if (lastarg) goto invalid; 350 config.hostip = strdup(argv[++i]); 351 } else if (!strcmp(argv[i],"-p")) { 352 if (lastarg) goto invalid; 353 config.hostport = atoi(argv[++i]); 354 } else if (!strcmp(argv[i],"-s")) { 355 if (lastarg) goto invalid; 356 config.hostsocket = strdup(argv[++i]); 357 } else if (!strcmp(argv[i],"-d")) { 358 if (lastarg) goto invalid; 359 config.datasize = atoi(argv[++i]); 360 if (config.datasize < 1) config.datasize=1; 361 if (config.datasize > 1024*1024) config.datasize = 1024*1024; 362 } else if (!strcmp(argv[i],"-r")) { 363 if (lastarg) goto invalid; 364 config.randomkeys = 1; 365 config.randomkeys_keyspacelen = atoi(argv[++i]); 366 if (config.randomkeys_keyspacelen < 0) 367 config.randomkeys_keyspacelen = 0; 368 } else if (!strcmp(argv[i],"-q")) { 369 config.quiet = 1; 370 } else if (!strcmp(argv[i],"-l")) { 371 config.loop = 1; 372 } else if (!strcmp(argv[i],"-I")) { 373 config.idlemode = 1; 374 } else if (!strcmp(argv[i],"--help")) { 375 exit_status = 0; 376 goto usage; 377 } else { 378 /* Assume the user meant to provide an option when the arg starts 379 * with a dash. We're done otherwise and should use the remainder 380 * as the command and arguments for running the benchmark. */ 381 if (argv[i][0] == '-') goto invalid; 382 return i; 383 } 384 } 385 386 return i; 387 388 invalid: 389 printf("Invalid option \"%s\" or option argument missing\n\n",argv[i]); 390 391 usage: 392 printf("Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]\n\n"); 393 printf(" -h <hostname> Server hostname (default 127.0.0.1)\n"); 394 printf(" -p <port> Server port (default 6379)\n"); 395 printf(" -s <socket> Server socket (overrides host and port)\n"); 396 printf(" -c <clients> Number of parallel connections (default 50)\n"); 397 printf(" -n <requests> Total number of requests (default 10000)\n"); 398 printf(" -d <size> Data size of SET/GET value in bytes (default 2)\n"); 399 printf(" -k <boolean> 1=keep alive 0=reconnect (default 1)\n"); 400 printf(" -r <keyspacelen> Use random keys for SET/GET/INCR, random values for SADD\n"); 401 printf(" Using this option the benchmark will get/set keys\n"); 402 printf(" in the form mykey_rand000000012456 instead of constant\n"); 403 printf(" keys, the <keyspacelen> argument determines the max\n"); 404 printf(" number of values for the random number. For instance\n"); 405 printf(" if set to 10 only rand000000000000 - rand000000000009\n"); 406 printf(" range will be allowed.\n"); 407 printf(" -q Quiet. Just show query/sec values\n"); 408 printf(" -l Loop. Run the tests forever\n"); 409 printf(" -I Idle mode. Just open N idle connections and wait.\n"); 410 exit(exit_status); 411 } 412 413 int showThroughput(struct aeEventLoop *eventLoop, long long id, void *clientData) { 414 REDIS_NOTUSED(eventLoop); 415 REDIS_NOTUSED(id); 416 REDIS_NOTUSED(clientData); 417 418 float dt = (float)(mstime()-config.start)/1000.0; 419 float rps = (float)config.donerequests/dt; 420 printf("%s: %.2f\r", config.title, rps); 421 fflush(stdout); 422 return 250; /* every 250ms */ 423 } 424 425 int main(int argc, const char **argv) { 426 int i; 427 char *data, *cmd; 428 int len; 429 430 client c; 431 432 signal(SIGHUP, SIG_IGN); 433 signal(SIGPIPE, SIG_IGN); 434 435 config.numclients = 50; 436 config.requests = 10000; 437 config.liveclients = 0; 438 config.el = aeCreateEventLoop(); 439 aeCreateTimeEvent(config.el,1,showThroughput,NULL,NULL); 440 config.keepalive = 1; 441 config.donerequests = 0; 442 config.datasize = 3; 443 config.randomkeys = 0; 444 config.randomkeys_keyspacelen = 0; 445 config.quiet = 0; 446 config.loop = 0; 447 config.idlemode = 0; 448 config.latency = NULL; 449 config.clients = listCreate(); 450 config.hostip = "127.0.0.1"; 451 config.hostport = 6379; 452 config.hostsocket = NULL; 453 454 i = parseOptions(argc,argv); 455 argc -= i; 456 argv += i; 457 458 config.latency = zmalloc(sizeof(long long)*config.requests); 459 460 if (config.keepalive == 0) { 461 printf("WARNING: keepalive disabled, you probably need 'echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse' for Linux and 'sudo sysctl -w net.inet.tcp.msl=1000' for Mac OS X in order to use a lot of clients/requests\n"); 462 } 463 464 if (config.idlemode) { 465 printf("Creating %d idle connections and waiting forever (Ctrl+C when done)\n", config.numclients); 466 c = createClient("",0); /* will never receive a reply */ 467 createMissingClients(c); 468 aeMain(config.el); 469 /* and will wait for every */ 470 } 471 472 /* Run benchmark with command in the remainder of the arguments. */ 473 if (argc) { 474 sds title = sdsnew(argv[0]); 475 for (i = 1; i < argc; i++) { 476 title = sdscatlen(title, " ", 1); 477 title = sdscatlen(title, (char*)argv[i], strlen(argv[i])); 478 } 479 480 do { 481 len = redisFormatCommandArgv(&cmd,argc,argv,NULL); 482 benchmark(title,cmd,len); 483 free(cmd); 484 } while(config.loop); 485 486 return 0; 487 } 488 489 /* Run default benchmark suite. */ 490 do { 491 data = zmalloc(config.datasize+1); 492 memset(data,'x',config.datasize); 493 data[config.datasize] = '\0'; 494 495 benchmark("PING (inline)","PING\r\n",6); 496 497 len = redisFormatCommand(&cmd,"PING"); 498 benchmark("PING",cmd,len); 499 free(cmd); 500 501 const char *argv[21]; 502 argv[0] = "MSET"; 503 for (i = 1; i < 21; i += 2) { 504 argv[i] = "foo:rand:000000000000"; 505 argv[i+1] = data; 506 } 507 len = redisFormatCommandArgv(&cmd,21,argv,NULL); 508 benchmark("MSET (10 keys)",cmd,len); 509 free(cmd); 510 511 len = redisFormatCommand(&cmd,"SET foo:rand:000000000000 %s",data); 512 benchmark("SET",cmd,len); 513 free(cmd); 514 515 len = redisFormatCommand(&cmd,"GET foo:rand:000000000000"); 516 benchmark("GET",cmd,len); 517 free(cmd); 518 519 len = redisFormatCommand(&cmd,"INCR counter:rand:000000000000"); 520 benchmark("INCR",cmd,len); 521 free(cmd); 522 523 len = redisFormatCommand(&cmd,"LPUSH mylist %s",data); 524 benchmark("LPUSH",cmd,len); 525 free(cmd); 526 527 len = redisFormatCommand(&cmd,"LPOP mylist"); 528 benchmark("LPOP",cmd,len); 529 free(cmd); 530 531 len = redisFormatCommand(&cmd,"SADD myset counter:rand:000000000000"); 532 benchmark("SADD",cmd,len); 533 free(cmd); 534 535 len = redisFormatCommand(&cmd,"SPOP myset"); 536 benchmark("SPOP",cmd,len); 537 free(cmd); 538 539 len = redisFormatCommand(&cmd,"LPUSH mylist %s",data); 540 benchmark("LPUSH (again, in order to bench LRANGE)",cmd,len); 541 free(cmd); 542 543 len = redisFormatCommand(&cmd,"LRANGE mylist 0 99"); 544 benchmark("LRANGE (first 100 elements)",cmd,len); 545 free(cmd); 546 547 len = redisFormatCommand(&cmd,"LRANGE mylist 0 299"); 548 benchmark("LRANGE (first 300 elements)",cmd,len); 549 free(cmd); 550 551 len = redisFormatCommand(&cmd,"LRANGE mylist 0 449"); 552 benchmark("LRANGE (first 450 elements)",cmd,len); 553 free(cmd); 554 555 len = redisFormatCommand(&cmd,"LRANGE mylist 0 599"); 556 benchmark("LRANGE (first 600 elements)",cmd,len); 557 free(cmd); 558 559 printf("\n"); 560 } while(config.loop); 561 562 return 0; 563 } 564