Hazelcast集群服務(1)——Hazelcast介紹


Hazelcast是什么

    “分布式”、“集群服務”、“網格式內存數據”、“分布式緩存“、“彈性可伸縮服務”——這些牛逼閃閃的名詞拿到哪都是ITer裝逼的不二之選。在Javaer的世界,有這樣一個開源項目,只需要引入一個jar包、只需簡單的配置和編碼即可實現以上高端技能,他就是 Hazelcast

    Hazelcast 是由Hazelcast公司(沒錯,這公司也叫Hazelcast!)開發和維護的開源產品,可以為基於jvm環境運行的各種應用提供分布式集群和分布式緩存服務。Hazelcast可以嵌入到任何使用Java、C++、.NET開發的產品中(C++、.NET只提供客戶端接入)。Hazelcast目前已經更新到3.X版本,Java中絕大部分數據結構都被其以為分布式的方式實現。比如Javaer熟悉的Map接口,當通過Hazelcast創建一個Map實例后,在節點A調用 Map::put("A","A_DATA") 方法添加數據,節點B使用 Map::get("A") 可以獲到值為"A_DATA" 的數據。Hazelcast 提供了 Map、Queue、MultiMap、Set、List、Semaphore、Atomic 等接口的分布式實現;提供了基於Topic 實現的消息隊列或訂閱\發布模式;提供了分布式id生成器(IdGenerator);提供了分布式事件驅動(Distributed Events);提供了分布式計算(Distributed Computing);提供了分布式查詢(Distributed Query)。總的來說在獨立jvm經常使用數據結果或模型 Hazelcast 都提供了分布式集群的實現。

    Hazelcast 有開源版本和商用版本。開源版本遵循 Apache License 2.0 開源協議免費使用。商用版本需要獲取特定的License,兩者之間最大的區別在於:商用版本提供了數據高密度存儲。我們都知道jvm有自己特定的GC機制,無論數據是在堆還是棧中,只要發現無效引用的數據塊,就有可能被回收。而Hazelcast的分布式數據都存放在jvm的內存中,頻繁的讀寫數據會導致大量的GC開銷。使用商業版的Hazelcast會擁有高密度存儲的特性,大大降低Jvm的內存開銷,從而降低GC開銷。

    很多開源產品都使用Hazelcast 來組建微服務集群,例如咱們的Vert.x,首選使用Hazelcast來組建分布式服務。有興趣可以看我的這篇分享——http://my.oschina.net/chkui/blog/678347 ,文中說明了Vert.x如何使用Hazelcast組建集群。

    附:

Hazelcast的特性

自治集群(無中心化)

    Hazelcast 沒有任何中心節點(文中的節點可以理解為運行在任意服務器的獨立jvm,下同),或者說Hazelcast 不需要特別指定一個中心節點。在運行的過程中,它自己選定集群中的某個節點作為中心點來管理所有的節點。

數據按應用分布式存儲

    Hazelcast 的數據是分布式存儲的。他會將數據盡量存儲在需要使用該項數據的節點上,以實現數據去中心化的目的。在傳統的數據存儲模型中(MySql、MongDB、Redis 等等)數據都是獨立於應用單獨存放,當需要提升數據庫的性能時,需要不斷加固單個數據庫應用的性能。即使是現在大量的數據庫支持集群模式或讀寫分離,但是基本思路都是某幾個庫支持寫入數據,其他的庫不斷的拷貝更新數據副本。這樣做的壞處一是會產生大量臟讀的問題,二是消耗大量的資源來傳遞數據——從數據源頻繁讀寫數據會耗費額外資源,當數據量增長或創建的主從服務越來越多時,這個消耗呈指數級增長。

    使用 Hazelcast 可以有效的解決數據中心化問題。他將數據分散的存儲在每個節點中,節點越多越分散。每個節點都有各自的應用服務,而Hazelcast集群會根據每個應用的數據使用情況分散存儲這些數據,在應用過程中數據會盡量“靠近”應用存放。這些在集群中的數據共享整個集群的存儲空間和計算資源。

