什么是 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 全都帮我们搞定。


  1. 键值存储:像 Redis / Map 一样,存 key-value
  2. 强一致:只要写成功,所有节点读到一模一样的最新数据
  3. 高可用:挂几台不影响整个集群
  4. Raft 算法:etcd 的 “大脑”,负责选主、同步数据、保证一致
  5. Watch 监听:key 一变,立刻推送给你,不用轮询
  6. 租约(Lease):服务一挂,key 自动过期,实现健康检查
  7. 事务、版本、历史:不怕覆盖,支持回滚

一句话:etcd = 分布式环境里永远可信的唯一数据源。


etcd 最常用的 5 大场景:

  1. 服务注册与发现:服务启动 → 注册到 etcd调用方 → 从 etcd 拉取地址服务挂了 → 自动摘除

  2. 统一配置中心:一份配置,全集群生效修改后实时推送到所有服务,不用重启

  3. 分布式锁:秒杀、限流、任务调度、防重复执行

  4. 集群选主:多节点里自动选主,主挂了自动切新主

  5. 云原生存储(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

所以:

  1. etcd 单节点部署无需配置集群参数,默认端口 2379/2380 直接使用;
  2. 配置文件 /etc/default/etcd 仅需修改节点名称、监听 IP、数据目录;
  3. etcdctl 操作必须指定 ETCDCTL_API=3,否则会出现命令不兼容报错;
  4. 单节点适合学习、测试,生产环境建议部署 3 节点集群保证高可用。

基于 etcd 的服务注册与发现中心

在分布式环境中,服务的部署数量会动态变化,IP 地址也不固定。为了让客户端能自动找到可用的服务节点,并在服务宕机时自动剔除,我们需要一个服务注册发现中心

etcd 是目前最常用的、高一致性的键值存储服务,非常适合做服务注册与发现。

下面将带你从零开始,使用 C++ 客户端etcd-cpp-apiv3实现完整的服务注册、服务发现、健康检查、动态上下线感知功能,包含环境部署、核心 API 讲解、代码实现和编译运行全流程。

基于 etcd 实现服务注册发现的三大核心流程:

  1. 服务注册:服务启动时,将自身IP:端口写入 etcd,并绑定租约
  2. 健康检查:服务通过KeepAlive自动续租,掉线后租约过期,etcd 自动删除注册信息;
  3. 服务发现:客户端监听 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::Clientetcd 客户端,实现注册、续租、查询、锁等操作
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 底层实现。

下面是服务注册类服务发现类的设计思想、代码实现、编译构建与运行测试,形成可直接复用的生产级组件。

核心目标

  1. 解耦:分离 etcd 操作与业务逻辑,业务层无需感知底层实现
  2. 易用:对外提供极简接口,实例化对象后一行代码完成注册 / 发现
  3. 健壮:内置连接重试、租约保活、异常重连机制
  4. 灵活:服务发现通过回调函数处理上下线事件,适配不同业务
类名职责核心能力
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 &reg_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 &reg_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 &reg_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 &reg_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

最终,我们实现了:

  1. 自动唯一实例:随机 ID 避免多服务注册覆盖
  2. 租约保活:服务掉线自动剔除
  3. 异常重连:保活 / 监控断开自动重试
  4. 回调解耦:业务逻辑与组件完全分离
  5. 递归监控:统一监控所有服务,无需配置
  6. 日志规范:统一日志输出,便于排查问题

本次封装将 etcd 服务注册发现的核心能力抽象为SvcProviderSvcWatcher两个高可用类:

  • 服务端:只需创建对象 + 调用registry()
  • 客户端:只需创建对象 + 实现回调 + 调用watch()

整套组件无侵入、易复用、高可靠,可直接用于 C++ 微服务项目的服务治理模块。

更多推荐