介绍
本文将从 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 版本配置到系统环境变量中,步骤如下:
-
编辑系统环境变量配置文件 打开
/etc/profile
(该文件对所有用户生效,若仅需当前用户生效,可编辑~/.bashrc
):bash
sudo vim /etc/profile
-
添加 API 版本配置在文件末尾添加以下内容,指定 Etcd 客户端 API 版本为 3:
bash
export ETCDCTL_API=3
-
加载配置使其生效执行以下命令,无需重启系统即可让配置生效:
bash
source /etc/profile
-
验证配置 重新执行
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 客户端(智能指针管理