Thrift白皮书(翻译)

Thrift是Facebook开发的一款用于高效跨语言服务实现的软件库和代码生成工具集。它允许开发人员在一个中立语言文件中定义数据类型和服务接口,并自动生成RPC客户端和服务端代码。Thrift的设计目标在于提供高效、可靠的跨语言通信,支持多种编程语言,如C++、Java、Python等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

注:本文翻译自 http://thrift.apache.org/static/files/thrift-20070401.pdf
Thrift:可扩展的、跨语言的服务实现
摘要
Thrift是Facebook为了促进高效、可扩展的后台服务的开发和实现而开发的一套软件库和代码生成工具集。它的主要目标是通过将各个语言中趋于大量定制的部分抽象进一个被各个语言实现的公共语言库中实现高效、可靠的跨语言通信。特别地,Thrift允许开发人员在一个单独的中性语言(不依赖任何语言)文件中定义数据类型和服务接口并编写用于构建RPC客户端、服务端的所有必要代码。
这篇文章详细说明了动机、我们在Thrift中所作的设计选择和一些特别有趣的实现细节。这篇文章并不是被当做调查研究来写,而是对我们做了什么以及为什么做的一个阐述。
1 简介
随着Facebook流量和网络架构的扩展,站点上的许多操作(搜索、广告选择和交付、事件日志)需求的资源已经呈现出激烈的、超出LAMP架构范围的技术需求。在我们对这些服务的实现中,为了对性能、开发的速度和灵活性、现存库的可用性等等组合进行优化,我们选择了多种语言。总的来说,Facebook的工程文化趋向于选择可用的最好的工具和实现,而不是标准化的任何一种编程语言,并且Facebook也不会情愿接收任何一种编程语言固有的局限性。
考虑到这种设计选择,我们面临着一个挑战——建立一个跨越多种编程语言的、透明的、高性能的桥梁。我们发现大多数可用方案要么太局限,不能提供充分自由的数据类型,要么性能不佳。
我们已经实现的方案将一个被多数编程语言实现的中性语言软件堆栈和一个关联的、可以将简单的接口和数据定义语言转换为客户端、服务端RPC库的代码生成引擎结合了在一起。相比动态系统,选择静态代码生成的方式允许我们创建不需要任何高级的运行时类型检查的有效代码。对于开发人员来说,它也被设计的尽可能简单,开发人员通常可以在一个简短的文件中定义一个复杂的服务所必要的所有数据结构和接口。
令人惊讶的是,针对这些相对常见问题的健壮的解决方案还不存在,我们早期就致力于使Thrift的实现成为开源的。
在评估网络环境下跨语言交互的挑战时,一些关键部分被确定下来:
类型:一个公共类型系统必须跨语言存在,而不需要应用程序开发人员使用定制的Thrift数据类型或者编写自己的序列化代码。也就是说,一个C++程序员应该能够透明地用一个强类型的STL映射交换动态的Python字典。任何程序员都不应该为了实现这一点而被强迫编写应用层以下的代码。第二节会详细说明Thrift的类型系统。
传输:每种语言必须有一个双向传输原始数据的公共接口。对于服务开发者来说,给定的传输是如何实现的,这些细节应该是不重要的。相同的应用程序代码应该能够依靠TCP套接字流、内存中的原始数据或者硬盘上的文件而运行。第三节将详细介绍Thrift的传输层。
协议:数据类型必须有一些通过传输层编码和解码自身的方式。其次,应用程序开发人员不需要关心这一层。服务使用的是XML还是二进制协议对应用程序代码应该是无形的(不重要的)。重要的是数据能够在一致的、连续的介质中读写。第四节将详细说明Thrift的协议层。
版本控制:对于健壮的服务,所涉及的数据类型必须提供一种对自己进行版本控制的机制。具体来说,数据类型应该能够在不中断服务(或者严重错误)的情况下添加或删除一个对象的字段,或者更新一个方法的参数列表。第五节将详细介绍Thrift的版本控制系统。
处理器:最后,生成能够处理数据流以完成远程过程调用的代码。第六节将详细介绍生成的代码和Tprocessor范例。
第七节讨论实现的细节,第八节描述了我们的结论。
2 类型
Thrift类型系统的目标是让程序员能够使用完全本地定义的类型进行开发,而不管他们使用何种编程语言。通过设计,Thrift的类型系统不引进任何特殊的动态类型或者包装器对象。它同时也不需要程序员编写任何用于对象序列化或者传输的代码。Thrift IDL是一种逻辑上的方式,该方式可以让程序员以最少的额外必要信息来声明他们的数据结构,从而告诉代码生成器如何安全地跨语言传输对象。
2.1 基本类型
类型系统基于几个基本类型。在考虑支持哪些类型时,我们的定位是清晰而简单的类型,而不是过多的类型。我们专注于所有编程语言中都有的可用的关键类型,放弃任何只在特定语言中才能使用的类型。
Thrift支持的基本类型如下:
bool:一个布尔值,true或者false
byte:一个有符号的byte
i16:一个16位带符号的integer
i32:一个32位带符号的integer
i64:一个64位带符号的integer
double:一个64位浮点型数字
string:一个与编码无关的文本或者二进制字符串
特别需要注意的是没有无符号整数类型。这是因为这些类型在很多语言中没有直接转换为对应语言的原始类型,所以这些类型的优势就消失了。进一步讲,在像Python这样的语言中,没有办法阻止一个程序员为整形变量分配负数值,从而导致不可预知的行为。从设计的立场来说,我们发现无符号整形很少用于算术目的,在实践中通常用于键或者标识符。在这种情况下,符号是不相关的(无关紧要的)。带符号整形具有相同的用途,并且在绝对必要的时候带符号整形可以安全地转换为无符号对应数(在C++中最常见)。
2.2 结构体
一个Thrift结构体定义了可以跨语言使用的公共对象。结构体在本质上等同于面向对象语言中的类。结构体具有一组强类型字段,每个字段都具有一个唯一的名称标识符。定义Thrift结构体的语法看起来与C语言中的结构体定义非常相似。字段可以有一个整形值(在该结构体内唯一)和可选的默认值作注解。字段标识符将会被自动分配如果省略的话,不过出于稍后讨论的版本控制原因,强烈建议使用字段标识符。
2.3 容器
Thrift容器是一种强类型容器,它能映射到大部分编程语言中使用的容器。容器的声明类似C++(或者Java范型)中容器的声明。有三种可用的类型:
list:一个包含元素的有序列表。可以直接转换为STL向量、Java ArrayList或者脚本语言中的数组。可以包含重复的元素。
set:一个无序的集合,集合中的元素是唯一的。可以直接转换为STL set、Java HashSet、Python中的set或者PHP/Ruby中的字典。
map<type1,type2>:一个严格的键——值映射,键是唯一的,可转换为STL map、Java HashMap、PHP associative array或者Python/Ruby dictionary。
即使提供了默认值,类型容器也没有显示固定。自定义代码生成器指令会在目标语言中替换自定义类型。唯一的要求是自定义类型支持必要的迭代。容器元素的类型可以是任一种有效的Thrift类型,元素也可以是另一个容器或者结构体。
struct Example{
1:i32 number=10,
2:i64 bigNumber,
3:double decimals,
4:string name=”thrifty”
}
在目标语言中,每个定义都会生成带有读、写两个方法的类型,这两个方法通过Thrift Tprotocol对象执行序列化和传输。
2.4 异常
异常在语法和功能上与struct是等价的,只不过异常使用exception关键字而不是struct关键字声明。
在目标语言中生成合适的继承于异常基类的对象,以便与给定语言中的异常无缝对接。这样的设计,目的在于使代码对应用程序开发人员尽量友好。
2.5 服务
服务是用Thrift类型定义的。服务的定义在语法上等同于在面向对象语言中定义一个接口(或者一个纯虚抽象类)。Thrift编译器生成实现全部功能接口的客户桩和服务桩。服务的定义形式如下:
service {
()
[throws ()]

}
一个例子:
service StringCache {
void set(1:i32 key, 2:string value),
string get(1:i32 key) throws (1:KeyNotFound knf),
void delete(1:i32 key)
}
注意,对于方法返回值来说,void是一个有效的类型,不仅仅是所有其他Thrift定义的类型。此外,一个异步修改关键字可以加到返回类型为void的方法上,这个关键字将使该方法不用等待服务器。注意,一个纯没有返回值的方法将会返回一个回复给客户端,这是为了保证当前操作在服务端已完成。async方法调用客户端将仅仅保证请求在传输层是成功的(在很多传输场景中,拜占庭将军的问题导致这是天生不可靠的。因此,在放弃方法调用是可接受的或者已知传输是可靠的情形下,应用程序开发人员才使用异步优化。)
还有一点需要注意的是,实际上方法的参数列表和异常列表是被当做结构体实现的。这三个构造在符号和行为上都是相同的。
3 传输
生成的代码使用传输层促进数据的传输。
3.1 接口
在Thrift的实现中有个关键的设计:将传输层与代码生成层解耦。虽然Thrift是将基于TCP/IP站的套接字流作为通信的底层,但没有必要将这个约束构建到系统中。相比于实际I/O操作所(通常涉及系统调用)带来的损耗,一个抽象的I/O(每个操作大概需要一个虚方法查找/方法调用)导致的性能损耗是不重要的。
根本来讲,生成的Thrift代码只需要知道如何读写数据就可以了。数据的来源和目的地是不重要的,可以是套接字、一段共享内存或者本地磁盘上的一个文件。Thrift的传输接口支持以下方法:
open:打开transport
close:关闭transport
isOpen:指示transport是否打开
read:从transport中读
write:往transport里写
flush:强制执行任何挂起的写操作
有一些文档没有说明的其他方法,这些方法用于帮助批量读取、可选的从生成的代码中发出读/写完成的信号。
除了上面提到的TTransport接口,还有一个用于接收或创建原始传输对象的TserverTransport接口。它的接口如下:
open:打开transport
listen:开始监听连接
accept:返回一个新的客户端transport
close:关闭transport
3.2 实现
传输接口是为了在任何编程语言中的简单实现而设计的。应用程序开发人员可以根据需求轻松定义新的传输机制。
3.2.1 TSocket
TSocket类是被所有目标语言实现的。它为TCP/IP套接字流提供了通用的、简单的接口。
3.2.2 TFileTransport
TFileTransport是磁盘文件到数据流的抽象。它可以将一组Thrift请求写到磁盘文件上。可以从日志中回放磁盘上的数据,用于加工处理或者再生成/模拟之前的事件。
3.2.3 Utilities
传输接口被设计成能够支持通过OOP技术进行简单的扩展,比如组合。一些简单的工具包括:TbufferedTransport,可以将读写缓冲在底层的传输上;TFramedTransport,以帧大小传输数据进行分块优化或者非阻塞操作;TMemoryBuffer,可以从进程的堆、栈内存空间直接读写数据。
4 Protocol
Thrift中第二个主要的抽象是将数据结构与传输表示分离。在传输数据时,Thrift会强制执行一个消息结构,但是这个消息结构对当前的协议编码是未知的。也就是说,无论数据被编码成XML,人工可读的ASCII或者很长的二进制形式,都不重要,只要数据支持一组确定的操作,能够允许生成的代码对这些数据进行决定性的读写就行。
4.1 接口
Thrift协议接口非常简单。它从根本上支持两件事:1)双向有序消息2)基类型、容器和结构体的编码。
writeMessageBegin(name, type, seq)
writeMessageEnd()
writeStructBegin(name)
writeStructEnd()
writeFieldBegin(name, type, id)
writeFieldEnd()
writeFieldStop()
writeMapBegin(name, type, id)
writeMapEnd()
writeListBegin(ktype, vtype, size)
writeListEnd()
writeSetBegin(etype, size)
writeSetEnd()
writeBool(bool)
writeByte(byte)
writeI16(i16)
writeI32(i32)
writeI64(i64)
writeDouble(double)
writeString(string)

