前言:
Thrift作為Facebook開源的RPC框架, 通過IDL中間語言, 並借助代碼生成引擎生成各種主流語言的rpc框架服務端/客戶端代碼. 不過Thrift的實現, 簡單使用離實際生產環境還是有一定距離, 本系列將對Thrift作代碼解讀和框架擴充, 使得它更加貼近生產環境. 本文主要講解thrift的服務化改造, 這邊側重於闡述對client(服務調用方)的改造和設計思想.
基礎概念:
傳統對client的優化, 主要是Client Manager化, 優化方式包括引入連接池, 支持Failover/LoadBalance機制. 這部分內容可以參考flume sdk文章, 里面對client的優化, 細心到了極致.
PRC服務化, 對於client(服務調用方)而言, 應該隱藏client和server端的交互細節(包括failover/loadbalance), 唯一需要暴露/使用的是服務方提供的接口. 簡而言之, 通過service接口進行rpc服務, 而不是采用client的api去訪問.
用thrift api作為例子
// *) Client API 調用 (EchoService.Client)client.echo("hello lilei"); ---(1) // *) Service 接口 調用 (EchoService.Iface)service.echo("hello lilei"); ---(2)
評注: (1) Client API的方式, 不推薦, (2) Service接口的方式(服務化), 推薦
面向接口編程:
先來看下thrift生成的類有那些
namespace java mmxf.thrift service EchoSerivce { string echo(1: string msg); }
其生成的類有如下所示:
// *) Thrift生成的EchoService代碼, 省略了函數和具體實現 public class EchoSerivce { // *) 接口類Iface, 同步接口 public interface Iface {} // *) 接口類AsyncIface, 異步接口 public interface AsyncIface {} // *) 具體類, 同步Client public static class Client {} // *) 具體類, 異步Client public static class AsyncClient {} }
評注: EchoService.Iface就是同步EchoSerivce的接口定義, 而EchoService.Client則是與服務端交互的具體客戶端實例.
面向接口編程, 采用裝飾者模式(Decorator Pattern, 接口+組合), 借助實現EchoService.Iface接口, 握有EchoService.Client實例的方式去實現. 這樣能達到服務化的初步雛形, 但這遠遠不夠.
服務化的基本特征:
RPC Client服務化的基本特征(個人觀點), 可以分為如下:
1). 泛型化, 作為一個服務框架存在, 不而是只用於具體模塊
2). 內部封裝的client需要實現client-manager化, 即支持連接池/failover/loadbalance
3). 通過訂閱服務的方式, 透明的調用服務提供方(不需要知道服務提供方的server ip:port 列表)
本文主要闡述思路, 服務訂閱放在后續的文章, 弱化Client-Manager, 但支持泛型化來實現一個簡單的client service解決方案.
解決方案:
對泛型Thrift Service的支持, 采用JDK自帶的動態代理來實現.
public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args); }
評注: Object proxy: 指被代理的對象, Method: 要調用的方法, Object[] args: 方法調用時所需要的參數
public class DynamicClientProxy<T> implements InvocationHandler { // *) 引入類Class, 以及RpcServer配置 public Object createProxy(Class<T> ts, RpcServerConfiguration configuration) { // *) 靜態類Proxy生成動態代理實例 return Proxy.newProxyInstance(ts.getClassLoader(), ts.getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // *) 外循環是簡單的failover機制, 用於失敗時重試 for ( ServerNode serverNode : serverNodes ) { // *) 創建TSocket對象 TSocket tsocket = new TSocket(ip, port); tsocket.setTimeout(timeout); // *) 二進制協議 TProtocol protocol = new TBinaryProtocol(tsocket); // *) 借助反射(構造函數)來產生實例對象 Class[] argsClass = new Class[] { TProtocol.class }; Constructor<T> cons = (Constructor<T>) ts.getConstructor(argsClass); T client = (T)cons.newInstance(protocol); tsocket.open(); // *) 反射調用, 這個最重要 return method.invoke(client, args); } } }
評注: 上述代碼中省略了不少, 大致是如上代碼所述的思路, 具體的代碼詳見附件.
創建定義DynamicClientProxy<T> 泛型類, 用於動態代理對象的創建.
調用代碼如下所示:
RpcServerConfiguration configuration = new RpcServerConfiguration(); configuration.getServerNodes().add(new ServerNode("127.0.0.1", 9010)); DynamicClientProxy<EchoService.Client> proxy = new DynamicClientProxy<EchoService.Client>(); EchoService.Iface service = (EchoService.Iface) proxy.createProxy(EchoService.Client.class, configuration); service.echo("hello dynamic");
評注: 是不是簡潔了不少, 對泛型的支持也比較優雅.
繼續改進:
上述的泛型代碼雖然靈活了不少, 但需要硬編碼, 是否可以借助spring來實現配置優化呢?
首先我們引入DynamicClientProxyFactory類, 該類用於產生具體的代理類
public class DynamicClientProxyFactory { public static Object createIface(String clazzIfaceName, List<String> servers) { // *) 內部類的表, 不用'.', 而使用'$'分割 int idx = clazzIfaceName.lastIndexOf('$'); // *) 創建內部類Iface String clazzClientName = clazzIfaceName.substring(0, idx) + "$Client"; Class clientClazz = Class.forName(clazzClientName); // *) 創建代理對象 DynamicClientProxy proxy = new DynamicClientProxy(); return proxy.createProxy(clientClazz, configuration); } }
同時spring中, 我們采用如下的方式來配置service接口的bean, 這邊采用了Bean Factory的方式創建實例對象
<bean id="echoService" class="com.lighting.rpc.core.client.DynamicClientProxyFactory" factory-method="createIface"> <constructor-arg> <value>mmxf.thrift.EchoService$Iface</value> </constructor-arg> <constructor-arg> <list> <value>127.0.0.1:9000</value> <value>127.0.0.1:9001</value> </list> </constructor-arg> </bean>
評注: 具體的參數是服務類的接口, 以及server ip:port列表. 同時采用@Resource的方式來注冊這個service bean即可.
服務體驗:
編寫測試用例
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("application_context.xml"); EchoService.Iface service = (EchoService.Iface) applicationContext.getBean("echoService"); System.out.println(service.echo("lilei"));
評注: 是不是很簡單明了.
總結:
RPC服務化方便編程, 也隱藏了服務端/客戶端的交互細節. 另一個好處是方便測試, 使用stub, 模擬各種異常和交互. 當然使用Client也可以, 不過這需要借助bug級神奇Mockito. 總得來說RPC服務話, 對rpc服務調用方而言, 大大降低了開發門檻和難度.
當前的不足:
1). 沒有實現對象池, 若實現了ThriftClientObjectPool, 代碼的整體架構會顯得更加簡單.
2). 沒有使用訂閱服務列表, 使得在配置中, 需要指定ip:port列表.
后續:
后續會編寫發布/訂閱服務列表的實現方案, 這部分需要和服務端編寫一起講述, 並實現ThriftClient的對象池. 敬請期待.
代碼下載鏈接: http://download.csdn.net/download/mmstar/7699445