├── 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 |
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 |
--------------------------------------------------------------------------------