大而化小。html
隨着業務的快速發展,數據量也在飛速增加。單個存儲節點的容量和併發讀寫都是有瓶頸的。怎麼辦呢?要解決這個問題,只要思考一個問題便可:在一億個數中找一個數,和在一百個數中找一個數,哪一個更快 ? 顯然是後者。java
應對數據量膨脹的有效之法便是數據分區。將一個大的數據集劃分爲多個小的數據集分別進行管理,叫作數據分區。數據分區是分治策略的一種形式。本文總結關於數據分區的基本知識以及實踐。node
總入口見:「互聯網應用服務端的經常使用技術思想與機制綱要」
算法
經常使用的數據分區方式有兩種:範圍分區和哈希分區。根據狀況,還能夠採用冷熱分區、混合分區等。數據庫
範圍分區數組
將整個數據集按照順序編號每 N 個一組劃分紅相對均勻的若干段連續範圍。連續範圍一般也是有序的。B+ 樹的內節點和葉子節點實際上就是一個有序範圍分區。範圍分區能夠有效支持範圍查找。緩存
最經常使用的範圍分區按時間分區。按時間分區有兩個問題:安全
哈希分區架構
經過一個或多個哈希函數,對數據的 key 計算出哈希值,而後按照哈希值取模來落到某個分區。哈希分區能讓數據分佈更加均勻,但沒法避免熱點 key 的熱點訪問。哈希分區不支持範圍查找。併發
採用哈希分區的例子: DB 按照業務 ID 取模進行分庫分表; ES 按照業務 ID 取模進行分片。
冷熱分區
將變化相對恆定的熱數據單獨放在一個分區裏,將冷數據放在歸檔分區裏。
冷熱分區的例子: 好比未完成訂單相對於已完成訂單是熱數據,並且未完成訂單的量在長期看來不會快速增加。所以,能夠將未完成訂單單獨放在一個 ES 索引裏(內部還能夠分片),提供搜索。
混合分區
結合使用範圍分區和哈希分區。可使用某個列的前綴來構建哈希分區,而使用整列數據來構建範圍分區。固然,這也增大了存儲空間開銷和運維開銷。
分區字段
分區字段的選擇一般遵循兩個原則:
分區數及大小
分區一般指的是邏輯分區,須要分配到物理節點上。一個物理節點一般有多個分區。要肯定分區數及分區大小。分區大小一般以某個數據量爲最大限度。
要估算分區數,須要拿到一些基本數據:
熱點數據
不管是範圍分區仍是哈希分區,瞬時大併發的熱點 key 的訪問都是難以免和應對的。熱點 key 訪問的可考慮方案:
輔助字段查詢
輔助字段的查詢,一般是先找到輔助字段所關聯的分區字段(主鍵),再按分區字段進行查詢。須要構建「輔助字段-分區字段」的映射信息。這個映射信息的存儲和分區有兩種方式:
分區再均衡
當數據量/訪問量劇增須要增長數據節點,或者機器宕機須要下線數據節點時,原有分區的數據須要在變動後的節點集合上從新分佈。稱爲分區再均衡。
分區再均衡的方法 hash Mod N 。靜態分區是採用固定分區數,動態分區則會增長或減小分區數。動態分區有利於讓分區數據大小不超出某個最大限制。
分區再均衡有兩種方案:
爲何 DB 通常採用固定分區 ? 由於 DB 每每要支持多個字段的查詢,除了主字段分區之外,還要考慮輔字段分區。動態分區會增大這種複雜性。而 K-V 存儲通常只支持主字段查詢,沒有額外要考慮。
DB
實際應用中,最多見的數據分區就是 DB 的分庫分表。分庫分表有水平和垂直兩個維度。水平,一般是按行;垂直,一般是按業務或字段。水平分庫,是將單個庫的數據切分爲多個庫;水平分表,是將單個表的數據切分爲多個表。 庫和表的 Schema 都是與原來徹底一致的。
那麼,什麼時候採用分庫,什麼時候採用分表呢 ? 分庫和分表的數量如何定?如何進行實際的分庫分表操做?有哪些要注意的事項呢?
分庫也能達到分表的效果。那麼什麼時候採用分表呢?若是表的數據量上漲,可是單庫的併發讀寫容量並無多少上漲,則採用分表會更簡單一些,運維成本應該也少一些。若是是由於須要支撐更多的併發讀寫,則首選分庫,能足夠解決併發讀寫的問題。單庫的併發讀寫通常保持在 1000-2000 之間。分庫以後,一般同時也實現了分表。若是不夠,再細分表。分庫分表的乘積數量一般選擇 2 的冪次,由於在將數據分佈到某個分區上時,須要進行取模操做,對 2 的 N 冪次取模只要取低 N 位便可。分庫和分表也須要考慮好幾年之用。通常 512, 1024 比較多。由於擴容時比較麻煩,須要進行分區再均衡。對於運行在線上的服務來講,若是須要人工來作,風險會比較高。
分庫分表的實際步驟:
對於第一點來講,要着重考慮數據不丟失、不重疊。要保證數據不丟失,則須要將切換的這一小段時間的數據積壓在新庫這邊,待開啓新庫讀寫後,這段時間的流量直接進入新庫,再同步到老庫。切換的瞬間,中止老庫的寫。要保證數據不重疊,須要有惟一索引作保證,或者代碼裏作兼容,且重疊數據量很小。
對於第二點來講,要考慮數據一致性。通常採用雙寫模式能夠避免這一點。也就是,切換以後,異步寫老庫。這樣,新流量老是進入老庫。或者評估業務影響,若是短暫的不一致不影響業務的話,作到最終一致性亦可。
在分庫分表以後,還須要分別考慮讀寫流量及相應的擴容。一般寫主讀從,讀流量更多,保證擴容在從庫上比較合適。由於從庫不直接影響線上服務。
ES
ES 的數據分區體如今分片(Shard)的概念。ES 的全部文檔(Document)存儲在索引(Index)裏。索引是一個邏輯名字空間,包含一個或多個物理分片。一個分片的底層即爲一個 Lucene 索引。ES 的全部文檔均衡存儲在集羣中各個節點的分片中。ES 也會在節點之間自動移動分片,以保證集羣的數據均衡。ES 分片分爲主分片和複製分片。每一個文檔都屬於某個主分片。主分片在索引建立以後就固定了。複製分片用來實現冗餘和容錯,複製分片是可變的。
ES 對文檔的操做是在分片的單位內進行的。實際上就是針對倒排索引的操做。倒排索引是不可變的,所以能夠放在內核文件緩衝區裏支持併發讀。ES 更新文檔必須重建索引,而不是直接更新現有索引。爲了支持高效更新索引,倒排索引的搜索結構是一個 commit point 文件指明的待寫入磁盤的 Segment 列表 + in-memory indexing buffer 。Segment 能夠看作是 ES 的可搜索最小單位。新文檔會先放在 in-memory indexing buffer 裏。當文檔更新時,新版本的文檔會移動到 commit point 裏,而老版本的文檔會移動到 .del 文件裏異步刪除。ES 經過 fsync 操做將 Segment 寫入磁盤進行持久化。因爲 ES 能夠直接打開處於文件緩衝區的 commit point 文件中的 Segment 進行查詢(默認 1s 刷新),使得查詢沒必要寫入磁盤後才能查詢到,從而作到準實時。
ES 分片策略會影響 ES 集羣的性能、安全和穩定性。ES 分片策略主要考慮的問題:分片算法如何?須要多少分片?分片大小如何 ? 分片算法能夠按照時間分區,也能夠按照取模分區。分片數估算有一個經驗法則:確保對於節點上已配置的每一個 GB,將分片數量保持在 20 如下。若是某個節點擁有 30GB 的堆內存,那其最多可有 600 個分片,可是在此限值範圍內,設置的分片數量越少,效果就越好。通常而言,這能夠幫助集羣保持良好的運行狀態。分片應當儘可能分佈在不一樣的節點上,避免資源爭用。
HBase
HBase 的數據分區體如今 Region。 Region 是 HBase 均衡地存儲數據的基本單元。Region 數據的均勻性,體如今 Rowkey 的設計上。 HBase Region 具備自動拆分能力,能夠指定拆分策略,Region 在達到指定條件時會自動拆分紅兩個。能夠指定的拆分策略有: IncreasingToUpperBoundRegionSplitPolicy 根據公式min(r^2*flushSize,maxFileSize) 肯定的大小;ConstantSizeRegionSplitPolicy Region 大小超過指定值 maxFileSize;DelimitedKeyPrefixRegionSplitPolicy 以指定分隔符的前綴 splitPoint,確保相同前綴的數據劃分到同一 Region;KeyPrefixRegionSplitPolicy 指定 Rowkey 前綴來劃分,適合於固定前綴。
除了 Region 自動拆分,還須要進行 Region 預分區。Region 預分區須要指定分爲多少個 Region ,每一個 Region 的 startKey 和 endKey (底層會轉化爲 byte 數組)。 若是數據可以比較均勻落到指定的 startKey 和 endKey, 就能夠避免後續頻繁的 Region Split。Region Split 雖然靈活,卻會消耗集羣資源,影響集羣性能和穩定性。
HBase Region 的大小及數量的肯定,可參考業界實踐 「HBase最佳實踐之Region數量&大小」。官方推薦的 Regionserver上的 Region 個數範圍在 20~200;每一個 Region 的大小在 10G~30G 之間。
範圍分區
假設有固定長度爲 strlen 的字符串,字符取值集合限於 a-z ,且取值隨機,要劃分爲 n 個分區。那麼分區範圍計算以下:
public class StringDividing { private static char[] chars = new char[] { 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z' }; private static final Integer CHAR_LEN = 26; public static List<String> divide(int strlen, int n) { int maxValue = maxValue(strlen); List<Integer> ranges = Dividing.divideBy(maxValue, n); return ranges.stream().map(num -> int2str(num, strlen)).collect(Collectors.toList()); } public static int maxValue(int m) { int multiply = 1; while (m>0) { multiply *= CHAR_LEN; m--; } return multiply - 1; } /** * 將整型轉換爲對應的字符串 */ private static String int2str(int num, int strlen) { if (num < CHAR_LEN) { return nchars('a', strlen-1) + chars[num]; } StringBuilder s = new StringBuilder(); while ( num >= CHAR_LEN) { int r = num % CHAR_LEN; num = num / CHAR_LEN; s.append(chars[r]); } s.append(chars[num % CHAR_LEN]); return s.reverse().toString() + nchars('a', strlen-s.length()); } private static String nchars(char c, int n) { StringBuilder s = new StringBuilder(); while (n > 0) { s.append(c); n--; } return s.toString(); } public static void main(String[] args) { for (int len=1; len < 6; len++) { divide(len,8).forEach( e -> System.out.println(e) ); } } } public class Dividing { public static List<Integer> divideBy(int totalSize, int num) { List<Integer> parts = new ArrayList<Integer>(); if (totalSize <= 0) { return parts; } int i = 0; int persize = totalSize / num; while (num > 0) { parts.add(persize*i); i++; num--; } return parts; } }
哈希分區
這裏抽取了 Dubbo 的一致性哈希算法實現。核心是 TreeMap[Long, T] virtualNodes 的變動操做和 key 的哈希計算。
package zzz.study.algorithm.dividing; import com.google.common.collect.Lists; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.function.BiConsumer; import java.util.function.Function; public class ConsistentHashLoadBalance { public static void main(String[] args) { List<String> nodes = Lists.newArrayList("192.168.1.1", "192.168.2.25", "192.168.3.255", "255.255.1.1"); ConsistentHashSelector<String> selector = new ConsistentHashSelector(nodes, Function.identity()); test(selector); selector.addNode("8.8.8.8"); test(selector); } private static void test(ConsistentHashSelector<String> selector) { Map<String, List<Integer>> map = new HashMap<>(); for (int i=1; i < 16000; i+=1) { String node = selector.select(String.valueOf(i)); List<Integer> objs = map.getOrDefault(node, new ArrayList<>()); objs.add(i); map.put(node, objs); } map.forEach( (key, values) -> { System.out.println(key + " contains: " + values.size() + " --- " + values); } ); } private static final class ConsistentHashSelector<T> { private final TreeMap<Long, T> virtualNodes; private final int replicaNumber = 160; private final Function<T, String> keyFunc; ConsistentHashSelector(List<T> nodes, Function<T, String> keyFunc) { this.virtualNodes = new TreeMap<Long, T>(); this.keyFunc = keyFunc; assert keyFunc != null; for (T node : nodes) { addNode(node); } } public boolean addNode(T node) { opNode(node, (m, no) -> virtualNodes.put(m,no)); return true; } public boolean removeNode(T node) { opNode(node, (m, no) -> virtualNodes.remove(m)); return true; } public void opNode(T node, BiConsumer<Long, T> hashFunc) { String key = keyFunc.apply(node); for (int i = 0; i < replicaNumber / 4; i++) { byte[] digest = md5(key + i); for (int h = 0; h < 4; h++) { long m = hash(digest, h); hashFunc.accept(m, node); } } } public T select(String key) { byte[] digest = md5(key); return selectForKey(hash(digest, 0)); } private T selectForKey(long hash) { Map.Entry<Long, T> entry = virtualNodes.ceilingEntry(hash); if (entry == null) { entry = virtualNodes.firstEntry(); } return entry.getValue(); } private long hash(byte[] digest, int number) { return (((long) (digest[3 + number * 4] & 0xFF) << 24) | ((long) (digest[2 + number * 4] & 0xFF) << 16) | ((long) (digest[1 + number * 4] & 0xFF) << 8) | (digest[number * 4] & 0xFF)) & 0xFFFFFFFFL; } private byte[] md5(String value) { MessageDigest md5; try { md5 = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e.getMessage(), e); } md5.reset(); byte[] bytes; try { bytes = value.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException(e.getMessage(), e); } md5.update(bytes); return md5.digest(); } } }
分治是最爲基本的計算機思想之一。而數據分區是應對海量數據處理的基本前提。常見數據分區有範圍分區和哈希分區兩種,根據狀況選用。
分區是邏輯概念。分區每每會分佈到多個機器節點上。數據分區要考慮數據均勻分佈問題、分區大小及分區數。數據分區加冗餘,構成了高可用分佈式系統的基礎。