There have been major changes in storage architecture since version 1.1.
Version 1.1 defines strict 2-level storage model between in-memory and disk. It gives worst-case guarantee O(1) for any ordered key read in terms of disk access.
This approach had its limitations, since it was unable to efficiently maintain memory limit with required performance. Additionally there was a need for multi-threaded compaction.
Sophia has evolved in a way that expands original ideas. Architecture has been designed to efficiently work with memory and large amount of keys.
In fact, it became even simplier.
Sophia's main storage unit is a Node.
Each Node represents a single file with associated in-memory region index and two in-memory key indexes. Node file consists of Branches.
Each Branch consists of a set of sorted Regions and Region Index.
A single Region holds keys with values. It has the same semantical meaning as a B-Tree page, but organized in a different way. It does not have a tree structure or any internal page-to-page relationships and thus no meta-data overhead.
A Region Index is represented by an ordered range of regions with their min and max keys and on-disk reference. Regions never overlap.
A Key Index is very similar to LSM zero-level (memtable), but has a different key lifecycle. All modifications first get into the index and hold up until they are explicitly removed by the merger.
Before getting added to the in-memory Key Index, modifications are first written to the Write-Ahead Log.
Database lifecycle is organized in terms of two major operations: Branch and Compaction.
When a Node's in-memory Key Index size reaches a special watermark value or global memory limit, Branch operation is scheduled.
When some Node branch counter reaches a special watermark value, Compaction operation is scheduled.
- rotate in-memory Key Index (use second one empty) (Node updates during Branch goes to second index)
- create new Regions and Region Index from Key Index
- create new Node Branch
- sequentially write Branch to the end of Node file
- free index and rotate
- sequentially read Node file into memory
- create iterator for each Branch
merge and split key-stream: 1. create one or more Nodes 2. delete Node
sequentially write new Node or Nodes
- redistribute online updates between new Nodes
- remove old Node
- rename new Node or Nodes when completed
All background operations are planned by special scheduler.
There is a game between available memory, a number of Branches and Search times.
Each additional branch says that there is a possible additional disk access during the search. During the search, only branch regions that have min >= key <= max are examined. In the same time, it is unable to maintain memory limits without branching, because compaction times are greater than possible rate of incoming data.
Sophia is designed to be read optimized. There is a high possibility that latest created Branches (hot data) are stored in the file system cache. Scheduler is aware about nodes which have largest in-memory Key Index and biggest number of Branches. These are processed first.
Additionally all operations are planned taking current system state in account, like memory usage statistics, current load profiler (redzone), operations priorities, checkpoint, backup, etc.
Sophia compaction is multi-threaded. Each worker (thread) requests scheduler for a new task. Basic unit of a background task is an operation on a single Node.
Sophia is designed to efficiently utilize available memory. If there is more memory available, then branch/compaction operations become more infrequent and system becomes more disk-efficient. Best performance can be obtained with no memory limit set. Sophia is Hard-Drive (and Flash) friendly, since all operations are delayed and executed in large sequential reads and writes, without overwrite.
Garbage collection (MVCC related) is executed automatically by Compaction task.
Also, scheduler periodically checks if there are any nodes which have large percentage of transactional versions (duplicates) stored per node.
Sophia has following algorithmic complexity (in terms of disk access):
set worst case is O(1) write-ahead-log append-only key write + in-memory node index search + in-memory index insert
delete worst case is O(1) (delete operation is equal to set)
get worst case is amortized O(max_branch_count_per_node) random region read from a single node file, which itself does in-memory key index search + in-memory region search
range worst case of full database traversal is amortized O(total_region_count) + in-memory key-index searches for each node