雲計算

面向應用的反範式化建模

作者:天穆


 

一、基礎:數據分佈


(一)擴展性:scale up & scale out

分佈式系統裡常見的擴展性問題有兩種:scale up 和 scale out。拿數據存儲舉例,如果一塊盤存不下,換一個更大的盤,從1t到4t到8t到更大的硬盤,但是這種方式很容易觸及到系統的容量上限。

因為不可能把一塊盤做的非常大,所以現在業界最常用的擴展性的方式是通過scale out方式,一塊硬盤不夠,用更多的盤,當一臺機器盤的數量達到上限,用更多的機器組成集群,如果集群不夠了,用更多的集群組成聯邦。再往前一步可以用一個機房,用更多機房,甚至全球分佈,擴展整個系統的容量。


1.png


(二)基本問題:數據分佈策略

做scale out的時候必然會面臨一個問題,當存儲數據的節點或盤變多了以後,必須要解決數據怎麼在硬盤和機器上分佈問題,也就是數據分佈策略。

理解數據分佈策略,可以從讀、寫兩個方面開始,比如寫的時候一個請求或者要寫一行數據,要寫到哪個機器上、寫到哪個盤上;從讀的角度來講,一個請求讀取數據,不可能訪問整個集群的所有盤或者所有機器,這樣讀取這行數據太慢了,所以必須有很好的算法或者是分佈策略,能夠讓讀、寫的請求能夠一次到達目標。最多可以有多一條的方式,但是最終期待的是一條就能獲取。

討論具體的分佈策略之前,要明白設計分佈策略的目標,第一是:負載均衡;第二是:線性擴展


負載均衡:是希望在寫的時候,能夠均勻的寫到每一臺機器上、每一塊盤上,整體是均勻的,不會某些盤或者某些機器成為寫熱點,也不會因為某些機器寫的太多,水位很高,其他機器都空著。從讀的角度來講也一樣,希望讀能夠很均勻的分佈在整個集群上,不會導致熱點和傾斜。

達到負載均衡以後,還要有線性擴展,比如集群擴縮容、盤壞了要下盤、還要加新的盤上來,這個時候希望無論機器怎麼擴容,盤怎麼增減,整個系統仍然處於負載均衡的狀態。只要保證這一點,當系統盤增加了或者機器增加的時候,整個系統仍然能夠處於線性擴容的關係,機器多了能夠存的數據就多了,能承載的吞吐也變多了,是整個數據分佈的目標。


總結:

負載均衡:

  • 寫:均勻的寫到集群的每一臺機器上,每一塊盤上;
  • 讀:均勻的從集群的每臺機器上、每塊盤上讀數據。


線性擴展:

  • 機器擴縮容,磁盤上下線,系統始終/最終處於負載均衡狀態;
  • 系統容量、吞吐與系統資源成正比(線性關係)。


2.png


(三)兩種分佈策略

目前業界有兩種比較典型的分佈策略,一種是順序分佈,一種是Hash分佈。


順序分佈:根據用戶定義組件,讓數據從最小的主鍵開始,依次往後排,如圖例所示:user_id和ts是聯合的主鍵,先按user_id 1、2、3、4、5排序,排完之後,再按ts進行排序。順序分佈是把整個表做拆分,例如:把user_id 等於1的分到一個Region裡面,user_idt等於2、3的分到一個Region裡面。


3.png


Hash公佈:需要有一個Hash算法,選一個分區鍵,經過算法得到所在機器的名字。常見的一種算法就是取模“分區 = user_id % 機器數”,可以拿user_id模上這個機器數,得到所在的機器。如圖例所示,假設有3臺機器,“%3 = 0”在第一臺機器上,“%3 = 1”在第二臺機器上,以此類推,是一種基於規則的分佈。


4.png


1)順序分佈:目前比較典型的產品有hbase,tidb。

