Rust資料庫內核實戰:從零構建LSM-Tree儲存引擎的6個核心模組

编程语言

資料庫內核的四大痛點,LSM-Tree如何破局

B+Tree寫放大嚴重——每次隨機寫都要刷盤整頁;LSM-Tree概念複雜——MemTable、SSTable、Compaction層層嵌套讓人頭大;Compaction策略選擇難——Leveled、Tiered、FIFO各有取捨;WAL實現困難——順序寫保證持久性但崩潰恢復邏輯繁瑣。2026年,Rust + LSM-Tree + tokio的組合給出了資料庫儲存引擎的最佳實踐:跳表MemTable零鎖並發、SSTable有序刷盤、WAL崩潰恢復、分層Compaction空間回收——寫效能比B+Tree提升10倍,讀放大可控

本文將從6個核心模組出發,帶你完成MemTable→SSTable→WAL→Compaction→Bloom Filter→Block Cache的完整實戰。

核心收穫

  • 掌握基於crossbeam跳表的MemTable並發讀寫實現
  • 理解SSTable磁碟格式設計與Block索引機制
  • 實現WAL預寫日誌保證崩潰恢復一致性
  • 應用Leveled Compaction策略回收空間與降低讀放大
  • 構建Bloom Filter與Block Cache加速點查路徑

目錄

  1. 核心概念速覽
  2. 問題分析:5大挑戰
  3. 模組1:MemTable記憶體表實現
  4. 模組2:SSTable磁碟持久化
  5. 模組3:WAL預寫日誌
  6. 模組4:Compaction合併策略
  7. 模組5:Bloom Filter布隆過濾器
  8. 模組6:Block Cache快取層
  9. 避坑指南:5個常見陷阱
  10. 報錯排查:10個常見錯誤
  11. 進階優化技巧
  12. 對比分析
  13. 總結展望
  14. 線上工具推薦

核心概念速覽

概念 說明
LSM-Tree Log-Structured Merge-Tree,追加寫有序結構,寫效能遠超B+Tree
MemTable 記憶體中的有序鍵值表,通常用跳表實現,寫滿後凍結刷盤
SSTable Sorted String Table,磁碟上的有序不可變檔案,按Block組織
Compaction 合併多個SSTable消除重複與刪除標記,降低讀放大
WAL Write-Ahead Log,預寫日誌保證崩潰後資料不遺失
Bloom Filter 機率型資料結構,快速判斷Key是否可能在SSTable中
Block Cache 快取SSTable的Data Block,減少磁碟IO
Sorted Run Compaction後的一組不重疊有序SSTable
Tombstone 刪除標記,Compaction時才真正清除

架構總覽

┌──────────────────────────────────────────────────────┐
│                  LSM-Tree Storage Engine              │
│  ┌────────────┐ ┌────────────┐ ┌──────────────────┐ │
│  │   Write    │ │   Read     │ │   Compaction     │ │
│  │   Path     │ │   Path     │ │   Scheduler      │ │
│  └─────┬──────┘ └─────┬──────┘ └────────┬─────────┘ │
│        │              │                 │            │
│  ┌─────▼──────────────▼─────────────────▼─────────┐ │
│  │  Active MemTable (SkipList)  │  WAL (Append)   │ │
│  └──────────────┬──────────────────────────────────┘ │
│                 │ freeze & flush                      │
│  ┌──────────────▼──────────────────────────────────┐ │
│  │  L0: [SST-0] [SST-1] [SST-2]  (允許重疊)       │ │
│  │  L1: [SST-3] [SST-4]          (Sorted Run)     │ │
│  │  L2: [SST-5] [SST-6] [SST-7] (Sorted Run)     │ │
│  │  L3: [SST-8] ... [SST-15]     (Sorted Run)     │ │
│  └─────────────────────────────────────────────────┘ │
│  ┌──────────────┐ ┌──────────────┐                   │
│  │Bloom Filter  │ │ Block Cache  │                   │
│  └──────────────┘ └──────────────┘                   │
└──────────────────────────────────────────────────────┘

問題分析:5大挑戰

