This is a fast metrics server, ideal for tracking throttling. Put values to the server, and then count them. Values expire according to the TTL seconds you set.
This repo provides both the client and server.
One would think Redis is a natural choice for this type of service. When you need to count short-lived, namespaced keys, Redis does not exactly meet this use case. There are solutions but significant application level code is required, or Redis needs to be wrapped in another service.
For example, one solution in Redis is to put everything at the top level like namespace:key:entry-id
and
run SCAN 0 MATCH namespace:key:*
. But it returns all the keys, which you can then count. It also requires that you
implement some sort of unique ID generation. Redis does support KEEPTTL
during SET
to each key.
Another approach in Redis is to store entries in a sorted set or other data structure, at a key like namespace:key
and
omit or expire entries in application logic. We also considered adding a service in front of Redis to handle that, but
that seemed about as much overhead as just writing exactly what we needed into this dracula project.
At the outset, we wanted to meet the following needs.
- every key entry has the same expiry time
- every key is in a namespace
- support write-heavy operations
- support tens of thousands of simultaneous reads and writes on low-end hardware
- AWS graviton / ARM support
- easy deploy
We were able to achieve these goals in a server that uses about 1.2MB of RAM on startup.
Pre-built binaries of dracula-server
and dracula-cli
are available in the Releases tab.
dracula container images are published to:
Example running the server:
docker run -d --rm -p "3509:3509/udp" --name dracula-server-test "ghcr.io/mailsac/dracula" /app/dracula-server -v
The dracula CLI is also included in the docker image. Dracula works from IP addresses, so to use docker you must first get the dracula server's container IP:
DOCKER_DRACULA_IP=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' dracula-server-test)
echo ${DOCKER_DRACULA_IP}
then it is possible to use the docker dracula-cli
and make a request to count some records:
docker run --rm "ghcr.io/mailsac/dracula" /app/dracula-cli -i ${DOCKER_DRACULA_IP} -put -k testing_key
docker run --rm "ghcr.io/mailsac/dracula" /app/dracula-cli -i ${DOCKER_DRACULA_IP} -count -k testing_key
# 1
Alternatively, build it (Golang 1.16+ recommended):
make build-server
Optionally set a pre-shared signing secret via environment variable:
export DRACULA_SECRET=very-secure3;
then run the server with default settings and verbose logging:
./dracula-server -v
Available server options:
./dracula-server -h
Usage of ./dracula-server:
-c 192.168.0.1:3509,192.168.0.2:3555
Enable cluster replication. Peers must be comma-separated ip:port like 192.168.0.1:3509,192.168.0.2:3555.
-h Print this help
-i string
Self peer IP and host like 192.168.0.1:3509 to identify self in the cluster
-p int
UDP this server will run on (default 3509)
-prom string
Enable prometheus metrics. May cause pauses. Example: '0.0.0.0:9090'
-s string
Optional pre-shared auth secret if not using env var DRACULA_SECRET
-t int
TTL secs - entries will expire after this many seconds (default 60)
-tcp int
TCP port this server will run on (default 3509)
-v Verbose logging
-version
Then use the cli for testing. Put keys to the server:
make build-cli
./dracula-cli -put -k asdf
./dracula-cli -put -k asdf
./dracula-cli -put -k asdfjkl
./dracula-cli -put -k jkl
./dracula-cli -count -k asdf
# > 2
./dracula-cli -keys -k "a*"
# > 1) asdf
# > 2) asdfjkl
./dracula-cli -keys -k "a*"
# > 1) asdf
# > 2) asdfjkl
# > 3) jkl
Keys can be namespaced with the -n
flag.
Dracula server can be embedded in your Go application:
package main
import (
"github.com/mailsac/dracula/server"
)
func main() {
var expireAfterSeconds int64 = 60
preSharedSecret := "supersecret"
s := server.NewServer(expireAfterSeconds, preSharedSecret)
err := s.Listen(3509, 3509)
if err != nil {
panic(err)
}
}
then use the Go client to put values and count them:
package main
import (
"fmt"
"github.com/mailsac/dracula/client"
"time"
)
const (
// include just one server or comma-separated pool
serverIPPortPool = "127.0.0.1:3509,192.168.0.1:3509"
namespace = "default"
)
var (
clientResponseTimeout = time.Second * 6
)
func main() {
c := client.NewClient(client.Config{
RemoteUDPIPPortList: serverIPPortPool,
RemoteTCPIPPortList: serverIPPortPool,
Timeout: clientResponseTimeout,
PreSharedSecret: "very-secure3",
})
c.Listen(9001)
// seed some entries
c.Put(namespace, "192.168.0.50")
c.Put(namespace, "192.168.0.50")
c.Put(namespace, "mailsac.com")
c.Put(namespace, "hello.msdc.co")
total, _ := c.Count(namespace, "192.168.0.50")
fmt.Println("192.168.0.50", total) // 2
// wait a while
time.Sleep(time.Second * 60)
// now entries will be expired
total, _ = c.Count(namespace, "mailsac.com")
fmt.Println("mailsac.com", total) // 0
}
Entries are grouped in a namespace
.
See server/server_test.go
for examples.
Basic garbage collection metrics are exposed when using the server flag --prom=0.0.0.0:9090
flag (you can use a custom host and port).
# HELP dracula_key_sum_in_gc_namespaces Count of key values in last garbed collected namespace valid keys
# TYPE dracula_key_sum_in_gc_namespaces gauge
dracula_key_sum_in_gc_namespaces 0
# HELP dracula_keys_in_gc_namespaces Count of unexpired keys in last garbage collected namespaces
# TYPE dracula_keys_in_gc_namespaces gauge
dracula_keys_in_gc_namespaces 0
# HELP dracula_max_namespaces_denom Denominator/portion of namespaces to be garbage collected each cleanup run
# TYPE dracula_max_namespaces_denom gauge
dracula_max_namespaces_denom 3
# HELP dracula_namespaces_count Number of top level key namespaces
# TYPE dracula_namespaces_count gauge
dracula_namespaces_count 3
# HELP dracula_namespaces_gc_count Number of namespaces which had keys garbage collected during last cleanup run
# TYPE dracula_namespaces_gc_count gauge
dracula_namespaces_gc_count 1
Rudimentary and experimental HA is possible via replication by using the -p
peers list and -i
self IP:host
pair flags such as:
dracula-server -p "127.0.0.1:3509,127.0.0.1:3519,127.0.0.1:3529" -i 127.0.0.1:3529
where clients can connect to the pool and maintain a list of -i
servers:
dracula-cli -i "127.0.0.1:3509,127.0.0.1:3519,127.0.0.1:3529" [...more flags]
All peers in the cluster are listed, as well as the self IP and host in the cluster. These flags tell the dracula server to replicate all PUT messages to peers.
In practice, replication only meets the use case of short-lived, imperfectly consistent metrics.
If you require exact replication across peers, this feature will not be tolerant to network partitioning and will not meet your needs.
Messages are sent over UDP and not reliable. The trade-off desired is speed. This project was initially implemented to be a throttling server, so missing a few messages wasn't a big deal. Also, UDP can cause TCP traffic on the same box to be slower under heavy load. This was ideal for our use-case, but may not be for yours.
A UDP message is limited to 1500 bytes. See protocol/
for exactly how messages are parsed.
The namespace can be 64 bytes and the data value can be 1419 bytes.
The maximum entries in a key is the highest value of uint32.
Authentication is just strong enough to make sure you aren't sending messages to the wrong server. It is assumed dracula is running in a trusted environment.
- Persistence and value storage
- Better support for high availability under network partitions
- Clients in other languages
- Retries
Please open an Issue to request a feature.
See dependencies listed in go.mod for copyright notices and licenses.
Copyright (c) 2021-2023 Forking Software LLC
Project | License SPDX Identifier |
---|---|
Dracula CLI | LGPL-3.0 |
Dracula Go Client | LGPL-3.0 |
Dracula Server | GPL-2.0 |
See the files LICENSE.clients.txt and LICENSE.server.txt at the root of this repo.
Dual licensing is available. Contact Forking Software LLC.