欢迎光临
我们一直在努力

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

Etcd-SDK 使用指南:C++ 服务注册与发现

摘要:本文详细介绍了如何在 C++ 项目中基于 Etcd 实现服务注册与发现。内容涵盖 Etcd 的安装配置、etcd-cpp-apiv3 客户端 SDK 的核心 API 使用、租约保活机制、数据监控机制,并提供了完整的服务注册与发现封装方案。通过 SvcProvider 和 SvcWatcher 两个核心组件,实现了服务自动注册、健康检查和服务动态发现功能,为微服务架构提供了可靠的服务治理基础。

C++脚手架仓库地址: https://gitee.com/chen-weifeng-cwf/developing-scaffolding-for-c

📋 目录

  • 1. Etcd 介绍
  • 2. 安装 Etcd
    • 2.1 基本安装
    • 2.2 节点配置
    • 2.3 运行验证
  • 3. 客户端 SDK
    • 3.1 依赖安装
    • 3.2 API 框架安装
    • 3.3 头文件
  • 4. 核心数据结构与 API
    • 4.1 Value — 键值对数据
    • 4.2 Event — 数据变化事件
    • 4.3 Response — 请求响应
    • 4.4 Client — Etcd 客户端
    • 4.5 KeepAlive — 租约保活
    • 4.6 Watcher — 数据监控
  • 5. 入门示例
    • 5.1 目录结构
    • 5.2 put.cc — 写入数据(带租约保活)
    • 5.3 get.cc — 获取数据并监控变化
    • 5.4 Makefile
    • 5.5 运行演示
  • 6. 服务管理封装
    • 6.1 设计思路
    • 6.2 服务注册客户端 — SvcProvider
    • 6.3 服务发现客户端 — SvcWatcher
  • 7. 完整使用示例
    • 7.1 目录结构
    • 7.2 registry.cc — 服务注册
    • 7.3 discovery.cc — 服务发现
    • 7.4 CMakeLists.txt
    • 7.5 运行演示
  • 8. 总结

Etcd 是一个分布式、高可用的键值存储系统,常用于服务发现和配置管理。本文详细介绍 Etcd 的安装配置,以及如何在 C++ 项目中通过 etcd-cpp-apiv3 客户端 SDK 实现服务注册与发现。


1. Etcd 介绍

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


2. 安装 Etcd

2.1 基本安装

# 安装 Etcd
sudo apt-get install etcd

# 启动 Etcd 服务
sudo systemctl start etcd

# 设置开机自启
sudo systemctl enable etcd

2.2 节点配置

如果是单节点集群,可以不做配置。Etcd 默认集群节点通信端口为 2380,客户端访问端口为 2379。

如需修改,编辑 /etc/default/etcd:

# 节点名称,默认为 "default"
ETCD_NAME="etcd1"

# 数据目录
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"

# 客户端连接的 URL
ETCD_LISTEN_CLIENT_URLS="http://192.168.65.132:2379,http://127.0.0.1:2379"

# 客户端访问的公开 URL
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.65.132: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"
#ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
#ETCD_INITIAL_CLUSTER_STATE="new"

# 以下为 SSL 安全配置(可选)
#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 –name etcd1 –initial-advertise-peer-urls http://192.168.65.132:2380 \\
–listen-peer-urls http://192.168.65.132:2380 \\
–listen-client-urls http://192.168.65.132:2379 \\
–advertise-client-urls http://192.168.65.132:2379 \\
–initial-cluster-token etcd-cluster \\
–initial-cluster etcd1=http://192.168.65.132:2380,etcd2=http://192.168.65.132:2381,etcd3=http://192.168.65.132:2382 \\
–initial-cluster-state new &> nohup1.out &

2.3 运行验证

使用 etcdctl 命令行工具测试:

$ etcdctl put mykey "this is awesome"
OK

$ etcdctl get mykey
mykey
this is awesome

$ etcdctl del mykey

常见问题:如果出现 No help topic for 'put' 报错,需声明 API 版本:

sudo vi /etc/profile
# 在末尾添加
export ETCDCTL_API=3

source /etc/profile


3. 客户端 SDK

Etcd v3 版本通信采用 gRPC API(HTTP/2 + protobuf),官方只维护了 Go 语言版本的 client 库。对于 C/C++ 项目,我们使用非官方的客户端库:etcd-cpp-apiv3。

  • GitHub 地址:https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3

