一個分散式服務集群管理通常需要一個協調服務,提供服務註冊、服務發現、配置管理、組服務等功能,而協調服務自身應是一個高可用的服務集群,ZooKeeper是廣泛應用且眾所周知的協調服務。協調服務自身的高可用需要選舉演算法來支撐,本文將講述選舉原理並以分散式服務集群NebulaBootstrap的協調服務N ...
一個分散式服務集群管理通常需要一個協調服務,提供服務註冊、服務發現、配置管理、組服務等功能,而協調服務自身應是一個高可用的服務集群,ZooKeeper是廣泛應用且眾所周知的協調服務。協調服務自身的高可用需要選舉演算法來支撐,本文將講述選舉原理並以分散式服務集群NebulaBootstrap的協調服務NebulaBeacon為例詳細說明協調服務的選舉實現。
為什麼要選NebulaBeacon來說明協調服務的選舉實現?一方面是我沒有讀過Zookeeper的代碼,更重要的另一方面是NebulaBeacon的選舉實現只有兩百多行代碼,簡單精煉,很容易講清楚。基於高性能C++網路框架Nebula實現的分散式服務集群NebulaBootstrap是一種用C++快速構建高性能分散式服務的解決方案。
為什麼要實現自己的協調服務而不直接用Zookeeper?想造個C++的輪子,整個集群都是C++服務,因為選了ZooKeeper而需要部署一套Java環境,配置也跟其他服務不是一個體系,實在不是一個好的選擇。Spring Cloud有Eureka,NebulaBootstrap有NebulaBeacon,未來NebulaBootstrap會支持ZooKeeper,不過暫無時間表,還是首推NebulaBeacon。
1. 選舉演算法選擇
Paxos演算法 和 ZooKeeper ZAB協議 是兩種較廣為人知的選舉演算法。ZAB協議主要用於構建一個高可用的分散式數據主備系統,例如ZooKeeper,而Paxos演算法則是用於構建一個分散式的一致性狀態機系統。也有很多應用程式採用自己設計的簡單的選舉演算法,這類型簡單的選舉演算法通常依賴電腦自身因素作為選舉因數,比如IP地址、CPU核數、記憶體大小、自定義序列號等。
Paxos規定了四種角色(Proposer,Acceptor,Learner,以及Client)和兩個階段(Promise和Accept)。 ZAB服務具有四種狀態:LOOKING、FOLLOWING、LEADING、OBSERVING。 NebulaBeacon是高可用分散式系統的協調服務,採用ZAP協議更為合適,不過ZAP協議還是稍顯複雜了,NebulaBeacon的選舉演算法實現基於節點的IP地址標識,選舉速度快,實現十分簡單。
2. 選舉相關數據結構
NebulaBeacon的選舉相關數據結構非常簡單:
const uint32 SessionOnlineNodes::mc_uiLeader = 0x80000000; ///< uint32最高位為1表示leader
const uint32 SessionOnlineNodes::mc_uiAlive = 0x00000007; ///< 最近三次心跳任意一次成功則認為線上
std::map<std::string, uint32> m_mapBeacon; ///< Key為節點標識,值為線上心跳及是否為leader標識
如上數據結構m_mapBeacon保存了Beacon集群各Beacon節點信息,以Beacon節點的IP地址標識為key排序,每次遍歷均從頭開始,滿足條件(1&&2 或者 1&&3)則標識為Leader:1. 節點線上;2. 已經成為Leader; 3. 整個列表中不存在線上的Leader,而節點處於線上節點列表的首位。
3. Beacon選舉流程
Beacon選舉基於節點IP地址標識,實現非常簡單且高效。
"beacon":["192.168.1.11:16000", "192.168.1.12:16000"]
進程啟動時首先檢查Beacon集群配置,若未配置其他Beacon節點信息,則預設只有一個Beacon節點,此時該節點在啟動時自動成為Leader節點。否則,向其他Beacon節點發送一個心跳消息,等待定時器回調檢查並選舉出Leader節點。選舉流程如下圖:
檢查是否線上就是通過檢查兩次定時器回調之間是否收到了其他Beacon節點的心跳消息。對m_mapBeacon的遍歷檢查判斷節點線上情況,對已離線的Leader節點置為離線狀態,若當前節點應成為Leader節點則成為Leader節點。
4. Beacon節點間選舉通信
Beacon節點間的選舉通信與節點心跳合為一體,這樣做的好處是當leader節點不可用時,fllower節點立刻可以成為leader節點,選舉過程只需每個fllower節點遍歷自己記憶體中各Beacon節點的心跳信息即可,無須在發現leader不線上才發起選舉,更快和更好地保障集群的高可用性。
Beacon節點心跳信息帶上了leader節點作為協調服務產生的新數據,fllower節點在接收心跳的同時完成了數據同步,保障任意一個fllower成為leader時已獲得集群所有需協調的信息並可隨時切換為leader。除定時器觸發的心跳帶上協調服務產生的新數據之外,leader節點產生新數據的同時會立刻向fllower發送心跳。
5. Beacon選舉實現
Beacon心跳協議proto:
/**
* @brief BEACON節點間心跳
*/
message Election
{
int32 is_leader = 1; ///< 是否主節點
uint32 last_node_id = 2; ///< 上一個生成的節點ID
repeated uint32 added_node_id = 3; ///< 新增已使用的節點ID
repeated uint32 removed_node_id = 4; ///< 刪除已廢棄的節點ID
}
檢查Beacon配置,若只有一個Beacon節點則自動成為Leader:
void SessionOnlineNodes::InitElection(const neb::CJsonObject& oBeacon)
{
neb::CJsonObject oBeaconList = oBeacon;
for (int i = 0; i < oBeaconList.GetArraySize(); ++i)
{
m_mapBeacon.insert(std::make_pair(oBeaconList(i) + ".1", 0));
}
if (m_mapBeacon.size() == 0)
{
m_bIsLeader = true;
}
else if (m_mapBeacon.size() == 1
&& GetNodeIdentify() == m_mapBeacon.begin()->first)
{
m_bIsLeader = true;
}
else
{
SendBeaconBeat();
}
}
發送Beacon心跳:
void SessionOnlineNodes::SendBeaconBeat()
{
LOG4_TRACE("");
MsgBody oMsgBody;
Election oElection;
if (m_bIsLeader)
{
oElection.set_is_leader(1);
oElection.set_last_node_id(m_unLastNodeId);
for (auto it = m_setAddedNodeId.begin(); it != m_setAddedNodeId.end(); ++it)
{
oElection.add_added_node_id(*it);
}
for (auto it = m_setRemovedNodeId.begin(); it != m_setRemovedNodeId.end(); ++it)
{
oElection.add_removed_node_id(*it);
}
}
else
{
oElection.set_is_leader(0);
}
m_setAddedNodeId.clear();
m_setRemovedNodeId.clear();
oMsgBody.set_data(oElection.SerializeAsString());
for (auto iter = m_mapBeacon.begin(); iter != m_mapBeacon.end(); ++iter)
{
if (GetNodeIdentify() != iter->first)
{
SendTo(iter->first, neb::CMD_REQ_LEADER_ELECTION, GetSequence(), oMsgBody);
}
}
}
接收Beacon心跳:
void SessionOnlineNodes::AddBeaconBeat(const std::string& strNodeIdentify, const Election& oElection)
{
if (!m_bIsLeader)
{
if (oElection.last_node_id() > 0)
{
m_unLastNodeId = oElection.last_node_id();
}
for (int32 i = 0; i < oElection.added_node_id_size(); ++i)
{
m_setNodeId.insert(oElection.added_node_id(i));
}
for (int32 j = 0; j < oElection.removed_node_id_size(); ++j)
{
m_setNodeId.erase(m_setNodeId.find(oElection.removed_node_id(j)));
}
}
auto iter = m_mapBeacon.find(strNodeIdentify);
if (iter == m_mapBeacon.end())
{
uint32 uiBeaconAttr = 1;
if (oElection.is_leader() != 0)
{
uiBeaconAttr |= mc_uiLeader;
}
m_mapBeacon.insert(std::make_pair(strNodeIdentify, uiBeaconAttr));
}
else
{
iter->second |= 1;
if (oElection.is_leader() != 0)
{
iter->second |= mc_uiLeader;
}
}
}
檢查線上leader,成為leader:
void SessionOnlineNodes::CheckLeader()
{
LOG4_TRACE("");
std::string strLeader;
for (auto iter = m_mapBeacon.begin(); iter != m_mapBeacon.end(); ++iter)
{
if (mc_uiAlive & iter->second)
{
if (mc_uiLeader & iter->second)
{
strLeader = iter->first;
}
else if (strLeader.size() == 0)
{
strLeader = iter->first;
}
}
else
{
iter->second &= (~mc_uiLeader);
}
uint32 uiLeaderBit = mc_uiLeader & iter->second;
iter->second = ((iter->second << 1) & mc_uiAlive) | uiLeaderBit;
if (iter->first == GetNodeIdentify())
{
iter->second |= 1;
}
}
if (strLeader == GetNodeIdentify())
{
m_bIsLeader = true;
}
}
6. Beacon節點切換leader
通過Nebula集群的命令行管理工具nebcli可以很方便的查看Beacon節點狀態,nebcli的使用說明見Nebcli項目的README。下麵啟動三個Beacon節點,並反覆kill掉Beacon進程和重啟,查看leader節點的切換情況。
啟動三個beacon節點:
nebcli): show beacon
node is_leader is_online
192.168.157.176:16000.1 yes yes
192.168.157.176:17000.1 no yes
192.168.157.176:18000.1 no yes
kill掉leader節點:
nebcli): show beacon
node is_leader is_online
192.168.157.176:16000.1 no no
192.168.157.176:17000.1 yes yes
192.168.157.176:18000.1 no yes
kill掉fllower節點:
nebcli): show beacon
node is_leader is_online
192.168.157.176:16000.1 no no
192.168.157.176:17000.1 yes yes
192.168.157.176:18000.1 no no
重啟被kill掉的兩個節點:
nebcli): show beacon
node is_leader is_online
192.168.157.176:16000.1 no yes
192.168.157.176:17000.1 yes yes
192.168.157.176:18000.1 no yes
fllower節點在原leader節點不可用後成為leader節點,且只要不宕機則一直會是leader節點,即使原leader節點重新變為可用狀態也不會再次切換。
7. 結束
開發Nebula框架目的是致力於提供一種基於C++快速構建高性能的分散式服務。如果覺得本文對你有用,別忘了到Nebula的Github或碼雲給個star,謝謝。