順序分佈的缺點

  • 第一,是比較依賴主鍵的值,如果user_id分佈不均勻,因為通常user_id是順序分配的,比如有1、2、3、4、5、6、7、8、9、10,user_id更大的時候,熱度會比較高,user_id小的時候,熱度會比較低。會產生一種問題,越往表的尾部越熱,頭部的可能就會冷一點,會產生數據傾斜以及訪問傾斜,需要通過額外設計或人工介入調整。
  • 第二,相同前綴的數據也可能會分開,比如上圖所示的“user_id = 3” 的數據,可能會被分到兩個Region上,當訪問等於3的所有數據時,必然會涉及到兩次Region。
  • 第三,因為有強大的干預能力,需要很複雜的路由表機制。

順序分佈優點

  • 第一,一個Region包含哪些數據,通過路由表決定,比如HBase的meta表,tidb裡面是PD。Region可以靈活分佈,比如讓user_id=1的數據,在Region裡面拆分,也可以把user_id=1、2、3合併。
  • 第二, Region在哪臺機器可以人工指定,比如可以讓Region 1單獨分一臺機器,Region 1、3共享另外一臺機器,在生產上,尤其是在有數據熱點場景下,有人工介入干預能力。


5.png


2)Hash分佈:是基於規則的分佈,選取分區鍵,user_id根據分佈算法或者Hash算法,得到所在的機器。比較典型的代表產品有cassandra、dynamodb。跟傳統關係數據庫裡面的分庫分表非常類似,因為沒有外部依賴,所以比較簡單。

缺點

第一,是在做擴縮容的時候,需要對很多的數據進行搬遷,所以需要一致性hash方案

第二,是分區無法靈活調整,因為是基於規則的,當數據基於分區鍵算好分區之後,所在的機器就確定了,不能靈活調整。

第三,有數據傾斜問題,比如有超大分區,比如user_id=1是個超大的用戶,記錄非常多,會產生熱點的問題,user_id=1的所有的數據強制分佈在某一臺機器上,數據特別多的話,這臺機器很快會達到上限。


6.png


(四)Hash分佈:分區鍵的選擇

如圖所示,基於直覺的方式是選user_id作為分區鍵,為什麼不能用把TS也放進去?

7.png

假設把TS放進去,user_id和TS一起算Hash,勢必會產生一種情況,就是user_id = 3的數據,可能分佈在整個集群的不同位置,做查詢的時候where user_id=3,等於3的所有數據會面臨查很多分區。而且 user_id=3下面的TS,沒法知道有多少,是一個不可預測的值,這時涉及到跨分區的查詢,這種查詢會退化成全面表掃描,是不能接受的。

選擇分區鍵要結合查詢的場景,選擇合適的分區鍵,儘量避免或者一定要避免跨分區的查詢。比如where user_id>3,這種是沒辦法直接高效的定位查詢,一定要掃全表;但是where user_id in (3, 6, 9, ...)這種,是可以拆分成多個請求逐個查詢,因為是可枚舉的。

二、Cassandra的數據模型

(一)Partition Key,Clustering key

Cassandra數據模型裡ts叫聚類鍵,user_id叫分區鍵,分區鍵和聚類鍵加一起,構成表的主鍵,主鍵要求唯一性。比如下圖所示的表裡面,user_id和TS放到一起一定要全局唯一,如果400有兩個,就是衝突的數據。

對於分區鍵和聚類鍵,可以有很多個,可以很多個Key作為分區鍵,也可以有很多Key作為聚類鍵。除了主鍵之外,Cassandra裡面還有非主鍵,或者叫屬性列或者叫數據列,比如location存具體數據,不參與數據排序。


8.png


(二)聯合主鍵與前綴匹配

key比較多的場景稱為聯合主鍵或,聯合主鍵如何排序以及查詢?如圖例所示的場景,分區鍵是city,有兩個聚類鍵,一個是last_name,一個是first_name。因為分區間鍵不參與排序,當我們做Hash分佈的時候,分區鍵在整個表裡面隨機分佈,但是在某一個特定的分區鍵下面,clustering key是順序分佈的。圖例中是按last_name前綴排序, p排在前面,w排在後面,在last_name相同的時候,再排下一個列, potter相同的時候, Harry排在前面,James排在後面,是這種排序規則。

9.png

因為是這樣排的,所以在查的時候,要從左到右依次去查,有以下幾種情況:

1.where city = 'hangzhou' and last_name = 'Potter',前綴掃描;