3.1 依赖安装

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

3.2 API 框架安装

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

3.3 头文件

#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Response.hpp>
#include <etcd/Value.hpp>


4. 核心数据结构与 API

4.1 Value — 键值对数据

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

4.2 Event — 数据变化事件

当 Etcd 监控的数据发生变化时,会通知客户端,同时返回改变前和改变后的数据。

namespace etcd {
class Event {
enum class EventType {
PUT, // 键值对新增或数据发生改变
DELETE_, // 键值对被删除
INVALID,
};

enum EventType event_type(); // 事件类型
const Value& kv(); // 当前键值对的数据
const Value& prev_kv(); // 改变前键值对的数据
};
}

4.3 Response — 请求响应

namespace etcd {
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(); // 触发的事件列表

using Values = std::vector<Value>;
Values const& values() const; // 多组数据的响应结果(针对目录)
};
}

4.4 Client — Etcd 客户端

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");

// 新增一个键值对
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> get(std::string const& key);

// 获取指定 key 目录下的数据列表
pplx::task<Response> ls(std::string const& key);

// 创建并获取一个存活 ttl 时间的租约
pplx::task<Response> leasegrant(int ttl);

// 获取一个租约保活对象
pplx::task<std::shared_ptr<KeepAlive>> leasekeepalive(int ttl);

// 删除指定 key(不存在时返回 ERROR_KEY_NOT_FOUND)
pplx::task<Response> rm(std::string const& key);

// 删除指定目录(不存在时返回 ERROR_KEY_NOT_FOUND)
pplx::task<Response> rmdir(std::string const& key, bool recursive = false);

// 监控一个 key 或子目录的变化
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);

// 执行事务
pplx::task<Response> txn(etcdv3::Transaction const& txn);
};
}

关于 pplx::task:这是微软并行库的异步结果对象。

  • 阻塞方式:get() — 阻塞直到任务执行完成,并获取任务结果
  • 非阻塞方式:wait() — 等待任务到达终止状态,然后返回任务状态

4.5 KeepAlive — 租约保活

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);

int64_t Lease(); // 返回租约 ID
void Cancel(); // 停止保活
};
}

4.6 Watcher — 数据监控

