研究分布式调用链工具pinpoint的时候,在源码里看到了Thrift相关的代码,所以来了兴趣,想研究研究这个框架。
Thrift 目前是 Apache 的一个项目,但是它是由facebook研发产生的。
在各个不同语言环境的系统中承担大量数据传输和通信的工作。
Thrift 支持包括上图中所有等28种语言,所以使得在使用不同编程语言的程序,可以很容易的传输共享数据和进行远程过程调用。
它是一个轻量级的,独立于语言的开发框架,在点对点rpc数据传输,数据的序列化上为我们提供了比较简洁的抽象和实现。
同样的,由于Thrift在传输数据时,采用的是二进制,相对于在HTTP协议中使用的xml和json占用体积更小,在很多高并发、数据量级大的系统中更加有优势。
我们通过定义一个以.thrift结尾的文件作为输入源,内容包含接口的定义,然后通过生成代码的方式,来构建不同语言的RPC客户端和服务端。
在 Thrift 的客户端和服务端交互中,各个组件都是比较灵活的,做到了充分的低耦合,所以我们可以基于不同的应用场景,选择不同的方式搭建或是语言实现。
由上图,可以看出,最上层是我们编写的Thrift代码,它是一种IDL(接口描述语言), 是用于描述的服务接口信息的。我们在这个文件中写好相关的注释,就可以用它来作为接口文档,提供给客户端使用了。同时,IDL文件,也是自动生成接口代码的输入源,定义出来的接口能够很灵活的支持扩展。
第二层是使用Thrift编译工具生成的代码,主要用于结构化的数据解析,发送和接收
通信协议层主要是定义数据传输的格式,对传输的数据进行序列化和反序列化, 从协议划分上来说,主要包含两个方面,一是文本,而是二进制,既然使用了Thrift,使用其二进制协议占多数,当然更是要结合实际的业务场景来分析考虑。
传输层负责直接从网络中读取和写入数据,它定义了具体的网络传输协议;比如说TCP/IP传输等。
Thrift 脚本可定义的数据类型包括以下几种类型:
图片来自 wangyangfu,其中,byte、i16、i32、i64分别表示:8/16/32/64位有符号整数。
其实 Thrift 的主要特点就是跨语言、能自动生成客户端。相对于 http 协议而言,算是比较小众。它是基于 socket,通过 TCP 协议实现,相比较无连接、无状态的 HTTP 协议的,每次打开、关闭连接会比较消耗性能 并且需要管理维护很多 IDL 文件。
基于thrift实现高效的二进制传输 而 http 大部分是通过 json 来实现的,字节大小和序列化耗时都比 Thrift 要更消耗性能。
总的来说,Thrift 更常见用于公司内部的系统服务之间调用,性能消耗比较低,保证服务质量,提高传输效率,而 http 主要用于对外的接口调用,浏览器、移动端、第三方接口等。
这里我使用的版本是 thrift-0.10.0.exe,下载地址:
http://archive.apache.org/dist/thrift/0.10.0/thrift-0.10.0.exe
将下载好的trift-0.10.0.exe 重命名为 thrift.exe 并且在控制面板--> Path环境变量中配置exe所在的目录 配置好后,命令行执行:
新建一个 maven 工程,在intf目录下新建一个 IUserInfo.thrift 文件,内容为:
struct User{
1: i32 id
2: string name
}
service UserService{
User getUser(1:i32 uid)
}
thrift -gen java IUserInfo.thrift
使用双面面命令即可将IDL文件编译成对应语言的接口文件。
执行完后,会生成两个Java类。当然idea开发工具也提供了Thrift的编译器支持,在插件仓库中搜索安装,配置相关的参数后,也能完成编译。
// 业务处理器
TProcessor processorr = new UserService.Processor<UserService.Iface>(new UserServiceImpl());
// 设置服务器
TServerSocket serverSocket = new TServerSocket(9999);
// 传输协议为二进制
TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory();
// 使用单线程阻塞 I/O 模型
TServer.Args simpleArgs = new TServer.Args(serverSocket).processor(processorr).protocolFactory(protocolFactory);
TServer server = new TSimpleServer(simpleArgs);
System.out.println("开启侠梦的Thrift服务器,监听端口:9999");
server.serve();
// 设置调用的服务地址-端口
TTransport tTransport = new TSocket("localhost", 9999);
// 使用二进制协议
TProtocol protocol = new TBinaryProtocol(tTransport);
// 使用的接口
UserService.Client client = new UserService.Client(protocol);
// 打开 Socket
tTransport.open();
System.out.println(client.getUser(1));
tTransport.close();
依次执行Server 端 和Client 后,效果如下:
至此,我们就完成了第一个Thrift 程序的编写。
pinpoint 中 为什么采用 Thrift 呢?
通过使用二进制格式(thrift)可以提高编码速度,虽然它使用和调试要难一些。也有利于减少网络使用,因为生成的数据比较小。
如果将一个长整型转换为固定长度的字符串, 数据大小一般是8个字节。然而,如果你用变长编码,数据大小可以是从1到10个字符,取决于给定数字的大小。
为了减小数据大小,pinpoint 使用 Thrift 的CompactProtocol协议(压缩协议)来编码数据,因为变长字符串和记录数据可以为编码格式做优化。pinpoint agent通过基于跟踪的根方法的时间开始来转换其他的时间来减少数据大小。
为了得到关于三个不同方法(见上图)被调用时间的数据,不得不在6个不同的点上测量时间,用固定长度编码这需要48个字节(6 * 8)。
以此同时,pinpoint agent 使用可变长度编码并根据对应的格式记录数据。然后在其他时间点通过和参考点比较来计算时间值(在vector中),根方法的起点被确认为参考点。这只需要占用少量的字节,因为vector使用小数字。图4中消耗了13个字节。
如果执行方法花费了更多时间,即使使用可变长度编码也会增加字节数量。但是,依然比固定长度编码更有效率。
本篇文章介绍了 跨语言的RPC框架 Thrift的入门知识,起源于研究Pinpoint的源码,在这里想说的是:研究阅读某个框架的源码其实是很耗费精力的一件事,因为不同的人读同样的代码会有不同的感受,这取决于各自的认知和知识深度不同。
但是我觉得思路应该都是一致的,那就是不应该局限在框架本身,而是站在创造者的角度,去思考,为什么最后造出了这样一个优秀的框架,它依赖的那些底层的硬件、或是软件。由点及面的将其剖析完整,希望能帮助到你。