天天看點

Hazelcast介紹Hazelcast是什麼Hazelcast的特性簡單使用例子Hazelcast運作結構Hazelcast分片概念與原理

摘要: 說明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源碼:https://github.com/hazelcast/hazelcast
  • 關于Hazelcast的問題可以到https://github.com/hazelcast/hazelcast/issues或http://stackoverflow.com。

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(, "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]: 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());
        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 [] {
    Member []:
    Member []: this
}

八月 ,  :: 下午 com.hazelcast.core.LifecycleService
資訊: []: [dev] [] Address[]: is STARTED
Map Value:Hello hazelcast map!
Queue Size :
Queue Value :Hello hazelcast!
Queue Value :Hello hazelcast queue!
Queue Size :
           

    至此,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}
           

    當然直接使用如下配置也是可以的。

Maven(pom.xml):

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

Gradle(build.gradle):

compile com.hazelcast:hazelcast-all:${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());
        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 的配置。

注:我在我的eclipse的console并沒有看到上面的資訊

class: com.hazelcast.instance.DefaultAddressPicker
info: Prefer IPv4 stack is true.
class: com.hazelcast.instance.DefaultAddressPicker
info: Picked Address[]:, using socket ServerSocket[addr=/:::::::,localport=], 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  ( - f88699) starting at Address[]:
class: com.hazelcast.system
info: []: [dev] [] Copyright (c) -, Hazelcast, Inc. All Rights Reserved.
           

    這一段輸出說明了目前執行個體的初始化端口号是5701。Hazelcast 預設使用5701端口。如果發現該端口被占用,會+1檢視5702是否可用,如果還是不能用會繼續向後探查直到5800。Hazelcast 預設使用5700到5800的端口,如果都無法使用會抛出啟動異常。

注:為什麼原作者敢這樣解釋,因為看如下配置檔案可以了解到:

hazelcast-default.xml

...
    <network>
        <port auto-increment="true" port-count="100"></port>
        <outbound-ports>
            <!--
            Allowed port range when connecting to other nodes.
             or * means use system provided port.
            -->
            <ports></ports>
        </outbound-ports>
        ...
    </network>
...
           
class: com.hazelcast.system
info: []: [dev] [] Configured Hazelcast Serialization version : 
class: com.hazelcast.spi.OperationService
info: []: [dev] [] Backpressure is disabled
class: com.hazelcast.spi.impl.operationexecutor.classic.ClassicOperationExecutor
info: []: [dev] [] Starting with  generic operation threads and  partition operation threads
           

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

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

    上面這一段輸出中,Creating MulticastJoiner表示使用多點傳播協定來組建叢集。還建立了6個用于維護非擁塞資訊輸出\輸出。

注:我用的最新版本3.8.1,輸出的資訊與原作者的不同:

INFO: [10.227.6.62]:5701 [dev] [3.8.1] Creating MulticastJoiner
四月 26, 2017 6:37:13 下午 com.hazelcast.spi.impl.operationexecutor.impl.OperationExecutorImpl
INFO: [10.227.6.62]:5701 [dev] [3.8.1] Starting 4 partition threads
四月 26, 2017 6:37:13 下午 com.hazelcast.spi.impl.operationexecutor.impl.OperationExecutorImpl
INFO: [10.227.6.62]:5701 [dev] [3.8.1] Starting 3 generic threads (1 dedicated for priority tasks)
四月 26, 2017 6:37:13 下午 com.hazelcast.core.LifecycleService
INFO: [10.227.6.62]:5701 [dev] [3.8.1] [10.227.6.62]:5701 is STARTING
四月 26, 2017 6:37:15 下午 com.hazelcast.system
           
Members [] {
    Member []:
    Member []: this
}

class: com.hazelcast.core.LifecycleService
info: []: [dev] [] Address[]: is STARTED
class: com.hazelcast.partition.InternalPartitionService
info: []: [dev] [] 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基本配置(http://my.oschina.net/chkui/blog/732408) 了解如何使用Hazelcast吧。

Hazelcast運作結構

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

Hazelcast介紹Hazelcast是什麼Hazelcast的特性簡單使用例子Hazelcast運作結構Hazelcast分片概念與原理

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

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

Hazelcast介紹Hazelcast是什麼Hazelcast的特性簡單使用例子Hazelcast運作結構Hazelcast分片概念與原理

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

Hazelcast分片概念與原理

    Hazelcast通過分片來存儲和管理所有進入叢集的資料,采用分片的方案目标是保證資料可以快速被讀寫、通過備援保證資料不會因節點退出而丢失、節點可線性擴充存儲能力。下面将從理論上說明Hazelcast是如何進行分片管理的。

分片

    Hazelcast的每個資料分片(shards)被稱為一個分區(Partitions)。分區是一些記憶體段,根據系統記憶體容量的不同,每個這樣的記憶體段都包含了幾百到幾千項資料條目,預設情況下,Hazelcast會把資料劃分為271個分區,并且每個分區都有一個備份副本。當啟動一個叢集成員時,這271個分區将會一起被啟動。

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

Hazelcast介紹Hazelcast是什麼Hazelcast的特性簡單使用例子Hazelcast運作結構Hazelcast分片概念與原理

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

Hazelcast介紹Hazelcast是什麼Hazelcast的特性簡單使用例子Hazelcast運作結構Hazelcast分片概念與原理

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

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

Hazelcast介紹Hazelcast是什麼Hazelcast的特性簡單使用例子Hazelcast運作結構Hazelcast分片概念與原理

    上面的幾個圖說明了的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和标記了他所屬的叢集節點。分區表的目标就是讓叢集中所有節點(包括“精簡節點”)都能擷取到資料存儲資訊,確定每個節點都知道資料在哪。叢集中最老的節點(通常情況下是第一個啟動的成員)定期發送分區表給所有的節點。以這種方式,當分區的所有權發生變動時,叢集中的所有節點都會被通知到。分區的所有權發生變動有很多種情況,比如,新加入一個節點、或節點離開叢集等。如果叢集中最早啟動的節點被關閉,那麼随後啟動的節點将會繼承發送分區表的任務,繼續将分區表發送給所有成員。

原文連結:Hazelcast叢集服務(1)——Hazelcast介紹