namespace etcd {
class Watcher {
Watcher(Client const& client,
std::string const& 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 Wait(std::function<void(bool)> callback); // 异步非阻塞,任务取消时回调
bool Cancel(); // 取消监控
};
}


5. 入门示例

5.1 目录结构

.
├── get.cc
├── put.cc
└── makefile

5.2 put.cc — 写入数据(带租约保活)

#include <etcd/Client.hpp>
#include <etcd/Response.hpp>
#include <etcd/KeepAlive.hpp>
#include <thread>

void put(const std::string &host,
const std::string &key,
const std::string &val,
std::shared_ptr<etcd::KeepAlive> &keepalive,
int ttl = 3) {

etcd::Client client(host);

// 1. 先创建一个租约
auto lease_resp = client.leasegrant(ttl).get();
if (lease_resp.is_ok() == false) {
std::cout << "申请租约失败:" << lease_resp.error_message() << std::endl;
exit(1);
}
auto lease_id = lease_resp.value().lease();

// 2. 使用租约写入键值对
auto resp = client.put(key, val, lease_id).get();
if (resp.is_ok() == false) {
std::cout << "新增数据失败: " << resp.error_message() << std::endl;
exit(1);
}

// 3. 设置保活回调(租约过期后自动重新注册)
auto handler = [host, key, val, &keepalive, ttl](std::exception_ptr eptr) {
try {
put(host, key, val, keepalive, ttl);
} catch (std::exception &e) {
std::cout << "Etcd KeepAlive 异常: " << e.what() << std::endl;
exit(1);
}
};
keepalive.reset(new etcd::KeepAlive(host, handler, ttl, lease_id));
}

int main() {
std::string etcd_host = "http://192.168.65.128:2379";
std::string key = "/test/instance";
std::string val = "112.23.23.120:9090";
std::shared_ptr<etcd::KeepAlive> keepalive;

put(etcd_host, key, val, keepalive);

std::cout << "回车后退出….\\n";
getchar();
return 0;
}

5.3 get.cc — 获取数据并监控变化

#include <etcd/Client.hpp>
#include <etcd/Watcher.hpp>

// Watcher 回调函数:处理数据变化通知
void watcherCallback(etcd::Response const& resp) {
if (resp.error_code()) {
std::cout << "监视出错: " << resp.error_code();
std::cout << "-" << resp.error_message() << std::endl;
} else {
for (auto &ev : resp.events()) {
if (ev.event_type() == etcd::Event::EventType::PUT) {
// 新增或修改:通过当前值查看
std::cout << ev.kv().key() << " 新增/修改数据:";
std::cout << ev.kv().as_string() << std::endl;
} else if (ev.event_type() == etcd::Event::EventType::DELETE_) {
// 删除:通过改变前的值了解哪个值被删除
std::cout << ev.prev_kv().key() << " 删除数据:";
std::cout << ev.prev_kv().as_string() << std::endl;
}
}
}
}

void watch(const std::string &host,
const std::string &key,
std::shared_ptr<etcd::Watcher> &watcher) {

etcd::Client etcd(host);

// 1. 先列出当前已有数据
auto resp = etcd.ls(key).get();
if (resp.is_ok()) {
for (int i = 0; i < resp.keys().size(); i++) {
std::cout << resp.key(i) << " = ";
std::cout << resp.value(i).as_string() << std::endl;
}
} else {
std::cout << "获取数据出错: " << resp.error_code();
std::cout << "-" << resp.error_message() << std::endl;
}

// 2. 设置 Watcher 监控数据变化(recursive=true 递归监控子目录)
auto cb = std::bind(watcherCallback, std::placeholders::_1);
watcher.reset(new etcd::Watcher(host, key, cb, true));

// 3. 异步等待,Watcher 取消时自动重连
watcher->Wait([host, key, &watcher](bool cancelled) {
if (cancelled) {
std::cout << "Etcd Watcher 重连被主动取消!" << std::endl;
return;
}
watch(host, key, watcher);
});
}

int main() {
std::string etcd_host = "http://192.168.65.128:2379";
std::string key = "/test";
std::shared_ptr<etcd::Watcher> watcher;

watch(etcd_host, key, watcher);

std::cout << "回车后退出….\\n";
getchar();
return 0;
}

5.4 Makefile

all: get put

get: get.cc
g++ -std=c++17 $^ -o $@ -lcpprest -letcd-cpp-api

put: put.cc
g++ -std=c++17 $^ -o $@ -lcpprest -letcd-cpp-api

5.5 运行演示

$ make
$ ./put &
回车后退出....

$ ./get
/test/instance = 112.23.23.120:9090
回车后退出....
/test/instance 删除数据:112.23.23.120:9090 # put 进程退出后,租约过期自动删除


6. 服务管理封装

Etcd 作为一个内存键值存储中心,且提供了数据改变通知功能,非常适合作为服务注册发现中心。典型的服务管理包括三个操作:

  • 服务注册:服务启动时,向 Etcd 注册服务名称和访问地址的键值对
  • 服务发现:客户端通过 Etcd 获取服务的访问地址信息
  • 健康检查:服务定期向 Etcd 发送心跳(租约保活),维持注册信息的有效性
  • 6.1 设计思路

    组件职责
    SvcProvider(服务注册) 将 {/服务名称/实例ID, 访问地址} 的键值对注册到 Etcd,并通过 KeepAlive 维持租约
    SvcWatcher(服务发现) 通过 ls 和 watch 监控 / 目录,感知所有服务的上线和下线

    6.2 服务注册客户端 — SvcProvider

    class SvcProvider {
    public:
    using ptr = std::shared_ptr<SvcProvider>;

    SvcProvider(const std::string &reg_center_addr,
    const std::string &svc_name,
    const std::string &svc_addr);

    // 注册服务,如:registry("user", "192.168.65.130:9000")
    // 实际 key = /user/<instance_id>, value = 192.168.65.130:9000
    bool registry();

    private:
    std::string make_key(); // 生成 key: /<svc_name>/<instance_id>

    private:
    std::string _reg_center_addr; // 注册中心地址
    std::string _instance_id; // 当前节点的唯一标识
    std::string _svc_name; // 提供的服务名称
    std::string _svc_addr; // 节点地址
    std::shared_ptr<etcd::KeepAlive> _keepalive; // 租约保活对象
    };

