1. 程式人生 > >關於大型站點技術演進的思考(七)--存儲的瓶頸(7)

關於大型站點技術演進的思考(七)--存儲的瓶頸(7)

平時 算法 images 切割 單位 結果 硬件 支持 權重

本文開篇提個問題給大家,關系數據庫的瓶頸有哪些?我想有些朋友看到這個問題肯定會說出自己平時開發中碰到了一個跟數據庫有關的什麽什麽問題,然後怎樣解決的等等。這種答案沒問題,可是卻沒有代表性。假設出現了一個新的存儲瓶頸問題,你在那個場景的處理經驗能夠套用在這個新問題上嗎?這個真的非常難說。

  事實上無論什麽樣的問題場景最後解決它都要落實到數據庫的話。那麽這個問題場景一定是擊中了數據庫的某個痛點,那麽我前面的六篇文章裏那些手段究竟是在解決數據庫的那些痛點,以下我總結下,詳細例如以下:

  痛點一:數據庫的連接數不夠用了。

換句話說就是在同一個時間內,要求和數據庫建立連接的請求超出了數據庫所同意的最大連接數,假設我們對超出的連接數沒有進行有效的控制讓它們直接落到了數據庫上,那麽就有可能會讓數據庫不堪重負,那麽我們就得要分散這些連接。或者讓請求排隊。

  痛點二:對於數據庫表的操作無非兩種一種是寫操作,一種是讀操作,在現實場景下非常難出現讀寫都成問題的事情。往往是當中一種表的操作出現了瓶頸問題所引起的。因為讀和寫都是操作同一個介質。這就導致假設我們不正確介質進行拆分去單獨解決讀的問題或者寫的問題會讓問題變的復雜化,最後非常難從根本上解決這個問題。

  痛點三:實時計算和海量數據的矛盾。本系列講存儲瓶頸問題事實上有一個範疇的,那就是本系列講到的手段都是在使用關系數據庫來完畢實時計算的業務場景。而現實中,數據庫裏表的數據都會隨著時間推移而不斷增長,當表的數據超出了一定規模後,受制於計算機硬盤、內存以及CPU本身的能力,我們非常難完畢對這些數據的實時處理。因此我們就必需要採取新的手段解決這些問題。

  我今天之所以總結下這三個痛點,主要是為了告訴大家當我們面對存儲瓶頸問題時候。我們要把問題終於落實到這個問題究竟是由於觸碰到了數據庫的那些痛點。這樣回過頭來再看我前面說到的技術手段,我就會知道該用什麽手段來解決這個問題了。

  好了,多余的話就講到這裏,以下開始本篇的主要內容了。首先給大夥看一張有趣的漫畫,例如以下圖所看到的:

技術分享

  身為程序猿的我看到這個漫畫感到非常沮喪,由於我們被機器打敗了。可是這個漫畫同一時候提醒了做軟件的程序猿,軟件的性能事實上和硬件有著不可切割的關系,也許我們碰到的存儲問題不一定是由我們的程序產生的,而是由於好的炮彈裝進了一個老舊過時的大炮裏,最後當然我們會感到炮彈的威力沒有達到我們的預期。除此之外了,也有可能我們的程序設計本身沒有有效的利用好已有的資源,所以在前文裏我提到假設我們知道存儲的瓶頸問題將會是站點首先發生故障的地方,那麽在數據庫建模時候我們要盡量減輕數據庫的計算功能,僅僅保留數據庫最主要的計算功能。而復雜的計算功能交由數據訪問層完畢。這事實上是為解決瓶頸問題打下了一個良好的基礎。最後我想強調一點,作為軟件project師常常會不自覺地忽視硬件對程序性能的影響。因此在設計方案時候考察下硬件和問題場景的關系也許能開拓我們解決這個問題的思路。

  上面的問題按本篇開篇的痛點總結的思路總結下的話。那麽就是例如以下:

  痛點四:當數據庫所在server的硬件有非常大提升時候。我們能夠優先考慮能否夠通過提升硬件性能的手段來提升數據庫的性能。

  在本系列的第一篇裏。我講到依據http無狀態的特點,我們能夠通過剝離webserver的狀態性主要是session的功能。那麽當站點負載增大我們能夠通過添加webserver的方式擴容站點的並發能力。事實上無論是讀寫分離方案,垂直拆分方案還是水平拆分方案細細體會下,它們也跟水平擴展web服務的方式有類似之處。這個類似之處也就是通過添加新的服務來擴展整個存儲的性能,那麽新的問題來了。前面的三種解決存儲瓶頸的方案也能做到像web服務那樣的水平擴展嗎?換句話說。當方案運行一段時間後。又出現了瓶頸問題,我們能夠通過添加server就能解決新的問題嗎?

  要回答清楚這個問題,我們首先要具體分析下web服務的水平擴展原理,web服務的水平擴展是基於http協議的無狀態。http的無狀態是指不同的http請求之間不存在不論什麽關聯關系,因此假設後臺有多個web服務處理http請求。每一個webserver都部署同樣的web服務,那麽無論那個web服務處理http請求,結果都是等價的。

