Etcd使用

介绍

本文将从 Etcd 命令行基础操作入手,解决 API 版本兼容问题,再深入讲解 C++ 客户端库的安装与使用,最终通过封装 Registry(服务注册)和 Discovery(服务发现)类,提供可直接复用的分布式服务管理方案。

一、Etcd 命令行基础操作与 API 版本适配

在使用 etcdctl(Etcd 官方命令行工具)时,最常见的问题是 API 版本不兼容 ------Etcd 默认可能启用 V2 版本 API,而 V3 版本才支持更丰富的分布式特性(如租约、Watcher 等)。以下是完整的键值对操作流程及问题解决方案。

1.1 基础键值对操作(V3 版本)

首先尝试创建一个键 mykey 并设置值为 this is awesome,命令如下:

bash

复制代码
etcdctl put mykey "this is awesome"
常见报错与原因

若执行后出现类似以下报错,说明当前使用的是 V2 版本 API,而 put 是 V3 版本的命令:

bash

复制代码
Error: unknown command "put" for "etcdctl"

1.2 永久配置 Etcd V3 API

为了让 etcdctl 默认使用 V3 版本 API,需将 API 版本配置到系统环境变量中,步骤如下:

  1. 编辑系统环境变量配置文件 打开 /etc/profile(该文件对所有用户生效,若仅需当前用户生效,可编辑 ~/.bashrc):

    bash

    复制代码
    sudo vim /etc/profile
  2. 添加 API 版本配置在文件末尾添加以下内容,指定 Etcd 客户端 API 版本为 3:

    bash

    复制代码
    export ETCDCTL_API=3
  3. 加载配置使其生效执行以下命令,无需重启系统即可让配置生效:

    bash

    复制代码
    source /etc/profile
  4. 验证配置 重新执行 put 命令,若成功输出以下内容,说明配置生效:

    bash

    复制代码
    etcdctl put mykey "this is awesome"
    # 成功输出:OK

    可进一步通过 get 命令验证键值对是否存在:

    bash

    复制代码
    etcdctl get mykey
    # 输出:
    # mykey
    # this is awesome

二、Etcd C++ 客户端库安装

要在 C++ 项目中使用 Etcd,需安装官方推荐的 etcd-cpp-apiv3 客户端库。该库依赖 Boost、Protobuf、gRPC 等基础组件,需先安装依赖再编译库文件。

2.1 安装依赖库(Ubuntu 系统)

按顺序执行以下命令,安装所有依赖组件:

复制代码
# 1. 安装 Boost 全量库(Etcd 依赖 Boost 进行网络和异步操作)
sudo apt-get install libboost-all-dev libssl-dev 

# 2. 安装 Protobuf 和 gRPC(Etcd 底层使用 gRPC 协议通信)
sudo apt-get install libprotobuf-dev protobuf-compiler-grpc 
sudo apt-get install libgrpc-dev libgrpc++-dev  

# 3. 安装 cpprestsdk(用于 HTTP 异步请求处理)
sudo apt-get install libcpprest-dev 

2.2 编译并安装 etcd-cpp-apiv3

从 GitHub 克隆源码并编译安装,指定安装路径为 /usr(系统默认库路径,方便项目引用):

复制代码
# 1. 克隆源码
git clone https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3.git

# 2. 创建编译目录并进入
cd etcd-cpp-apiv3 
mkdir build && cd build 

# 3. CMake 配置(指定安装路径为 /usr)
cmake .. -DCMAKE_INSTALL_PREFIX=/usr 

# 4. 编译(-j$(nproc) 表示使用所有 CPU 核心加速编译)
make -j$(nproc) 

# 5. 安装到系统路径
sudo make install

三、Etcd 核心概念与 C++ 客户端类解析

在进行代码实现前,需先理解 Etcd 用于服务注册与发现的核心概念,以及 etcd-cpp-apiv3 库中的关键类 ------ 这些类是实现功能的基础。

3.1 核心概念

  • 服务注册:服务启动时,向 Etcd 写入自身的「服务标识 - 地址 / 端口」键值对,并通过「租约」确保服务下线后键值对自动删除(避免无效服务地址残留)。
  • 服务发现:客户端通过 Etcd 读取指定「服务目录」下的所有键值对,获取可用服务地址;同时通过「Watcher」监听目录变化,实时感知服务上线 / 下线。
  • 租约(Lease):Etcd 中的临时键机制,租约到期前需「续租」,否则键值对自动删除(用于服务健康检测)。
  • Watcher:Etcd 的事件监听机制,可监听指定键 / 目录的新增、删除、修改事件,实时同步数据变化。

