文章目录
etcd简介
etcd 是一个分布式、高可用的键值存储系统(以key: val
的形式做数据存储),主要用于在分布式系统中安全地存储和管理关键数据。 它最著名的角色是 Kubernetes 的"大脑",负责存储整个集群的状态和配置信息。
etcd 服务器类似于一个数据库,存储键值对数据。所有客户端都可以通过长连接共享这些数据。一个客户端修改了键值对,etcd 服务器会通知所有正在监听该键值对的客户端。
常用场景:服务发现(Service Discovery)
- 场景:在微服务架构中,服务实例的 IP 和端口是动态变化的。服务启动时可以将自己的地址注册到 etcd,消费者则从 etcd 查询可用的服务地址。
- 类比:就像电话簿,服务在这里"登记"和"查找"。
图示:
etcd安装与使用
安装:
shell
sudo apt-get install etcd
配置文件:
shell
vim /etc/default/etcd
更改绑定监听地址,使用外部网络能访问:
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0"
更改外部访问的地址:
ETCD_ADVERTISE_CLIENT_URLS="http://公网ip:端口"
重新启动:
shell
sudo systemctl restart etcd
配置指定API版本:
shell
vim /etc/profile
在末尾加:
export ETCDCTL_API=3
重新加载配置文件:
shell
source /etc/profile
验证:
搭建c++
客户端,使用第三方库API
接口
下载库:
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
库源码(可选):
shell
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对象
:客户端操作句柄对象
在构建该对象时需要传入参数:url_or_endpoints,即字符串或字符串向量,指定 etcd 服务地址。键值数据的推送和拉取,租约的设置等都是通过该对象提供的接口完成。KeepAlive保活对象
:针对一个租约可以不断进行续租,从而维持租约数据的有效性。
该对象通过Client对象提供的leasekeepalive()租约接口返回值再进行get()而得到。Response对象
:针对请求进行响应。
该对象通常由Client对象提供的接口put()/ls()返回而得到。Value对象
:存放键值对数据。Watcher对象
:进行数据变化通知。
构建该对象需要参数:- client对象
- 要监听的键名
- 变化回调函数
- 是否递归监听前缀匹配的所有键,通常填true
图示:
示例:
shell
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <thread>
int main(int argc,char* argv[])
{
//创建客户端
etcd::Client client("http://127.0.0.1:2379");
//指定租约并获取租约保活对象
auto keep_alive = client.leasekeepalive(3).get();
//获取租约id
auto lease_id = keep_alive->Lease();
//向etcd新增数据
auto rsp = client.put("/source/user","127.0.0.1:6000",lease_id).get();
if(rsp.is_ok()==false)
std::cout<<"新增数据失败"<<std::endl;
rsp = client.put("/source/file","127.0.0.1:6001",lease_id).get();
if(rsp.is_ok()==false)
std::cout<<"新增数据失败"<<std::endl;
std::this_thread::sleep_for(std::chrono::seconds(10));
return 0;
}
shell
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <etcd/SyncClient.hpp>
void callback(const etcd::Response& rsp)
{
if(rsp.is_ok()==false)
{
std::cout<<"错误事件通知"<<rsp.error_message()<<std::endl;
return;
}
for(auto const& ev: rsp.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[])
{
//创建客户端
etcd::Client client("http://127.0.0.1:2379");
//获取数据
auto rsp = client.ls("/source").get();
if(rsp.is_ok()==false)
{
std::cout<<"数据获取失败"<<std::endl;
return -1;
}
int n = rsp.keys().size();
for(int i = 0;i<n;i++)
{
std::cout<<rsp.value(i).as_string()<<":"<<rsp.key(i)<<std::endl;
}
auto watcher = etcd::Watcher(client,"/source",callback,true);
watcher.Wait();
return 0;
}
Makefile:
Makefile
all:put get
put:put.cc
g++ -o $@ $^ -letcd-cpp-api -lcpprest
get:get.cc
g++ -o $@ $^ -letcd-cpp-api -lcpprest
.PHONY:clean
clean:
rm -rf put get

以服务发现场景为例进行二次封装
使用原生 API 接口较为复杂繁琐,接下来我们针对服务发现场景进行二次封装
两个客户端:
- 服务注册客户端:向服务器新增服务信息数据,并进行保活
- 服务发现客户端:从服务器查找服务信息数据,并进行改变事件监控
思想:
- 封装服务注册客户端类:
提供一个接口:向服务器新增数据并保活
参数:注册中心地址、新增的服务信息 - 封装服务发现客户端类:
提供两个设置回调函数的接口:提供服务上线和下线的事件处理接口
提供一个设置根目录的接口:用于获取指定目录下的数据以及监控目录下的数据的改变
示例:
shell
#include <etcd/Client.hpp>
#include <etcd/KeepAlive.hpp>
#include <etcd/Response.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
#include <etcd/SyncClient.hpp>
#include <memory>
#include <functional>
class Registry
{
public:
Registry(const std::string& host)
:_client(std::make_shared<etcd::Client>(host))
,_keep_alive(_client->leasekeepalive(3).get())
,_lease_id(_keep_alive->Lease())
{}
bool registry(const std::string& key,const std::string& val)
{
auto rsp = _client->put(key,val,_lease_id,true).get();
if(rsp.is_ok()==false)
{
//建议替换为日志输出
std::cout<<key<<": "<<val<<"注册失败"<<std::endl;
return false;
}
else return true;
}
~Registry()
{
_client->Cancel();
}
private:
std::shared_ptr<etcd::Client> _client;
std::shared_ptr<etcd::KeepAlive> _keep_alive;
uint64_t _lease_id;
};
class Discovery
{
public:
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 rsp = _client->ls(basedir).get();
if(rsp.is_ok()==false)
{
std::cout<<"获取数据失败 "<<rsp.error_message()<<std::endl;
}
int sz = rsp.keys().size();
for(int i=0;i<sz;i++)
{
if(_del_cb)
_put_cb(rsp.key(i),rsp.value(i).as_string());
}
_watcher = std::make_shared<etcd::Watcher>(*_client,basedir
,std::bind(&Discovery::callback,this, std::placeholders::_1),true);
}
void callback(const etcd::Response& rsp)
{
if(rsp.is_ok()==false)
{
std::cout<<"错误事件通知"<<rsp.error_message()<<std::endl;
return;
}
for(auto const& ev: rsp.events())
{
if (ev.event_type() == etcd::Event::EventType::PUT) {
std::cout << "上线服务:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;
}else if (ev.event_type() == etcd::Event::EventType::DELETE_) {
std::cout << "下线服务:" << ev.kv().key() << "-" << ev.kv().as_string() << std::endl;
}
}
}
~Discovery()
{
_watcher->Cancel();
}
private:
NotifyCallback _put_cb;
NotifyCallback _del_cb;
std::shared_ptr<etcd::Client> _client;
std::shared_ptr<etcd::Watcher> _watcher;
};
非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!