基於netty+Spring+Zookeeper的分佈式RPC框架

開發筆記

參考文章

啓動示意圖

在這裏插入圖片描述

對示意圖中出現的單詞意義解釋

provider

服務提供者

consumer

服務消費者

Zookeeper

Zookeeper集羣,作爲服務的註冊中心使用

zk節點結構

模仿dubbo

  • 根路徑:/nicerpc
  • 服務路徑在根路徑下,根據不同的服務名命名:/nicerpc/serviceName,比如/nicerpc/com.w.service.api.UserService,一個服務下有providers,consumers,routers,configurations節點
  • 服務提供者都在服務路徑下的providers裏:/nicerpc/serviceName/providers/providerHost+"#" +providerPort,比如/nicerpc/com.w.service.api.UserService/192.168.12.12#6666;後期框架優化,需要往節點的數據域附帶這些host,port等信息。
  • consumer類似provider
  • 某一個服務的配置信息在/nicerpc/serviceName/configurations下,暫時並未用到
  • 路由信息在/nicerpc/serviceName/routers下,暫時並未用到

step1~6是啓動順序

  • step1,provider的服務暴露

    1. provider啓動netty的ServerBootstrap,進入nio的select監聽狀態
    2. provider掃描指定包,基於Spring的BeanPostProcessor找到標註了@Remote註解的Bean
    3. 根據這些Bean實現的接口的名字——ServiceName——去zk路徑/nicerpc/serviceName/providers下注冊自己爲臨時帶序列號的節點,如果沒有這些節點需要進行初始化建立這些節點;
    4. 同時將這些服務名與其具體實現的映射記錄在concurrentHashMap裏
  • step2,provider在zk註冊完畢後,二者之間就有一條長連接,用於zk監視該機器狀態

  • step3,consumer的服務發現

    1. consumer啓動netty的Bootstrap,進入nio的select監聽狀態

    2. consumer掃描指定包,基於Spring的BeanPostProcessor找到標註了@RemoteInvoke的域,給這些域通過Field.set()方法設置一個基於SpringCGLib的代理類,在該代理類的intercept方法裏,我們將會去使用netty進行一次rpc調用

    3. consumer通過ServerManager管理與Provider的連接,而在ServerManager內部,通過維護三個ConcurrentHashMap來管理連接

      • realServerPathMap:保存serviceName與當前主機列表的映射
        ​ key:com.nicerpc.nicerpc_demo.api.UserService

        ​ value:set{“192.168.1.2#8081”,“192.168.11.68#8081”}

      • hostAndPortManagerMap:保存serviceName與當前使用的provider所在的主機+端口的映射

        ​ key:com.nicerpc.nicerpc_demo.api.UserService

        ​ value:host#port

      • connectionManagerMap:保存與某臺主機上的一個進程(這個進程可能有多個服務)的一條鏈接(連接緩存)

        ​ key:host#port
        value:ChannelFuture實例

    4. 如果是第一次連接,會去將自己註冊到zk的consumers下,並進行服務發現,即向zk拉取相應的serviceName/providers下的所有子節點,根據節點信息更新三個Map,並根據realServerPathMap裏的內容進行負載均衡選舉,使用選舉結果進行異步連接,並更新三個Map。

  • step4,consumer在zk註冊完畢後,二者之間就有一條長連接,用於zk監視該機器狀態

  • step5,consumer在拿到了目標provider的地址後,發起rpc調用。

    1. 通過初始化時,註冊的動態代理類的intercept方法實現,在該方法內,封裝一個在consumer-provider裏傳遞的媒介——ClientRequest,他封裝了一個請求。
    2. 將clientRequest通過JSON格式化後,編碼後發送給provider
    3. 通過Condition等併發工具類,異步獲取服務端返回結果,並進行解碼,返回給業務代碼
  • step6,provider對consumer的rpc調用的響應

    1. 接收到新消息後,經過解碼,將Json字符串重新格式化爲ClientRequest對象
    2. 拿到請求的serviceName,methodName等,從緩存beanMethodMap中取出bean和method,執行method的invoke方法,將返回值封裝進Response內,經過JSON格式化,編碼,再異步的發送回consumer

實現細節

對於“當server的狀態沒有還沒有同步到consumer的服務器列表裏”這種情況該如何獲取連接

一般情況下,因爲server的狀態通過Zookeeper不能同步的更新到consumer本地的服務器列表(realServerSet)內,所以,當一臺甚至幾臺server同時宕掉而consumer不知道這些server宕掉並且去嘗試去調用的時候,可能會造成不應該的消息發送失敗。

對於這種情況,我的思考過程如下:

思考第一階段

當本地對於該主機沒有可以用的future緩存的時候,該怎麼辦?先不考慮真實的server狀態是否更新到providers本地緩存表裏,也不考慮基於netty的connect操作默認是異步的

我覺得如果沒有的話,不管是future == null(的確沒連接過)的情況,還是future的channel是isNotActive(可能是provider關閉)的情況,都需要重新建立一個新的連接,原因如下所示:
經過思考,我覺得應該是重新使用負載均衡策略進行選舉,因爲負載均衡選舉是根據最新的providers本地緩存表進行的(至少當時是直接使用providers本地緩存表進行選舉的,後面改成了接受一個Set<String>形參,根據這個形參來進行選舉),所以他可以保證選舉出來的provider都是可用的,假如重連當前的provider的話,如果該provider所在的主機進程關閉,雖然providers本地緩存表會更新,但我們在這裏重連還是會失敗,所以應該以providers本地緩存表爲準,應該直接重新選舉,然後根據選舉結果進行重新連接,如果連接成功的話就返回,如果連接失敗的話就再次遞歸調用本方法。

思考第二階段

現在考慮真實的server狀態不能同步的更新到consumer的providers本地緩存表裏,而且同一臺機器被負載均衡選中多次,並且該機器已經下線,該怎麼辦?
因爲providers本地緩存表並不是跟真實的服務器情況同步更新的,所以在真實的服務器情況同步的這段時間內,如果負載均衡每次負載的都是同一臺機器(根據負載均衡策略不同是可能發生這種情況的),而這臺機器如果是已經下線的(但它的下線狀態還沒有被更新到providers本地緩存表中),在這段時間內,程序可能會一直遞歸下去!

所以我們需要在負載均衡前,嘗試重連接一次,如果重連接成功的話就返回,如果失敗的話,從providers本地緩存表中remove掉,然後進行負載均衡,這樣的話,如果所有機器都宕掉,我們也會一臺一臺的將所有機器刪除掉,從而最後在負載均衡的時候發現providers本地緩存表中爲空,會拋出異常,終止,就不會無限的遞歸下去了。

思考第三階段

考慮基於netty的connect操作默認是異步的

因爲netty的connect操作默認是異步的,所以沒辦法同步的獲取到連接是否成功的結果。當然,future支持sync,await等操作(這裏,使用netty時,需要注意IO超時,與await超時的區別),但是這些都需要阻塞,都會付出不小的代價,會影響框架的性能。但是,如果不使用這些函數,我們就沒辦法達到同步獲取連接結果的需求,也就沒辦法通過連接結果來決定是否在providers本地緩存表remove掉當前集羣選舉出來的這個provider,就還是可能出現思考階段二出現的問題。

所以,我想到了針對這種想到了名爲fastGetConnection的解決方案,中和了以上性能與程序無限遞歸之間的矛盾。方案如下:
在發現當前主機沒有可用的future緩存的時候,首先關閉該future,然後構造一個從providers本地緩存表的副本,從該副本中剔除當前provider,使用這個副本進行fastGetConnection(Set<String> serverSet)操作。

fastGetConnection(Set<String> serverSet):
每次都使用形參serverSet進行負載均衡選舉,並使用選舉結果進行連接,並使用返回的future.await(long,TimeUnit)設置等待超時時間,當await阻塞完畢後(不管future是否連接完畢,阻塞完畢可能是超時,可能是連接完畢),檢查是否連接成功,如果連接成功則返回,否則在傳進來的serverSet中remove掉剛剛那個server,並遞歸調用fastGetConnection,這裏我們可以通過對超時時間進行調優,快速的獲取一個客戶端的連接了,當遇到網絡波動的時候,我們選取到的是一個連通最快的provider,而且serverSet.remove(server)這個操作是在副本上進行的,不會影響真實的server列表的更新。

也就是在providers本地緩存表的副本上,進行選舉,快速連接,根據連接結果在副本上remove或者返回連接,真實的providers情況不去管它,任其自由地更新,哪怕我們remove掉一個只是網絡比較慢的但還可以用的provider也不會影響到它在providers本地緩存表是否存在。

PS:增加一個補救措施

然後又經過思考,決定在加一個補救措施,就是normalGetConnection,如果通過fastGetConnection獲取不到(這種情況只會發生在把providers本地緩存表副本掏空後,負載均衡選舉發生異常的時候),會接着通過normalGetConnection獲取。normalGetConnection就僅僅是根據當前的providers本地緩存表列表做一個負載均衡選舉,然後根據選舉結果直接進行異步連接,不管連接結果是不是成功

存在的問題

await超時時間的設置多少合適,而且也不清楚使用await帶來的性能損耗。

以下是知識筆記

Netty

關於addListener方法

  • 經過測試,channelFuture.channel().xxx().addListener(listener)
    方法添加的監聽器只對單種IO事件的單次操作有效。
  • 比如,channelFuture.channel().connect().addListener()添加的監聽器只會在connect完成後調用,不會在…channel().writeAndFlush()完成後調用
  • 再比如,channelFuture.channel().writeAndFlush().addListener()添加的監聽器只會在本次writeAndFlush()完成後執行,假如接着執行了另一次writeAndFlush()操作,該監聽器不會再次觸發
  • ps:監聽器會在isDone()返回true後被立即調用

關於sync方法

  • sync會阻塞到當前IO操作直到其isDone()返回true。
  • isDone()代表着完成了,結果可能是成功,失敗,被中斷等。
  • 該IO操作是否成功還是得看isSuccess()方法。

關於await()方法

  • await(),await(long,TimeUnit),awaitUnInterrupt等方法,底層調用了Object的wait方法,他會阻塞到isDone返回true,上面說到了,isDone返回true有多種結果,不一定成功。
  • 當設置了超時時間時,只要到達了超時時間,就不管isDone是否返回true了,會直接返回。

關於bootstrap的connect方法

  • 經測試,使用同一個bootstrap去連接同一個主機上的同一個進程(ip、port)都相同,返回的是不同的兩個future(也就是兩個連接),二者可以併發的讀寫數據。

dubbo

  • 自動發現: 基於註冊中心目錄服務,使服務消費方能動態的查找服務提供方,使地址透明,使服務提供方可以平滑增加或減少機器。
  • 集羣容錯: 提供基於接口方法的透明遠程過程調用,包括多協議支持,以及軟負載均衡,失敗容錯,地址路由,動態配置等集羣支持。
  • 遠程通訊: 提供對多種基於長連接的NIO框架抽象封裝,包括多種線程模型,序列化,以及“請求-響應”模式的信息交換方式。

nicerpc實現的功能

  • 客戶端超時,超時的鏈接自動關閉
  • 分離業務模塊
  • 增加zk模塊,從zk獲取服務器列表
  • 客戶端動態管理連接
  • Netty實現RPC服務器
  • 定義自己的簡單通信協議
  • Client與RPC服務器使用長連接進行異步通信
  • 客戶端動態代理使用SpringCGLib,BeanPostProcessor接口
  • 服務器註冊到Zookeeper,客戶端通過Zookeeper監聽服務器狀態
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章