高性能分布式计算与存储系统设计概要——暨2012年工作3年半总结
作者:張峻崇?原文鏈接在此。
2012年底,末日之后,看到大家都在寫年末總結(jié),我也忍不住想一試。工作已經(jīng)3年半了,頭一次寫總結(jié)。雖然到現(xiàn)在仍是無(wú)名小碼農(nóng)一名,但工作這些年,技術(shù)著實(shí)有不少積累。成長(zhǎng)最大的,當(dāng)然就是這篇文章標(biāo)題提到的——高性能分布式計(jì)算與存儲(chǔ)系統(tǒng)的設(shè)計(jì)和研發(fā)過(guò)程,這也是我自2010年供職于國(guó)內(nèi)最大的某著名網(wǎng)站之后,和這個(gè)系統(tǒng)一起成長(zhǎng),親眼見證和伴隨著它的發(fā)展,從一個(gè)嬰兒一樣的"Demo"程序,成長(zhǎng)為現(xiàn)在可以處理千萬(wàn)級(jí)日PV的強(qiáng)大系統(tǒng),直到2012年我離開。我也順勢(shì)積累了Unix/Linux服務(wù)器、多線程、I/O、海量數(shù)據(jù)處理、注重高性能與效率的C/C++編程等寶貴的碼農(nóng)財(cái)富,當(dāng)然,遺憾和不足,仍然是有許多的。
2012年,其實(shí)是自工作以來(lái),技術(shù)積淀最多的一年。因?yàn)?#xff0c;在2012年,我終于學(xué)會(huì)了獨(dú)立思考,我不再像以前一樣,許許多的技術(shù)只是需要用到的時(shí)候,匆忙的google(有時(shí)候還要先匆忙的先FQ),我發(fā)現(xiàn),“好記性不如爛筆頭”,古訓(xùn)確實(shí)毋庸置疑,有大量的、瑣碎的技術(shù)經(jīng)驗(yàn)、編程細(xì)節(jié)、技巧,需要積淀下來(lái),可能單條的細(xì)節(jié)與技巧,并不會(huì)對(duì)一個(gè)人的職業(yè)生涯產(chǎn)生什么影響,但把它們都積聚起來(lái),就會(huì)強(qiáng)大許多,很實(shí)際的,帶來(lái)的技術(shù)提升,能帶來(lái)更高的Offer。所以,2012年,我開始到博客園寫技術(shù)博客,和眾多園友分享我對(duì)技術(shù)的一知半解,共同進(jìn)步;也終于耐下心,為自己做了一個(gè)簡(jiǎn)單的個(gè)人主頁(yè),雖然10年前,我就可以做出這樣的東西……;我成為了更忠實(shí)的蘋果粉,所以我嘗試去做iOS創(chuàng)業(yè),雖然這和我的主要工作研究方向并不一致,當(dāng)我看到自己做的demo在自己iPhone 4s上跑起來(lái),我突然又有一了一種久違的興奮——那是每一個(gè)程序員,都體會(huì)過(guò)的,小小的成就感;2012年,我開始接觸和了解許多以前從來(lái)不懂的技術(shù):Hadoop、GoogleFS、JVM、XCode、ARC……小到如何將vim打造成一個(gè)IDE……;2012年,我暫時(shí)離開了生活工作了6年的北京,來(lái)到了陌生的上海,雖然明年我可能就會(huì)回到北京,在上海這個(gè)繁華的城市,我又體會(huì)到了一種和京城碼農(nóng)感覺一樣的,技術(shù)氛圍和文化;最后必須一提的,2012年,我結(jié)婚了,并喜得一龍子,在這篇總結(jié)里,衷心的對(duì)我老婆說(shuō)一聲:“老婆,謝謝你,我愛你。”
? ? ? ? 接下來(lái),該進(jìn)入這篇文章的正題了, 就是簡(jiǎn)單地談?wù)?#xff0c;我這兩年,主要做的東西——高性能分布式計(jì)算與存儲(chǔ)系統(tǒng)。
? ? ? ? 這個(gè)系統(tǒng)看名字十分牛比,所涉足的目前互聯(lián)網(wǎng)最領(lǐng)先的技術(shù)領(lǐng)域。具體有什么用途? 在我之前供職的公司,它主要是作為中間層,給網(wǎng)站頁(yè)面提供緩存服務(wù)的,并且,它對(duì)付的難題,是大數(shù)據(jù)、海量數(shù)據(jù),相信,每一個(gè)日PV超過(guò)千萬(wàn)級(jí)的網(wǎng)站,都必須會(huì)有類似的系統(tǒng)存在,如果,你曾經(jīng)看過(guò),博客園里的《淘寶技術(shù)發(fā)展》等類似文章,就一定不會(huì)對(duì)我接來(lái)將要提到的許多概念和術(shù)語(yǔ)感到陌生。對(duì)于這樣大流量,需要處理大數(shù)據(jù)的網(wǎng)站而言,由Web的邏輯直接調(diào)用管理數(shù)據(jù)存儲(chǔ),是非常不科學(xué)的,實(shí)際上也是不可能的,大數(shù)據(jù)、高并發(fā)的對(duì)數(shù)據(jù)庫(kù)進(jìn)行讀寫,通常數(shù)據(jù)庫(kù)都會(huì)掛掉,從而使網(wǎng)站也掛掉,必須要在Web和數(shù)據(jù)庫(kù)之間,通過(guò)技術(shù)手段實(shí)現(xiàn)一種“轉(zhuǎn)換”或“控制”,或“均衡”或“過(guò)渡”,我不知道這樣用詞是否正確,你只要明白其中的意思就好了。這樣的技術(shù)手段有許多,所實(shí)現(xiàn)的東東也有許多,我們用到的,就是被稱為“中間層”的一個(gè)邏輯層,在這個(gè)層,將數(shù)據(jù)庫(kù)的海量數(shù)據(jù)抓出來(lái),做成緩存,運(yùn)行在服務(wù)器的內(nèi)存中,同理,當(dāng)有新的數(shù)據(jù)到來(lái),也先做成緩存,再想辦法,持久化到數(shù)據(jù)庫(kù)中,就是這樣簡(jiǎn)單的思路,但實(shí)現(xiàn)起來(lái),從零到有,可以說(shuō)難如登天,但是,任何事物,都是在曲折中,不斷發(fā)展前進(jìn)的,這是中學(xué)我們就學(xué)過(guò)的哲學(xué)理論。這個(gè)系統(tǒng),就被我們稱簡(jiǎn)為“緩存系統(tǒng)”,它最大的好處,就是砍掉了每天上千萬(wàn)次的數(shù)據(jù)庫(kù)讀寫操作,取代而之的,是讀取服務(wù)器中提供緩存服務(wù)的進(jìn)程所控制的內(nèi)存,所以你知道,這里面節(jié)省了多少的資源申請(qǐng)、競(jìng)爭(zhēng)、I/O……當(dāng)然,后面你也會(huì)發(fā)現(xiàn),它會(huì)帶來(lái)許多新的問(wèn)題,最顯著的問(wèn)題,就是數(shù)據(jù)的同步和一致性,后面我會(huì)講到。
? ? ? ??現(xiàn)在,讓我們先看看, 這個(gè)系統(tǒng),發(fā)展到我離開它的時(shí)候,長(zhǎng)什么樣子?(由于涉及到商業(yè)機(jī)密,具體的技術(shù)不能提供)
?
? ? ? ? 就是這樣的一張架構(gòu)圖,代表著可以處理每日上千萬(wàn)PV的系統(tǒng),涉及到許多的技術(shù),讓我們一個(gè)部分一個(gè)部分解讀它。
? ? ? ? 首先,從當(dāng)我有一個(gè)web請(qǐng)求到達(dá)時(shí),將會(huì)發(fā)生怎樣的事情說(shuō)起。比如,我是一個(gè)用戶,我在這個(gè)網(wǎng)站登陸,我的“個(gè)人”頁(yè)面上,將會(huì)加載許許多多的東西,有許許多多的圖片、文字、消息等,我們舉其中一個(gè)例子,我將要得到我的好友列表——friend list。通過(guò)常識(shí)可以知道,這個(gè)friend list,不是隨機(jī)的、臨時(shí)的,而肯定是一個(gè)(一組)持久化存儲(chǔ)于數(shù)據(jù)庫(kù)里的數(shù)據(jù),我們就是一個(gè)用戶請(qǐng)求得到他的friend list說(shuō)起,來(lái)解讀這張架構(gòu)圖。如果我的網(wǎng)站流量很小,每天不超過(guò)10萬(wàn)PV,峰值可能就幾百個(gè)上千個(gè)用戶,同時(shí)請(qǐng)求他們的friend list,那么,現(xiàn)今任何一種語(yǔ)言配上任何一種數(shù)據(jù)庫(kù)的搭配,只要稍做處理,都可以很好的完成這個(gè)工作——從數(shù)據(jù)庫(kù)中,讀出該用戶的friend list,然后訪回給web,如果用戶對(duì)好友列表作了任何修改,web馬上將修改內(nèi)容寫入數(shù)據(jù)庫(kù),形成新的friend list。然而,當(dāng)訪問(wèn)流量持續(xù)提升,達(dá)到千萬(wàn)級(jí)、甚至億級(jí)PV的時(shí)候,剛才說(shuō)的方法就不可行了。因?yàn)?#xff0c;同時(shí)可能有幾十萬(wàn)甚至上百萬(wàn)用戶,通過(guò)web請(qǐng)求從數(shù)據(jù)庫(kù)中讀(如果寫將會(huì)更糟糕)上百條萬(wàn)數(shù)據(jù),數(shù)據(jù)庫(kù)將不堪重負(fù),形成巨大的延遲甚至掛掉。通過(guò)上面的系統(tǒng),來(lái)解決這樣的問(wèn)題。
? ? ? ??現(xiàn)在,我們要設(shè)計(jì)和研發(fā)的上述系統(tǒng),當(dāng)一個(gè)web頁(yè)面提交一個(gè)獲取friend list的請(qǐng)求后,它首先將根據(jù)一定的規(guī)則,通過(guò)負(fù)載均衡,然后到達(dá)相應(yīng)的master節(jié)點(diǎn)。上面我們提到的是DNS負(fù)載均衡,這得眾多負(fù)載均衡技術(shù)中的一種方法。也就是說(shuō),我有許許多多的master節(jié)點(diǎn)(上圖的scalabe表明,我是可擴(kuò)展的,只要有條件,可隨意橫向擴(kuò)展節(jié)點(diǎn),以提高速度、容災(zāi)、容量等指標(biāo)),每個(gè)master節(jié)點(diǎn)的IP地址(域名)當(dāng)然不一樣,通過(guò)DNS負(fù)載均衡,合理地把該請(qǐng)求,送到相對(duì)“空閑”的master節(jié)點(diǎn)服務(wù)器。現(xiàn)在解釋一下master節(jié)點(diǎn)服務(wù)器和slave節(jié)點(diǎn)服務(wù)器的功能:slave節(jié)點(diǎn),主要用于"Running services",即,實(shí)際處理請(qǐng)求的緩存服務(wù)進(jìn)程,通常運(yùn)行在slave節(jié)點(diǎn)上;master節(jié)點(diǎn),主要用于分發(fā)通過(guò)負(fù)載均衡的請(qǐng)求(當(dāng)然,master節(jié)點(diǎn)上也可以運(yùn)行一些“緩存服務(wù)進(jìn)程”,即并發(fā)流量不高、較輔助的一些服務(wù)),找到用于處理實(shí)際請(qǐng)求的合適的slave節(jié)點(diǎn),將該請(qǐng)求交給它處理,再次實(shí)現(xiàn)了一道“負(fù)載均衡”,同時(shí),需要分布式計(jì)算的內(nèi)容,將可能同時(shí)分發(fā)到幾個(gè)slave節(jié)點(diǎn),之后再對(duì)結(jié)果進(jìn)行合并返回(Map-Reduce原理)。
? ? ? ? 好了,現(xiàn)在我們已經(jīng)知道,一個(gè)friend list請(qǐng)求已經(jīng)通過(guò)DNS負(fù)載均衡、通過(guò)master節(jié)點(diǎn)進(jìn)行分配,到達(dá)了相應(yīng)的slave節(jié)點(diǎn)上。我們還知道,所說(shuō)的“緩存”?,正是slave節(jié)點(diǎn)中所運(yùn)行的services進(jìn)程中所管理的內(nèi)存,提供同樣功能的service可能會(huì)有很多份,同時(shí)運(yùn)行在不同slave節(jié)點(diǎn)上,以提供高并發(fā)和分布式計(jì)算的功能。例如,獲得friend list就是這樣的service,因?yàn)檫@個(gè)功能太常用了,所以,在我們的系統(tǒng)中,這樣的服務(wù)可能同時(shí)提供5份、10份甚至更多,那么我這個(gè)獲取friend list的請(qǐng)求,究竟被分配到哪個(gè)slave節(jié)點(diǎn)上的service處理呢?這正是剛才提到的master節(jié)點(diǎn)來(lái)完成這一工作。再比如,我現(xiàn)在需要獲取“二度關(guān)系”的列表(關(guān)于六度人脈理論,可google),所謂“二度關(guān)系”,就是好友的好友,那么我要取這樣的列表,即friend's every friend list,這樣的請(qǐng)求,將會(huì)把取每個(gè)friend list分配(Map)到不同slave節(jié)點(diǎn)上去做(根據(jù)一定的規(guī)則),然后再進(jìn)行合并(Reduce)(當(dāng)然,熟悉算法的同學(xué)可能已經(jīng)發(fā)現(xiàn),這樣去獲取請(qǐng)求,非常的笨拙,有沒有更好的方法呢?當(dāng)然有!因?yàn)楹糜训暮糜?#xff0c;其實(shí)就是好友的friend list與我和好友的共同好友common friend list的“差集”,對(duì)嗎?,所以我不用去取好友的每個(gè)好友的friend list,而只用取2次就可以通過(guò)計(jì)算完成請(qǐng)求,這又節(jié)省了多少資源呢?假如我有100個(gè)好友,1000個(gè),10000萬(wàn)個(gè)?會(huì)節(jié)省多少次計(jì)算呢?這也證明,一個(gè)良好的算法,對(duì)改善程序性能,有多么大的幫助!)
? ? ? ? 好,我們繼續(xù)。現(xiàn)在,我的獲取friend list的請(qǐng)求,已經(jīng)在被某個(gè)slave節(jié)點(diǎn)中的負(fù)責(zé)這一功能的service進(jìn)程處理,它將根據(jù)一定規(guī)則,給出兩種可能的處理方式:
? ? ? ? 1、 我這個(gè)用戶非常活躍,經(jīng)常登陸網(wǎng)站(一定的規(guī)則,認(rèn)為緩存未到過(guò)期時(shí)間),且我這個(gè)slave節(jié)點(diǎn)自上次“重建緩存”(即重新從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù),建立緩存,后面會(huì)談)后,沒有發(fā)生過(guò)down機(jī)重啟行為(又一定的規(guī)則),我也沒有收到過(guò)master節(jié)點(diǎn)發(fā)送過(guò)來(lái)要求更新緩存(即從數(shù)據(jù)庫(kù)中比較數(shù)據(jù)并更新)的Notification(通知),或是在一定條件下我這個(gè)slave節(jié)點(diǎn)對(duì)它掌握的緩存數(shù)據(jù)版本(版本管理系統(tǒng)原理,思考一下svn的工作原理)和數(shù)據(jù)庫(kù)進(jìn)行了一次比較(注意,比較數(shù)據(jù)版本可認(rèn)為只是一個(gè)int值,且是原子操作,這和比較整條數(shù)據(jù)是否一致在性能上有天壤之別)發(fā)現(xiàn)是最新的數(shù)據(jù)版本,那么,我這個(gè)slave節(jié)點(diǎn)將直接返回緩存數(shù)據(jù),而沒有任何數(shù)據(jù)庫(kù)讀操作,也就是說(shuō),我這一次獲取friend list的請(qǐng)求,得到的是緩存數(shù)據(jù),當(dāng)然,這個(gè)緩存數(shù)據(jù)肯定是最新的、正確的、和數(shù)據(jù)庫(kù)中的持久化數(shù)據(jù)是一致的,后面會(huì)提到怎樣來(lái)盡量保證這一點(diǎn);
? ? ? ? 2、第1點(diǎn)中的“一定規(guī)則”不滿足時(shí),即我這個(gè)slave節(jié)點(diǎn)的緩存和數(shù)據(jù)庫(kù)中的數(shù)據(jù)可能存在不一致的沒有其它辦法,我必須從數(shù)據(jù)庫(kù)中讀取數(shù)據(jù),更新緩存,然后再返回。但同時(shí)注意,slave節(jié)點(diǎn)中的service服務(wù)進(jìn)程,將認(rèn)為此用戶現(xiàn)在活躍,可能還會(huì)請(qǐng)求一些相關(guān)、類似的數(shù)據(jù)(如馬上可能進(jìn)行添加好友、刪除好友等操作),所以去數(shù)據(jù)庫(kù)讀取數(shù)據(jù)的時(shí)候,將不會(huì)只讀friend list,可能與用戶有關(guān)的其它一部分?jǐn)?shù)據(jù),會(huì)被同時(shí)讀取并更新緩存,如果負(fù)責(zé)這一部分?jǐn)?shù)據(jù)的緩存服務(wù)并不是當(dāng)前的service進(jìn)程,或在其它slave節(jié)點(diǎn),或同時(shí)還有幾份service進(jìn)程在工作,那么slave節(jié)點(diǎn)將提交“更新緩存”請(qǐng)求給master節(jié)點(diǎn),通過(guò)master節(jié)點(diǎn)發(fā)出Notification給相關(guān)slave節(jié)點(diǎn)的相關(guān)service進(jìn)程,從而,盡可能使每一次讀取數(shù)據(jù)庫(kù)的作用最大化,而如果稍后用戶果然進(jìn)行了我們猜測(cè)的行為(可認(rèn)為cache命中),結(jié)果將同第1點(diǎn),直接通過(guò)緩存返回?cái)?shù)據(jù)而且保證了數(shù)據(jù)的正確和一致性。
? ? ? ? 好了,剛剛提到的都是“讀操作”,相比“寫操作”, 其數(shù)據(jù)一致性更容易保證,之后我們將講述“寫操作”的工作原理。現(xiàn)在,讓我們先跳過(guò)這一部分,繼續(xù)看架構(gòu)圖。slave節(jié)點(diǎn)之后,就是實(shí)際的數(shù)據(jù)存儲(chǔ)了,使用了MySQL、Redis,MySQL主從之間的協(xié)同是DBA的工作,不在此篇討論,Redis主要存儲(chǔ)K-V鍵值對(duì)數(shù)據(jù),比如用戶id和用戶昵稱,是最常用的K-V對(duì)之一,通過(guò)Redis進(jìn)行存儲(chǔ),再結(jié)合上述的工作過(guò)程,可保證這個(gè)系統(tǒng)的高性能。而架構(gòu)圖最右下角的Hadoop與MongoDB,是可選的MySQL替代方案,其實(shí),正是未來(lái)的主要發(fā)展方向。如果slave節(jié)點(diǎn)中的service服務(wù)進(jìn)程與Hadoop良好結(jié)合,系統(tǒng)的性能將更上一層樓。順便說(shuō)一句,master、slave節(jié)點(diǎn)都是由C++開發(fā)的。Why C++?可參考酷殼上的一篇文章《C++ Performance per $》。
在上面,我們主要討論了,這個(gè)系統(tǒng)怎樣處理大數(shù)據(jù)的“讀”操作,當(dāng)然還有一些細(xì)節(jié)沒有講述。下篇,我們將主要講述,“寫”操作是如何被處理的。我們都知道,如果只有“讀”,那幾乎是不用做任何數(shù)據(jù)同步的,也不會(huì)有并發(fā)安全問(wèn)題,之所以,會(huì)產(chǎn)生這樣那樣的問(wèn)題,會(huì)導(dǎo)致緩存和數(shù)據(jù)庫(kù)的數(shù)據(jù)不一致,其實(shí)根源就在于“寫”操作的存在。下面,讓我們看一看,當(dāng)系統(tǒng)需要寫一條數(shù)據(jù)的時(shí)候,又會(huì)發(fā)生怎樣的事情?
? ? ? ?同樣,我們還是以friend list為例。現(xiàn)在,我登陸了這個(gè)網(wǎng)站,獲取了friend list之后,我添加了一個(gè)好友,那么,我的friend list必定要做修改和更新(當(dāng)然,添加好友這一個(gè)動(dòng)作肯定不會(huì)只有修改更新friend list這一個(gè)請(qǐng)求,但我們以此為例,其它請(qǐng)求也是類似處理),那么,這個(gè)要求修改和更新friend list的請(qǐng)求,和獲取friend list請(qǐng)求類似,在被slave節(jié)點(diǎn)中的服務(wù)進(jìn)程處理之前,也是先通過(guò)DNS負(fù)載均衡,被分配到合適的master節(jié)點(diǎn),再由master節(jié)點(diǎn),分配到合適的相對(duì)空閑的負(fù)責(zé)這一功能的slave點(diǎn)上。現(xiàn)在假設(shè),前面我們已經(jīng)講過(guò),獲取friend list這樣的請(qǐng)求,非常常用,所以,提供這一供能的服務(wù)進(jìn)程將會(huì)有多份,比如,有10份,服務(wù)進(jìn)程編號(hào)為0~9,同時(shí)運(yùn)行在10個(gè)(也可能僅運(yùn)行在1個(gè)~9個(gè)slave節(jié)點(diǎn)上!)slave節(jié)點(diǎn)上,具體分配請(qǐng)求的時(shí)候,選擇哪一個(gè)slave節(jié)點(diǎn)和哪一份服務(wù)進(jìn)程呢?這當(dāng)然有許多種規(guī)則去影響分配策略,我們就舉一個(gè)最簡(jiǎn)單的例子,采用用戶id對(duì)10取模,得到0~9的結(jié)果,即是所選擇的服務(wù)進(jìn)程編號(hào),假設(shè)我的用戶id尾號(hào)為9,那么我這個(gè)請(qǐng)求,只會(huì)被分配到編號(hào)為9的服務(wù)進(jìn)程去處理(當(dāng)然,所有用戶id尾號(hào)為9的都是如此),編號(hào)為9的服務(wù)進(jìn)程,也只負(fù)責(zé)為數(shù)據(jù)庫(kù)中用戶id尾號(hào)為9的那些數(shù)據(jù)做緩存,而用戶id尾號(hào)為0~8的緩存則由其它服務(wù)進(jìn)程來(lái)處理。如果所需的請(qǐng)求是以剛才這種方式工作的,那么現(xiàn)在我要求修改和更新friend list的這個(gè)請(qǐng)求,將只會(huì)被分配到服務(wù)進(jìn)程編號(hào)為9的進(jìn)程來(lái)處理,我們稱之為“單點(diǎn)模型”(也就是說(shuō),同一條數(shù)據(jù)只會(huì)有一份可用緩存,備份節(jié)點(diǎn)上的的不算),你可能已經(jīng)猜到了,還會(huì)有“多點(diǎn)模型”——即同時(shí)有好幾個(gè)服務(wù)進(jìn)程都會(huì)負(fù)責(zé)同樣的緩存數(shù)據(jù),這是更復(fù)雜的情況,我們稍后再討論。
?
? ? ? ?現(xiàn)在,我們接著說(shuō)“單點(diǎn)模型” 。這個(gè)修改和更新friend list的請(qǐng)求到了編號(hào)為9的服務(wù)進(jìn)程中后,如何被處理呢?緩存肯定先要被處理,之后才考慮緩存去和數(shù)據(jù)庫(kù)同步一致,這大家都知道(否則還要這個(gè)系統(tǒng)干嘛?)大家還知道,只要涉及到并發(fā)的讀寫,就肯定存在并發(fā)沖突和安全問(wèn)題,這又如何解決呢?我們有兩種方式,來(lái)進(jìn)行讀寫同步。
1、 第一種方式,就是傳統(tǒng)的,加鎖方式——通過(guò)加鎖,可以有效地保證緩存中數(shù)據(jù)的同步和正確,但缺點(diǎn)也非常明顯,當(dāng)服務(wù)進(jìn)程中同時(shí)存在讀寫操作的線程時(shí),將會(huì)存在嚴(yán)重的鎖競(jìng)爭(zhēng),進(jìn)而重新形成性能瓶頸。好在,通常使用這種方式處理的業(yè)務(wù)需求,都經(jīng)過(guò)上述的一些負(fù)載均衡、分流措施之后,鎖的粒度不會(huì)太大,還是上述例子,我最多也就鎖住了所有用戶id尾號(hào)為9的這部分緩存數(shù)據(jù)更新,其它90%的用戶則不受影響。再具體些,鎖住的緩存數(shù)據(jù)可以更小,甚至僅鎖住我這個(gè)用戶的緩存數(shù)據(jù),那么,鎖產(chǎn)生的性能瓶頸影響就會(huì)更小了(為什么鎖的粒度不可能小到總是直接鎖住每個(gè)用戶的緩存數(shù)據(jù)呢?答案很簡(jiǎn)單,你不可能有那么多的鎖同時(shí)在工作,數(shù)據(jù)庫(kù)也不可能為每個(gè)用戶建一張表),即鎖的粒度是需要平衡和調(diào)整的。好,現(xiàn)在繼續(xù),我要求修改和更新friend list的請(qǐng)求,已經(jīng)被服務(wù)進(jìn)程中的寫進(jìn)程在處理,它將會(huì)申請(qǐng)獲得對(duì)這部分緩存數(shù)據(jù)的鎖,然后進(jìn)行寫操作,之后釋放鎖,傳統(tǒng)的鎖工作流程。在這期間,讀操作將被阻塞等待,可想而知,如果鎖的粒度很大,將有多少讀操作處于阻塞等待狀態(tài),那么該系統(tǒng)的高性能就無(wú)從談起了。
2、有沒有更好的方法呢?當(dāng)然有,這就是無(wú)鎖的工作方式。首先,我們的網(wǎng)站,是一個(gè)讀操作遠(yuǎn)大于寫操作的網(wǎng)站(如果需求相反,可能處理的方式也就相反了),也就是說(shuō),大多數(shù)時(shí)候,讀操作不應(yīng)該被寫操作阻塞,應(yīng)優(yōu)先保證讀操作,如果產(chǎn)生了寫操作,再想辦法使讀操作“更新”一次,進(jìn)而使得讀寫同步。這樣的工作方式,其實(shí)很像版本管理工具,如svn的工作原理:即,每個(gè)人,都可以讀,不會(huì)因?yàn)橛腥嗽谶M(jìn)行寫,使得讀被阻塞;當(dāng)我讀到數(shù)據(jù)后,由于有人寫,可能已經(jīng)不是最新的數(shù)據(jù)了,svn在你嘗試提交寫的時(shí)候,進(jìn)行判斷,如果版本不一致,則重新讀,合并,再寫。我們的系統(tǒng)也是按類似的方式工作的:即每個(gè)線程,都可以讀,但讀之前先比較一下版本號(hào),然后讀緩存數(shù)據(jù),讀完之后準(zhǔn)備返回給Web時(shí),再次比較版本號(hào),如果發(fā)現(xiàn)版本已經(jīng)被更新(當(dāng)然你讀的數(shù)據(jù)頂多是“老”數(shù)據(jù),但不至于是錯(cuò)誤的數(shù)據(jù),Why?還是參考svn,這是"Copy and Write"原理,即我寫的那一份數(shù)據(jù),是copy出來(lái)寫的,寫完再copy回去,不會(huì)在你讀出的那一份上寫),則必須重新讀,直到讀到的緩存數(shù)據(jù)版本號(hào)是最新的。前面已經(jīng)說(shuō)過(guò),比較和更新版本號(hào),可認(rèn)為是原子操作(比如,利用CAS操作可以很好的完成這一點(diǎn),關(guān)于CAS操作,可以google到一大堆東西),所以,整個(gè)處理流程就實(shí)現(xiàn)了無(wú)鎖化,這樣,在大數(shù)據(jù)高并發(fā)的時(shí)候,沒有鎖瓶頸產(chǎn)生。然而,你可能已經(jīng)發(fā)現(xiàn)其中的一些問(wèn)題,最顯著的問(wèn)題,就是可能多讀不止一次數(shù)據(jù),如果讀的數(shù)據(jù)較多較大,又要產(chǎn)生性能瓶頸了(苦!沒有辦法),并且可能產(chǎn)生延遲,造成差的用戶體驗(yàn)。那么,又如何來(lái)解決這些問(wèn)題呢?其實(shí),我們是根據(jù)實(shí)際的業(yè)務(wù)需求來(lái)做權(quán)衡的,如果,所要求的請(qǐng)求,允許一定的延遲存在,實(shí)時(shí)性要求不是最高,比如,我看我好友發(fā)的動(dòng)態(tài),這樣的緩存數(shù)據(jù),并不要求實(shí)時(shí)性非常高,稍稍有延遲是允許的,你可以想象一下,如果你的好友發(fā)了一個(gè)狀態(tài),你完全沒有必要,其實(shí)也不可能在他點(diǎn)擊“發(fā)布”之后,你的動(dòng)態(tài)就得到了更新,其實(shí)只要在一小段時(shí)間內(nèi)(比如10秒?)你的動(dòng)態(tài)更新了,看到了他新發(fā)布了狀態(tài),就足夠了。假設(shè)是這樣的請(qǐng)求,且如果我采用第1種加鎖的方式所產(chǎn)生的性能瓶頸更大,那么,將采用這種無(wú)鎖的工作方式,即當(dāng)讀寫有沖突時(shí)候,讀操作重新讀所產(chǎn)生的開銷或延遲,是可以忍受的。比較幸運(yùn)的是,同時(shí)有多個(gè)讀寫線程操作同一條緩存數(shù)據(jù)導(dǎo)致多次的重讀行為,其實(shí)并不是總是發(fā)生,也就是說(shuō),我們系統(tǒng)的大數(shù)據(jù)并發(fā),主要在多個(gè)進(jìn)程線程同時(shí)讀不同條的數(shù)據(jù)這一業(yè)務(wù)需求上,這也很容易理解,每個(gè)用戶登陸,都是讀他們各自的friend list(不同條數(shù)據(jù),且在不同的slave節(jié)點(diǎn)上),只不過(guò),這些請(qǐng)求是并發(fā)的(如果不進(jìn)行分布式處理會(huì)沖垮服務(wù)器或數(shù)據(jù)庫(kù)),但是并不總是會(huì),許多用戶都要同時(shí)讀某一條friend list同時(shí)我還在更新該條friend list導(dǎo)致多次無(wú)效的重讀行為。
? ? ? ?我們繼續(xù)上面的friend list。現(xiàn)在,我的friend list已經(jīng)在緩存中被修改和更新了。無(wú)論是采用方式1還是方式2進(jìn)行,在這期間,如果恰好有其它線程來(lái)讀我的friend list,那么總之會(huì)受到影響,如果是方式1,該請(qǐng)求將等待寫完畢;而如果是方式2,該請(qǐng)求將讀2次(也可能更多,但實(shí)在不常見)。這樣的處理方式,應(yīng)該不是最好的,但前面已經(jīng)說(shuō)過(guò)了,我們的系統(tǒng),主要解決:大流量高并發(fā)地讀寫多條數(shù)據(jù),而不是一條。接下來(lái),該考慮和數(shù)據(jù)庫(kù)同步的事情了。
? ? ? ?恩,剛才說(shuō)了那么多,你有沒有發(fā)現(xiàn),經(jīng)過(guò)我修改和更新friend list后,緩存中的數(shù)據(jù)和數(shù)據(jù)庫(kù)不一致了呢?顯然,數(shù)據(jù)庫(kù)中的數(shù)據(jù),已經(jīng)過(guò)期了,需要對(duì)其更新。現(xiàn)在,slave節(jié)點(diǎn)中的編號(hào)為9的服務(wù)進(jìn)程,更新完了自己的緩存數(shù)據(jù)后(修改更新我的friend list),將“嘗試”向數(shù)據(jù)庫(kù)更新。注意,用詞“嘗試”表明該請(qǐng)求不一定會(huì)被馬上得到滿足。其實(shí),服務(wù)進(jìn)程對(duì)數(shù)據(jù)庫(kù)的更新,是批量進(jìn)行的,可認(rèn)為是一個(gè)TaskContainer(任務(wù)容器),每間隔一段時(shí)間,或得到一定的任務(wù)數(shù)量,則成批地向數(shù)據(jù)庫(kù)進(jìn)行更新操作,而不是每過(guò)來(lái)一個(gè)請(qǐng)求,更新緩存后就更新一次數(shù)據(jù)庫(kù)(你現(xiàn)在知道了這樣做又節(jié)省了多少次數(shù)據(jù)庫(kù)操作!)。那么,為什么可以這樣做呢?因?yàn)?#xff0c;我們已經(jīng)有了緩存,緩存就是我們的保障,在“單點(diǎn)模型”下,緩存更新后,任何讀緩存的操作,都只會(huì)讀到該緩存,不需要經(jīng)過(guò)數(shù)據(jù)庫(kù),參看上篇中提到過(guò)此問(wèn)題。所以,數(shù)據(jù)庫(kù)的寫更新操作,可以“聚集”,可以一定延遲之后,再進(jìn)行處理。你會(huì)發(fā)現(xiàn),既然如此,我就可以對(duì)這些操作進(jìn)行合并、優(yōu)化,比如,兩個(gè)寫請(qǐng)求都是操作同一張表,那么可以合并成一條,沒錯(cuò),這其實(shí)已經(jīng)涉及到SQL優(yōu)化的領(lǐng)域了。當(dāng)然,你也會(huì)發(fā)現(xiàn),現(xiàn)在緩存中的新數(shù)據(jù)還沒有進(jìn)行持久化,如果在這個(gè)時(shí)間點(diǎn),slave節(jié)點(diǎn)機(jī)器down掉了,那么,這部分?jǐn)?shù)據(jù)就丟失了!所以,這個(gè)延遲時(shí)間并不會(huì)太長(zhǎng),通常10秒已經(jīng)足夠了。即,每10秒,整理一下我這個(gè)服務(wù)進(jìn)程中已經(jīng)更新緩存未更新DB的請(qǐng)求,然后統(tǒng)一處理,如果更杞人憂天(雖然考慮數(shù)據(jù)安全性決不能說(shuō)是杞人憂天,但你要明白,其實(shí)任何實(shí)時(shí)服務(wù)器發(fā)生down行為總是會(huì)有數(shù)據(jù)丟失的,只是或多或少),則延遲間隔可以更短一些,則DB壓力更大一些,再次需要進(jìn)行實(shí)際的考量和權(quán)衡。至此,我的friend list修改和更新請(qǐng)求,就全部完成了,雖然,可能在幾十秒之前,就已經(jīng)在頁(yè)面上看到了變化(通過(guò)緩存返回的數(shù)據(jù))。
? ? ? ?那么,讀和寫都已經(jīng)講述了,還有其它問(wèn)題嗎?問(wèn)題還不少。剛才討論的,都是“單點(diǎn)模型”。
? ? ? ?即,每一條數(shù)據(jù)庫(kù)中的數(shù)據(jù),都只有一份緩存數(shù)據(jù)與之對(duì)應(yīng)。然而,實(shí)際上,“多點(diǎn)模型”是必須存在的,而且是更強(qiáng)大的處理方式,也帶來(lái)同步和一致性的更多難題,即每一條數(shù)據(jù),可能有多份緩存與之對(duì)應(yīng)。即多個(gè)slave節(jié)點(diǎn)上的服務(wù)進(jìn)程中,都有一份對(duì)應(yīng)DB中相同數(shù)據(jù)的緩存,這個(gè)時(shí)候,又將如何同步呢?我們解決的方式,叫做“最終一致性”原則,關(guān)于最終一致性模型,又可以google到一大堆,特別要提出的是GoogleFS的多點(diǎn)一致性同步,就是通過(guò)“最終一致性”來(lái)解決的,通俗的講,就是同一條數(shù)據(jù),同一時(shí)刻,只能被一個(gè)節(jié)點(diǎn)修改。假設(shè),我現(xiàn)在的業(yè)務(wù),是“多點(diǎn)模型”,比如,我的friend list,是多點(diǎn)模型,有多份緩存(雖然實(shí)際并不是這樣的),那么,我對(duì)friend list的修改和更新,將只會(huì)修改我被分配到的slave節(jié)點(diǎn)服務(wù)進(jìn)程中的緩存,其它服務(wù)進(jìn)程或slave節(jié)點(diǎn)的緩存,以及數(shù)據(jù)庫(kù),將必須被同步更新,這是如何做到的呢?這又要用到上篇曾提到的Notification(通知服務(wù)),這個(gè)模塊雖然沒有在架構(gòu)圖中出現(xiàn),卻是這個(gè)系統(tǒng)中最核心的一種服務(wù)(當(dāng)然,它也是多份的,呵呵),即,當(dāng)一條數(shù)據(jù)是多點(diǎn)模型時(shí),當(dāng)某一個(gè)服務(wù)進(jìn)程對(duì)其進(jìn)行修改和更新后,將通過(guò)向master節(jié)點(diǎn)提交Notificaion并通知其它服務(wù)進(jìn)程或其它slave節(jié)點(diǎn),告知他們的緩存已經(jīng)過(guò)期,需要進(jìn)行更新,這個(gè)更新,可能由所進(jìn)行修改更新的服務(wù)進(jìn)程,發(fā)送緩存數(shù)據(jù)給其它進(jìn)程或節(jié)點(diǎn),也由可能等待DB更新之后,由其它節(jié)點(diǎn)從DB進(jìn)行更新,從而間接保證多點(diǎn)一致性。等等,剛才不是說(shuō),通常10秒才批量更新DB嗎?那是因?yàn)樵趩吸c(diǎn)模型下,這樣做是合理的,但在多點(diǎn)模型下,雖然也是批理對(duì)數(shù)據(jù)庫(kù)進(jìn)行更新,但這樣的延遲通常非常小,可認(rèn)為即時(shí)對(duì)數(shù)據(jù)庫(kù)進(jìn)行批量更新,然后,通過(guò)Notification通知所有有這一條數(shù)據(jù)的節(jié)點(diǎn),更新他們的緩存。由此可見,多點(diǎn)模型,所可能產(chǎn)生的問(wèn)題是不少的。那么,為什么要用多點(diǎn)模型呢?假設(shè)我有這樣的業(yè)務(wù):大數(shù)據(jù)高并發(fā)的讀某一條數(shù)據(jù),非常非常多的讀,但寫很少,比如一張XX門的熱門圖片,有很多很多的請(qǐng)求來(lái)自不同的用戶都需要這個(gè)條數(shù)據(jù)的緩存,多點(diǎn)模型即是完美的選擇。我許多slave節(jié)點(diǎn)上都有它的緩存,而很少更新,則可最大限度的享用到多點(diǎn)模型帶來(lái)的性能提升。
? ? ? ?還有一些問(wèn)題,不得不說(shuō)一下。就是down機(jī)和定期緩存更新的問(wèn)題。先說(shuō)宕機(jī),很顯然,緩存是slave節(jié)點(diǎn)中的服務(wù)進(jìn)程的內(nèi)存,一旦節(jié)點(diǎn)宕機(jī),緩存就丟失了,這時(shí)就需要前面我提到過(guò)的“重建緩存”,這通常是由master節(jié)點(diǎn)發(fā)出的,master節(jié)點(diǎn)負(fù)責(zé)監(jiān)控各個(gè)slave節(jié)點(diǎn)(當(dāng)然也可以是其它master節(jié)點(diǎn))的運(yùn)行狀況,如果發(fā)現(xiàn)某個(gè)slave節(jié)點(diǎn)宕機(jī)(沒有了“心跳”,如果你了解一些Hadoop,你會(huì)發(fā)現(xiàn)它也是這樣工作的),則在slave節(jié)點(diǎn)重新運(yùn)行之后(可能進(jìn)行了重啟),master節(jié)點(diǎn)將通知該slave節(jié)點(diǎn),重建其所負(fù)責(zé)的數(shù)據(jù)的緩存,從哪重建,當(dāng)然是從數(shù)據(jù)庫(kù)了,這需要一定的時(shí)間(在我們擁有百萬(wàn)用戶之后,重建一個(gè)slave節(jié)點(diǎn)所負(fù)責(zé)的數(shù)據(jù)的緩存通常需要幾分鐘),那么,從宕機(jī)到slave節(jié)點(diǎn)重建緩存完畢這一段時(shí)間,服務(wù)由誰(shuí)提供呢?顯然備份節(jié)點(diǎn)就出馬了。其實(shí)在單點(diǎn)模型下,如果考慮了備份節(jié)點(diǎn),則其實(shí)所有的請(qǐng)求都是多點(diǎn)模型。只不過(guò)備份節(jié)點(diǎn)并不是總是會(huì)更新它的緩存,而是定期,或收到Notification時(shí),才會(huì)進(jìn)行更新。master節(jié)點(diǎn)在發(fā)現(xiàn)某個(gè)slave節(jié)點(diǎn)宕機(jī)后,可以馬上指向含有同樣數(shù)據(jù)的備份節(jié)點(diǎn),保證緩存服務(wù)不中斷。那么,備份節(jié)點(diǎn)的緩存數(shù)據(jù)是否是最新的呢?有可能不是。雖然,通常每次對(duì)數(shù)據(jù)庫(kù)完成批量更新后,都會(huì)通知備份節(jié)點(diǎn),去更新這些緩存,但還是有可能存在不一致的情況。所以,備份節(jié)點(diǎn)的工作方式,是特別的,即對(duì)于每次請(qǐng)求的緩存都采用Pull(拉)方式,如何Pull?前面提到的版本管理系統(tǒng)再次出馬,即每次讀之前,先比較版本,再讀,寫也是一樣的。所以,備份節(jié)點(diǎn)的性能,并不會(huì)很高,而且,通常需要同時(shí)負(fù)責(zé)幾個(gè)slave節(jié)點(diǎn)的數(shù)據(jù)的備份,所以,存在被沖垮的可能性,還需要slave節(jié)點(diǎn)盡快恢復(fù),然后把服務(wù)工作重新還給它。
? ? ? ?再說(shuō)定期緩存更新的問(wèn)題。通常,所有的slave節(jié)點(diǎn),都會(huì)被部署在夜深人靜的某個(gè)時(shí)候(如02:00~06:00),用戶很少的時(shí)候,定期進(jìn)行緩存更新,以盡可能保證數(shù)據(jù)的同步和一致性,且第二天上午,大量請(qǐng)求到達(dá)時(shí),基本都能從緩存返回最新數(shù)據(jù)。而備份節(jié)點(diǎn),則可能每30分鐘,就進(jìn)行一次緩存更新。咦?前面你不是說(shuō),備份節(jié)點(diǎn)上每次讀都要Pull,比較版本并更新緩存,才會(huì)返回嗎?是的,那為什么還要定期更新呢?答案非常簡(jiǎn)單,因?yàn)槿绻蟛糠志彺娑际亲钚碌臄?shù)據(jù),只比較版本而沒有實(shí)際的更新操作,所消耗的性能很小很小,所以定期更新,在發(fā)生slave節(jié)點(diǎn)宕機(jī)轉(zhuǎn)由備份節(jié)點(diǎn)工作的時(shí)候,有很大的幫助。
? ? ? ?最后,再說(shuō)一下Push(推送)方式,即,每次有數(shù)據(jù)改動(dòng),都強(qiáng)制去更新所有緩存。這種方式很消耗性能,但更能保證實(shí)時(shí)性。而通常我們使用的,都是Pull(拉)方式,即無(wú)論是定期更新緩存,還是收到Notification(雖然收通知是被“推”了一把)后更新緩存,其實(shí)都是拉,把新的數(shù)據(jù)拉過(guò)來(lái),就好了。在實(shí)際的系統(tǒng)中,兩種方式都有,還是那句話,看需求,再?zèng)Q定處理方式。
? ? ? ?好了,終于寫完了這篇總結(jié),看到上篇發(fā)布后,得到了許許多多園友的鼓勵(lì)和支持,在此一并感謝!相信也有不少園友,已經(jīng)看到了這個(gè)系統(tǒng)的許多不足和瓶頸,確實(shí),它并不是一個(gè)完美的系統(tǒng),還需要不斷進(jìn)化。我寫出這篇文章,也是希望和大家多多交流,共同進(jìn)步。馬上就是2013年了,希望自己能有更好的發(fā)展,也希望所有的朋友,都能更上一層樓!
? ? ? ? (全文完,Jone Zhang,張峻崇,2012.12.28)?
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的高性能分布式计算与存储系统设计概要——暨2012年工作3年半总结的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 【Anychat音视频开发】apache
- 下一篇: Android Virtual Devi