挑戰 痛點描述 LSM方案
寫放大與讀放大 B+Tree隨機寫放大10-30倍,LSM讀放大需多層查找 MemTable批次刷盤+Compaction合併降低寫放大,Bloom Filter+Cache降低讀放大
Compaction策略選擇 Leveled寫放大高、Tiered空間放大高、FIFO不適合持久化 混合策略:L0用Tiered快速合併,L1+用Leveled控制空間
並發控制 MemTable讀寫競爭、Compaction與讀操作衝突 crossbeam無鎖跳表+讀寫分離(Active/Immutable MemTable)
崩潰恢復 未刷盤資料遺失、WAL部分寫入 WAL原子寫入+checksum校驗+重啟回放
記憶體管理 MemTable佔用不可控、Block Cache淘汰策略 大小閾值觸發凍結+LRU Cache限制記憶體上限

模組1:MemTable記憶體表實現

MemTable是LSM-Tree寫入的第一站——所有寫操作先進入MemTable,寫滿後凍結為Immutable MemTable等待刷盤。crossbeam-skiplist提供無鎖並發跳表,是Rust生態中最適合MemTable的資料結構。

基於crossbeam跳表的MemTable

use crossbeam_skiplist::SkipMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

pub struct MemTable {
    table: Arc<SkipMap<Vec<u8>, Vec<u8>>>,
    approximate_size: AtomicUsize,
    capacity: usize,
}

impl MemTable {
    pub fn new(capacity: usize) -> Self {
        Self {
            table: Arc::new(SkipMap::new()),
            approximate_size: AtomicUsize::new(0),
            capacity,
        }
    }

    pub fn put(&self, key: Vec<u8>, value: Vec<u8>) {
        let entry_size = key.len() + value.len();
        self.table.insert(key, value);
        self.approximate_size.fetch_add(entry_size, Ordering::Relaxed);
    }

    pub fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
        self.table.get(key).map(|entry| entry.value().clone())
    }

    pub fn delete(&self, key: Vec<u8>) {
        self.table.insert(key, Vec::new());
    }

    pub fn is_full(&self) -> bool {
        self.approximate_size.load(Ordering::Relaxed) >= self.capacity
    }

    pub fn iter(&self) -> impl Iterator<Item = (Vec<u8>, Vec<u8>)> + '_ {
        self.table.iter().map(|entry| {
            (entry.key().clone(), entry.value().clone())
        })
    }

    pub fn len(&self) -> usize {
        self.table.len()
    }
}

MemTable凍結與切換

use tokio::sync::RwLock;

pub struct MemTableManager {
    active: Arc<RwLock<MemTable>>,
    immutable: Arc<RwLock<Option<MemTable>>>,
    capacity: usize,
}

impl MemTableManager {
    pub fn new(capacity: usize) -> Self {
        Self {
            active: Arc::new(RwLock::new(MemTable::new(capacity))),
            immutable: Arc::new(RwLock::new(None)),
            capacity,
        }
    }

    pub async fn put(&self, key: Vec<u8>, value: Vec<u8>) -> Result<(), String> {
        let active = self.active.read().await;
        if active.is_full() {
            drop(active);
            self.freeze_and_switch().await?;
            let active = self.active.read().await;
            active.put(key, value);
        } else {
            active.put(key, value);
        }
        Ok(())
    }

    pub async fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
        if let Some(val) = self.active.read().await.get(key) {
            return Some(val);
        }
        self.immutable.read().await.as_ref().and_then(|t| t.get(key))
    }

    async fn freeze_and_switch(&self) -> Result<(), String> {
        let mut active = self.active.write().await;
        let mut immutable = self.immutable.write().await;

        if immutable.is_some() {
            return Err("Immutable MemTable still flushing".into());
        }

        let frozen = std::mem::replace(&mut *active, MemTable::new(self.capacity));
        *immutable = Some(frozen);
        Ok(())
    }

    pub async fn take_immutable(&self) -> Option<MemTable> {
        self.immutable.write().await.take()
    }
}

模組2:SSTable磁碟持久化

SSTable是LSM-Tree的磁碟儲存單元——不可變、有序、按Block組織。每個SSTable由Data Block、Meta Block、Index Block和Footer組成,Index Block記錄每個Data Block的起始Key和偏移量。

SSTable格式設計

┌─────────────────────────────────────┐
│  Data Block 0  [key1, val1, ...]   │
│  Data Block 1  [keyN, valN, ...]   │
│  ...                                │
│  Meta Block (Bloom Filter)          │
│  Index Block [block0_offset, ...]   │
│  Footer [meta_offset, index_offset] │
└─────────────────────────────────────┘

