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
;
75 typedef struct _client
{
76 redisContext
*context
;
78 char *randptr
[10]; /* needed for MSET against 10 keys */
80 unsigned int written
; /* bytes of 'obuf' already written */
81 long long start
; /* start time of a request */
82 long long latency
; /* request latency */
86 static void writeHandler(aeEventLoop
*el
, int fd
, void *privdata
, int mask
);
87 static void createMissingClients(client c
);
90 static long long ustime(void) {
94 gettimeofday(&tv
, NULL
);
95 ust
= ((long)tv
.tv_sec
)*1000000;
100 static long long mstime(void) {
104 gettimeofday(&tv
, NULL
);
105 mst
= ((long)tv
.tv_sec
)*1000;
106 mst
+= tv
.tv_usec
/1000;
110 static void freeClient(client c
) {
112 aeDeleteFileEvent(config
.el
,c
->context
->fd
,AE_WRITABLE
);
113 aeDeleteFileEvent(config
.el
,c
->context
->fd
,AE_READABLE
);
114 redisFree(c
->context
);
117 config
.liveclients
--;
118 ln
= listSearchKey(config
.clients
,c
);
120 listDelNode(config
.clients
,ln
);
123 static void freeAllClients(void) {
124 listNode
*ln
= config
.clients
->head
, *next
;
128 freeClient(ln
->value
);
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
);
140 static void randomizeClientKey(client c
) {
144 for (i
= 0; i
< c
->randlen
; i
++) {
145 r
= random() % config
.randomkeys_keyspacelen
;
146 snprintf(buf
,sizeof(buf
),"%012zu",r
);
147 memcpy(c
->randptr
[i
],buf
,12);
151 static void clientDone(client c
) {
152 if (config
.requests_finished
== config
.requests
) {
157 if (config
.keepalive
) {
160 config
.liveclients
--;
161 createMissingClients(c
);
162 config
.liveclients
++;
167 static void readHandler(aeEventLoop
*el
, int fd
, void *privdata
, int mask
) {
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
);
179 if (redisBufferRead(c
->context
) != REDIS_OK
) {
180 fprintf(stderr
,"Error: %s\n",c
->context
->errstr
);
183 if (redisGetReply(c
->context
,&reply
) != REDIS_OK
) {
184 fprintf(stderr
,"Error: %s\n",c
->context
->errstr
);
188 if (reply
== (void*)REDIS_REPLY_ERROR
) {
189 fprintf(stderr
,"Unexpected error reply, exiting...\n");
193 if (config
.requests_finished
< config
.requests
)
194 config
.latency
[config
.requests_finished
++] = c
->latency
;
200 static void writeHandler(aeEventLoop
*el
, int fd
, void *privdata
, int mask
) {
206 /* Initialize request when nothing was written. */
207 if (c
->written
== 0) {
208 /* Enforce upper bound to number of requests. */
209 if (config
.requests_issued
++ >= config
.requests
) {
214 /* Really initialize: randomize keys and set start time. */
215 if (config
.randomkeys
) randomizeClientKey(c
);
220 if (sdslen(c
->obuf
) > c
->written
) {
221 void *ptr
= c
->obuf
+c
->written
;
222 int nwritten
= write(c
->context
->fd
,ptr
,sdslen(c
->obuf
)-c
->written
);
223 if (nwritten
== -1) {
225 fprintf(stderr
, "Writing to socket: %s\n", strerror(errno
));
229 c
->written
+= nwritten
;
230 if (sdslen(c
->obuf
) == c
->written
) {
231 aeDeleteFileEvent(config
.el
,c
->context
->fd
,AE_WRITABLE
);
232 aeCreateFileEvent(config
.el
,c
->context
->fd
,AE_READABLE
,readHandler
,c
);
237 static client
createClient(const char *cmd
, size_t len
) {
238 client c
= zmalloc(sizeof(struct _client
));
239 if (config
.hostsocket
== NULL
) {
240 c
->context
= redisConnectNonBlock(config
.hostip
,config
.hostport
);
242 c
->context
= redisConnectUnixNonBlock(config
.hostsocket
);
244 if (c
->context
->err
) {
245 fprintf(stderr
,"Could not connect to Redis at ");
246 if (config
.hostsocket
== NULL
)
247 fprintf(stderr
,"%s:%d: %s\n",config
.hostip
,config
.hostport
,c
->context
->errstr
);
249 fprintf(stderr
,"%s: %s\n",config
.hostsocket
,c
->context
->errstr
);
252 c
->obuf
= sdsnewlen(cmd
,len
);
256 /* Find substrings in the output buffer that need to be randomized. */
257 if (config
.randomkeys
) {
258 char *p
= c
->obuf
, *newline
;
259 while ((p
= strstr(p
,":rand:")) != NULL
) {
260 newline
= strstr(p
,"\r\n");
261 assert(newline
-(p
+6) == 12); /* 12 chars for randomness */
262 assert(c
->randlen
< (signed)(sizeof(c
->randptr
)/sizeof(char*)));
263 c
->randptr
[c
->randlen
++] = p
+6;
268 redisSetReplyObjectFunctions(c
->context
,NULL
);
269 aeCreateFileEvent(config
.el
,c
->context
->fd
,AE_WRITABLE
,writeHandler
,c
);
270 listAddNodeTail(config
.clients
,c
);
271 config
.liveclients
++;
275 static void createMissingClients(client c
) {
278 while(config
.liveclients
< config
.numclients
) {
279 createClient(c
->obuf
,sdslen(c
->obuf
));
281 /* Listen backlog is quite limited on most systems */
289 static int compareLatency(const void *a
, const void *b
) {
290 return (*(long long*)a
)-(*(long long*)b
);
293 static void showLatencyReport(void) {
295 float perc
, reqpersec
;
297 reqpersec
= (float)config
.requests_finished
/((float)config
.totlatency
/1000);
299 printf("====== %s ======\n", config
.title
);
300 printf(" %d requests completed in %.2f seconds\n", config
.requests_finished
,
301 (float)config
.totlatency
/1000);
302 printf(" %d parallel clients\n", config
.numclients
);
303 printf(" %d bytes payload\n", config
.datasize
);
304 printf(" keep alive: %d\n", config
.keepalive
);
307 qsort(config
.latency
,config
.requests
,sizeof(long long),compareLatency
);
308 for (i
= 0; i
< config
.requests
; i
++) {
309 if (config
.latency
[i
]/1000 != curlat
|| i
== (config
.requests
-1)) {
310 curlat
= config
.latency
[i
]/1000;
311 perc
= ((float)(i
+1)*100)/config
.requests
;
312 printf("%.2f%% <= %d milliseconds\n", perc
, curlat
);
315 printf("%.2f requests per second\n\n", reqpersec
);
317 printf("%s: %.2f requests per second\n", config
.title
, reqpersec
);
321 static void benchmark(const char *title
, const char *cmd
, int len
) {
324 config
.title
= title
;
325 config
.requests_issued
= 0;
326 config
.requests_finished
= 0;
328 c
= createClient(cmd
,len
);
329 createMissingClients(c
);
331 config
.start
= mstime();
333 config
.totlatency
= mstime()-config
.start
;
339 /* Returns number of consumed options. */
340 int parseOptions(int argc
, const char **argv
) {
345 for (i
= 1; i
< argc
; i
++) {
346 lastarg
= (i
== (argc
-1));
348 if (!strcmp(argv
[i
],"-c")) {
349 if (lastarg
) goto invalid
;
350 config
.numclients
= atoi(argv
[++i
]);
351 } else if (!strcmp(argv
[i
],"-n")) {
352 if (lastarg
) goto invalid
;
353 config
.requests
= atoi(argv
[++i
]);
354 } else if (!strcmp(argv
[i
],"-k")) {
355 if (lastarg
) goto invalid
;
356 config
.keepalive
= atoi(argv
[++i
]);
357 } else if (!strcmp(argv
[i
],"-h")) {
358 if (lastarg
) goto invalid
;
359 config
.hostip
= strdup(argv
[++i
]);
360 } else if (!strcmp(argv
[i
],"-p")) {
361 if (lastarg
) goto invalid
;
362 config
.hostport
= atoi(argv
[++i
]);
363 } else if (!strcmp(argv
[i
],"-s")) {
364 if (lastarg
) goto invalid
;
365 config
.hostsocket
= strdup(argv
[++i
]);
366 } else if (!strcmp(argv
[i
],"-d")) {
367 if (lastarg
) goto invalid
;
368 config
.datasize
= atoi(argv
[++i
]);
369 if (config
.datasize
< 1) config
.datasize
=1;
370 if (config
.datasize
> 1024*1024) config
.datasize
= 1024*1024;
371 } else if (!strcmp(argv
[i
],"-r")) {
372 if (lastarg
) goto invalid
;
373 config
.randomkeys
= 1;
374 config
.randomkeys_keyspacelen
= atoi(argv
[++i
]);
375 if (config
.randomkeys_keyspacelen
< 0)
376 config
.randomkeys_keyspacelen
= 0;
377 } else if (!strcmp(argv
[i
],"-q")) {
379 } else if (!strcmp(argv
[i
],"-l")) {
381 } else if (!strcmp(argv
[i
],"-I")) {
383 } else if (!strcmp(argv
[i
],"--help")) {
387 /* Assume the user meant to provide an option when the arg starts
388 * with a dash. We're done otherwise and should use the remainder
389 * as the command and arguments for running the benchmark. */
390 if (argv
[i
][0] == '-') goto invalid
;
398 printf("Invalid option \"%s\" or option argument missing\n\n",argv
[i
]);
401 printf("Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]\n\n");
402 printf(" -h <hostname> Server hostname (default 127.0.0.1)\n");
403 printf(" -p <port> Server port (default 6379)\n");
404 printf(" -s <socket> Server socket (overrides host and port)\n");
405 printf(" -c <clients> Number of parallel connections (default 50)\n");
406 printf(" -n <requests> Total number of requests (default 10000)\n");
407 printf(" -d <size> Data size of SET/GET value in bytes (default 2)\n");
408 printf(" -k <boolean> 1=keep alive 0=reconnect (default 1)\n");
409 printf(" -r <keyspacelen> Use random keys for SET/GET/INCR, random values for SADD\n");
410 printf(" Using this option the benchmark will get/set keys\n");
411 printf(" in the form mykey_rand000000012456 instead of constant\n");
412 printf(" keys, the <keyspacelen> argument determines the max\n");
413 printf(" number of values for the random number. For instance\n");
414 printf(" if set to 10 only rand000000000000 - rand000000000009\n");
415 printf(" range will be allowed.\n");
416 printf(" -q Quiet. Just show query/sec values\n");
417 printf(" -l Loop. Run the tests forever\n");
418 printf(" -I Idle mode. Just open N idle connections and wait.\n");
422 int showThroughput(struct aeEventLoop
*eventLoop
, long long id
, void *clientData
) {
423 REDIS_NOTUSED(eventLoop
);
425 REDIS_NOTUSED(clientData
);
427 float dt
= (float)(mstime()-config
.start
)/1000.0;
428 float rps
= (float)config
.requests_finished
/dt
;
429 printf("%s: %.2f\r", config
.title
, rps
);
431 return 250; /* every 250ms */
434 int main(int argc
, const char **argv
) {
441 signal(SIGHUP
, SIG_IGN
);
442 signal(SIGPIPE
, SIG_IGN
);
444 config
.numclients
= 50;
445 config
.requests
= 10000;
446 config
.liveclients
= 0;
447 config
.el
= aeCreateEventLoop();
448 aeCreateTimeEvent(config
.el
,1,showThroughput
,NULL
,NULL
);
449 config
.keepalive
= 1;
451 config
.randomkeys
= 0;
452 config
.randomkeys_keyspacelen
= 0;
456 config
.latency
= NULL
;
457 config
.clients
= listCreate();
458 config
.hostip
= "127.0.0.1";
459 config
.hostport
= 6379;
460 config
.hostsocket
= NULL
;
462 i
= parseOptions(argc
,argv
);
466 config
.latency
= zmalloc(sizeof(long long)*config
.requests
);
468 if (config
.keepalive
== 0) {
469 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");
472 if (config
.idlemode
) {
473 printf("Creating %d idle connections and waiting forever (Ctrl+C when done)\n", config
.numclients
);
474 c
= createClient("",0); /* will never receive a reply */
475 createMissingClients(c
);
477 /* and will wait for every */
480 /* Run benchmark with command in the remainder of the arguments. */
482 sds title
= sdsnew(argv
[0]);
483 for (i
= 1; i
< argc
; i
++) {
484 title
= sdscatlen(title
, " ", 1);
485 title
= sdscatlen(title
, (char*)argv
[i
], strlen(argv
[i
]));
489 len
= redisFormatCommandArgv(&cmd
,argc
,argv
,NULL
);
490 benchmark(title
,cmd
,len
);
492 } while(config
.loop
);
497 /* Run default benchmark suite. */
499 data
= zmalloc(config
.datasize
+1);
500 memset(data
,'x',config
.datasize
);
501 data
[config
.datasize
] = '\0';
503 benchmark("PING (inline)","PING\r\n",6);
505 len
= redisFormatCommand(&cmd
,"PING");
506 benchmark("PING",cmd
,len
);
509 const char *argv
[21];
511 for (i
= 1; i
< 21; i
+= 2) {
512 argv
[i
] = "foo:rand:000000000000";
515 len
= redisFormatCommandArgv(&cmd
,21,argv
,NULL
);
516 benchmark("MSET (10 keys)",cmd
,len
);
519 len
= redisFormatCommand(&cmd
,"SET foo:rand:000000000000 %s",data
);
520 benchmark("SET",cmd
,len
);
523 len
= redisFormatCommand(&cmd
,"GET foo:rand:000000000000");
524 benchmark("GET",cmd
,len
);
527 len
= redisFormatCommand(&cmd
,"INCR counter:rand:000000000000");
528 benchmark("INCR",cmd
,len
);
531 len
= redisFormatCommand(&cmd
,"LPUSH mylist %s",data
);
532 benchmark("LPUSH",cmd
,len
);
535 len
= redisFormatCommand(&cmd
,"LPOP mylist");
536 benchmark("LPOP",cmd
,len
);
539 len
= redisFormatCommand(&cmd
,"SADD myset counter:rand:000000000000");
540 benchmark("SADD",cmd
,len
);
543 len
= redisFormatCommand(&cmd
,"SPOP myset");
544 benchmark("SPOP",cmd
,len
);
547 len
= redisFormatCommand(&cmd
,"LPUSH mylist %s",data
);
548 benchmark("LPUSH (again, in order to bench LRANGE)",cmd
,len
);
551 len
= redisFormatCommand(&cmd
,"LRANGE mylist 0 99");
552 benchmark("LRANGE (first 100 elements)",cmd
,len
);
555 len
= redisFormatCommand(&cmd
,"LRANGE mylist 0 299");
556 benchmark("LRANGE (first 300 elements)",cmd
,len
);
559 len
= redisFormatCommand(&cmd
,"LRANGE mylist 0 449");
560 benchmark("LRANGE (first 450 elements)",cmd
,len
);
563 len
= redisFormatCommand(&cmd
,"LRANGE mylist 0 599");
564 benchmark("LRANGE (first 600 elements)",cmd
,len
);
568 } while(config
.loop
);