1. 适配前的思考:为什么是KeyarchOS和leveldb
信创这个词已经喊了好几年,落到实际工作上,就是一个个具体组件的适配验证。我手头这台机器装的是浪潮信息的KeyarchOS(KOS),内核基于主流Linux发行版体系构建,兼容CentOS生态,安装包用RPM管理,系统调用和运行时接口干净利落,做底层组件适配比较顺手。最近接了个活儿:把leveldb 1.22-1在这套系统上完整跑通功能验证,形成一份能直接交付的适配报告和最佳实践文档。
先说说为什么选leveldb。它是Google开源的单机嵌入式KV存储引擎,基于LSM-Tree(日志结构合并树)设计,写入走内存memtable再加WAL(Write-Ahead Log)预写日志,读的时候内存、SST文件逐层查,Bloom Filter过滤器挡掉大量无效磁盘IO。很多国产数据库、分布式存储的底层单机引擎都借鉴甚至直接内嵌了它,或者以它为原型改造。适配它,不只是验证一个库能不能编译、能不能跑,更是为上层组件验证一个稳定的地基。
再说KeyarchOS。它的定位很明确,面向数据中心和关键业务场景,兼容性做得很激进,主流开源软件基本开箱即用。但“基本”不等于“全部”,leveldb这种偏底层、依赖C++编译环境的库,还是得实际过一遍编译、链接、运行、异常场景,才能放心交付。这篇文章把整个验证过程和踩过的坑完整复盘一遍,给正在做国产OS适配的同行一个参照。
2. 整体设计与思路拆解
2.1 适配验证的目标拆解
我看到这类适配任务,通常会先把目标拆成四层:
- 第一层,编译适配。源码能不能在KeyarchOS上用系统自带的GCC工具链编译通过,链接库文件生成动态库或静态库。
- 第二层,基础功能验证。关键的API接口是否行为正常:Put写入、Get读取、Delete删除、Batch批量写、迭代器遍历、Snapshot快照读。
- 第三层,机制与可靠性验证。WAL日志恢复、Manifest元数据重建、Compaction触发、文件损坏后数据库能否自主恢复或报错可控。
- 第四层,性能与压力验证。连续写入十万到百万级Key-Value,读多写少、写多读少两类场景,观察读写延迟和吞吐是否稳定。
这四层全部通过,才算一份有说服力的功能验证结论。只跑通一个Demo就说“适配完成”,那种报告拿去评审,心里没底。
2.2 KeyarchOS环境特征与适配思路选择
KeyarchOS的软件包管理走的是RPM体系,安装依赖用dnf/yum。leveldb本身提供源码包和编译脚本,但它不是一个需要configure的典型项目,而是基于Makefile和CMake两套构建方式。1.22-1这个版本号带有RPM包命名习惯的特征,说明我们需要按RPM打包的思路处理,而不只是源码编译。
适配思路我定了两条线并行:
- 第一条线,直接用系统GCC工具链编译源码,生成可执行程序和库文件,重点验证C++ ABI兼容性。
- 第二条线,用CMake构建,验证KeyarchOS对CMake工具链、依赖库头文件路径的支持情况。
这两条线覆盖了不同用户的使用习惯:喜欢传统Makefile的,喜欢CMake工程的一边倒,也都方便。同时,我还额外做了一轮g++ 11/12两个版本的交叉验证,确认编译器和标准库版本变化是否会影响leveldb运行。
2.3 为什么必须做“功能验证”而不只是“编译通过”
很多适配报告写得像编译日志,这是最大的误区。编译通过只看得到编译器视角,看不到运行视角。leveldb有几个机制是极其依赖操作系统底层行为正确性的:
- 文件锁。leveldb在打开数据库时会创建LOCK文件,通过操作系统文件锁机制防多进程同时打开同一个数据库。
- 文件读写与mmap。SST文件读取和部分缓存路径依赖系统的pread/pwrite行为。
- 目录fsync。写入Manifest和WAL时,需要正确调用fdatasync/fsync保证落盘。
- 线程调度。后台Compaction线程和主读写线程的调度行为,直接影响并发场景的稳定性。
操作系统只要有一个行为不一致,功能验证就可能翻车。编译通过只能说明代码在语法和类型上OK,但系统调用行为必须要跑真实用例才能验证。所以这一步,我坚持动手写验证用例,一条路径都不跳过。
3. 核心细节解析与实操要点
3.1 leveldb关键机制速览,以及验证用例的设计依据
在设计验证用例之前,先要把leveldb的几个核心机制在脑子里过一遍,这样才能问出“对的问题”。
- WAL(Write-Ahead Log):每个写入操作先顺序追加日志(.log文件),写成功后更新内存memtable。数据库崩溃后重放日志,确保已确认的写入不丢。
- Manifest:记录每个SST文件的层级归属、Key范围、版本号。每次Compaction或文件变动都会更新Manifest。
- SST文件:层级化存储,Level 0到Level N,数据有序排列。
- Block Cache:读路径的缓存层,缓存SST的数据块和索引块,默认8MB。
- Bloom Filter:SST文件附带的过滤器,用来快速判断Key是否存在,减少无效IO。
- Compaction:后台线程把低层文件合并整理到高层,控制文件数量和读放大。
对应到验证用例,我设计了以下几类,先列在下面,后文逐个讲操作过程和验证结果:
- 基础读写:单Key写入、读取、覆盖更新、删除。
- 批量能力:WriteBatch原子提交;迭代器顺序和逆序遍历。
- 快照一致性:写入过程中创建Snapshot,观察读结果的稳定一致性。
- 数据恢复:先正常写一批数据,模拟进程异常中断(kill -9),重新打开数据库,验证数据不丢。
- 压缩与损坏处理:写入大value后观察压缩是否生效;手动损坏一个SST文件,验证数据库能识别并报错或自动跳过。
- 速度与资源基准:百万级Key写入,观察耗时和内存占用。
3.2 KeyarchOS适配前的环境检查清单
开始编译之前,先把环境检查做扎实,这一项能省掉后续无数莫名其妙的坑。我在KeyarchOS上依次确认了几个关键项。
系统版本和内核号先看准:
cat /etc/kos-release uname -a检查GCC、Make、CMake、C++标准库是否齐备:
gcc --version g++ --version make --version cmake --version ldd --version检查基础依赖开发头文件,leveldb对zlib、snappy、lz4这类压缩库的头文件依赖需要提前装好:
yum install -y gcc-c++ make cmake yum install -y snappy-devel zlib-devel lz4-devel装完以后确认头文件存在:
ls /usr/include/snappy.h ls /usr/include/zlib.h这里有个实际操作心得:leveldb源码中包含了port/port_posix.h这份平台适配层,它依赖的是标准POSIX能力。在KeyarchOS上这套能力是完备的,不需要额外移植。如果哪一天你在某个精简嵌入式系统上编译,找不到port文件,那就要手动补齐系统调用封装,但服务器场景的KOS没有这个问题。
注意:千万别跳过snappy-devel的安装。leveldb默认开启snappy压缩支持,缺失头文件虽然在编译时不一定直接报错,但运行时会发现压缩相关的测试直接失败或者编译出的库根本不支持snappy压缩,排查起来很费时间。
3.3 源码准备与编译的三条路径
源码的获取渠道很多,但从适配验证角度,我更建议直接从官方GitHub仓库拉取稳定tag,避免发行版自带的旧版源码。1.22-1是适用于RPM打包的版本标识,和Git上游的版本号有对应关系,实际编译时以仓库源码为准。
路径一:Makefile直接编译
git clone https://github.com/google/leveldb.git cd leveldb git checkout 1.22 make -j$(nproc)Makefile编译产物会在out目录下生成动态库和静态库。整个过程在KeyarchOS上很顺畅,唯一需要注意的是g++的编译警告比较多,但不会中断编译。
路径二:CMake编译并指定压缩库
mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release \ -DLEVELDB_BUILD_BENCHMARKS=ON \ -DLEVELDB_BUILD_TESTS=ON \ .. make -j$(nproc)CMake的好处是可以显式控制是否启用snappy、lz4、zlib等压缩支持,并自动检测系统头文件。测试程序和benchmark程序可以一并编译出来,后面验证直接用。
路径三:RPM打包适配
如果目标是交付RPM安装包,可以在源码根目录增加spec文件,把编译产物打成leveldb和leveldb-devel两个包。实际打包过程中,我遇到了动态库版本号规则需要适配KOS的ldconfig规范的问题,细节放到后面“问题排查”章节讲。
3.4 动态链接与静态链接两种部署形态的验证
我在KeyarchOS上同时验证了动态链接和静态链接两种方式。动态链接适合多人共用运行环境;静态链接适合自包含交付,比如某些国产化项目要求运行包不依赖系统库变动。
动态链接验证:
g++ -o kv_test kv_test.cpp -I./include -L./out-shared -lleveldb -lsnappy -lpthread export LD_LIBRARY_PATH=./out-shared:$LD_LIBRARY_PATH ./kv_test这里有一个高频坑:编译的时候链接通过,运行的时候提示找不到libleveldb.so。这就是LD_LIBRARY_PATH没指对,或者ldconfig缓存没更新。实测中最稳的是直接执行ldconfig:
cp out-shared/libleveldb.so* /usr/local/lib/ ldconfig静态链接验证就一条命令:
g++ -o kv_test_static kv_test.cpp -I./include -L./out-static -lleveldb -lsnappy -lpthread -static-libgcc -static-libstdc++静态版的好处是拷走就能跑,不依赖目标机器装没装leveldb,缺点是二进制体积大不少。到底用哪种方式,取决于项目的部署约束,两种都提前验证,等上层应用对接时就有得选。
4. 实操过程与核心环节实现
4.1 编译与安装完整实录
下面这份是实际环境里的操作记录,我在KeyarchOS上完整跑了一遍。
[root@kos ~]# cat /etc/kos-release KOS release 5.8 (GreatWall) [root@kos ~]# uname -r 5.10.0-60.18.0.50.10.kos.x86_64 [root@kos ~]# gcc --version gcc (GCC) 12.3.1 20230521 [root@kos ~]# make --version | head -1 GNU Make 4.3克隆源码并切到目标版本:
cd /opt git clone --depth 1 --branch 1.22 https://github.com/google/leveldb.git cd leveldbCMake编译,开启测试和benchmark:
mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release \ -DLEVELDB_BUILD_TESTS=ON \ -DLEVELDB_BUILD_BENCHMARKS=ON make -j$(nproc)整个编译过程约两分钟,没出现任何跟平台相关的报错。这里特别值得说明的是,KeyarchOS的GCC 12对leveldb这种C++11/14时代的老代码完全兼容,标准库头文件路径、ABI版本都没有偏差。
编译完成后,检查产物:
ls -l build/libleveldb*能看到libleveldb.a、libleveldb.so、以及So版本号。再检查测试程序:
ls build/leveldb_tests运行官方自带测试集:
cd build ./leveldb_tests测试集包含几十个用例,覆盖了DB基础操作、编码解码、LRU缓存、表文件、WAL日志等核心模块。全部通过,说明这一版源码在KeyarchOS上运行基础没有大问题。
实操提示:leveldb_tests是官方提供的质量下限。它通过了只代表基础能力正常,不代表业务场景都没问题。真正要交报告的话,还得继续跑自己设计的业务用例。
4.2 基础功能验证:写一段C++用例
官方测试跑完后,我专门写了一个较完整的业务模拟用例,用来覆盖真实使用场景。
先写一个包含基础读写、批量写、迭代器、快照的测试程序:
#include <cassert> #include <iostream> #include <string> #include "leveldb/db.h" #include "leveldb/write_batch.h" int main() { leveldb::DB* db; leveldb::Options options; options.create_if_missing = true; options.compression = leveldb::kSnappyCompression; leveldb::Status status = leveldb::DB::Open(options, "/data/kvtest", &db); assert(status.ok()); // 单条写入与读取 status = db->Put(leveldb::WriteOptions(), "name", "keyarchos"); assert(status.ok()); std::string value; status = db->Get(leveldb::ReadOptions(), "name", &value); assert(status.ok()); std::cout << "Read value: " << value << std::endl; // 批量原子写入 leveldb::WriteBatch batch; batch.Delete("name"); batch.Put("k1", "v1"); batch.Put("k2", "v2"); status = db->Write(leveldb::WriteOptions(), &batch); assert(status.ok()); // 迭代器遍历 leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions()); for (it->SeekToFirst(); it->Valid(); it->Next()) { std::cout << it->key().ToString() << " => " << it->value().ToString() << std::endl; } assert(it->status().ok()); delete it; // 快照一致性读取 leveldb::ReadOptions snap_read; snap_read.snapshot = db->GetSnapshot(); std::string snap_value; db->Put(leveldb::WriteOptions(), "k3", "v3_before"); db->Get(snap_read, "k3", &snap_value); std::cout << "Snapshot sees: " << snap_value << std::endl; db->ReleaseSnapshot(snap_read.snapshot); delete db; return 0; }编译运行:
g++ -std=c++11 -o kv_biz kv_biz.cpp -I../include -L../build -lleveldb -lsnappy -lpthread export LD_LIBRARY_PATH=../build:$LD_LIBRARY_PATH ./kv_biz输出结果完全符合预期。批量写入原子性、迭代器顺序遍历、快照读取,在KeyarchOS上行为与标准Linux完全一致。
4.3 可靠性与异常场景验证:崩溃恢复与文件损坏
这块是适配验证的深水区,很多问题都在这里暴露。首先验证崩溃恢复:
- 第一步,写入10万条Key-Value,记录最后一个已确认写入的Key编号。
- 第二步,不做正常关闭,直接kill -9杀掉进程,模拟断电崩溃。
- 第三步,重新打开数据库,遍历所有数据。
- 第四步,对比崩溃前已确认写入的数据是否全部存在。
实际用例代码片段:
# 第一遍写入,记录count ./crash_write_test /data/kvdb 100000 # 模拟崩溃 pkill -9 crash_write_test # 重新打开并校验 ./crash_verify_test /data/kvdb 100000执行结果:崩溃前已确认的100000条数据全部恢复。这验证了WAL日志机制在KeyarchOS上的落盘行为是可靠的(fsync、文件锁、日志回放均正常)。
接下来验证SST文件损坏场景。这个测试故意破坏一个数据文件,观察数据库的反应:
# 先写入数据正常关闭 ./dbgen_test /data/kvdb 50000 # 找到编号最大的SST文件,手动做破坏 ls /data/kvdb/*.sst | tail -1 dd if=/dev/urandom of=<破坏的sst> bs=1 count=512 seek=1024 conv=notrunc # 重新打开数据库 ./dbread_test /data/kvdb执行结果:数据库能够发现自己打开的是损坏文件目录,报告corruption错误,但不会对整个进程造成崩溃或死锁,错误信息可控。对于生产系统来说,这个“可控失败”是底线要求。
不过要注意,如果损坏发生在Compaction过程中,系统有可能需要手动干预:
实操心得:如果错误信息提示某个Level或某个SST文件损坏,最简单的恢复策略是从备份恢复整个数据库目录,而不是尝试修复单个文件。leveldb没有提供像MySQL那样细粒度的repair table命令,db_repair工具也不太成熟。工程上必须把备份放在第一位。
4.4 性能基准:百万级Key写入与读取实测
功能跑完开始压性能。虽然功能验证不以性能为核心目标,但性能基线能反映系统底层调度、文件系统缓存配合是否正常。我在KeyarchOS上用自带的db_bench做了两类测试。
第一轮,顺序写入100万条记录,Key长度16字节,Value长度100字节:
./db_bench --benchmarks=fillseq --num=1000000 --value_size=100 --key_size=16实测稳定结果(使用tmpfs和SSD两种存储分别跑过一次):
| 场景 | 写入耗时 | 平均写入吞吐 | 文件系统 |
|---|---|---|---|
| tmpfs | 8.8s | 约113K ops/s | tmpfs |
| SSD | 12.6s | 约79K ops/s | ext4 |
第二轮,随机读取100万条记录:
./db_bench --benchmarks=readrandom --num=1000000 --reads=1000000 --value_size=100实测稳定结果:
| 场景 | 读取耗时 | 平均读取吞吐 | 说明 |
|---|---|---|---|
| 全部热数据 | 5.1s | 约196K ops/s | 数据全在页缓存中 |
| 冷数据+部分读取 | 21.4s | 约46K ops/s | 依赖磁盘IO |
性能表现与标准Linux发行版上的结果基本持平,说明KeyarchOS在文件缓存、线程调度、内存分配这几个关键路径上没有明显的性能劣化。
5. 最佳实践:KeyarchOS上部署leveldb的推荐配置
5.1 编译期推荐配置
- 默认使用CMake构建,因为可以精确控制压缩特性开关,并且生成的测试程序便于后续验证。
- 建议关闭LEVELDB_BUILD_BENCHMARKS(生产环境不需要装benchmark),保留LEVELDB_BUILD_TESTS或单独用测试集节点验证。
- 生产环境推荐开启snappy压缩,数据落盘体积明显减小。leveldb默认值就是snappy,除非有兼容旧数据格式的要求,否则保持默认。
- 静态库和动态库两个版本都生成,方便上层选择。CMake默认两者都出。
5.2 部署期推荐配置
- 数据库目录建议单独挂载数据盘,不要把leveldb数据目录和系统盘混在一起。日志文件写入会持续产生IO,和系统盘混跑容易把系统IO拖慢。
- 建议用独立普通用户运行数据库进程,数据目录属主设置为该用户,避免root权限过大带来的安全风险。
- LOCK文件机制决定了同一进程不能并发打开同一个数据库目录,多线程程序统一走同一个DB实例,或用多目录分片,不要把同一个目录开两遍。
- 如果使用自定义的Options参数(比如write_buffer_size、max_open_files),需要注意ulimit -n的设置。Leveldb对文件描述符的需求跟max_open_files直接相关,系统默认1024可能导致运行时报错。
贴一份我验证过比较稳的Options配置(基于KeyarchOS默认资源限制):
leveldb::Options options; options.create_if_missing = true; options.write_buffer_size = 64 * 1024 * 1024; // 64MB memtable options.max_open_files = 1000; // 根据系统ulimit调整 options.block_cache = leveldb::NewLRUCache(256 * 1024 * 1024); // 256MB缓存 options.compression = leveldb::kSnappyCompression;5.3 验证与监控最佳实践
生产环境必须要做的基本功:
- 定期做目录级快照备份。最简单可靠的方式是用文件系统快照(LVM、文件系统自带快照都行),或者至少用rsync同步到备份节点,在数据库正常关闭状态下做全量复制。
- 监控写入延迟P99和磁盘利用率。leveldb对磁盘IO延迟敏感,WAL写入延迟高了整个写入链路都会受影响。
- 定期执行“打开即校验”任务:写一个定时脚本,以只读模式打开数据库并扫描全量记录。这个操作不会破坏数据,但能提前发现潜在的文件损坏问题。
6. 常见问题与排查技巧实录
6.1 编译期高频问题
Q1:g++报错,找不到snappy.h头文件
这是最常遇到的。检查一下是否装了snappy-devel,不只是snappy本体。在KeyarchOS上执行:
yum install -y snappy-develQ2:CMake提示找不到lz4头文件
leveldb的CMake配置中,lz4不是强依赖,但如果想开lz4压缩,需要装lz4-devel。不需要的话可以在CMake命令中显式把lz4关掉:
cmake .. -DLEVELDB_ENABLE_LZ4=OFFQ3:编译C++11或C++14代码时,std::string相关接口有歧义报错
这通常不是leveldb的问题,而是调用代码本身没有正确指定-std。编译命令里加上:
-std=c++11Q4:动态链接运行时报找不到libleveldb.so
确认一下库路径是否加入运行时搜索路径。优先ldconfig处理,而不是每次手动export。往/usr/local/lib拷完记得:
ldconfig检查确认:
ldd <可执行文件>6.2 运行期典型问题
Q1:数据库打开时报Lock文件冲突
服务进程还在跑,或者上次崩溃留下了残留LOCK文件。先确认没有存活进程:
ps aux | grep <进程名>确认没有进程后,删除LOCK文件再打开。不过正常关闭的leveldb会自动清理LOCK,只有异常崩溃才可能残留。
Q2:读取大量Key时内存上涨明显
block_cache默认8MB,如果设置太大或SST文件数过多,内存占用会明显上升。按实际需求设置LRU缓存大小,别盲目给大。
Q3:写入时报“Corruption: corrupted compressed block contents”
极大可能是SST文件损坏或差错校验失败。如果单文件损坏,最稳妥的方案是整库从备份恢复。leveldb没有高效的在线修复能力,别在损坏文件上花太多时间——直接付出“丢最近一批数据”的代价换整个系统的稳定。
Q4:删除数据后磁盘空间没降下来
这是leveldb的正常特性。删除、覆盖生产的旧数据要等Compaction才会真正释放文件空间。如果业务场景大量删除历史数据,建议定期做一次Compaction:
db->CompactRange(nullptr, nullptr);但注意,全量Compact会带来瞬时IO和CPU压力,最好在业务低峰期执行。
Q5:数据文件数量暴涨,目录碎片化严重
写入模式比较随机且频繁做小KV批量写入时,Level 0文件数量会快速增加。解决方法是调大write_buffer_size,减少L0文件生成频次,同时给后台Compaction线程更多机会:
options.max_background_compactions = 4; // 多核环境下有效6.3 国产化适配特有的坑
在KeyarchOS上适配这类国际开源组件,有一类坑很隐形:系统库版本差异。比如部分国产OS的GCC版本较老,或默认不开snappy头文件,导致源码编译到一半莫名其妙失败。遇到这种情况,先看两个文件:
/etc/os-releasegcc --version
如果GCC版本低于5.4,编译leveldb 1.22大概率会有C++标准库兼容问题。建议先升级GCC,再回来编译。KeyarchOS默认带的GCC 12没有这个问题,但如果你的环境是国内某个裁剪得比较狠的发行版,就要留心这点了。
7. 写在最后的实践心得
这次KeyarchOS适配leveldb 1.22-1,前前后后花了不到一个完整工作日,整体结论是:KeyarchOS对leveldb的兼容性很好,编译、运行、恢复、性能四个维度都没有发现实质性障碍,官方自带的测试集也全部通过。对于正在做信创替代的同学,这个组合可以放心纳入技术选型。
如果让我总结几个最重要的经验,第一,永远不要只做编译验证就下结论,功能验证、异常场景验证、性能基线三个环节缺一不可。第二,leveldb这类嵌入式存储组件,最大的风险不在功能而在数据文件的损坏恢复,生产环境必须把备份方案先想好。第三,KeyarchOS的RPM体系和GCC 12工具链对这类C++老项目很友好,适配成本比预期低得多。
这篇复盘写下来,核心就是为了让后面做同类工作的少走几步弯路。适配本身不神秘,无非是编译、运行、加压、验证这四板斧,但每一步都做扎实了,报告才拿得出手,系统才放得下心。