Thrift:可扩展的跨语言服务实现

官网地址

https://thrift.apache.org/

windows 安装文档 Apache Thrift - Windows Install

原文地址(Thrift白皮书):https://thrift.apache.org/static/files/thrift-20070401.pdf

Thrift:可扩展的跨语言服务实现

作者: Mark Slee、Aditya Agarwal 、Marc Kwiatkowski

           Facebook,156 University Ave,Palo Alto,CA {mcslee,aditya,marc}@facebook.com

摘要

Thrift is a software library and set of code-generation tools developed at Facebook to expedite development and implementation of efficient and scalable backend services. Its primary goal is to enable efficient and reliable communication across programming languages by abstracting the portions of each language that tend to require the most customization into a common library that is implemented in each language. Specifically, Thrift allows developers to define datatypes and service interfaces in a single language-neutral file and generate all the necessary code to build RPC clients and servers.
This paper details the motivations and design choices we made in Thrift, as well as some of the more interesting implementation details. It is not intended to be taken as research, but rather it is an exposition on what we did and why.

Thrift是Facebook开发的一个软件库和一套代码生成工具,用于加 快开发和实施高效和可扩展的后端服务。它的主要目标是通过将每种语言中需要最多定制的部分抽象到一个在每种语言中实现的公共库中,来实现跨编程语言的有效和可靠的通信。具体来说,Thrift 允许开发人员在一个语言中立的文件中定义数据库和服务接口,并生成所有必要的代码来构建远程服务调用客户端和服务器。
本文详细介绍了我们在Thrift中所做的动机和设计选择,以及一些更有趣的实现细节。本文并非是对thrift的深入研究,而是对我们所做的事情和原因的阐述。
1. Introduction
As Facebook’s traffic and network structure have scaled, the resource demands of many operations on the site (i.e. search, ad selection and delivery, event logging) have presented technical requirements drastically outside the scope of the LAMP framework.
In our implementation of these services, various programming languages have been selected to optimize for the right combination of performance, ease and speed of development, availability of existing libraries, etc. By and large, Facebook’s engineering culture has tended towards choosing the best tools and implementations available over standardizing on any one programming language and begrudgingly accepting its inherent limitations.
Given this design choice, we were presented with the challenge of building a transparent, high-performance bridge across many programming languages. We found that most available solutions were either too limited, did not offer sufficient datatype freedom, or suffered from subpar performance. 1(See Appendix A for a discussion of alternative systems. )
The solution that we have implemented combines a language-neutral software stack implemented across numerous programming languages and an associated code generation engine that transforms a simple interface and data definition language into client and server remote procedure call libraries. Choosing static code generation over a dynamic system allows us to create validated code that can be run without the need for any advanced introspective run-time type checking. It is also designed to be as simple as possible for the developer, who can typically define all the necessary data structures and interfaces for a complex service in a single
short file.
Surprised that a robust open solution to these relatively common problems did not yet exist, we committed early on to making the Thrift implementation open source.
In evaluating the challenges of cross-language interaction in a networked environment, some key components were identified:
Types. A common type system must exist across programming languages without requiring that the application developer use custom Thrift datatypes or write their own serialization code. That is, a C++ programmer should be able to transparently exchange a strongly typed STL map for a dynamic Python dictionary. Neither programmer should be forced to write any code below the application layer to achieve this. Section 2 details the Thrift type system.
Transport. Each language must have a common interface to bidirectional raw data transport. The specifics of how a given transport is implemented should not matter to the service developer. The same application code should be able to run against TCP stream sockets, raw data in memory, or files on disk. Section 3 details the Thrift Transport layer.
Protocol. Datatypes must have some way of using the Transport layer to encode and decode themselves. Again, the application developer need not be concerned by this layer. Whether the service uses an XML or binary protocol is immaterial to the application code. All that matters is that the data can be read and written in a consistent, deterministic matter. Section 4 details the Thrift
Protocol layer.
Versioning. For robust services, the involved datatypes must provide a mechanism for versioning themselves. Specifically, it should be possible to add or remove fields in an object or alter the argument list of a function without any interruption in service (or, worse yet, nasty segmentation faults). Section 5 details Thrift’s versioning system.
Processors. Finally, we generate code capable of processing data streams to accomplish remote procedure calls. Section 6 details the generated code and TProcessor paradigm.
Section 7 discusses implementation details, and Section 8 describes our conclusions.

1. 介绍

