【C++ 脚手架】etcd 的介绍与使用

etcd 介绍与使用

Etcd 是⼀个 golang 编写的分布式、⾼可⽤的⼀致性键值存储系统,⽤于配置共享和服务发现等。它使⽤ Raft ⼀致性算法来保持集群数据的⼀致性,且客户端通过⻓连接 watch 功能,能够及时收到数据变化通知。

服务发现与注册:这是 etcd 最经典的应用场景。微服务启动时将自己的地址注册到 etcd 的指定目录下,其他服务通过 etcd 查找所需服务的地址。配合 Watch 机制,客户端可以实时感知服务实例的上线和下线。

一、etcd 安装

安装 etcd

bash 复制代码
sudo apt-get install etcd

启动 etcd 服务

bash 复制代码
sudo systemctl start etcd

设置开启自动启动 etcd

bash 复制代码
sudo systemctl enable etcd

查看启动状态

bash 复制代码
sudo systemctl status etcd

1.1 节点配置

如果是单节点集群其实就可以不⽤进⾏配置,默认 etcd 的集群节点通信端⼝为 2380,客⼾端访问端⼝为 2379。

若需要修改,则可以配置:/etc/default/etcd

conf 复制代码
# 节点名称,默认为 "default"
ETCD_NAME="etcd1"
# 数据⽬录,默认为 "${name}.etcd"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
# ⽤于客⼾端连接的 URL
ETCD_LISTEN_CLIENT_URLS="http://172.16.168.129:2379,http://127.0.0.1:2379"
# ⽤于客⼾端访问的公开,也就是提供服务的 URL
ETCD_ADVERTISE_CLIENT_URLS="http://172.16.168.129:2379,http://127.0.0.1:2379"
# ⽤于集群节点间通信的 URL
ETCD_LISTEN_PEER_URLS="http://192.168.65.132:2380"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.65.132:2380"
# ⼼跳间隔时间-毫秒
ETCD_HEARTBEAT_INTERVAL=100
# 选举超时时间-毫秒
ETCD_ELECTION_TIMEOUT=1000

# 以下为集群配置,若⽆集群则需要注销
# 初始集群状态和配置--集群中所有节点
# ETCD_INITIAL_CLUSTER="etcd1=http://192.168.65.132:2380,etcd2=http://192.168.65.132:2381,etcd3=http://192.168.65.132:2382"
# 初始集群令牌-集群的ID
# ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
# ETCD_INITIAL_CLUSTER_STATE="new"
# 以下为安全配置,如果要求SSL连接etcd的话,把下⾯的配置启⽤,并修改⽂件路径
# ETCD_CERT_FILE="/etc/ssl/client.pem"
# ETCD_KEY_FILE="/etc/ssl/client-key.pem"
# ETCD_CLIENT_CERT_AUTH="true"
# ETCD_TRUSTED_CA_FILE="/etc/ssl/ca.pem"
# ETCD_AUTO_TLS="true"
# ETCD_PEER_CERT_FILE="/etc/ssl/member.pem"
# ETCD_PEER_KEY_FILE="/etc/ssl/member-key.pem"
# ETCD_PEER_CLIENT_CERT_AUTH="false"
# ETCD_PEER_TRUSTED_CA_FILE="/etc/ssl/ca.pem"
# ETCD_PEER_AUTO_TLS="true"

1.2 运行验证

bash 复制代码
etcdctl put mykey "this is awesome"

如果出现报错

bash 复制代码
etcdctl put mykey "this is awesome"
No help topic for 'put'

sudo vi /etc/profile 在末尾声明环境变量 ETCDCTL_API=3 以确定etcd版本。

conf 复制代码
export ETCDCTL_API=3

完毕后,加载配置⽂件,并重新执⾏测试指令

bash 复制代码
source /etc/profile
etcdctl put mykey "this is awesome"
OK
etcdctl get mykey
mykey
this is awesome
etcdctl del mykey

1.3 etcd-cpp-apiv3 第三方库

etcd-cpp-apiv3 是⼀个 etcd 的 C++ 版本客⼾端 API。它依赖于 mipsasm, boost, protobuf, gRPC, cpprestsdk 等库。

依赖安装

bash 复制代码
sudo apt-get install libboost-all-dev libssl-dev
sudo apt-get install libprotobuf-dev protobuf-compiler-grpc
sudo apt-get install libgrpc-dev libgrpc++-dev
sudo apt-get install libcpprest-dev

etcd-cpp-apiv3 安装

bash 复制代码
git clone https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3.git
cd etcd-cpp-apiv3
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
make -j$(nproc) && sudo make install

二、常用接口介绍

2.1 头文件

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

2.2 Value

etcd::Value 类是用于封装从 etcd 服务器返回的键值对数据及其元信息。