3.2 关键 C++ 类解析

etcd-cpp-apiv3 库封装了 Etcd 的核心功能,以下是实现服务注册与发现必须掌握的类:

类名 核心作用 关键方法
Event 封装 Etcd 事件信息(如键值对新增 / 删除) event_type():获取事件类型(PUT/DELETE_/INVALID);kv():获取当前键值对;prev_kv():获取事件前的键值对
Response 封装 Etcd 操作的响应结果(成功 / 失败信息、返回数据) is_ok():判断操作是否成功;error_message():获取错误信息;events():获取事件列表(Watcher 场景)
KeepAlive 封装租约的「续租」逻辑,确保租约不失效 Lease():获取租约 ID;Cancel():停止续租(服务下线时调用)
Client Etcd 客户端核心类,提供键值对操作、租约管理的接口 put():写入键值对(支持绑定租约);ls():读取目录下所有键值对;leasegrant():创建租约;leasekeepalive():创建续租对象
Watcher 封装 Etcd 监听逻辑,实时感知键 / 目录变化 构造函数:指定监听的客户端、键 / 目录、回调函数;Wait():阻塞等待监听事件;Cancel():停止监听

四、C++ 实战:服务注册与发现基础实现

本节通过两个基础示例,分别演示「服务注册」和「服务发现 + Watcher 监听」的核心逻辑,帮助理解底层调用流程。

4.1 示例 1:服务注册(带租约)

服务启动时,向 Etcd 的 /service 目录下注册自身地址,并通过租约确保服务下线后键值对自动删除。

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

int main()
{
    // 1. Etcd 服务地址(默认端口 2379)
    const std::string etcd_host = "http://127.0.0.1:2379";

    // 2. 创建 Etcd 客户端实例
    etcd::Client client(etcd_host);

    // 3. 创建租约并启动续租(租约有效期 10 秒,需定期续租)
    // leasekeepalive() 返回 pplx::task(异步任务),get() 阻塞获取结果
    auto keep_alive = client.leasekeepalive(10).get();

    // 获取租约 ID(后续绑定到键值对)
    int64_t lease_id = keep_alive->Lease();
    std::cout << "创建租约成功,租约 ID:" << lease_id << std::endl;

    // 4. 注册服务
    auto resp = client.put("/service/user", "127.0.0.1:15535", lease_id).get();
    if (!resp.is_ok())
    {
        std::cerr << "服务注册失败:" << resp.error_message() << std::endl;
        return -1;
    }
    std::cout << "user 服务注册成功:/service/user -> 127.0.0.1:15535" << std::endl;

    // 5. 注册第二个服务(无租约,永久存在)
    auto resp2 = client.put("/service/friend", "127.0.0.1:9090").get();
    if (resp2.is_ok())
    {
        std::cout << "friend 服务注册成功:/service/friend -> 127.0.0.1:9090" << std::endl;
    }

    // 6. 模拟服务运行(10 秒后退出,租约停止续租,user 服务键值对自动删除)
    std::this_thread::sleep_for(std::chrono::seconds(10));

    // 主动停止续租(可选,程序退出时会自动释放)
    keep_alive->Cancel();
    std::cout << "服务停止,租约已取消" << std::endl;

    return 0;
}

4.2 示例 2:服务发现与 Watcher 监听

客户端读取 Etcd 中 /service 目录下的所有服务,并监听目录变化,实时打印服务上线 / 下线信息。

cpp

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

// 7. 事件回调函数:监听到服务变化时触发
void on_service_change(const etcd::Response &resp)
{
    // 检查响应是否正常
    if (!resp.is_ok())
    {
        std::cerr << "Watcher 错误:" << resp.error_message() << std::endl;
        return;
    }

    // 遍历所有事件(可能批量触发)
    for (const auto &ev : resp.events())
    {
        if (ev.event_type() == etcd::Event::EventType::PUT)
        {
            // 服务上线/更新事件
            std::cout << "[服务上线] " << ev.kv().key() << " -> " << ev.kv().as_string() << std::endl;
        }
        else if (ev.event_type() == etcd::Event::EventType::DELETE_)
        {
            // 服务下线事件(通过 prev_kv() 获取下线前的服务信息)
            std::cout << "[服务下线] " << ev.prev_kv().key() << " -> " << ev.prev_kv().as_string() << std::endl;
        }
    }
}

