Movatterモバイル変換


[0]ホーム

URL:


Skip to main contentLinkMenuExpand(external link)DocumentSearchCopyCopied
Pocket Flow

Chapter 4: DBImpl - The Database General Manager

In the previous chapters, we’ve explored some key ingredients of LevelDB:

But how do all these pieces work together? Who tells LevelDB to write to the WAL first,then the MemTable? Who decides when the MemTable is full and needs to be flushed to an SSTable? Who coordinates reading data from both memoryand disk files?

What’s the Problem? Orchestrating Everything

Imagine a large library. You have librarians putting books on shelves (SSTables), a front desk clerk taking newly returned books (MemTable), and a security guard logging everyone who enters (WAL). But someone needs to be in charge of the whole operation – theGeneral Manager.

This manager doesn’t shelve every book themselves, but they direct the staff, manage the budget, decide when to rearrange sections (compaction), and handle emergencies (recovery). Without a manager, it would be chaos!

LevelDB needs a similar central coordinator to manage all its different parts and ensure they work together smoothly and correctly.

DBImpl: The General Manager of LevelDB

TheDBImpl class is the heart of LevelDB’s implementation. It’s theGeneral Manager of our database library. It doesn’tcontain the data itself (that’s in MemTables and SSTables), but itorchestrates almost every operation.

  • It takes requests from your application (likePut,Get,Delete).
  • It directs these requests to the right components (WAL, MemTable, TableCache).
  • It manages the state of the database (like which MemTable is active, which files exist).
  • It initiates and manages background tasks like flushing the MemTable and running compactions.
  • It handles the recovery process when the database starts up.

Almost every interaction you have with a LevelDB database object ultimately goes throughDBImpl.

Key Responsibilities of DBImpl

Think of theDBImpl general manager juggling several key tasks:

  1. Handling Writes (Put,Delete,Write): Ensuring data is safely written to the WAL and then the MemTable. Managing the process when the MemTable fills up.
  2. Handling Reads (Get,NewIterator): Figuring out where to find the requested data – checking the active MemTable, the soon-to-be-flushed immutable MemTable, and finally the various SSTable files on disk (using helpers likeVersion & VersionSet andTable / SSTable & TableCache).
  3. Background Maintenance (Compaction): Deciding when and how to run compactions to clean up old data, merge SSTables, and keep reads efficient. It schedules and oversees this background work.
  4. Startup and Recovery: When the database opens,DBImpl manages locking the database directory, reading the manifest file (Version & VersionSet), and replaying theWAL to recover any data that wasn’t flushed before the last shutdown or crash.
  5. Snapshot Management: Handling requests to create and release snapshots, which provide a consistent view of the database at a specific point in time.

DBImpl uses other components extensively to perform these tasks. It holds references to the active MemTable (mem_), the immutable MemTable (imm_), the WAL (log_), theTableCache, and theVersionSet (which tracks all the SSTable files).

How DBImpl Handles Writes

Let’s trace a simplePut operation:

  1. Request: Your application callsdb->Put("mykey", "myvalue").
  2. DBImpl Entry: This call enters theDBImpl::Put method (which typically wraps the operation in aWriteBatch and callsDBImpl::Write).
  3. Queueing (Optional):DBImpl manages a queue of writers to ensure writes happen in order. It might group multiple concurrent writes together for efficiency (BuildBatchGroup).
  4. Making Room: Before writing,DBImpl checks if there’s space in the currentMemTable (mem_). If not (MakeRoomForWrite), it might:
    • Pause briefly if Level-0 SSTable count is high (slowdown trigger).
    • Wait if theimmutable MemTable (imm_) is still being flushed.
    • Wait if Level-0 SSTable count is too high (stop trigger).
    • Trigger a MemTable switch:
      • Mark the currentmem_ as read-only (imm_).
      • Create a new emptymem_.
      • Create a new WAL file (logfile_).
      • Schedule a background task (MaybeScheduleCompaction) to flush the oldimm_ to an SSTable.
  5. Write to WAL:DBImpl writes the operation(s) to the current WAL file (log_->AddRecord(...)). If requested (options.sync), it ensures the WAL data is physically on disk (logfile_->Sync()).
  6. Write to MemTable: Only after the WAL write succeeds,DBImpl inserts the data into the activeMemTable (mem_->Add(...) viaWriteBatchInternal::InsertInto).
  7. Return: Control returns to your application.

