不会 etcd,别说你懂微服务!一篇打通核心概念

什么是 etcd
etcd 是一款高可用、强一致、分布式键值存储系统,专门用来解决分布式系统里配置怎么存、服务怎么找、集群怎么管的问题。
简单说,etcd 就是分布式架构里的 “统一配置中心 + 服务通讯录 + 集群大脑”。它把多台机器之间的数据一致性、节点故障、配置同步、服务发现全部封装好,让你不用手写分布式协议、不用处理复杂的集群逻辑,就能拥有一个永远在线、数据永远一致、变更实时推送的分布式存储。
它的核心就是:在一堆机器里,维护一份大家都认可、永远不丢、永远一致的数据。
开发分布式系统时,你是否也有过这样的烦恼?
想做微服务,结果服务一多,IP 端口写死在配置里,改一个地方要全网更新;想做统一配置,结果服务启动后配置变了,还得重启才能生效;想做集群选主、分布式锁,结果要自己写 Raft、自己处理心跳、自己处理脑裂,几百行代码下去还一堆 Bug;想做服务注册发现,结果服务挂了不知道,调用方还在疯狂请求死节点,线上直接雪崩;更糟的是,不同服务用不同配置来源,有的读文件、有的读数据库,一上线就配置不一致,排查问题查到怀疑人生。
轻则服务重启、配置不同步,重则线上流量异常、集群脑裂、服务不可用。
而 etcd 就是为解决这些痛点而来 —— 这款工业级、云原生标配的分布式键值存储,核心就是让你用极简的 API,快速实现配置管理、服务注册发现、分布式锁、集群选主等所有分布式协调功能。它自带 Raft 强一致性、多节点高可用、实时监听、多版本控制,堪称微服务 & 分布式系统的 “基础设施底座”。
Kubernetes 整个集群都跑在 etcd 上,就足够说明它有多稳、多强。
先一句话讲明白:etcd 到底在干嘛?
我们平时写单机程序,数据存在内存或本地文件:
std::string config = read_from_file("config.json");
在分布式系统里,我们可能希望:
- 多台机器共享同一份配置
- 配置一改所有服务立刻感知
- 服务上线自动登记、下线自动剔除
- 集群里永远只有一个 “老大”(选主)
- 数据绝对不能乱、不能丢、不能不一致
etcd 做的就是这件事:
// 像读本地配置一样,读分布式全局配置
std::string addr = etcd_get("/service/user");
我们完全不用管:
- 数据存在哪台机器
- 挂了一台怎么办
- 怎么保证大家数据一样
- 怎么实时通知变更
etcd 全都帮我们搞定。
- 键值存储:像 Redis / Map 一样,存 key-value
- 强一致:只要写成功,所有节点读到一模一样的最新数据
- 高可用:挂几台不影响整个集群
- Raft 算法:etcd 的 “大脑”,负责选主、同步数据、保证一致
- Watch 监听:key 一变,立刻推送给你,不用轮询
- 租约(Lease):服务一挂,key 自动过期,实现健康检查
- 事务、版本、历史:不怕覆盖,支持回滚
一句话:etcd = 分布式环境里永远可信的唯一数据源。
etcd 最常用的 5 大场景:
-
服务注册与发现:服务启动 → 注册到 etcd调用方 → 从 etcd 拉取地址服务挂了 → 自动摘除
-
统一配置中心:一份配置,全集群生效修改后实时推送到所有服务,不用重启
-
分布式锁:秒杀、限流、任务调度、防重复执行
-
集群选主:多节点里自动选主,主挂了自动切新主
-
云原生存储(K8s 底座)Kubernetes 所有对象:Pod、Service、Deployment… 全存在 etcd
同样是键值对存储机制,我们对比一下其他键值对存储机制的组件:
| 组件 | 定位 | 一致性 | 适合 |
|---|---|---|---|
| etcd | 分布式协调、配置、注册中心 | 强一致(Raft) | 微服务、K8s、配置、锁 |
| Redis | 缓存、高频数据 | 最终一致 | 缓存、会话、计数 |
| ZooKeeper | 分布式协调 | 强一致 | 老一代注册中心 |
| Consul | 服务发现 + 健康检查 | 强一致 | 服务网格、多数据中心 |
现代架构:etcd 几乎是标配。
简单提前上手就不了,我们后面边学习 etcd 再进行相应的代码编写测试!下面我们需要一个前提,那不说也知道 --- 安装!
服务端安装
首先,我们需要在系统中安装 etcd。etcd 是一个分布式键值存储,常用于服务注册发现与统一配置管理,是微服务架构的核心组件。
以下是 Linux 系统上安装 etcd 的基本步骤:
安装 etcd
sudo apt-get install etcd
启动 etcd 服务
sudo systemctl start etcd
设置 etcd 开机自启
sudo systemctl enable etcd
etcd 单节点部署与基础配置【需要可看,否则跳过】
etcd 是一款高可用的分布式键值存储组件,常用于服务发现、配置共享等场景,单节点部署是学习和测试环境的首选方案,无需复杂集群配置,开箱即用。本文将详细讲解 etcd 单节点的配置、启动与基础操作验证,新手也能快速上手。
etcd 默认使用两个固定端口,单节点集群无需修改即可直接使用:
- 2380 端口:集群节点间通信端口(peer 通信)
- 2379 端口:客户端访问端口(client 访问)
单节点配置文件修改
etcd 的默认配置文件路径为 /etc/default/etcd,单节点部署仅需配置基础参数,集群相关配置直接注释即可。
完整配置示例
# 节点名称,默认为 "default"
ETCD_NAME="etcd1"
# 数据存储目录,默认为 "${name}.etcd"
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"
# 对外暴露的节点间通信 URL
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_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"
注意:将配置中的
192.168.65.132替换为你的服务器实际内网 IP。
单节点启动命令
无需依赖配置文件,也可通过命令行参数直接启动 etcd 单节点,后台运行并输出日志:
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,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.65.132:2379,http://127.0.0.1:2379 \
--initial-cluster-token etcd-cluster \
--initial-cluster etcd1=http://192.168.65.132:2380 \
--initial-cluster-state new &> nohup1.out &
命令说明:
&> nohup1.out &:将日志输出到文件并后台常驻运行- 单节点场景下,
--initial-cluster仅需配置自身节点即可
运行验证与常见问题解决
etcd 启动后,使用 etcdctl 客户端工具进行键值对操作,验证服务可用性。
1. 基础操作命令
# 写入键值对
etcdctl put mykey "this is awesome"
# 查询键值对
etcdctl get mykey
# 删除键值对
etcdctl del mykey
2. 报错解决:No help topic for 'put'
如果执行 put 命令出现以下报错:
No help topic for 'put'
原因:etcdctl 默认使用 V2 版本 API,不支持 V3 版本的 put/get/del 命令,需手动指定 API 版本。
解决方案:
编辑环境变量配置文件
sudo vi /etc/profile
在文件末尾添加环境变量
export ETCDCTL_API=3
加载配置文件生效
source /etc/profile
重新执行命令,验证成功
# 写入
etcdctl put mykey "this is awesome"
# 输出:OK
# 查询
etcdctl get mykey
# 输出:
# mykey
# this is awesome
所以:
- etcd 单节点部署无需配置集群参数,默认端口 2379/2380 直接使用;
- 配置文件
/etc/default/etcd仅需修改节点名称、监听 IP、数据目录; etcdctl操作必须指定ETCDCTL_API=3,否则会出现命令不兼容报错;- 单节点适合学习、测试,生产环境建议部署 3 节点集群保证高可用。
基于 etcd 的服务注册与发现中心
在分布式环境中,服务的部署数量会动态变化,IP 地址也不固定。为了让客户端能自动找到可用的服务节点,并在服务宕机时自动剔除,我们需要一个服务注册发现中心。
etcd 是目前最常用的、高一致性的键值存储服务,非常适合做服务注册与发现。
下面将带你从零开始,使用 C++ 客户端etcd-cpp-apiv3实现完整的服务注册、服务发现、健康检查、动态上下线感知功能,包含环境部署、核心 API 讲解、代码实现和编译运行全流程。
基于 etcd 实现服务注册发现的三大核心流程:
- 服务注册:服务启动时,将自身
IP:端口写入 etcd,并绑定租约; - 健康检查:服务通过
KeepAlive自动续租,掉线后租约过期,etcd 自动删除注册信息; - 服务发现:客户端监听 etcd 目录变化,实时感知服务上线 / 下线,维护可用服务列表。
技术栈:etcd v3 (gRPC+Protobuf) + C++ 客户端 (etcd-cpp-apiv3)
环境部署(C++ etcd 客户端)
官方仅提供 Go 语言客户端,C++ 开发使用第三方开源库etcd-cpp-apiv3。
1. 安装依赖库
# 安装boost、ssl、protobuf、gRPC、cpprestsdk
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
2. 编译安装 etcd-cpp-apiv3
# 拉取源码
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
客户端核心 API 类讲解
etcd-cpp-apiv3封装了 etcd v3 的所有操作,核心类如下:
| 类名 | 作用 |
|---|---|
etcd::Client | etcd 客户端,实现注册、续租、查询、锁等操作 |
etcd::KeepAlive | 租约保活,自动定时向 etcd 发送心跳 |
etcd::Watcher | 目录监控,实时感知服务上下线 |
etcd::Response | 响应结果,包含键值、事件、错误信息 |
etcd::Event | 变更事件(PUT = 上线 / DELETE = 下线) |
namespace etcd {
// 用于表示一个键值数据,包含 key / value / 租约ID 等信息
class Value {
bool is_dir(); // 判断当前数据是否是一个目录
std::string const& key(); // 获取键名
std::string const& as_string(); // 获取值的字符串内容
int64_t lease(); // 获取当前键值绑定的租约 ID
};
// 服务上下线事件:用于描述数据发生了什么变化
class Event {
enum class EventType {
PUT, // 数据新增 / 修改 → 服务上线
DELETE_, // 数据被删除 → 服务下线
INVALID // 无效事件
};
EventType event_type(); // 获取事件类型(上线/下线)
const Value& kv(); // 获取变化后的最新数据
const Value& prev_kv(); // 获取变化之前的旧数据
};
// etcd 服务端响应的统一封装
class Response {
bool is_ok(); // 判断请求是否成功
std::string const& error_message(); // 获取错误信息
Value const& value(); // 获取当前结果数据
Value const& prev_value(); // 获取之前的数据
std::vector<Event> const& events(); // 获取本次所有变化事件
};
// 租约保活对象:用于不断给 etcd 发送心跳,防止服务被判定下线
class KeepAlive {
KeepAlive(const Client& client, int ttl, int64_t lease_id = 0);
int64_t Lease(); // 获取当前租约的 ID
void Cancel(); // 停止保活(服务主动下线)
};
// etcd 客户端核心类:提供注册、发现、租约、保活等操作
class Client {
Client(std::string const& etcd_url); // 连接 etcd 服务器
// 存储键值对(服务注册)
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> ls(std::string const& key);
// 创建一个租约(手动方式)
pplx::task<Response> leasegrant(int ttl);
// 创建一个自动保活的租约(推荐!自动心跳)
pplx::task<std::shared_ptr<KeepAlive>> leasekeepalive(int ttl);
// 手动销毁租约(服务下线)
pplx::task<Response> leaserevoke(int64_t lease_id);
};
// 服务监听器:监听服务上下线事件
class Watcher {
Watcher(
Client const& client,
std::string const& key, // 要监听的服务目录
std::function<void(Response)> callback, // 事件回调
bool recursive = false
);
bool Wait(); // 阻塞等待事件
bool Cancel(); // 停止监听
};
} // namespace etcd
Value 代表 etcd 里的一条服务数据,相当于服务节点的 “身份卡片”。它里面会完整保存三件关键信息:服务的唯一标识 key(比如 /service/user/192.168.10.129:9000)、服务的实际地址 value(比如 192.168.10.129:9000),还有这条服务信息绑定的租约 ID。不管是服务注册、服务发现,还是后续的上下线通知,传递的核心数据都是 Value 对象,它是服务信息的载体。这里要注意:一个 Value 只对应一个服务节点,而不是一组服务。比如我们启动了 3 个 user 服务,就会有 3 个不同的 Value,分别对应 3 个不同的节点 key,不会混在一起。
Event 是服务变化的 “消息通知”。在分布式集群里,服务不会一直不变 —— 可能新服务启动上线,也可能旧服务宕机下线,每当这些情况发生,etcd 不会让客户端反复去查询,而是主动推送一条 Event 给监听的客户端。这里要注意:一个 Event 只描述一个节点的一次变化,不是批量通知。比如同时上线两个服务,etcd 会分别发出两条事件,而不是合成一条。
PUT 表示服务上线,DELETE 表示服务下线。每条事件里还会附带变化前后的服务数据,能清楚看到 “哪个服务变了、变化后是什么样子、变化前是什么样子”,方便客户端更新自己的服务列表。这里要注意:不仅仅是主动关闭服务会产生 DELETE 事件,服务宕机、断电、进程崩溃后,租约过期被自动删除,同样会触发 DELETE 事件,这是客户端能感知节点失效的关键。
Response 是 etcd 所有操作的 “统一回复”。不管你是调用 put 注册服务、ls 查询服务列表,还是通过 Watcher 监听事件,etcd 都会返回一个 Response 对象。通过它能直观判断操作是否成功,同时查询到的服务数据、监听到的事件列表也都在里面。这里要注意:不同接口返回的内容是不一样的。put 返回的是执行结果,ls 返回的是多条数据列表,watch 返回的是事件列表,不能用同一套逻辑去解析所有 Response。
KeepAlive 是服务的 “心跳保活器”,核心作用是给 etcd 持续发送 “我还活着” 的信号。为什么需要这个功能?因为分布式系统里,服务可能会突然宕机,此时服务没法主动告诉 etcd “我下线了”。如果没有保活机制,这条失效的服务信息会一直存在 etcd 中,导致客户端调用到无效地址。而 KeepAlive 会定时发送心跳,只要服务正常运行,租约就一直有效;一旦宕机,心跳中断,租约会过期,etcd 就会自动删除这条服务信息。这里要注意:租约的真正意义,就是为了避免无效服务数据永远残留。没有租约,etcd 就不知道服务是否还活着,注册中心就失去了高可用的意义。
Client 是与 etcd 交互的 “总入口”,相当于操作 etcd 的 “工具箱”。所有和服务注册发现相关的操作都通过它完成,不用再找其他接口。这里要注意:Client 内部已经处理了连接和线程安全,整个程序通常只需要创建一个实例,反复使用即可,不需要每次操作都新建一个客户端。
put 是服务注册的核心接口,作用是把服务的 key 和 value 存储到 etcd 里。服务启动时,会把自己的唯一 key 和地址存在对应的服务目录下。这里要注意:每个服务节点必须使用不同的 key,通常在服务名后面附加自身地址,例如 /service/user/192.168.10.129:9000。如果多个节点用相同 key,后注册的会覆盖先注册的,导致节点丢失。
get 用于获取 单个 key 对应的单个 value,适合一对一的数据查询,比如根据用户 ID 查姓名、根据配置项查值。这里要注意:服务注册发现中绝对不能用 get!因为一个服务下面会有多个节点,是以目录形式存储的,不是单个 key 对应单个 value。如果用 get,只能拿到一个节点,漏掉其他所有节点,客户端无法实现负载均衡和高可用。
ls 用于获取 一个目录下的所有子节点数据,是服务发现真正使用的接口。举个例子:/service/user 下面有 3 个节点,key 分别是 /service/user/192.168.10.129:9000、/service/user/192.168.10.129:9001、/service/user/192.168.10.129:9002。调用 ls ("/service/user") 就能一次性拿到全部 3 个节点,而 get 只能拿到其中一个。所以服务发现必须用 ls。
leasekeepalive 是创建 “自动心跳” 的接口,调用后会自动维持租约,不需要开发者自己写定时任务。只要服务活着,心跳就不断;服务崩溃,租约自动过期,数据自动删除。这里要注意:这是服务高可用最核心的机制。没有 leasekeepalive,服务挂了 etcd 不知道,注册中心就会一直保留无效地址,让客户端调用失败。
leaserevoke 是手动撤销租约的接口,用于服务优雅下线。主动退出时调用,etcd 会立刻删除服务信息,并通知所有客户端。这里要注意:优雅下线和宕机过期删除,最终效果是一样的,都会触发 DELETE 事件。区别只是一个主动、一个被动,都能让客户端及时感知服务不可用。
Watcher 是服务监听器,用于监听目录或 key 的变化。一旦有服务上线或下线,就会触发回调。这里要注意:在服务发现中,我们应该监听服务目录,例如 /service/user,而不是监听某个具体节点。只有监听目录,才能感知所有节点的上下线,实现完整的服务动态管理。
租约和保活不是多余的设计,而是为了解决分布式系统的 “无效服务残留” 问题:
- 服务正常运行 → 保活心跳不断 → 租约有效 → 服务信息保留;
- 服务宕机 / 主动下线 → 心跳停止 / 租约撤销 → 服务信息自动删除 → 客户端收到下线通知;最终实现 “无效服务自动清理,客户端只调用健康服务” 的核心目标,这也是服务注册发现中心能稳定工作的关键。