SSTable Builder

use std::io::{BufWriter, Write};
use std::fs::File;
use crc32fast::Hasher;

const BLOCK_SIZE: usize = 4 * 1024;

pub struct SsTableBuilder {
    data_blocks: Vec<Vec<u8>>,
    current_block: Vec<u8>,
    index_entries: Vec<IndexEntry>,
    first_key_in_block: Option<Vec<u8>>,
    block_count: usize,
}

#[derive(Clone)]
pub struct IndexEntry {
    pub first_key: Vec<u8>,
    pub offset: u64,
    pub length: u32,
}

impl SsTableBuilder {
    pub fn new() -> Self {
        Self {
            data_blocks: vec![],
            current_block: vec![],
            index_entries: vec![],
            first_key_in_block: None,
            block_count: 0,
        }
    }

    pub fn add(&mut self, key: &[u8], value: &[u8]) {
        if self.first_key_in_block.is_none() {
            self.first_key_in_block = Some(key.to_vec());
        }

        let entry_len = 4 + key.len() + 4 + value.len();
        if self.current_block.len() + entry_len > BLOCK_SIZE && !self.current_block.is_empty() {
            self.finish_block();
        }

        if self.first_key_in_block.is_none() {
            self.first_key_in_block = Some(key.to_vec());
        }

        self.current_block.extend_from_slice(&(key.len() as u32).to_le_bytes());
        self.current_block.extend_from_slice(key);
        self.current_block.extend_from_slice(&(value.len() as u32).to_le_bytes());
        self.current_block.extend_from_slice(value);
    }

    fn finish_block(&mut self) {
        if self.current_block.is_empty() {
            return;
        }
        let offset = self.data_blocks.len() as u64 * BLOCK_SIZE as u64;
        let length = self.current_block.len() as u32;

        self.index_entries.push(IndexEntry {
            first_key: self.first_key_in_block.take().unwrap_or_default(),
            offset,
            length,
        });

        let mut block = std::mem::take(&mut self.current_block);
        block.resize(BLOCK_SIZE, 0);
        self.data_blocks.push(block);
        self.block_count += 1;
    }

    pub fn build(mut self, path: &std::path::Path) -> std::io::Result<()> {
        self.finish_block();

        let file = File::create(path)?;
        let mut writer = BufWriter::new(file);

        for block in &self.data_blocks {
            writer.write_all(block)?;
        }

        let index_offset = self.data_blocks.len() as u64 * BLOCK_SIZE as u64;
        for entry in &self.index_entries {
            writer.write_all(&(entry.first_key.len() as u32).to_le_bytes())?;
            writer.write_all(&entry.first_key)?;
            writer.write_all(&entry.offset.to_le_bytes())?;
            writer.write_all(&entry.length.to_le_bytes())?;
        }

        writer.write_all(&index_offset.to_le_bytes())?;
        writer.flush()?;
        Ok(())
    }
}

模組3:WAL預寫日誌

WAL(Write-Ahead Log)是LSM-Tree崩潰恢復的保障——每次寫操作先追加到WAL,再寫入MemTable。崩潰後重啟回放WAL即可恢復未刷盤的資料。

WAL實現

use std::io::{BufWriter, Write, BufReader, Read};
use std::fs::{File, OpenOptions};
use std::path::Path;
use crc32fast::Hasher;

const WAL_RECORD_HEADER_SIZE: usize = 4 + 4 + 4;

pub enum WalRecord {
    Put { key: Vec<u8>, value: Vec<u8> },
    Delete { key: Vec<u8> },
}

pub struct WalWriter {
    writer: BufWriter<File>,
}

impl WalWriter {
    pub fn create(path: &Path) -> std::io::Result<Self> {
        let file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(path)?;
        Ok(Self { writer: BufWriter::new(file) })
    }

    pub fn append(&mut self, record: &WalRecord) -> std::io::Result<()> {
        let mut payload = Vec::new();
        match record {
            WalRecord::Put { key, value } => {
                payload.push(0u8);
                payload.extend_from_slice(&(key.len() as u32).to_le_bytes());
                payload.extend_from_slice(key);
                payload.extend_from_slice(&(value.len() as u32).to_le_bytes());
                payload.extend_from_slice(value);
            }
            WalRecord::Delete { key } => {
                payload.push(1u8);
                payload.extend_from_slice(&(key.len() as u32).to_le_bytes());
                payload.extend_from_slice(key);
            }
        }

        let mut hasher = Hasher::new();
        hasher.update(&payload);
        let checksum = hasher.finalize();

        self.writer.write_all(&checksum.to_le_bytes())?;
        self.writer.write_all(&(payload.len() as u32).to_le_bytes())?;
        self.writer.write_all(&payload)?;
        self.writer.flush()?;
        Ok(())
    }
}

pub struct WalReplayer;

impl WalReplayer {
    pub fn replay(path: &Path) -> std::io::Result<Vec<WalRecord>> {
        let file = File::open(path)?;
        let mut reader = BufReader::new(file);
        let mut records = Vec::new();

        loop {
            let mut header = [0u8; WAL_RECORD_HEADER_SIZE];
            match reader.read_exact(&mut header) {
                Ok(()) => {}
                Err(_) => break,
            }

            let checksum = u32::from_le_bytes(header[0..4].try_into().unwrap());
            let len = u32::from_le_bytes(header[4..8].try_into().unwrap()) as usize;

            let mut payload = vec![0u8; len];
            reader.read_exact(&mut payload)?;

            let mut hasher = Hasher::new();
            hasher.update(&payload);
            if hasher.finalize() != checksum {
                tracing::warn!("WAL checksum mismatch, stopping replay");
                break;
            }

            let record = match payload[0] {
                0 => {
                    let key_len = u32::from_le_bytes(payload[1..5].try_into().unwrap()) as usize;
                    let key = payload[5..5 + key_len].to_vec();
                    let val_len = u32::from_le_bytes(
                        payload[5 + key_len..9 + key_len].try_into().unwrap()
                    ) as usize;
                    let value = payload[9 + key_len..9 + key_len + val_len].to_vec();
                    WalRecord::Put { key, value }
                }
                1 => {
                    let key_len = u32::from_le_bytes(payload[1..5].try_into().unwrap()) as usize;
                    let key = payload[5..5 + key_len].to_vec();
                    WalRecord::Delete { key }
                }
                _ => break,
            };
            records.push(record);
        }

        Ok(records)
    }
}

模組4:Compaction合併策略

Compaction是LSM-Tree空間回收的核心——合併多個SSTable消除重複Key和Tombstone,降低讀放大。Leveled Compaction保證每層只有一個Sorted Run,空間放大可控但寫放大較高。

Leveled Compaction實現

use std::collections::BTreeMap;
use std::path::PathBuf;

pub struct CompactionScheduler {
    base_dir: PathBuf,
    level_size_multiplier: usize,
    max_level: usize,
}

impl CompactionScheduler {
    pub fn new(base_dir: PathBuf) -> Self {
        Self {
            base_dir,
            level_size_multiplier: 10,
            max_level: 6,
        }
    }

    pub fn should_compact(&self, level: usize, sst_count: usize) -> bool {
        let max_count = self.level_capacity(level);
        sst_count > max_count
    }

    fn level_capacity(&self, level: usize) -> usize {
        if level == 0 { 4 } else { self.level_size_multiplier.pow(level as u32) }
    }

    pub fn pick_compaction(
        &self,
        level: usize,
        sstables: &[SsTableMeta],
    ) -> Option<CompactionTask> {
        if sstables.is_empty() {
            return None;
        }

        let input_ssts: Vec<SsTableMeta> = if level == 0 {
            sstables.to_vec()
        } else {
            vec![sstables[0].clone()]
        };

        let smallest_key = input_ssts.iter()
            .map(|s| s.smallest_key.clone())
            .min()?;
        let largest_key = input_ssts.iter()
            .map(|s| s.largest_key.clone())
            .max()?;

        Some(CompactionTask {
            level,
            input_ssts,
            smallest_key,
            largest_key,
            target_level: level + 1,
        })
    }

    pub fn execute_compaction(
        &self,
        task: &CompactionTask,
    ) -> std::io::Result<Vec<SsTableMeta>> {
        let mut merged: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();

        for sst in &task.input_ssts {
            let reader = SsTableReader::open(&sst.path)?;
            for (key, value) in reader.iter() {
                if value.is_empty() {
                    merged.remove(&key);
                } else {
                    merged.insert(key, value);
                }
            }
        }

        let mut new_ssts = Vec::new();
        let mut builder = SsTableBuilder::new();
        let mut count = 0;

        for (key, value) in merged {
            builder.add(&key, &value);
            count += 1;
            if count >= 1024 * 4 {
                let sst_id = format!("L{}-{:06}", task.target_level, new_ssts.len());
                let path = self.base_dir.join(&sst_id);
                builder.build(&path)?;
                new_ssts.push(SsTableMeta {
                    id: sst_id,
                    path,
                    smallest_key: Vec::new(),
                    largest_key: Vec::new(),
                    level: task.target_level,
                });
                builder = SsTableBuilder::new();
                count = 0;
            }
        }

        if count > 0 {
            let sst_id = format!("L{}-{:06}", task.target_level, new_ssts.len());
            let path = self.base_dir.join(&sst_id);
            builder.build(&path)?;
            new_ssts.push(SsTableMeta {
                id: sst_id,
                path,
                smallest_key: Vec::new(),
                largest_key: Vec::new(),
                level: task.target_level,
            });
        }

        Ok(new_ssts)
    }
}

#[derive(Clone)]
pub struct SsTableMeta {
    pub id: String,
    pub path: PathBuf,
    pub smallest_key: Vec<u8>,
    pub largest_key: Vec<u8>,
    pub level: usize,
}

pub struct CompactionTask {
    pub level: usize,
    pub input_ssts: Vec<SsTableMeta>,
    pub smallest_key: Vec<u8>,
    pub largest_key: Vec<u8>,
    pub target_level: usize,
}

pub struct SsTableReader;

impl SsTableReader {
    pub fn open(_path: &PathBuf) -> std::io::Result<Self> { Ok(Self) }
    pub fn iter(&self) -> Vec<(Vec<u8>, Vec<u8>)> { vec![] }
}

模組5:Bloom Filter布隆過濾器

Bloom Filter是LSM-Tree讀路徑的關鍵最佳化——在開啟SSTable之前快速判斷Key是否可能存在,避免無效磁碟IO。誤判率可控,空間開銷極小。

Bloom Filter實現

use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

pub struct BloomFilter {
    bitmap: Vec<u64>,
    num_bits: usize,
    num_hashes: usize,
}

impl BloomFilter {
    pub fn new(expected_items: usize, fp_rate: f64) -> Self {
        let num_bits = Self::optimal_num_bits(expected_items, fp_rate);
        let num_hashes = Self::optimal_num_hashes(num_bits, expected_items);
        let bitmap_len = (num_bits + 63) / 64;

        Self {
            bitmap: vec![0u64; bitmap_len],
            num_bits,
            num_hashes,
        }
    }

    pub fn insert(&mut self, key: &[u8]) {
        let hashes = self.hash_key(key);
        for i in 0..self.num_hashes {
            let bit_pos = hashes[i] % self.num_bits;
            let word_idx = bit_pos / 64;
            let bit_idx = bit_pos % 64;
            self.bitmap[word_idx] |= 1u64 << bit_idx;
        }
    }

    pub fn may_contain(&self, key: &[u8]) -> bool {
        let hashes = self.hash_key(key);
        for i in 0..self.num_hashes {
            let bit_pos = hashes[i] % self.num_bits;
            let word_idx = bit_pos / 64;
            let bit_idx = bit_pos % 64;
            if self.bitmap[word_idx] & (1u64 << bit_idx) == 0 {
                return false;
            }
        }
        true
    }

    fn hash_key(&self, key: &[u8]) -> Vec<usize> {
        let mut hasher1 = DefaultHasher::new();
        key.hash(&mut hasher1);
        let h1 = hasher1.finish() as usize;

        let mut hasher2 = DefaultHasher::new();
        key.hash(&mut hasher2);
        let h2 = hasher2.finish() as usize;

        (0..self.num_hashes)
            .map(|i| h1.wrapping_add(i.wrapping_mul(h2)))
            .collect()
    }

    fn optimal_num_bits(n: usize, p: f64) -> usize {
        let ln2 = std::f64::consts::LN_2;
        ((-(n as f64) * p.ln()) / (ln2 * ln2)).ceil() as usize
    }

    fn optimal_num_hashes(m: usize, n: usize) -> usize {
        ((m as f64 / n as f64) * std::f64::consts::LN_2).ceil() as usize
    }
}

模組6:Block Cache快取層

Block Cache快取熱點SSTable Data Block,減少磁碟IO。LRU淘汰策略在記憶體受限時自動驅逐冷資料。

