Обновить

Комментарии 13

Сделайте бенч

Running 1 benchmarks...
Benchmark lensdb-bench: RUNNING...
benchmarking Core Operations/set operation
time                 364.0 μs   (343.0 μs .. 397.6 μs)
                     0.863 R²   (0.766 R² .. 0.972 R²)
mean                 434.6 μs   (401.7 μs .. 483.6 μs)
std dev              143.0 μs   (99.53 μs .. 207.3 μs)
variance introduced by outliers: 98% (severely inflated)

benchmarking Core Operations/get operation
time                 232.2 μs   (221.4 μs .. 248.3 μs)
                     0.969 R²   (0.943 R² .. 0.991 R²)
mean                 249.4 μs   (239.2 μs .. 263.4 μs)
std dev              40.42 μs   (30.57 μs .. 52.04 μs)
variance introduced by outliers: 91% (severely inflated)

benchmarking Core Operations/delete operation
time                 519.6 μs   (492.0 μs .. 556.8 μs)
                     0.955 R²   (0.932 R² .. 0.978 R²)
mean                 645.1 μs   (601.4 μs .. 700.3 μs)
std dev              183.1 μs   (145.3 μs .. 227.1 μs)
variance introduced by outliers: 97% (severely inflated)

benchmarking Core Operations/exists operation
time                 2.379 μs   (2.371 μs .. 2.386 μs)
                     1.000 R²   (1.000 R² .. 1.000 R²)
mean                 2.392 μs   (2.378 μs .. 2.410 μs)
std dev              48.11 ns   (33.22 ns .. 67.71 ns)
variance introduced by outliers: 22% (moderately inflated)

benchmarking Storage Operations/storage statistics
time                 330.2 μs   (327.1 μs .. 333.1 μs)
                     0.999 R²   (0.998 R² .. 0.999 R²)
mean                 329.8 μs   (326.9 μs .. 334.6 μs)
std dev              12.01 μs   (8.662 μs .. 16.29 μs)
variance introduced by outliers: 31% (moderately inflated)

benchmarking Storage Operations/cleanup operations
time                 332.0 μs   (326.5 μs .. 339.4 μs)
                     0.998 R²   (0.996 R² .. 0.999 R²)
mean                 332.1 μs   (328.6 μs .. 336.2 μs)
std dev              13.21 μs   (10.30 μs .. 17.36 μs)
variance introduced by outliers: 35% (moderately inflated)

benchmarking Protocol Operations/message serialization
time                 546.5 ns   (471.6 ns .. 634.0 ns)
                     0.907 R²   (0.872 R² .. 0.971 R²)
mean                 608.5 ns   (531.9 ns .. 771.8 ns)
std dev              366.1 ns   (131.9 ns .. 595.8 ns)
variance introduced by outliers: 100% (severely inflated)

benchmarking Protocol Operations/message deserialization
time                 776.0 ns   (591.8 ns .. 1.026 μs)
                     0.641 R²   (0.543 R² .. 0.813 R²)
mean                 772.3 ns   (655.8 ns .. 957.5 ns)
std dev              471.1 ns   (315.4 ns .. 772.8 ns)
variance introduced by outliers: 100% (severely inflated)

benchmarking Protocol Operations/response serialization
time                 449.2 ns   (442.4 ns .. 457.3 ns)
                     0.997 R²   (0.995 R² .. 0.998 R²)
mean                 458.9 ns   (448.0 ns .. 478.2 ns)
std dev              49.68 ns   (27.01 ns .. 83.54 ns)
variance introduced by outliers: 91% (severely inflated)

benchmarking Concurrent Operations/concurrent sets
time                 2.178 ms   (2.128 ms .. 2.230 ms)
                     0.997 R²   (0.995 R² .. 0.998 R²)
mean                 2.185 ms   (2.161 ms .. 2.214 ms)
std dev              90.47 μs   (74.03 μs .. 116.4 μs)
variance introduced by outliers: 27% (moderately inflated)

benchmarking Concurrent Operations/concurrent gets
time                 748.2 μs   (725.9 μs .. 788.1 μs)
                     0.974 R²   (0.956 R² .. 0.991 R²)
mean                 764.4 μs   (741.2 μs .. 799.7 μs)
std dev              97.61 μs   (73.92 μs .. 126.4 μs)
variance introduced by outliers: 83% (severely inflated)

Benchmark lensdb-bench: FINISH


это при альфе версии так, в будущем я думаю еще ускорить и оптимизировать

