1. 引言与架构总览
RocksDB 作为 Meta (Facebook) 开源的高性能 KV 存储引擎,基于 LSM-Tree (Log-Structured Merge Tree) 架构,被广泛应用于各类数据库(如 CockroachDB, TiKV)和流计算引擎(如 Flink)的底层存储。然而,在从 x86 迁移到 ARM64(鲲鹏)架构的过程中,原生的 RocksDB 往往面临着'水土不服'的问题:
- 指令集差异:x86 依赖 SSE4.2/AVX 指令集加速 CRC32 和内存操作,而 ARM64 需要特定的 NEON 和 Crypto 扩展。
- 内存模型差异:ARM64 采用弱内存模型(Weak Memory Model),对锁和原子操作的实现要求更为严苛,稍有不慎就会导致性能大幅回退。
- 流水线特性:鲲鹏 920 处理器拥有独特的流水线和缓存层级,通用的代码无法充分利用其 ILP(指令级并行)能力。
BoostKit for RocksDB 正是为了解决这些问题而生。通过深入源码层面的剖析,我们来看看 BoostKit 是如何通过指令级优化、硬件加速和算法改良,将 RocksDB 在鲲鹏上的性能提升到极致的。
2. 算力释放:ARM64 指令集与硬件加速
在存储系统中,计算密集型任务(如校验和压缩)往往是 CPU 的瓶颈所在。本章深入探讨如何利用 ARM64 指令集和鲲鹏硬件加速引擎来释放算力。
2.1 CRC32C 的指令级流水线重构
在 RocksDB 中,CRC32C(循环冗余校验)无处不在。无论是 WAL (Write Ahead Log) 的写入,还是 SST 文件的读取,每一条记录都需要进行校验。
2.1.1 源码定位
- 头文件:
rocksdb-main/util/crc32c_arm64.h
- 实现文件:
rocksdb-main/util/crc32c_arm64.cc
2.1.2 优化原理:从串行到三路并行
原生 ARM64 的 __crc32c 指令虽然比软件查表快,但单条指令的延迟仍然存在。为了榨干鲲鹏 CPU 的流水线性能,引入了三路并行(3-Way Parallel)计算策略。
#define CRC32C24BYTES(ITR) \
crc1 = crc32c_u64(crc1, *(buf64 + BLK_LENGTH + (ITR))); \
crc2 = crc32c_u64(crc2, *(buf64 + BLK_LENGTH * 2 + (ITR))); \
crc0 = crc32c_u64(crc0, *(buf64 + (ITR)));
代码解读:
- 数据分块:输入数据被逻辑上分为三段(Block 0, Block 1, Block 2)。
- 独立计算:
crc0, crc1, crc2 三个寄存器分别维护三段数据的校验值。
- 指令并行:由于
crc0, crc1, crc2 之间没有数据依赖(Data Dependency),鲲鹏 920 的超标量乱序执行引擎可以同时发射这三条指令,极大地提升了 IPC。
示例代码:
#include <iostream>
#include <cstdint>
#include <vector>
uint32_t crc32c_u64(uint32_t crc, uint64_t val) {
crc ^= (uint32_t)val;
for (int i = 0; i < 64; i++)
crc = (crc >> 1) ^ (0x82f63b78 & -(crc & 1));
return crc;
}
int main() {
std::vector<uint64_t> buf64(24, 0x12345678abcdef00ULL);
uint32_t crc0 = 0, crc1 = 0, crc2 = 0;
for (int i = 0; i < 8; i++) {
crc0 = crc32c_u64(crc0, buf64[i]);
crc1 = crc32c_u64(crc1, buf64[i + 8]);
crc2 = crc32c_u64(crc2, buf64[i + 16]);
}
std::cout << "CRC0: " << crc0 << "\n";
std::cout << "CRC1: " << crc1 << "\n";
std::cout << "CRC2: " << crc2 << "\n";
return 0;
}
2.1.3 PMULL 与硬件预取
对于更大的数据块,引入了基于 PMULL (Polynomial Multiply) 指令的优化,允许一次性处理 64 字节数据。同时,利用 PRFM 指令进行硬件预取:
#define PREF4X64L1(buffer, PREF_OFFSET, ITR) \
__asm__("PRFM PLDL1KEEP, [%x[v],%[c]]" ::[v] "r"(buffer), \
[c] "I"((PREF_OFFSET) + ((ITR) + 0) * 64));
这显式地告诉 CPU 提前将数据搬运到 L1 缓存,掩盖了内存访问延迟。
2.2 KAE 硬件压缩集成
鲲鹏处理器配备了 KAE (Kunpeng Acceleration Engine) 硬件加速引擎,可以卸载 Zlib/Gzip 等压缩任务。
2.2.1 集成原理
RocksDB 本身通过 kZlibCompression 接口调用系统的 zlib 库。提供了兼容 zlib 接口的 KAE 加速库 (libwd, libkae)。通过动态库劫持或链接替换的方式,RocksDB 可以透明地使用硬件加速,而无需修改 RocksDB 的压缩逻辑源码。
2.2.2 部署与收益
- 配置:设置
LD_LIBRARY_PATH 优先加载 KAE 库,并在 RocksDB 中开启 kZlibCompression。
- 收益:相比软件 Zlib,KAE 硬件加速通常能提供 30% - 50% 的压缩吞吐量提升,同时 CPU 占用率降低 40% 以上。
示例代码:
#include <iostream>
#include <zlib.h>
#include <vector>
#include <chrono>
#include <string>
int main() {
std::vector<unsigned char> input(1024 * 1024, 'A');
std::vector<unsigned char> compressed(input.size() * 2);
std::vector<unsigned char> decompressed(input.size());
uLongf compressed_size, decompressed_size;
auto start = std::chrono::high_resolution_clock::now();
compressed_size = compressed.size();
int ret = compress(compressed.data(), &compressed_size, input.data(), input.size());
auto end = std::chrono::high_resolution_clock::now();
double compress_ms = std::chrono::duration<double, std::milli>(end - start).count();
if (ret == Z_OK) {
std::cout << << compressed_size << << compress_ms << << std::endl;
} {
std::cout << << ret << std::endl;
}
start = std::chrono::high_resolution_clock::();
decompressed_size = decompressed.();
ret = (decompressed.(), &decompressed_size, compressed.(), compressed_size);
end = std::chrono::high_resolution_clock::();
decompress_ms = std::chrono::<, std::milli>(end - start).();
(ret == Z_OK) {
std::cout << << decompressed_size << << decompress_ms << << std::endl;
} {
std::cout << << ret << std::endl;
}
valid = (input == decompressed);
std::cout << << (valid ? : ) << std::endl;
;
}
3. 存储引擎内核:LSM-Tree 与 I/O 极致优化
I/O 效率是存储引擎的生命线。本章分析 RocksDB 在 LSM-Tree 维护和 I/O 路径上的优化。
3.1 block_prefetcher.cc 源码解析
在全表扫描(Scan)或 Compaction 时,顺序读取是主要模式。这里主要对 BoostKit RocksDB 的 block_prefetcher.cc 源码进行解析,实现了自适应指数级预取。
核心源码:
readahead_limit_ = offset + len + readahead_size_;
readahead_size_ = std::min(max_auto_readahead_size, readahead_size_ * 2);
优化逻辑:
- 检测到连续读取后,预取窗口大小从 8KB 指数级增长到 2MB。
- 大幅减少系统调用次数,充分利用 SSD 的顺序读带宽。
- 在 Linux 下,底层协同调用
readahead 系统调用,利用 Page Cache 机制。
RocksDB 顺序 Scan 代码:
#include <rocksdb/db.h>
#include <rocksdb/options.h>
#include <rocksdb/slice.h>
#include <iostream>
int main() {
rocksdb::DB* db;
rocksdb::Options options;
options.create_if_missing = true;
rocksdb::Status s = rocksdb::DB::Open(options, "/tmp/rocksdb_test", &db);
if (!s.ok()) {
std::cerr << s.ToString() << std::endl;
return -1;
}
for (int i = 0; i < 1000000; ++i) {
db->Put(rocksdb::WriteOptions(), "key" + std::to_string(i), std::string(1024, 'A'));
}
rocksdb::ReadOptions ro;
ro.fill_cache = false;
auto it = db->NewIterator(ro);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
}
delete it;
delete db;
return 0;
}
3.2 LSM-Tree 的多线程并发 Compaction
Compaction 是 LSM-Tree 最消耗性能的操作。在鲲鹏 128 核环境下,单线程 Compaction 极易成为瓶颈。
3.2.1 Sub-compaction 并行化
推荐开启 Sub-compaction,将一个大 Compaction 拆分为多个小任务并行执行。
void CompactionJob::GenSubcompactionBoundaries() {
}
RocksDB 通过采样 SST 文件中的 Key 分布,智能地计算出拆分边界,确保多个线程负载均衡。
多线程 Sub-Compaction 实战代码:
rocksdb::Options options;
options.create_if_missing = true;
options.max_background_jobs = 32;
options.max_subcompactions = 16;
options.level0_file_num_compaction_trigger = 4;
options.write_buffer_size = 64 * 1024 * 1024;
3.3 鲲鹏亲和性调优 (NUMA)
鲲鹏 920 采用多 NUMA 节点设计。跨 NUMA 访问内存会带来显著延迟。
- 策略:通过
numactl 绑定进程,并配置 options.max_background_compactions 充分利用本地 NUMA 节点的 CPU 核心。
4. 并发与缓存:多核架构下的锁竞争消除
在 64 核甚至 128 核的高并发场景下,锁竞争(Lock Contention)是性能杀手。本章介绍 RocksDB 的无锁化设计。
4.1 无锁并发 MemTable
MemTable 的写入通常需要加锁。BoostKit RocksDB 利用 InlineSkipList 实现了无锁并发写入。
4.1.1 CAS 实现 (memtable/inlineskiplist.h)
template <bool UseCAS> bool InlineSkipList<Comparator>::Insert(...) {
if (UseCAS) {
while (true) {
if (splice->prev_[i]->CASNext(i, splice->next_[i], x)) {
break;
}
}
}
}
通过 ARM64 的 CAS 或 LDXR/STXR 指令,多个线程可以并发修改 SkipList 的指针,彻底消除了写入互斥锁。
4.1.2 开启配置
options.allow_concurrent_memtable_write = true;
options.enable_pipelined_write = true;
4.2 Block Cache 的分片 (Sharding)
全局唯一的 LRU Cache 锁在大并发下会成为热点。
4.2.1 源码解读 (cache/sharded_cache.cc)
RocksDB 将缓存划分为多个 Shard(分片),每个 Shard 有独立的锁。
int shard_id = Shard(hash);
return shards_[shard_id]->Insert(key, hash, value, ...);
4.2.2 调优建议
在鲲鹏服务器上,建议将分片数设置为 64 (2^6):
std::shared_ptr<Cache> cache = NewLRUCache(capacity, -1, false, 0.5, nullptr, 6);
5. 总结
BoostKit for RocksDB 并不是对参数做简单调优,而是深入 ARM64 与鲲鹏处理器特性的系统级优化。
在计算侧,通过 CRC32C 三路并行、PMULL 指令和硬件预取,充分释放鲲鹏 CPU 的指令级并行能力;通过 KAE 硬件压缩,将原本占用大量 CPU 的压缩计算卸载到专用加速引擎,实现更高吞吐与更低 CPU 开销。
在 I/O 与存储引擎层面,借助 自适应预取、并行 Sub-Compaction、NUMA 亲和性绑定,显著提升了 LSM-Tree 在大核数 ARM 服务器上的扩展性。
在并发与缓存设计上,通过 无锁 MemTable 和分片 Block Cache,有效消除了多核场景下的锁竞争问题。
总体来看,BoostKit 通过 指令级优化 + 硬件加速 + 架构感知设计,让 RocksDB 在鲲鹏平台上不仅'能跑',而且'跑得快、跑得稳',为 ARM64 服务器承载高性能存储与数据库负载提供了成熟可落地的解决方案。