随着Facebook的流量和网络结构的扩大,网站上许多操作(即搜 索,广告选择和交付,事件记录)的资源需求已经提出了技术要求,大大超出了LAMP框架的范围。在我们实现这些服务的过程中, 我们选择了各种编程语言来优化性能、开发的易用性和速度、现有库的可用性等的正确组合。总的来说,Facebook的工程文化倾向于选择最好的工具和实现,而不是标准化任何一种编程语言,并不情愿地接受其固有的局限性。
鉴于这种设计选择,我们面临的挑战是在许多编程语言之间建立一 个透明,高性能的桥梁。我们发现,大多数可用的解决方案要么太有限,没有提供足够的数据类型自由,或遭受低于标准的性能。 我们实现的解决方案结合了一个跨多种编程语言实现的语言中性软件栈和一个相关的代码生成引擎,该引擎将简单的接口和数据定义语言转换为客户端和服务器远程过程调用库。选择静态代码生成而不是动态系统允许我们创建经过验证的代码,这些代码可以在不需要任何高级内省运行时类型检查的情况下运行。它还被设计为对开发人员来说尽可能简单,开发人员通常可以在一个简短的文件中为复杂的服务定义所有必要的数据结构和接口。
对于这些相对常见的问题还没有一个健壮的开放解决方案,我们感到惊讶,我们很早就承诺将Thrift实现开源。(有关替代系统的讨论,请参见附录A。 )
在评估网络环境中跨语言互动的挑战时,确定了一些关键组成部分:
类型通用:类型系统必须跨编程语言存在,而不需要应用程序开发人员使用自定义Thrift数据库或编写自己的序列化代码。也就是说, C++程序员应该能够透明地将强类型STL映射交换为动态Python字典。任何程序员都不应该被迫在应用程序层以下编写任何代码来实现这一点。
第2节详细介绍了Thrift 类型系统。
运输每种语言都必须有一个双向原始数据传输的通用接口。如何实现给定传输的细节对服务开发人员来说并不重要。相同的应用程序代码应该能够针对TCP流套接字、内存中的原始数据或磁盘上的文件运行。
第3节详细介绍了Thrift Transport层。 议定书数据库必须有某种方法使用传输层来编码和解码自己。同样,应用程序开发人员不需要关心这一层。服务使用XML还是二进制协议对应用程序代码来说并不重要。重要的是,数据可以以一致的、确定性的方式被读取和写入。
第4节详细介绍了Thrift 协议层。
版本化。对于健壮的服务,所涉及的数据库必须提供一种对自身进行版本控制的机制。具体来说,应该可以在对象中添加或删除字段,或者修改函数的参数列表,而不会中断服务(或者更糟糕的
是,讨厌的分段错误)。
第5节详细介绍了Thrift的版本控制系统。
处理器.最后,我们生成能够处理数据流的代码来完成远程过程调用。
第6节详细介绍了生成的代码和TProcessor范例。
第7节讨论了实现细节,第8节描述了我们的结论。
2. Types
The goal of the Thrift type system is to enable programmers to develop using completely natively defined types, no matter what programming language they use. By design, the Thrift type system
does not introduce any special dynamic types or wrapper objects. It also does not require that the developer write any code for object serialization or transport. The Thrift IDL (Interface Definition Language) file is logically a way for developers to annotate their data structures with the minimal amount of extra information necessary to tell a code generator how to safely transport the objects across languages.
2.1 Base Types
The type system rests upon a few base types. In considering which types to support, we aimed for clarity and simplicity over abundance, focusing on the key types available in all programming lan guages, ommitting any niche types available only in specific languages.
The base types supported by Thrift are:
bool A boolean value, true or false
byte A signed byte
i16 A 16-bit signed integer
i32 A 32-bit signed integer
i64 A 64-bit signed integer
double A 64-bit floating point number
string An encoding-agnostic text or binary string Of particular note is the absence of unsigned integer types. Because these types have no direct translation to native primitive types in many languages, the advantages they afford are lost. Further, there is no way to prevent the application developer in a language like Python from assigning a negative value to an integer variable,leading to unpredictable behavior. From a design standpoint, we observed that unsigned integers were very rarely, if ever, used for arithmetic purposes, but in practice were much more often used as keys or identifiers. In this case, the sign is irrelevant. Signed integers serve this same purpose and can be safely cast to their unsigned counterparts (most commonly in C++) when absolutely necessary.
2.2 Structs
A Thrift struct defines a common object to be used across languages. A struct is essentially equivalent to a class in object oriented programming languages. A struct has a set of strongly typed fields, each with a unique name identifier. The basic syntax for defining a Thrift struct looks very similar to a C struct definition.
Fields may be annotated with an integer field identifier (unique to the scope of that struct) and optional default values. Field identifiers will be automatically assigned if omitted, though they are strongly encouraged for versioning reasons discussed later.
2.3 Containers
Thrift containers are strongly typed containers that map to the most commonly used containers in common programming languages.
They are annotated using the C++ template (or Java Generics) style.
There are three types available:
list<type> An ordered list of elements. Translates directly into an STL vector , Java ArrayList , or native array in scripting languages. May contain duplicates.
set<type> An unordered set of unique elements. Translates into an STL set , Java HashSet , set in Python, or native dictionary in PHP/Ruby.
map<type1,type2> A map of strictly unique keys to values Translates into an STL map , Java HashMap , PHP associative array, or Python/Ruby dictionary.
While defaults are provided, the type mappings are not explicitly fixed. Custom code generator directives have been added to substitute custom types in destination languages (i.e. hash map or
Google’s sparse hash map can be used in C++). The only requirement is that the custom types support all the necessary iteration primitives. Container elements may be of any valid Thrift type, including other containers or structs.
struct Example {
1:i32 number=10,
2:i64 bigNumber,
3:double decimals,
4:string name="thrifty"
}
In the target language, each definition generates a type with two methods, read and write , which perform serialization and transport of the objects using a Thrift TProtocol object.
2.4 Exceptions
Exceptions are syntactically and functionally equivalent to structs except that they are declared using the exception keyword instead of the struct keyword.
The generated objects inherit from an exception base class as appropriate in each target programming language, in order to seamlessly integrate with native exception handling in any given language. Again, the design emphasis is on making the code familiar to the application developer.
2.5 Services
Services are defined using Thrift types. Definition of a service is semantically equivalent to defining an interface (or a pure virtual abstract class) in object oriented programming. The Thrift compiler generates fully functional client and server stubs that implement the
interface. Services are defined as follows:
service <name> {
<returntype> <name>(<arguments>)
[throws (<exceptions>)]
...
}
An example:
service StringCache {
void set(1:i32 key, 2:string value),
string get(1:i32 key) throws (1:KeyNotFound knf),
void delete(1:i32 key)
}
Note that void is a valid type for a function return, in addition to all other defined Thrift types. Additionally, an async modifier keyword may be added to a void function, which will generate
code that does not wait for a response from the server. Note that a pure void function will return a response to the client which guarantees that the operation has completed on the server side. With async method calls the client will only be guaranteed that the request succeeded at the transport layer. (In many transport scenarios this is inherently unreliable due to the Byzantine Generals’ Problem. Therefore, application developers should take care only to use the async optimization in cases where dropped method calls are acceptable or the transport is known to be reliable.)
Also of note is the fact that argument lists and exception lists for functions are implemented as Thrift structs. All three constructs are identical in both notation and behavior.

2. 类型

Thrift类型系统的目标是使程序员能够使用完全本地定义的类型进行 开发,无论他们使用什么编程语言。通过设计,Thrift类型系统没有引入任何特殊的动态类型或包装器对象。它也不需要开发人员为对象序列化或传输编写任何代码。Thrift IDL(Interface Definition Language 接口定义语言)文件在逻辑上是开发人员注释其数据结构的一种方式,其所需的额外信息量最少,可以告诉代码生成器如何安全地跨语言传输对象。

2.1 基本类型

类型系统基于几个基本类型。在考虑支持哪些类型时,我们的目标是清晰和简单,而不是丰富,重点关注所有编程语言中可用的关键类型,忽略仅在特定语言中提供的任何利基类型。
Thrift支持的基本类型包括:
bool布尔值,true或false
byte有符号字节
i16 16位有符号整数
i32 32位有符号整数
i64 64位有符号整数
double 64位浮点数
string与编码无关的文本或二进制字符串
特别值得注意的是没有无符号整数类型。由于这些类型在许多语言中不能直接转换为本机基元类型,因此它们所提供的优势就丧失了。此外,没有办法阻止Python等语言的应用程序开发人员将负值分配给整数变量,从而导致不可预测的行为。从设计的角度来看,我们观察到无符号整数很少(如果有的话)用于算术目的,但在实践中更常用作键或标识符。在这种情况下,符号是无关紧要的。有符号整数也有同样的用途,并且在绝对必要时可以安全地转换为它们的无符号对应物(最常见于C++)。

2.2 Structs

Thrift结构定义了一个跨语言使用的公共对象。结构本质上等同于面向对象编程语言中的类。结构有一组强类型字段,每个字段都有唯一的名称标识符。定义Thrift结构体的基本语法看起来与C结构体定义非常相似。字段可以用整数字段标识符(对于该结构的作用域是唯一的)和可选的默认值进行注释。字段标识符如果被省略将被自动分配,尽管出于稍后讨论的版本控制原因,强烈建议使用它们。

2.3 容器

Thrift容器是强类型容器,映射到常见编程语言中最常用的容器。它们使用C++模板(或Java泛型)样式进行注释。有三种类型可用:
list元素的有序列表。直接转换为STL向量、Java ArrayList或脚本语言中的原生数组。可能包含重复项。
set唯一元素的无序集合。转换为STL集、Java HashSet、Python中的集或PHP/Ruby中的本地字典。
map<type 1,type 2>严格唯一键到值的映射转换为STL映射、Java HashMap、PHP关联数组或Python/Ruby字典。
虽然提供了默认值,但类型映射没有显式固定。自定义代码生成器指令已被添加,以替代目标语言中的自定义类型(例如,哈希映射或 Google的稀疏哈希映射可以在C++中使用)。唯一的要求是自定义类型支持所有必要的迭代基元。容器元素可以是任何有效的Thrift类型, 包括其他容器或结构。
struct Example {
1:i32 number=10,
2:i64 bigNumber,
3:double decimals,
4:string name="thrifty"
}
在目标语言中,每个定义生成一个类型,具有两个方法,读和写,它们使用Thrift TProtocol对象执行对象的序列化和传输。

2.4 例外

除了使用exception关键字而不是struct关键字声明它们之外,它们在语法和功能上都与struct等效。
所生成的对象在每种目标编程语言中适当地从异常基类继承,以便与任何给定语言中的本机异常处理无缝集成。同样,设计的重点是让应用程序开发人员熟悉代码。

2.5 服务

