发布于2026-07-13 阅读(0)
扫一扫,手机访问
AdvisKV 是我用 C++17 从零写的一个分布式 KV 存储系统原型,包含 Meta、SDM、Storage、SDK 四个主要模块,覆盖建库建表、路由查询、KV 读写、副本数扩缩容、坏副本替换、Raft、WAL/snapshot 恢复,跑了gtest + E2E 测试和 benchmark。(但是控制面高可用、自动 rebalance、接入rocksdb这些还没实现)
算算日子,又是大半年没更新博客了。上次动笔还是 OB 复赛结束后,写的一篇凄凄惨惨的总结文,一晃半年就这么过去了。简单交代下背景吧。比赛结束后,开始准备找实习。本来以为会很难,结果面了五六家,从字节基架到 OB 数据库内核,再到一些游戏引擎、自动驾驶的小厂,主要为了刷刷经验值。最初想投字节给 OB 的面试攒点底气,没想到字节效率高得离谱,没几天就发了 offer。最后纠结了一下,害怕一头扎进数据库内核把路走窄了,想了想,还是去基础架构试试。
实习差不多一个月,发现自己能学到的东西有限,实际代码开发少得可怜,每天基本就是排查问题、改点简单的代码、跑跑性能测试。心里开始犯嘀咕:照这个节奏下去,到秋招怕不是要废了。组内核心项目又不可能让我上手写代码,总得自己找点事干。巧的是,组里的项目恰好是分布式 KV,于是就想:不如把这套逻辑搞明白,自己从头写一个分布式 KV 出来。至少以后面试被问到实习做了什么,还能理直气壮地说自己私下撸了个分布式 KV,也不算白来一趟。
有点尴尬的是,自己设定的场景和组内项目差异太大,加上 V1 版本功能上没法实现得很全面,最后魔改一通,除了模块名字,几乎和组内的项目没什么关系了。
以上就是写出 AdvisKV 的初衷。说白了,多少也抱着想做出一个自己代表作的心态。毕竟之前写的那些 rmdb、miniob、seekdb 的代码,严格来说都不算是我自己的项目。
开始写这篇文档的时候,项目已经基本上完成得差不多了——至少 benchmark 能完整跑通(笑)。当时想着架构应该不会再大改了,顶多后期优化下性能,删掉那些乱七八糟的注释,可以动笔了。当然,后来的事实证明我又重构了很多次。
简单说,AdvisKV 是我用 C++17 写的一个分布式 KV 存储系统原型,一个学习向的系统原型。说白了,就是给自己的秋招简历加点料。同时也希望完成第一个从头干到尾的项目。期望别太高,定位就是一个学习原型。
如果概括这个项目的主线,大概是:Meta/SDM 作为控制面维护表、副本和路由的期望状态,Storage 作为数据面负责 KV 读写和 Raft 复制,控制面和数据面通过心跳和 reconciler 不断把系统状态收敛到预期。
整个项目分成四个模块:
Meta 负责 DB、Table 这些元数据和 DDL。SDM 负责 Storage 节点注册、心跳、ReplicaGroup 编排、期望副本下发和 Route 维护。Storage 负责 KV 数据读写、Raft 和本地持久化。SDK 对外提供基于 db + table + key 的 put/get/delete 接口。目前可以跑通建库建表、路由查询、KV 读写、副本数调整、自动替换坏副本、Raft、WAL、snapshot、recover 等功能。有两百多个 gtest 单测,一批 E2E 测试(比如最基础的端到端 KV 链路、leader 崩溃后切主、follower 追日志和追 snapshot、Meta/SDM 重启恢复等),还有 benchmark 和 metrics。
详细的实现数据和 benchmark 就不在这里展开了。关于这个项目,我更想写的是自己做下来的感受——也一直都是这个风格,所以就不搞太官方的味道了。
这个分布式 KV 的数据模型和正常的数据库差不多,有 db、table 的概念。
用户可以创建自己的 DB,然后在 DB 里面创建 table。每个 table 在创建时可以指定 shard 数和 replica 数。一个 table 会被拆成多个 shard,每个 shard 又会有多个 replica。需要说明一下,V1 版本里 shard 数是在创建 table 时确定的,后续暂时不能动态修改;replica 数现在可以通过 alter_table 调整,也支持缩到 0 再扩回来。也就是说,目前不是完全没有扩缩容,只是支持了副本数这条链路,自动 rebalance 和 shard 数变更还没做——以后大概率也不会做了,这个项目应该到此为止了。
# 项目里提供了一个交互式 CLI adviskvctl,方便演示
# 建库建表
adviskv> create_db demo_db dc1
OK db_id=1
# create_table
adviskv> create_table demo_db demo_table 4 3 default
OK table_id=1
# wait_table [timeout_ms]
adviskv> wait_table demo_db demo_table
OK table_state=NORMAL
# put
adviskv> put demo_db demo_table hello world
OK
# get
adviskv> get demo_db demo_table hello
OK value="world"
adviskv> delete demo_db demo_table hello
OK
# route
adviskv> route demo_db demo_table hello
Route
table: demo_db.demo_table
key: hello
shard: table_id=1 shard_id=2
replicas:
- replica_id=1:2:0 endpoint=127.0.0.1:50051 role=LEADER
- replica_id=1:2:1 endpoint=127.0.0.1:50052 role=FOLLOWER
- replica_id=1:2:2 endpoint=127.0.0.1:50053 role=FOLLOWER
# key hello 会先定位到 table 某个 shard,然后 SDK 再通过 SDM 查询该 shard 对应的 route,通过 endpoint 把请求发送到对应的 leader 的 Storage 节点
# 副本的扩缩容
# alter_table
adviskv> alter_table demo_db demo_table 2
OK table_id=1 replica_count=2
目前支持的操作是最基本的同步 put、get、delete。这些操作交给 SDK 模块处理。可以创建一个 KVClient 进行 put、get 等操作。顺带一提,CLI 里面命令可以写 delete,SDK 里对应的方法名叫 del。
SDK 的使用方式:
#include
#include "sdk/client.h"
int main() {
adviskv::sdk::KVClientConf conf;
conf.db_name = "demo_db";
conf.table_name = "demo_table";
conf.sdm_host = "127.0.0.1";
conf.sdm_port = 50049;
conf.sdm_timeout_ms = 3000;
conf.storage_timeout_ms = 3000;
adviskv::sdk::KVClient client(conf);
adviskv::Status put_status = client.put("hello", "world");
if (put_status.fail()) {
std::cerr << put_status.to_string() << std::endl;
return 1;
}
adviskv::Value value;
adviskv::Status get_status = client.get("hello", &value);
if (get_status.ok()) {
std::cout << value << std::endl; // value:"world"
}
return 0;
}
Minimal Overview