這個原理假設平移到數據庫,那麽就是每一個數據庫操作落到隨意一臺數據庫server都是等價的,那麽這個等價就要求每一個不同的物理數據庫都得存儲同樣的數據,這麽一來就沒法解決讀寫失衡,解決海量數據的問題了。當然這樣做看起來似乎能夠解決連接數的問題,可是面對寫操作就麻煩了,由於寫數據時候我們必須保證兩個數據庫的數據同步問題。這就把問題變復雜了,所以web服務的水平擴展是不適用於數據庫的。這也變相說明。分庫分表的數據庫本身就擁有非常強的狀態性。

  只是web服務的水平擴展還代表一個思想。那就是當業務操作超出了單機server的處理能力。那麽我們能夠通過添加server的方式水平拓展整個webserver的處理能力,這個思想放到數據庫而言。肯定是適用的。那麽我們就能夠定義下數據庫的水平擴展,詳細例如以下:

  數據庫的水平擴展是指通過添加server的方式提升整個存儲層的性能。

  數據庫的讀寫分離方案,垂直拆分方案還有水平拆分方案事實上都是以表為單位進行的,假如我們把數據庫的表作為一個操作原子。讀寫分離方案和垂直拆分方案都沒有打破表的原子性,而且都是以表為著力點進行。因此假設我們添加server來擴容這些方案的性能,肯定會觸碰表原子性的紅線,那麽這個方法也就演變成了水平拆分方案了,由此我們能夠得出一個結論:

  數據庫的水平擴展基本都是基於水平拆分進行的,也就是說數據庫的水平擴展是在數據庫水平拆分後再進行一次水平拆分。水平擴展的次數也就代表的水平拆分叠代的次數。因此要談好數據庫的水平擴展問題。我們首先要更加仔細的分析下水平拆分的方案,當然這裏所說的水平拆分方案指的是狹義的水平拆分。

  數據庫的水平擴展事實上就是讓被水平拆分的表的數據跟進一步的分散,而數據的離散規則是由水平拆分的主鍵設計方案所決定的。在前文裏我推崇了一個使用sequence及自增列的方案。當時我給出了兩種實現手段。一種是通過設置不同的起始數和同樣的步長,這樣來拆分數據的分布,還有一種是通過估算每臺server的存儲承載能力。通過設定自增的起始值和最大值來拆分數據,我當時說到方案一我們能夠通過設置不同步長的間隔,這樣我們為我們之後的水平擴展帶來便利。方案二起始也能夠設定新的起始值也來完畢水平擴展,可是無論哪個方案進行水平擴展後。有個新問題我們不得不去面對,那就是數據分配的不均衡。由於原有的server會有歷史數據的負擔問題。

