目录
一、简介
二、安装【Ubuntu】
安装etcd
安装C++API
三、写一个示例
3.0写一个示例代码
3.1获取一个etcd服务
3.2获取租约(写端操作)
3.3使用租约(写端操作)
3.4销毁租约(写端操作)
3.5获取etcd服务中的服务列表(读端操作)
3.6监听状态变化(读端操作)
一、简介
Etcd是一个golang编写的分布式、高可用的一致性键值存储系统,用于配置共享和服务发现等。它使用Raft一致性算法来保持集群数据的一致性,且客户端通过长连接watch功能,能够及时收到数据变化通知。
这样的简介比较干涩也不太好理解,我们换个说法,如果你开发过集群式的网络服务,你应该知道,通常情况下,你需要指定一台网关主机转发来自用户的请求,这些请求将被转发到对应的应用服务器上,然后进行业务处理。但是这里就有一个问题,当我们上线一个主机、或者下线一个主机的时候网关机器是很难进行感知的(下线相对来说好感知,可以发送网络包进行探测),但是一个新的服务主机上线就是个麻烦事,我们怎么才能通知这个服务上线了?简单点来说,这个时候就需要我们有一台管理主机,用来管理服务的上下线通知,当有新服务上下线时,就立即通知网关主机。
其实你也可以将etcd看作是一个键值存储的数据库,服务主机上线时,就将我们主机的信息放入到数据库中,当网关主机需要获取服务信息时,就需要对这个数据进行读操作,这个时候不就可以让网关机感知服务的上下线了吗?当然etcd也会主动的将变化信息发送给所有监听变化的主机上。

二、安装【Ubuntu】
安装etcd
安装etcd
sudo apt-get install etcd
启动etcd
sudo systemctl start etcd
设置开机自启
sudo systemctl enable etcd
添加使用的etcd版本到环境变量
export ETCDCTL_API=3
重新加载环境变量
source /etc/profile
测试向etcd中写入一个键值对
etcdctl put mykey "test"
获取一下
etcdctl get mykey