int main()
{
    const std::string etcd_host = "http://127.0.0.1:2379";
    etcd::Client client(etcd_host);

    // 8. 初始获取 /service 目录下的所有服务(服务发现)
    auto resp = client.ls("/service").get();
    if (!resp.is_ok())
    {
        std::cerr << "获取服务列表失败:" << resp.error_message() << std::endl;
        return -1;
    }

    // 打印初始服务列表
    std::cout << "初始服务列表:" << std::endl;
    int service_count = resp.keys().size();
    for (int i = 0; i < service_count; ++i)
    {
        std::cout << "- " << resp.key(i) << " -> " << resp.value(i).as_string() << std::endl;
    }

    // 9. 创建 Watcher 监听 /service 目录(递归监听子键变化)
    // 构造函数参数:客户端、监听目录、回调函数、是否递归监听
    etcd::Watcher watcher(client, "/service", on_service_change, true);

    // 10. 阻塞等待监听事件(程序持续运行,直到手动终止)
    std::cout << "\n开始监听服务变化(Ctrl+C 退出)..." << std::endl;
    watcher.Wait();

    return 0;
}

五、工程化封装:Registry 与 Discovery 类

基础示例仅演示核心逻辑,实际项目中需将代码封装为可复用的类,降低耦合度。以下是 Registry(服务注册)和 Discovery(服务发现)的工程化封装实现,包含日志打印(需自行实现 logger.hpp)和智能指针管理。

5.1 服务注册类:Registry

封装租约创建、服务注册、自动续租逻辑,服务销毁时自动停止续租。

复制代码
#pragma once

#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <memory>  // 用于智能指针
#include <string>
#include "logger.hpp"  // 自定义日志库

class Registry
{
public:
    // 智能指针类型定义(方便外部管理对象生命周期)
    using ptr = std::shared_ptr<Registry>;


    Registry(const std::string &host, int lease_ttl = 10)
        : _lease_ttl(lease_ttl)
    {
        // 创建 Etcd 客户端
        _client = std::make_shared<etcd::Client>(host);
        if (!_client)
        {
            LOG_FATAL("创建 Etcd 客户端失败");
            throw std::runtime_error("Failed to create Etcd client");
        }

        // 创建租约并启动续租
        try
        {
            _keepalive = _client->leasekeepalive(_lease_ttl).get();
            _lease_id = _keepalive->Lease();
            LOG_INFO("Etcd 租约创建成功,租约 ID:{},有效期:{} 秒", _lease_id, _lease_ttl);
        }
        catch (const std::exception &e)
        {
            LOG_FATAL("创建 Etcd 租约失败:{}", e.what());
            throw;  // 向上抛出异常,让调用者处理
        }
    }

    
    ~Registry()
    {
        if (_keepalive)
        {
            _keepalive->Cancel();
            LOG_INFO("Etcd 租约已取消,租约 ID:{}", _lease_id);
        }
    }

    

    bool register_service(const std::string &service_key, const std::string &service_value)
    {
        if (service_key.empty() || service_value.empty())
        {
            LOG_ERROR("服务键或值不能为空");
            return false;
        }

        try
        {
            // 绑定租约写入键值对
            auto resp = _client->put(service_key, service_value, _lease_id).get();
            if (resp.is_ok())
            {
                LOG_INFO("服务注册成功:{} -> {}", service_key, service_value);
                return true;
            }
            else
            {
                LOG_ERROR("服务注册失败:{},错误信息:{}", service_key, resp.error_message());
                return false;
            }
        }
        catch (const std::exception &e)
        {
            LOG_ERROR("服务注册异常:{} -> {},异常信息:{}", service_key, service_value, e.what());
            return false;
        }
    }

private:
    std::shared_ptr<etcd::Client> _client;       // Etcd 客户端(智能指针管理
相关推荐
2301_789015622 小时前
算法与数据结构——排序算法大全
c语言·开发语言·数据结构·c++·算法·排序算法·visual studio
光头闪亮亮3 小时前
基于 wxWidgets 框架的桌面应用程序-WebView 浏览器控件与Go后端数据交互
c++
无限进步_3 小时前
冒泡排序的多种实现方式详解
c语言·数据结构·c++·算法
ajassi20004 小时前
开源 C++ QT QML 开发(十六)进程--共享内存
c++·qt·开源
默|笙4 小时前
【c++】set和map的封装
android·数据库·c++
十五年专注C++开发5 小时前
QT 中的元对象系统(六):connect函数详解
开发语言·c++·qt·设计模式·系统架构·qevent
earthzhang20215 小时前
【1008】计算(a+b)/c的值
c语言·数据结构·c++·算法·青少年编程
BS_Li5 小时前
C++11(列表初始化、右值引用和移动语义)
c++·列表初始化·右值引用和移动语义
勇闯逆流河6 小时前
【C++】红黑树详解
开发语言·数据结构·c++