整体架构的粗略介绍
项目分为四个模块:meta、sdm、storage、sdk。
SDK
先聊聊最外层的 SDK。SDK 本身并不算分布式 KV 的服务端模块,就是一个客户端库,给使用者执行 put、get 这类操作。
拿 put 操作来说,在当前版本里,SDK 会先向 SDM 查询 route,也就是路由表。这个 route 包含当前 db/table 对应的 shard 信息,以及每个 shard 下面的 replica 节点信息。SDK 根据 key 算出它落在哪个 shard 上,然后从这个 shard 的 replica 列表中找到 leader 节点,直接给这个节点发送 put 操作。删除操作也是这个流程。V1 版本里,get 操作也只有 leader 才能执行,后续版本可能会优化这块。
Meta
Minimal architecture diagram:

Meta 模块负责 DDL 和 DB/Table 这些元信息。
创建 table 时,请求先打到 Meta。Meta 收到 CreateTable 之后,先在自己的 catalog 里记录这个 table,并把状态标记成“创建中”。然后 Meta 向 SDM 发送 PlaceTable 请求,让 SDM 负责后续的 replica 放置和 route 生成。
注意一点:CreateTable 返回成功并不代表 table 立刻就可以读写。这个接口本身是异步的,返回 OK 只代表 Meta 接受了这个 DDL,不保证 DDL 最终成功。table 是否真正 ready,取决于 Meta 和 SDM 里的后台 reconciler 能否把状态推进到最终状态。
后面加上了副本数调整功能,这里多了 AlterTableReplicaCount。用户发起 alter_table 时,Meta 先把 catalog 里的 replica_count 和状态改掉,然后通知 SDM 去把每个 shard 的目标副本数调到新值。这个过程也不是同步等所有 replica 调整完,最终还是靠后台 reconciler 推回来。
创建 DB 这类操作就不会发送给 SDM,因为 DB 是只有 Meta 才会知道的概念,SDM 不会在乎 DB,也不会持久化它。SDM 只关心 table,以及对应的 shard_count 和 replica_count 来进行编排。虽然 SDK 通过传递 db_name、table_name 和 hash(key) 找到路由表,但 db_name 实际上是附在 table 上的,所以不需要 SDM 知道 DB 的存在,它只需要了解 table 的内容就够了。
SDM
Minimal architecture diagram:

SDM 维护了一个非常重要的东西:route。route 以 shard 为单位,每个 shard 都有一个对应的 route。SDK 每次做 put/get/delete 时,先拿 db_name 和 table_name 去 SDM 查询路由表,然后根据 key 算出对应的 shard,再从 shard 的 replica 列表中找到 leader 节点,最后把真正的 KV 请求发给 storage。
另外,SDM 还负责 storage 节点的注册和心跳。Storage 启动后,向 SDM 注册自己,然后周期性发送 heartbeat。Heartbeat 不只是告诉 SDM 节点还没掉线,还会把本机 replica 的状态、raft role、成员信息(leader 会传)等发过去;SDM 也在 heartbeat response 里下发期望的 Replica,让 storage 的 NodeAgent 在本机创建、删除 replica,或者做成员变更。SDM 和 storage 这边用的是经典的“观测-期望”模式:SDM 维护期望状态,让 NodeAgent 去收敛。自动 rebalance 的话就没再做了——再搞也赶不上秋招了。
另外,SDM 需要提供 route,SDK 的链路是通过 db_name + table_name + key 向 SDM 查询路由表。所以 SDM 需要了解 table 的概念,也需要知道每个 table 下面有多少个 shard。Table 里面还有 ReplicaGroup 这一层,每个 shard 对应一个 group,用来维护这个 shard 希望有几个副本、哪些 replica 是当前还想保留的成员。Shard 作为逻辑数据,并没有在 SDM 里专门出现,而是通过 ReplicaGroup + Route 的方式在代码层面实现。
由于用了经典的“观测-期望”,后台任务目前主要靠几类后台 reconcile 去推动状态:TableReconciler 负责判断 table 是否 ready/deleting,ReplicaGroupReconciler 负责根据 target_replica_count 去补副本、删副本、替换坏副本,Route 负责在 replica ready 之后发布可用路由。说起来有点绕,但核心想法就是不断对齐“应该是什么样子”和“现在实际是什么样子”。后面讲到 SDM 的时候再细说。
Storage
Minimal architecture diagram:

Storage 负责数据的读写。前面的 Meta 和 SDM 更偏控制面,一个管元信息,一个管路由和节点状态;Storage 这边是真正处理 put/get/delete 请求,以及维护数据副本的地方。
拿 put/delete 操作来说,SDK 直接把请求打到对应 shard 的 leader 所在的 storage 节点上。写请求打到 storage 后,进入 replica 内部,交给 Raft 做复制。首先本地写一条 Raft log,然后发送给其他 follower,等待这条 log 被多数派确认并提交之后,会 apply 到 KV engine。当前版本的 KV engine 还是简单的 map engine,主要是先把分布式链路跑通。后续有考虑接 rocksdb,不过还只是“maybe”。
get 操作目前也是 leader-only 的状态,最终读请求只会打到 leader 上。不过加了一个读一致性机制,读之前会确认自己仍然是合法 leader,并确保本地状态机至少 apply 到对应的 read index,避免读到旧 leader 或者还没 apply 完的状态。
此外,Storage 还做了 WAL 和 snapshot 相关的持久化逻辑。当前实现里,WAL 主要承载 Raft log,用来记录每个 replica 的写入日志;snapshot 则保存已经 apply 之后的 KV 状态。服务重启时,可以通过 snapshot 加上后续 WAL 恢复 replica 状态。
V1的局限
还有很多没完成的内容,包括:
- Meta 和 SDM 在 V1 版本里只跑单进程,没有高可用。
- Storage 侧的 KV engine 直接用了 map,还没接 rocksdb。
- Shard 不支持 split,也没有实现自动 rebalance。
- 目前只支持 leader 的 put 和 read,不支持 follower read。
写在最后
最早写完这篇博客是在 6 月 12 号,但后来又做了大规模的重构,并且加上了扩缩容功能——为了这个功能重构了好多东西,拖到现在已经一个月了。为什么非要支持扩缩容?当时想着把项目收尾,了解到了 tinykv 这个项目,发现它的完全体几乎是全方面吊打我的项目,悲痛欲绝之下,想着怎么也得再多实现一个功能吧,不然也太小丑了。(如果再给我一次机会,可能不会写这个项目了,直接去实现 tinykv 不香吗……)
总的来说,大体上把项目介绍了一下。这个项目完成得不算好,应该有很多设计不太合理的地方。毕竟没有系统学习过分布式,很多内容都是边看、边查、边问、边写出来的。很多设计甚至一开始并没有想清楚,经常写到一半发现不对,再慢慢修修补补出来。
如果有同学抱着学习分布式 KV 的目的来看这个项目,发现某些设计看起来很别扭,那大概率不是你理解错了,很可能就是这里还没设计好。后面的文章里,我也会尽量把这些地方讲清楚,包括哪些是当前版本的取舍,哪些是后续应该继续改进的地方。
这篇就作为 AdvisKV 文档系列的开坑文,暂且写到这里吧。to be continue。
本文转载于:https://juejin.cn/post/7660222428955164698 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。
产品推荐
-
售后无忧
立即购买>
- DAEMON Tools Lite 10【序列号终身授权 + 中文版 + Win】
-
¥150.00
office旗舰店
-
售后无忧
立即购买>
- DAEMON Tools Ultra 5【序列号终身授权 + 中文版 + Win】
-
¥198.00
office旗舰店
-
售后无忧
立即购买>
- DAEMON Tools Pro 8【序列号终身授权 + 中文版 + Win】
-
¥189.00
office旗舰店
-
售后无忧
立即购买>
- CorelDRAW X8 简体中文【标准版 + Win】
-
¥1788.00
office旗舰店
-
正版软件
- ~a在C语言中是按位取反操作符,用于对变量a的每一位进行取反操作。具体来说,如果某一位是0,则变为1;如果是1,则变为0。需要注意的是,C语言中并没有“~”这个
- 在C语言中,~操作符的作用是对操作数的每一位进行取反操作。1)对于整数5,取反后变为-6;2)在嵌入式系统中,可用于控制LED灯开关;3)需注意有符号整数取反可能导致符号位变化;4)在网络编程中,可用于快速切换标志位状态。
-
前天 08-18 13:51
150
-
正版软件
- using namespace 使用中遇到的问题怎么解决
- 命名空间的基本概念与常见引入问题在C++等编程语言中,命名空间(namespace)是一种将代码标识符(如变量、函数、类名)封装在特定名称下的机制,其主要目的是避免命名冲突,尤其是在大型项目或使用多个第三方库时。使用“using namespace”指令可以将指定命名空间中的所有名称引入当前作用域,
-
13天前
0
-
正版软件
- c语言函数递归 实操经验总结:这些技巧很实用
- 理解递归的基本原理在C语言中,递归是一种函数调用自身的编程技术。要掌握它,首先需要理解其核心思想:将一个复杂的大问题,分解为一个或几个与原问题相似但规模更小的子问题,直到子问题足够简单,可以直接求解。这个过程通常包含两个关键部分:递归出口和递归体。递归出口定义了问题何时不再继续分解,即最简单、可直接
-
13天前
0
-
正版软件
- c语言函数递归 怎么选?常见方案对比分析
- 递归函数的基本概念与适用场景在C语言编程中,递归是一种函数调用自身的编程技巧。它并非适用于所有问题,但在处理某些具有自相似结构的问题时,能提供极其清晰和优雅的解决方案。递归的核心思想是将一个大规模问题分解为一个或多个同类型但规模更小的子问题,直到子问题简单到可以直接求解。典型的适用场景包括树形结构的
-
13天前
0
-
正版软件
- Objective-C 内存管理入门:从 alloc 到 dealloc 的生命周期详解
- 理解内存管理的基石在Objective-C的编程世界中,内存管理是开发者必须掌握的核心技能之一。它直接关系到应用的性能、稳定性与资源利用效率。与一些采用自动垃圾回收机制的语言不同,Objective-C在很长一段时间里,依赖一套基于引用计数的、需要开发者部分介入的管理规则。这套规则的核心思想是明确的
-
13天前
0
最新发布
-
1
-
2
-
3
- C语言中\n是什么意思?换行转义字符详解
- 346天前
-
4
- 探析Spring Boot框架的优点和特色
- 662天前
-
5
- 深入比较PyCharm社区版和专业版的功能
- 600天前
-
6
- 专家观点:谷歌是否会继续支持Golang的探讨
- 576天前
-
7
-
8
- Python实战教程:批量转换多种音乐格式
- 1208天前
-
9
- 如何在在线答题中实现试卷的自动批改和自动评分
- 1036天前
相关推荐
热门关注
-
- Nero AI Video Upscaler
- ¥27.30-¥39.00
-
- DockFlow
- ¥45.00-¥45.00
-
- 远离手机
- ¥10.00-¥10.00
-
- Trym
- ¥19.00-¥19.00
-
- Scherlokk
- ¥99.00-¥99.00