經過前面兩篇文章(《架構設計:系統間通訊(12)——RPC實例Apache Thrift 中篇》、《架構設計:系統間通訊(11)——RPC實例Apache Thrift 上篇》)的介紹,相信讀者已經能夠將Apache Thrift應用到實際工做中,而且理解了爲何Apache Thrift的性能要比大多數RPC框架優秀。但若是您使用過Apache thrift,那麼相信您會發現它的一些不足(或者說是全部單純的RPC框架的不足):node
因爲Apache Thrift使用IDL定義RCP 調用接口,實現跨語言性。那麼一旦當業務發生變化後,是否要從新編寫IDL,從新生成接口代碼呢?spring
若是以上的事實成立,那若是在生成環境使用了多種語言,且服務節點又不少的狀況下。豈不是從新部署的工做量會很大?緩存
另外,生產環境的服務是不能停機的?那麼就會出現一部分接口是新部署的,另一部分接口是還未更新的。服務者怎麼保證接口的穩定呢?服務器
再說,個人生產環境下一共有20個相對獨立運行的系統:計費系統、客戶系統、訂單系統、庫存系統、物流系統、稅務聯動系統,等等。負責他們的開發團隊都是不同的。如何在某個系統的接口發生變更後,通知到其它系統「個人接口變更了」?即使是不能通知到全部系統「個人接口變更了」,又如何作到以前的接口也同樣可使用呢?網絡
顯然以上這些問題,單純使用Apache Thrift(或者單純的某一款RPC框架)是沒法解決的;使用人工的方式就更不要想解決了。若是您的相關係統只有2-3個,又或者每一個系統的服務節點數量也很少(例如五、6個),那麼以上這些問題還不太明顯。可是隨着您的系統愈來愈大,系統間協做愈來愈複雜,那麼這些問題就會凸現出來,甚至成爲影響您架構擴容的顯著問題。架構
解決這個問題的方式,阿里的作法是在衆多系統的RPC通訊的上層再架一層專門進行RPC通訊的協調管理,稱之爲服務治理框架(DUBBO框架,目前這個框架已經開源,在後面的文章中,我會花比較大的篇幅進行介紹。和DUBBO框架相似的還有Taobao的HSF)。事實上如今的軟件架構中,都是使用類似的「服務治理」思想,來解決這個問題的。以下圖所示:框架
當服務提供者可以向外部系統提供調用服務時(不管這個調用服務是基於RPC的仍是基於Http的,通常來講前者居多),它會首先向「服務管理組件」註冊這個服務,包括服務名、訪問權限、優先級、版本、參數、真實訪路徑、有效時間等等基本信息。分佈式
當某一個服務使用者須要調用服務時,首先會向「服務管理組件」詢問服務的基本信息。固然「服務管理組件」還會驗證服務使用者是否有權限進行調用、是否符合調用的前置條件等等過濾。最終「服務管理組件」將真實的服務提供者所在位置返回給服務使用者。工具
服務使用者拿到真實服務提供者的基本信息、調用權限後,再向真實的服務提供者發出調用請求,進行正式的業務調用過程。oop
在服務治理的思想中,包含幾個重要元素:
服務管理組件:這個組件是「服務治理」的核心組件,您的服務治理框架有多強大,主要取決於您的服務管理組件功能有多強大。它至少具備的功能包括:服務註冊管理、訪問路由;另外,它還能夠具備:服務版本管理、服務優先級管理、訪問權限管理、請求數量限制、連通性管理、註冊服務集羣、節點容錯、事件訂閱-發佈、狀態監控,等等功能。
服務提供者(服務生產者):即服務的具體實現,而後按照服務治理框架特定的規範發佈到服務管理組件中。這意味着什麼呢?這意味着,服務提供者不必定按照RPC調用的方式發佈服務,而是按照整個服務治理框架所規定的方式進行發佈(若是服務治理框架要求服務提供者以RPC調用的形式進行發佈,那麼服務提供者就必須以RPC調用的形式進行發佈;若是服務治理框架要求服務提供者以Http接口的形式進行發佈,那麼服務提供者就必須以Http接口的形式進行發佈,但後者這種狀況通常不會出現)。
服務使用者(服務消費者):即調用這個服務的用戶,調用者首先到服務管理組件中查詢具體的服務所在的位置;服務管理組件收到查詢請求後,將向它返回具體的服務所在位置(視服務管理組件功能的不一樣,還有可能進行這些計算:判斷服務調用者是否有權限進行調用、是否須要生成認證標記、是否須要從新檢查服務提供者的狀態、讓調用者使用哪個服務版本等等)。服務調用者在收到具體的服務位置後,向服務提供者發起正式請求,而且返回相應的結果。第二次調用時,服務請求者就能夠像服務提供者直接發起調用請求了(固然,您能夠有一個服務提供期限的設置,使用租約協議就能夠很好的實現)。
爲了更深刻理解服務治理框架的做用、工做原理,下面咱們就以Apache Thrift爲服務治理框架基礎技術,來實現一個簡單的服務治理框架。爲了保證快速實現,咱們使用zookeeper做爲服務管理組件的基礎技術(若是您不清楚zookeeper的相關技術點,能夠參考我另外的幾篇文章《hadoop系列:zookeeper(1)——zookeeper單點和集羣安裝》、《hadoop系列:zookeeper(2)——zookeeper核心原理(選舉)》、《hadoop系列:zookeeper(3)——zookeeper核心原理(事件)》)。下圖爲簡單的工做原理:
Zookeeper是一個分佈式的,開放源碼的分佈式應用程序協調服務,是Hadoop和Hbase的重要組件。這裏咱們使用Zookeeper共享「已註冊的服務」。爲了保證全部服務提供者都可以向Zookeeper註冊提供的服務,咱們須要在Zookeeper上肯定一個 服務提供者和服務使用者 協商一致的「服務描述格式」。
要設計這個「服務描述格式」,首先就要清楚Zookeeper是如何記錄信息的。因爲我在其餘文章中,已經詳細講解過Zookeeper的信息記錄方式了,因此這裏就只進行一些關鍵要素的講解:
Zookeeper採用樹型結構目錄結構記錄信息。樹的深度沒有限制(但實際中,不可能創建很深的樹結構),每個節點成爲znode。
每個znode都有一個名稱,爲了不出現字符集編碼問題,請不要使用中文做爲znode的名稱。另外,同一個znode下的子級znode名稱,不容許重複。
一個znode容許存儲最多1MB大小的數據信息。
znode根據建立性質的不同,可分爲四種行爲類型不同的znode。它們是:PERSISTENT、PERSISTENT_SEQUENTIAL、EPHEMERAL、EPHEMERAL_SEQUENTIAL。
PERSISTENT-持久化節點:建立這個節點的客戶端在與zookeeper服務的鏈接斷開後,這個節點也不會被刪除(除非您使用API強制刪除)。
PERSISTENT_SEQUENTIAL-持久化順序編號節點:當客戶端請求建立這個節點A後,zookeeper會根據parent-znode的zxid狀態,爲這個A節點編寫一個全目錄惟一的編號(這個編號只會一直增加)。當客戶端與zookeeper服務的鏈接斷開後,這個節點也不會被刪除。
EPHEMERAL-臨時目錄節點:建立這個節點的客戶端在與zookeeper服務的鏈接斷開後,這個節點(還有涉及到的子節點)就會被刪除。
EPHEMERAL_SEQUENTIAL-臨時順序編號目錄節點:當客戶端請求建立這個節點A後,zookeeper會根據parent-znode的zxid狀態,爲這個A節點編寫一個全目錄惟一的編號(這個編號只會一直增加)。當建立這個節點的客戶端與zookeeper服務的鏈接斷開後,這個節點被刪除
那麼按照Zookeeper的這些工做特色,咱們對「服務描述格式」的結構進行了以下圖所示的設計:
Zookeeper的根目錄名字叫作Service,這是一個持久化的znode節點,而且不須要存儲任何數據。
當某一個服務提供者啓動後,它將鏈接到Zookeeper集羣,而且在Service目錄下,建立一個以提供的服務名爲znode名稱的臨時節點(例如上圖所示的znode,分別叫作ServiceName一、ServiceName二、ServiceName3)。
每個Service的子級znode都使用JSON格式存儲兩個信息,分別是這個服務的真實訪問路徑和訪問端口。
這樣一來,當某一個服務提供者因爲某些緣由不能再提供服務,而且斷掉和zookeeper的鏈接後,它所註冊的服務就會消失。經過zookeeper的通知機制(或者等待客戶端的下一次詢問),客戶端就會知道已經沒有某一個服務了。
對於服務調用者(服務使用者)而言,實際上並非每一次調用服務前,都須要請求zookeeper詢問訪問地址。而是隻須要詢問一次,若是找到相關的服務,則記錄到本地;待到下一次請求時,直接尋找本地的歷史記錄便可。
Apache Thrift的基本使用這裏就再也不贅述了,若是您對Apache Thrift的基本使用還不清楚,請查看前文。對於Apache Thrift的使用,在咱們這個自行設計的服務治理框架中,要解決的重要問題,就是保證作到新增一個服務時,不須要從新改變IDL定義,不須要從新生成代碼。
這個問題主要的解決思路就是將Apache Thrift的接口定義進行泛化,即這個接口不調用具體的業務,而只給出調用者須要調用的接口名稱(包括參數),而後在服務器端,以反射的進行具體服務的調用。IDL文件進行以下的定義:
# 這個結構體定義了服務調用者的請求信息 struct Request { # 傳遞的參數信息,使用格式進行表示 1:required binary paramJSON; # 服務調用者請求的服務名,使用serviceName屬性進行傳遞 2:required string serviceName } # 這個結構體,定義了服務提供者的返回信息 struct Reponse { # RESCODE 是處理狀態代碼,是一個枚舉類型。例如RESCODE._200表示處理成功 1:required RESCODE responeCode; # 返回的處理結果,一樣使用JSON格式進行描述 2:required binary responseJSON; } # 異常描述定義,當服務提供者處理過程出現異常時,向服務調用者返回 exception ServiceException { # EXCCODE 是異常代碼,也是一個枚舉類型。 # 例如EXCCODE.PARAMNOTFOUND表示須要的請求參數沒有找到 1:required EXCCODE exceptionCode; # 異常的描述信息,使用字符串進行描述 2:required string exceptionMess; } # 這個枚舉結構,描述各類服務提供者的響應代碼 enum RESCODE { _200=200; _500=500; _400=400; } # 這個枚舉結構,描述各類服務提供者的異常種類 enum EXCCODE { PARAMNOTFOUND = 2001; SERVICENOTFOUND = 2002; } # 這是通過泛化後的Apache Thrift接口 service DIYFrameworkService { Reponse send(1:required Request request) throws (1:required ServiceException e); }123456789101112131415161718192021222324252627282930313233343536373839404142
在給出所有示例代碼前,首先就要把咱們自定製的這個「服務治理」框架的設計思路講清楚。這樣各位讀者在看示例代碼的時候纔不至於看昏過去。上文已經講過,整個「服務治理」框架主要由四部分構成:基於zookeeper的服務管理器、服務提供者、服務調用者、爲跨語言準備的IDL描述。
基於zookeeper的服務管理器,最重要的就是zookeeper中的目錄結構如何設計的問題,這個問題在前文中已經講得比較清楚,無須贅述了;爲跨語言準備的IDL描述文件,以及爲何這樣設計IDL描述也已經在上文中講清楚了;那麼對於服務調用者來講,最主要的就是兩步調用過程:先查詢zookeeper服務管理器,找到要調用的服務地址,而後請求具體服務,基本上是比較簡單的,無需花很長的篇幅說明設計思路;
那麼要說清楚整個「服務治理」框架的設計思路,最主要的仍是說清楚服務提供者的設計思路。由於基本上全部業務過程、事件監聽調用,都發生在服務提供者這一端。
下圖表達了服務提供者的軟件結構設計思路:
從上圖能夠看到,整個服務端的設計分爲三層:
最外層由Zookeeper客戶端和Apache Thrift服務構成。Zookeeper客戶端用於向Zookeeper服務集羣註冊「提供的服務」;Apache Thrift用於接受服務調用者的請求,並按照格式響應處理結果。
因爲咱們定義的Apache Thrift接口(DIYFrameworkService)已經被泛化,因此具體的業務處理不能由Apache Thrift的實現(DIYFrameworkServiceImpl)來處理。因爲這個緣由,那麼在服務端的設計中,就必須有一個服務代理層,這個服務代理層最重要的功能,就是根據Thrift收到的請求參數,決定調用哪一個真實服務(在下文專門介紹具體代碼的章節中,還將介紹如何集成spring,對代理層進行優化)。
根據軟件功能需求的要求,具體的服務實現能夠有多個。在設計中咱們規定,全部的具體業務實現者,必須實現BusinessService接口中的handle方法。而且返回的類型都必須繼承AbstractPojo。
這裏咱們提供的示例設計,是爲了讓各位讀者瞭解「服務治理」的基本設計原理。咱們目前介紹的示例若是要應用到實際工做中,那麼還須要按照讀者本身的業務特色進行調整、修改甚至是從新設計。對於這個示例提供的功能來講,咱們提供一些簡單的,具備表明意義的就能夠了:
zookeeper服務:服務提供者的zookeeper客戶端只負責鏈接到zookeeper服務集羣,而且向zookeeper服務集羣註冊「服務提供者所提供的服務」。註冊zookeeper時所依據的目錄結構見上文中zookeeper目錄結構設計的介紹。爲了處理簡單,zookeeper服務並不考慮性能問題,無需監聽zookeeper集羣上任何目錄結構的變化事件,也無需將遠程zookeeper集羣上的目錄結構緩存到本地。設計的目錄結構也無需考慮一個服務由多個服務節點同時提供服務的狀況。也無需考慮訪問權限、訪問優先級的問題。
Apache Thrift服務:服務提供者的Apache Thrift只負責提供遠程RPC調用的監聽服務。並且IDL的設計也很簡單(參見上文中對IDL定義格式的介紹),只要的開發語言採用JAVA,無需生成多語言的代碼。採用阻塞同步的網絡通信模式,無需考慮Apache Thrift的性能問題。
服務代理:在正式的生產環境中,實際上服務代理層須要負責的工做是最多的。例如它要對服務請求者的令牌環進行判斷,以便肯定服務是否過時;要對請求者的權限進行驗證;要管理具體的服務實現的註冊,以便向zookeeper客戶端告知註冊狀況;要決定具體執行哪個服務實現,等等工做。可是爲了讓示例簡潔,服務代理層只提供一個簡單的註冊管理和具體服務實現的調用。
服務實如今整個實例代碼中,咱們只提供一個服務:實現BusinessService服務層接口(business.impl.QueryUserDetailServiceImpl),查詢用戶詳細信息的服務。而且向服務代理層註冊這個服務爲:」queryUserDetailService」 -> 「business.impl.QueryUserDetailServiceImpl」
業務層模型設計
服務層設計
以上兩種類簡圖和附帶的說明,已經把示例工程中重要的設計詳情進行了描述。固然工程中還有其餘類,可是它們主要仍是起輔助做用。例如工具類:JSONUtils、DateUtils;自定義異常:BizException;響應代碼:ResponseCode;應用程序啓動類:MainProcessor;這些咱們將在下文具體代碼中進行講解。
(接下文)