etcd实战指南:从安装集群到C++封装,解锁分布式服务治理的“钥匙”

文章目录

本篇摘要

本文介绍etcd------分布式高可用键值存储系统,详述其Linux安装、集群配置(3节点示例)、数据存取与健康检查,以及C++客户端API(etcd-cpp-apiv3)的安装与使用,含服务注册/发现封装,辅以代码与效果演示,帮助快速上手etcd实战。

etcd介绍与使用

1·介绍与安装

Etcd 是用 Go 编写的分布式、高可用一致性键值存储系统,用于配置共享和服务发现,采用 Raft 算法保证数据一致,客户端支持长连接 watch 功能以及时获取数据变化通知,较 Zookeeper 更轻量化。

在 Linux 系统安装 Etcd 的步骤:

  • 安装 Etcd:执行命令"sudo apt - get install etcd";
  • 启动 Etcd 服务:执行命令"sudo systemctl start etcd";
  • 设置 Etcd 开机自启:执行命令"sudo systemctl enable etcd"。

2·集群配置使用介绍(了解即可)

1. 怎么配集群?(3 节点示例)

每个节点(比如 IP 是 192.168.1.1192.168.1.2192.168.1.3)运行类似下面的命令:

bash 复制代码
etcd \
  --name 节点名(如 node1) \
  --data-dir 存储目录(如 /var/lib/etcd) \
  --listen-peer-urls http://0.0.0.0:2380 \      # 节点间通信地址
  --listen-client-urls http://0.0.0.0:2379 \    # 客户端访问地址
  --initial-cluster "node1=http://192.168.1.1:2380,node2=http://192.168.1.2:2380,node3=http://192.168.1.3:2380" \  # 所有节点信息
  --initial-cluster-state new                   # 首次启动用 new
  • 所有节点的 --initial-cluster 必须写全量节点信息(名字+IP:端口)。
  • --name 每个节点不同(如 node1/node2/node3)。
  • 首次启动所有节点都用 --initial-cluster-state new,后面加节点才用 existing
2. 怎么用?
  • 查集群状态(任意节点执行):

    bash 复制代码
    etcdctl member list      # 看有哪些节点
    etcdctl endpoint health  # 检查节点是否健康
  • 存取数据(和单节点一样):

    bash 复制代码
    etcdctl put my-key "hello"      # 写数据
    etcdctl get my-key              # 读数据
3. 注意啥?
  • 节点间网络要通(尤其是 2380 和 2379 端口)。
  • 首次配置所有节点信息必须一致,别漏节点!
  • 生产环境建议用 3 或 5 节点,别只用 1 个(会单点故障)。

一句话 : etcd 集群就是多个节点一起存数据,互相备份,用时按普通 etcd 操作就行(类似之前的redis集群设置使用)!

3·验证是否客户端成功安装并启用

  • 这里可以看到默认本地客户端与远端(先本地后远端)都完成了ipv4与ipv6连接,也就是本机端口与对应的etcd服务端端口完成连接了。
  • 正常使用(类似redis客户端使用)。

可能会出现报错如:No help topic for 'put'

  • 此时可以对配置文件进行添加环境变量然后刷新配置即可(即对应声明etcd版本,先(sudo vi /etc/profile)打开对应文件,然后末尾添加环境变量:export ETCDCTL_API=3,进行source /etc/profile 即可)。

4·安装对应C++客户端API

etcd - cpp - apiv3 是 etcd 的 C++ 版本客户端 API,依赖 mipsasm、boost、protobuf、gRPC、cpprestsdk 等库,GitHub 地址为 https://github.com/etcd - cpp - apiv3/etcd - cpp - apiv3 。

依赖安装(C++ 依赖)

  1. 执行 sudo apt - get install libboost - all - dev libssl - dev 安装 boost 相关和 ssl 开发包。
  2. 执行 sudo apt - get install libprotobuf - dev protobuf - compiler - grpc 安装 protobuf 及其编译器相关包。
  3. 执行 sudo apt - get install libgrpc - dev libgrpc + + - dev 安装 gRPC 相关开发包。
  4. 执行 sudo apt - get install libcpprest - dev 安装 cpprestsdk 开发包。

