In branches/tail_header on the svn repository is an working version of new pure tail append code for CouchDB.

Right now in trunk we have zero-overwrite storage, which meant we never overwrite any previously committed data, or meta data, or even index structures.

The exception to the rule is the file header, in all previous versions, CouchDB stores the database header in the head of the file, but it's written twice, one after another, each 2k header copy integrity checked. If the power fails in the middle of writing one copy, the other should still be available.

With zero-overwrite storage, all btree updates happen at the end of the file, but document summaries were written into a buffer internally, so that docs are written contiguously in buffers (usually) near then end of the file. Big file attachments were written into internally linked buffers also near the end of the file. This design proves very robust, offers reasonable update performance and good document retrieval times. Its weaknesses, like all single storage formats, is if the file gets corrupted, the database maybe be unrecoverable.

One form of corruption that's seems fairly common (we've seen it at least 3 times), is file truncation, which is to say the end of the file goes missing. This seems to happen sometimes after a file system fills up, or machine suffers a power loss.

Unfortunately, when file truncation happens with CouchDB, it's not just the last blocks of data that are lost, it's the whole file, because the last bits of data it writes is the root btree node that's necessary to find the remaining indexes. It's possible to write a tool to scan back and attempt to find the correct offset pointers to restore the file, but that's pretty expensive and wouldn't always be correct.

To fix this, the tail_header branch I created uses something like zero overwrite storage, and takes it a little further and uses append-only storage, with every single update or deletion causing an update to the very end of the file, making the file grow. Even the header is stored at the end of the file (more accurate to be called a trailer I guess).

With this design, any file truncation simply results in an earlier version of the database. If a commit is interrupted before the header gets completely written, then the next time the database is open, the commit data is skipped over as it scans backward looking for a valid header.

Every 4k, a single byte has either a value of 1 or 0. A value of 1 means a header immediately follows the byte, otherwise it's a regular storage block. Every regular write to the file, if it spans the special byte, is split up and the special byte inserted. When reading from the file, the special bytes are automatically stripped out from the data.

When a file is first opened, the header is searched for by scanning back through the blocks, looking for a valid header that passes all the integrity checks. Usually this will be very fast, but could be a long scan depending how much data was written but not before failure.

Besides being very robust in the face of truncation, this format has the advantage of potentially speeding up the commits greatly, as everything is written sequentially at the end of the file, allowing tons of data to be written out without ever having to do a head seek. And fsync can be called fewer times now. If you have an application where you don't mind losing your most recent updates, you could turn off fsync all together. However, this assumes ordered-sequential writes, that the FS will never write out the later bytes before the earlier bytes.

Large file attachments have more overhead as the files are broken up into ~4k chunks, and stores a point to each chunk. The means opening a document requires also loading up the pointers to each chunk, instead of a single pointer like before.

Upsides:
- Extremely robust storage format. Data truncations, as caused by OS crashes, incomplete copies, etc, still allow for earlier versions of the database to be recovered.
- Faster commit speeds (in theory).
- OS level backups are to simply copy the new bytes over. (hmmm but this won't work with compaction or if we automatically truncate to valid header on file open). - Views index updates never require a fsync. (assuming ordered- sequential writes)

Downsides:
- Every update to the database will have up to 4k of overhead for header writing (the actual header is smaller, but must be written 4k aligned). - Individually updated documents are more sparse on disk by default, making long view builds slower (in theory) as the disk will need to seek forward more often. (but compaction will fix this)
- On file open, must seek back through the file to find a valid header.
- More overhead for large file attachments.

Work to be done:
- More options for when to do fsync or not, to optimize for underlying file system (before header write, after header write, not at all, etc) - Rollback? Do we want to support rolling back the file to previous versions? - Truncate on open? - When we open a file, do we want to automatically truncate off any uncommitted garbage that could be left over? - Compact should write attachments in one stage of copying, then the documents themselves, right now attachment and document writes are interleaved per-document. - Live upgrade of 0.9.0. It would be nice to be able to serve old style files to allow for zero downtime on upgrade. Right now the branch doesn't understand old files at all. - Possibly we need to fsync on database file open, since the file might be in the FS cache but not on disk due to a previous CouchDB crash. This can cause problems if the view indexer (or any indexer, like lucene) updates its index and it gets committed to disk, but the most recent version of the database still isn't committed. Then if the OS crashes or powerloss occurs, the index files might unknowingly reflect lost state in the database, which would be fixable only by doing a complete view rebuild.

Feedback on all this welcome. Please try out the branch to shake out any bugs or performance problems that might be lurking.

-Damien

Reply via email to