由浅入深了解Thrift之服务模型和序列化机制

一、Thrift介绍                                                                                     

    Thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎。其允许你定义一个简单的定义文件中的数据类型和服务接口。以作为输入文件,编译器生成代码用来方便地生成RPC客户端和服务器通信的无缝跨编程语言。

二、Thrift基础架构                                                                               

    Thrift是一个客户端和服务端的架构体系,数据通过socket传输;

具有自己内部定义的传输协议规范(TProtocol)和传输数据标准(TTransports);

通过IDL脚本对传输数据的数据结构(struct) 和传输数据的业务逻辑(service)根据不同的运行环境快速的构建相应的代码;

通过自己内部的序列化机制对传输的数据进行简化和压缩提高高并发、 大型系统中数据交互的性能。

  • Thrift 支持的数据类型

基本类型:
  bool: 布尔值
  byte: 8位有符号整数
  i16: 16位有符号整数
  i32: 32位有符号整数
  i64: 64位有符号整数
  double: 64位浮点数
  string: UTF-8编码的字符串
  binary: 二进制串
结构体类型:
  struct: 定义的结构体对象
容器类型:
  list: 有序元素列表
  set: 无序无重复元素集合
  map: 有序的key/value集合
异常类型:
  exception: 异常类型
服务类型:
  service: 具体对应服务的类

  • 协议

     Thrift可以让你选择客户端与服务端之间传输通信协议的类别,在传输协议上总体上划分为文本(text)和二进制(binary)传输协议, 为节约带宽,提供传输效率,一般情况下使用二进制类型的传输协议为多数,但有时会还是会使用基于文本类型的协议,这需要根据项目/产品中的实际需求:
    1、TBinaryProtocol – 二进制编码格式进行数据传输。
    2、TCompactProtocol – 这种协议非常有效的,使用Variable-Length Quantity (VLQ) 编码对数据进行压缩。
    3、TJSONProtocol – 使用JSON的数据编码协议进行数据传输。
    4、TSimpleJSONProtocol – 这种节约只提供JSON只写的协议,适用于通过脚本语言解析
    5、TDebugProtocol – 在开发的过程中帮助开发人员调试用的,以文本的形式展现方便阅读。

  • 传输层

    1、TSocket- 使用堵塞式I/O进行传输,也是最常见的模式。
    2、TFramedTransport- 使用非阻塞方式,按块的大小,进行传输,类似于Java中的NIO。
    3、TFileTransport- 顾名思义按照文件的方式进程传输,虽然这种方式不提供Java的实现,但是实现起来非常简单。
    4、TMemoryTransport- 使用内存I/O,就好比Java中的ByteArrayOutputStream实现。

    5、TZlibTransport- 使用执行zlib压缩,不提供Java的实现。

    

三、Thrift网络服务模型                                                                              

   Thrif 提供网络模型:单线程、多线程、事件驱动。从另一个角度划分为:阻塞服务模型、非阻塞服务模型。

  • 阻塞服务

       TSimpleServer

       TThreadPoolServer

  • 非阻塞服务模型

      TNonblockingServer

      THsHaServer

      TThreadedSelectorServer

     1、TSimpleServer

      TSimpleServer实现是非常的简单,循环监听新请求的到来并完成对请求的处理,是个单线程阻塞模型。由于是一次只能接收和处理一个socket连接,效率比较低,在实际开发过程中很少用到它。

     2、TThreadPoolServer

       ThreadPoolServer为解决了TSimpleServer不支持并发和多连接的问题, 引入了线程池。但仍然是多线程阻塞模式即实现的模型是One Thread Per Connection。

       线程池采用能线程数可伸缩的模式,线程池中的队列采用同步队列(SynchronousQueue)。

       ThreadPoolServer拆分了监听线程(accept)和处理客户端连接的工作线程(worker), 监听线程每接到一个客户端, 就投给线程池去处理。

import org.apache.thrift.TException;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.transport.TServerSocket;

/**
 * 注册服务端
 *     线程池服务模型,使用标准的阻塞式IO,预先创建一组线程处理请求
 */
public class ThriftTThreadPoolServer {
    // 注册端口
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws TException {
        TProcessor tprocessor = new HelloWorld.Processor<HelloWorld.Iface>(new HelloWorldImpl());
        // 阻塞IO
        TServerSocket serverTransport = new TServerSocket(SERVER_PORT);
        // 多线程服务模型
        TThreadPoolServer.Args tArgs = new TThreadPoolServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        // 客户端协议要一致
        tArgs.protocolFactory(new TBinaryProtocol.Factory());
         // 线程池服务模型,使用标准的阻塞式IO,预先创建一组线程处理请求。
        TServer server = new TThreadPoolServer(tArgs);
        System.out.println("Hello TThreadPoolServer....");
        server.serve(); // 启动服务
    }
}
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

/**
 * 客户端调用
 * 阻塞
 */
public class BlockClient {
    public static final String SERVER_IP = "127.0.0.1";
    public static final int SERVER_PORT = 8080;
    public static final int TIMEOUT = 30000;

    public static void main(String[] args) throws TException {
        // 设置传输通道
        TTransport transport = new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT);
        // 协议要和服务端一致
        // 使用二进制协议 
        TProtocol protocol = new TBinaryProtocol(transport);
        // 创建Client
        HelloWorld.Client client = new HelloWorld.Client(protocol);
        transport.open();
        String result = client.sayHello("thrift");
        System.out.println("result : " + result);
        // 关闭资源
        transport.close();
    }
}

 

    TThreadPoolServer模式优点:

       线程池模式中,数据读取和业务处理都交由线程池完成,主线程只负责监听新连接,因此在并发量较大时新连接也能够被及时接受。线程池模式比较适合服务器端能预知最多有多少个客户端并发的情况,这时每个请求都能被业务线程池及时处理,性能也非常高。

    TThreadPoolServer模式缺点:

       线程池模式的处理能力受限于线程池的工作能力,当并发请求数大于线程池中的线程数时,新请求也只能排队等待。

    3、TNonblockingServer

       TNonblockingServer采用单线程非阻塞(NIO)的模式, 借助Channel/Selector机制, 采用IO事件模型来处理。所有的socket都被注册到selector中,在一个线程中通过seletor循环监控所有的socket,每次selector结束时,处理所有的处于就绪状态的socket,对于有数据到来的socket进行数据读取操作,对于有数据发送的socket则进行数据发送,对于监听socket则产生一个新业务socket并将其注册到selector中。

private void select() {
  try {
    selector.select();  // wait for io events.
    // process the io events we received
    Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
    while (!stopped_ && selectedKeys.hasNext()) {
      SelectionKey key = selectedKeys.next();
      selectedKeys.remove();
      if (key.isAcceptable()) {
        handleAccept(); // deal with accept
      } else if (key.isReadable()) {
        handleRead(key);    // deal with reads
      } else if (key.isWritable()) {
        handleWrite(key); // deal with writes
      }
    }
  } catch (IOException e) {
  }
}

  select代码里对accept/read/write等IO事件进行监控和处理, 唯一可惜的这个单线程处理. 当遇到handler里有阻塞的操作时, 会导致整个服务被阻塞住。

import org.apache.thrift.TException;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.server.TNonblockingServer;
import org.apache.thrift.server.TServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TNonblockingServerSocket;

/**
 * 注册服务端
 *     使用非阻塞式IO,服务端和客户端需要指定 TFramedTransport 数据传输的方式  
 */
public class ThriftTNonblockingServer {
    // 注册端口
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws TException {
        // 处理器
        TProcessor tprocessor = new HelloWorld.Processor<HelloWorld.Iface>(new HelloWorldImpl());
        // 传输通道 - 非阻塞方式  
        TNonblockingServerSocket serverTransport = new TNonblockingServerSocket(SERVER_PORT);
        // 异步IO,需要使用TFramedTransport,它将分块缓存读取。  
        TNonblockingServer.Args tArgs = new TNonblockingServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.transportFactory(new TFramedTransport.Factory());
        // 使用高密度二进制协议 
        tArgs.protocolFactory(new TCompactProtocol.Factory());
        // 使用非阻塞式IO,服务端和客户端需要指定TFramedTransport数据传输的方式
        TServer server = new TNonblockingServer(tArgs);
        System.out.println("Hello TNonblockingServer....");
        server.serve(); // 启动服务
    }
}
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;

/**
 * 客户端调用
 * 非阻塞
 */
public class NonblockingClient {
    public static final String SERVER_IP = "127.0.0.1";
    public static final int SERVER_PORT = 8080;
    public static final int TIMEOUT = 30000;

    public static void main(String[] args) throws TException {
        // 设置传输通道,对于非阻塞服务,需要使用TFramedTransport,它将数据分块发送  
        TTransport transport = new TFramedTransport(new TSocket(SERVER_IP, SERVER_PORT, TIMEOUT));
        // 协议要和服务端一致
        //HelloTNonblockingServer
        // 使用高密度二进制协议 
        TProtocol protocol = new TCompactProtocol(transport);
        
        // 使用二进制协议 
        //TProtocol protocol = new TBinaryProtocol(transport);
        HelloWorld.Client client = new HelloWorld.Client(protocol);
        transport.open();
        String result = client.sayHello("jack");
        System.out.println("result : " + result);
        // 关闭资源
        transport.close();
    }
}

      TNonblockingServer模式优点:

        相比于TSimpleServer效率提升主要体现在IO多路复用上,TNonblockingServer采用非阻塞IO,同时监控多个socket的状态变化;

      TNonblockingServer模式缺点:

        TNonblockingServer模式在业务处理上还是采用单线程顺序来完成,在业务处理比较复杂、耗时的时候,例如某些接口函数需要读取数据库执行时间较长,此时该模式效率也不高,因为多个调用请求任务依然是顺序一个接一个执行。

   4、THsHaServer

     THsHaServer类是TNonblockingServer类的子类,为解决TNonblockingServer的缺点, THsHa引入了线程池去处理, 其模型把读写任务放到线程池去处理即多线程非阻塞模式。HsHa是: Half-sync/Half-async的处理模式, Half-aysnc是在处理IO事件上(accept/read/write io), Half-sync用于handler对rpc的同步处理上。因此可以认为THsHaServer半同步半异步。

     THsHaServer的优点:

      与TNonblockingServer模式相比,THsHaServer在完成数据读取之后,将业务处理过程交由一个线程池来完成,主线程直接返回进行下一次循环操作,效率大大提升;

    THsHaServer的缺点:

     主线程需要完成对所有socket的监听以及数据读写的工作,当并发请求数较大时,且发送数据量较多时,监听socket上新连接请求不能被及时接受。

   5、TThreadedSelectorServer

     TThreadedSelectorServer是大家广泛采用的服务模型,其多线程服务器端使用非堵塞式I/O模型,是对TNonblockingServer的扩充, 其分离了Accept和Read/Write的Selector线程, 同时引入Worker工作线程池。

    (1)一个AcceptThread线程对象,专门用于处理监听socket上的新连接;

    (2)若干个SelectorThread对象专门用于处理业务socket的网络I/O操作,所有网络数据的读写均是有这些线程来完成;

    (3)一个负载均衡器SelectorThreadLoadBalancer对象,主要用于AcceptThread线程接收到一个新socket连接请求时,决定将这个新连接请求分配给哪个SelectorThread线程。

    (4)一个ExecutorService类型的工作线程池,在SelectorThread线程中,监听到有业务socket中有调用请求过来,则将请求读取之后,交个ExecutorService线程池中的线程完成此次调用的具体执行

                                                                    

    MainReactor就是Accept线程, 用于监听客户端连接, SubReactor采用IO事件线程(多个), 主要负责对所有客户端的IO读写事件进行处理. 而Worker工作线程主要用于处理每个rpc请求的handler回调处理(这部分是同步的)。因此其也是Half-Sync/Half-Async(半异步-半同步)的 。

    TThreadedSelectorServer模式对于大部分应用场景性能都不会差,因为其有一个专门的线程AcceptThread用于处理新连接请求,因此能够及时响应大量并发连接请求;另外它将网络I/O操作分散到多个SelectorThread线程中来完成,因此能够快速对网络I/O进行读写操作,能够很好地应对网络I/O较多的情况。

import org.apache.thrift.TException;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadedSelectorServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TNonblockingServerSocket;

/**
 * TFramedTransport 数据传输的方式
 */
public class ThriftTThreadedSelectorServer {
    // 注册端口
    public static final int SERVER_PORT = 8080;

    public static void main(String[] args) throws TException {        
        TProcessor tprocessor = new HelloWorld.Processor<HelloWorld.Iface>(new HelloWorldImpl());
        
        TThreadedSelectorServer.Args tArgs = new TThreadedSelectorServer.Args(serverTransport);
        tArgs.processor(tprocessor);
        tArgs.transportFactory(new TFramedTransport.Factory());
        // 二进制协议
        tArgs.protocolFactory(new TBinaryProtocol.Factory());
        
        TServer server = new TThreadedSelectorServer(tArgs);
        System.out.println("Hello TThreadedSelectorServer....");
        server.serve(); // 启动服务
        
    }

}

 

四、Thrift序列化机制                                                                           

   Thrift提供了可扩展序列化机制, 不但兼容性好而且压缩率高。

   thrift 数据格式描述

   thrift的向后兼容性(Version)借助属性标识(数字编号id + 属性类型type)来实现, 可以理解为在序列化后(属性数据存储由 field_name:field_value => id+type:field_value)

 我们定义IDL文件形如

namespace java stu.thrift;
 
struct User {
  1: required string name
  2: required string address
}

是不是和我们使用序列化的数据xml/json有了很大的差别,那么我们来比较是常见的数据传输格式

数据传输格式 类型 优点 缺点
Xml 文本

1、良好的可读性

2、序列化的数据包含完整的结构

3、调整不同属性的顺序对序列化/反序列化不影响

1、数据传输量大

2、不支持二进制数据类型

Json 文本

1、良好的可读性

2、调整不同属性的顺序对序列化/反序列化不影响

1、丢弃了类型信息, 比如"price":100, 对price类型是int/double解析有二义性

2、不支持二进制数据类型

Thrift 二进制 高效

1、不宜读

2、向后兼容有一定的约定限制,采用id递增的方式标识并以optional修饰来添加

Google Protobuf 二进制 高效

1、不宜读

2、向后兼容有一定的约定限制

 

 

 由于本人经验有限,文章中难免会有错误,请浏览文章的您指正或有不同的观点共同探讨!

 

posted @ 2015-11-07 15:31  三石雨  阅读(9606)  评论(1编辑  收藏  举报