2. where city = 'hangzhou' and last_name = 'Potter' and first_name = 'James',單行讀;

這兩種可以很高效的完成,因為查詢的掃描範圍和結果集一樣大,有一些場景不能很好的支持,如:

3. where city = 'hangzhou' and first_name = 'Harry',跳過了last_name列,直接查first_name,這種查詢first_name不能夠用於圈定掃描範圍,會變成一個filter,直接對每一行數據過濾,查詢的掃描範圍是city = 'hangzhou'的所有數據,為每一行數據基於first_name = 'Harry'做過濾,假設'hangzhou'是一個很大的Partition Key,數據量很多,這個查詢會非常低效。

4. where city >= 'hangzhou',當city >= 'hangzhou'的時候,就是一個跨分區鍵的查詢,也不能被支持。

5.where city = 'hangzhou' and last_name >='P' and first_name = 'James',first_name進入filter。

6.where city = 'hangzhou' and last_name >='P' and first_name = 'Ron';

這兩個查詢從表上看,James排在前面,Ron排在後面,但是事實上last_name是範圍查詢,first_name字段變成filter來掃,而不是用來縮小查詢範圍,所以說5和6兩個語句的掃描範圍一樣。


10.png


(三)邏輯分區:一組具有相同前綴的行

一個Partition Key的值代表一個分區,但本質上來講,並不是物理上的分區,比如一塊盤、一個機器,有物理的實體跟其對應,但是分區不會有一個文件或者實體跟其對應,分區是一種邏輯概念。

在這裡面把分區定義成是一組具有相同前綴的行,前綴是Partition Key,如下圖所示,Partition Key等於杭州,杭州這兩行數據就是一個分區,等於上海的就是另外一個分區,這種就是叫邏輯分區。在物理上沒有一個有力度的實體跟它對應,所以它的數量可以無窮大,這裡的city是一個字符串,可以有無窮多的數據組合,city分區鍵可以無窮無盡的分區。

11.png

  • 分區鍵:值域可能非常大(比如long),分區鍵的每一個值,都代表了一個"分區";
  • "分區"的數量可能會非常大;
  • "分區"的本質:一組具有相同前綴的行,"前綴"即分區鍵的值;
  • 所有的分區都是"邏輯分區";
  • 線性擴展。


線性擴展:是指分區根據一致性Hash算法劃分到某一個機器上,一臺機器可以服務很多分區,機器數量增加之後,能夠承載的分區數量也會相應的增加,能夠獲得線性擴展能力。除非產生了一些巨大的分區,這些分區把一些機器佔滿了,這種情況下線性擴展能力是受限的。

三、範式與反範式設計


(一)範式化與反範式化


範式化:是傳統關係型數據庫要求的概念,數據庫剛出現的時候,盤都比較貴,存儲空間都比較貴,數據庫的表設計必須要滿足降低數據冗餘度的原則,需要範式化的設計,減少數據冗餘度。

另外需要增加數據的一致性校驗,比如有很多表,一些表來存買家,一些表存賣家,一些表來存訂單,通過主鍵和外鍵之間的關係進行關聯,通過外建描述數據的完整性,也是範式化設計的一部分。這種通常是用於關係數據庫的設計,而且能夠很好的解決複雜業務的設計,通過一整套的方法論,業務模型進行抽象。

在NoSQL系統裡面,強調反範式化的設計,通過增加冗餘度換取更好的性能。帶來的一個問題就是數據冗餘,存儲空間開銷上升,但是現在存儲越來越便宜了,成本並沒有上升很多。


範式化(Normalization)

  • 目的:

➢ 降低數據冗餘度;

➢ 增加數據的完整性(如外鍵)。

• 通常用於關係型數據庫的設計


反範式化(Denormalization)

  • 增加冗餘度,用空間換時間;
  • 數據在多個地方都有,存在一致性問題。


(二)示例

