Thrift 個人實戰--Thrift 服務化 Client的改造


 

前言:
  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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM