架構設計之資料分片

bigfan發表於2021-08-04

資料分片技術作為目前架構設計中處理大資料的一種常規手段,當前被廣泛用於快取、資料庫、訊息佇列等中介軟體的開發與使用當中,例如在資料量較大的專案當中,系統的效能瓶頸主要來自於與資料庫的互動,而通過合理的設計資料庫分片規則,可將系統中的資料分佈在不同的物理資料庫中,平衡了單點的資料量與訪問壓力,達到提升應用系統資料處理速度的目的,從而提高系統的整體效能;

資料庫分片的概念

資料分片概念就是按照一定的規則,將資料集劃分成相對獨立的資料子集,然後將資料子集分佈到不同的節點上,這個節點可以是邏輯上節點,也可以是物理上的節點。資料分片需要按照一定的規則,不同的分散式場景需要設計不同的規則,但基本都遵循同樣的原則:按照最主要、最頻繁使用的訪問方式來分片。在常規的專案開發當中,一般有以下三種方式對資料進行分片:hash方式、一致性hash、按照資料範圍,每種分片方式是否適用,一方面需要結合專案的實際情況與規模,另一方面也要從幾個常規的維度去評估:

1、 資料分片策略,也就是具體的分片方式

2、 資料分片節點的動態擴充套件,隨著資料量的逐步增長,是否能夠通過增加節點來動態擴充套件適應

3、 資料分片節點的負載均衡‘,結合分片策略能否保證資料均勻的分佈在各個節點上以及各個節點的負載壓力是否均衡

4、 資料分片的可用性,當其中一個節點產生異常,能否將該節點的資料轉移到其他節點上

下面我們就對三種常規的分片模式做個基本的介紹

hash方式

通過對資料(一般為Key值)先進行hash計算再取模的方式是一種簡單且使用頻繁的分片方式,也就是Hash(Key)%N,這裡的N大部分情況下就是我們的結點個數,這種方式相對簡單實用,一般場景下能夠滿足我們的要求。但Hash取模方式主要的問題是節點擴容或縮減的時候,會產生大量的資料遷移,比如從N臺裝置擴容到N+1臺,絕大部分的資料都要在裝置間進行遷移。該種方式程式碼實現較為簡單,既可以採用jdk自帶的hash方式也可以採用其他hash演算法,大家可以自行搜尋具體實現。

一致性hash

一致性hash是將資料按照特徵值對映到一個首尾相接的hash環上,同時也將節點對映到這個環上。對於資料,從資料在環上的位置開始,順時針找到的第一個節點即為資料的儲存節點。這種模式的優點在於節點一旦需要擴容或縮減的時候只會影響到hash環上相鄰的節點,不會發生大規模的資料遷移。分片方式如下圖所示

但是常規的一致性hash分片模式也有缺點,一致性hash方式在增加節點的時候,只能分攤一個已存在節點的壓力,在其中一個節點掛掉的時候,該節點的壓力也會被全部轉移到下一個節點。理想的目標是當節點動態發生變化時,已存在的所有節點都能參與進來,達到新的均衡狀態。因此在實際開發中一般會引入虛擬節點(virtual node)的概念,即不是將物理節點對映在hash環上,而是將虛擬節點對映到hash環上。虛擬節點的數目遠大於物理節點,因此一個物理節點需要負責多個虛擬節點的真實儲存。運算元據的時候,先通過hash環找到對應的虛擬節點,再通過虛擬節點與物理節點的對映關係找到對應的物理節點。

