dubbo學習小結

dubbo學習小結html

 

參考:java

https://blog.csdn.net/paul_wei2008/article/details/19355681node

https://blog.csdn.net/liweisnake/article/details/63251252linux

https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/git

https://www.jianshu.com/p/b9f3f6a16911web

 

 

Dubbo基本原理機制redis

分佈式服務框架:
–高性能和透明化的RPC遠程服務調用方案
–SOA服務治理方案
-Apache MINA 框架基於Reactor模型通訊框架,基於tcp長鏈接
 
Dubbo缺省協議採用單一長鏈接和NIO異步通信,
適合於小數據量大併發的服務調用,以及服務消費者機器數遠大於服務提供者機器數的狀況
分析源代碼,基本原理以下:
 
  1. client一個線程調用遠程接口,生成一個惟一的ID(好比一段隨機字符串,UUID等),Dubbo是使用AtomicLong從0開始累計數字的
  2. 將打包的方法調用信息(如調用的接口名稱,方法名稱,參數值列表等),和處理結果的回調對象callback,所有封裝在一塊兒,組成一個對象object
  3. 向專門存放調用信息的全局ConcurrentHashMap裏面put(ID, object)
  4. 將ID和打包的方法調用信息封裝成一對象connRequest,使用IoSession.write(connRequest)異步發送出去
  5. 當前線程再使用callback的get()方法試圖獲取遠程返回的結果,在get()內部,則使用synchronized獲取回調對象callback的鎖, 再先檢測是否已經獲取到結果,若是沒有,而後調用callback的wait()方法,釋放callback上的鎖,讓當前線程處於等待狀態。
  6. 服務端接收到請求並處理後,將結果(此結果中包含了前面的ID,即回傳)發送給客戶端,客戶端socket鏈接上專門監聽消息的線程收到消息,分析結果,取到ID,再從前面的ConcurrentHashMap裏面get(ID),從而找到callback,將方法調用結果設置到callback對象裏。
  7. 監聽線程接着使用synchronized獲取回調對象callback的鎖(由於前面調用過wait(),那個線程已釋放callback的鎖了),再notifyAll(),喚醒前面處於等待狀態的線程繼續執行(callback的get()方法繼續執行就能拿到調用結果了),至此,整個過程結束。
  • 當前線程怎麼讓它「暫停」,等結果回來後,再向後執行?
     答:先生成一個對象obj,在一個全局map裏put(ID,obj)存放起來,再用synchronized獲取obj鎖,再調用obj.wait()讓當前線程處於等待狀態,而後另外一消息監聽線程等到服 務端結果來了後,再map.get(ID)找到obj,再用synchronized獲取obj鎖,再調用obj.notifyAll()喚醒前面處於等待狀態的線程。
  • 正如前面所說,Socket通訊是一個全雙工的方式,若是有多個線程同時進行遠程方法調用,這時創建在client server之間的socket鏈接上會有不少雙方發送的消息傳遞,先後順序也多是亂七八糟的,server處理完結果後,將結果消息發送給client,client收到不少消息,怎麼知道哪一個消息結果是原先哪一個線程調用的?
     答:使用一個ID,讓其惟一,而後傳遞給服務端,再服務端又回傳回來,這樣就知道結果是原先哪一個線程的了。

 

 

 

 

zookeeper入門shell

zookeeper可謂是目前使用最普遍的分佈式組件了。其功能和職責單一,但卻很是重要。數據庫

在現今這個年代,介紹zookeeper的書和文章可謂多如牛毛,本人不才,試圖經過本身的理解來介紹zookeeper,但願經過一個初學者的視角來學習zookeeper,以期讓人更加深刻和平穩的理解zookeeper。其中參考了很多教程和書,相關書目列在文末,也感謝這些做者。apache

學習新的框架,先讓咱們搞清楚他是什麼,這是它的內涵,而後再介紹它能作什麼,這是它的外延,內涵和外延共同來定義框架自己,會對框架有較爲深入的理解,在應用層面上知道如何用。其次再搞清楚zookeeper相關的理論基礎,其目的是知道zookeeper是如何被髮明的,是否可以借鑑以便從此本身可以用到其餘地方。最後搞清楚zookeeper中一些設計的原理和細節,目的也是搞清前因後果,學會「術」從而應用到別的地方。固然了,加深的理解一樣可以幫助認識zookeeper自己,在使用時才知道爲何這樣用。

首先,

zookeeper究竟是什麼?

zookeeper其實是yahoo開發的,用於分佈式中一致性處理的框架。最初其做爲研發hadoop時的副產品。因爲分佈式系統中一致性處理較爲困難,其餘的分佈式系統沒有必要 費勁重複造輪子,故隨後的分佈式系統中大量應用了zookeeper,以致於zookeeper成爲了各類分佈式系統的基礎組件,其地位之重要,可想而知。著名的hadoop,kafka,dubbo 都是基於zookeeper而構建。

要想理解zookeeper究竟是作啥的,那首先得理解清楚,什麼是一致性。

所謂的一致性,實際上就是圍繞着「看見」來的。誰能看見?可否看見?何時看見?舉個例子:淘寶後臺賣家,在後臺上架一件大促的商品,經過服務器A提交到主數據庫,假設剛提交後立馬就有用戶去經過應用服務器B去從數據庫查詢該商品,就會出現一個現象,賣家已經更新成功了,然而買家卻看不到;而通過一段時間後,主數據庫的數據同步到了從數據庫,買家就能查到了。

假設賣家更新成功以後買家立馬就能看到賣家的更新,則稱爲強一致性;

若是賣家更新成功後買家不能看到賣家更新的內容,則稱爲弱一致性;

而賣家更新成功後,買家通過一段時間最終能看到賣家的更新,則稱爲最終一致性。

