Thrift 作为常用的RPC框架,我实习的项目也是以此构建的,因此记录相关知识点以备复盘。

Thrift 相关知识点

需求:微服务

为了实现技术解耦,业务上引入了「微服务」的概念,将不同的业务逻辑分散为独立的服务,需要时再相互调用,提高了可维护性和可靠性。

微服务的实现有如下两个痛点:

跨语言调用

不同功能间的跨进程调用(由于种种原因,这些功能可能由不同语言实现),需要解决跨语言的支持问题。跨语言调用的主体称作「工作单元」,可以是某个进程,也可以是某个内存中的数据结构。常见的跨语言场景可以按如下分类:

  • 工作单元内部协作:直接解释某些脚本语言(如 Lua 等)。
  • 工作单元外部协作:
    • 单进程:动态链接库调用(通信的前提是不同语言之间有相同的二进制接口 ABI)
    • 多进程:远程过程调用(RPC),thrift 就是用来实现 RPC 的一种方案。

Thrift 定义了一种中间语言(接口定义语言 IDL)来作为沟通的统一标准,再由自己的编译器将 IDL 分别编译为不同语言的代码(Java,C++,Python 等),或反向操作。

依赖底层细节

调用方和服务提供方除了要声明 所需/提供 的服务,传入/传出 参数以外,还要关心一些底层细节的实现,如将调用请求转化为字节流(序列化),网络传输(socket),解码字节流(反序列化)等。

Thrift 协议栈封装了这些底层实现,用户只要在 Thrift 描述文件中声明自己的服务,并编写实现代码,就可以提供一个让调用方直接调用的接口。

协议栈结构

Client Function Server
Code Code
ServiceClient Generate Code Processor
^^ TServer
TProtocol Data Stream TProtocol
TTransport Byte Stream TTransport
Underlying I/O Socket/File/Zip Underlying I/O

其中 TProtocol 和 TTransport 作为运行时库使用:

  • TTransport 实现了底层的 I/O 功能,将 Message 以字节流的形式传输。对应于底层的 Sockect/File 等 这些 IO 模块,都有一个对应的实现 TSocket/TFileTransport 。
  • TProtocal 实现将 Message 和「结构化数据」之间来回编码/解码(Encode/Decode),前者用以与 TTransport 交互,后者用以与上层 TServer 或者 ServiceClient 交互。比如,可以从 Message 中取出 4 Bytes 并「Decode」为 int32,或者反过来。
  • TServer 接收 Client 请求并转发给上层 Processor 处理。
  • TProcessor 响应请求,转发RPC请求、解析参数、调用用户逻辑、返回值等。

由于 Thrift 协议栈的封装,用户只需要关心:

  • 使用 IDL 定义数据结构和声明服务
  • 使用代码生成工具将此 IDL 编译成具体语言的代码框架
  • 在此框架的基础上编写完整的代码,实现功能

数据类型

Thrift 定义了常用的一些静态数据类型,使代码生成器能够在不同语言之间以相对小的代价传输数据。

  • 基本类型

    • bool
    • byte(有符号字节)
    • i16,i32,i64(有符号整数)
    • double(64位浮点数)
    • string(二进制字符串,或编码无关的文本如注释等)
  • Struct 结构体,与 C 结构体相似,结构体域内每一个字段用一个「整形域标识符」标注,如

    1
    2
    3
    4
    5
    struct Phone {
    1: i32 id,
    2: string number,
    3: PhoneType type
    }
  • enum 枚举

    1
    2
    3
    4
    5
    6
    enum Operation {
    ADD = 1,
    SUBTRACT = 2,
    MULTIPLY = 3,
    DIVIDE = 4
    }
  • Containers 容器,每种容器映射为各语言中常见的数据结构。

    • list -> STL vector, Java ArrayList, 脚本语言 nativearray
    • set -> SET set, Java HashSet, Python set, PHP/Ruby nativedictionary
    • map -> STL map, Java HashMap, PHP associativearray, Python/Ruby dictionary
      TProtocol 中定义了 read 和 write 对容器对象进行(反)序列化和传输。
  • Exceptions 异常,结构与 struct 完全一样,用来生成异常基类。

  • Services 服务,定义一个服务,相当于面向对象编程中定义一个接口或者抽象类,里面可以定义多个函数/方法。

    1
    2
    3
    4
    service <service name> {
    <returntype> <func name> (<arguments>) [throws (<exections>)]
    ...
    }

    返回类型可以是 void。如果调用 void,客户端会等待服务器发送一个操作完成的应答。如果不在乎调用失败,可以在 void 前加上 async,使客户端不等待服务器应答。

各层协议栈作用

TServer

TServer 接收 Client 请求,并转发到某个 TProcessor 处理。支持如下模型:

  • TSimpleServer: BIO 单线程服务器,用于调试
  • TThreadedServer:BIO 多线程服务器,一个请求处理一个线程。处理大量更新,延迟较低,吞吐量高,但线程多导致 CPU 占用高。
  • TThreadPoolServer:BIO 多线程服务器,比前者增加了线程池管理
  • TNonBlockingServer:NIO 多线程服务器,需要调用 TFramedTransport 模块。用较少线程处理大量并发,但延迟较高。

事件驱动器:libevent,一种对 epoll 的封装。

TTransport

以字节流的形式发送和接受数据,对应底层的目的地 Socket,File,和 Zip 等 I/O 出入口,具体有:

  • TSocket
  • THttpTransport
  • TFileTransport
  • TZlibTransport

对上面的对象使用装饰器模式进行变形,可以进一步得到以下类:

  • TBufferedTransport

  • TFramedTransport

  • TMemoryBuffer

  • TServerTransport

TProtocol

解析 TTransport 中的字节流,变为可读的数据(结构)流。传输协议分为文本和二进制格式:

  • TBinaryProtocol:
  • TCompactProtocol:对整数采用变长二进制编码
  • TJSONProtocol
  • TSimpleJSONProtocol
  • TDebugProtocol

TProtocol 完成双向有序的消息传递,对前面提到的数据类型进行编码,或者按一定格式读取数据流。

TProcessor

操作 Tserver 请求的 InputProtocol 和 OutputProtocol,从前者读出 Client 的请求,从后者写入用户逻辑给出的返回值。
一次 RPC 调用的处理过程包括:

  • TServer 接收一个请求,调用 TProcessor.process 处理
  • TProcessor.process 调用 TTransport.readMessageBegin 读出 RPC 调用的名称和类型
  • TProcessor.process_fn 根据 RPC 调用名称到 processMap 中查找对应的 RPC 处理函数,调用进行响应。
  • RPC 处理函数会读出 TProtocol 的数据,调用对应请求参数的解析类,跳过不符合要求的参数。然后调用用户逻辑,响应请求,最后用一个类打包返回值,再写入 TProtocol。

ThriftClient

类似 TProcessor,Client 操作 TProtocol。

  • Send,将调用参数封装进一个 Struct 写入 TProtocol,发送给 TServer。
  • Send 结束后进入 Receive 等待响应。用解析类解析返回值,完成本次调用。

Thrift Generator

用于自动生成代码框架。