天天看點

架構設計:系統間通信(13)——RPC執行個體Apache Thrift 下篇(1)1、服務治理2、設計一個服務治理架構2-1、涉及技術2-2、服務提供者設計思路

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)。事實上現在的軟體架構中,都是使用相似的“服務治理”思想,來解決這個問題的。如下圖所示:

架構設計:系統間通信(13)——RPC執行個體Apache Thrift 下篇(1)1、服務治理2、設計一個服務治理架構2-1、涉及技術2-2、服務提供者設計思路
  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核心原理(事件)》)。下圖為簡單的工作原理:

架構設計:系統間通信(13)——RPC執行個體Apache Thrift 下篇(1)1、服務治理2、設計一個服務治理架構2-1、涉及技術2-2、服務提供者設計思路

2-1、涉及技術

2-1-1、使用Zookeeper

Zookeeper是一個分布式的,開放源碼的分布式應用程式協調服務,是Hadoop和Hbase的重要元件。這裡我們使用Zookeeper共享“已注冊的服務”。為了保證所有服務提供者都能夠向Zookeeper注冊提供的服務,我們需要在Zookeeper上确定一個 服務提供者和服務使用者 協商一緻的“服務描述格式”。

要設計這個“服務描述格式”,首先就要清楚Zookeeper是如何記錄資訊的。由于我在其他文章中,已經詳細講解過Zookeeper的資訊記錄方式了,是以這裡就隻進行一些關鍵要素的講解:

  • Zookeeper采用樹型結構目錄結構記錄資訊。樹的深度沒有限制(但實際中,不可能建立很深的樹結構),每一個節點成為znode。
  • 每一個znode都有一個名稱,為了避免出現字元集編碼問題,請不要使用中文作為znode的名稱。另外,同一個znode下的子級znode名稱,不允許重複。
  • 一個znode允許存儲最多1MB大小的資料資訊。
架構設計:系統間通信(13)——RPC執行個體Apache Thrift 下篇(1)1、服務治理2、設計一個服務治理架構2-1、涉及技術2-2、服務提供者設計思路
  • 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服務的連接配接斷開後,這個節點被删除
架構設計:系統間通信(13)——RPC執行個體Apache Thrift 下篇(1)1、服務治理2、設計一個服務治理架構2-1、涉及技術2-2、服務提供者設計思路

那麼按照Zookeeper的這些工作特點,我們對“服務描述格式”的結構進行了如下圖所示的設計:

架構設計:系統間通信(13)——RPC執行個體Apache Thrift 下篇(1)1、服務治理2、設計一個服務治理架構2-1、涉及技術2-2、服務提供者設計思路
  • 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檔案進行如下的定義:

# 這個結構體定義了服務調用者的請求資訊
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);
}           

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

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

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

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

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

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

架構設計:系統間通信(13)——RPC執行個體Apache Thrift 下篇(1)1、服務治理2、設計一個服務治理架構2-1、涉及技術2-2、服務提供者設計思路

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

  • 最外層由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、模組化設計

  • 業務層模型設計
架構設計:系統間通信(13)——RPC執行個體Apache Thrift 下篇(1)1、服務治理2、設計一個服務治理架構2-1、涉及技術2-2、服務提供者設計思路
  • 服務層設計
架構設計:系統間通信(13)——RPC執行個體Apache Thrift 下篇(1)1、服務治理2、設計一個服務治理架構2-1、涉及技術2-2、服務提供者設計思路

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

(接下文)

繼續閱讀