而在我談到狹義水平拆分時候,數據分配的均勻問題曾被我作為水平技術拆分的長處。可是到了擴展就出現了數據分配的不均衡了,數據的不均衡會造成系統計算資源利用率混亂,更要命的是它還會影響到上層的計算操作。比如海量數據的排序查詢。由於數據分配不均衡,那麽局部排序的偏差會變得更大。解決問題的手段僅僅有一個。那就是對數據依據平均原則又一次分布,這就得進行大規模的數據遷移了,由此可見,除非我們認為數據是否分布均勻對業務影響不大,不須要調整數據分布,那麽這個水平擴展還是非常有效果,可是假設業務系統不能容忍數據分布的不均衡,那麽我們的水平擴展就相當於又一次做了一遍水平拆分。那是相當的麻煩。事實上這些還不是最要命的。假設一個系統後臺數據庫要做水平擴展。水平擴展後又要做數據遷移,這個擴展的表還是一個核心業務表,那麽方案上線時候必定導致數據庫停止服務一段時間。

  數據庫的水平擴展本質上就是水平拆分的叠代操作,換句話說水平擴展就是在已經進行了水平拆分後再拆分一次,擴展的主要問題就是新的水平拆分能否繼承前一次的水平拆分。從而實現僅僅做少量的改動就能達到我們的業務需求。那麽我們假設想解決問題就得回到問題的源頭,我們的前一次水平拆分能否良好的支持興許的水平拆分。那麽為了做到這點我們究竟要註意哪些問題呢?我個人覺得應該主要註意兩個問題。它們各自是:水平擴展和數據遷移的關系問題以及排序的問題

  問題一:水平擴展和數據遷移的關系問題

在我上邊的樣例裏,我們所做的水平拆分的主鍵設計方案都是基於一個平均的原則進行的,假設新的server增加後就會破壞數據平均分配的原則,為了保證數據分布的均勻我們就不能不將數據做對應的遷移。這個問題推而廣之。就算我們水平拆分沒有過分強調平均原則。或者使用其它維度來切割數據,假設這個維度在水平擴展時候和原庫原表有關聯關系,那麽結果都有可能導致數據的遷移問題。由於水平擴展是非常easy產生數據遷移問題。

  對於一個實時系統而言,核心的業務表發生數據遷移是一件風險非常大成本非常高的事情。拋開遷移的操作危急,數據遷移會導致系統停機,這點是全部系統相關方非常難接受的。那麽怎樣解決水平擴展的數據遷移問題了,那麽這個時候一致性哈希就派上用場了,一致性哈希是固定哈希算法的衍生,以下我們就來簡介下一致性哈希的原理,首先我看看以下這張圖:

技術分享

  一致性哈希使用時候首先要計算出用來做水平拆分server的數字哈希值,並將這些哈希值配置到0~232的圓上,接著計算出被存儲數據主鍵的數字哈希值,並把它們映射到這個圓上。然後從數據映射到的位置開始順時針查找,並將數據保存在找到的第一個server上,假設主鍵的哈希值超過了232,那麽該記錄就會保存在第一臺server上。這些如上圖的第一張圖。

  那麽有一天我們要加入新的server了,也就是要做水平擴展了,如上圖的第二張圖。新節點(圖上node5)僅僅會影響到的原節點node4。即順時針方向的第一個節點,因此一致性哈希能最大限度的抑制數據的又一次分布。

  上面的例圖裏我們僅僅使用了4個節點,加入一個新節點影響到了25%左右的數據。這個影響度還是有點大,那有沒有辦法還能減少點影響了,那麽我們能夠在一致性哈希算法的基礎上進行改進。一致性哈希上的分布節點越多,那麽加入和刪除一個節點對於整體影響最小。可是現實裏我們不一定真的是用那麽多節點,那麽我們能夠添加大量的虛擬節點來進一步抑制數據分布不均衡。

  前文裏我將水平拆分的主鍵設計方案類比分布式緩存技術memcached。事實上水平拆分在數據庫技術裏也有一個專屬的概念代表他,那就是數據的分區,僅僅只是水平拆分的這個分區粒度更大,操作的動靜也更大,筆者這裏之所以提這個主要是由於寫存儲瓶頸一定會受到我自己經驗和知識的限制,假設有朋友由於看了本文而對存儲問題發生了興趣。那麽我這裏也能夠指明一個學習的方向,這樣就能避免一些價值不高的探索過程。讓學習的效率會更高點。

  問題二:水平擴展的排序問題。當我們要做水平擴展時候肯定有個這種因素在作怪:數據量太大了。