抗單點故障

    集群中的節點是無中心化的,每個節點都有可能隨時退出或隨時進入。因此,在集群中存儲的數據都會有一個備份(可以配置備份的個數,也可以關閉數據備份)。這樣的方式有點類似於 hadoop,某項數據存放在一個節點時,在其他節點必定有至少一個備份存在。當某個節點退出時,節點上存放的數據會由備份數據替代,而集群會重新創建新的備份數據。

簡易性

    所有的 Hazelcast 功能只需引用一個jar包,除此之外,他不依賴任何第三方包。因此可以非常便捷高效的將其嵌入到各種應用服務器中,而不必擔心帶來額外的問題(jar包沖突、類型沖突等等)。他僅僅提供一系列分布式功能,而不需要綁定任何框架來使用,因此適用於任何場景。

    除了以上特性,Hazelcast 還支持服務器/客戶端模型,支持腳本管理、能夠和 Docker 快速整合等等。

簡單使用例子

    前面說了那么多概念,必須要來一點干貨了。下面是一個使用 Hazelcast 的極簡例子。文中的所有代碼都在github上:https://github.com/chkui/hazelcast-demo

    首先引入Hazelcast的jar包。

    Maven(pom.xml)

<dependency>
    <groupId>com.hazelcast</groupId>
    <artifactId>hazelcast</artifactId>
    <version>${hazelcast.vertsion}</version>
</dependency>

    Gradle(build.gradle)

compile com.hazelcast:hazelcast:${hazelcast.vertsion}

    先創一個建 Hazelcast 節點:

//org.palm.hazelcast.getstart.HazelcastGetStartServerMaster 
public class HazelcastGetStartServerMaster {
    public static void main(String[] args) {
        // 創建一個 hazelcastInstance實例
        HazelcastInstance instance = Hazelcast.newHazelcastInstance();
        // 創建集群Map
        Map<Integer, String> clusterMap = instance.getMap("MyMap");
        clusterMap.put(1, "Hello hazelcast map!");

        // 創建集群Queue
        Queue<String> clusterQueue = instance.getQueue("MyQueue");
        clusterQueue.offer("Hello hazelcast!");
        clusterQueue.offer("Hello hazelcast queue!");
    }
}

    上面的代碼使用 Hazelcast 實例創建了一個節點。然后通過這個實例創建了一個分布式的Map和分布式的Queue,並向這些數據結構中添加了數據。運行這個main方法,會在console看到以下內容:

Members [1] {
    Member [192.168.1.103]:5701 this
}

     隨后再創建另外一個節點:

// org.palm.hazelcast.getstart.HazelcastGetStartServerSlave
public class HazelcastGetStartServerSlave {
    public static void main(String[] args) {
        //創建一個 hazelcastInstance實例
        HazelcastInstance instance = Hazelcast.newHazelcastInstance();
        Map<Integer, String> clusterMap = instance.getMap("MyMap");
        Queue<String> clusterQueue = instance.getQueue("MyQueue");
        
        System.out.println("Map Value:" + clusterMap.get(1));
        System.out.println("Queue Size :" + clusterQueue.size());
        System.out.println("Queue Value 1:" + clusterQueue.poll());
        System.out.println("Queue Value 2:" + clusterQueue.poll());
        System.out.println("Queue Size :" + clusterQueue.size());
    }
}

    該節點的作用是從Map、Queue中讀取數據並輸出。運行會看到以下輸出

Members [2] {
    Member [192.168.1.103]:5701
    Member [192.168.1.103]:5702 this
}

八月 06, 2016 11:33:29 下午 com.hazelcast.core.LifecycleService
信息: [192.168.1.103]:5702 [dev] [3.6.2] Address[192.168.1.103]:5702 is STARTED
Map Value:Hello hazelcast map!
Queue Size :2
Queue Value 1:Hello hazelcast!
Queue Value 2:Hello hazelcast queue!
Queue Size :0

    至此,2個節點的集群創建完畢。第一個節點向map實例添加了{key:1,value:"Hello hazelcast map!"},向queue實例添加[“Hello hazelcast!”,“Hello hazelcast queue!”],第二個節點讀取並打印這些數據。

    除了直接使用Hazelcast服務來組建集群,Hazelcast還提供了區別於服務端的客戶端應用包。客戶端與服務端最大的不同是:他不會存儲數據也不能修改集群中的數據。目前客戶端有C++、.Net、Java多種版本。

    使用客戶端首先要引入客戶端jar包。

    Maven(pom.xml)