我们实现两个模块:服务注册端(registry) + 服务发现端(discoverer)
1. 服务注册端(registry.cc)
功能:服务启动注册、自动心跳保活、进程退出自动下线
#include <iostream>
#include <etcd/Client.hpp>
#include <etcd/Response.hpp>
#include <etcd/KeepAlive.hpp>
#include <string>
int main() {
// etcd服务地址
std::string etcd_host = "http://127.0.0.1:2379";
// 服务注册key(规范:/服务名/实例名,避免覆盖)
std::string service_key = "/service/user/server-1";
// 服务实际地址
std::string service_addr = "192.168.65.132:9090";
// 创建etcd客户端
etcd::Client etcd_client(etcd_host);
// ===================== 租约+保活(核心:健康检查)=====================
// 创建3秒租约,自动保活(断开连接3秒后自动过期删除)
std::shared_ptr<etcd::KeepAlive> keep_alive = etcd_client.leasekeepalive(3).get();
int64_t lease_id = keep_alive->Lease();
std::cout << "租约创建成功,ID: " << lease_id << std::endl;
// ===================== 服务注册 =====================
auto response = etcd_client.put(service_key, service_addr, lease_id).get();
if (!response.is_ok()) {
std::cerr << "服务注册失败:" << response.error_message() << std::endl;
return -1;
}
std::cout << "服务注册成功!\nkey: " << service_key << "\nvalue: " << service_addr << std::endl;
// 阻塞等待,按回车退出服务
std::cout << "\n按回车下线服务..." << std::endl;
getchar();
// 手动撤销租约(服务主动下线)
etcd_client.leaserevoke(lease_id).get();
std::cout << "服务已下线" << std::endl;
return 0;
}
2. 服务发现端(discoverer.cc)
功能:获取当前在线服务、实时监听上下线事件、打印变更信息
#include <iostream>
#include <etcd/Client.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Response.hpp>
#include <string>
// 服务变更回调函数(上线/下线)
void watch_callback(const etcd::Response& resp) {
if (!resp.is_ok()) {
std::cerr << "监听异常:" << resp.error_message() << std::endl;
return;
}
// 遍历所有变更事件
for (const auto& event : resp.events()) {
std::string key = event.kv().key();
std::string addr = event.kv().as_string();
if (event.event_type() == etcd::Event::EventType::PUT) {
std::cout << "[服务上线] " << key << " => " << addr << std::endl;
}
else if (event.event_type() == etcd::Event::EventType::DELETE_) {
std::cout << "[服务下线] " << key << " => " << event.prev_kv().as_string() << std::endl;
}
}
}
int main() {
std::string etcd_host = "http://127.0.0.1:2379";
std::string service_path = "/service/user"; // 监听的服务根目录
etcd::Client etcd_client(etcd_host);
// ===================== 首次获取所有在线服务 =====================
auto resp = etcd_client.ls(service_path).get();
std::cout << "======== 当前在线服务 ========" << std::endl;
if (resp.is_ok()) {
for (int i = 0; i < resp.keys().size(); ++i) {
std::cout << resp.key(i) << " = " << resp.value(i).as_string() << std::endl;
}
}
// ===================== 监听目录变化(递归监听) =====================
std::cout << "\n开始监听服务变化..." << std::endl;
etcd::Watcher watcher(etcd_host, service_path, watch_callback, true);
// 阻塞监听
getchar();
watcher.Cancel();
return 0;
}
# 后台启动etcd
etcd --listen-client-urls=http://127.0.0.1:2379 --advertise-client-urls=http://127.0.0.1:2379 &
make
启动服务发现端
./discoverer
启动服务注册端
./registry
观察效果
- 发现端打印:服务上线信息
- 注册端按回车退出:发现端打印:服务下线信息
服务管理封装实战
在微服务架构中,直接使用 etcd 原生 API 实现服务注册发现会存在代码冗余、耦合度高、复用性差的问题。因此我们需要对 etcd 客户端进行面向对象封装,将服务注册、服务发现、健康检查、上下线感知的核心逻辑抽象为独立类,对外提供极简接口,让业务层只需关注服务本身,无需关心 etcd 底层实现。
下面是服务注册类和服务发现类的设计思想、代码实现、编译构建与运行测试,形成可直接复用的生产级组件。