LRU Block Cache實現

use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;

pub struct BlockCache {
    cache: Arc<RwLock<LruCache<Vec<u8>, Vec<u8>>>>,
}

struct LruCache<K, V> {
    entries: HashMap<K, V>,
    order: Vec<K>,
    capacity: usize,
}

impl<K: Clone + Eq + std::hash::Hash, V> LruCache<K, V> {
    fn new(capacity: usize) -> Self {
        Self {
            entries: HashMap::new(),
            order: Vec::new(),
            capacity,
        }
    }

    fn get(&mut self, key: &K) -> Option<&V> {
        if self.entries.contains_key(key) {
            self.order.retain(|k| k != key);
            self.order.push(key.clone());
            self.entries.get(key)
        } else {
            None
        }
    }

    fn put(&mut self, key: K, value: V) {
        if self.entries.contains_key(&key) {
            self.order.retain(|k| k != &key);
            self.order.push(key.clone());
            self.entries.insert(key, value);
            return;
        }

        if self.entries.len() >= self.capacity {
            if let Some(evicted) = self.order.first().cloned() {
                self.order.remove(0);
                self.entries.remove(&evicted);
            }
        }

        self.order.push(key.clone());
        self.entries.insert(key, value);
    }
}

impl BlockCache {
    pub fn new(capacity: usize) -> Self {
        Self {
            cache: Arc::new(RwLock::new(LruCache::new(capacity))),
        }
    }

    pub async fn get(&self, sst_id: &str, block_offset: u64) -> Option<Vec<u8>> {
        let key = format!("{}:{}", sst_id, block_offset).into_bytes();
        self.cache.write().await.get(&key).cloned()
    }

    pub async fn put(&self, sst_id: &str, block_offset: u64, data: Vec<u8>) {
        let key = format!("{}:{}", sst_id, block_offset).into_bytes();
        self.cache.write().await.put(key, data);
    }
}

避坑指南:5個常見陷阱

坑1:MemTable無限增長導致OOM

// ❌ 錯誤:不設容量上限
let memtable = MemTable::new(usize::MAX);

// ✅ 正確:設定合理容量,寫滿後凍結刷盤
let memtable = MemTable::new(64 * 1024 * 1024); // 64MB
if memtable.is_full() {
    manager.freeze_and_switch().await?;
    flush_to_sstable(immutable).await?;
}

坑2:WAL未fsync導致崩潰丟資料

// ❌ 錯誤:只flush不fsync
writer.flush()?;

// ✅ 正確:fsync確保資料落盤
writer.flush()?;
file.sync_all()?;

坑3:Compaction期間刪除SSTable導致讀失敗

// ❌ 錯誤:Compaction完成立即刪除舊SSTable
for sst in &old_sstables { std::fs::remove_file(&sst.path)?; }

// ✅ 正確:引用計數歸零後再刪除(或延遲刪除)
for sst in &old_sstables {
    if sst.ref_count.load(Ordering::SeqCst) == 0 {
        std::fs::remove_file(&sst.path)?;
    } else {
        pending_deletions.push(sst.clone());
    }
}

坑4:Bloom Filter誤判率設定過高

// ❌ 錯誤:誤判率0.1,10%的無效IO
let filter = BloomFilter::new(100000, 0.1);

// ✅ 正確:誤判率0.01,生產環境推薦
let filter = BloomFilter::new(100000, 0.01);

坑5:SSTable Block大小設定不合理

// ❌ 錯誤:Block太小,索引膨脹
const BLOCK_SIZE: usize = 256;

// ✅ 正確:4KB-64KB,兼顧壓縮率和索引大小
const BLOCK_SIZE: usize = 4 * 1024;

報錯排查:10個常見錯誤

