xref: /redis-3.2.3/src/redis-benchmark.c (revision 5e1d2d30)
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