name, type, seq = readMessageBegin()
readMessageEnd()
name = readStructBegin()
readStructEnd()
name, type, id = readFieldBegin()
readFieldEnd()
k, v, size = readMapBegin()
readMapEnd()
etype, size = readListBegin()
readListEnd()
etype, size = readSetBegin()
readSetEnd()
bool = readBool()
byte = readByte()
i16 = readI16()
i32 = readI32()
i64 = readI64()
double = readDouble()
string = readString()

注意,除了writeFieldStop()之外,每一个写方法都有一个配对的读方法。这是一个特殊的方法,它指示结构体的结束。读取结构体的过程是readFieldBegin(),直到遇到stop字段,然后readFieldEnd()。生成的代码依靠着调用序列,确保一个协议编码器所写的一切内容都能被配对的协议解码器读取。需要进一步注意的是,这一组功能相对必要性而言,在设计上更健壮。例如,writeStructEnd()不是严格必要的,因为结构体的结尾处可能stop字段进行指示。这个方法对于冗长的协议来说是一个便利,在冗长的协议中,它可以更清晰地分开这些调用(比如XML中的结束标记)。
4.2 结构
Thrift的结构被设计成能够支持编码成协议流。实现不应该需要构造或计算结构整个数据的长度,而应优先对其进行编码。在很多场景中,这对性能非常重要。考虑一长串相对较大的字符串。如果协议接口被要求将读写一个列表作为原子操作,那么实现需要在编码任何数据之前对整个列表进行线性遍历。然而,如果能被写成迭代的列表被执行,相应的读操作可能并行执行,理论上提供了一个端到端的(kN—C)加速,N是列表的大小,k是与序列化单个元素相关的因素,C是在数据正在被写和变成可读之间的固定延迟偏移。
类似地,结构不会优先编码其数据的长度。相反,结构体被编码为字段序列,每个字段有一个类型说明符和一个唯一的字段标识符。注意,包含类型说明符可以允许协议在没有任何生成的代码或者访问原始IDL文件的条件下安全地解析和解译。结构体由具有特殊的STOP类型的字段头终止。因为所有的基本类型都可以被明确地读取,所有结构体(即使是包含其他结构体的结构体)可以被明确地读取。Thrift协议是自定届的,没有任何的框架,无关于编码格式。在流是不必要的或者结构是优点的场景中,Thrift协议可以轻易地通过TFrameTransport抽象加入到传输层。
4.3 实现
Facebook已经实现并部署了一个节省空间的二进制协议,该协议被大部分后台服务使用。本质上,它以扁平二进制的形式写所有数据。整数类型被转换为网络字节序,字符串以其字节长度作为前缀,所有的消息和字段头都是用基本的整数序列化结构写的。字段的字符串名称被省去了——当使用生成的代码时,字段标识符就足够了。
为了代码的简单和清晰,我们决定不使用一些极端的存储优化(即:将小整数打包成ASCII或者使用7位连续格式)。当我们遇到性能关键的使用场景时,可以很容易进行这些更改。
5 版本管理
在面对版本控制和数据定义变更时,Thrift是非常健壮的。这对已部署的服务进行分阶段的升级是至关重要的。系统必须支持从日志文件中读取数据,以及从过时的客户端向新服务器发送请求,反之亦然。
5.1 字段标识符
Thrift中的版本控制是通过字段标识符实现的。Thrift结构体中的每一个成员的字段头都被一个唯一的字段标识符编码。字段标识符和类型说明符的组合使得字段被唯一确定。Thrift定义语言支持自动分配字段标识符,但是始终显式指定字段标识符是一个好的编程做法。标识符指定如下:
struct Example {
1:i32 number=10,
2:i64 bigNumber,
3:double decimals,
4:string name=”thrifty”
}
为了避免手动和自动分配的标识符之间的冲突,缺省标识符的字段会被分配一个从-1递减的标识符,Thrift只支持分配正的标识符。
当数据被反序列化时,生成的代码可以通过这些标识符正确识别字段,并且确定其在定义文件中是否有对应的字段。如果一个字段标识符无法识别,生成的代码可以通过类型说明符跳过该未知字段,且不会有任何错误。这可能是因为所有的数据类型都是自定届的。
字段标识符可以(也应该)在方法的参数列表中被指定。事实上,参数列表不仅在后端表示为结构体,实际上在编译器前段共享相同的代码。这允许对方法参数进行版本安全的更改。
service StringCache {
void set(1:i32 key, 2:string value),
string get(1:i32 key) thows (1:KeyNotFound knf),
void delete(1:i32 key)
}
指定字段标识符的语法被选择能够响应它们的结构。结构可以看做是一个字典,标识符是键,值是强类型命名字段。
字段标识符内部使用Thrift类型中的i16。注意,然而TProtocol抽象可以以任意格式编码标识符。
5.2 变量名
当遇到意外字段时,它可被安全地忽略或者丢弃。当没有找到预期字段时,必须有某种方式告知开发人员——字段没有出现。通过所定义对象内部的isset实现。(isset功能是隐藏性的,在PHP中是null,在Python中是none,在Ruby中是nil)。本质上来讲,每个Thrift内置的isset为每个字段包含一个布尔值,用于指示该字段是否存在于结构体中。当读取器接收到一个结构体时,应该在直接操作之前检查正在被设置的字段。
class Example{
public:
Example():
Number(10),
bigNumber(0),
decimals(0),
name(“thrifty”){}

int32_t number;
int64_t bigNumber;
double decimals;
std::string name;

struct __isset{
–isset():
Number(false),
bigNumber(false),
decimals(false),
name(false){}
bool number;
bool bigNumber;
bool decimals;
bool name;
} --isset;

}
5.3 实例分析
在四种情景中可能出现版本不匹配。
1.添加字段,旧的客户端,新的服务端:在这种情况下,旧的客户端不会发送新的字段。新的服务端认为字段没有被设置,并且为过时的请求实现默认的行为。
2.移除字段,旧的客户端,新的服务器:在这种情况下,旧的客户端依旧发送被移除的字段,新的服务器直接忽略该字段。
3.添加字段,新的客户端,旧的服务端:新的客户端发送一个旧服务端不识别的字段,旧的服务端忽略该字段并且照常处理。
4.移除字段,新的客户端,旧的服务端:这是最危险的情况。因为旧的服务器不大可能为缺失的字段提供合适的默认行为。在这种情况下,建议在新客户端之前先推出新服务端。
5.4 协议/传输版本控制
TProtocol抽象被设计为允许协议实现以它们认为合适的方式自由地对自己进行版本管理。具体来说,任何协议实现都可以自由地在writeMessageBegin()调用中发送它喜欢的任何内容。如何在实现级处理版本控制完全取决于实现者。关键是协议编码的更改和接口定义语言的更改是完全隔离的。
注意,TTransport接口也是一样的。比如,我们希望向TFileTransport添加一些新的校验和或者错误检测,我们可以简单地将一个版本头添加到它写入文件的数据中。这样它仍然可以接受没有给定头的旧日志文件。
6 RPC实现
6.1 TProcessor
Thrift设计中最后一个核心接口是TProcessor,这可能是最简单的构造。接口如下:
interface TProcessor {
bool process(TProtocol in, TProtocol out)
throws TException
}
这儿的关键设计思想是,我们构建的复杂系统可以从根本上分解为输入和输出的代理或服务。在大多数情况下,实际上只有一个输入和输出(一个RPC客户端)需要处理。
6.2 生成的代码
当定义一个服务时,我们生成一个TProcessor实例,该实例能够使用一些帮助程序处理对该服务的RPC请求。基本结构(伪C++)如下:
Service.thrift
=> Service.cpp
Interface ServiceIf
Class ServiceClient : virtual ServiceIf
TProtocol in
TProtocol out
Class ServiceProcessor : TProcessor
ServiceIf handler

