1. Thrift框架
Thrift是Facebook开源出来的通信服务框架,典型的C/S架构模式,支持跨语言编程,例如Java, C++,Python等主流语言,能够友好地解决各大系统的数据通信问题和多种语言运行环境不同所引起的信息交互问题。
Thrift采用一种IDL编码通信的方式,跟业界在以前通常采用的CORBA通信协议标准方式有点类似。它通过创建IDL文件,生成并编写相关代码文件,实现其相关的代码,编译装载即可使用。
Thrift通信框架的编码架构是一种层级的架构,主要是分5层:网络通信层(Socket),传输层(TTransport),协议层(TProtocol),和接口实现层(Interface),业务逻辑层(Business)。如下图:
Thrift实现了多种类型的TProtocol和TTransport。
1.1 TProtocol 协议
TProtocol是用来序列化数据结构写入到TTransport中,或者是把从TTransport中读出来二进制流反序列化成结构化的数据。它的实现有TBinaryProtocol, TCompactProtocol, TJSONProtocol,TSimpleJSONProtocol, TTupleProtocol等。
- TBinaryProtocol: 把数据结构转换成二进制流的方式,在该层没有进行相关数据的压缩。
- TCompactProtocol: 把数据结构换一定的规则进行压缩,减少二进制流的大小。主要从以下两个方面入手:1. 尽量使用变长的整数,2. 对于复杂的结构(例如嵌套,多属性,短字符串,集全,值比较小的整形或长整形。
1.2 TTransport 协议
TTransport封装了Socket通信的细节,提供了多种通信方式,例如TSocket, TFramedTransport, TFileTransport,TMemoryInputTransport, TZibTransport等。
- TSocket: 是一种典型的阻塞式I/O传输模式。在Java实现中,TSocket继承了TIOStreamTransport类,该类就管理着输入输出流。其中TServerSocket继承TServerTransport类,用于服务端,当客户端有请求是就会创建一个TSocket与客户端进行通信。因此这种很适合于流式传输的C/S通信方式,当然其它的TTransport也可以调用这种方式。
- TFramedTransport: 可以采用非阻塞的方式进行块大小的传输。
- TFileTransport:采用文件传输方式。
1.3 TServer 服务
Thrift也提供TServer类用于启动服务,其有三种实现方式,其中一种是TSimpleServer,TThreadedSelectorServer, THsHaServer等。
2. HBase Thrift服务
上面我们大概地介绍了一下Thrift框架的结构,下面我们具体分析一下在HBase里面的Thrift服务性能。
HBase把Thrift结合起来可以向外部应用提供HBase服务。由于HBase本来就自带有JAVA Client API接口,因此它主要方便其它语言使用HBase。
HBase实现了两套Thrift Server服务,有两种Thrift IDL文件,提供了两套数据结构:
- 第一套有TCell, ColumnDescriptor,TRegionInfo等,它的API比较全,它不仅有读写API,同时也有创建删除等API;
- 第二套有TTimeRange, TColumn, TColumnValue等,它更加接近HBase Java API的调用方式,但是它的API比较少,只有读写表的API),它们最后都是通过HBase Client的Java API来完成操作。
分析HBase Thrift的读写性能,主要从以下三大方面入手:请求延迟量(Latency)、系统吞吐量(TPS)、每秒调用数(RPC),并且尽量找出它的应用瓶颈所在。在此,它会与HBase原生提供的Java API做对比。
测试环境为:在一个机房里,共有30台服务器位于同一个交换机之下。它们搭有Hadoop 1.0.3和HBase 0.9.4的分布式存储计算集群。其中有一台机器上搭有ThriftServer,命为Thrift 2, 有一台机器专门用作访问Thrift和HBase的客户端。机器的配置都是统一的:48G内存,1000M的网卡,8个核,每个核都能开启超线程。
我们将对HBase的API和Thrift接口进行测试,涉及到随机写(randomWrite),随机读(randomRead),顺序写(sequentialWrite),顺序读(sequentialRead),批量读(scan)。每个线程会操作1G的数据。下图的测试数据是不同的线程数下的测试结果。
1. 总时长
2. 单请求Latency
3. 吞吐量Throughput
从表中可以看出,这5类操作的测试结果所显示的图形都不一样。
3. 测试总结
SequentialWrite(RandomWrite):
在单线程的情况下,API的Latency达到0.04毫秒(0.02毫秒),而thrift2就达到了1.64毫秒(2.08毫秒),它们之间的差别有40(>100)倍。
随着线程数据 的增加,latency不断的变大,而randomWrite API明显比sequential API的latency增大快些。
SequentialRead(randomRead):
在单线程的情况下,API和和thrift2的差别是不到2倍。而且比起SequentialWrite(RandomWrite),它们所耗时间有7至25倍。
Scan:
Scan的访问速度介于写于读之间,它比sequentialWrite/randomWrit慢些,而比sequentialRead/randomRead快些。
4. 测试结果分析
4.1. Write 比Read 快原因分析
4.1.1 写操作
1. 在HBase里面做put、delete、increment等变更操作时,通常都会记录一下WAL。因此在变更操作时主要有两步:写WAL和写MemStore。从常识和经验来说,写MemStore就是写内存,这是很快的;但是写WAL是写HDFS,而且是写远程的硬盘,这应该不会快到哪里去的,即使是是通过异步写WAL时也需要等待写HDFS成功的返回值。仔细分析一下源代码可以发现:RegionServer启了一个后台的线程LogSyncer,它是在有数据时不断地循环地作sync操作,而变更操作对WAL主动调用sync函数时很少去完成做真正的sync操作,它会经常去比较txid值的大小,当小于syncedTillHere值时就直接返回了。
写操作的关键代码如下:
private void syncer(long txid) throws IOException { Writer tempWriter; synchronized (this.updateLock) { if (this.closed) return; tempWriter = this.writer; // guaranteed non-null } // if the transaction that we are interested in is already // synced, then return immediately. if (txid <= this.syncedTillHere) { //比较 return; } try { long doneUpto; long now = System.currentTimeMillis(); // First flush all the pending writes to HDFS. Then // issue the sync to HDFS. If sync is successful, then update // syncedTillHere to indicate that transactions till this // number has been successfully synced. synchronized (flushLock) { if (txid <= this.syncedTillHere) { //比较 return; } doneUpto = this.unflushedEntries.get(); List<Entry> pending = logSyncerThread.getPendingWrites(); try { logSyncerThread.hlogFlush(tempWriter, pending); } catch(IOException io) { synchronized (this.updateLock) { // HBASE-4387, HBASE-5623, retry with updateLock held tempWriter = this.writer; logSyncerThread.hlogFlush(tempWriter, pending); } } } // another thread might have sync'ed avoid double-sync'ing if (txid <= this.syncedTillHere) { //比较 return; } try { tempWriter.sync(); } catch(IOException io) { synchronized (this.updateLock) { // HBASE-4387, HBASE-5623, retry with updateLock held tempWriter = this.writer; tempWriter.sync(); } } //更新syncedTillHere this.syncedTillHere = Math.max(this.syncedTillHere, doneUpto); syncTime.inc(System.currentTimeMillis() - now); if (!this.logRollRunning) { checkLowReplication(); try { if (tempWriter.getLength() > this.logrollsize) { //LogRoller requestLogRoll(); } } catch (IOException x) { LOG.debug("Log roll failed and will be retried. (This is not an error)"); } } } catch (IOException e) { LOG.fatal("Could not sync. Requesting close of hlog", e); requestLogRoll(); throw e; } }