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 版本配置到系统环境变量中,步骤如下:
编辑系统环境变量配置文件打开
/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 客户端(智能指针管理