分布式kv存储

本文介绍了一个使用raft分布式共识算法实现的分布式一致性KV存储系统,该系统提供了HTTP和RPC接口。通过分析KVStoreServlet、KVServer和RaftNode的结构,阐述了如何在集群中进行键值操作、Leader选举、日志复制和快照安装。项目已开源,欢迎参与贡献。
摘要由CSDN通过智能技术生成

引言

注册中心作为服务治理框架的核心,负责所有服务的注册,服务之间的调用也都通过注册中心来请求服务发现。注册中心重要性不言而喻,一旦宕机,全部的服务都会出现问题,所以我们需要多个注册中心组成集群来提供服务。

本项目中,通过 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(
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值