更多的一致性例子能夠參考文獻2,裏面列舉了10種一致性的例子,若是要給一致性下個定義,能夠是分佈式系統中狀態或數據保持同步和一致。特別須要注意一致性跟事務的區別,能夠記得學習數據庫時特別強調ACID,故而知足ACID的數據庫可以作事務,其中C便是一致性,所以,事務是一致性的一種特例,比起一致性更難達成。

如何保證在分佈式環境下數據的最終一致,這個就是zookeeper須要解決的問題。對於這些問題,有哪些挑戰,zookeeper又是如何解決這些挑戰的,下一篇文章將會主要涉及這個主題。

一些常見的解決一致性問題的方式:

1. 查詢重試補償。對於分佈式應用中不肯定的狀況,先使用查詢接口查詢到當前狀態,若是當前狀態不一致則採用補償接口對狀態進行重試推動,或者回滾接口對業務作回滾。典型的場景如銀行跟支付寶之間的交互。支付寶發送一個轉帳請求到銀行,如一直未收到響應,則能夠經過銀行的查詢接口查詢該筆交易的狀態,如該筆交易對方未收到,則採起補償的模式進行推送。

2. 定時任務推送。對於上面的狀況,有可能一次推送搞不定,因而須要2次,3次推送。不要懷疑,支付寶內最初掉單率很高,全靠後續不斷的定時任務推送增長成功率。

3. TCC。try-confirm-cancel。其實是兩階段協議,第二階段的能夠實現提交操做或是逆操做。

 

zookeeper到底能作什麼?

在業界的實際應用是什麼?瞭解這些應用,會對zookeeper可以作的事有更直觀的認識。

hadoop:

鼻祖級應用,ResourceManager在整個hadoop中算是單點,爲了實現其高可用,分爲主備ResourceManager,zookeeper在其中管理整個ResourceManager。

能夠想象,主備ResourceManager最初是主RM提供服務,若是一切安好,則zookeeper無用武之地。然而,總歸會出現主RM提供不了服務的狀況。因而會出現主備切換的狀況,而zookeeper正是爲主備切換保駕護航。

先來推理一下,主備切換會出現什麼問題。傳統的主備切換,可讓主備之間維持心跳鏈接,一旦備機發現主機心跳檢測不到了,則本身切換爲主機,原來的主機等待救援。這種方式有兩個問題,一是因爲網絡抖動,負載過大等問題,備機檢測不到心跳並不能說明主機必定掛了,有可能必定時間後主機或網絡恢復,這時候主機並不知道備機已經切換爲主機,2臺主機互相爭用,可能形成腦裂;二是若是一些數據集中在主機上面,則備機切換時因爲同步延時勢必會損失掉一部分的數據。

如何解決這些問題?早期的方式提供了很多解決方案,好比備機一旦切換爲主機,則經過電源控制直接切斷主機電源,簡單粗暴,可是此刻備機已是單點,若是主機是由於量撐不住而掛,那備機有可能會重蹈覆轍,最終致使整個服務不可用。

zookeeper又是如何解決這個問題的呢?

1. zookeeper做爲第三方集羣參與到主備節點中去,當主備啓動時會在zookeeper上競爭建立一個臨時鎖節點,爭用成功者則充當主機,其他備機

2. 全部備機會監聽該臨時鎖節點,一旦主機與zookeeper間session失效,則臨時節點被刪除

3. 一旦臨時節點被刪除,備機開始從新申請建立臨時鎖節點,從新爭用爲主機;

4. 用zookeeper如何解決腦裂?實際上主機爭用到節點後經過對根節點作一個ACL權限控制,則其餘搶佔的機器因爲沒法更新臨時鎖節點,只有放棄成爲備機。

zookeeper使用了很是簡單又現成的方式來解決的這個問題,比起其餘方案方便很多,這也是爲啥zookeeper流行的緣由。說白了,就是把複雜操做封裝化精簡化

 

dubbo:

做爲業界知名的分佈式soa框架,dubbo的主要的服務註冊發現功能即是由zookeeper來提供的。

對於一個服務框架,註冊中心是其核心中的核心,雖然暫時掛掉並不會致使整個服務出問題,可是一旦掛掉,總體風險就很高。考慮通常狀況,註冊中心就是單臺機器的時候,其實現很容易,全部機器起來都去註冊服務給它,而且全部調用方都跟它保持長鏈接,一旦服務有變,即經過長鏈接來通知到調用方。可是當服務集羣規模擴大時,這事情就不簡單了,單機保持鏈接數有限,並且容易故障。

做爲一個穩定的服務化框架,dubbo能夠選擇並推薦zookeeper做爲註冊中心。其底層將zookeeper經常使用的客戶端zkclient和curator封裝成爲ZookeeperClient。

1. 當服務提供者服務啓動時,向zookeeper註冊一個節點

2. 服務消費者則訂閱其父節點的變化,諸如啓動中止都可以經過節點建立刪除得知,異常狀況好比被調用方掉線也能夠經過臨時節點session 斷開自動刪除得知

3. 服務消費方同時也會將本身訂閱的服務以節點建立的方式放到zookeeper

4. 因而能夠獲得映射關係,諸如誰提供了服務,誰訂閱了誰提供的服務,基於這層關係再作監控,就能輕易得知整個系統狀況。

 

zookeeper的基本數據模型

一句話,相似linux文件系統的節點模型

其節點有以下有趣而又重要的特性:

1. 同一時刻多臺機器建立同一個節點,只有一個會爭搶成功。利用這個特性能夠作分佈式鎖。

2. 臨時節點的生命週期與會話一致,會話關閉則臨時節點刪除。這個特性常常用來作心跳,動態監控,負載等動做

3. 順序節點保證節點名全局惟一。這個特性能夠用來生成分佈式環境下的全局自增加id

 

經過zookeeper提供的原語服務,能夠對zookeeper能作的事情有個精確和直觀的認識

zookeeper提供的原語服務

1. 建立節點。

2. 刪除節點

3. 更新節點

