]>
git.saurik.com Git - redis.git/blob - src/redis-benchmark.c
1 /* Redis benchmark utility.
3 * Copyright (c) 2009-2012, 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.
49 #define REDIS_NOTUSED(V) ((void) V)
51 static struct config
{
55 const char * hostsocket
;
60 int requests_finished
;
64 int randomkeys_keyspacelen
;
79 typedef struct _client
{
80 redisContext
* context
;
82 char * randptr
[ 32 ]; /* needed for MSET against 10 keys */
84 unsigned int written
; /* bytes of 'obuf' already written */
85 long long start
; /* start time of a request */
86 long long latency
; /* request latency */
87 int pending
; /* Number of pending requests (sent but no reply received) */
91 static void writeHandler ( aeEventLoop
* el
, int fd
, void * privdata
, int mask
);
92 static void createMissingClients ( client c
);
95 static long long ustime ( void ) {
99 gettimeofday (& tv
, NULL
);
100 ust
= (( long ) tv
. tv_sec
)* 1000000 ;
105 static long long mstime ( void ) {
109 gettimeofday (& tv
, NULL
);
110 mst
= (( long ) tv
. tv_sec
)* 1000 ;
111 mst
+= tv
. tv_usec
/ 1000 ;
115 static void freeClient ( client c
) {
117 aeDeleteFileEvent ( config
. el
, c
-> context
-> fd
, AE_WRITABLE
);
118 aeDeleteFileEvent ( config
. el
, c
-> context
-> fd
, AE_READABLE
);
119 redisFree ( c
-> context
);
122 config
. liveclients
--;
123 ln
= listSearchKey ( config
. clients
, c
);
125 listDelNode ( config
. clients
, ln
);
128 static void freeAllClients ( void ) {
129 listNode
* ln
= config
. clients
-> head
, * next
;
133 freeClient ( ln
-> value
);
138 static void resetClient ( client c
) {
139 aeDeleteFileEvent ( config
. el
, c
-> context
-> fd
, AE_WRITABLE
);
140 aeDeleteFileEvent ( config
. el
, c
-> context
-> fd
, AE_READABLE
);
141 aeCreateFileEvent ( config
. el
, c
-> context
-> fd
, AE_WRITABLE
, writeHandler
, c
);
143 c
-> pending
= config
. pipeline
;
146 static void randomizeClientKey ( client c
) {
150 for ( i
= 0 ; i
< c
-> randlen
; i
++) {
151 r
= random () % config
. randomkeys_keyspacelen
;
152 snprintf ( buf
, sizeof ( buf
), " %0 12zu" , r
);
153 memcpy ( c
-> randptr
[ i
], buf
, 12 );
157 static void clientDone ( client c
) {
158 if ( config
. requests_finished
== config
. requests
) {
163 if ( config
. keepalive
) {
166 config
. liveclients
--;
167 createMissingClients ( c
);
168 config
. liveclients
++;
173 static void readHandler ( aeEventLoop
* el
, int fd
, void * privdata
, int mask
) {
180 /* Calculate latency only for the first read event. This means that the
181 * server already sent the reply and we need to parse it. Parsing overhead
182 * is not part of the latency, so calculate it only once, here. */
183 if ( c
-> latency
< 0 ) c
-> latency
= ustime ()-( c
-> start
);
185 if ( redisBufferRead ( c
-> context
) != REDIS_OK
) {
186 fprintf ( stderr
, "Error: %s \n " , c
-> context
-> errstr
);
190 if ( redisGetReply ( c
-> context
,& reply
) != REDIS_OK
) {
191 fprintf ( stderr
, "Error: %s \n " , c
-> context
-> errstr
);
195 if ( reply
== ( void *) REDIS_REPLY_ERROR
) {
196 fprintf ( stderr
, "Unexpected error reply, exiting... \n " );
200 freeReplyObject ( reply
);
202 if ( config
. requests_finished
< config
. requests
)
203 config
. latency
[ config
. requests_finished
++] = c
-> latency
;
205 if ( c
-> pending
== 0 ) {
216 static void writeHandler ( aeEventLoop
* el
, int fd
, void * privdata
, int mask
) {
222 /* Initialize request when nothing was written. */
223 if ( c
-> written
== 0 ) {
224 /* Enforce upper bound to number of requests. */
225 if ( config
. requests_issued
++ >= config
. requests
) {
230 /* Really initialize: randomize keys and set start time. */
231 if ( config
. randomkeys
) randomizeClientKey ( c
);
236 if ( sdslen ( c
-> obuf
) > c
-> written
) {
237 void * ptr
= c
-> obuf
+ c
-> written
;
238 int nwritten
= write ( c
-> context
-> fd
, ptr
, sdslen ( c
-> obuf
)- c
-> written
);
239 if ( nwritten
== - 1 ) {
241 fprintf ( stderr
, "Writing to socket: %s \n " , strerror ( errno
));
245 c
-> written
+= nwritten
;
246 if ( sdslen ( c
-> obuf
) == c
-> written
) {
247 aeDeleteFileEvent ( config
. el
, c
-> context
-> fd
, AE_WRITABLE
);
248 aeCreateFileEvent ( config
. el
, c
-> context
-> fd
, AE_READABLE
, readHandler
, c
);
253 static client
createClient ( char * cmd
, size_t len
) {
255 client c
= zmalloc ( sizeof ( struct _client
));
257 if ( config
. hostsocket
== NULL
) {
258 c
-> context
= redisConnectNonBlock ( config
. hostip
, config
. hostport
);
260 c
-> context
= redisConnectUnixNonBlock ( config
. hostsocket
);
262 if ( c
-> context
-> err
) {
263 fprintf ( stderr
, "Could not connect to Redis at " );
264 if ( config
. hostsocket
== NULL
)
265 fprintf ( stderr
, " %s : %d : %s \n " , config
. hostip
, config
. hostport
, c
-> context
-> errstr
);
267 fprintf ( stderr
, " %s : %s \n " , config
. hostsocket
, c
-> context
-> errstr
);
270 /* Suppress hiredis cleanup of unused buffers for max speed. */
271 c
-> context
-> reader
-> maxbuf
= 0 ;
272 /* Queue N requests accordingly to the pipeline size. */
273 c
-> obuf
= sdsempty ();
274 for ( j
= 0 ; j
< config
. pipeline
; j
++)
275 c
-> obuf
= sdscatlen ( c
-> obuf
, cmd
, len
);
278 c
-> pending
= config
. pipeline
;
280 /* Find substrings in the output buffer that need to be randomized. */
281 if ( config
. randomkeys
) {
283 while (( p
= strstr ( p
, ":rand:" )) != NULL
) {
284 assert ( c
-> randlen
< ( signed )( sizeof ( c
-> randptr
)/ sizeof ( char *)));
285 c
-> randptr
[ c
-> randlen
++] = p
+ 6 ;
290 /* redisSetReplyObjectFunctions(c->context,NULL); */
291 aeCreateFileEvent ( config
. el
, c
-> context
-> fd
, AE_WRITABLE
, writeHandler
, c
);
292 listAddNodeTail ( config
. clients
, c
);
293 config
. liveclients
++;
297 static void createMissingClients ( client c
) {
300 while ( config
. liveclients
< config
. numclients
) {
301 createClient ( c
-> obuf
, sdslen ( c
-> obuf
)/ config
. pipeline
);
303 /* Listen backlog is quite limited on most systems */
311 static int compareLatency ( const void * a
, const void * b
) {
312 return (*( long long *) a
)-(*( long long *) b
);
315 static void showLatencyReport ( void ) {
317 float perc
, reqpersec
;
319 reqpersec
= ( float ) config
. requests_finished
/(( float ) config
. totlatency
/ 1000 );
320 if (! config
. quiet
&& ! config
. csv
) {
321 printf ( "====== %s ====== \n " , config
. title
);
322 printf ( " %d requests completed in %.2f seconds \n " , config
. requests_finished
,
323 ( float ) config
. totlatency
/ 1000 );
324 printf ( " %d parallel clients \n " , config
. numclients
);
325 printf ( " %d bytes payload \n " , config
. datasize
);
326 printf ( " keep alive: %d \n " , config
. keepalive
);
329 qsort ( config
. latency
, config
. requests
, sizeof ( long long ), compareLatency
);
330 for ( i
= 0 ; i
< config
. requests
; i
++) {
331 if ( config
. latency
[ i
]/ 1000 != curlat
|| i
== ( config
. requests
- 1 )) {
332 curlat
= config
. latency
[ i
]/ 1000 ;
333 perc
= (( float )( i
+ 1 )* 100 )/ config
. requests
;
334 printf ( "%.2f%% <= %d milliseconds \n " , perc
, curlat
);
337 printf ( "%.2f requests per second \n\n " , reqpersec
);
338 } else if ( config
. csv
) {
339 printf ( " \" %s \" , \" %.2f \"\n " , config
. title
, reqpersec
);
341 printf ( " %s : %.2f requests per second \n " , config
. title
, reqpersec
);
345 static void benchmark ( char * title
, char * cmd
, int len
) {
348 config
. title
= title
;
349 config
. requests_issued
= 0 ;
350 config
. requests_finished
= 0 ;
352 c
= createClient ( cmd
, len
);
353 createMissingClients ( c
);
355 config
. start
= mstime ();
357 config
. totlatency
= mstime ()- config
. start
;
363 /* Returns number of consumed options. */
364 int parseOptions ( int argc
, const char ** argv
) {
369 for ( i
= 1 ; i
< argc
; i
++) {
370 lastarg
= ( i
== ( argc
- 1 ));
372 if (! strcmp ( argv
[ i
], "-c" )) {
373 if ( lastarg
) goto invalid
;
374 config
. numclients
= atoi ( argv
[++ i
]);
375 } else if (! strcmp ( argv
[ i
], "-n" )) {
376 if ( lastarg
) goto invalid
;
377 config
. requests
= atoi ( argv
[++ i
]);
378 } else if (! strcmp ( argv
[ i
], "-k" )) {
379 if ( lastarg
) goto invalid
;
380 config
. keepalive
= atoi ( argv
[++ i
]);
381 } else if (! strcmp ( argv
[ i
], "-h" )) {
382 if ( lastarg
) goto invalid
;
383 config
. hostip
= strdup ( argv
[++ i
]);
384 } else if (! strcmp ( argv
[ i
], "-p" )) {
385 if ( lastarg
) goto invalid
;
386 config
. hostport
= atoi ( argv
[++ i
]);
387 } else if (! strcmp ( argv
[ i
], "-s" )) {
388 if ( lastarg
) goto invalid
;
389 config
. hostsocket
= strdup ( argv
[++ i
]);
390 } else if (! strcmp ( argv
[ i
], "-d" )) {
391 if ( lastarg
) goto invalid
;
392 config
. datasize
= atoi ( argv
[++ i
]);
393 if ( config
. datasize
< 1 ) config
. datasize
= 1 ;
394 if ( config
. datasize
> 1024 * 1024 * 1024 ) config
. datasize
= 1024 * 1024 * 1024 ;
395 } else if (! strcmp ( argv
[ i
], "-P" )) {
396 if ( lastarg
) goto invalid
;
397 config
. pipeline
= atoi ( argv
[++ i
]);
398 if ( config
. pipeline
<= 0 ) config
. pipeline
= 1 ;
399 } else if (! strcmp ( argv
[ i
], "-r" )) {
400 if ( lastarg
) goto invalid
;
401 config
. randomkeys
= 1 ;
402 config
. randomkeys_keyspacelen
= atoi ( argv
[++ i
]);
403 if ( config
. randomkeys_keyspacelen
< 0 )
404 config
. randomkeys_keyspacelen
= 0 ;
405 } else if (! strcmp ( argv
[ i
], "-q" )) {
407 } else if (! strcmp ( argv
[ i
], "--csv" )) {
409 } else if (! strcmp ( argv
[ i
], "-l" )) {
411 } else if (! strcmp ( argv
[ i
], "-I" )) {
413 } else if (! strcmp ( argv
[ i
], "-t" )) {
414 if ( lastarg
) goto invalid
;
415 /* We get the list of tests to run as a string in the form
416 * get,set,lrange,...,test_N. Then we add a comma before and
417 * after the string in order to make sure that searching
418 * for ",testname," will always get a match if the test is
420 config
. tests
= sdsnew ( "," );
421 config
. tests
= sdscat ( config
. tests
,( char *) argv
[++ i
]);
422 config
. tests
= sdscat ( config
. tests
, "," );
423 sdstolower ( config
. tests
);
424 } else if (! strcmp ( argv
[ i
], "--help" )) {
428 /* Assume the user meant to provide an option when the arg starts
429 * with a dash. We're done otherwise and should use the remainder
430 * as the command and arguments for running the benchmark. */
431 if ( argv
[ i
][ 0 ] == '-' ) goto invalid
;
439 printf ( "Invalid option \" %s \" or option argument missing \n\n " , argv
[ i
]);
443 "Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>] \n\n "
444 " -h <hostname> Server hostname (default 127.0.0.1) \n "
445 " -p <port> Server port (default 6379) \n "
446 " -s <socket> Server socket (overrides host and port) \n "
447 " -c <clients> Number of parallel connections (default 50) \n "
448 " -n <requests> Total number of requests (default 10000) \n "
449 " -d <size> Data size of SET/GET value in bytes (default 2) \n "
450 " -k <boolean> 1=keep alive 0=reconnect (default 1) \n "
451 " -r <keyspacelen> Use random keys for SET/GET/INCR, random values for SADD \n "
452 " Using this option the benchmark will get/set keys \n "
453 " in the form mykey_rand:000000012456 instead of constant \n "
454 " keys, the <keyspacelen> argument determines the max \n "
455 " number of values for the random number. For instance \n "
456 " if set to 10 only rand:000000000000 - rand:000000000009 \n "
457 " range will be allowed. \n "
458 " -P <numreq> Pipeline <numreq> requests. Default 1 (no pipeline). \n "
459 " -q Quiet. Just show query/sec values \n "
460 " --csv Output in CSV format \n "
461 " -l Loop. Run the tests forever \n "
462 " -t <tests> Only run the comma separated list of tests. The test \n "
463 " names are the same as the ones produced as output. \n "
464 " -I Idle mode. Just open N idle connections and wait. \n\n "
466 " Run the benchmark with the default configuration against 127.0.0.1:6379: \n "
467 " $ redis-benchmark \n\n "
468 " Use 20 parallel clients, for a total of 100k requests, against 192.168.1.1: \n "
469 " $ redis-benchmark -h 192.168.1.1 -p 6379 -n 100000 -c 20 \n\n "
470 " Fill 127.0.0.1:6379 with about 1 million keys only using the SET test: \n "
471 " $ redis-benchmark -t set -n 1000000 -r 100000000 \n\n "
472 " Benchmark 127.0.0.1:6379 for a few commands producing CSV output: \n "
473 " $ redis-benchmark -t ping,set,get -n 100000 --csv \n\n "
474 " Fill a list with 10000 random elements: \n "
475 " $ redis-benchmark -r 10000 -n 10000 lpush mylist ele:rand:000000000000 \n\n "
480 int showThroughput ( struct aeEventLoop
* eventLoop
, long long id
, void * clientData
) {
481 REDIS_NOTUSED ( eventLoop
);
483 REDIS_NOTUSED ( clientData
);
485 if ( config
. csv
) return 250 ;
486 float dt
= ( float )( mstime ()- config
. start
)/ 1000.0 ;
487 float rps
= ( float ) config
. requests_finished
/ dt
;
488 printf ( " %s : %.2f \r " , config
. title
, rps
);
490 return 250 ; /* every 250ms */
493 /* Return true if the named test was selected using the -t command line
494 * switch, or if all the tests are selected (no -t passed by user). */
495 int test_is_selected ( char * name
) {
497 int l
= strlen ( name
);
499 if ( config
. tests
== NULL
) return 1 ;
501 memcpy ( buf
+ 1 , name
, l
);
504 return strstr ( config
. tests
, buf
) != NULL
;
507 int main ( int argc
, const char ** argv
) {
515 signal ( SIGHUP
, SIG_IGN
);
516 signal ( SIGPIPE
, SIG_IGN
);
518 config
. numclients
= 50 ;
519 config
. requests
= 10000 ;
520 config
. liveclients
= 0 ;
521 config
. el
= aeCreateEventLoop ( 1024 * 10 );
522 aeCreateTimeEvent ( config
. el
, 1 , showThroughput
, NULL
, NULL
);
523 config
. keepalive
= 1 ;
526 config
. randomkeys
= 0 ;
527 config
. randomkeys_keyspacelen
= 0 ;
532 config
. latency
= NULL
;
533 config
. clients
= listCreate ();
534 config
. hostip
= "127.0.0.1" ;
535 config
. hostport
= 6379 ;
536 config
. hostsocket
= NULL
;
539 i
= parseOptions ( argc
, argv
);
543 config
. latency
= zmalloc ( sizeof ( long long )* config
. requests
);
545 if ( config
. keepalive
== 0 ) {
546 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 " );
549 if ( config
. idlemode
) {
550 printf ( "Creating %d idle connections and waiting forever (Ctrl+C when done) \n " , config
. numclients
);
551 c
= createClient ( "" , 0 ); /* will never receive a reply */
552 createMissingClients ( c
);
554 /* and will wait for every */
557 /* Run benchmark with command in the remainder of the arguments. */
559 sds title
= sdsnew ( argv
[ 0 ]);
560 for ( i
= 1 ; i
< argc
; i
++) {
561 title
= sdscatlen ( title
, " " , 1 );
562 title
= sdscatlen ( title
, ( char *) argv
[ i
], strlen ( argv
[ i
]));
566 len
= redisFormatCommandArgv (& cmd
, argc
, argv
, NULL
);
567 benchmark ( title
, cmd
, len
);
569 } while ( config
. loop
);
574 /* Run default benchmark suite. */
576 data
= zmalloc ( config
. datasize
+ 1 );
577 memset ( data
, 'x' , config
. datasize
);
578 data
[ config
. datasize
] = '\0' ;
580 if ( test_is_selected ( "ping_inline" ) || test_is_selected ( "ping" ))
581 benchmark ( "PING_INLINE" , "PING \r\n " , 6 );
583 if ( test_is_selected ( "ping_mbulk" ) || test_is_selected ( "ping" )) {
584 len
= redisFormatCommand (& cmd
, "PING" );
585 benchmark ( "PING_BULK" , cmd
, len
);
589 if ( test_is_selected ( "set" )) {
590 len
= redisFormatCommand (& cmd
, "SET foo:rand:000000000000 %s " , data
);
591 benchmark ( "SET" , cmd
, len
);
595 if ( test_is_selected ( "get" )) {
596 len
= redisFormatCommand (& cmd
, "GET foo:rand:000000000000" );
597 benchmark ( "GET" , cmd
, len
);
601 if ( test_is_selected ( "incr" )) {
602 len
= redisFormatCommand (& cmd
, "INCR counter:rand:000000000000" );
603 benchmark ( "INCR" , cmd
, len
);
607 if ( test_is_selected ( "lpush" )) {
608 len
= redisFormatCommand (& cmd
, "LPUSH mylist %s " , data
);
609 benchmark ( "LPUSH" , cmd
, len
);
613 if ( test_is_selected ( "lpop" )) {
614 len
= redisFormatCommand (& cmd
, "LPOP mylist" );
615 benchmark ( "LPOP" , cmd
, len
);
619 if ( test_is_selected ( "sadd" )) {
620 len
= redisFormatCommand (& cmd
,
621 "SADD myset counter:rand:000000000000" );
622 benchmark ( "SADD" , cmd
, len
);
626 if ( test_is_selected ( "spop" )) {
627 len
= redisFormatCommand (& cmd
, "SPOP myset" );
628 benchmark ( "SPOP" , cmd
, len
);
632 if ( test_is_selected ( "lrange" ) ||
633 test_is_selected ( "lrange_100" ) ||
634 test_is_selected ( "lrange_300" ) ||
635 test_is_selected ( "lrange_500" ) ||
636 test_is_selected ( "lrange_600" ))
638 len
= redisFormatCommand (& cmd
, "LPUSH mylist %s " , data
);
639 benchmark ( "LPUSH (needed to benchmark LRANGE)" , cmd
, len
);
643 if ( test_is_selected ( "lrange" ) || test_is_selected ( "lrange_100" )) {
644 len
= redisFormatCommand (& cmd
, "LRANGE mylist 0 99" );
645 benchmark ( "LRANGE_100 (first 100 elements)" , cmd
, len
);
649 if ( test_is_selected ( "lrange" ) || test_is_selected ( "lrange_300" )) {
650 len
= redisFormatCommand (& cmd
, "LRANGE mylist 0 299" );
651 benchmark ( "LRANGE_300 (first 300 elements)" , cmd
, len
);
655 if ( test_is_selected ( "lrange" ) || test_is_selected ( "lrange_500" )) {
656 len
= redisFormatCommand (& cmd
, "LRANGE mylist 0 449" );
657 benchmark ( "LRANGE_500 (first 450 elements)" , cmd
, len
);
661 if ( test_is_selected ( "lrange" ) || test_is_selected ( "lrange_600" )) {
662 len
= redisFormatCommand (& cmd
, "LRANGE mylist 0 599" );
663 benchmark ( "LRANGE_600 (first 600 elements)" , cmd
, len
);
667 if ( test_is_selected ( "mset" )) {
668 const char * argv
[ 21 ];
670 for ( i
= 1 ; i
< 21 ; i
+= 2 ) {
671 argv
[ i
] = "foo:rand:000000000000" ;
674 len
= redisFormatCommandArgv (& cmd
, 21 , argv
, NULL
);
675 benchmark ( "MSET (10 keys)" , cmd
, len
);
679 if (! config
. csv
) printf ( " \n " );
680 } while ( config
. loop
);