Is LMDB a LevelDB Killer?
Updated: May 27, 2021
Was pointed at this blog post: Is LMDB a LevelDB Killer? via twitter about LevelDB, and decided to write a more complete response here. (Sorry, it’s just really hard to carry on a deep discussion or analysis only 140 characters at a time.)
There were a few good questions raised, and some that seemed a bit too obvious, but I’ll just take things in order.
Tradeoffs - I’m not sure how anyone can say this only gets a passing mention. We state quite clearly that LMDB is read-optimized, not write-optimized. I wrote this for the OpenLDAP Project; LDAP workloads are traditionally 80-90% reads. Write performance was not the goal of this design, read performance is. We make no claims that LMDB is a silver bullet, good for every situation. It’s not meant to be - but it is still far better at many things than all of the other DBs out there that *do* claim to be good for everything.
For the most part there aren’t a lot of other tradeoffs to discuss. I designed LMDB based on ~13 years of experience working with the BerkeleyDB transactional store, so it’s mostly a distillation of what we liked and didn’t like about BDB. I think the papers and presentations already cover those design aspects. Of course it’s also a product of ~35 years of experience writing optimal code. There are probably many paths that I take implicitly because it’s obvious to me that doing them any other way would be stupid, and it would take someone else’s questions to draw my attention to these unconscious choices. So before going further, let me say Thanks to Paul Banks for taking the time to write his comments and questions.
re: the personal note - I would have more respect for the LevelDB project if they had spelled out its own limitations as openly and as up-front as LMDB does. As it is, they are pushing it as a fast and lightweight DB suitable for everything, when it clearly is neither lightweight nor fit for general use. It is *certainly* not fit for use in high transaction rate heavy workloads. And from a practical perspective, data stored in a DB is only useful when you actually retrieve it again and *use* it. (Assuming you *can* retrieve it again; some folks like the MongoDB guys seem to forget that detail.) As such, a DB optimized for writes at the expense of read performance makes no sense. You can only swing so far; once you get to 50% writes/50% reads you are actually not *optimized* for anything at all. And if you go beyond that, to e.g. 60% writes/40% reads then you might as well use MongoDB and just pretend you wrote the data.
Write Amplification - I already summarized this in a comment on another blog, but I’ll repeat it here for convenience - in any logging-based design, you get write amplification on the order of 2x the size of your user data writes. This is because most data gets written twice - once first to the log, and then eventually to the main database. In a Copy-On-Write B+tree design, you get write amplification the order of 1x the size of your writes, plus the log of the number of keys - i.e., the depth of the tree. The data only gets written once, but you also must make copies of every page in the path from the leaf back to the root of the tree - this is also already clearly explained in my LMDB presentations. In the final analysis, there is a definite trade here - when all of your data items are “small” the logging approach will be more economical. When your data items are “large” the COW approach is better. The effect of this tradeoff is pretty clearly illustrated in our microbenchmarks - all of the other DB engines choke on the large value operations.
As for the fact that I/Os are always page-sized - I shouldn’t have to spell that out any more than already done in the presentations. LMDB is a disk-based B+tree design. That in itself should tell you that its I/Os are page-sized, since a page is the fundamental unit of disk I/O in a filesystem. The slides showing the operation of the tree are clearly labeled as operating on units of pages. LMDB relies on the OS’s virtual memory manager to page data in and out when the DB size is larger than RAM. Again, note the use of the word page.
Paul writes in his blog
In fact if you just skimmed the benchmarks you might have missed it but in all write configurations (sync, async, random, sequential, batched) except for batched-sequential writes, leveldb performs better, occasionally significantly better.
I’ve stated this many times on the OpenLDAP mailing lists but it obviously applies to our larger audience - you have to actually read what’s in front of you. You’re dealing with textual information and if you skip over anything then you’re missing out. Attention to detail matters, in code and in prose. In this case, the “tiny” detail is that we never expected or claimed LMDB would be fast for writes. In fact, in one case where LMDB beat LevelDB, we plainly state:
Also, surprisingly, MDB beats LevelDB’s write performance here.
I would think that makes our perspective very clear - LMDB is not a write-optimized design. The fact that it occasionally beats other designs that *do* claim to be designed for high write throughput is more a testament to their poor implementations, than to any conscious design effort on my part.
I disagree that we ignore this read vs write tradeoff. We are totally up-front about it in all of our presentations. Only someone who skims the information and isn’t paying attention could miss such a fundamental point.
File Fragmentation - this can certainly become a problem, but in practice it is a *worse* problem for other DB designs. See our HyperDex benchmark for a more long-running test. I have a couple simple rules for writing efficient code. A relevant one here is “don’t do the same work more than once” - the big problem with all logging-based systems is the massive amount of redundant writing they do. Compound that with any compaction/merging they require and the result is a quagmire of wasted resources. When you decide “random I/O is bad”, then set out to develop a new DB engine to combat this problem, and wind up still performing miserably compared to a plain random I/O workload, there is no other way to put it than “Fail.”
For the record, in regards to our HyperDex benchmarks, I don’t consider running a database at 50x the size of system RAM to be a smart deployment choice. RAM is cheap these days. If you’re deploying clusters with thousands of nodes with only 8GB of RAM on each to solve your “big data” problems You’re Doing It Wrong. You’ll occupy more of your compute resources in cluster management overhead than in actually working on your tasks.
Compression - this is utterly off the mark. LMDB’s purpose is to be a compact and efficient transactional embedded B+tree. Whatever else you may want it to do, first and foremost it must operate efficiently on its own. Any DB design that *requires* compression in order to perform well is a broken design. And any DB design that *precludes* compression is a broken design too. But you can add whatever frills and other layers on top of LMDB as you like. Case in point, the CentiDB project implements block compression perfectly well on top of LMDB.
The microbenches were run without compression because we were benchmarking DB engines, not compression libraries.
Whatever other libraries get used with the DB engine are purely incidental. So LevelDB requires the Snappy compression library and the tcmalloc memory allocator in its build prereqs - tests with all of these components included aren’t showing how well *LevelDB* works. We can use Snappy and tcmalloc with other DB engines too; their use only muddies the picture. But you’re right Paul, it would be interesting to run tests where all of the DB engines were also using Snappy and tcmalloc. The before/after comparisons would be particularly enlightening, to show the impact of each component on overall performance. (We did a series of benchmarks on malloc implementations for OpenLDAP slapd a few years back. Again, quite revealing, but we only did it after already thoroughly analyzing and optimizing slapd’s usage of malloc on its own. Always get the foundation right first, before adding frills.)
There’s another important point to be made here - LMDB was designed for *efficiency* - this is not the same goal as designing for *performance* as LevelDB claims to be. Frequently you see discussions saying “since this task is I/O bound, we can use free CPU cycles to compress the data and reduce the I/O bandwidth requirements.” LevelDB assumes there are free CPU cycles to use, and consumes them with abandon in a vain attempt to maximize performance. In our HyperDex testing we see what happens when this assumption is followed thru - HyperLevelDB continuously chews up 100% of all 4 cores of the test machine for the entire duration of the tests. This is an extremely myopic approach to design though, as it precludes anyone else getting any useful work done on the machine. In contrast, LMDB minimizes I/O bandwidth requirements simply by being more efficient in what it writes. As a result, very little I/O bandwidth is used and extremely little CPU is used, which makes it feasible for your higher level apps that are using the DB to actually get their work done.
Large Transactions - preemptive note - LMDB 0.9.7 has been released and the limit on transaction size is gone. As for the rest - again a naive comment about LevelDB not doing the bulk of its disk IO in the commit path. (Completely ignoring the fact that LevelDB doesn’t even support transactions…) The problem is in assuming that you have the entire machine to yourself and that nothing else is competing for compute resources while you shove the major work into a background thread. This simply won’t be true on a busy, heavily loaded system, and on an idle system no one will care either way.
Disk Reclamation - In our experience of 15 years of OpenLDAP deployments, databases never shrink. Any time you delete a record you will probably be adding a new one shortly thereafter.
Some final thoughts - “more data required” - please, do conduct your own tests and report your results. The only benchmark that is truly meaningful is the one you conduct on your own actual workloads. LMDB may not be the be-all/end-all of DB engines. There may well be workloads out there for which it is not the best choice today.
But LMDB is also a design for tomorrow; solving yesterday’s problems today is for slow-moving leviathans. Expending excessive amounts of CPU and disk resources to “solve” the random I/O problem is futile if your end result is still inferior to straight random access. And moreover, with the imminent arrival of multiple non-volatile RAM solutions looming on the market, it’s foolish to waste any further effort on it.
Thanks again Paul, for taking the time to study LMDB and write a thoughtful post with your questions. It’s always good to check assumptions.