mirror of
https://github.com/scylladb/scylladb.git
synced 2026-04-21 09:00:35 +00:00
Split the `log_record` to `log_record_header` type that has the record
metadata fields and the mutation as a separate field which is the actual
record data:
struct log_record {
log_record_header header;
canonical_mutation mut;
};
Both the header and mutation have variable serialized size. When a
record is serialized in a write_buffer, we first put a small
`record_header` that has the header size and data size, then the
serialized header and data follow. The `log_location` of a record points
to the beginning of the `record_header`, and the size includes the
`record_header`.
This allows us to read a record header without reading the data when
it's not needed and avoid deserializing it:
* on recovery, when scanning all segments, we read only the record
headers.
* on compaction, we read the record header first to determine if the
record is alive, if yes then we read the data.
Closes scylladb/scylladb#29457
220 lines
9.1 KiB
Markdown
220 lines
9.1 KiB
Markdown
# Logstor
|
|
|
|
## Introduction
|
|
|
|
Logstor is a log-structured storage engine for ScyllaDB optimized for key-value workloads. It provides an alternative storage backend for key-value tables - tables with a partition key only, with no clustering columns.
|
|
|
|
Unlike the traditional LSM-tree based storage, logstor uses a log-structured approach with in-memory indexing, making it particularly suitable for workloads with frequent overwrites and point lookups.
|
|
|
|
## Architecture
|
|
|
|
Logstor consists of several key components:
|
|
|
|
### Components
|
|
|
|
#### Primary Index
|
|
|
|
The primary index is entirely in memory and it maps a partition key to its location in the log segments. It consists of a B-tree per each table that is ordered token.
|
|
|
|
#### Segment Manager
|
|
|
|
The `segment_manager` handles the allocation and management of fixed-size segments (default 128KB). Segments are grouped into large files (default 32MB). Key responsibilities include:
|
|
|
|
- **Segment allocation**: Provides segments for writing new data
|
|
- **Space reclamation**: Tracks free space in each segment
|
|
- **Compaction**: Copies live data from sparse segments to reclaim space
|
|
- **Recovery**: Scans segments on startup to rebuild the index
|
|
- **Separator**: Rewrites segments that have records from different compaction groups into new segments that are separated by compaction group.
|
|
|
|
The data in the segments consists of records of type `log_record`. Each record contains the value for some key as a `canonical_mutation` and additional metadata.
|
|
|
|
The `segment_manager` receives new writes via a `write_buffer` and writes them sequentially to the active segment with 4k-block alignment.
|
|
|
|
#### Write Buffer
|
|
|
|
The `write_buffer` manages a buffer of log records and handles the serialization of the records including headers and alignment. It can be used to write multiple records to the buffer and then write the buffer to the segment manager.
|
|
|
|
The `buffered_writer` manages multiple write buffers for user writes, an active buffer and multiple flushing ones, to batch writes and manage backpressure.
|
|
|
|
### Data Flow
|
|
|
|
**Write Path:**
|
|
1. Application writes mutation to logstor
|
|
2. Mutation is converted to a log record
|
|
3. Record is written to write buffer
|
|
4. The buffer is switched and written to the active segment.
|
|
5. Index is updated with new record locations
|
|
6. Old record locations (for overwrites) are marked as free
|
|
|
|
**Read Path:**
|
|
1. Application requests data for a partition key
|
|
2. Index lookup returns record location
|
|
3. Segment manager reads record from disk
|
|
4. Record is deserialized into a mutation and returned
|
|
|
|
**Separator:**
|
|
1. When a record is written to the active segment, it is also written to its compaction group's separator buffer. The separator buffer holds a reference to the original segment.
|
|
2. The separator buffer is flushed when it's full, or requested to flush for other reason. It is written into a new segment in the compaction group, and it updates the location of the records from the original mixed segments to the new segments in the compaction group.
|
|
3. After the separator buffer is flushed and all records from the original segment are moved, it releases the reference of the segment. When there are no more reference to the segment it is freed.
|
|
|
|
**Compaction:**
|
|
1. The amount of live data is tracked for each segment in its segment_descriptor. The segment descriptors are stored in a histogram by live data.
|
|
2. A segment set from a single compaction group is submitted for compaction.
|
|
3. Compaction picks segments for compaction from the segment set. It chooses segments with the lowest utilization such that compacting them results in net gain of free segments.
|
|
4. It reads the segments, finding all live records, and writing them into a write buffer. When the buffer is full it is flushed into a new segment, and for each recording updating the index location to the new location.
|
|
5. After all live records are rewritten the old segments are freed.
|
|
|
|
## Usage
|
|
|
|
### Enabling Logstor
|
|
|
|
To use logstor, enable the experimental feature in the configuration:
|
|
|
|
```yaml
|
|
experimental_features:
|
|
- logstor
|
|
```
|
|
|
|
### Creating Tables
|
|
|
|
Tables using logstor must have no clustering columns, and created with the `storage_engine` property equals to 'logstor':
|
|
|
|
```cql
|
|
CREATE TABLE keyspace.user_profiles (
|
|
user_id uuid PRIMARY KEY,
|
|
name text,
|
|
email text,
|
|
metadata frozen<map<text, text>>
|
|
) WITH storage_engine = 'logstor';
|
|
```
|
|
|
|
### Basic Operations
|
|
|
|
**Insert/Update:**
|
|
|
|
```cql
|
|
INSERT INTO keyspace.table_name (pk, v) VALUES (1, 'value1');
|
|
INSERT INTO keyspace.table_name (pk, v) VALUES (2, 'value2');
|
|
|
|
-- Overwrite with new value
|
|
INSERT INTO keyspace.table_name (pk, v) VALUES (1, 'updated_value');
|
|
```
|
|
|
|
Currently, updates must write the full row. Updating individual columns is not yet supported. Each write replaces the entire partition.
|
|
|
|
**Select:**
|
|
|
|
```cql
|
|
SELECT * FROM keyspace.table_name WHERE pk = 1;
|
|
-- Returns: (1, 'updated_value')
|
|
|
|
SELECT pk, v FROM keyspace.table_name WHERE pk = 2;
|
|
-- Returns: (2, 'value2')
|
|
|
|
SELECT * FROM keyspace.table_name;
|
|
-- Returns: (1, 'updated_value'), (2, 'value2')
|
|
```
|
|
|
|
**Delete:**
|
|
|
|
```cql
|
|
DELETE FROM keyspace.table_name WHERE pk = 1;
|
|
```
|
|
|
|
## On-Disk Format
|
|
|
|
### Files
|
|
|
|
Segments are stored in large pre-allocated files on disk. Each file holds a fixed number of segments determined by the `file_size / segment_size` ratio. File names follow the pattern:
|
|
|
|
```
|
|
ls_{shard_id}-{file_id}-Data.db
|
|
```
|
|
|
|
Files are pre-formatted (zero-filled) before use.
|
|
|
|
### Segments
|
|
|
|
Each segment is a contiguous fixed-size region within a file (default 128KB). A segment is identified by a `log_segment_id` (a 32-bit integer index), which maps to a file and offset within that file.
|
|
|
|
A segment contains one or more **buffers** written sequentially. Each buffer is 4KB-block-aligned. A segment has one of two kinds:
|
|
|
|
- **mixed**: written by normal user writes; may contain records from multiple tables and compaction groups. Contains multiple buffers.
|
|
- **full**: written by compaction or the separator; contains records from a single table and token range. Contains exactly one buffer.
|
|
|
|
### Buffer Layout
|
|
|
|
A buffer within a segment has the following layout:
|
|
|
|
```
|
|
buffer_header
|
|
(segment_header)? -- present only when kind == full
|
|
record_1
|
|
record_2
|
|
...
|
|
record_n
|
|
zero_padding -- to align the entire buffer to block_alignment (4096 bytes)
|
|
```
|
|
|
|
buffer_header, segment_header, and records are aligned by `record_alignment` (8 bytes).
|
|
|
|
#### Buffer Header
|
|
|
|
A serialized form of `write_buffer::buffer_header`.
|
|
|
|
| Offset | Size | Field | Description |
|
|
|--------|------|-------------|-------------|
|
|
| 0 | 4 | `magic` | `0x4C475342` ("LGSB"). Used to detect valid buffers during recovery. |
|
|
| 4 | 4 | `data_size` | Size in bytes of all record data following the header(s). |
|
|
| 8 | 2 | `seg_gen` | Segment generation number. Incremented each time the segment slot is reused. Used during recovery to discard stale data. |
|
|
| 10 | 1 | `kind` | Segment kind: `0` = mixed, `1` = full. |
|
|
| 11 | 1 | `version` | Version of the write buffer format. |
|
|
| 12 | 4 | `crc` | CRC32 of all other fields in the buffer header. Used for validating the header. |
|
|
|
|
#### Segment Header (full segments only)
|
|
|
|
Immediately follows the buffer header when `kind == full`.
|
|
|
|
A serialized form of `write_buffer::segment_header`.
|
|
|
|
| Offset | Size | Field | Description |
|
|
|--------|------|---------------|-------------|
|
|
| 16 | 16 | `table` | UUID of the table this segment belongs to. |
|
|
| 32 | 8 | `first_token` | Minimum token of all records in the segment (raw token number). |
|
|
| 40 | 8 | `last_token` | Maximum token of all records in the segment (raw token number). |
|
|
|
|
#### Records
|
|
|
|
Each record within the buffer is structured as:
|
|
|
|
```
|
|
record_header (8 bytes)
|
|
log_record_header (header_size bytes)
|
|
canonical_mutation (data_size bytes)
|
|
zero_padding -- to align to record_alignment (8 bytes)
|
|
```
|
|
|
|
**Record Header** (`write_buffer::record_header`):
|
|
|
|
| Offset | Size | Field | Description |
|
|
|--------|------|---------------|-------------|
|
|
| 0 | 4 | `header_size` | Size in bytes of the serialized `log_record_header` that follows. |
|
|
| 4 | 4 | `data_size` | Size in bytes of the serialized `canonical_mutation` that follows `log_record_header`. |
|
|
|
|
**Log Record Header** (`log_record_header`):
|
|
|
|
The `header_size` bytes immediately following the record header are the IDL-serialized form of `log_record_header`, which contains:
|
|
- `key`: the partition key (`primary_index_key`), including a `decorated_key` with a token and partition key bytes.
|
|
- `generation`: a 16-bit write generation number, used during recovery to resolve conflicts when the same key appears in multiple segments.
|
|
- `table`: UUID of the table this record belongs to.
|
|
|
|
**Mutation Data**:
|
|
|
|
The `data_size` bytes immediately following the log record header are the IDL-serialized `canonical_mutation`, which holds the full partition value.
|
|
|
|
**Record Location** (`log_location`):
|
|
|
|
The `log_location` stored in the index for each record points to the start of the `record_header`:
|
|
- `offset`: byte offset from the start of the segment to the `record_header`.
|
|
- `size`: total size including `record_header` + `log_record_header` + `canonical_mutation`
|