简介
Thrift 是一个创建可互操作性和可伸缩性服务的框架。Thrift原来由Facebook开发,后来捐献给了Apache以促进更多的使用。Thrift是在Apache 2.0许可下发布的。
Thrift通过简单明了的接口定义语言( Interface Definition Language , IDL),允许你定义和创建可被很多语言消费和使用的服务。Thrift通过使用代码生成,能够创建一组用来生成客户端或服务端的文件。除了互操作性,Thrift通过它独特的序列化机制(在时间和空间上都是有效率的)能做到非常高效的。
Facebook对编程语言的选择是基于什么语言对当前任务最适合。虽然很实用,但当这些应用程序需要互相调用时,这种灵活性导致一些困难。在分析之后,Facebook的工程师们没有发现目前的任何东西能够满足他们的互操作性、高效传输和简单的需求。出于这一需求,Facebook的工程师开发了高效的协议和服务基础设施,这就是Thrift。Facebook现在使用Thrift作为他们的后端服务 - 这就是它被设计出来的原因。
Thrift 架构
Thrift包含创建客户端和服务端的完整堆栈。(原文:Thrift includes a complete stack for creating clients and servers)
ps:stack不好翻译。
下图描述了Thrift的堆栈:
堆栈的顶部由你的Thrift定义文件生成。Thrift services就是生成的client和processor代码。在图中以棕色表示。用来发送的数据结构(除了内置类型)同样在生成的代码里。即图中红色部分。protocol与transport是Thrift运行库的一部分。所以使用Thrift,你可以定义service,并且能够自由地改变protocol和transport而不用重新生成代码。
Thrift也包含服务基础设施(Server infrastructure),用来把protocols和transports绑定在一起。可用的server有: blocking, non-blocking, single and multithreaded servers。
堆栈的"Underlying I/O"部分在不同的语言中是不同的。对于Java网络I/O,Thrift库对内置库进行了增强,而C++实现使用自己自定义的实现。
Thrift支持的Protocols, Transports和Servers
Thrift使你可以在协议(protocol)、传输(transport)和服务器(server)之间独立选择。Thrift最初是用C++开发的,所以在C++实现中对这些的选择有最大的可变性。
Thrift同时支持文体和二进制的协议。二进制协议性能优于文本协议。文本协议有时也有用(如debug时)。Thrift支持的协议有:
- TBinaryProtocol - 一种直接的二进制格式,将数值编码为二进制,而不是转换为文本
- TCompactProtocol - 非常高效,密集的数据编码
- TDenseProtocol - 与 TCompactProtocol 相似, 但除去了传输内容的元信息,并且在接收方添加了回去。 TDenseProtocol仍然在实现阶段,在Java实现中不可用。
- TJSONProtocol - 使用JSON数据编码。
- TSimpleJSONProtocol - 一个只写的协议,使用JSON. 适用于脚本语言的解析。
- TDebugProtocol - 使用人类可读的文本格式来帮助调试。
上面的协议(protocol)描述了“什么”被传输,Thrift的传输(transport)就是描述“如何传输”。Thrift支持的传输有:
- TSocket - 使用阻塞socket I/O 进行传输。
- TFramedTransport - 使用帧来发送数据,其中每个帧前面都有一个长度。当使用非阻塞服务器时,需要进行这种传输
- TFileTransport - 此传输写入文件。虽然这个传输没有包含在Java实现中,但是实现起来应该足够简单。
- TMemoryTransport - 使用内存作为I/O. Java实现在内部使用一个简单的ByteArrayOutputStream。
- TZlibTransport - 使用 zlib 进行压缩. 用于与另一种运输一起使用。在Java实现中不可用。
Lastly, Thrift provides a number of servers:
最后,Thrift提供许多服务器(server):
- TSimpleServer - 使用std阻塞io的单线程服务器。用于测试。
- TThreadPoolServer - 使用std阻塞io的多线程服务器。
- TNonblockingServer - 使用非阻塞io的多线程服务器(Java实现使用NIO通道)。TFramedTransport 必须与这种服务器一起使用。
Thrift中每个服务器(server)只允许使用一个服务(service)。尽管这确实是一种限制,但可以使用一种变通的方法容纳多个服务。通过定义一个组合服务(它扩展了给定服务器应该处理的所有其他服务)一个单独的服务器能够容纳多个服务。如果这个变通方法不能满足你的需要,你可以创建多个服务器。这个场景将意味着你将使用不必要的资源(端口、内存等)。
TCompactProtocol
考虑到TCompactProtocol是Thrift的Java实现中效率最高的方法和这篇文章使用的示例代码,对该协议的进一步解释是必要的。这个协议为每一个数据写数字标签,接收方需要将这些标签与数据进行适当匹配。如果数据不存在,则不存在标签/数据对。
对于整型,使用来自MIDI文件格式的Variable-Length Quantity (VLQ) 编码执行压缩。VLQ是一种相对简单的格式,第个字节中7位或8位用来存储信息,第8位作为延续位。VLQ最差情况的编码是可以接受的。对于32位整数,它是5个字节。对于64位整数,它是10个字节。下图表示在十进制106903 (0x1A197)如何用VLQ表示,它比用32位来存储节省1个字节:
将原数值二进制按7位进行分割,则能分成3部分。最低7位前面补0,其他的前面补1(因为超过128)。最后得到一个3字节的编码 (0x86C317)。比原来32bit (4字节)节省了一个字节。还原值如下:
128^2 * 6 + 128^1 * 67 + 128^0 * 23 = 106903
创建Thrift服务
创建一个Thrift服务首先需要创建一个描述服务的Thrift文件,生成服务的代码,最后编写一些启动服务的服务端代码和调用服务的客户端代码。
定义
course.thrift
// 指定java命名空间: com.willjava.thrift.hello.gen, 生成代码时将以这个为包结构
namespace java com.willjava.thrift.hello.gen
// senum定义枚举类型,但并不会生成枚举类
senum PhoneType {
"HOME",
"WORK",
"MOBILE",
"OTHER"
}
// struct定义简单结构,PhoneType只是简单的字符串类型
// 注意每个元素前的数值标识符,当序列化/反序列化时,这些标识符用来加速解析元数据和减小元数据大小的
// 这些数值标识符是传输的内容,而不是元素的名字
struct Phone {
1: i32 id,
2: string number,
3: PhoneType type
}
// Thrift支持多种集合类型 - ist, set and map
struct Person {
1: i32 id,
2: string firstName,
3: string lastName,
4: string email,
5: list<Phone> phones
}
struct Course {
1: i32 id,
2: string number,
3: string name,
4: Person instructor,
5: string roomNumber,
6: list<Person> students
}
// service有抛异常的,异常要在service前面定义,不然生成代码时会提示找不到异常定义
exception CourseNotFound {
1: string message
}
exception UnacceptableCourse {
1: string message
}
// 定义service, 注意方法参数和异常同样需要序数
service CourseService {
list<string> getCourseInventory(),
Course getCourse(1:string courseNumber) throws (1: CourseNotFound cnf),
void addCourse(1:Course course) throws (1: UnacceptableCourse uc),
void deleteCourse(1:string courseNumber) throws (1: CourseNotFound cnf)
}
代码生成
Thrift 对许多语言的支持参差不齐,如Python只支持TBinaryProtocol。完整列表如下:
- Cocoa
- C++
- C#
- Erlang
- Haskell
- Java
- OCaml
- Perl
- PHP
- Python
- Ruby
- Smalltalk
下面以生成Java代码为例:
-- thrift的windows版本见参考资料
-- out参数指定输出目录,不指定会在当前目录新建gen-java目录作为目标目录
-- gen参数指定生成代码类型
> thrift-0.10.0.exe -out . --gen java course.thrift
|-- src/main/java
| `-- com
| `-- willjava
| `-- thrift
| `-- hello
| `-- gen
| |-- Course.java
| |-- CourseNotFoundException.java
| |-- CourseService.java
| |-- Person.java
| |-- Phone.java
| `-- UnacceptableCourseException.java
如你想象的一样,每个Thrift结构和异常都单独生成一个文件。如上面提到的,senum并不会生成Enum类型。相反,它在Phone中生成一个简单的String类型,在validate方法中有一个注释,说明该类型的值应该在这里进行验证(对,需要自己实现验证逻辑)。最后,CourseSevice.java被生成。这个文件包含创建客户端与服务端的类。
创建Java服务端
Handler.java 实现 CourseService,实现thrift文件定义的4个方法。这里简单对map进行操作。
public class Handler implements CourseService.Iface {
private static final Map<String, Person> instructorMap = new HashMap<>();
static {
// instructor 1
Person instructor1 = new Person();
instructor1.setId(1);
instructor1.setFirstName("instructor1_firstName");
instructor1.setLastName("instructor1_lastName");
instructor1.setEmail("instructor1@mail.com");
Phone p1 = new Phone();
p1.setId(1);
p1.setNumber("130123456");
p1.setType("WORK");
Phone p2 = new Phone();
p2.setId(2);
p2.setNumber("1311234567");
p2.setType("HOME");
instructor1.setPhones(Arrays.asList(p1, p2));
// instructor 2
Person instructor2 = new Person();
instructor2.setId(2);
instructor2.setFirstName("instructor2_firstName");
instructor2.setLastName("instructor2_lastName");
instructor2.setEmail("instructor2@mail.com");
Phone p3 = new Phone();
p1.setId(3);
p1.setNumber("22222222222222");
p1.setType("WORK");
Phone p4 = new Phone();
p2.setId(4);
p2.setNumber("222222222223");
p2.setType("HOME");
instructor2.setPhones(Arrays.asList(p3, p4));
instructorMap.put("C001", instructor1);
instructorMap.put("C002", instructor2);
}
private static final Map<String, Course> courseMap = new HashMap<>();
static {
// math
Course math = new Course();
math.setId(1);
math.setName("Math");
math.setNumber("C001");
math.setRoomNumber("R001");
math.setInstructor(instructorMap.get("C001"));
// physics
Course physics = new Course();
physics.setId(2);
physics.setName("Physics");
physics.setNumber("C002");
physics.setRoomNumber("R002");
physics.setInstructor(instructorMap.get("C002"));
courseMap.put("C001", math);
courseMap.put("C002", physics);
}
@Override
public List<String> getCourseInventory() throws TException {
List<String> courseNameList = new ArrayList<>();
courseMap.forEach((k, v) -> courseNameList.add(v.getName()));
return courseNameList;
}
@Override
public Course getCourse(String courseNumber) throws CourseNotFound, TException {
Course course = courseMap.get(courseNumber);
if (Objects.isNull(course)) {
throw new CourseNotFound();
}
return course;
}
@Override
public void addCourse(Course course) throws UnacceptableCourse, TException {
System.out.println("addCourse: " + course);
}
@Override
public void deleteCourse(String courseNumber) throws CourseNotFound, TException {
System.out.println("deleteCourse: " + courseNumber);
courseMap.remove(courseNumber);
}
}
CourseServer.java 启动 CourseService, 这里使用TCompactProtocol协议、TFramedTransport传输和非阻塞服务器(non-blocking server)。TFramedTransport必须搭配non-blocking server使用。
public class CourseServer {
public static void main(String[] args) throws Exception {
TNonblockingServerSocket socket = new TNonblockingServerSocket(7777);
THsHaServer.Args serverParams = new THsHaServer.Args(socket);
serverParams.protocolFactory(new TCompactProtocol.Factory());
serverParams.transportFactory(new TFramedTransport.Factory());
serverParams.processor(new CourseService.Processor(new Handler()));
TServer server = new THsHaServer(serverParams);
server.serve();
}
}
创建Java客户端
public class CourseClient {
public static void main(String[] args) throws Exception {
TSocket socket = new TSocket("127.0.0.1", 7777);
socket.setTimeout(3000);
TTransport transport = new TFramedTransport(socket);
TProtocol protocol = new TCompactProtocol(transport);
CourseService.Client client = new CourseService.Client(protocol);
transport.open();
//All hooked up, start using the service
List<String> classInv = client.getCourseInventory();
System.out.println("Received " + classInv.size() + " class(es).");
client.deleteCourse("C001");
classInv = client.getCourseInventory();
System.out.println("Received " + classInv.size() + " class(es).");
transport.close();
}
}
运行Thrift
先运行服务端,再运行客户端。
Thrift与其他框架的比较
为了验证Thrift的价值,我决定拿它与其他一些实际上容易使用的服务技术进行比较。因为近来RESTful webservices似乎很流行,我比较了Thrift和REST。尽管Protocol Buffers不包含服务基础设施,但它以类型于Thrift的TCompactProtocol的方式传输对象,因此与它比较是有用的。最后,我也比较了 RMI,因为它使用二进制传输,所以能够当作一种Java二进制对象传输的“参考实现”。
为了进行比较,我比较了每个服务技术的文件大小和运行时性能。对于REST,我比较了基于XML的和基于JSON的REST。对于Thrift,我选择java中最高效的传输方式 - TCompactProtocol。
大小比较
为了比较大小,每种技术我都传输相同的对象,1个Course对象、5个Person对象、1个Phone对象。为了记录文件大小,我使用了如下技术:
服务技术 | 记录方法 |
---|---|
Thrift | Custom client that forked the returning input stream to a file. |
Protocol Buffers | Stream to a file. Excludes messaging overhead. |
RMI | Object serialization of the response. Excludes messaging overhead. |
REST | Use wget from the commandline redirecting the response to a file. |
下面图表为结果,以Byte为单位,不包含TCP/IP开销。
服务技术 | 大小* | 比TCompactProtocol大的百分比 |
---|---|---|
Thrift — TCompactProtocol | 278 | N/A |
Thrift — TBinaryProtocol | 460 | 65.47% |
Protocol Buffers** | 250 | -10.07% |
RMI (using Object Serialization for estimate)** | 905 | 225.54% |
REST — JSON | 559 | 101.08% |
REST — XML | 836 | 200.72% |
*Smaller is better.
** Excludes messaging overhead. Includes only transported objects.
Thrift has a clear advantage in the size of its payload particularly compared to RMI and XML-based REST. Protocol Buffers from Google is effectively the same given that the Protocol Buffers number excludes messaging overhead.
性能比较
见原文
结论
Thrift是创建可以从多种语言调用的高性能服务的强大库。如果你的应用程度需要多语言通信,需要考虑速度,并且客户端与服务端面位于同一位置,Thrift对你来说将是很好的选择。在考虑速度和互操作性的单台机器上,Thrift也可能是IPC的一个很好的选择。
Thrift被设计用于客户端和服务器位于同一位置的地方,如在数据中心。如果你考虑在服务端与客户端不在同一位置的环境中使用,你应该会遇到一些挑战。尤其上面提到的异步调用的问题,以及安全性的缺乏可能会带来挑战。虽然安全问题可以通过新的传输来解决,但是异步调用的问题可能需要在Thrift的核心领域进行工作。另外,由于Thrift支持大量的语言绑定,你可能需要对你使用的每一种语言进行更改。
如果复合服务工作环境对你不起作用,那么在一些部署场景中,Thrift的一个服务器一个服务的限制可能会带来问题。例如,如果Thrift服务位于防火墙的一端,而客户端则位于防火墙的另一端,那么一些数据中心可能存在开放过多端口的问题。