Thrift 相关知识点
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
5struct Phone {
1: i32 id,
2: string number,
3: PhoneType type
}enum 枚举
1
2
3
4
5
6enum 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
4service <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
用于自动生成代码框架。