本章將會提供一些關於使用Table Store的表的建議。

設計良好的主鍵

Table Store會根據表的分區鍵將表的資料自動切分成多個分區,每個分區調度到一台服務節點上。分區鍵的值是最小的分區單位,相同的分區索引值下的資料無法再做切分。為了防止某一個分區索引值的資料成為訪問熱點達到單機服務能力上限,應用程式需要讓資料的分布和訪問量的分布儘可能均勻。

Table Store會對錶中的行按主鍵進行排序,合理設計主鍵可以讓資料在分區上的分布更加均勻,從而能夠充分地利用Table Store水平擴充的特點。

選取分區鍵時,建議遵循以下幾個原則:

  • 單個分區索引值中的資料不宜過大,建議不超過 10 GB。

    说明 10 GB 是限制為了避免訪問熱點,而不是資料存放區的限制。
  • 一張表內,不同分區索引值中的資料在邏輯上是獨立的。

  • 訪問壓力不要集中在小範圍連續的分區索引值中。

使用樣本

例如,有一張表,裡面儲存的是某大學內所有學生使用學生卡消費的記錄。主鍵列有學生卡 ID(CardID),商家 ID(SellerID),消費終端 ID(DeviceID),訂單號(OrderNumber)。同時我們有如下約定:

  • 每一張學生卡對應一個 CardID,每一個商家對應一個 SellerID。

  • 每一個消費終端對應 DeviceID,DeviceID 在全域是唯一的。

  • 在每一個消費終端上產生的每一筆消費記錄一個 OrderNumber。一個消費終端產生的 OrderNumber 是唯一的,但是在全域範圍內 OrderNumber 不唯一。例如,不同的消費終端有可能產生兩條完全不同的消費記錄,但是它們的 OrderNumber 相同。

  • 同一個消費終端產生的 OrderNumber 按時間排序,新的消費記錄比老的消費記錄擁有更大的 OrderNumber。

  • 每筆消費記錄均會被即時寫入這張表中。

為高效利用Table Store,在設計Table Store的表的主鍵時,需考慮表的分區鍵:

分區方式 說明
使用 CardID 作為表的分區鍵 使用 CardID 作為表的分區鍵是一個比較好的選擇。每天每張卡產生的消費記錄數從總體上來講是均勻的,每一個分區中的訪問壓力也應該是均勻的。以 CardID 作為表的分區鍵可以較好地利用預留讀/寫輸送量資源。
使用 SellerID 作為表的分區鍵 使用 SellerID 作為表的分區鍵不是一個好的選擇。因為學校內的商鋪數量相對較少,同時一些商鋪可能產生大量的消費記錄成為熱點,不利於訪問壓力的均勻分配。
使用 DeviceID 作為表的分區鍵 使用 DeviceID 作為表的分區鍵是一個比較好的選擇。儘管每家商鋪的消費記錄數可能相差較大,但是每天每台消費終端上產生的消費記錄數是可預期的。消費終端每天產生消費記錄的條數取決於收銀員操作的速度,這就決定了一台消費終端產生的消費記錄數是受限的。因此,使用 DeviceID 作為表的分區鍵也可以保證訪問壓力的相對均勻。
使用 OrderNumber 作為表的分區鍵 使用 OrderNumber 作為表的分區鍵不是一個好的選擇。因為 OrderNumber 是順序增長的,因此在同一段時間內產生的消費訂單的 OrderNumber 的值會集中在一個較小的範圍內,這些消費訂單記錄會集中寫入到個別的分區,以致預留讀/寫輸送量沒能得到高效利用。如果必須使用 OrderNumber 作為分區鍵,建議在 OrderNumber 上進行雜湊散列,將雜湊值作為 OrderNumber 的首碼,保證資料和訪問壓力的均勻。

總結

可以根據需求將 CardID 和 DeviceID 作為表的分區鍵,而不應該使用 SellerID 和 OrderNumber。之後再根據應用程式的實際需求來設計剩餘的主鍵列。

通過拼接方式使用分區鍵

建議Table Store中表的同一分區索引值下的資料量大小不超過 10 GB。如果您的表中單個分區索引值的所有行的總資料量大小可能超過 10 GB,在設計表時可以將原來的多個主鍵列拼接成分區鍵。

使用樣本