ServiceHandler.cpp
Class ServiceHandler : virtual ServiceIf

TServer.cpp
TServer(TProcessor prcessor,
TServerTransport transport,
TTransportFactory tfactory,
TProtocolFactory pfactory)
serve()
从Thrift定义文件,我们生成虚服务接口。一个客户端类被生成,该类实现了接口并使用两个TProcotol实例执行I/O操作。生成的处理器实现TProcessor接口。生成的代码具有通过process()调用处理RPC调用的所有逻辑,并将服务接口的实例作为参数,由应用程序开发人员实现。
用户用独立的、非生成的源码提供应用程序接口的实现。
6.3 TServer
最后,Thrift核心库提供了TServer抽象。TServer对象通常工作如下。
1.使用TServerTransport获得一个TTransport
2.使用TTransportFactory可选地将原始传输转换为适当的应用传输(这里通常使用TBufferedTransportFactory)
3.使用TProtocolFactory为TTransport创建一个输入输出协议
4.调用TProcessor对象的process()方法
这些层被适当地分开,这样服务端代码就不需要知道正在运行的任何传输、编码或应用程序。当处理器处理RPC时,服务器封装围绕连接处理、线程等的逻辑。应用程序开发人员唯一要编写的代码在于定义Thrift文件和接口实现。
Facebook已经部署了多个Server实现,包括single-threaded TSimpleServer,thread-per-connection TThreadedServer和thread-pooling TThreadPoolServer。
TProcessor接口在设计上非常通用。没有要求TServer接受生成的TProcessor对象。Thrift允许应用程序开发人员轻松地编写任何类型的服务器来操作TProcotol对象(比如,一个服务器可以简单地流处理特定类型的对象,而不需要任何实际的RPC方法调用)。
7 实现细节
7.1 目标语言
Thrift目前支持五种语言:C++,Java,Python,Ruby和PHP。在Facebook,我们主要使用C++,Java和Python部署服务器。在PHP中实现的Thrift服务也嵌入到了Apache web服务器中,使用一个TTransport接口的THttpClient实现提供对我们许多前端构造的透明后端访问。
尽管Thrift被设计的比典型的web技术更高效、更健壮,在我们设计基于XML的REST web服务API时,我们注意到Thrift可以轻松地用来定义我们的服务接口。虽然目前我们不适用SOAP信封(在作者看来,已经有太多重复的企业级Java软件做这样的事),我们能够快速扩展Thrift来为我们的服务生成XML结构定义文件,以及为我们管理不同版本的web服务实现提供框架。虽然公共web服务无关于Thrift的核心用例和场景,Thrift促进了快速迭代并且为我们提供了将整个基于XML的web服务迁移到高性能系统上(如果需要的话)的能力。
7.2 生成的结构
我们有意识地决定让生成的结构尽可能地透明。所有的字段均可公开访问,没有get()和set()。类似地,不强制使用isset对象。我们不包含任何FieldNotSetException构造。开发人员可以选择使用这些字段编写更健壮的代码,但是对于完全忽略isset构造的开发人员来说,系统是健壮的,并且在所有情况下提供合适的默认行为。
这个选择的动机是简化应用程序开发。我们声明的目标是不是让开发人员学习所选语言的丰富的新库,而是生成允许他们使用各自语言熟悉的结构的代码。
我们还将生成对象的read()和write()公开,以便这些对象能在RPC客户端和服务端上下文之外可以被使用。Thrift是一个有用的工具,它可以简单地生成易于跨语言序列化的对象。
7.3 RPC方法识别
RPC中的方法调用是通过把方法名作为一个字符串发送而实现的。这种方法的一个问题是,较长的方法名需要更多的带宽。我们尝试使用固定大小的哈希来识别方法,但最后得出的结论是节省出来的带宽不足以抹平这么做带来的麻烦。如果没有元存储系统,可靠地处理接口定义文件中的不同版本之间的冲突是不可能的(也就是说,要为文件的当前版本生成不冲突的散列,我们必须了解文件的任何以前版本中曾经存在的所有冲突)。
我们希望在方法调用时避免太多不必要的字符串比较。为了处理这个问题,我们生成从字符串到函数指针的映射,以便在通常情况下通过定时哈希查询有效地完成调用。这需要使用一些有趣的代码构造。因为Java没有函数指针,流程函数都是实现公共接口的私有成员类。
private class ping implements ProcessFunction {
public void process(int seqid, TProtocol iprot, TProtocol oprot)
throws TException
{ … }
}