api 框架安装(从源码构建安装)

  1. 执行 git clone https://github.com/etcd - cpp - apiv3/etcd - cpp - apiv3.git 克隆项目仓库。
  2. 进入克隆后的目录 cd etcd - cpp - apiv3
  3. 创建并进入 build 目录 mkdir build && cd build
  4. 执行 cmake.. - DCMAKE_INSTALL_PREFIX = / usr 配置构建参数。
  5. 执行 make - j $( nproc ) && sudo make install 编译并安装(利用多核加速编译,然后执行安装)。

5·基于C++代码的简单调用

首先先看下发送客户端与接收客户端的调用流程:

  • 大致过程就是首先发送客户端可以设置自己的约期时间(可有可无),然后向服务端发送构建对应键值对请求即put(并且小于约期时间ttl时间内进行向服务端发送续约请求)。
  • 服务端收到对应键值对请求进行保存,然后约期时间倒计时,如果到时间了,发送客户端没有续约就删除对应键值(value)。
  • 接收客户端进行发送接收请求即ls,然后服务端返回对应的所有键值对的类似数组,然后客户端可以遍历查看对应kv。
  • 客户端还可以向服务端启动监控机制,也就是告诉服务端只有它管理的键值对有变化就发出答复给接收客户端,接收客户端可以根据对应事件状态进行对应信息获取与处理等。

这里只需要了解下常用的基本的接口的使用即可(比如下面例子应用的)。

下面对应构建发送客户端与接收客户端模拟下这个过程:

这里只需要知道:

发送客户端职责:进行构建键值对+保活机制处理。

接收客户端职责:进行查询对应所有键值对+监控机制操作。

实现思路:

发送客户端进行对应发送键值对,一个设置约期一个不设置(客户端默认连接存在就续约),然后过一段时间退出发送客户端;接收客户端进行ls和watch进行查看与监控。

测试效果:

  • 发送客户端进行注册键值对(服务),然后指定一段时间退出(也就是对一个服务不再续约)。
  • 此时接收客户端这边查到对应的服务。
  • 发送客户端退出后,接收客户端这边监控到了被删除的键值对也就是服务。

代码如下:

put.cc:

cpp 复制代码
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <thread>
#include <chrono>

int main()
{
    const std::string host = "http://127.0.0.1:2379";
    // 创建对应的推送客户端
    etcd::Client client(host);
    // 设置保活机制://返回的是包装任务的智能指针,先get拿到里面的值也就是智能指针(shared_ptr)
    auto keep_alive = client.leasekeepalive(3).get();//默认3秒服务端查一次leaseid,如果不存在就销毁键值对(val置空),客户端一旦退出就leaseid消失
    // 设置保活id,交给服务端管理:
    int64_t lease_id = keep_alive->Lease();
    // 发送对应键值对构建:
    auto resp1 = client.put("/service/user", "127.0.0.1:8080", lease_id).get();//返回的是包装任务的智能指针,先get拿到里面的值也就是智能指针(shared_ptr)

    // 失败注册的处理
    if (resp1.is_ok() == false)
    {
        std::cout << "新增数据失败:" << resp1.error_message() << std::endl;
        return -1;
    }

    auto resp2 = client.put("/service/friend", "127.0.0.1:8081").get();

    if (resp2.is_ok() == false)
    {
        std::cout << "新增数据失败:" << resp1.error_message() << std::endl;
        return -1;
    }
    std::cout << "数据添加成功!" << std::endl;
    // 反之注册端退出导致的续约失败不方便看效果因此进行休眠:
    using namespace std::literals;
    std::this_thread::sleep_for(10s);
}

get.cc:

cpp 复制代码
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <thread>
#include <chrono>
#include <etcd/Value.hpp>
#include <etcd/Watcher.hpp>

