大型網站架構改進歷程:儲存的瓶頸(7)

夏天的森林發表於2015-02-09

本文開篇提個問題給大家,關聯式資料庫的瓶頸有哪些?我想有些朋友看到這個問題肯定會說出自己平時開發中碰到了一個跟資料庫有關的什麼什麼問題,然後如何解決的等等,這樣的答案沒問題,但是卻沒有代表性,如果出現了一個新的儲存瓶頸問題,你在那個場景的處理經驗可以套用在這個新問題上嗎?這個真的很難說。

其實不管什麼樣的問題場景最後解決它都要落實到資料庫的話,那麼這個問題場景一定是擊中了資料庫的某個痛點,那麼我前面的六篇文章裡那些手段到底是在解決資料庫的那些痛點,下面我總結下,具體如下:

痛點一:資料庫的連線數不夠用了。換句話說就是在同一個時間內,要求和資料庫建立連線的請求超出了資料庫所允許的最大連線數,如果我們對超出的連線數沒有進行有效的控制讓它們直接落到了資料庫上,那麼就有可能會讓資料庫不堪重負,那麼我們就得要分散這些連線,或者讓請求排隊。

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

痛點三:實時計算和海量資料的矛盾。本系列講儲存瓶頸問題其實有一個範疇的,那就是本系列講到的手段都是在使用關聯式資料庫來完成實時計算的業務場景,而現實中,資料庫裡表的資料都會隨著時間推移而不斷增長,當表的資料超出了一定規模後,受制於計算機硬碟、記憶體以及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保證這個叢集的可靠性,好了,他們做法裡最關鍵的一點來了,如何保證主鍵獲取的高效性,他說他們沒有讓每次生成主鍵的操作都是直接訪問叢集,而是在叢集和主鍵使用者之間做了個代理層,叢集也不是頻繁生成主鍵的,而是每次生成一大批主鍵,這一大批主鍵值按佇列的方式快取在代理層了,每次主鍵使用者獲取主鍵時候,佇列就消耗一個主鍵,當然他們的系統還會檢查主鍵使用的比率,當比率到達閥值時候叢集就會收到通知,馬上開始生成新的一批主鍵值,然後將這些值追加到代理層佇列裡,為了保證主鍵生成的可靠性以及主鍵生成的連續性,這個主鍵佇列只要收到一次主鍵請求操作就消費掉這個主鍵,也不關心這個主鍵到底是否真的被正常使用過,當時我還提出了一個自己的疑問,要是代理掛掉了呢?那麼叢集該如何再生成主鍵值了,他說他們的系統沒有單點系統,就算是代理層也是分散式的,所以非常可靠,就算全部伺服器全掛了,那麼這個時候主鍵生成伺服器叢集也不會再重複生成已經生成過的主鍵值,當然每次生成完主鍵值後,為了安全起見,主鍵生成服務會把生成的最大主鍵值持久化儲存。

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

自從和這位朋友聊到了主鍵生成服務的設計問題後以及我今天講到的一致性雜湊的問題,我現在有點摒棄前文裡說到的固定雜湊演算法的主鍵設計方案了,這個摒棄也是有條件限制的,主鍵生成服務的方案其實是讓固定雜湊方案更加完善,但是如果主鍵本身沒有排序性,只有唯一性,那麼這個做法對於排序查詢起不到什麼作用,到了水平擴充套件,固定雜湊排序的擴充套件會導致大量資料遷移,風險和成本太高,而一致性雜湊是固定雜湊的進化版,因此當我們想使用雜湊來分佈資料時候,還不如一開始就使用一致性雜湊,這樣就為後續的系統升級和維護帶來很大的便利。

有網友在留言裡還提到了雜湊演算法分佈資料的一個問題,那就是硬體的效能對資料平均分配的影響,如果水平拆分所使用的伺服器效能存在差異,那麼平均分配是會造成熱點問題的出現,如果我們不去改變硬體的差異性,那麼就不得不在分配原則上加入權重的演算法來動態調整資料的分佈,這樣就製造了人為的資料分佈不均衡,那麼到了上層的計算操作時候某些場景我們也會不自覺的加入權重的維度。但是作為筆者的我對這個做法是有異議的,這些異議具體如下:

異議一:我個人認為不管什麼系統引入權重都是把問題複雜化的操作,權重往往都是權益之計,如果隨著時間推移還要進一步擴充套件權重演算法,那麼問題就變得越加複雜了,而且我個人認為權重是很難進行合理處理的,權重如果還要演進會變得異常複雜,這個複雜度可能會遠遠超出分散式系統,資料拆分本身的難度,因此除非迫不得已我們還是儘量不去使用什麼權重,就算有權重也不要輕易使用,看有沒有方式可以消除權重的根本問題。

異議二:如果我們的系統後臺資料庫都是使用獨立伺服器,那麼一般都會讓最好的伺服器服務於資料庫,這個做法本身就說明了資料庫的重要性,而且我們對資料庫的任何分庫分表的解決方案都會很麻煩,很繁瑣甚至很危險,因此本篇開始提出瞭如果我們解決瓶頸問題前先考慮下硬體的問題,如果硬體可以解決掉問題,優先採取硬體方案,這就說明我們合理對待儲存問題的前提就是讓資料庫的硬體跟上時代的要求,那麼如果有些硬體出現了效能瓶頸,是不是我們忽視了硬體的重要性了?

異議三:均勻分佈資料不僅僅可以合理利用計算資源,它還會給業務操作帶來好處,那麼我們擴充套件資料庫時候就讓各個伺服器本身能力均衡,這個其實不難的,如果老的伺服器實在太老了,用新伺服器替換掉,雖然會有全庫遷移的問題,但是這麼粗粒度的資料平移,那可是比任何拆分方案的資料遷移難度低的多的。

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

相關文章