架構設計之數據分片


數據分片技術作為目前架構設計中處理大數據的一種常規手段,當前被廣泛用於緩存、數據庫、消息隊列等中間件的開發與使用當中,例如在數據量較大的項目當中,系統的性能瓶頸主要來自於與數據庫的交互,而通過合理的設計數據庫分片規則,可將系統中的數據分布在不同的物理數據庫中,平衡了單點的數據量與訪問壓力,達到提升應用系統數據處理速度的目的,從而提高系統的整體性能;

數據庫分片的概念

數據分片概念就是按照一定的規則,將數據集划分成相對獨立的數據子集,然后將數據子集分布到不同的節點上,這個節點可以是邏輯上節點,也可以是物理上的節點。數據分片需要按照一定的規則,不同的分布式場景需要設計不同的規則,但基本都遵循同樣的原則:按照最主要、最頻繁使用的訪問方式來分片。在常規的項目開發當中,一般有以下三種方式對數據進行分片: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有着相對固定的實現方式,按數據范圍則需要結合業務數據屬性進行分析,我們要意識到數據分片在項目中不是一個孤立的問題,它關系着數據備份、一致性、可用性、負載均衡、數據訪問與操作等等一系列問題,所以需要系統性的去學習與思考,本文內容只是一個基礎性的闡述與總結,其中如有不足與不正確的地方還望指正與海涵,十分感謝。

 

關注微信公眾號,查看更多技術文章。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM