[C++项目组件]Etcd的简单介绍和使用

Etcd的使用

1.Etcd介绍

Etcd 是一个 golang 编写的分布式、高可用的一致性键值存储系统,用于配置共享和服务发现等。它使用 Raft 一致性算法来保持集群数据的一致性,且客户端通过长连接 watch 功能,能够及时收到数据变化通知,相较于 Zookeeper 框架更加轻量化。以下是关于 etcd 的安装与使用方法的详细介绍。

Etcd在项目中的作用是:作为服务注册发现中心。

2.搭建服务注册发现中心

使用 Etcd 作为服务注册发现中心,需定义服务注册和发现逻辑,通常涉及以下操作:

  1. 服务注册:服务启动时,向 Etcd 注册自身地址和端口。
  2. 服务发现:客户端通过 Etcd 获取服务的地址和端口,用于远程调用。
  3. 健康检查:服务定期向 Etcd 发送心跳,维持注册信息的有效性。

此外,etcd 采用 golang 编写,v3 版本通信采用 grpc API(即 HTTP2 + protobuf);官方仅维护 Go 语言版本的 client 库,若用 C/C++ 则需寻找非官方的 client 开发库。

3.客户端类与接口介绍

cpp 复制代码
// pplx::task 并行库异步结果对象
// 阻塞方式 get(): 阻塞直到任务执行完成,并获取任务结果
// 非阻塞方式 wait(): 等待任务到达终止状态,然后返回任务状态

namespace etcd {
class Value {
	bool is_dir(); //判断是否是一个目录
	std::string const& key() //键值对的 key 值
	std::string const& as_string()//键值对的 val 值
	int64_t lease() //用于创建租约的响应中,返回租约 ID
}

// etcd 会监控所管理的数据的变化,一旦数据产生变化会通知客户端
// 在通知客户端的时候,会返回改变前的数据和改变后的数据
class Event {
	enum class EventType {
		PUT,      //键值对新增或数据发生改变
		DELETE_,  //键值对被删除
		INVALID,
	};
	enum EventType event_type()
	const Value& kv()
	const Value& prev_kv()
}

class Response {
	bool is_ok()
	std::string const& error_message()
	Value const& value()//当前的数值 或者 一个请求的处理结果
	Value const& prev_value()//之前的数值
	Value const& value(int index)//
	std::vector<Event> const& events();//触发的事件
}

class KeepAlive {
KeepAlive(Client const& client, int ttl, int64_t lease_id = 0);
	// 返回租约 ID
	int64_t Lease();
	// 停止保活动作
	void Cancel();
}

class Client {
	// etcd_url: "http://127.0.0.1:2379"
	Client(std::string const& etcd_url, std::string const& load_balancer = "round_robin");
	// Put a new key-value pair 新增一个键值对
	pplx::task<Response> put(std::string const& key, std::string const& value);
	// 新增带有租约的键值对 (一定时间后,如果没有续租,数据自动删除)
	pplx::task<Response> put(std::string const& key, std::string const& value, const int64_t leaseId);
	// 获取一个指定 key 目录下的数据列表
	pplx::task<Response> ls(std::string const& key);
	// 创建并获取一个存活 ttl 时间的租约
	pplx::task<Response> leasegrant(int ttl);
	// 获取一个租约保活对象,其参数 ttl 表示租约有效时间
	pplx::task<std::shared_ptr<KeepAlive>> leasekeepalive(int ttl);
	// 撤销一个指定的租约
	pplx::task<Response> leaserevoke(int64_t lease_id);
	//数据锁
	pplx::task<Response> lock(std::string const& key);
}

class Watcher {
	Watcher(Client const& client, std::string const& key, // 要监控的键值对 key  
	std::function<void(Response)> callback, //发生改变后的回调 
	bool recursive = false); //是否递归监控目录下的所有数据改变
	
	Watcher(std::string const& address, std::string const& key,
	std::function<void(Response)> callback,
	bool recursive = false);
	
	// 阻塞等待,直到监控任务被停止
	bool Wait();
	bool Cancel();
}

4.使用样例

put.cc

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

int main(int argc, char* argv[])
{
    std::string etcd_host = "http://127.0.0.1:2379";
    // 实例化客户端对象
    etcd::Client client(etcd_host);
    // 获取租约保活对象 -- 伴随着一个指定的有效时长的租约
    auto keep_alive = client.leasekeepalive(3).get();
    // 获取租约id
    auto lease_id = keep_alive->Lease();
    // 向etcd新增数据
    auto resp = client.put("/service/user", "127.0.0.1:8080", lease_id).get();
    if (resp.is_ok() == false) {
        std::cout << "新增数据失败: " << resp.error_message() << std::endl;
        return -1;
    }
    auto resp2 = client.put("/service/friend", "127.0.0.1:9090").get();
    if (resp2.is_ok() == false) {
        std::cout << "新增数据失败: " << resp2.error_message() << std::endl;
        return -1;
    }
    std::this_thread::sleep_for(std::chrono::seconds(10));

    return 0;
}

get.cc

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

int main(int argc, char* argv[])
{
    std::string etcd_host = "http://127.0.0.1:2379";
    // 实例化客户端对象
    etcd::Client client(etcd_host);
    // 获取租约保活对象 -- 伴随着一个指定的有效时长的租约
    auto keep_alive = client.leasekeepalive(3).get();
    // 获取租约id
    auto lease_id = keep_alive->Lease();
    // 向etcd新增数据
    auto resp = client.put("/service/user", "127.0.0.1:8080", lease_id).get();
    if (resp.is_ok() == false) {
        std::cout << "新增数据失败: " << resp.error_message() << std::endl;
        return -1;
    }
    auto resp2 = client.put("/service/friend", "127.0.0.1:9090").get();
    if (resp2.is_ok() == false) {
        std::cout << "新增数据失败: " << resp2.error_message() << std::endl;
        return -1;
    }
    std::this_thread::sleep_for(std::chrono::seconds(10));

    return 0;
}

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

在服务的注册与发现中,主要基于 etcd 所提供的可以设置有效时间的键值对存储来实现

服务注册

在 etcd 里,会存储一个带租期(比如 ns 时长)的保活键值对,用来表示提供指定服务的节点主机。比如有这样的键值对:<key, val> -- </service/user/instance-1, 127.0.0.1:9000>

再看这个 key 的结构:

  • /service 是主目录,下面会存不同服务的键值对;
  • /user 是服务名称,说明这个键值对对应"用户服务"的节点;
  • /instance-1 是节点实例名称,因为提供用户服务可能有多个节点,每个节点得有独立且唯一的实例名。

当这个键值对注册好后,服务发现的一方可以通过目录去查找这些键值对。而且如果注册的节点退出了,保活失败,3秒后租约就会失效,对应的键值对会被删除,etcd 会通知发现方数据失效,这样就能实现服务下线的通知功能。

服务发现

服务发现分为两个过程:

  • 客户端刚启动时,会通过 ls 目录浏览,获取 /service 路径下所有键值对;
  • 对关注的服务进行 watcher 观测,一旦数据有新增或删除的变化,就会收到通知,进而对节点进行管理。

ls 的路径是 /service,会获取到 /service/user/service/firend 等路径下所有能提供服务的实例节点数据;若 ls 路径为 /service/user,则会获取到 /service/user/instancd-1/service/user/instance-2 等所有提供用户服务的实例节点数据。

客户端会把发现的所有 <实例 - 地址> 管理起来,方便节点管理:

  • 收到新增数据通知,就往本地管理里添加新增的节点地址(即服务上线);
  • 收到删除数据通知,就从本地管理里删除对应的节点地址(即服务下线)。

由于管理了所有能提供服务的节点主机地址,所以需要进行 RPC 调用时,只需根据服务名称,获取一个能提供服务的主机节点地址去访问即可,这里的获取策略采用 RR 轮转策略(轮询策略)。

封装思想

要把 etcd 的操作全都封装起来,不用操心内部数据管理,只需要对外提供四个基础操作接口:

  • 服务注册:往 etcd 里添加 <服务-主机地址> 这样的数据;
  • 服务发现:获取当前所有能提供服务的信息;
  • 设置服务上线的处理回调接口;
  • 设置服务下线的处理回调接口。

这样封装后,外部的 rpc 调用模块,既能先获取所有当前的服务信息、建立通信连接来做 rpc 调用,也能在有新服务上线时新增连接,还能在服务下线时移除连接。

cpp 复制代码
#pragma once
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <functional>
#include "logger.hpp"

namespace yjt_im
{
    // 服务注册客户端类
    class Registry
    {
    public:
        using ptr = std::shared_ptr<Registry>;
        Registry(const std::string &host) : _client(std::make_shared<etcd::Client>(host)),
                                            _keep_alive(_client->leasekeepalive(3).get()),
                                            _lease_id(_keep_alive->Lease()) {}
        ~Registry() { _keep_alive->Cancel(); }
        bool registry(const std::string &key, const std::string &val)
        {
            auto resp = _client->put(key, val, _lease_id).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> _keep_alive;
        uint64_t _lease_id;
    };

    // 服务发现客户端类
    class Discovery
    {
    public:
        using ptr = std::shared_ptr<Discovery>;
        using NotifyCallback = std::function<void(std::string, std::string)>;
        Discovery(const std::string &host,
                  const std::string &basedir,
                  const NotifyCallback &put_cb,
                  const NotifyCallback &del_cb) : _client(std::make_shared<etcd::Client>(host)),
                                                  _put_cb(put_cb), _del_cb(del_cb)
        {
            // 先进行服务发现,先获取到当前已有的数据
            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,
                                                       std::bind(&Discovery::callback, this, std::placeholders::_1), true);
        }
        ~Discovery()
        {
            _watcher->Cancel();
        }

    private:
        void callback(const etcd::Response &resp)
        {
            if (resp.is_ok() == false)
            {
                LOG_ERROR("收到一个错误的事件通知: {}", resp.error_message());
                return;
            }
            for (auto const &ev : resp.events())
            {
                if (ev.event_type() == etcd::Event::EventType::PUT)
                {
                    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());
                }
            }
        }

    private:
        NotifyCallback _put_cb;
        NotifyCallback _del_cb;
        std::shared_ptr<etcd::Client> _client;
        std::shared_ptr<etcd::Watcher> _watcher;
    };
}
相关推荐
蔗理苦3 小时前
2025-10-01 Python不基础 1——字节码和虚拟机
开发语言·python
lly2024063 小时前
PostgreSQL LIMIT 语句详解
开发语言
shark_dev3 小时前
C/C++ 数据类型选择笔记:int、long long、char、string、float、double
c语言·c++
序属秋秋秋3 小时前
《C++进阶之C++11》【lambda表达式 + 包装器】
c++·笔记·学习·c++11·lambda表达式·包装器
山,离天三尺三3 小时前
线程中互斥锁和读写锁相关区别应用示例
linux·c语言·开发语言·面试·职场和发展
零一iTEM3 小时前
NS4168输出音频通过ESP32C3测试
c++·单片机·嵌入式硬件·mcu·音视频·智能家居
charlie1145141913 小时前
精读C++20设计模式——行为型设计模式:解释器模式
c++·学习·设计模式·解释器模式·c++20
讓丄帝愛伱3 小时前
阿里开源 Java 诊断神器Arthas
java·linux·开发语言·开源
郭源潮14 小时前
《Muduo网络库:实现Channel通道以及Poller抽象基类》
服务器·c++·网络库