分布式 KV 存储
引言
注册中心作为服务治理框架的核心,负责所有服务的注册,服务之间的调用也都通过注册中心来请求服务发现。注册中心重要性不言而喻,一旦宕机,全部的服务都会出现问题,所以我们需要多个注册中心组成集群来提供服务。
本项目中,通过 raft 分布式共识算法,简单实现了分布式一致性的 KV 存储系统,对接口进行了封装,并且提供了 HTTP 接口和 RPC 接口。为以后注册中心集群的实现打下了基础。
项目链接:https://github.com/zavier-wong/acid
用例
在深入源码之前,先简单了解一下使用样例。在 acid/example/kvhttp
下提供了一个基于 kvraft 模块实现的简单分布式 kv 存储的前后端。
kvhttp
├── CMakeLists.txt # cmakelists
├── KVHttpServer接口文档.md # 接口文档
├── index.html # 前端
└── kvhttp_server.cpp # 后端
先用 cmake 构建 kvhttp,然后使用 ./kvhttp_server 1
,./kvhttp_server 2
,./kvhttp_server 3
来启动三个节点组成一个 kv 集群。
现在双击 index.html 启动前端,就可以通过 web 来于 kv 集群交互。
每个节点都会创建一个 ./kvhttp-x
目录来存储状态,./kvhttp-x/raft-state
存储的是 raft 的持久化数据,当日志的长度大于阈值,节点会全量序列化当前时刻的数据以快照的形式存储在 ./kvhttp-x/snapshot/
目录下,并以 raft 的 term 和 index 来命名快照。
这里建议读者先完整运行一遍用例,再继续接下来的学习。
设计思路
用例 kvhttp_server 通过 acid::http::KVStoreServlet
来处理 http 请求,KVStoreServlet 转发请求给acid::kvraft::KVServer
暴露出来操作 KV 的接口,KVServer 可以看成是一个持有所有已提交的键值对的 map,并且封装了 acid::raft::RaftNode
,KVServer 将所有的 KV 操作提交给了 RaftNode,等待操作在集群间达成共识后更新自己的 map。
如下图,KVServer 为 HttpServer 和 RaftNode 之间的通信建立起了桥梁。
1 2 3
┌───────────────┐ ┌─────────────────────┐ ┌─────────────────┐
│ │ │ ┌───────────────┐ │ │ │
│ ◄──────┼────┼──┤ ├──┼───┼─────────► │
│ │ │ │ RaftNode │ │ │ │
│ ────────┼────┼──┤ ├──┼───┼────────── │
│ │ │ └───┬──────▲────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌───▼──────┴────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ KVServer │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └───┬──────▲────┘ │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌───▼──────┴────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ HttpServer │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └───────────────┘ │ │ │
└───────────────┘ └─────────────────────┘ └─────────────────┘
KVStoreServlet
acid::http::KVStoreServlet
是 Http 服务器的实现。这并不是我们关注的重点。我们需要关注的只是其通过acid::kvraft::KVServer
中的哪些方法来提供服务。
class KVStoreServlet : public Servlet {
public:
using ptr = std::shared_ptr<KVStoreServlet>;
KVStoreServlet(std::shared_ptr<acid::kvraft::KVServer> store);
/**
* request 和 response 都以 json 来交互
* request json 格式:
* {
* "command": "put",
* "key": "name",
* "value": "zavier"
* }
* response json 格式:
* {
* "msg": "OK",
* "value": "" // 如果 request 的 command 是 get 请求返回对应 key 的 value, 否则为空
* "data": { // 如果 request 的 command 是 dump 请求返回数据库的全部键值对
* "key1": "value1",
* "key2": "value2",
* ...
* }
* }
*/
int32_t handle(HttpRequest::ptr request, HttpResponse::ptr response, HttpSession::ptr session) override;
private:
// KV 存储服务器
std::shared_ptr<acid::kvraft::KVServer> m_store;
};
int32_t KVStoreServlet::handle(HttpRequest::ptr request,
HttpResponse::ptr response,
HttpSession::ptr session) {
nlohmann::json req = request->getJson();
nlohmann::json resp;
co_defer_scope {
response->setJson(resp);
};
...
Params params(req);
...
const std::string& command = *params.command;
if (command == "dump") {
const auto& data = m_store->getData();
...
return 0;
} else if (command == "clear") {
kvraft::CommandResponse commandResponse = m_store->Clear();
...
return 0;
}
...
const std::string& key = *params.key;
kvraft::CommandResponse commandResponse;
if (command == "get") {
commandResponse = m_store->Get(key);
resp["msg"] = kvraft::toString(commandResponse.error);
resp["value"] = commandResponse.value;
} else if (command == "delete") {
commandResponse = m_store->Delete(key);
resp["msg"] = kvraft::toString(commandResponse.error);
} else if (command == "put") {
...
const std::string& value = *params.value;
commandResponse = m_store->Put(key, value);
resp["msg"] = kvraft::toString(commandResponse.error);
} else if (command == "append") {
...
const std::string& value = *params.value;
commandResponse = m_store->Append(key, value);
resp["msg"] = kvraft::toString(commandResponse.error);
} else {
resp["msg"] = "command not allowed";
}
return 0;
}
可以看到 KVStoreServlet 只是将请求简单转发给 KVServer。
KVServer
KVServer 是连接 raft 服务器与 http 服务器的桥梁,是实现键值存储功能的重要组件,但是其实现很简单。
class KVServer {
public:
...
using KVMap = std::map<std::string, std::string>;
KVServer(std::map<int64_t, std::string>& servers,
int64_t id, Persister::ptr persister,
int64_t maxRaftState = 1000);
...
void start();
CommandResponse handleCommand(CommandRequest request);
CommandResponse Get(const std::string& key);
CommandResponse Put(const std::string& key, const std::string& value);
CommandResponse Append(const std::string& key, const std::string& value);
CommandResponse Delete(const std::string& key);
CommandResponse Clear();
const KVMap& getData() const {
return m_data;}
private:
void applier();
void saveSnapshot(int64_t index);
void readSnapshot(Snapshot::ptr snap);
...
CommandResponse applyLogToStateMachine(const CommandRequest& request);
private:
...
KVMap m_data;
Persister::ptr m_persister;
std::unique_ptr<RaftNode> m_raft;
co::co_chan<raft::ApplyMsg> m_applychan;
...
int64_t m_maxRaftState = -1;
};
先看几个字段,m_data 是一个由 map 实现的键值存储,m_persister 是一个持久化管理模块,m_raft 指向了一个 raft 服务器,m_applychan 是接收 raft 达成共识消息的 channel, m_maxRaftState 是一个阈值,超过之后 KVServer 会生成快照替换日志。
从简单的 start 函数开始看
void KVServer::start() {
readSnapshot(m_persister->loadSnapshot());
go [this] {
applier();
};
m_raft->start();
}
star 里会通过 m_persister 从本地加载一个最近的快照,并调用 readSnapshot 来恢复之前的状态,然后启动一个协程执行 applier 函数,最后启动 raft 服务器并阻塞在这里。
再看一下在协程里执行的 applier 函数,不断从 m_applychan 里接收 raft 达成共识的消息,再根据消息类型进行对应的操作。
void KVServer::applier() {
ApplyMsg msg{
};
while (m_applychan.pop(msg)) {
std::unique_lock<MutexType> lock(m_mutex);
SPDLOG_LOGGER_DEBUG(g_logger, "Node[{}] tries to apply message {}", m_id, msg.toString(