void callback(const etcd::Response &resp)
{

    if (resp.is_ok() == false)
    {
        std::cout << "收到一个错误的事件通知:" << resp.error_message() << std::endl;
        return;
    }

    // 发生变化的键值对会被填充进去,这里没有对应的下标防止只好范围for
    for (auto &ev : resp.events())
    {

        if (ev.event_type() == etcd::Event::EventType::PUT)
        {
            std::cout << "服务信息发生了改变:\n";
            std::cout << "当前的值:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;
            std::cout << "原来的值:" << ev.prev_kv().key() << "-" << ev.prev_kv().as_string() << std::endl;
        }

        else if (ev.event_type() == etcd::Event::EventType::DELETE_)
        {
            std::cout << "服务信息下线被删除:\n";
            std::cout << "当前的值:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;
            std::cout << "原来的值:" << ev.prev_kv().key() << "-" << ev.prev_kv().as_string() << std::endl;
        }
    }
}
int main()
{
    const std::string host = "http://127.0.0.1:2379";
    // 创建对应的推送客户端
    etcd::Client client(host);
    // 进行查询对应根目录下的服务:
    auto resp = client.ls("/service").get();
    if (resp.is_ok() == false)
    {
        std::cout << "获取键值对数据失败: " << resp.error_message() << std::endl;
        return -1;
    }
    int sz = resp.keys().size();
    for (int i = 0; i < sz; i++)
    {

        std::cout << resp.value(i).as_string() << "可以提供" << resp.key(i) << "服务\n";
    }

    // 进行时刻监听服务变化(异步线程):
    etcd::Watcher watcher(client, "/service", callback, true);//告诉服务端监控的根目录下有服务变化就发应答,一旦有变化就去调用callback,拿着服务端发来的答复

    // 监听不变后保证主线程不退出:
    watcher.Wait();//回收异步线程

    return 0;
}

makefile:

6.封装服务发现与注册功能

如果在项目中一直想上面那样写一堆进行调用必然会杂乱不美观,因此提供常用的注册与查询监控等接口,用户使用的时候就不用自己从头再写一遍,直接传递对应依赖参数调用api即可。

封装思路:

四个基础操作接口 :

  • 服务注册:向 etcd 中写入 <服务-主机地址> 这样的键值对,让其他服务能发现它。
  • 服务发现:从 etcd 里读取当前所有可提供服务的信息,便于其他服务找到下游节点,然后进行对应监控工作。
  • 设置"服务上线"回调:当有新服务注册(上线)时,执行预先定义好的逻辑(比如建立 RPC 连接)。
  • 设置"服务下线"回调:当某个服务注销或掉线时,触发对应的清理或断开逻辑(比如移除无效连接)。

封装后的收益:

外部的 RPC 调用模块只需调用这几个接口,就能方便地:先拉取全量服务列表建立连接,之后也能自动感知"新服务上线时新增连接、旧服务下线时移除连接",从而更优雅地实现服务治理与 RPC 通信。

测试效果:

  • 启动封装的对应的etcd程序,它就会把对应的服务注册进去,然后执行获取+监控(阻塞住)。
  • 此时启动另一个发送客户端完成其他服务注册,发现注册成功。
  • 发送客户端退出,对应的约期服务到期直接被删除。

对应代码:

etcd.hpp:

cpp 复制代码
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <thread>
#include <chrono>
#include <etcd/Response.hpp>
#include <etcd/Value.hpp>
#include <etcd/Watcher.hpp>
#include "log.hpp"
class Registry
{

public:
    using Ptr = std::shared_ptr<Registry>;
    Registry(const std::string &host) : _client(std::make_shared<etcd::Client>(host)),
                                        _keepalive(_client->leasekeepalive(3).get()),//直接拿到对应的shared_ptr
                                        _leaseid(_keepalive->Lease())
    {
    }

    bool RegistryDatas(const std::string &key, const std::string &val)
    {
        auto resp = _client->put(key, val, _leaseid).get();
        if (resp.is_ok() == false)
        {
            LOG_ERROR("注册数据失败:{}", resp.error_message());
            return false;
        }
        return true;
    }

private:
    std::shared_ptr<etcd::Client> _client;
    std::shared_ptr<etcd::KeepAlive> _keepalive;
    int64_t _leaseid;
};

class Discovery
{

public:
    using callback = function<void(const std::string &, const std::string &)>;
    using Ptr = std::shared_ptr<Discovery>;
    Discovery(const std::string &host, const std::string &basedir, callback put_cb, callback del_cb)
        : _client(std::make_shared<etcd::Client>(host)), _put_cb(put_cb), _del_cb(del_cb) // 如果不想获取对应新添加还是删除的监控信息就传递nullptr

    {
        // 进行查询
        auto resp = _client->ls(basedir).get();
        if (resp.is_ok() == false)
        {
            LOG_ERROR("获取服务信息数据失败:{}", resp.error_message());
        }

        int sz = resp.keys().size();
        for (int i = 0; i < sz; ++i)
        {
            if (_put_cb)
                _put_cb(resp.key(i), resp.value(i).as_string());
        }

        // 进行监控
        _watcher = std::make_shared<etcd::Watcher>(*_client.get(), basedir, 
          bind(&Discovery::CallBack,this,std::placeholders::_1) , true);

        //_watcher->Wait();
    }