cpp 复制代码
namespace etcd {
    class Value {
        public:
            bool is_dir();// 判断是否是⼀个⽬录
            std::string const& key() // 键值对的key值
            std::string const& as_string()// 键值对的val值
            int64_t lease() // ⽤于创建租约的响应中,返回租约ID
    }
}

2.3 Event

etcd::Event 类用于封装 etcd 中数据变化的事件通知,当客户端通过 Watch 机制监听的键发生变化时,etcd 会将变化信息包装成 Event 对象推送给客户端。

cpp 复制代码
//etcd会监控所管理的数据的变化,⼀旦数据产⽣变化会通知客⼾端
//在通知客⼾端的时候,会返回改变前的数据和改变后的数据
namespace etcd {
    class Event {
        enum class EventType {
            PUT, // 键值对新增或数据发⽣改变
            DELETE_, // 键值对被删除
            INVALID,
        };
        enum EventType event_type() // 返回事件类型
        const Value& kv() // 当前键值对的数据
    }
}

2.4 Response

etcd::Response 类是封装 etcd 所有操作(读、写、Watch 等)返回结果的统一容器,它根据不同的操作类型提供不同的数据访问接口。

cpp 复制代码
namespace etcd {
    class Response {
        bool is_ok() // 判断操作是否成功,失败时可通过 error_message() 获取错误信息。
        std::string const& error_message()
        Value const& value()// 返回当前值或操作结果
        Value const& prev_value()// 返回修改前的旧值
        Value const& value(int index) // 从多值响应中按索引获取某个 Value,适用于批量操作
        std::vector<Event> const& events();// 专门用于 Watch 操作,返回一个或多个 Event 对象,表示发生的数据变更事件(如 PUT、DELETE)。
        const std::vector<Value>& values() const; // 用于查询目录(前缀查询)时,返回匹配的所有键值对列表。
    }
}

2.5 Client

etcd::Client 类是 etcd C++ 客户端库提供的核心接口类,封装了与 etcd 服务器进行交互的所有主要操作。

  • 异步非阻塞 :所有操作都返回 pplx::task<Response> 异步任务对象,可通过 get() 阻塞等待或通过 wait() 实现非阻塞回调。

  • 统一响应 :所有操作都返回 Response 对象,通过 is_ok() 判断成功与否,通过 value()values()events() 等获取不同类型的结果数据。

cpp 复制代码
//pplx::task 并⾏库异步结果对象
//阻塞⽅式 get(): 阻塞直到任务执⾏完成,并获取任务结果
//⾮阻塞⽅式 wait(): 等待任务到达终⽌状态,然后返回任务状态
namespace etcd {
    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);
        pplx::task<Response> get(std::string const& key);
        //获取⼀个指定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);
        //Returns etcdv3::ERROR_KEY_NOT_FOUND if the key does not exist
        pplx::task<Response> rm(std::string const& key);
        //Returns etcdv3::ERROR_KEY_NOT_FOUND if the no key been deleted.
        pplx::task<Response> rmdir(std::string const& key, bool recursive = false);
        //Watches for changes of a key or a subtree
        pplx::task<Response> watch(std::string const& key, bool recursive = false);
        //撤销⼀个指定的租约
        pplx::task<Response> leaserevoke(int64_t lease_id);
        //数据锁
        pplx::task<Response> lock(std::string const& key);
        pplx::task<Response> unlock(std::string const& lock_key);
        //Execute a etcd transaction
        pplx::task<Response> txn(etcdv3::Transaction const& txn);
    }
}

2.6 KeepAlive

KeepAlive 类是 etcd 租约的保活管理器,用于自动维护租约的生命周期,防止租约过期导致关联的键被自动删除。

当创建一个带 TTL 的租约时,需要定期向 etcd 发送心跳续约。KeepAlive 对象会在后台自动完成这个续约工作,开发者无需手动编写心跳循环逻辑。

  • std::function<void(std::exception_ptr)> 回调函数在 KeepAlive 后台保活线程发生异常时被调用。

    • 网络连接失败:无法连接到 etcd 服务器发送心跳请求
    • 心跳请求超时:发送续约请求后长时间未收到响应
    • etcd 服务器返回错误:如租约已不存在、权限不足等
    • 租约已被撤销:其他地方调用了 leaserevoke 撤销了该租约
  • 调用特点

    • 异步调用:在后台线程中触发,不会阻塞主线程
    • 可多次调用:如果保活过程中多次出现异常(如网络抖动后恢复又再次故障),回调可能被多次触发
cpp 复制代码
namespace etcd {
    class KeepAlive {
        KeepAlive(Client const& client, int ttl, int64_t lease_id = 0);
        KeepAlive(std::string const& address,
        std::function<void(std::exception_ptr)> const& handler,
        int ttl,
        int64_t lease_id = 0);
        //返回租约ID
        int64_t Lease();
        //停⽌保活动作
        void Cancel();
    }   
}