Here’s a highly simplified view of theWrite method:

// --- Simplified from db/db_impl.cc ---StatusDBImpl::Write(constWriteOptions&options,WriteBatch*updates){// ... acquire mutex, manage writer queue (omitted) ...// Step 4: Make sure there's space. This might trigger a MemTable switch// and schedule background work. May wait if MemTable is full or// too many L0 files exist.Statusstatus=MakeRoomForWrite(updates==nullptr/* force compact? */);if(status.ok()&&updates!=nullptr){// ... potentially group multiple concurrent writes (BuildBatchGroup) ...// Step 5: Add the batch to the Write-Ahead Logstatus=log_->AddRecord(WriteBatchInternal::Contents(updates));if(status.ok()&&options.sync){// Ensure log entry is on disk if requestedstatus=logfile_->Sync();// ... handle sync error by recording background error ...}// Step 6: Insert the batch into the active MemTable (only if WAL ok)if(status.ok()){status=WriteBatchInternal::InsertInto(updates,mem_);}}// ... update sequence number, manage writer queue, release mutex ...returnstatus;// Step 7: Return status to caller}

Explanation: This code shows the core sequence: check/make room (MakeRoomForWrite), write to the log (log_->AddRecord), potentially sync the log (logfile_->Sync), and finally insert into the MemTable (InsertInto(..., mem_)). Error handling and writer coordination are omitted for clarity.

sequenceDiagram    participant App as Application    participant DBImpl    participant WriterQueue as Writer Queue    participant LogWriter as log::Writer (WAL)    participant MemTable as Active MemTable (RAM)    App->>DBImpl: Put("key", "value") / Write(batch)    DBImpl->>WriterQueue: Add writer to queue    Note over DBImpl: Waits if not front of queue    DBImpl->>DBImpl: MakeRoomForWrite()?    alt MemTable Full / L0 Trigger        DBImpl->>DBImpl: Switch MemTable, Schedule Flush    end    DBImpl->>LogWriter: AddRecord(batch_data)    opt Sync Option Enabled      DBImpl->>LogWriter: Sync() Log File    end    LogWriter-->>DBImpl: Log Write Status    alt Log Write OK        DBImpl->>MemTable: InsertInto(batch_data)        MemTable-->>DBImpl: Insert Status        DBImpl->>WriterQueue: Remove writer, Signal next        DBImpl-->>App: Return OK    else Log Write Failed        DBImpl->>WriterQueue: Remove writer, Signal next        DBImpl-->>App: Return Error Status    end

How DBImpl Handles Reads

Reading data involves checking different places in a specific order to ensure the most recent value is found:

  1. Request: Your application callsdb->Get("mykey").
  2. DBImpl Entry: The call entersDBImpl::Get.
  3. Snapshot:DBImpl determines the sequence number to read up to (either from the providedReadOptions::snapshot or the current latest sequence number).
  4. Check MemTable: It first checks the activeMemTable (mem_). If the key is found (either a value or a deletion marker), the search stops, and the result is returned.
  5. Check Immutable MemTable: If not found inmem_, and if an immutable MemTable (imm_) exists (one that’s waiting to be flushed), it checksimm_. If found, the search stops.
  6. Check SSTables: If the key wasn’t found in memory,DBImpl asks the currentVersion (managed byVersionSet) to find the key in the SSTable files (current->Get(...)). TheVersion object knows which files might contain the key and uses theTableCache to access them efficiently.
  7. Update Stats (Optional): If the read involved checking SSTables,DBImpl might update internal statistics about file access (current->UpdateStats). If a file is read frequently, this might trigger a future compaction (MaybeScheduleCompaction).
  8. Return: The value found (or a “Not Found” status) is returned to the application.

A simplified view ofGet:

