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 CLIENT_CONNECTING 0 49 #define CLIENT_SENDQUERY 1 50 #define CLIENT_READREPLY 2 51 52 #define REDIS_NOTUSED(V) ((void) V) 53 54 static struct config { 55 int debug; 56 int numclients; 57 int requests; 58 int liveclients; 59 int donerequests; 60 int keysize; 61 int datasize; 62 int randomkeys; 63 int randomkeys_keyspacelen; 64 aeEventLoop *el; 65 char *hostip; 66 int hostport; 67 char *hostsocket; 68 int keepalive; 69 long long start; 70 long long totlatency; 71 long long *latency; 72 char *title; 73 list *clients; 74 int quiet; 75 int loop; 76 int idlemode; 77 } config; 78 79 typedef struct _client { 80 redisContext *context; 81 int state; 82 sds obuf; 83 unsigned int written; /* bytes of 'obuf' already written */ 84 int replytype; 85 long long start; /* start time of a request */ 86 long long latency; /* request latency */ 87 } *client; 88 89 /* Prototypes */ 90 static void writeHandler(aeEventLoop *el, int fd, void *privdata, int mask); 91 static void createMissingClients(client c); 92 93 /* Implementation */ 94 static long long ustime(void) { 95 struct timeval tv; 96 long long ust; 97 98 gettimeofday(&tv, NULL); 99 ust = ((long)tv.tv_sec)*1000000; 100 ust += tv.tv_usec; 101 return ust; 102 } 103 104 static long long mstime(void) { 105 struct timeval tv; 106 long long mst; 107 108 gettimeofday(&tv, NULL); 109 mst = ((long)tv.tv_sec)*1000; 110 mst += tv.tv_usec/1000; 111 return mst; 112 } 113 114 static void freeClient(client c) { 115 listNode *ln; 116 aeDeleteFileEvent(config.el,c->context->fd,AE_WRITABLE); 117 aeDeleteFileEvent(config.el,c->context->fd,AE_READABLE); 118 redisFree(c->context); 119 sdsfree(c->obuf); 120 zfree(c); 121 config.liveclients--; 122 ln = listSearchKey(config.clients,c); 123 assert(ln != NULL); 124 listDelNode(config.clients,ln); 125 } 126 127 static void freeAllClients(void) { 128 listNode *ln = config.clients->head, *next; 129 130 while(ln) { 131 next = ln->next; 132 freeClient(ln->value); 133 ln = next; 134 } 135 } 136 137 static void resetClient(client c) { 138 aeDeleteFileEvent(config.el,c->context->fd,AE_WRITABLE); 139 aeDeleteFileEvent(config.el,c->context->fd,AE_READABLE); 140 aeCreateFileEvent(config.el,c->context->fd,AE_WRITABLE,writeHandler,c); 141 c->written = 0; 142 c->state = CLIENT_SENDQUERY; 143 c->start = ustime(); 144 c->latency = -1; 145 } 146 147 static void randomizeClientKey(client c) { 148 char *p; 149 char buf[32]; 150 long r; 151 152 p = strstr(c->obuf, "_rand"); 153 if (!p) return; 154 p += 5; 155 r = random() % config.randomkeys_keyspacelen; 156 sprintf(buf,"%ld",r); 157 memcpy(p,buf,strlen(buf)); 158 } 159 160 static void clientDone(client c) { 161 if (config.donerequests == config.requests) { 162 freeClient(c); 163 aeStop(config.el); 164 return; 165 } 166 if (config.keepalive) { 167 resetClient(c); 168 if (config.randomkeys) randomizeClientKey(c); 169 } else { 170 config.liveclients--; 171 createMissingClients(c); 172 config.liveclients++; 173 freeClient(c); 174 } 175 } 176 177 static void readHandler(aeEventLoop *el, int fd, void *privdata, int mask) { 178 client c = privdata; 179 void *reply = NULL; 180 REDIS_NOTUSED(el); 181 REDIS_NOTUSED(fd); 182 REDIS_NOTUSED(mask); 183 184 /* Calculate latency only for the first read event. This means that the 185 * server already sent the reply and we need to parse it. Parsing overhead 186 * is not part of the latency, so calculate it only once, here. */ 187 if (c->latency < 0) c->latency = ustime()-(c->start); 188 189 if (redisBufferRead(c->context) != REDIS_OK) { 190 fprintf(stderr,"Error: %s\n",c->context->errstr); 191 exit(1); 192 } else { 193 if (redisGetReply(c->context,&reply) != REDIS_OK) { 194 fprintf(stderr,"Error: %s\n",c->context->errstr); 195 exit(1); 196 } 197 if (reply != NULL) { 198 if (config.donerequests < config.requests) 199 config.latency[config.donerequests++] = c->latency; 200 clientDone(c); 201 } 202 } 203 } 204 205 static void writeHandler(aeEventLoop *el, int fd, void *privdata, int mask) { 206 client c = privdata; 207 REDIS_NOTUSED(el); 208 REDIS_NOTUSED(fd); 209 REDIS_NOTUSED(mask); 210 211 if (c->state == CLIENT_CONNECTING) { 212 c->state = CLIENT_SENDQUERY; 213 c->start = ustime(); 214 c->latency = -1; 215 } 216 if (sdslen(c->obuf) > c->written) { 217 void *ptr = c->obuf+c->written; 218 int nwritten = write(c->context->fd,ptr,sdslen(c->obuf)-c->written); 219 if (nwritten == -1) { 220 if (errno != EPIPE) 221 fprintf(stderr, "Writing to socket: %s\n", strerror(errno)); 222 freeClient(c); 223 return; 224 } 225 c->written += nwritten; 226 if (sdslen(c->obuf) == c->written) { 227 aeDeleteFileEvent(config.el,c->context->fd,AE_WRITABLE); 228 aeCreateFileEvent(config.el,c->context->fd,AE_READABLE,readHandler,c); 229 c->state = CLIENT_READREPLY; 230 } 231 } 232 } 233 234 static client createClient(int replytype) { 235 client c = zmalloc(sizeof(struct _client)); 236 if (config.hostsocket == NULL) { 237 c->context = redisConnectNonBlock(config.hostip,config.hostport); 238 } else { 239 c->context = redisConnectUnixNonBlock(config.hostsocket); 240 } 241 if (c->context->err) { 242 fprintf(stderr,"Could not connect to Redis at "); 243 if (config.hostsocket == NULL) 244 fprintf(stderr,"%s:%d: %s\n",config.hostip,config.hostport,c->context->errstr); 245 else 246 fprintf(stderr,"%s: %s\n",config.hostsocket,c->context->errstr); 247 exit(1); 248 } 249 c->replytype = replytype; 250 c->state = CLIENT_CONNECTING; 251 c->obuf = sdsempty(); 252 c->written = 0; 253 redisSetReplyObjectFunctions(c->context,NULL); 254 aeCreateFileEvent(config.el,c->context->fd,AE_WRITABLE,writeHandler,c); 255 listAddNodeTail(config.clients,c); 256 config.liveclients++; 257 return c; 258 } 259 260 static void createMissingClients(client c) { 261 while(config.liveclients < config.numclients) { 262 client new = createClient(c->replytype); 263 sdsfree(new->obuf); 264 new->obuf = sdsdup(c->obuf); 265 if (config.randomkeys) randomizeClientKey(c); 266 } 267 } 268 269 static int compareLatency(const void *a, const void *b) { 270 return (*(long long*)a)-(*(long long*)b); 271 } 272 273 static void showLatencyReport(void) { 274 int i, curlat = 0; 275 float perc, reqpersec; 276 277 reqpersec = (float)config.donerequests/((float)config.totlatency/1000); 278 if (!config.quiet) { 279 printf("====== %s ======\n", config.title); 280 printf(" %d requests completed in %.2f seconds\n", config.donerequests, 281 (float)config.totlatency/1000); 282 printf(" %d parallel clients\n", config.numclients); 283 printf(" %d bytes payload\n", config.datasize); 284 printf(" keep alive: %d\n", config.keepalive); 285 printf("\n"); 286 287 qsort(config.latency,config.requests,sizeof(long long),compareLatency); 288 for (i = 0; i < config.requests; i++) { 289 if (config.latency[i]/1000 != curlat || i == (config.requests-1)) { 290 curlat = config.latency[i]/1000; 291 perc = ((float)(i+1)*100)/config.requests; 292 printf("%.2f%% <= %d milliseconds\n", perc, curlat); 293 } 294 } 295 printf("%.2f requests per second\n\n", reqpersec); 296 } else { 297 printf("%s: %.2f requests per second\n", config.title, reqpersec); 298 } 299 } 300 301 static void prepareForBenchmark(char *title) { 302 config.title = title; 303 config.start = mstime(); 304 config.donerequests = 0; 305 } 306 307 static void endBenchmark(void) { 308 config.totlatency = mstime()-config.start; 309 showLatencyReport(); 310 freeAllClients(); 311 } 312 313 void parseOptions(int argc, char **argv) { 314 int i; 315 316 for (i = 1; i < argc; i++) { 317 int lastarg = i==argc-1; 318 319 if (!strcmp(argv[i],"-c") && !lastarg) { 320 config.numclients = atoi(argv[i+1]); 321 i++; 322 } else if (!strcmp(argv[i],"-n") && !lastarg) { 323 config.requests = atoi(argv[i+1]); 324 i++; 325 } else if (!strcmp(argv[i],"-k") && !lastarg) { 326 config.keepalive = atoi(argv[i+1]); 327 i++; 328 } else if (!strcmp(argv[i],"-h") && !lastarg) { 329 config.hostip = argv[i+1]; 330 i++; 331 } else if (!strcmp(argv[i],"-p") && !lastarg) { 332 config.hostport = atoi(argv[i+1]); 333 i++; 334 } else if (!strcmp(argv[i],"-s") && !lastarg) { 335 config.hostsocket = argv[i+1]; 336 i++; 337 } else if (!strcmp(argv[i],"-d") && !lastarg) { 338 config.datasize = atoi(argv[i+1]); 339 i++; 340 if (config.datasize < 1) config.datasize=1; 341 if (config.datasize > 1024*1024) config.datasize = 1024*1024; 342 } else if (!strcmp(argv[i],"-r") && !lastarg) { 343 config.randomkeys = 1; 344 config.randomkeys_keyspacelen = atoi(argv[i+1]); 345 if (config.randomkeys_keyspacelen < 0) 346 config.randomkeys_keyspacelen = 0; 347 i++; 348 } else if (!strcmp(argv[i],"-q")) { 349 config.quiet = 1; 350 } else if (!strcmp(argv[i],"-l")) { 351 config.loop = 1; 352 } else if (!strcmp(argv[i],"-D")) { 353 config.debug = 1; 354 } else if (!strcmp(argv[i],"-I")) { 355 config.idlemode = 1; 356 } else { 357 printf("Wrong option '%s' or option argument missing\n\n",argv[i]); 358 printf("Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]\n\n"); 359 printf(" -h <hostname> Server hostname (default 127.0.0.1)\n"); 360 printf(" -p <port> Server port (default 6379)\n"); 361 printf(" -s <socket> Server socket (overrides host and port)\n"); 362 printf(" -c <clients> Number of parallel connections (default 50)\n"); 363 printf(" -n <requests> Total number of requests (default 10000)\n"); 364 printf(" -d <size> Data size of SET/GET value in bytes (default 2)\n"); 365 printf(" -k <boolean> 1=keep alive 0=reconnect (default 1)\n"); 366 printf(" -r <keyspacelen> Use random keys for SET/GET/INCR, random values for SADD\n"); 367 printf(" Using this option the benchmark will get/set keys\n"); 368 printf(" in the form mykey_rand000000012456 instead of constant\n"); 369 printf(" keys, the <keyspacelen> argument determines the max\n"); 370 printf(" number of values for the random number. For instance\n"); 371 printf(" if set to 10 only rand000000000000 - rand000000000009\n"); 372 printf(" range will be allowed.\n"); 373 printf(" -q Quiet. Just show query/sec values\n"); 374 printf(" -l Loop. Run the tests forever\n"); 375 printf(" -I Idle mode. Just open N idle connections and wait.\n"); 376 printf(" -D Debug mode. more verbose.\n"); 377 exit(1); 378 } 379 } 380 } 381 382 int showThroughput(struct aeEventLoop *eventLoop, long long id, void *clientData) { 383 REDIS_NOTUSED(eventLoop); 384 REDIS_NOTUSED(id); 385 REDIS_NOTUSED(clientData); 386 387 float dt = (float)(mstime()-config.start)/1000.0; 388 float rps = (float)config.donerequests/dt; 389 printf("%s: %.2f\r", config.title, rps); 390 fflush(stdout); 391 return 250; /* every 250ms */ 392 } 393 394 int main(int argc, char **argv) { 395 client c; 396 397 signal(SIGHUP, SIG_IGN); 398 signal(SIGPIPE, SIG_IGN); 399 400 config.debug = 0; 401 config.numclients = 50; 402 config.requests = 10000; 403 config.liveclients = 0; 404 config.el = aeCreateEventLoop(); 405 aeCreateTimeEvent(config.el,1,showThroughput,NULL,NULL); 406 config.keepalive = 1; 407 config.donerequests = 0; 408 config.datasize = 3; 409 config.randomkeys = 0; 410 config.randomkeys_keyspacelen = 0; 411 config.quiet = 0; 412 config.loop = 0; 413 config.idlemode = 0; 414 config.latency = NULL; 415 config.clients = listCreate(); 416 config.hostip = "127.0.0.1"; 417 config.hostport = 6379; 418 config.hostsocket = NULL; 419 420 parseOptions(argc,argv); 421 config.latency = zmalloc(sizeof(long long)*config.requests); 422 423 if (config.keepalive == 0) { 424 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"); 425 } 426 427 if (config.idlemode) { 428 printf("Creating %d idle connections and waiting forever (Ctrl+C when done)\n", config.numclients); 429 prepareForBenchmark("IDLE"); 430 c = createClient(0); /* will never receive a reply */ 431 c->obuf = sdsempty(); 432 createMissingClients(c); 433 aeMain(config.el); 434 /* and will wait for every */ 435 } 436 437 do { 438 prepareForBenchmark("PING"); 439 c = createClient(REDIS_REPLY_STATUS); 440 c->obuf = sdscat(c->obuf,"PING\r\n"); 441 createMissingClients(c); 442 aeMain(config.el); 443 endBenchmark(); 444 445 prepareForBenchmark("PING (multi bulk)"); 446 c = createClient(REDIS_REPLY_STATUS); 447 c->obuf = sdscat(c->obuf,"*1\r\n$4\r\nPING\r\n"); 448 createMissingClients(c); 449 aeMain(config.el); 450 endBenchmark(); 451 452 prepareForBenchmark("MSET (10 keys, multi bulk)"); 453 c = createClient(REDIS_REPLY_ARRAY); 454 c->obuf = sdscatprintf(c->obuf,"*%d\r\n$4\r\nMSET\r\n", 11); 455 { 456 int i; 457 char *data = zmalloc(config.datasize+2); 458 memset(data,'x',config.datasize); 459 for (i = 0; i < 10; i++) { 460 c->obuf = sdscatprintf(c->obuf,"$%d\r\n%s\r\n",config.datasize,data); 461 } 462 zfree(data); 463 } 464 createMissingClients(c); 465 aeMain(config.el); 466 endBenchmark(); 467 468 prepareForBenchmark("SET"); 469 c = createClient(REDIS_REPLY_STATUS); 470 c->obuf = sdscat(c->obuf,"*3\r\n$3\r\nSET\r\n$20\r\nfoo_rand000000000000\r\n"); 471 { 472 char *data = zmalloc(config.datasize+2); 473 memset(data,'x',config.datasize); 474 data[config.datasize] = '\r'; 475 data[config.datasize+1] = '\n'; 476 c->obuf = sdscatprintf(c->obuf,"$%d\r\n",config.datasize); 477 c->obuf = sdscatlen(c->obuf,data,config.datasize+2); 478 } 479 createMissingClients(c); 480 aeMain(config.el); 481 endBenchmark(); 482 483 prepareForBenchmark("GET"); 484 c = createClient(REDIS_REPLY_STRING); 485 c->obuf = sdscat(c->obuf,"GET foo_rand000000000000\r\n"); 486 createMissingClients(c); 487 aeMain(config.el); 488 endBenchmark(); 489 490 prepareForBenchmark("INCR"); 491 c = createClient(REDIS_REPLY_INTEGER); 492 c->obuf = sdscat(c->obuf,"INCR counter_rand000000000000\r\n"); 493 createMissingClients(c); 494 aeMain(config.el); 495 endBenchmark(); 496 497 prepareForBenchmark("LPUSH"); 498 c = createClient(REDIS_REPLY_INTEGER); 499 c->obuf = sdscat(c->obuf,"LPUSH mylist bar\r\n"); 500 createMissingClients(c); 501 aeMain(config.el); 502 endBenchmark(); 503 504 prepareForBenchmark("LPOP"); 505 c = createClient(REDIS_REPLY_STRING); 506 c->obuf = sdscat(c->obuf,"LPOP mylist\r\n"); 507 createMissingClients(c); 508 aeMain(config.el); 509 endBenchmark(); 510 511 prepareForBenchmark("SADD"); 512 c = createClient(REDIS_REPLY_STATUS); 513 c->obuf = sdscat(c->obuf,"SADD myset counter_rand000000000000\r\n"); 514 createMissingClients(c); 515 aeMain(config.el); 516 endBenchmark(); 517 518 prepareForBenchmark("SPOP"); 519 c = createClient(REDIS_REPLY_STRING); 520 c->obuf = sdscat(c->obuf,"SPOP myset\r\n"); 521 createMissingClients(c); 522 aeMain(config.el); 523 endBenchmark(); 524 525 prepareForBenchmark("LPUSH (again, in order to bench LRANGE)"); 526 c = createClient(REDIS_REPLY_STATUS); 527 c->obuf = sdscat(c->obuf,"LPUSH mylist bar\r\n"); 528 createMissingClients(c); 529 aeMain(config.el); 530 endBenchmark(); 531 532 prepareForBenchmark("LRANGE (first 100 elements)"); 533 c = createClient(REDIS_REPLY_ARRAY); 534 c->obuf = sdscat(c->obuf,"LRANGE mylist 0 99\r\n"); 535 createMissingClients(c); 536 aeMain(config.el); 537 endBenchmark(); 538 539 prepareForBenchmark("LRANGE (first 300 elements)"); 540 c = createClient(REDIS_REPLY_ARRAY); 541 c->obuf = sdscat(c->obuf,"LRANGE mylist 0 299\r\n"); 542 createMissingClients(c); 543 aeMain(config.el); 544 endBenchmark(); 545 546 prepareForBenchmark("LRANGE (first 450 elements)"); 547 c = createClient(REDIS_REPLY_ARRAY); 548 c->obuf = sdscat(c->obuf,"LRANGE mylist 0 449\r\n"); 549 createMissingClients(c); 550 aeMain(config.el); 551 endBenchmark(); 552 553 prepareForBenchmark("LRANGE (first 600 elements)"); 554 c = createClient(REDIS_REPLY_ARRAY); 555 c->obuf = sdscat(c->obuf,"LRANGE mylist 0 599\r\n"); 556 createMissingClients(c); 557 aeMain(config.el); 558 endBenchmark(); 559 560 printf("\n"); 561 } while(config.loop); 562 563 return 0; 564 } 565