Apache Thrift 下篇(1)

轉載地址:http://blog.csdn.net/yinwenjie/article/details/49869535

1、服務治理

通過前面兩篇文章(《架構設計:系統間通信(12)——RPC實例Apache Thrift 中篇》、《架構設計:系統間通信(11)——RPC實例Apache Thrift 上篇》)的介紹,相信讀者已經可以將Apache Thrift應用到實際工作中,並且理解了爲什麼Apache Thrift的性能要比大多數RPC框架優秀。但如果您使用過Apache thrift,那麼相信您會發現它的一些不足(或者說是所有單純的RPC框架的不足):

  • 由於Apache Thrift使用IDL定義RCP 調用接口,實現跨語言性。那麼一旦當業務發生變化後,是否要重新編寫IDL,重新生成接口代碼呢?

  • 如果以上的事實成立,那如果在生成環境使用了多種語言,且服務節點又很多的情況下。豈不是重新部署的工作量會很大?

  • 另外,生產環境的服務是不能停機的?那麼就會出現一部分接口是新部署的,另外一部分接口是還未更新的。服務者怎麼保證接口的穩定呢?

  • 再說,我的生產環境下一共有20個相對獨立運行的系統:計費系統、客戶系統、訂單系統、庫存系統、物流系統、稅務聯動系統,等等。負責他們的開發團隊都是不一樣的。如何在某個系統的接口發生變動後,通知到其它系統“我的接口變動了”?即便是不能通知到所有系統“我的接口變動了”,又如何做到之前的接口也一樣可以使用呢?

顯然以上這些問題,單純使用Apache Thrift(或者單純的某一款RPC框架)是無法解決的;使用人工的方式就更不要想解決了。如果您的相關係統只有2-3個,又或者每個系統的服務節點數量也不多(例如5、6個),那麼以上這些問題還不太明顯。但是隨着您的系統越來越大,系統間協作越來越複雜,那麼這些問題就會凸現出來,甚至成爲影響您架構擴容的顯著問題。

解決這個問題的方式,阿里的做法是在衆多系統的RPC通信的上層再架一層專門進行RPC通信的協調管理,稱之爲服務治理框架(DUBBO框架,目前這個框架已經開源,在後面的文章中,我會花比較大的篇幅進行介紹。和DUBBO框架類似的還有Taobao的HSF)。事實上現在的軟件架構中,都是使用相似的“服務治理”思想,來解決這個問題的。如下圖所示:

這裏寫圖片描述

  1. 當服務提供者能夠向外部系統提供調用服務時(無論這個調用服務是基於RPC的還是基於Http的,一般來說前者居多),它會首先向“服務管理組件”註冊這個服務,包括服務名、訪問權限、優先級、版本、參數、真實訪路徑、有效時間等等基本信息。

  2. 當某一個服務使用者需要調用服務時,首先會向“服務管理組件”詢問服務的基本信息。當然“服務管理組件”還會驗證服務使用者是否有權限進行調用、是否符合調用的前置條件等等過濾。最終“服務管理組件”將真實的服務提供者所在位置返回給服務使用者。

  3. 服務使用者拿到真實服務提供者的基本信息、調用權限後,再向真實的服務提供者發出調用請求,進行正式的業務調用過程。

在服務治理的思想中,包含幾個重要元素:

  • 服務管理組件:這個組件是“服務治理”的核心組件,您的服務治理框架有多強大,主要取決於您的服務管理組件功能有多強大。它至少具有的功能包括:服務註冊管理、訪問路由;另外,它還可以具有:服務版本管理、服務優先級管理、訪問權限管理、請求數量限制、連通性管理、註冊服務集羣、節點容錯、事件訂閱-發佈、狀態監控,等等功能。

  • 服務提供者(服務生產者):即服務的具體實現,然後按照服務治理框架特定的規範發佈到服務管理組件中。這意味着什麼呢?這意味着,服務提供者不一定按照RPC調用的方式發佈服務,而是按照整個服務治理框架所規定的方式進行發佈(如果服務治理框架要求服務提供者以RPC調用的形式進行發佈,那麼服務提供者就必須以RPC調用的形式進行發佈;如果服務治理框架要求服務提供者以Http接口的形式進行發佈,那麼服務提供者就必須以Http接口的形式進行發佈,但後者這種情況一般不會出現)。

  • 服務使用者(服務消費者):即調用這個服務的用戶,調用者首先到服務管理組件中查詢具體的服務所在的位置;服務管理組件收到查詢請求後,將向它返回具體的服務所在位置(視服務管理組件功能的不同,還有可能進行這些計算:判斷服務調用者是否有權限進行調用、是否需要生成認證標記、是否需要重新檢查服務提供者的狀態、讓調用者使用哪一個服務版本等等)。服務調用者在收到具體的服務位置後,向服務提供者發起正式請求,並且返回相應的結果。第二次調用時,服務請求者就可以像服務提供者直接發起調用請求了(當然,您可以有一個服務提供期限的設置,使用租約協議就可以很好的實現)。

