十六號(hào)…… 四月十六號(hào)。一九六零年四月十六號(hào)下午三點(diǎn)之前的一分鐘你和我在一起,因?yàn)槟阄視?huì)記住這一分鐘。從現(xiàn)在開始我們就是一分鐘的朋友,這是事實(shí),你改變不了,因?yàn)橐呀?jīng)過去了。我明天會(huì)再來。
—— 《阿飛正傳》
現(xiàn)實(shí)生活中時(shí)間是很重要的概念,時(shí)間可以記錄事情發(fā)生的時(shí)刻、比較事情發(fā)生的先后順序。分布式系統(tǒng)的一些場(chǎng)景也需要記錄和比較不同節(jié)點(diǎn)間事件發(fā)生的順序,但不同于日常生活使用物理時(shí)鐘記錄時(shí)間,分布式系統(tǒng)使用邏輯時(shí)鐘記錄事件順序關(guān)系,下面我們來看分布式系統(tǒng)中幾種常見的邏輯時(shí)鐘。
物理時(shí)鐘 vs 邏輯時(shí)鐘
可能有人會(huì)問,為什么分布式系統(tǒng)不使用物理時(shí)鐘(physical clock)記錄事件?每個(gè)事件對(duì)應(yīng)打上一個(gè)時(shí)間戳,當(dāng)需要比較順序的時(shí)候比較相應(yīng)時(shí)間戳就好了。
這是因?yàn)楝F(xiàn)實(shí)生活中物理時(shí)間有統(tǒng)一的標(biāo)準(zhǔn),而分布式系統(tǒng)中每個(gè)節(jié)點(diǎn)記錄的時(shí)間并不一樣,即使設(shè)置了 NTP 時(shí)間同步節(jié)點(diǎn)間也存在毫秒級(jí)別的偏差[1][2]。因而分布式系統(tǒng)需要有另外的方法記錄事件順序關(guān)系,這就是邏輯時(shí)鐘(logical clock)。
Lamport timestamps
Leslie Lamport 在1978年提出邏輯時(shí)鐘的概念,并描述了一種邏輯時(shí)鐘的表示方法,這個(gè)方法被稱為L(zhǎng)amport時(shí)間戳(Lamport timestamps)[3]。
分布式系統(tǒng)中按是否存在節(jié)點(diǎn)交互可分為三類事件,一類發(fā)生于節(jié)點(diǎn)內(nèi)部,二是發(fā)送事件,三是接收事件。Lamport時(shí)間戳原理如下:
圖1: Lamport timestamps space time (圖片來源: wikipedia)
假設(shè)有事件a、b,C(a)、C(b)分別表示事件a、b對(duì)應(yīng)的Lamport時(shí)間戳,如果C(a) < C(b),則有a發(fā)生在b之前(happened before),記作 a -> b,例如圖1中有 C1 -> B1。通過該定義,事件集中Lamport時(shí)間戳不等的事件可進(jìn)行比較,我們獲得事件的偏序關(guān)系(partial order)。
如果C(a) = C(b),那a、b事件的順序又是怎樣的?假設(shè)a、b分別在節(jié)點(diǎn)P、Q上發(fā)生,Pi、Qj分別表示我們給P、Q的編號(hào),如果 C(a) = C(b) 并且 Pi< Qj,同樣定義為a發(fā)生在b之前,記作 a => b。假如我們對(duì)圖1的A、B、C分別編號(hào)Ai = 1、Bj = 2、Ck = 3,因 C(B4) = C(C3) 并且 Bj < Ck,則 B4 => C3。
通過以上定義,我們可以對(duì)所有事件排序、獲得事件的全序關(guān)系(total order)。上圖例子,我們可以從C1到A4進(jìn)行排序。
Vector clock
Lamport時(shí)間戳幫助我們得到事件順序關(guān)系,但還有一種順序關(guān)系不能用Lamport時(shí)間戳很好地表示出來,那就是同時(shí)發(fā)生關(guān)系(concurrent)[4]。例如圖1中事件B4和事件C3沒有因果關(guān)系,屬于同時(shí)發(fā)生事件,但Lamport時(shí)間戳定義兩者有先后順序。
Vector clock是在Lamport時(shí)間戳基礎(chǔ)上演進(jìn)的另一種邏輯時(shí)鐘方法,它通過vector結(jié)構(gòu)不但記錄本節(jié)點(diǎn)的Lamport時(shí)間戳,同時(shí)也記錄了其他節(jié)點(diǎn)的Lamport時(shí)間戳[5][6]。Vector clock的原理與Lamport時(shí)間戳類似,使用圖例如下:
圖2: Vector clock space time (圖片來源: wikipedia)
假設(shè)有事件a、b分別在節(jié)點(diǎn)P、Q上發(fā)生,Vector clock分別為Ta、Tb,如果 Tb[Q] > Ta[Q] 并且 Tb[P] >= Ta[P],則a發(fā)生于b之前,記作 a -> b。到目前為止還和Lamport時(shí)間戳差別不大,那Vector clock怎么判別同時(shí)發(fā)生關(guān)系呢?
如果 Tb[Q] > Ta[Q] 并且 Tb[P] < Ta[P],則認(rèn)為a、b同時(shí)發(fā)生,記作 a <-> b。例如圖2中節(jié)點(diǎn)B上的第4個(gè)事件 (A:2,B:4,C:1) 與節(jié)點(diǎn)C上的第2個(gè)事件 (B:3,C:2) 沒有因果關(guān)系、屬于同時(shí)發(fā)生事件。
Version vector
基于Vector clock我們可以獲得任意兩個(gè)事件的順序關(guān)系,結(jié)果或?yàn)橄群箜樞蚧驗(yàn)橥瑫r(shí)發(fā)生,識(shí)別事件順序在工程實(shí)踐中有很重要的引申應(yīng)用,最常見的應(yīng)用是發(fā)現(xiàn)數(shù)據(jù)沖突(detect conflict)。
分布式系統(tǒng)中數(shù)據(jù)一般存在多個(gè)副本(replication),多個(gè)副本可能被同時(shí)更新,這會(huì)引起副本間數(shù)據(jù)不一致[7],Version vector的實(shí)現(xiàn)與Vector clock非常類似[8],目的用于發(fā)現(xiàn)數(shù)據(jù)沖突[9]。下面通過一個(gè)例子說明Version vector的用法[10]:
圖3: Version vector
Vector clock只用于發(fā)現(xiàn)數(shù)據(jù)沖突,不能解決數(shù)據(jù)沖突。如何解決數(shù)據(jù)沖突因場(chǎng)景而異,具體方法有以最后更新為準(zhǔn)(last write win),或?qū)_突的數(shù)據(jù)交給client由client端決定如何處理,或通過quorum決議事先避免數(shù)據(jù)沖突的情況發(fā)生[11]。
由于記錄了所有數(shù)據(jù)在所有節(jié)點(diǎn)上的邏輯時(shí)鐘信息,Vector clock和Version vector在實(shí)際應(yīng)用中可能面臨的一個(gè)問題是vector過大,用于數(shù)據(jù)管理的元數(shù)據(jù)(meta data)甚至大于數(shù)據(jù)本身[12]。
解決該問題的方法是使用server id取代client id創(chuàng)建vector (因?yàn)閟erver的數(shù)量相對(duì)client穩(wěn)定),或設(shè)定最大的size、如果超過該size值則淘汰最舊的vector信息[10][13]。
小結(jié)
以上介紹了分布式系統(tǒng)里邏輯時(shí)鐘的表示方法,通過Lamport timestamps可以建立事件的全序關(guān)系,通過Vector clock可以比較任意兩個(gè)事件的順序關(guān)系并且能表示無因果關(guān)系的事件,將Vector clock的方法用于發(fā)現(xiàn)數(shù)據(jù)版本沖突,于是有了Version vector。
[1] Time is an illusion, George Neville-Neil, 2016
[2] There is No Now, Justin Sheehy, 2015
[3] Time, Clocks, and the Ordering of Events in a Distributed System, Leslie Lamport, 1978
[4] Timestamps in Message-Passing Systems That Preserve the Partial Ordering, Colin J. Fidge, 1988
[5] Virtual Time and Global States of Distributed Systems, Friedemann Mattern, 1988
[6] Why Vector Clocks are Easy, Bryan Fink, 2010
[7] Conflict Management, CouchDB
[8] Version Vectors are not Vector Clocks, Carlos Baquero, 2011
[9] Detection of Mutual Inconsistency in Distributed Systems, IEEE Transactions on Software Engineering , 1983
[10] Dynamo: Amazon’s Highly Available Key-value Store, Amazon, 2007
[11] Conflict Resolution, Jeff Darcy , 2010
[12] Why Vector Clocks Are Hard, Justin Sheehy, 2010
[13] Causality Is Expensive (and What To Do About It), Peter Bailis ,2014
選舉(election)是分布式系統(tǒng)實(shí)踐中常見的問題,通過打破節(jié)點(diǎn)間的對(duì)等關(guān)系,選得的leader(或叫master、coordinator)有助于實(shí)現(xiàn)事務(wù)原子性、提升決議效率。 多數(shù)派(quorum)的思路幫助我們?cè)诰W(wǎng)絡(luò)分化的情況下達(dá)成決議一致性,在leader選舉的場(chǎng)景下幫助我們選出唯一leader。租約(lease)在一定期限內(nèi)給予節(jié)點(diǎn)特定權(quán)利,也可以用于實(shí)現(xiàn)leader選舉。
下面我們就來學(xué)習(xí)分布式系統(tǒng)理論中的選舉、多數(shù)派和租約。
選舉(electioin)
一致性問題(consistency)是獨(dú)立的節(jié)點(diǎn)間如何達(dá)成決議的問題,選出大家都認(rèn)可的leader本質(zhì)上也是一致性問題,因而如何應(yīng)對(duì)宕機(jī)恢復(fù)、網(wǎng)絡(luò)分化等在leader選舉中也需要考量。
Bully算法[1]是最常見的選舉算法,其要求每個(gè)節(jié)點(diǎn)對(duì)應(yīng)一個(gè)序號(hào),序號(hào)最高的節(jié)點(diǎn)為leader。leader宕機(jī)后次高序號(hào)的節(jié)點(diǎn)被重選為leader,過程如下:
(a). 節(jié)點(diǎn)4發(fā)現(xiàn)leader不可達(dá),向序號(hào)比自己高的節(jié)點(diǎn)發(fā)起重新選舉,重新選舉消息中帶上自己的序號(hào)
(b)(c). 節(jié)點(diǎn)5、6接收到重選信息后進(jìn)行序號(hào)比較,發(fā)現(xiàn)自身的序號(hào)更大,向節(jié)點(diǎn)4返回OK消息并各自向更高序號(hào)節(jié)點(diǎn)發(fā)起重新選舉
(d). 節(jié)點(diǎn)5收到節(jié)點(diǎn)6的OK消息,而節(jié)點(diǎn)6經(jīng)過超時(shí)時(shí)間后收不到更高序號(hào)節(jié)點(diǎn)的OK消息,則認(rèn)為自己是leader
(e). 節(jié)點(diǎn)6把自己成為leader的信息廣播到所有節(jié)點(diǎn)
回顧《分布式系統(tǒng)理論基礎(chǔ) - 一致性、2PC和3PC》就可以看到,Bully算法中有2PC的身影,都具有提議(propose)和收集反饋(vote)的過程。
在一致性算法Paxos、ZAB[2]、Raft[3]中,為提升決議效率均有節(jié)點(diǎn)充當(dāng)leader的角色。ZAB、Raft中描述了具體的leader選舉實(shí)現(xiàn),與Bully算法類似ZAB中使用zxid標(biāo)識(shí)節(jié)點(diǎn),具有最大zxid的節(jié)點(diǎn)表示其所具備的事務(wù)(transaction)最新、被選為leader。
多數(shù)派(quorum)
在網(wǎng)絡(luò)分化的場(chǎng)景下以上Bully算法會(huì)遇到一個(gè)問題,被分隔的節(jié)點(diǎn)都認(rèn)為自己具有最大的序號(hào)、將產(chǎn)生多個(gè)leader,這時(shí)候就需要引入多數(shù)派(quorum)[4]。多數(shù)派的思路在分布式系統(tǒng)中很常見,其確保網(wǎng)絡(luò)分化情況下決議唯一。
多數(shù)派的原理說起來很簡(jiǎn)單,假如節(jié)點(diǎn)總數(shù)為2f+1,則一項(xiàng)決議得到多于 f 節(jié)點(diǎn)贊成則獲得通過。leader選舉中,網(wǎng)絡(luò)分化場(chǎng)景下只有具備多數(shù)派節(jié)點(diǎn)的部分才可能選出leader,這避免了多l(xiāng)eader的產(chǎn)生。
多數(shù)派的思路還被應(yīng)用于副本(replica)管理,根據(jù)業(yè)務(wù)實(shí)際讀寫比例調(diào)整寫副本數(shù)Vw、讀副本數(shù)Vr,用以在可靠性和性能方面取得平衡[5]。
租約(lease)
選舉中很重要的一個(gè)問題,以上尚未提到:怎么判斷l(xiāng)eader不可用、什么時(shí)候應(yīng)該發(fā)起重新選舉?最先可能想到會(huì)通過心跳(heart beat)判別leader狀態(tài)是否正常,但在網(wǎng)絡(luò)擁塞或瞬斷的情況下,這容易導(dǎo)致出現(xiàn)雙主。
租約(lease)是解決該問題的常用方法,其最初提出時(shí)用于解決分布式緩存一致性問題[6],后面在分布式鎖[7]等很多方面都有應(yīng)用。
租約的原理同樣不復(fù)雜,中心思想是每次租約時(shí)長(zhǎng)內(nèi)只有一個(gè)節(jié)點(diǎn)獲得租約、到期后必須重新頒發(fā)租約。假設(shè)我們有租約頒發(fā)節(jié)點(diǎn)Z,節(jié)點(diǎn)0、1和2競(jìng)選leader,租約過程如下:
(a). 節(jié)點(diǎn)0、1、2在Z上注冊(cè)自己,Z根據(jù)一定的規(guī)則(例如先到先得)頒發(fā)租約給節(jié)點(diǎn),該租約同時(shí)對(duì)應(yīng)一個(gè)有效時(shí)長(zhǎng);這里假設(shè)節(jié)點(diǎn)0獲得租約、成為leader
(b). leader宕機(jī)時(shí),只有租約到期(timeout)后才重新發(fā)起選舉,這里節(jié)點(diǎn)1獲得租約、成為leader
租約機(jī)制確保了一個(gè)時(shí)刻最多只有一個(gè)leader,避免只使用心跳機(jī)制產(chǎn)生雙主的問題。在實(shí)踐應(yīng)用中,zookeeper、ectd可用于租約頒發(fā)。
小結(jié)
在分布式系統(tǒng)理論和實(shí)踐中,常見leader、quorum和lease的身影。分布式系統(tǒng)內(nèi)不一定事事協(xié)商、事事民主,leader的存在有助于提升決議效率。
本文以leader選舉作為例子引入和講述quorum、lease,當(dāng)然quorum和lease是兩種思想,并不限于leader選舉應(yīng)用。
最后提一個(gè)有趣的問題與大家思考,leader選舉的本質(zhì)是一致性問題,Paxos、Raft和ZAB等解決一致性問題的協(xié)議和算法本身又需要或依賴于leader,怎么理解這個(gè)看似“蛋生雞、雞生蛋”的問題?[8]
[1] Elections in a Distributed Computing System, Hector Garcia-Molina, 1982
[2] ZooKeeper’s atomic broadcast protocol: Theory and practice, Andre Medeiros, 2012
[3] In Search of an Understandable Consensus Algorithm, Diego Ongaro and John Ousterhout, 2013
[4] A quorum-based commit protocol, Dale Skeen, 1982
[5] Weighted Voting for Replicated Data, David K. Gifford, 1979
[6] Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency, Cary G. Gray and David R. Cheriton, 1989
[7] The Chubby lock service for loosely-coupled distributed systems, Mike Burrows, 2006
[8] Why is Paxos leader election not done using Paxos?
引言
狹義的分布式系統(tǒng)指由網(wǎng)絡(luò)連接的計(jì)算機(jī)系統(tǒng),每個(gè)節(jié)點(diǎn)獨(dú)立地承擔(dān)計(jì)算或存儲(chǔ)任務(wù),節(jié)點(diǎn)間通過網(wǎng)絡(luò)協(xié)同工作。廣義的分布式系統(tǒng)是一個(gè)相對(duì)的概念,正如Leslie Lamport所說[1]:
What is a distributed systeme. Distribution is in the eye of the beholder.
To the user sitting at the keyboard, his IBM personal computer is a nondistributed system.
To a flea crawling around on the circuit board, or to the engineer who designed it, it's very much a distributed system.
一致性是分布式理論中的根本性問題,近半個(gè)世紀(jì)以來,科學(xué)家們圍繞著一致性問題提出了很多理論模型,依據(jù)這些理論模型,業(yè)界也出現(xiàn)了很多工程實(shí)踐投影。下面我們從一致性問題、特定條件下解決一致性問題的兩種方法(2PC、3PC)入門,了解最基礎(chǔ)的分布式系統(tǒng)理論。
一致性(consensus)
何為一致性問題?簡(jiǎn)單而言,一致性問題就是相互獨(dú)立的節(jié)點(diǎn)之間如何達(dá)成一項(xiàng)決議的問題。分布式系統(tǒng)中,進(jìn)行數(shù)據(jù)庫(kù)事務(wù)提交(commit transaction)、Leader選舉、序列號(hào)生成等都會(huì)遇到一致性問題。這個(gè)問題在我們的日常生活中也很常見,比如牌友怎么商定幾點(diǎn)在哪打幾圈麻將:
《賭圣》,1990
假設(shè)一個(gè)具有N個(gè)節(jié)點(diǎn)的分布式系統(tǒng),當(dāng)其滿足以下條件時(shí),我們說這個(gè)系統(tǒng)滿足一致性:
有人可能會(huì)說,決定什么時(shí)候在哪搓搓麻將,4個(gè)人商量一下就ok,這不很簡(jiǎn)單嗎?
但就這樣看似簡(jiǎn)單的事情,分布式系統(tǒng)實(shí)現(xiàn)起來并不輕松,因?yàn)樗媾R著這些問題:
假設(shè)現(xiàn)實(shí)場(chǎng)景中也存在這樣的問題,我們看看結(jié)果會(huì)怎樣:
我: 老王,今晚7點(diǎn)老地方,搓夠48圈不見不散! …… (第二天凌晨3點(diǎn)) 隔壁老王: 沒問題! // 消息延遲 我: …… ---------------------------------------------- 我: 小張,今晚7點(diǎn)老地方,搓夠48圈不見不散! 小張: No …… (兩小時(shí)后……) 小張: No problem! // 宕機(jī)節(jié)點(diǎn)恢復(fù) 我: …… ----------------------------------------------- 我: 老李頭,今晚7點(diǎn)老地方,搓夠48圈不見不散! 老李: 必須的,大保健走起! // 拜占庭將軍
(這是要打麻將呢?還是要大保健?還是一邊打麻將一邊大保健……)
還能不能一起愉快地玩耍...
我們把以上所列的問題稱為系統(tǒng)模型(system model),討論分布式系統(tǒng)理論和工程實(shí)踐的時(shí)候,必先劃定模型。例如有以下兩種模型:
2比1多了節(jié)點(diǎn)恢復(fù)、網(wǎng)絡(luò)分化的考量,因而對(duì)這兩種模型的理論研究和工程解決方案必定是不同的,在還沒有明晰所要解決的問題前談解決方案都是一本正經(jīng)地耍流氓。
一致性還具備兩個(gè)屬性,一個(gè)是強(qiáng)一致(safety),它要求所有節(jié)點(diǎn)狀態(tài)一致、共進(jìn)退;一個(gè)是可用(liveness),它要求分布式系統(tǒng)24*7無間斷對(duì)外服務(wù)。FLP定理(FLP impossibility)[3][4] 已經(jīng)證明在一個(gè)收窄的模型中(異步環(huán)境并只存在節(jié)點(diǎn)宕機(jī)),不能同時(shí)滿足 safety 和 liveness。
FLP定理是分布式系統(tǒng)理論中的基礎(chǔ)理論,正如物理學(xué)中的能量守恒定律徹底否定了永動(dòng)機(jī)的存在,F(xiàn)LP定理否定了同時(shí)滿足safety 和 liveness 的一致性協(xié)議的存在。
《怦然心動(dòng) (Flipped)》,2010
工程實(shí)踐上根據(jù)具體的業(yè)務(wù)場(chǎng)景,或保證強(qiáng)一致(safety),或在節(jié)點(diǎn)宕機(jī)、網(wǎng)絡(luò)分化的時(shí)候保證可用(liveness)。2PC、3PC是相對(duì)簡(jiǎn)單的解決一致性問題的協(xié)議,下面我們就來了解2PC和3PC。
2PC
2PC(tow phase commit)兩階段提交[5]顧名思義它分成兩個(gè)階段,先由一方進(jìn)行提議(propose)并收集其他節(jié)點(diǎn)的反饋(vote),再根據(jù)反饋決定提交(commit)或中止(abort)事務(wù)。我們將提議的節(jié)點(diǎn)稱為協(xié)調(diào)者(coordinator),其他參與決議節(jié)點(diǎn)稱為參與者(participants, 或cohorts):
2PC, phase one
在階段1中,coordinator發(fā)起一個(gè)提議,分別問詢各participant是否接受。
2PC, phase two
在階段2中,coordinator根據(jù)participant的反饋,提交或中止事務(wù),如果participant全部同意則提交,只要有一個(gè)participant不同意就中止。
在異步環(huán)境(asynchronous)并且沒有節(jié)點(diǎn)宕機(jī)(fail-stop)的模型下,2PC可以滿足全認(rèn)同、值合法、可結(jié)束,是解決一致性問題的一種協(xié)議。但如果再加上節(jié)點(diǎn)宕機(jī)(fail-recover)的考慮,2PC是否還能解決一致性問題呢?
coordinator如果在發(fā)起提議后宕機(jī),那么participant將進(jìn)入阻塞(block)狀態(tài)、一直等待coordinator回應(yīng)以完成該次決議。這時(shí)需要另一角色把系統(tǒng)從不可結(jié)束的狀態(tài)中帶出來,我們把新增的這一角色叫協(xié)調(diào)者備份(coordinator watchdog)。coordinator宕機(jī)一定時(shí)間后,watchdog接替原coordinator工作,通過問詢(query) 各participant的狀態(tài),決定階段2是提交還是中止。這也要求 coordinator/participant 記錄(logging)歷史狀態(tài),以備coordinator宕機(jī)后watchdog對(duì)participant查詢、coordinator宕機(jī)恢復(fù)后重新找回狀態(tài)。
從coordinator接收到一次事務(wù)請(qǐng)求、發(fā)起提議到事務(wù)完成,經(jīng)過2PC協(xié)議后增加了2次RTT(propose+commit),帶來的時(shí)延(latency)增加相對(duì)較少。
3PC
3PC(three phase commit)即三階段提交[6][7],既然2PC可以在異步網(wǎng)絡(luò)+節(jié)點(diǎn)宕機(jī)恢復(fù)的模型下實(shí)現(xiàn)一致性,那還需要3PC做什么,3PC是什么鬼?
在2PC中一個(gè)participant的狀態(tài)只有它自己和coordinator知曉,假如coordinator提議后自身宕機(jī),在watchdog啟用前一個(gè)participant又宕機(jī),其他participant就會(huì)進(jìn)入既不能回滾、又不能強(qiáng)制commit的阻塞狀態(tài),直到participant宕機(jī)恢復(fù)。這引出兩個(gè)疑問:
相比2PC,3PC增加了一個(gè)準(zhǔn)備提交(prepare to commit)階段來解決以上問題:
圖片截取自wikipedia
coordinator接收完participant的反饋(vote)之后,進(jìn)入階段2,給各個(gè)participant發(fā)送準(zhǔn)備提交(prepare to commit)指令。participant接到準(zhǔn)備提交指令后可以鎖資源,但要求相關(guān)操作必須可回滾。coordinator接收完確認(rèn)(ACK)后進(jìn)入階段3、進(jìn)行commit/abort,3PC的階段3與2PC的階段2無異。協(xié)調(diào)者備份(coordinator watchdog)、狀態(tài)記錄(logging)同樣應(yīng)用在3PC。
participant如果在不同階段宕機(jī),我們來看看3PC如何應(yīng)對(duì):
因?yàn)橛辛藴?zhǔn)備提交(prepare to commit)階段,3PC的事務(wù)處理延時(shí)也增加了1個(gè)RTT,變?yōu)?個(gè)RTT(propose+precommit+commit),但是它防止participant宕機(jī)后整個(gè)系統(tǒng)進(jìn)入阻塞態(tài),增強(qiáng)了系統(tǒng)的可用性,對(duì)一些現(xiàn)實(shí)業(yè)務(wù)場(chǎng)景是非常值得的。
小結(jié)
以上介紹了分布式系統(tǒng)理論中的部分基礎(chǔ)知識(shí),闡述了一致性(consensus)的定義和實(shí)現(xiàn)一致性所要面臨的問題,最后討論在異步網(wǎng)絡(luò)(asynchronous)、節(jié)點(diǎn)宕機(jī)恢復(fù)(fail-recover)模型下2PC、3PC怎么解決一致性問題。
閱讀前人對(duì)分布式系統(tǒng)的各項(xiàng)理論研究,其中有嚴(yán)謹(jǐn)?shù)赝评怼⒆C明,有一種數(shù)學(xué)的美;觀現(xiàn)實(shí)中的分布式系統(tǒng)實(shí)現(xiàn),是綜合各種因素下妥協(xié)的結(jié)果。
[1] Solved Problems, Unsolved Problems and Problems in Concurrency, Leslie Lamport, 1983
[2] The Byzantine Generals Problem, Leslie Lamport,Robert Shostak and Marshall Pease, 1982
[3] Impossibility of Distributed Consensus with One Faulty Process, Fischer, Lynch and Patterson, 1985
[4] FLP Impossibility的證明, Daniel Wu, 2015
[5] Consensus Protocols: Two-Phase Commit, Henry Robinson, 2008
[6] Consensus Protocols: Three-phase Commit, Henry Robinson, 2008
[7] Three-phase commit protocol, Wikipedia
在談?wù)摂?shù)據(jù)庫(kù)架構(gòu)和數(shù)據(jù)庫(kù)優(yōu)化的時(shí)候,我們經(jīng)常會(huì)聽到“分庫(kù)分表”、“分片”、“Sharding”…這樣的關(guān)鍵詞。讓人感到高興的是,這些朋友所服務(wù)的公司業(yè)務(wù)量正在(或者即將面臨)高速增長(zhǎng),技術(shù)方面也面臨著一些挑戰(zhàn)。讓人感到擔(dān)憂的是,他們系統(tǒng)真的就需要“分庫(kù)分表”了嗎?“分庫(kù)分表”有那么容易實(shí)踐嗎?為此,筆者整理了分庫(kù)分表中可能遇到的一些問題,并結(jié)合以往經(jīng)驗(yàn)介紹了對(duì)應(yīng)的解決思路和建議。
垂直分表在日常開發(fā)和設(shè)計(jì)中比較常見,通俗的說法叫做“大表拆小表”,拆分是基于關(guān)系型數(shù)據(jù)庫(kù)中的“列”(字段)進(jìn)行的。通常情況,某個(gè)表中的字段比較多,可以新建立一張“擴(kuò)展表”,將不經(jīng)常使用或者長(zhǎng)度較大的字段拆分出去放到“擴(kuò)展表”中,如下圖所示:
在字段很多的情況下,拆分開確實(shí)更便于開發(fā)和維護(hù)(筆者曾見過某個(gè)遺留系統(tǒng)中,一個(gè)大表中包含100多列的)。某種意義上也能避免“跨頁”的問題(MySQL、MSSQL底層都是通過“數(shù)據(jù)頁”來存儲(chǔ)的,“跨頁”問題可能會(huì)造成額外的性能開銷,這里不展開,感興趣的朋友可以自行查閱相關(guān)資料進(jìn)行研究)。
拆分字段的操作建議在數(shù)據(jù)庫(kù)設(shè)計(jì)階段就做好。如果是在發(fā)展過程中拆分,則需要改寫以前的查詢語句,會(huì)額外帶來一定的成本和風(fēng)險(xiǎn),建議謹(jǐn)慎。
垂直分庫(kù)在“微服務(wù)”盛行的今天已經(jīng)非常普及了。基本的思路就是按照業(yè)務(wù)模塊來劃分出不同的數(shù)據(jù)庫(kù),而不是像早期一樣將所有的數(shù)據(jù)表都放到同一個(gè)數(shù)據(jù)庫(kù)中。如下圖:
系統(tǒng)層面的“服務(wù)化”拆分操作,能夠解決業(yè)務(wù)系統(tǒng)層面的耦合和性能瓶頸,有利于系統(tǒng)的擴(kuò)展維護(hù)。而數(shù)據(jù)庫(kù)層面的拆分,道理也是相通的。與服務(wù)的“治理”和“降級(jí)”機(jī)制類似,我們也能對(duì)不同業(yè)務(wù)類型的數(shù)據(jù)進(jìn)行“分級(jí)”管理、維護(hù)、監(jiān)控、擴(kuò)展等。
眾所周知,數(shù)據(jù)庫(kù)往往最容易成為應(yīng)用系統(tǒng)的瓶頸,而數(shù)據(jù)庫(kù)本身屬于“有狀態(tài)”的,相對(duì)于Web和應(yīng)用服務(wù)器來講,是比較難實(shí)現(xiàn)“橫向擴(kuò)展”的。數(shù)據(jù)庫(kù)的連接資源比較寶貴且單機(jī)處理能力也有限,在高并發(fā)場(chǎng)景下,垂直分庫(kù)一定程度上能夠突破IO、連接數(shù)及單機(jī)硬件資源的瓶頸,是大型分布式系統(tǒng)中優(yōu)化數(shù)據(jù)庫(kù)架構(gòu)的重要手段。
然后,很多人并沒有從根本上搞清楚為什么要拆分,也沒有掌握拆分的原則和技巧,只是一味的模仿大廠的做法。導(dǎo)致拆分后遇到很多問題(例如:跨庫(kù)join,分布式事務(wù)等)。
水平分表也稱為橫向分表,比較容易理解,就是將表中不同的數(shù)據(jù)行按照一定規(guī)律分布到不同的數(shù)據(jù)庫(kù)表中(這些表保存在同一個(gè)數(shù)據(jù)庫(kù)中),這樣來降低單表數(shù)據(jù)量,優(yōu)化查詢性能。最常見的方式就是通過主鍵或者時(shí)間等字段進(jìn)行Hash和取模后拆分。如下圖所示:
水平分表,能夠降低單表的數(shù)據(jù)量,一定程度上可以緩解查詢性能瓶頸。但本質(zhì)上這些表還保存在同一個(gè)庫(kù)中,所以庫(kù)級(jí)別還是會(huì)有IO瓶頸。所以,一般不建議采用這種做法。
水平分庫(kù)分表與上面講到的水平分表的思想相同,唯一不同的就是將這些拆分出來的表保存在不同的數(shù)據(jù)中。這也是很多大型互聯(lián)網(wǎng)公司所選擇的做法。如下圖:
某種意義上來講,有些系統(tǒng)中使用的“冷熱數(shù)據(jù)分離”(將一些使用較少的歷史數(shù)據(jù)遷移到其他的數(shù)據(jù)庫(kù)中。而在業(yè)務(wù)功能上,通常默認(rèn)只提供熱點(diǎn)數(shù)據(jù)的查詢),也是類似的實(shí)踐。在高并發(fā)和海量數(shù)據(jù)的場(chǎng)景下,分庫(kù)分表能夠有效緩解單機(jī)和單庫(kù)的性能瓶頸和壓力,突破IO、連接數(shù)、硬件資源的瓶頸。當(dāng)然,投入的硬件成本也會(huì)更高。同時(shí),這也會(huì)帶來一些復(fù)雜的技術(shù)問題和挑戰(zhàn)(例如:跨分片的復(fù)雜查詢,跨分片事務(wù)等)
垂直分庫(kù)帶來的問題和解決思路:
在拆分之前,系統(tǒng)中很多列表和詳情頁所需的數(shù)據(jù)是可以通過sql join來完成的。而拆分后,數(shù)據(jù)庫(kù)可能是分布式在不同實(shí)例和不同的主機(jī)上,join將變得非常麻煩。而且基于架構(gòu)規(guī)范,性能,安全性等方面考慮,一般是禁止跨庫(kù)join的。那該怎么辦呢?首先要考慮下垂直分庫(kù)的設(shè)計(jì)問題,如果可以調(diào)整,那就優(yōu)先調(diào)整。如果無法調(diào)整的情況,下面筆者將結(jié)合以往的實(shí)際經(jīng)驗(yàn),總結(jié)幾種常見的解決思路,并分析其適用場(chǎng)景。
全局表
所謂全局表,就是有可能系統(tǒng)中所有模塊都可能會(huì)依賴到的一些表。比較類似我們理解的“數(shù)據(jù)字典”。為了避免跨庫(kù)join查詢,我們可以將這類表在其他每個(gè)數(shù)據(jù)庫(kù)中均保存一份。同時(shí),這類數(shù)據(jù)通常也很少發(fā)生修改(甚至幾乎不會(huì)),所以也不用太擔(dān)心“一致性”問題。
字段冗余
這是一種典型的反范式設(shè)計(jì),在互聯(lián)網(wǎng)行業(yè)中比較常見,通常是為了性能來避免join查詢。
舉個(gè)電商業(yè)務(wù)中很簡(jiǎn)單的場(chǎng)景:
“訂單表”中保存“賣家Id”的同時(shí),將賣家的“Name”字段也冗余,這樣查詢訂單詳情的時(shí)候就不需要再去查詢“賣家用戶表”。
字段冗余能帶來便利,是一種“空間換時(shí)間”的體現(xiàn)。但其適用場(chǎng)景也比較有限,比較適合依賴字段較少的情況。最復(fù)雜的還是數(shù)據(jù)一致性問題,這點(diǎn)很難保證,可以借助數(shù)據(jù)庫(kù)中的觸發(fā)器或者在業(yè)務(wù)代碼層面去保證。當(dāng)然,也需要結(jié)合實(shí)際業(yè)務(wù)場(chǎng)景來看一致性的要求。就像上面例子,如果賣家修改了Name之后,是否需要在訂單信息中同步更新呢?
數(shù)據(jù)同步
定時(shí)A庫(kù)中的tab_a表和B庫(kù)中tbl_b有關(guān)聯(lián),可以定時(shí)將指定的表做同步。當(dāng)然,同步本來會(huì)對(duì)數(shù)據(jù)庫(kù)帶來一定的影響,需要性能影響和數(shù)據(jù)時(shí)效性中取得一個(gè)平衡。這樣來避免復(fù)雜的跨庫(kù)查詢。筆者曾經(jīng)在項(xiàng)目中是通過ETL工具來實(shí)施的。
系統(tǒng)層組裝
在系統(tǒng)層面,通過調(diào)用不同模塊的組件或者服務(wù),獲取到數(shù)據(jù)并進(jìn)行字段拼裝。說起來很容易,但實(shí)踐起來可真沒有這么簡(jiǎn)單,尤其是數(shù)據(jù)庫(kù)設(shè)計(jì)上存在問題但又無法輕易調(diào)整的時(shí)候。
具體情況通常會(huì)比較復(fù)雜。下面筆者結(jié)合以往實(shí)際經(jīng)驗(yàn),并通過偽代碼方式來描述。
簡(jiǎn)單的列表查詢的情況
偽代碼很容易理解,先獲取“我的提問列表”數(shù)據(jù),然后再根據(jù)列表中的UserId去循環(huán)調(diào)用依賴的用戶服務(wù)獲取到用戶的RealName,拼裝結(jié)果并返回。
有經(jīng)驗(yàn)的讀者一眼就能看出上訴偽代碼存在效率問題。循環(huán)調(diào)用服務(wù),可能會(huì)有循環(huán)RPC,循環(huán)查詢數(shù)據(jù)庫(kù)…不推薦使用。再看看改進(jìn)后的:
這種實(shí)現(xiàn)方式,看起來要優(yōu)雅一點(diǎn),其實(shí)就是把循環(huán)調(diào)用改成一次調(diào)用。當(dāng)然,用戶服務(wù)的數(shù)據(jù)庫(kù)查詢中很可能是In查詢,效率方面比上一種方式更高。(坊間流傳In查詢會(huì)全表掃描,存在性能問題,傳聞不可全信。其實(shí)查詢優(yōu)化器都是基本成本估算的,經(jīng)過測(cè)試,在In語句中條件字段有索引的時(shí)候,條件較少的情況是會(huì)走索引的。這里不細(xì)展開說明,感興趣的朋友請(qǐng)自行測(cè)試)。
簡(jiǎn)單字段組裝的情況下,我們只需要先獲取“主表”數(shù)據(jù),然后再根據(jù)關(guān)聯(lián)關(guān)系,調(diào)用其他模塊的組件或服務(wù)來獲取依賴的其他字段(如例中依賴的用戶信息),最后將數(shù)據(jù)進(jìn)行組裝。
通常,我們都會(huì)通過緩存來避免頻繁RPC通信和數(shù)據(jù)庫(kù)查詢的開銷。
列表查詢帶條件過濾的情況
在上述例子中,都是簡(jiǎn)單的字段組裝,而不存在條件過濾。看拆分前的SQL:
這種連接查詢并且還帶條件過濾的情況,想在代碼層面組裝數(shù)據(jù)其實(shí)是非常復(fù)雜的(尤其是左表和右表都帶條件過濾的情況會(huì)更復(fù)雜),不能像之前例子中那樣簡(jiǎn)單的進(jìn)行組裝了。試想一下,如果像上面那樣簡(jiǎn)單的進(jìn)行組裝,造成的結(jié)果就是返回的數(shù)據(jù)不完整,不準(zhǔn)確。
有如下幾種解決思路:
查出所有的問答數(shù)據(jù),然后調(diào)用用戶服務(wù)進(jìn)行拼裝數(shù)據(jù),再根據(jù)過濾字段state字段進(jìn)行過濾,最后進(jìn)行排序和分頁并返回。
這種方式能夠保證數(shù)據(jù)的準(zhǔn)確性和完整性,但是性能影響非常大,不建議使用。
查詢出state字段符合/不符合的UserId,在查詢問答數(shù)據(jù)的時(shí)候使用in/not in進(jìn)行過濾,排序,分頁等。過濾出有效的問答數(shù)據(jù)后,再調(diào)用用戶服務(wù)獲取數(shù)據(jù)進(jìn)行組裝。
這種方式明顯更優(yōu)雅點(diǎn)。筆者之前在某個(gè)項(xiàng)目的特殊場(chǎng)景中就是采用過這種方式實(shí)現(xiàn)。
跨庫(kù)事務(wù)(分布式事務(wù))的問題
按業(yè)務(wù)拆分?jǐn)?shù)據(jù)庫(kù)之后,不可避免的就是“分布式事務(wù)”的問題。以往在代碼中通過spring注解簡(jiǎn)單配置就能實(shí)現(xiàn)事務(wù)的,現(xiàn)在則需要花很大的成本去保證一致性。這里不展開介紹,
感興趣的讀者可以自行參考《分布式事務(wù)一致性解決方案》,鏈接地址:
http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency
本篇中主要描述了幾種常見的拆分方式,并著重介紹了垂直分庫(kù)帶來的一些問題和解決思路。讀者朋友可能還有些問題和疑惑。
1. 我們目前的數(shù)據(jù)庫(kù)是否需要進(jìn)行垂直分庫(kù)?
根據(jù)系統(tǒng)架構(gòu)和公司實(shí)際情況來,如果你們的系統(tǒng)還是個(gè)簡(jiǎn)單的單體應(yīng)用,并且沒有什么訪問量和數(shù)據(jù)量,那就別著急折騰“垂直分庫(kù)”了,否則沒有任何收益,也很難有好結(jié)果。
切記,“過度設(shè)計(jì)”和“過早優(yōu)化”是很多架構(gòu)師和技術(shù)人員常犯的毛病。
2. 垂直拆分有沒有原則或者技巧?
沒有什么黃金法則和標(biāo)準(zhǔn)答案。一般是參考系統(tǒng)的業(yè)務(wù)模塊拆分來進(jìn)行數(shù)據(jù)庫(kù)的拆分。比如“用戶服務(wù)”,對(duì)應(yīng)的可能就是“用戶數(shù)據(jù)庫(kù)”。但是也不一定嚴(yán)格一一對(duì)應(yīng)。有些情況下,數(shù)據(jù)庫(kù)拆分的粒度可能會(huì)比系統(tǒng)拆分的粒度更粗。筆者也確實(shí)見過有些系統(tǒng)中的某些表原本應(yīng)該放A庫(kù)中的,卻放在了B庫(kù)中。有些庫(kù)和表原本是可以合并的,卻單獨(dú)保存著。還有些表,看起來放在A庫(kù)中也OK,放在B庫(kù)中也合理。
如何設(shè)計(jì)和權(quán)衡,這個(gè)就看實(shí)際情況和架構(gòu)師/開發(fā)人員的水平了。
3. 上面舉例的都太簡(jiǎn)單了,我們的后臺(tái)報(bào)表系統(tǒng)中join的表都有n個(gè)了,
分庫(kù)后該怎么查?
有很多朋友跟我提過類似的問題。其實(shí)互聯(lián)網(wǎng)的業(yè)務(wù)系統(tǒng)中,本來就應(yīng)該盡量避免join的,如果有多個(gè)join的,要么是設(shè)計(jì)不合理,要么是技術(shù)選型有誤。請(qǐng)自行科普下OLAP和OLTP,報(bào)表類的系統(tǒng)在傳統(tǒng)BI時(shí)代都是通過OLAP數(shù)據(jù)倉(cāng)庫(kù)去實(shí)現(xiàn)的(現(xiàn)在則更多是借助離線分析、流式計(jì)算等手段實(shí)現(xiàn)),而不該向上面描述的那樣直接在業(yè)務(wù)庫(kù)中執(zhí)行大量join和統(tǒng)計(jì)。
由于篇幅關(guān)系,下篇中我們?cè)倮^續(xù)細(xì)聊“水平分庫(kù)分表”相關(guān)的話題。
在之前的文章中,我介紹了分庫(kù)分表的幾種表現(xiàn)形式和玩法,也重點(diǎn)介紹了垂直分庫(kù)所帶來的問題和解決方法。本篇中,我們將繼續(xù)聊聊水平分庫(kù)分表的一些技巧。
關(guān)系型數(shù)據(jù)庫(kù)本身比較容易成為系統(tǒng)性能瓶頸,單機(jī)存儲(chǔ)容量、連接數(shù)、處理能力等都很有限,數(shù)據(jù)庫(kù)本身的“有狀態(tài)性”導(dǎo)致了它并不像Web和應(yīng)用服務(wù)器那么容易擴(kuò)展。在互聯(lián)網(wǎng)行業(yè)海量數(shù)據(jù)和高并發(fā)訪問的考驗(yàn)下,聰明的技術(shù)人員提出了分庫(kù)分表技術(shù)(有些地方也稱為Sharding、分片)。同時(shí),流行的分布式系統(tǒng)中間件(例如MongoDB、ElasticSearch等)均自身友好支持Sharding,其原理和思想都是大同小異的。
在很多中小項(xiàng)目中,我們往往直接使用數(shù)據(jù)庫(kù)自增特性來生成主鍵ID,這樣確實(shí)比較簡(jiǎn)單。而在分庫(kù)分表的環(huán)境中,數(shù)據(jù)分布在不同的分片上,不能再借助數(shù)據(jù)庫(kù)自增長(zhǎng)特性直接生成,否則會(huì)造成不同分片上的數(shù)據(jù)表主鍵會(huì)重復(fù)。簡(jiǎn)單介紹下使用和了解過的幾種ID生成算法。
其中,Twitter 的Snowflake算法是筆者近幾年在分布式系統(tǒng)項(xiàng)目中使用最多的,未發(fā)現(xiàn)重復(fù)或并發(fā)的問題。該算法生成的是64位唯一Id(由41位的timestamp+ 10位自定義的機(jī)器碼+ 13位累加計(jì)數(shù)器組成)。這里不做過多介紹,感興趣的讀者可自行查閱相關(guān)資料。
在開始分片之前,我們首先要確定分片字段(也可稱為“片鍵”)。很多常見的例子和場(chǎng)景中是采用ID或者時(shí)間字段進(jìn)行拆分。這也并不絕對(duì)的,我的建議是結(jié)合實(shí)際業(yè)務(wù),通過對(duì)系統(tǒng)中執(zhí)行的sql語句進(jìn)行統(tǒng)計(jì)分析,選擇出需要分片的那個(gè)表中最頻繁被使用,或者最重要的字段來作為分片字段。
常見的分片策略有隨機(jī)分片和連續(xù)分片這兩種,如下圖所示:
當(dāng)需要使用分片字段進(jìn)行范圍查找時(shí),連續(xù)分片可以快速定位分片進(jìn)行高效查詢,大多數(shù)情況下可以有效避免跨分片查詢的問題。后期如果想對(duì)整個(gè)分片集群擴(kuò)容時(shí),只需要添加節(jié)點(diǎn)即可,無需對(duì)其他分片的數(shù)據(jù)進(jìn)行遷移。但是,連續(xù)分片也有可能存在數(shù)據(jù)熱點(diǎn)的問題,就像圖中按時(shí)間字段分片的例子,有些節(jié)點(diǎn)可能會(huì)被頻繁查詢壓力較大,熱數(shù)據(jù)節(jié)點(diǎn)就成為了整個(gè)集群的瓶頸。而有些節(jié)點(diǎn)可能存的是歷史數(shù)據(jù),很少需要被查詢到。
隨機(jī)分片其實(shí)并不是隨機(jī)的,也遵循一定規(guī)則。通常,我們會(huì)采用Hash取模的方式進(jìn)行分片拆分,所以有些時(shí)候也被稱為離散分片。隨機(jī)分片的數(shù)據(jù)相對(duì)比較均勻,不容易出現(xiàn)熱點(diǎn)和并發(fā)訪問的瓶頸。但是,后期分片集群擴(kuò)容起來需要遷移舊的數(shù)據(jù)。使用一致性Hash算法能夠很大程度的避免這個(gè)問題,所以很多中間件的分片集群都會(huì)采用一致性Hash算法。離散分片也很容易面臨跨分片查詢的復(fù)雜問題。
很少有項(xiàng)目會(huì)在初期就開始考慮分片設(shè)計(jì)的,一般都是在業(yè)務(wù)高速發(fā)展面臨性能和存儲(chǔ)的瓶頸時(shí)才會(huì)提前準(zhǔn)備。因此,不可避免的就需要考慮歷史數(shù)據(jù)遷移的問題。一般做法就是通過程序先讀出歷史數(shù)據(jù),然后按照指定的分片規(guī)則再將數(shù)據(jù)寫入到各個(gè)分片節(jié)點(diǎn)中。
此外,我們需要根據(jù)當(dāng)前的數(shù)據(jù)量和QPS等進(jìn)行容量規(guī)劃,綜合成本因素,推算出大概需要多少分片(一般建議單個(gè)分片上的單表數(shù)據(jù)量不要超過1000W)。
如果是采用隨機(jī)分片,則需要考慮后期的擴(kuò)容問題,相對(duì)會(huì)比較麻煩。如果是采用的范圍分片,只需要添加節(jié)點(diǎn)就可以自動(dòng)擴(kuò)容。
一般來講,分頁時(shí)需要按照指定字段進(jìn)行排序。當(dāng)排序字段就是分片字段的時(shí)候,我們通過分片規(guī)則可以比較容易定位到指定的分片,而當(dāng)排序字段非分片字段的時(shí)候,情況就會(huì)變得比較復(fù)雜了。為了最終結(jié)果的準(zhǔn)確性,我們需要在不同的分片節(jié)點(diǎn)中將數(shù)據(jù)進(jìn)行排序并返回,并將不同分片返回的結(jié)果集進(jìn)行匯總和再次排序,最后再返回給用戶。如下圖所示:
上面圖中所描述的只是最簡(jiǎn)單的一種情況(取第一頁數(shù)據(jù)),看起來對(duì)性能的影響并不大。但是,如果想取出第10頁數(shù)據(jù),情況又將變得復(fù)雜很多,如下圖所示:
有些讀者可能并不太理解,為什么不能像獲取第一頁數(shù)據(jù)那樣簡(jiǎn)單處理(排序取出前10條再合并、排序)。其實(shí)并不難理解,因?yàn)楦鞣制?jié)點(diǎn)中的數(shù)據(jù)可能是隨機(jī)的,為了排序的準(zhǔn)確性,必須把所有分片節(jié)點(diǎn)的前N頁數(shù)據(jù)都排序好后做合并,最后再進(jìn)行整體的排序。很顯然,這樣的操作是比較消耗資源的,用戶越往后翻頁,系統(tǒng)性能將會(huì)越差。
在使用Max、Min、Sum、Count之類的函數(shù)進(jìn)行統(tǒng)計(jì)和計(jì)算的時(shí)候,需要先在每個(gè)分片數(shù)據(jù)源上執(zhí)行相應(yīng)的函數(shù)處理,然后再將各個(gè)結(jié)果集進(jìn)行二次處理,最終再將處理結(jié)果返回。如下圖所示:
Join是關(guān)系型數(shù)據(jù)庫(kù)中最常用的特性,但是在分片集群中,join也變得非常復(fù)雜。應(yīng)該盡量避免跨分片的join查詢(這種場(chǎng)景,比上面的跨分片分頁更加復(fù)雜,而且對(duì)性能的影響很大)。通常有以下幾種方式來避免:
全局表的概念之前在“垂直分庫(kù)”時(shí)提過。基本思想一致,就是把一些類似數(shù)據(jù)字典又可能會(huì)產(chǎn)生join查詢的表信息放到各分片中,從而避免跨分片的join。
在關(guān)系型數(shù)據(jù)庫(kù)中,表之間往往存在一些關(guān)聯(lián)的關(guān)系。如果我們可以先確定好關(guān)聯(lián)關(guān)系,并將那些存在關(guān)聯(lián)關(guān)系的表記錄存放在同一個(gè)分片上,那么就能很好的避免跨分片join問題。在一對(duì)多關(guān)系的情況下,我們通常會(huì)選擇按照數(shù)據(jù)較多的那一方進(jìn)行拆分。如下圖所示:
這樣一來,Data Node1上面的訂單表與訂單詳細(xì)表就可以直接關(guān)聯(lián),進(jìn)行局部的join查詢了,Data Node2上也一樣。基于ER分片的這種方式,能夠有效避免大多數(shù)業(yè)務(wù)場(chǎng)景中的跨分片join問題。
隨著spark內(nèi)存計(jì)算的興起,理論上來講,很多跨數(shù)據(jù)源的操作問題看起來似乎都能夠得到解決。可以將數(shù)據(jù)丟給spark集群進(jìn)行內(nèi)存計(jì)算,最后將計(jì)算結(jié)果返回。
跨分片事務(wù)也分布式事務(wù),想要了解分布式事務(wù),就需要了解“XA接口”和“兩階段提交”。值得提到的是,MySQL5.5x和5.6x中的xa支持是存在問題的,會(huì)導(dǎo)致主從數(shù)據(jù)不一致。直到5.7x版本中才得到修復(fù)。Java應(yīng)用程序可以采用Atomikos框架來實(shí)現(xiàn)XA事務(wù)(J2EE中JTA)。感興趣的讀者可以自行參考《分布式事務(wù)一致性解決方案》,鏈接地址:
http://www.infoq.com/cn/articles/solution-of-distributed-system-transaction-consistency
讀完上面內(nèi)容,不禁引起有些讀者的思考,我們的系統(tǒng)是否需要分庫(kù)分表嗎?
其實(shí)這點(diǎn)沒有明確的判斷標(biāo)準(zhǔn),比較依賴實(shí)際業(yè)務(wù)情況和經(jīng)驗(yàn)判斷。依照筆者個(gè)人的經(jīng)驗(yàn),一般MySQL單表1000W左右的數(shù)據(jù)是沒有問題的(前提是應(yīng)用系統(tǒng)和數(shù)據(jù)庫(kù)等層面設(shè)計(jì)和優(yōu)化的比較好)。當(dāng)然,除了考慮當(dāng)前的數(shù)據(jù)量和性能情況時(shí),作為架構(gòu)師,我們需要提前考慮系統(tǒng)半年到一年左右的業(yè)務(wù)增長(zhǎng)情況,對(duì)數(shù)據(jù)庫(kù)服務(wù)器的QPS、連接數(shù)、容量等做合理評(píng)估和規(guī)劃,并提前做好相應(yīng)的準(zhǔn)備工作。如果單機(jī)無法滿足,且很難再?gòu)钠渌矫鎯?yōu)化,那么說明是需要考慮分片的。這種情況可以先去掉數(shù)據(jù)庫(kù)中自增ID,為分片和后面的數(shù)據(jù)遷移工作提前做準(zhǔn)備。
很多人覺得“分庫(kù)分表”是宜早不宜遲,應(yīng)該盡早進(jìn)行,因?yàn)閾?dān)心越往后公司業(yè)務(wù)發(fā)展越快、系統(tǒng)越來越復(fù)雜、系統(tǒng)重構(gòu)和擴(kuò)展越困難…這種話聽起來是有那么一點(diǎn)道理,但我的觀點(diǎn)恰好相反,對(duì)于關(guān)系型數(shù)據(jù)庫(kù)來講,我認(rèn)為“能不分片就別分片”,除非是系統(tǒng)真正需要,因?yàn)閿?shù)據(jù)庫(kù)分片并非低成本或者免費(fèi)的。
這里筆者推薦一個(gè)比較靠譜的過渡技術(shù)–“表分區(qū)”。主流的關(guān)系型數(shù)據(jù)庫(kù)中基本都支持。不同的分區(qū)在邏輯上仍是一張表,但是物理上卻是分開的,能在一定程度上提高查詢性能,而且對(duì)應(yīng)用程序透明,無需修改任何代碼。筆者曾經(jīng)負(fù)責(zé)優(yōu)化過一個(gè)系統(tǒng),主業(yè)務(wù)表有大約8000W左右的數(shù)據(jù),考慮到成本問題,當(dāng)時(shí)就是采用“表分區(qū)”來做的,效果比較明顯,且系統(tǒng)運(yùn)行的很穩(wěn)定。
最后,有很多讀者都想了解當(dāng)前社區(qū)中有沒有開源免費(fèi)的分庫(kù)分表解決方案,畢竟站在巨人的肩膀上能省力很多。當(dāng)前主要有兩類解決方案:
基于應(yīng)用程序?qū)用娴腄DAL(分布式數(shù)據(jù)庫(kù)訪問層)
比較典型的就是淘寶半開源的TDDL,當(dāng)當(dāng)網(wǎng)開源的Sharding-JDBC等。分布式數(shù)據(jù)訪問層無需硬件投入,技術(shù)能力較強(qiáng)的大公司通常會(huì)選擇自研或參照開源框架進(jìn)行二次開發(fā)和定制。對(duì)應(yīng)用程序的侵入性一般較大,會(huì)增加技術(shù)成本和復(fù)雜度。通常僅支持特定編程語言平臺(tái)(Java平臺(tái)的居多),或者僅支持特定的數(shù)據(jù)庫(kù)和特定數(shù)據(jù)訪問框架技術(shù)(一般支持MySQL數(shù)據(jù)庫(kù),JDBC、MyBatis、Hibernate等框架技術(shù))。
數(shù)據(jù)庫(kù)中間件,比較典型的像mycat(在阿里開源的cobar基礎(chǔ)上做了很多優(yōu)化和改進(jìn),屬于后起之秀,也支持很多新特性),基于Go語言實(shí)現(xiàn)kingSharding,比較老牌的Atlas(由360開源)等。這些中間件在互聯(lián)網(wǎng)企業(yè)中大量被使用。另外,MySQL 5.x企業(yè)版中官方提供的Fabric組件也號(hào)稱支持分片技術(shù),不過國(guó)內(nèi)使用的企業(yè)較少。
中間件也可以稱為“透明網(wǎng)關(guān)”,大名鼎鼎的mysql_proxy大概是該領(lǐng)域的鼻祖(由MySQL官方提供,僅限于實(shí)現(xiàn)“讀寫分離”)。中間件一般實(shí)現(xiàn)了特定數(shù)據(jù)庫(kù)的網(wǎng)絡(luò)通信協(xié)議,模擬一個(gè)真實(shí)的數(shù)據(jù)庫(kù)服務(wù),屏蔽了后端真實(shí)的Server,應(yīng)用程序通常直接連接中間件即可。而在執(zhí)行SQL操作時(shí),中間件會(huì)按照預(yù)先定義分片規(guī)則,對(duì)SQL語句進(jìn)行解析、路由,并對(duì)結(jié)果集做二次計(jì)算再最終返回。引入數(shù)據(jù)庫(kù)中間件的技術(shù)成本更低,對(duì)應(yīng)用程序來講侵入性幾乎沒有,可以滿足大部分的業(yè)務(wù)。增加了額外的硬件投入和運(yùn)維成本,同時(shí),中間件自身也存在性能瓶頸和單點(diǎn)故障問題,需要能夠保證中間件自身的高可用、可擴(kuò)展。
總之,不管是使用分布式數(shù)據(jù)訪問層還是數(shù)據(jù)庫(kù)中間件,都會(huì)帶來一定的成本和復(fù)雜度,也會(huì)有一定的性能影響。所以,還需讀者根據(jù)實(shí)際情況和業(yè)務(wù)發(fā)展需要慎重考慮和選擇。
本文根據(jù)白輝在2016ArchSummit全球架構(gòu)師(深圳)峰會(huì)上的演講整理而成。ArchSummit北京站即將在12月2日開幕,更多專題講師信息請(qǐng)到北京站官網(wǎng)查詢。
非常榮幸在這里跟大家一起來探討“海量服務(wù)架構(gòu)探索”相關(guān)專題的內(nèi)容。
我叫白輝,花名是七公。2014年之前主要在阿里B2B負(fù)責(zé)資金中心、評(píng)價(jià)、任務(wù)中心等系統(tǒng)。2015年加入蘑菇街,隨著蘑菇街的飛速成長(zhǎng),經(jīng)歷了網(wǎng)站技術(shù)架構(gòu)的大
變革。今天分享的內(nèi)容來自于去年我們做的事情,題目用了一個(gè)關(guān)鍵詞是“籬笆”,籬笆的英文是Barrier,是指2015年蘑菇街面臨的問題和艱巨的困難。我們?cè)竭^了這些籬笆,取得了很好的成果。
今天分享的內(nèi)容主要分為五部分。第一部分,概述電商系統(tǒng)發(fā)展中期面臨的一般性問題。第二部分,如何解決面臨的問題,主要的策略是做拆分、做服務(wù)化。第三、四部分,服務(wù)化之后業(yè)務(wù)的大增長(zhǎng)、網(wǎng)站流量飛速的增加、“雙11”大促等的挑戰(zhàn)很大,我們做了服務(wù)的專項(xiàng)系統(tǒng)優(yōu)化以及穩(wěn)定性治理。第五部分,進(jìn)行了總結(jié)和展望。
我們先看第一部分的內(nèi)容。
我總結(jié)了一下,一般電商系統(tǒng)發(fā)展到中期都會(huì)面臨三個(gè)方面的問題(如圖)。第一方面是業(yè)務(wù)問題。比如,一開始做業(yè)務(wù)的時(shí)候可能很隨意,一是并不考慮業(yè)務(wù)模型、系統(tǒng)架構(gòu),二是業(yè)務(wù)之間的耦合比較嚴(yán)重,比如交易和資金業(yè)務(wù),有可能資金和外部第三方支付公司的交互狀態(tài)耦合在交易系統(tǒng)里,這些非常不利于業(yè)務(wù)發(fā)展。第二方面是系統(tǒng)問題。2014年我們面臨單體應(yīng)用,400人開發(fā)一個(gè)大應(yīng)用,擴(kuò)展性很差,業(yè)務(wù)比較難做。第三方面是支撐問題,比如關(guān)于環(huán)境、開發(fā)框架和質(zhì)量工具等。這些是電商系統(tǒng)發(fā)展到中期都會(huì)面臨的問題,中期的概念是用戶過了千萬,PV過了1億。
我們來看一下蘑菇街2015年初面臨的問題。蘑菇街2015年用戶過億,PV過10億,業(yè)務(wù)在超高速發(fā)展,每年保持3倍以上的增長(zhǎng)。電商促銷、交易、支付等業(yè)務(wù)形態(tài)都在快速膨脹,我們需要快速支持業(yè)務(wù)發(fā)展,而不是成為業(yè)務(wù)的瓶頸。那么就是要去做系統(tǒng)的拆分和服務(wù)化。
第二部分的內(nèi)容,是關(guān)于蘑菇街系統(tǒng)拆分與服務(wù)化的歷程。
按照如下幾條思路(見圖),我們進(jìn)行系統(tǒng)拆分以及服務(wù)化。最開始,大家在同一個(gè)應(yīng)用里開發(fā)一些業(yè)務(wù)功能,都是選擇速度最快的方式,所有的DB和業(yè)務(wù)代碼都是在一起的。首先我們將DB做垂直拆分。第二步是做業(yè)務(wù)系統(tǒng)垂直拆分,包括交易、資金等。第三步是在系統(tǒng)拆完了之后要考慮提供什么樣的API來滿足業(yè)務(wù)的需求?這里我們要做數(shù)據(jù)建模+業(yè)務(wù)建模,數(shù)據(jù)建模方面包括數(shù)據(jù)表的設(shè)計(jì)和擴(kuò)展支持,數(shù)據(jù)模型應(yīng)該非常穩(wěn)定;業(yè)務(wù)建模方面,使用標(biāo)準(zhǔn)和靈活的API,而且盡量不用修改代碼或者改少量代碼就能支持業(yè)務(wù)需求。第四步是需要將業(yè)務(wù)邏輯下沉到服務(wù),Web層專注于展示邏輯和編排,不要涉及過多業(yè)務(wù)的事情。然后用SOA中間件建設(shè)服務(wù)化系統(tǒng)。最后會(huì)做一些服務(wù)的治理。
來看一個(gè)API服務(wù)化的例子,在做服務(wù)化之前和做服務(wù)化之后,交易創(chuàng)建下單業(yè)務(wù)有什么不一樣。服務(wù)化之前我們面臨的問題有:入口分散,如果要在底層做任何一個(gè)微小的改動(dòng),十幾個(gè)入口需要幾十個(gè)人配合修改,這是非常不合理的一種方式;多端維護(hù)多套接口,成本非常高;還有穩(wěn)定性的問題,依賴非常復(fù)雜,維護(hù)很難。我剛到蘑菇街的時(shí)候,一次大促活動(dòng)就導(dǎo)致數(shù)據(jù)庫(kù)崩潰,暴露了系統(tǒng)架構(gòu)很大的問題和總量上的瓶頸。按照上面提到幾條思路去做服務(wù)化,看看有了哪些改善?首先是API統(tǒng)一,多個(gè)端、多個(gè)業(yè)務(wù)都用統(tǒng)一的API提供;其次是依賴有效管理起來,大事務(wù)拆分成多個(gè)本地小事務(wù);最后降低了鏈路風(fēng)險(xiǎn),邏輯更加清晰,穩(wěn)定性更好。
2015年3月我來到蘑菇街之后,先制訂了服務(wù)化的規(guī)范,探討了到底什么是標(biāo)準(zhǔn)的服務(wù)化。在做服務(wù)化的過程中,發(fā)現(xiàn)大家代碼風(fēng)格完全不一樣,所以制定編碼規(guī)范非常重要。2015年8月,我們完成了各個(gè)模塊的改造,包括用戶、商品、交易、訂單、促銷、退款等,然后有了服務(wù)化架構(gòu)1.0的體系。在此基礎(chǔ)之上,我們進(jìn)一步做了提升流量和穩(wěn)定性等更深度的建設(shè)。2015年9月,我們實(shí)施了分庫(kù)分表和鏈路性能提升優(yōu)化,2015年10月做了服務(wù)治理和服務(wù)保障。
接下來,以服務(wù)架構(gòu)和服務(wù)體系建設(shè)為主線,講一下去年整個(gè)網(wǎng)站架構(gòu)升級(jí)的過程。
在服務(wù)化1.0體系完成之后,我們得到了一個(gè)簡(jiǎn)單的體系,包含下單服務(wù)、營(yíng)銷服務(wù)、店鋪服務(wù)、商品服務(wù)和用戶服務(wù),還有簡(jiǎn)單的RPC框架Tesla。當(dāng)時(shí),我們并沒有做很多性能優(yōu)化的事情,但是通過業(yè)務(wù)流程化簡(jiǎn)和邏輯優(yōu)化,每秒最大訂單數(shù)從400提升到1K,基礎(chǔ)服務(wù)也都搭建了起來。
有了1.0初步的服務(wù)化體系之后,更進(jìn)一步,我們一是要繼續(xù)深入網(wǎng)站如資金等的服務(wù)化,二是要做服務(wù)內(nèi)部的建設(shè),比如容量、性能,這也是接下來要講的內(nèi)容。
這個(gè)鏈路(見圖)是比較典型的電商鏈路,有商品頁、下單、支付、營(yíng)銷和庫(kù)存等內(nèi)容。一開始每個(gè)點(diǎn)都有瓶頸,每個(gè)瓶頸都是一個(gè)籬笆,我們要正視它,然后翻越它。
我們先來看第一個(gè)籬笆墻:下單的瓶頸。
2015年“3.21”大促的時(shí)候,DB崩潰了,這個(gè)瓶頸很難突破。下一個(gè)訂單要插入很多條數(shù)據(jù)記錄到單DB的DB表。我們已經(jīng)用了最好的硬件,但是瓶頸依然存在,最主要的問題就是DB單點(diǎn),需要去掉單點(diǎn),做成可水平擴(kuò)展的。流量上來了,到DB的行寫入數(shù)是2萬/秒,對(duì)DB的壓力很大。寫應(yīng)該控制在一個(gè)合理的量,DB負(fù)載維持在較低水平,主從延時(shí)也才會(huì)在可控范圍內(nèi)。所以DB單點(diǎn)的問題非常凸顯,這座大山必須邁過去,我們做了一個(gè)分庫(kù)分表組件TSharding來實(shí)施分庫(kù)分表。
將我們寫的分庫(kù)分表工具與業(yè)界方案對(duì)比,業(yè)界有淘寶TDDL Smart Client的方式,還有Google的Vitess等的Proxy方式,這兩種成熟方案研發(fā)和運(yùn)維的成本都太高,短期內(nèi)我們接受不了,所以借鑒了Mybatis Plugin的方式,但Mybatis Plugin不支持?jǐn)?shù)據(jù)源管理,也不支持事務(wù)。我大概花了一周時(shí)間寫了一個(gè)組件——自研分庫(kù)分表組件TSharding(https://github.com/baihui212/tsharding),然后快速做出方案,把這個(gè)組件應(yīng)用到交易的數(shù)據(jù)庫(kù),在服務(wù)層和DAO層,訂單容量擴(kuò)展到千億量級(jí),并且可以繼續(xù)水平擴(kuò)展。TSharding上線一年之后,我們將其開放出來。
第二個(gè)籬笆墻就是營(yíng)銷服務(wù)RT的問題。促銷方式非常多,包括各種紅包、滿減、打折、優(yōu)惠券等。實(shí)際上促銷的接口邏輯非常復(fù)雜,在“雙11”備戰(zhàn)的時(shí)候,面對(duì)這個(gè)復(fù)雜的接口,每輪鏈路壓測(cè)促銷服務(wù)都會(huì)發(fā)現(xiàn)問題,之后優(yōu)化再壓測(cè),又發(fā)現(xiàn)新的問題。我們來一起看看遇到的各種問題以及是如何解決的。首先是壓測(cè)出現(xiàn)接口嚴(yán)重不可用,這里可以看到DB查詢頻次高,響應(yīng)很慢,流量一上來,這個(gè)接口就崩潰了。那怎么去排查原因和解決呢?
首先是SQL優(yōu)化,用工具識(shí)別慢SQL,即全鏈路跟蹤系統(tǒng)Lurker。
這張圖我簡(jiǎn)單介紹一下。遇到SQL執(zhí)行效率問題的時(shí)候,就看是不是執(zhí)行到最高效的索引,掃表行數(shù)是不是很大,是不是有filesort。有ORDER BY的時(shí)候,如果要排序的數(shù)據(jù)量不大或者已經(jīng)有索引可以走到,在數(shù)據(jù)庫(kù)的內(nèi)存排序緩存區(qū)一次就可以排序完。如果一次不能排序完,那就先拿到1000個(gè)做排序,然后輸出到文件,然后再對(duì)下1000個(gè)做排序,最后再歸并起來,這就是filesort的大致過程,效率比較低。所以盡量要走上索引,一般類的查詢降低到2毫秒左右可以返回。
其次是要讀取很多優(yōu)惠規(guī)則和很多優(yōu)惠券,數(shù)據(jù)量大的時(shí)候DB是很難扛的,這時(shí)候我們要做緩存和一些預(yù)處理。特別是查詢DB的效率不是很高的時(shí)候,盡量緩存可以緩存的數(shù)據(jù)、盡量緩存多一些數(shù)據(jù)。但如果做緩存,DB和緩存數(shù)據(jù)的一致性是一個(gè)問題。在做數(shù)據(jù)查詢時(shí),首先要看本地緩存有沒有開啟,如果本地緩存沒有打開,就去查分布式緩存,如果分布式緩存中沒有就去查DB,然后從DB獲取數(shù)據(jù)過來。需要盡量保持DB、緩存數(shù)據(jù)的一致性,如果DB有變化,可以異步地做緩存數(shù)據(jù)失效處理,數(shù)據(jù)百毫秒內(nèi)就失效掉,減少不一致的問題。
另外,如果讀到本地緩存,這個(gè)內(nèi)存訪問比走網(wǎng)絡(luò)請(qǐng)求性能直接提升了一個(gè)量級(jí),但是帶來的弊端也很大,因?yàn)楸镜鼐彺鏇]有辦法及時(shí)更新,平時(shí)也不能打開,因?yàn)闀?huì)帶來不一致問題。但大促高峰期間我們會(huì)關(guān)閉關(guān)鍵業(yè)務(wù)數(shù)據(jù)變更入口,開啟本地緩存,把本地緩存設(shè)置成一分鐘失效,一分鐘之內(nèi)是可以緩存的,也能容忍短暫的數(shù)據(jù)不一致,所以這也是一個(gè)很好的做法。同樣的思路,我們也會(huì)把可能會(huì)用到的數(shù)據(jù)提前放到緩存里面,做預(yù)處理。在客戶端進(jìn)行數(shù)據(jù)預(yù)處理,要么直接取本地?cái)?shù)據(jù),或者在本地直接做計(jì)算,這樣更高效,避免了遠(yuǎn)程的RPC。大促期間我們就把活動(dòng)價(jià)格信息預(yù)先放到商品表中,這樣部分場(chǎng)景可以做本地計(jì)價(jià),有效解決了計(jì)價(jià)接口性能的問題。
再就是讀容量問題,雖然緩存可以緩解壓力,但是DB還是會(huì)有幾十K的讀壓力,單點(diǎn)去扛也是不現(xiàn)實(shí)的,所以要把讀寫分離,如果從庫(kù)過多也有延時(shí)的風(fēng)險(xiǎn),我們會(huì)把數(shù)據(jù)庫(kù)的并行復(fù)制打開。
我們來看一下數(shù)據(jù)。這是去年“雙11”的情況(如圖)。促銷服務(wù)的RT得到了有效控制,所以去年“雙11”平穩(wěn)度過。
接下來講一個(gè)更基礎(chǔ)、更全局的優(yōu)化,就是異步化。比如說下單的流程,有很多業(yè)務(wù)是非實(shí)時(shí)性要求的,比如下單送優(yōu)惠券,如果在下單的時(shí)候同步做,時(shí)間非常長(zhǎng),風(fēng)險(xiǎn)也更大,其實(shí)業(yè)務(wù)上是非實(shí)時(shí)性或者準(zhǔn)實(shí)時(shí)性的要求,可以做異步化處理,這樣可以減少下單對(duì)機(jī)器數(shù)量的要求。另外是流量高峰期的一些熱點(diǎn)數(shù)據(jù)。大家可以想象一下,下單的時(shí)候,一萬個(gè)人競(jìng)爭(zhēng)同一條庫(kù)存數(shù)據(jù),一萬個(gè)節(jié)點(diǎn)鎖在這個(gè)請(qǐng)求上,這是多么恐怖的事情。所以我們會(huì)有異步隊(duì)列去削峰,先直接修改緩存中的庫(kù)存數(shù)目,改完之后能讀到最新的結(jié)果,但是不會(huì)直接競(jìng)爭(zhēng)DB,這是異步隊(duì)列削峰很重要的作用。還有,數(shù)據(jù)庫(kù)的競(jìng)爭(zhēng)非常厲害,我們需要把大事務(wù)做拆分,盡量讓本地事務(wù)足夠小,同時(shí)也要讓多個(gè)本地事務(wù)之間達(dá)到一致。
異步是最終達(dá)到一致的關(guān)鍵,異步的處理是非常復(fù)雜的。可以看一下這個(gè)場(chǎng)景(見圖),這是一個(gè)1-6步的處理過程,如果拆分成步驟1、2、3、4、end,然后到5,可以異步地做;6也一樣,并且5和6可以并行執(zhí)行。同時(shí),這個(gè)步驟走下來鏈路更短,保障也更容易;步驟5和6也可以單獨(dú)保障。所以異步化在蘑菇街被廣泛使用。
異步化之后面臨的困難也是很大的,會(huì)有分布式和一致性的問題。交易創(chuàng)建過程中,訂單、券和庫(kù)存要把狀態(tài)做到絕對(duì)一致。但下單的時(shí)候如果先鎖券,鎖券成功了再去減庫(kù)存,如果減庫(kù)存失敗了就是很麻煩的事情,因?yàn)閮?yōu)化券服務(wù)在另外一個(gè)系統(tǒng)里,如果要同步調(diào)用做券的回滾,有可能這個(gè)回滾也會(huì)失敗,這個(gè)時(shí)候處理就會(huì)非常復(fù)雜。我們的做法是,調(diào)用服務(wù)超時(shí)或者失敗的時(shí)候,我們就認(rèn)為失敗了,就會(huì)異步發(fā)消息通知回滾。優(yōu)惠券服務(wù)和庫(kù)存服務(wù)被通知要做回滾時(shí),會(huì)根據(jù)自身的狀態(tài)來判斷是否要回滾,如果鎖券成功了券就回滾,減庫(kù)存也成功了庫(kù)存做回滾;如果庫(kù)存沒有減就不用回滾。所以我們是通過異步發(fā)消息的方式保持多個(gè)系統(tǒng)之間的一致性;如果不做異步就非常復(fù)雜,有的場(chǎng)景是前面所有的服務(wù)都調(diào)用成功,第N個(gè)服務(wù)調(diào)用失敗。另外的一致性保障策略包括Corgi MQ生產(chǎn)端發(fā)送失敗會(huì)自動(dòng)重試保證發(fā)成功,消費(fèi)端接收ACK機(jī)制保證最終的一致。另外,與分布式事務(wù)框架比起來,異步化方案消除了二階段提交等分布式事務(wù)框架的侵入性影響,降低了開發(fā)的成本和門檻。
另一個(gè)場(chǎng)景是,服務(wù)調(diào)用上會(huì)有一些異步的處理。以購(gòu)物車業(yè)務(wù)為例,購(gòu)物車列表要調(diào)用10個(gè)Web服務(wù),每一個(gè)服務(wù)返回的時(shí)間都不一樣,比如第1個(gè)服務(wù)20毫秒返回,第10個(gè)服務(wù)40毫秒返回,串行執(zhí)行的效率很低。而電商類的大多數(shù)業(yè)務(wù)都是IO密集型的,而且數(shù)據(jù)量大時(shí)還要分批查詢。所以我們要做服務(wù)的異步調(diào)用。比如下圖中這個(gè)場(chǎng)景,步驟3處理完了之后callback馬上會(huì)處理,步驟4處理完了callback也會(huì)馬上處理,步驟3和4并不相互依賴,且處理可以同時(shí)進(jìn)行了,提高了業(yè)務(wù)邏輯執(zhí)行的并行度。目前我們是通過JDK7的Future和Callback實(shí)現(xiàn)的,在逐步往JDK8的Completable Future遷移。這是異步化在網(wǎng)站整體的應(yīng)用場(chǎng)景,異步化已經(jīng)深入到我們網(wǎng)站的各個(gè)環(huán)節(jié)。
剛才我們講了鏈路容量的提升、促銷RT的優(yōu)化,又做了異步化的一些處理。那么優(yōu)化之后怎么驗(yàn)證來優(yōu)化的效果呢?到底有沒有達(dá)到預(yù)期?我們有幾個(gè)壓測(cè)手段,如線下單機(jī)壓測(cè)識(shí)別應(yīng)用單機(jī)性能瓶頸,單鏈路壓測(cè)驗(yàn)證集群水位及各層核?系統(tǒng)容量配比,還有全鏈路壓測(cè)等。
這是去年“雙11”之前做的壓測(cè)(見圖),達(dá)到了5K容量的要求。今年對(duì)每個(gè)點(diǎn)進(jìn)一步深入優(yōu)化,2016年最大訂單提升到了10K,比之前提升了25倍。實(shí)際上這些優(yōu)化可以不斷深入,不僅可以不斷提高單機(jī)的性能和單機(jī)的QPS,還可以通過對(duì)服務(wù)整體上的優(yōu)化達(dá)到性能的極致,并且可以引入一些廉價(jià)的機(jī)器(如云主機(jī))來支撐更大的量。
我們?yōu)槭裁匆鲞@些優(yōu)化?業(yè)務(wù)的發(fā)展會(huì)對(duì)業(yè)務(wù)系統(tǒng)、服務(wù)框架提出很多很高的要求。因此,我們對(duì)Tesla做了這些改善(見圖),服務(wù)的配置推送要更快、更可靠地到達(dá)客戶端,所以有了新的配置中心Metabase,也有了Lurker全鏈路監(jiān)控,服務(wù)和服務(wù)框架的不斷發(fā)展推動(dòng)了網(wǎng)站其他基礎(chǔ)中間件產(chǎn)品的誕生和發(fā)展。2015年的下半年我們進(jìn)行了一系列中間件的自研和全站落地。
我們得到了服務(wù)架構(gòu)1.5的體系(見圖),首先是用戶服務(wù)在最底層,用戶服務(wù)1200K的QPS,庫(kù)存250K,商品服務(wù)400K,營(yíng)銷200K,等等。
接下來我們看一下這一階段,Tesla開始做服務(wù)管控,真正成為了一個(gè)服務(wù)框架。我們最開始做發(fā)布的時(shí)候,客戶端、服務(wù)端由于做的只是初級(jí)的RPC調(diào)用,如果服務(wù)端有變更,客戶端可能是幾秒甚至數(shù)十秒才能拉到新配置,導(dǎo)致經(jīng)常有客戶投訴。有了對(duì)服務(wù)變更推送更高的要求后,我們就有了Matabase配置中心,服務(wù)端如果有發(fā)布或者某一刻崩潰了,客戶端馬上可以感知到,這樣就完成了整個(gè)服務(wù)框架連接優(yōu)化的改進(jìn),真正變成服務(wù)管控、服務(wù)治理框架的開端。
有了上面講到的服務(wù)化改進(jìn)和性能提升之后,是不是大促的時(shí)候看一看監(jiān)控就行了?其實(shí)不是。大流量來的時(shí)候,萬一導(dǎo)致整個(gè)網(wǎng)站崩潰了,一分鐘、兩分鐘的損失是非常大的,所以還要保證服務(wù)是穩(wěn)的和高可用的。只有系統(tǒng)和服務(wù)是穩(wěn)定的,才能更好地完成業(yè)務(wù)指標(biāo)和整體的經(jīng)營(yíng)目標(biāo)。
下面會(huì)講一下服務(wù)SLA保證的內(nèi)容。
首先SLA體現(xiàn)在對(duì)容量、性能、程度的約束,包括程度是多少的比例。那么要保證這個(gè)SLA約束和目標(biāo)達(dá)成,首先要把關(guān)鍵指標(biāo)監(jiān)控起來;第二是依賴治理、邏輯優(yōu)化;第三是負(fù)載均衡、服務(wù)分組和限流;第四是降級(jí)預(yù)案、容災(zāi)、壓測(cè)、在線演練等。這是我們服務(wù)的關(guān)鍵指標(biāo)的監(jiān)控圖(見上圖)。支付回調(diào)服務(wù)要滿足8K QPS,99%的RT在30ms內(nèi),但是圖中監(jiān)控說明SLA未達(dá)到,RT程度指標(biāo)方面要優(yōu)化。
服務(wù)的SLA保證上,服務(wù)端超時(shí)和限流非常重要。如果沒有超時(shí),很容易引起雪崩。我們來講一個(gè)案例,有次商品服務(wù)響應(yīng)變慢,就導(dǎo)致上層的其他服務(wù)都慢,而且商品服務(wù)積壓了很多請(qǐng)求在線程池中,很多請(qǐng)求響應(yīng)過慢導(dǎo)致客戶端等待超時(shí),客戶端早就放棄調(diào)用結(jié)果結(jié)束掉了,但是在商品服務(wù)線程池線程做處理時(shí)拿到這個(gè)請(qǐng)求還會(huì)處理,客戶都跑了,再去處理,客戶也拿不到這個(gè)結(jié)果,最后還會(huì)造成上層服務(wù)請(qǐng)求的堵塞,堵塞原因緩解時(shí)產(chǎn)生洪流。
限流是服務(wù)穩(wěn)定的最后一道保障。一個(gè)是HTTP服務(wù)的限流,一個(gè)是RPC服務(wù)的限流。我們服務(wù)的處理線程是Tesla框架分配的,所以服務(wù)限流可以做到非常精確,可以控制在服務(wù)級(jí)別和服務(wù)方法級(jí)別,也可以針對(duì)來源做限流。
我們做了這樣一系列改造之后,服務(wù)框架變成了有完善的監(jiān)控、有負(fù)載均衡、有服務(wù)分組和限流等完整管控能力的服務(wù)治理框架。服務(wù)分組之后,如果通用的服務(wù)崩潰了,購(gòu)買鏈路的服務(wù)可以不受影響,這就做到了隔離。這樣的一整套服務(wù)體系(如圖)就構(gòu)成了我們的服務(wù)架構(gòu)2.0,最終網(wǎng)站的可用性做到了99.979%,這是今年6月份的統(tǒng)計(jì)數(shù)據(jù)。我們還會(huì)逐步把服務(wù)的穩(wěn)定性和服務(wù)質(zhì)量做到更好。
最后總結(jié)一下,服務(wù)框架的體系完善是一個(gè)漫長(zhǎng)的發(fā)展過程,不需要一開始就很強(qiáng)、什么都有的服務(wù)框架,最早可能就是一個(gè)RPC框架。服務(wù)治理慢慢隨著業(yè)務(wù)量增長(zhǎng)也會(huì)發(fā)展起來,服務(wù)治理是服務(wù)框架的重要組成部分。另外,Tesla是為蘑菇街業(yè)務(wù)體系量身打造的服務(wù)框架。可以說服務(wù)框架是互聯(lián)網(wǎng)網(wǎng)站架構(gòu)的核心和持續(xù)發(fā)展的動(dòng)力。選擇開源還是自建,要看團(tuán)隊(duì)能力、看時(shí)機(jī)。我們要深度定制服務(wù)框架,所以選擇了自研,以后可能會(huì)開源出來。
服務(wù)框架是隨著業(yè)務(wù)發(fā)展不斷演變的,我們有1.0、1.5和2.0架構(gòu)的迭代。要前瞻性地謀劃和實(shí)施,要考慮未來三年、五年的容量。有一些系統(tǒng)瓶頸可能是要提前解決的,每一個(gè)場(chǎng)景不一樣,根據(jù)特定的場(chǎng)景選擇最合適的方案。容量和性能關(guān)鍵字是一切可擴(kuò)展、Cache、IO、異步化。目前我們正在做的是服務(wù)治理和SLA保障系統(tǒng)化,未來會(huì)做同城異地的雙活。
謝謝大家!
感謝陳興璐對(duì)本文的審校。
依賴分為兩種,本地的lib依賴,遠(yuǎn)程的服務(wù)依賴。
本地的依賴其實(shí)是很復(fù)雜的問題。從操作系統(tǒng)的apt-get,到各種語言的pip, npm。包管理是無窮無盡的問題。但是所有的本地依賴已經(jīng)被docker終結(jié)了。無論是依賴了什么,全部給你打包起來,從操作系統(tǒng)開始。除了你依賴的cpu指令集沒法給你打包成鏡像了,其他都給打包了。
docker之后,依賴問題就只剩遠(yuǎn)程服務(wù)依賴的問題。這個(gè)問題就是服務(wù)注冊(cè)發(fā)現(xiàn)與調(diào)度需要解決的問題。從軟件工程的角度來說,所有的解耦問題都可以通過抽取lib的方式解決。lib也可以實(shí)現(xiàn)獨(dú)立的發(fā)布周期,良好定義的IDL接口。所以如果非必要,請(qǐng)不要把lib依賴升級(jí)成網(wǎng)絡(luò)服務(wù)依賴的角度。除非是從非功能性需求的角度,比如獨(dú)立的擴(kuò)縮容,支持scale out這些。很多時(shí)候微服務(wù)是因?yàn)榛趌ib的工具鏈支持不全,使得大家義無反顧地走上了拆分網(wǎng)絡(luò)服務(wù)的不歸路。
服務(wù)名又稱之為Service Qualifier,是一個(gè)人類可理解的英文標(biāo)識(shí)。所謂的服務(wù)注冊(cè)和發(fā)現(xiàn)就是在一個(gè)Service Qualifier下注冊(cè)一堆Endpoint。一個(gè)Endpoint就是一個(gè)ip+端口的網(wǎng)絡(luò)服務(wù)。就是一個(gè)非常類似DNS的名字服務(wù),其實(shí)DNS本身就可以做服務(wù)的注冊(cè)和發(fā)現(xiàn),用SRV類型記錄。
名字服務(wù)的存在意義是簡(jiǎn)化服務(wù)的使用方,也就是主調(diào)方。過去在使用方的代碼里需要填入一堆ip加端口的配置,現(xiàn)在有了名字服務(wù)就可以只填一個(gè)服務(wù)名,實(shí)際在運(yùn)行時(shí)用服務(wù)名找到那一堆endpoint。
從名字服務(wù)的角度來講并不比DNS要強(qiáng)多少。可能也就是通過“服務(wù)發(fā)現(xiàn)的lib”幫你把ip和端口都獲得了。而DNS默認(rèn)lib(也就是libc的getHostByName)只支持host獲取,并不能獲得port。當(dāng)然既然你都外掛了一個(gè)服務(wù)發(fā)現(xiàn)的lib了,和libc做對(duì)比也就優(yōu)勢(shì)公平了。
lib提供的接口類似
$endpoints = listServiceEnpoints('redis'); echo($endpoints[0]['ip]);
甚至可以直接提供拼接url的接口
$url = getServiceUrl('order', '/newOrder'); # http://xxx:yyy/newOrder
傳統(tǒng)DNS的服務(wù)發(fā)現(xiàn)機(jī)制是緩存加上TTL過期時(shí)間,新的endpoint要傳播到使用方需要各級(jí)緩存的刷新。而且即便endpoint沒有更新,因?yàn)門TL到期了也要去上游刷新。為了減少網(wǎng)絡(luò)間定時(shí)刷新endpoint的流量,一般TTL都設(shè)得比較長(zhǎng)。
而另外一個(gè)極端是gossip協(xié)議。所有人連接到所有人。一個(gè)服務(wù)的endpoint注冊(cè)了,可以通過gossip協(xié)議很快廣播到全部的節(jié)點(diǎn)上去。但是gossip的缺點(diǎn)是不基于訂閱的。無論我是不是使用這個(gè)服務(wù),我都會(huì)被動(dòng)地被gossip這個(gè)服務(wù)的endpoint。這樣就造成了無謂的網(wǎng)絡(luò)間帶寬的開銷。
比較理想的更新方式是基于訂閱的。如果業(yè)務(wù)對(duì)某個(gè)服務(wù)進(jìn)行了發(fā)現(xiàn),那么緩存服務(wù)器就保持一個(gè)訂閱關(guān)系獲得最新的endpoint。這樣可以比定時(shí)刷新更及時(shí),也消耗更小。這個(gè)方面要黑一下etcd 2.0,它的基于http連接的watch方案要求每個(gè)watch獨(dú)占一個(gè)tcp連接,嚴(yán)重限制了watch的數(shù)量。而etcd 3.0基于gRPC的實(shí)現(xiàn)就修復(fù)了這個(gè)問題。而consul的msgpack rpc從一開始就是復(fù)用tcp連接的。
圖中的observer是類似的zookeeper的observer角色,是為了幫權(quán)威服務(wù)器分擔(dān)watch壓力的存在。也就是說服務(wù)發(fā)現(xiàn)的核心其實(shí)是一個(gè)基于訂閱的層級(jí)消息網(wǎng)絡(luò)。服務(wù)注冊(cè)和發(fā)現(xiàn)并不承諾任何的一致性,它只是盡力地進(jìn)行分發(fā),并不保證所有的節(jié)點(diǎn)對(duì)一個(gè)服務(wù)的endpoint是哪些有一致的view,因?yàn)檫@并沒有價(jià)值。因?yàn)橐粋€(gè)qualifier下的多個(gè)endpoint by design 就是等價(jià)的,只要有足夠的endpint能夠承擔(dān)負(fù)載,對(duì)于abc三個(gè)endpoint具體是讓ab可見,還是bc可見,并無任何影響。
DNS的方案是在每臺(tái)機(jī)器上裝一個(gè)dnsmasq做為緩存服務(wù)器。服務(wù)發(fā)現(xiàn)也是類似的,在每臺(tái)機(jī)器上有一個(gè)agent進(jìn)程。如果dnsmasq掛了,dns域名就會(huì)解析失敗,這樣的可用性是不夠的。服務(wù)發(fā)現(xiàn)的agent會(huì)把服務(wù)的配置和endpoint dump一份成本機(jī)的文件,服務(wù)發(fā)現(xiàn)的lib在無法訪問agent的時(shí)候會(huì)降級(jí)去讀取本機(jī)的文件,從而保證足夠的可用性。當(dāng)然你要愿意搞什么共享內(nèi)存,也沒人阻攔。
無法實(shí)現(xiàn)對(duì)dns服務(wù)器的降級(jí)。因?yàn)槟呐率墙导?jí)到 /etc/hosts 的實(shí)現(xiàn),其一個(gè)巨大的缺陷是 /etc/hosts 對(duì)于一個(gè)域名只能填一個(gè)ip,無法滿足擴(kuò)展性。而如果這一個(gè)ip填的是代理服務(wù)器的話,則失去了做服務(wù)發(fā)現(xiàn)的意義,都有代理了那就讓代理去發(fā)現(xiàn)服務(wù)好了。
更進(jìn)一步,很多基于zk的方案是把服務(wù)發(fā)現(xiàn)的agent和業(yè)務(wù)進(jìn)程做到一個(gè)進(jìn)程里去了。所以就不需要擔(dān)心外掛的進(jìn)程是否還存活的問題了。
這點(diǎn)上和DNS是類似的。理論來說ttl設(shè)置為0的DNS服務(wù)器也可以起到負(fù)載均衡的作用。通過把權(quán)重分發(fā)到服務(wù)發(fā)現(xiàn)的agent上,可以讓業(yè)務(wù)“每次發(fā)現(xiàn)”的endpoint都不一樣,從而達(dá)到均衡負(fù)載的作用。權(quán)重的實(shí)現(xiàn)通過簡(jiǎn)單的隨機(jī)算法就可以實(shí)現(xiàn)。
通過軟負(fù)載均衡理論上可以實(shí)現(xiàn)小流量,灰度地讓一個(gè)新的endpoint加入集群。也可以實(shí)現(xiàn)某一些endpoint承擔(dān)更大的調(diào)用量,以達(dá)到在線壓測(cè)的目的。
不要小瞧了這么一點(diǎn)調(diào)權(quán)的功能。能夠中央調(diào)度,智能調(diào)度流量,是非常有用的。
故障檢測(cè)其實(shí)是好做的。無非就是一個(gè)qualifier下掛了很多個(gè)endpoint,根據(jù)某種探活機(jī)制摘掉其中已經(jīng)無法提供正常服務(wù)的endpoint。摘除最好是軟摘除,這樣不會(huì)出現(xiàn)一個(gè)閃失把所有endpoint全摘掉的問題。比如zookeeper的臨時(shí)節(jié)點(diǎn)就是硬摘除,不可取。
在業(yè)務(wù)拿到endpoint之后,做完了rpc可以知道這個(gè)endpoint是否可用。這個(gè)時(shí)候?qū)ndpoint的健康狀態(tài)本地做一個(gè)投票累積。如果endpoint連續(xù)不可用則標(biāo)記為故障,被臨時(shí)摘除。過一段時(shí)間之后再重新放出小黑屋,進(jìn)行探活。這個(gè)過程和nginx對(duì)upstream的被動(dòng)探活是非常類似的。
被動(dòng)探活的好處是非常敏感而且真實(shí)可信(不可用就是我不能調(diào)你,就是不可用),本地投票完了立即就可以判定故障。缺陷是每個(gè)主調(diào)方都需要獨(dú)立去進(jìn)行重復(fù)的判定。對(duì)于故障的endpoint,為了探活其是否存活需要以latency做為代價(jià)。
被動(dòng)探活不會(huì)和具體的rpc機(jī)制綁定。無論是http還是thrift,無論是redis還是mysql,只要是網(wǎng)絡(luò)調(diào)用都可以通過rpc后投票的方式實(shí)現(xiàn)被動(dòng)探活。
主動(dòng)探活比較難做,而且效果也未必好:
所有的主動(dòng)探活的問題都在于需要指定如何去探測(cè)。不是tcp連接得上就算是能提供服務(wù)的。
主動(dòng)探活受到網(wǎng)絡(luò)路由的影響,a可以訪問b,并不帶表c也可以訪問b
主動(dòng)探測(cè)帶來額外的網(wǎng)絡(luò)開銷,探測(cè)不能過于頻繁
主動(dòng)探測(cè)的發(fā)起者過少則容易對(duì)發(fā)起者產(chǎn)生很大的探活壓力,需要很高的性能
consul 的本機(jī)主動(dòng)探活是一個(gè)很有意思的組合。避免了主動(dòng)探活的一些缺點(diǎn),可以是被動(dòng)探活的一些補(bǔ)充。
無論是zookeeper那樣一來tcp連接的心跳(tcp連接的保持其實(shí)也是定時(shí)ttl發(fā)ip包保持的)。還是etcd,consul支持的基于ttl的心跳。都是類似的。
改進(jìn)版本的心跳。減少整體的網(wǎng)絡(luò)間通信量。
服務(wù)endpoint注冊(cè)比endpoint摘除要難得多。
無狀態(tài)服務(wù)的注冊(cè)沒有任何約束。不管是中央管理服務(wù)注冊(cè)表,用web界面注冊(cè)。還是和部署系統(tǒng)聯(lián)動(dòng),在進(jìn)程啟動(dòng)時(shí)自動(dòng)注冊(cè)都可以做。
有狀態(tài)服務(wù),比如redis的某個(gè)分片的master。其有兩個(gè)約束:
一致性:同一個(gè)分片不能有兩個(gè)master
可用性:分片不能沒有master,當(dāng)master掛了,要自發(fā)選舉出新的master
除非是在數(shù)據(jù)層協(xié)議上做ack(paxos,raft)或者協(xié)議本身支持沖突解決(crdt),否則基于服務(wù)注冊(cè)來實(shí)現(xiàn)的分布式要么犧牲一致性,要么犧牲可用性。
有狀態(tài)服務(wù)的注冊(cè)需求,和普通的注冊(cè)發(fā)現(xiàn)需求是本質(zhì)不同的。有狀態(tài)服務(wù)需要的是一個(gè)一致性決策機(jī)制,在consistency和availability之間取平衡。這個(gè)機(jī)制可以是外掛一個(gè)zookeeper,也可以是集群的數(shù)據(jù)節(jié)點(diǎn)自身做一個(gè)gossip的投票機(jī)制。
而普通的注冊(cè)和發(fā)現(xiàn)就是要給廣播渠道,提供visibility。盡可能地讓endpoint曝光到其使用方那。不同的問題需要的解決方案是不同的。對(duì)于有狀態(tài)服務(wù)的注冊(cè)表需要非常可靠的故障檢測(cè)機(jī)制,不能隨意摘除master。而用于廣播的服務(wù)注冊(cè)表則很隨意,故障檢測(cè)機(jī)制也可以做到盡可能錯(cuò)殺三千不放過一個(gè)。廣播的機(jī)制需要解決的問題是大集群,怎么讓服務(wù)可見。而數(shù)據(jù)節(jié)點(diǎn)的選主要解決的是相對(duì)小的集群,怎么保持一致地情況下盡量可用。拿zookeeper的臨時(shí)節(jié)點(diǎn)這樣的機(jī)制放在大集群背景下,去做無狀態(tài)節(jié)點(diǎn)探活就是技術(shù)用錯(cuò)了地方。
比如kafka,其有狀態(tài)服務(wù)部分的注冊(cè)和發(fā)現(xiàn)是用zookeeper實(shí)現(xiàn)的。而無狀態(tài)服務(wù)的注冊(cè)與發(fā)現(xiàn)是用data node自身提供集群的metadata來實(shí)現(xiàn)的。也就是消費(fèi)者和生產(chǎn)者是不需要從zookeeper里去集群分片信息的(也就是服務(wù)注冊(cè)表),而是從data node拿。這個(gè)時(shí)候data node其是充當(dāng)了一個(gè)服務(wù)發(fā)現(xiàn)的agent的作用。如果不用data node干這個(gè)活,我們把data node的內(nèi)容放到DNS里去,其實(shí)也是可以work的。只是這些存儲(chǔ)的給業(yè)務(wù)使用的客戶端lib已經(jīng)把這些邏輯寫好了,沒有人會(huì)去修改這個(gè)默認(rèn)行為了。
但是廣播用途的服務(wù)注冊(cè)和發(fā)現(xiàn),比如DNS不是只提供visibility而不能保證任何consistency嗎?那我讀到分片信息是舊的,把slave當(dāng)master用了怎么辦呢?所有做得好的存儲(chǔ)分片選主方案,在data node上自己是知道自己的角色的。如果你使用錯(cuò)了,像redis cluster會(huì)回一個(gè)move指令,相當(dāng)于http 302讓你去別的地方做這個(gè)操作。kafka也是類似的。
libc只支持getHostByName,任何更高級(jí)的服務(wù)發(fā)現(xiàn)都需要挖空心思想怎么簡(jiǎn)化接入。反正操作系統(tǒng)和語言自身的工具鏈上是沒有標(biāo)準(zhǔn)的支持的。每個(gè)公司都有一套自己的玩法。行業(yè)嚴(yán)重缺乏標(biāo)準(zhǔn)。
無論哪種方式都是要修改業(yè)務(wù)代碼的。即便是用proxy方式接入,業(yè)務(wù)代碼里也得寫死固定的proxy ip才行。從可讀性的角度來說,固定proxy ip的可讀性是最差的,而用服務(wù)名或者域名是可讀性最好的。
最笨拙的方法,也是最保險(xiǎn)的。業(yè)務(wù)代碼直接寫服務(wù)名,獲得endpoint。
探活也就是硬改各種rpc的lib,在調(diào)用后面加上投票的代碼。
因?yàn)樗械恼Z言基本上都支持DNS域名解析。利用這一層的接口,用鉤子換掉lib的實(shí)際實(shí)現(xiàn)。業(yè)務(wù)代碼里寫域名,端口固定。
socket的鉤子要難做得多,而且僅僅tcp4層探活也是不夠的(http 500了往往也要認(rèn)為對(duì)方是掛了的)。
實(shí)際上考慮golang這種沒有l(wèi)ibc的,java這種自己緩存域名結(jié)果的,鉤子的方案其實(shí)沒有想得那么美好。
proxy其實(shí)是一種簡(jiǎn)化服務(wù)發(fā)現(xiàn)接入方式的手段。業(yè)務(wù)可以不用知道服務(wù)名,而是使用固定的ip和端口訪問。由proxy去做服務(wù)發(fā)現(xiàn),把請(qǐng)求轉(zhuǎn)給對(duì)方。
http的proxy也很成熟,在proxy里對(duì)rpc結(jié)果進(jìn)行跳票也有現(xiàn)成的工具(比如nginx)。很多公司都是這種本地proxy的架構(gòu),比如airbnb,yelp,eleme,uber。當(dāng)用lib方式接業(yè)務(wù)接不動(dòng)的時(shí)候,大家都會(huì)往這條路上轉(zhuǎn)的。
遠(yuǎn)程proxy的缺陷是固定ip導(dǎo)致了路由是固定的。這條路由上的所有路由器和交換機(jī)都是故障點(diǎn)。無法做到多條網(wǎng)絡(luò)路由冗余容錯(cuò)。而且需要用lvs做虛ip,也引入了運(yùn)維成本。
而且遠(yuǎn)程proxy無法支持分區(qū)部署多套環(huán)境。除非引入bgp anycast這樣妖孽的實(shí)現(xiàn)。讓同一個(gè)ip在不同的idc里路由到不同的服務(wù)器。
國(guó)內(nèi)大部分的網(wǎng)游都是分區(qū)分服的。這種架構(gòu)就是一種簡(jiǎn)化的存儲(chǔ)層數(shù)據(jù)分片。存儲(chǔ)層的數(shù)據(jù)分片一般都做得非常完善,可以做到key級(jí)別的搬遷(當(dāng)你訪問key的時(shí)候告訴你我可以響應(yīng),還是告訴你搬遷到哪里去了),可以做到訪問錯(cuò)了shard告訴你正確的shard在哪里。而分區(qū)部署往往是沒有這么完善的。
所以為了支持分區(qū)部署。往往是給不同分區(qū)的服務(wù)區(qū)不同的服務(wù)名。比如模塊叫 chat,那么給hb_set(華北大區(qū))的chat模塊就命名為hb_set.chat,給hn_set(華南大區(qū))的chat模塊就命名為hn_set.chat。當(dāng)時(shí)如果我們是gamesvr模塊,需要訪問chat模塊,代碼都是同一份,我怎么知道應(yīng)該訪問hn_set.chat還是hb_set.chat呢?這個(gè)就需要讓gamesvr先知道自己所在的set,然后去訪問同set下的其他模塊。
again,這種分法也就是因?yàn)榉謪^(qū)部署做為一個(gè)大的組合系統(tǒng)沒法像一個(gè)孤立地存儲(chǔ)做得那么好。像kafka的broker,哪怕你訪問的不是它的本地分片,它可以幫你去做proxy連接到正確的分片上。而我們沒法要求一個(gè)組合出來的業(yè)務(wù)系統(tǒng)也做到這么完備地程度。所以湊合著用吧。
但是這種分法也有問題。有一些模塊如果不是分區(qū)的,是全局的怎么辦?這個(gè)時(shí)候服務(wù)發(fā)現(xiàn)就得起一個(gè)路由表的作用,把不同分區(qū)的服務(wù)通過路由串起來。
前段時(shí)間,看到redis作者發(fā)布的一篇文章《Is Redlock safe?》,Redlock是redis作者基于redis設(shè)計(jì)的分布式鎖的算法。文章起因是有一位分布式的專家寫了一篇文章《How to do distributed locking》,質(zhì)疑Redlock的正確性。redis作者則在《Is Redlock safe?》文章中給予回應(yīng),一來一回甚是精彩。文本就為讀者一一解析兩位專家的爭(zhēng)論。
在了解兩位專家的爭(zhēng)論前,讓我先從我了解的分布式鎖一一道來。文章中提到的分布式鎖均為排他鎖。
我第一次接觸分布式鎖用的是mysql的鎖表。當(dāng)時(shí)我并沒有分布式鎖的概念。只知道當(dāng)時(shí)有兩臺(tái)交易中心服務(wù)器處理相同的業(yè)務(wù),每個(gè)交易中心處理訂單的時(shí)候需要保證另一個(gè)無法處理。于是用mysql的一張表來控制共享資源。表結(jié)構(gòu)如下:
CREATE TABLE `lockedOrder` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主碼', `type` tinyint(8) unsigned NOT NULL DEFAULT '0' COMMENT '操作類別', `order_id` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的order_id', `memo` varchar(1024) NOT NULL DEFAULT '', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數(shù)據(jù)時(shí)間,自動(dòng)生成', PRIMARY KEY (`id`), UNIQUE KEY `uidx_order_id` (`order_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的訂單';
order_id記錄了訂單號(hào),type和memo用來記錄下是那種類型的操作鎖定的訂單,memo用來記錄一下操作內(nèi)容。這張表能完成分布式鎖的主要原因正是由于把order_id設(shè)置為了UNIQUE KEY
,所以同一個(gè)訂單號(hào)只能插入一次。于是對(duì)鎖的競(jìng)爭(zhēng)就交給了數(shù)據(jù)庫(kù),處理同一個(gè)訂單號(hào)的交易中心把訂單號(hào)插入表中,數(shù)據(jù)庫(kù)保證了只有一個(gè)交易中心能插入成功,其他交易中心都會(huì)插入失敗。lock和unlock的偽代碼也非常簡(jiǎn)單:
def lock : exec sql: insert into lockedOrder(type,order_id,memo) values (type,order_id,memo) if result == true : return true else : return false def unlock : exec sql: delete from lockedOrder where order_id='order_id'
讀者可以發(fā)現(xiàn),這個(gè)鎖從功能上有幾個(gè)問題:
這把鎖沒有過期時(shí)間,如果交易中心鎖定了訂單,但異常宕機(jī)后,這個(gè)訂單就無法鎖定了。這里為了讓鎖能夠失效,需要在應(yīng)用層加上定時(shí)任務(wù),去刪除過期還未解鎖的訂單。clear_timeout_lock的偽代碼很簡(jiǎn)單,只要執(zhí)行一條sql即可。
def clear_timeout_lock : exec sql : delete from lockedOrder where update_time < ADDTIME(NOW(),'-00:02:00')
這里設(shè)置過期時(shí)間為2分鐘,也是從業(yè)務(wù)場(chǎng)景考慮的,如果訂單處理時(shí)間可能超過2分鐘的話,這個(gè)時(shí)候還需要加大。
這把鎖是不能重入的,意思就是即使一個(gè)交易中心獲得了鎖,在它為解鎖前,之后的流程如果有再去獲取鎖的話還會(huì)失敗,這樣就可能出現(xiàn)死鎖。這個(gè)問題我們當(dāng)時(shí)沒有處理,如果要處理這個(gè)問題的話,需要增加字段,在insert的時(shí)候,把該交易中心的標(biāo)識(shí)加進(jìn)來,這樣再獲取鎖的時(shí)候, 通過select,看下鎖定的人是不是自己。lock的偽代碼版本如下:
def lock : exec sql: insert into lockedOrder(type,order_id,memo) values (type,order_id,memo) if result == true : return true else : exec sql : select id from lockedOrder where order_id='order_id' and memo = 'TradeCenterId' if count > 0 : return true else return false
在鎖定失敗后,看下鎖是不是自己,如果是自己,那依然鎖定成功。不過這個(gè)方法解鎖又遇到了困難,第一次unlock就把鎖給釋放了,后面的流程都是在沒鎖的情況下完成,就可能出現(xiàn)其他交易中心也獲取到這個(gè)訂單鎖,產(chǎn)生沖突。解決這個(gè)辦法的方法就是給鎖加計(jì)數(shù)器,記錄下lock多少次。unlock的時(shí)候,只有在lock次數(shù)為0后才能刪除數(shù)據(jù)庫(kù)的記錄。
可以看出,數(shù)據(jù)庫(kù)鎖能實(shí)現(xiàn)一個(gè)簡(jiǎn)單的避免共享資源被多個(gè)系統(tǒng)操作的情況。我以前在盛大的時(shí)候,發(fā)現(xiàn)盛大特別喜歡用數(shù)據(jù)庫(kù)鎖。盛大的前輩們會(huì)說,盛大基本上實(shí)現(xiàn)分布式鎖用的都是數(shù)據(jù)庫(kù)鎖。在并發(fā)量不是那么恐怖的情況下,數(shù)據(jù)庫(kù)鎖的性能也不容易出問題,而且由于數(shù)據(jù)庫(kù)的數(shù)據(jù)具有持久化的特性,一般的應(yīng)用也足夠應(yīng)付。但是除了上面說的數(shù)據(jù)庫(kù)鎖的幾個(gè)功能問題外,數(shù)據(jù)庫(kù)鎖并沒有很好的應(yīng)付數(shù)據(jù)庫(kù)宕機(jī)的場(chǎng)景,如果數(shù)據(jù)庫(kù)宕機(jī),會(huì)帶來的整個(gè)交易中心無法工作。當(dāng)時(shí)我也沒想過這個(gè)問題,我們整個(gè)交易系統(tǒng),數(shù)據(jù)庫(kù)是個(gè)單點(diǎn),不過數(shù)據(jù)庫(kù)實(shí)在是太穩(wěn)定了,兩年也沒出過任何問題。隨著工作經(jīng)驗(yàn)的積累,構(gòu)建高可用系統(tǒng)的概念越來越強(qiáng),系統(tǒng)中是不允許出現(xiàn)單點(diǎn)的。現(xiàn)在想想,通過數(shù)據(jù)庫(kù)的同步復(fù)制,以及使用vip切換Master就能解決這個(gè)問題。
后來我開始接觸緩存服務(wù),知道很多應(yīng)用都把緩存作為分布式鎖,比如redis。使用緩存作為分布式鎖,性能非常強(qiáng)勁,在一些不錯(cuò)的硬件上,redis可以每秒執(zhí)行10w次,內(nèi)網(wǎng)延遲不超過1ms,足夠滿足絕大部分應(yīng)用的鎖定需求。
redis鎖定的原理是利用setnx命令,即只有在某個(gè)key不存在情況才能set成功該key,這樣就達(dá)到了多個(gè)進(jìn)程并發(fā)去set同一個(gè)key,只有一個(gè)進(jìn)程能set成功。
僅有一個(gè)setnx命令,redis遇到的問題跟數(shù)據(jù)庫(kù)鎖一樣,但是過期時(shí)間這一項(xiàng),redis自帶的expire功能可以不需要應(yīng)用主動(dòng)去刪除鎖。而且從 Redis 2.6.12 版本開始,redis的set命令直接直接設(shè)置NX和EX屬性,NX即附帶了setnx數(shù)據(jù),key存在就無法插入,EX是過期屬性,可以設(shè)置過期時(shí)間。這樣一個(gè)命令就能原子的完成加鎖和設(shè)置過期時(shí)間。
緩存鎖優(yōu)勢(shì)是性能出色,劣勢(shì)就是由于數(shù)據(jù)在內(nèi)存中,一旦緩存服務(wù)宕機(jī),鎖數(shù)據(jù)就丟失了。像redis自帶復(fù)制功能,可以對(duì)數(shù)據(jù)可靠性有一定的保證,但是由于復(fù)制也是異步完成的,因此依然可能出現(xiàn)master節(jié)點(diǎn)寫入鎖數(shù)據(jù)而未同步到slave節(jié)點(diǎn)的時(shí)候宕機(jī),鎖數(shù)據(jù)丟失問題。
redis作者鑒于單點(diǎn)redis作為分布式鎖的可能出現(xiàn)的鎖數(shù)據(jù)丟失問題,提出了Redlock算法,該算法實(shí)現(xiàn)了比單一節(jié)點(diǎn)更安全、可靠的分布式鎖管理(DLM)。下面我就介紹下Redlock的實(shí)現(xiàn)。
Redlock算法假設(shè)有N個(gè)redis節(jié)點(diǎn),這些節(jié)點(diǎn)互相獨(dú)立,一般設(shè)置為N=5,這N個(gè)節(jié)點(diǎn)運(yùn)行在不同的機(jī)器上以保持物理層面的獨(dú)立。
算法的步驟如下:
使用Redlock算法,可以保證在掛掉最多2個(gè)節(jié)點(diǎn)的時(shí)候,分布式鎖服務(wù)仍然能工作,這相比之前的數(shù)據(jù)庫(kù)鎖和緩存鎖大大提高了可用性,由于redis的高效性能,分布式緩存鎖性能并不比數(shù)據(jù)庫(kù)鎖差。
介紹了Redlock,就可以說起文章開頭提到了分布式專家和redis作者的爭(zhēng)論了。
該專家提到,考慮分布式鎖的時(shí)候需要考慮兩個(gè)方面:性能和正確性。
如果使用高性能的分布式鎖,對(duì)正確性要求不高的場(chǎng)景下,那么使用緩存鎖就足夠了。
如果使用可靠性高的分布式鎖,那么就需要考慮嚴(yán)格的可靠性問題。而Redlock則不符合正確性。為什么不符合呢?專家列舉了幾個(gè)方面。
現(xiàn)在很多編程語言使用的虛擬機(jī)都有GC功能,在Full GC的時(shí)候,程序會(huì)停下來處理GC,有些時(shí)候Full GC耗時(shí)很長(zhǎng),甚至程序有幾分鐘的卡頓,文章列舉了HBase的例子,HBase有時(shí)候GC幾分鐘,會(huì)導(dǎo)致租約超時(shí)。而且Full GC什么時(shí)候到來,程序無法掌控,程序的任何時(shí)候都可能停下來處理GC,比如下圖,客戶端1獲得了鎖,正準(zhǔn)備處理共享資源的時(shí)候,發(fā)生了Full GC直到鎖過期。這樣,客戶端2又獲得了鎖,開始處理共享資源。在客戶端2處理的時(shí)候,客戶端1 Full GC完成,也開始處理共享資源,這樣就出現(xiàn)了2個(gè)客戶端都在處理共享資源的情況。
專家給出了解決辦法,如下圖,看起來就是MVCC,給鎖帶上token,token就是version的概念,每次操作鎖完成,token都會(huì)加1,在處理共享資源的時(shí)候帶上token,只有指定版本的token能夠處理共享資源。
然后專家還說到了算法依賴本地時(shí)間,而且redis在處理key過期的時(shí)候,依賴gettimeofday方法獲得時(shí)間,而不是monotonic clock,這也會(huì)帶來時(shí)間的不準(zhǔn)確。比如一下場(chǎng)景,兩個(gè)客戶端client 1和client 2,5個(gè)redis節(jié)點(diǎn)nodes (A, B, C, D and E)。
總結(jié)專家關(guān)于Redlock不可用的兩點(diǎn):
所以專家給出的結(jié)論是,只有在有界的網(wǎng)絡(luò)延遲、有界的程序中斷、有界的時(shí)鐘錯(cuò)誤范圍,Redlock才能正常工作,但是這三種場(chǎng)景的邊界又是無法確認(rèn)的,所以專家不建議使用Redlock。對(duì)于正確性要求高的場(chǎng)景,專家推薦了Zookeeper,關(guān)于使用Zookeeper作為分布式鎖后面再討論。
redis作者看到這個(gè)專家的文章后,寫了一篇博客予以回應(yīng)。作者很客氣的感謝了專家,然后表達(dá)出了對(duì)專家觀點(diǎn)的不認(rèn)同。
I asked for an analysis in the original Redlock specification here: http://redis.io/topics/distlock. So thank you Martin. However I don’t agree with the analysis.
redis作者關(guān)于使用token解決鎖超時(shí)問題可以概括成下面五點(diǎn):
專家說到的另一個(gè)時(shí)鐘問題,redis作者也給出了解釋。客戶端實(shí)際獲得的鎖的時(shí)間是默認(rèn)的超時(shí)時(shí)間,減去獲取鎖所花費(fèi)的時(shí)間,如果獲取鎖花費(fèi)時(shí)間過長(zhǎng)導(dǎo)致超過了鎖的默認(rèn)超時(shí)間,那么此時(shí)客戶端并不能獲取到鎖,不會(huì)存在專家提出的例子。
看了兩位專家你來我回的爭(zhēng)辯,相信讀者會(huì)對(duì)Redlock有了更多的認(rèn)識(shí)。這里我也想就分布式專家提到的兩個(gè)問題結(jié)合redis作者的觀點(diǎn),說說我的想法。
第一個(gè)問題我概括為,在一個(gè)客戶端獲取了分布式鎖后,在客戶端的處理過程中,可能出現(xiàn)鎖超時(shí)釋放的情況,這里說的處理中除了GC等非抗力外,程序流程未處理完也是可能發(fā)生的。之前在說到數(shù)據(jù)庫(kù)鎖設(shè)置的超時(shí)時(shí)間2分鐘,如果出現(xiàn)某個(gè)任務(wù)占用某個(gè)訂單鎖超過2分鐘,那么另一個(gè)交易中心就可以獲得這把訂單鎖,從而兩個(gè)交易中心同時(shí)處理同一個(gè)訂單。正常情況,任務(wù)當(dāng)然秒級(jí)處理完成,可是有時(shí)候,加入某個(gè)rpc請(qǐng)求設(shè)置的超時(shí)時(shí)間過長(zhǎng),一個(gè)任務(wù)中有多個(gè)這樣的超時(shí)請(qǐng)求,那么,很可能就出現(xiàn)超過自動(dòng)解鎖時(shí)間了。當(dāng)初我們的交易模塊是用C++寫的,不存在GC,如果用java寫,中間還可能出現(xiàn)Full GC,那么鎖超時(shí)解鎖后,自己客戶端無法感知,是件非常嚴(yán)重的事情。我覺得這不是鎖本身的問題,上面說到的任何一個(gè)分布式鎖,只要自帶了超時(shí)釋放的特性,都會(huì)出現(xiàn)這樣的問題。如果使用鎖的超時(shí)功能,那么客戶端一定得設(shè)置獲取鎖超時(shí)后,采取相應(yīng)的處理,而不是繼續(xù)處理共享資源。Redlock的算法,在客戶端獲取鎖后,會(huì)返回客戶端能占用的鎖時(shí)間,客戶端必須處理該時(shí)間,讓任務(wù)在超過該時(shí)間后停止下來。
第二個(gè)問題,自然就是分布式專家沒有理解Redlock。Redlock有個(gè)關(guān)鍵的特性是,獲取鎖的時(shí)間是鎖默認(rèn)超時(shí)的總時(shí)間減去獲取鎖所花費(fèi)的時(shí)間,這樣客戶端處理的時(shí)間就是一個(gè)相對(duì)時(shí)間,就跟本地時(shí)間無關(guān)了。
由此看來,Redlock的正確性是能得到很好的保證的。仔細(xì)分析Redlock,相比于一個(gè)節(jié)點(diǎn)的redis,Redlock提供的最主要的特性是可靠性更高,這在有些場(chǎng)景下是很重要的特性。但是我覺得Redlock為了實(shí)現(xiàn)可靠性,卻花費(fèi)了過大的代價(jià)。
分析了這么多原因,我覺得Redlock的問題,最關(guān)鍵的一點(diǎn)在于Redlock需要客戶端去保證寫入的一致性,后端5個(gè)節(jié)點(diǎn)完全獨(dú)立,所有的客戶端都得操作這5個(gè)節(jié)點(diǎn)。如果5個(gè)節(jié)點(diǎn)有一個(gè)leader,客戶端只要從leader獲取鎖,其他節(jié)點(diǎn)能同步leader的數(shù)據(jù),這樣,分區(qū)、超時(shí)、沖突等問題都不會(huì)存在。所以為了保證分布式鎖的正確性,我覺得使用強(qiáng)一致性的分布式協(xié)調(diào)服務(wù)能更好的解決問題。
提到分布式協(xié)調(diào)服務(wù),自然就想到了zookeeper。zookeeper實(shí)現(xiàn)了類似paxos協(xié)議,是一個(gè)擁有多個(gè)節(jié)點(diǎn)分布式協(xié)調(diào)服務(wù)。對(duì)zookeeper寫入請(qǐng)求會(huì)轉(zhuǎn)發(fā)到leader,leader寫入完成,并同步到其他節(jié)點(diǎn),直到所有節(jié)點(diǎn)都寫入完成,才返回客戶端寫入成功。
zookeeper還有幾個(gè)特質(zhì),讓它非常適合作為分布式鎖服務(wù)。
zookeeper實(shí)現(xiàn)鎖的方式是客戶端一起競(jìng)爭(zhēng)寫某條數(shù)據(jù),比如/path/lock,只有第一個(gè)客戶端能寫入成功,其他的客戶端都會(huì)寫入失敗。寫入成功的客戶端就獲得了鎖,寫入失敗的客戶端,注冊(cè)watch事件,等待鎖的釋放,從而繼續(xù)競(jìng)爭(zhēng)該鎖。
如果要實(shí)現(xiàn)tryLock,那么競(jìng)爭(zhēng)失敗就直接返回false即可。
zookeeper實(shí)現(xiàn)的分布式鎖簡(jiǎn)單、明了,分布式鎖的關(guān)鍵技術(shù)都由zookeeper負(fù)責(zé)實(shí)現(xiàn)了。可以看下《從Paxos到Zookeeper:分布式一致性原理與實(shí)踐》書里貼出來的分布式鎖實(shí)現(xiàn)步驟
需要使用zookeeper的分布式鎖功能,可以使用curator-recipes庫(kù)。Curator是Netflix開源的一套ZooKeeper客戶端框架,curator-recipes庫(kù)里面集成了很多zookeeper的應(yīng)用場(chǎng)景,分布式鎖的功能在org.apache.curator.framework.recipes.locks包里面,《跟著實(shí)例學(xué)習(xí)ZooKeeper的用法: 分布式鎖》文章里面詳細(xì)的介紹了curator-recipes分布式鎖的使用,想要使用分布式鎖功能的朋友們不妨一試。
文章寫到這里,基本把我關(guān)于分布式鎖的了解介紹了一遍。可以實(shí)現(xiàn)分布式鎖功能的,包括數(shù)據(jù)庫(kù)、緩存、分布式協(xié)調(diào)服務(wù)等等。根據(jù)業(yè)務(wù)的場(chǎng)景、現(xiàn)狀以及已經(jīng)依賴的服務(wù),應(yīng)用可以使用不同分布式鎖實(shí)現(xiàn)。文章介紹了redis作者和分布式專家關(guān)于Redlock,雖然最終覺得Redlock并不像分布式專家說的那樣缺乏正確性,不過我個(gè)人覺得,如果需要最可靠的分布式鎖,還是使用zookeeper會(huì)更可靠些。curator-recipes庫(kù)封裝的分布式鎖,java應(yīng)用也可以直接使用。而且如果開始依賴zookeeper,那么zookeeper不僅僅提供了分布式鎖功能,選主、服務(wù)注冊(cè)與發(fā)現(xiàn)、保存元數(shù)據(jù)信息等功能都能依賴zookeeper,這讓zookeeper不會(huì)那么閑置。
參考資料:
理論上:
mutex和spinlock都是用于多進(jìn)程/線程間訪問公共資源時(shí)保持同步用的,只是在lock失敗的時(shí)候處理方式有所不同。首先,當(dāng)一個(gè)thread 給一個(gè)mutex上鎖失敗的時(shí)候,thread會(huì)進(jìn)入sleep狀態(tài),從而讓其他的thread運(yùn)行,其中就包裹已經(jīng)給mutex上鎖成功的那個(gè)thread,被占用的lock一旦釋放,就會(huì)去wake up 那個(gè)sleep的thread。其次,當(dāng)一個(gè)thread給一個(gè)spinlock上鎖失敗的時(shí)候,thread會(huì)在spinlock上不停的輪訊,直到成功,所以他不會(huì)進(jìn)入sleep狀態(tài)(當(dāng)然,時(shí)間片用完了,內(nèi)核會(huì)自動(dòng)進(jìn)行調(diào)度)。
存在的問題:
無論是mutex還是spinlock,如果一個(gè)thread去給一個(gè)已經(jīng)被其他thread占用的鎖上鎖,那么從此刻起到其他thread對(duì)此鎖解鎖的時(shí)間長(zhǎng)短將會(huì)導(dǎo)致mutex和spinlock出現(xiàn)下面的問題。
mutex的問題是,它一旦上鎖失敗就會(huì)進(jìn)入sleep,讓其他thread運(yùn)行,這就需要內(nèi)核將thread切換到sleep狀態(tài),如果mutex又在很短的時(shí)間內(nèi)被釋放掉了,那么又需要將此thread再次喚醒,這需要消耗許多CPU指令和時(shí)間,這種消耗還不如讓thread去輪訊。也就是說,其他thread解鎖時(shí)間很短的話會(huì)導(dǎo)致CPU的資源浪費(fèi)。
spinlock的問題是,和上面正好相反,如果其他thread解鎖的時(shí)間很長(zhǎng)的話,這種spinlock進(jìn)行輪訊的方式將會(huì)浪費(fèi)很多CPU資源。
解決方法:
對(duì)于single-core/single-CPU,spinlock將一直浪費(fèi)CPU資源,如果采用mutex,反而可以立刻讓其他的thread運(yùn)行,可能去釋放mutex lock。對(duì)于multi-core/mutil-CPU,會(huì)存在很多短時(shí)間被占用的lock,如果總是去讓thread sleep,緊接著去wake up,這樣會(huì)浪費(fèi)很多CPU資源,從而降低了系統(tǒng)性能,所以應(yīng)該盡量使用spinlock。
現(xiàn)實(shí)情況:
由于程序員不太可能確定每個(gè)運(yùn)行程序的系統(tǒng)CPU和core的個(gè)數(shù),所以也不可能去確定使用那一種lock。因此現(xiàn)在的操作系統(tǒng)通常不太區(qū)分mutex和spinlock了。實(shí)際上,大多數(shù)現(xiàn)代操作系統(tǒng)已經(jīng)使用了混合mutex(hybrid mutex)和混合spinlock(hybrid spinlock)。說白了就是將兩者的特點(diǎn)相結(jié)合。
hydrid mutex:在一個(gè)multi-core系統(tǒng)上,hybrid mutex首先像一個(gè)spinlock一樣,當(dāng)thread加鎖失敗的時(shí)候不會(huì)立即被設(shè)置成sleep,但是,當(dāng)過了一定的時(shí)間(或則其他的策略)還沒有獲得lock,就會(huì)被設(shè)置成sleep,之后再被wake up。而在一個(gè)single-core系統(tǒng)上,hybrid mutex就不會(huì)表現(xiàn)出spinlock的特性,而是如果加鎖失敗就直接被設(shè)置成sleep。
hybrid spinlock:和hybrid mutex相似,只不過,thread加鎖失敗后在spinlock一段很短的時(shí)間后,會(huì)被stop而不是被設(shè)置成sleep,stop是正常的進(jìn)程調(diào)度,應(yīng)該會(huì)比先讓thread sleep然后再wake up的開銷小一些。
總結(jié):
寫程序的時(shí)候,如果對(duì)mutex和spinlock有任何疑惑,請(qǐng)選擇使用mutex。
原文參考:http://stackoverflow.com/questions/5869825/when-should-one-use-a-spinlock-instead-of-mutex