wedb_embed : Redis-Compatible Embedded LSM-Tree Disk Database Engine
Embedded database engine providing Redis-compatible data structures and APIs, built on the fjall LSM-Tree storage engine.
-
Disk I/O Throughput: As a disk database, WeDb performance scales directly with underlying storage I/O capability. Achieves ~40x overall speedup over Redis on Apple M2 (NVMe SSD), and ~20x overall speedup on GitHub Actions (Linux cloud virtual disks). The performance gap is primarily driven by differences in underlying disk I/O throughput and latency.
-
Bounded Memory Footprint: Resident memory is bounded by
cache_size(default 512MB) andmax_memtable_size, independent of total disk data volume.
- Why an Embedded Redis Engine
- Quickstart
- Performance & Resource Comparison
- Storage Architecture & Encoding Design
- Runtime Architecture & Threading Model
- Tech Stack
Why an Embedded Redis Engine
Much like SQLite is to MySQL/PostgreSQL, wedb_embed is an embedded disk-based database engine for the Redis ecosystem.
In traditional relational database systems, MySQL employs a client-server architecture with an external daemon process communicating over network or Unix domain sockets; SQLite provides an in-process library that stores data directly into local disk files with zero daemon overhead.
In key-value and structured data domains, traditional Redis relies on an external server daemon with full in-memory RAM residency. In standalone applications, edge computing, CLI tools, and microservices, this architecture incurs distinct systemic bottlenecks:
-
IPC and Protocol Serialization Overhead: Every read and write operation traverses socket buffers, triggers OS context switches, and requires RESP protocol encoding and decoding. Even on localhost, round-trip latency typically remains in the 20–50 microsecond range while consuming CPU cycles.
-
RAM Costs and Memory Limits: Redis keeps datasets and pointer structures resident in physical RAM. As dataset volume expands to tens of gigabytes, memory hardware costs escalate and remain bounded by host RAM capacity. Background AOF/RDB persistence can further increase memory usage via Copy-On-Write mechanisms.
-
Deployment and Operational Overhead: Managing external daemon processes requires process supervisors, port allocation, configuration syncing, and health monitoring.
wedb_embed embeds the storage engine directly into the application process:
-
In-Process Direct Invocation: Redis-compatible data operations execute directly via Rust function calls in memory, avoiding socket I/O, syscalls, and inter-process context switches. P95 latency for core commands is reduced to nanosecond and microsecond ranges.
-
LSM-Tree Disk Persistence & Bounded Memory Budget: Datasets persist on disk using LZ4 block compression. Memory consumption does not grow linearly with total dataset size, but is strictly bounded by LSM-Tree parameters:
cache_size(Default 512MB): Global shared SSTable Block Cache budget for caching hot data pages;max_memtable_size(64MB for data / 32MB for metadata): Active in-memory write buffer limit before flushing to immutable SSTables;with_kv_separation(4KB threshold): Large values are stored in separate append-only Blob files to reduce write amplification. In a 5GB structured dataset benchmark, Redis maintains 4814 MB RSS in RAM, whilewedb_embedholds resident memory (RSS) to 334 MB (a 93% reduction), and reduces physical disk footprint from Redis AOF's 7652 MB down to 1180 MB (an 85% savings) via block compression.
-
16 Redis-Compatible Data Models: Supports String, Hash (with field-level TTL), List, Set, ZSet, Bitmap, JSON, Bloom/Cuckoo Filters, TimeSeries, Geo, HyperLogLog, TDigest, SortedInt, Stream, Full-Text Search, and HNSW Vector Retrieval.
-
Multi-Tenant and Multi-DB Isolation: Supports up to $2^{64}$ isolated tenants and databases. Passing
Nonetonsordbautomatically allocates sequential numerical IDs and opens a new instance. -
Crash Consistency: Relies on Write-Ahead Logging (WAL) and cross-keyspace atomic write batches (
WriteBatch) to maintain data integrity across crashes.
Quickstart
Installation
Basic Usage & Multi-Tenant
use Result;
use ;
Click here for more examples (all 16 data structures and multi-tenant APIs)
Performance & Resource Comparison
Ubuntu CI (GitHub Actions Runner)
Hardware & Test Environment
CPU: AMD EPYC 7763 64-Core Processor (4 cores) Memory: 15.6 GB Disk: Azure Managed Virtual Disk (Cloud Standard SSD) OS: Ubuntu 24.04.4 LTS (Linux 6.17.0-1022-azure) Rust: 1.98.0 (88d9e12ae 2026-08-18) Redis: v8.10.1
Physical Footprint & Memory Benchmark (5GB Dataset Scale)
| Resource Metric | wedb_embed (Embedded LSM+LZ4) | Redis (v8.10.1 AOF Mode) | Resource Savings |
|---|---|---|---|
| Dataset Scale | 5,000,000 Structured Items | 5,000,000 Structured Items | All 14 Data Formats |
| Raw Uncompressed Payload | 4377 MB | 4377 MB | Structured Payload |
| Physical Disk Footprint | 1179 MB | 7531 MB | Saves 84% |
| Resident Memory (RSS) | 281 MB | 4800 MB | Saves 94% |
wedb_embed vs Redis Core Command Benchmark
| Command | wedb_embed P95 Latency | Redis P95 Latency | Speedup |
|---|---|---|---|
SET |
7.4 us | 28.6 us | 3.9x |
GET |
5.5 us | 20.8 us | 3.8x |
MSET |
54.8 us | 34.8 us | 0.6x |
MGET |
5.3 us | 25.9 us | 4.9x |
INCRBY |
1.2 us | 28.5 us | 23.6x |
DECRBY |
1.1 us | 28.4 us | 26.8x |
APPEND |
1.2 us | 28.3 us | 23.9x |
STRLEN |
0.28 us | 18.7 us | 66.3x |
GETDEL |
8.5 us | 58.0 us | 6.8x |
GETRANGE |
0.36 us | 19.7 us | 55.4x |
SETRANGE |
1.5 us | 29.6 us | 19.5x |
HSET |
3.0 us | 29.5 us | 9.9x |
HGET |
1.3 us | 19.7 us | 15.1x |
HMGET |
3.5 us | 28.2 us | 8.2x |
HEXISTS |
1.1 us | 20.5 us | 19.1x |
HLEN |
0.46 us | 18.9 us | 40.8x |
HDEL |
4.6 us | 24.6 us | 5.3x |
HGETALL |
3.3 us | 20.1 us | 6.0x |
HKEYS |
3.2 us | 18.9 us | 6.0x |
HVALS |
3.4 us | 19.2 us | 5.7x |
HINCRBY |
1.7 us | 29.5 us | 17.1x |
LPUSH |
2.2 us | 41.1 us | 18.9x |
RPUSH |
2.4 us | 28.6 us | 11.9x |
LPOP |
2.5 us | 58.5 us | 23.5x |
RPOP |
2.5 us | 29.0 us | 11.6x |
LLEN |
0.47 us | 37.1 us | 78.4x |
LRANGE |
3.9 us | 35.3 us | 9.1x |
LINDEX |
0.70 us | 36.6 us | 52.4x |
LSET |
1.3 us | 28.6 us | 22.8x |
LREM |
11.4 us | 60.9 us | 5.4x |
LTRIM |
1.1 us | 21.2 us | 18.7x |
SADD |
1.4 us | 23.9 us | 17.0x |
SREM |
4.0 us | 25.2 us | 6.3x |
SISMEMBER |
0.72 us | 20.5 us | 28.5x |
SCARD |
0.46 us | 19.0 us | 41.8x |
SMEMBERS |
3.4 us | 18.9 us | 5.6x |
SPOP |
8.1 us | 59.4 us | 7.3x |
SRANDMEMBER |
3.6 us | 18.6 us | 5.2x |
ZADD |
3.2 us | 29.8 us | 9.3x |
ZSCORE |
0.77 us | 20.9 us | 27.1x |
ZRANGE |
4.0 us | 19.5 us | 4.9x |
ZCARD |
0.50 us | 19.1 us | 38.5x |
ZCOUNT |
3.3 us | 18.9 us | 5.7x |
ZINCRBY |
3.3 us | 30.6 us | 9.2x |
ZRANK |
3.4 us | 19.4 us | 5.7x |
ZREVRANGE |
6.4 us | 18.9 us | 2.9x |
ZPOPMIN |
7.1 us | 61.4 us | 8.6x |
ZREM |
4.5 us | 23.8 us | 5.3x |
SETBIT |
12.2 us | 45.5 us | 3.7x |
GETBIT |
0.45 us | 36.8 us | 82.6x |
BITCOUNT |
0.36 us | 27.6 us | 77.4x |
BITPOS |
0.50 us | 28.5 us | 57.0x |
PFADD |
3.0 us | 29.5 us | 9.8x |
PFCOUNT |
8.1 us | 36.8 us | 4.6x |
GEOADD |
3.1 us | 31.8 us | 10.2x |
GEODIST |
1.5 us | 19.3 us | 12.6x |
GEOPOS |
0.72 us | 18.9 us | 26.3x |
GEOHASH |
0.77 us | 19.4 us | 25.0x |
XADD |
2.3 us | 30.4 us | 13.2x |
XLEN |
0.59 us | 18.6 us | 31.7x |
XRANGE |
4.0 us | 31.5 us | 7.8x |
XREAD |
4.0 us | 32.6 us | 8.2x |
XDEL |
3.9 us | 61.4 us | 15.6x |
DEL |
3.1 us | 21.3 us | 6.9x |
EXISTS |
0.24 us | 19.1 us | 78.8x |
EXPIRE |
0.87 us | 30.0 us | 34.6x |
TTL |
0.30 us | 20.8 us | 69.7x |
JSON.SET |
3.6 us | 39.6 us | 10.9x |
JSON.GET |
1.5 us | 36.9 us | 24.3x |
JSON.DEL |
9.1 us | 74.3 us | 8.2x |
JSON.NUMINCRBY |
3.8 us | 38.0 us | 10.0x |
JSON.ARRLEN |
1.3 us | 36.6 us | 27.2x |
JSON.TYPE |
1.4 us | 36.4 us | 25.6x |
BF.ADD |
32.8 us | 37.9 us | 1.2x |
BF.EXISTS |
0.62 us | 36.5 us | 58.5x |
BF.INFO |
0.37 us | 36.5 us | 98.8x |
CF.ADD |
4.3 us | 37.5 us | 8.8x |
CF.EXISTS |
0.80 us | 18.8 us | 23.5x |
CF.DEL |
10.9 us | 37.9 us | 3.5x |
TDIGEST.ADD |
2.6 us | 18.5 us | 7.1x |
TDIGEST.QUANTILE |
1.1 us | 18.8 us | 17.6x |
TDIGEST.BYRANK |
1.2 us | 18.8 us | 16.1x |
TDIGEST.CDF |
1.3 us | 18.6 us | 14.7x |
TS.ADD |
13.5 us | 19.0 us | 1.4x |
TS.GET |
1.6 us | 18.8 us | 11.7x |
TS.RANGE |
21.2 us | 18.8 us | 0.9x |
TS.INCRBY |
10.7 us | 19.1 us | 1.8x |
FT.SEARCH |
28.4 us | 19.7 us | 0.7x |
FT.TAG |
28.0 us | 18.8 us | 0.7x |
VECTOR.KNN |
4.3 us | 26.3 us | 6.1x |
Storage Architecture & Encoding Design
graph TD
Client["Application Code (Rust API)"] --> WeDb["WeDb Database Engine"]
WeDb --> NS["Namespace Tenant Handle<br/>(Zero-Heap Struct)"]
NS --> DB["Db Database Handle<br/>(Scope Isolation)"]
subgraph KeyComposer["Key Composer & Compact Encoding"]
Tag["1-Byte Fast Tag (#[repr(u8)] KeyTag)"]
OPPV["OPPV Order-Preserving Varint (1~9 B)"]
SmallKey["SmallKey 64B Stack Buffer (Zero-Heap)"]
Subkey["SubkeyComposer Prefix Reuse"]
end
subgraph Engine["Storage Engine & Transaction (LSM-Tree Core)"]
Batch["DbBatch Atomic Batch (Cross-Partition WAL)"]
Catalog["Catalog Metadata & $2^{64}$ Tenant Map"]
Blob["KV Separation Engine (Large Values >= 4KB)"]
end
subgraph Storage["Fjall LSM-Tree Dual-Partition Storage"]
DataKS["data Partition<br/>(String values, subkeys & Blob refs | 8KB Block | LZ4)"]
MetaKS["meta Partition<br/>(Metadata, Versioning & Tenant Catalog | 4KB Block | In-Memory Hash Index)"]
end
DB --> KeyComposer
KeyComposer --> Engine
Engine --> DataKS
Engine --> MetaKS
Engine --> Blob
Dual-Partition Physical Storage & Prefix Encoding
-
Dual-Partition Architecture (
data/meta):- Data Partition (
data): Stores String raw values, composite structure subkeys, and large Value Blob references. Configured with 8KB block size, LZ4 compression, and large-value KV separation (large values persist in append-only Blob files to reduce write amplification). - Metadata Partition (
meta): Stores composite structure metadata (KeyMeta), version counters, and Catalog tenant directory. Configured with 4KB block size and in-memory hash indexing to ensure sub-microsecond point lookups.
- Data Partition (
-
1-Byte Fast Tag (
KeyTag): Metadata and subkey prefixes use#[repr(u8)] KeyTagencoding (e.g.\x01[key]), avoiding string tag overhead. -
Scope Prefix & Multi-Tenant Isolation: Multi-tenant and multi-DB scopes are encoded by
KeyComposerinto compact\x00[oppv(ns_id)][oppv(db)]physical prefixes, providing collision-free isolation for up to $2^{64}$ tenants and databases.
Order-Preserving Prefix Varint (OPPV)
- Database and namespace numerical IDs are encoded using OPPV (Order-Preserving Prefix Varint):
- Values $0 \sim 127$ occupy only 1 byte, reducing storage compared to fixed 8-byte big-endian integers;
- Encoded byte order strictly matches numerical magnitude: $\forall a < b \implies \text{encode}(a) < \text{encode}(b)$, allowing direct lexicographical range scans.
Tenant Catalog & Cascading Deregistration
- Tenant namespaces and databases use numerical
u64identifiers. PassingNonetonsordbauto-allocates global sequential IDs. - Active databases are maintained persistently in the Catalog directory (
\x00\x71[oppv(ns_id)][oppv(db_id)]). - Cascading deletion (
rm()) purges keys, metadata, and updates catalog entries at database and tenant levels.
Memory-Efficient Streaming Iterators
-
WeDb::iter(&self, begin: u64) -> Namespaces: Stream iterates active tenant namespaces starting from the specifiedbeginoffset with $O(1)$ memory overhead. -
Namespace::iter(&self, begin: u64) -> Dbs: Stream parses active database IDs within the namespace directly from the Catalog directory.
Runtime Architecture & Threading Model
wedb_embed is engineered for thread-per-core asynchronous runtimes powered by Linux io_uring (such as compio),
taking full advantage of single-core execution, zero shared state, and CPU core pinning.
graph LR
subgraph CompioModel["compio Thread-per-Core Model"]
direction TB
C1["CPU Core 0 (Worker 0)<br/>Pinned to Physical Core"] --> S1["Stack Buffer SmallKey (64B)<br/>L1/L2 Cache Hit (Zero Invalidation)"]
C2["CPU Core 1 (Worker 1)<br/>Pinned to Physical Core"] --> S2["Stack Buffer SmallKey (64B)<br/>L1/L2 Cache Hit (Zero Invalidation)"]
S1 --> IO1["Direct Sync / io_uring<br/>No Work-Stealing | Zero Syscall Context Switch"]
S2 --> IO2["Direct Sync / io_uring<br/>No Work-Stealing | Zero Syscall Context Switch"]
end
subgraph TokioModel["Tokio Multi-Threaded Work-Stealing Model"]
direction TB
T1["Worker Thread A"] <-->|"Cross-Core Task Stealing<br/>L1/L2 Cache Thrashing | NUMA Migration"| T2["Worker Thread B"]
T1 --> ST["Heap-Allocated State Machine (Send + 'static)<br/>Mutex Contention | Breaks Stack Lifetimes"]
T2 --> SB["Blocking Pool Handoff<br/>Context Switches | Latency Amplification"]
end
Thread-per-Core Architecture Design
-
Stack Lifetimes & Zero-Heap Allocation: Key composition leverages
SmallKey64-byte stack buffers andSubkeyComposerprefix memory reuse. Under a thread-per-core model, execution stays within the CPU core's stack frame without calling global heap allocators (such asjemallocorglibc malloc), avoiding allocator lock contention. -
CPU Cache Line Locality: Worker threads are statically pinned to physical CPU cores. Hot data structures (LSM-Tree memtable index, Bloom filter bitsets, Catalog metadata cache) remain resident in L1/L2 caches, avoiding cache invalidation broadcasts across cores.
-
Lock-Free & Lightweight Concurrency: Namespace and catalog directories use
papaya::HashMaplock-free concurrent hash tables for wait-free reads; low-frequency metadata writes useparking_lotadaptive spinlocks that complete immediately in single-core contexts without kernel futex transitions. -
In-Process Synchronous Direct Calls: Storage engine APIs are synchronous direct calls. In a single-threaded
compioevent loop, microsecond and nanosecond lookups complete inline, coexisting smoothly with completion-basedio_uringasynchronous I/O without wrapping Futures into cross-thread state machines.
Pitfalls of Multi-Threaded Work-Stealing Runtimes
Using multi-threaded work-stealing and epoll runtimes (like tokio) introduces performance overhead:
-
Cross-Core Task Stealing and Cache Invalidation: Schedulers migrate tasks across cores. Resuming a Future on another core or NUMA node flushes L1/L2 data and instruction caches, increasing P99 tail latency.
-
Violation of Stack Lifetimes: The
Send + 'staticrequirement prevents stack-borrowed structures (&[u8]slices, stackSmallKey) from crossingawaitpoints, forcing heap allocation (Box/Arc/Vec<u8>). -
Event Loop Blocking & Thread Pool Handoff: Synchronous storage access on worker threads blocks the event loop; offloading tasks to blocking thread pools introduces context switches and queueing delays, increasing operation latencies.
-
Multi-Core Memory Bus Contention: Concurrent access across cores induces cache-line bouncing and memory bus lock contention, capping throughput.
Tech Stack
- Language: Rust Edition 2024
- Storage Engine:
fjallLSM-Tree storage engine - JSON Engine:
sonic-rsSIMD JSON parser - Non-Cryptographic Hash:
rapidhash - String & Memory:
hipstrcompact string representation - Concurrency & Sync:
parking_lot - Bitset & Algorithms:
roaring,memchr,crc32fast,fastrand - Timestamps:
coarsetime - Number Formatting:
zmij,itoa - Enum Derivation:
strum - Error Handling:
thiserror
wedb_embed : Redis 兼容的嵌入式 LSM-Tree 磁盘数据库引擎
嵌入式数据库引擎,提供 Redis 兼容数据结构与接口,基于 fjall LSM-Tree 存储引擎构建。
-
磁盘 I/O 影响: WeDb 为磁盘数据库,性能表现与底层存储硬件 I/O 吞吐紧密相关。 在 Apple M2(NVMe SSD)实测综合性能约为 Redis 的 40 倍; 在 GitHub Actions(Linux 云端虚拟磁盘)实测综合性能约为 Redis 的 20 倍。 两者的性能倍率差异主要源于底层磁盘硬件的 I/O 吞吐与读写延迟差异。
-
内存预算控制: 常驻内存由
cache_size(默认 512MB 块缓存)与max_memtable_size参数控制, 不随磁盘数据量线性膨胀。
为什么需要嵌入式 Redis 引擎
如同 SQLite 之于 MySQL/PostgreSQL,wedb_embed 是 Redis 生态的嵌入式磁盘数据库引擎。
在传统关系型数据库体系中,MySQL 采用独立服务端守护进程与网络套接字通信架构; SQLite 则以进程内嵌入式库直接将数据持久化于本地磁盘文件,无需独立守护进程与跨进程调用。
在键值与复合数据结构领域,传统 Redis 采用独立守护进程与物理内存常驻架构。 在单机部署、边缘计算、命令行工具与微服务场景中,该架构存在以下系统层面的瓶颈:
-
进程间通信与协议开销: 每次数据读写均需经过序列化、操作系统套接字缓冲区、进程上下文切换与 RESP 协议解析。 即便在本地主机通信,套接字往返延迟通常也在 20~50 微秒区间,并持续消耗 CPU 周期。
-
物理内存成本与容量边界: Redis 将业务数据与内部指针常驻于物理内存中。 当数据规模增长至数十 GB 时,内存硬件成本上升,且严格受限于单机物理内存容量。 开启 AOF 或 RDB 持久化时,写时复制(Copy-on-Write)机制可能导致内存翻倍。
-
部署与守护运维复杂度: 独立进程需要额外的进程守护、端口监听、配置同步与健康检查, 增加了软件交付与运维的维护负担。
wedb_embed 将存储引擎直接编译并运行在应用程序进程空间内:
-
进程内直接调用: 所有 Redis 兼容数据操作通过 Rust 函数直接调用, 避免了套接字 I/O、系统调用与跨进程上下文切换。 在同等硬件下,核心指令的 P95 延迟降低至纳秒与微秒级。
-
LSM-Tree 磁盘持久化与内存预算控制: 数据经过 LZ4 块压缩存储于本地磁盘文件。 常驻内存不随数据总量线性增长,由 LSM-Tree 参数严格控制:
cache_size(默认 512MB):全局共享的 SSTable 块缓存上限,用于缓存热点数据页;max_memtable_size(数据分区 64MB / 元数据分区 32MB):内存写缓冲上限,达到阈值后自动异步刷盘生成不可变 SSTable;with_kv_separation(大 Value 分离阈值 4KB):大对象直接写入独立 Blob 文件,降低写放大与内存占用。 在 5GB 结构化数据实测中,Redis 物理内存占用达 4814 MB(RSS),wedb_embed常驻内存为 334 MB(降低 93%),物理落盘体积由 Redis AOF 的 7652 MB 压缩至 1180 MB(减少 85%)。
-
16 种 Redis 兼容数据模型: 在底层键值引擎之上,支持 String、Hash(支持字段级 TTL)、List、Set、ZSet、Bitmap、JSON、 Bloom/Cuckoo 过滤器、TimeSeries、Geo、HyperLogLog、TDigest、SortedInt、Stream、全文检索与 HNSW 向量检索。
-
多租户与多库物理隔离: 原生支持 $2^{64}$ 个独立租户与数据库。 命名空间(
ns)与数据库(db)传入None时自动分配递增编号并创建新实例。 -
崩溃一致性保障: 基于预写日志 WAL 与跨分区原子批处理
WriteBatch,保障断电与异常退出场景下的数据完整性。
快速上手
添加依赖
基础用法与多租户
use Result;
use ;
点此查看完整示例代码(包含 16 种数据结构与多租户详细用法)
性能与资源实测对比
Ubuntu CI (GitHub Actions Runner)
硬件与测试环境
CPU: AMD EPYC 7763 64-Core Processor (4核) 内存: 15.6 GB 硬盘: Azure Managed Virtual Disk (Cloud Standard SSD) 系统: Ubuntu 24.04.4 LTS (Linux 6.17.0-1022-azure) Rust: 1.98.0 (88d9e12ae 2026-08-18) Redis: v8.10.1
真实物理落盘与内存占用实测 (5GB 数据规模)
| 资源维度 | wedb_embed (嵌入式 LSM+LZ4) | Redis (v8.10.1 AOF持久化) | 资源节省比例 |
|---|---|---|---|
| 测试数据规模 | 5,000,000 条全格式结构化数据 | 5,000,000 条全格式结构化数据 | 14 种数据格式等比实测 |
| 原始数据载荷 | 4377 MB | 4377 MB | 真实结构化载荷 |
| 实际物理落盘大小 | 1179 MB | 7531 MB | 节省 84% |
| 进程常驻内存 (RSS) | 281 MB | 4800 MB | 节省 94% |
wedb_embed vs Redis 核心指令性能对比
| 指令 | wedb_embed P95延迟 | Redis P95延迟 | 性能领先 |
|---|---|---|---|
SET |
7.4 us | 28.6 us | 3.9x |
GET |
5.5 us | 20.8 us | 3.8x |
MSET |
54.8 us | 34.8 us | 0.6x |
MGET |
5.3 us | 25.9 us | 4.9x |
INCRBY |
1.2 us | 28.5 us | 23.6x |
DECRBY |
1.1 us | 28.4 us | 26.8x |
APPEND |
1.2 us | 28.3 us | 23.9x |
STRLEN |
0.28 us | 18.7 us | 66.3x |
GETDEL |
8.5 us | 58.0 us | 6.8x |
GETRANGE |
0.36 us | 19.7 us | 55.4x |
SETRANGE |
1.5 us | 29.6 us | 19.5x |
HSET |
3.0 us | 29.5 us | 9.9x |
HGET |
1.3 us | 19.7 us | 15.1x |
HMGET |
3.5 us | 28.2 us | 8.2x |
HEXISTS |
1.1 us | 20.5 us | 19.1x |
HLEN |
0.46 us | 18.9 us | 40.8x |
HDEL |
4.6 us | 24.6 us | 5.3x |
HGETALL |
3.3 us | 20.1 us | 6.0x |
HKEYS |
3.2 us | 18.9 us | 6.0x |
HVALS |
3.4 us | 19.2 us | 5.7x |
HINCRBY |
1.7 us | 29.5 us | 17.1x |
LPUSH |
2.2 us | 41.1 us | 18.9x |
RPUSH |
2.4 us | 28.6 us | 11.9x |
LPOP |
2.5 us | 58.5 us | 23.5x |
RPOP |
2.5 us | 29.0 us | 11.6x |
LLEN |
0.47 us | 37.1 us | 78.4x |
LRANGE |
3.9 us | 35.3 us | 9.1x |
LINDEX |
0.70 us | 36.6 us | 52.4x |
LSET |
1.3 us | 28.6 us | 22.8x |
LREM |
11.4 us | 60.9 us | 5.4x |
LTRIM |
1.1 us | 21.2 us | 18.7x |
SADD |
1.4 us | 23.9 us | 17.0x |
SREM |
4.0 us | 25.2 us | 6.3x |
SISMEMBER |
0.72 us | 20.5 us | 28.5x |
SCARD |
0.46 us | 19.0 us | 41.8x |
SMEMBERS |
3.4 us | 18.9 us | 5.6x |
SPOP |
8.1 us | 59.4 us | 7.3x |
SRANDMEMBER |
3.6 us | 18.6 us | 5.2x |
ZADD |
3.2 us | 29.8 us | 9.3x |
ZSCORE |
0.77 us | 20.9 us | 27.1x |
ZRANGE |
4.0 us | 19.5 us | 4.9x |
ZCARD |
0.50 us | 19.1 us | 38.5x |
ZCOUNT |
3.3 us | 18.9 us | 5.7x |
ZINCRBY |
3.3 us | 30.6 us | 9.2x |
ZRANK |
3.4 us | 19.4 us | 5.7x |
ZREVRANGE |
6.4 us | 18.9 us | 2.9x |
ZPOPMIN |
7.1 us | 61.4 us | 8.6x |
ZREM |
4.5 us | 23.8 us | 5.3x |
SETBIT |
12.2 us | 45.5 us | 3.7x |
GETBIT |
0.45 us | 36.8 us | 82.6x |
BITCOUNT |
0.36 us | 27.6 us | 77.4x |
BITPOS |
0.50 us | 28.5 us | 57.0x |
PFADD |
3.0 us | 29.5 us | 9.8x |
PFCOUNT |
8.1 us | 36.8 us | 4.6x |
GEOADD |
3.1 us | 31.8 us | 10.2x |
GEODIST |
1.5 us | 19.3 us | 12.6x |
GEOPOS |
0.72 us | 18.9 us | 26.3x |
GEOHASH |
0.77 us | 19.4 us | 25.0x |
XADD |
2.3 us | 30.4 us | 13.2x |
XLEN |
0.59 us | 18.6 us | 31.7x |
XRANGE |
4.0 us | 31.5 us | 7.8x |
XREAD |
4.0 us | 32.6 us | 8.2x |
XDEL |
3.9 us | 61.4 us | 15.6x |
DEL |
3.1 us | 21.3 us | 6.9x |
EXISTS |
0.24 us | 19.1 us | 78.8x |
EXPIRE |
0.87 us | 30.0 us | 34.6x |
TTL |
0.30 us | 20.8 us | 69.7x |
JSON.SET |
3.6 us | 39.6 us | 10.9x |
JSON.GET |
1.5 us | 36.9 us | 24.3x |
JSON.DEL |
9.1 us | 74.3 us | 8.2x |
JSON.NUMINCRBY |
3.8 us | 38.0 us | 10.0x |
JSON.ARRLEN |
1.3 us | 36.6 us | 27.2x |
JSON.TYPE |
1.4 us | 36.4 us | 25.6x |
BF.ADD |
32.8 us | 37.9 us | 1.2x |
BF.EXISTS |
0.62 us | 36.5 us | 58.5x |
BF.INFO |
0.37 us | 36.5 us | 98.8x |
CF.ADD |
4.3 us | 37.5 us | 8.8x |
CF.EXISTS |
0.80 us | 18.8 us | 23.5x |
CF.DEL |
10.9 us | 37.9 us | 3.5x |
TDIGEST.ADD |
2.6 us | 18.5 us | 7.1x |
TDIGEST.QUANTILE |
1.1 us | 18.8 us | 17.6x |
TDIGEST.BYRANK |
1.2 us | 18.8 us | 16.1x |
TDIGEST.CDF |
1.3 us | 18.6 us | 14.7x |
TS.ADD |
13.5 us | 19.0 us | 1.4x |
TS.GET |
1.6 us | 18.8 us | 11.7x |
TS.RANGE |
21.2 us | 18.8 us | 0.9x |
TS.INCRBY |
10.7 us | 19.1 us | 1.8x |
FT.SEARCH |
28.4 us | 19.7 us | 0.7x |
FT.TAG |
28.0 us | 18.8 us | 0.7x |
VECTOR.KNN |
4.3 us | 26.3 us | 6.1x |
存储架构与编码设计
graph TD
Client["应用业务代码 (Rust API)"] --> WeDb["WeDb 数据库引擎"]
WeDb --> NS["Namespace 租户句柄<br/>(零堆分配结构体)"]
NS --> DB["Db 数据库句柄<br/>(作用域隔离)"]
subgraph KeyComposer["键编排与紧凑编码 (KeyComposer)"]
Tag["1 字节紧凑标签 (#[repr(u8)] KeyTag)"]
OPPV["OPPV 保序变长整型 (1~9 字节)"]
SmallKey["SmallKey 64B 栈上缓冲 (零堆分配)"]
Subkey["SubkeyComposer 前缀内存复用"]
end
subgraph Engine["存储引擎与事务层 (LSM-Tree Core)"]
Batch["DbBatch 原子批处理 (跨分区 WAL)"]
Catalog["Catalog 元数据目录与 $2^{64}$ 租户索引"]
Blob["KV 分离引擎 (大 Value Blob 存储 >= 4KB)"]
end
subgraph Storage["Fjall LSM-Tree 双分区存储引擎"]
DataKS["data 数据分区<br/>(String 数据、复合子键与 Blob 引用 | 8KB 块 | LZ4 压缩)"]
MetaKS["meta 元数据分区<br/>(结构元数据、版本号与租户 Catalog | 4KB 块 | 100% 内存哈希索引)"]
end
DB --> KeyComposer
KeyComposer --> Engine
Engine --> DataKS
Engine --> MetaKS
Engine --> Blob
双分区物理存储与前缀编排
-
双分区物理架构 (
data/meta):- 数据分区 (
data): 存储 String 原始值、复合结构子键与大 Value Blob 引用。 采用 8KB 块大小与 LZ4 块压缩,配置大对象 KV 分离(大 Value 写入独立 Blob 文件以降低写放大)。 - 元数据分区 (
meta): 存储复合结构元数据(KeyMeta)、版本计数器与 Catalog 租户目录。 采用 4KB 块大小与内存哈希索引,保障元数据点查的亚微秒级延迟。
- 数据分区 (
-
1 字节紧凑标签 (
KeyTag): 复合结构元数据与子键前缀采用#[repr(u8)] KeyTag编码(如\x01[key]), 避免字符串标签带来的存储与解析开销。 -
作用域前缀与多租户隔离: 多租户与多数据库统一由
KeyComposer编码为\x00[oppv(ns_id)][oppv(db)]物理前缀, 支持 $2^{64}$ 个租户与数据库的隔离存储。
保序变长整型编码
- 数据库编号与租户 ID 采用 OPPV(Order-Preserving Prefix Varint) 编码:
- 数值 $0 \sim 127$ 仅占用 1 字节,较固定 8 字节大端序降低空间占用;
- 编码后的字节序与原始数值大小顺序一致:$\forall a < b \implies \text{encode}(a) < \text{encode}(b)$,支持底层直接进行范围扫描。
租户目录编排与级联注销
- 命名空间与数据库采用
u64编号体系,ns或db传入None自动分配全局递增 ID。 - 通过 Catalog 目录(
\x00\x71[oppv(ns_id)][oppv(db_id)])持久化维护激活索引。 - 支持数据库级与租户级的级联删除与注销(
rm()),清理数据并更新 Catalog 目录索引。
内存友好流式迭代
-
WeDb::iter(&self, begin: u64) -> Namespaces: 基于 Catalog 前缀流式扫描已激活的租户命名空间列表, 支持从指定begin起始偏移开始扫描,辅助内存复杂度为 $O(1)$。 -
Namespace::iter(&self, begin: u64) -> Dbs: 基于 Catalog 前缀流式解析该租户下所有已激活的数据库编号。
运行时生态与线程模型设计
wedb_embed 针对基于 Linux io_uring 的 Thread-per-Core(单线程单核心)异步运行时(例如 compio)进行协同设计,
利用单核独占、无共享状态与绑核特性发挥硬件缓存局部性。
graph LR
subgraph CompioModel["compio 线程模型 (Thread-per-Core)"]
direction TB
C1["CPU 核心 0 (工作线程 0)<br/>绑定物理核心"] --> S1["栈缓冲 SmallKey (64B)<br/>L1/L2 缓存命中"]
C2["CPU 核心 1 (工作线程 1)<br/>绑定物理核心"] --> S2["栈缓冲 SmallKey (64B)<br/>L1/L2 缓存命中"]
S1 --> IO1["同步调用 / io_uring<br/>无工作窃取 | 无上下文切换"]
S2 --> IO2["同步调用 / io_uring<br/>无工作窃取 | 无上下文切换"]
end
subgraph TokioModel["Tokio 工作窃取模型 (Work-Stealing)"]
direction TB
T1["工作线程 A"] <-->|"跨核任务窃取<br/>L1/L2 缓存失效 | NUMA 迁移"| T2["工作线程 B"]
T1 --> ST["堆分配状态机 (Send + 'static)<br/>互斥锁竞争 | 破坏栈生命周期"]
T2 --> SB["阻塞线程池切换<br/>线程上下文切换 | 延迟放大"]
end
一线程一核心架构设计
-
栈上生命周期与零堆分配: 物理键构建采用
SmallKey64 字节栈缓冲与SubkeyComposer前缀复用。 在单线程单核心模型下,执行上下文保持在当前 CPU 核心的栈帧内, 无需向全局堆分配器(如jemalloc或glibc malloc)申请内存,避免了多线程堆分配锁争用。 -
CPU 缓存行局部性: 工作线程与物理 CPU 核心静态绑定,任务执行不发生跨核迁移。 热点数据结构(LSM-Tree 内存表索引、布隆过滤器位图、Catalog 元数据缓存)常驻于当前 CPU 的 L1/L2 缓存, 避免多核缓存一致性协议广播无效化消息导致的缓存行失效。
-
无锁与轻量并发元数据: 命名空间与租户目录采用
papaya::HashMap无锁并发哈希表,读操作无等待; 低频的元数据写操作采用parking_lot自适应自旋锁,在单核独占环境下自旋即完成, 不触发内核态 Futex 上下文挂起与跨核唤醒。 -
同步内嵌调用: 存储引擎 API 均为同步内存与磁盘直接调用。 在单线程事件循环中,微秒级与纳秒级查找就地执行, 与底层
io_uring异步 I/O 配合,避免将 Future 包装为跨线程状态机。
传统多线程工作窃取运行时问题分析
在基于多线程工作窃取与 epoll 的通用异步运行时中,存在以下性能与调度开销:
-
跨核心任务窃取导致缓存失效: 调度器在工作线程空闲时跨核窃取任务。 同一请求的 Future 在
await恢复后可能被调度到其他 CPU 核心或跨 NUMA 节点, 导致 L1/L2 缓存失效,增加访存延迟与 P99 尾部延迟。 -
破坏栈生命周期约束: 通用异步任务通常要求满足
Send + 'static约束, 栈上分配的短期借用结构(如&[u8]切片、栈上SmallKey)无法跨await点存活, 需重新分配至堆内存(Box/Arc/Vec<u8>),增加了堆分配开销。 -
事件循环阻塞与线程池切换: 在单线程事件循环中直接执行磁盘读取或耗时查找可能阻塞事件循环; 若转发至阻塞线程池,则引入线程上下文切换、跨线程通道传递与调度排队, 使微秒级操作延迟增加。
-
多核心内存总线争用: 多线程并发访问共享实例时,跨核心原子操作与锁争用会导致内存总线锁争用, 限制高并发下的吞吐扩展能力。
技术栈
- 开发语言:Rust Edition 2024
- 存储引擎:
fjallLSM-Tree 存储引擎 - JSON 引擎:
sonic-rsSIMD 指令解析 - 非加密哈希:
rapidhash - 字符串与内存:
hipstr紧凑字符串 - 并发同步:
parking_lot - 集合与位运算:
roaring、memchr、crc32fast、fastrand - 时间戳处理:
coarsetime - 数值与浮点序列化:
zmij、itoa - 枚举派生:
strum - 错误处理:
thiserror