4. 獲取節點信息

5. 權限控制

6. 事件監聽

實際上,就是對節點的增刪查改加上權限控制與事件監聽,可是經過對這些原語的組合以及不一樣場景的使用,能夠實現不少用法。參考文獻5

1. 數據發佈訂閱。即註冊中心,見上面dubbo用法。主要經過對節點管理作到發佈以及事件監聽作到訂閱

2. 負載均衡。見上面kafka用法

3. 命名服務。zookeeper的節點結構自然支持命名服務,即把信息集中存儲,並以樹狀管理,方便統一查閱

4. 分佈式協調通知。協調通知實際上與發佈訂閱相似,因爲引入的第三方的zookeeper,實際上對不少種協調通知作了解耦,好比參考文獻4中提到的消息推送,心跳檢測等

5. 集羣管理與master選舉。經過上面的第二點特性,能夠輕易得知集羣機器存活情況,從而輕鬆管理集羣;經過上面第一點特性,能夠作出master爭搶。

6. 分佈式鎖。實際上就是第一點特性的應用。

7. 分佈式隊列。實際上就是第三點特性的應用。

8. 分佈式的併發等待。相似於多線程的join問題,主任務的執行依賴於其餘子任務所有執行完畢,在單機多線程裏能夠用join,可是分佈式環境下如何實現呢。利用zookeeper,能夠建立一個主任務節點,旗下子任務一旦執行完畢,則在主任務節點下掛一個子任務節點,等節點數量足夠,則認爲主任務能夠開始執行。

能夠發現,全部的原語就是zookeeper的基礎,而其餘的用法總結無非是將原語放到不一樣場景下的歸類罷了。

 

相信到這裏你對zookeeper應該有個初步的瞭解和大體的印象了。

本系列文章分爲:

zookeeper入門系列-概述

zookeeper入門系列-理論基礎-分佈式事務

zookeeper入門系列-理論基礎-paxos協議

zookeeper入門系列-理論基礎-zab協議

zookeeper入門系列-理論基礎-raft協議

zookeeper入門系列-設計細節

 

參考文獻:

保證分佈式系統數據一致性的6種方案 http://weibo.com/ttarticle/p/show?id=2309403965965003062676

解決分佈式系統的一致性問題,咱們須要瞭解哪些理論? http://mp.weixin.qq.com/s/hGnpHfn7a7yxjPBP78i4bg

分佈式系統的事務處理  http://coolshell.cn/articles/10910.html

ZooKeeper典型應用場景一覽 http://jm.taobao.org/2011/10/08/1232/

zookeeper中的基本概念 http://www.hollischuang.com/archives/1280

zookeeper入門使用 http://www.importnew.com/23025.html

 

 

安裝和配置詳解

本文介紹的 Zookeeper 是以 3.2.2 這個穩定版本爲基礎,最新的版本能夠經過官網 http://hadoop.apache.org/zookeeper/來獲取,Zookeeper 的安裝很是簡單,下面將從單機模式和集羣模式兩個方面介紹 Zookeeper 的安裝和配置。

單機模式

單機安裝很是簡單,只要獲取到 Zookeeper 的壓縮包並解壓到某個目錄如:/home/zookeeper-3.2.2 下,Zookeeper 的啓動腳本在 bin 目錄下,Linux 下的啓動腳本是 zkServer.sh,在 3.2.2 這個版本 Zookeeper 沒有提供 windows 下的啓動腳本,因此要想在 windows 下啓動 Zookeeper 要本身手工寫一個,如清單 1 所示:

清單 1. Windows 下 Zookeeper 啓動腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
setlocal
set ZOOCFGDIR=%~dp0%..\conf
set ZOO_LOG_DIR=%~dp0%..
set ZOO_LOG4J_PROP=INFO,CONSOLE
set CLASSPATH=%ZOOCFGDIR%
 
set CLASSPATH=%~dp0..\*;%~dp0..\lib\*;%CLASSPATH%
set CLASSPATH=%~dp0..\build\classes;%~dp0..\build\lib\*;%CLASSPATH%
set ZOOCFG=%ZOOCFGDIR%\zoo.cfg
set ZOOMAIN=org.apache.zookeeper.server.ZooKeeperServerMain
java "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%"
-cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %*
endlocal

在你執行啓動腳本以前,還有幾個基本的配置項須要配置一下,Zookeeper 的配置文件在 conf 目錄下,這個目錄下有 zoo_sample.cfg 和 log4j.properties,你須要作的就是將 zoo_sample.cfg 更名爲 zoo.cfg,由於 Zookeeper 在啓動時會找這個文件做爲默認配置文件。下面詳細介紹一下,這個配置文件中各個配置項的意義。

1
2
3
tickTime=2000
dataDir=D:/devtools/zookeeper-3.2.2/build
clientPort=2181
  • tickTime:這個時間是做爲 Zookeeper 服務器之間或客戶端與服務器之間維持心跳的時間間隔,也就是每一個 tickTime 時間就會發送一個心跳。
  • dataDir:顧名思義就是 Zookeeper 保存數據的目錄,默認狀況下,Zookeeper 將寫數據的日誌文件也保存在這個目錄裏。
  • clientPort:這個端口就是客戶端鏈接 Zookeeper 服務器的端口,Zookeeper 會監聽這個端口,接受客戶端的訪問請求。

當這些配置項配置好後,你如今就能夠啓動 Zookeeper 了,啓動後要檢查 Zookeeper 是否已經在服務,能夠經過 netstat – ano 命令查看是否有你配置的 clientPort 端口號在監聽服務。

集羣模式

Zookeeper 不只能夠單機提供服務,同時也支持多機組成集羣來提供服務。實際上 Zookeeper 還支持另一種僞集羣的方式,也就是能夠在一臺物理機上運行多個 Zookeeper 實例,下面將介紹集羣模式的安裝和配置。