安装C++API
安装依赖
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
cmake .. -DCMAKE_INSTALL_PREFIX=/usr
构建并安装
make -j$(nproc) && sudo make install
三、写一个示例
3.0写一个示例代码
//write.cpp
#include <etcd/Client.hpp>
#include <etcd/Response.hpp>
#include <etcd/KeepAlive.hpp>
#include <thread>
#include <chrono>
#include <string>void RegistryService(etcd::Client& etcd,const std::string& serviceKey,const std::string& serviceValue,size_t liveTime)
{//获取resphone对象auto res_lease = etcd.leasekeepalive(liveTime).get();//获取租约IDint64_t leaseid = res_lease->Lease();//将键值与租约绑定etcd.put(serviceKey,serviceValue,leaseid);//休眠该执行流20sstd::this_thread::sleep_for(std::chrono::seconds(20));std::cout<<"程序已退出"<<std::endl;
}int main()
{try{ etcd::Client etcd("http://127.0.0.1:2379");size_t time=20; //单位:秒RegistryService(etcd,"/test/test1","127.0.0.1:8888",time);RegistryService(etcd,"/test/test2","127.0.0.1:8889",time);RegistryService(etcd,"/test/test3","127.0.0.1:8890",time);}catch(const std::exception& e){std::cerr << e.what() << '\n';}return 0;
}
//reader.cpp#include <etcd/Client.hpp>
#include <etcd/Watcher.hpp>
#include <etcd/Value.hpp>
void WatchListen(etcd::Response res)
{for(auto e:res.events()){if(e.event_type()==etcd::Event::EventType::PUT){std::cout<<"键值发生修改"<<std::endl;std::cout<<"before: "<<e.prev_kv().key()<<":"<<e.prev_kv().as_string()<<std::endl;std::cout<<"now: "<<e.kv().key()<<":"<<e.kv().as_string()<<std::endl;}else if(e.event_type()==etcd::Event::EventType::DELETE_){std::cout<<"数据发生删除"<<std::endl;std::cout<<"now: "<<e.kv().key()<<":"<<e.kv().as_string()<<std::endl;}}
}int main()
{etcd::Client etcd("http://127.0.0.1:2379");etcd::Response res = etcd.ls("/test").get();for(auto e:res.events()){std::cout<<"当前值"<<e.kv().key()<<e.kv().as_string()<<std::endl;}etcd::Watcher watcher(etcd,"/test",WatchListen,true);watcher.Wait();return 0;
}
//makefile
all:reader writer
reader:reader.cppg++ -o $@ $^ -letcd-cpp-api -lcpprest -std=c++17
writer:writer.cppg++ -o $@ $^ -letcd-cpp-api -lcpprest -std=c++17
3.1获取一个etcd服务
无论是想要注册的服务主机,还是想要获取服务的网关机,都需要创建一个etcd客户端类,这一点应该不难理解,因为注册的服务器需要将服务主机的信息交给etcd服务器,让其进行通知其它网关机;同理,网关机如果想要获取etcd中存储的信息,也就必须要连接上etcd才可以获取。
在3.0的示例代码中,获取etcd服务的语句是:
etcd::Client etcd("http://127.0.0.1:2379");
3.2获取租约(写端操作)
为了方便介绍,我们暂且把服务提供主机叫做“写端”,需要获取服务主机信息的网关机叫做“读端”,在获取王etcd服务之后,读写端的操作就出现了差异,写端肯定是要向etcd服务器中写入数据,而读端肯定是要从etcd服务中读取数据。
如果我们向向etcd服务中写入数据,我们必须要申请一个租约,租约其实就是一个过期时间,即你写入的这个数据,etcd服务需要给你保留多久,那么现在我们先来看看如何获取一个租约,根据官方文档中所提到的有两个方法可以获取租约一个是KeepAlive类中的Lease方法,一个是通过leasegrant方法获取;前者获取后其会定期自动重置租约时长,后者则不会,所以如果你问我推荐使用哪种方式来获取租约,我个人就更倾向于前者。
auto res_lease = etcd.leasekeepalive(liveTime).get();
//获取租约ID
int64_t leaseid = res_lease->Lease();
需要说明的是,为什么etcd要先get获取一个对象而后在使用这个对象获取租约ID?
这是因为etcd中的大部分操作都是支持异步的,租约的获取也不例外,而是用get方法则是在阻塞在原地等待资源就绪,也就是我们常说的同步获取资源,而如果想在这期间去完成其它的工作,你可以不立即使用get方法,而是将get返回的结果进行托管(设置一个回调函数),当资源就绪的时候就会执行你设置好的回调函数,那借此机会,我们看看如何异步获取一个租约ID。
int64_t lease_id1=-1;
pplx::task<std::shared_ptr<etcd::KeepAlive>> res = etcd.leasekeepalive(liveTime);
//这里的回调函数,是一个lamada表达式
res.then([&lease_id1](std::shared_ptr<etcd::KeepAlive> res){ lease_id1 = res->Lease();}
).wait();
//模拟去做其它的事情
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout<<lease_id1<<std::endl;
在这个代码中我们使用执行流休眠来模拟申请资源期间完成其它工作,此外需要注意的是,如果你将资源设置为异步获取,你必须要保证主执行流不会执行的很快以至于资源还没申请成功需要被使用,所以建议在使用异步获取的资源之前最好先做一个资源获取成功的校验。
3.3使用租约(写端操作)
使用租约就比较简单了,我们只需要指定我们需要存入的服务名称、主机信息和我们的租约ID就可以了,其中serviceKey指的时我们服务名称,serviceValue指的是我们的主机信息。leaseid就是之前申请的租约ID,这个ID对应着一个租约,如果租约过期了,etcd就会帮我们自动删除保存在etcd服务器上的服务信息。
etcd.put(serviceKey,serviceValue,leaseid);
需要说明的是serviceKey的最好是像文件目录格式一样的结构,这一点一会我们读取etcd服务器内容时,我们在说明。

3.4销毁租约(写端操作)
其实我写的代码,是错的,错就错在没有销毁租约,如果不销毁租约就会发生这样一种情况,就是当你程序退出了,但是etcd服务器中的租约没过期,也就继续保存着你的服务信息,这个时候etcd默认你的主机还在,但是如果此时有其它用户请求到来且网关主机还把这个请求交给了这台已经退出的主机那就会导致请求丢失。

在你明确某个租约退出时,你可以使用leaserevoke来释放租约,来避免租约未被即时释放放的问题。
etcd.leaserevoke(leaseid);
3.5获取etcd服务中的服务列表(读端操作)
读端操作相对简单的多,因为即便不是我们的简单示例,在实际生产中,请求的处理通常都是交给应用服务主机来进行处理的,而读端更像是一个“传话人”,我们使用Client类中的ls方法就可以获取服务的整个列表,还记得我画的图3吗,这里获取的不是目录而是所有的节点,即图3中的主机A、主机B、……,目录的作用起到的是一个指示服务类型的作用。当然,我这里只画了两层结构,实际上可能比两层结构更多。
etcd::Response res = etcd.ls("/test").get();for(auto e:res.events())
{std::cout<<"当前值"<<e.kv().key()<<e.kv().as_string()<<std::endl;
}
3.6监听状态变化(读端操作)
这个功能就是,在最开始提到的,etcd的一个重要功能,它可以通知其它主机,某一个服务主机的上线,要使用这个功能需要我们使用Watcher类,在这个类中,我们填入etcd主机信息、监听目录、状态变化时需要执行的回调函数、最后一个参数要设置为true表示监听整个目录还是监听单个主机,监听整个目录则将此值置为true,否则置为false。

需要一提的是,使用watcher监听状态变化是需要阻塞的,这是因为watcher是一个异步操作,也就是说如果你不阻塞住主执行流而且还让其退出,那么就会造成watcher执行流一并退出而导致无法监听变化。
四、结语
本片文章只讲解了一些基本操作,其实还有一些问题尚且没有明晰:
- etcd是一个集群服务,当有多个etcd服务器是如何保证一致性。
- etcd是如何保证并发安全的。
- etcd容灾都做了哪些工作。
这些问题就不在本文中阐述了,也许过几天我会在出一个补档内容了,来介绍一下这部分内容,说句心里话,博主在写这篇文章的时候也是在学习etcd,初学者学习内容难免纰漏,如果屏幕前的你发现了问题,还请多多指教。对于哪些没看懂或者想要了解更多的小伙伴可以去看看源码或者官方的示例代码,这会对你理解etcd有很大的帮助。