├── logo.png ├── LICENSE ├── README.md └── main.go /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidwall/doppio/HEAD/logo.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019, Joshua J Baker 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 10 | SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 12 | OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 13 | CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Doppio 5 |

6 | 7 | Doppio is a fast experimental LRU cache server on top of [ristretto](https://github.com/dgraph-io/ristretto), [redcon](https://github.com/tidwall/redcon), and [evio](https://github.com/tidwall/evio). With support for the Redis protocol. 8 | 9 | ## Features 10 | 11 | - Multithreaded read and write operations. 12 | - Simplified Redis protocol support. Most Redis clients will be able to use Doppio. 13 | - Auto eviction of older items when the server is at optional cache capacity. 14 | - Optional `--single-threaded` flag for single-threaded, event-loop networking mode. 15 | 16 | ## Getting Started 17 | 18 | ### Building 19 | 20 | To start using Doppio, install Go and run `go get`: 21 | 22 | ``` 23 | $ go get -u github.com/tidwall/doppio 24 | ``` 25 | 26 | This will build the application. 27 | 28 | 29 | ### Running 30 | 31 | Start the server by running the `doppio` application: 32 | 33 | ``` 34 | $ ./doppio 35 | 36 | 6307:M 26 Sep 17:10:50.304 * Server started on port 6380 (darwin/amd64, 12 threads, 1.0 GB capacity) 37 | ``` 38 | 39 | ### Command line interface 40 | 41 | Use the `redis-cli` application provided by the [Redis](https://github.com/antirez/redis) project. 42 | 43 | ``` 44 | $ redis-cli -p 6380 45 | > SET hello world 46 | OK 47 | 48 | > GET hello 49 | "world" 50 | 51 | > DEL hello 52 | (integer) 1 53 | 54 | > GET hello 55 | (nil) 56 | ``` 57 | 58 | ### Options 59 | 60 | Choose LRU capacity using the `-c` flag. 61 | 62 | ```sh 63 | $ ./doppio -c 1gb # max capactiy of 1 GB 64 | $ ./doppio -c 16gb # max capactiy of 16 GB 65 | $ ./doppio -c 500mb # max capactiy of 500 MB 66 | ``` 67 | 68 | Run in single-threaded mode using the `--single-threaded` flag. 69 | 70 | ```sh 71 | $ ./doppio --single-threaded 72 | ``` 73 | 74 | ## Performance 75 | 76 | Using the `redis-benchmark` tool provided by the [Redis](https://github.com/antirez/redis) project we `SET` 10,000,000 random keys and then follow it up with 10,000,000 `GET` operations. 77 | 78 | Running on a big 48 thread r5.12xlarge server at AWS. 79 | 80 | ### Doppio 81 | 82 | ``` 83 | $ redis-benchmark -p 6380 -q -t SET,GET -P 1024 -r 1000000000 -n 10000000 84 | SET: 7886435.50 requests per second 85 | GET: 10482180.00 requests per second 86 | ``` 87 | 88 | ### Redis 89 | 90 | ``` 91 | $ redis-benchmark -p 6379 -q -t SET,GET -P 1024 -r 1000000000 -n 10000000 92 | SET: 1171646.31 requests per second 93 | GET: 1762114.50 requests per second 94 | ``` 95 | 96 | 97 | ### Single-threaded mode 98 | 99 | Using the `--single-threaded` flag or `GOMAXPROCS=1`. 100 | 101 | ``` 102 | $ redis-benchmark -p 6380 -q -t SET,GET -P 1024 -r 1000000000 -n 10000000 103 | SET: 1721763.00 requests per second 104 | GET: 1942124.62 requests per second 105 | ``` 106 | 107 | ## Contact 108 | 109 | Josh Baker [@tidwall](http://twitter.com/tidwall) 110 | 111 | ## License 112 | Doppio source code is available under the MIT [License](/LICENSE). 113 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/dgraph-io/ristretto" 11 | "github.com/dustin/go-humanize" 12 | "github.com/tidwall/evio" 13 | "github.com/tidwall/redcon" 14 | "github.com/tidwall/redlog" 15 | ) 16 | 17 | var log = redlog.New(os.Stderr) 18 | var cache *ristretto.Cache 19 | var port int 20 | var capacity uint64 21 | var threads int 22 | 23 | func main() { 24 | var capflag string 25 | var single bool 26 | flag.IntVar(&port, "p", 6380, "Server port") 27 | flag.BoolVar(&single, "single-threaded", runtime.GOMAXPROCS(0) == 1, 28 | "Run in Single-threaded mode") 29 | flag.StringVar(&capflag, "c", "1gb", 30 | "Cache capacity of the database, such as 4gb, 500mb, etc.") 31 | flag.Parse() 32 | x, err := humanize.ParseBytes(capflag) 33 | if err != nil { 34 | log.Fatalf("Invalid cache capacity %v", capflag) 35 | } 36 | capacity = uint64(x) 37 | cache, err = ristretto.NewCache(&ristretto.Config{ 38 | MaxCost: int64(x), 39 | NumCounters: int64(x) * 10, 40 | BufferItems: 64, 41 | }) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | if single { 46 | threads = 1 47 | } else { 48 | threads = runtime.GOMAXPROCS(0) 49 | } 50 | 51 | if threads == 1 { 52 | useEvio() 53 | } else { 54 | useRedcon() 55 | } 56 | } 57 | 58 | func printMast() { 59 | fmt.Printf("\n%s\n\n", strings.Join([]string{ 60 | "d8888b. .d88b. d8888b. d8888b. d888888b .d88b. ", 61 | "88 `8D .8P Y8. 88 `8D 88 `8D `88' .8P Y8. ", 62 | "88 88 88 88 88oodD' 88oodD' 88 88 88 ", 63 | "88 .8D `8b d8' 88 88 .88. `8b d8' ", 64 | "Y8888D' `Y88P' 88 88 Y888888P `Y88P' ", 65 | }, "\n")) 66 | 67 | threadss := fmt.Sprintf("%d threads", threads) 68 | if threads == 1 { 69 | threadss = "single-threaded" 70 | } 71 | log.Printf("Server started on port %d (%s/%s, %s, %s capacity)\n", 72 | port, runtime.GOOS, runtime.GOARCH, threadss, 73 | humanize.Bytes(capacity)) 74 | } 75 | 76 | func useEvio() { 77 | var events evio.Events 78 | events.NumLoops = threads 79 | 80 | events.Serving = func(srv evio.Server) (action evio.Action) { 81 | printMast() 82 | return 83 | } 84 | 85 | events.Opened = func(ec evio.Conn) ( 86 | out []byte, opts evio.Options, action evio.Action, 87 | ) { 88 | ec.SetContext(&client{}) 89 | return 90 | } 91 | 92 | events.Closed = func(ec evio.Conn, err error) (action evio.Action) { 93 | return 94 | } 95 | 96 | events.Data = func(ec evio.Conn, in []byte) ( 97 | out []byte, action evio.Action, 98 | ) { 99 | c := ec.Context().(*client) 100 | data := c.is.Begin(in) 101 | var complete bool 102 | var err error 103 | var args [][]byte 104 | for action == evio.None { 105 | complete, args, _, data, err = 106 | redcon.ReadNextCommand(data, args[:0]) 107 | if err != nil { 108 | action = evio.Close 109 | out = redcon.AppendError(out, err.Error()) 110 | break 111 | } 112 | if !complete { 113 | break 114 | } 115 | if len(args) > 0 { 116 | out, action = handleCommand(out, args) 117 | } 118 | } 119 | c.is.End(data) 120 | return 121 | } 122 | log.Fatal(evio.Serve(events, fmt.Sprintf("tcp://:%d", port))) 123 | } 124 | 125 | type client struct { 126 | is evio.InputStream 127 | addr string 128 | } 129 | 130 | func useRedcon() { 131 | go func() { 132 | printMast() 133 | }() 134 | log.Fatal(redcon.ListenAndServe(fmt.Sprintf(":%d", port), 135 | func(conn redcon.Conn, cmd redcon.Command) { 136 | out, action := handleCommand(nil, cmd.Args) 137 | if len(out) > 0 { 138 | conn.WriteRaw(out) 139 | } 140 | if action == evio.Close { 141 | conn.Close() 142 | } 143 | }, nil, nil)) 144 | } 145 | 146 | func handleCommand(out []byte, args [][]byte) ([]byte, evio.Action) { 147 | var action evio.Action 148 | switch strings.ToUpper(string(args[0])) { 149 | default: 150 | out = redcon.AppendError(out, 151 | "ERR unknown command '"+string(args[0])+"'") 152 | case "QUIT": 153 | out = redcon.AppendOK(out) 154 | action = evio.Close 155 | case "SHUTDOWN": 156 | out = redcon.AppendOK(out) 157 | log.Fatal("Shutting server down, bye bye") 158 | case "PING": 159 | if len(args) == 1 { 160 | out = redcon.AppendString(out, "PONG") 161 | } else if len(args) == 2 { 162 | out = redcon.AppendBulk(out, args[1]) 163 | } else { 164 | out = redcon.AppendError(out, "ERR invalid number of arguments") 165 | } 166 | case "ECHO": 167 | if len(args) != 2 { 168 | out = redcon.AppendError(out, "ERR invalid number of arguments") 169 | } else if len(args) == 2 { 170 | out = redcon.AppendBulk(out, args[1]) 171 | } 172 | case "SET": 173 | if len(args) != 3 { 174 | out = redcon.AppendError(out, "ERR invalid number of arguments") 175 | } else { 176 | cache.Set(string(args[1]), string(args[2]), int64(len(args[2]))) 177 | out = redcon.AppendOK(out) 178 | } 179 | case "GET": 180 | if len(args) != 2 { 181 | out = redcon.AppendError(out, "ERR invalid number of arguments") 182 | } else if val, ok := cache.Get(string(args[1])); !ok { 183 | out = redcon.AppendNull(out) 184 | } else { 185 | out = redcon.AppendBulkString(out, val.(string)) 186 | } 187 | case "DEL": 188 | if len(args) < 2 { 189 | out = redcon.AppendError(out, "ERR invalid number of arguments") 190 | } else { 191 | for i := 1; i < len(args); i++ { 192 | cache.Del(string(args[i])) 193 | } 194 | out = redcon.AppendInt(out, int64(len(args)-1)) 195 | } 196 | } 197 | return out, action 198 | } 199 | --------------------------------------------------------------------------------