本文開篇提個問題給大家,關系數據庫的瓶頸有哪些?我想有些朋友看到這個問題肯定會說出自己平時開發中碰到了一個跟數據庫有關的什么什么問題,然后如何解決的等等,這樣的答案沒問題,但是卻沒有代表性,如果出現了一個新的存儲瓶頸問題,你在那個場景的處理經驗可以套用在這個新問題上嗎?這個真的很難說。
其實不管什么樣的問題場景最后解決它都要落實到數據庫的話,那么這個問題場景一定是擊中了數據庫的某個痛點,那么我前面的六篇文章里那些手段到底是在解決數據庫的那些痛點,下面我總結下,具體如下:
痛點一:數據庫的連接數不夠用了。換句話說就是在同一個時間內,要求和數據庫建立連接的請求超出了數據庫所允許的最大連接數,如果我們對超出的連接數沒有進行有效的控制讓它們直接落到了數據庫上,那么就有可能會讓數據庫不堪重負,那么我們就得要分散這些連接,或者讓請求排隊。
痛點二:對于數據庫表的操作無非兩種一種是寫操作,一種是讀操作,在現實場景下很難出現讀寫都成問題的事情,往往是其中一種表的操作出現了瓶頸問題所引起的,由于讀和寫都是操作同一個介質,這就導致如果我們不對介質進行拆分去單獨解決讀的問題或者寫的問題會讓問題變的復雜化,最后很難從根本上解決問題。
痛點三:實時計算和海量數據的矛盾。本系列講存儲瓶頸問題其實有一個范疇的,那就是本系列講到的手段都是在使用關系數據庫來完成實時計算的業務場景,而現實中,數據庫里表的數據都會隨著時間推移而不斷增長,當表的數據超出了一定規模后,受制于計算機硬盤、內存以及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保證這個集群的可靠性,好了,他們做法里最關鍵的一點來了,如何保證主鍵獲取的高效性,他說他們沒有讓每次生成主鍵的操作都是直接訪問集群,而是在集群和主鍵使用者之間做了個代理層,集群也不是頻繁生成主鍵的,而是每次生成一大批主鍵,這一大批主鍵值按隊列的方式緩存在代理層了,每次主鍵使用者獲取主鍵時候,隊列就消耗一個主鍵,當然他們的系統還會檢查主鍵使用的比率,當比率到達閥值時候集群就會收到通知,馬上開始生成新的一批主鍵值,然后將這些值追加到代理層隊列里,為了保證主鍵生成的可靠性以及主鍵生成的連續性,這個主鍵隊列只要收到一次主鍵請求操作就消費掉這個主鍵,也不關心這個主鍵到底是否真的被正常使用過,當時我還提出了一個自己的疑問,要是代理掛掉了呢?那么集群該如何再生成主鍵值了,他說他們的系統沒有單點系統,就算是代理層也是分布式的,所以非常可靠,就算全部服務器全掛了,那么這個時候主鍵生成服務器集群也不會再重復生成已經生成過的主鍵值,當然每次生成完主鍵值后,為了安全起見,主鍵生成服務會把生成的最大主鍵值持久化保存。
其實這位朋友的主鍵設計方案其實核心設計起點就是為了解決主鍵的排序問題,這也為實際使用單獨主鍵設計方案找到了一個很現實的場景。如果能做到保證主鍵的順序性,同時數據落地時候根據這個順序依次進行的,那么在單庫做排序查詢的精確度就會很高,查詢時候我們把查詢的條數均勻分布到各個服務器的表上,最后匯總的排序結果也是近似精確的。
自從和這位朋友聊到了主鍵生成服務的設計問題后以及我今天講到的一致性哈希的問題,我現在有點摒棄前文里說到的固定哈希算法的主鍵設計方案了,這個摒棄也是有條件限制的,主鍵生成服務的方案其實是讓固定哈希方案更加完善,但是如果主鍵本身沒有排序性,只有唯一性,那么這個做法對于排序查詢起不到什么作用,到了水平擴展,固定哈希排序的擴展會導致大量數據遷移,風險和成本太高,而一致性哈希是固定哈希的進化版,因此當我們想使用哈希來分布數據時候,還不如一開始就使用一致性哈希,這樣就為后續的系統升級和維護帶來很大的便利。
有網友在留言里還提到了哈希算法分布數據的一個問題,那就是硬件的性能對數據平均分配的影響,如果水平拆分所使用的服務器性能存在差異,那么平均分配是會造成熱點問題的出現,如果我們不去改變硬件的差異性,那么就不得不在分配原則上加入權重的算法來動態調整數據的分布,這樣就制造了人為的數據分布不均衡,那么到了上層的計算操作時候某些場景我們也會不自覺的加入權重的維度。但是作為筆者的我對這個做法是有異議的,這些異議具體如下:
異議一:我個人認為不管什么系統引入權重都是把問題復雜化的操作,權重往往都是權益之計,如果隨著時間推移還要進一步擴展權重算法,那么問題就變得越加復雜了,而且我個人認為權重是很難進行合理處理的,權重如果還要演進會變得異常復雜,這個復雜度可能會遠遠超出分布式系統,數據拆分本身的難度,因此除非迫不得已我們還是盡量不去使用什么權重,就算有權重也不要輕易使用,看有沒有方式可以消除權重的根本問題。
異議二:如果我們的系統后臺數據庫都是使用獨立服務器,那么一般都會讓最好的服務器服務于數據庫,這個做法本身就說明了數據庫的重要性,而且我們對數據庫的任何分庫分表的解決方案都會很麻煩,很繁瑣甚至很危險,因此本篇開始提出了如果我們解決瓶頸問題前先考慮下硬件的問題,如果硬件可以解決掉問題,優先采取硬件方案,這就說明我們合理對待存儲問題的前提就是讓數據庫的硬件跟上時代的要求,那么如果有些硬件出現了性能瓶頸,是不是我們忽視了硬件的重要性了?
異議三:均勻分布數據不僅僅可以合理利用計算資源,它還會給業務操作帶來好處,那么我們擴展數據庫時候就讓各個服務器本身能力均衡,這個其實不難的,如果老的服務器實在太老了,用新服務器替換掉,雖然會有全庫遷移的問題,但是這么粗粒度的數據平移,那可是比任何拆分方案的數據遷移難度低的多的。
好了,本篇就寫到這里,祝大家工作生活愉快!
文章列表