<dependency>
    <groupId>com.hazelcast</groupId>
    <artifactId>hazelcast-client</artifactId>
    <version>${hazelcast.version}</version>
</dependency>

    Gradle(build.gradle)

compile com.hazelcast:hazelcast-client:${hazelcast.vertsion}

    創建一個client節點。

public class HazelcastGetStartClient {
    public static void main(String[] args) {
        ClientConfig clientConfig = new ClientConfig();
        HazelcastInstance instance = HazelcastClient.newHazelcastClient(clientConfig);
        Map<Integer, String> clusterMap = instance.getMap("MyMap");
        Queue<String> clusterQueue = instance.getQueue("MyQueue");
        
        System.out.println("Map Value:" + clusterMap.get(1));
        System.out.println("Queue Size :" + clusterQueue.size());
        System.out.println("Queue Value 1:" + clusterQueue.poll());
        System.out.println("Queue Value 2:" + clusterQueue.poll());
        System.out.println("Queue Size :" + clusterQueue.size());
    }
}

    然后先啟動 HazelcastGetStartServerMaster::main,再啟動 HazelcastGetStartClient::main。可以看到客戶端輸出:

Members [1] {
    Member [192.168.197.54]:5701
}

八月 08, 2016 10:54:22 上午 com.hazelcast.core.LifecycleService
信息: HazelcastClient[hz.client_0_dev][3.6.2] is CLIENT_CONNECTED
Map Value:Hello hazelcast map!
Queue Size :2
Queue Value 1:Hello hazelcast!
Queue Value 2:Hello hazelcast queue!
Queue Size :0

    至此,客戶端功能也創建完畢 。可以看到客戶端的console輸出內容比服務端少了很多,這是因為客戶端不必承載服務端的數據處理功能,也不必維護各種節點信息。

例子運行解析

    下面我們根據console的輸出來看看 Hazelcast 啟動時到底干了什么事。(下面的輸出因環境或IDE不同,可能會有差異)

class: com.hazelcast.config.XmlConfigLocator
info: Loading 'hazelcast-default.xml' from classpath. 

    這里輸出的內容表示Hazelcast啟動時加載的配置文件。如果用戶沒有提供有效的配置文件,Hazelcast會使用默認配置文件。后續的文章會詳細說明 Hazelcast 的配置。

class: com.hazelcast.instance.DefaultAddressPicker
info: Prefer IPv4 stack is true.
class: com.hazelcast.instance.DefaultAddressPicker
info: Picked Address[192.168.197.54]:5701, using socket ServerSocket[addr=/0:0:0:0:0:0:0:0,localport=5701], bind any local is true

    這一段輸出說明了當前 Hazelcast 的網絡環境。首先是檢測IPv4可用且檢查到當前的IPv4地址是192.168.197.54。然后使用IPv6啟用socket。在某些無法使用IPv6的環境上,需要強制指定使用IPv4,增加jvm啟動參數:-Djava.net.preferIPv4Stack=true 即可。

class: com.hazelcast.system
info: Hazelcast 3.6.2 (20160405 - 0f88699) starting at Address[192.168.197.54]:5701
class: com.hazelcast.system
info: [192.168.197.54]:5701 [dev] [3.6.2] Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved.

     這一段輸出說明了當前實例的初始化端口號是5701。Hazelcast 默認使用5701端口。如果發現該端口被占用,會+1查看5702是否可用,如果還是不能用會繼續向后探查直到5800。Hazelcast 默認使用5700到5800的端口,如果都無法使用會拋出啟動異常。

class: com.hazelcast.system
info: [192.168.197.54]:5701 [dev] [3.6.2] Configured Hazelcast Serialization version : 1
class: com.hazelcast.spi.OperationService
info: [192.168.197.54]:5701 [dev] [3.6.2] Backpressure is disabled
class: com.hazelcast.spi.impl.operationexecutor.classic.ClassicOperationExecutor
info: [192.168.197.54]:5701 [dev] [3.6.2] Starting with 2 generic operation threads and 4 partition operation threads.

     這一段說明了數據的序列化方式和啟用的線程。Hazelcast 在節點間傳遞數據有2種序列化方式,在后續的文章中會詳細介紹。Hazelcast 會控制多個線程執行不同的工作,有負責維持節點連接的、有負責數據分區管理的。