2.7 Watcher

Watcher 类是 etcd Watch 机制的封装器,用于监听指定键或目录的变化,并在数据变更时自动触发回调函数。

创建 Watcher 对象后,它会与 etcd 建立一个长连接,持续监听目标 key 的变化。一旦有 PUT 或 DELETE 事件发生,就会调用预设的回调函数,并传入包含变更详情的 Response 对象。

cpp 复制代码
namespace etcd {
    class Watcher {
        Watcher(Client const& client, // 通过已有的 Client 对象创建 Watcher
        std::string const& key, // 要监控的键值对key
        std::function<void(Response)> callback, // 发⽣改变后的回调
        bool recursive = false); // 是否递归监控⽬录下的所有数据改变
        Watcher(std::string const& address, // 直接指定 etcd 地址创建 Watcher
        std::string const& key,
        std::function<void(Response)> callback,
        bool recursive = false);
        //阻塞等待,直到监控任务被停⽌
        bool Wait();    
        //异步⾮阻塞,设置任务停⽌时的回调函数,当任务被主动取消则传⼊true
        bool Wait(std::function<void(bool)> callback);
        bool Cancel();
    }
}

三、使用案例

服务注册的核心思想是:服务实例通过带租约的方式将自身地址写入 etcd,并依靠后台自动续约来维持注册信息的存活,从而实现服务的动态注册与自动摘除。

  • 创建租约 :服务启动时,调用 client.leasegrant(ttl) 向 etcd 申请一个带有 TTL(如 3 秒)的租约。这个租约相当于一个"定时器",到期后会自动删除所有关联数据。

  • 绑定注册 :通过 client.put(key, val, lease_id) 将服务信息(如 IP:端口)作为键值对写入 etcd,并将该键值对与上一步的租约 ID 绑定。此时服务注册成功。

  • 启动保活 :创建 KeepAlive 对象,它会自动在后台定期向 etcd 发送心跳续约。只要服务正常运行,租约就不会过期,注册信息一直存在。

  • 故障摘除:如果服务崩溃或网络中断,心跳停止,租约到期后 etcd 会自动删除该服务实例的键,实现服务的自动下线。

此外,代码中的 keepalive_callback 是一个异常恢复机制:当保活线程因网络抖动等原因意外停止时,会递归调用 put 函数重新创建租约和保活对象,确保服务注册能够自动恢复,增强了系统的健壮性。

3.1 etcd_server

cpp 复制代码
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Response.hpp>
#include <etcd/Value.hpp>
#include <iostream>
#include <string>
#include <memory>

void put(
    const std::string &host,
    const std::string &key,
    const std::string &val,
    std::shared_ptr<etcd::KeepAlive> &keepalive,
    int ttl = 3
) {
    // 1.创建与etcd交互的客户端
    etcd::Client client(host);
    // 2.创建并获取存活ttl时间的租约
    etcd::Response lease_resp = client.leasegrant(ttl).get();
    if(!lease_resp.is_ok()) {
        std::cout << "创建租约失败: " << lease_resp.error_message() << std::endl;
        return;
    }
    // 3.获取租约id
    int64_t lease_id = lease_resp.value().lease();
    // 4.注册key,val,并与租约绑定
    etcd::Response resp = client.put(key , val , lease_id).get();
    if(!resp.is_ok()) {
        std::cout << "数据新增失败: " << resp.error_message() << std::endl;
        return;
    }
    // 5.创建保活对象
    auto keepalive_callback = [host , key , val , &keepalive](std::exception_ptr eptr){
        // 注意悬空引用,而keepalive是在主程序中的,并且还需要修改keepalive,所以必须使用引用
        put(host , key , val , keepalive);
    };
    // keepalive_callback KeepAlive 内部的后台线程因网络断开、etcd 服务端无响应、心跳发送失败等原因意外停止时触发
    keepalive.reset(new etcd::KeepAlive(host , keepalive_callback  , ttl , lease_id));
}

int main() {

    // 提供服务方,进行服务的注册
    std::string etcd_host = "http://172.16.168.129:2379";
    std::string key = "/user/instance1";
    std::string val = "172.16.168.129:9000";
    // 创建租约保活对象
    std::shared_ptr<etcd::KeepAlive> keep_alive;
    put(etcd_host , key , val , keep_alive);
    getchar();

    return 0;
}

3.2 etcd_client

