Apache Thrift 入门

简介

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 stack

堆栈的顶部由你的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服务位于防火墙的一端,而客户端则位于防火墙的另一端,那么一些数据中心可能存在开放过多端口的问题。

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容