服务使用Thrift类型定义。服务的定义在语义上等同于在面向对象编程中定义接口(或纯虚拟抽象类)。Thrift编译器生成实现该接口的全功能客户端和服务器存根。服务定义如下:
service <name> {
<returntype> <name>(<arguments>)
[throws (<exceptions>)]
...
}
一个例子:
service StringCache {
void set(1:i32 key, 2:string value),
string get(1:i32 key) throws (1:KeyNotFound knf),
void delete(1:i32 key)
}
注意,除了所有其他定义的Thrift类型之外,void是函数返回的有效类型。此外,可以将async修饰符关键字添加到void函数中,这将生成不等待服务器响应的代码。请注意,纯void函数将向客户端返回一个响应,以确保操作已在服务器端完成。使用异步方法调用时,客户端只能保证请求在传输层成功。(在许多运输场景中,由于拜占庭将军问题,这本质上是不可靠的。因此,应用程序开发人员应该注意只在可以接受丢弃的方法调用或已知传输可靠的情况下使用异步优化。)
同样值得注意的是,函数的参数列表和异常列表是作为Thrift结构实现的。这三个构造在符号和行为上都是相同的。
3. Transport
The transport layer is used by the generated code to facilitate data transfer.
3.1 Interface
A key design choice in the implementation of Thrift was to decouple the transport layer from the code generation layer. Though Thrift is typically used on top of the TCP/IP stack with streaming
sockets as the base layer of communication, there was no compelling reason to build that constraint into the system. The performance tradeoff incurred by an abstracted I/O layer (roughly one virtual method lookup / function call per operation) was immaterial compared to the cost of actual I/O operations (typically invoking system calls).
Fundamentally, generated Thrift code only needs to know how to read and write data. The origin and destination of the data are irrelevant; it may be a socket, a segment of shared memory, or a
file on the local disk. The Thrift transport interface supports the following methods:
open Opens the tranpsort
close Closes the tranport
isOpen Indicates whether the transport is open
read Reads from the transport
write Writes to the transport
flush Forces any pending writes
There are a few additional methods not documented here which are used to aid in batching reads and optionally signaling the completion of a read or write operation from the generated code.
In addition to the above TTransport interface, there is a TServerTransport interface used to accept or create primitive
transport objects. Its interface is as follows:
open Opens the transport
listen Begins listening for connections
accept Returns a new client transport
close Closes the transport
3.2 Implementation
The transport interface is designed for simple implementation in any programming language. New transport mechanisms can be easily defined as needed by application developers.
3.2.1 TSocket
The TSocket class is implemented across all target languages. It provides a common, simple interface to a TCP/IP stream socket.
3.2.2
TFileTransport
The TFileTransport is an abstraction of an on-disk file to a data stream. It can be used to write out a set of incoming Thrift requests to a file on disk. The on-disk data can then be replayed from the log, either for post-processing or for reproduction and/or simulation of past events.
3.2.3 Utilities
The Transport interface is designed to support easy extension using common OOP techniques, such as composition. Some simple utilites include the TBufferedTransport , which buffers the
writes and reads on an underlying transport, the TFramedTransport , which transmits data with frame size headers for chunking optimization or nonblocking operation, and the TMemoryBuffer ,
which allows reading and writing directly from the heap or stack
memory owned by the process.

3. 传输

生成的代码使用传输层来促进数据传输。

3.1 接口

实现Thrift的一个关键设计选择是将传输层与代码生成层解耦。虽然Thrift通常用于TCP/IP协议栈的顶部, 套接字作为通信的基础层,没有令人信服的理由将该约束构建到系统中。与实际I/O操作(通常是调用系统调用)的成本相比,抽象 I/O层(每个操作大约一个虚拟方法查找/函数调用)所带来的性能折衷是无关紧要的。
从根本上说,生成的Thrift代码只需要知道如何读取和写入数据。
数据的来源和目的地是无关紧要的;它可能是一个套接字、一段共享内存或本地磁盘上的一个文件。Thrift传输接口支持以下方法:
open打开tranpsort
close关闭传输
isOpen指示传输是否打开
read从传输中读取
write写入传输
flush强制执行任何挂起的写入
还有一些其他的方法没有在这里记录,它们用于帮助重写,并可选地从生成的代码中发出读或写操作完成的信号。
除了上面的TTransport接口之外,还有一个TServerTransport接口用于接受或创建原始传输对象。其界面如下:
• open  打开运输
listen开始侦听连接
accept返回一个新的客户端传输
close关闭传输

3.2 执行

传输接口设计用于在任何编程语言中简单实现。应用程序开发人员可以根据需要轻松定义新的传输机制。

3.2.1 TSocket

TSocket类在所有目标语言中实现。它为TCP/IP流套接字提供了一个通用、简单的接口。

3.2.2 TFileTransport

TFileTransport是磁盘上文件到数据流的抽象。它可以用于将一组传入的Thrift请求写入磁盘上的文件。然后可以从日志中重放磁盘上的数据,用于后处理或用于再现和/或模拟过去的事件。

3.2.3 Utilities工具类

