Thrift 是一套轻量级、跨语言、全栈式的RPC(远程过程调用)解决方案,最初由Facebook开发,后面进入Apache开源项目,包含了代码生成、序列化框架和RPC框架三大部分。大致相当于protoc + protobuffer + grpc。三部分的具体作用如下:
整体架构图:
Thrift技术栈分层从下向上分别为:传输层(Transport Layer)、协议层(Protocol Layer)、处理(Processor Layer)和服务层(Server Layer)。
数据类型 | 类型标志(一个字节) | 值 |
---|---|---|
bool | 2 | 一个字节 |
byte | 3 | 一个字节 |
double | 4 | 八个字节 |
i16 | 6 | 两个字节值 |
i32 | 8 | 四个字节值 |
i64 | 10 | 八个字节值 |
string | 11 | 四个字节数据长度+数据的值 |
binary | 11 | 四个字节数据长度+数据的值 |
struct | 12 | 多个连续的field数据+一个字节停止符(0) |
map | 13 | 一个字节的key类型标志+一个字节的val类型标志+四个字节的数据长度+数据的值(key+val) |
set | 14 | 一个字节的val类型标志+四个字节的数据长度+数据的值 |
list | 15 | 一个字节的val类型标志+四个字节的数据长度+数据的值 |
传输层为网络IO操作提供了一个简单抽象,使得Thrift的其它部分(序列化、请求处理等)与底层网络处理解耦。传输层主要有两个接口:TTransport和TServerTransport。
协议层定义了一种将内存中的数据结构映射到传输格式的机制,它指定了各种数据类型在使用底层传输时如何编码/解码。协议实现管理编码方案并负责(反)序列化,因此也可以称为序列化协议。协议的一些例子包括 JSON、XML、纯文本、压缩二进制等。
序列化是将数据结构或对象转换成可以存储和传输的字节序列的过程。
协议层定义了一个TProtocol接口。TProtocol中定义了一系列WriteXxx()和ReadXxx()方法,提供的就是对数据的编码和解码。Thrift通过不同的Protocol实现类来提供不同序列化方式。在序列化协议上总体划分为文本(text)和二进制(binary)协议。常用协议有以下几种:
前面讲序列化的时候,特别指出是序列化协议,但Thrift实现的时候没有区分那么明显,我们一般说的BinaryProtocol和CompactProtocol实现其实就是通讯协议(可以理解里面包含了序列化协议),就是在前面在序列化协议基础上添加了Message传输的协议部分。之所以分开讲,是更了方便理解Thrift不仅仅是RPC框架还是个序列化框架。回到通讯协议,典型常见的HTTP协议为例,可以理解主要含三部分:路由信息(URL)+控制信息(Header)+数据负载(Body),Thrift通讯协议也是如此,下面主要分析一下BinaryProtocol通讯协议的实现。
首先BinaryProtocol分为严格模式和非严格模式,严格模式下会带上版本Version信息,非严格模式没有版本,默认为严格模式。其中通讯的消息类型主要有四种typeId:
整个通讯协议具体实现如下:
四个字节的版本(含调用类型),四个字节的消息名称长度,消息名称,四个字节的流水号,消息负载数据的值,一个字节的结束标记。伪代码如下:
goversion := uint32(VERSION_1) | uint32(typeId)
WriteI32(int32(version))
WriteString(name)
WriteI32(seqId)
WriteBody(body)
WriteByte(STOP)
四个字节的消息名称长度,消息名称,一个字节调用类型,四个字节的流水号,消息负载数据的值,一个字节的结束标记。
goWriteString(name) WriteByte(typeId) WriteI32(seqId) WriteBody(body) WriteByte(STOP)
处理层主要负责读取解码后的数据和输出编码前的数据,处理的是具体的消息交换。
在Thrift消息交换过程中,主要包括三种消息:Message、RequestStruct、ResponseStruct。
gotype TMessageType int32
const (
INVALID_TMESSAGE_TYPE TMessageType = 0
CALL TMessageType = 1 // 正常请求
REPLY TMessageType = 2 // 正常回复
EXCEPTION TMessageType = 3 // 异常回复
ONEWAY TMessageType = 4 // 单向请求,since Apache Thrift 0.9.3
)
注意:RequestStruct和ResponseStruct跟我们平时编写的XxxRequest和XxxResponse不是一回事,确切的说,我们编写的XxxRequest和XxxResponse只是RequestStruct和ResponseStruct中的一个字段。
处理层接口是TProcessor:
gotype TProcessor interface {
Process(ctx context.Context, in, out TProtocol) (bool, TException)
// ProcessorMap returns a map of thrift method names to TProcessorFunctions.
ProcessorMap() map[string]TProcessorFunction
// AddToProcessorMap adds the given TProcessorFunction to the internal
// processor map at the given key.
// If one is already set at the given key, it will be replaced with the new
// TProcessorFunction.
AddToProcessorMap(string, TProcessorFunction)
}
在Thrift原生代码库中没有TProcessor的具体实现。具体实现是由Thrift编译器根据idl文件生成的。TProcessor会读取解码后的Message,根据Message信息找到对应的Handler(由我们程序员实现),将解码后的RequestStruct交给Handler处理,然后将Handler处理的结果封装成ResponseStruct,交由协议层编码后发送出去。
服务器将上述所有各种功能汇集在一起:
gotype TServer interface {
ProcessorFactory() TProcessorFactory
ServerTransport() TServerTransport
InputTransportFactory() TTransportFactory
OutputTransportFactory() TTransportFactory
InputProtocolFactory() TProtocolFactory
OutputProtocolFactory() TProtocolFactory
// Starts the server
Serve() error
// Stops the server. This is optional on a per-implementation basis. Not
// all servers are required to be cleanly stoppable.
Stop() error
}
TServer在Java中有多种实现:
在go语言中只有一个实现TSimplerServer,内部实现使用了Reactor网络IO模型:
IDL(Interface Definition Language)用于定义和生成跨语言的数据传输和服务接口。用户在IDL中声明自己的服务,然后通过Thrift编译将IDL生成服务端和客户端代码(可以为不同语言),从而实现服务端和客户端跨语言的支持。
优势
IDL 是Thrift 中用于定义数据结构和服务接口的语言。它类似于一种模式或规范,描述了:
gostruct Person {
1: required i32 id;
2: required string name;
3: optional string email;
}
goservice UserService {
void createUser(1: Person user),
Person getUserById(1: i32 userId),
void updateUser(1: i32 userId, 2: Person updatedUser),
void deleteUser(1: i32 userId)
}
在 Thrift 中,IDL 允许开发者定义复杂的数据结构和跨语言的服务接口,而无需关心底层实现细节。IDL 是平台无关的,这意味着你可以使用相同的定义来生成多种编程语言的代码,从而实现不同平台之间的数据交换和通信。 Thrift IDL语法可参考官方文档http://thrift.apache.org/docs/idl ,这里主要提几点常见误解的地方。
代码生成是指通过 Thrift 的IDL文件生成具体编程语言的代码,包括:
生成的代码对于每种编程语言都是特定的,它们负责将 Thrift 的IDL文件翻译成目标语言所能理解和使用的形式。这样,开发者可以在各种不同的平台上使用 Thrift 生成的代码,实现跨语言的通信和数据交换,而无需手动编写复杂的序列化和网络通信代码。
序列化本质是把rpc的参数转换成一段连续的二进制流,还可以对参数进行压缩,使传输的流量更少。 thrift比较常见的序列化有2种: Binary序列化 和 Compact序列化,二者的区别是前者基本就是将参数顺序的写入连续的二进制流,后者对参数进行了一定的压缩。 序列化后的二进制流格式如下
powershell[id(2字节) + 类型标志(1字节) + 值] + [id(2字节) + 类型标志(1字节) + 值] + .....
由于二进制流中存在 id序号 和 类型flag,当server收到协议后,会根据服务端注册的idl, 根据 id序号到二进制流中找对应的buffer, 并会对类型做进一步比对。 Thrift提供了多种序列化协议,以满足不同的性能和存储需求,以下是对Thrift常用协议的详细解释:
Binary协议是一种二进制的序列化方式,具有高传输效率,但数据不可读。Binary协议采用TLV(Type-Length-Value)编码结构。 特点:
Compact协议是一种二进制压缩序列化方式,与Binary协议相比,Compact协议在编码方式上进行了优化,以最大化节省空间开销。Compact协议在大部分字段的编码方式上与Binary协议保持一致。区别在于整数类型(i16、i32、i64三种类型)采用了先zigzag编码 ,再varint压缩编码实现
不定长无符号整数编码,将整数类型由定长存储转为变长存储,减少空间浪费。每个字节,我们只使用低7位,最高的一位作为一个标志位(msb):
该编码好处在于对于小数采用更少字节,缺点在于对于大数,反而会比binary的空间开销更大。但大部分使用都是小数,整体来看,压缩作用较为明显。 举例,对i32类型的7进行编码,value值占4个字节,可以说前面3个字节都浪费了。
go00000000 00000000 00000000 00000111
而以i32类型的955为例,可以看出,由原来的4字节压缩到了2字节。 实现逻辑如下
gofunc PutUvarint(buf []byte, x uint64) int {
i := 0
for x >= 0x80 {
buf[i] = byte(x) | 0x80
x >>= 7
i++
}
buf[i] = byte(x)
return i + 1
}
func Uvarint(buf []byte) (uint64, int) {
var x uint64
var s uint
for i, b := range buf {
if b < 0x80 {
if i > 9 || i == 9 && b > 1 {
return 0, -(i + 1) // overflow
}
return x | uint64(b)<<s, i + 1
}
x |= uint64(b&0x7f) << s
s += 7
}
return 0, 0
}
将有符号整数编码为无符号整数,使得负数的绝对值较小,解决了绝对值较小的负数经过varint编码后空间开销较大的问题(正数原码,负数补码)。假设有符号数直接采用varint编码,因为负数最高位是1,比如i32就都会使用5个字节了,反而使用更多字节,为了解决有符号负数问题,先采用zigzag编码将有符号数映射到无符号数上,
特点
举例子,假设我们有一个有符号整数列表 [-5, 0, 3, -2, 7],我们希望对这些整数进行Zigzag编码。 原始整数表示:
go-5: 1011 (4比特的二进制补码表示)
0: 0000 (4比特的二进制补码表示)
3: 0011 (4比特的二进制补码表示)
-2: 1110 (4比特的二进制补码表示)
7: 0111 (4比特的二进制补码表示)
Zigzag编码的步骤:
对于上面的例子:
go-5 映射为 9
0 映射为 0
3 映射为 6
-2 映射为 3
7 映射为 14
因此,Zigzag编码后的整数列表为 [9, 0, 6, 3, 14]。 这个编码过程可以保留整数之间的顺序,有效地减少了需要存储和传输的数据量,特别是在序列化和压缩整数数据时非常有用。
JSON(JavaScript Object Notation)协议是一种轻量级的数据交换格式,易于人阅读和编写,也易于机器解析和生成。
SimpleJSON 是一种简化的 JSON 协议,专注于简单的数据结构序列化和反序列化。它通常用于那些不需要完整 Thrift 功能的场景,比如快速原型开发或者对数据格式要求不严格的应用。
远程过程调用(简称RPC,英文为Remote Procedure Call)是一种计算机通信协议。通过该协议,程序可以在一台计算机上调用另一台计算机(通常是通过开放网络)的子程序,程序员在调用时无需额外编写交互代码,就像调用本地程序一样简单。RPC采用客户端-服务器(Client/Server)模式,经典的实现方式是通过发送请求和接受回应来进行信息交互。
RPC(Remote Procedure Call,远程过程调用)一般的流程步骤如下:
常见的RPC框架 这些框架在不同的场景和需求下有各自的优势,选择适合自己项目的RPC框架需要考虑到技术栈、性能需求、跨语言支持等因素。
为了进一步理解Thrift、RPC和IDL三者之间的关系,我们回顾下它们各自的功能和定义,探讨它们在分布式系统中的角色和相互作用。
关系: Thrift vs RPC:
Thrift vs IDL:
Thrift 包含了 IDL 的概念,即用于描述数据结构和服务接口的语言。Thrift 使用自己的IDL来定义服务接口和数据类型,然后生成对应的代码。 IDL 本身是一种语言中立的接口描述语言,用于定义接口的规范和结构,以便于在不同语言和平台之间进行通信。 RPC vs IDL:
RPC 是一种通信协议和模式,IDL 是用于描述接口和数据结构的语言。RPC 实现了IDL中定义的接口,通过序列化和网络传输来进行远程调用。 综上所述,Thrift 是一个具体的框架和工具集,它使用IDL来定义服务接口和数据结构,并实现了RPC的通信模式,使得开发者能够在分布式系统中方便地进行跨语言和跨平台的远程服务调用。
本文作者:sora
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!