    核心实现:

    SvcProvider::SvcProvider(const std::string &reg_center_addr,
    const std::string &svc_name,
    const std::string &svc_addr)
    : _reg_center_addr(reg_center_addr)
    , _instance_id(biteutil::Random::code())
    , _svc_name(svc_name)
    , _svc_addr(svc_addr) {}

    std::string SvcProvider::make_key() {
    std::stringstream ss;
    ss << "/" << _svc_name << "/" << _instance_id;
    return ss.str();
    }

    bool SvcProvider::registry() {
    // 1. 实例化 etcd 客户端对象
    etcd::Client client(_reg_center_addr);
    wait_for_connection(client);

    // 2. 创建 3s 租约
    auto lease_resp = client.leasegrant(3).get();
    if (lease_resp.is_ok() == false) {
    ERR("创建租约失败: {}", lease_resp.error_message());
    return false;
    }
    auto lease_id = lease_resp.value().lease();

    // 3. 向服务器添加数据(带租约)
    auto resp = client.put(make_key(), _svc_addr, lease_id).get();
    if (resp.is_ok() == false) {
    ERR("添加数据失败: {}", lease_resp.error_message());
    return false;
    }

    // 4. 实例化保活对象,对租约进行保活
    auto handler = [this](const std::exception_ptr &eptr) {
    this->registry(); // 异常时自动重新注册
    };
    _keepalive.reset(new etcd::KeepAlive(_reg_center_addr, handler, 3, lease_id));
    return true;
    }

    连接等待辅助函数:

    void wait_for_connection(etcd::Client &client) {
    while (!client.head().get().is_ok()) {
    WRN("连接 etcd 服务器失败…");
    std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    }

    6.3 服务发现客户端 — SvcWatcher

    class SvcWatcher {
    public:
    using ptr = std::shared_ptr<SvcWatcher>;
    // 回调函数类型:参数1-服务名称,参数2-节点地址
    using ModCallbck = std::function<void(std::string, std::string)>;

    SvcWatcher(const std::string &reg_center_addr,
    ModCallbck &&online_callback,
    ModCallbck &&offline_callback);

    bool watch(); // 开始监控

    private:
    void callback(const etcd::Response &resp);
    std::string parse_key(const std::string &key); // 从 key 中提取服务名称

    private:
    std::string _reg_center_addr; // 注册中心地址
    ModCallbck _online_callback; // 服务上线回调
    ModCallbck _offline_callback; // 服务下线回调
    std::shared_ptr<etcd::Watcher> _watcher; // 监控对象
    };

    核心实现:

    // 从 key 中解析服务名称,需与注册时的格式对应
    // key 格式: /user/instance_id → 解析出 "user"
    std::string SvcWatcher::parse_key(const std::string &key) {
    std::vector<std::string> arry;
    biteutil::STR::split(key, "/", arry);
    return arry[0];
    }

    void SvcWatcher::callback(const etcd::Response &resp) {
    if (resp.is_ok() == false) {
    ERR("监控出错: {}", resp.error_message());
    return;
    }
    const auto &events = resp.events();
    for (auto &e : events) {
    if (e.event_type() == etcd::Event::EventType::PUT) {
    std::string svc_name = parse_key(e.kv().key());
    std::string svc_addr = e.kv().as_string();
    DBG("{} 服务上线节点: {}", svc_name, svc_addr);
    if (_online_callback) _online_callback(svc_name, svc_addr);

    } else if (e.event_type() == etcd::Event::EventType::DELETE_) {
    std::string svc_name = parse_key(e.prev_kv().key());
    std::string svc_addr = e.prev_kv().as_string();
    DBG("{} 服务下线节点: {}", svc_name, svc_addr);
    if (_offline_callback) _offline_callback(svc_name, svc_addr);

    } else {
    WRN("无效通知类型");
    }
    }
    }

    bool SvcWatcher::watch() {
    // 1. 连接 etcd 服务器
    etcd::Client client(_reg_center_addr);
    wait_for_connection(client);

    // 2. 浏览根目录,获取当前所有已注册的服务节点
    auto resp = client.ls("/").get();
    if (resp.is_ok() == true) {
    auto values = resp.values();
    for (auto &v : values) {
    std::string svc_name = parse_key(v.key());
    std::string svc_addr = v.as_string();
    DBG("已有 {} 节点: {}", svc_name, svc_addr);
    if (_online_callback) _online_callback(svc_name, svc_addr);
    }
    }

    // 3. 递归监控根目录,获取服务上下线的实时通知
    auto cb = std::bind(&SvcWatcher::callback, this, std::placeholders::_1);
    _watcher.reset(new etcd::Watcher(_reg_center_addr, "/", cb, true));

    _watcher->Wait([this](bool cond) {
    if (cond == true) { return; }
    this->watch(); // Watcher 断开时自动重连
    });
    return true;
    }


    7. 完整使用示例

    7.1 目录结构

    .
    ├── CMakeLists.txt
    ├── discovery.cc
    └── registry.cc

    7.2 registry.cc — 服务注册

    #include "bite_scaffold/etcd.h"
    #include "bite_scaffold/log.h"
    #include <iostream>

    int main() {
    bitelog::bitelog_init();

    const std::string url = "http://192.168.65.128:2379";
    const std::string svc_name = "user";
    const std::string svc_addr = "192.168.65.128:9000";

    // 1. 实例化服务提供者对象
    bitesvc::SvcProvider provider(url, svc_name, svc_addr);
    // 2. 注册服务
    provider.registry();

    getchar();
    return 0;
    }

    7.3 discovery.cc — 服务发现

    #include "bite_scaffold/etcd.h"
    #include "bite_scaffold/log.h"
    #include <iostream>

    void online(const std::string &svc_name, const std::string &svc_addr) {
    INF("新服务 {} 上线节点: {}", svc_name, svc_addr);
    }

    void offline(const std::string &svc_name, const std::string &svc_addr) {
    INF("新服务 {} 下线节点: {}", svc_name, svc_addr);
    }

    int main() {
    bitelog::bitelog_init();

    const std::string url = "http://192.168.65.128:2379";

    // 1. 实例化服务监控者对象
    bitesvc::SvcWatcher watcher(url, online, offline);
    // 2. 开始发现服务
    watcher.watch();

    getchar();
    return 0;
    }

    7.4 CMakeLists.txt

    cmake_minimum_required(VERSION 3.1.3)
    project(discovery VERSION 1.0)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++17")

    set(server_target "discovery")
    add_executable(${server_target} ${CMAKE_CURRENT_SOURCE_DIR}/discovery.cc)

    set(client_target "registry")
    add_executable(${client_target} ${CMAKE_CURRENT_SOURCE_DIR}/registry.cc)

    find_package(bite_scaffold REQUIRED)
    target_link_libraries(${server_target} PRIVATE bite_scaffold::bite_scaffold)
    target_link_libraries(${client_target} PRIVATE bite_scaffold::bite_scaffold)

    7.5 运行演示

    $ mkdir build && cd build
    $ cmake ..
    $ make

    # 先启动服务发现端
    $ ./discovery &
    [06:16:10][debug]: user 服务上线节点: 192.168.65.128:9000
    [06:16:10][info]: 新服务 user 上线节点: 192.168.65.128:9000

    # 再启动服务注册端
    $ ./registry

    当 registry 退出(租约过期)时,discovery 端会收到服务下线通知。


    8. 总结

    本文系统介绍了基于 Etcd 的 C++ 服务注册与发现方案:

    层次内容
    基础设施 Etcd 安装、配置、集群部署
    SDK 层 etcd-cpp-apiv3 的安装与核心 API(Client、Value、Event、Response)
    基础用法 put/get 操作、租约(LeaseGrant)、保活(KeepAlive)、监控(Watcher)
    封装层 SvcProvider(服务注册 + 保活自动重连)、SvcWatcher(服务发现 + 递归监控 + 断线重连)
    架构要点 key 命名规范(/服务名/实例ID)、租约 TTL 设置、回调解耦、目录递归监控

    掌握了这些知识,你就可以在微服务架构中利用 Etcd 实现可靠的服务注册与发现,为分布式系统提供动态的服务路由能力。

    赞(0)
    未经允许不得转载:171主机测评 » 【C++脚手架】etcd 的介绍与使用
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址