下圖所示,是一個部門和部門下的僱員之間的表設計,比如有個department表,存 depId和名字,還有一個user的表,來存每一個人和userId,要描述一個部門還有哪些人的時候,需要把這兩張表關聯起來。記錄表的depId和userId之間的關係,當查一個部門有哪些人的時候,要先掃這個部門的人員表,得到這個部門的userId信息,比如查depId=2,得到的userId是1和2,這時轉user表拿到1和2兩個ID的用戶名,同時拿depId=2的depName,才能獲取depName是Math,一次查詢,需要有三張表,這是範式化設計。


12.png


反範式化設計,就用一張表來代替。如下圖所示,depName和userName直接存在一起,查詢一次搞定。缺點是名字重複存在,depName內容也重複存在,數據冗餘度增加。另外,當修改名字的時候,要改很多地方。


13.png


(三)反範式化優缺點


優點:

  • 多個表的數據統一到一張表裡;
  • JOIN不是必須的(大部分NoSQL也不支持join),查詢更高效;
  • 採用寬表設計,從業務設計來講業務更簡潔,查詢更簡潔,整個業務模式會更清晰,SQL會更簡單,維護性會更好;
  • 當業務出現問題的時候,調查問題的效率得到相應的提升。


缺點:

  • 冗餘存儲,空間開銷增加。但是因為現在存儲變便宜了,所以說成本沒有增加。
  • 數據冗餘之後,帶來的一致性的問題,比如只有一張表,Math存了兩次,但是假設當有很多張表的時候,都有Math字段,會面臨在多張表之間處理一致性問題。


(四)原則

反範式化設計的基本原則是:

  • 根據讀寫模式來設計表,設計主鍵;
  • 使用分區鍵來規劃數據分佈:一次查詢需要的數據,儘可能在一個分區裡;
  • 使用聚類鍵來保證數據在分區內的唯一性,並控制結果集中的數據的排序(ASC/DESC);
  • 設計好主鍵以後,使用非主鍵列來記錄額外信息。這個時候非主鍵包含了很多業務字段,比如訂單存儲,希望其包含訂單金額、訂單ID、買家名字、賣家信息、商品信息等,是一張大寬表,可以通過一次或者是少量的查詢,得到需要的所有數據,避免join,提升整個系統的查詢性能。
  • 反範式化設計:將原本需要通過join得到的數據,都包含進來。



四、典型場景分析


(一)典型場景一:物流詳情


場景描述:

  • 電商物流訂單,每個訂單會經歷多輪中轉最後達到用戶手中。每一次中轉會產生一個事件,比如已攬收、裝車、到達xx中轉站、派送中、已簽收。
  • 需要記錄全網所有物流訂單的狀態變化,為用戶提供訂變更記錄的查詢能力。
  • 訂單數據量極大,可能有上百億;體量不能影響讀寫性能。


場景抽象:

  • 寫:記錄一個訂單的一次狀態變更。
  • 讀:讀取一個訂單最近N條記錄;讀取一個訂單的全部記錄。

如下圖所示:表中有兩列主鍵,orderId指訂單的ID,是分區鍵;gmtCreated指事件產生的時間,是聚類鍵;非主鍵列detail指的是一次事件的信息,比如已攬收或到達的狀態,是數據列。


14.png


1)物流詳情:高表設計

"高表"設計:

  • 行不斷增加,一行描述一個訂單的一個事件。
  • 一個訂單的所有數據,由連續的一組行來描述(一個邏輯分區)。查一個訂單的所有數據時,事實是查一組具有相同前綴的行,就是查一個分區的數據。

優缺點:

  • 單個分區鍵下的key數量可以很多;
  • 過多的數據將導致寬分區的產生,應避免;
  • 無論數據量多大,單次next()的RT可控:流式ResultSet。

高表設計可以避免很大的行產生,因為所有的變化都產生在行裡面,不是產生在列裡面。可以很好的解決orderId的問題,如果某一個訂單數據量特別多的時候,會產生寬分區,需要避免。常規做法是增加維度,拆開分到不同的分區裡面。

高表設計無論數據量多大,單次讀下一行數據的時間不變,有流式ResultSet能力,一次加載一部分數據。


2)物流詳情:寬表設計(不推薦)

寬表設計:

  • 用一行來描述一個訂單的所有事件,每一列是一個事件,用事件的發生事件作列名;
  • 也可將所有事件encode到一個列裡。

15.png

