etcd 是一个用 Go 编写的分布式、高可用的键值存储,底层基于 Raft 达成共识。它提供长连接 watch 机制,客户端可以实时感知数据变化;用来做服务注册发现再合适不过,比 ZooKeeper 更轻便。
安装 etcd
在 Ubuntu 上直接 apt 安装:
sudo apt install etcd
sudo systemctl start etcd
sudo systemctl enable etcd
单节点情况下默认配置就够用——节点间通信端口 2380,客户端访问端口 2379。如果要自定义节点名、数据目录或监听地址,编辑 /etc/default/etcd,参考下面的配置:
ETCD_NAME="etcd1"
ETCD_DATA_DIR="/var/lib/etcd/default.etcd"
ETCD_LISTEN_CLIENT_URLS="http://192.168.65.132:2379,http://127.0.0.1:2379"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.65.132:2379,http://127.0.0.1:2379"
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"
#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"
修改配置后重新加载,然后验证一下:
source /etc/profile
export ETCDCTL_API=3 # 如果在 /etc/profile 里加过这行可以省略
etcdctl put key "SnowK"
etcdctl get key
如果 put 报错 No help topic for 'put',就是因为没声明 ETCDCTL_API=3,加上就好了。
服务注册发现的设计
用 etcd 做注册中心,基本流程就是:
- 服务启动时将自己的 IP:Port 写入 etcd(注册)。
- 客户端从 etcd 拉取服务地址列表(发现)。
- 已注册的服务定期续租(健康检查),一旦停止续租,etcd 会清理掉对应键,调用方通过 watch 感知到节点下线。
官方只维护了 Go 的客户端库,C++ 这边社区方案的 etcd-cpp-apiv3 算是比较完整的。
etcd-cpp-apiv3
etcd-cpp-apiv3 是一个 C++ 的 etcd 客户端,基于 gRPC,依赖 boost、protobuf、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
# 编译安装
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
库的几个核心类:
- Client:连接 etcd 的主入口,支持 put、ls、leasegrant、leasekeepalive、lock 等操作,返回
pplx::task<Response>,可以.get()阻塞等待,也可以用.wait()或.then()异步处理。 - KeepAlive:通过
Client::leasekeepalive创建,内部自动定时续租。对象析构时停止续租,对应租约到期后键值会被删除。 - Response:操作响应,可以检查
is_ok()、获取错误信息、单个或批量 value。 - Value:表示一个键值对,
key()和as_string()获取内容。 - Watcher:监控指定 key 的变化。提供回调,支持递归监控目录下的所有子节点。
- Event:在回调中给出变更事件,分为
PUT和DELETE_两种,同时提供当前值和前值。
类声明大致如下:
namespace etcd {
class Value {
public:
bool is_dir();
std::string const& key();
std::string const& as_string();
int64_t lease();
};
class Event {
public:
enum class EventType {
PUT,
DELETE_,
INVALID,
};
enum EventType event_type() const;
const Value& kv() const;
const Value& prev_kv();
};
class Response {
public:
bool is_ok();
std::string const& error_message();
const Value& value();
const Value& prev_value();
const Value& value(int index);
const std::vector<Event>& events();
};
class KeepAlive {
public:
KeepAlive(Client const& client, int ttl, int64_t lease_id = 0);
int64_t Lease();
void Cancel();
};
class Client {
public:
Client(std::string const& etcd_url, std::string const& load_balancer = "round_robin");
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);
pplx::task<Response> lock(std::string const& key);
};
class Watcher {
public:
Watcher(Client const& client, std::string const& 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();
};
}
实际跑一下
下面是一个简单的例子:服务端注册两个服务 user 和 friend,user 绑定了 3 秒 TTL 的租约,10 秒后程序退出,租约过期会被清理;客户端通过 ls 发现服务,再用 Watcher 监控 /service 目录的变化。
运行结果预期:
127.0.0.1:6633 可以提供/service/friend 服务
127.0.0.1:3366 可以提供/service/user 服务
服务信息下线被删除:当前的值:/service/user-
原来的值:/service/user-127.0.0.1:3366
注册端 put.cc:
#include<iostream>
#include<thread>
#include<etcd/Client.hpp>
#include<etcd/KeepAlive.hpp>
#include<etcd/Response.hpp>
int main(int argc, char* argv[]) {
std::string etcd_host = "http://127.0.0.1:2379";
etcd::Client client(etcd_host);
// 创建一个 3 秒 TTL 的租约,keep_alive 会自动续租
auto keep_alive = client.leasekeepalive(3).get();
auto lease_id = keep_alive->Lease();
// 注册 user 服务,绑定租约
auto resp1 = client.put("/service/user", "127.0.0.1:3366", lease_id).get();
if (!resp1.is_ok()) {
std::cout << "新增数据失败:" << resp1.error_message() << std::endl;
return -1;
}
// 注册 friend 服务,不带租约(永久有效)
auto resp2 = client.put("/service/friend", "127.0.0.1:6633").get();
if (!resp2.is_ok()) {
std::cout << "新增数据失败:" << resp2.error_message() << std::endl;
return -1;
}
// 保持 10 秒,然后退出时会释放租约
std::this_thread::sleep_for(std::chrono::seconds(10));
return 0;
}
发现端 get.cc:
#include<iostream>
#include<thread>
#include<etcd/Client.hpp>
#include<etcd/KeepAlive.hpp>
#include<etcd/Response.hpp>
#include<etcd/Watcher.hpp>
#include<etcd/Value.hpp>
void CallBack(const etcd::Response& resp) {
if (!resp.is_ok()) {
std::cout << "收到一个错误的事件通知:" << resp.error_message() << std::endl;
return;
}
for (const auto& ev : resp.events()) {
if (ev.event_type() == etcd::Event::EventType::PUT) {
std::cout << "服务信息发生了改变:" << std::endl;
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()) {
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) << "服务" << std::endl;
}
// 监控 /service 目录的变化(递归)
auto watcher = etcd::Watcher(client, "/service", CallBack, true);
watcher.Wait();
return 0;
}
Makefile:
all: get put
get: get.cc
g++ -o $@ $^ -std=c++17 -letcd-cpp-api -lcpprest
put: put.cc
g++ -o $@ $^ -std=c++17 -letcd-cpp-api -lcpprest
.PHONY:clean
clean:
rm get put
这样就搭好了一个最基本的服务注册发现框架。实际用的时候,可以把 keep_alive 放到一个后台线程里保持,服务发现逻辑结合负载均衡策略,Watcher 实时更新可用列表。


