淺談叢集的分類

本文主要介紹叢集部署相關的知識,介紹叢集部署的基礎,叢集的分類、叢集的負載均衡技術,叢集的可用性以及叢集的容錯機制。隨後介紹Redis-Cluster以及Mysql的架構以及主從複製原理。

單臺伺服器本身會受到頻寬、記憶體、處理器等多方面因素的影響,當透過垂直擴充套件已經無法提升系統性能時,我們就需要透過水平擴充套件,即以叢集的方 式來部署服務。叢集在提升效能的同時,也可以

減輕單機器的壓力,提高系統的可用性。這就是我們常說的加機器,簡單粗暴,但非常有效。

提到叢集,一定會想到分散式,不過要注意這兩者的區別。叢集強調的是服務的多個副本冗餘部署;分散式強調的是將整個系統拆分成多個子系統獨立開發、維護和執行,每個子系統仍然以叢集的方式部署。Redis-Cluster就是典型的分散式系統,每個節點採用一主多從的叢集方式部署。

淺談叢集的分類

Redis-Cliuster架構圖

對於叢集的分類,網上說最多的就是分成高效能叢集、高可用叢集、負載均衡叢集,但我卻不敢苟同。我認為三者之間是分不開的,你中有我,我中有你,比如透過負載均衡就可以實現高效能、高可用,或者說負載均衡是手段,高效能高可用是結果。所以我更傾向於將叢集分成兩種:即無狀態服務的水平擴充套件叢集和有狀態的主從叢集。

無狀態服務的水平擴充套件叢集

該方式實現的前提是服務本身是無狀態的,舉例來說,如果服務是一個Web應用,訪問介面需要使用者登入態,如果session是保留在本地伺服器上,那麼這個服務就是有狀態的。如果使用者的多次請求打到了不同的機器上,就會出現重複登入的問題。但如果session儲存在中介軟體中,比如Redis,那麼就是無狀態的。

透過機器的水平擴容,就構建了服務的叢集,隨後透過負載均衡的演算法,將請求分散到不同的機器上。

負載均衡幾個比較重要的指標:負載均衡演算法、容錯機制以及健康檢測。

負載均衡演算法

1、隨機演算法

思想:服務叢集中的每個服務都配置一個權重weight,預設是100。如果所有權重相等,則隨機選擇一個;如果權重不等,即為加權隨機演算法。比如現在有兩個服務A,B,權重分別是100,200,那麼選取比例就是 1/3,2/3。

Dubbo實現演算法:

@Override protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { // Number of invokers int length = invokers。size(); // Every invoker has the same weight? boolean sameWeight = true; // the weight of every invokers int[] weights = new int[length]; // the first invoker‘s weight int firstWeight = getWeight(invokers。get(0), invocation); weights[0] = firstWeight; // The sum of weights int totalWeight = firstWeight; for (int i = 1; i < length; i++) { int weight = getWeight(invokers。get(i), invocation); // save for later use weights[i] = weight; // Sum totalWeight += weight; if (sameWeight && weight != firstWeight) { sameWeight = false; } } if (totalWeight > 0 && !sameWeight) { 隨機選取小於總權重的整數 int offset = ThreadLocalRandom。current()。nextInt(totalWeight); //這個是關鍵,如果減去總權重是負數,即為選中的Invoker for (int i = 0; i < length; i++) { offset -= weights[i]; if (offset < 0) { return invokers。get(i); } } } //完全隨機 return invokers。get(ThreadLocalRandom。current()。nextInt(length)); }

2、RoundRobin輪詢

最簡單的輪詢就是依次輪詢,比如有N臺伺服器,編號從0到N-1依次輪詢執行,沒有任何的複雜演算法。

下面是一個簡單的實現:

public class RoundRobinBalance { public static final List selectList = Lists。newArrayList(1,2,3); private int getWeight(Integer integer){ return integer; } private AtomicInteger atomicInteger = new AtomicInteger(-1); public int simpleRoundRobin(){ int index = atomicInteger。addAndGet(1); int length = selectList。size(); if (index >= length){ index = atomicInteger。addAndGet(-length); } return selectList。get(index); }}

但實際中,每臺伺服器可能配置不同,我們需要為不同的伺服器增加不同的權重,部分伺服器需要大機率被選中,權重就設定大一些,否則就設定小一些。帶權重的輪詢俗稱WRR演算法。

WRR目前有兩種實現方式,一種是Nginx實現的平滑最大權重,dubbo也是參考這種模式。二是LVS實現的演算法,其優先選擇權重最大的伺服器。

Nginx & Dubbo:不同權重的伺服器,每次選擇權重最大的,選中後,該權重會減去總權重,隨後會加上其初始值。

類似下面的過程:

In case of { 5, 1, 1 } weights this gives the following sequence ofcurrent_weight’s: a b c 0 0 0 (initial state) 5 1 1 (a selected) -2 1 1 3 2 2 (a selected) -4 2 2 1 3 3 (b selected) 1 -4 3 6 -3 4 (a selected) -1 -3 4 4 -2 5 (c selected) 4 -2 -2 9 -1 -1 (a selected) 2 -1 -1 7 0 0 (a selected) 0 0 0

看一下dubbo的實現:

public class RoundRobinLoadBalance extends AbstractLoadBalance { public static final String NAME = “roundrobin”; @Override protected Invoker doSelect(List> invokers, URL url, Invocation invocation) { String key = invokers。get(0)。getUrl()。getServiceKey() + “。” + invocation。getMethodName(); ConcurrentMap map = methodWeightMap。get(key); if (map == null) { methodWeightMap。putIfAbsent(key, new ConcurrentHashMap()); map = methodWeightMap。get(key); } int totalWeight = 0; long maxCurrent = Long。MIN_VALUE; long now = System。currentTimeMillis(); Invoker selectedInvoker = null; WeightedRoundRobin selectedWRR = null; //找出當前權重最大的,並計算總權重。 for (Invoker invoker : invokers) { String identifyString = invoker。getUrl()。toIdentityString(); WeightedRoundRobin weightedRoundRobin = map。get(identifyString); int weight = getWeight(invoker, invocation); long cur = weightedRoundRobin。increaseCurrent(); weightedRoundRobin。setLastUpdate(now); //找出權重最大的 if (cur > maxCurrent) { maxCurrent = cur; selectedInvoker = invoker; selectedWRR = weightedRoundRobin; } totalWeight += weight; } } if (selectedInvoker != null) { //對於選中的,會對其權重進行抵減,值為totalWeight。 selectedWRR。sel(totalWeight); return selectedInvoker; } }}

LVS的加權輪詢每次都會選擇權重最大的,的wiki說明:

Supposing that there is a server set S = {S0, S1, …, Sn-1};W(Si) indicates the weight of Si;i indicates the server selected last time, and i is initialized with -1;cw is the current weight in scheduling, and cw is initialized with zero; max(S) is the maximum weight of all the servers in S;gcd(S) is the greatest common divisor of all server weights in S;while (true) { i = (i + 1) mod n; if (i == 0) { cw = cw - gcd(S); if (cw <= 0) { cw = max(S); if (cw == 0) return NULL; } } if (W(Si) >= cw) return Si;}

Python版的實現:

class Store: __slots__ = (‘index’, ‘weight’) def __init__(self, index, weight): self。index = index self。weight = weightdef weighted(dataset): current = Store(index=-1, weight=0) dataset_length = len(dataset) dataset_max_weight = 0 dataset_gcd_weight = 0 for _, weight in dataset: if dataset_max_weight < weight: dataset_max_weight = weight dataset_gcd_weight = gcd(dataset_gcd_weight, weight) def get_next(): while True: current。index = (current。index + 1) % dataset_length if current。index == 0: current。weight = current。weight - dataset_gcd_weight if current。weight <= 0: current。weight = dataset_max_weight if current。weight == 0: return None if dataset[current。index][1] >= current。weight: return dataset[current。index][0] return get_next

3、最少連線數均衡(Least Connection)

思想:對每一臺伺服器都會記錄當前正在處理的連線數量,當有新的服務連線請求時,將把當前請求分配給連線數最少的伺服器。如果有多臺伺服器都滿足最小連線數,就變成隨機演算法。此種均衡演算法適合長時間處理的請求服務。感興趣的可以看下Dubbo的實現:

LeastActiveLoadBalance。

4、處理能力均衡

思想:把服務請求分配當前系統負載最小的伺服器,由於考慮到了內部伺服器的處理能力及當前網路執行狀況,所以此種均衡演算法相對來說更加精確。

5、目標地址雜湊:

思想:根據請求的目標IP地址,作為雜湊鍵(Hash Key)從散列表找出對應的伺服器。

6、源地址雜湊:

思想:根據請求的源IP地址,作為雜湊鍵(Hash Key)散列表找出對應的伺服器。

7、一致性Hash

相比較普通hash演算法,一致性hash演算法在擴縮容時,影響的資料範圍比較小,方便客戶端做路由快取。針對該演算法,在之前寫的文章小米分庫分表的實踐 中有過介紹。在使用時為了避免資料傾斜,如果伺服器數量較少的情況,要使用虛擬節點。Dubbo的實現方式是預設增加160個虛擬節點。並對每個ip+port後增加數字 1,2,3。。。。。 。一致性Hash的儲存結構最好的是使用紅黑樹TreeMap,因為其實現完美滿足一致性hash演算法。

容錯機制

dubbo客戶端透過watch註冊中心的節點實現動態變化,此外支援不同方式的叢集容錯:

1)failover

失敗自動切換,自動重試其他機器,常用於

操作。注意這裡強調的是讀請求,對於寫請求,最好不要使用該模式。舉例,如果某個介面是扣減金額介面,因為出現死鎖或其他問題,導致介面超時了,此時會繼續呼叫其他的伺服器,這就導致介面被呼叫了兩次或者多次。如果介面沒有設計冪等(哪位大牛會不考慮冪等呢),那問題就比較嚴重了。

(2)failfast

快速失敗,一次呼叫失敗就立即失敗,常用於非冪等的

操作或者是對時延要求較高的場景。

(3)failsafe

呼叫異常時忽略異常,不報錯。該模式多用於一些不是特別重要的介面呼叫,比如郵件通知、資料統計等等。

(4)failback

失敗後自動記錄請求,然後定時重發,比較適合於一些非同步操作,比如傳送訊息。

(5)forking

並行呼叫多個provider,只要一個成功就立即返回。常用於實時性要求比較高的讀操作,但是會浪費更多的服務資源,可透過forks=“2”來設定最大的並行數

(6)broadcacst

逐個呼叫所有的provider,任何一個provider出錯則報錯。常用於通知所有提供者更新快取或日誌等本地資源資訊。

健康檢測

對於負載均衡的伺服器,應該具備某種健康檢測機制,確保如果某個伺服器異常下線,需要將其從負載均衡列表中摘除。健康檢測可以是被動檢測,也可以是主動檢測。

被動檢測思想類似於熔斷,當多次呼叫超時或者失敗,會在一定時間內不再訪問對應伺服器。下面是參考資料中的一個示意圖,闡述得比較清晰。

淺談叢集的分類

被動檢測的最大問題就是對於異常的機器不能及時摘除,導致仍然會有請求分發到異常機器上。因此

如Dubbo+nacos實現的負載均衡,nacos透過和客戶端和服務端建立連線,每幾秒都會檢測目標機器是否異常,如果異常會從當前註冊列表中摘除。不過在實際使用中要注意,摘除異常並不是實時的,是定時發生的,也就是說存在客戶端依然會呼叫異常Ip的可能。

負載均衡分類

目前負載均衡的實現有很多種,從TCP/IP協議棧的角度劃分,包括四層負載均衡、七層負載均衡。

四層負載均衡:即在傳輸層實現的負載均衡,如LVS,Nginx都支援四層負載均衡。

七層負載均衡:即在應用層實現的負載均衡,如HTTP協議、DNS協議等,HTTP協議實現的負載均衡如Nginx,Dubbo等。

從軟硬體角度可劃分出DNS負載均衡、軟體負載均衡、硬體負載均衡。

硬體負載均衡需要購買專門的硬體產品實現,而且都很昂貴,基本上是淘汰的,過時的。

軟體負載均衡是透過軟體來實現的,價格比較便宜,效能也比較優越。如nginx,lvs都是優秀的負載均衡軟體。

在實際應用中,通常情況會結合著使用不同的負載均衡技術。如下圖所示:

淺談叢集的分類

七層負載均衡:

Nginx HTTP協議

DNS DNS協議

Dubbo HTTP協議

四層負載均衡:

Nginx

LVS

Nginx負載均衡

nginx支援七層和四層負載均衡兩種,不過四層負載均衡需要第三方外掛的支援。

Nginx七層負載均衡的配置非常簡單。

upstream account_backend { server 127。0。0。1:9090 max_fails=3 fail_timeout=10s weight=2; server 127。0。0。2:9090 max_fails=3 fail_timeout=10s; server 127。0。0。3:9090 max_fails=3 fail_timeout=10s;}

當請求過來時,會根據某種演算法選擇某一個ip進行訪問。Nginx預設使用加權輪詢,權重預設是1。此外,Nginx還內建了ip hash演算法,透過對客戶端ip進行hash選擇一個伺服器,透過該方法可保證同一使用者的可只打到一個後端機器上。

upstream account_backend { ip_hash; server 127。0。0。1:9090 max_fails=3 fail_timeout=10s ; server 127。0。0。2:9090 max_fails=3 fail_timeout=10s; server 127。0。0。3:9090 max_fails=3 fail_timeout=10s;}

當後端伺服器叢集中某個掛掉了,Nginx會自動將請求轉到其他節點上。但是它預設不會將該異常節點摘除,後續的請求還是可能打到異常機器上的。至於這點,可以看這篇文章,該文章詳細介紹了該如何進行後端健康檢測的: Nginx負載均衡中後端節點伺服器健康檢查 - 運維筆記 - 散盡浮華 - 部落格園,主要介紹了淘寶團隊開發的nginx繼承模組,透過主動檢測實現自動剔除異常的server。

DNS負載均衡

DNS負載均衡實現的是廣域網的全域性負載均衡,透過為域名配置不同的A記錄,實現將請求按照權重分散到不同的ip。這裡的ip並不一定是實際的伺服器ip,可以是nginx叢集,也可以是lvs的vip。

下面是騰訊雲DNSPod的一個實操:

淺談叢集的分類

優點

近乎零成本,因為域名註冊商的這種解析都是免費的;

部署方便,除了網路拓撲的簡單擴增,加伺服器只需要在配置里加上相應ip。

缺點

健康檢查,如果某臺伺服器宕機,DNS伺服器是無法知曉的,仍舊會將訪問分配到此伺服器。修改DNS記錄全部生效時間需要很久。

快取問題。一般低階DNS都會快取域名IP對映關係,如果某個出現問題或者發生變化,不會及時更新。

不知道大家聽說過2009年的暴風影音事件,因為兩家遊戲公司惡意競爭,一個公司惡意攻擊另外一家公司網站,導致DnsPod伺服器受到Ddos攻擊,再加上當時著名的暴風影音使用了免費的DnsPod伺服器,也受到了影響,那時的暴風今非昔比,使用者量巨大,導致大量域名解析請求達到了電信的DNS伺服器,佔用了典型機房的1/3的頻寬,於是乎,被電信封掉了ip。這一封不要緊,直接導致使用DnsPod解析的網站全部無法訪問,數量達到10萬+。有感興趣的可以看看歷史回顧,很精彩,DnsPod創始人很厲害,叫吳洪聲,作為一個大專生,靠著愛好和堅持搭建了DnsPod,現在已經被騰訊收購了,妥妥的人生贏家。

LVS負載均衡

四層負載均衡是針對七層負載均衡的一個補充,當Nginx的併發超過上線時,就需要透過LVS+Nginx叢集實現高併發。

主要模式主要有以下幾種:

LVS-NAT 主要透過網路地址轉換,修改目的IP實現。Network Address Translation

LVS-TUN 主要封裝一層IP頭 IP Tunneling

LVS-DR 主要是修改目的MAC Direct Routing

LVS-FULLNAT 這個是阿里研發的一種模式,主要是解決多vlan的場景,它會修改請求報文的(源/目的)地址、(源/目的)埠。

先看幾個LVS的名詞定義:

CIP:客戶端ip

Director:負載排程叢集的主機,簡稱DR

VIP:Virtual IP,向外提供服務的IP

RIP:Real Server IP,內部真正提供服務的IP

DIP:DR主機用於內部通訊的IP

1、LVS-NAT

流程示意圖:

淺談叢集的分類

其請求處理流程:

1。客戶端傳送請求到LVS, 目標IP地址為VIP

2。 LVS根據某種負載均衡演算法選擇一個Real-server,並記錄連線資訊到hash表中,然後修改客戶端的request的目的IP地址為選擇的RS(這個RIP只是內部通訊用的),隨後將請求發給RS,此時源IP為CIP,目的IP為RIP;

3。 RS收到request包後,發現目的IP是自己的IP,於是處理請求,然後傳送reply給LVS,此時源IP為RIP,目的IP是CIP;

4。 LVS收到reply包後,修改reply包的的源地址為VIP,原埠為VIP埠;

5。 LVS將reply傳送給客戶端;

6。 客戶端來的屬於本次連線的包,查hash表,然後發給對應的RS;

7。 客戶端傳送完畢,此次連線結束時,LVS自動從hash表中刪除此條記錄;

上面流程的特點是:

1、DR和RS必須是在同一個網段內,RS的閘道器配置成DIP,RIP和DIP都是用於內網機器間通訊的IP。我感覺說的直白點,DR就是區域網閘道器的角色;

2、所有請求響應都要經過DR,這必然會導致DR會成為整個網路的瓶頸。

2、LVS-TUN

淺談叢集的分類

1。客戶端Client 傳送request包到LVS伺服器, 目標地址VIP;

2。 LVS按照演算法選擇後端的一個Real-server,並將記錄一條訊息到hash表中,然後將客戶端的request包封裝到一個新的IP包裡,新IP包的目的IP是RIP,源IP是DIP,然後轉發給RS;

3。 RS收到包後,解封裝,取出客戶端的request包,發現還有一個IP包,目的地址是VIP,而RS發現在自己的虛擬網絡卡tunl0上有這個IP地址,於是處理客戶端的請求,處理完成透過虛擬網絡卡傳送給eth0網口傳送出去,此時源IP為VIP,目的地址是CIP;

4。 該客戶端的後面的request包,LVS直接按照hash表中的記錄直接轉發給Real-server,當傳輸完畢或者連線超時,那麼將刪除hash表中的記錄。

該模式中:

RIP,VIP,DIP都可以是公網地址,可以跨網段;

請求報文都走DR,響應不走,直接由RS傳送給Client。這個特點使得TUN的效能要相比NAT提升了幾倍,且解決了跨網段問題,問題就在於其維護成本挺高的;

3、LVS-DR

淺談叢集的分類

1、客戶端Client 傳送request包到LVS伺服器, 目標地址VIP;

2、LVS根據負載均衡演算法選擇一臺Realserver,將此RIP所在網絡卡的mac地址作為目標mac地址,傳送到局域網裡,因為是在資料鏈路層,所以必須在同一區域網;

3、RS在區域網中收到這個幀,拆開後發現目標IP不是自己的IP,正常來講RS會拋棄的,但由於我們在loopback介面上配置了VIP,所以RS會接收該請求包並進行處理。

4、處理後,RS直接經過網路傳送給客戶端,源IP為VIP,目的IP為CIP。

LVS-DR技術解決了NAT的DR瓶頸問題,提高效能,但其最大限制就是DR和RIP必須是同一網段的,不能解決跨網段的問題。

4、LVS-FULLNAT

這個是阿里做的,LVS的DR和NAT模式要求RS和DR在同一個vlan中,導致部署成本過高;TUN模式雖然可以跨vlan,但RealServer上需要部署ipip隧道模組等,網路拓撲上需要連通外網,較複雜,不易運維。

為了解決上述問題,開發出FULLNAT,該模式和NAT模式的區別是:資料包進入時,除了做DNAT,還做SNAT(使用者ip->內網ip),從而實現LVS-RealServer間可以跨vlan通訊,RealServer只需要連線到內網。

淺談叢集的分類

當客戶端訪問VIP後,DR會將源IP改成DIP,目的IP改成RIP。剩下的就是內網呼叫的事情了,可以是同網段,也可以是不同網段,至於不同Vlan如何通訊,這個就不多說了,就是路由策略配置的問題了。

FULLNAT相對於之前那三種方式,配置維護都很簡單,可以跨網段。現在也是LVS主流的使用模式。

LVS本身的部署有以下幾種模式:

1、單機模式,一個LVS對應多個Nginx server;

2、一主一備+KeepAlived,提高可用性;

3、叢集模式。透過FullNAT+OSPF-ECMP,並使用一致性hash演算法實現分流。

目前最多的就是叢集部署方式,其特點為:

LVS 和上聯的三層交換機間執行 OSPF 協議。 OSPF 開放最短路徑優先,是一種常用的路由協議。

上聯交換機透過 ECMP 等價路由,將資料流分發給 LVS 叢集。ECMP充分應用在各大路由協議中,如OSPF,ISIS、EIGRP、BGP等。它可保證如果存在多條相等路徑,會充分利用多條路由,從而實現基於流的負載均衡(如果不開ECMP,那麼當選擇一條路由後,或許都會一直使用該路由,其他相等路徑都不會走)。

LVS 叢集再轉發給業務伺服器。

淺談叢集的分類

目前我們公司的所有服務幾乎都用了LVS。即便時我開發的結算系統,雖然是ToB和內部財務使用,也同樣用了LVS,用了兩臺Real Server,1臺DIP,利用權重輪詢。不過現在還把一臺給停了,另外一臺權重配置成100%了,哈哈。

有狀態的需要儲存資料的主從叢集

有些伺服器為了實現高可用性,會冗餘部署多臺伺服器,並設定主從關係。可以是一主多從,也可以是多主多從。對於這種型別的叢集部署,重要的是完成資料的複製以及當主伺服器down時,可以實現自動或者手動切換,某臺slave可以自動切換成主伺服器。其中最典型的例子就是Mysql主從以及Redis中的主從部署,本文主要是介紹這兩種。

Mysql高可用架構

Mysql的高可用架構有很多中,有一主一從,一主多從,雙主+KeepAlive等。其中最常用的就是一主多從。透過水平擴充套件的方式可以實現Mysql資料庫的高可用,同時實現流量分攤,並能夠在某臺master異常時,能夠透過一定手段完成切換。

淺談叢集的分類

引入叢集的部署模式後,在帶來高可用、高效能的同時也引入了新問題,比如如何保證主從資料庫的資料一致性、如何解決主從延遲、如何完成流量的切換等問題。

資料需要從主庫同步到從資料庫,從而保證資料的一致性,並透過不同的複製模式來實現強一致性、弱一致性、最終一致性等。先看一下Mysql主從複製的流程:

主庫將資料更改記錄到binlog中;

從庫開啟一個IO執行緒,並在主庫開啟一個特殊的binlog dump執行緒,將binlog資料 複製到從庫的relay log中;從庫並不是不斷輪詢去做同步操作。而是當主庫有資料更改時,會給主庫傳送一個訊號量。主庫返回的資訊不僅僅有binlog內容,還有新的binlog檔名,它以及下一個更新的binlog檔案的位置。

從庫使用SQL執行緒讀取relay log中的資料,並將其寫入到從庫資料表。

可以看一下slave的processlist:

ysql> show processlist\G;*************************** 1。 row *************************** Id: 4 User: system user Host: db: NULLCommand: Connect Time: 5205 State: Waiting for master to send event Info: NULL*************************** 2。 row *************************** Id: 5 User: system user Host: db: NULLCommand: Connect Time: 5113 State: Slave has read all relay log; waiting for more updates Info: NULL*************************** 3。 row ***************************

上面就是從庫的兩個執行緒,一個IO執行緒,一個SQL執行緒。實際上在Mysql5。6之後,SQL執行緒不僅僅只有一個,可以是多SQL執行緒併發執行。

淺談叢集的分類

傳統的複製是根據檔案位置開始進行的,但自從5。6之後引入了一個叫GTID的概念。GTID,global transaction identifieds全域性事務標識。這個標識是唯一的,它可保證一個事務只能被執行一次。

在主從複製過程中,Master在更新資料時會生成一個GTID,並寫在binlog中。從庫的SQL執行緒在執行中繼日誌的過程中,會檢查從庫的binlog是否已經有了某個GTID記錄,如果有就略過,如果沒有就執行。

我們可以檢視當前Mysql是否已經開啟了GTID。

mysql> show variables like “gtid_mode”;+————————-+————-+| Variable_name | Value |+————————-+————-+| gtid_mode | ON |+————————-+————-+

Mysq複製方式主要包括非同步複製、同步複製和半同步複製。

非同步複製:當master執行了相應的寫操作,執行成功後直接返回客戶端。隨後非同步得將資料同步到slave。這種好處就是在完成複製的同時,不會犧牲系統的響應速率,但最大的問題就是不能保證資料一致性;

同步複製:當master在執行寫操作時,必須要等到所有slave庫的relay log寫完,才能夠返回給客戶端。該模式可以較好低保證資料一致性,但卻嚴重影響系統性能,從庫越多,效能越差;

半同步複製:該複製模式是非同步方式和同步複製的折中方案,只要slave中有一個成功寫入relay log,master就可直接返回,如果在一定時間內,沒有slave寫入成功,則複製模式變為非同步複製。

Mysql預設是採用非同步複製的,可能其更看重的是系統性能,系統崩潰的機率還是比較低的,在實際中,我們可以根據自身使用場景來選擇複製模式。

binlog模式

目前binlog的記錄模式主要有三種,基於語句(statment),基於行(Row)模式,混合(mixed)模式。

基於語句的模式就是binlog記錄的是所有可能影響資料變化的sql語句。然後在從庫中再完全相同地執行一遍。這種模式比較好的地方是隻記錄語句,不用記錄每一行的變化,資料量相對不大,節省頻寬。比如一條批次update,只需要傳一條語句,不用傳每個更改的資料。但這種模式的缺點就是雖然主從庫執行的sql完全相同。但主從庫的上下文環境是不同的,而sql可能是嚴格依賴於環境的,比如當前時間戳等等。

基於行的模式,是記錄被修改的資料,而不是sql語句。這種模式使得從庫並不會因為環境不同,導致資料不能完全一致。但基於行的模式,可能會導致binlog的資料量特別大,就像上面說到的update,可能會在binlog產生更多的資料記錄。這給複製造成了更大的開銷。

mix模式,該模式是上述兩種模式的組合。Mysql會根據實際的sql來決定選擇哪個模式進行寫入。

Redis-Cluster

Redis的系統架構經過了多次的演進,最開始的架構模式是一主多從,但由於其無法實現故障自動切換,隨後增加了sentinel哨兵機制,即在主從的基礎上增加一個哨兵進行監控,從而實現master出現故障時可自動切換。

淺談叢集的分類

從上面示意圖可以看出,哨兵機制需要額外的機器節點來實現,完全和Redis無關,不儲存資料,只用來監控,這在一定程度上造成了資源的浪費。基於此,Redis官方又開發出一套新的Redis架構,即Redis-Cluster,一個純分散式的系統架構,示意圖在文章最開始已經畫出來了,這裡再貼上一下。

淺談叢集的分類

多個master節點透過Gossip協議實現元資訊的互動和通訊,實現命令包括Meet,Ping,Pong,Fail等,本文對此不做過多闡述,只講述每個master節點和其slave所構成的叢集模式。

Redis-Cluster的master-slave主要有以下幾個作用:

1、保持高可用;

當某個master節點下線時,會自動切換到某個slave,slave會升級為新的master,從而實現故障的自動切換。具體流程是:

1、透過Ping訊息,master節點在一定的超時時間內沒有響應,就被標記為疑似下線,如果有多個節點認為該master節點疑似下線,那麼該master就標記為下線。

2、master下的slave發起選舉,其他的master節點會為相應的slave進行投票,根據Raft協議,選擇出新的master節點;

這裡要注意兩點。一是slave發起選舉,二是master投票。

Slave在認為master進入FAIL之後會delay一段時間才會發起選舉,其原因是為了讓master的FAIL狀態在叢集內廣播。不同的slave的delay時間是不同的,從而避免一個master的多個slave在同一時間發起election。

master投票的條件:

Slave所屬的master的狀態為FAIL

Slave的currentEpoch大於等於master的currentEpoch

Slave的configEpoch大於等於master認為該slave所屬master的configEpoch

Master維護一個lastVoteEpoch欄位,針對每一個epoch只會投票一次,一旦投票後就會拒絕所有更小的epoch

投票就會回覆ACK,否則就忽略

3、slave選舉為新的master節點後,會被分配舊master的雜湊槽。

2、實現讀寫分離;

Redis-Cluster中的master節點對外提供寫服務,slave可以對外提供讀服務。

如果要實現高可用和讀寫分離,前提是要實現主從複製,即保證主從節點的資料的一致性。Redis的主從複製主要包括兩種 全量複製和部分複製。其中全量複製通常發生在第一次同步過程或者在部分複製出現異常時。

當在slave中使用slaveOf(Redi5。0之後使用

replicaof),確定slave和master的關係。

slave of ip port

具體複製流程可見下圖,來自小林coding:

淺談叢集的分類

1、執行slaveof之後,主從伺服器會建立一個長連線。隨後從伺服器向主伺服器傳送psync命令(早期版本是syncc),psync命令有兩個引數,一個是runid,表示的是主伺服器id;一個是offset,表示的是偏移量。第一次執行時,從伺服器不知道主伺服器的runId,所以引數是“?”,且沒有讀取任何資料,偏移量是-1;

2、主伺服器收到命令後,會開始執行全量複製。首先會執行bgsave命令,生成RDB檔案,隨後將其傳送給從伺服器,slave收到RDB檔案之後,會載入RDB檔案資料。

3、在上一步執行期間,有可能還會有寫命令進入,而這些寫命令執行結果並沒有在RDB檔案中,因此主伺服器為了處理這部分資料,在執行間隙會將寫命令寫入到一個叫做replication buffer緩衝區中。當slave載入完RDB檔案之後,主伺服器會將緩衝區中的寫命令傳送給slave,隨後slave繼續執行這些寫命令;

4、完成第一次全量同步之後,後續只要主伺服器有寫命令產生,就會將寫命令非同步地同步給從伺服器。

5、如果某個slave網路斷開之後恢復了。從伺服器會給master傳送psync命令,其中會傳入主伺服器的runid以及讀偏移量。

6、主伺服器會判斷從伺服器要讀取的偏移量資料是否在

repl_backlog_buffer中,如果在就執行增量複製;如果不在,則執行全量複製。

淺談叢集的分類

本文主要講述了兩種不同的叢集模式,一是無狀態的叢集;二是有狀態,需做資料儲存的叢集兩種。無狀態叢集介紹了負載均衡分類以及負載均衡演算法;有狀態叢集介紹了主從複製的原理和流程。

參考資料:

深入淺出負載均衡 - SegmentFault 思否

[萬字長文] 吃透負載均衡 - 高效能架構探索

Dubbo 一致性Hash負載均衡實現剖析 | Apache DubboDUBBO LOGO

Nginx負載均衡(僅學習) - 知乎

千與千尋-Mysql複製

詳解nginx的原生被動健康檢查機制&災備使用(含測試)_無影V隨風的部落格-CSDN部落格_被動健康檢查

高併發場景 LVS 安裝及高可用實現 - 慘綠少年 - 部落格園

Redis哨兵模式(sentinel)學習總結及部署記錄(主從複製、讀寫分離、主從切換) - 散盡浮華 - 部落格園

主從複製是怎麼實現的? | 小林coding

深度探索MySQL主從複製原理 - 知乎