Abstract

This guide contains detailed information about using BookKeeper for logging. It discusses the basic operations BookKeeper supports, and how to create logs and perform basic read and write operations on these logs.

BookKeeper introduction

BookKeeper is a replicated service to reliably log streams of records. In BookKeeper, servers are "bookies", log streams are "ledgers", and each unit of a log (aka record) is a "ledger entry". BookKeeper is designed to be reliable; bookies, the servers that store ledgers, can crash, corrupt data, discard data, but as long as there are enough bookies behaving correctly the service as a whole behaves correctly.

The initial motivation for BookKeeper comes from the namenode of HDFS. Namenodes have to log operations in a reliable fashion so that recovery is possible in the case of crashes. We have found the applications for BookKeeper extend far beyond HDFS, however. Essentially, any application that requires an append storage can replace their implementations with BookKeeper. BookKeeper has the advantage of writing efficiently, replicating for fault tolerance, and scaling throughput with the number of servers through striping.

At a high level, a bookkeeper client receives entries from a client application and stores it to sets of bookies, and there are a few advantages in having such a service:

In slightly more detail...

BookKeeper implements highly available logs, and it has been designed with write-ahead logging in mind. Besides high availability due to the replicated nature of the service, it provides high throughput due to striping. As we write entries in a subset of bookies of an ensemble and rotate writes across available quorums, we are able to increase throughput with the number of servers for both reads and writes. Scalability is a property that is possible to achieve in this case due to the use of quorums. Other replication techniques, such as state-machine replication, do not enable such a property.

An application first creates a ledger before writing to bookies through a local BookKeeper client instance. Upon creating a ledger, a BookKeeper client writes metadata about the ledger to ZooKeeper. Each ledger currently has a single writer. This writer has to execute a close ledger operation before any other client can read from it. If the writer of a ledger does not close a ledger properly because, for example, it has crashed before having the opportunity of closing the ledger, then the next client that tries to open a ledger executes a procedure to recover it. As closing a ledger consists essentially of writing the last entry written to a ledger to ZooKeeper, the recovery procedure simply finds the last entry written correctly and writes it to ZooKeeper.

Note that currently this recovery procedure is executed automatically upon trying to open a ledger and no explicit action is necessary. Although two clients may try to recover a ledger concurrently, only one will succeed, the first one that is able to create the close znode for the ledger.

Bookkeeper elements and concepts

BookKeeper uses four basic elements:

Bookkeeper initial design

A set of bookies implements BookKeeper, and we use a quorum-based protocol to replicate data across the bookies. There are basically two operations to an existing ledger: read and append. Here is the complete API list (mode detail here):

There is only a single client that can write to a ledger. Once that ledger is closed or the client fails, no more entries can be added. (We take advantage of this behavior to provide our strong guarantees.) There will not be gaps in the ledger. Fingers get broken, people get roughed up or end up in prison when books are manipulated, so there is no deleting or changing of entries.

p. A simple use of BooKeeper is to implement a write-ahead transaction log. A server maintains an in-memory data structure (with periodic snapshots for example) and logs changes to that structure before it applies the change. The application server creates a ledger at startup and store the ledger id and password in a well known place (ZooKeeper maybe). When it needs to make a change, the server adds an entry with the change information to a ledger and apply the change when BookKeeper adds the entry successfully. The server can even use asyncAddEntry to queue up many changes for high change throughput. BooKeeper meticulously logs the changes in order and call the completion functions in order.