    ~Discovery(){
      //这里如果不主动析构watch对象,作为类成员变量也会自动析构的
      _watcher->Cancel();//主动释放掉watch的资源,比如让异步线程结束,但本身watch对象还是存在的(强制结束爆出警告)
      _watcher->Wait();
    }

private:
    // watcher的回调函数:

    void CallBack(const etcd::Response &resp)

    {

        if (resp.is_ok() == false)
        {
            LOG_ERROR("收到一个错误的事件通知: {}", resp.error_message());
            return;
        }
        // 发生变化的键值对会被填充进去,这里没有对应的下标防止只好范围for
        for (auto &ev : resp.events())
        {

            if (ev.event_type() == etcd::Event::EventType::PUT)
            {
                if (_put_cb)
                    if (_put_cb)
                        _put_cb(ev.kv().key(), ev.kv().as_string());
                LOG_DEBUG("新增服务:{}-{}", ev.kv().key(), ev.kv().as_string());
            }

            else if (ev.event_type() == etcd::Event::EventType::DELETE_)
            {
                if (_del_cb)
                    _del_cb(ev.prev_kv().key(), ev.prev_kv().as_string());
                LOG_DEBUG("下线服务:{}-{}", ev.prev_kv().key(), ev.prev_kv().as_string());
            }
        }
    }
    std::shared_ptr<etcd::Client> _client;
    std::shared_ptr<etcd::Watcher> _watcher;
    callback _put_cb; // watch的时候对于发现新添加的键值对数据进行调用,或者查询的时候调用
    callback _del_cb; // watch的时候对于删除键值对数据进行调用
};

test.cc:

cpp 复制代码
#include "../../common/etcd.hpp"
#include <gflags/gflags.h>
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

DEFINE_string(host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(basedir, "/service", "监控服务的根目录");

void put_cb(const std::string &k, const std::string &v)
{
     LOG_DEBUG("处理新添加的服务: {}---{}", k, v);
}
void del_cb(const std::string &k, const std::string &v)
{
     LOG_DEBUG("删除的服务原先是: {}---{}", k, v);
}

int main(int argc, char *argv[])
{
     init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
     google::ParseCommandLineFlags(&argc, &argv, true);
     Registry::Ptr registry = std::make_shared<Registry>(FLAGS_host);

     registry->RegistryDatas(FLAGS_basedir + "/chat/", "127.0.0.1:8080");
     registry->RegistryDatas(FLAGS_basedir + "/user/", "127.0.0.1:8081");

     Discovery::Ptr discovery = std::make_shared<Discovery>(FLAGS_host, FLAGS_basedir, put_cb, del_cb);

     std::this_thread::sleep_for(1000s);
     return 0;
}

本篇小结

本篇带你了解了从etcd基础概念到集群搭建,再到C++客户端开发与功能封装,本文系统梳理了etcd的核心操作与应用流程,通过图文与代码示例,清晰呈现了其作为服务发现与配置中心的高效实践路径,实用性强。

相关推荐
默默在路上2 小时前
CentOS Stream 9 安装hadoop单机伪分布式模式
大数据·hadoop·分布式
星火开发设计2 小时前
C++ deque 全面解析与实战指南
java·开发语言·数据结构·c++·学习·知识
码界奇点2 小时前
基于Spring与Netty的分布式配置管理系统设计与实现
java·分布式·spring·毕业设计·源代码管理
点云SLAM2 小时前
C++设计模式之单例模式(Singleton)以及相关面试问题
c++·设计模式·面试·c++11·单例模式(singleton)
草莓熊Lotso2 小时前
Qt 信号与槽深度解析:从基础用法到高级实战(含 Lambda 表达式)
java·运维·开发语言·c++·人工智能·qt·数据挖掘
脏脏a3 小时前
C++ STL list 模拟实现:从底层链表到容器封装
开发语言·c++·stl·双链表
【D'accumulation】4 小时前
Kafka地址映射不通(很常见的问题)
分布式·kafka
数翊科技10 小时前
深度解析 HexaDB分布式 DDL 的全局一致性
分布式
你怎么知道我是队长11 小时前
C语言---typedef
c语言·c++·算法