寬表設計,用一行來描述一個訂單的所有事件,每一次事件通過一列來描述。

如上圖所示,把時間作為列名,每一個列記錄了一個訂單的某一次事件。也可以把後面的列合到一起,變成一個列。

優缺點:

  • 單行讀;讀一個訂單的所有的數據時,只做單行讀,業務會更簡單。
  • 無法預知列名,列數量,每一行的列都可能不一樣,強依賴schema-free能力;
  • 只能讀所有數據,不容易實現topN讀取;
  • 超大行風險:個別行的列特別多;會影響性能。


所以在物流詳情場景下,不推薦寬表設計,建議用高表設計。


(二)典型場景二:時序類---監控系統

如下圖所示,是CPU監控,對整個集群的多臺機器做 CPU指標的監控,CPU指標有user、system、idle等不同類型,還有很多主機如host,一臺機器的某一個CPU user指標有很多點位,比如這裡面192.168.1.1機器在CPU type裡面產生了兩個點,一個是30,一個是40,這個是時間線。

16.png

這個表裡面列出來的是監控系統裡面需要的數據,在這個數據場景下,怎麼選擇分區鍵、聚類鍵以及監控數據的存儲,可以有以下幾種選擇:

分區鍵怎麼選:

  • metric;
  • metric + host;
  • metric + host + type;
  • metric + type + host。

監控數據怎麼存?

  • 一行一個點;
  • 一行存所有點;
  • 一行存有限個點:如1分鐘/1小時內產生的點。


1)分區鍵:只使用metric

只使用metric作為分區鍵,意味著分區只有 CPU,如果加入網絡、磁盤,每一個metric是一個分區,意味著所有被監控的對象,所有機器的所有數據,都在一個分區下面,很容易觸發單分區限制。

17.png

因為有變量和不變量的問題, CPU指標本身是不變量,即使未來新增指標,通常也是低頻事件。但被監控的機器是變量,會不斷的增加,可能數量巨大(比如物流訂單的數量)

  • 單分區限制:所有機器的指標都聚集在一個分區裡,被監控的機器可能無限增長,但單機的承受能力不會線性增長。
  • 業務側:識別變量和不變量
  • cpu指標本身是不變量,即使未來新增指標,通常也是低頻事件;
  • 被監控的機器是變量,會不斷的增加,可能數量巨大(比如物流訂單的數量)。


2)分區鍵:metric + host

metric + host策略可以很好的控制了單分區的數據量,不會出現寬分區。因為除了 host以外,沒有其他維度大幅度的變化量,如下圖所示,type和TS都不會太大變化,TS本質上是作為host下面的一個子集存在,比如在做一個查詢的時候,要查某一臺機器的某一段時間範圍的 CPU指標,肯定希望這臺機器的數據都排在一起,用一個查詢搞定,所以TS不能夠放在分區鍵裡面。

18.png

這個設計的缺點是,併發讀寫同一個機器的cpu指標,請求都路由給同一臺機器,不利於併發。


優缺點總結:

  • 很好的控制了單分區的數據量,不會出現寬分區;
  • 單機的所有類別的cpu指標都在一個分區裡;
  • 併發讀寫同一個機器的cpu指標,請求都路由給同一臺機器,不利於併發。


3)分區鍵:metric + host + type

metric + host + type策略,把指標類別也加到分區鍵裡面,可以很好的適配併發查詢模式,提高整個集群的吞吐。因為 metric + host + type整體作為分區鍵,只有三個全相等的時候,才會分在一個分區裡面。

19.png

另外一種方式是host和type交換位置,其對採用一致性hash的cassandra來說,沒有區別。但是對於順序分佈來講,可能會有一點區別,因為改變了key的值域範圍,可能導致值變少了,這個時候會產生聚合效應,可能導致一些潛在的問題。總結:

  • 同一臺機器的不同cpu類別的指標在不同的分區裡,很好的支持併發訪問;
  • host和type的順序
  • 對採用一致性hash的cassandra來說,沒有區別。


4)優化:type合併至metric中