When the application server dies, a backup server will come online, get the last snapshot and then it will open the ledger of the old server and read all the entries from the time the snapshot was taken. (Since it doesn't know the last entry number it will use MAX_INTEGER). Once all the entries have been processed, it will close the ledger and start a new one for its use.

A client library takes care of communicating with bookies and managing entry numbers. An entry has the following fields:

FieldTypeDescription
Ledger numberlongThe id of the ledger of this entry
Entry numberlongThe id of this entry
last confirmed ( LC )longid of the last recorded entry
databyte[]the entry data (supplied by application)
authentication codebyte[]Message authentication code that includes all other fields of the entry

The client library generates a ledger entry. None of the fields are modified by the bookies and only the first three fields are interpreted by the bookies.

To add to a ledger, the client generates the entry above using the ledger number. The entry number will be one more than the last entry generated. The LC field contains the last entry that has been successfully recorded by BookKeeper. If the client writes entries one at a time, LC is the last entry id. But, if the client is using asyncAddEntry, there may be many entries in flight. An entry is considered recorded when both of the following conditions are met:

LC seems mysterious right now, but it is too early to explain how we use it; just smile and move on.

Once all the other fields have been field in, the client generates an authentication code with all of the previous fields. The entry is then sent to a quorum of bookies to be recorded. Any failures will result in the entry being sent to a new quorum of bookies.

To read, the client library initially contacts a bookie and starts requesting entries. If an entry is missing or invalid (a bad MAC for example), the client will make a request to a different bookie. By using quorum writes, as long as enough bookies are up we are guaranteed to eventually be able to read an entry.

Bookkeeper metadata management

There are some meta data that needs to be made available to BookKeeper clients:

We maintain this information in ZooKeeper. Bookies use ephemeral nodes to indicate their availability. Clients use znodes to track ledger creation and deletion and also to know the end of the ledger and the bookies that were used to store the ledger. Bookies also watch the ledger list so that they can cleanup ledgers that get deleted.

Closing out ledgers

The process of closing out the ledger and finding the last entry is difficult due to the durability guarantees of BookKeeper:

If the ledger was closed gracefully, ZooKeeper will have the last entry and everything will work well. But, if the BookKeeper client that was writing the ledger dies, there is some recovery that needs to take place.

The problematic entries are the ones at the end of the ledger. There can be entries in flight when a BookKeeper client dies. If the entry only gets to one bookie, the entry should not be readable since the entry will disappear if that bookie fails. If the entry is only on one bookie, that doesn't mean that the entry has not been recorded successfully; the other bookies that recorded the entry might have failed.

The trick to making everything work is to have a correct idea of a last entry. We do it in roughly three steps:

  1. Find the entry with the highest last recorded entry, LC ;
  2. Find the highest consecutively recorded entry, LR ;
  3. Make sure that all entries between LC and LR are on a quorum of bookies;

Data Management in Bookies

This section gives an overview of how a bookie manages its ledger fragments.

Basic

Bookies manage data in a log-structured way, which is implemented using three kind of files:

Since updating index files would introduce random disk I/O, for performance consideration, index files are updated lazily by a Sync Thread running in the background. Before index pages are persisted to disk, they are gathered in LedgerCache for lookup.

Add Entry

When a bookie receives entries from clients to be written, these entries will go through the following steps to be persisted to disk:

  1. Append the entry in Entry Log, return its position { logId , offset } ;
  2. Update the index of this entry in Ledger Cache ;
  3. Append a transaction corresponding to this entry update in Journal ;
  4. Respond to BookKeeper client ;

Data Flush

Ledger index pages are flushed to index files in the following two cases:

  1. LedgerCache memory reaches its limit. There is no more space available to hold newer index pages. Dirty index pages will be evicted from LedgerCache and persisted to index files.
  2. A background thread Sync Thread is responsible for flushing index pages from LedgerCache to index files periodically.

Besides flushing index pages, Sync Thread is responsible for rolling journal files in case that journal files use too much disk space.

The data flush flow in Sync Thread is as follows:

  1. Records a LastLogMark in memory. The LastLogMark contains two parts: first one is txnLogId (file id of a journal) and the second one is txnLogPos (offset in a journal). The LastLogMark indicates that those entries before it have been persisted to both index and entry log files.
  2. Flushes dirty index pages from LedgerCache to index file, and flushes entry log files to ensure all buffered entries in entry log files are persisted to disk.
    1. Ideally, a bookie just needs to flush index pages and entry log files that contains entries before LastLogMark. There is no such information in LedgerCache and Entry Log mapping to journal files, though. Consequently, the thread flushes LedgerCache and Entry Log entirely here, and may flush entries after the LastLogMark. Flushing more is not a problem, though, just redundant.
  3. Persists LastLogMark to disk, which means entries added before LastLogMark whose entry data and index page were also persisted to disk. It is the time to safely remove journal files created earlier than txnLogId.
    1. If the bookie has crashed before persisting LastLogMark to disk, it still has journal files containing entries for which index pages may not have been persisted. Consequently, when this bookie restarts, it inspects journal files to restore those entries; data isn't lost.

Using the above data flush mechanism, it is safe for the Sync Thread to skip data flushing when the bookie shuts down. However, in Entry Logger, it uses BufferedChannel to write entries in batches and there might be data buffered in BufferedChannel upon a shut down. The bookie needs to ensure Entry Logger flushes its buffered data during shutting down. Otherwise, Entry Log files become corrupted with partial entries.

As described above, EntryLogger#flush is invoked in the following two cases:
* in Sync Thread : used to ensure entries added before LastLogMark are persisted to disk.
* in ShutDown : used to ensure its buffered data persisted to disk to avoid data corruption with partial entries.

Data Compaction

In bookie server, entries of different ledgers are interleaved in entry log files. A bookie server runs a Garbage Collector thread to delete un-associated entry log files to reclaim disk space. If a given entry log file contains entries from a ledger that has not been deleted, then the entry log file would never be removed and the occupied disk space never reclaimed. In order to avoid such a case, a bookie server compacts entry log files in Garbage Collector thread to reclaim disk space.

There are two kinds of compaction running with different frequency, which are Minor Compaction and Major Compaction. The differences of Minor Compaction and Major Compaction are just their threshold value and compaction interval.

  1. Threshold : Size percentage of an entry log file occupied by those undeleted ledgers. Default minor compaction threshold is 0.2, while major compaction threshold is 0.8.
  2. Interval : How long to run the compaction. Default minor compaction is 1 hour, while major compaction threshold is 1 day.

NOTE: if either Threshold or Interval is set to less than or equal to zero, then compaction is disabled.

The data compaction flow in Garbage Collector Thread is as follows:

  1. Garbage Collector thread scans entry log files to get their entry log metadata, which records a list of ledgers comprising an entry log and their corresponding percentages.
  2. With the normal garbage collection flow, once the bookie determines that a ledger has been deleted, the ledger will be removed from the entry log metadata and the size of the entry log reduced.
  3. If the remaining size of an entry log file reaches a specified threshold, the entries of active ledgers in the entry log will be copied to a new entry log file.
  4. Once all valid entries have been copied, the old entry log file is deleted.