Transport接口被设计为支持使用常见的OOP技术(如组合)进行简单的扩展。一些简单的实用程序包括TBufferedTransport,它缓冲底层传输上的写入和读取; TFramedTransport,它传输带有帧大小报头的数据, 用于分块优化或非阻塞操作;以及TMemoryBuffer,它允许直接从进程拥有的堆或堆栈内存中进行阅读和写入。
4. Protocol
A second major abstraction in Thrift is the separation of datastructure from transport representation. Thrift enforces a certain messaging structure when transporting data, but it is agnostic to the protocol encoding in use. That is, it does not matter whether data is encoded as XML, human-readable ASCII, or a dense binary format as long as the data supports a fixed set of operations that allow it to be deterministically read and written by generated code.
4.1 Interface
The Thrift Protocol interface is very straightforward. It fundamentally supports two things: 1) bidirectional sequenced messaging, and 2) encoding of base types, containers, and structs.
writeMessageBegin(name, type, seq)
writeMessageEnd()
writeStructBegin(name)
writeStructEnd()
writeFieldBegin(name, type, id)
writeFieldEnd()
writeFieldStop()
writeMapBegin(ktype, vtype, size)
writeMapEnd()
writeListBegin(etype, 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()
Note that every write function has exactly one read counterpart, with the exception of writeFieldStop() . This is a special method that signals the end of a struct. The procedure for reading a struct is to readFieldBegin() until the stop field is encountered, and then to readStructEnd() . The generated code relies upon this call sequence to ensure that everything written by a protocol encoder can be read by a matching protocol decoder. Further note that this set of functions is by design more robust than necessary.
For example, writeStructEnd() is not strictly necessary, as the end of a struct may be implied by the stop field. This method is a convenience for verbose protocols in which it is cleaner to separate these calls (e.g. a closing </struct> tag in XML).
4.2 Structure
Thrift structures are designed to support encoding into a streaming protocol. The implementation should never need to frame or compute the entire data length of a structure prior to encoding it. This is critical to performance in many scenarios. Consider a long list of relatively large strings. If the protocol interface required reading or writing a list to be an atomic operation, then the implementation would need to perform a linear pass over the entire list before encoding any data. However, if the list can be written as iteration is performed, the corresponding read may begin in parallel, theoretically offering an end-to-end speedup of ( kN − C) , where N is the size of the list, k the cost factor associated with serializing a single element, and C is fixed offset for the delay between data being written and becoming available to read.
Similarly, structs do not encode their data lengths a priori. Instead, they are encoded as a sequence of fields, with each field having a type specifier and a unique field identifier. Note that the inclusion of type specifiers allows the protocol to be safely parsed and decoded without any generated code or access to the original IDL file. Structs are terminated by a field header with a special STOP type. Because all the basic types can be read deterministically, all structs (even those containing other structs) can be read deterministically. The Thrift protocol is self-delimiting without any framing and regardless of the encoding format.
In situations where streaming is unnecessary or framing is advantageous, it can be very simply added into the transport layer, using the TFramedTransport abstraction.
4.3 Implementation
Facebook has implemented and deployed a space-efficient binary protocol which is used by most backend services. Essentially, it writes all data in a flat binary format. Integer types are converted
to network byte order, strings are prepended with their byte length, and all message and field headers are written using the primitive integer serialization constructs. String names for fields are omitted - when using generated code, field identifiers are sufficient.
We decided against some extreme storage optimizations (i.e. packing small integers into ASCII or using a 7-bit continuation format) for the sake of simplicity and clarity in the code. These alter
ations can easily be made if and when we encounter a performancecritical use case that demands them.

4. 协议

Thrift中的第二个主要抽象是数据结构与传输表示的分离Thrift在传输数据时强制执行特定的消息传递结构,但它与所使用的协议编码无关。也就是说,数据编码为XML、人类可读的ASCII还是密
集的二进制格式并不重要,只要数据支持一组固定的操作,允许生成的代码确定地读取和写入数据。

4.1 接口

Thrift Protocol接口非常简单。它基本上支持两件事:
1)双向顺序消息传递,以及
2)基本类型,容器和结构的编码。
writeMessageBegin(name, type, seq)
writeMessageEnd()
writeStructBegin(name)
writeStructEnd()
writeFieldBegin(name, type, id)
writeFieldEnd()
writeFieldStop()
writeMapBegin(ktype, vtype, size)
writeMapEnd()
writeListBegin(etype, 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(),直到遇到停止字段,然后
readStructEnd()。生成的代码依赖于这个调用序列,以确保由协议编码器写入的所有内容都可以被匹配的协议解码器读取。进一步注意,这组函数通过设计比必要的更鲁棒。例如,writeStructEnd() 不是严格必要的,因为结构的结束可能由停止字段暗示。这个方法对于冗长协议来说很方便,因为它可以更清晰地分离这些调用(例如XML中的结束标记)。

4.2结构

Thrift结构被设计为支持编码成流协议。在对结构进行编码之前,实现永远不需要对结构的整个数据长度进行帧化或计算。这在许多情 况下对性能至关重要。考虑一长串相对较大的字符串。如果协议接口要求阅读或写入列表是原子操作,那么实现将需要在编码任何数据之前对整个列表执行线性传递。然而,如果可以在执行迭代时写入列表,则相应的读取可以并行地开始,理论上提供(kN-C)的端到端加速,其中N是列表的大小,k是与串行化单个元素相关联的成本因子,并且C是数据被写入和变得可用于读取之间的延迟的固定偏移。
类似地,结构体也不会先验地对它们的数据长度进行编码。相反, 它们被编码为字段序列,每个字段都有一个类型说明符和一个唯一的字段标识符。请注意,包含类型说明符允许安全地解析和解码协议,而无需生成任何代码或访问原始IDL文件。结构由具有特殊STOP类型的字段头终止。因为所有的基本类型都可以被确定性地读取,所以所有的结构(甚至是包含其他结构的结构)都可以被确定性地读取。Thrift协议是自定界的,没有任何框架,也不考虑编码格式。
在不需要流式传输或成帧比较有利的情况下,可以使用TFramedTransport抽象将其非常简单地添加到传输层中。

4.3 执行

Facebook已经实现并部署了一个节省空间的二进制协议,该协议被大多数后端服务使用。本质上,它以平面二进制格式写入所有数据。字符串类型被转换为网络字节顺序,字符串前面加上它们的字节长度,所有消息和字段头都使用原始整数序列化结构编写。字段的字符串名称被省略-当使用生成的代码时,字段标识符就足够了。
为了代码的简单和清晰,我们决定不进行一些极端的存储优化(例如,将小整数打包到ASCII中或使用7位连续格式)。如果我们遇到性能关键的用例需要这些更改,可以很容易地进行这些更改。
5. Versioning
Thrift is robust in the face of versioning and data definition
changes. This is critical to enable staged rollouts of changes to
deployed services. The system must be able to support reading of
old data from log files, as well as requests from out-of-date clients
to new servers, and vice versa.
5.1 Field Identifiers
Versioning in Thrift is implemented via field identifiers. The field
header for every member of a struct in Thrift is encoded with
a unique field identifier. The combination of this field identifier
and its type specifier is used to uniquely identify the field. The
Thrift definition language supports automatic assignment of field
identifiers, but it is good programming practice to always explicitly
specify field identifiers. Identifiers are specified as follows:
struct Example {
1:i32 number=10,
2:i64 bigNumber,
3:double decimals,
4:string name="thrifty"
}
To avoid conflicts between manually and automatically assigned
identifiers, fields with identifiers omitted are assigned identifiers
decrementing from -1, and the language only supports the manual
assignment of positive identifiers.
When data is being deserialized, the generated code can use these
identifiers to properly identify the field and determine whether it
aligns with a field in its definition file. If a field identifier is not
recognized, the generated code can use the type specifier to skip
the unknown field without any error. Again, this is possible due to
the fact that all datatypes are self delimiting.
Field identifiers can (and should) also be specified in function
argument lists. In fact, argument lists are not only represented as
structs on the backend, but actually share the same code in the
compiler frontend. This allows for version-safe modification of
method parameters
service StringCache {
void set(1:i32 key, 2:string value),
string get(1:i32 key) throws (1:KeyNotFound knf),
void delete(1:i32 key)
}
The syntax for specifying field identifiers was chosen to echo their
structure. Structs can be thought of as a dictionary where the iden
tifiers are keys, and the values are strongly-typed named fields.
Field identifiers internally use the i16 Thrift type. Note, however,
that the TProtocol abstraction may encode identifiers in any for
mat.
5.2 Isset
When an unexpected field is encountered, it can be safely ignored
and discarded. When an expected field is not found, there must be
some way to signal to the developer that it was not present. This
is implemented via an inner isset structure inside the defined
objects. (Isset functionality is implicit with a null value in PHP,
None in Python and nil in Ruby.) Essentially, the inner isset
object of each Thrift struct contains a boolean value for each field
which denotes whether or not that field is present in the struct.
When a reader receives a struct, it should check for a field being
set before operating directly on it.
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 Case Analysis
There are four cases in which version mismatches may occur.
1. Added field, old client, new server. In this case, the old client
does not send the new field. The new server recognizes that the
field is not set, and implements default behavior for out-of-date
requests.
2. Removed field, old client, new server. In this case, the old client
sends the removed field. The new server simply ignores it.
3. Added field, new client, old server. The new client sends a field
that the old server does not recognize. The old server simply
ignores it and processes as normal.
4. Removed field, new client, old server. This is the most danger
ous case, as the old server is unlikely to have suitable default
behavior implemented for the missing field. It is recommended
that in this situation the new server be rolled out prior to the
new clients.
5.4 Protocol/Transport Versioning
The TProtocol abstractions are also designed to give protocol
implementations the freedom to version themselves in whatever
manner they see fit. Specifically, any protocol implementation is
free to send whatever it likes in the writeMessageBegin() call.
It is entirely up to the implementor how to handle versioning at the
protocol level. The key point is that protocol encoding changes are
safely isolated from interface definition version changes.
Note that the exact same is true of the TTransport interface. For
example, if we wished to add some new checksumming or error
detection to the TFileTransport , we could simply add a version
header into the data it writes to the file in such a way that it would
still accept old log files without the given header.

5. 版本控制

Thrift在版本控制和数据定义更改方面非常强大。这对于支持对已部署服务的更改进行分阶段部署至关重要。系统必须能够支持从日志文件中阅读旧数据,以及从过时的客户端到新服务器的请求,反之亦然。

5.1 字段标识符

Thrift中的版本控制通过字段标识符实现。Thrift中结构体的每个成员的字段头都用唯一的字段标识符编码。此字段标识符及其类型说明符的组合用于唯一标识字段。Thrift定义语言支持字段标识符的自动分配,但始终显式指定字段标识符是良好的编程实践。标识符指定如下:
struct Example {
  1:i32 number=10,
  2:i64 bigNumber,
  3:double decimals,
  4:string name="thrifty"
}
为了避免手动和自动分配的标识符之间的冲突,省略了标识符的字段被分配从-1递减的标识符,并且该语言仅支持正标识符的手动分配。
当数据被格式化时,生成的代码可以使用这些标识符来正确地标识字段,并确定它是否与其定义文件中的字段对齐。如果字段标识符无法识别,则生成的代码可以使用类型说明符跳过未知字段而不会出现任何错误。同样,这是可能的,因为所有的数据库都是自定界的。
字段标识符也可以(也应该)在函数参数列表中指定。事实上,参数列表不仅在后端表示为结构,而且实际上在编译器前端共享相同的代码。这允许对方法参数进行版本安全的修改
service StringCache {
  void set(1:i32 key, 2:string value),
  string get(1:i32 key) throws (1:KeyNotFound knf),
  void delete(1:i32 key)
}
选择指定字段标识符的语法是为了反映它们的结构。结构可以看作是一个字典,其中标识符是键,值是强类型的命名字段。
字段标识符在内部使用i16 Thrift类型。但是,请注意,TProtocol抽象可以以任何格式对标识符进行编码。

5.2 Isset

当遇到意外字段时,可以安全地忽略和丢弃它。当没有找到预期的字段时,必须有某种方式向开发人员发出信号,表明它不存在。这是通过定义对象内部的isset结构实现的。(Isset功能在PHP中是隐式的, 在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 Implementation
6.1 TProcessor
The last core interface in the Thrift design is the TProcessor, perhaps the most simple of the constructs. The interface is as follows:
interface TProcessor {
   bool process(TProtocol in, TProtocol out)
   throws TException
}
The key design idea here is that the complex systems we build can fundamentally be broken down into agents or services that operate on inputs and outputs. In most cases, there is actually just one input and output (an RPC client) that needs handling.
6.2 Generated Code
When a service is defined, we generate a TProcessor instance capable of handling RPC requests to that service, using a few helpers. The fundamental structure (illustrated in pseudo-C++) is as follows:
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 processor,
TServerTransport transport,
TTransportFactory tfactory,
TProtocolFactory pfactory)
serve()
From the Thrift definition file, we generate the virtual service in terface. A client class is generated, which implements the interface and uses two TProtocol instances to perform the I/O operations.
The generated processor implements the TProcessor interface.
The generated code has all the logic to handle RPC invocations via the process() call, and takes as a parameter an instance of the service interface, as implemented by the application developer.
The user provides an implementation of the application interface in separate, non-generated source code.
6.3 TServer
Finally, the Thrift core libraries provide a TServer abstraction. The TServer object generally works as follows.
Use the TServerTransport to get a TTransport
Use the TTransportFactory to optionally convert the primitive transport into a suitable application transport (typically the TBufferedTransportFactory is used here)
Use the TProtocolFactory to create an input and output protocol for the TTransport
Invoke the process() method of the TProcessor object The layers are appropriately separated such that the server code needs to know nothing about any of the transports, encodings,
or applications in play. The server encapsulates the logic around connection handling, threading, etc. while the processor deals with RPC. The only code written by the application developer lives in the definitional Thrift file and the interface implementation.
Facebook has deployed multiple TServer implementations, including the single-threaded TSimpleServer, thread-per-connection TThreadedServer , and thread-pooling TThreadPoolServer .
The TProcessor interface is very general by design. There is no requirement that a TServer take a generated TProcessor object.
Thrift allows the application developer to easily write any type of server that operates on TProtocol objects (for instance, a server could simply stream a certain type of object without any actual RPC method invocation).

6. RPC Implementation (RPC实现)

6.1 处理器

Thrift设计中的最后一个核心接口是TProcessor,这可能是最简单的构造。界面如下:

interface TProcessor {
bool process(TProtocol in, TProtocol out)
throws TException
}


这里的关键设计思想是,我们构建的复杂系统可以从根本上分解为对输入和输出进行操作的代理或服务。在大多数情况下,实际上只需要处理一个输入和输出(RPC客户端)。

6.2 生成代码

当一个服务被定义时,我们生成一个TProcessor实例,它能够处理对该服务的RPC请求,使用一些helper。基本结构(在伪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 processor,
      TServerTransport transport,
      TTransportFactory tfactory,
      TProtocolFactory pfactory)
serve()
从Thrift定义文件中,我们生成虚拟服务接口。生成一个客户端类,它实现接口并使用两个TProtocol实例来执行I/O操作。生成的处理器实现TProcessor接口。生成的代码具有通过process()调用处理RPC调用的所有逻辑,并接受服务接口的实例作为参数,由应用程序开发人员实现。
用户在单独的、非生成的源代码中提供应用程序接口的实现。

6.3 TServer

最后,Thrift核心库提供了一个TServer抽象。TServer对象通常按如下方式工作。
使用TServerTransport获取TTransport
使用TTransportFactory可以选择性地将原语传输转换为合适的应
用程序传输(这里通常使用TBufferedTransportFactory)
使用TProtocolFactory为TTransport创建输入和输出协议
调用TProcessor对象的process()方法
这些层被适当地分开,这样服务器代码就不需要知道任何传输、编码或应用程序。服务器封装连接处理、线程等逻辑,而处理器处理RPC。
应用程序开发人员编写的唯一代码存在于定义的Thrift文件和接口实现中。
Facebook已经部署了多个TServer实现,包括单线程的 TSimpleServer、每连接线程的TThreadedServer和线程池化的 TThreadPoolServer。
TProcessor接口在设计上非常通用。TServer不需要接受生成的 TProcessor对象。Thrift允许应用程序开发人员轻松地编写任何类型的服务器,这些服务器在TProtocol对象上运行(例如,服务器可以简单地流式传输某种类型的对象,而无需任何实际的RPC方法调用)。
7. Implementation Details
7.1 Target Languages
Thrift currently supports five target languages: C++, Java, Python, Ruby, and PHP. At Facebook, we have deployed servers predominantly in C++, Java, and Python. Thrift services implemented in PHP have also been embedded into the Apache web server, providing transparent backend access to many of our frontend constructs using a THttpClient implementation of the TTransport interface.
Though Thrift was explicitly designed to be much more efficient and robust than typical web technologies, as we were designing our XML-based REST web services API we noticed that Thrift could be easily used to define our service interface. Though we do not currently employ SOAP envelopes (in the authors’ opinions there is already far too much repetitive enterprise Java software to do that sort of thing), we were able to quickly extend Thrift to generate XML Schema Definition files for our service, as well as a framework for versioning different implementations of our web service. Though public web services are admittedly tangential to Thrift’s core use case and design, Thrift facilitated rapid iteration and affords us the ability to quickly migrate our entire XML-based web service onto a higher performance system should the need arise.
7.2 Generated Structs
We made a conscious decision to make our generated structs as transparent as possible. All fields are publicly accessible; there are no set() and get() methods. Similarly, use of the isset object is not enforced. We do not include any FieldNotSetException construct. Developers have the option to use these fields to write more robust code, but the system is robust to the developer ignoring the isset construct entirely and will provide suitable default behavior in all cases.
This choice was motivated by the desire to ease application development. Our stated goal is not to make developers learn a rich new library in their language of choice, but rather to generate code that allow them to work with the constructs that are most familiar in each language. We also made the read() and write() methods of the generated objects public so that the objects can be used outside of the context of RPC clients and servers. Thrift is a useful tool simply for generating objects that are easily serializable across programming languages.
7.3 RPC Method Identification
Method calls in RPC are implemented by sending the method name as a string. One issue with this approach is that longer method names require more bandwidth. We experimented with using fixedsize hashes to identify methods, but in the end concluded that the savings were not worth the headaches incurred. Reliably dealing with conflicts across versions of an interface definition file is impossible without a meta-storage system (i.e. to generate nonconflicting hashes for the current version of a file, we would have to know about all conflicts that ever existed in any previous version of the file).
We wanted to avoid too many unnecessary string comparisons upon method invocation. To deal with this, we generate maps from strings to function pointers, so that invocation is effectively accomplished via a constant-time hash lookup in the common case. This requires the use of a couple interesting code constructs. Because Java does not have function pointers, process functions are all private member classes implementing a common interface.
private class ping implements ProcessFunction {
public void process(int seqid,
TProtocol iprot,
TProtocol oprot)
throws TException
{ ...}
}
HashMap<String,ProcessFunction> processMap_ =
new HashMap<String,ProcessFunction>();
In C++, we use a relatively esoteric language construct: member
function pointers.
std::map<std::string,
void (ExampleServiceProcessor::*)(int32_t,
facebook::thrift::protocol::TProtocol* ,
facebook::thrift::protocol::TProtocol*)>
processMap_;
Using these techniques, the cost of string processing is minimized, and we reap the benefit of being able to easily debug corrupt or misunderstood data by inspecting it for known string method
names.
7.4 Servers and Multithreading
Thrift services require basic multithreading to handle simultaneous requests from multiple clients. For the Python and Java implementations of Thrift server logic, the standard threading libraries distributed with the languages provide adequate support.
For the C++ implementation, no standard multithread runtime library exists. Specifically, robust, lightweight, and portable thread manager and timer class implementations do not exist. We investigated existing implementations, namely boost::thread , boost::threadpool, ACE Thread Manager and ACE Timer .
While boost::threads [1] provides clean, lightweight and robust implementations of multi-thread primitives (mutexes, conditions, threads) it does not provide a thread manager or timer implementation.
boost::threadpool [2] also looked promising but was not far enough along for our purposes. We wanted to limit the dependency on third-party libraries as much as possible. Because boost::threadpool is not a pure template library and requires runtime libraries and because it is not yet part of the official Boost distribution we felt it was not ready for use in Thrift. As boost::threadpool evolves and especially if it is added to the Boost distribution we may reconsider our decision to not use it.
ACE has both a thread manager and timer class in addition to multithread primitives. The biggest problem with ACE is that it is ACE.
Unlike Boost, ACE API quality is poor. Everything in ACE has large numbers of dependencies on everything else in ACE - thus forcing developers to throw out standard classes, such as STL collections, in favor of ACE’s homebrewed implementations. In addition, unlike Boost, ACE implementations demonstrate little understanding of the power and pitfalls of C++ programming and take no advantage of modern templating techniques to ensure compile time safety and reasonable compiler error messages. For all these reasons, ACE was rejected. Instead, we chose to implement our own library, described in the following sections.
7.5 Thread Primitives
The Thrift thread libraries are implemented in the namespace facebook::thrift::concurrency and have three components:
primitives
thread pool manager
timer manager
As mentioned above, we were hesitant to introduce any additional dependencies on Thrift. We decided to use boost::shared ptr because it is so useful for multithreaded application, it requires no link-time or runtime libraries (i.e. it is a pure template library) and it is due to become part of the C++0x standard.
We implement standard Mutex and Condition classes, and a Monitor class. The latter is simply a combination of a mutex and  condition variable and is analogous to the Monitor implementation provided for the Java Object class. This is also sometimes referred to as a barrier. We provide a Synchronized guard class to allow Java-like synchronized blocks. This is just a bit of syntactic
sugar, but, like its Java counterpart, clearly delimits critical sections of code. Unlike its Java counterpart, we still have the ability to programmatically lock, unlock, block, and signal monitors.
void run() {
{Synchronized s(manager->monitor);
if (manager->state == TimerManager::STARTING) {
manager->state = TimerManager::STARTED;
manager->monitor.notifyAll();
}
}
}
We again borrowed from Java the distinction between a thread and a runnable class. A Thread is the actual schedulable object. The Runnable is the logic to execute within the thread. The Thread
implementation deals with all the platform-specific thread creation and destruction issues, while the Runnable implementation deals with the application-specific per-thread logic. The benefit of this approach is that developers can easily subclass the Runnable class without pulling in platform-specific super-classes.
7.6 Thread, Runnable, and shared ptr
We use boost::shared ptr throughout the ThreadManager and TimerManager implementations to guarantee cleanup of dead objects that can be accessed by multiple threads. For Thread class
implementations, boost::shared ptr usage requires particular attention to make sure Thread objects are neither leaked nor dereferenced prematurely while creating and shutting down threads.
Thread creation requires calling into a C library. (In our case the POSIX thread library, libpthread , but the same would be true for WIN32 threads). Typically, the OS makes few, if any, guarantees about when ThreadMain , a C thread’s entry-point function, will be called. Therefore, it is possible that our thread create call, ThreadFactory::newThread() could return to the caller well before that time. To ensure that the returned Thread object is not prematurely cleaned up if the caller gives up its reference prior to the ThreadMain call, the Thread object makes a weak referenence to itself in its start method.
With the weak reference in hand the ThreadMain function can attempt to get a strong reference before entering the Runnable::run method of the Runnable object bound to the Thread . If no strong references to the thread are obtained between exiting Thread::start and entering ThreadMain , the weak reference returns null and the function exits immediately.
The need for the Thread to make a weak reference to itself has a significant impact on the API. Since references are managed through the boost::shared ptr templates, the Thread object must have a reference to itself wrapped by the same boost::shared ptr envelope that is returned to the caller. This necessitated the use of the factory pattern. ThreadFactory creates the raw Thread object and a boost::shared ptr wrapper, and calls a private helper method of the class implementing the Thread interface (in this case, PosixThread::weakRef ) to allow it to make add weak reference to itself through the boost::shared ptr envelope.
Thread and Runnable objects reference each other. A Runnable object may need to know about the thread in which it is executing, and a Thread, obviously, needs to know what Runnable object itis hosting. This interdependency is further complicated because the  lifecycle of each object is independent of the other. An application may create a set of Runnable object to be reused in different threads, or it may create and forget a Runnable object once a thread has been created and started for it.
The Thread class takes a boost::shared ptr reference to the hosted Runnable object in its constructor, while the Runnable class has an explicit thread method to allow explicit binding of
the hosted thread. ThreadFactory::newThread binds the objects to each other.
7.7 ThreadManager
ThreadManager creates a pool of worker threads and allows applications to schedule tasks for execution as free worker threads become available. The ThreadManager does not implement dynamic thread pool resizing, but provides primitives so that applications can add and remove threads based on load. This approach was chosen because implementing load metrics and thread pool size is very application specific. For example some applications may want to adjust pool size based on running-average of work arrival rates that are measured via polled samples. Others may simply wish to react immediately to work-queue depth high and low water marks.
Rather than trying to create a complex API abstract enough to capture these different approaches, we simply leave it up to the particular application and provide the primitives to enact the desired policy and sample current status.
7.8 TimerManager
TimerManager allows applications to schedule Runnable objects for execution at some point in the future. Its specific task is to allows applications to sample ThreadManager load at regular intervals and make changes to the thread pool size based on application policy. Of course, it can be used to generate any number of timer or alarm events.
The default implementation of TimerManager uses a single thread to execute expired Runnable objects. Thus, if a timer operation needs to do a large amount of work and especially if it needs to do blocking I/O, that should be done in a separate thread.
7.9 Nonblocking Operation
Though the Thrift transport interfaces map more directly to a blocking I/O model, we have implemented a high performance TNonBlockingServer in C++ based on libevent and the TFramedTransport .
We implemented this by moving all I/O into one tight event loop using a state machine. Essentially, the event loop reads framed requests into TMemoryBuffer objects. Once entire requests are ready, they are dispatched to the TProcessor object which can read directly from the data in memory.
7.10 Compiler
The Thrift compiler is implemented in C++ using standard lex/ yacc lexing and parsing. Though it could have been implemented with fewer lines of code in another language (i.e. Python Lex-Yacc
(PLY) or ocamlyacc ), using C++ forces explicit definition of the language constructs. Strongly typing the parse tree elements (debatably) makes the code more approachable for new developers.
Code generation is done using two passes. The first pass looks only for include files and type definitions. Type definitions are not checked during this phase, since they may depend upon include files. All included files are sequentially scanned in a first pass. Once the include tree has been resolved, a second pass over all files is taken that inserts type definitions into the parse tree and raises an error on any undefined types. The program is then generated against the parse tree.
Due to inherent complexities and potential for circular dependencies, we explicitly disallow forward declaration. Two Thrift structs cannot each contain an instance of the other. (Since we do not allow null struct instances in the generated C++ code, this would actually be impossible.)
7.11 TFileTransport
The TFileTransport logs Thrift requests/structs by framing incoming data with its length and writing it out to disk. Using a framed on-disk format allows for better error checking and helps
with the processing of a finite number of discrete events. The TFileWriterTransport uses a system of swapping in-memory buffers to ensure good performance while logging large amounts
of data. A Thrift log file is split up into chunks of a specified size;
logged messages are not allowed to cross chunk boundaries. A message that would cross a chunk boundary will cause padding to be added until the end of the chunk and the first byte of the message are aligned to the beginning of the next chunk. Partitioning the file into chunks makes it possible to read and interpret data from a particular point in the file.

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 生成的结构

我们做了一个有意识的决定,使我们生成的结构尽可能透明。所有字段都是可公开访问的;没有set()和get()方法。同样,也不强制使用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>();
In C++, we use a relatively esoteric language construct: member
function pointers.
std::map<std::string,
void (ExampleServiceProcessor::*)(int32_t,
facebook::thrift::protocol::TProtocol* ,
facebook::thrift::protocol::TProtocol*)>
processMap_;
在C++中,我们使用一个相对深奥的语言结构:成员函数指针。
std::map processMap_;
使用这些技术,字符串处理的成本被最小化,并且我们可以通过检查已知字符串方法名称来轻松调试损坏或误解的数据。

7.4 服务器和多线程

Thrift服务需要基本的多线程来处理来自多个客户端的同时请求。 对于Thrift服务器逻辑的Python和Java实现,与语言一起分发的标准线程库提供了足够的支持。对于C++实现,不存在标准的多线程运行时库。具体来说,不存在健壮、轻量级和可移植的线程管理器和计时器类实现。我们研究了现有的实现,即boost::thread、 boost::threadpool、ACE Thread Manager和ACE Timer。
虽然boost::threads[1]提供了多线程原语(互斥体,条件,线程)的干净,轻量级和健壮的实现,但它不提供线程管理器或计时器实现。
boost::threadpool[2]看起来也很有前途,但对于我们的目的来说还不够沿着。我们希望尽可能地限制对第三方库的依赖。因为boost::threadpool不是一个纯粹的模板库,需要运行时库,而且因为它还不是官方Boost发行版的一部分,我们觉得它还没有准备好在Thrift中使用。随着boost::threadpool的发展,特别是如果它被添加到Boost发行版中,我们可能会重新考虑不使用它的决定。
除了多线程原语之外,ACE还具有线程管理器和计时器类。ACE最大的问题是它是ACE。与Boost不同,ACE API质量较差。ACE中的所有内容都对ACE中的其他内容有大量的依赖性-因此迫使开发人员放弃标准类,如STL集合,而支持ACE的自制实现。此外,与Boost不同,ACE实现对C++编程的功能和缺陷了解甚少,并且没有利用现代模板技术来确保编译时安全性和合理的编译器错误消息。由于这些原因,ACE被拒绝了。相反,我们选择实现我们自己的库,在下面的部
分中描述。

7.5 线程基元

Thrift线程库在命名空间中实现
facebook::thrift::concurrency有三个组件:
基元
线程池管理程序
定时器管理器
如上所述,我们不愿意引入任何额外的依赖Thrift。我们决定使用 boost::shared ptr,因为它对多线程应用程序非常有用,它不需要链接时或运行时库(即它是一个纯模板库),并且它将成为C++0x 标准的一部分。
我们实现了标准的Mutex和Condition类,以及一个Monitor类。后者只是互斥量和Monitor类条件变量,类似于为Java Object类提供的Monitor实现。这有时也被称为障碍。我们提供了Synchronized guard类来允许类似Java的同步块。这只是一点语法糖,但与Java对应物一样,它清楚地划分了代码的关键部分。与Java对应物不同,我们仍然能够以编程方式锁定、解锁、阻止和发送信号监视器。
=====
我们再次借用了Java中线程和可运行类之间的区别。线程是实际的可扩展对象。Runnable是在线程中执行的逻辑。Thread实现处理所有特定于平台的线程创建和销毁问题,而Runnable实现处理特定于应用程序的每线程逻辑。这种方法的好处是,开发人员可以轻松地子类化Runnable类,而无需引入特定于平台的超类。

7.6 线程、可运行和共享ptr

我们在整个ThreadManager和TimerManager实现中使用boost:: shared ptr来保证清理可以被多个线程访问的死对象。对于Thread类实现,boost::shared ptr的使用需要特别注意,以确保在创建和关闭线程时,Thread对象既不会泄漏,也不会过早地取消引用。
创建线程需要调用C库。(In我们的例子是POSIX线程库libpthread, 但对于WIN32线程也是如此)。通常情况下,操作系统很少(如果有的话)保证何时调用ThreadMain(C线程的入口点函数)。因此,我们的线程创建调用ThreadFactory::newThread()可能会在该时间之前返回给调用者。为了确保在调用者在ThreadMain调用之前放弃其引用时不会过早地清理返回的Thread对象,Thread对象在其 start方法中对自身进行弱引用。
这种相互依赖性更加复杂,因为每个对象的生命周期都是相互独立的。应用程序可以创建一组Runnable对象,以便在不同的线程中重用,或者它可以在创建并启动线程后创建并忘记Runnable对象。
Thread类在其构造函数中接受对托管Runnable对象的boost:: shared ptr引用,而Runnable类有一个显式的线程方法来允许托管线程的显式绑定。ThreadFactory::newThread将对象相互绑定。

7.7 ThreadManager

ThreadManager创建一个工作线程池,并允许应用程序在空闲工作线程可用时调度任务执行。ThreadManager不实现动态线程池管理,但提供了原语,以便应用程序可以根据负载添加和删除线程。之所以选择这种方法,是因为实现负载度量和线程池大小是非常特定于应用程序的。例如,一些应用程序可能希望根据通过轮询样本测量的工作到达率的运行平均值来调整池大小。其他人可能只是希望立即对工作队列深度高低水位标记做出反应。我们并不试图创建一个足够抽象的复杂 API来捕获这些不同的方法,而是简单地将其留给特定的应用程序,并提供原语来执行所需的策略和采样当前状态。

7.8 TimerManager

TimerManager允许应用程序调度Runnable对象在将来的某个时间执行。它的具体任务是允许应用程序定期对ThreadManager负载进行采样,并根据应用程序策略更改线程池大小。当然,它可以用来生成任意数量的定时器或报警事件。
TimerManager的默认实现使用单个线程来执行过期的Runnable对象。因此,如果一个计时器操作需要做大量的工作,特别是如果它需要做阻塞I/O,那么应该在一个单独的线程中完成。

7.9 非阻塞操作

虽然Thrift传输接口更直接地映射到阻塞I/O模型,但我们已经实现了高性能基于libevent和TFrameedTransport的C++中的TNonBlockingServer。 我们通过使用状态机将所有I/O移动到一个紧密的事件循环中来实现这 一点。从本质上讲,事件循环将帧请求读入TMemoryBuffer对象。一旦整个请求都准备好了,它们就被分派到TProcessor对象,该对象可以直接从内存中的数据读取。

7.10 编译器

Thrift编译器使用标准的lex/yacc词法分析和解析在C++中实现。虽然它可以用另一种语言(即Python Lex-Yacc(Python)或ocamlyacc)中更少的代码行来实现,但使用C++强制显式定义语言结构。对解析树元素进行强类型化(有争议)可以使新开发人员更容易理解代码。
代码生成使用两次传递完成。第一遍只查找包含文件和类型定义。在此阶段不检查类型定义,因为它们可能依赖于包含文件。所有包含的文件在第一次扫描中顺序扫描。一旦解析了包含树,将对所有文件进行第二次遍历,将类型定义插入到解析树中,并对任何未定义的类型引发错误。然后根据解析树生成程序。
由于固有的复杂性和循环依赖的可能性,我们显式地不允许向前声明。两个Thrift结构体不能各自包含另一个的实例。(由于我们不允许在生成的C++代码中使用null结构实例,这实际上是不可能的。)

7.11 TFileTransport

TFileTransport通过将传入数据按其长度成帧并将其写入磁盘来记录Thrift请求/结构。使用磁盘上的框架格式可以更好地进行错误检查,并有助于处理有限数量的离散事件。TFileWriterTransport使用内存中的交换系统
缓冲区,以确保在记录大量数据时具有良好的性能。Thrift日志文件被拆分为指定大小的块;
记录的消息不允许跨越块边界。跨越块边界的消息将导致添加填充,直到块的末尾和消息的第一个字节
与下一个块的开头对齐。将文件划分为块,可以从文件中的特定点读取和解释数据。
8. Facebook Thrift Services
Thrift has been employed in a large number of applications at Facebook, including search, logging, mobile, ads and the developer platform. Two specific usages are discussed below.
8.1 Search
Thrift is used as the underlying protocol and transport layer for the Facebook Search service. The multi-language code generation is well suited for search because it allows for application development in an efficient server side language (C++) and allows the Facebook PHP-based web application to make calls to the search service using Thrift PHP libraries. There is also a large variety of search stats, deployment and testing functionality that is built on top of generated Python code. Additionally, the Thrift log file format is used as a redo log for providing real-time search index updates.
Thrift has allowed the search team to leverage each language for its strengths and to develop code at a rapid pace.
8.2 Logging
The Thrift TFileTransport functionality is used for structured logging. Each service function definition along with its parameters can be considered to be a structured log entry identified by the function name. This log can then be used for a variety of purposes, including inline and offline processing, stats aggregation and as a redo log.

8.脸书Thrift服务


Thrift已在Facebook的大量应用程序中使用,包括搜索、日志、移动、广告和开发者平台。下面讨论两种具体用法。


8.1搜索


Thrift被用作Facebook搜索服务的底层协议和传输层。多语言代码生成非常适合搜索,因为它允许使用高效的服务器端语言(C++)进行应用程序开发,并允许基于Facebook PHP的web应用程序使用Thrift PHP库调用搜索服务。在生成的Python代码之上还构建了各种各样的搜索统计数据、部署和测试功能。此外,Thrift日志文件格式用作重做日志,用于提供实时搜索索引更新。
Thrift使搜索团队能够利用每种语言的优势,并快速开发代码。


8.2日志


Thrift TFileTransport功能用于结构化日志记录。每个服务功能定义及其参数都可以被视为由功能名称标识的结构化日志条目。然后,此日志可用于各种目的,包括内联和离线处理、统计数据聚合以及作为重做日志。
9. Conclusions
Thrift has enabled Facebook to build scalable backend services efficiently by enabling engineers to divide and conquer. Application developers can focus on application code without worrying about the sockets layer. We avoid duplicated work by writing buffering and I/O logic in one place, rather than interspersing it in each application.
Thrift has been employed in a wide variety of applications at Facebook, including search, logging, mobile, ads, and the developer platform. We have found that the marginal performance cost incurred by an extra layer of software abstraction is far eclipsed by the gains in developer efficiency and systems reliability.

9.结论


Thrift通过让工程师分而治之,使Facebook能够高效地构建可扩展的后端服务。应用程序开发人员可以专注于应用程序代码,而不必担心套接字层。我们通过在一个地方编写缓冲和I/O逻辑来避免重复工作,而不是将其分散在每个应用程序中。
Thrift已在Facebook的各种应用程序中使用,包括搜索、日志、移动、广告和开发平台。我们发现,额外一层软件抽象所带来的边际性能成本远远超过了开发人员效率和系统可靠性的提高。
A. Similar Systems
The following are software systems similar to Thrift. Each is
(very!) briefly described:
SOAP. XML-based. Designed for web services via HTTP, excessive XML parsing overhead.
CORBA. Relatively comprehensive, debatably overdesigned and heavyweight. Comparably cumbersome software installation.
COM. Embraced mainly in Windows client softare. Not an entirely open solution.
Pillar. Lightweight and high-performance, but missing versioning and abstraction.
Protocol Buffers. Closed-source, owned by Google. Described in Sawzall paper.
Acknowledgments
Many thanks for feedback on Thrift (and extreme trial by fire) are due to Martin Smith, Karl Voskuil and Yishan Wong.
Thrift is a successor to Pillar, a similar system developed by Adam D’Angelo, first while at Caltech and continued later at Facebook.
Thrift simply would not have happened without Adam’s insights.
References
[1] Kempf, William, “Boost.Threads” , http://www.boost.org/doc/ html/threads.html
[2] Henkel, Philipp, “threadpool” , http://threadpool.sourceforge.net


A.类似系统


以下是与Thrift类似的软件系统。每一个都是(非常!)简要的描述:
•SOAP。基于XML。通过HTTP为web服务设计,XML解析开销过大。
•CORBA。相对全面,有争议的过度设计和重量级。软件安装相对繁琐。
•COM。主要在Windows客户端软件中使用。这不是一个完全开放的解决方案。
•Pillar。轻量级和高性能,但缺少版本控制和抽象。
•Protocal Buffers。闭源代码,归谷歌所有。Sawzall论文中有描述。


致谢


感谢Martin Smith、Karl Voskuil和Yishan Wong对Thrift(与极限考验)的反馈。
Thrift是Pillar的继任者,Pillar是Adam D'Angelo开发的一个类似系统,最初在加州理工学院工作,后来在Facebook继续工作。
没有Adam 的洞察力,Thrift根本不会产生。


引用文档

[1] Kempf, William, “Boost.Threads” , 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、付费专栏及课程。

余额充值