Zookeeper 的集羣模式的安裝和配置也不是很複雜,所要作的就是增長几個配置項。集羣模式除了上面的三個配置項還要增長下面幾個配置項:

1
2
3
4
initLimit=5
syncLimit=2
server.1=192.168.211.1:2888:3888
server.2=192.168.211.2:2888:3888
  • initLimit:這個配置項是用來配置 Zookeeper 接受客戶端(這裏所說的客戶端不是用戶鏈接 Zookeeper 服務器的客戶端,而是 Zookeeper 服務器集羣中鏈接到 Leader 的 Follower 服務器)初始化鏈接時最長能忍受多少個心跳時間間隔數。當已經超過 10 個心跳的時間(也就是 tickTime)長度後 Zookeeper 服務器尚未收到客戶端的返回信息,那麼代表這個客戶端鏈接失敗。總的時間長度就是 5*2000=10 秒
  • syncLimit:這個配置項標識 Leader 與 Follower 之間發送消息,請求和應答時間長度,最長不能超過多少個 tickTime 的時間長度,總的時間長度就是 2*2000=4 秒
  • server.A=B:C:D:其中 A 是一個數字,表示這個是第幾號服務器;B 是這個服務器的 ip 地址;C 表示的是這個服務器與集羣中的 Leader 服務器交換信息的端口;D 表示的是萬一集羣中的 Leader 服務器掛了,須要一個端口來從新進行選舉,選出一個新的 Leader,而這個端口就是用來執行選舉時服務器相互通訊的端口。若是是僞集羣的配置方式,因爲 B 都是同樣,因此不一樣的 Zookeeper 實例通訊端口號不能同樣,因此要給它們分配不一樣的端口號。

除了修改 zoo.cfg 配置文件,集羣模式下還要配置一個文件 myid,這個文件在 dataDir 目錄下,這個文件裏面就有一個數據就是 A 的值,Zookeeper 啓動時會讀取這個文件,拿到裏面的數據與 zoo.cfg 裏面的配置信息比較從而判斷究竟是那個 server。

數據模型

Zookeeper 會維護一個具備層次關係的數據結構,它很是相似於一個標準的文件系統,如圖 1 所示:

圖 1 Zookeeper 數據結構

圖 1 Zookeeper 數據結構

Zookeeper 這種數據結構有以下這些特色:

  1. 每一個子目錄項如 NameService 都被稱做爲 znode,這個 znode 是被它所在的路徑惟一標識,如 Server1 這個 znode 的標識爲 /NameService/Server1
  2. znode 能夠有子節點目錄,而且每一個 znode 能夠存儲數據,注意 EPHEMERAL 類型的目錄節點不能有子節點目錄
  3. znode 是有版本的,每一個 znode 中存儲的數據能夠有多個版本,也就是一個訪問路徑中能夠存儲多份數據
  4. znode 能夠是臨時節點,一旦建立這個 znode 的客戶端與服務器失去聯繫,這個 znode 也將自動刪除,Zookeeper 的客戶端和服務器通訊採用長鏈接方式,每一個客戶端和服務器經過心跳來保持鏈接,這個鏈接狀態稱爲 session,若是 znode 是臨時節點,這個 session 失效,znode 也就刪除了
  5. znode 的目錄名能夠自動編號,如 App1 已經存在,再建立的話,將會自動命名爲 App2
  6. znode 能夠被監控,包括這個目錄節點中存儲的數據的修改,子節點目錄的變化等,一旦變化能夠通知設置監控的客戶端,這個是 Zookeeper 的核心特性,Zookeeper 的不少功能都是基於這個特性實現的,後面在典型的應用場景中會有實例介紹

如何使用

Zookeeper 做爲一個分佈式的服務框架,主要用來解決分佈式集羣中應用系統的一致性問題,它能提供基於相似於文件系統的目錄節點樹方式的數據存儲,可是 Zookeeper 並非用來專門存儲數據的,它的做用主要是用來維護和監控你存儲的數據的狀態變化。經過監控這些數據狀態的變化,從而能夠達到基於數據的集羣管理,後面將會詳細介紹 Zookeeper 可以解決的一些典型問題,這裏先介紹一下,Zookeeper 的操做接口和簡單使用示例。

經常使用接口列表

客戶端要鏈接 Zookeeper 服務器能夠經過建立 org.apache.zookeeper. ZooKeeper 的一個實例對象,而後調用這個類提供的接口來和服務器交互。

前面說了 ZooKeeper 主要是用來維護和監控一個目錄節點樹中存儲的數據的狀態,全部咱們可以操做 ZooKeeper 的也和操做目錄節點樹大致同樣,如建立一個目錄節點,給某個目錄節點設置數據,獲取某個目錄節點的全部子目錄節點,給某個目錄節點設置權限和監控這個目錄節點的狀態變化。

這些接口以下表所示:

表 1 org.apache.zookeeper. ZooKeeper 方法列表

除了以上這些上表中列出的方法以外還有一些重載方法,如都提供了一個回調類的重載方法以及能夠設置特定 Watcher 的重載方法,具體的方法能夠參考 org.apache.zookeeper. ZooKeeper 類的 API 說明。

基本操做

下面給出基本的操做 ZooKeeper 的示例代碼,這樣你就能對 ZooKeeper 有直觀的認識了。下面的清單包括了建立與 ZooKeeper 服務器的鏈接以及最基本的數據操做:

清單 2. ZooKeeper 基本的操做示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 建立一個與服務器的鏈接
ZooKeeper zk = new ZooKeeper("localhost:" + CLIENT_PORT,
        ClientBase.CONNECTION_TIMEOUT, new Watcher() {
            // 監控全部被觸發的事件
            public void process(WatchedEvent event) {
                System.out.println("已經觸發了" + event.getType() + "事件!");
            }
        });
// 建立一個目錄節點
zk.create("/testRootPath", "testRootData".getBytes(), Ids.OPEN_ACL_UNSAFE,
   CreateMode.PERSISTENT);
