zteln
Hello Elixir community,
I’ve written a small embedded key-value database in Elixir called Goblin. It’s based on the Log-Structured Merge (LSM) tree architecture and is a pure Elixir implementation with zero external dependencies. The API syntax is heavily inspired by CubDB by @lucaong, but Goblin is optimized for write-heavy workloads.
Some features include:
- Background processes for automatic flushing and compaction of Sorted String Tables (SST)
- ACID transactions with conflict detection
- Concurrent reads
- Range queries (
Goblin.select/2returns a stream over the data) - Write-ahead logging and database state tracking through a manifest
Some future work is planned, including compression of SSTs in deeper levels, transaction retries on conflicts, and SST verification via CRCs.
Similar projects in Erlang & Elixir:
- CubDB - COW B+tree embedded key-value database
- LevelEd - LSM key-value database intended as a backend for a Riak KV database
A lot of inspiration was drawn from RocksDB, Facebook’s production LSM database.
Goblin is still in a very early phase and any feedback would be greatly appreciated!
Github:
Trending in Announcing
Hey everyone!
Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New
Samly can be used to enable SAML 2.0 Single Sign On in a Plug/Phoenix application.
This library uses Erlang esaml to provide
plug enabl...
New
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries.
offset-based pagination with...
New
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
Hobbes is a low-level distributed database for the Elixir programming language.
Hobbes provides a simple, safe, and scalable storage lay...
New
Hi there! We created Gust: A task orchestrator inspired by Airflow.
For those who have never heard about Aiflow, it’s a Python-based wor...
New
Hi all!
I want to present a small library which provides a mix task for generating an Entity-Relationship Diagram for Ecto schemas.
You...
New
Other Trending Topics
I am happy to introduce the very α version of the new programming language compiled to BEAM.
Welcome Cure.
It has literally three kille...
New
This showed up on my feed.. anyone heard of it? Just hype?
Ox Alpha is a reasoning model designed for coding, sustained ag...
New
It’s not that it’s vocabulary is too advanced. It’s something worse.
I get lost trying to follow even a paragraph written by Claude. It’...
New
Today we’re releasing Oban for Python. Not an Oban client in Python. Not a pythonx wrapper embedded in Elixir. Nope, it’s a fully operati...
New
@hugobarauna, Dr. Dimitrios Koutmos (my brother) and I (Alex Koutmos) have been hard at work on writing a book on how you can use Elixir ...
New
Introductory paragraph
I’ll be looking for a keen junior or someone that has a couple of years experience in the real world (so you’ve be...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #blog-post
- #ai
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming











Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
lucaong
This is great, super interesting! Happy to see that
Looking forward to find some time to delve into this and try it out. LSM trees definitely provide some interesting possibilities and optimizations.
CubDBis providing some inspiration, and that the ecosystem is growinggarrison
Nice work! Code looks good and I love the name.
A few things:
I see you mention transactions with snapshot isolation. Could you provide a brief overview of the concurrency control impl? I think I found some of it in
transaction.ex.Keeping a lock per SST is a valid approach, but I am personally more fond of maintaining a global snapshot window and deleting SSTs outside of that window. I can’t remember which one RocksDB does, though.
I know you’re probably at a stage where benchmarks aren’t really meaningful, but would you be willing to share roughly what numbers you’re seeing? I ask only because I’m literally implementing an Elixir LSM right now and I’m curious what to expect.
Anyway, please keep at it! The ecosystem needs more databases
zteln
Thanks @lucaong! Looking forward to hearing your feedback on it!
Thanks @garrison for the questions and especially for mentioning the snapshot isolation, it made me realize that it was not working as intended in Goblin. It is now fixed. Once a transaction starts, then that transaction gets a fallback read function that first checks the immutable mem tables from the writer (snapshot of current mem table and flushing mem tables) for the key and then checks the store if not found in the mem tables. When checking the store then it matches against a sequence number from when the transaction started so that no later writes after the start of the transaction are returned. Upon commit, then read and write sets of the transaction are checked for potential conflicts.
Interesting about the global snapshot, are SSTs removed from this snapshot once signalled for deletion and no more reads against this SST are ongoing? If you have a reference I will gladly read it.
Here is an early sample of a benchmark for writing via
put_multi/2(it is not complete yet though):Asd
Hi, good library, I’ve been working on something similar like 2 months ago: GitHub - hissssst/nanolsm: Tiny Elixir LSM KV implementation · GitHub. I love how in the recent months there have been like 4 new embedded Elixir DB projects published. Maybe we should start a discord server about it, hehe
I’ve read your code and I have these questions and notes:
etswith the names of opened files and key ranges:filemodule which spawns a process with a new file descriptor on every read call and essentially introduces a copy (while message passing) on every read. You don’t share the descriptor between processes, so I’d just suggest to use a:prim_fileinstead, to avoid copying or use opened:filedescriptors and store them in ets (from the point 1)etstoohas_conflictchecks are performed againststate.transactions[pid].commitwhich is always an empty list, and it looks like these conflict checks must be implemented against current memtables in the writer. Looks like a bugRight now the implementation has bottlenecks in reads and writes, since all reads and writes are genserver calls to the singletones (Store and Writer), has problems with WAL durability and overall performance. I would be happy to share the knowledge about implementing parallel reads and writes, improving WAL and performance
garrison
My eyes always go right to the consistency guarantees because that’s the tricky part
So this is an optimistic (OCC) approach. I see the code that checks the read/write sets and it looks like it only searches the memtables. That means if memtables have been flushed during the transaction those writes would be missed. But if memtables cannot be flushed while a transaction is active over them then I think this is correct.
Presumably the memtables contain writes from transactions older than the current transaction, so this will result in false positives which is unfortunate. If you assign every txn a read/commit seqno and check that against the write seqnos you should be able to check conflicts more accurately. That means adding seqnos to the writes in the memtables (which is generally how it’s done).
If you want to guarantee snapshot isolation you only need to check write-write conflicts, so you only need to check the write set. If you want to guarantee serializability you instead need to check the read set against the past writes. In neither case do you need to check both.
See snapshot isolation and serializability.
garrison
Unfortunately Erlang does not support direct I/O so the OS will be caching those blocks for you anyway no matter what you do. It would save you copying memory from the kernel into the refc binary but I wonder how much overhead that actually is. It would also save recomputing the checksum if you trust your RAM, and of course you kinda have to trust RAM anyway or you’re done for (so use ECC).
Tragically, without direct I/O it’s not possible to build a safe storage engine in current year, so I think a NIF will be unavoidable in my case. Not looking forward to that.
Personally I found that using maps for stuff like this upsets the generational GC (causing nasty latency spikes), and using
:etsfixed that (presumably because it gets everything off the heap fast). So now I just abuse:etsfor basically everything lol.But yeah, in this case you want an ordered map anyway so
Mapis off the table.I had been meaning to ask you about this. This module is not documented (from what I saw) so I assume this is some BEAM dark magic?
Asd
If you’re talking about
O_DIRECT, I agree, but such block caching is useless, since values and keys are not decoded in it and it still needs to be marshalled. I was thinking about decoded values cache with index or at least binsearch tuple, not just a “block as bytes cache”But yeah,
prim_filehas other issues too. I was talking about with erlang core team about it on erlangforum and they said that they are planning to rewrite the fs modules, but they don’t have a deadlines, estimate or plan for it, and the strangest part is that I asked if I can contribute and work on the impl and they declined saying that they are unwilling to accept contributions. That was strangeYeah. Goblin does a very interesting trick where it’s implementation of transactions uses snapshots, and these snapshots cost nothing to create when you use maps as memtables, since they are immutable by default. Very nice trick, but it bottlenecks the performance since it requires a singleton
It’s not a dark magic, enterprise databases in Erlang are using it directly. It is a NIF which wraps libc calls like
pread,fopenand such. But it comes with limitations which can break the VM. One of the limitations is that it must always be uses only by a single process. It uses NIF resource mechanism which works in a way where if some process dies (or is killed) while using the resource, it will close the file descriptor. So if you share the prim_file descriptor (which just holds an integer libc file descriptor) and it gets closed in one process, other process will continue using it and when somewhere a new file descriptor is created, it will be the same number as this closed one and the process will accidentally read from the new one, breaking a lot of internal VM assumptions and potentially crashing the VM.But if you open descriptor, never share it and use it in the process it was opened in, you’re all good
zteln
@Asd Thanks for your questions and bug finds! nanoLSM looks good and seems interesting as well, I will look more into it. I will try to answer all your questions.
etstables, good insight!:filewhich normally spawns a pid for the file descriptor, but I use the:rawmode when opening a file, which opens a:prim_filedescriptor instead, which you suggested, so I believe this is the same.Please do share how you would implement parallel reads and writes, sounds interesting!
@garrison Thanks for the insights, that clears up the transaction handling. With the read/commit number, do you mean a sequence number is given to the transaction when it starts and reads for that transaction can only read below this seq no? And write/write conflicts are between the start seq no and the latest seq no in the writer then? En
etstable in the writer makes a lot of sense, clearing up some of the bottleneck performances @Asd mentioned as well. I will fix the transaction handling to allow snapshot isolation and disregard serializability. Blocking flushes until there are no ongoing transactions seems like a natural implementation then.garrison
Pretty much. But in your case a transaction would only start at the current
seqnoand so only the active memtable (or future active memtables) will receive new writes. Meaning you would only actually have to do multiversion reads against those memtables.By holding a snapshot of the database itself (i.e. of the manifest) open you ensure that none of the SSTs relevant to active transactions are deleted early. That does mean long-running transactions would hold memtables open (I wonder how RocksDB deals with that), but long-running transactions are bad anyway.
A conflict occurs when a key is written in the period between the
beginandcommittimestamps of a transaction. For snapshot isolation, write-write conflicts are checked. As far as I am aware this is a historical accident, i.e. at the time they thought it was correct and then later found out it was not. There are nasty anomalies which can happen with snapshot isolation, most famously write skew. I am quite fond of the less-famous read-only anomaly, which is much harder to understand but also much more disturbing.Perhaps surprisingly, with OCC the only difference between snapshot isolation and serializability is validating the read set instead of the write set. This causes more conflicts but has the benefit of actually being intuitively correct, unlike snapshot isolation and other weak consistency levels which have failure modes that are extremely difficult for the average dev to understand (or anyone, really).
Given that the implementations are almost identical I would strongly recommend that you default to serializability. Specifically strict serializability, which should be trivial since this is not a distributed database.
garrison
It would be much better to actually sync before returning from transaction
commit(). You should always default to proper durability guarantees. If users want to turn them off that’s their own business, but it’s best not to hand footguns to unsuspecting devs.Generally you want to bunch a number of transactions together before syncing the WAL. This optimization is known as “group commit”. After the batch is committed you then unblock the clients’ commit calls.