前不久公司請來了位互聯網界的技術大牛跟咱們作了一次大型網站架構的培訓,兩天12個小時信息量很是大,知識的廣度和難度也很是大,培訓完後我很難 完整理出所有聽到的知識,今天我換了個思路是回味此次培訓,這個思路就是經過本人目前的經驗和技術水平來思考下大型網站技術演進的過程。php
大型網站定義html
首先咱們要思考一個問題,什麼樣的網站纔是大型網站,從網站的技術指標角度考慮這個問題人們很容易犯一個毛病就是認爲網站的訪問量是衡量的指標,懂點 行的人也許會認爲是網站在單位時間裏的併發量的大小來做爲指標,若是按這些標準那麼像hao123這樣的網站就是大型網站了,以下圖所示:前端
其實這種網站訪問量很是大,併發數也很是高,可是它卻能用最爲簡單的Web技術來實現:咱們只要保持網站的充分的靜態化,多部署幾臺服務器,那麼就算地球上全部人都用它,網站也能正常運行。java
大型網站是技術和業務的結合,一個知足某些用戶需求的網站只要技術和業務兩者有一方難度很大,必然會讓企業投入更多的、更優秀的人力成本實現它,那麼這樣的網站就是所謂的大型網站了。node
服務器部署mysql
一個初建的網站每每用戶羣都是很小的,最簡單的網站架構就能解決實際的用戶需求,固然爲了保證網站的穩定性和安全性,咱們會把網站的應用部署到至少兩 臺機器上,後臺的存儲使用數據庫,若是經濟實力容許,數據庫使用單臺服務器部署,因爲數據是網站的生命線,所以咱們經常會把部署數據庫的服務器使用的好 點,這個網站結構以下所示:程序員
這個結構很是簡單,其實大部分初建網站開發裏每每業務邏輯沒有企業級系統那麼複雜,因此只要有個好的idea,建設一個新網站的成本是很是低的,所使用的技術手段也是很是的基本和簡單。web
咱們要準備三臺服務器,並且還要租個機房放置咱們的服務器,這些成本對於草根和屌絲仍是很是高的,幸運的是當下不少大公司和機構提供了雲平臺,咱們可 以花費不多的錢將本身的應用部署到雲平臺上,這種作法咱們甚至不用去考慮把應用、數據庫分開部署的問題,更加進一步的下降了網站開發和運維的成本,可是這 種作法也有一個問題,就是網站的小命被這個雲平臺捏住了,若是雲平臺掛了,俺們的網站服務也就跟着掛了。算法
這裏我先講講本身獨立使用服務器部署網站的問題,若是咱們要把網站服務應用使用多臺服務器部署,這麼作的目的通常有兩個:sql
不過要作到以上兩點,並非咱們簡單將網站分開部署就能夠知足的,由於大多數網站在用戶使用時候都是要保持用戶的狀態,具體點就是網站要記住請求是歸屬到那一個客戶端,而這個狀態在網站開發裏就是經過會話session來體現的。
Session機制
分開部署的Web應用服務要解決的一個首要問題就是要保持不一樣物理部署服務器之間的session同步問題,從而達到當用戶第一次請求訪問到服務器A,第二個請求訪問到服務器B,網站任然知道這兩個請求是同一我的,解決方案很直接:服務器A和服務器B上的session信息要時刻保持同步,那麼如何保證兩臺服務器之間session信息的同步呢?
爲了回答上面的問題,咱們首先要理解下session的機制,session信息在Web容器裏都是存儲在內存裏的,Web容器會給每一個鏈接它的客戶 端生成一個sessionid值,這個sessionid值會被Web容器置於http協議裏的cookie域下,當響應被客戶端處理後,客戶端本地會存 儲這個sessionid值,用戶之後的每一個請求都會讓這個sessionid值隨cookie一塊兒傳遞到服務器,服務器經過sessionid找到內存 中存儲的該用戶的session內容,session在內存的數據結構是一個map的格式。
那麼爲了保證不一樣服務器之間的session共享,那麼最直接的方案就是讓服務器之間session不斷的傳遞和複製,例如java開發裏經常使用的 tomcat容器就採用這種方案,之前我測試過tomcat這種session同步的性能,我發現當須要同步的Web容器越多,Web應用所能承載的併發 數並無由於服務器的增長而線性提高,當服務器數量達到一個臨界值後,整個Web應用的併發數甚至還會降低,爲何會這樣了?
緣由很簡單,不一樣服務器之間session的傳遞和複製會消耗服務器自己的系統資源,當服務器數量越大,消耗的資源越多,當用戶請求越頻繁,系統消耗 資源也會愈來愈大。若是咱們多部署服務器的目的只是想保證系統的穩定性,採用這種方案仍是不錯的,不過web應用最好部署少點,這樣纔不會影響到web應 用的性能問題,若是咱們還想提高網站的併發量那麼就得采起其餘的方案了。
Session案例解析
時下使用的比較多的方案就是使用獨立的緩存服務器,也就是將session的數據存儲在一臺獨立的服務器上,若是以爲存在一臺服務器不安全,那麼能夠使用memcached這樣的分佈式緩存服務器進行存儲,這樣既能夠知足了網站穩定性問題也提高了網站的併發能力。
不過早期的淘寶在這個問題解決更加巧妙,他們將session的信息直接存儲到瀏覽器的cookie裏,每次請求cookie信息都會隨着http一 起傳遞到web服務器,這樣就避免了Web服務器之間session信息同步的問題,這種方案會讓不少人詬病,詬病的緣由是cookie的不安全性是總所 周知的,若是有人惡意截取cookie信息那麼網站不就不安全了嗎?這個答案還真很差說,可是我以爲咱們僅僅是跟蹤用戶的狀態,把session存在 cookie裏其實也沒什麼大不了的。
其實如此專業的淘寶這麼作其實仍是頗有深意的,還記得本文開篇提到的hao123網站,它是能夠承載高併發的網站,它之因此能夠作到這一點,緣由很簡 單它是個靜態網站,靜態網站的特色就是不須要記錄用戶的狀態,靜態網站的服務器不須要使用寶貴的系統資源來存儲大量的session會話信息,這樣它就有 更多系統資源來處理請求,而早期淘寶將cookie存在客戶端也是爲了達到這樣的目的,因此這個方案在淘寶網站架構裏仍是使用了很長時間的。
在個人公司裏客戶端的請求到達Web服務器以前,會先到F5,F5是一個用來作負載均衡的硬件設備,它的做用是將用戶請求均勻的分發到後臺的服務器集 羣,F5是硬件的負載均衡解決方案,若是咱們沒那麼多錢買這樣的設備,也有軟件的負載均衡解決方案,這個方案就是大名鼎鼎的LVS了。
這些負載均衡設備除了能夠分發請求外它們還有個能力,這個能力是根據http協議的特色設計的,一個http請求從客戶端到達最終的存儲服務器以前可 能會通過不少不一樣的設備,若是咱們把一個請求比做高速公路上的一輛汽車,這些設備也能夠叫作這些節點就是高速路上的收費站,這些收費站都能根據本身的需求 改變http報文的內容,因此負載均衡設備能夠記住每一個sessionid值對應的後臺服務器,當一個帶有sessionid值的請求經過負載均衡設備時 候,負載均衡設備會根據該sessionid值直接找到指定的web服務器,這種作法有個專有名詞就是session粘滯,這種作法也比那種 session信息在不一樣服務器之間拷貝複製要高效,不過該作法仍是比存cookie的效率低下,並且對於網站的穩定性也有必定影響即若是某臺服務器掛掉 了,那麼鏈接到該服務器的用戶的會話都會失效。
解決session的問題的本質也就是解決session的存儲問題,其本質也就是解決網站的存儲問題,一個初建的網站在早期的運營期須要解決的問題基本都是由存儲致使的。
上文裏我提到時下不少新建的Web應用會將服務器部署後雲平臺裏,好的雲平臺裏或許會幫助咱們解決負載均衡和session同步的問題,可是雲平臺裏 有個問題很難解決那就是數據庫的存儲問題,若是咱們使用的雲平臺發生了重大事故,致使雲平臺存儲的數據丟失,這種會不會致使咱們在雲平臺裏數據庫的信息也 會丟失了,雖然這個事情的機率不高,可是發生這種事情的概率仍是有的,雖然不少雲平臺都聲稱本身多麼可靠,可是真實可靠性有多高不是局中人還真不清楚哦, 所以使用雲平臺咱們首要考慮的就是要作好數據備份,假如真發生了數據丟失,對於一個快速成長的網站而言可能很是致命。
大型網站最常遇到的存儲瓶頸
寫到這裏一個嬰兒般的網站就這樣被咱們創造出來了,咱們但願網站能健康快速的成長,若是網站真的按咱們預期成長了,那麼必定會有一天咱們製造的寶寶屋已經知足不了現實的需求,這個時候咱們應該如何抉擇了?
換掉,所有換掉,使用新的架構例如咱們之前長提的SOA架構,分佈式技術,這個方法不錯,可是SOA和分佈式技術是很難的,成本是很高的,若是這時候咱們經過添加幾臺服務器就能解決問題的話,咱們絕對不要去選擇什麼分佈式技術,由於這個成本過高了。
上面我講到幾種session共享的方案,這個方案解決了應用的水平擴展問題,那麼當咱們網站出現瓶頸時候就多加幾臺服務器不就好了嗎?那麼這裏就有個問題了,當網站成長很快,網站首先碰到的瓶頸究竟是哪一個方面的問題?
本人是作金融網站的,咱們所作的網站有個特色就是當用戶訪問到咱們所作的網站時候,目的都很明確就是爲了付錢。用戶到了咱們所作的網站時候都但願能快 點,再快點完成本網站的操做,不少用戶在使用咱們作的網站時候不太去關心網站的其餘內容,所以咱們所作的網站相對於數據庫而言就是讀寫比例其實很是的均 勻,甚至不少場景寫比讀要高。這個特色是不少專業服務網站的特色,其實這樣的網站和企業開發的特色很相似:業務操做的重要度超過了業務展現的重要度,所以 專業性網站吸納企業系統開發的特色比較多。
可是大部分咱們平常經常使用的網站,咱們逗留時間很長的網站按數據庫角度而言每每是讀遠遠大於寫,例如大衆點評網站它的讀寫比率每每是9比1。
12306或許是中國最著名的網站之一,我記得12306早期常常出現一個問題就是用戶登陸總是登不上,甚至在高峯期整個網站掛掉,頁面顯示503網 站拒絕訪問的問題,這個現象很好理解就是網站併發高了,大量人去登陸網站,購票,系統掛掉了,最後全部的人都不能使用網站了。當網站出現503拒絕訪問時 候,那麼這個網站就出現了最致命的問題,解決大用戶訪問的確是個超級難題,可是當高併發沒法避免時候,整個網站都不能使用這個只能說網站設計上發生了致命 錯誤。
一個好的網站設計在應對超出本身能力的併發時候咱們首先應該是不讓他掛掉,由於這種結果是誰都不能使用,咱們但願那些在可接受的請求下,讓在可接受請 求範圍內的請求仍是能夠正常使用,超出的請求能夠被拒絕,可是它們絕對不能影響到全網站的穩定性,如今咱們看到了12306網站的峯值從未減小過,並且是 越變越多,可是12306出現全站掛掉的問題是愈來愈少了。經過12036網站改變咱們更進一步思考下網站的瓶頸問題。
排除一些不可控的因素,網站在高併發下掛掉的緣由90%都是由於數據庫不堪重負所致,而應用的瓶頸每每只有在解決了存儲瓶頸後纔會暴露,那麼咱們要升級網站能力的第一步工做就是提高數據庫的承載能力,對於讀遠大於寫的網站咱們採起的方式就是將數據庫從讀寫這個角度拆分,具體操做就是將數據庫讀寫分離,以下圖所示:
咱們這時要設計兩個數據庫,一個數據庫主要負責寫操做咱們稱之爲主庫,一個數據庫專門負責讀操做咱們稱之爲副庫,副庫的數據都是從主庫導入的,數據庫 的讀寫分離能夠有效地保證關鍵數據的安全性,可是有個缺點就是當用戶瀏覽數據時候,讀的數據都會有點延時,這種延時比起全站不可用那確定是能夠接受的。
不過針對12306的場景,僅僅讀寫分離仍是遠遠不夠的,特別是負責讀操做的副庫,在高訪問下也是很容易達到性能的瓶頸的,那麼咱們就得使用新的解決方案:使用分佈式緩存,不過緩存的缺點就是不能有效的實時更新,所以咱們使用緩存前首先要對讀操做的數據進行分類,對於那些常常不發生變化的數據能夠事先存放到緩存裏,緩存的訪問效率很高,這樣會讓讀更加高效,同時也減輕了數據庫的訪問壓力。
至於用於寫操做的主庫,由於大部分網站讀寫的比例是嚴重失衡,因此讓主庫達到瓶頸仍是比較難的,不過主庫也有一個讀的壓力就是主庫和副庫的數據同步問 題,不過同步時候數據都是批量操做,而不是像請求那樣進行少許數據讀取操做,讀取操做特別多,所以想達到瓶頸仍是有必定的難度的。聽人說,美國牛逼的 facebook對數據的任何操做都是事先合併爲批量操做,從而達到減輕數據庫壓力的目的。
上面的方案咱們能夠保證在高併發下網站的穩定性,可是針對於讀,若是數據量太大了,就算網站不掛掉了,用戶能很快的在海量數據裏檢索到所須要的信息又 成爲了網站的一個瓶頸,若是用戶須要很長時間才能得到本身想要的數據,不少用戶會失去耐心從而放棄對網站的使用,那麼這個問題又該如何解決了?
如何解決海量數據下的「讀」問題
解決方案就是咱們常用的百度,谷歌哪裏得來,對於海量數據的讀咱們能夠採用搜索技術,咱們能夠將數據庫的數據導出到文件裏,對文件創建索引,使用 倒排索引技術來檢索信息,咱們看到了百度,谷歌有整個互聯網的信息咱們任然能很快的檢索到數據,搜索技術是解決快速讀取數據的一個有效方案,不過這個讀取 仍是和數據庫的讀取有所區別的,若是用戶查詢的數據是經過數據庫的主鍵字段,或者是經過很明確的創建了索引的字段來檢索,那麼數據庫的查詢效率是很高的, 可是使用網站的人跟喜歡使用一些模糊查詢來查找本身的信息,那麼這個操做在數據庫裏就是個like操做,like操做在數據庫裏效率是很低的,這個時候使 用搜索技術的優點就很是明顯了,搜索技術很是適合於模糊查詢操做。
503錯誤
在上面,我講到某些網 站在高併發下會報出503錯誤,503錯誤的含義是指網站服務端暫時沒法提供服務,503還表達了網站服務端如今有問題,可是之後可能會提供正常的服務, 對http協議熟悉的人都知道,5開頭的響應碼錶達了服務端出現了問題,在咱們開發測試時候最爲常見的是500錯誤,500表明的含義是服務端程序出現了 錯誤致使網站沒法正常提供服務,500一般是服務端異常和錯誤所致,若是生產系統裏發現了500錯誤,那麼只能說明網站存在邏輯性的錯誤,這每每是系統上 線前的測試作的不到位所致。回到503錯誤,我上文解釋爲拒絕訪問,其實更加準確的回答應該是服務不可用,那麼爲何我會說503錯誤在高併發的狀況下 90%的緣由是數據庫所致呢?
上文我作出了詳細的解釋,可是今天我回味了一下,發現那個解釋還不是太突出重點,問題的重點是在高併發的狀況整個網站系統首先暴露出問題的是數據庫,若是咱們把整個網站系統比做一個盛水的木桶,那麼木桶最短的那個板就是數據庫了,通常而言網站的服務應用出問題都會是解決存儲問題以後纔會出現。
數據庫出現了瓶頸並非程序存在邏輯性錯誤,數據庫瓶頸的表現就是數據庫由於承受了太多的訪問後,數據庫沒法迅速的作出響應,嚴重時候數據庫會拒絕進 一步操做死鎖在哪裏不能作出任何反應。數據庫猶如一把巨型的大鎖,不少人爭搶這個鎖時候會致使這個大鎖徹底被鎖死,最終請求的處理就停留在這個大鎖上,最 終致使網站提示出503錯誤,503錯誤最終會傳遞到全部的客戶端上,最終的現象就是全站不可用了。
session共享的一個方案是將session數據存儲在外部一個獨立的緩存服務器裏,我開始說用一臺服務器作緩存服務器,後面提到若是以爲一臺服 務器作緩存不安全,那麼採用分佈式緩存服務器例如memcached,那麼這裏就有一個問題了,爲了保證Web服務的可用性,咱們會把Web服務分開部署 到不一樣的服務器上,這些服務器都是對等關係,其中一臺服務器不能正常提供服務不會影響到整個網站的穩定性,那麼咱們採起memcached集羣是否是能夠 達到一樣的效果了?
即緩存服務器集羣中一臺服務器掛掉,不會影響到用戶對網站的使用了?問題的答案是使人失望了,假如咱們使用兩臺服務器作緩存服務器來存儲 session信息,那麼若是其中一臺服務器掛掉了,那麼網站將會有一半的用戶將不能正常使用網站,緣由是他們的session信息丟失了,網站沒法正常 的跟蹤用戶的會話狀態。
我之因此提到這個問題是想告訴你們以memcached爲表明的分佈式緩存和咱們傳統理解的分佈式系統是有區別的,傳統的分佈式系統都會包含一個容災 維護系統穩定性的功能,但實際的分佈式技術是多種多樣的,例如memcached的分佈式技術並非爲了解決容災維護系統穩定性的模式設計,換個說法就是 memcached集羣的設計是沒有過度考慮冗餘的問題,而只有適當的冗餘才能保證系統的健壯性問題。分佈式技術的實現是千差萬別的,每一個優秀的分佈式系 統都有自身獨有的特色。
memcached技術
全面的講述memcached技術並不是本文的主題,並且這個主題也不是一兩句話能說清楚的,這裏我簡單的介紹下memcached實現的原理,當網站 使用緩存集羣時候,緩存數據是經過必定的算法將緩存數據儘可能均勻分佈到不一樣服務器上,若是用戶A的緩存在服務器A上,那麼服務器B上是沒有該用戶的緩存數 據,早期的memcache數據分佈式的算法是根據緩存數據的key即鍵值計算出一個hash值,這個hash值再除以緩存服務器的個數,獲得的餘數會對 應某一臺服務器,例如1對應服務器A,2對應服務器B,那麼餘數是1的key值緩存就會存儲在服務器A上,這樣的算法會致使某一臺服務器掛掉,那麼網站損 失的緩存數據的佔比就會比較高,爲了解決這個問題,memcached引入了一致性hash算法。
關於一致性hash網上有不少資料,這裏我就貼出一個連接,本文就不作過多論述了。連接地址以下:http://blog.csdn.net/kongqz/article/details/6695417
一致性hash能夠服務器宕機時候這臺服務器對整個緩存數據的影響最小。
讀/寫分離方案
上文裏我講到了讀寫分離的設計方案,而讀寫分離方案主要是應用於網站讀寫比例嚴重失衡的網站,而互聯網上絕大部分網站都是讀操做的比例遠遠大於寫操 做,這是網站的主流,若是一個網站讀寫比例比較均衡,那麼這個網站通常都是提供專業服務的網站,這種網站對於我的而言是一個提供生活便利的工具,它們和企 業軟件相似。大部分關注大型網站架構技術關心的重點應該是那種對於讀寫比例失衡的網站,由於它們作起來更加有挑戰性。
將數據庫進行讀寫分離是網站解決存儲瓶頸的第一步,爲何說是第一步呢?由於讀寫分離從業務角度而言它是一種粗粒度的數據拆分,所以它所包含的業務復 雜度比較低,容易操做和被掌控,從技術而言,實現手段也相對簡單,所以讀寫分離是一種低成本解決存儲瓶頸的一種手段,這種方案是一種改良方案而不是革命性 的的方案,無論是從難度,仍是影響範圍或者是經濟成本角度考慮都是很容易讓相關方接受的。
那麼咱們僅僅將數據庫作讀寫分離爲什麼能產生好的效率了?咱們首先要了解下硬盤的機制,硬盤的物理機制就有一個大圓盤飛速旋轉,而後有個磁頭不斷掃描這 個大圓盤,這樣的物理機制就會致使硬盤數據的順序操做比隨機操做效率更高,這點對於硬盤的讀和寫還算公平,可是寫操做在高併發狀況下會有點複雜,寫操做有 個特性就是咱們要保證寫操做的準確性,可是高併發下可能會出現多個用戶同時修改某一條數據,爲了保證數據能被準確的修改,那麼咱們一般要把並行的操做轉變爲串行操做, 這個時候就會出現一個鎖機制,鎖機制的實現是很複雜的,它會消耗不少系統性能,若是寫操做摻雜了讀操做狀況就更復雜,效率會更加低效,相對於寫操做讀操做 就單純多了,若是咱們的數據只有讀操做,那麼讀的性能也就是硬盤順序讀能力和隨機讀能力的體現,即便摻雜了併發也不會對其有很大的影響,所以若是把讀操做 和寫操做分離,效率天然會獲得很大提高。
爲何要引入緩存系統和搜索技術?
既然讀寫分離能夠提高存儲系統的效率,那麼爲何咱們又要引入緩存系統和搜索技術了?緩存將數據存在內存中,內存效率是硬盤的幾萬倍,這樣的好處不言而喻,而選擇搜索技術背後的原理就不一樣了,數據庫存儲的數據稱之爲結構化數據。
結構化數據的限制不少,當結構化數據遇到了變幻無窮的隨機訪問時候,其效率會變得異常低效,可是若是一個網站不能提供靈活、高效的隨機訪問能力,那麼 這個網站就會變得單板沒有活力。例如咱們在淘寶裏查找咱們想要的商品,可是時常咱們並不清楚本身到底想買啥,若是是在實體店裏店員會引導咱們的消費,可是 網站又如何引導咱們的消費,那麼咱們必需要賦予網站經過人們簡單意向隨機找到各類不一樣的商品,這個對於數據庫就是一個like操做的,可是數據裏數據量達 到了必定規模之後like的低效是沒法讓人忍受的,這時候搜索技術在隨機訪問的能力正好能夠彌補數據庫這塊的不足。
拆分數據
業務再接着的增加下去,數據量也會隨之愈來愈大了,這樣發展下去總有一天主庫也會產生瓶頸了,那麼接下來咱們又該如何解決主庫的瓶頸了?
方法很簡單就是咱們要拆分主庫的數據了,那麼我該以什麼維度拆分數據了?一個數據庫裏有不少張表,不一樣的表都針對不一樣的業務,網站的不一樣業務所帶來的 數據量也不是不一樣的,這個時候系統的短板就是那些數據量最大的表,因此咱們要把那些會讓數據庫產生瓶頸的表拆出來,例如電商系統裏商品表和交易表每每數據 量很是大,那麼咱們能夠把這兩種表創建在單獨的兩個數據庫裏,這樣就拆分了數據庫的壓力,這種作法叫作數據垂直拆分,不過垂直拆分會給原有的數據庫查詢,特別是有事務的相關操做產生影響,這些問題咱們必需要進行改造,關於這個問題,我將在下篇裏進行討論。
當咱們的系統作完了讀寫分離,數據垂直拆分後,咱們的網站還在迅猛發展,最終必定又會達到新的數據庫瓶頸,固然這些瓶頸首先仍是出如今那些數據量大的 表裏,這些表數據的處理已經超出了單臺服務器的能力,這個時候咱們就得對這個單庫單表的數據進行更進一步的拆分,也就是將一張表分佈到兩臺不一樣的數據庫裏,這個作法就是叫作數據的水平拆分了。
這兩篇文章咱們能夠理出一個解決大型網站數據瓶頸的一個脈絡了,具體以下:
單庫數據庫-->數據庫讀寫分離-->緩存技術-->搜索技術-->數據的垂直拆分-->數據的水平拆分
以上的每一個技術細節在具體實現中可能存在很大的不一樣,可是問題的原因大體是一致的,咱們理清這個脈絡就是想告訴你們咱們若是碰到這樣的問題應該按何種思路進行思考和設計解決方案。
存儲的瓶頸寫到如今就要進入到深水區了,若是咱們所作的網站已經到了作數據庫垂直拆分和水平拆分的階段,那麼此時咱們所面臨的技術難度的挑戰也會大大加強。
這裏咱們先回顧下數據庫的垂直拆分和水平拆分的定義:
垂直拆分:把一個數據庫中不一樣業務單元的數據分到不一樣的數據庫裏。
水平拆分:是根據必定的規則把同一業務單元的數據拆分到多個數據庫裏。
垂直拆分是一個粗粒度的拆分數據,它主要是將原來在一個數據庫下的表拆分到不一樣的數據庫裏,水平拆分粒度比垂直拆分要更細點,它是將一張表拆到不一樣數 據庫裏,粒度的粗細也會致使實現技術的難度的也不同,很明顯水平拆分的技術難度要遠大於垂直拆分的技術難度。難度意味着投入的成本的增長以及咱們須要承 擔的風險的加大,咱們作系統開發必定要有個清晰的認識:能用簡單的方案解決問題,就必定要絕不猶豫的捨棄複雜的方案,當系統須要使用高難度技術的時候,咱們必定要讓本身感覺到這是無可奈何的。
我是以java工程師應聘進了我如今的公司,因此在我轉到專職前端前,我也作過很多java的應用開發,當時我在公司的前輩告訴我,咱們公司的數據庫 建模很簡單,怎麼個簡單法了,數據庫的表之間都沒有外鍵,數據庫不許寫觸發器,能夠寫寫存儲過程,可是存儲過程決不能用於處理生產業務邏輯,而只能是一些 輔助工做,例如導入導出寫數據啊,後面據說就算是數據庫作到了讀寫分離,數據之間同步也最好是用java程序作,也不要使用存儲過程,除非無可奈何。開始 我還不太理解這些作法,這種不理解不是指我質疑了公司的作法,而是我在想若是一個數據庫咱們就用了這麼一點功能,那還不如讓數據庫公司爲咋們定製個閹割版 算了,不過在我學習了hadoop以後我有點理解這個背後的深意了,其實做爲存儲數據的數據庫,它和咱們開發出的程序的本質是同樣的那就是:存儲和計算, 那麼當數據庫做爲一個業務系統的存儲介質時候,那麼它的存儲對業務系統的重要性要遠遠大於它所能承擔的計算功能,當數據庫做爲互聯網系統的存儲介質時候, 若是這個互聯網系統成長迅速,那麼這個時候咱們對數據庫存儲的要求就會愈來愈高,最後估計咱們都想把數據庫的計算特性給閹割掉,固然數據庫基本的增刪改查 咱們是不能捨棄的,由於它們是數據庫和外界溝通的入口,咱們若是接觸過具備海量數據的數據庫,咱們會發現讓數據庫運行的單個sql語句都會變得異常簡潔和 簡單,由於這個時候咱們知道數據庫已經在存儲這塊承擔了太多的負擔,那麼咱們能幫助數據庫的手段只能是儘可能下降它運算的壓力。
回到關於數據庫垂直拆分和水平拆分的問題,假如咱們的數據庫設計按照咱們公司業務數據庫爲藍本的話,那麼數據庫進行了水平拆分咱們會碰到什麼樣的問題了?爲了回答這個問題我就要比較下拆分前和拆分後會給調用數據庫的程序帶來怎樣的不一樣,不一樣主要是兩點:
第一點:被拆出的表和原庫的其餘表有關聯查詢即便用join查詢的操做須要進行改變;
第二點:某些增刪改(注意:通常業務庫設計不多使用物理刪除,由於這個操做十分危險,這裏的刪每每是邏輯刪除,通常作法就是更新下 記錄的狀態,本質是一個更新操做)牽涉到拆分的表和原庫其餘表共同完成,那麼該操做的事務性就會被打破,若是處理很差,假如碰到操做失敗,業務沒法作到回 滾,這會對業務操做的安全性帶來極大的風險。
關於解決第一點的問題仍是相對比較簡單的,方式方法也不少,下面我來說講我所知道的一些方法,具體以下:
方法一:在垂直拆表時候,咱們先梳理下使用到join操做sql查詢,梳理的維度是以被拆分出的表爲原點,若是 是弱依賴的join表咱們改寫下sql查詢語句,若是是強依賴的join表則隨拆分表一塊兒拆分,這個方法很簡單也很可控,可是這個技術方案存在一個問題, 就是讓拆分粒度變大,拆分的業務規則被幹擾,這麼拆分很容易犯一個問題就是一個數據庫裏總會存在這樣一些表,就是不少數據庫都會和它關聯,咱們很難拆解這 些關聯關係,當咱們沒法理清時候就會把該表作冗餘,即不一樣數據庫存在雷同表,隨着業務增加,這種表的數據同步就成爲了數據庫的一個軟肋,最終它會演變爲整 個數據庫系統的短板甚至是全系統的短板。
方法二:咱們拆表的準則仍是按業務按需求在數據庫層面進行,等數據庫拆好後,再改寫原來受到影響的join查詢 語句,這裏我要說明的是查詢語句修改的成本很低,由於查詢操做是個只讀操做,它不會改變任何底層的東西,若是數據表跨庫,咱們能夠把join查詢拆分爲多 次查詢,最後將查詢結果在內存中概括和合並,其實咱們若是主動拆庫,毫不會把換個不一樣的數據庫產品創建新庫,確定是使用相同數據庫,同類型的數據庫基本都 支持跨庫查詢,不過跨庫查詢據說效率不咋地,咱們能夠有選擇的使用。這種方案也有個致命的缺點,咱們作數據庫垂直拆分毫不可能一次到位,通常都是屢次迭 代,而該方案的影響面很大,關聯方過多,每次拆表幾乎要檢查全部相關的sql語句,這會致使系統不斷累積不可預知的風險。
如下三段內容是方法三:
無論是方法一仍是方法二,都有一個很根本的缺陷就是數據庫和上層業務操做耦合度很高,每次數據庫的變遷都致使業務開發跟隨作大量的同步工做,這樣的後果就 是資源浪費,作服務的人不能每天被數據庫牽着鼻子走,這樣業務系統的平常維護和業務擴展會很存問題,那麼咱們必定要有一個服務和數據庫解耦方案,那麼這裏 咱們就得借鑑ORM技術了。(這裏我要說明下,方法一和方法二我都是以修改sql闡述的,在現實開發裏不少系統會使用ORM技術,互聯網通常用 ibatis和mybatis這種半ORM的產品,由於它們能夠直接寫sql和數據庫最爲親近,若是使用hibernate則就不一樣了,可是 hibernate雖然大部分不是直接寫sql,可是它只不過是對數據庫操做作了一層映射,本質手段是一致,因此上文的sql能夠算是一種指代,它也包括 ORM裏的映射技術)
傳統的ORM技術例如hibernate還有mybatis都是針對單庫進行的,並不能幫咱們解決垂直拆分的問題,所以咱們必須本身開發一套解決跨庫操 做的ORM系統,這裏我只針對查詢的ORM談談本身的見解(講到這裏是否是有些人會有種似成相識的感受,這個不是和分佈式系統很像嗎)。
其實具體怎麼重構有問題的sql不是我想討論的問題,由於這是個技術手段或者說是一個技術上的技巧問題,我這裏重點講講這個ORM與服務層接口的交互, 對於服務層而言,服務層最怕的就是被數據庫牽着鼻子走,由於當數據庫要進行重大改變時候,服務層老是千方百計讓本身不要發生變化,對於數據庫層而言服務層 的建議都應該是合理,數據庫層要把服務層當作本身的需求方,這樣雙方纔能齊心合力完成這件重要的工做,那麼服務層通常是怎樣和數據庫層交互的呢?
從傳統的ORM技術咱們能夠找到答案,具體的方式有兩種:
第一種:以hibernate爲表明的,hibernate框架有一套本身的查詢語言就是hql,它 相似於sql,自定義一套查詢語言看起來很酷,也很是靈活,可是實現難度很是之高,由於這種作法至關於咱們要本身編寫一套新的編程語言,若是這個語言設計 很差,使用者又理解不深刻,最後每每會事與願違,就像hibernate的hql,咱們常常令可直接使用sql也不肯意使用hql,這其中的原因用過的人 必定很好理解的。
第二種:就是數據層給服務層提供調用方法,每一個方法對應一個具體的數據庫操做,就算底層數據庫發生重大變遷,只要提供給服務端的方法定義不變,那麼數據庫的變遷對服務層影響度也會最低。
前面我提到技術難度是咱們選擇技術的一個重要指標,相比之下第二種方案將會是咱們的首選。
垂直拆分數據庫還會帶來另外一個問題就是對事務的影響,垂直拆分數據庫會致使原來的事務機制變成了分佈式事務,解決分佈式事務問題是很是難的,特別是若是我 們想使用業界推出的解決分佈式事務方案,那麼要本身實現個分佈式事務就更難了,不過這裏我要說明一下,我這裏說的更難是和我寫本文有關,我本篇文章之因此 如今才寫是由於我想先研究下業界推出的分佈式解決方案,可是這些方案的原理看得我很沮喪,我就想若是咱們直接用方案的接口實現了它,由於仍是不懂他的不少 原理,那麼這些方案其實就是不可控方案,說不定使用過多就會給系統埋下定時炸彈,所以這裏我就只提提這些方案,有興趣的童鞋能夠去研究下:
1、X/OPEN組織推出的分佈式事務規範XA,其中還包括該組織定義的分佈式事務處理模型X/OPEN;
2、大型網站一致性理論CAP/BASE
3、 PAXOS協議。
這裏特別要提的是PAXOS協議,我之前寫過好幾篇關於zookeeper的文章,zookeeper框架有一個特性就是它自己是一個分佈式文件系 統,當咱們往zookeeper寫數據時候,zookeeper集羣能保證咱們的寫操做的可靠性,這個可靠性和咱們使用線程安全來控制寫數據同樣,絕對不 會讓寫操做出錯,之因此zookeeper能作到這點,是由於zookeeper內部有一個相似PAXOS協議的協議,這個協議相似一個選舉方案,它能保 證寫入操做的原子性。
其實事務也是和線程安全技術相似,只不過事務是要保證一個業務操做的原子性問題,固然事務還要有個特色就是回滾機制即業務操做失敗,事務能夠保證系統恢復到業務操做前的狀態,回滾機制的本質實際上是維護業務操做的狀態性,具體點我這裏列舉個例子:當 系統將要執行一個業務操做時候,咱們首先爲業務系統定義一個初始狀態,業務執行操做時候咱們能夠定義一個執行狀態,操做成功就是一個成功狀態,操做失敗就 是一個操做失敗狀態,若是業務操做是失敗狀態,咱們可讓業務回滾到初始狀態,更進一步若是執行狀態超時也能夠將整個業務狀態回退到初始狀態,其實全部事 務回滾機制的本質基本都是如此。記得不久前,在羣裏有個羣友就問你們如何實現分佈式事務,他想要知道的分佈式事務是有沒有一種技術能像 咱們操做數據庫或者是jdbc那樣一個commit,一個rollback就搞定,可是現實中的分佈式事務比commit和rollback複雜的多,不 可能簡單的讓咱們寫幾個標記就能實現分佈式事務,固然業界是有方案的,就是我上面提到的,若是有人真想知道能夠本身研究下,不過我本人如今仍是不太懂上面 這些技術的原理和思想。
其實當時我立刻給那位羣友一個解答,我說咱們開發時候是常常碰到分佈式事務,可是咱們解決分佈式事務大多數從業務角度來解決的,而沒去選擇純技術手 段,由於技術手段太複雜難以控制。這個答案可能不會令提問者滿意,可是我如今仍是堅持這個觀點,這個觀點符合我提到的原則,當技術方案難度太高,咱們就不 要輕易選擇使用它,由於這麼作是很危險的,今天我就舉個例子吧,這樣可能更有說服力。我如今作的系統不少業務操做常常要和其餘系統共同完成,其餘系統有我 們公司本身的系統,也有其餘企業的系統,這裏我仍是把業務操做比做一輛在高速公路的汽車,那麼每一個系統就是高速公路上的一個收費站,業務每到一個收費站, 該系統的數據庫就會在對應的數據庫的某張表裏某條記錄上記錄一個狀態,當汽車跑徹底程,各個收費站就會相互通知,告訴你們任務完成,最終將全部的狀態置爲 已完成,若是失敗,就廢掉這輛汽車,收費站之間也會相互通知,讓全部的記錄狀態迴歸到初始狀態,就當歷來沒有這輛汽車來過。這個作法的原理就是使用了事務 回滾的本質,狀態的變遷和回退,這個作法在業務系統開發裏也有個專有術語就是工做流。其實大多數問如何實現分佈式事務如何實現的問題的本質就是想解決事務 的回滾問題,咱們其實不要被這個分佈式事務的名字給嚇住了,其實有不少不起眼的技術手段和業務手段都能達到相同的目的。
晚上11點了,看來本文今天寫不完了,今天就到此爲止,最後我要總結下本文的內容,具體以下:
1. 大型網站解決存儲瓶頸的問題,咱們要找準存儲這個關鍵點,由於數據庫實際上是存儲和運算的組合體,可是在咱們這個場景下,存儲 是第一位的,當存儲是瓶頸時候咱們要狠下心來儘可能多的拋棄數據的計算特色,因此上文中我提出咱們數據庫就不要濫用計算功能了例如觸發器、存儲過程等等。
2. 數據庫剝離計算功能不表明不要數據的計算功能,由於沒有數據的計算功能數據庫也就沒價值了,那麼咱們要將數據庫的計算功能進 行遷移,遷移到程序裏面,通常大型系統程序和數據庫都是分開部署到不一樣服務器上,所以程序裏處理數據計算就不會影響到數據庫所在服務器的性能,就可讓安 裝數據庫的服務器專心服務於存儲。
3. 咱們要盡一切可能的把數據庫的變化對服務層的影響降到最低,最好是數據庫作拆分後,現有業務不要任何的更改,那麼咱們就得設 計一個全新的數據訪問層,這個數據訪問層將數據庫和服務層進行解耦,任何數據庫的變化都由數據訪問層消化,數據訪問層對外接口要高度統一,不要輕易改變。
4. 若是咱們設計了數據訪問層來解決數據庫拆分的問題,數據訪問層加上數據庫其實就組合出了一個分佈式數據庫的解決方案,因而可知拆分數據庫的難度是很高的,由於數據庫將擁有分佈式的特性,而分佈式開發就意味開發難度的增長。
5. 對於分佈式事務的處理,咱們儘可能要從具體問題具體分析,不要一感受這個事務操做本質是分佈式事務就去尋找通用的分佈式事務技術手段,這樣的想法實際上是迴避困難的思想,結果可能會是把問題搞得更加複雜。
若是數據庫須要進行水平拆分,這實際上是一件很開心的事情,由於它表明公司的業務正在迅猛的增加,對於開發人員而言那就是有不盡的項目能夠作,雖然會感受很忙,可是人過的充實,內心也踏實。
數據庫水平拆分簡單說來就是先將原數據庫裏的一張表在作垂直拆分出來放置在單獨的數據庫和單獨的表裏後更進一步的把原本是一個總體的表進一步拆分紅多張表,每一張表都用獨立的數據庫進行存儲。當 表被水平拆分後,原數據表成爲了一個邏輯的概念,而這個邏輯表的業務含義須要多張物理表協同完成,所以數據庫的表被水平拆分後,那麼咱們對這張表的操做已 經超出了數據庫自己提供給咱們現有的手段,換句話說咱們對錶的操做會超出數據庫自己所擁有的處理能力,這個時候我就須要設計相關的方案來彌補數據庫缺失的 能力,這就是數據庫水平拆分最大的技術難點所在。
數據庫的水平拆分是數據庫垂直拆分的升級版,它和垂直拆分更像繼承機制裏的父子關係,所以水平拆分後,垂直拆分所遇到的join查詢的問題以及分佈式事務的問題任然存在,因爲表被物理拆解增長了邏輯表的維度,這也給垂直拆分裏碰到的兩個難題增長了更多的維度,所以水平拆分裏join查詢的問題和分佈式事務會變得更加複雜。水平拆分除了垂直拆分兩個難題外,它還會產生新的技術難題,這些難題具體以下:
難題一:數據庫的表被水平拆分後,該表的主鍵設計會變得十分困難;
難題二:原來單表的查詢邏輯會面臨挑戰。
在準備本篇文章時候,我看到一些資料裏還提到了一些難題,這些難題是:
難題三:水平拆分表後,外鍵的設計也會變得十分困難;
難題四:這個難題是針對數據的新增操做的,大體的意思是,咱們到底按什麼規則把須要存儲的數據存儲在拆分出的那個具體的物理數據表裏。
難題三的問題,我在上篇已經給出瞭解答,這裏我進行必定的補充,其實外鍵問題在垂直拆分就已經存在,不過在講垂直拆分時候咱們沒有講到這個問題,這主 要是我設定了一個前提,就是數據表在最原始的數據建模階段就要拋棄全部外鍵的設計,並將外鍵的邏輯拋給服務層去完成,咱們要盡全力減輕數據庫承擔的運算壓 力,其實除了減輕數據庫運算壓力外,咱們還要將做爲存儲原子的表保持相對的獨立性,互不關聯,那麼要作到這點最直接的辦法就是去掉表與表之間關聯的象徵: 外鍵,這樣咱們就能夠從根基上爲未來數據庫作垂直拆分和水平拆分打下堅實的基礎。
至於難題四,其實問題的本質是分庫分表後具體的數據在哪裏落地的問題,而數據存儲在 表裏的關鍵障礙其實就是主鍵,試想一下,咱們設計張表,全部字段咱們都准許能夠爲空,可是表裏有個字段是絕對不能爲空的,那就是主鍵,主鍵是數據在數據庫 裏身份的象徵,所以咱們在主鍵設計上是能夠體現出該數據的落地規則,那麼難題四也會隨之解決。所以下文我會重點講解前兩個水平拆分的難題。
首先是水平拆分裏的主鍵設計問題,拋開全部主鍵所能表明的業務含義,數據庫裏標的主鍵本質是表達表裏的某一條記錄的惟一性,在設計數據庫的時候咱們可 以由一個絕對不可重複的字段表示主鍵,也能夠使用多個字段組合起來表達這種惟一性,使用一個字段表示主鍵,這已是很原子級的操做,無法作進一步的修改, 可是若是使用多個字段表示一個主鍵對於水平拆分而言就會碰到問題了,這個問題主要是體如今數據到底落地於哪一個數據庫,關於主鍵對數據落地的影響我會在把相 關知識講解完畢後再着重闡述,這裏要提的是當碰到聯合主鍵時候咱們能夠設定一個沒有任何業務含義的字段來替代,不過這個要看場景了,我傾向於將聯合主鍵各 個字段裏的值合併爲一個字段來表示主鍵,若是有的朋友認爲這樣會致使數據冗餘,那麼能夠乾脆去掉原來作聯合主鍵的相關字段就是用一個字段表示,只不過歸併 字段時候使用一個分隔符,這樣方便服務層進行業務上的拆分。
由上所述,這裏我給出水平拆分主鍵設計的第一個原則:被水平拆分的表的主鍵設計最好使用一個字段表示。
若是咱們的主鍵只是表達記錄惟一性的話,那麼水平拆分時候相對要簡單的多,例如在Oracle數據庫裏有一個sequence機制,這其實就是一個自 增數的算法,自增機制幾乎全部關係數據庫都有,也是咱們平時最喜歡使用的主鍵字段設計方案,若是咱們要拆分的表,使用了自增字段,同時這個自增字段只是用 來表達記錄惟一性,那麼水平拆分時候處理起來就簡單多了,我這裏給出兩個經典方案,方案以下:
方案一:自增列都有設定步長的特性,假如咱們打算把一張表只拆分爲兩個物理表,那麼咱們能夠在其中一張表裏把主鍵的自增列的步長設計爲 2,起始值爲1,那麼它的自增規律就是1,3,5,7依次類推,另一張物理表的步長咱們也能夠設置爲2,若是起始值爲2,那麼自增規律就是 2,4,6,8以此類推,這樣兩張表的主鍵就絕對不會重複了,並且咱們也不用另外作兩張物理表相應的邏輯關聯了。這種方案還有個潛在的好處,那就是步長的 大小和水平數據拆分的粒度關聯,也是咱們爲水平拆分的擴容留有餘量,例如咱們把步長設計爲9,那麼理論上水平拆分的物理表能夠擴容到9個。
方案二:拆分出的物理表咱們容許它最多存儲多少數據,咱們其實事先經過必定業務技術規則大體估算出來,假如咱們 估算一張表咱們最多讓它存儲2億條,那麼咱們能夠這麼設定自增列的規律,第一張物理表自增列從1開始,步長就設爲1,第二種物理表的自增列則從2億開始, 步長也設爲1,自增列都作最大值的限制,其餘的依次類推。
那麼若是表的主鍵不是使用自增列,而是業務設計的 惟一字段,那麼咱們又如何處理主鍵分佈問題了?這種場景很典型,例如交易網站裏必定會有訂單表,流水錶這樣的設計,訂單表裏有訂單號,流水錶裏有流水號, 這些編號都是按必定業務規則定義而且保證它的惟一性,那麼前面的自增列的解決方案就無法完成它們作水平拆分的主鍵問題,那麼碰到這個狀況咱們又該如何解決 了?咱們仔細回味下數據庫的水平拆分,它其實和分佈式緩存何其的相似,數據庫的主鍵就至關於分佈式緩存裏的鍵值,那麼咱們能夠按照分佈式緩存的方案來設計主鍵的模型,方案以下:
方案一:使用整數哈希求餘的算法,字符串若是進行哈希運算會得出一個值,這個值是該字符串的惟一標誌,若是咱們 稍微改變下字符串的內容,計算的哈希值確定是不一樣,兩個不一樣的哈希值對應兩個不一樣字符串,一個哈希值有且只對應惟一一個字符串,加密算法裏的 MD5,SHA都是使用哈希算法的原理計算出一個惟一標示的哈希值,經過哈希值的匹配能夠判斷數據是否被篡改過。不過大多數哈希算法最後得出的值都是一個 字符加數字的組合,這裏我使用整數哈希算法,這樣計算出的哈希值就是一個整數。接下來咱們就要統計下咱們用於作水平拆分的服務器的數量,假如服務器的數量 是3個,那麼接着咱們將計算的整數哈希值除以服務器的數量即取模計算,經過獲得的餘數來選擇服務器,該算法的原理圖以下所示:
方案二:就是方案一的升級版一致性哈希,一致性哈希最大的做用是保證當咱們要擴展物理數據表的數量時候以及物理表集羣中某臺服務器失效時候纔會體現,這個問題我後續文章會詳細討論物理數據庫擴容的問題,所以這裏先不展開討論了。
由上所述,咱們發如今數據庫進行水平拆分時候,咱們設定的算法都是經過主鍵惟一性進行的,根據主鍵惟一性設計的 特色,最終數據落地於哪一個物理數據庫也是由主鍵的設計原則所決定的,回到上文裏我提到的若是原庫的數據表使用聯合字段設計主鍵,那麼咱們就必須首先合併聯 合主鍵字段,而後經過上面的算法來肯定數據的落地規則,雖然不合並一個字段看起來也不是太麻煩,可是在我多年開發裏,把惟一性的字段分割成多個字段,就等 於給主鍵增長了維度,字段越多,維度也就越大,到了具體的業務計算了咱們不得不時刻留心這些維度,結果就很容易出錯,我我的認爲若是數據庫已經到了水平拆分階段了,那麼就說明數據庫的存儲的重要性大大加強,爲了讓數據庫的存儲特性變得純粹乾淨,咱們就得盡力避免增長數據庫設計的複雜性,例如去掉外鍵,還有這裏的合併聯合字段爲一個字段,其實爲了下降難度,哪怕作點必要的冗餘也是值得。
解決數據庫表的水平拆分後的主鍵惟一性問題有一個更加直接的方案,這也是不少人碰到此類問題很天然想到的方法,那就是把主鍵生成規則作成一個主鍵生成系統,放置在單獨一臺服務器上 統一輩子成,每次新增數據主鍵都從這個服務器裏獲取,主鍵生成的算法其實很簡單,不少語言都有計算UUID的功能,UUID是根據所在服務器的相關的硬件信 息計算出的全球惟一的標示,可是這裏我並無首先拿出這個方案,由於它相好比我前面的方案缺點太多了,下面我要細數下它的缺點,具體以下:
缺點一:把主鍵生成放到外部服務器進行,這樣咱們就不得不經過網絡通訊完成主鍵值的傳遞,而網絡是計算機體系裏效率最低效的方式,所以它會影響數據新增的效率,特別是數據量很大時候,新增操做很頻繁時候,該缺點會被放大不少;
缺點二:若是咱們使用UUID算法作主鍵生成的算法,由於UUID是依賴單臺服務器進行,那麼整個水平拆分的物理數據庫 集羣,主鍵生成器就變成整個體系的短板,並且是關鍵短板,主鍵生成服務器若是失效,整個系統都會沒法使用,而一張表須要被水平拆分,並且拆分的表是業務表 的時候,那麼這張表在整個系統裏的重要度天然很高,它若是作了水平拆分後出現單點故障,這對於整個系統都是致命的。固然有人確定說,既然有單點故障,那麼 咱們就作個集羣系統,問題不是解決了嗎?這個想法的確能夠解決我上面闡述的問題,可是我前文講到過,現實的軟件系統開發裏咱們要堅守一個原則那就是有簡單方案儘可能選擇簡單的方案解決問題,引入集羣就是引入了分佈式系統,這樣就爲系統開發增長了開發難度和運維風險,若是咱們上文的方案就能解決咱們的問題,咱們何須自討苦吃作這麼複雜的方案呢?
缺點三:使用外部系統生成主鍵使得咱們的水平拆分數據庫的方案增長了狀態性,而我上面提到的方案都是無狀態的, 有狀態的系統會相互影響,例如使用外部系統生成主鍵,那麼當數據操做增大時候,必然會形成在主鍵系統上資源競爭的事情發生,若是咱們對主鍵系統上的競爭狀 態處理很差,頗有可能形成主鍵系統被死鎖,這也就會產生我前文裏說到的503錯誤,而無狀態的系統是不存在資源競爭和死鎖的問題,這洋就提高了系統的健壯 性,無狀態系統另外一個優點就是水平擴展很方便。
這裏我列出單獨主鍵生成系統的缺點不是想說明我以爲這種解決方案徹底不可取,這個要看具體的業務場景,根據做者個人經驗尚未找到一個很合適使用單獨主鍵生成器的場景。
上文裏我提出的方案還有個特色就是能保證數據在不一樣的物理表裏均勻的分佈,均勻分佈能保證不一樣物理表的負載均衡,這樣就不會產生系統熱點,也不會讓某 臺服務器比其餘服務器作的事情少而閒置資源,均勻分配資源能夠有效的利用資源,下降生產的成本提升生產的效率,可是均勻分佈式數據每每會給咱們業務運算帶來不少麻煩。
水平拆分數據庫後咱們還要考慮水平擴展問題,例如若是咱們事先使用了3臺服務器完成了水平拆分,若是系統運行到必定階段,該表又遇到存儲瓶頸了,咱們就得水平擴容數據庫,那麼若是咱們的水平拆分方案開始設計的很差,那麼擴容時候就會碰到不少的麻煩。
上文裏我遺留了兩個問題,一個問題是數據庫作了水平拆分之後,若是咱們對主鍵的設計採起一種均勻分佈的策略,那麼它對於被水平拆分出的表後續的查詢 操做將有何種影響,第二個問題就是水平拆分的擴容問題。這兩個問題在深刻下去,本系列就愈來愈技術化了,可能最終不少朋友讀完後仍是沒有找到解決實際問題 的啓迪,並且我以爲這些問題都是像BAT這樣巨型互聯網公司纔會認真思考的,所以本篇我打算換個角度來闡述本文的後續內容。
這裏咱們首先要明確一個問題,究竟是什麼因素促使咱們去作數據庫的垂直拆分和水平拆分的呢?答案很簡單就是業務發展的需求,前文裏的水平拆分技術方案 基本都是拋棄變幻無窮的業務規則的限制,儘可能將水平拆分的問題歸爲一個簡單的技術實現方案,而純技術手段時常是看起來很美,可是到了面對現實問題時候,常 常會變得那麼蒼白和無力。
水平拆分的難題裏我還有個難題沒有講述,就是水平拆分後對查詢操做的影響,特別是對單表查詢的影響,這點估計也是大夥最爲關心的問題,今天我不在延着 水平拆分的技術手段演進是闡述上文的遺留問題,而是我要把前面提到的技術手段和一些典型場景結合起來探討如何解決網站存儲的瓶頸問題。
前文中我總結過一個解決存儲瓶頸的脈絡,具體以下:
單庫數據庫-->數據庫讀寫分離-->緩存技術-->搜索技術-->數據的垂直拆分-->數據的水平拆分
這個脈絡給一些朋友產生了誤解,就是認爲這個過程應該是個串行的過程,其實在實際的場景下這個過程每每是並行的,可是裏面有一個元素應該是串行的或者說思考時候有個前後問題,那就是對數據庫層的操做,具體以下:
單庫數據庫-->數據庫讀寫分離-->數據的垂直拆分-->數據的水平拆分
而緩存技術和搜索技術在數據庫的任意階段裏均可以根據實際的業務需求隨時切入其中幫助數據庫減輕沒必要要的壓力。例如,當網站的後臺數據庫仍是單庫的時候,數據庫漸漸出現了瓶頸問題,而這個瓶頸又沒有達到須要採起大張旗鼓作讀寫分離方案的程度,那麼我這個時候能夠考慮引入緩存機制。不過要合理的使用緩存咱們首先要明確緩存自己的特色,這些特色以下所示:
特色一:緩存主要是適用於讀操做,而且緩存的讀操做的效率要遠遠高於從數據庫以及硬盤讀取數據的效率。
特色二:緩存的數據是存儲在內存當中,所以當系統重啓,宕機等等異常場景下,緩存數據就會不可逆的丟失,且沒法恢復,所以緩存不能 做爲可靠存儲設備,這就致使一個問題,緩存裏的數據必須首先從數據庫裏同步到內存中,而使用緩存的目的就是爲了解決數據庫的讀操做效率低下的問題,數據庫 的數據同步到緩存的操做會由於數據庫的效率低下而在性能上大打折扣,因此緩存適合的場景是那些固定不變的數據以及業務對實時性變化要求不高的數據。
根據緩存的上述兩個特色,咱們能夠把數據庫裏和上述描述相似操做的相關數據遷移到緩存裏,那樣咱們就從數據庫上剝離了那些對數據庫價值不高的操做,讓數據庫專心作有價值的操做,這樣也是減輕數據庫壓力的一種手段。
不過這個手段侷限性很強,侷限性主要是一臺計算機了用於存儲緩存的內存的大小都是遠遠要低於硬盤,而且內存的價格要遠貴於硬盤,若是咱們將大規模的數 據從硬盤往內存遷移,從資源成本和利用率角度考慮性價比仍是很低的,所以緩存每每都是用於轉存那些不會常常變化的數據字典,以及常常會被讀,而修改較少的 數據,可是這些數據的規模也是有必定限度的,所以當單庫數據庫出現了瓶頸時候立刻就着手進行讀寫分離方案的設計性價比仍是很高的。
前文我講到咱們之因此選擇數據庫讀寫分離是主要緣由是由於數據庫的讀寫比例嚴重失衡所致,可是作了讀寫分離必然有個問題不可避免,寫庫向讀庫同步數據 必定會存在必定的時間差,若是咱們想減少讀庫和寫庫數據的時間差,那麼任然會致使讀庫由於寫的粒度過細而發生部分性能的損失,可是時間差過大,或許又會無 法知足實際的業務需求,所以這個時間差的設計必定要基於實際的業務需求合理的設計。
同步的時間差的問題仍是個小問題,也比較好解決,可是如何根據實際的業務需求作讀寫分離這其實仍是很是有挑戰性的,這裏我舉個很常見的例子來講明讀寫 分離的難度問題,咱們這裏以淘寶爲例,淘寶是個C2C的電商網站,它是互聯網公司提供一個平臺,商家自助接入這個平臺,在這個平臺上賣東西,這個和線下很 多大賣場的模式相似。淘寶是個大平臺,它的交易表裏必定是要記下全部商戶的交易數據,可是針對單個商家他們只會關心本身的網店的銷售數據,這就有一個問題 了,若是某一個商家要查詢本身的交易信息,淘寶就要從成千上萬的交易信息裏檢索出該商家的交易信息,那麼若是咱們把全部交易信息放在一個交易表裏,確定有 商家會有這樣的疑問,個人網店天天交易額不大,爲何我查詢交易數據的速度和那些大商家同樣慢了?那麼咱們到底該如何是解決這樣的場景了?
碰到這樣的狀況,當網站的交易規模變大後就算咱們把交易表作了讀寫分離估計也是無法解決實際的問題,就算咱們作的完全點把交易表垂直拆分出來估計仍是解決不了問題,由於一個業務數據庫擁有不少張表,可是真正壓力大的表畢竟是少數,這個符合28原則,而數據庫大部分的關鍵問題又都是在那些數據壓力大的表裏,就算咱們把這些表單獨作讀寫分離甚至作垂直拆分,其實只是把數據庫最大的問題遷移出原來數據庫,而不是在解決該表的實際問題。
若是咱們要解決交易表的問題咱們首先要對交易表作業務級的拆分,那麼咱們要爲交易表增長一個業務維度:實時交易和歷史交易,通常而言實時交易以當天及 當天24小時爲界,歷史交易則是除去當天交易外的全部歷史交易數據。實時交易數據和歷史交易數據有着很大不一樣,實時交易數據讀與寫是比較均衡的,不少時候 估計寫的頻率會遠高於讀的頻率,可是歷史交易表這點上和實時交易就徹底不一樣了,歷史交易表的讀操做頻率會遠大於寫操做頻率,若是咱們將交易表作了實時交易 和歷史交易的拆分後,那麼讀寫分離方案適合的場景是歷史交易查詢而非實時交易查詢,不過歷史交易表的數據是從實時交易表裏同步過來的,根據這兩張表的業務 特性,咱們能夠按以下方案設計,具體以下:
咱們能夠把實時交易表設計成 兩張表,把它們分別叫作a表和b表,a表和b表按天交替進行使用,例現在天咱們用a表記錄實時交易,明天咱們就用b表記錄實時交易,固然咱們事先能夠用個 配置表記錄今天到底使用那張表進行實時交易記錄,爲何要如此麻煩的設計實時交易表了?這麼作的目的是爲了實時交易數據同步到歷史數據時候提供便利,通常 咱們會在凌晨0點切換實時交易表,過時的實時交易表數據會同步到歷史交易表裏,這個時候須要數據遷移的實時交易表是全表數據遷移,效率是很是低下,假如實 時交易表的數據量很大的時候,這種導入同步操做會變得十分耗時,因此咱們設計兩張實時交易表進行切換來把數據同步的 風險降到最低。因而可知,歷史交易表天天基本都只作一次寫操做,除非同步出了問題,纔會重複進行寫操做,可是寫的次數確定是很低的,因此歷史交易表的讀寫 比例失衡是很是嚴重的。不過實時交易表的切換也是有技術和業務風險的,爲了保證明時交易表的高效性,咱們通常在數據同步操做成功後會清空實時交易表 的數據,可是咱們很難保證這個同步會不會有問題,所以同步時候咱們最好作下備份,此外,兩個表切換的時候確定會碰到這樣的場景,就是有人在凌晨0點前作了 交易,可是這個交易是在零點後作完,假如實時交易表會記錄交易狀態的演變過程,那麼在切換時候就有可能兩個實時表的數據沒有作好接力,所以咱們同步到歷史 交易表的數據必定要保持一個原則就是已經完成交易的數據,沒有完成的交易數據兩張實時交易還要完成一個業務上的接力,這就是業界常說的數據庫日切的問題。
歷史交易表自己就是爲讀使用的,因此咱們從業務角度將交易表拆分紅實時交易表和歷史交易表自己就是在爲交易表作讀寫分離,竟然了設計了歷史交易表咱們就作的乾脆點,把歷史交易表作垂直拆分,將它從原數據庫裏 拆分出來獨立建表,隨着歷史交易的增大,上文裏所說的某個商戶想快速檢索出本身的數據的難題並無獲得根本的改善,爲了解決這個難題咱們就要分析下難題的 根源在那裏。這個根源很簡單就是咱們把全部商戶的數據不加區別的放進了一張表裏,無論是交易量大的商戶仍是交易量小的商戶,想要查詢出本身的數據都要進行 全表檢索,而關係數據庫單 表記錄達到必定數據量後全表檢索就會變的異常低效,例如DB2當數據量超過了1億多,mysql單表超過了100萬條後那麼全表查詢這些表的記錄都會存在 很大的效率問題,那麼咱們就得對歷史交易表進一步拆分,由於問題根源是單表數據量太大了,那咱們就能夠對單表的數據進行拆分,把單表分紅多表,這個場景就 和前面說的水平拆分裏把原表變成邏輯表,原表的數據分散到各個獨立的邏輯表裏的方式一致,不過這裏咱們沒有一開始作水平拆分,那是會把問題變麻煩,咱們只 要在一個數據庫下對單表進行拆分便可,這樣也能知足咱們的要求,而且避免了水平拆分下的跨庫寫做的難題。接下來咱們又有一個問題了那就是咱們按什麼維度拆 分這張單表呢?
咱們按照前文講到的水平拆分裏主鍵設計方案執行嗎?固然不行哦,由於那些方案明顯提高不了商戶檢索數據的效率問題,因此咱們要首先分析下商戶檢索數據的方式,商戶通常會按這幾個維度檢索數據,這些維度分別是:商戶號、交易時 間、交易類型,固然還有其餘的維度,我這裏就以這三個維度爲例闡述下面的內容,商戶查詢數據效率低下的根本緣由是全表檢索,其實商戶查詢至少有一個維度那 就是商戶號來進行查詢,若是咱們把該商戶的數據存入到一張單獨的表裏,天然查詢的效率會有很大的提高,可是在實際系統開發裏咱們不多經過商戶號進行拆分 表,這是爲何呢?由於一個電商平臺的商戶是個動態的指標,會常常發生變化,其次,商戶號的粒度很細,若是使用商戶號拆分表的必然會有這樣的後果那就是我 們可能要頻繁的建表,隨着商戶的增長表的數量也會增長,形成數據的碎片化,同時不一樣的商戶交易量是不同的,按商戶建表會形成數據存儲的嚴重不平衡。若是 使用交易類型來拆分表,雖然維度的粒度比商戶號小,可是會形成數據的分散化,也就是說咱們查詢一個商戶的所有交易數據會存在很大問題。因而可知拆表時候如 何有效的控制維度的粒度以及數據的彙集度是拆分的關鍵所在,由於使用交易時間這個維度就會讓拆分更加合理,不過期間的維度的設計也是頗有學問的,下面咱們看看騰訊分析的維度,以下所示:
騰訊分析的維度是今天這個其實至關於實時交易查詢,除此以外都是對歷史數據查詢,它們分爲昨天、最近7天和最近30天,咱們若是要對歷史交易表進行拆 分也是能夠參照騰訊分析的維度進行,不過無論咱們選擇什麼維度拆分數據,那麼都是犧牲該維度成全了其餘維度,例如咱們按騰訊分析的維度拆分數據,那麼咱們 想靈活使用時間查詢數據將會受到限制。
咱們把歷史交易數 據經過交易時間維度進行了拆分,雖然獲得了效率提高,可是歷史交易數據表是個累積表,隨着時間推移,首先是月表,接下來是周表都會由於數據累積產生查詢效 率低下的問題,這個時候咱們又該如何解決了?這個時候咱們須要再引進一個維度,那麼這個時候咱們能夠選擇商戶號這個維度,可是商戶號做爲拆分維度是有必定 問題的,由於會形成數據分佈不均衡,那麼咱們就得將維度的粒度由小變粗,其實一個電商平臺上每每少數商戶是完成了大部分電商平臺的交易,所以咱們能夠根據 必定指標把重要商戶拆分出來,單獨建表,這樣就能夠平衡了數據的分佈問題。
咱們總結下上面的案例,咱們會獲得不少的啓迪,我將這些啓迪總結以下:
啓迪一:數據庫的讀寫分離不是簡單的把主庫數據導入到讀庫裏就能解決問題,讀數據庫和寫數據的分離的目的是爲了讓讀和寫操做不能相互影響效率。
啓迪二:解決讀的瓶頸問題的本質是減小數據的檢索範圍,數據檢索的範圍越小,讀的效率也就越高;
啓迪三:數據庫的垂直拆分和水平拆分首先不該該從技術角度進行,而是經過業務角度進行,若是數據庫進行業務角度的水平拆分,那麼拆 分的維度每每是要根據該表的某個字段進行的,這個字段選擇要有必定原則,這個原則主要是該字段的維度的粒度不能過細,該字段的維度範圍不能常常的動態發生 變化,最後就是該維度不能讓數據分佈嚴重失衡。
回到現實的開發裏,對於一個數據庫作拆表,分表的工做實際上是一件很讓人惱火的工做,這主要是有如下緣由所形成的,具體以下所述:
緣由一:一個數據庫其實容納多少張表是有必定限制的,就算沒有超過這個限制,若是原庫原本有30張表,咱們拆分後變成了60張,接着是120張,那麼數據庫自己管理這麼多表也會消耗不少性能,所以公司的DBA每每會控制那些過多分表的行爲。
緣由二:每次拆表後,都會牽涉到歷史數據的遷移問題,這個遷移風險很大,遷移方案若是設計的不完善可能會致使數據丟失或者損壞,若是關鍵數據發生了丟失和損壞,結果可能很是致命。所以在設計數據庫分表分庫方案時候咱們要儘可能讓受影響的數據範圍變得最小。
緣由三:每次拆表和分表都會讓系統的相關方繃緊神經,方案執行後,會有很長時間的監控和觀察期,因此拆數據庫時常是一件使人討厭的事情。
緣由四:爲了保證新方案執行後確保系統沒有問題,咱們經常會讓新舊系統並行運行一段時間,這樣能夠保證若是新方案出現問題,問題的影響面最低,可是這種作法也有一個惡果就是會致使數據遷移方案要進行動態調整,從而增長遷移數據的風險
所以當公司不得不作這件事情時候,公司都會很天然去考慮第三種解決方案,第三種解決方案是指儘可能不改變原數據庫的功能,而是另起爐竈,使用新技術來解決咱們的問題,例如前文所說的搜索技術解決數據庫like的低效問題就是其中方案之一,該方案只要咱們將數據庫的表按必定時間導入到文件系統,而後對文件創建倒排索引,讓like查詢效率更好,這樣就不用改變原數據庫的功能,又能減輕數據庫的壓力。
如今經常使用的第三種解決方案就是使用NoSql數據庫,NoSql數據庫大多都是針對文件進行的,所以咱們能夠和使用搜索引擎那樣把數據導入到文件裏就好了,NoSql基本都採用Key/Value這種簡單的數據結構,這種數據結構和關係數據庫比 起來更加的靈活,對原始數據的約束最少,因此在NoSql數據庫裏建表咱們能夠很靈活的把列和行的特性交叉起來用,這句話可能不少人不太理解,下面我舉個 例子解釋下,例如hadoop技術體系裏的hbase,hbase是一個基於列族的數據庫,使用hbase時候咱們就能夠經過列來靈活的拆分數據,好比我 們能夠把中國的省份做爲一個列,將該省份的數據都放入到這個列下面,在省這個維度下咱們能夠接着在定義一個列的維度,例如軟件行業,屬於軟件行業的數據放 在這個列下面,最終提供用戶查詢時候咱們就能夠減小數據檢索的範圍,最終達到提高查詢效率的目的。因而可知當咱們用慣了關係數據庫後,學習像hbase這 樣的Nosql數據庫咱們會很是的不適應,由於關係數據庫的表有固定模式,也就是咱們常說的結構化數據,當表的定義好了後,就算裏面沒有數據,那麼這個結 構也就固定了,咱們使用表的時候都是按這個模型下面,咱們幾乎感受不到它,可是到了hbase的使用就不一樣了,hbase使用時候咱們都在不停的爲數據增 加結構化模型,並且這個維度是以列爲維度的,而關係數據庫裏列肯定後咱們使用時候是沒法改變的,這就是學習hbase的最大困難之一。Hbase之因此這麼麻煩的設計這樣的計算模型,終極目的就是爲了讓海量數據按不一樣維度存儲起來,使用時候盡全力檢索數據檢索的數量,從而達到海量數據快速讀取的目的。
在講數據庫水平拆分時候,我列出了水平拆分數據庫須要解決的兩個難題,它們分別是主鍵的設計問題和單表查詢的問題,主鍵問題前文已經作了比較詳細的講述了,可是第二個問題我沒有講述,今天我將會講講如何解決數據表被垂直拆分後的單表查詢問題。
要解決數據表被水平拆分後的單表查詢問題,咱們首先要回到問題的源頭,咱們爲何須要將數據庫的表進行水平拆分。下面咱們來推導下咱們最終下定決心作水平拆分表的演進過程,具體以下:
第一個演進過程:進行了讀寫分離的表在數據增加後須要進行水平拆分嗎?回答這個疑問咱們首先要想一想進行讀寫分離操做的表真的是由於數據量大嗎?答案實際上是否認的。最基本的讀寫分離的目的是爲了解決數據庫的某張表讀寫比率嚴重失衡的問題, 舉個例子,有一張表天天會增長1萬條數據,也就是說咱們的系統天天會向這張表作1萬次寫的操做,固然也有可能咱們還會更新或者刪除這張表的某些已有的記 錄,這些操做咱們把它歸併到寫操做,那麼這張表一天咱們隨意定義個估值吧2萬5千次寫操做,其實這種表的數據量並不大,一年下來也就新增的幾百萬條數據, 一個大型的商業級別的關係數據庫, 當咱們爲表創建好索引和分區後,查詢幾百萬條數據它的效率並不低,這麼說來查詢的效率問題還不必定是讀寫分離的源頭。其實啊,這張表除了寫操做天天還承受 的讀操做可能會是10萬,20萬甚至更高,這個時候問題來了,像oracle和mysql這樣鼎鼎大名的關係數據庫默認的最大鏈接數是100,通常上了生 產環境咱們可能會設置爲150或者200,這些鏈接數已經到了這些關係數據庫的最大極限了,若是再加以提高,數據庫性能會嚴重降低,最終頗有可能致使數據 庫因爲壓力過大而變成了一個巨鎖,最終致使系統發生503的錯誤,如是咱們就會想到採用讀寫分離方案,將數據庫的讀操做遷移到專門的讀庫裏,若是系統的負 載指標和我列舉的例子相仿,那麼遷移的讀庫甚至不用作什麼垂直拆分就能知足實際的業務需求,由於咱們的目的只是爲了減輕數據庫的鏈接壓力。
第二個演進過程:隨着公司業務的不斷增加,系統的運行的壓力也愈來愈大了,咱們已經瞭解了系統的第一個瓶頸是從存儲開 始了,如是咱們開始談論方案如何解決存儲的問題,這時咱們發現咱們已經作了讀寫分離,也使用了緩存,甚至連搜索技術也用上了,那麼下個階段就是垂直拆分 了,垂直拆分很簡單就是把表從數據庫裏拆出來,單獨建庫建表,可是這種直截了當的方案想一想就能感到這樣的作法彷佛沒有打中系統的痛點,那麼系統的痛點到底 是什麼呢?根據數據庫自己的特性,咱們會發現痛點主要是三個方面組成:
第一個方面:數據庫的鏈接數的限制。原庫的某些表可能承擔數據庫80%的鏈接,極端下甚至能夠超過90%的連 接,並且這些表的業務操做十分的頻繁,當其餘小衆業務的表須要進行操做時候,搞很差由於鏈接數被所有佔用而不得不排隊等待空閒鏈接的出現,那麼這個時候我 們就會考慮把這張表作垂直拆分,這樣就減輕了原數據庫鏈接的壓力,使得數據庫鏈接負載變得比較均衡。
第二個方面是數據庫的讀操做,第三個方面是數據庫的寫操做,雖然把讀和寫分紅兩個方面,可是這兩個方面在咱們作 垂直拆分時候要結合起來考慮。首先咱們要分析下數據庫的寫操做,單獨的寫操做效率都是很高的,無論咱們的寫是單條記錄的寫操做,仍是批量的寫操做,這些寫 操做的數據量就是咱們要去寫的數據的大小,所以控制寫的數據量的大小是一件很容易很自然的操做,因此這些操做不會形成數據庫太大負擔,詳細點的話,對於數 據庫而言,新增操做無非是在原來數據後面追加些記錄,而修改操做或者刪除操做通常都是經過創建了高效索引的字段來定位數據後再進行的操做,所以它的性能也 是很是高的。而讀操做看起來比寫操做簡單(例如:讀操做不存在像事務這些烏七八糟因素的干擾),可是當讀操做面對海量數據時候就嚴重挑戰着數據庫和硬盤的 極限能力,所以讀操做很容易產生瓶頸問題,並且這個瓶頸無論問題表是否讀寫失衡都會面臨的。前文裏我詳細列舉了一個交易表設計的 案例,其中咱們能夠看到數據庫垂直拆分在實際應用裏的運用,在例子裏咱們首先根據業務特色將交易表分紅了實時交易表和歷史交易表,這個作法其實就是將原交 易表的讀和寫進行分離,可是這種分離和純粹的讀寫分離相比會更加有深意,這個深意就是拆分實時和歷史交易表也就是在分拆原表的讀寫操做的關聯性,換句話 說,若是咱們不這麼作的話,那麼交易表的每次寫和每次讀幾乎等價,這樣咱們無法單獨解決讀的性能問題,分出了歷史交易表後咱們再對歷史交易表來作讀的優 化,那麼這也不會影響到寫操做,這樣把問題的複雜度給下降了。在案例裏咱們對歷史交易表進行了業務級別的水平拆分,可是這個拆分是以如何提高讀的效率進行 的,所以前文講到的水平拆分裏主鍵設計方案基本上派不上用場,由於這兩種水平拆分的出發點是不一樣的,那麼使用的手段和達到效果也將不同。
由上所述,咱們能夠把數據庫的水平拆分從新定義下,我在這幾篇文章裏一直講述的水平拆分本質是從數據庫技術來定義的,我把它們稱爲狹義的水平拆分,與狹義相對的就是廣義的水平拆分,例如上文例子裏把交易表根據業務特性分爲實時交易表和歷史交易表,這種行爲也是一種水平拆分,可是這個拆分不會遵照我前面講到主鍵設計方案,可是它的確達到水平拆分的目的,因此這樣的水平拆分就屬於廣義的水平拆分了。
第三個演進過程:到了三個演進過程咱們就會考慮到真正的水平拆分了,也就是上面提到的狹義的水平拆分了,狹義的水平拆分執行的理由有兩個,一個那就是數據量太大了,另外一個是數據表的讀寫的關聯性很難進行拆分了, 這點和垂直拆分有所不一樣,作垂直拆分的考慮不必定是由於數據量過大,例如某種表數據量不大,可是負載太重,很容易讓數據庫達到鏈接的極限值,咱們也會採起 垂直拆分手段來解決問題,此外,咱們想減輕寫操做和讀操做的關聯性,從而能單獨對有瓶頸的寫操做或讀操做作優化設計,那麼咱們也會考慮到垂直拆分,固然數 據量實在是太大的表咱們想優化,首先也會考慮到垂直拆分,由於垂直拆分是針對海量數據優化的起始手段,可是垂直拆分可不必定能解決海量數據的問題。
狹義水平拆分的使用的前提是由於數據量太大,到底多大了,咱們舉個例子來講明下,假如某個電商平臺一天的交易筆數有2億筆,咱們用來存儲數據的關係數據庫單表記錄到了5千萬條後,查詢性能就會嚴重降低,那麼若是咱們把這兩億條數據所有存進這個數據庫,那麼隨着數據的累積,實時交易查詢基本已經無法正常完成了,這個時候咱們就得考慮把實時交易表進行狹義的水平拆分,狹義的水平拆分首先碰到的難點就是主鍵設計的問題,主鍵設計問題也就說明狹義水平拆分其實解決的是海量數據寫的問題,若是這張表讀操做不多,或者基本沒有,這個水平拆分是很好設計的,可是一張表只寫不讀,對於做爲業務系統的後臺數據庫那基本是很是罕見的,。
前文講到的主鍵設計方案其實基本沒有什麼業務上的意義,它解決的主要問題是讓寫入的數據分佈均勻,從而能合理使用存儲資源,可是這個合理分佈式存儲資源卻會給查詢操做帶來極大的問題,甚至有時能夠說狹義水平拆分後數據查詢變得困難就是由這種看起來合理的主鍵設計方案所致。
咱們仍是以實時交易表的實例來講明問題,一個電商平臺下會接入不少不一樣的商戶,可是不一樣的商戶天天產生的交易量是不一樣,也就是說商戶的維度會讓咱們使交易數據變得嚴重的不均衡,可能電商平臺下不到5%的商戶完成了全天交易量的80%,而其餘95%的商戶僅僅完成20%的交易量,可是做爲業務系統的數據表,進行讀操做首先被限制和約束的條件就是商戶號,若是要爲咱們設計的實時交易表進行狹義的水平拆分,作拆分前咱們要明確這個拆分是由交易量大的少許商戶所致,而不是所有的商戶所致的。若是按照均勻分佈主鍵的設計方案, 不加商戶區分的分佈數據,那麼就會發生產生少許交易數據的商戶的查詢行爲也要承受交易量大的商戶數據的影響,而能產生大量交易數據的商戶也沒有由於本身的 貢獻度而獲得應有的高級服務,碰到這個問題其實很是好解決,就是在作狹義水平拆分前,咱們先作一次廣義的水平拆分,把交易量大的商戶交易和交易量小的商戶 交易拆分出來,交易量小的商戶用一張表記錄,這樣交易量小的商戶也會很happy的查詢出須要的數據,內心也是美滋滋的。接下來咱們就要對交易量大的商戶的交易表開始作狹義的水平拆分了,爲這些重點商戶作專門的定製化服務。
作狹義水平拆分前,咱們有個問題須要過一下,在狹義水平拆分前咱們須要先作一下廣義的水平拆分嗎?這個我這裏很差說,具體要看實際的業務場景,可是針對我列舉的實時交易的例子而言,我以爲沒那個必要,所以拆分出的重點商戶交易量原本就很大,每一個都在挑戰數據庫讀能力的極限,更重要的是實時交易數據的時間粒度已經很小了,再去作廣義水平拆分難度很大,並且很難作好,因此這個時候咱們仍是直接使用狹義的水平拆分。拆分完畢後咱們就要解決查詢問題了。
作實時查詢的標準作法就是分頁查詢了,在講述如何解決分頁查詢前,咱們看看咱們在淘寶裏搜索【衣服】這個條件的分頁狀況,以下圖所示:
咱們看到一共才100頁,淘寶上衣服的商品最多了,竟然搜索出來的總頁數只有100頁,這是否是在挑戰咱們的常識啊,淘寶的這個作法也給咱們在實現水 平拆分後如何作分頁查詢一種啓迪。要說明這個啓迪前咱們首先要看看傳統的分頁是如何作的,傳統分頁的作法是首先使用select count(1) form table這樣的語句查詢出須要查詢數據的總數,而後再根據每頁顯示的記錄條數,查詢出須要顯示的記錄,而後頁面根據記錄總數,每頁的條數,和查詢的結果 來完成分頁查詢。回到咱們的交易表 實例裏,有一個重要商戶在作實時交易查詢,但是這個時候該商戶已經產生了1千萬筆交易了,假如每頁顯示10條,記錄那麼咱們就要分紅100萬頁,這要是真 顯示在頁面上,絕對能讓咱們這些開發人員像哥倫布發現新大陸那樣驚奇,反正我見過的最多分頁也就是200多頁,仍是在百度搜索發現的。其實當數據庫一 張表的數據量很是大的時候,select的count查詢效率就很是低下,這個查詢有時也會近似個全表檢索,因此count查詢還沒結束咱們就會失去等待 結果的耐心了,更不要是說等把數據查詢出來了,因此這個時候咱們能夠學習下淘寶的作法,當商戶第一次查詢咱們准許他查詢有限的數據。我本身所作的一個項目 的作法就是這樣的,當某個商戶的交易量實在是很大時候咱們其實不會計算數據的總筆數,而是一次性查詢出1000條數據,這1000條數據查詢出來後存入到 緩存裏,頁面則只分100頁,當用戶必定要查詢100頁後的數據,咱們再去追加查詢,不過實踐下來,商戶基本不多會查詢100頁後的數據,經常看了5,6 頁就會中止查詢了。不過商戶也時常會有查詢所有數據的需求,可是商戶有這種需求的目的也不是想在分頁查詢裏看的,通常都是爲了比對數據使用的,這個時候我 們通常是提供一個發起下載查詢所有交易的功能頁面,商戶根據本身的條件先發起這樣的需求,而後咱們系統會在後臺單獨起個線程查詢出所有數據,生成一個固定格式的文件,最後經過一些有效手段通知商戶數據生成好了,讓商戶下載文件便可。
對於進行了狹義水平拆分的表作分頁查詢咱們一般都不會是全表查詢,而是抽取全局的數據的一部分結果呈現給用戶,這個作法其實和不少市場調查的方式類 似,市場調查咱們一般是找一些樣本採集相關數據,經過分析這些樣本數據推導出全局的一個發展趨勢,那麼這些樣本選擇的合理性就和最終的結論有很大關係,回 到狹義水平拆分的表作分頁查詢,咱們爲了及時知足用戶需求,咱們只是取出了所有數據中的一部分,可是這一部分數據是否知足用戶的需求,這個問題是頗有學問 的,若是是交易表, 咱們每每是按時間前後順序查詢部分數據,因此這裏其實使用到了一個時間的維度,其餘業務的表可能這個維度會不同,但確定是有個維度約束咱們到底返回那些 部分的數據。這個維度能夠用一個專有的名詞指代那就是排序,具體點就是要那個字段進行升序仍是降序查詢,看到這裏確定有人會有異議,那就是這種抽樣式的查 詢,確定會致使查詢的命中率的問題,即查出來的數據不必定所有都是咱們要的,其實要想讓數據排序正確,最好就是作全量排序,可是一到全量排序那就是全表查 詢,作海量數據的全表排序查詢對於分頁這種場景是沒法完成的。回到淘寶的例子,咱們相信淘寶確定沒有返回所有數據,而是抽取了部分數據分頁,也就是淘寶查 詢時候加入了維度,每一個淘寶的店家都但願本身的商戶放在搜索的前列,那麼淘寶就可讓商家掏錢,付了錢之後淘寶改變下商家在這個維度裏的權重,那麼商家的 商品就能夠排名靠前了。
狹義水平拆分的自己對排序也有很大的影響,水平拆分後咱們一個分頁查詢可能要從不一樣數據庫不一樣的物理表裏去取數據,單表下咱們能夠先經過數據庫的 排序算法獲得必定的數據,可是局部的排序到了全局可能就不正確了,這個又該怎麼辦了?其實由上面內容咱們能夠知道要知足對海量數據的全部查詢限制是很是難 的,時常是根本就沒法知足,咱們只能作到儘可能多知足些查詢限制,也就是海量查詢只能作到儘可能接近查詢限制的條件,而很難徹底知足,這個時候我前面提到的主 鍵分佈方案就能起到做用了,咱們在設計狹 義水平拆分表主鍵分佈時候是儘可能保持數據分佈均衡,那麼若是咱們查詢要從多張不一樣物理表裏取的時候,例如咱們要查1000條數據,而狹義水平拆分出了兩個 物理數據庫,那麼咱們就能夠每一個數據庫查詢500條,而後在服務層歸併成1000條數據,在服務層排序,這種場景下若是咱們的主鍵設計時候還包含點業務意 義,那麼這個排序的精確度就會獲得很大提高。假如用戶對排序不敏感,那就更好作了,分頁時候若是每頁規定顯示10條,咱們能夠把10條數據平均分配給兩個 數據庫,也就是顯示10條A庫的數據,再顯示5條B庫的數據。
看到這裏有些細心的朋友可能還會有疑問,那就是竟然排序是分頁查詢的痛點,那麼咱們能夠不用數據庫查詢,而使用搜索技術啊,NoSql數據庫啊,的確這些技術能夠更好的解決分頁問題,可是關係數據庫過 渡到搜索引擎和NoSql數據庫首先須要咱們轉化數據,而狹義的水平拆分的數據表自己數據量很大,這個轉化過程咱們是無法快速完成的,若是咱們對延時容忍 度那麼高,其實咱們就不必去作數據庫的狹義水平拆分了。這個問題反過來講明瞭使用狹義拆分數據表的業務場景,那就是:針對數據量很大的表同時該表的讀寫的關聯性是無法有效拆分的。
最後我要講的是,若是系統到了狹義水平拆分都無法解決時候,咱們就要拋棄傳統的關係數據方案了,將該業務所有使用NoSql數據庫解決或者像不少大型互聯網公司那樣,改寫開源的mysql數據庫。文章寫道這裏,我仍是想說一個觀點,若是一個系統有很強烈需求去作狹義的水平拆分,那麼這個公司的某個業務那確定是很是的大了,因此啊,這個方案以公司爲單位應該有點小衆了。
本文開篇提個問題給你們,關係數據庫的瓶頸有哪些?我想有些朋友看到這個問題確定會說出本身平時開發中碰到了一個跟數據庫有關的什麼什麼問題,而後 如何解決的等等,這樣的答案沒問題,可是卻沒有表明性,若是出現了一個新的存儲瓶頸問題,你在那個場景的處理經驗能夠套用在這個新問題上嗎?這個真的很難 說。
其實無論什麼樣的問題場景最後解決它都要落實到數據庫的話,那麼這個問題場景必定是擊中了數據庫的某個痛點,那麼我前面的六篇文章裏那些手段究竟是在解決數據庫的那些痛點,下面我總結下,具體以下:
痛點一:數據庫的鏈接數不夠用了。換句話說就是在同一個時間內,要求和數據庫創建鏈接的請求超出了數據庫所容許的最大鏈接數,若是 咱們對超出的鏈接數沒有進行有效的控制讓它們直接落到了數據庫上,那麼就有可能會讓數據庫不堪重負,那麼咱們就得要分散這些鏈接,或者讓請求排隊。
痛點二:對於數據庫表的操做無非兩種一種是寫操做,一種是讀操做,在現實場景下很難出現讀寫都成問題的事情,每每是其中一種表的操 做出現了瓶頸問題所引發的,因爲讀和寫都是操做同一個介質,這就致使若是咱們不對介質進行拆分去單獨解決讀的問題或者寫的問題會讓問題變的複雜化,最後很 難從根本上解決問題。
痛點三:實時計算和海量數據的矛盾。本系列講存儲瓶頸問題其實有一個範疇的,那就是本系列講到的手段都是在使用關係數據庫來完成實 時計算的業務場景,而現實中,數據庫裏表的數據都會隨着時間推移而不斷增加,當表的數據超出了必定規模後,受制於計算機硬盤、內存以及CPU自己的能力, 咱們很難完成對這些數據的實時處理,所以咱們就必需要採起新的手段解決這些問題。
我今天之因此總結下這三個痛點,主要是爲了告訴你們當咱們面對存儲瓶頸問題時候,咱們要把問題最終落實到這個問題究竟是由於觸碰到了數據庫的那些痛點,這樣回過頭來再看我前面說到的技術手段,我就會知道該用什麼手段來解決問題了。
好了,多餘的話就說到這裏,下面開始本篇的主要內容了。首先給大夥看一張有趣的漫畫,以下圖所示:
身爲程序員的我看到這個漫畫感到很沮喪,由於咱們被機器戰勝了。可是這個漫畫同時提醒了作軟件的程序員,軟件的性能其實和硬件有着不可分割的關係,也許咱們碰到的存儲問 題不必定是由咱們的程序產生的,而是由於好的炮彈裝進了一個老舊過期的大炮裏,最後固然咱們會感到炮彈的威力沒有達到咱們的預期。除此以外了,也有可能我 們的程序設計自己沒有有效的利用好已有的資源,因此在前文裏我提到若是咱們知道存儲的瓶頸問題將會是網站首先發生問題的地方,那麼在數據庫建模時候咱們要儘可能減輕數據庫的計算功能,只保留數據庫最基本的計算功能,而複雜的計算功能交由數據訪問層完成,這實際上是爲解決瓶頸問題打下了一個良好的基礎。最後我想強調一點,做爲軟件工程師常常會不自覺地忽視硬件對程序性能的影響,所以在設計方案時候考察下硬件和問題場景的關係或許能開拓咱們解決問題的思路。
上面的問題按本篇開篇的痛點總結的思路總結下的話,那麼就是以下:
痛點四:當數據庫所在服務器的硬件有很大提高時候,咱們能夠優先考慮是否能夠經過提高硬件性能的手段來提高數據庫的性能。
在本系列的第一篇裏,我講到根據http無狀態的特色,咱們能夠經過剝離web服務器的狀態性主要是session的功能,那麼當網站負載增大咱們可 以經過增長web服務器的方式擴容網站的併發能力。其實無論是讀寫分離方案,垂直拆分方案仍是水平拆分方案細細體會下,它們也跟水平擴展web服務的方式 有相似之處,這個相似之處也就是經過增長新的服務來擴展整個存儲的性能,那麼新的問題來了,前面的三種解決存儲瓶頸的方案也能作到像web服務那樣的水平 擴展嗎?換句話說,當方案執行一段時間後,又出現了瓶頸問題,咱們能夠經過增長服務器就能解決新的問題嗎?
要回答清楚這個問題,咱們首先要詳細分析下web服務的水平擴展原理,web服務的水平擴展是基於http協議的無狀態,http的無狀態是指不一樣的 http請求之間不存在任何關聯關係,所以若是後臺有多個web服務處理http請求,每一個web服務器都部署相同的web服務,那麼無論那個web服務 處理http請求,結果都是等價的。這個原理若是平移到數據庫,那麼就是每一個數據庫操做落到任意一臺數據庫服務器都是等價的,那麼這個等價就要求每一個不一樣的物理數據庫都得存儲相 同的數據,這麼一來就無法解決讀寫失衡,解決海量數據的問題了,固然這樣作看起來彷佛能夠解決鏈接數的問題,可是面對寫操做就麻煩了,由於寫數據時候咱們 必須保證兩個數據庫的數據同步問題,這就把問題變複雜了,因此web服務的水平擴展是不適用於數據庫的。這也變相說明,分庫分表的數據庫自己就擁有很強的 狀態性。
不過web服務的水平擴展還表明一個思想,那就是當業務操做超出了單機服務器的處理能力,那麼咱們能夠經過增長服務器的方式水平拓展整個web服務器的處理能力,這個思想放到數據庫而言,確定是適用的。那麼咱們就能夠定義下數據庫的水平擴展,具體以下:
數據庫的水平擴展是指經過增長服務器的方式提高整個存儲層的性能。
數據庫的讀寫分離方案,垂直拆分方案還有水平拆分方案其實都是以表爲單位進行的,假如咱們把數據庫的表做爲一個操做原子,讀寫分離方案和垂直拆分方案 都沒有打破錶的原子性,而且都是以表爲着力點進行,所以若是咱們增長服務器來擴容這些方案的性能,確定會觸碰表原子性的紅線,那麼這個方案也就演變成了水 平拆分方案了,由此咱們能夠得出一個結論:
數據庫的水平擴展基本都是基於水平拆分進行的,也就是說數據庫的水平擴展是在數據庫水平拆分後再進行一次水平拆分,水平擴展的次數 也就表明的水平拆分迭代的次數。所以要談好數據庫的水平擴展問題,咱們首先要更加細緻的分析下水平拆分的方案,固然這裏所說的水平拆分方案指的是狹義的水 平拆分。
數據庫的水平擴展其實就是讓被水平拆分的表的數據跟進一步的分散,而數據的離散規則是由水平拆分的主鍵設計方案所決定的,在前文裏我推崇了一個使用 sequence及自增列的方案,當時我給出了兩種實現手段,一種是經過設置不一樣的起始數和相同的步長,這樣來拆分數據的分佈,另外一種是經過估算每臺服務 器的存儲承 載能力,經過設定自增的起始值和最大值來拆分數據,我當時說到方案一咱們能夠經過設置不一樣步長的間隔,這樣咱們爲咱們以後的水平擴展帶來便利,方案二起始 也能夠設定新的起始值也來完成水平擴展,可是無論哪一個方案進行水平擴展後,有個新問題咱們不得不去面對,那就是數據分配的不均衡,由於原有的服務器會有歷 史數據的負擔問題。而在我談到狹義水平拆分時候,數據分配的均勻問題曾被我做爲水平技術拆分的優勢,可是到了擴展就出現了數據分配的不均衡了,數據的不均 衡會形成系統計算資源利用率混亂,更要命的是它還會影響到上層的計算操做,例如海量數據的排序查詢,由於數據分配不均衡,那麼局部排序的誤差會變得更大。 解決這個問題的手段只有一個,那就是對數據根據平均原則從新分佈,這就得進行大規模的數據遷移了,因而可知,除非咱們以爲數據是否分佈均勻對業務影響不 大,不須要調整數據分佈,那麼這個水平擴展仍是頗有效果,可是若是業務系統不能容忍數據分佈的不均衡,那麼咱們的水平擴展就至關於從新作了一遍水平拆分, 那是至關的麻煩。其實這些還不是最要命的,若是一個系統後臺數據庫要作水平擴展,水平擴展後又要作數據遷移,這個擴展的表仍是一個核心業務表,那麼方案上線時候必然致使數據庫中止服務一段時間。
數據庫的水平擴展本質上就是水平拆分的迭代操做,換句話說水平擴展就是在已經進行了水平拆分後再拆分一次,擴展的主要問題就是新的水平拆分是否能繼承前一次的水平拆分,從而實現只作少許的修改就能達到咱們的業務需求,那麼咱們若是想解決這個問題就得回到問題的源頭,咱們的前一次水平拆分是否能良好的支持後續的水平拆分,那麼爲了作到這點咱們到底要注意哪些問題呢?我我的認爲應該主要注意兩個問題,它們分別是:水平擴展和數據遷移的關係問題以及排序的問題。
問題一:水平擴展和數據遷移的關係問題。在我上邊的例子裏,咱們所作的水平拆分的主鍵設計方案都 是基於一個平均的原則進行的,若是新的服務器加入後就會破壞數據平均分配的原則,爲了保證數據分佈的均勻咱們就不能不將數據作相應的遷移。這個問題推而廣 之,就算咱們水平拆分沒有過度強調平均原則,或者使用其餘維度來分割數據,若是這個維度在水平擴展時候和原庫原表有關聯關係,那麼結果都有可能致使數據的 遷移問題,由於水平擴展是很容易產生數據遷移問題。
對於一個實時系統而言,核心的業務表發生數據遷移是一件風險很大成本很高的事情,拋開遷移的操做危險,數據遷移會致使系統停機,這點是全部系統相關方 很難接受的。那麼如何解決水平擴展的數據遷移問題了,那麼這個時候一致性哈希就派上用場了,一致性哈希是固定哈希算法的衍生,下面咱們就來簡單介紹下一致 性哈希的原理,首先我看看下面這張圖:
一致性哈希使用時候首先要計算出用來作水平拆分服務器的數字哈希值,並將這些哈希值配置到0~232的圓上,接着計算出被存儲數據主鍵的數字哈希值,並把它們映射到這個圓上,而後從數據映射到的位置開始順時針查找,並將數據保存在找到的第一個服務器上,若是主鍵的哈希值超過了232,那麼該記錄就會保存在第一臺服務器上。這些如上圖的第一張圖。
那麼有一天咱們要添加新的服務器了,也就是要作水平擴展了,如上圖的第二張圖,新節點(圖上node5)只會影響到的原節點node4,即順時針方向的第一個節點,所以一致性哈希能最大限度的抑制數據的從新分佈。
上面的例圖裏咱們只使用了4個節點,添加一個新節點影響到了25%左右的數據,這個影響度仍是有點大,那有沒有辦法還能下降點影響了,那麼咱們能夠在 一致性哈希算法的基礎上進行改進,一致性哈希上的分佈節點越多,那麼添加和刪除一個節點對於整體影響最小,可是現實裏咱們不必定真的是用那麼多節點,那麼 咱們能夠增長大量的虛擬節點來進一步抑制數據分佈不均衡。
前文裏我將水平拆分的主鍵設計方案類比分佈式緩存技術memcached,其實水平拆分在數據庫技術裏也有一個專屬的概念表明他,那就是數據的分區,只不過水平拆分的這個分區粒度更大,操做的動靜也更大,筆者這裏之因此提這個主要是由於寫存儲瓶頸必定會受到我本身經驗和知識的限制,若是有朋友由於看了本文而對存儲問題發生了興趣,那麼我這裏也能夠指明一個學習的方向,這樣就能避免一些價值不高的探索過程,讓學習的效率會更高點。
問題二:水平擴展的排序問題。當咱們要作水平擴展時候確定有個這樣的因素在做怪:數據量太大了。前文裏我說道過 海量數據會對讀操做帶來嚴重挑戰,對於實時系統而言,要對海量數據作實時查詢幾乎是件沒法完成的工做,可是現實中咱們仍是須要這樣的操做,但是當碰到如此 操做咱們通常採起抽取部分結果數據的方式來知足查詢的實時性,要想讓這些少許的數據能讓用戶滿意,而不會產生太大的業務誤差,那麼排序就變變得十分重要 了。
不過這裏的排序必定要加上一個範疇,首先咱們要明確一點啊,對海量數據進行全排序,而這個全排序還要以實時的要求進行,這個是根本沒法完成的,爲何 說沒法完成,由於這些都是在挑戰硬盤讀寫速度,內存讀寫速度以及CPU的運算能力,假如1Tb的數據上面這三個要素不包括排序操做,讀取操做能在10毫秒 內完成,也許海量數據的實時排序纔有可能,可是目前計算機是絕對沒有這個能力的。
那麼現實場景下咱們是如何解決海量數據的實時排序問題的呢?爲了解決這個問題咱們就必須有點逆向思惟的意識了,另闢蹊徑的處理排序難題。第一種方式就是縮小須要排序的數據大小,那麼數據庫的 分區技術是一個很好的手段,除了分區手段外,其實還有一個手段,前面我講到使用搜索技術能夠解決數據庫讀慢的難題,搜索庫自己能夠當作一個讀庫,那麼搜索 技術是怎麼來解決快速讀取海量數據的難題了,它的手段是使用索引,索引比如一本書的目錄,咱們想從書裏檢索咱們想要的信息,咱們最有效率的方式就是先查詢 目錄,找到本身想要看的標題,而後對應頁碼,把書直接翻到那一頁,存儲系 統索引的本質和書的目錄同樣,只不過計算機領域的索引技術更加的複雜。其實爲數據創建索引,自己就是一個縮小數據範圍和大小的一種手段,這點它和分區是類 似的。咱們其實能夠把索引當作一張數據庫的映射表,通常存儲系統爲了讓索引高效以及爲了擴展索引查找數據的精確度,存儲系統在創建索引的時候還會跟索引建 立好排序,那麼當用戶作實時查詢時候,他根據索引字段查找數據,由於索引自己就有良好的排序,那麼在查詢的過程裏就能夠免去排序的操做,最終咱們就能夠高 效的獲取一個已經排好序的結果集。
如今咱們回到水平拆分海量數據排序的場景,前文裏我提到了海量數據作分頁實時查詢能夠採用一種抽樣的方式進行,雖然用戶的意圖是想進行海量數據查詢, 可是人不可能一會兒消化掉所有海量數據的特色,所以咱們能夠只對海量數據的部分進行操做,但是因爲用戶的本意是全量數據,咱們給出的抽樣數據如何能更加精 確點,那麼就和咱們在分佈數據時候分佈原則有關係,具體落實的就是主鍵設計方案了,碰到這樣的場景就得要求咱們的主鍵具備排序的特色,那麼咱們就不得不探 討下水平拆分裏主鍵的排序問題了。
在前文裏我提到一種使用固定哈希算法來設計主鍵的方案,當時提到的限制條件就是主鍵自己沒有排序特性,只有惟一性,所以哈希出來的值是惟一的,這種哈希方式其實不能保證數據分佈時候每臺服務器上落地數據有一個前後的時間順序,它只能保證在海量數據存儲分佈式時候各個服務器近似均勻,所以這樣的主鍵設計方案碰 到分頁查詢有排序要求時候實際上是起不到任何做用的,所以若是咱們想讓主鍵有個前後順序最好使用遞增的數字來表示,可是遞增數字的設計方案若是按照我前面的 起始數,步長方式就會有一個問題,那就是單庫單表的順序性能夠保障,跨庫跨表之間的順序是很難保證的,這也說明咱們對於水平拆分的主鍵字段對於邏輯表進行 全排序也是一件沒法完成的任務。
那麼咱們到底該如何解決這個問題了,那麼咱們只得使用單獨的主鍵生成服務器了,前文裏我曾經批評了主鍵生成服務器方案,文章發表後有個朋友找到我談論 了下這個問題,他說出了他們計劃的一個作法,他們本身研發了一個主鍵生成服務器,由於懼怕這個服務器單點故障,他們把它作成了分佈式,他們本身設計了 一套簡單的UUID算法,使得這個算法適合集羣的特色,他們打算用zookeeper保證這個集羣的可靠性,好了,他們作法裏最關鍵的一點來了,如何保證 主鍵獲取的高效性,他說他們沒有讓每次生成主鍵的操做都是直接訪問集羣,而是在集羣和主鍵使用者之間作了個代理層,集羣也不是頻繁生成主鍵的,而是每次生 成一大批主鍵,這一大批主鍵值按隊列的方式緩存在代理層了,每次主鍵使用者獲取主鍵時候,隊列就消耗一個主鍵,固然他們的系統還會檢查主鍵使用的比率,當 比率到達閥值時候集羣就會收到通知,立刻開始生成新的一批主鍵值,而後將這些值追加到代理層隊列裏,爲了保證主鍵生成的可靠性以及主鍵生成的連續性,這個 主鍵隊列只要收到一次主鍵請求操做就消費掉這個主鍵,也不關心這個主鍵究竟是否真的被正常使用過,當時我還提出了一個本身的疑問,要是代理掛掉了呢?那麼 集羣該如何再生成主鍵值了,他說他們的系統沒有單點系統,就算是代理層也是分佈式的,因此很是可靠,就算所有服務器全掛了,那麼這個時候主鍵生成服務器集羣也不會再重複生成已經生成過的主鍵值,固然每次生成完主鍵值後,爲了安全起見,主鍵生成服務會把生成的最大主鍵值持久化保存。
其實這位朋友的主鍵設計方案其實核心設計起點就是爲了解決主鍵的排序問題,這也爲實際使用單獨主鍵設計方案找到了一個很現實的場景。若是能作到保證主 鍵的順序性,同時數據落地時候根據這個順序依次進行的,那麼在單庫作排序查詢的精確度就會很高,查詢時候咱們把查詢的條數均勻分佈到各個服務器的表上,最 後彙總的排序結果也是近似精確的。
自從和這位朋友聊到了主鍵生成服務的設計問題後以及我今天講到的一致性哈希的問題,我如今有點摒棄前文裏說到的固定哈希算法的主鍵設計方案了, 這個摒棄也是有條件限制的,主鍵生成服務的方案實際上是讓固定哈希方案更加完善,可是若是主鍵自己沒有排序性,只有惟一性,那麼這個作法對於排序查詢起不到 什麼做用,到了水平擴展,固定哈希排序的擴展會致使大量數據遷移,風險和成本過高,而一致性哈希是固定哈希的進化版,所以當咱們想使用哈希來分佈數據時 候,還不如一開始就使用一致性哈希,這樣就爲後續的系統升級和維護帶來很大的便利。
有網友在留言裏還提到了哈希算法分佈數據的一個問題,那就是硬件的性能對數據平均分配的影響,若是水平拆分所使用的服務器性能存在差別,那麼平均分配 是會形成熱點問題的出現,若是咱們不去改變硬件的差別性,那麼就不得不在分配原則上加入權重的算法來動態調整數據的分佈,這樣就製造了人爲的數據分佈不均 衡,那麼到了上層的計算操做時候某些場景咱們也會不自覺的加入權重的維度。可是做爲筆者的我對這個作法是有異議的,這些異議具體以下:
異議一:我我的認爲無論什麼系統引入權重都是把問題複雜化的操做,權重每每都是權益之計,若是隨着時間推移還要進一步擴展權重算 法,那麼問題就變得越加複雜了,並且我我的認爲權重是很難進行合理處理的,權重若是還要演進會變得異常複雜,這個複雜度可能會遠遠超出分佈式系統,數據拆 分自己的難度,所以除非無可奈何咱們仍是儘可能不去使用什麼權重,就算有權重也不要輕易使用,看有沒有方式能夠消除權重的根本問題。
異議二:若是咱們的系統後臺數據庫都是使用獨立服務器,那麼通常都會讓最好的服務器服務於數據庫,這個作法自己就說明了數據庫的重 要性,並且咱們對數據庫的任何分庫分表的解決方案都會很麻煩,很繁瑣甚至很危險,所以本篇開始提出了若是咱們解決瓶頸問題前先考慮下硬件的問題,若是硬件 能夠解決掉問題,優先採起硬件方案,這就說明咱們合理對待存儲問題的前提就是讓數據庫的硬件跟上時代的要求,那麼若是有些硬件出現了性能瓶頸,是否是咱們 忽視了硬件的重要性了?
異議三:均勻分佈數據不只僅能夠合理利用計算資源,它還會給業務操做帶來好處,那麼咱們擴展數據庫時候就讓各個服務器自己能力均 衡,這個其實不難的,若是老的服務器實在太老了,用新服務器替換掉,雖然會有全庫遷移的問題,可是這麼粗粒度的數據平移,那但是比任何拆分方案的數據遷移 難度低的多的。
在開始本篇主要內容前,咱們一塊兒看看下面的幾張截圖,首先是第一張圖,以下圖所示:
這是一家電商網站的首頁,當咱們第一次打開這個首頁,網站會彈出一個強制性的對話框,讓用戶選擇貨物配送的地址,若是是淘寶和京東的話,那麼這個選擇配貨地址的選項是在商品裏,以下圖是淘寶的選擇配送地點:
下圖是京東選擇配貨地點:
那麼圖一跟京東和淘寶有什麼區別呢?圖一的電商強制用戶選擇地區後,那麼咱們在查詢這個商品時候會由於地區不一樣,顯示的查詢結果會不同,這個就和網 站作國際化有點像,不過網站國際化是切語言和語言相關的靜態資源,可是電商這個地域的選擇是和業務相關的,不一樣的地域查詢結果是不相同的,這個選擇地域的 彈出框很像一個路由器。相比之下,淘寶和京東把商品的配送和 商品相關,那麼咱們在這些網站裏查詢商品時候,實際上是按照全國查詢的,全國不一樣的地方查詢同一個條件所得到到的結果是一致的。從業務角度而言,這說明第一 個電商的業務沒有全國鋪開,就算是鋪開了,地域的差別也影響到物流的問題,而淘寶和京東則正是一個全國意義的大型電商網站了。
回到技術的角度,這兩種不一樣的作法有沒有可能還和技術問題有關了?今天我就來探討下這個問題。
無論網站大 與小,一個網站確定能夠分爲客戶端、服務端和存儲端,勾連這不一樣的組成部分是網絡,網絡是一種通信設施,距離的遠近會直接影響到網絡傳輸的效率問題,如是 乎就出現了像CDN這樣的技術,不少大型互聯網公司還會在不一樣的城市創建機房,這些手段的目的就是在解決距離對網絡傳輸效率的影響,可是當這種就近解決問 題的方案落到存儲層的時候,問題就來了。上篇裏我說道web服務的水平擴展問題,這種水平擴展是基於一種無狀態性的原理設計的,可是到了存儲層咱們無論怎 麼拆分它,它都很難消除狀態的問題,也就是存儲層有狀態性是它的自然屬性。特別是碰到一個競爭性的存儲資源時候,這種狀態性會變得很是頑固,例如商品的庫 存問題,若是咱們把庫存數據對等的平移到不一樣地域的數據中心,那麼如何保證不一樣地方的庫存信息老是準確的,這就成爲了難題。這種問題放在一個小國家不是什 麼問題,可是放到地大物博的中國那就很成問題了。因此存儲是這種就近方案的短板了。
我曾瞭解到中國一家大型信息企業在設計它 們第一代系統時候,就考慮到了這種地域性差別對系統設計的影響,它們的第一代系統在存儲層這塊就設計成了一個雙核系統,什麼叫作存儲層的雙核系統了?它們 的作法是在北京和上海分別創建兩個數據中心,系統的存儲層分別部署在北京的數據中心和上海的數據中心,兩個數據中心是等價的,那麼中國北部的交易就走北京 數據中心,中國南部的交易就走上海的數據中心。可是系統上線後,發現這種雙核設計方案成 爲了整個系統的夢魘了,這個夢魘的最核心的問題就是數據的同步問題,由於該企業是一個全國性業務的企業,所以有大量交易須要南北數據中心同步完數據後才能 正常完成,可是想從北京和上海同步數據的效率是異常的低效,我曾經看過一份資料,裏面說有機構作了一個測試,當兩個數據中心的距離超過了80千米,那麼網 絡的延遲性基本是沒法忍受的,固然不差錢的企業能夠專門鋪設專線來鏈接兩個數據中心,這種專線的成本高的嚇人,我曾聽人說就在上海,若是鋪專線從浦東到浦 西,那麼這條專線基本是用人民幣鋪就的,更況且是從北京到上海鋪專線,就算企業不差這些錢延遲性也嚴重影響了企業業務的發展。除了延遲性外,經過網絡大規 模傳輸數據,數據的可靠性是很難保證的,也就是網絡傳輸時候常常沒有道理的丟包,這就形成了不少重複性傳輸,使得同步數據的效率更加的低效。
由於存儲層 這種雙核設計缺陷,該企業立刻從事了二代系統的設計和開發,而這個二代系統核心業務就是解決這個存儲層的雙核問題。那到底該怎麼解決了?把雙核變成單核, 既然兩個數據中心這麼麻煩,那咱們就搞一個數據中心算了,既省錢有沒那麼多麻煩事情,這個確定不是解決問題的正確思路了,雙核設計的出發點是很是有現實意 義和價值的,最後該公司使用了一個新的方案替代雙核,這個方案稱之爲主備方案,存儲層任然部署到兩個數據中心,到了業務運行階段,一個數據中心爲主,一個 數據中心爲輔,不過這個主備方案毫不是一般意義的數據備份方案,他實際上是吸取了單核和雙核方案的優勢,同時儘可能避免單核和雙核的缺點,那麼這點上這個主備 方案是如何作到的呢?
首先咱們仍是要把系統業務交易分下類,系統有些交易對於實時性啊,數據的正確性啊要求很是高,那麼這樣的業務場景使用單核存儲系統比較合適,一個業務 系統不可能全是這樣的實時性交易,也有一些交易對實時性要求比較差,固然咱們仍是得要考察下這種交易對於延時容忍度,具體就是通常延時多久用戶是能夠接受 的,這點很是重要,由於就算是主備方案,那麼數據仍是會有同步的操做,只不過這個同步的時間粒度上會更粗些,咱們能夠以系統和業務角度合理設置一個同步時 間間隔,若是延時性交易的延時時間超過了這個間隔時間的話,那麼這樣的業務場景實際上是能夠就近處理的,沒有必要將這些請求都發送到主數據中心,這樣能夠減 輕主數據中心的運行壓力。該企業的二代信息系統還有個要求就是過了天天的零點,前一天的數據必須在兩個數據中心完成同步,換句話說,兩個數據中心數據的差別性最大容忍度是天,爲何要這樣作了?有的朋友看到了必定認爲這是爲了備份數據,的確這是目的之一,但這個作法還有更大的深意,雙核設計除了解決距離對網絡效率的影響外,還有個重要的目的就是容災,我記得幾年前,有個朋友告訴我他們公司網站掛 了6個小時,我當時很奇怪,我就問大家系統難道不是分佈式嗎?他說他們線上系統沒有單點,那爲何網站還會整個掛掉了?答案真的讓人不敢相信,由於他們的 機房漏雨了,機房的線路短路了,那個朋友告訴我這件事情之後,他們公司又在附近租了個新機房作容災,防止此類事情再發生了。這種狀況真的能夠稱之爲天災 了,不過這樣的事情機率很低,但是一旦發生就會很是致命,記得日本爆發九級大地震的時候,我看到一個新網報道,報道里面有好多大型計算機倒掉了,而這個機 房的機器的做用幾乎關係到亞洲互聯網系統的命脈,你們都知道每一個網站都有本身的域名,域名是一個網站的入口,而日本那個機房放置的服務器就是全球赫赫有名 的13臺服務器之一,專門用來解析域名的DNS服務器,若是這些機器掛掉了,可能發生一整個國家都不能正常使用互聯網。可是天災畢竟是局部的,所以全國甚 至全球設立不一樣的數據中心用來容災是不少大型互聯網公司必須走的道路,回到本文的主備方案,爲了保證數據中心的容災性,那麼咱們再設計主備方案同時還要保證主備數據中心能夠迅速切換,當一個數據中心出現問題時候能夠立刻把輔助的數據中心轉化爲主數據中心。爲了保證這種切換的可靠性,該企業常常在晚上交易量小的時候,把主備來回切換跑跑。
回到開篇提到的那三張截圖,那個一開始彈出地域選擇框的電商網站,當咱們選擇不一樣的地域時候,查詢一樣的商品最後顯示的商品列表是不一樣,而京東雖然也有地域選擇,可是咱們切換地域後查詢商品後結果基本沒有變化,至於淘寶和天貓壓根就沒有讓咱們選擇地域的選項,配送都是在商品這邊進行選擇的。可能淘寶和天貓沒有自營業務,所以天貓很難控制裏面商家的地域區別,京東和前面哪家電商網站由於大部分是直營業務,所以配送地址和他們倉儲所在地是有關係的,其實這個作法衍生下的話,地域其實還能夠作到數據中心的劃分,例如江滬浙用一個數據中心,中部地區用一個數據中心,那麼這種方式就能夠幫助咱們解決存儲層的就近問題,從這裏咱們彷佛也能夠看出B2C和C2C的業務場景的一些區別。
由此我能夠作一個總結,首先存儲層作到對等多核的體系基本是不可能的,主備的方案能夠解決單核和多核的缺點,同時能夠發揚單核和多核的優勢,距離的遠近也能產生業務的差別性,咱們能夠經過這種差別性把數據中心變成分散式,這樣還能夠解決數據訪問的就近原則。
美國的互聯網公司規模很大,他們從一開始就是全球化的,那麼對於美國的大型互聯網公司將數據中心分散化和本地化就變的很是重要,因此好的存儲層的分佈設計方案是完成網站全 球佈局任務的基礎。可是對於不少中小企業,或者是剛剛創業的公司能在不一樣地域創建數據中心,或者不差錢可是能快速的創建不一樣地域的數據中心實際上是很是難的 事情,那麼這個時候咱們找一家全球性的雲平臺例如亞馬遜的雲平臺,或者咱們的業務就侷限在中國,使用個本土優秀的雲平臺也是一種不錯的選擇,雲計算的推廣 使得創業者的成本愈來愈低了。
好了,本系列的文章到此爲止,本系列都是在講數據庫的問題,我曾經說過任何程序或軟件都是計算和存儲的結合體,本系列着重講到的是存儲,時下不少大型 互聯網公司在存儲這塊已經發生了很大的變化,在關係數據庫這塊都已經作到了去商業關係數據庫,而使用開源的關係數據庫,並將這些開源的關係數據進行了大規 模的改造,這個作法應該算是互聯網領域關係數據庫發展的前沿了,同時將關係數據庫很難作到的事情用Nosql數據庫來替代也是一種大趨勢。
本系列講述時候設置了一個很大的前提,那就是儘可能保持關係數據庫存儲的 本性,所以我將不少計算建議遷移到應用層,這個觀點我有不少理由說明它的好處,可是現實中是不是最好的方法,這個就要具體看了,所以我不想去苛求這麼作的 合理性,可是邏輯上合理的方案老是會有不少借鑑意義的,這就是我想表達的,至於關於存儲層的計算我傾向於在數據訪問層裏作,所以按照個人思路,最終這個關 係數據庫存儲層就會變成一個分佈式數據庫,數據訪問層固然也是使用分佈式系統原理來作,講解分佈式系統也是本文章後續想討論,若是我有時間接着寫這個大系 列博客我會在分佈式系統這塊繼續講解數據訪問層的設計問題。
來源:夏天的森林