В общем, не быстро: большой разброс и значительно медленнее хешмапы вроде dashmap (Rust). Причём, не похоже на время disk io.

Советы от LLM (ну, чем богаты...)

-- Вероятно, у тебя что-то вроде:
atomically $ do
  store <- readTVar globalMap
  let newStore = Map.insert key (serialize value) store  -- ❌ Копирование всей Map!
  writeTVar globalMap newStore

Проблема не в сериализации, а в:

  1. Иммутабельность + STM = копирование всей структуры при каждой записи (даже с ByteString)

  2. GC — видно по 98% outliers: каждая запись создаёт новые объекты, сборщик мусора "останавливает мир"

  3. Data.Map = дерево с логарифмической сложностью и аллокациями на каждый узел

Я бы наоборот, взял dashmap и потихоньку шаг за шагом прикручивать сетевой io и персистентность

Ну типа такого

use dashmap::DashMap;
use tokio::net::TcpListener;
use bytes::Bytes;

struct LensDB {
    store: DashMap<Bytes, Entry>,
    wal: WalWriter,  // добавишь потом
}

struct Entry {
    value: Bytes,
    expires_at: Option<Instant>,
}

impl LensDB {
    // Этап 0 — чистый DashMap
    fn get(&self, key: &[u8]) -> Option<Bytes> {
        self.store.get(key).map(|e| e.value.clone())
    }
    
    fn set(&self, key: Bytes, value: Bytes) {
        self.store.insert(key, Entry { value, expires_at: None });
    }
}

// Этап 1 — добавляем TCP
async fn serve(db: Arc<LensDB>, addr: &str) -> Result<()> {
    let listener = TcpListener::bind(addr).await?;
    
    loop {
        let (socket, _) = listener.accept().await?;
        let db = db.clone();
        
        tokio::spawn(async move {
            handle_connection(db, socket).await
        });
    }
}

А потом усложнял бы

use tokio::sync::mpsc;

struct WalWriter {
    tx: mpsc::Sender<WalEntry>,
}

impl LensDB {
    async fn set_durable(&self, key: Bytes, value: Bytes) {
        // 1. Сначала в память (быстро)
        self.store.insert(key.clone(), Entry { value: value.clone(), .. });
        
        // 2. Асинхронно в WAL (не блокирует)
        let _ = self.wal.tx.send(WalEntry::Set(key, value)).await;
    }
}

// Отдельный таск пишет на диск
async fn wal_flusher(mut rx: mpsc::Receiver<WalEntry>, file: File) {
    let mut buffer = Vec::with_capacity(4096);
    
    loop {
        // Собираем батч
        while let Ok(entry) = rx.try_recv() {
            entry.encode_into(&mut buffer);
        }
        
        if !buffer.is_empty() {
            file.write_all(&buffer).await?;
            // fsync по таймеру или по размеру
            buffer.clear();
        }
        
        tokio::time::sleep(Duration::from_millis(1)).await;
    }
}

Ожидаемые результаты на уровне Redis (100-200 kops/sec).

И затраты на сериализацию можно смотреть только если уже упёрлись в остальные ограничения.

P.S. код довольно кривой, только для объяснения базового алгоритма

Смотрели ли в сторону rocksdb? Интересно было бы сравнить

Тоже читал и пытался понять принципиальные отличия от RocksDB и прочих производных LevelDB.

ну, проект находится в стадии разработки, в будущем он будет в разы удобнее и лучше, особенно для масштабных проектов или где нужно сохранять оптимизацию = работоспособность

Привет! Интересный прокет.

Вопросы:

  • TTL для записей есть? как реализован, если есть? Мне надо избавляться от старых записей без лишних телодвижений.

  • Как настраивается соотношение "в памяти" к "на диске"? Если в кубере запускать, то не надо заходить за лимиты.

  1. пока что нет ttl-реализации, но в дальнейшем, думаю, добавлю

  2. контроль использования памяти/диска на уровне конфигурации есть частично (max_memory), но жёстких ограничений нет, это контролируется на уровне окружения (например, kubernetes)

Вы там memcached изобретаете что ли?

Нет. Персистентность тоже в дизайне упомянута

В традиционных NoSQL‑системах, таких как Redis, данные передаются в виде строк или сериализованных объектов

Не передаю́тся, а МОГУТ передаваться, что часто и делается, но это не является ограничением. Здесь например туда решили класть msgpack - бинарный формат сообщений, и сохранили 50% памяти.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации