關于大型網(wǎng)站技術演進的思考:存儲的瓶頸(1-3)
前不久公司請來了位互聯(lián)網(wǎng)界的技術大牛跟我們做了一次大型網(wǎng)站架構的培訓,兩天12個小時信息量非常大,知識的廣度和難度也非常大,培訓完后我很難完整理出全部聽到的知識,今天我換了個思路是回味這次培訓,這個思路就是通過本人目前的經(jīng)驗和技術水平來思考下大型網(wǎng)站技術演進的過程。
首先我們要思考一個問題,什么樣的網(wǎng)站才是大型網(wǎng)站,從網(wǎng)站的技術指標角度考慮這個問題人們很容易犯一個毛病就是認為網(wǎng)站的訪問量是衡量的指標,懂點行的人也許會認為是網(wǎng)站在單位時間里的并發(fā)量的大小來作為指標,如果按這些標準那么像hao123這樣的網(wǎng)站就是大型網(wǎng)站了,如下圖所示:
其實這種網(wǎng)站訪問量非常大,并發(fā)數(shù)也非常高,但是它卻能用最為簡單的web技術來實現(xiàn):我們只要保持網(wǎng)站的充分的靜態(tài)化,多部署幾臺服務器,那么就算地球上所有人都用它,網(wǎng)站也能正常運行。
我覺得大型網(wǎng)站是技術和業(yè)務的結合,一個滿足某些用戶需求的網(wǎng)站只要技術和業(yè)務二者有一方難度很大,必然會讓企業(yè)投入更多的、更優(yōu)秀的人力成本實現(xiàn)它,那么這樣的網(wǎng)站就是所謂的大型網(wǎng)站了。
一個初建的網(wǎng)站往往用戶群都是很小的,最簡單的網(wǎng)站架構就能解決實際的用戶需求,當然為了保證網(wǎng)站的穩(wěn)定性和安全性,我們會把網(wǎng)站的應用部署到至少兩臺機器上,后臺的存儲使用數(shù)據(jù)庫,如果經(jīng)濟實力允許,數(shù)據(jù)庫使用單臺服務器部署,由于數(shù)據(jù)是網(wǎng)站的生命線,因此我們常常會把部署數(shù)據(jù)庫的服務器使用的好點,這個網(wǎng)站結構如下所示:
這個結構非常簡單,其實大部分初建網(wǎng)站開發(fā)里往往業(yè)務邏輯沒有企業(yè)級系統(tǒng)那么復雜,所以只要有個好的idea,建設一個新網(wǎng)站的成本是非常低的,所使用的技術手段也是非常的基本和簡單,不過該圖我們要準備三臺服務器,而且還要租個機房放置我們的服務器,這些成本對于草根和屌絲還是非常高的,幸運的是當下很多大公司和機構提供了云平臺,我們可以花費很少的錢將自己的應用部署到云平臺上,這種做法我們甚至不用去考慮把應用、數(shù)據(jù)庫分開部署的問題,更加進一步的降低了網(wǎng)站開發(fā)和運維的成本,但是這種做法也有一個問題,就是網(wǎng)站的小命被這個云平臺捏住了,如果云平臺掛了,俺們的網(wǎng)站服務也就跟著掛了。
這里我先講講自己獨立使用服務器部署網(wǎng)站的問題,如果我們要把網(wǎng)站服務應用使用多臺服務器部署,這么做的目的一般有兩個:
- 保證網(wǎng)站的可用性,多臺服務器部署應用,那么其中一些服務器掛掉了,只要網(wǎng)站還有服務器能正常運轉(zhuǎn),那么網(wǎng)站對外任然可以正常提供服務。
- 提高網(wǎng)站的并發(fā)量,服務器越多那么網(wǎng)站能夠服務的用戶,單位時間內(nèi)能承載的請求數(shù)也就越大。
不過要做到以上兩點,并不是我們簡單將網(wǎng)站分開部署就可以滿足的,因為大多數(shù)網(wǎng)站在用戶使用時候都是要保持用戶的狀態(tài),具體點就是網(wǎng)站要記住請求是歸屬到那一個客戶端,而這個狀態(tài)在網(wǎng)站開發(fā)里就是通過會話session來體現(xiàn)的。分開部署的web應用服務要解決的一個首要問題就是要保持不同物理部署服務器之間的session同步問題,從而達到當用戶第一次請求訪問到服務器A,第二個請求訪問到服務器B,網(wǎng)站任然知道這兩個請求是同一個人,解決方案很直接:服務器A和服務器B上的session信息要時刻保持同步,那么如何保證兩臺服務器之間session信息的同步呢?
為了回答上面的問題,我們首先要理解下session的機制,session信息在web容器里都是存儲在內(nèi)存里的,web容器會給每個連接它的客戶端生成一個sessionid值,這個sessionid值會被web容器置于http協(xié)議里的cookie域下,當響應被客戶端處理后,客戶端本地會存儲這個sessionid值,用戶以后的每個請求都會讓這個sessionid值隨cookie一起傳遞到服務器,服務器通過sessionid找到內(nèi)存中存儲的該用戶的session內(nèi)容,session在內(nèi)存的數(shù)據(jù)結構是一個map的格式。那么為了保證不同服務器之間的session共享,那么最直接的方案就是讓服務器之間session不斷的傳遞和復制,例如java開發(fā)里常用的tomcat容器就采用這種方案,以前我測試過tomcat這種session同步的性能,我發(fā)現(xiàn)當需要同步的web容器越多,web應用所能承載的并發(fā)數(shù)并沒有因為服務器的增加而線性提升,當服務器數(shù)量達到一個臨界值后,整個web應用的并發(fā)數(shù)甚至還會下降,為什么會這樣了?
原因很簡單,不同服務器之間session的傳遞和復制會消耗服務器本身的系統(tǒng)資源,當服務器數(shù)量越大,消耗的資源越多,當用戶請求越頻繁,系統(tǒng)消耗資源也會越來越大。如果我們多部署服務器的目的只是想保證系統(tǒng)的穩(wěn)定性,采用這種方案還是不錯的,不過web應用最好部署少點,這樣才不會影響到web應用的性能問題,如果我們還想提升網(wǎng)站的并發(fā)量那么就得采取其他的方案了。
時下使用的比較多的方案就是使用獨立的緩存服務器,也就是將session的數(shù)據(jù)存儲在一臺獨立的服務器上,如果覺得存在一臺服務器不安全,那么可以使用memcached這樣的分布式緩存服務器進行存儲,這樣既可以滿足了網(wǎng)站穩(wěn)定性問題也提升了網(wǎng)站的并發(fā)能力。
不過早期的淘寶在這個問題解決更加巧妙,他們將session的信息直接存儲到瀏覽器的cookie里,每次請求cookie信息都會隨著http一起傳遞到web服務器,這樣就避免了web服務器之間session信息同步的問題,這種方案會讓很多人詬病,詬病的原因是cookie的不安全性是總所周知的,如果有人惡意截取cookie信息那么網(wǎng)站不就不安全了嗎?這個答案還真不好說,但是我覺得我們僅僅是跟蹤用戶的狀態(tài),把session存在cookie里其實也沒什么大不了的。
其實如此專業(yè)的淘寶這么做其實還是很有深意的,還記得本文開篇提到的hao123網(wǎng)站,它是可以承載高并發(fā)的網(wǎng)站,它之所以可以做到這一點,原因很簡單它是個靜態(tài)網(wǎng)站,靜態(tài)網(wǎng)站的特點就是不需要記錄用戶的狀態(tài),靜態(tài)網(wǎng)站的服務器不需要使用寶貴的系統(tǒng)資源來存儲大量的session會話信息,這樣它就有更多系統(tǒng)資源來處理請求,而早期淘寶將cookie存在客戶端也是為了達到這樣的目的,所以這個方案在淘寶網(wǎng)站架構里還是使用了很長時間的。
在我的公司里客戶端的請求到達web服務器之前,會先到F5,F(xiàn)5是一個用來做負載均衡的硬件設備,它的作用是將用戶請求均勻的分發(fā)到后臺的服務器集群,F(xiàn)5是硬件的負載均衡解決方案,如果我們沒那么多錢買這樣的設備,也有軟件的負載均衡解決方案,這個方案就是大名鼎鼎的LVS了,這些負載均衡設備除了可以分發(fā)請求外它們還有個能力,這個能力是根據(jù)http協(xié)議的特點設計的,一個http請求從客戶端到達最終的存儲服務器之前可能會經(jīng)過很多不同的設備,如果我們把一個請求比作高速公路上的一輛汽車,這些設備也可以叫做這些節(jié)點就是高速路上的收費站,這些收費站都能根據(jù)自己的需求改變http報文的內(nèi)容,所以負載均衡設備可以記住每個sessionid值對應的后臺服務器,當一個帶有sessionid值的請求通過負載均衡設備時候,負載均衡設備會根據(jù)該sessionid值直接找到指定的web服務器,這種做法有個專有名詞就是session粘滯,這種做法也比那種session信息在不同服務器之間拷貝復制要高效,不過該做法還是比存cookie的效率低下,而且對于網(wǎng)站的穩(wěn)定性也有一定影響即如果某臺服務器掛掉了,那么連接到該服務器的用戶的會話都會失效。
解決session的問題的本質(zhì)也就是解決session的存儲問題,其本質(zhì)也就是解決網(wǎng)站的存儲問題,一個初建的網(wǎng)站在早期的運營期需要解決的問題基本都是由存儲導致的。上文里我提到時下很多新建的web應用會將服務器部署后云平臺里,好的云平臺里或許會幫助我們解決負載均衡和session同步的問題,但是云平臺里有個問題很難解決那就是數(shù)據(jù)庫的存儲問題,如果我們使用的云平臺發(fā)生了重大事故,導致云平臺存儲的數(shù)據(jù)丟失,這種會不會導致我們在云平臺里數(shù)據(jù)庫的信息也會丟失了,雖然這個事情的概率不高,但是發(fā)生這種事情的幾率還是有的,雖然很多云平臺都聲稱自己多么可靠,但是真實可靠性有多高不是局中人還真不清楚哦,因此使用云平臺我們首要考慮的就是要做好數(shù)據(jù)備份,假如真發(fā)生了數(shù)據(jù)丟失,對于一個快速成長的網(wǎng)站而言可能非常致命。
寫到這里一個嬰兒般的網(wǎng)站就這樣被我們創(chuàng)造出來了,我們希望網(wǎng)站能健康快速的成長,如果網(wǎng)站真的按我們預期成長了,那么一定會有一天我們制造的寶寶屋已經(jīng)滿足不了現(xiàn)實的需求,這個時候我們應該如何抉擇了?換掉,全部換掉,使用新的架構例如我們以前長提的SOA架構,分布式技術,這個方法不錯,但是SOA和分布式技術是很難的,成本是很高的,如果這時候我們通過添加幾臺服務器就能解決問題的話,我們絕對不要去選擇什么分布式技術,因為這個成本太高了。上面我講到幾種session共享的方案,這個方案解決了應用的水平擴展問題,那么當我們網(wǎng)站出現(xiàn)瓶頸時候就多加幾臺服務器不就行了嗎?那么這里就有個問題了,當網(wǎng)站成長很快,網(wǎng)站首先碰到的瓶頸到底是哪個方面的問題?
本人是做金融網(wǎng)站的,我們所做的網(wǎng)站有個特點就是當用戶訪問到我們所做的網(wǎng)站時候,目的都很明確就是為了付錢,用戶到了我們所做的網(wǎng)站時候都希望能快點,再快點完成本網(wǎng)站的操作,很多用戶在使用我們做的網(wǎng)站時候不太去關心網(wǎng)站的其他內(nèi)容,因此我們所做的網(wǎng)站相對于數(shù)據(jù)庫而言就是讀寫比例其實非常的均勻,甚至很多場景寫比讀要高,這個特點是很多專業(yè)服務網(wǎng)站的特點,其實這樣的網(wǎng)站和企業(yè)開發(fā)的特點很類似:業(yè)務操作的重要度超過了業(yè)務展示的重要度,因此專業(yè)性網(wǎng)站吸納企業(yè)系統(tǒng)開發(fā)的特點比較多。但是大部分我們?nèi)粘3S玫木W(wǎng)站,我們逗留時間很長的網(wǎng)站按數(shù)據(jù)庫角度而言往往是讀遠遠大于寫,例如大眾點評網(wǎng)站它的讀寫比率往往是9比1。
12306或許是中國最著名的網(wǎng)站之一,我記得12306早期經(jīng)常出現(xiàn)一個問題就是用戶登錄老是登不上,甚至在高峰期整個網(wǎng)站掛掉,頁面顯示503網(wǎng)站拒絕訪問的問題,這個現(xiàn)象很好理解就是網(wǎng)站并發(fā)高了,大量人去登錄網(wǎng)站,購票,系統(tǒng)掛掉了,最后所有的人都不能使用網(wǎng)站了。當網(wǎng)站出現(xiàn)503拒絕訪問時候,那么這個網(wǎng)站就出現(xiàn)了最致命的問題,解決大用戶訪問的確是個超級難題,但是當高并發(fā)無法避免時候,整個網(wǎng)站都不能使用這個只能說網(wǎng)站設計上發(fā)生了致命錯誤,一個好的網(wǎng)站設計在應對超出自己能力的并發(fā)時候我們首先應該是不讓他掛掉,因為這種結果是誰都不能使用,我們希望那些在可接受的請求下,讓在可接受請求范圍內(nèi)的請求還是可以正常使用,超出的請求可以被拒絕,但是它們絕對不能影響到全網(wǎng)站的穩(wěn)定性,現(xiàn)在我們看到了12306網(wǎng)站的峰值從未減少過,而且是越變越多,但是12306出現(xiàn)全站掛掉的問題是越來越少了。通過12036網(wǎng)站改變我們更進一步思考下網(wǎng)站的瓶頸問題。
排除一些不可控的因素,網(wǎng)站在高并發(fā)下掛掉的原因90%都是因為數(shù)據(jù)庫不堪重負所致,而應用的瓶頸往往只有在解決了存儲瓶頸后才會暴露,那么我們要升級網(wǎng)站能力的第一步工作就是提升數(shù)據(jù)庫的承載能力,對于讀遠大于寫的網(wǎng)站我們采取的方式就是將數(shù)據(jù)庫從讀寫這個角度拆分,具體操作就是將數(shù)據(jù)庫讀寫分離,如下圖所示:
我們這時要設計兩個數(shù)據(jù)庫,一個數(shù)據(jù)庫主要負責寫操作我們稱之為主庫,一個數(shù)據(jù)庫專門負責讀操作我們稱之為副庫,副庫的數(shù)據(jù)都是從主庫導入的,數(shù)據(jù)庫的讀寫分離可以有效的保證關鍵數(shù)據(jù)的安全性,但是有個缺點就是當用戶瀏覽數(shù)據(jù)時候,讀的數(shù)據(jù)都會有點延時,這種延時比起全站不可用那肯定是可以接受的。不過針對12306的場景,僅僅讀寫分離還是遠遠不夠的,特別是負責讀操作的副庫,在高訪問下也是很容易達到性能的瓶頸的,那么我們就得使用新的解決方案:使用分布式緩存,不過緩存的缺點就是不能有效的實時更新,因此我們使用緩存前首先要對讀操作的數(shù)據(jù)進行分類,對于那些經(jīng)常不發(fā)生變化的數(shù)據(jù)可以事先存放到緩存里,緩存的訪問效率很高,這樣會讓讀更加高效,同時也減輕了數(shù)據(jù)庫的訪問壓力。至于用于寫操作的主庫,因為大部分網(wǎng)站讀寫的比例是嚴重失衡,所以讓主庫達到瓶頸還是比較難的,不過主庫也有一個讀的壓力就是主庫和副庫的數(shù)據(jù)同步問題,不過同步時候數(shù)據(jù)都是批量操作,而不是像請求那樣進行少量數(shù)據(jù)讀取操作,讀取操作特別多,因此想達到瓶頸還是有一定的難度的。聽人說,美國牛逼的facebook對數(shù)據(jù)的任何操作都是事先合并為批量操作,從而達到減輕數(shù)據(jù)庫壓力的目的。
上面的方案我們可以保證在高并發(fā)下網(wǎng)站的穩(wěn)定性,但是針對于讀,如果數(shù)據(jù)量太大了,就算網(wǎng)站不掛掉了,用戶能很快的在海量數(shù)據(jù)里檢索到所需要的信息又成為了網(wǎng)站的一個瓶頸,如果用戶需要很長時間才能獲得自己想要的數(shù)據(jù),很多用戶會失去耐心從而放棄對網(wǎng)站的使用,那么這個問題又該如何解決了?
解決方案就是我們經(jīng)常使用的百度,谷歌哪里得來,對于海量數(shù)據(jù)的讀我們可以采用搜索技術,我們可以將數(shù)據(jù)庫的數(shù)據(jù)導出到文件里,對文件建立索引,使用倒排索引技術來檢索信息,我們看到了百度,谷歌有整個互聯(lián)網(wǎng)的信息我們?nèi)稳荒芎芸斓臋z索到數(shù)據(jù),搜索技術是解決快速讀取數(shù)據(jù)的一個有效方案,不過這個讀取還是和數(shù)據(jù)庫的讀取有所區(qū)別的,如果用戶查詢的數(shù)據(jù)是通過數(shù)據(jù)庫的主鍵字段,或者是通過很明確的建立了索引的字段來檢索,那么數(shù)據(jù)庫的查詢效率是很高的,但是使用網(wǎng)站的人跟喜歡使用一些模糊查詢來查找自己的信息,那么這個操作在數(shù)據(jù)庫里就是個like操作,like操作在數(shù)據(jù)庫里效率是很低的,這個時候使用搜索技術的優(yōu)勢就非常明顯了,搜索技術非常適合于模糊查詢操作。
OK,很晚了,關于存儲的問題今天就寫在這里,下一篇我將接著這個主題講解,解決存儲問題是很復雜的,下篇我盡量講仔細點。
#p#
上篇里我講到某些網(wǎng)站在高并發(fā)下會報出503錯誤,503錯誤的含義是指網(wǎng)站服務端暫時無法提供服務的含義,503還表達了網(wǎng)站服務端現(xiàn)在有問題但是以后可能會提供正常的服務,對http協(xié)議熟悉的人都知道,5開頭的響應碼表達了服務端出現(xiàn)了問題,在我們開發(fā)測試時候最為常見的是500錯誤,500代表的含義是服務端程序出現(xiàn)了錯誤導致網(wǎng)站無法正常提供服務,500通常是服務端異常和錯誤所致,如果生產(chǎn)系統(tǒng)里發(fā)現(xiàn)了500錯誤,那么只能說明網(wǎng)站存在邏輯性的錯誤,這往往是系統(tǒng)上線前的測試做的不到位所致?;氐?03錯誤,我上文解釋為拒絕訪問,其實更加準確的回答應該是服務不可用,那么為什么我會說503錯誤在高并發(fā)的情況下90%的原因是數(shù)據(jù)庫所致呢?上文我做出了詳細的解釋,但是今天我回味了一下,發(fā)現(xiàn)那個解釋還不是太突出重點,問題的重點是在高并發(fā)的情況整個網(wǎng)站系統(tǒng)首先暴露出問題的是數(shù)據(jù)庫,如果我們把整個網(wǎng)站系統(tǒng)比作一個盛水的木桶,那么木桶最短的那個板就是數(shù)據(jù)庫了,一般而言網(wǎng)站的服務應用出問題都會是解決存儲問題之后才會出現(xiàn)。
數(shù)據(jù)庫出現(xiàn)了瓶頸并不是程序存在邏輯性錯誤,數(shù)據(jù)庫瓶頸的表現(xiàn)就是數(shù)據(jù)庫因為承受了太多的訪問后,數(shù)據(jù)庫無法迅速的做出響應,嚴重時候數(shù)據(jù)庫會拒絕進一步操作死鎖在哪里不能做出任何反應。數(shù)據(jù)庫猶如一把巨型的大鎖,很多人爭搶這個鎖時候會導致這個大鎖完全被鎖死,最終請求的處理就停留在這個大鎖上最終導致網(wǎng)站提示出503錯誤,503錯誤最終會傳遞到所有的客戶端上,最終的現(xiàn)象就是全站不可用了。
上文里我講到session共享的一個方案是將session數(shù)據(jù)存儲在外部一個獨立的緩存服務器里,我開始說用一臺服務器做緩存服務器,后面提到如果覺得一臺服務器做緩存不安全,那么采用分布式緩存服務器例如memcached,那么這里就有一個問題了,為了保證web服務的可用性,我們會把web服務分開部署到不同的服務器上,這些服務器都是對等關系,其中一臺服務器不能正常提供服務不會影響到整個網(wǎng)站的穩(wěn)定性,那么我們采取memcached集群是不是可以達到同樣的效果了?即緩存服務器集群中一臺服務器掛掉,不會影響到用戶對網(wǎng)站的使用了?問題的答案是令人失望了,假如我們使用兩臺服務器做緩存服務器來存儲session信息,那么如果其中一臺服務器掛掉了,那么網(wǎng)站將會有一半的用戶將不能正常使用網(wǎng)站,原因是他們的session信息丟失了,網(wǎng)站無法正常的跟蹤用戶的會話狀態(tài)。我之所以提到這個問題是想告訴大家以memcached為代表的分布式緩存和我們傳統(tǒng)理解的分布式系統(tǒng)是有區(qū)別的,傳統(tǒng)的分布式系統(tǒng)都會包含一個容災維護系統(tǒng)穩(wěn)定性的功能,但實際的分布式技術是多種多樣的,例如memcached的分布式技術并不是為了解決容災維護系統(tǒng)穩(wěn)定性的模式設計,換個說法就是memcached集群的設計是沒有過分考慮冗余的問題,而只有適當?shù)娜哂嗖拍鼙WC系統(tǒng)的健壯性問題。分布式技術的實現(xiàn)是千差萬別的,每個優(yōu)秀的分布式系統(tǒng)都有自身獨有的特點。
全面的講述memcached技術并非本文的主題,而且這個主題也不是一兩句話能說清楚的,這里我簡單的介紹下memcached實現(xiàn)的原理,當網(wǎng)站使用緩存集群時候,緩存數(shù)據(jù)是通過一定的算法將緩存數(shù)據(jù)盡量均勻分不到不同服務器上,如果用戶A的緩存在服務器A上,那么服務器B上是沒有該用戶的緩存數(shù)據(jù),早期的memcache數(shù)據(jù)分布式的算法是根據(jù)緩存數(shù)據(jù)的key即鍵值計算出一個hash值,這個hash值再除以緩存服務器的個數(shù),得到的余數(shù)會對應某一臺服務器,例如1對應服務器A,2對應服務器B,那么余數(shù)是1的key值緩存就會存儲在服務器A上,這樣的算法會導致某一臺服務器掛掉,那么網(wǎng)站損失的緩存數(shù)據(jù)的占比就會比較高,為了解決這個問題,memcached引入了一致性hash算法。關于一致性hash網(wǎng)上有很多資料,這里我就貼出一個鏈接,本文就不做過多論述了。
一致性hash可以服務器宕機時候這臺服務器對整個緩存數(shù)據(jù)的影響最小。
上文里我講到了讀寫分離的設計方案,而讀寫分離方案主要是應用于網(wǎng)站讀寫比例嚴重失衡的網(wǎng)站,而互聯(lián)網(wǎng)上絕大部分網(wǎng)站都是讀操作的比例遠遠大于寫操作,這是網(wǎng)站的主流,如果一個網(wǎng)站讀寫比例比較均衡,那么這個網(wǎng)站一般都是提供專業(yè)服務的網(wǎng)站,這種網(wǎng)站對于個人而言是一個提供生活便利的工具,它們和企業(yè)軟件類似。大部分關注大型網(wǎng)站架構技術關心的重點應該是那種對于讀寫比例失衡的網(wǎng)站,因為它們做起來更加有挑戰(zhàn)性。
將數(shù)據(jù)庫進行讀寫分離是網(wǎng)站解決存儲瓶頸的第一步,為什么說是第一步呢?因為讀寫分離從業(yè)務角度而言它是一種粗粒度的數(shù)據(jù)拆分,因此它所包含的業(yè)務復雜度比較低,容易操作和被掌控,從技術而言,實現(xiàn)手段也相對簡單,因此讀寫分離是一種低成本解決存儲瓶頸的一種手段,這種方案是一種改良方案而不是革命性的的方案,不管是從難度,還是影響范圍或者是經(jīng)濟成本角度考慮都是很容易讓相關方接受的。
那么我們僅僅將數(shù)據(jù)庫做讀寫分離為何能產(chǎn)生好的效率了?回答這個問題我們首先要了解下硬盤的機制,硬盤的物理機制就有一個大圓盤飛速旋轉(zhuǎn),然后有個磁頭不斷掃描這個大圓盤,這樣的物理機制就會導致硬盤數(shù)據(jù)的順序操作比隨機操作效率更高,這點對于硬盤的讀和寫還算公平,但是寫操作在高并發(fā)情況下會有點復雜,寫操作有個特性就是我們要保證寫操作的準確性,但是高并發(fā)下可能會出現(xiàn)多個用戶同時修改某一條數(shù)據(jù),為了保證數(shù)據(jù)能被準確的修改,那么我們通常要把并行的操作轉(zhuǎn)變?yōu)榇胁僮鳎?/strong>這個時候就會出現(xiàn)一個鎖機制,鎖機制的實現(xiàn)是很復雜的,它會消耗很多系統(tǒng)性能,如果寫操作摻雜了讀操作情況就更復雜,效率會更加低效,相對于寫操作讀操作就單純多了,如果我們的數(shù)據(jù)只有讀操作,那么讀的性能也就是硬盤順序讀能力和隨機讀能力的體現(xiàn),即使摻雜了并發(fā)也不會對其有很大的影響,因此如果把讀操作和寫操作分離,效率自然會得到很大提升。
既然讀寫分離可以提升存儲系統(tǒng)的效率,那么為什么我們又要引入緩存系統(tǒng)和搜索技術了?緩存將數(shù)據(jù)存在內(nèi)存中,內(nèi)存效率是硬盤的幾萬倍,這樣的好處不言而喻,而選擇搜索技術的背后的原理就不同了,數(shù)據(jù)庫存儲的數(shù)據(jù)稱之為結構化數(shù)據(jù),結構化數(shù)據(jù)的限制很多,當結構化數(shù)據(jù)遇到了千變?nèi)f化的隨機訪問時候,其效率會變得異常低效,但是如果一個網(wǎng)站不能提供靈活、高效的隨機訪問能力,那么這個網(wǎng)站就會變得單板沒有活力,例如我們在淘寶里查找我們想要的商品,但是時常我們并不清楚自己到底想買啥,如果是在實體店里店員會引導我們的消費,但是網(wǎng)站又如何引導我們的消費,那么我們必須要賦予網(wǎng)站通過人們簡單意向隨機找到各種不同的商品,這個對于數(shù)據(jù)庫就是一個like操作的,但是數(shù)據(jù)里數(shù)據(jù)量達到了一定規(guī)模以后like的低效是無法讓人忍受的,這時候搜索技術在隨機訪問的能力正好可以彌補數(shù)據(jù)庫這塊的不足。
業(yè)務再接著的增長下去,數(shù)據(jù)量也會隨之越來越大了,這樣發(fā)展下去總有一天主庫也會產(chǎn)生瓶頸了,那么接下來我們又該如何解決主庫的瓶頸了?方法很簡單就是我們要拆分主庫的數(shù)據(jù)了,那么我該以什么維度拆分數(shù)據(jù)了?一個數(shù)據(jù)庫里有很多張表,不同的表都針對不同的業(yè)務,網(wǎng)站的不同業(yè)務所帶來的數(shù)據(jù)量也不是不同的,這個時候系統(tǒng)的短板就是那些數(shù)據(jù)量最大的表,所以我們要把那些會讓數(shù)據(jù)庫產(chǎn)生瓶頸的表拆出來,例如電商系統(tǒng)里商品表和交易表往往數(shù)據(jù)量非常大,那么我們可以把這兩種表建立在單獨的兩個數(shù)據(jù)庫里,這樣就拆分了數(shù)據(jù)庫的壓力,這種做法叫做數(shù)據(jù)垂直拆分,不過垂直拆分會給原有的數(shù)據(jù)庫查詢,特別是有事務的相關操作產(chǎn)生影響,這些問題我們必須要進行改造,關于這個問題,我將在下篇里進行討論。
當我們的系統(tǒng)做完了讀寫分離,數(shù)據(jù)垂直拆分后,我們的網(wǎng)站還在迅猛發(fā)展,最終一定又會達到新的數(shù)據(jù)庫瓶頸,當然這些瓶頸首先還是出現(xiàn)在那些數(shù)據(jù)量大的表里,這些表數(shù)據(jù)的處理已經(jīng)超出了單臺服務器的能力,這個時候我們就得對這個單庫單表的數(shù)據(jù)進行更進一步的拆分,也就是將一張表分布到兩臺不同的數(shù)據(jù)庫里,這個做法就是叫做數(shù)據(jù)的水平拆分了。
Ok,今天內(nèi)容就講到這里了,有這兩篇文章我們可以理出一個解決大型網(wǎng)站數(shù)據(jù)瓶頸的一個脈絡了,具體如下:
單庫數(shù)據(jù)庫-->數(shù)據(jù)庫讀寫分離-->緩存技術-->搜索技術-->數(shù)據(jù)的垂直拆分-->數(shù)據(jù)的水平拆分
以上的每個技術細節(jié)在具體實現(xiàn)中可能存在很大的不同,但是問題的緣由大致是一致的,我們理清這個脈絡就是想告訴大家我們?nèi)绻龅竭@樣的問題應該按何種思路進行思考和設計解決方案,好了,今天就寫到這里了,晚安。
#p#
存儲的瓶頸寫到現(xiàn)在就要進入到深水區(qū)了,如果我們所做的網(wǎng)站已經(jīng)到了做數(shù)據(jù)庫垂直拆分和水平拆分的階段,那么此時我們所面臨的技術難度的挑戰(zhàn)也會大大增強。
這里我們先回顧下數(shù)據(jù)庫的垂直拆分和水平拆分的定義:
垂直拆分:把一個數(shù)據(jù)庫中不同業(yè)務單元的數(shù)據(jù)分到不同的數(shù)據(jù)庫里。
水平拆分:是根據(jù)一定的規(guī)則把同一業(yè)務單元的數(shù)據(jù)拆分到多個數(shù)據(jù)庫里。
垂直拆分是一個粗粒度的拆分數(shù)據(jù),它主要是將原來在一個數(shù)據(jù)庫下的表拆分到不同的數(shù)據(jù)庫里,水平拆分粒度比垂直拆分要更細點,它是將一張表拆到不同數(shù)據(jù)庫里,粒度的粗細也會導致實現(xiàn)技術的難度的也不一樣,很明顯水平拆分的技術難度要遠大于垂直拆分的技術難度。難度意味著投入的成本的增加以及我們需要承擔的風險的加大,我們做系統(tǒng)開發(fā)一定要有個清晰的認識:能用簡單的方案解決問題,就一定要毫不猶豫的舍棄復雜的方案,當系統(tǒng)需要使用高難度技術的時候,我們一定要讓自己感受到這是迫不得已的。
我是以java工程師應聘進了我現(xiàn)在的公司,所以在我轉(zhuǎn)到專職前端前,我也做過不少java的應用開發(fā),當時我在公司的前輩告訴我,我們公司的數(shù)據(jù)庫建模很簡單,怎么個簡單法了,數(shù)據(jù)庫的表之間都沒有外鍵,數(shù)據(jù)庫不準寫觸發(fā)器,可以寫寫存儲過程,但是存儲過程決不能用于處理生產(chǎn)業(yè)務邏輯,而只能是一些輔助工作,例如導入導出寫數(shù)據(jù)啊,后面聽說就算是數(shù)據(jù)庫做到了讀寫分離,數(shù)據(jù)之間同步也最好是用java程序做,也不要使用存儲過程,除非迫不得已。開始我還不太理解這些做法,這種不理解不是指我質(zhì)疑了公司的做法,而是我在想如果一個數(shù)據(jù)庫我們就用了這么一點功能,那還不如讓數(shù)據(jù)庫公司為咋們定制個閹割版算了,不過在我學習了hadoop之后我有點理解這個背后的深意了,其實作為存儲數(shù)據(jù)的數(shù)據(jù)庫,它和我們開發(fā)出的程序的本質(zhì)是一樣的那就是:存儲和計算,那么當數(shù)據(jù)庫作為一個業(yè)務系統(tǒng)的存儲介質(zhì)時候,那么它的存儲對業(yè)務系統(tǒng)的重要性要遠遠大于它所能承擔的計算功能,當數(shù)據(jù)庫作為互聯(lián)網(wǎng)系統(tǒng)的存儲介質(zhì)時候,如果這個互聯(lián)網(wǎng)系統(tǒng)成長迅速,那么這個時候我們對數(shù)據(jù)庫存儲的要求就會越來越高,最后估計我們都想把數(shù)據(jù)庫的計算特性給閹割掉,當然數(shù)據(jù)庫基本的增刪改查我們是不能舍棄的,因為它們是數(shù)據(jù)庫和外界溝通的入口,我們?nèi)绻佑|過具有海量數(shù)據(jù)的數(shù)據(jù)庫,我們會發(fā)現(xiàn)讓數(shù)據(jù)庫運行的單個sql語句都會變得異常簡潔和簡單,因為這個時候我們知道數(shù)據(jù)庫已經(jīng)在存儲這塊承擔了太多的負擔,那么我們能幫助數(shù)據(jù)庫的手段只能是盡量降低它運算的壓力。
回到關于數(shù)據(jù)庫垂直拆分和水平拆分的問題,假如我們的數(shù)據(jù)庫設計按照我們公司業(yè)務數(shù)據(jù)庫為藍本的話,那么數(shù)據(jù)庫進行了水平拆分我們會碰到什么樣的問題了?為了回答這個問題我就要比較下拆分前和拆分后會給調(diào)用數(shù)據(jù)庫的程序帶來怎樣的不同,不同主要是兩點:
第一點:被拆出的表和原庫的其他表有關聯(lián)查詢即使用join查詢的操作需要進行改變;
第二點:某些增刪改(注意:一般業(yè)務庫設計很少使用物理刪除,因為這個操作十分危險,這里的刪往往是邏輯刪除,一般做法就是更新下記錄的狀態(tài),本質(zhì)是一個更新操作)牽涉到拆分的表和原庫其他表共同完成,那么該操作的事務性就會被打破,如果處理不好,假如碰到操作失敗,業(yè)務無法做到回滾,這會對業(yè)務操作的安全性帶來極大的風險。
關于解決第一點的問題還是相對比較簡單的,方式方法也很多,下面我來講講我所知道的一些方法,具體如下:
方法一:在垂直拆表時候,我們先梳理下使用到join操作sql查詢,梳理的維度是以被拆分出的表為原點,如果是弱依賴的join表我們改寫下sql查詢語句,如果是強依賴的join表則隨拆分表一起拆分,這個方法很簡單也很可控,但是這個技術方案存在一個問題,就是讓拆分粒度變大,拆分的業(yè)務規(guī)則被干擾,這么拆分很容易犯一個問題就是一個數(shù)據(jù)庫里總會存在這樣一些表,就是很多數(shù)據(jù)庫都會和它關聯(lián),我們很難拆解這些關聯(lián)關系,當我們無法理清時候就會把該表做冗余,即不同數(shù)據(jù)庫存在雷同表,隨著業(yè)務增長,這種表的數(shù)據(jù)同步就成為了數(shù)據(jù)庫的一個軟肋,最終它會演變?yōu)檎麄€數(shù)據(jù)庫系統(tǒng)的短板甚至是全系統(tǒng)的短板。
方法二:我們拆表的準則還是按業(yè)務按需求在數(shù)據(jù)庫層面進行,等數(shù)據(jù)庫拆好后,再改寫原來受到影響的join查詢語句,這里我要說明的是查詢語句修改的成本很低,因為查詢操作是個只讀操作,它不會改變?nèi)魏蔚讓拥臇|西,如果數(shù)據(jù)表跨庫,我們可以把join查詢拆分為多次查詢,最后將查詢結果在內(nèi)存中歸納和合并,其實我們?nèi)绻鲃硬饚?,絕不會把換個不同的數(shù)據(jù)庫產(chǎn)品建立新庫,肯定是使用相同數(shù)據(jù)庫,同類型的數(shù)據(jù)庫基本都支持跨庫查詢,不過跨庫查詢聽說效率不咋地,我們可以有選擇的使用。這種方案也有個致命的缺點,我們做數(shù)據(jù)庫垂直拆分絕不可能一次到位,一般都是多次迭代,而該方案的影響面很大,關聯(lián)方過多,每次拆表幾乎要檢查所有相關的sql語句,這會導致系統(tǒng)不斷累積不可預知的風險。
以下三段內(nèi)容是方法三:
不管是方法一還是方法二,都有一個很根本的缺陷就是數(shù)據(jù)庫和上層業(yè)務操作耦合度很高,每次數(shù)據(jù)庫的變遷都導致業(yè)務開發(fā)跟隨做大量的同步工作,這樣的后果就是資源浪費,做服務的人不能天天被數(shù)據(jù)庫牽著鼻子走,這樣業(yè)務系統(tǒng)的日常維護和業(yè)務擴展會很存問題,那么我們一定要有一個服務和數(shù)據(jù)庫解耦方案,那么這里我們就得借鑒ORM技術了。(這里我要說明下,方法一和方法二我都是以修改sql闡述的,在現(xiàn)實開發(fā)里很多系統(tǒng)會使用ORM技術,互聯(lián)網(wǎng)一般用ibatis和mybatis這種半ORM的產(chǎn)品,因為它們可以直接寫sql和數(shù)據(jù)庫最為親近,如果使用hibernate則就不同了,但是hibernate雖然大部分不是直接寫sql,但是它只不過是對數(shù)據(jù)庫操作做了一層映射,本質(zhì)手段是一致,所以上文的sql可以算是一種指代,它也包括ORM里的映射技術)
傳統(tǒng)的ORM技術例如hibernate還有mybatis都是針對單庫進行的,并不能幫我們解決垂直拆分的問題,因此我們必須自己開發(fā)一套解決跨庫操作的ORM系統(tǒng),這里我只針對查詢的ORM談談自己的看法(講到這里是不是有些人會有種似成相識的感覺,這個不是和分布式系統(tǒng)很像嗎)。
其實具體怎么重構有問題的sql不是我想討論的問題,因為這是個技術手段或者說是一個技術上的技巧問題,我這里重點講講這個ORM與服務層接口的交互,對于服務層而言,服務層最怕的就是被數(shù)據(jù)庫牽著鼻子走,因為當數(shù)據(jù)庫要進行重大改變時候,服務層總是想方設法讓自己不要發(fā)生變化,對于數(shù)據(jù)庫層而言服務層的建議都應該是合理,數(shù)據(jù)庫層要把服務層當做自己的需求方,這樣雙方才能齊心協(xié)力完成這件重要的工作,那么服務層一般是怎樣和數(shù)據(jù)庫層交互的呢?
從傳統(tǒng)的ORM技術我們可以找到答案,具體的方式有兩種:
第一種:以hibernate為代表的,hibernate框架有一套自己的查詢語言就是hql,它類似于sql,自定義一套查詢語言看起來很酷,也非常靈活,但是實現(xiàn)難度非常之高,因為這種做法相當于我們要自己編寫一套新的編程語言,如果這個語言設計不好,使用者又理解不深入,最后往往會事與愿違,就像hibernate的hql,我們經(jīng)常令可直接使用sql也不愿意使用hql,這其中的緣由用過的人一定很好理解的。
第二種:就是數(shù)據(jù)層給服務層提供調(diào)用方法,每個方法對應一個具體的數(shù)據(jù)庫操作,就算底層數(shù)據(jù)庫發(fā)生重大變遷,只要提供給服務端的方法定義不變,那么數(shù)據(jù)庫的變遷對服務層影響度也會最低。
前面我提到技術難度是我們選擇技術的一個重要指標,相比之下第二種方案將會是我們的首選。
垂直拆分數(shù)據(jù)庫還會帶來另一個問題就是對事務的影響,垂直拆分數(shù)據(jù)庫會導致原來的事務機制變成了分布式事務,解決分布式事務問題是非常難的,特別是如果我們想使用業(yè)界推出的解決分布式事務方案,那么要自己實現(xiàn)個分布式事務就更難了,不過這里我要說明一下,我這里說的更難是和我寫本文有關,我本篇文章之所以現(xiàn)在才寫是因為我想先研究下業(yè)界推出的分布式解決方案,但是這些方案的原理看得我很沮喪,我就想如果我們直接用方案的接口實現(xiàn)了它,因為還是不懂他的很多原理,那么這些方案其實就是不可控方案,說不定使用過多就會給系統(tǒng)埋下定時炸彈,因此這里我就只提提這些方案,有興趣的童鞋可以去研究下:
一、X/OPEN組織推出的分布式事務規(guī)范XA,其中還包括該組織定義的分布式事務處理模型X/OPEN;
二、大型網(wǎng)站一致性理論CAP/BASE
三、 PAXOS協(xié)議。
這里特別要提的是PAXOS協(xié)議,我以前寫過好幾篇關于zookeeper的文章,zookeeper框架有一個特性就是它本身是一個分布式文件系統(tǒng),當我們往zookeeper寫數(shù)據(jù)時候,zookeeper集群能保證我們的寫操作的可靠性,這個可靠性和我們使用線程安全來控制寫數(shù)據(jù)一樣,絕對不會讓寫操作出錯,之所以zookeeper能做到這點,是因為zookeeper內(nèi)部有一個類似PAXOS協(xié)議的協(xié)議,這個協(xié)議類似一個選舉方案,它能保證寫入操作的原子性。
其實事務也是和線程安全技術類似,只不過事務是要保證一個業(yè)務操作的原子性問題,當然事務還要有個特點就是回滾機制即業(yè)務操作失敗,事務可以保證系統(tǒng)恢復到業(yè)務操作前的狀態(tài),回滾機制的本質(zhì)其實是維護業(yè)務操作的狀態(tài)性,具體點我這里列舉個例子:當系統(tǒng)將要執(zhí)行一個業(yè)務操作時候,我們首先為業(yè)務系統(tǒng)定義一個初始狀態(tài),業(yè)務執(zhí)行操作時候我們可以定義一個執(zhí)行狀態(tài),操作成功就是一個成功狀態(tài),操作失敗就是一個操作失敗狀態(tài),如果業(yè)務操作是失敗狀態(tài),我們可以讓業(yè)務回滾到初始狀態(tài),更進一步如果執(zhí)行狀態(tài)超時也可以將整個業(yè)務狀態(tài)回退到初始狀態(tài),其實所有事務回滾機制的本質(zhì)基本都是如此。記得不久前,在群里有個群友就問大家如何實現(xiàn)分布式事務,他想要知道的分布式事務是有沒有一種技術能像我們操作數(shù)據(jù)庫或者是jdbc那樣一個commit,一個rollback就搞定,但是現(xiàn)實中的分布式事務比commit和rollback復雜的多,不可能簡單的讓我們寫幾個標記就能實現(xiàn)分布式事務,當然業(yè)界是有方案的,就是我上面提到的,如果有人真想知道可以自己研究下,不過我本人現(xiàn)在還是不太懂上面這些技術的原理和思想。
其實當時我馬上給那位群友一個解答,我說我們開發(fā)時候是經(jīng)常碰到分布式事務,但是我們解決分布式事務大多數(shù)從業(yè)務角度來解決的,而沒去選擇純技術手段,因為技術手段太復雜難以控制。這個答案可能不會令提問者滿意,但是我現(xiàn)在還是堅持這個觀點,這個觀點符合我提到的原則,當技術方案難度過高,我們就不要輕易選擇使用它,因為這么做是很危險的,今天我就舉個例子吧,這樣可能更有說服力。我現(xiàn)在做的系統(tǒng)很多業(yè)務操作經(jīng)常要和其他系統(tǒng)共同完成,其他系統(tǒng)有我們公司自己的系統(tǒng),也有其他企業(yè)的系統(tǒng),這里我還是把業(yè)務操作比作一輛在高速公路的汽車,那么每個系統(tǒng)就是高速公路上的一個收費站,業(yè)務每到一個收費站,該系統(tǒng)的數(shù)據(jù)庫就會在對應的數(shù)據(jù)庫的某張表里某條記錄上記錄一個狀態(tài),當汽車跑完全程,各個收費站就會相互通知,告訴大家任務完成,最終將所有的狀態(tài)置為已完成,如果失敗,就廢掉這輛汽車,收費站之間也會相互通知,讓所有的記錄狀態(tài)回歸到初始狀態(tài),就當從來沒有這輛汽車來過。這個做法的原理就是使用了事務回滾的本質(zhì),狀態(tài)的變遷和回退,這個做法在業(yè)務系統(tǒng)開發(fā)里也有個專有術語就是工作流。其實大多數(shù)問如何實現(xiàn)分布式事務如何實現(xiàn)的問題的本質(zhì)就是想解決事務的回滾問題,我們其實不要被這個分布式事務的名字給嚇住了,其實有很多不起眼的技術手段和業(yè)務手段都能達到相同的目的。
晚上11點了,看來本文今天寫不完了,今天就到此為止,最后我要總結下本文的內(nèi)容,具體如下:
1. 大型網(wǎng)站解決存儲瓶頸的問題,我們要找準存儲這個關鍵點,因為數(shù)據(jù)庫其實是存儲和運算的組合體,但是在我們這個場景下,存儲是第一位的,當存儲是瓶頸時候我們要狠下心來盡量多的拋棄數(shù)據(jù)的計算特點,所以上文中我提出我們數(shù)據(jù)庫就不要濫用計算功能了例如觸發(fā)器、存儲過程等等。
2. 數(shù)據(jù)庫剝離計算功能不代表不要數(shù)據(jù)的計算功能,因為沒有數(shù)據(jù)的計算功能數(shù)據(jù)庫也就沒價值了,那么我們要將數(shù)據(jù)庫的計算功能進行遷移,遷移到程序里面,一般大型系統(tǒng)程序和數(shù)據(jù)庫都是分開部署到不同服務器上,因此程序里處理數(shù)據(jù)計算就不會影響到數(shù)據(jù)庫所在服務器的性能,就可以讓安裝數(shù)據(jù)庫的服務器專心服務于存儲。
3. 我們要盡一切可能的把數(shù)據(jù)庫的變化對服務層的影響降到最低,最好是數(shù)據(jù)庫做拆分后,現(xiàn)有業(yè)務不要任何的更改,那么我們就得設計一個全新的數(shù)據(jù)訪問層,這個數(shù)據(jù)訪問層將數(shù)據(jù)庫和服務層進行解耦,任何數(shù)據(jù)庫的變化都由數(shù)據(jù)訪問層消化,數(shù)據(jù)訪問層對外接口要高度統(tǒng)一,不要輕易改變。
4. 如果我們設計了數(shù)據(jù)訪問層來解決數(shù)據(jù)庫拆分的問題,數(shù)據(jù)訪問層加上數(shù)據(jù)庫其實就組合出了一個分布式數(shù)據(jù)庫的解決方案,由此可見拆分數(shù)據(jù)庫的難度是很高的,因為數(shù)據(jù)庫將擁有分布式的特性,而分布式開發(fā)就意味開發(fā)難度的增加。
5. 對于分布式事務的處理,我們盡量要從具體問題具體分析,不要一感覺這個事務操作本質(zhì)是分布式事務就去尋找通用的分布式事務技術手段,這樣的想法其實是回避困難的思想,結果可能會是把問題搞得更加復雜。
好了,今天就寫到這里吧,祝大家晚安,生活愉快!
本文出自:http://www.cnblogs.com/sharpxiajun/