例如,上一小節中提到的學生卡消費記錄表,假設主鍵為 [DeviceID, SellerID, CardID, OrderNumber]。DeviceID 是該表的分區鍵,單個 DeviceID 中所有行的資料量總大小可能超過 10 GB,可以將 DeviceID、SellerID 和 CardID 拼接作為表的第一個主鍵列(即分區鍵)。

原來的表如下所示:

DeviceID SellerID CardID OrderNumber attrs
16 'a100' 66661 200001 ...
54 'a100' 6777 200003 ...
54 'a1001' 6777 200004 ...
167 'a101' 283408 200002 ...

將 DeviceID、SellerID 和 CardID 拼接成分區鍵後的表如下所示:

CombineDeviceIDSellerIDCardID OrderNumber attrs
'16:a100:66661' 200001 ...
'167:a101:283408' 200002 ...
'54:a1001:6777' 200004 ...
'54:a100:6777' 200003 ...

在原來的表中,Device=54 的兩行是屬於同一個分區索引值為 54 下的兩條消費記錄。在新的表中,這兩條消費記錄擁有不同的分區索引值。通過拼接多列主鍵列形成分區鍵的表減少了單個分區索引值下的總資料量大小。

選擇將 DeviceID、SellerID 和 CardID 拼接成分區鍵,而不選擇將 DeviceID 和 SellerID 進行拼接的原因是,前一節提到的消費記錄表約定所有 DeviceID 相同的消費記錄其 SellerID 也相同,因此僅僅拼接 DeviceID 和 SellerID 並不能解決單個分區索引值的資料量過大的問題。

但是,拼接主鍵列形成表有一些小瑕疵。DeviceID 是一個 Integer 類型的主鍵列,在原來的表中,DeviceID=54 的消費記錄在 DeviceID=167 的前面。將前三列主鍵列拼接成 String 類型的主鍵列後,DeviceID=54 的消費記錄在 DeviceID=167 的後面。假如應用程式需要範圍讀取 DeviceID 在 [15, 100) 之間所有的消費記錄,上面的表無法滿足需求。

為了應對這種狀況,可以在 DeviceID 高位補 0。補 0 的個數取決於 DeviceID 最大位元。假設 DeviceID 的取值範圍是 [0, 999999],可以將 DeviceID 高位補 0 至 6 位後再進行拼接,得到的表如下所示:

CombineDeviceIDSellerIDCardID OrderNumber attrs
'000016:a100:66661' 200001 ...
'000054:a1001:6777' 200004 ...
'000054:a100:6777' 200003 ...
'000167:a101:283408' 200002 ...

經過高位補 0 後的表依然有一些問題。在原來的表中,DeviceID=54 的兩行,SellerID='a1001' 的行應該在 SellerID='a100' 的後面。產生這種現象的原因是,'000054:a1001' 的字典序小於 '000054:a100:',但是 'a1001' 的字典序大於 'a100',串連符 : 影響了字典序。在選取串連符時,應該選取比所有可用字元的 ASCII 碼都小的字元作為串連符。在該表中,SellerID 的取值為數字、大小寫英文字母。我們可以使用 , 作為串連符,因為 , 比所有 SellerID 可用字元的 ASCII 碼都小。

使用 , 拼接後的表如下所示:

CombineDeviceiDSellerIDCardID OrderNumber attrs
'000016,a100,66661' 200001 ...
'000054,a100,6777' 200003 ...
'000054,a1001,6777' 200004 ...
'000167,a101,283408' 200002 ...

上面經過拼接形成的分區鍵的表的記錄順序就和原來的表保持一致了。

總結

當表中單個分區索引值的所有行的資料量總大小可能超過 10 GB 時,可以將多個主鍵列拼接成分區鍵,以避免單分區索引值的資料量大小限制。在拼接分區鍵時需要注意以下事項:

  • 選取需要拼接的多個主鍵列,必須能有效地將原來表中相同的分區索引值的記錄,變成擁有不同分區索引值的記錄。

  • 拼接 Integer 類型主鍵列時可以在高位補 0,保持記錄的順序一致。

  • 選取串連符時需要考慮串連符對新的分區鍵的字典序的影響,選取比所有可用字元都小的串連符是一個比較安全的選擇。

在分區鍵中加入雜湊首碼

使用樣本

設計良好的主鍵一節中提到,盡量不要使用 OrderNumber 作為表的分區鍵。因為 OrderNumber 是順序增長的,消費記錄總是被寫入最新的 OrderNumber 範圍之內,舊的 OrderNumber 不再有寫入壓力,造成訪問壓力不均勻的現象,以致預留讀/寫輸送量得不到高效利用。如果必須使用順序增長的索引值作為分區鍵,我們可以對分區鍵拼接雜湊首碼,讓相連的 OrderNumber 在表中隨機分布,使訪問壓力分布均勻。

以 OrderNumber 為分區鍵的消費記錄表如下所示:

OrderNumber DeviceID SellerID CardID attrs
200001 16 'a100' 66661 ...
200002 167 'a101' 283408 ...
200003 54 'a100' 6777 ...
200004 54 'a1001' 6777 ...
200005 66 'b304' 178994 ...

對 OrderNumber 使用 md5 演算法計算首碼(您也可以採取其他雜湊散列演算法),拼接成 HashOrderNumber。因為 md5 演算法計算得到的雜湊字串可能過長,我們只需要取前幾位就能達到讓 OrderNumber 相連的記錄在表中隨機分布的目的。在這個例子中我們取前 4 位:

HashOrderNumber DeviceID SellerID CardID attrs
'2e38200004' 54 'a1001' 6777 ...
'a5a9200003' 54 'a100' 6777 ...
'c335200005' 66 'b304' 178994 ...
'db6e200002 167 'a101' 283408 ...
'ddba200001' 16 'a100' 66661 ...

在後續訪問消費記錄時,使用相同的演算法對 OrderNumber 計算雜湊首碼,即可得到對應消費記錄的 HashOrderNumber。在分區鍵中加入雜湊首碼的弊端是,原來連續的記錄會被打散,無法再使用 GetRange 操作讀取一段範圍內在邏輯上連續的記錄。

並行寫入資料

Table Store的表會被切分成多個分區,這些分區被分散在多個Table Store的伺服器上。如果有一批資料要上傳到Table Store中,同時這批資料是按主鍵排列順序的,若按順序寫入資料,可能會導致寫入壓力集中在某個分區中,而其他的分區處於空閑狀態,無法有效利用預留讀/寫輸送量,影響資料匯入速度。

可以採取以下任一措施來提升匯入資料的速率:

  • 將未經處理資料順序打亂後再進行匯入,以保證寫入資料均勻地分配在各個分區上。

  • 使用多個背景工作執行緒並行匯入資料。把大的資料集合切分成很多個小集合,背景工作執行緒隨機選取小集合進行資料匯入。

區分冷資料和熱資料

資料往往具有時效性。例如,在設計良好的主鍵一節中提到的儲存消費記錄表,近期產生的消費記錄被訪問的可能性較大,因為應用程式需要及時地對消費記錄進行處理和統計,或者查詢最近的消費記錄。但是年代久遠的消費記錄被查詢的可能性不大,這些資料漸漸成為冷資料,但仍然佔用儲存空間。

其次,表中存在大量冷資料會導致資料訪問壓力不均勻,從而導致表上配置的預留讀/寫輸送量無法被充分利用。例如,已經畢業的學生的卡片,不會再產生消費記錄。假如 CardID 是隨著卡片申請時間遞增的,以 CardID 作為分區鍵,會導致已經畢業的學生的 CardID 沒有訪問壓力卻被分配到預留讀/寫輸送量,造成浪費。

為瞭解決這種問題,可以用不同的表來區分冷熱資料,並設定不同的預留讀/寫輸送量。例如,將消費記錄按月份分表,每一個新的自然月就換一張新的表。當月的消費記錄表需要不停寫入新的消費記錄,同時有查詢操作。當月的消費記錄表可以設定一個較大的預留讀/寫輸送量配置來滿足訪問需求。前幾個月的表由於不再寫入新資料或者寫入的新資料量較少,查詢的請求較多,因此前幾個月的消費記錄表可以設定較小的預留寫輸送量,較大的預留讀輸送量。而曆史超過一年的消費記錄表,由於再被使用的可能性不大,可以設定較小的預留讀/寫輸送量配置。已經超出維護年限的消費記錄表可以將資料匯出,存入 OSS(Object Storage Service)歸檔,或直接刪除。