class: com.hazelcast.instance.Node
info: [192.168.197.54]:5701 [dev] [3.6.2] Creating MulticastJoiner
class: com.hazelcast.core.LifecycleService
info: [192.168.197.54]:5701 [dev] [3.6.2] Address[192.168.197.54]:5701 is STARTING
class: com.hazelcast.nio.tcp.nonblocking.NonBlockingIOThreadingModel
info: [192.168.197.54]:5701 [dev] [3.6.2] TcpIpConnectionManager configured with Non Blocking IO-threading model: 3 input threads and 3 output threads
class: com.hazelcast.cluster.impl.MulticastJoiner
info: [192.168.197.54]:5701 [dev] [3.6.2] 

     上面這一段輸出中,Creating MulticastJoiner表示使用組播協議來組建集群。還創建了6個用於維護非擁塞信息輸出\輸出。

Members [1] {
    Member [192.168.197.54]:5701
    Member [192.168.197.54]:5702 this
}

class: com.hazelcast.core.LifecycleService
info: [192.168.197.54]:5701 [dev] [3.6.2] Address[192.168.197.54]:5701 is STARTED
class: com.hazelcast.partition.InternalPartitionService
info: [192.168.197.54]:5701 [dev] [3.6.2] Initializing cluster partition table arrangement...

    Members[2]表示當前集群只有2個節點。2個節點都在ip為192.168.197.54的這台設備上,2個節點分別占據了5701端口和5702端口。端口后面的this說明這是當前節點,而未標記this的是其他接入集群的節點。最后InternalPartitionService輸出的信息表示集群初始化了“數據分片”,后面會介紹“數據分片”的概念和原理。

    上面就是Hazelcast在默認情況下執行的啟動過程,可以看出在初始化的過程中我們可以有針對性的修改一些Hazelcast的行為:

  1. 使用默認配置文檔 hazelcast-default.xml 來啟動集群。因此我們可以自定義這個配置文件來影響Hazelcast 的行為。
  2. 啟用IPv4或IPv6來建立集群,因此可以知道Hazelcast集群的通信是基於TCP、UDP,需要打開socket支持集群交互。因此我們可以指定使用的通訊方案。
  3. Hazelcast會啟動多個線程來執行不同的工作,有些負責維護數據、有些負責集群通信、有些負責一些基礎操作。因此我們可以配置和管理這些線程。
  4. Hazelcast默認使用MulitCast(組播協議)來組建集群,因此在局域網環境他可以無需配置自己完成集群組建。因此我們可以指定使用TCP/IP或其他通訊協議。
  5. Hazelcast會自己探尋可以使用的端口,默認情況下會使用5700到5800間沒有被占用的端口。因此我們可以配置這些端口如何使用。
  6. Hazelcast初始化一個名為“數據分片”的方案來管理和存儲數據。因此我們可以調整和控制這些數據分片。

    以上所有紅色字體的部分都可以通過配置文件來影響。在后續的文章中會詳細介紹相關的 配置說明(待續)。

-----------------------------------亮瞎人的分割線-----------------------------------

    如果對Hazelcast的基本原理沒什么興趣,就不用向下看“運行結構“和“數據分片原理”了,直接去下一篇了解如何使用Hazelcast吧。

Hazelcast運行結構

    Hazelcast的官網上列舉了2種運行模式,一種是p2p(點對點)模式、一種是在點對點模式上擴展的C/S模式。下圖是p2p模式的拓補結構。

    在p2p模式中,所有的節點(Node)都是集群中的服務節點,提供相同的功能和計算能力。每個節點都分擔集群的總體性能,每增加一個節點都可以線性增加集群能力。

    在p2p服務集群的基礎上,我們可以增加許多客戶端接入到集群中,這樣就形成了集群的C/S模式,提供服務集群視作S端,接入的客戶端視作C端。這些客戶端不會分擔集群的性能,但是會使用集群的各種資源。下圖的結構就是客戶端接入集群的情況。

    可以為客戶端提供特別的緩存功能,告知集群讓那些它經常要使用的數存放在“離它最近”的節點。