2、設計一個服務治理框架

爲了更深入理解服務治理框架的作用、工作原理,下面我們就以Apache Thrift爲服務治理框架基礎技術,來實現一個簡單的服務治理框架。爲了保證快速實現,我們使用zookeeper作爲服務管理組件的基礎技術(如果您不清楚zookeeper的相關技術點,可以參考我另外的幾篇文章《hadoop系列:zookeeper(1)——zookeeper單點和集羣安裝》、《hadoop系列:zookeeper(2)——zookeeper核心原理(選舉)》、《hadoop系列:zookeeper(3)——zookeeper核心原理(事件)》)。下圖爲簡單的工作原理:

這裏寫圖片描述

2-1、涉及技術

2-1-1、使用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,分別叫做ServiceName1、ServiceName2、ServiceName3)。

  • 每一個Service的子級znode都使用JSON格式存儲兩個信息,分別是這個服務的真實訪問路徑和訪問端口。

  • 這樣一來,當某一個服務提供者由於某些原因不能再提供服務,並且斷掉和zookeeper的連接後,它所註冊的服務就會消失。通過zookeeper的通知機制(或者等待客戶端的下一次詢問),客戶端就會知道已經沒有某一個服務了。

  • 對於服務調用者(服務使用者)而言,實際上並不是每一次調用服務前,都需要請求zookeeper詢問訪問地址。而是只需要詢問一次,如果找到相關的服務,則記錄到本地;待到下一次請求時,直接尋找本地的歷史記錄即可。

2-1-2、使用Apache Thrift

Apache Thrift的基本使用這裏就不再贅述了,如果您對Apache Thrift的基本使用還不清楚,請查看前文。對於Apache Thrift的使用,在我們這個自行設計的服務治理框架中,要解決的重要問題,就是保證做到新增一個服務時,不需要重新改變IDL定義,不需要重新生成代碼。

這個問題主要的解決思路就是將Apache Thrift的接口定義進行泛化,即這個接口不調用具體的業務,而只給出調用者需要調用的接口名稱(包括參數),然後在服務器端,以反射的進行具體服務的調用。IDL文件進行如下的定義:

<code class="hljs ruby has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 這個結構體定義了服務調用者的請求信息</span>
struct <span class="hljs-constant" style="box-sizing: border-box;">Request</span> {
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 傳遞的參數信息,使用格式進行表示</span>
    <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span><span class="hljs-symbol" style="color: rgb(0, 102, 102); box-sizing: border-box;">:required</span> binary paramJSON;
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 服務調用者請求的服務名,使用serviceName屬性進行傳遞</span>
    <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span><span class="hljs-symbol" style="color: rgb(0, 102, 102); box-sizing: border-box;">:required</span> string serviceName
}

<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 這個結構體,定義了服務提供者的返回信息</span>
struct <span class="hljs-constant" style="box-sizing: border-box;">Reponse</span> {
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># RESCODE 是處理狀態代碼,是一個枚舉類型。例如RESCODE._200表示處理成功</span>
    <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span><span class="hljs-symbol" style="color: rgb(0, 102, 102); box-sizing: border-box;">:required</span>  <span class="hljs-constant" style="box-sizing: border-box;">RESCODE</span> responeCode;
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 返回的處理結果,同樣使用JSON格式進行描述</span>
    <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span><span class="hljs-symbol" style="color: rgb(0, 102, 102); box-sizing: border-box;">:required</span>  binary responseJSON;
}

<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 異常描述定義,當服務提供者處理過程出現異常時,向服務調用者返回</span>
exception <span class="hljs-constant" style="box-sizing: border-box;">ServiceException</span> {
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># EXCCODE 是異常代碼,也是一個枚舉類型。</span>
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 例如EXCCODE.PARAMNOTFOUND表示需要的請求參數沒有找到</span>
    <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span><span class="hljs-symbol" style="color: rgb(0, 102, 102); box-sizing: border-box;">:required</span> <span class="hljs-constant" style="box-sizing: border-box;">EXCCODE</span> exceptionCode;
    <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 異常的描述信息,使用字符串進行描述</span>
    <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span><span class="hljs-symbol" style="color: rgb(0, 102, 102); box-sizing: border-box;">:required</span> string exceptionMess;
}

<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 這個枚舉結構,描述各種服務提供者的響應代碼</span>
enum <span class="hljs-constant" style="box-sizing: border-box;">RESCODE</span> {
    _20<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">200</span>;
    _50<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">500</span>;
    _40<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>=<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">400</span>;
}

