「分佈式」、「集羣服務」、「網格式內存數據」、「分佈式緩存「、「彈性可伸縮服務」——這些牛逼閃閃的名詞拿到哪都是ITer裝逼的不二之選。在Javaer的世界,有這樣一個開源項目,只須要引入一個jar包、只需簡單的配置和編碼便可實現以上高端技能,他就是 Hazelcast。html
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 都提供了分佈式集羣的實現。java
Hazelcast 有開源版本和商用版本。開源版本遵循 Apache License 2.0 開源協議無償使用。商用版本須要獲取特定的License,二者之間最大的區別在於:商用版本提供了數據高密度存儲。咱們都知道jvm有本身特定的GC機制,不管數據是在堆仍是棧中,只要發現無效引用的數據塊,就有可能被回收。而Hazelcast的分佈式數據都存放在jvm的內存中,頻繁的讀寫數據會致使大量的GC開銷。使用商業版的Hazelcast會擁有高密度存儲的特性,大大下降Jvm的內存開銷,從而下降GC開銷。git
不少開源產品都使用Hazelcast 來組建微服務集羣,例如我們的Vert.x,首選使用Hazelcast來組建分佈式服務。有興趣能夠看個人這篇分享——http://my.oschina.net/chkui/blog/678347 ,文中說明了Vert.x如何使用Hazelcast組建集羣。github
附:數據庫
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的行爲:
以上全部紅色字體的部分均可以經過配置文件來影響。在後續的文章中會詳細介紹相關的 配置說明(待續)。
-----------------------------------亮瞎人的分割線-----------------------------------
若是對Hazelcast的基本原理沒什麼興趣,就不用向下看「運行結構「和「數據分片原理」了,直接去下一篇瞭解如何使用Hazelcast吧。
Hazelcast的官網上列舉了2種運行模式,一種是p2p(點對點)模式、一種是在點對點模式上擴展的C/S模式。下圖是p2p模式的拓補結構。
在p2p模式中,全部的節點(Node)都是集羣中的服務節點,提供相同的功能和計算能力。每一個節點都分擔集羣的整體性能,每增長一個節點均可以線性增長集羣能力。
在p2p服務集羣的基礎上,咱們能夠增長許多客戶端接入到集羣中,這樣就造成了集羣的C/S模式,提供服務集羣視做S端,接入的客戶端視做C端。這些客戶端不會分擔集羣的性能,可是會使用集羣的各類資源。下圖的結構就是客戶端接入集羣的狀況。
能夠爲客戶端提供特別的緩存功能,告知集羣讓那些它常常要使用的數存放在「離它最近」的節點。
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),而後進行如下處理:
由於byte[]是和271進行同模運算,所以計算結果必定會在0~270之間,根據這個值能夠指定到用於存放數據的分區。
當建立分區之後,集羣中的全部成員必須知道每一個分區被存儲到了什麼節點。所以集羣還須要維護一個分區表來追蹤這些信息。
當啓動第一個節點時,一個分區表將隨之建立。表中包含分區的ID和標記了他所屬的集羣節點。分區表的目標就是讓集羣中全部節點(包括「精簡節點」)都能獲取到數據存儲信息,確保每一個節點都知道數據在哪。集羣中最老的節點(一般狀況下是第一個啓動的成員)按期發送分區表給全部的節點。以這種方式,當分區的全部權發生變更時,集羣中的全部節點都會被通知到。分區的全部權發生變更有不少種狀況,好比,新加入一個節點、或節點離開集羣等。若是集羣中最先啓動的節點被關閉,那麼隨後啓動的節點將會繼承發送分區表的任務,繼續將分區表發送給全部成員。