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因网络故障等原因意外停止(cancel为false),会递归调用get函数重新建立监控,确保服务发现的可靠性。如果是被主动取消(cancel为true),则直接返回。
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