服务发现的核心思想是:客户端通过主动查询获取当前可用服务列表,并通过 Watcher 机制实时监听目录变化,实现服务列表的动态更新。

  • 主动查询获取初始列表 :调用 client.ls(key) 浏览指定监控目录(如 /),获取当前所有已注册的服务实例。如果目录为空(返回错误),会循环等待重试,直到有服务上线。

  • 输出当前服务信息 :遍历 resp.values() 获取所有键值对,打印出每个服务的 key(实例标识)和 value(地址信息如 172.16.168.129:9000)。

  • 创建 Watcher 监听变更 :通过 etcd::Watcher(host, key, watcher_callback, true) 创建监控器,true 表示递归监控整个目录树。当目录下有新增(PUT)、修改或删除(DELETE_) 事件时,watcher_callback 会被自动调用,并输出变更详情(包括旧值和新值)。

  • 异常恢复机制 :调用 watcher->Wait(callback) 开启异步非阻塞监控。如果 Watcher 因网络故障等原因意外停止(cancelfalse),会递归调用 get 函数重新建立监控,确保服务发现的可靠性。如果是被主动取消(canceltrue),则直接返回。

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

void get(const std::string& host ,  const std::string& key , std::shared_ptr<etcd::Watcher>& watcher) {
    // 1.创建与etcd交互的客户端
    etcd::Client client(host);
    // 2.浏览所监控目录下的所有数据
    etcd::Response resp = client.ls(key).get();
    while(!resp.is_ok()) {
        std::cout << "目前还没有能提供该服务的节点: " << resp.error_message() << std::endl; 
        std::this_thread::sleep_for(std::chrono::seconds(1));
        resp = client.ls(key).get();
    }
    // 3.获取结果
    std::vector<etcd::Value> values = resp.values();
    for(auto& value : values) {
        std::cout << "key: " << value.key() << " " << "value: " << value.as_string() << std::endl;
    }
    // 4.数据监控
    auto watcher_callback = [](const etcd::Response& resp){
        if (!resp.is_ok()) {
            std::cout << "监控出错: " << resp.error_message() << std::endl;
            return;
        }   
        // resp.events() 返回一个事件数组,因为一次 watcher 触发可能包含多个变化
        const std::vector<etcd::Event>& events = resp.events();
        for(auto& event : events) {
            if(event.event_type() == etcd::Event::EventType::PUT) {
                std::cout << event.kv().key() << "数据改变: " << event.prev_kv().as_string() << "->" << event.kv().as_string() << std::endl;
            } else if(event.event_type() == etcd::Event::EventType::DELETE_) {
                std::cout << event.prev_kv().key() << "数据删除!" << std::endl;
            } else {
                assert(false);
            }
        }
    };
    // watcher_callback回调触发时机:监控的目录下,只要有key新增或修改某个key的值或某个key被删除了就会调用该回调处理
    watcher.reset(new etcd::Watcher(host, key , watcher_callback , true));  //true表示递归监控目录下所有数据
    // 5.开启异步非阻塞监控
    watcher->Wait([host , key , &watcher](bool cancel) mutable {
        // lambda 默认捕获参数为 const 熟悉,可以添加 mutable 关键字
        if(cancel) {
            // 任务被主动取消
            return;
        }
        get(host , key , watcher);
    });
}

int main() {

    // 调用服务方,进行服务发现
    std::string etcd_host = "http://172.16.168.129:2379";
    std::string key = "/";
    // 创建监控服务对象
    std::shared_ptr<etcd::Watcher> watcher;
    get(etcd_host , key , watcher);
    getchar();

    return 0;
}

3.3 Makfile

makefile 复制代码
all: etcd_client etcd_server
etcd_client:etcd_client.cc
	g++ -std=c++17 $^ -o $@ -lcpprest -letcd-cpp-api
etcd_server:etcd_server.cc
	g++ -std=c++17 $^ -o $@ -lcpprest -letcd-cpp-api

clean:
	rm -rf etcd_client etcd_server
相关推荐
liu****2 小时前
第16届省赛蓝桥杯大赛C/C++大学B组(京津冀)
开发语言·数据结构·c++·算法·蓝桥杯
fengfuyao9852 小时前
VC++基于服务器的点对点文件传输实例
服务器·开发语言·c++
山甫aa2 小时前
List 容器 -----C++的stl学习
开发语言·c++·学习
CoderCodingNo2 小时前
【GESP】C++四、五级练习题 luogu-P1177 【模板】排序
数据结构·c++·算法
森G2 小时前
58、最佳实践与注意事项---------多线程、竟态条件和同步
c++·qt
CheerWWW2 小时前
C++学习笔记——箭头运算符、std::vector的使用、静态链接、动态链接
c++·笔记·学习
郭涤生3 小时前
原子操作的内存顺序
c++
ALex_zry3 小时前
C++模板元编程实战技巧
网络·c++·windows
ambition202423 小时前
斐波那契取模问题的深入分析:为什么提前取模是关键的
c语言·数据结构·c++·算法·图论