How the Mito2 Storage Engine Handles Time-Series Data with Region Workers and Memtables
The Mito2 storage engine assigns each region to a dedicated worker thread that serializes all write operations, buffering incoming time-series rows in a column-oriented TimeSeriesMemtable before flushing to immutable SST files, eliminating cross-thread contention on hot paths.
The Mito2 engine serves as the default storage backend for the mito table type in GreptimeDB, designed specifically for high-throughput time-series workloads. It combines a region-worker model with specialized memtables to provide lock-free ingestion and efficient columnar buffering. This article examines the source code in greptimeteam/greptimedb to explain how these components coordinate writes, flushes, and reads.
Architecture Overview: Region Workers and Serial Execution
Mito2 distributes workload across a fixed-size pool of region workers defined by MitoConfig::num_region_workers in src/mito2/src/config.rs. By default, the pool size equals half the available CPU cores. Each worker runs a dedicated Tokio task that listens on an mpsc channel for WorkerRequest messages, creating a single-threaded event loop that processes all mutations for its assigned regions.
This design guarantees that only one thread ever modifies a region’s internal state. Because the worker serializes requests, operations on region metadata and memtables require no locks, removing a major source of contention in high-write scenarios.
Region Ownership and the Single-Threaded Write Path
A region represents a logical partition of a table—essentially a contiguous range of rows sharing the same primary key space. In src/mito2/src/region.rs, the codebase enforces a strict rule: only the region worker thread this region belongs to can modify the metadata. This binding lasts for the lifetime of the region.
Each region maintains an in-memory memtable that buffers incoming writes before they are committed to disk. Because access is restricted to the owning worker, appends to the memtable are lock-free, allowing the engine to achieve high ingestion rates for time-series data.
TimeSeriesMemtable Design for Columnar Storage
For time-series tables, Mito2 uses the TimeSeriesMemtable implementation located in src/mito2/src/memtable/time_series.rs. The memtable type is selected via the region option memtable.type = "time_series" defined in src/mito2/src/region/options.rs (line 584).
Unlike generic row-oriented buffers, the TimeSeriesMemtable stores each column as a separate vector, keyed by the timestamp column. This columnar layout maximizes cache locality during bulk appends and enables efficient compression algorithms before data is frozen into SST files. The implementation also registers the time_series_memtable metric (line 612) to track buffer utilization.
Write Path: Ingestion to Persistence
The write flow demonstrates how region workers and memtables cooperate:
- Request Routing: A SQL
INSERTgenerates aSenderWriteRequestthat is routed to the appropriate worker viaWorkerRequest::Write(handled insrc/mito2/src/worker/handle_write.rs, line 43). - Batching: The worker calls
handle_write_requests, which groups rows and forwards them to the region’s memtable. - Buffering: Because the region uses
memtable.type = "time_series", aTimeSeriesMemtableinstance stores each column in contiguous vectors and updates thetime_series_memtablemetric. - Flush Trigger: When the memtable exceeds its size threshold or the
CHECK_REGION_INTERVALtimer fires (defined insrc/mito2/src/worker.rs, line 94), the worker invokes theFlushSchedulerfromsrc/mito2/src/flush.rs. - Persistence: The flush writes the memtable contents to a new SST file, updates the region manifest, and clears the buffer—all without interrupting the single-threaded event loop.
Read Path: Direct Scans Without Worker Coordination
Reads bypass the region workers entirely to avoid blocking ingestion. The Engine::read implementation (in src/mito2/src/engine.rs) opens the target region and performs a parallel scan across on-disk SST files and the current TimeSeriesMemtable (if active). Because the memtable is column-oriented, time-range filters and aggregations execute with minimal CPU overhead and optimal cache usage.
Background Maintenance: Flush, Compaction, and GC
The same worker thread that handles writes also schedules background maintenance, ensuring strict ordering between mutations and housekeeping:
- FlushScheduler (
src/mito2/src/flush.rs): Converts full memtables to SST files. - CompactionScheduler (
src/mito2/src/compaction.rs): Merges multiple SST files into larger, sorted files to improve read efficiency. - GcLimiter (
src/mito2/src/gc.rs): Removes obsolete SST files after compaction via theLocalGcWorker.
Because these operations run on the worker thread, they cannot race with ongoing writes to the same region.
Summary
- Region workers provide a single-threaded execution context per region, eliminating lock contention on metadata and memtables.
- TimeSeriesMemtables store data in columnar vectors optimized for time-series patterns, configured via
memtable.type = 'time_series'. - The write path serializes requests through
handle_write_requests, buffering in memtables until a flush is triggered. - Reads bypass workers entirely, scanning SST files and memtables directly for low-latency queries.
- Flush, compaction, and garbage collection are coordinated by the worker thread to maintain consistency.
Frequently Asked Questions
What is the difference between region workers and generic thread pools in Mito2?
Region workers are specialized, long-lived Tokio tasks that own specific regions for their entire lifetime. Unlike generic thread pools that dispatch tasks to any available thread, a region worker guarantees that all writes, flushes, and compactions for its assigned regions execute on the same thread, preserving order and avoiding locks.
How does TimeSeriesMemtable optimize storage for time-series workloads?
The TimeSeriesMemtable stores each column as a separate contiguous vector rather than as rows, which aligns with time-series access patterns where queries typically scan ranges of timestamps across specific metrics. This columnar layout improves CPU cache hit rates and allows vectorized compression before data is written to SST files.
When does Mito2 trigger a flush from memtable to SST?
A flush occurs when the active memtable reaches the configured size limit or when the periodic CHECK_REGION_INTERVAL timer expires, whichever comes first. The worker thread evaluates these conditions in its main loop and invokes the FlushScheduler to write the immutable memtable to a new SST file.
Can read operations block write ingestion in the Mito2 storage engine?
No. Reads operate on a snapshot of the region’s SST files and memtables without acquiring locks or sending requests to the region worker. This design ensures that heavy analytical queries cannot stall the single-threaded write path, maintaining consistent ingestion throughput.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →