一篇關於motan的很不錯的文章

從零開發一款RPC框架,說難也難說簡單也簡單。難的是你的設計將如何面對實際中的複雜應用場景;簡單的是其思想可以僅僅濃縮成一行方法調用。motan是今年(2016年)新浪微博開源的一款RPC框架,據說在新浪微博正支撐着千億次調用。對於她的詳細介紹和讚譽是新聞稿和README的責任,本文則會從RPC的基本原理入手,分析和講解motan的各層的實現思想。

爲了避免落入俗套,本文將盡力避免對各種概念的解釋(這是wiki的責任),並默認讀者已經有了相關RPC框架的使用經驗(比如dubbo)。爲了使得文章充滿’乾貨’,本文重點講解的是motan的實現原理,並配有一定量的代碼予以解釋。這會使讀者不但知曉RPC框架的設計思路,也能知道相關生產代碼究竟是如何編寫的(在這個浮躁的年代,只有算法和代碼是最乾的貨)。

本文先簡單解釋一下RPC的本質,然後直接給出motan的整體架構,並解釋每層的具體含義。然後我們將從一段XML的配置入手,詳細講解provider與consumer的初始化流程以及調用和服務流程。最後挑選幾個比較重要的機制進行講解。

在開始之前,讓我們思考以下幾個問題:

  • 如何與Spring做集成?
  • SPI機制的實現原理是什麼?爲什麼要用這個?
  • 這麼多的配置,究竟是如何在類間傳遞和保存的?
  • 服務降級是如何做的?
  • motan協議究竟是什麼樣子的?
  • 服務發現和註冊究竟是怎麼做的?或者說客戶端是如何找到服務端的?
  • 爲什麼要用動態代理,到底proxy了什麼?
  • cluster的負載均衡和ha究竟是如何實現的?
  • transport層是怎麼進行抽象的?
  • client對server的心跳檢測是怎麼實現的呢?
  • client端的連接池是如何管理的?
  • server端的連接是如何共享的?

概述

對RPC本質的理解

對於軟件工程師來講,形如object.method()的方法調用實在是太過熟悉,當我們在同一個JVM進程內執行方法調用的時候,一切都顯得順其自然。然而如果我們將上述的調用過程拆分成兩個部分—-方法的調用端和方法的實現端,然後將他們分別放置到不同的進程中,使得調用端和實現端能夠做到跨操作系統,跨網絡。這便是RPC的本質。

從功能角度來講,RPC框架可以分爲服務治理型多語言型。motan顯然屬於前者,因此對motan框架可以簡單的理解爲:分離方法的調用和實現,並具雙端服務治理功能

motan的整體架構

分層是軟件設計的基礎工具,幾乎所有軟件都有分層的影子,motan也不例外,見下圖:

從上圖可以看到,motan的架構層次非常清晰(有點類似dubbo,關於區別請參考後記),從上到下分爲6層,其中右側部分爲通用組件服務。跟其他所有RPC一樣,motan框架也有兩個核心脈絡,其一是客戶端(對應圖左側垂直箭頭),和服務端(對應於右側箭頭),服務端不需要進行動態代理,理由很簡單,因爲服務端已經有了實現,不需要在進行代理。最下面的endpoint(client)到endpoint(server)就是TCP全雙工鏈接。

先來說公共組件部分:

  • SPI:Service Provider Interface,主要通過ExtensionLoader提供擴展點功能,用來動態裝載接口具體實現,以提供客戶端擴展能力。
  • Logger:使用slf4j,提供整個框架統一的日誌記錄工具。
  • Statistic:使用定時回調方式,收集和記錄框架監控信息。
  • URL:非Jdk裏的URL。他對協議,路徑,參數的統一抽象,系統也是使用URL來保存和讀取配置信息的。
  • Switcher:提供開關控制服務,能夠控制框架關鍵路徑的升級和降級。
  • Exception:統一異常處理,分爲業務異常,框架異常,服務異常等。

再來說每一個層的作用:

  • Config層:主要提供了配置讀取,解析,實體生成。同時他也是整個框架的入口,
  • Proxy層:服務端無proxy,客戶端具有代理功能,他通過InvocationHandler來攔截方法調用。目前只使用了jdk原生動態代理工具。
  • Registry層:用來進行服務發現和註冊,server端進行服務的註冊,client進行服務發現。目前有zk,consul的實現,還有對directUrl(就是p2p,不借助中心)的特殊處理。
  • Cluster層:集羣功能,他提供了集羣的服務暴露、服務引用、負載均衡,HA等。
  • Protocol層:協議層,目前只支持injvm和motan協議,主要就是提供給下層協議編碼和解析,並且使用filter/pipe模式進行訪問日誌,最大併發量控制等功能。
  • Transport層:主要處理網絡連接,他主要抽象了Client和Server接口。使用netty框架。

從兩端看核心調用鏈

privder端

初始化過程

請看下面配置

<bean id="motanDemoServiceImpl" class="com.weibo.motan.demo.server.MotanDemoServiceImpl"/>

<motan:registry regProtocol="zookeeper"

name="registry"

address="127.0.0.1:2181"/>

<motan:protocol id="demoMotan" default="true" name="motan"/>

<motan:basicService export="demoMotan:8002"

group="motan-demo-rpc"

registry="registry"/>

<motan:service interface="com.weibo.motan.demo.service.MotanDemoService"

ref="motanDemoServiceImpl" basicService="serviceBasicConfig">

</motan:service>

如果有dubbo的使用經驗,上述配置非常容易看懂。簡單的說,上述代碼使用motan協議,在8002端口上暴露了motanDemoServiceImpl服務。任何client都可以連接到8002端口,並使用motan協議調用這個服務。這個服務提供的功能也很簡單

public class MotanDemoServiceImpl implements MotanDemoService {

public String hello(String name) {

System.out.println(name);

return "Hello " + name + "!";

}

}

這,看似簡單,但,究竟都發生了什麼?

配置解析

所謂配置解析實際上就是完成一種映射功能,即xml->java的過程。那麼做到xml到對象的映射呢?如果是普通的xml文件,我們完全可以使用JAXB;如果是spring,它提供了自定義解析機制,這裏可以參考相關Spring文檔,大概分爲三步:

  1. 定義自己的xsd文件。
  2. 在META-INF中分別方式spring.handlers文件和spring.schemas文件,一個是具體的解析器的配置,一個是motan.xsd的具體路徑。
  3. 繼承NamespaceHandlerSupport抽象類或實現NamespaceHandler接口完成具體解析工作。

我們這裏不需要關心解析過程,因爲他實在乏陳。只需要知道這些配置將會映射成ServiceConfig對象(對應<motan:service>標籤)。需要注意的是,每個不同的service標籤都會生成不同的ServiceConfig對象,即所謂的prototype模式,相反,有些配置使用的是單例,比如ProcotolConfig。

服務export過程

放在Spring容器中的bean一定會經過一個初始化過程,ServiceConfig對象也不例外,他會監聽應用上下文事件,並當整個容器初始化完畢後,調用export()方法進行服務暴露。export則主要做了一下兩件事:

  1. 將我們第一步解析生成的配置實體類轉換成URL類(通過uri形式來表示配置)
  2. 代理給ConfigHandler,並生成Exporter對象

URL對象是整個框架的核心對象,他保存了一系列配置,分爲註冊URL和服務URL,註冊URL是指到Registry服務的地址,服務URL則是具體使用的服務串。

比如在這裏生成的url對象是註冊URL:

zookeeper://127.0.0.1:2181/com.weibo.api.motan.registry.RegistryService?group=default_rpc

他表示了註冊中心的地址,path則是與zookeeper的樹形結構相對應。URL中還包含一個parameters的HashMap,他保存了<registry>的配置,以及一個叫做embed的屬性,embed則是保存了服務URL(注意與註冊URL區別)

embed=motan://192.168.122.1:8001/com.weibo.motan.demo.service.MotanDemoService?maxContentLength=1048576&module=motan-demo-rpc&export=demoMotan:8001&maxServerConnection=80000&group=motan-demo-rpc …..

有了URL配置後,我們將暴露過程代理給ConfigHandler類。這個類是一個輔助類,我們也可以成它爲膠水代碼,連接了配置層和協議層。


public <T> Exporter<T> export(Class<T> interfaceClass, T ref, List<URL> registryUrls) {

.....

// 查找協議

Protocol protocol = new ProtocolFilterDecorator(ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(protocolName));

//創建provider ref是具體實現類,serviceUrl就是上文的服務url,interfaceClass就是服務接口

Provider<T> provider = new DefaultProvider<T>(ref, serviceUrl, interfaceClass);

//暴露過程代理給具體協議,Exporter是一次暴露的核心接口

Exporter<T> exporter = protocol.export(provider, serviceUrl);

//註冊服務,暴露之後在註冊到zk上

register(registryUrls, serviceUrl);

return exporter;

}

可以看到上述過程是先進行服務暴露,然後再註冊到中心,通知refer端。原因很明顯,如果先通知上線,這個時候服務並沒有暴露完畢,refer端實際上是無法調用的。另外,暴露過程又代理給具體的協議實現,這裏使用motan協議,即DefaultRpcProtocol對象(個人覺得應該叫做MotanProtocol),他實際上又代理給了內部類DefaultRpcExporter。在DefaultRpcExporter的類初始化中,根據當前系統的SPI配置,找到EndpointFactory的實現類,並創建一個Server對象,由Server對象的server.open()最終完成transport的網絡層暴露。

transport層服務暴露

motan只實現了基於netty的transport層(可以理解,大部分場景netty都可以解決),在server.open()方法中,只是僅僅根據上下文傳遞過來的參數對bootstrap進行配置,然後bind端口,完成對指定端口的監聽。在對bootstrap初始化的時候有兩點可能值得注意。一是我們需要知道他都注入了哪些pipeline


pipeline.addLast("channel_manage", channelManage);

pipeline.addLast("decoder", new NettyDecoder(codec, NettyServer.this, maxContentLength));

pipeline.addLast("encoder", new NettyEncoder(codec, NettyServer.this));

pipeline.addLast("handler", handler);

可以看到,decoder/encoder用來解碼,編碼具體協議;chanelMessage是用來進行channel併發連接數限制;handler則是具體的channel處理器,用於處理心跳包,客戶端業務請求和消息路由。

二是在實際開發中,經常是多個provider暴露在相同的端口號下,那麼當一個request來的時候,具體會路由到哪一個provider類呢?這個路由機制是由ProviderMessageRouter實現的,它的實現思路也比較簡單:在其內部維護一個Map<String, Provider>,並通過group/interface/version進行key生成,從而對provider進行定位。因此我們雖然一個provider的實現類是一樣的,但是不同的組或相同組不同版本之間也是可以一起暴露服務的。

至此,我們的com.weibo.motan.demo.service.MotanDemoService服務已在8002端口暴露,關於服務的registry註冊實現機制請參考後文,registry機制

服務調用過程

當一個Request到達服務端後,是如何被調用執行的呢?我們知道ProviderMessageRouter可以對Request進行路由,當netty的channel收到這個實現Request之後,將會定位到具體Providershi,然後調用provider.call()執行服務。

try {

//直接調用call方法

return provider.call(request);

} catch (Exception e) {

//產生異常,將會封裝到Response中

Response response = new DefaultResponse();

response.setException(new MotanBizException("provider call process error", e));

return response;

}

DefaultProvider是Provider的默認實現,其中的defaultProvider.invoke()執行真正的調用:

public Response invoke(Request request) {

DefaultResponse response = new DefaultResponse();

Method method = lookup(request);

....

try {

//反射調用,proxyImpl就是spring上下文中具體實現的Service

Object value = method.invoke(proxyImpl, request.getArguments());

//設置返回值

response.setValue(value);

} catch (Exception e) {

...

} catch (Throwable t) {

...

}

// 傳遞rpc版本和attachment信息方便不同rpc版本的codec使用。

response.setRpcProtocolVersion(request.getRpcProtocolVersion());

response.setAttachments(request.getAttachments());

return response;

}

以上代碼的執行實際上是在bootstrap初始化的工作線程池中執行的,該線程池並沒有採用JCU中的線程池,而是使用了StandardThreadExecutor,這個線程池是tomcat所使用的線程池。jCU中的線程池當線程數超過core的時候會放到隊列,隊列滿了之後,在繼續提高到max,然後在執行reject策略,這個比較適合於CPU密集型任務。StandardThreadExecutor則當到達max的時候再放入隊列,隊列滿了之後再執行reject策略,這比較適合處理網絡請求。

到此,我們知道服務端的具體調用也僅僅是Method.invode()而已,並沒有什麼太大的祕密。 到目前爲止我們已經追蹤了服務端的兩個重要過程:服務暴露服務調用。如果不配合相關源碼的閱讀,可能會覺得有些枯燥,在這配一張說明圖片。

refer端

初始化過程

還是請先看下面的xml配置

<motan:registry regProtocol="zookeeper" name="registry" address="127.0.0.1:2181"/>

<motan:protocol default="true" name="motan"

haStrategy="failover"

loadbalance="roundrobin"/>

<motan:basicReferer group="motan-demo-rpc"

protocol="motan"

registry="registry"/>

<motan:referer id="motanDemoReferer"

interface="com.weibo.motan.demo.service.MotanDemoService"

basicReferer="motantestClientBasicConfig"/>

客戶端調用

MotanDemoService service = (MotanDemoService) ctx.getBean("motanDemoReferer");

System.out.println(service.hello("motan"));

很簡單,就是要調用遠端的MotanDemoService服務的hello方法。那麼具體實現過程是怎麼樣的呢?

首先與provider端一樣,配置解析也需要藉助spring框架,但稍有不同,refer端的配置是映射到RefererConfigBean中,他是一個FactoryBean,這意味着需要調用getObject()獲得真正的對象。爲什麼要使用FactoryBean而不是像provider端那樣直接生成對象呢?這是因爲我們refer端只能調用接口(沒有實現),接口是無法直接使用的,它需要被動態代理進行封裝,產生代理對象,再把代理對象放入spring容器。因此使用FactoryBean實際上是爲了方便創建代理對象。

getObject()中,RefererConfigBean會調用子類RefererConfig的initRef()方法,從而開啓了客戶端初始化過程。

cluster初始化

上文的initRef()中最重要的當屬Cluster的初始化。什麼是Cluster?他其實代表provider服務集羣,每一個具體的provider服務都被抽象成一個Referer接口,這個接口與客戶端的Exporter相對應。因此,Cluster的本質是Referer的容器,並且提供了負載均衡和HA服務(參考下文cluster集羣管理)。每一個Cluster都被ClusterSupport封裝起來,以提供對Cluster的刷新機制和生命週期管理。一個RefererConfig只會對應一個Cluster和一個ClusterSupport。

服務發現與刷新

當Cluster初始化完畢後,將會調用registry.subscribe(subUrl, this);完成服務訂閱。所謂的’訂閱’,不過是一個邏輯操作,主要是將客戶端節點寫到zk的/motan/group/xxxx.xxxx/client/ip/節點下。實際上的訂閱其實是通過watch機制實現的(詳見後文registry機制),當發生服務變更,將會通知客戶端刷新集羣。那麼當客戶端第一次啓動的時候,他是如何感知到服務端呢?在客戶端第一次執行subscribe的時候,將會調用discover(),然後調用NotifyListener(這裏使用的是監聽器模式),ClusterSupport實現了該接口,並通過registry回調notify(),從而能夠感知到zk中有哪些已經註冊的服務以及他們的真實地址。具體的通知過程如下代碼:

@Override

public synchronized void notify(URL registryUrl, List<URL> urls) {

....

// 通知都是全量通知,在設入新的referer後,cluster內部需要把不再使用的referer進行回收,避免資源泄漏

// 判斷urls中是否包含權重信息,並通知loadbalance。

processWeights(urls);

List<Referer<T>> newReferers = new ArrayList<Referer<T>>();

for (URL u : urls) {

if (!u.canServe(url)) {

continue;

}

Referer<T> referer = getExistingReferer(u, registryReferers.get(registryUrl));

if (referer == null) {

// careful u: serverURL, refererURL的配置會被serverURL的配置覆蓋

URL refererURL = u.createCopy();

mergeClientConfigs(refererURL);

referer = protocol.refer(interfaceClass, refererURL, u);

}

if (referer != null) {

newReferers.add(referer);

}

}

if (CollectionUtil.isEmpty(newReferers)) {

onRegistryEmpty(registryUrl);

return;

}

// 此處不銷燬referers,由cluster進行銷燬

registryReferers.put(registryUrl, newReferers);

refreshCluster();

}