引入虛擬節點後的一致性hash需要維護的後設資料也會增加:第一,虛擬節點在hash環上的問題,且虛擬節點的數目又比較多;第二,虛擬節點與物理節點的對映關係。但帶來的好處是明顯的,當一個物理節點失效時,hash環上多個虛擬節點失效,對應的壓力也就會發散到多個其餘的虛擬節點,事實上也就是多個其餘的物理節點。在增加物理節點的時候同樣如此。除此之外,可以根據物理節點的效能來調整每一個物理節點對於虛擬節點的數量,充分、合理利用資源。下面看下引入虛擬節點的一致性hash的程式碼實現

    /**
     * 節點資訊
     *
     */
    class Node {
     
        private String host;//IP資訊
     
        private int load;//負載因子
    
        public String getHost() {
            return host;
        }
     
        public void setHost(String host) {
            this.host = host;
        }
        
        public int getLoad() {
            return load;
        }
    
        public void setLoad(int load) {
            this.load = load;
        }
     
    
        public Node(String host, int load) {
            super();
            this.host = host;
            this.load = load;
        }
     
        @Override
        public String toString() {
            return "Node [host=" + host + ", 負載因子=" + load + "]";
        }
    }


     // 真實節點列表
    private static List<Node> realNodes = new ArrayList<Node>();
 
    // 虛擬節點,key是Hash值,value是虛擬節點資訊
    private static SortedMap<Integer, String> virtualMap = new TreeMap<Integer, String>();
 
    static {
        //初始化真實節點列表
        realNodes.add(new Node("192.168.1.1", 5));
        realNodes.add(new Node("192.168.1.2", 10));
        realNodes.add(new Node("192.168.1.3", 20));
        realNodes.add(new Node("192.168.1.4", 5));
        for (Node node : realNodes) { //新增虛擬節點
            for (int i = 0; i < node.getLoad(); i++) {
                String server = node.getHost();
                String virtualNode = server + "&&VN" + i;
                int hash = getHash(virtualNode);
                virtualMap.put(hash, virtualNode);
            }
        }
    }
    
    /**
     * FNV1_32_HASH演算法
     */
    private static int getHash(String str) {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < str.length(); i++)
            hash = (hash ^ str.charAt(i)) * p;
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;
        // 如果算出來的值為負數則取其絕對值
        if (hash < 0)
            hash = Math.abs(hash);
        return hash;
    }
 
    /**
     * 獲取被分配的節點名
     * 
     * @param node
     * @return
     */
    public static Node getNode(String key) {
        int hash = getHash(key);//
        Integer keyNode = null;
        // 得到大於該Hash值的所有Map
        SortedMap<Integer, String> subMap = virtualMap.tailMap(hash);
        if (subMap.isEmpty()) {//在這裡形成一個環形結構
             //如果沒有比該key的hash值大的,則從第一個node開始
            keyNode = virtualMap.firstKey();
        } else {
            //獲取第一個key值,也就是順時針第一個節點
            keyNode = subMap.firstKey();
        }
        String virtualNode = virtualMap.get(keyNode);//獲取虛擬節點
        String realNodeName = virtualNode.substring(0, virtualNode.indexOf("&&"));
        for (Node node : realNodes) {//根據虛擬節點獲取真實節點
            if (node.getHost().equals(realNodeName)) {
                return node;
            }
        }
        return null;
    }

按資料範圍(range based)

按資料範圍分片其實也就是基於資料的業務屬性進行分片,如唯一編碼、時間戳、使用頻率等,比如在資料庫層面按ID範圍、按時間進行分庫、分表、分片,按資料被訪問頻率分為熱點庫與歷史庫等方法,都是按資料範圍方式的具體應用。基於資料範圍的分片模式需要貼合專案實際場景,使用中需要注意以下幾點:

1、 分片與擴充套件實現比較簡單,結合ID範圍、時間結合業務自行實現即可;

2、較為依賴備份機制,否則某個節點發生異常無法迅速恢復,可用性較難保證;​

3、對資料規模要有前瞻性的評估,例如按時間分片,需要考慮單位時間片內資料分佈是否均勻;

4、注意各分片資料之間的效能平衡,因為在常規場景下,無論採用哪種基於資料範圍的分片模式,都是距離當前時間點較近的資料被訪問和操作的機率較大,所以要特別注意隨著資料規模與時間的推移,歷史資料規模不斷膨脹導致的整體效能下降。

 

綜上是對專案開發中我們使用的資料分片模式的一個簡單總結,hash與一致性hash有著相對固定的實現方式,按資料範圍則需要結合業務資料屬性進行分析,我們要意識到資料分片在專案中不是一個孤立的問題,它關係著資料備份、一致性、可用性、負載均衡、資料訪問與操作等等一系列問題,所以需要系統性的去學習與思考,本文內容只是一個基礎性的闡述與總結,其中如有不足與不正確的地方還望指正與海涵,十分感謝。

 

關注微信公眾號,檢視更多技術文章。

 

相關文章