HashMap<String,ProcessFunction> processMap_ =
new HashMap<>(String,ProcessFunction);
在C++中,我们使用一个相对深奥的语言结构:成员函数指针。
std::map<std::string,
void(ExampleServiceProcessor::)(int32_t,
facebook::thrift::protocol::TProtocol
,
facebook::thrift::protocol::TProtocol*)>
processMap_;
使用这些技术,可以将字符串处理的成本降到最低,并且通过检查已知字符串方法名称,可以轻松调试损坏或误解的数据。
7.4 服务器和多线程
Thrift服务器需要基本的多线程来处理来自多个客户端的同时请求。对于Thrift服务端逻辑的Python和Java实现,这些语言的标准库提供了丰富的支持。对于C++实现,不存在标准的多线程运行时库。具体来说,不存在健壮、轻量级和可移植的线程管理器和计时器类实现。我们研究了现有的实现,即boost::thread, boost::threadpool, ACE_Thread_Manager和ACE_Timer。
虽然boost::thread提供了干净、轻量级和健壮的多线程原语实现(互斥、条件、线程),但它不提供线程管理器或计时器实现。
boost::threadpool看起来很有希望,但是对于我们的目的来说还不够。我们希望尽可能地限制对第三方库的依赖。因为boost::threadpool不是一个纯模板库,并且需要运行时库,而且因为它不是官方boost发行版的一部分,我们认为它还没有准备好在Thrift中使用。随着boost::threadpool的发展,特别是如果它被加入到boost的发行版中,我们可能会重新考虑不使用它的决定。
除了多线程原语之外,ACE还就有线程管理器和计时器类。最大的问题是它是ACE。与Boost不同,ACE API的质量很差。ACE中的所有东西都对ACE中的其他东西有大量依赖关系——因此迫使开发人员丢弃标准类,比如:STL集合,转而使用ACE的实现。此外,与Boost不同的是,ACE实现显现出对C++编程的功能和缺陷了解甚少,也没有利用现代化的模板技术来确保编译时安全和合理的编译器错误信息。由于这些原因,ACE被拒绝了。代替的是,我们选择实现我们自己的库,如下文所述。
7.5 线程原语
Thrift线程库在命名空间facebook::thrift::concurrency中实现,并拥有三个组件:
primitives
thread pool manager
timer manager
如上文所述,我们不愿在Thrift上再增加依赖。我们决定使用boost::shared_ptr,因为它对于多线程应用程序非常有用,它不需要任何链接时库或者运行时库(即它是一个纯模板库),并且它将成为C++0X标准的一部分。
我们实现了标准的互斥和条件类,以及一个Monitor类。后者是互斥量和条件量的简单组合,类似于为Java对象类提供的Monitor实现。这有时也被称为障碍。我们提供了一个同步保护类来允许类似Java的同步块。这只是一点语法糖,但是,与它的Java等价物一样,它清除地划分了代码的关键部分。与Java等价物不同的是,我们仍然能够以编程方式锁定、解锁、阻塞和信号监视。
void run() {
{Synchronized s(manager->monitor);
if(manger->state == TimerManager::STARTING){
manager->state = TimerManager::STARTED;
manager->monitor.notifyAll();
}
}
}
我们再次借用了Java中线程和可运行类之间的区别。线程是实际的可调度对象。Runnable是在线程中执行的逻辑。线程实现处理所有特定于平台的线程创建和销毁问题,而Runnable实现处理特定于应用程序的每个线程逻辑。这种方法的好处是,开发人员可以容易地子类化Runnable类,而不需要引入特定平台的超类。
7.6 Thread,Runnable,and shared_ptr
我们在ThreadManager和TimerManager实现中使用boost::share_ptr来确保清除那些能被多线程访问的死对象。对于线程类实现,boost::shared_ptr的使用需要特别注意,确保线程对象在创建和关闭时不会被泄漏或过早地取消引用。
线程的创建需要调用C库。(在我们的例子中,POSIX线程库是libpthread,但是WIN32线程也是如此。)通常,OS很少(如果有的话)保证什么时候调用ThreadMain(C线程的入口点函数)。因此,我们的线程创建调用,ThreadFactory::newThread()可能会在那个时间之前返回给调用者。为了确保返回的线程对象不会被过早地清除,如果调用者在ThreadMain调用之前放弃了它的引用,线程对象在它的start方法中对自己做了一个弱引用。
有了弱引用在手,ThreadMain函数可以尝试在进入绑定到线程的Runnable对象的Runnable::run方法之前获得一个强引用。如果在退出Thread::start和进入ThreadMain之间没有获得对线程的强引用,弱引用将返回null,并且方法立即返回。
线程对自身进行弱引用的这个需求对API有重要的意义。由于引用是通过boost::shared_ptr管理的,线程对象必须有一个对自身的引用,该引用由返回给调用者的相同boost::shared_ptr信封包装。这使工厂模式必须被使用。ThreadFactory创建原始线程对象和一个boost::shared_ptr包装器,并且调用实现Thread接口的类的私有helper方法(在本例中是PosixThread::weakRef),以便允许它通过boost::shared_ptr信封向自身添加弱引用。
线程和可运行对象互相引用。一个可运行对象可能需要知道它正在执行的线程,而一个线程显然需要知道它托管的是什么Runnable对象。这种互相依赖的关系更加复杂,因为每个对象的生命周期彼此独立。一个应用程序可以创建一组Runnable对象,以便在不同的线程中重复使用,也可以在创建并启动一个线程后创建并忘记Runnable对象。
线程类在其构造函数中接收对托管Runnable对象的boost::shared_ptr引用,而Runnable类有一个显示的线程方法来允许显示绑定托管线程。ThreadFactory::newThread将对象彼此绑定。
7.7 线程管理器
线程管理器创建一个工作线程池,并允许应用程序在空闲工作线程可用时为执行调度任务。线程管理器不实现动态线程池大小调整,但提供了基本类型,以便应用程序可以根据负载添加或移除线程。之所以选择这种方法,是因为实现负载指标和线程池大小是非常特定于具体应用程序的。例如,一些应用程序可能希望根据由调查样本测量而得的工作到达率的滑动平均值来调整线程池大小。其他的应用程序可能只希望对工作队列深度、高水位、低水位立即做出反应。我们不需要创建一个足够复杂的API抽象来捕获这些不同的方法,我们只需要将其留给特定的应用程序,为其提供原语来设定期望的策略和样本现状。
7.8 计时器管理器
计时器管理器允许应用程序在将来的某个时间点为执行调度Runnable对象。它的特定任务是允许应用程序定期取样ThreadManager负载,并根据应用程序策略更改线程池大小。当然,它可以用来生成任意数量的定时器或报警事件。
ThreadManager的默认实现是使用一个单独的线程来执行到期的Runnable对象。因此,如果计时器操作需要做大量的工作,尤其是需要阻塞I/O时,应该在单独的线程中完成。
7.9 非阻塞操作
虽然Thrift传输接口更直接地映射到阻塞I/O模型,但是我们在C++中实现了一个基于libevent和TFramedTransport的高性能TNonBlockingServer。我们通过使用状态机将所有I/O移动到一个紧密的事件循环中来实现这一点。本质上,事件循环将框架请求读入TMemoryBuffer对象。一旦整个请求就绪,它们就被发送到TProcessor对象,该对象可以从内存中的数据读取数据。
7.10 编译器
Thrift编译器使用C++实现,使用标准的lex/yacc进行词法分析和语法分析。虽然它可以在其他语言(例如:Python Lex-Yacc(PLY) 或者ocamlyacc)中以更少的代码实现,但是使用C++强制显式地定义语言结构。对解析树元素(可辩论地)进行强类型的输入使新的开发人员更容易接近代码。
代码生成使用两次传递完成。第一次传递只查找包含文件和类型定义。在此阶段不检查类型定义,因为它们可能依赖于包含文件。所有的包含文件在第一次传递中按顺序扫描。一旦包含树被解析,将对所有文件进行二次遍历:将类型定义插入到解析树中,并对任何未定义的类型引发错误。然后根据解析树生成程序。
由于固有的复杂性和循环依赖的潜在性,我们明确拒绝前向声明。两个Thrift结构不能各自包含另一个结构的实例。(由于生成的C++代码中不允许空结构实例,这实际上是不可能的)
7.11 TFileTransport
TFileTransport通过设置传入数据的长度并且将其写入磁盘,从而记录Thrift的请求/结构。使用带边框的磁盘格式可以更好的检查的错误,并有助于处理有限数量的离散事件。TFileWriteTransport使用一个交换内存缓冲区的系统来确保在记录大量数据时的良好性能。一个Thrift日志文件被分割成指定大小的块,已记录的消息不允许跨越块的边界。跨边界的消息将导致填充物被添加,直到块的结尾和消息的第一个字节被对齐到下一个块的开头为止。将文件分割成块使得从文件的特定点读取和结束数据成为可能。
8 Facebook Thrift Service
Thrift在Facebook被大量应用在大量的应用程序中,包括搜索、日志、移动业务、广告和开发者平台。下面讨论两个具体的用法。
8.1 搜索
Thrift被用作Facebook搜索服务的底层协议和传输层。多语言代码生成非常适合搜索,因为它允许使用高效的服务端语言(C++)开发应用程序,并且允许基于Facebook PHP的web应用程序使用Thrift PHP库调用搜索服务。还有大量基于生成的Python代码构建的搜索统计、部署和测试功能。此外,Thrift日志文件格式是被用作提供实时搜索索引更新的重做日志。Thrift也允许搜索团队利用每种语言的优势并快速开发代码。
8.2 日志
Thrift TFileTransport功能用于结构化日志记录。可以将每个服务函数定义以及其参数视为由函数名标识的结构化日志条目。然后可以将此日志用于各种目的,包括内联和脱机处理、统计信息聚合和重做日志。
9 总结
Thrift使Facebook能够通过工程师的分工协作高效地构建可伸缩的后台服务。应用程序开发人员可以专注于应用程序代码,而不必担心套接字层。我们通过在一个地方编写缓冲和I/O逻辑来避免重复的工作,而不是将其穿插在每个应用程序中。
Thrift在Facebook得到了大量的应用,包括搜索、日志、移动业务、广告和开发者平台。我们发现,额外的软件抽象层所带来的边际性能成本被开发人员的效率和系统可靠性所带来的收益远远超过。
10 类似的系统
下面是与Thrift类似的软件系统。每个都是(非常)简要的描述:
SOAP:基于XML的,通过HTTP为web服务设计的,过多的XML解析开销。
CORBA:相对全面,有过度设计的争议,重量级的。安装相当麻烦。
COM:主要包含在Windows客户端软件。不是一个完全开放的解决方案。
Pillar:轻量级,高性能,但是缺少版本控制和抽象。
Protocol Buffers:闭源的,谷歌所有,Sawzall论文上有相关描述。
11 鸣谢
感谢Martin Smith,Karl Voskuil和Yishan Wong对于Thrift(以及火的极端考验)的反馈。
Thrift是Pillar的继承者。Pillar是AdamD’Angelo开发的一个类似系统,最初是在加州理工学院开发的,后来在Facebook继续开发。没有Adam的真知灼见,Thrift根本不会发生。
12 参考文献
[1] Kempf, William, “Boost.Thread”, http://www.boost.org/doc/html/threads.html
[2]Henkel, Philipp, “threadpool”, http://threadpool.sourceforge.net

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值