通過上述代碼,我們能夠看到客戶端是如何處理zk通知的,它主要就是根據新的URL(從zk端獲得)創建Referer對象,並且刷新整個集羣。刷新操作主要將新的Referer加入集羣,並將舊的Referer對象釋放掉。需要注意,這裏並沒有直接釋放Referer資源,而是採用了延遲機制,主要考慮到Referer可能正在執行中,馬上銷燬會影響正常請求。默認延遲時間是1s,主要考慮1s中對業務執行來說時間已經夠長了。

動態代理

當Cluster準備完畢,zk中也訂閱了相關服務之後,剩下的工作就只需要對接口進行動態代理。motan中只提供了jdk原生的動態代理實現:

@SuppressWarnings("unchecked")

public <T> T getProxy(Class<T> clz, InvocationHandler invocationHandler) {

return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {clz}, invocationHandler);

}

不過我們能夠通過SPI機制提供自己的實現。我個人認爲搞那麼多不同的代理實現方案並沒有太大意義,實際工作中基本也只是使用一種而已。

調用過程

當客戶端初始化完畢之後,我們就能正常使用motan進行方法調用了。從上文我們知道,對接口的調用,實際上是被動態代理了,那麼動態代理的執行入口是哪裏呢?RefererInvocationHandler提供了這個入口。主要實現如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

//創建一個request,這個request中的參數將會被序列化

DefaultRequest request = new DefaultRequest();

//設置request參數

request.setRequestId(RequestIdGenerator.getRequestId());

request.setArguments(args);

request.setMethodName(method.getName());

request.setParamtersDesc(ReflectUtil.getMethodParamDesc(method));

request.setInterfaceName(clz.getName());

request.setAttachment(URLParamType.requestIdFromClient.getName(), String.valueOf(RequestIdGenerator.getRequestIdFromClient()));

// 當 referer配置多個protocol的時候,比如A,B,C,

// 那麼正常情況下只會使用A,如果A被開關降級,那麼就會使用B,B也被降級,那麼會使用C

for (Cluster<T> cluster : clusters) {

String protocolSwitcher = MotanConstants.PROTOCOL_SWITCHER_PREFIX + cluster.getUrl().getProtocol();

Switcher switcher = switcherService.getSwitcher(protocolSwitcher);

//降級開關

if (switcher != null && !switcher.isOn()) {

continue;

}

.....

try {

//真正的執行入口

response = cluster.call(request);

return response.getValue();

} catch (RuntimeException e) {

//異常處理

.....

}

}

.....

}

可以看到整個代理過程實際上是執行了以下幾個步驟:

  1. 創建和設置Request對象
  2. 代理給Cluster
  3. 處理異常

創建request對象沒有什麼好說的。Cluster.call()實際上是重頭戲,他在方法執行內部代理給了haStrategy.call(),然後ha策略使用LoadBalance選擇一個或一批Referer對象,並根據具體策略調用這個Referer的call()方法。Referer接口在motan協議的默認實現是DefaultRpcReferer,他在初始化的時候通過EndpointFactory創建了一個Client對象,他其實就是NettyClient,然後調用client.request(),從而使rpc請求順利進入了transport層。

在transport層,NettyClient通過使用commons-pool維護了一個長連接緩存,每次調用都通過連接池獲得Channel對象。調用之後,再放回緩存池。

Channel channel = null;

Response response = null;

try {

// 拿一個連接

channel = borrowObject();

....

// 執行異步調用

response = channel.request(request);

// 返回連接

returnObject(channel);

} catch (Exception e) {

.....

}

// 如果是異步調用直接返回response,如果不是就包裝到DefaultResponse,在

// getValue()的時候會進行阻塞

response = asyncResponse(response, async);

return response;

channel.request(request) 中,會將Request寫入到netty的channel中,ChannelFuture writeFuture = this.channel.write(request);,之後進行序列化,然後真正的執行socket write送到內核協議棧,長途漫漫後,到達服務端。

以上就是客戶端的初始化過程以及方法調用過程,爲了便於理解,可以參考下圖:

核心機制的實現

cluster集羣管理

我們上文說過,在客戶端方法調用鏈中,Cluster.call()用於保證高可用,並能夠進行軟負載均衡。上文說過,Cluster的本質是Referer對象的容器。見下圖:

HaStrategy策略接口調用負載均衡器,負載均衡器從衆多Referer中選擇一個或若干Referer,將他們交給HaStrategy,然後HaStrategy根據特定的策略進行Referer的調用。

motan支持的HaStrategy只有兩種:FailfastHatrategy和FailoverStrategy(dubbo有很多種實現比如failsafe,failback等),failfast很容易理解,如果出錯馬上拋出異常,快速失敗,實現代碼也很簡單:

@Override

public Response call(Request request, LoadBalance<T> loadBalance) {

Referer<T> refer = loadBalance.select(request);

return refer.call(request);

}

failover策略是目前最常用的,其實現思路其實也不難。我們通過負載均衡器選擇一組Referer(選擇算法根據不同LB有不同實現),然後只調用第一個,當出現錯誤的時候(非業務異常),我們嘗試調用n次(可配置),如果n次都失敗了,那麼我們就調用下一個Referer,如果這組Referer都調用失敗,則拋出異常。

關於loadbalance算法,motan支持以下幾種:

  • ActivieWegithLoadBalance:最少併發優先,怎麼計算併發量?很簡單,通過原子計數器即可。
  • ConfigurableWeightLoadBalance:可配權重,無多講,類似ng。
  • ConsistentHashLoadBanlance:一致性hash,網上很多現成的實現,可以使用md5配合TreeMap實現。這裏使用了request.arguments作爲hash入參。
  • LocalFirstLoadBalacne:本地服務優先。
  • RandomLoadBalance:使用math.random()實現。
  • RoundRoubinLoadBalance:使用原子計數器,進行id分配,在配合取模操作即可。注意避免負數,可以做0x7fffffff的mask。

協議編碼

一般RPC框架的協議編碼分爲兩個部分,其一是對協議體的序列化,其二是對協議頭的編碼格式。採用固定協議頭的好處是容易處理tcp的粘包和半包。motan的協議體默認使用的序列化協議是hession或json,同樣我們可以通過SPI進行擴展。那麼都有哪些會被序列化成協議體呢?爲了能夠在服務端路由到具體實現類,接口名+方法名+方法參數+方法參數描述以及附加信息(比如接口版本等)會被序列化到協議體當中。

針對協議頭,motan的實現也非常簡單。頭部一共16個字節:

協議位 意義
0-15 bit magic
16-23 bit version
24-28 bit extend flag
29-30 bit event 比如normal, exception等
31 bit 0 is request , 1 is response
32-95 bit request id
96-127 bit body content length

switcher機制

Switcher就是開關機制,他提供了一種降級控制能力。具體的實現主要有一下幾個類:

  1. Switcher,開關實體類,維護了開關狀態
  2. SwitcherService,Switcher的容器,提供註冊等管理服務
  3. SwitcherListener,針對Switcher的監聽器,用於監聽開關變化
  4. LocalSwitcherService,SwitcherService接口的具體實現
  5. MotanSwitcherUtil,提供給整個框架靜態方法,也是開關服務的入口

使用的時候,我們通過MotanSwitcherUtil註冊開關和監聽器,然後調用SwitcherService.setValue()設置開關狀態或者得到SwitcherService.getSwitcher()得到開關狀態。然後就可以在需要使用的地方,加上判斷即可。

SPI機制

類似dubbo,motan也使用了擴展點機制(SPI),以提供客戶端動態擴展能力。使用比較方便,只需要在/META-INF/下放置一個文本文件,文件名就是接口名比如(com.weibo.api.motan.transport.EndpointFactory),文件內容就是接口實現類全路徑比如:com.weibo.api.motan.transport.netty.NettyEndpointFactory。因此框架在需要打樁的地方調用

endpointFactory =

ExtensionLoader

.getExtensionLoader(EndpointFactory.class)

.getExtension(

url.getParameter(URLParamType.endpointFactory.getName(), URLParamType.endpointFactory.getValue())

);

即可得到被注入的實現類。真是怎麼實現的呢?其主要思路就是通過io操作讀取classpath下的相應文件(/META-INF/xxxx),並進行解析,反射得到接口的實現類,並調用newInstance()創建實例。如果他是單例,就將其緩存,如果不是單例則無需緩存直接返回。@SpiMeta打在接口上,用來標識其生命週期。@Spi打在實現類上,通過name屬性作爲唯一引用標識,即可打通xml的配置和具體的實現。

