Apache Thrift 入门

作者: WillReading | 来源:发表于2018-07-29 12:05 被阅读19次

    简介

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

    参考资料

    相关文章

      网友评论

        本文标题:Apache Thrift 入门

        本文链接:https://www.haomeiwen.com/subject/wvxamftx.html