節(jié)點(diǎn)通信
-
通信流程
在分布式存儲(chǔ)中需要提供維護(hù)節(jié)點(diǎn)元數(shù)據(jù)信息的機(jī)制劫樟,所謂元數(shù)據(jù)是指:節(jié)點(diǎn)負(fù)責(zé)那些數(shù)據(jù),是否出現(xiàn)故障等狀態(tài)信息织堂。常見的元數(shù)據(jù)維護(hù)方式分為:集中式和P2P方式毅哗。Redis集群采用P2P的Gossip(留言)協(xié)議,Gossip協(xié)議工作原理就是節(jié)點(diǎn)彼此不斷通信交換信息捧挺,一段時(shí)間后所有的節(jié)點(diǎn)都會(huì)知道集群完整的信息虑绵,這種方式類似留言傳播。
通信過程說明闽烙;
1)集群中的每個(gè)節(jié)點(diǎn)都會(huì)單獨(dú)開辟一個(gè)TCP通道翅睛,用于節(jié)點(diǎn)之間彼此通信,通信端口號(hào)在基礎(chǔ)端口上加10000.
2)每個(gè)節(jié)點(diǎn)在固定周期內(nèi)通過特定規(guī)則選擇幾個(gè)節(jié)點(diǎn)發(fā)送ping消息黑竞。
3)接收到ping消息的節(jié)點(diǎn)用pong消息作為相應(yīng)捕发。
集群中每個(gè)節(jié)點(diǎn)通過一定規(guī)則挑選要通信的節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)可能知道全部節(jié)點(diǎn)很魂,也可能僅知道部分節(jié)點(diǎn)扎酷,只要這些節(jié)點(diǎn)彼此可以正常通信,最終它們會(huì)達(dá)到一致的狀態(tài)遏匆。當(dāng)節(jié)點(diǎn)出故障法挨、新節(jié)點(diǎn)加入、主從角色變化幅聘、槽信息變更等時(shí)間發(fā)生時(shí)凡纳,通過不斷的ping/pong消息通信,經(jīng)過一段時(shí)間后所有的節(jié)點(diǎn)都會(huì)知道整個(gè)集群全部節(jié)點(diǎn)的最新狀態(tài)帝蒿,從而達(dá)到集群狀態(tài)同步的目的荐糜。
-
Gossip消息
Gossip協(xié)議的主要職責(zé)就是信息交換。信息交換的載體就是節(jié)點(diǎn)彼此發(fā)送的Gossip消息,了解這些消息有助于我們理解集群如何完成信息交換暴氏。
常用的Gossip消息可分為:ping消息延塑、pong消息、meet消息答渔、fail消息等关带。
meet消息:用于通知新節(jié)點(diǎn)加入。消息發(fā)送者通知接收者加入到當(dāng)前集群研儒,meet消息通信正常完成后豫缨,接收節(jié)點(diǎn)會(huì)加入到集群中并進(jìn)行周期性的ping独令、pong消息交換端朵。
ping消息:集群內(nèi)交換最頻繁的消息,集群內(nèi)每個(gè)節(jié)點(diǎn)每秒向多個(gè)其他節(jié)點(diǎn)發(fā)送ping消息燃箭,用于檢測節(jié)點(diǎn)是否在線和交換彼此狀態(tài)信息冲呢。ping消息發(fā)送封裝了自身節(jié)點(diǎn)和部分其他節(jié)點(diǎn)的狀態(tài)數(shù)據(jù)。
pong消息:當(dāng)接收到ping招狸、meet消息時(shí)敬拓,作為響應(yīng)消息回復(fù)給發(fā)送方確認(rèn)消息正常通信。pong消息內(nèi)部封裝了自身狀態(tài)數(shù)據(jù)裙戏。節(jié)點(diǎn)也可以向集群內(nèi)廣播自身的pong消息來通知整個(gè)集群對自身狀態(tài)進(jìn)行更新乘凸。
fail消息:當(dāng)節(jié)點(diǎn)判定集群內(nèi)另一個(gè)節(jié)點(diǎn)下線是,會(huì)向集群內(nèi)廣播一個(gè)fail消息累榜,其他節(jié)點(diǎn)接收到fail消息之后吧對應(yīng)節(jié)點(diǎn)更新為下線狀態(tài)营勤。
所有的消息格式劃分為:消息頭和消息體。消息頭包含發(fā)送節(jié)點(diǎn)自身狀態(tài)數(shù)據(jù)壹罚,接收節(jié)點(diǎn)根據(jù)消息頭就可以獲取到發(fā)送節(jié)點(diǎn)的相關(guān)數(shù)據(jù)葛作,結(jié)構(gòu)如下:
typedef struct { char sig[4]; /* 信號(hào)標(biāo)示 */ uint32_t totlen; /* 消息總長度 */ uint16_t ver; /* 協(xié)議版本 */ uint16_t type; /* 消息類型,用于區(qū)分meet猖凛,ping赂蠢,ping等消息 */ uint16_t count; /* 消息體包含的節(jié)點(diǎn)數(shù)量,僅用于meet辨泳,ping虱岂,pong消息類型 */ uint64_t currentEpoch; /* 當(dāng)前發(fā)送節(jié)點(diǎn)的配置紀(jì)元 */ uint64_t configEpoch; /* 主節(jié)點(diǎn)/從節(jié)點(diǎn)的主節(jié)點(diǎn)配置紀(jì)元 */ uint64_t offset; /* 復(fù)制偏移量 */ char sender[CLUSTER_NAMELEN]; /* 發(fā)送節(jié)點(diǎn)的nodeId */ unsigned char myslots[CLUSTER_SLOTS/8]; /* 發(fā)送節(jié)點(diǎn)負(fù)責(zé)的槽信息 */ char slaveof[CLUSTER_NAMELEN]; /* 如果發(fā)送節(jié)點(diǎn)是從節(jié)點(diǎn),記錄對應(yīng)主節(jié)點(diǎn)的nodeId */ uint16_t port; /* 端口號(hào) */ uint16_t flags; /* 發(fā)送節(jié)點(diǎn)標(biāo)識(shí)菠红,區(qū)分主從角色量瓜,是否下線等 */ unsigned char state; /* 發(fā)送節(jié)點(diǎn)所處的集群狀態(tài) */ unsigned char mflags[3]; /* 消息標(biāo)識(shí) */ union clusterMsgData data; /* 消息正文 */ } clusterMsg;
集群內(nèi)所有的消息都采用相同的消息頭結(jié)構(gòu)clusterMsg,它包含了發(fā)送節(jié)點(diǎn)關(guān)鍵信息途乃,如節(jié)點(diǎn)id绍傲、槽映射、節(jié)點(diǎn)標(biāo)識(shí)(主從角色,是否下線)等烫饼。消息體在Redis內(nèi)部采用clusterMsgData結(jié)構(gòu)聲明猎塞,結(jié)構(gòu)如下:
union clusterMsgData { /* ping,meet,pong 消息體 */ struct { /* gossip消息結(jié)構(gòu)數(shù)組 */ clusterMsgDataGossip gossip[1]; } ping; /* FAIL 消息體 */ struct { clusterMsgDataFail about; } fail; // ... };
消息體clusterMsgData定義發(fā)送消息的數(shù)據(jù),其中ping杠纵、meet荠耽、pong都采用clusterMsgDataGossip數(shù)組作為消息體數(shù)據(jù),實(shí)際消息類型使用消息頭的type屬性區(qū)分比藻。每個(gè)消息體包含該節(jié)點(diǎn)的多個(gè)clusterMsgDataGossip結(jié)構(gòu)數(shù)據(jù)铝量,用于信息交換,結(jié)構(gòu)如下:
typedef struct { char nodename[CLUSTER_NAMELEN]; /* 節(jié)點(diǎn)的nodeId */ uint32_t ping_sent; /* 最后一次向該節(jié)點(diǎn)發(fā)送ping消息時(shí)間 */ uint32_t pong_received; /* 最后一次接收該節(jié)點(diǎn)pong消息時(shí)間 */ char ip[NET_IP_STR_LEN]; /* IP */ uint16_t port; /* port */ uint16_t flags; /* 該節(jié)點(diǎn)標(biāo)識(shí) */ } clusterMsgDataGossip;
當(dāng)接收到ping银亲、meet消息時(shí)慢叨,接收節(jié)點(diǎn)會(huì)解析消息內(nèi)容并根據(jù)自身的識(shí)別情況作出相應(yīng)處理,執(zhí)行解析消息頭和消息體的流程:
解析消息頭過程:消息頭包含了發(fā)送節(jié)點(diǎn)的信息务蝠,如果發(fā)送節(jié)點(diǎn)是新節(jié)點(diǎn)且消息是meet類型拍谐,則加入本地節(jié)點(diǎn)列表;如果是已知節(jié)點(diǎn)馏段,則嘗試更新發(fā)送節(jié)點(diǎn)的狀態(tài)轩拨,如槽映射關(guān)系、主從角色等狀態(tài)院喜。
解析消息體過程:如果消息體的clusterMsgDataGossip數(shù)組包含的節(jié)點(diǎn)是新節(jié)點(diǎn)亡蓉,則嘗試發(fā)起與新節(jié)點(diǎn)的meet握手流程;如果是已知節(jié)點(diǎn)喷舀,則根據(jù)clusterMsgDataGossip中的flags字段判斷該節(jié)點(diǎn)是否下線砍濒,用于故障轉(zhuǎn)移。
消息處理完后回復(fù)pong消息元咙,內(nèi)容同樣包含消息頭和消息體梯影,發(fā)送節(jié)點(diǎn)接收到回復(fù)的pong消息后,采用類似的流程解析處理消息并更新與接收節(jié)點(diǎn)最后通信時(shí)間庶香,完成那個(gè)一次消息通信甲棍。
-
節(jié)點(diǎn)選擇
雖然Gossip協(xié)議的信息交換機(jī)制具有天然的分布式特性,但它是有成本的赶掖。由于內(nèi)部需要頻繁地進(jìn)行節(jié)點(diǎn)信息交換感猛,而ping/pong消息會(huì)攜帶當(dāng)前節(jié)點(diǎn)和部分其他節(jié)點(diǎn)的狀態(tài)數(shù)據(jù),勢必會(huì)加重帶寬和計(jì)算的負(fù)擔(dān)奢赂。Redis集群內(nèi)節(jié)點(diǎn)通信采用固定頻率(定時(shí)任務(wù)每秒執(zhí)行10次)陪白。因此節(jié)點(diǎn)每次選擇需要通信的節(jié)點(diǎn)列表變得非常重要。通信節(jié)點(diǎn)選擇過多雖然可以做到信息及時(shí)交換但成本過高膳灶。節(jié)點(diǎn)選擇過少會(huì)降低集群內(nèi)所有節(jié)點(diǎn)彼此信息交換頻率咱士,從而影響故障判定立由、新節(jié)點(diǎn)發(fā)現(xiàn)等需求的速度。因此Redis集群Gossip協(xié)議需要兼顧信息交換實(shí)時(shí)性和成本開銷序厉。
消息交換的成本主要體現(xiàn)在單位時(shí)間選擇發(fā)送消息的節(jié)點(diǎn)數(shù)量和每個(gè)消息攜帶的數(shù)據(jù)量锐膜。
-
選擇發(fā)送消息的節(jié)點(diǎn)數(shù)量
集群內(nèi)每個(gè)節(jié)點(diǎn)維護(hù)定時(shí)任務(wù)默認(rèn)每秒執(zhí)行10次,每秒會(huì)隨機(jī)選取5個(gè)節(jié)點(diǎn)找出最久沒有通信的節(jié)點(diǎn)發(fā)送ping消息弛房,用于保證Gossip信息交換的隨機(jī)性道盏。每100毫秒都會(huì)掃描本地節(jié)點(diǎn)列表,如果發(fā)現(xiàn)節(jié)點(diǎn)最近一次接收pong消息的時(shí)間大于cluster_node_timeout/2文捶,則立刻發(fā)送ping消息荷逞,防止該節(jié)點(diǎn)的信息太長時(shí)間未更新。根據(jù)以上規(guī)則的出每個(gè)節(jié)點(diǎn)每秒需要發(fā)送ping消息的數(shù)量=1 + 10 * num(node.pong_received > cluster_node_timeout/2)粹排,因此cluster_node_timeout參數(shù)對消息發(fā)送的節(jié)點(diǎn)數(shù)量影響非常大种远。當(dāng)我們的帶寬資源緊張是,可以適當(dāng)調(diào)大這個(gè)參數(shù)恨搓,如從默認(rèn)15秒改為30秒來減低帶寬占用率院促。過度調(diào)大cluster_node_timeout會(huì)影響消息交換的頻率從而影響故障轉(zhuǎn)移筏养、槽信息更新斧抱、新節(jié)點(diǎn)發(fā)現(xiàn)的速度。因此需要根據(jù)業(yè)務(wù)容忍度和資源消耗進(jìn)行平衡渐溶。同時(shí)整個(gè)集群消息總交換量也跟節(jié)點(diǎn)數(shù)成正比辉浦。
-
消息數(shù)據(jù)量
每個(gè)ping消息的數(shù)據(jù)量體現(xiàn)在消息頭和消息體中,其中消息頭主要占用空間的字段是myslots[CLUSTER_SLOTS/8]茎辐,占用2KB宪郊,這塊空間占用相對固定。消息體會(huì)攜帶一定數(shù)量的其他節(jié)點(diǎn)信息用于信息交換拖陆。具體數(shù)量見一下偽代碼:
def get_wanted(): int total_size = size(cluster.nodes) # 默認(rèn)包含節(jié)點(diǎn)總量的1/10 int wanted = floor(total_size/10); if wanted > 3: # 至少攜帶3個(gè)其他節(jié)點(diǎn)信息 wanted = 3; if wanted > total_size - 2 : # 最多包含total_size - 2 個(gè) wanted = total_size - 2; return wanted;
根據(jù)偽代碼可以看出消息體攜帶數(shù)據(jù)量跟集群的節(jié)點(diǎn)數(shù)息息相關(guān)弛槐,更大的集群每次消息通信的成本也就更高,因此對于Redis集群來說并不是大而全的集群更好依啰,對于集群規(guī)暮醮控制的建議在后面的章節(jié)會(huì)介紹。
-