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