3 命名服務
命名服務(NameService)也是分布式系統中比較常見的一類場景,在《Java網絡高級編程》一書中提到,命名服務是分布式系統最基本的公共服務之一。在分布式系統中,被命名的實體通常可以是集群中的機器、提供的服務地址或遠程對象等一這些我們都可以統稱它們為名字(Name),其中較為常見的就是一些分布式服務框架(如RPC、RMI)中的服務地址列表,通過使用命名服務,客戶端應用能夠根據指定名字來獲取資源的實體、服務地址和提供者的信息等。
Java語言中的JNDI便是一種典型的命名服務。JNDI是Java命名與目錄接口(Java?Naming and Directory Interface)的縮寫,是J2EE體系中重要的規范之一,標準的J2EE容器都提供了對JNDI規范的實現。因此,在實際開發中,開發人員常常使用應用服務器自帶的JNDI實現來完成數據源的配置與管理一使用JNDI方式后,開發人員可以完全不需要關心與數據庫相關的任何信息,包括數據庫類型、JDBC驅動類型以及數據庫賬戶等。
ZooKeeper提供的命名服務功能與JNDI技術有相似的地方,都能夠幫助應用系統通過一個資源引用的方式來實現對資源的定位與使用。另外,廣義上命名服務的資源定位都不是真正意義的實體資源一在分布式環境中,上層應用僅僅需要一個全局唯一的名字,類似于數據庫中的唯一主鍵。 下面我們來看看如何使用ZooKeeper來實現一套分布式全局唯一ID的分配機制。
所謂ID,就是一個能夠唯一標識某個對象的標識符。在我們熟悉的關系型數據庫中,各個表都需要一個主鍵來唯一標識每條數據庫記錄,這個主鍵就是這樣的唯一ID。在過去的單庫單表型系統中,通常可以使用數據庫字段自帶的auto_increment屬性來自動為每條數據庫記錄生成一個唯一的ID,數據庫會保證生成的這個ID在全局唯一。但是隨著數據庫數據規模的不斷增大,分庫分表隨之出現,而auto_increment 屬性僅能針對單一表中的記錄自動生成ID,因此在這種情況下,就無法再依靠數據庫的auto_increment 屬性來唯一標識一條記錄了。于是,我們必須尋求一種能夠在分布式環境下生成全局唯一ID的方法。
說起全局唯一ID,相信讀者都會聯想到UUID。沒錯,UUID是通用唯一識別碼(Universally Unique Identifier) 的簡稱,是一種在分布式系統中廣泛使用的用于唯一標識元素的標準,最典型的實現是GUID (Globally Unique ldentifier,全局唯一標識符),主流ORM框架Hibernate有對UUID的直接支持。
確實,UUID是一個非常不錯的全局唯一ID生成方式,能夠非常簡便地保證分布式環境中的唯一性。一個標準的UUID 是一個包含32位字符和4個短線的字符串,例如“e70f1357-f260-46ff-a32d-53a086c57ade”。UUID的優勢自然不必多說,我們重點來看看
它的缺陷。
(1)長度過長
UUID最大的問題就在于生成的字符串過長。顯然,和數據庫中的INT類型相比,存儲一個UUID需要花費更多的空間。
(2)含義不明
上面我們已經看到一個典型的UUID是類似于“e70f1357- f260-46fF- a32d-53a086c57ade"的一個字符串。根據這個字符串,開發人員從字面上基本看不出任何其表達的含義,這將會大大影響問題排查和開發調試的效率。
接下來,我們結合一個分布式任務調度系統來看看如何使用ZooKeeper來實現這類全局唯一ID的生成。
通過調用ZooKeeper節點創建的API接口可以創建一個順序節點,并且在API返回值中會返回這個節點的完整名字。利用這個特性,我們就可以借助ZooKeeper來生成全局唯一的ID了,如下圖所示。
結合上圖,我們來講解對于一個任務列表的主鍵,使用ZooKeeper生成唯一ID 的基本步驟。
(1)所有客戶端都會根據自己的任務類型,在指定類型的任務下面通過調用create()接口來創建一個順序節點,例如創建“job-”節點。
(2)節點創建完畢后,create()接口會返回一個完整的節點名,例如“job-000000003"。
(3)客戶端拿到這個返回值后,拼接上type類型,例如“type2-job 000000003”, 這就可以作為一個全局唯一的ID了。
在ZooKeeper中,每一個數據節點都能夠維護--份子節點的順序順列,當客戶端對其創建一個順序子節點的時候ZooKeeper會自動以后綴的形式在其子節點上添加一個序號,在這個場景中就是利用了ZooKeeper的這個特性。
4 分布式協調/通知
分布式協調/通知服務是分布式系統中不可缺少的一個環節,是將不同的分布式組件有機結合起來的關鍵所在。對于一個在多臺機器上部署運行的應用而言,通常需要一個協調者(Coordinator)來控制整個系統的運行流程,例如分布式事務的處理、機器間的互相協調等。同時,引入這樣一個協調者,便于將分布式協調的職責從應用中分離出來,從而可以大大減少系統之間的耦合性,而且能夠顯著提高系統的可擴展性。
ZooKeeper中特有的Watcher注冊與異步通知機制,能夠很好地實現分布式環境下不同機器,甚至是不同系統之間的協調與通知,從而實現對數據變更的實時處理。基于ZooKeeper實現分布式協調與通知功能,通常的做法是不同的客戶端都對ZooKeeper上同一個數據節點進行Watcher注冊,監聽數據節點的變化(包括數據節點本身及其子節點),如果數據節點發生變化,那么所有訂閱的客戶端都能夠接收到相應的Watcher通知,并做出相應的處理。
MySQL數據復制總線:Mysql_Replicator
MySQL數據復制總線(以下簡稱“復制總線”)是一個實時數據復制框架,用于在不同的MySQL數據庫實例之間進行異步數據復制和數據變化通知。整個系統是一個由MySQL數據庫集群、消息隊列系統、任務管理監控平臺以及ZooKeeper 集群等組件共同構成的一個包含數據生產者、復制管道和數據消費者等部分的數據總線系統,下圖所示是該系統的整體結構圖。
?
在該系統中,ZooKeeper主要負責進行一系列的分布式協調工作,在具體的實現上,根據功能將數據復制組件劃分為三個核心子模塊:Core、 Server 和Monitor,每個模塊分別為一個單獨的進程,通過ZooKeeper進行數據交換。
Core實現了數據復制的核心邏輯,其將數據復制封裝成管道,并抽象出生產者和消費者兩個概念,其中生產者通常是MySQL數據庫的Binlog日志。
Server負責啟動和停止復制任務。
Monitor負責監控任務的運行狀態,如果在數據復制期間發生異常或出現故障會進行告警。
三個子模塊之間的關系如下圖所示。
每個模塊作為獨立的進程運行在服務端,運行時的數據和配置信息均保存在ZooKeeper上,Web控制臺通過ZooKeeper上的數據獲取到后臺進程的數據,同時發布控制信息。
任務注冊
Core進程在啟動的時候,首先會向/mysql_replicator/tasks節點(以下簡稱“任務列表節點”)注冊任務。例如,對于一個“復制熱門商品”的任務,Task 所在機器在啟動的時候,會首先在任務列表節點上創建一個子節點,例如/mysql_replicator/tasks/copy_hot_item(以下簡稱“任務節點")。如果在注冊過程中發現該子節點已經存在,說明已經有其他Task機器注冊了該任務,因此自己不需要再創建該節點了。
任務熱備份
為了應對復制任務故障或者復制任務所在主機故障,復制組件采用“熱備份”的容災方式,即將同一個復制任務部署在不同的主機上,我們稱這樣的機器為“任務機器”,主、備任務機器通過ZooKeeper互相檢測運行健康狀況。
為了實現上述熱備方案,無論在第一步中是否創建了任務節點,每臺任務機器都需要在/mysql_replicator/tasks/copy_hot_item/instances節點上將自己的主機名注冊上去。注意,這里注冊的節點類型很特殊,是一個臨時的順序節點。在注冊完這個子節點后,通常一個完整的節點名如下: /mysql_replicator/tasks/copy_hot_item/intsances/[Hostname]-I,其中最后的序列號就是臨時順序節點的精華所在。
在完成該子節點的創建后,每臺任務機器都可以獲取到自己創建的節點的完成節點名以及所有子節點的列表,然后通過對比判斷自己是否是所有子節點中序號最小的。如果自己是序號最小的子節點,那么就將自己的運行狀態設置為RUNNING,其余的任務機器則將自己設置為STANDBY,我們將這樣的熱備份策略稱為“小序號優先”策略。
熱備切換
完成運行狀態的標識后,任務的客戶端機器就能夠正常工作了,其中標記為RUNNING的客戶端機器進行正常的數據復制,而標記為STANDBY的客戶端機器則進入待命狀態。這里所謂待命狀態,就是說一旦標記為RUNNING的機器出現故障停止了任務執行,那么就需要在所有標記為STANDBY的客戶端機器中再次按照“小序號優先”策略來選出RUNNING機器來執行,具體的做法就是標記為STANDBY的機器都需要在/mysql_replicator/tasks/copy_hot item/instances節點上注冊一個“子節點列表變更”的Watcher監聽,用來訂閱所有任務執行機器的變化情況——一旦RUNNING機器宕機與ZooKeeper斷開連接后,對應的節點就會消失,于是其他機器也就接收到了這個變更通知,從而開始新一輪的RUNNING選舉。
記錄執行狀態
既然使用了熱備份,那么RUNNING任務機器就需要將運行時的上下文狀態保留給STANDBY任務機器。在這個場景中,最主要的上下文狀態就是數據復制過程中的一些進度信息,例如Binlog日志的消費位點,因此需要將這些信息保存到ZooKeeper上以便共享。在Mysql_Replicator的設計中,選擇了/mysq_replicator/tasks/copy_hot_item/lastCommit作為Binlog日志消費位點的存儲節點,RUNNING任務機器會定時向這個節點寫人當前的Binlog日志消費位點。
控制臺協調
在Mysql_Replicator中,Server主要的工作就是進行任務的控制,通過ZooKeeper來對不同的任務進行控制與協調。Server會將每個復制任務對應生產者的元數據,即庫名、表名、用戶名與密碼等數據庫信息以及消費者的相關信息以配置的形式寫入任務節點/mysql_replicator/tasks/copy_hot_item中去,以便該任務的所有任務機器都能夠共享該復制任務的配置。
冷備切換
到目前為止我們已經基本了解了Mysql_Replicator的工作原理,現在再回過頭來看上面提到的熱備份。在該熱備份方案中,針對一個任務,都會至少分配兩臺任務機器來進行熱備份,但是在一定規模的大型互聯網公司中,往往有許多MySQL實例需要進行數據復制,每個數據庫實例都會對應一個復制任務,如果每個任務都進行雙機熱備份的話,那么顯然需要消耗太多的機器。
和熱備份中比較大的區別在于,Core進程被配置了所屬Group(組)。舉個例子來說,假如一個Core進程被標記了group1,那么在Core進程啟動后,會到對應的ZooKeepergroup1節點下面獲取所有的Task列表,假如找到了任務“copy_hot_item”之后,就會遍歷這個Task列表的instances 節點,但凡還沒有子節點的,則會創建一個臨時的順序節點: /mysql_replicator/task-groups/group1/copy_hot_item/instances/[Hostname]-1——當然,在這個過程中,其他Core進程也會在這個instances節點下創建類似的子節點。和熱備份中的“小序號優先”策略一樣,順序小的Core進程將自己標記為RUNNING,不同之處在于,其他Core進程則會自動將自己創建的子節點刪除,然后繼續遍歷下一個Task節點一我們將這樣的過程稱為“冷備份掃描”。就這樣,所有Core進程在一個掃描周期內不斷地對相應的Group下面的Task進行冷備份掃描。整個過程可以通過如下圖所示的流程圖來表示。
冷熱備份對比
從上面的講解中,我們基本對熱備份和冷備份兩種運行方式都有了一定的了解,現在再來對比下這兩種運行方式。在熱備份方案中,針對一個任務使用了兩臺機器進行熱備份,借助ZooKeeper的Watcher通知機制和臨時順序節點的特性,能夠非常實時地進行互相協調,但缺陷就是機器資源消耗比較大。而在冷備份方案中,采用了掃描機制,雖然降低了任務協調的實時性,但是節省了機器資源。
一種通用的分布式系統機器間通信方式
在絕大部分的分布式系統中,系統機器間的通信無外乎心跳檢測、工作進度匯報和系統調度這三種類型。接下來,我們將圍繞這三種類型的機器通信來講解如何基于ZooKeeper去實現一種分布式系統間的通信方式。
心跳檢測
機器間的心跳檢測機制是指在分布式環境中,不同機器之間需要檢測到彼此是否在正常運行,例如A機器需要知道B機器是否正常運行。在傳統的開發中,我們通常是通過主機之間是否可以相互PING通來判斷,更復雜一點的話,則會通過在機器之間建立長連接,通過TCP連接固有的心跳檢測機制來實現上層機器的心跳檢測,這些確實都是一些非常常見的心跳檢測方法。
下面來看看如何使用ZooKeeper來實現分布式機器間的心跳檢測。基于ZooKeeper的臨時節點特性,可以讓不同的機器都在ZooKeeper的一個指定節點下創建臨時子節點,不同的機器之間可以根據這個臨時節點來判斷對應的客戶端機器是否存活。通過這種方式,檢測系統和被檢測系統之間并不需要直接相關聯,而是通過ZooKeeper上的某個節點進行關聯,大大減少了系統耦合。
工作進度匯報
在一個常見的任務分發系統中,通常任務被分發到不同的機器上執行后,需要實時地將自己的任務執行進度匯報給分發系統。這個時候就可以通過ZooKeeper來實現。在ZooKeeper上選擇一個節點,每個任務客戶端都在這個節點下面創建臨時子節點,這樣便可以實現兩個功能:
通過判斷臨時節點是否存在來確定任務機器是否存活;
各個任務機器會實時地將自己的任務執行進度寫到這個臨時節點上去,以便中心系
統能夠實時地獲取到任務的執行進度。
系統調度
使用ZooKeeper,能夠實現另--種系統調度模式:一個分布式系統由控制臺和一些客戶端系統兩部分組成,控制臺的職責就是需要將--些指令信息發送給所有的客戶端,以控制它們進行相應的業務邏輯。后臺管理人員在控制臺上做的一些操作,實際上就是修改了ZooKeeper上某些節點的數據,而ZooKeeper進一步把這些數據變更以事件通知的形式發送給了對應的訂閱客戶端。
總之,使用ZooKeeper來實現分布式系統機器間的通信,不僅能省去大量底層網絡通信和協議設計上重復的工作,更為重要的一點是大大降低了系統之間的耦合,能夠非常方便地實現異構系統之間的靈活通信。