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