前文裏我說道過海量數據會對讀操作帶來嚴重挑戰,對於實時系統而言。要對海量數據做實時查詢差點兒是件無法完畢的工作,但是現實中我們還是須要這種操作,但是當碰到如此操作我們一般採取抽取部分結果數據的方式來滿足查詢的實時性。要想讓這些少量的數據能讓用戶愜意,而不會產生太大的業務偏差。那麽排序就變變得十分重要了。

  只是這裏的排序一定要加上一個範疇,首先我們要明白一點啊。對海量數據進行全排序,而這個全排序還要以實時的要求進行,這個是根本無法完畢的。為什麽說無法完畢,由於這些都是在挑戰硬盤讀寫速度,內存讀寫速度以及CPU的運算能力,假如1Tb的數據上面這三個要素不包含排序操作,讀取操作能在10毫秒內完畢。或許海量數據的實時排序才有可能。可是眼下計算機是絕對沒有這個能力的。

  那麽現實場景下我們是怎樣解決海量數據的實時排序問題的呢?為了解決問題我們就必須有點逆向思維的意識了,另辟蹊徑的處理排序難題。第一種方式就是縮小須要排序的數據大小,那麽數據庫的分區技術是一個非常好的手段,除了分區手段外,事實上另一個手段,前面我講到使用搜索技術能夠解決數據庫讀慢的難題,搜索庫本身能夠當做一個讀庫,那麽搜索技術是怎麽來解決高速讀取海量數據的難題了,它的手段是使用索引,索引好比一本書的文件夾。我們想從書裏檢索我們想要的信息,我們最有效率的方式就是先查詢文件夾。找到自己想要看的標題,然後相應頁碼,把書直接翻到那一頁,存儲系統索引的本質和書的文件夾一樣。僅僅只是計算機領域的索引技術更加的復雜。事實上為數據建立索引,本身就是一個縮小數據範圍和大小的一種手段,這點它和分區是類似的。我們事實上能夠把索引當做一張數據庫的映射表。一般存儲系統為了讓索引高效以及為了擴展索引查找數據的準確度,存儲系統在建立索引的時候還會跟索引建立好排序,那麽當用戶做實時查詢時候,他依據索引字段查找數據,由於索引本身就有良好的排序,那麽在查詢的過程裏就能夠免去排序的操作,終於我們就能夠高效的獲取一個已經排好序的結果集。

  如今我們回到水平拆分海量數據排序的場景。前文裏我提到了海量數據做分頁實時查詢能夠採用一種抽樣的方式進行。盡管用戶的意圖是想進行海量數據查詢。但是人不可能一下子消化掉所有海量數據的特點。因此我們能夠僅僅對海量數據的部分進行操作。但是因為用戶的本意是全量數據。我們給出的抽樣數據怎樣能更加精確點,那麽就和我們在分布數據時候分布原則有關系,詳細落實的就是主鍵設計方案了。碰到這種場景就得要求我們的主鍵具有排序的特點,那麽我們就不得不探討下水平拆分裏主鍵的排序問題了。

  在前文裏我提到一種使用固定哈希算法來設計主鍵的方案,當時提到的限制條件就是主鍵本身沒有排序特性,僅僅有唯一性,因此哈希出來的值是唯一的,這種哈希方式事實上不能保證數據分布時候每臺server上落地數據有一個先後的時間順序,它僅僅能保證在海量數據存儲分布式時候各個server近似均勻。因此這種主鍵設計方案碰到分頁查詢有排序要求時候事實上是起不到不論什麽作用的。因此假設我們想讓主鍵有個先後順序最好使用遞增的數字來表示,可是遞增數字的設計方案假設依照我前面的起始數。步長方式就會有一個問題。那就是單庫單表的順序性能夠保障。跨庫跨表之間的順序是非常難保證的,這也說明我們對於水平拆分的主鍵字段對於邏輯表進行全排序也是一件無法完畢的任務。

  那麽我們究竟該怎樣解決問題了。那麽我們僅僅得使用單獨的主鍵生成server了,前文裏我以前批評了主鍵生成server方案,文章發表後有個朋友找到我談論了下這個問題,他說出了他們計劃的一個做法,他們自己研發了一個主鍵生成server,由於害怕這個server單點故障,他們把它做成了分布式,他們自己設計了一套簡單的UUID算法,使得這個算法適合集群的特點。他們打算用zookeeper保證這個集群的可靠性,好了。他們做法裏最關鍵的一點來了,怎樣保證主鍵獲取的高效性。他說他們沒有讓每次生成主鍵的操作都是直接訪問集群,而是在集群和主鍵使用者之間做了個代理層,集群也不是頻繁生成主鍵的。而是每次生成一大批主鍵。這一大批主鍵值按隊列的方式緩存在代理層了,每次主鍵使用者獲取主鍵時候。隊列就消耗一個主鍵,當然他們的系統還會檢查主鍵使用的比率,當比率到達閥值時候集群就會收到通知。立即開始生成新的一批主鍵值。然後將這些值追加到代理層隊列裏,為了保證主鍵生成的可靠性以及主鍵生成的連續性,這個主鍵隊列僅僅要收到一次主鍵請求操作就消費掉這個主鍵,也不關心這個主鍵究竟是否真的被正常使用過,當時我還提出了一個自己的疑問,要是代理掛掉了呢?那麽集群該怎樣再生成主鍵值了,他說他們的系統沒有單點系統。就算是代理層也是分布式的,所以很可靠,就算所有server全掛了。那麽這個時候主鍵生成server集群也不會再反復生成已經生成過的主鍵值,當然每次生成完主鍵值後,為了安全起見,主鍵生成服務會把生成的最大主鍵值持久化保存。

  事實上這位朋友的主鍵設計方案事實上核心設計起點就是為了解決主鍵的排序問題,這也為實際使用單獨主鍵設計方案找到了一個非常現實的場景。假設能做到保證主鍵的順序性,同一時候數據落地時候依據這個順序依次進行的,那麽在單庫做排序查詢的準確度就會非常高,查詢時候我們把查詢的條數均勻分布到各個server的表上,最後匯總的排序結果也是近似精確的。

  自從和這位朋友聊到了主鍵生成服務的設計問題後以及我今天講到的一致性哈希的問題,我如今有點摒棄前文裏說到的固定哈希算法的主鍵設計方案了,這個摒棄也是有條件限制的,主鍵生成服務的方案事實上是讓固定哈希方案更加完好,可是假設主鍵本身沒有排序性。僅僅有唯一性,那麽這個做法對於排序查詢起不到什麽作用,到了水平擴展。固定哈希排序的擴展會導致大量數據遷移。風險和成本太高,而一致性哈希是固定哈希的進化版,因此當我們想使用哈希來分布數據時候,還不如一開始就使用一致性哈希,這樣就為興許的系統升級和維護帶來非常大的便利。

  有網友在留言裏還提到了哈希算法分布數據的一個問題,那就是硬件的性能對數據平均分配的影響,假設水平拆分所使用的server性能存在差異,那麽平均分配是會造成熱點問題的出現。假設我們不去改變硬件的差異性,那麽就不得不在分配原則上增加權重的算法來動態調整數據的分布,這樣就制造了人為的數據分布不均衡。那麽到了上層的計算操作時候某些場景我們也會不自覺的增加權重的維度。可是作為筆者的我對這個做法是有異議的,這些異議詳細例如以下:

  異議一:我個人覺得無論什麽系統引入權重都是把問題復雜化的操作,權重往往都是權益之計。假設隨著時間推移還要進一步擴展權重算法,那麽問題就變得越加復雜了。並且我個人覺得權重是非常難進行合理處理的,權重假設還要演進會變得異常復雜,這個復雜度可能會遠遠超出分布式系統,數據拆分本身的難度。因此除非迫不得已我們還是盡量不去使用什麽權重。就算有權重也不要輕易使用,看有沒有方式能夠消除權重的根本問題。

  異議二:假設我們的系統後臺數據庫都是使用獨立server,那麽一般都會讓最好的server服務於數據庫。這個做法本身就說明了數據庫的重要性,並且我們對數據庫的不論什麽分庫分表的解決方式都會非常麻煩。非常繁瑣甚至非常危急,因此本篇開始提出了假設我們解決瓶頸問題前先考慮下硬件的問題,假設硬件能夠解決掉問題,優先採取硬件方案。這就說明我們合理對待存儲問題的前提就是讓數據庫的硬件跟上時代的要求,那麽假設有些硬件出現了性能瓶頸,是不是我們忽視了硬件的重要性了?

  異議三:均勻分布數據不只能夠合理利用計算資源,它還會給業務操作帶來優點。那麽我們擴展數據庫時候就讓各個server本身能力均衡,這個事實上不難的,假設老的server實在太老了,用新server替換掉,盡管會有全庫遷移的問題。但是這麽粗粒度的數據平移。那但是比不論什麽拆分方案的數據遷移難度低的多的。

  好了。本篇就寫到這裏,祝大家工作生活愉快!

關於大型站點技術演進的思考(七)--存儲的瓶頸(7)