核心目标
- 解耦:分离 etcd 操作与业务逻辑,业务层无需感知底层实现
- 易用:对外提供极简接口,实例化对象后一行代码完成注册 / 发现
- 健壮:内置连接重试、租约保活、异常重连机制
- 灵活:服务发现通过回调函数处理上下线事件,适配不同业务
| 类名 | 职责 | 核心能力 |
|---|---|---|
| SvcProvider | 服务提供者(服务注册) | 自动生成唯一实例、租约创建、服务注册、自动保活、异常重注册 |
| SvcWatcher | 服务消费者(服务发现) | 获取当前在线服务、监听目录变化、触发上下线回调、异常重监听 |
- 注册 Key 格式:
/服务名/实例ID(实例 ID 随机生成,避免多实例冲突) - 注册 Value:服务地址
IP:PORT - 监听路径:根目录
/(递归监听所有服务)
公共工具函数
提供 etcd 连接重试功能,确保客户端稳定连接注册中心。
#include <iostream>
#include <thread>
#include <chrono>
#include <etcd/Client.hpp>
#include <etcd/Response.hpp>
// 等待etcd连接成功(失败则每秒重试)
extern void wait_for_connection(etcd::Client &client);
void wait_for_connection(etcd::Client &client) {
while (!client.head().get().is_ok()) {
std::cerr << "[WARN] 连接etcd服务器失败,正在重试..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
std::cout << "[INFO] 连接etcd服务器成功" << std::endl;
}
服务注册类封装(SvcProvider)
服务启动时自动注册,内置租约 + 保活实现健康检查,服务掉线自动过期剔除。
_reg_center_addr:etcd 注册中心地址_instance_id:随机实例 ID(唯一标识)_svc_name:服务名称_svc_addr:服务地址(IP:PORT)_keepalive:租约保活对象
#include <string>
#include <sstream>
#include <memory>
#include <etcd/KeepAlive.hpp>
#include "random.hpp" // 随机ID工具
#include "log.hpp" // 日志工具
namespace bitesvc {
class SvcProvider {
public:
using ptr = std::shared_ptr<SvcProvider>;
// 构造:注册中心地址 + 服务名 + 服务地址
SvcProvider(const std::string ®_center_addr,
const std::string &svc_name,
const std::string &svc_addr);
// 服务注册入口(对外接口)
bool registry();
private:
// 生成唯一key:/服务名/实例ID
std::string make_key();
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 ®_center_addr,
const std::string &svc_name,
const std::string &svc_addr)
: _reg_center_addr(reg_center_addr)
, _instance_id(biteutil::Random::code()) // 生成唯一实例ID
, _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. 创建3秒租约(超时自动删除服务)
auto lease_resp = client.leasegrant(3).get();
if (!lease_resp.is_ok()) {
LOG_ERR("创建租约失败: {}", lease_resp.error_message());
return false;
}
int64_t lease_id = lease_resp.value().lease();
// 3. 注册服务(绑定租约)
std::string key = make_key();
auto put_resp = client.put(key, _svc_addr, lease_id).get();
if (!put_resp.is_ok()) {
LOG_ERR("服务注册失败: {}", put_resp.error_message());
return false;
}
LOG_INFO("服务注册成功 | key={} | addr={}", key, _svc_addr);
// 4. 启动保活 + 异常自动重注册
auto retry_handler = [this](const std::exception_ptr &e) {
LOG_ERR("保活断开,尝试重新注册...");
this->registry();
};
_keepalive = std::make_shared<etcd::KeepAlive>(client, retry_handler, 3, lease_id);
return true;
}
} // end namespace bitesvc
服务发现类封装(SvcWatcher)
实现:首次获取所有在线服务,实时监听服务上线 / 下线事件,通过回调函数通知业务层,内置异常重监听机制
_reg_center_addr:etcd 地址_online_callback:服务上线回调_offline_callback:服务下线回调_watcher:etcd 监视器
#include <string>
#include <vector>
#include <memory>
#include <functional>
#include <etcd/Watcher.hpp>
#include "str_util.hpp" // 字符串切割工具
#include "log.hpp"
namespace bitesvc {
class SvcWatcher {
public:
using ptr = std::shared_ptr<SvcWatcher>;
// 回调函数:(服务名, 服务地址)
using ModCallback = std::function<void(std::string, std::string)>;
SvcWatcher(const std::string ®_center_addr,
ModCallback &&online_callback,
ModCallback &&offline_callback);
// 启动服务发现(对外接口)
bool watch();
private:
// 事件回调处理
void callback(const etcd::Response &resp);
// 从key解析服务名
std::string parse_key(const std::string &key);
private:
std::string _reg_center_addr;
ModCallback _online_callback;
ModCallback _offline_callback;
std::shared_ptr<etcd::Watcher> _watcher;
};
SvcWatcher::SvcWatcher(const std::string ®_center_addr,
ModCallback &&online_callback,
ModCallback &&offline_callback)
: _reg_center_addr(reg_center_addr)
, _online_callback(std::move(online_callback))
, _offline_callback(std::move(offline_callback))
{}
std::string SvcWatcher::parse_key(const std::string &key) {
// 格式:/user/instance-id -> 解析出 user
std::vector<std::string> parts;
biteutil::STR::split(key, "/", parts);
return parts.empty() ? "" : parts[0];
}
void SvcWatcher::callback(const etcd::Response &resp) {
if (!resp.is_ok()) {
LOG_ERR("监控异常: {}", resp.error_message());
return;
}
for (const auto &event : resp.events()) {
std::string svc_name = parse_key(event.kv().key());
std::string svc_addr;
if (event.event_type() == etcd::Event::EventType::PUT) {
svc_addr = event.kv().as_string();
LOG_INFO("服务上线 | {} -> {}", svc_name, svc_addr);
if (_online_callback) _online_callback(svc_name, svc_addr);
}
else if (event.event_type() == etcd::Event::EventType::DELETE_) {
svc_addr = event.prev_kv().as_string();
LOG_INFO("服务下线 | {} -> {}", svc_name, svc_addr);
if (_offline_callback) _offline_callback(svc_name, svc_addr);
}
}
}
bool SvcWatcher::watch() {
// 1. 连接etcd
etcd::Client client(_reg_center_addr);
wait_for_connection(client);
// 2. 首次获取所有在线服务
auto list_resp = client.ls("/").get();
if (list_resp.is_ok()) {
for (const auto &val : list_resp.values()) {
std::string name = parse_key(val.key());
std::string addr = val.as_string();
LOG_INFO("已在线服务 | {} -> {}", name, addr);
if (_online_callback) _online_callback(name, addr);
}
}
// 3. 递归监控根目录
auto cb = std::bind(&SvcWatcher::callback, this, std::placeholders::_1);
_watcher = std::make_shared<etcd::Watcher>(client, "/", cb, true);
// 4. 异常自动重监听
_watcher->Wait([this](bool stopped) {
if (!stopped) {
LOG_ERR("监控断开,重新启动...");
this->watch();
}
});
return true;
}
} // end namespace bitesvc
我们来使用一下:
1. 服务注册(registry.cc)
#include "etcd_svc.h"
#include "log.h"
int main() {
// 日志初始化
bitelog::bitelog_init();
// etcd地址
std::string etcd_url = "http://127.0.0.1:2379";
// 服务名 + 服务地址
std::string service_name = "user";
std::string service_addr = "192.168.65.128:9000";
// 一行创建服务提供者
bitesvc::SvcProvider provider(etcd_url, service_name, service_addr);
// 一行注册
provider.registry();
// 阻塞运行
getchar();
return 0;
}
2. 服务发现(discovery.cc)
#include "etcd_svc.h"
#include "log.h"
// 服务上线处理
void on_service_online(const std::string &name, const std::string &addr) {
LOG_INFO("【业务处理】新服务上线: {} -> {}", name, addr);
}
// 服务下线处理
void on_service_offline(const std::string &name, const std::string &addr) {
LOG_INFO("【业务处理】服务下线: {} -> {}", name, addr);
}
int main() {
bitelog::bitelog_init();
std::string etcd_url = "http://127.0.0.1:2379";
// 创建发现器 + 传入回调
bitesvc::SvcWatcher watcher(etcd_url, on_service_online, on_service_offline);
// 启动发现
watcher.watch();
getchar();
return 0;
}
项目构建(CMakeLists.txt)
cmake_minimum_required(VERSION 3.10)
project(etcd_service_discovery)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -Wall")
# 可执行文件
add_executable(registry registry.cc)
add_executable(discovery discovery.cc)
# 链接依赖
target_link_libraries(registry
PRIVATE
etcd-cpp-api
cpprest
pthread
)
target_link_libraries(discovery
PRIVATE
etcd-cpp-api
cpprest
pthread
)
mkdir build && cd build
cmake ..
make -j
etcd --listen-client-urls=http://127.0.0.1:2379 --advertise-client-urls=http://127.0.0.1:2379 &
./discovery
./registry
# 服务发现端输出
[INFO] 连接etcd服务器成功
[INFO] 已在线服务 | user -> 192.168.65.128:9000
[INFO]【业务处理】新服务上线: user -> 192.168.65.128:9000
# 注册端退出后
[INFO]【业务处理】服务下线: user -> 192.168.65.128:9000
最终,我们实现了:
- 自动唯一实例:随机 ID 避免多服务注册覆盖
- 租约保活:服务掉线自动剔除
- 异常重连:保活 / 监控断开自动重试
- 回调解耦:业务逻辑与组件完全分离
- 递归监控:统一监控所有服务,无需配置
- 日志规范:统一日志输出,便于排查问题
本次封装将 etcd 服务注册发现的核心能力抽象为SvcProvider和SvcWatcher两个高可用类:
- 服务端:只需创建对象 + 调用
registry() - 客户端:只需创建对象 + 实现回调 + 调用
watch()
整套组件无侵入、易复用、高可靠,可直接用于 C++ 微服务项目的服务治理模块。
更多推荐
所有评论(0)