在使用etcd之前肯定要了解它是什么,为什么要选择使用它?
什么是 etcd?
Etcd 是一个 golang 编写的分布式、高可用的一致性键值存储系统,用于配置共享和服务发现等。它使用 Raft 一致性算法来保持集群数据的一致性,且客户端通过长连接watch 功能,能够及时收到数据变化通知,相较于 Zookeeper 框架更加轻量化。
它主要用于:
- 存储关键配置信息
- 服务发现
- 分布式锁
- 集群协调
etcd 就像是分布式系统的 “共享内存”,让多个节点能够安全可靠地共享状态信息。
通俗理解 ——etcd 就像一个可以跨进程、跨机器、跨网络共享的 “超级 map 容器”
什么场景使用它?
| 适合用 etcd 的场景 | 概括 | 为什么用 etcd |
|---|---|---|
| Kubernetes 集群 | 容器集群的 “大脑” | 存集群状态,保证所有组件数据一致 |
| 配置中心 | 多服务共享的配置 | 改一次全同步,不用重启服务 |
| 服务发现 | 找可用的服务地址 | 服务上下线自动更新,不用手动改配置 |
| 分布式锁 | 多节点抢资源时排队 | 保证同一时间只有一个节点操作 |
| 全局计数器 | 跨机器统计数字(如总订单数) | 多人同时操作也不会算错 |
安装 etcd
首先,需要在你的系统中安装 Etcd。Etcd 是一个分布式键值存储,通常用于服务发现
和配置管理。以下是在 Linux 系统上安装 Etcd 的基本步骤:
安装 Etcd:
sudo apt-get install etcd
启动 Etcd 服务:
sudo systemctl start etcd
设置 Etcd 开机自启:
sudo systemctl enable etcd
运行验证
etcdctl put mykey "this is awesome"
如果出现报错:
No help topic for 'put'
则 sudo vi /etc/profile 在末尾声明环境变量 ETCDCTL_API=3 以确定 etcd 版本。
export ETCDCTL_API=3
完毕后,加载配置文件,并重新执行测试指令
ubuntu@VM-16-12-ubuntu:~/code$ source /etc/profile
ubuntu@VM-16-12-ubuntu:~/code$ etcdctl put mykey "this is awesome"
OK
ubuntu@VM-16-12-ubuntu:~/code$ etcdctl get mykey
mykey
this is awesome
ubuntu@VM-16-12-ubuntu:~/code$ etcdctl del mykey
搭建服务注册发现中心
使用 Etcd 作为服务注册发现中心,你需要定义服务的注册和发现逻辑。这通常涉及到
以下几个操作:
- 服务注册:服务启动时,向 Etcd 注册自己的地址和端口。
- 服务发现:客户端通过 Etcd 获取服务的地址和端口,用于远程调用。
- 健康检查:服务定期向 Etcd 发送心跳,以维持其注册信息的有效性。
etcd 采用 golang 编写,v3 版本通信采用 grpc API,即(HTTP2+protobuf);官方只维护了 go 语言版本的
client 库, 因此需要找到 C/C++ 非官方的 client 开发库:etcd-cpp-apiv3
etcd-cpp-apiv3 是一个 etcd 的 C++版本客户端 API。它依赖于 mipsasm, boost, protobuf, gRPC, cpprestsdk 等库。
etcd-cpp-apiv3 的 GitHub 地址是:https://github.com/etcd-cpp-apiv3/etcd-cpp-apiv3
依赖安装:
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
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
etcd操作命令
基本键值操作
# 写入键值对
etcdctl put /message "Hello etcd"
# 读取键值
etcdctl get /message
# 读取键的详细信息(包括版本等元数据)
etcdctl get /message -w json
# 读取前缀匹配的所有键
etcdctl get / --prefix
# 删除键
etcdctl del /message
# 删除前缀匹配的所有键
etcdctl del / --prefix
监听键变化
etcd 的 Watch 机制可以实时监听键的变化:
# 监听单个键
etcdctl watch /config/timeout
# 在另一个终端执行,触发上面的监听
etcdctl put /config/timeout 30s
# 监听前缀
etcdctl watch /config --prefix
租约与临时键
租约 (Lease) 机制可以创建自动过期的临时键,非常适合服务注册等场景:
# 创建一个30秒的租约
LEASE_ID=$(etcdctl lease grant 30 | awk '{print $2}')
echo "Lease ID: $LEASE_ID"
# 创建绑定到租约的键
etcdctl put /services/api-server "192.168.1.100:8080" --lease=$LEASE_ID
# 续租(延长租约有效期)
etcdctl lease keep-alive $LEASE_ID
# 撤销租约(立即删除所有绑定的键)
etcdctl lease revoke $LEASE_ID
事务操作
etcd 支持条件事务,实现 Compare-And-Swap 等原子操作:
# 事务:如果/message的值是"Hello etcd",则更新为"Hello world",否则不做操作
etcdctl txn --interactive
compares:
value("/message") = "Hello etcd"
success requests (get, put, del):
put("/message", "Hello world")
failure requests:
get("/message")
etcd 简单使用流程
客户端类与接口预览
C++
//pplx::task 并行库异步结果对象
//阻塞方式 get(): 阻塞直到任务执行完成,并获取任务结果
//非阻塞方式 wait(): 等待任务到达终止状态,然后返回任务状态
namespace etcd {
class Value
{
bool is_dir();//判断是否是一个目录
std::string const&
key() //键值对的 key 值
std::string const&
as_string()//键值对的 val 值
int64_t lease() //用于创建租约的响应中,返回租约 ID
}
//etcd 会监控所管理的数据的变化,一旦数据产生变化会通知客户端
//在通知客户端的时候,会返回改变前的数据和改变后的数据
class Event
{
enum class EventType
{
PUT, //键值对新增或数据发生改变
DELETE_,//键值对被删除
INVALID,
};
enum EventType event_type()
const Value&
kv()
const Value&
prev_kv()
}
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();
//触发的事件
}
class KeepAlive
{
KeepAlive(Client const& client, int ttl, int64_t lease_id =
0);
//返回租约 ID
int64_t Lease();
//停止保活动作
void Cancel();
}
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);
//获取一个指定 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);
//撤销一个指定的租约
pplx::task<Response>
leaserevoke(int64_t lease_id);
//数据锁
pplx::task<Response>
lock(std::string const& key);
}
class Watcher
{
Watcher(Client const& client,
std::string const& key, //要监控的键值对 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 Cancel();
}
etcd 作为分布式键值存储,核心常用场景是 “数据写入(注册)” 与 “数据读取 + 变化监听(发现)”。下面结合 put.cc(写入 / 注册)和 get.cc(读取 / 监听)两份代码,一步步拆解 etcd 的简单使用流程,从环境准备到功能验证全覆盖。
put.cc :
- 定义 etcd 地址 (http://127.0.0.1:2379)
- 创建客户端对象 Client
- 创建租约并保活 client.leasekeepalive(3).get();
- 写入键值对(两种方式,有租约/没有租约) client.put
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <thread>
int main(int argc, char *argv[])
{
std::string etcd_host = "http://127.0.0.1:2379";
//实例化客户端对象
etcd::Client client(etcd_host);
//获取租约保活对象--伴随着创建一个指定有效时长的租约
auto keep_alive = client.leasekeepalive(3).get();
//获取租约ID
auto lease_id = keep_alive->
Lease();
//向etcd新增数据
//1.有租约的数据
auto resp1 = client.put("/service/user", "127.0.0.1:8080", lease_id).get();
if (resp1.is_ok() == false) {
std::cout <<
"新增数据失败:" << resp1.error_message() << std::endl;
return -1;
}
//2.没有租约的数据
auto resp2 = client.put("/service/friend", "127.0.0.1:9090").get();
if (resp2.is_ok() == false) {
std::cout <<
"新增数据失败:" << resp2.error_message() << std::endl;
return -1;
}
std::this_thread::sleep_for(std::chrono::seconds(10));
return 0;
}
get.cc :
- 定义回调函数 callback (处理监控到的键值对事件)
- 连接 etcd + 读数据 ( /service 路径下的所有键值对信息)
- 启动监听 (watcher)
- 调用 watcher.Wait() 使程序保持运行以持续监控。
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <thread>
void callback(const etcd::Response &resp) {
if (resp.is_ok() == false) {
std::cout <<
"收到一个错误的事件通知:" << resp.error_message() << std::endl;
return;
}
for (auto const& ev : resp.events()) {
if (ev.event_type() == etcd::Event::EventType::PUT) {
std::cout <<
"服务信息发生了改变:\n" ;
std::cout <<
"当前的值:" << ev.kv().key() <<
"-" << ev.kv().as_string() << std::endl;
std::cout <<
"原来的值:" << ev.prev_kv().key() <<
"-" << ev.prev_kv().as_string() << std::endl;
}else if (ev.event_type() == etcd::Event::EventType::DELETE_) {
std::cout <<
"服务信息下线被删除:\n";
std::cout <<
"当前的值:" << ev.kv().key() <<
"-" << ev.kv().as_string() << std::endl;
std::cout <<
"原来的值:" << ev.prev_kv().key() <<
"-" << ev.prev_kv().as_string() << std::endl;
}
}
}
int main(int argc, char *argv[])
{
std::string etcd_host = "http://127.0.0.1:2379";
//实例化客户端对象
etcd::Client client(etcd_host);
//获取指定的键值对信息
auto resp = client.ls("/service").get();
if (resp.is_ok() == false) {
std::cout <<
"获取键值对数据失败: " << resp.error_message() << std::endl;
return -1;
}
int sz = resp.keys().size();
for (int i = 0; i < sz;
++i) {
std::cout << resp.value(i).as_string() <<
"可以提供" << resp.key(i) <<
"服务\n";
}
//实例化一个键值对事件监控对象
auto watcher = etcd::Watcher(client, "/service", callback, true);
watcher.Wait();
return 0;
}
etcd的头文件
| 头文件 | 核心作用 | 包含的主要类 / 接口 |
|---|---|---|
| <etcd/Client.hpp> | etcd 客户端核心类,所有与 etcd 的交互入口 | etcd::Client:客户端主类,提供键值操作(put/get/del)、租约管理(leasegrant)、事务(txn)等所有核心接口,通过客户端对象连接 etcd 服务,发起各种请求 |
| <etcd/KeepAlive.hpp> | 租约保活管理,处理租约的自动续期 | etcd::KeepAlive:租约保活类,通过 leasekeepalive() 创建,负责定期向 etcd 发送续期请求,维持租约有效,提供 Lease() 获取租约 ID、Cancel() 停止保活等接口 |
| <etcd/Response.hpp> | 请求响应处理,封装 etcd 服务器的返回结果 | etcd::Response:响应类,包含请求的执行结果(成功 / 失败)、返回数据(键值对、事件、租约信息等),提供 is_ok()(判断成功)、error_message()(错误信息)、kv()(键值数据)等方法解析响应 |
| <etcd/Watcher.hpp> | 数据变化监听,实时捕获键的新增 / 删除 / 修改 | etcd::Watcher:监听器类,通过指定键或前缀创建监听,当数据变化时触发回调函数 |
| <etcd/Value.hpp> | 键值数据封装,存储单个键值对的详细信息 | etcd::Value(或 etcd::KeyValue):键值对类,包含键(key())、值(as_string())、版本(version())、租约 ID(lease())等元数据,用于从 Response 中提取具体的键值信息 |
etcd地址
http://127.0.0.1:2379 是 etcd 服务的默认本地地址
实际场景中地址会如何变化?
- 远程服务器部署:如果 etcd 部署在另一台服务器(如 IP 192.168.1.100),地址会变为 http://192.168.1.100:2379。
- 集群模式:etcd 集群由多个节点组成时,客户端地址通常是多个节点的地址列表(用逗号分隔),例如:
std::string etcd_host = "http://node1:2379,http://node2:2379,http://node3:2379";
这样即使某个节点故障,客户端仍能连接到其他节点。
- 自定义端口:如果启动时通过 --listen-client-urls 参数修改了端口(如改为 2380),地址会变为 http://xxx.xxx.xxx.xxx:2380。
注意:这里的地etcd地址属于虚地址,实际存储在 etcd 集群的磁盘上
租约
etcd 租约(Lease)是一种带有效期的资源管理机制。
核心作用是为 etcd 中的键值对(Key-Value)绑定 “生命周期”—— 租约到期或失效时,所有绑定它的键值对会被自动删除,无需手动清理。
它就像给数据加了 “定时自动删除” 功能,尤其适合解决分布式场景下的 “僵尸数据” 问题(如服务下线后残留的注册信息)。
租约的核心概念
- 租约 ID(Lease ID):每个租约的唯一标识,由 etcd 自动生成(或手动指定),用于关联键值对。
- TTL(Time-To-Live):租约的有效期(单位:秒),如 3 秒、30 秒,到期后租约自动失效。
- 保活(KeepAlive):若想延长租约有效期,客户端需定期向 etcd 发送 “保活请求”,etcd 收到后会重置租约的 TTL,相当于 “续期”。
- 键值绑定:一个租约可绑定多个键值对,所有绑定的键会随租约失效而同步删除。
makefile:
all : put get
put : put.cc
g++ -std=c++17 $^ -o $@ -letcd-cpp-api -lcpprest
get : get.cc
g++ -std=c++17 $^ -o $@ -letcd-cpp-api -lcpprest
.PHONY:clean
clean:
rm -f all


基于 etcd 的服务注册与发现模块
通过封装 etcd 的 C++ 客户端接口,实现了分布式系统中常见的服务注册与发现功能:
服务注册:将服务信息(键值对)注册到 etcd,并通过租约(lease)机制维持服务活性。
服务发现:从 etcd 中获取指定目录下的服务信息,并监控该目录的变化(新增 / 删除服务),通过回调函数通知上层处理。
#pragma once
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <functional>
#include "logger.hpp" // 自定义的log日志类
namespace bite_im{
//服务注册客户端类
class Registry
{
public:
using ptr = std::shared_ptr<Registry>
;
Registry(const std::string &host):
_client(std::make_shared<etcd::Client>
(host)) ,
_keep_alive(_client->
leasekeepalive(3).get()),
_lease_id(_keep_alive->
Lease()){
}
~Registry() { _keep_alive->
Cancel();
}
bool registry(const std::string &key, const std::string &val) {
auto resp = _client->
put(key, val, _lease_id).get();
if (resp.is_ok() == false) {
LOG_ERROR("注册数据失败:{}", resp.error_message());
return false;
}
return true;
}
private:
std::shared_ptr<etcd::Client> _client;
std::shared_ptr<etcd::KeepAlive> _keep_alive;
uint64_t _lease_id;
};
//服务发现客户端类
class Discovery
{
public:
using ptr = std::shared_ptr<Discovery>
;
using NotifyCallback = std::function<
void(std::string, std::string)>
;
Discovery(const std::string &host,
const std::string &basedir,
const NotifyCallback &put_cb,
const NotifyCallback &del_cb):
_client(std::make_shared<etcd::Client>
(host)) ,
_put_cb(put_cb), _del_cb(del_cb){
//先进行服务发现,先获取到当前已有的数据
auto resp = _client->
ls(basedir).get();
if (resp.is_ok() == false) {
LOG_ERROR("获取服务信息数据失败:{}", resp.error_message());
}
int sz = resp.keys().size();
for (int i = 0; i < sz;
++i) {
if (_put_cb) _put_cb(resp.key(i), resp.value(i).as_string());
}
//然后进行事件监控,监控数据发生的改变并调用回调进行处理
_watcher = std::make_shared<etcd::Watcher>
(*_client.get(), basedir,
std::bind(&Discovery::callback, this, std::placeholders::_1), true);
}
~Discovery() {
_watcher->
Cancel();
}
private:
void callback(const etcd::Response &resp) {
if (resp.is_ok() == false) {
LOG_ERROR("收到一个错误的事件通知: {}", resp.error_message());
return;
}
for (auto const& ev : resp.events()) {
if (ev.event_type() == etcd::Event::EventType::PUT) {
if (_put_cb) _put_cb(ev.kv().key(), ev.kv().as_string());
LOG_DEBUG("新增服务:{}-{}", ev.kv().key(), ev.kv().as_string());
}else if (ev.event_type() == etcd::Event::EventType::DELETE_) {
if (_del_cb) _del_cb(ev.prev_kv().key(), ev.prev_kv().as_string());
LOG_DEBUG("下线服务:{}-{}", ev.prev_kv().key(), ev.prev_kv().as_string());
}
}
}
private:
NotifyCallback _put_cb;
NotifyCallback _del_cb;
std::shared_ptr<etcd::Client> _client;
std::shared_ptr<etcd::Watcher> _watcher;
};
}
关键类与成员解析
1. Registry 类(服务注册)
class Registry
{
public:
using ptr = std::shared_ptr<Registry>
;
Registry(const std::string &host):
_client(std::make_shared<etcd::Client>
(host)) ,
_keep_alive(_client->
leasekeepalive(3).get()),
_lease_id(_keep_alive->
Lease()){
}
~Registry() { _keep_alive->
Cancel();
}
bool registry(const std::string &key, const std::string &val) {
auto resp = _client->
put(key, val, _lease_id).get();
if (resp.is_ok() == false) {
LOG_ERROR("注册数据失败:{}", resp.error_message());
return false;
}
return true;
}
private:
std::shared_ptr<etcd::Client> _client;
std::shared_ptr<etcd::KeepAlive> _keep_alive;
uint64_t _lease_id;
};
功能:向 etcd 注册服务,并通过租约机制自动维持服务心跳,确保服务下线时自动注销。
核心成员:
- _client:etcd 客户端实例,用于与 etcd 服务器通信。
- _keep_alive:租约保活器,维持租约的有效性(定时向 etcd 发送心跳)。
- _lease_id:租约 ID,绑定服务注册的键值对,租约过期后键值对自动删除。
主要方法:
- 构造函数:初始化 etcd 客户端,创建一个 3 秒过期的租约,并启动保活机制。
- registry 方法:将键值对(服务信息)注册到 etcd,关联当前租约 ID。
- 析构函数:取消租约保活,使服务信息在租约过期后自动删除。
2. Discovery 类(服务发现)
//服务发现客户端类
class Discovery
{
public:
using ptr = std::shared_ptr<Discovery>
;
using NotifyCallback = std::function<
void(std::string, std::string)>
;
Discovery(const std::string &host,
const std::string &basedir,
const NotifyCallback &put_cb,
const NotifyCallback &del_cb):
_client(std::make_shared<etcd::Client>
(host)) ,
_put_cb(put_cb), _del_cb(del_cb){
//先进行服务发现,先获取到当前已有的数据
auto resp = _client->
ls(basedir).get();
if (resp.is_ok() == false) {
LOG_ERROR("获取服务信息数据失败:{}", resp.error_message());
}
int sz = resp.keys().size();
for (int i = 0; i < sz;
++i) {
if (_put_cb) _put_cb(resp.key(i), resp.value(i).as_string());
}
//然后进行事件监控,监控数据发生的改变并调用回调进行处理
_watcher = std::make_shared<etcd::Watcher>
(*_client.get(), basedir,
std::bind(&Discovery::callback, this, std::placeholders::_1), true);
}
~Discovery() {
_watcher->
Cancel();
}
private:
void callback(const etcd::Response &resp) {
if (resp.is_ok() == false) {
LOG_ERROR("收到一个错误的事件通知: {}", resp.error_message());
return;
}
for (auto const& ev : resp.events()) {
if (ev.event_type() == etcd::Event::EventType::PUT) {
if (_put_cb) _put_cb(ev.kv().key(), ev.kv().as_string());
LOG_DEBUG("新增服务:{}-{}", ev.kv().key(), ev.kv().as_string());
}else if (ev.event_type() == etcd::Event::EventType::DELETE_) {
if (_del_cb) _del_cb(ev.prev_kv().key(), ev.prev_kv().as_string());
LOG_DEBUG("下线服务:{}-{}", ev.prev_kv().key(), ev.prev_kv().as_string());
}
}
}
private:
NotifyCallback _put_cb;
NotifyCallback _del_cb;
std::shared_ptr<etcd::Client> _client;
std::shared_ptr<etcd::Watcher> _watcher;
};
功能:从 etcd 中发现指定目录下的服务信息,并实时监控该目录的变化(新增 / 删除服务)。
核心成员:
- _client:etcd 客户端实例。
- _watcher:监听器,监控指定目录的键值对变化。
- _put_cb/_del_cb:回调函数,分别在服务新增(PUT 事件)和服务下线(DELETE 事件)时触发。
主要方法:
- 构造函数:初始化客户端,先获取指定目录下已有的所有服务信息并通过 - _put_cb 回调通知;然后启动监听器,监控后续变化。
- callback 方法:内部事件处理函数,解析 etcd 推送的事件(PUT/DELETE),并调用对应的回调函数。
- 析构函数:取消监听器,停止监控。
3. 测试
registry.cc
#include "etcd.hpp"
#include <gflags/gflags.h>
int main(int argc, char *argv[]){
google::ParseCommandLineFlags(&argc, &argv, true);
init_logger(false, " ", 0);
Registry::ptr rclient = std::make_shared<Registry>
("http://127.0.0.1:2379");
rclient->
registry("/service/user/instance", "127.0.0.1:8080");
std::this_thread::sleep_for(std::chrono::seconds(600));
return 0;
}
discovery.cc
#include "etcd.hpp"
#include <gflags/gflags.h>
void online(const std::string &service_name, const std::string &service_host) {
LOG_DEBUG("上线服务: {}-{}", service_name, service_host);
}
void offline(const std::string &service_name, const std::string &service_host) {
LOG_DEBUG("上线服务: {}-{}", service_name, service_host);
}
int main(int argc, char *argv[]){
google::ParseCommandLineFlags(&argc, &argv, true);
init_logger(false, " ", 0);
Discovery::ptr rclient = std::make_shared<Discovery>
("http://127.0.0.1:2379", "/service", online, offline);
std::this_thread::sleep_for(std::chrono::seconds(600));
return 0;
}
makefile:
all : put get
put : registry.cc
g++ -std=c++17 $^ -o $@ -lspdlog -lfmt -lgflags -letcd-cpp-api -lcpprest
get : discovery.cc
g++ -std=c++17 $^ -o $@ -lspdlog -lfmt -lgflags -letcd-cpp-api -lcpprest
.PHONY:clean
clean:
rm -f put get

9090是上次测试遗留的
浙公网安备 33010602011771号