Hazelcast分片概念與原理

   Hazelcast通過分片來存儲和管理所有進入集群的數據,采用分片的方案目標是保證數據可以快速被讀寫、通過冗余保證數據不會因節點退出而丟失、節點可線性擴展存儲能力。下面將從理論上說明Hazelcast是如何進行分片管理的。

分片

    Hazelcast的每個數據分片(shards)被稱為一個分區(Partitions)。分區是一些內存段,根據系統內存容量的不同,每個這樣的內存段都包含了幾百到幾千項數據條目,默認情況下,Hazelcast會把數據划分為271個分區,並且每個分區都有一個備份副本。當啟動一個集群成員時,這271個分區將會一起被啟動。

    下圖展示了集群只有一個節點時的分區情況。

                            

    從一個節點的分區情況可以看出,當只啟動一個節點時,所有的271個分區都存放在一個節點中。然后我們啟動第二個節點。會出現下面這樣的分區方式。

                            

    二個節點的圖中,用黑色文字標記的表示主分區,用藍色文字標記的表示復制分區(備份分區)。第一個成員有135個主分區(黑色部分),所有的這些分區都會在第二個成員中有一個副本(藍色部分),同樣的,第一個成員也會有第二個成員的數據副本。

    當增加更多的成員時,Hazelcast會將主數據和備份數據一個接一個的遷移到新成員上,最終達成成員之間數據均衡且相互備份。當Hazelcast發生擴展的時候,只有最小數量的分區被移動。下圖呈現了4個成員節點的分區分布情況。

                           

    上面的幾個圖說明了的Hazelcast是如何執行分區的。通常情況下,分區的分布情況是無序的,他們會隨機分布在集群中的各個節點中。最重要的是,Hazelcast會平均分配成員之前的分區,並均勻在的成員之間創建備份。

    在Hazelcast 3.6版本中,新增了一種集群成員:“精簡成員”(lite members),他的特點是不擁有任何分區。“精簡成員”的目標是用於“高密度運算”任務(computationally-heavy task executions。估計是指CPU密集型運算)或者注冊監聽(listener) 。雖然“精簡成員”沒有自己的分區,但是他們同樣可以訪問集群中其他成員的分區。

    總的來說,當集群中的節點發送變動時(進入或退出),都會導致分區在節點中移動並再平衡,以確保數據均勻存儲。但若是“精簡節點”的進入或退出,並不會出現重新划分分區情況,因為精簡節點並不會保存任何分區。

數據分區管理

    創建了分區以后,Hazelcast會將所有的數據存放到每個分區中。它通過哈希運算將數據分布到每個分區中。獲取存儲數據Key值(例如map)或value值(例如topic、list),然后進行以下處理:

  1. 將設定的key或value轉換成byte[];
  2. 對轉換后的byte[]進行哈希計算;
  3. 將哈希計算的結果和分區的數量(271)進行模運算(同余運算、mod運算、%運算)。

    因為byte[]是和271進行同模運算,因此計算結果一定會在0~270之間,根據這個值可以指定到用於存放數據的分區。

分區表

    當創建分區以后,集群中的所有成員必須知道每個分區被存儲到了什么節點。因此集群還需要維護一個分區表來追蹤這些信息。

    當啟動第一個節點時,一個分區表將隨之創建。表中包含分區的ID和標記了他所屬的集群節點。分區表的目標就是讓集群中所有節點(包括“精簡節點”)都能獲取到數據存儲信息,確保每個節點都知道數據在哪。集群中最老的節點(通常情況下是第一個啟動的成員)定期發送分區表給所有的節點。以這種方式,當分區的所有權發生變動時,集群中的所有節點都會被通知到。分區的所有權發生變動有很多種情況,比如,新加入一個節點、或節點離開集群等。如果集群中最早啟動的節點被關閉,那么隨后啟動的節點將會繼承發送分區表的任務,繼續將分區表發送給所有成員。

原文地址:https://my.oschina.net/chkui/blog/729698


免責聲明!

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



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