目錄
?一、簡介
二、安裝【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有很大的幫助。