// --- Simplified from db/db_impl.cc ---StatusDBImpl::Get(constReadOptions&options,constSlice&key,std::string*value){Statuss;SequenceNumbersnapshot;// ... (Step 3) Determine snapshot sequence number ...mutex_.Lock();// Need lock to access mem_, imm_, current versionMemTable*mem=mem_;MemTable*imm=imm_;Version*current=versions_->current();mem->Ref();// Increase reference countsif(imm!=nullptr)imm->Ref();current->Ref();mutex_.Unlock();// Unlock for potentially slow lookupsLookupKeylkey(key,snapshot);// Internal key format for lookup// Step 4: Check active MemTableif(mem->Get(lkey,value,&s)){// Found in mem_ (value or deletion marker)}// Step 5: Check immutable MemTable (if it exists)elseif(imm!=nullptr&&imm->Get(lkey,value,&s)){// Found in imm_}// Step 6: Check SSTables via current Versionelse{Version::GetStatsstats;// To record file access statss=current->Get(options,lkey,value,&stats);// Step 7: Maybe update stats and schedule compactionif(current->UpdateStats(stats)){mutex_.Lock();MaybeScheduleCompaction();// Needs lockmutex_.Unlock();}}// Decrease reference countsmutex_.Lock();mem->Unref();if(imm!=nullptr)imm->Unref();current->Unref();mutex_.Unlock();returns;// Step 8: Return status}

Explanation: This shows the order of checking:mem->Get,imm->Get, and finallycurrent->Get (which searches SSTables). It also highlights the reference counting (Ref/Unref) needed because these components might be changed or deleted by background threads while the read is in progress. The lock is held only when accessing shared pointers, not during the actual data lookup.

sequenceDiagram    participant App as Application    participant DBImpl    participant MemTable as Active MemTable (RAM)    participant ImmMemTable as Immutable MemTable (RAM)    participant Version as Current Version    participant TableCache as TableCache (SSTables)    App->>DBImpl: Get("key")    DBImpl->>MemTable: Get(lkey)?    alt Key Found in MemTable        MemTable-->>DBImpl: Return value / deletion        DBImpl-->>App: Return value / NotFound    else Key Not Found in MemTable        MemTable-->>DBImpl: Not Found        DBImpl->>ImmMemTable: Get(lkey)?        alt Key Found in ImmMemTable            ImmMemTable-->>DBImpl: Return value / deletion            DBImpl-->>App: Return value / NotFound        else Key Not Found in ImmMemTable            ImmMemTable-->>DBImpl: Not Found            DBImpl->>Version: Get(lkey) from SSTables?            Version->>TableCache: Find key in relevant SSTables            TableCache-->>Version: Return value / deletion / NotFound            Version-->>DBImpl: Return value / deletion / NotFound            DBImpl-->>App: Return value / NotFound        end    end

Managing Background Work (Compaction)

DBImpl is responsible for kicking off background work. It doesn’tdo the compaction itself (that logic is largely withinCompaction andVersionSet), but it manages thetriggering and the background thread.

  • When is work needed?DBImpl checks if work is needed in a few places:
    • After a MemTable switch (MakeRoomForWrite schedules flush ofimm_).
    • After a read operation updates file stats (Get might callMaybeScheduleCompaction).
    • After a background compaction finishes (it checks ifmore compaction is needed).
    • When explicitly requested (CompactRange).
  • Scheduling: If work is needed and a background task isn’t already running,DBImpl::MaybeScheduleCompaction sets a flag (background_compaction_scheduled_) and asks theEnv (Environment object, handles OS interactions) to schedule a function (DBImpl::BGWork) to run on a background thread.
  • Performing Work: The background thread eventually callsDBImpl::BackgroundCall, which locks the mutex and callsDBImpl::BackgroundCompaction. This method decideswhat work to do:
    • Ifimm_ exists, it callsCompactMemTable (which usesWriteLevel0Table ->BuildTable) to flush it.
    • Otherwise, it asks theVersionSet to pick an appropriate SSTable compaction (versions_->PickCompaction()).
    • It then callsDoCompactionWork to perform the actual SSTable compaction (releasing the main lock during the heavy lifting).
  • Signaling: Once background work finishes, it signals (background_work_finished_signal_.SignalAll()) any foreground threads that might be waiting (e.g., a write operation waiting forimm_ to be flushed).

Here’s the simplified scheduling logic:

// --- Simplified from db/db_impl.cc ---voidDBImpl::MaybeScheduleCompaction(){mutex_.AssertHeld();// Must hold lock to check/change stateif(background_compaction_scheduled_){// Already scheduled}elseif(shutting_down_.load(std::memory_order_acquire)){// DB is closing}elseif(!bg_error_.ok()){// Background error stopped activity}elseif(imm_==nullptr&&// No MemTable flush needed ANDmanual_compaction_==nullptr&&// No manual request AND!versions_->NeedsCompaction()){// VersionSet says no work needed// No work to be done}else{// Work needs to be done! Schedule it.background_compaction_scheduled_=true;env_->Schedule(&DBImpl::BGWork,this);// Ask Env to run BGWork later}}

Explanation: This function checks several conditions under a lock. If there’s an immutable MemTable to flush (imm_ != nullptr) or theVersionSet indicates compaction is needed (versions_->NeedsCompaction()) and no background task is already scheduled, it marks one as scheduled and tells the environment (env_) to run theBGWork function in the background.

flowchart TD    A["Write/Read/Compact finishes"] --> B{"Need Compaction?"}    B -->|Yes| C{"BG Task Scheduled?"}    B -->|No| Z["Idle"]    C -->|Yes| Z    C -->|No| D["Mark BG Scheduled = true"]    D --> E["Schedule BGWork"]    E --> F["Background Thread Pool"]    F -->|Runs| G["DBImpl::BGWork"]    G --> H["DBImpl::BackgroundCall"]    H --> I{"Compact imm_ OR Pick/Run SSTable Compaction?"}    I --> J["Perform Compaction Work"]    J --> K["Mark BG Scheduled = false"]    K --> L["Signal Waiting Threads"]    L --> B

Recovery on Startup

When you open a database,DBImpl::Open orchestrates the recovery process:

  1. Lock: It locks the database directory (env_->LockFile) to prevent other processes from using it.
  2. Recover VersionSet: It callsversions_->Recover(), which reads theMANIFEST file to understand the state of SSTables from the last clean run.
  3. Find Logs: It scans the database directory for any.log files (WAL files) that are newer than the ones recorded in theMANIFEST. These logs represent writes that might not have been flushed to SSTables before the last shutdown/crash.
  4. Replay Logs: For each relevant log file found, it callsDBImpl::RecoverLogFile.
    • InsideRecoverLogFile, it creates alog::Reader.
    • It reads records (which are serializedWriteBatches) from the log file one by one.
    • For each record, it applies the operations (WriteBatchInternal::InsertInto) to a temporary in-memoryMemTable.
    • This effectively rebuilds the state of the MemTable(s) as they were just before the crash/shutdown.
  5. Finalize State: Once all logs are replayed, the recovered MemTable becomes the activemem_. If the recovery process itself filled the MemTable,RecoverLogFile might even flush it to a Level-0 SSTable (WriteLevel0Table).DBImpl updates theVersionSet with the recovered sequence number and potentially writes a newMANIFEST.
  6. Ready: The database is now recovered and ready for new operations.

Here’s a conceptual snippet from the recovery logic:

// --- Conceptual, simplified from DBImpl::RecoverLogFile ---// Inside loop processing a single log file during recovery:while(reader.ReadRecord(&record,&scratch)&&status.ok()){// Check if record looks like a valid WriteBatchif(record.size()<12){/* report corruption */continue;}// Parse the raw log record back into a WriteBatch objectWriteBatchInternal::SetContents(&batch,record);// Create a MemTable if we don't have one yet for this logif(mem==nullptr){mem=newMemTable(internal_comparator_);mem->Ref();}// Apply the operations from the batch TO THE MEMTABLEstatus=WriteBatchInternal::InsertInto(&batch,mem);// ... handle error ...// Keep track of the latest sequence number seenconstSequenceNumberlast_seq=/* ... get sequence from batch ... */;if(last_seq>*max_sequence){*max_sequence=last_seq;}// If the MemTable gets full *during recovery*, flush it!if(mem->ApproximateMemoryUsage()>options_.write_buffer_size){status=WriteLevel0Table(mem,edit,nullptr);// Flush to L0 SSTablemem->Unref();mem=nullptr;// Will create a new one if needed// ... handle error ...}}// After loop, handle the final state of 'mem'

