1 /* Redis benchmark utility.
3 * Copyright (c) 2009-2010, Salvatore Sanfilippo <antirez at gmail dot com>
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
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.
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.
48 #define REDIS_NOTUSED(V) ((void) V)
50 static struct config
{
54 const char *hostsocket
;
59 int requests_finished
;
63 int randomkeys_keyspacelen
;
78 typedef struct _client
{
79 redisContext
*context
;
81 char *randptr
[32]; /* needed for MSET against 10 keys */
83 unsigned int written
; /* bytes of 'obuf' already written */
84 long long start
; /* start time of a request */
85 long long latency
; /* request latency */
86 int pending
; /* Number of pending requests (sent but no reply received) */
90 static void writeHandler(aeEventLoop
*el
, int fd
, void *privdata
, int mask
);
91 static void createMissingClients(client c
);
94 static long long ustime(void) {
98 gettimeofday(&tv
, NULL
);
99 ust
= ((long)tv
.tv_sec
)*1000000;
104 static long long mstime(void) {
108 gettimeofday(&tv
, NULL
);
109 mst
= ((long)tv
.tv_sec
)*1000;
110 mst
+= tv
.tv_usec
/1000;
114 static void freeClient(client c
) {
116 aeDeleteFileEvent(config
.el
,c
->context
->fd
,AE_WRITABLE
);
117 aeDeleteFileEvent(config
.el
,c
->context
->fd
,AE_READABLE
);
118 redisFree(c
->context
);
121 config
.liveclients
--;
122 ln
= listSearchKey(config
.clients
,c
);
124 listDelNode(config
.clients
,ln
);
127 static void freeAllClients(void) {
128 listNode
*ln
= config
.clients
->head
, *next
;
132 freeClient(ln
->value
);
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
);
142 c
->pending
= config
.pipeline
;
145 static void randomizeClientKey(client c
) {
149 for (i
= 0; i
< c
->randlen
; i
++) {
150 r
= random() % config
.randomkeys_keyspacelen
;
151 snprintf(buf
,sizeof(buf
),"%012zu",r
);
152 memcpy(c
->randptr
[i
],buf
,12);
156 static void clientDone(client c
) {
157 if (config
.requests_finished
== config
.requests
) {
162 if (config
.keepalive
) {
165 config
.liveclients
--;
166 createMissingClients(c
);
167 config
.liveclients
++;
172 static void readHandler(aeEventLoop
*el
, int fd
, void *privdata
, int mask
) {
179 /* Calculate latency only for the first read event. This means that the
180 * server already sent the reply and we need to parse it. Parsing overhead
181 * is not part of the latency, so calculate it only once, here. */
182 if (c
->latency
< 0) c
->latency
= ustime()-(c
->start
);
184 if (redisBufferRead(c
->context
) != REDIS_OK
) {
185 fprintf(stderr
,"Error: %s\n",c
->context
->errstr
);
189 if (redisGetReply(c
->context
,&reply
) != REDIS_OK
) {
190 fprintf(stderr
,"Error: %s\n",c
->context
->errstr
);
194 if (reply
== (void*)REDIS_REPLY_ERROR
) {
195 fprintf(stderr
,"Unexpected error reply, exiting...\n");
199 freeReplyObject(reply
);
201 if (config
.requests_finished
< config
.requests
)
202 config
.latency
[config
.requests_finished
++] = c
->latency
;
204 if (c
->pending
== 0) clientDone(c
);
212 static void writeHandler(aeEventLoop
*el
, int fd
, void *privdata
, int mask
) {
218 /* Initialize request when nothing was written. */
219 if (c
->written
== 0) {
220 /* Enforce upper bound to number of requests. */
221 if (config
.requests_issued
++ >= config
.requests
) {
226 /* Really initialize: randomize keys and set start time. */
227 if (config
.randomkeys
) randomizeClientKey(c
);
232 if (sdslen(c
->obuf
) > c
->written
) {
233 void *ptr
= c
->obuf
+c
->written
;
234 int nwritten
= write(c
->context
->fd
,ptr
,sdslen(c
->obuf
)-c
->written
);
235 if (nwritten
== -1) {
237 fprintf(stderr
, "Writing to socket: %s\n", strerror(errno
));
241 c
->written
+= nwritten
;
242 if (sdslen(c
->obuf
) == c
->written
) {
243 aeDeleteFileEvent(config
.el
,c
->context
->fd
,AE_WRITABLE
);
244 aeCreateFileEvent(config
.el
,c
->context
->fd
,AE_READABLE
,readHandler
,c
);
249 static client
createClient(char *cmd
, size_t len
) {
251 client c
= zmalloc(sizeof(struct _client
));
253 if (config
.hostsocket
== NULL
) {
254 c
->context
= redisConnectNonBlock(config
.hostip
,config
.hostport
);
256 c
->context
= redisConnectUnixNonBlock(config
.hostsocket
);
258 if (c
->context
->err
) {
259 fprintf(stderr
,"Could not connect to Redis at ");
260 if (config
.hostsocket
== NULL
)
261 fprintf(stderr
,"%s:%d: %s\n",config
.hostip
,config
.hostport
,c
->context
->errstr
);
263 fprintf(stderr
,"%s: %s\n",config
.hostsocket
,c
->context
->errstr
);
266 /* Queue N requests accordingly to the pipeline size. */
267 c
->obuf
= sdsempty();
268 for (j
= 0; j
< config
.pipeline
; j
++)
269 c
->obuf
= sdscatlen(c
->obuf
,cmd
,len
);
272 c
->pending
= config
.pipeline
;
274 /* Find substrings in the output buffer that need to be randomized. */
275 if (config
.randomkeys
) {
277 while ((p
= strstr(p
,":rand:")) != NULL
) {
278 assert(c
->randlen
< (signed)(sizeof(c
->randptr
)/sizeof(char*)));
279 c
->randptr
[c
->randlen
++] = p
+6;
284 /* redisSetReplyObjectFunctions(c->context,NULL); */
285 aeCreateFileEvent(config
.el
,c
->context
->fd
,AE_WRITABLE
,writeHandler
,c
);
286 listAddNodeTail(config
.clients
,c
);
287 config
.liveclients
++;
291 static void createMissingClients(client c
) {
294 while(config
.liveclients
< config
.numclients
) {
295 createClient(c
->obuf
,sdslen(c
->obuf
)/config
.pipeline
);
297 /* Listen backlog is quite limited on most systems */
305 static int compareLatency(const void *a
, const void *b
) {
306 return (*(long long*)a
)-(*(long long*)b
);
309 static void showLatencyReport(void) {
311 float perc
, reqpersec
;
313 reqpersec
= (float)config
.requests_finished
/((float)config
.totlatency
/1000);
314 if (!config
.quiet
&& !config
.csv
) {
315 printf("====== %s ======\n", config
.title
);
316 printf(" %d requests completed in %.2f seconds\n", config
.requests_finished
,
317 (float)config
.totlatency
/1000);
318 printf(" %d parallel clients\n", config
.numclients
);
319 printf(" %d bytes payload\n", config
.datasize
);
320 printf(" keep alive: %d\n", config
.keepalive
);
323 qsort(config
.latency
,config
.requests
,sizeof(long long),compareLatency
);
324 for (i
= 0; i
< config
.requests
; i
++) {
325 if (config
.latency
[i
]/1000 != curlat
|| i
== (config
.requests
-1)) {
326 curlat
= config
.latency
[i
]/1000;
327 perc
= ((float)(i
+1)*100)/config
.requests
;
328 printf("%.2f%% <= %d milliseconds\n", perc
, curlat
);
331 printf("%.2f requests per second\n\n", reqpersec
);
332 } else if (config
.csv
) {
333 printf("\"%s\",\"%.2f\"\n", config
.title
, reqpersec
);
335 printf("%s: %.2f requests per second\n", config
.title
, reqpersec
);
339 static void benchmark(char *title
, char *cmd
, int len
) {
342 config
.title
= title
;
343 config
.requests_issued
= 0;
344 config
.requests_finished
= 0;
346 c
= createClient(cmd
,len
);
347 createMissingClients(c
);
349 config
.start
= mstime();
351 config
.totlatency
= mstime()-config
.start
;
357 /* Returns number of consumed options. */
358 int parseOptions(int argc
, const char **argv
) {
363 for (i
= 1; i
< argc
; i
++) {
364 lastarg
= (i
== (argc
-1));
366 if (!strcmp(argv
[i
],"-c")) {
367 if (lastarg
) goto invalid
;
368 config
.numclients
= atoi(argv
[++i
]);
369 } else if (!strcmp(argv
[i
],"-n")) {
370 if (lastarg
) goto invalid
;
371 config
.requests
= atoi(argv
[++i
]);
372 } else if (!strcmp(argv
[i
],"-k")) {
373 if (lastarg
) goto invalid
;
374 config
.keepalive
= atoi(argv
[++i
]);
375 } else if (!strcmp(argv
[i
],"-h")) {
376 if (lastarg
) goto invalid
;
377 config
.hostip
= strdup(argv
[++i
]);
378 } else if (!strcmp(argv
[i
],"-p")) {
379 if (lastarg
) goto invalid
;
380 config
.hostport
= atoi(argv
[++i
]);
381 } else if (!strcmp(argv
[i
],"-s")) {
382 if (lastarg
) goto invalid
;
383 config
.hostsocket
= strdup(argv
[++i
]);
384 } else if (!strcmp(argv
[i
],"-d")) {
385 if (lastarg
) goto invalid
;
386 config
.datasize
= atoi(argv
[++i
]);
387 if (config
.datasize
< 1) config
.datasize
=1;
388 if (config
.datasize
> 1024*1024*1024) config
.datasize
= 1024*1024*1024;
389 } else if (!strcmp(argv
[i
],"-P")) {
390 if (lastarg
) goto invalid
;
391 config
.pipeline
= atoi(argv
[++i
]);
392 if (config
.pipeline
<= 0) config
.pipeline
=1;
393 } else if (!strcmp(argv
[i
],"-r")) {
394 if (lastarg
) goto invalid
;
395 config
.randomkeys
= 1;
396 config
.randomkeys_keyspacelen
= atoi(argv
[++i
]);
397 if (config
.randomkeys_keyspacelen
< 0)
398 config
.randomkeys_keyspacelen
= 0;
399 } else if (!strcmp(argv
[i
],"-q")) {
401 } else if (!strcmp(argv
[i
],"--csv")) {
403 } else if (!strcmp(argv
[i
],"-l")) {
405 } else if (!strcmp(argv
[i
],"-I")) {
407 } else if (!strcmp(argv
[i
],"-t")) {
408 if (lastarg
) goto invalid
;
409 /* We get the list of tests to run as a string in the form
410 * get,set,lrange,...,test_N. Then we add a comma before and
411 * after the string in order to make sure that searching
412 * for ",testname," will always get a match if the test is
414 config
.tests
= sdsnew(",");
415 config
.tests
= sdscat(config
.tests
,(char*)argv
[++i
]);
416 config
.tests
= sdscat(config
.tests
,",");
417 sdstolower(config
.tests
);
418 } else if (!strcmp(argv
[i
],"--help")) {
422 /* Assume the user meant to provide an option when the arg starts
423 * with a dash. We're done otherwise and should use the remainder
424 * as the command and arguments for running the benchmark. */
425 if (argv
[i
][0] == '-') goto invalid
;
433 printf("Invalid option \"%s\" or option argument missing\n\n",argv
[i
]);
437 "Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]\n\n"
438 " -h <hostname> Server hostname (default 127.0.0.1)\n"
439 " -p <port> Server port (default 6379)\n"
440 " -s <socket> Server socket (overrides host and port)\n"
441 " -c <clients> Number of parallel connections (default 50)\n"
442 " -n <requests> Total number of requests (default 10000)\n"
443 " -d <size> Data size of SET/GET value in bytes (default 2)\n"
444 " -k <boolean> 1=keep alive 0=reconnect (default 1)\n"
445 " -r <keyspacelen> Use random keys for SET/GET/INCR, random values for SADD\n"
446 " Using this option the benchmark will get/set keys\n"
447 " in the form mykey_rand:000000012456 instead of constant\n"
448 " keys, the <keyspacelen> argument determines the max\n"
449 " number of values for the random number. For instance\n"
450 " if set to 10 only rand:000000000000 - rand:000000000009\n"
451 " range will be allowed.\n"
452 " -P <numreq> Pipeline <numreq> requests. Default 1 (no pipeline).\n"
453 " -q Quiet. Just show query/sec values\n"
454 " --csv Output in CSV format\n"
455 " -l Loop. Run the tests forever\n"
456 " -t <tests> Only run the comma separated list of tests. The test\n"
457 " names are the same as the ones produced as output.\n"
458 " -I Idle mode. Just open N idle connections and wait.\n\n"
460 " Run the benchmark with the default configuration against 127.0.0.1:6379:\n"
461 " $ redis-benchmark\n\n"
462 " Use 20 parallel clients, for a total of 100k requests, against 192.168.1.1:\n"
463 " $ redis-benchmark -h 192.168.1.1 -p 6379 -n 100000 -c 20\n\n"
464 " Fill 127.0.0.1:6379 with about 1 million keys only using the SET test:\n"
465 " $ redis-benchmark -t set -n 1000000 -r 100000000\n\n"
466 " Benchmark 127.0.0.1:6379 for a few commands producing CSV output:\n"
467 " $ redis-benchmark -t ping,set,get -n 100000 --csv\n\n"
468 " Fill a list with 10000 random elements:\n"
469 " $ redis-benchmark -r 10000 -n 10000 lpush mylist ele:rand:000000000000\n\n"
474 int showThroughput(struct aeEventLoop
*eventLoop
, long long id
, void *clientData
) {
475 REDIS_NOTUSED(eventLoop
);
477 REDIS_NOTUSED(clientData
);
479 if (config
.csv
) return 250;
480 float dt
= (float)(mstime()-config
.start
)/1000.0;
481 float rps
= (float)config
.requests_finished
/dt
;
482 printf("%s: %.2f\r", config
.title
, rps
);
484 return 250; /* every 250ms */
487 /* Return true if the named test was selected using the -t command line
488 * switch, or if all the tests are selected (no -t passed by user). */
489 int test_is_selected(char *name
) {
491 int l
= strlen(name
);
493 if (config
.tests
== NULL
) return 1;
495 memcpy(buf
+1,name
,l
);
498 return strstr(config
.tests
,buf
) != NULL
;
501 int main(int argc
, const char **argv
) {
508 signal(SIGHUP
, SIG_IGN
);
509 signal(SIGPIPE
, SIG_IGN
);
511 config
.numclients
= 50;
512 config
.requests
= 10000;
513 config
.liveclients
= 0;
514 config
.el
= aeCreateEventLoop(1024*10);
515 aeCreateTimeEvent(config
.el
,1,showThroughput
,NULL
,NULL
);
516 config
.keepalive
= 1;
519 config
.randomkeys
= 0;
520 config
.randomkeys_keyspacelen
= 0;
525 config
.latency
= NULL
;
526 config
.clients
= listCreate();
527 config
.hostip
= "127.0.0.1";
528 config
.hostport
= 6379;
529 config
.hostsocket
= NULL
;
532 i
= parseOptions(argc
,argv
);
536 config
.latency
= zmalloc(sizeof(long long)*config
.requests
);
538 if (config
.keepalive
== 0) {
539 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");
542 if (config
.idlemode
) {
543 printf("Creating %d idle connections and waiting forever (Ctrl+C when done)\n", config
.numclients
);
544 c
= createClient("",0); /* will never receive a reply */
545 createMissingClients(c
);
547 /* and will wait for every */
550 /* Run benchmark with command in the remainder of the arguments. */
552 sds title
= sdsnew(argv
[0]);
553 for (i
= 1; i
< argc
; i
++) {
554 title
= sdscatlen(title
, " ", 1);
555 title
= sdscatlen(title
, (char*)argv
[i
], strlen(argv
[i
]));
559 len
= redisFormatCommandArgv(&cmd
,argc
,argv
,NULL
);
560 benchmark(title
,cmd
,len
);
562 } while(config
.loop
);
567 /* Run default benchmark suite. */
569 data
= zmalloc(config
.datasize
+1);
570 memset(data
,'x',config
.datasize
);
571 data
[config
.datasize
] = '\0';
573 if (test_is_selected("ping_inline") || test_is_selected("ping"))
574 benchmark("PING_INLINE","PING\r\n",6);
576 if (test_is_selected("ping_mbulk") || test_is_selected("ping")) {
577 len
= redisFormatCommand(&cmd
,"PING");
578 benchmark("PING_BULK",cmd
,len
);
582 if (test_is_selected("set")) {
583 len
= redisFormatCommand(&cmd
,"SET foo:rand:000000000000 %s",data
);
584 benchmark("SET",cmd
,len
);
588 if (test_is_selected("get")) {
589 len
= redisFormatCommand(&cmd
,"GET foo:rand:000000000000");
590 benchmark("GET",cmd
,len
);
594 if (test_is_selected("incr")) {
595 len
= redisFormatCommand(&cmd
,"INCR counter:rand:000000000000");
596 benchmark("INCR",cmd
,len
);
600 if (test_is_selected("lpush")) {
601 len
= redisFormatCommand(&cmd
,"LPUSH mylist %s",data
);
602 benchmark("LPUSH",cmd
,len
);
606 if (test_is_selected("lpop")) {
607 len
= redisFormatCommand(&cmd
,"LPOP mylist");
608 benchmark("LPOP",cmd
,len
);
612 if (test_is_selected("sadd")) {
613 len
= redisFormatCommand(&cmd
,
614 "SADD myset counter:rand:000000000000");
615 benchmark("SADD",cmd
,len
);
619 if (test_is_selected("spop")) {
620 len
= redisFormatCommand(&cmd
,"SPOP myset");
621 benchmark("SPOP",cmd
,len
);
625 if (test_is_selected("lrange") ||
626 test_is_selected("lrange_100") ||
627 test_is_selected("lrange_300") ||
628 test_is_selected("lrange_500") ||
629 test_is_selected("lrange_600"))
631 len
= redisFormatCommand(&cmd
,"LPUSH mylist %s",data
);
632 benchmark("LPUSH (needed to benchmark LRANGE)",cmd
,len
);
636 if (test_is_selected("lrange") || test_is_selected("lrange_100")) {
637 len
= redisFormatCommand(&cmd
,"LRANGE mylist 0 99");
638 benchmark("LRANGE_100 (first 100 elements)",cmd
,len
);
642 if (test_is_selected("lrange") || test_is_selected("lrange_300")) {
643 len
= redisFormatCommand(&cmd
,"LRANGE mylist 0 299");
644 benchmark("LRANGE_300 (first 300 elements)",cmd
,len
);
648 if (test_is_selected("lrange") || test_is_selected("lrange_500")) {
649 len
= redisFormatCommand(&cmd
,"LRANGE mylist 0 449");
650 benchmark("LRANGE_500 (first 450 elements)",cmd
,len
);
654 if (test_is_selected("lrange") || test_is_selected("lrange_600")) {
655 len
= redisFormatCommand(&cmd
,"LRANGE mylist 0 599");
656 benchmark("LRANGE_600 (first 600 elements)",cmd
,len
);
660 if (test_is_selected("mset")) {
661 const char *argv
[21];
663 for (i
= 1; i
< 21; i
+= 2) {
664 argv
[i
] = "foo:rand:000000000000";
667 len
= redisFormatCommandArgv(&cmd
,21,argv
,NULL
);
668 benchmark("MSET (10 keys)",cmd
,len
);
672 if (!config
.csv
) printf("\n");
673 } while(config
.loop
);