354 lines
27 KiB
HTML
354 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
<link rel="canonical" href="https://notes.eatonphil.com/2024-07-01-a-write-ahead-log-is-not-a-universal-part-of-durability.html">
|
|
<title>A write-ahead log is not a universal part of durability | notes.eatonphil.com</title>
|
|
<meta name="description" content="A write-ahead log is not a universal part of durability" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<link rel="stylesheet" type="text/css" href="/style.css" />
|
|
<link rel="alternate" type="application/rss+xml" href="/rss.xml" />
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=IBM+Plex+Mono">
|
|
|
|
<script async onload="loadGA()" src="https://www.googletagmanager.com/gtag/js?id=UA-58109156-2"></script>
|
|
<script>
|
|
function loadGA() {
|
|
window.dataLayer = window.dataLayer || [];
|
|
function gtag(){dataLayer.push(arguments);}
|
|
gtag('js', new Date());
|
|
gtag('config', 'UA-58109156-2');
|
|
}
|
|
</script>
|
|
<script defer src="https://cdn.usefathom.com/script.js" data-site="CEPUOLOQ"></script>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="lfw">
|
|
<div class="container">
|
|
<div class="row">
|
|
<a href="https://eatonphil.com/2025-holiday-fundraiser.html">2025 Holiday Fundraiser</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="container">
|
|
<div>
|
|
<div class="row">
|
|
<div>
|
|
<a href="https://eatonphil.com" class="sm-link">
|
|
Home
|
|
</a>
|
|
<a href="/" class="sm-link">
|
|
Blog
|
|
</a>
|
|
<a href="/rss.xml" class="sm-link">
|
|
RSS
|
|
</a>
|
|
</div>
|
|
|
|
<div class="subscribe">
|
|
<!-- <a href="https://eatonphil.com/hire-me.html">Hire me</a> -->
|
|
<!--
|
|
Hardcode link to home page because some pages don't
|
|
have a subscribe box. Some pages hide the subscribe.
|
|
-->
|
|
<a href="https://eatonphil.com/subscribe.html">
|
|
Subscribe
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<hr />
|
|
<div class="">
|
|
<h2>July 1, 2024</h2>
|
|
<h1>A write-ahead log is not a universal part of durability</h1>
|
|
|
|
<div class="row" style="padding-bottom: 5px">
|
|
<div class="tags"><a href="/tags/databases.html" class="tag">databases</a></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<div class="container">
|
|
<div class="col-6">
|
|
<div class="post">
|
|
<p>A database does not need a write-ahead log (WAL) to achieve
|
|
durability. A database can write its long-term data structure durably
|
|
to disk before returning to a client. Granted, this is a bad idea! And
|
|
granted, a WAL <b>is</b> critical for durability <b>by design</b> in most
|
|
databases. But I think it's helpful to understand WALs by
|
|
understanding what you <b>could</b> do without them.</p>
|
|
<p>So let's look at what terrible design we can make for a durable
|
|
database that has no write-ahead log. To motivate the idea of, and
|
|
build an intuition for, a write-ahead log.</p>
|
|
<p>Thank you to Alex Miller for reviewing a version of this post.</p>
|
|
<p>But first, what is durability?</p>
|
|
<h3 id="durability">Durability</h3><p>Durability happens in the context of a request a client makes to a
|
|
data system (either an embedded system like SQLite or RocksDB or a
|
|
standalone system like Postgres). Durability is a spectrum of
|
|
guarantees the server provides when a client requests to write some
|
|
data: that either the request succeeds and the data is safely written
|
|
to disk, or the request fails and the client must retry or decide to
|
|
do something else.</p>
|
|
<p>It can be difficult to set an absolute definition for durability since
|
|
different databases have different concepts of what can go wrong with
|
|
disks (also called a "storage fault model"), or they have no concept
|
|
at all.</p>
|
|
<p>Let's start from the beginning.</p>
|
|
<h4 id="an-in-memory-database">An in-memory database</h4><p>An in-memory database has no durability at all. Here is pseudo-code
|
|
for an in-memory database service.</p>
|
|
<div class="highlight"><pre><span></span><span class="n">db</span> <span class="o">=</span> <span class="n">btree</span><span class="p">()</span>
|
|
|
|
<span class="k">def</span> <span class="nf">handle_write</span><span class="p">(</span><span class="n">req</span><span class="p">):</span>
|
|
<span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">key</span><span class="p">,</span> <span class="n">req</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
|
|
<span class="k">return</span> <span class="mi">200</span><span class="p">,</span> <span class="p">{}</span>
|
|
|
|
<span class="k">def</span> <span class="nf">handle_read</span><span class="p">(</span><span class="n">req</span><span class="p">):</span>
|
|
<span class="n">value</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">key</span><span class="p">)</span>
|
|
<span class="k">return</span> <span class="mi">200</span><span class="p">,</span> <span class="p">{</span><span class="s2">"value"</span><span class="p">:</span> <span class="n">value</span><span class="p">}</span>
|
|
</pre></div>
|
|
<p>Throughout this post, for the sake of code brevity, imagine that the
|
|
environment is concurrent and that data races around shared mutable
|
|
values like <code>db</code> are protected somehow.</p>
|
|
<h4 id="writing-to-disk">Writing to disk</h4><p>If we want to achieve the most basic level of durability, we can write
|
|
this database to a file.</p>
|
|
<div class="highlight"><pre><span></span><span class="n">f</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"kv.db"</span><span class="p">)</span>
|
|
<span class="n">db</span> <span class="o">=</span> <span class="n">btree</span><span class="o">.</span><span class="n">init_from_disk</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
|
|
|
<span class="k">def</span> <span class="nf">handle_write</span><span class="p">(</span><span class="n">req</span><span class="p">):</span>
|
|
<span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">key</span><span class="p">,</span> <span class="n">req</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
|
|
<span class="n">db</span><span class="o">.</span><span class="n">write_to_disk</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
|
<span class="k">return</span> <span class="mi">200</span><span class="p">,</span> <span class="p">{}</span>
|
|
|
|
<span class="k">def</span> <span class="nf">handle_read</span><span class="p">(</span><span class="n">req</span><span class="p">):</span>
|
|
<span class="n">value</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">key</span><span class="p">)</span>
|
|
<span class="k">return</span> <span class="mi">200</span><span class="p">,</span> <span class="p">{</span><span class="s2">"value"</span><span class="p">:</span> <span class="n">value</span><span class="p">}</span>
|
|
</pre></div>
|
|
<p><code>btree.write_to_disk</code> will call
|
|
<a href="https://linux.die.net/man/2/pwrite">pwrite(2)</a> under the hood. And
|
|
we'll assume it does copy-on-write for only changed pages. So imagine
|
|
we have a large database represented by a btree that takes up 10GiB on
|
|
disk. With the btree algorithm, if we write a single entry to the
|
|
btree, often only a single (often 4Kib) page will get written rather
|
|
than all pages (holding all values) in the tree. At the same time, in
|
|
the worst case, the entire tree (all 10GiB of data) may need to get
|
|
rewritten.</p>
|
|
<p>But this code isn't crash-safe. If the virtual or physical machine
|
|
this code is running on reboots, the data we wrote to the file may not
|
|
actually be on disk.</p>
|
|
<h4 id="fsync">fsync</h4><p>File data is buffered by the operating system by default. By general
|
|
consensus, writing data without flushing the operating system buffer
|
|
is not considered durable. Every so often a new database will show up
|
|
on Hacker News claiming to beat all other databases on insert speed
|
|
until a commenter points out the new database doesn't actually flush
|
|
data to disk.</p>
|
|
<p>In other words, the commonly accepted requirement for durability is
|
|
that not only do you write data to a file on disk but you
|
|
<a href="https://man7.org/linux/man-pages/man2/fsync.2.html">fsync(2)</a> the
|
|
file you wrote. This forces the operating system to flush to disk any
|
|
data it has buffered.</p>
|
|
<div class="highlight"><pre><span></span><span class="n">f</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"kv.db"</span><span class="p">)</span>
|
|
<span class="n">db</span> <span class="o">=</span> <span class="n">btree</span><span class="o">.</span><span class="n">init_from_disk</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
|
|
|
<span class="k">def</span> <span class="nf">handle_write</span><span class="p">(</span><span class="n">req</span><span class="p">):</span>
|
|
<span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">key</span><span class="p">,</span> <span class="n">req</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
|
|
<span class="n">db</span><span class="o">.</span><span class="n">write_to_disk</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
|
<span class="n">f</span><span class="o">.</span><span class="n">fsync</span><span class="p">()</span> <span class="c1"># Force a flush</span>
|
|
<span class="k">return</span> <span class="mi">200</span><span class="p">,</span> <span class="p">{}</span>
|
|
|
|
<span class="k">def</span> <span class="nf">handle_read</span><span class="p">(</span><span class="n">req</span><span class="p">):</span>
|
|
<span class="n">value</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">key</span><span class="p">)</span>
|
|
<span class="k">return</span> <span class="mi">200</span><span class="p">,</span> <span class="p">{</span><span class="s2">"value"</span><span class="p">:</span> <span class="n">value</span><span class="p">}</span>
|
|
</pre></div>
|
|
<p>Furthermore you must not ignore fsync failure. How you deal with fsync
|
|
failure is up to you, but exiting immediately with a message that the
|
|
user should restore from a backup is sometimes considered acceptable.</p>
|
|
<p>Databases don't like to fsync because it's slow. Many major databases
|
|
offer modes where they do not fsync data files before returning a
|
|
success to a client. Postgres
|
|
<a href="https://www.postgresql.org/docs/current/runtime-config-wal.html#GUC-FSYNC">offers</a>
|
|
this unsafe mode, though does not default to it and warns against
|
|
it. MongoDB offers this unsafe mode but <a href="https://www.mongodb.com/docs/manual/core/journaling/#journaling-process">does not
|
|
default</a>
|
|
to it.</p>
|
|
<p class="note">
|
|
An earlier version of this post said that MongoDB would unsafely
|
|
flush on an interval. Daniel Gomez Ferro from MongoDB messaged me
|
|
that while the docs are confusing, the default write concern
|
|
"majority" does actually imply "j: true" which means data is
|
|
synchronized (i.e. fsync-ed) before returning a success to a client.
|
|
</p><p>Almost every database trades safety for performance in some
|
|
regard. For example, few databases but SQLite and Cockroach default to
|
|
Serializable Isolation. While it is commonly agreed that basically no
|
|
level below Serializable Isolation (that all other databases default
|
|
to) can be reasoned about. Other databases offer Serializable
|
|
Isolation, they just don't default to it. Because it can be slow.</p>
|
|
<h4 id="group-commit">Group commit</h4><p>But let's get back to fsync. One way to amortize the cost of fsync is
|
|
to delay requests so that you write data from each of them and then
|
|
fsync the data from all requests. This is sometimes called group
|
|
commit.</p>
|
|
<p>For example, we could update the database in-memory but have a
|
|
background thread serialize to disk and call fsync only every 5ms.</p>
|
|
<div class="highlight"><pre><span></span><span class="n">f</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"kv.db"</span><span class="p">)</span>
|
|
<span class="n">db</span> <span class="o">=</span> <span class="n">btree</span><span class="o">.</span><span class="n">init_from_disk</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
|
|
|
<span class="n">group_commit_sems</span> <span class="o">=</span> <span class="p">[]</span>
|
|
|
|
<span class="nd">@background_worker</span><span class="p">()</span>
|
|
<span class="k">def</span> <span class="nf">group_commit</span><span class="p">():</span>
|
|
<span class="k">for</span><span class="p">:</span>
|
|
<span class="k">if</span> <span class="n">clock</span><span class="p">()</span> <span class="o">%</span> <span class="mi">5</span><span class="n">ms</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
|
|
<span class="n">db</span><span class="o">.</span><span class="n">write_to_disk</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
|
<span class="n">f</span><span class="o">.</span><span class="n">fsync</span><span class="p">()</span> <span class="c1"># Durably flush for the group</span>
|
|
<span class="k">for</span> <span class="n">sem</span> <span class="ow">in</span> <span class="n">group_commit_sems</span><span class="p">:</span>
|
|
<span class="n">sem</span><span class="o">.</span><span class="n">signal</span><span class="p">()</span>
|
|
|
|
<span class="k">def</span> <span class="nf">handle_write</span><span class="p">(</span><span class="n">req</span><span class="p">):</span>
|
|
<span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">key</span><span class="p">,</span> <span class="n">req</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
|
|
<span class="n">sem</span> <span class="o">=</span> <span class="n">semaphore</span><span class="p">()</span>
|
|
<span class="n">group_commit_sems</span><span class="o">.</span><span class="n">push</span><span class="p">(</span><span class="n">sem</span><span class="p">)</span>
|
|
<span class="n">sem</span><span class="o">.</span><span class="n">wait</span><span class="p">()</span>
|
|
<span class="k">return</span> <span class="mi">200</span><span class="p">,</span> <span class="p">{}</span>
|
|
|
|
<span class="k">def</span> <span class="nf">handle_read</span><span class="p">(</span><span class="n">req</span><span class="p">):</span>
|
|
<span class="n">value</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">key</span><span class="p">)</span>
|
|
<span class="k">return</span> <span class="mi">200</span><span class="p">,</span> <span class="p">{</span><span class="s2">"value"</span><span class="p">:</span> <span class="n">value</span><span class="p">}</span>
|
|
</pre></div>
|
|
<p>It is critical that <code>handle_write</code> waits to return a success until the
|
|
write is durable via fsync.</p>
|
|
<p>So to reiterate, the key idea for durability of a client request is
|
|
that you have some version of the client message stored on disk
|
|
durably with fsync before returning a success to a client.</p>
|
|
<p>From now on in this post, when you see "durable" or "durability", it
|
|
means that the data has been written and fsync-ed to disk.</p>
|
|
<h3 id="optimizing-durable-writes">Optimizing durable writes</h3><p>A key insight is that it's silly to serialize the entire permanent
|
|
structure of the database to disk every time a user writes.</p>
|
|
<p>We could just write the user's message itself to an append-only
|
|
log. And then only periodically write the entire btree to disk. So
|
|
long as we have fsync-ed the append-only log file, we can safely
|
|
return to the user even if the btree itself has not yet been written
|
|
to disk.</p>
|
|
<p>The additional logic this requires is that on startup we must read the
|
|
btree from disk and then replay the log on top of the btree.</p>
|
|
<div class="highlight"><pre><span></span><span class="n">f</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"kv.db"</span><span class="p">,</span> <span class="s2">"rw"</span><span class="p">)</span>
|
|
<span class="n">db</span> <span class="o">=</span> <span class="n">btree</span><span class="o">.</span><span class="n">init_from_disk</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
|
|
|
<span class="n">log_f</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s2">"kv.log"</span><span class="p">,</span> <span class="s2">"rw"</span><span class="p">)</span>
|
|
<span class="n">l</span> <span class="o">=</span> <span class="n">log</span><span class="o">.</span><span class="n">init_from_disk</span><span class="p">()</span>
|
|
<span class="k">for</span> <span class="n">log</span> <span class="ow">in</span> <span class="n">l</span><span class="o">.</span><span class="n">read_logs_from</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">last_log_index</span><span class="p">):</span>
|
|
<span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">log</span><span class="o">.</span><span class="n">key</span><span class="p">,</span> <span class="n">log</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
|
|
|
|
<span class="n">group_commit_sems</span> <span class="o">=</span> <span class="p">[]</span>
|
|
|
|
<span class="nd">@background_worker</span><span class="p">()</span>
|
|
<span class="k">def</span> <span class="nf">group_commit</span><span class="p">():</span>
|
|
<span class="k">for</span><span class="p">:</span>
|
|
<span class="n">log_accumulator</span> <span class="o">=</span> <span class="n">log_page</span><span class="p">()</span>
|
|
<span class="k">if</span> <span class="n">clock</span><span class="p">()</span> <span class="o">%</span> <span class="mi">5</span><span class="n">ms</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
|
|
<span class="k">for</span> <span class="p">(</span><span class="n">log</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="ow">in</span> <span class="n">group_commit_sems</span><span class="p">:</span>
|
|
<span class="n">log_accumulator</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">log</span><span class="p">)</span>
|
|
|
|
<span class="n">log_f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">log_accumulator</span><span class="o">.</span><span class="n">page</span><span class="p">())</span> <span class="c1"># Write out all log entries at once</span>
|
|
<span class="n">log_f</span><span class="o">.</span><span class="n">fsync</span><span class="p">()</span> <span class="c1"># Durably flush wal data</span>
|
|
<span class="k">for</span> <span class="p">(</span><span class="n">_</span><span class="p">,</span> <span class="n">sem</span><span class="p">)</span> <span class="ow">in</span> <span class="n">group_commit_sems</span><span class="p">:</span>
|
|
<span class="n">sem</span><span class="o">.</span><span class="n">signal</span><span class="p">()</span>
|
|
|
|
<span class="k">if</span> <span class="n">clock</span><span class="p">()</span> <span class="o">%</span> <span class="mi">1</span><span class="n">m</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
|
|
<span class="n">db</span><span class="o">.</span><span class="n">write_to_disk</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
|
<span class="n">f</span><span class="o">.</span><span class="n">fsync</span><span class="p">()</span> <span class="c1"># Durably flush db data</span>
|
|
|
|
<span class="k">def</span> <span class="nf">handle_write</span><span class="p">(</span><span class="n">req</span><span class="p">):</span>
|
|
<span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">key</span><span class="p">,</span> <span class="n">req</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
|
|
<span class="n">sem</span> <span class="o">=</span> <span class="n">semaphore</span><span class="p">()</span>
|
|
<span class="n">log</span> <span class="o">=</span> <span class="n">req</span>
|
|
<span class="n">group_commit_sems</span><span class="o">.</span><span class="n">push</span><span class="p">((</span><span class="n">log</span><span class="p">,</span> <span class="n">sem</span><span class="p">))</span>
|
|
<span class="n">sem</span><span class="o">.</span><span class="n">wait</span><span class="p">()</span> <span class="c1"># This time waiting for only the log to be written and flushed, not the btree.</span>
|
|
<span class="k">return</span> <span class="mi">200</span><span class="p">,</span> <span class="p">{}</span>
|
|
|
|
<span class="k">def</span> <span class="nf">handle_read</span><span class="p">(</span><span class="n">req</span><span class="p">):</span>
|
|
<span class="n">value</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">req</span><span class="o">.</span><span class="n">key</span><span class="p">)</span>
|
|
<span class="k">return</span> <span class="mi">200</span><span class="p">,</span> <span class="p">{</span><span class="s2">"value"</span><span class="p">:</span> <span class="n">value</span><span class="p">}</span>
|
|
</pre></div>
|
|
<p>This is a write-ahead log!</p>
|
|
<p>Consider a few scenarios. One request writes the smallest key ever
|
|
seen. And one request within the same millisecond writes the largest
|
|
key ever seen. Writing these to disk on the btree means modifying at
|
|
least two pages spread out in space on disk.</p>
|
|
<p>But if we only have to durably write these two messages to a log, they
|
|
can likely both be included in the same log page. ("Likely" so long as
|
|
key and values are small enough that multiple can fit into the same
|
|
page.)</p>
|
|
<p>That is, it's cheaper to write only these small messages representing
|
|
the client request to disk. And we save the structured btree
|
|
persistence for a less frequent durable write.</p>
|
|
<h3 id="filesystem-and-disk-bugs">Filesystem and disk bugs</h3><p>Sometimes filesystems will write data to the wrong place. Sometimes
|
|
disks corrupt data. A solution to both of these is to checksum the
|
|
data on write, store the checksum on disk, and confirm the checksum on
|
|
read. This combined with a background process called scrubbing to
|
|
validate unread data can help you learn quickly when your data has
|
|
been corrupted and you must recover from backup.</p>
|
|
<p>MongoDB's default storage engine WiredTiger <b>does</b> checksum data <a href="https://github.com/wiredtiger/wiredtiger/blob/develop/src/docs/tune-checksum.dox#L3">by
|
|
default</a>.</p>
|
|
<p>But some databases famous for integrity do not. Postgres does <a href="https://www.postgresql.org/docs/current/checksums.html">no data
|
|
checksumming</a>
|
|
by default:</p>
|
|
<blockquote><p>By default, data pages are not protected by checksums, but this can
|
|
optionally be enabled for a cluster. When enabled, each data page
|
|
includes a checksum that is updated when the page is written and
|
|
verified each time the page is read. Only data pages are protected by
|
|
checksums; internal data structures and temporary files are not.</p>
|
|
</blockquote>
|
|
<p>SQLite likewise does no checksumming by default. Checksumming is an
|
|
<a href="https://www.sqlite.org/cksumvfs.html">optional extension</a>:</p>
|
|
<blockquote><p>The checksum VFS extension is a VFS shim that adds an 8-byte
|
|
checksum to the end of every page in an SQLite database. The checksum
|
|
is added as each page is written and verified as each page is
|
|
read. The checksum is intended to help detect database corruption
|
|
caused by random bit-flips in the mass storage device.</p>
|
|
</blockquote>
|
|
<p>But even this isn't perfect. Disks and nodes can fail completely. At
|
|
that point you can only improve durability by introducing redundancy
|
|
across disks (and/or nodes), for example, via distributed consensus.</p>
|
|
<h3 id="other-reasons-you-<em>need</em>-a-wal?">Other reasons you <em>need</em> a WAL?</h3><p>Some databases (like SQLite) require a write-ahead log to implement
|
|
aspects of ACID transactions. But this need not be a requirement for
|
|
ACID transactions if you do MVCC (SQLite does not). See my previous
|
|
post on <a href="https://notes.eatonphil.com/2024-05-16-mvcc.html">implementing
|
|
MVCC</a> for details.</p>
|
|
<p>Logical replication (also called change data capture (CDC)) is another
|
|
interesting feature that requires a write-ahead log. The idea is that
|
|
the log already preserves the exact order and changes that affect the
|
|
database's "state machine". So we could copy these changes out of the
|
|
system by tracking the write-ahead log, preserving change order, and
|
|
apply these changes to a foreign system.</p>
|
|
<p>But again, just CDC is not about durability. It's an ancillary feature
|
|
that write-ahead logs make simple.</p>
|
|
<h3 id="conclusion">Conclusion</h3><p>A few key points. One, durability primarily matters if it is
|
|
established before returning a success to the client. Second, a
|
|
write-ahead log is a cheap way to get durability.</p>
|
|
<p>And finally, durability is a spectrum. You need to read the docs for
|
|
your database to understand what it does and does not.</p>
|
|
<p><blockquote class="twitter-tweet"><p lang="en" dir="ltr">Here's a new post about durability and write-ahead logs. Write-ahead logs are used almost everywhere. But to build an intuition for why, it is helpful to imagine what you would do without a WAL. And to explore the meaning of durability.<a href="https://t.co/nzS2pMz22z">https://t.co/nzS2pMz22z</a> <a href="https://t.co/m1n9x8CNcp">pic.twitter.com/m1n9x8CNcp</a></p>— Phil Eaton (@eatonphil) <a href="https://twitter.com/eatonphil/status/1807741130093556098?ref_src=twsrc%5Etfw">July 1, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p>
|
|
<style>.feedback{display:initial;}</style>
|
|
</div>
|
|
<div class="feedback">
|
|
<h4>Feedback</h4>
|
|
<p>As always,
|
|
please <a href="mailto:phil@eatonphil.com">email</a>
|
|
or <a href="https://twitter.com/eatonphil">tweet me</a>
|
|
with questions, corrections, or ideas!</p>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<footer>
|
|
<div class="container">
|
|
<div>
|
|
<div id="subscribe">
|
|
<iframe frameBorder="0" src="https://cdn.forms-content-1.sg-form.com/e8ed4c3b-dd46-11f0-b6e6-ce075f2c5f80"></iframe>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</body>
|
|
</html>
|