Explanation: This loop reads each record (aWriteBatch) from the log file usingreader.ReadRecord. It then applies the batch’s changes directly to an in-memoryMemTable (InsertInto(&batch, mem)), effectively replaying the lost writes. It even handles flushing this MemTable if it fills up during the recovery process.

The DBImpl Class (Code Glimpse)

The definition ofDBImpl indb_impl.h shows the key components it manages:

// --- Simplified from db/db_impl.h ---classDBImpl:publicDB{public:DBImpl(constOptions&options,conststd::string&dbname);~DBImpl()override;// Public API methods (implementing DB interface)StatusPut(...)override;StatusDelete(...)override;StatusWrite(...)override;StatusGet(...)override;Iterator*NewIterator(...)override;constSnapshot*GetSnapshot()override;voidReleaseSnapshot(...)override;// ... other public methods ...private:// Friend classes allow access to private membersfriendclassDB;structCompactionState;// Helper struct for compactionsstructWriter;// Helper struct for writer queue// Core methods for internal operationsStatusRecover(VersionEdit*edit,bool*save_manifest);voidCompactMemTable();StatusRecoverLogFile(...);StatusWriteLevel0Table(...);StatusMakeRoomForWrite(...);voidMaybeScheduleCompaction();staticvoidBGWork(void*db);// Background task entry pointvoidBackgroundCall();voidBackgroundCompaction();StatusDoCompactionWork(...);// ... other private helpers ...// == Key Member Variables ==Env*constenv_;// OS interaction layerconstInternalKeyComparatorinternal_comparator_;// For sorting keysconstOptionsoptions_;// Database configuration optionsconststd::stringdbname_;// Database directory pathTableCache*consttable_cache_;// Cache for open SSTable filesFileLock*db_lock_;// Lock file handle for DB directoryport::Mutexmutex_;// Main mutex protecting shared statestd::atomic<bool>shutting_down_;// Flag indicating DB closureport::CondVarbackground_work_finished_signal_GUARDED_BY(mutex_);// For waitingMemTable*mem_GUARDED_BY(mutex_);// Active memtable (accepts writes)MemTable*imm_GUARDED_BY(mutex_);// Immutable memtable (being flushed)std::atomic<bool>has_imm_;// Fast check if imm_ is non-nullWritableFile*logfile_;// Current WAL file handleuint64_tlogfile_number_GUARDED_BY(mutex_);// Current WAL file numberlog::Writer*log_;// WAL writer objectVersionSet*constversions_GUARDED_BY(mutex_);// Manages SSTables/Versions// Queue of writers waiting for their turnstd::deque<Writer*>writers_GUARDED_BY(mutex_);// List of active snapshotsSnapshotListsnapshots_GUARDED_BY(mutex_);// Files being generated by compactionsstd::set<uint64_t>pending_outputs_GUARDED_BY(mutex_);// Is a background compaction scheduled/running?boolbackground_compaction_scheduled_GUARDED_BY(mutex_);// Error status from background threadsStatusbg_error_GUARDED_BY(mutex_);// Compaction statisticsCompactionStatsstats_[config::kNumLevels]GUARDED_BY(mutex_);};

Explanation: This header showsDBImpl inheriting from the publicDB interface. It contains references to essential components like theEnv,Options,TableCache,MemTable (mem_ andimm_), WAL (log_,logfile_), andVersionSet. Crucially, it also has amutex_ to protect shared state accessed by multiple threads (foreground application threads and background compaction threads) and condition variables (background_work_finished_signal_) to allow threads to wait for background work.

Conclusion

DBImpl is the central nervous system of LevelDB. It doesn’t store the data itself, but it acts as theGeneral Manager, receiving requests and coordinating the actions of all the other specialized components like the MemTable, WAL, VersionSet, and TableCache. It handles the intricate dance between fast in-memory writes, durable logging, persistent disk storage, background maintenance, and safe recovery. UnderstandingDBImpl’s role is key to seeing how all the pieces of LevelDB fit together to create a functional database.

One toolDBImpl uses to make writes efficient and atomic is theWriteBatch. Let’s see how that works next.

Next up:Chapter 5: WriteBatch


Generated byAI Codebase Knowledge Builder


[8]ページ先頭

©2009-2025 Movatter.jp