客戶端心跳檢測

客戶端心跳檢測就是定時檢測服務端是否可用。爲什麼要有客戶端心跳檢測?很明顯,對於客戶端來講,服務端是永遠不可控的,當出現某個服務端故障,客戶端無法調用的時候,除了將該Referer從負載均衡器中拿掉外,還要定時(500ms)發送心跳檢測包,以檢測服務端是否可用。當服務又變得可用的時候,我們再將其上線。如下代碼:

executorService.scheduleWithFixedDelay(new Runnable() {

@Override

public void run() {

for (Map.Entry<Client, HeartbeatFactory> entry : endpoints.entrySet()) {

Client endpoint = entry.getKey();

try {

// 如果節點是存活狀態,那麼沒必要走心跳

if (endpoint.isAvailable()) {

continue;

}

HeartbeatFactory factory = entry.getValue();

endpoint.heartbeat(factory.createRequest());

} catch (Exception e) {

LoggerUtil.error("HeartbeatEndpointManager send heartbeat Error: url=" + endpoint.getUrl().getUri(), e);

}

}

}

}, MotanConstants.HEARTBEAT_PERIOD, MotanConstants.HEARTBEAT_PERIOD, TimeUnit.MILLISECONDS);

Registry機制

Registry是Broker模式中最重要的角色之一,在RPC框架中他主要提供以下功能:

  • 服務發現,對應接口DiscoveryService,提供給客戶端,以查詢服務端真實地址。
  • 服務註冊,對應接口RegistryService,提供給服務端,以註冊真實地址。
  • 變更通知,對應接口NotifyListener,對上層進行變更通知,以刷新集羣。

motan中,Registry接口繼承了DiscoveryService和RegistryService,主要有以下實現類:

  • DirectRegistry:實際上就是client直接連接到server上,不走配置中心。
  • LocalRegistry:將服務註冊發現都保存在本地。
  • AbstractRegistry:提供模板方法,幫助子類解決日誌記錄,URL處理等。
  • FailbackRegistry:提供處理無法連接到中心的失敗重試功能。
  • CommandFailbackRegistry:提供命令功能,用於通過管理界面進行服務治理。
  • ZookeeperRegistry和ConsulRegistry,提供zk和consul的相應實現。

以zk爲例,當服務com.weibo.motan.demo.service.MotanDemoService被註冊的時候,將會在zk上生成如下樹形結構:

/motan/group/com.weibo.motan.demo.service.MotanDemoService/server/ip/

當客戶端發生訂閱的時候的樹形結構:

/motan/group/com.weibo.motan.demo.service.MotanDemoService/server/ip/

以上都是ZNODE的臨時節點,並且將配置信息存在ip節點中。當客戶端或服務端的session退出後,節點將會自動銷燬。對於客戶端還需要watch server目錄下的所有child ZNODE,當發生變更(比如下線、配置變更),則會調用NotifyListener進行集羣的刷新操作。

後記

至此我們就基本講完了motan的核心實現,實際上還有一些內容並沒有講解到。比如,motan是如何記錄訪問日誌的(filter)?統計信息是如何處理的(callback)?服務治理平臺都有哪些功能,是如何實現的(command)等等。這些功能相對比較容易,由於篇幅限制,就需要讀者自己閱讀相關源碼了。

很多人都會問,motan與其他RPC框架的區別是什麼?這裏的區別主要是指與dubbo的區別。我從頭到尾review了一遍motan的代碼,發現與dubbo最大的區別就是:motan的設計更加精簡,配置更加簡單。他幾乎就是一個剪裁版的dubbo,砍掉了大部分不常用的配置和特性,而簡單則意味着對二次開發更加友好。motan同時提供了一套簡單的服務治理平臺(雖然功能還很不完善),幾乎就是開箱即用。對於性能,我沒有親自跑過基準測試,所以這裏不敢做太多的評價。因此dubbo是大而全,motan是小而精。

關於SPI機制,個人認爲設計的並不是很優美,充滿着代碼的壞味道,現在大多數的開源項目都在使用guice(比如ES),或者阿里的cooma。因此這種SPI的設計方式並不是很贊同,不過對於不希望依賴其他三方框架的系統來說(比如JDK),倒也還成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章