序號 報錯資訊 原因 解決方法
1 Cannot allocate memory MemTable無容量限制導致OOM 設定MemTable容量上限,寫滿後凍結刷盤
2 WAL checksum mismatch WAL部分寫入或磁碟故障 截斷損壞記錄,回放有效部分
3 SSTable not found: L0-000001 Compaction刪除了正在讀取的SSTable 實現引用計數或延遲刪除機制
4 Compaction stuck: level overflow L0檔案數超過閾值未及時合併 調整Compaction觸發閾值或增加合併執行緒
5 Bloom filter false positive rate too high 預期元素數與實際不符 重新計算Bloom Filter參數,增大bitmap
6 Block cache hit rate below 10% 快取容量不足或存取模式不集中 增大Cache容量或調整LRU策略
7 crossbeam-skiplist: key already exists 重複插入相同Key 使用insert覆蓋而非insert_if_absent
8 IO error: Too many open files SSTable檔案控制代碼未釋放 實現檔案控制代碼池或限制並發開啟數
9 tokio: task panicked during Compaction Compaction合併時記憶體溢位 分批合併,限制單次Compaction記憶體使用
10 data corruption after crash recovery WAL與MemTable狀態不一致 確保WAL先於MemTable寫入,使用兩階段提交

進階優化技巧

1. 前綴壓縮(Prefix Compression)

SSTable Block內相鄰Key共享前綴,減少儲存空間。例如user:1001user:1002共享user:前綴,僅儲存差異部分。

2. 分區Compaction(Partitioned Compaction)

將大SSTable按Key Range分區,Compaction只合併重疊分區,避免全量重寫。RocksDB的Partitioned Index Filter即採用此策略。

3. 動態Level調整(Dynamic Level)

啟動時根據實際資料量動態調整每層大小倍數,避免空層浪費Compaction資源。LevelDB的dynamic_level_bytes選項即為此最佳化。

4. Write Stall流量控制

當L0檔案數過多時主動限制寫入速率,防止讀延遲飆升。設定level0_slowdown_writes_triggerlevel0_stop_writes_trigger兩級閾值。

5. 並行Compaction

多執行緒並行執行不同Level的Compaction任務,利用多核加速空間回收。tokio的spawn_blocking適合包裝CPU密集型的合併操作。


對比分析

維度 LSM-Tree B+Tree LSM-B+Tree Hybrid
寫效能 ⭐極高(順序寫) ⭐低(隨機寫+頁分裂) ⭐高(寫走LSM)
讀效能 ⭐中(需多級查找) ⭐高(單次樹查找) ⭐高(熱資料走Cache)
寫放大 ⭐中(Compaction開銷) ⭐高(頁重寫+日誌) ⭐中
空間放大 ⭐中(多版本+Tombstone) ⭐低(原地更新) ⭐低
崩潰恢復 ⭐快(WAL回放) ⭐慢(Redo/Undo Log) ⭐快
範圍查詢 ⭐好(SSTable有序) ⭐好(葉子鏈結串列) ⭐好
並發控制 ⭐簡單(追加寫) ⭐複雜(鎖粒度細) ⭐中
適用場景 寫密集型 讀密集型 混合負載

選型建議

  • LSM-Tree:寫密集、時序資料、日誌儲存(推薦首選)
  • B+Tree:讀密集、事務頻繁、點查為主
  • Hybrid:讀寫混合、對延遲敏感的OLTP場景

總結展望

本文從6個核心模組構建了完整的LSM-Tree儲存引擎:MemTable記憶體表→SSTable磁碟持久化→WAL預寫日誌→Compaction合併策略→Bloom Filter布隆過濾器→Block Cache快取層。Rust的所有權系統保證了記憶體安全,crossbeam無鎖跳表提供了高並發MemTable,tokio非同步執行時讓Compaction與讀寫並行不悖。

未來方向:列族(Column Family)支援多資料隔離、分散式Compaction跨節點並行、ZSTD壓縮降低儲存成本、持久化記憶體(PMEM)消除WAL瓶頸。LSM-Tree儲存引擎的本質不是「追加寫代替原地更新」,而是「用有序合併的確定性替代隨機寫的不可預測性」。


線上工具推薦

相關閱讀

外部參考


總結:Rust資料庫內核儲存引擎的6個核心模組構成了從記憶體到磁碟的完整資料鏈路:MemTable無鎖並發寫入→SSTable有序刷盤持久化→WAL崩潰恢復保障→Compaction空間回收與讀最佳化→Bloom Filter快速過濾→Block Cache熱點加速。Rust的所有權系統讓記憶體安全零成本,crossbeam和tokio讓並發與非同步自然融合。記住,LSM-Tree儲存引擎的本質不是「用追加寫替代原地更新」,而是「用有序合併的確定性替代隨機寫的不可預測性」。

本站提供瀏覽器本地工具,免註冊即可試用 →

#Rust数据库内核#存储引擎#LSM Tree#B+Tree#WAL#2026#编程语言