<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 這個枚舉結構,描述各種服務提供者的異常種類</span>
enum <span class="hljs-constant" style="box-sizing: border-box;">EXCCODE</span> {
    <span class="hljs-constant" style="box-sizing: border-box;">PARAMNOTFOUND</span> = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2001</span>;
    <span class="hljs-constant" style="box-sizing: border-box;">SERVICENOTFOUND</span> = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2002</span>;
}

<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># 這是經過泛化後的Apache Thrift接口</span>
service <span class="hljs-constant" style="box-sizing: border-box;">DIYFrameworkService</span> {
    <span class="hljs-constant" style="box-sizing: border-box;">Reponse</span> send(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span><span class="hljs-symbol" style="color: rgb(0, 102, 102); box-sizing: border-box;">:required</span> <span class="hljs-constant" style="box-sizing: border-box;">Request</span> request) throws (<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span><span class="hljs-symbol" style="color: rgb(0, 102, 102); box-sizing: border-box;">:required</span> <span class="hljs-constant" style="box-sizing: border-box;">ServiceException</span> e);
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li><li style="box-sizing: border-box; padding: 0px 5px;">34</li><li style="box-sizing: border-box; padding: 0px 5px;">35</li><li style="box-sizing: border-box; padding: 0px 5px;">36</li><li style="box-sizing: border-box; padding: 0px 5px;">37</li><li style="box-sizing: border-box; padding: 0px 5px;">38</li><li style="box-sizing: border-box; padding: 0px 5px;">39</li><li style="box-sizing: border-box; padding: 0px 5px;">40</li><li style="box-sizing: border-box; padding: 0px 5px;">41</li><li style="box-sizing: border-box; padding: 0px 5px;">42</li></ul>

2-2、服務提供者設計思路

在給出全部示例代碼前,首先就要把我們自定製的這個“服務治理”框架的設計思路講清楚。這樣各位讀者在看示例代碼的時候才不至於看昏過去。上文已經講過,整個“服務治理”框架主要由四部分構成:基於zookeeper的服務管理器、服務提供者、服務調用者、爲跨語言準備的IDL描述。

基於zookeeper的服務管理器,最重要的就是zookeeper中的目錄結構如何設計的問題,這個問題在前文中已經講得比較清楚,無須贅述了;爲跨語言準備的IDL描述文件,以及爲什麼這樣設計IDL描述也已經在上文中講清楚了;那麼對於服務調用者來說,最主要的就是兩步調用過程:先查詢zookeeper服務管理器,找到要調用的服務地址,然後請求具體服務,基本上是比較簡單的,無需花很長的篇幅說明設計思路;

那麼要說清楚整個“服務治理”框架的設計思路,最主要的還是說清楚服務提供者的設計思路。因爲基本上所有業務過程、事件監聽調用,都發生在服務提供者這一端。

2-2-1、服務提供者設計

下圖表達了服務提供者的軟件結構設計思路:

這裏寫圖片描述

從上圖可以看到,整個服務端的設計分爲三層:

  • 最外層由Zookeeper客戶端和Apache Thrift服務構成。Zookeeper客戶端用於向Zookeeper服務集羣註冊“提供的服務”;Apache Thrift用於接受服務調用者的請求,並按照格式響應處理結果。

  • 由於我們定義的Apache Thrift接口(DIYFrameworkService)已經被泛化,所以具體的業務處理不能由Apache Thrift的實現(DIYFrameworkServiceImpl)來處理。由於這個原因,那麼在服務端的設計中,就必須有一個服務代理層,這個服務代理層最重要的功能,就是根據Thrift收到的請求參數,決定調用哪個真實服務(在下文專門介紹具體代碼的章節中,還將介紹如何集成spring,對代理層進行優化)。

  • 根據軟件功能需求的要求,具體的服務實現可以有多個。在設計中我們規定,所有的具體業務實現者,必須實現BusinessService接口中的handle方法。並且返回的類型都必須繼承AbstractPojo。

2-2-2、功能邊界確認

這裏我們提供的示例設計,是爲了讓各位讀者瞭解“服務治理”的基本設計原理。我們目前介紹的示例如果要應用到實際工作中,那麼還需要按照讀者自己的業務特點進行調整、修改甚至是重新設計。對於這個示例提供的功能來說,我們提供一些簡單的,具有代表意義的就可以了:

  • 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”

2-2-3、建模設計

  • 業務層模型設計

這裏寫圖片描述

  • 服務層設計

這裏寫圖片描述

以上兩種類簡圖和附帶的說明,已經把示例工程中重要的設計詳情進行了描述。當然工程中還有其他類,但是它們主要還是起輔助作用。例如工具類:JSONUtils、DateUtils;自定義異常:BizException;響應代碼:ResponseCode;應用程序啓動類:MainProcessor;這些我們將在下文具體代碼中進行講解。

(接下文)

發佈了8 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章