metric + host + type策略還有另一個優化,type合併至metric中,如下圖所示,是時序數據庫建模時的特點,type在時序數據庫裡面叫 tag,標籤的意思,可以有很多標籤,比如IP是一種標籤,所在機房可能也是一個標籤,甚至可以有業務的標籤, CPU的 type也是一種標籤,這種場景下,可以把標籤合到 metric中。

20.png

因為type是可枚舉的,只有幾種,不會增加也不會減少。合並過去之後,減少了列,存儲的列變少以後,可以提高性能,減少開銷,僅數據量較大時,體量小的時候看不出來。

還有一種場景,儲存的是進程ID,這種時候沒辦法合併,比如監控每一個進程的網絡流量,這個時候進程ID沒法合到 Metric裡面,因為進程ID不可預估,而且不可枚舉。


21.png


5)數據點位的存儲:寬表 or 高表

數據點位的存儲指的是每個時間點metric的值,依然可以有高表和寬表的設計。如下圖所示,高表設計,把TS放到Clustering key裡面,一行存儲一個數據點。高表設計因為一行只有一列,容易擴展多值監控,對於像經緯度,一個點位有兩個值,在高表設計裡面很容易擴展。

22.png

寬表設計,一行存儲這個某個機器的某個指標的所有點,這種寬表設計還是會存在單行上線的問題,列多了以後會有性能問題。

融合設計,一行記錄有限個點,比如存1分鐘採集的所有的點或者1小時的點,結合高表和寬表的設計,粒度選擇合適的時候,得到最優的性能,而且能夠配合整個系統內部機制,如cache,bloomfilter等。

當然這些例子,在時序場景下面比較簡單,能夠解決簡單業務的時序設計問題,對於業界的實際數據庫來講,但生產使用時有很多考量因素。


總結:

  • 高表設計:一行存儲一個數據點,如上表所示;
  • 容易擴展多值監控:如經緯度;
  • 寬表設計:一行存儲這個某個機器的某個指標的所有點;
  • 單行上限;
  • 融合設計:一行記錄有限個點:如1分鐘,1小時內採集到的所有點;
  • 粒度選擇:由指標的採集頻率決定,以控制單行的列數;
  • 適當的控制行數,可以配合一些內部優化機制:如cache,bloomfilter等;

注意:時序建模的原理很簡單,但生產使用時有很多考量因素,各TSDB都有不同的側重點。應根據業務實際需要選擇合適的模型,沒有銀彈


(三)常見誤區


1)常見誤區一:分頁查詢

常見的分頁查詢誤區,從Mexico[MOU23] 過來的用戶很容易遇到一個問題拆請求,如下圖所示,做一個大表的掃描,user id=3的數據可能非常多,為了避免一次返回太多的數據,需要對請求進行拆分。

23.png

比如按TS進行分頁,先掃500的,再掃下500,再掃下500,一次一次掃。在 MySQL裡面這樣做是合理的,因為RPC一次返回所有記錄。但是在Cassandra裡面沒必要,因為Cassandra用的是一種流式ResultSet方式,在系統設計層面,已經考慮到了不斷往下next的情況,已經做了請求拆分。比如第一次next的時候,會新加載500行的數據,等到這500行數據消化完了,再下一次next,會加載下500行數據,如此往復,直到所有結果集返回。


總結:

流式ResultSet:

  • 為了避免單次RPC返回過多數據導致RT過高,CQL driver會自動對請求進行拆分;
  • 第一次next()調用會從服務端load N行數據,之後的N-1次next()只從內存消費數據;
  • 下一次next()會再加載N行數據到客戶端,如此往復,直到所有結果集返回。

參見:https://docs.datastax.com/en/developer/java-driver/3.2/manual/paging/

結論:不要為了拆分大請求而進行分頁。


2)常見誤區二:修改主鍵

  • 場景1:修改主鍵的schema,在MySQL裡面可以,但在Cassandra裡面不允許,只能重新建表。
  • 場景2:修改主鍵的值,本身就是錯誤的說法。考慮java的map的key, key能修改嗎?修改key的邏輯就是刪除老key,寫入新key。從數據庫角度來講,沒有修改主建操作,只有刪除、添加這兩種操作,非主鍵可以修改。

Leave a Reply

Your email address will not be published. Required fields are marked *