// 建立一個子目錄節點
zk.create("/testRootPath/testChildPathOne", "testChildDataOne".getBytes(),
   Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
System.out.println(new String(zk.getData("/testRootPath",false,null)));
// 取出子目錄節點列表
System.out.println(zk.getChildren("/testRootPath",true));
// 修改子目錄節點數據
zk.setData("/testRootPath/testChildPathOne","modifyChildDataOne".getBytes(),-1);
System.out.println("目錄節點狀態:["+zk.exists("/testRootPath",true)+"]");
// 建立另一個子目錄節點
zk.create("/testRootPath/testChildPathTwo", "testChildDataTwo".getBytes(),
   Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
System.out.println(new String(zk.getData("/testRootPath/testChildPathTwo",true,null)));
// 刪除子目錄節點
zk.delete("/testRootPath/testChildPathTwo",-1);
zk.delete("/testRootPath/testChildPathOne",-1);
// 刪除父目錄節點
zk.delete("/testRootPath",-1);
// 關閉鏈接
zk.close();

輸出的結果以下:

1
2
3
4
5
6
7
8
已經觸發了 None 事件!
  testRootData
  [testChildPathOne]
目錄節點狀態:[5,5,1281804532336,1281804532336,0,1,0,0,12,1,6]
已經觸發了 NodeChildrenChanged 事件!
  testChildDataTwo
已經觸發了 NodeDeleted 事件!
已經觸發了 NodeDeleted 事件!

當對目錄節點監控狀態打開時,一旦目錄節點的狀態發生變化,Watcher 對象的 process 方法就會被調用。

ZooKeeper 典型的應用場景

Zookeeper 從設計模式角度來看,是一個基於觀察者模式設計的分佈式服務管理框架,它負責存儲和管理你們都關心的數據,而後接受觀察者的註冊,一旦這些數據的狀態發生變化,Zookeeper 就將負責通知已經在 Zookeeper 上註冊的那些觀察者作出相應的反應,從而實現集羣中相似 Master/Slave 管理模式,關於 Zookeeper 的詳細架構等內部細節能夠閱讀 Zookeeper 的源碼

下面詳細介紹這些典型的應用場景,也就是 Zookeeper 到底能幫咱們解決那些問題?下面將給出答案。

統一命名服務(Name Service)

分佈式應用中,一般須要有一套完整的命名規則,既可以產生惟一的名稱又便於人識別和記住,一般狀況下用樹形的名稱結構是一個理想的選擇,樹形的名稱結構是一個有層次的目錄結構,既對人友好又不會重複。說到這裏你可能想到了 JNDI,沒錯 Zookeeper 的 Name Service 與 JNDI 可以完成的功能是差很少的,它們都是將有層次的目錄結構關聯到必定資源上,可是 Zookeeper 的 Name Service 更加是普遍意義上的關聯,也許你並不須要將名稱關聯到特定資源上,你可能只須要一個不會重複名稱,就像數據庫中產生一個惟一的數字主鍵同樣。

Name Service 已是 Zookeeper 內置的功能,你只要調用 Zookeeper 的 API 就能實現。如調用 create 接口就能夠很容易建立一個目錄節點。

配置管理(Configuration Management)

配置的管理在分佈式應用環境中很常見,例如同一個應用系統須要多臺 PC Server 運行,可是它們運行的應用系統的某些配置項是相同的,若是要修改這些相同的配置項,那麼就必須同時修改每臺運行這個應用系統的 PC Server,這樣很是麻煩並且容易出錯。

像這樣的配置信息徹底能夠交給 Zookeeper 來管理,將配置信息保存在 Zookeeper 的某個目錄節點中,而後將全部須要修改的應用機器監控配置信息的狀態,一旦配置信息發生變化,每臺應用機器就會收到 Zookeeper 的通知,而後從 Zookeeper 獲取新的配置信息應用到系統中。

圖 2. 配置管理結構圖

圖 2. 配置管理結構圖

集羣管理(Group Membership)

Zookeeper 可以很容易的實現集羣管理的功能,若有多臺 Server 組成一個服務集羣,那麼必需要一個「總管」知道當前集羣中每臺機器的服務狀態,一旦有機器不能提供服務,集羣中其它集羣必須知道,從而作出調整從新分配服務策略。一樣當增長集羣的服務能力時,就會增長一臺或多臺 Server,一樣也必須讓「總管」知道。

Zookeeper 不只可以幫你維護當前的集羣中機器的服務狀態,並且可以幫你選出一個「總管」,讓這個總管來管理集羣,這就是 Zookeeper 的另外一個功能 Leader Election。

它們的實現方式都是在 Zookeeper 上建立一個 EPHEMERAL 類型的目錄節點,而後每一個 Server 在它們建立目錄節點的父目錄節點上調用 getChildren(String path, boolean watch) 方法並設置 watch 爲 true,因爲是 EPHEMERAL 目錄節點,當建立它的 Server 死去,這個目錄節點也隨之被刪除,因此 Children 將會變化,這時 getChildren上的 Watch 將會被調用,因此其它 Server 就知道已經有某臺 Server 死去了。新增 Server 也是一樣的原理。

Zookeeper 如何實現 Leader Election,也就是選出一個 Master Server。和前面的同樣每臺 Server 建立一個 EPHEMERAL 目錄節點,不一樣的是它仍是一個 SEQUENTIAL 目錄節點,因此它是個 EPHEMERAL_SEQUENTIAL 目錄節點。之因此它是 EPHEMERAL_SEQUENTIAL 目錄節點,是由於咱們能夠給每臺 Server 編號,咱們能夠選擇當前是最小編號的 Server 爲 Master,假如這個最小編號的 Server 死去,因爲是 EPHEMERAL 節點,死去的 Server 對應的節點也被刪除,因此當前的節點列表中又出現一個最小編號的節點,咱們就選擇這個節點爲當前 Master。這樣就實現了動態選擇 Master,避免了傳統意義上單 Master 容易出現單點故障的問題。

圖 3. 集羣管理結構圖

圖 3. 集羣管理結構圖

這部分的示例代碼以下,完整的代碼請看附件:

清單 3. Leader Election 關鍵代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void findLeader() throws InterruptedException {
        byte[] leader = null;
        try {
            leader = zk.getData(root + "/leader", true, null);
        } catch (Exception e) {
            logger.error(e);
        }
        if (leader != null) {
            following();
        } else {
            String newLeader = null;
            try {
                byte[] localhost = InetAddress.getLocalHost().getAddress();
                newLeader = zk.create(root + "/leader", localhost,
                ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
            } catch (Exception e) {
                logger.error(e);
            }
            if (newLeader != null) {
                leading();
            } else {
                mutex.wait();
            }
        }
    }

共享鎖(Locks)

共享鎖在同一個進程中很容易實現,可是在跨進程或者在不一樣 Server 之間就很差實現了。Zookeeper 卻很容易實現這個功能,實現方式也是須要得到鎖的 Server 建立一個 EPHEMERAL_SEQUENTIAL 目錄節點,而後調用 getChildren方法獲取當前的目錄節點列表中最小的目錄節點是否是就是本身建立的目錄節點,若是正是本身建立的,那麼它就得到了這個鎖,若是不是那麼它就調用 exists(String path, boolean watch) 方法並監控 Zookeeper 上目錄節點列表的變化,一直到本身建立的節點是列表中最小編號的目錄節點,從而得到鎖,釋放鎖很簡單,只要刪除前面它本身所建立的目錄節點就好了。

圖 4. Zookeeper 實現 Locks 的流程圖

圖 4. Zookeeper 實現 Locks 的流程圖

同步鎖的實現代碼以下,完整的代碼請看附件:

清單 4. 同步鎖的關鍵代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void getLock() throws KeeperException, InterruptedException{
        List< String > list = zk.getChildren(root, false);
        String[] nodes = list.toArray(new String[list.size()]);
        Arrays.sort(nodes);
        if(myZnode.equals(root+"/"+nodes[0])){
            doAction();
        }
        else{
            waitForLock(nodes[0]);
        }
    }
    void waitForLock(String lower) throws InterruptedException, KeeperException {
        Stat stat = zk.exists(root + "/" + lower,true);
        if(stat != null){
            mutex.wait();
        }
        else{
            getLock();
        }
    }

隊列管理

Zookeeper 能夠處理兩種類型的隊列:

  1. 當一個隊列的成員都聚齊時,這個隊列纔可用,不然一直等待全部成員到達,這種是同步隊列。
  2. 隊列按照 FIFO 方式進行入隊和出隊操做,例如實現生產者和消費者模型。

同步隊列用 Zookeeper 實現的實現思路以下:

建立一個父目錄 /synchronizing,每一個成員都監控標誌(Set Watch)位目錄 /synchronizing/start 是否存在,而後每一個成員都加入這個隊列,加入隊列的方式就是建立 /synchronizing/member_i 的臨時目錄節點,而後每一個成員獲取 / synchronizing 目錄的全部目錄節點,也就是 member_i。判斷 i 的值是否已是成員的個數,若是小於成員個數等待 /synchronizing/start 的出現,若是已經相等就建立 /synchronizing/start。

用下面的流程圖更容易理解:

圖 5. 同步隊列流程圖

圖 5. 同步隊列流程圖

同步隊列的關鍵代碼以下,完整的代碼請看附件:

清單 5. 同步隊列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void addQueue() throws KeeperException, InterruptedException{
        zk.exists(root + "/start",true);
        zk.create(root + "/" + name, new byte[0], Ids.OPEN_ACL_UNSAFE,
        CreateMode.EPHEMERAL_SEQUENTIAL);
        synchronized (mutex) {
            List< String > list = zk.getChildren(root, false);
            if (list.size() < size) {
                mutex.wait();
            } else {
                zk.create(root + "/start", new byte[0], Ids.OPEN_ACL_UNSAFE,
                 CreateMode.PERSISTENT);
            }
        }
}

當隊列沒盡是進入 wait(),而後會一直等待 Watch 的通知,Watch 的代碼以下:

1
2
3
4
5
6
7
8
public void process(WatchedEvent event) {
        if(event.getPath().equals(root + "/start") &&
         event.getType() == Event.EventType.NodeCreated){
            System.out.println("獲得通知");
            super.process(event);
            doAction();
        }
    }

FIFO 隊列用 Zookeeper 實現思路以下:

實現的思路也很是簡單,就是在特定的目錄下建立 SEQUENTIAL 類型的子目錄 /queue_i,這樣就能保證全部成員加入隊列時都是有編號的,出隊列時經過 getChildren( ) 方法能夠返回當前全部的隊列中的元素,而後消費其中最小的一個,這樣就能保證 FIFO。

下面是生產者和消費者這種隊列形式的示例代碼,完整的代碼請看附件:

清單 6. 生產者代碼
1
2
3
4
5
6
7
8
9
boolean produce(int i) throws KeeperException, InterruptedException{
        ByteBuffer b = ByteBuffer.allocate(4);
        byte[] value;
        b.putInt(i);
        value = b.array();
        zk.create(root + "/element", value, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.PERSISTENT_SEQUENTIAL);
        return true;
    }
清單 7. 消費者代碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int consume() throws KeeperException, InterruptedException{
        int retvalue = -1;
        Stat stat = null;
        while (true) {
            synchronized (mutex) {
                List< String > list = zk.getChildren(root, true);
                if (list.size() == 0) {
                    mutex.wait();
                } else {
                    Integer min = new Integer(list.get(0).substring(7));
                    for(String s : list){
                        Integer tempValue = new Integer(s.substring(7));
                        if(tempValue < min) min = tempValue;
                    }
                    byte[] b = zk.getData(root + "/element" + min,false, stat);
                    zk.delete(root + "/element" + min, 0);
                    ByteBuffer buffer = ByteBuffer.wrap(b);
                    retvalue = buffer.getInt();
                    return retvalue;
                }
            }
        }
}

總結

Zookeeper 做爲 Hadoop 項目中的一個子項目,是 Hadoop 集羣管理的一個必不可少的模塊,它主要用來控制集羣中的數據,如它管理 Hadoop 集羣中的 NameNode,還有 Hbase 中 Master Election、Server 之間狀態同步等。

本文介紹的 Zookeeper 的基本知識,以及介紹了幾個典型的應用場景。這些都是 Zookeeper 的基本功能,最重要的是 Zoopkeeper 提供了一套很好的分佈式集羣管理的機制,就是它這種基於層次型的目錄樹的數據結構,並對樹中的節點進行有效管理,從而能夠設計出多種多樣的分佈式的數據管理模型,而不只僅侷限於上面提到的幾個經常使用應用場景。

 

 

 

 

認識Netty

什麼是Netty?

Netty 是一個利用 Java 的高級網絡的能力,隱藏其背後的複雜性而提供一個易於使用的 API 的客戶端/服務器框架。
Netty 是一個普遍使用的 Java 網絡編程框架(Netty 在 2011 年得到了Duke's Choice Award,見https://www.java.net/dukeschoice/2011)。它活躍和成長於用戶社區,像大型公司 Facebook 和 Instagram 以及流行 開源項目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其強大的對於網絡抽象的核心代碼。

以上是摘自《Essential Netty In Action》這本書,本文的內容也是本人讀了這本書以後的一些整理心得,若有不當之處歡迎大蝦們指正

Netty和Tomcat有什麼區別?

Netty和Tomcat最大的區別就在於通訊協議,Tomcat是基於Http協議的,他的實質是一個基於http協議的web容器,可是Netty不同,他能經過編程自定義各類協議,由於netty可以經過codec本身來編碼/解碼字節流,完成相似redis訪問的功能,這就是netty和tomcat最大的不一樣。

有人說netty的性能就必定比tomcat性能高,其實否則,tomcat從6.x開始就支持了nio模式,而且後續還有arp模式——一種經過jni調用apache網絡庫的模式,相比於舊的bio模式,併發性能獲得了很大提升,特別是arp模式,而netty是否比tomcat性能更高,則要取決於netty程序做者的技術實力了。

爲何Netty受歡迎?

如第一部分所述,netty是一款收到大公司青睞的框架,在我看來,netty可以受到青睞的緣由有三:

  1. 併發高
  2. 傳輸快
  3. 封裝好

Netty爲何併發高

Netty是一款基於NIO(Nonblocking I/O,非阻塞IO)開發的網絡通訊框架,對比於BIO(Blocking I/O,阻塞IO),他的併發性能獲得了很大提升,兩張圖讓你瞭解BIO和NIO的區別:

 
阻塞IO的通訊方式

 
非阻塞IO的通訊方式

從這兩圖能夠看出,NIO的單線程能處理鏈接的數量比BIO要高出不少,而爲何單線程能處理更多的鏈接呢?緣由就是圖二中出現的 Selector
當一個鏈接創建以後,他有兩個步驟要作,第一步是接收完客戶端發過來的所有數據,第二步是服務端處理完請求業務以後返回response給客戶端。NIO和BIO的區別主要是在第一步。
在BIO中,等待客戶端發數據這個過程是阻塞的,這樣就形成了一個線程只能處理一個請求的狀況,而機器能支持的最大線程數是有限的,這就是爲何BIO不能支持高併發的緣由。
而NIO中,當一個Socket創建好以後,Thread並不會阻塞去接受這個Socket,而是將這個請求交給Selector,Selector會不斷的去遍歷全部的Socket,一旦有一個Socket創建完成,他會通知Thread,而後Thread處理完數據再返回給客戶端—— 這個過程是阻塞的,這樣就能讓一個Thread處理更多的請求了。
下面兩張圖是基於BIO的處理流程和netty的處理流程,輔助你理解兩種方式的差異:
 
BIO的處理流程

 
NIO的處理流程

 

除了BIO和NIO以外,還有一些其餘的IO模型,下面這張圖就表示了五種IO模型的處理流程:


 
五種常見的IO模型
  • BIO,同步阻塞IO,阻塞整個步驟,若是鏈接少,他的延遲是最低的,由於一個線程只處理一個鏈接,適用於少鏈接且延遲低的場景,好比說數據庫鏈接。
  • NIO,同步非阻塞IO,阻塞業務處理但不阻塞數據接收,適用於高併發且處理簡單的場景,好比聊天軟件。
  • 多路複用IO,他的兩個步驟處理是分開的,也就是說,一個鏈接可能他的數據接收是線程a完成的,數據處理是線程b完成的,他比BIO能處理更多請求,可是比不上NIO,可是他的處理性能又比BIO更差,由於一個鏈接他須要兩次system call,而BIO只須要一次,因此這種IO模型應用的很少。
  • 信號驅動IO,這種IO模型主要用在嵌入式開發,不參與討論。
  • 異步IO,他的數據請求和數據處理都是異步的,數據請求一次返回一次,適用於長鏈接的業務場景。

以上摘自Linux IO模式及 select、poll、epoll詳解

Netty爲何傳輸快

Netty的傳輸快其實也是依賴了NIO的一個特性——零拷貝。咱們知道,Java的內存有堆內存、棧內存和字符串常量池等等,其中堆內存是佔用內存空間最大的一塊,也是Java對象存放的地方,通常咱們的數據若是須要從IO讀取到堆內存,中間須要通過Socket緩衝區,也就是說一個數據會被拷貝兩次才能到達他的的終點,若是數據量大,就會形成沒必要要的資源浪費。
Netty針對這種狀況,使用了NIO中的另外一大特性——零拷貝,當他須要接收數據的時候,他會在堆內存以外開闢一塊內存,數據就直接從IO讀到了那塊內存中去,在netty裏面經過ByteBuf能夠直接對這些數據進行直接操做,從而加快了傳輸速度。
下兩圖就介紹了兩種拷貝方式的區別,摘自Linux 中的零拷貝技術,第 1 部分

 
傳統數據拷貝

 

 
零拷貝

上文介紹的ByteBuf是Netty的一個重要概念,他是netty數據處理的容器,也是Netty封裝好的一個重要體現,將在下一部分作詳細介紹。

爲何說Netty封裝好?

要說Netty爲何封裝好,這種用文字是說不清的,直接上代碼:

  • 阻塞I/O
public class PlainOioServer { public void serve(int port) throws IOException { final ServerSocket socket = new ServerSocket(port); //1 try { for (;;) { final Socket clientSocket = socket.accept(); //2 System.out.println("Accepted connection from " + clientSocket); new Thread(new Runnable() { //3 @Override public void run() { OutputStream out; try { out = clientSocket.getOutputStream(); out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8"))); //4 out.flush(); clientSocket.close(); //5 } catch (IOException e) { e.printStackTrace(); try { clientSocket.close(); } catch (IOException ex) { // ignore on close } } } }).start(); //6 } } catch (IOException e) { e.printStackTrace(); } } } 
  • 非阻塞IO
public class PlainNioServer { public void serve(int port) throws IOException { ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); ServerSocket ss = serverChannel.socket(); InetSocketAddress address = new InetSocketAddress(port); ss.bind(address); //1 Selector selector = Selector.open(); //2 serverChannel.register(selector, SelectionKey.OP_ACCEPT); //3 final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes()); for (;;) { try { selector.select(); //4 } catch (IOException ex) { ex.printStackTrace(); // handle exception break; } Set<SelectionKey> readyKeys = selector.selectedKeys(); //5 Iterator<SelectionKey> iterator = readyKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); try { if (key.isAcceptable()) { //6 ServerSocketChannel server = (ServerSocketChannel)key.channel(); SocketChannel client = server.accept(); client.configureBlocking(false); client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, msg.duplicate()); //7 System.out.println( "Accepted connection from " + client); } if (key.isWritable()) { //8 SocketChannel client = (SocketChannel)key.channel(); ByteBuffer buffer = (ByteBuffer)key.attachment(); while (buffer.hasRemaining()) { if (client.write(buffer) == 0) { //9 break; } } client.close(); //10 } } catch (IOException ex) { key.cancel(); try { key.channel().close(); } catch (IOException cex) { // 在關閉時忽略 } } } } } } 
  • Netty
public class NettyOioServer { public void server(int port) throws Exception { final ByteBuf buf = Unpooled.unreleasableBuffer( Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8"))); EventLoopGroup group = new OioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); //1 b.group(group) //2 .channel(OioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer<SocketChannel>() {//3 @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { //4 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5 } }); } }); ChannelFuture f = b.bind().sync(); //6 f.channel().closeFuture().sync(); } finally { group.shutdownGracefully().sync(); //7 } } } 

從代碼量上來看,Netty就已經秒殺傳統Socket編程了,可是這一部分博大精深,僅僅貼幾個代碼豈能說明問題,在這裏給你們介紹一下Netty的一些重要概念,讓你們更理解Netty。

  • Channel
    數據傳輸流,與channel相關的概念有如下四個,上一張圖讓你瞭解netty裏面的Channel。


     
    Channel一覽
    • Channel,表示一個鏈接,能夠理解爲每個請求,就是一個Channel。
    • ChannelHandler,核心處理業務就在這裏,用於處理業務請求。
    • ChannelHandlerContext,用於傳輸業務數據。
    • ChannelPipeline,用於保存處理過程須要用到的ChannelHandler和ChannelHandlerContext。
  • ByteBuf
    ByteBuf是一個存儲字節的容器,最大特色就是使用方便,它既有本身的讀索引和寫索引,方便你對整段字節緩存進行讀寫,也支持get/set,方便你對其中每個字節進行讀寫,他的數據結構以下圖所示:
 
ByteBuf數據結構

他有三種使用模式:

  1. Heap Buffer 堆緩衝區
    堆緩衝區是ByteBuf最經常使用的模式,他將數據存儲在堆空間。
  2. Direct Buffer 直接緩衝區
    直接緩衝區是ByteBuf的另一種經常使用模式,他的內存分配都不發生在堆,jdk1.4引入的nio的ByteBuffer類容許jvm經過本地方法調用分配內存,這樣作有兩個好處
    • 經過免去中間交換的內存拷貝, 提高IO處理速度; 直接緩衝區的內容能夠駐留在垃圾回收掃描的堆區之外。
    • DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 以外的內存, GC對此」無能爲力」,也就意味着規避了在高負載下頻繁的GC過程對應用線程的中斷影響.
  3. Composite Buffer 複合緩衝區
    複合緩衝區至關於多個不一樣ByteBuf的視圖,這是netty提供的,jdk不提供這樣的功能。

除此以外,他還提供一大堆api方便你使用,在這裏我就不一一列出了,具體參見ByteBuf字節緩存

  • Codec
    Netty中的編碼/解碼器,經過他你能完成字節與pojo、pojo與pojo的相互轉換,從而達到自定義協議的目的。
    在Netty裏面最有名的就是HttpRequestDecoder和HttpResponseEncoder了。

Netty入門教程2——動手搭建HttpServer
Netty入門教程3——Decoder和Encoder
Netty入門教程4——如何實現長鏈接

以上就是我對《Netty實戰》這本書的一些心得和書外的一些相關知識整理,若是有不一樣的看法,歡迎討論!

相關文章
相關標籤/搜索