DiceDB(dicedb.io) |
DiceDB(dicedb.io) |
It’s written in Go.
These are the real numbers - https://dzone.com/articles/performance-and-scalability-analy...
Does not match with your benchmarks.
We had to write a small benchmark utility (membench) ourselves because the long-term metrics that we are optimizing need to be evaluated in a different way.
Also, the scripts, utilities, and infra configurations are mentioned. Feel free to run it.
One example among many:
https://github.com/DiceDB/dice/blob/0e241a9ca253f17b4d364cdf... defines func ExpandID, which reads from cycleMap without locking the package-global mutex; and func NextID, which writes to cycleMap under a lock of the package-global mutex. So writes are synchronized, but only between each other, and not with reads, so concurrent calls to ExpandID and NextID would race.
This is all fine as a hobby project or whatever, but very far from any kind of production-capable system.
This PR attempted to fix the memory model violation I mentioned in the parent comment, but also added in an extra change that swapped the sync.Mutex to a sync.RWMutex. The PR description claimed 2 benefits: "Eliminates the data race, ensuring thread safety" -- correct! at least to some level; but also "Improves performance by allowing concurrent ExpandID calls, which is likely a common operation" -- which is totally unsubstantiated, and very likely false, as RWMutex is only faster than a regular Mutex under very narrowly-defined load patterns.
In any case, the PR had no kind of test or benchmark to validate either of these claims, so not a great start by the author. But then a maintainer chimed in with a comment that expressed concerns about edge-condition performance details, without any kind of data or evidence, and apparently didn't care about (or know about?) the much more important fixes that the PR made re: data races.
https://github.com/DiceDB/dice/pull/1588#issuecomment-274521...
> I tried changing this, but I did not see any benefit in benchmark numbers.
No apparent understanding of the bugs in this code, nor how changes may or may not fix those bugs, nor really how performance is defined or can be meaningfully evaluated.
Again, hobby project or whatever, all good. But the authors and maintainers of this project are clearly, demonstrably, in over their heads on this one.
I understand the need for correct lock-free impls: Given OP's description, simply avoiding read mutexes can't be the way to go about it?
I could be wrong but the primary in-memory storage appears to be a standard Go map with locking. Is this a temporary choice for iterative development, and is there a longer-term plan to adopt a more optimized or custom data structure ?
I find the DiceDB's reactivity mechanism very intriguing, particularly the "re-execution" of the entire watch command (i.e re-running GET.WATCH mykey on key modification), it's an intriguing design choice.
From what I understand is the Eval func executes client side commands this seem to be laying foundation for more complex watch command that can be evaluated before sending notifications to clients.
But I have the following question.
What is the primary motivation behind re-executing the entire command, as opposed to simply notifying clients of a key change (as in Redis Pub/Sub or streams)? Is the intent to simplify client-side logic by handling complex key dependencies on the server?
Given that re-execution seems computationally expensive, especially with multiple watchers or more complex (hypothetical) watch commands, how are potential performance bottlenecks addressed?
How does this "re-execution" approach compare in terms of scalability and consistency to more established methods like server-side logic (e.g., Lua scripts in Redis) or change data capture (CDC) ?
Are there plans to support more complex watch commands beyond GET.WATCH (e.g. JSON.GET.WATCH), and how would re-execution scale in those cases?
I'm curious about the trade-offs considered in choosing this design and how it aligns with the project's overall goals. Any insights into these design decisions would help me understand its use-cases.
Thanks
Kinda refreshing to see someone own it and run with it
Reactive looks promising, doesn't look much useful in realworld for a cache. For example, a client subscribes for something and the machines goes down, what happens to reactivity?
| Metric | DiceDB | Redis |
| -------------------- | -------- | -------- |
| Throughput (ops/sec) | 15655 | 12267 |
| GET p50 (ms) | 0.227327 | 0.270335 |
| GET p90 (ms) | 0.337919 | 0.329727 |
| SET p50 (ms) | 0.230399 | 0.272383 |
| SET p90 (ms) | 0.339967 | 0.331775 |
UPD Nevermind, I didn't have my eyes open. Sorry for the confusion.Something I still fail to understand is where you can actually spend 20ms while answering a GET request in a RAM keyvalue storage (unless you implement it in Java).
I never gained much experience with existing opensource implementations, but when I was building proprietary solutions at my previous workplace, the in-memory response time was measured in tens-hundreds of microseconds. The lower bound of latency is mostly defined by syscalls so using io_uring should in theory result in even better timings, even though I never got to try it in production.
If you read from nvme AND also do the erasure-recovery across 6 nodes (lrc-12-2-2) then yes, you got into tens of milliseconds. But seeing these numbers for a single node RAM DB just doesn't make sense and I'm surprised everyone treats them as normal.
Does anyone has experience with low-latency high-throughput opensource keyvalue storages? Any specific implementation to recommend?
Aren’t these numbers .2 ms, ie 200 microseconds?
If it is built into your language/framework, you can completely ignore the problem of updating the client, as it happens automatically.
Hope that makes sense.
Is there a plan to commercialise this product? (Offer commercial support, features, etc.) I could not find anything obvious from the home page.
I was typing that out and felt like something was wrong but couldn’t put my finger on what.
Would be great to disclose details of this one. I'm interested in using what DiceDB achieves higher throughput.
FYI this is a misspelling of "higher"
Anyway to persist data in case of reboots?
That's the only thing missing here.
Is Go the only SDK ?
But certainly you could build something to handle these and most other needs in this realm with mostly just redis, using streams for what needs to be more robust, in tandem with pub/sub, keyspace notifs, etc. in the areas they are suited to.
I'm skeptical that the re-execution approach can scale for complex queries, the latency and throughput improvements would be offseted by the computational cost and bottlenecks introduced for achieving it via its reactivity mechanism (query subscription), this might not work at scale and serve niche use cases.
There are various ways throughput and latency for kv stores can be improved, so bar is really high here.
The messaging with Dice seems unclear and confusing to describe its purpose/use-cases over alternatives, or how it achieves them, which could just be how it's marketed. But it seems to be a collection of ideas and a WIP project.
I think reducing data fetching complexity and complex key dependencies for end clients could be appealing, and it would be great to have it at the KV store level, but there is no reason this type of reactivity can't be implemented on top of various clients for existing KV stores (like Redis). And basic WATCH with transactions are even offered out of the box in them.
Deno kv seem nice but its vendor locked, also there are many others like dragonfly, valkey etc, redis could still work, even something over sqlite can work, deno has a selfhosted kv on top of sqlite - https://github.com/denoland/denokv
Also with dice its creator had made this talk
https://hasgeek.com/rootconf/2024/sub/how-we-made-dicedb-a-t...
From that and the thread so far it seems, they want to make some super cache by building a realtime multi-threaded kv store, improving latency and reducing its read load via its reactivity mechanism. Solving the problem of cache invalidation.
Not sure how this will be achieved but there is no harm in trying. From what is said and shared, rationale behind this design and its tradeoffs are not clear, code could be fixed/improved but providing clarity on this is essential for adoption.
As if nobody ever uses anything else.
Description from GitHub:
> DiceDB is an open-source, fast, reactive, in-memory database optimized for modern hardware. Commonly used as a cache, it offers a familiar interface while enabling real-time data updates through query subscriptions. It delivers higher throughput and lower median latencies, making it ideal for modern workloads.
DiceDB is an in-memory database that is also reactive. So, instead of polling the database for changes, the database pushes the resultset if you subscribe to it.
We have a similar set of commands as Redis, but are not Redis-compliant.
This application may be very capable, but I agree with the person saying that its use-case isn't clear on the home page, you have to go deeper into the docs. "Smarter than a database" also seems kind of debatable.
Feels arrogant. "Of course you already know what this is, how could you not?"
"What is DiceDB? DiceDB is an open-source, fast, reactive, in-memory database optimized for modern hardware. Commonly used as a cache, it offers a familiar interface while enabling real-time data updates through query subscriptions. It delivers higher throughput and lower median latencies, making it ideal for modern workloads."
> DiceDB is an open-source, fast, reactive, in-memory database optimized for modern hardware.
A Redis-like database with a Redis-like interface. No info about drop-in compatibility, I assume no.
Should be the first sentence on their website and repo.
You may want to search for realtime databases.
What are those goals? I was struggling to interpret a meaningful roadmap from the issue & commit history.
We have the benchmarks, and we will be sharing the numbers in subsequent releases.
But, there is still a chance that I may come to bite us and limit us to a smaller scale, and we are ready for it.
If you expose something to enough people you'll get some unreasonable takes and interpretations of it. It's important to ignore them.
Quite literally the main function of dice is to give you random numbers. Looking over the website and readme I could not surmise why they would call it DiceDB except for "it sounds nice", but it's absolutely not unreasonable to look at the name and have a thought "it's probably a joke project about random results".
Reasonable people realize this and won't discard a project as a joke because of such a teneous connection, and the fact they've gotten traction is a testament to that.
Their goal is to unload the "main" thread from performing i/o related tasks like socket reading and parsing, so it could only spend its precious time on datastore operations. This creates an asymmetrical architecture with I/O threads scaling to any number of CPUs, but the main thread is the only one that touches the hashtable and its entries. It helps a lot in cases where datastore operations are relatively lightweight, like SET/GET with short string values, but its impact will be insignificant for CPU heavy operations like lua EVALs, sorted sets, lists, MGET/MSET etc.
(Author of Dragonfly here)
This seems more like Redis though
Yes, per [1] Google did restrict their use of io_uring on “production servers“, and in Android, ChromeOS etc.
However, within that same post, and what is often missed when that post is quoted, is that Google wrote that they did in fact “consider [io_uring] safe” for use by trusted components:
> For these reasons, we currently consider it safe only for use by trusted components.
A database like TigerBeetle is typically deployed in a trusted environment, and is such a trusted component.
[1] https://security.googleblog.com/2023/06/learnings-from-kctf-...
I agree. Nobody said anything about discarding nothing though. Only that it's a reasonable first thought to have upon hearing the name. And it is.
The saying is cute but does not really convey information the reader is after. And that spot is where you want people to immediately understand what it is.
If I'm reading this correctly, they are recommending a lock in this situation. However, they are saying the implementations has two options, either raise an error reporting the race (if the implementation is told to do so), or, because the value being read is not larger than a machine word, reply to the read with a correct value from a previous write. If true then it cannot reply with corrupted data.
The spec says
> A read r of a memory location x holding a value that is not larger than a machine word must observe some write w such that r does not happen before w and there is no write w' such that w happens before w' and w' happens before r. That is, each read must observe a value written by a preceding or concurrent write.
These rules apply only if the value isn't larger than a machine word. Otherwise,
> Reads of memory locations larger than a single machine word ... can lead to inconsistent values not corresponding to a single write.
The size of a machine word is different depending on how a program is compiled, so whether or not a value is larger than a machine word isn't know-able by the program itself.
And even if you can assert that your program will only be built where a machine word is always at least of size e.g. uint64, the spec only guarantees that unsynchronized reads of a uint64 will return some previous valid write, it doesn't guarantee anything about which value is returned. So `x=1; x=3; x=2;` concurrently with `print(x); print(x); print(x)` can print `1 1 1` or `3 3 3` or `2 1 1` or `3 2 1` and so on. It won't return a corrupted uint64, but it can return any prior uint64, which is still a data race, and almost certainly useless to the application.
> that unsynchronized reads of a uint64 will return some previous valid write, it doesn't guarantee anything about which value is returned
Your the second person saying this, so is my interpretation that this is dissallowed by the part that you quoted incorrect?
> must observe some write w such that r does not happen before w and there is no write w' such that w happens before w' and w' happens before r
edit: somebody is answering this below by the way
Is that the kind of uncertainty you want in your production systems? Or is your only requirement that they don’t serve “corrupt” data?
Don’t be “clever”. Use locks.
> I understand the need for correct lock-free impls: Given OP's description, simply avoiding read mutexes can't be the way to go about it?
I did note that the documentation recommends a lock.
> read the first value ever set to the variable for the entire lifetime of the program
That is not my reading of the current memory model? It seems to specifically prohibit this behaviour in requirement 3:
> 2. w does not happen before any other write w' (to x) that happens before r.
cycleMap is definitely not thread-safe. The authors knew this, to some extent, because they synchronized writes via an adjacent mutex. But they didn't synchronize reads thru the same mutex, which is the issue.
Like, imagine a page that only said "SuperTransport -- 0 to 100 in 5 seconds", but it is not clear for the reader if it is a car or a horse or a plane or a parcel service...
... and the reader has to go and guess "hmm, guess due to the acceleration it is probably a car or a motorbike -- wonder of it is for sale or for rent?".
Just put "fast on premise key/value database" in the big font that was there -- if that is what it is. That is purely a guess from me, no idea if that is what it is.
> The happens before relation is defined as the transitive closure of the union of the sequenced before and synchronized before relations.
Without synchronization, the degenerate sequencing is perfectly valid.
That’s the problem with being “clever” - you miss a definition and your entire mental model is busted.