00-Thrift简介(官方文档略读)

Thrift简介

摘要

FaceBook开发的,用来加快开发和实施高效、可扩展的后端服务的代码生成工具集。它主要目标是通过抽象服务中需要的接口,形成一个各种语言通信用的公用接口文件。
本文介绍涉及某些执行细节的设计选择,介绍我们做了什么,以及为什么这么做!

1 介绍

Facebook有很多网络请求已经脱离了传统的LAMP框架,例如搜索、广告、日志等。在这些服务中,各种编程语言都有各种性能、易用性、速度等各方面的优势,在这些选择中,Facebook的工程师更喜欢为不同的服务选择合适的变成语言,而不是受某一种语言的限制。
鉴于此,我们建立了透明、高性能、跨语言的挑战。同时,我们也发现,大部分的解决方案都有各种数据类型或者性能表现上的限制。

倾向于静态代码生成,而不是动态系统,这样做的优势是可以避免复杂的运行时检查。

关键的组件:
1. 类型。通用类型必须跨编程语言,并且不需要用户自定义数据类型。
2. 传输。每个语言都有一个双向原始数据传输接口。如何传输本身不应该是服务开发人员关心。
3. 协议。数据类型使用传输层进行编码和解码的方式。开发人员不应该关心这一层。
4. 版本。鲁棒性的服务,所涉及的数据类型必须提供一种机制进行版本控制。可以修改、添加字段等。
5. 处理器。生成的代码应该能够处理流数据来完成远程过程调用。

2 类型

数据类型的目标是,无论使用什么语言,尽量使程序员可以使用本机定义的类型或者自己的序列化代码。IDL文件从逻辑上是为开发人员解释他们需要的最小的数据信息结构。

2.1 基本类型

  • bool // true or false
  • byte // one byte
  • i16 // 16-bit signed int
  • i32 // 32-bit signed int
  • i64 // 64-bit signed int
  • double // 64-bit floating point number
  • string // encoding-agnostic text or binary string

需要注意的是,没有unsigned整数,因为有些语言没有对应的类型。

2.2 结构

Thrift中的struct定义了一个所有语言的通用object。它本质上等同于一个OOP中的一个类。结构具有一组强类型字段,每个都有一个唯一标志。它非常类似C语言中的struct。字段可能会附加整数字段标识符和可选的默认值。字段标识符将自动分配如果省略,但是强烈建议,以便后续版本控制。

2.3 容器

Thrift容器使用的是常见语言类型中自带的,他们会注明使用C++模版风格,有三种可选:
- list 有序元素列表。可转化为STL Vector, Java ArrayList或者脚本语言的本机数组。
- set 无序元素。可转化为 STL set,Java HashSet或者其他语言中的dict。
- map

struct Example{
    1:i32 number=10,
    2:i64 bigNumber,
    3:double decimals,
    4:string name="thrifty"
}

在生成的目标语言中,每个变量都产生两个方法,read和write,他们在Thrift TProtocol中用来处理序列化和传输任务。

2.4 异常

异常在语法上类似结构体,只是他们使用异常关键字而不是结构体。异常类继承于标准异常。

2.5 服务(Services)

使用Thrift类型定义服务,服务的定义在语义上等效于面向对象中接口定义(纯虚抽象类)。编译器生成完全功能的客户端和服务器stub代码。服务定义Demo如下:

service<name>{
    <return type><name>(<args>)
        [throws(<exceptions>)]...
}

// exp:
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是有效的返回值,并且async标志可能会添加到void返回的函数,它会使得函数不会等待服务器的返回结果。但是,一个单纯的void返回值函数会等待服务器执行结束才返回。添加有async方法的函数仅仅会产生一个传输层的成功标志。

3 传输

传输层用来传输序列化之后的内容。

3.1 接口

Thrift中一个关键的设计就是将传输层从序列化层去耦。一般使用TCP/IP流式socket传输内容。传输层极度抽象。
幸运的是,生成的Thrift代码只需要指导如何read、write数据就可以了。数据的来源和目的地不是很关心,它可以是一个socket、shared memery、file等等。一般Thrift传输层接口支持如下方法:
- open // 打开传输层
- close // 关闭传输层
- isOpen // 表明传输层是否打开
- read // 从传输层中读取内容
- write // 向传输层写入内容
- flush // 强制刷出所有剩余数据内容

TServerTransport接口用来接收或者创建一个私有的object,定义如下:
- open // 打开传输层
- listen // 开始监听到来的连接
- accept // 返回一个新的client传输层
- close // 关闭传输层

3.2 执行(实现)

传输层已经针对所有语言设计,新的机器很容易被开发者定义。

3.2.1 TSocket

TSocket实现了所有语言,它提供了一个通用的简单的TCP/IP接口。

3.2.2 TFileTransport

这个类实现了磁盘文件转变为数据流的抽象。它可以用来将流转换为文件。

3.2.3 实用程序

传输层接口使用OOP思想,易于被扩展。比如TBufferedTransport用于缓冲读取和写入底层传输数据,TFramedTransport用于分块传输或者非阻塞操作,TMemoryBuffer用于读写内存中的数据。

4 协议

Thrift中的第二个重要的抽象是将传输从数据结构序列化&反序列化中分离。无论是否使用xml或者其他二进制格式,只要数据支持一套固定的操作,它都能够正确地读写。

4.1 接口

Thrift接口协议非常简单,它支持两件事情:
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()

4.2 结构

Thrift支持将就结构编码为流协议,编码实现时永远不要添加Frame或者长度。这在很多情况下时性能的关键,特别是很大的字符串。
…后续看不懂了。

4.3 实现

Facebook已经实现了大部分后端的二进制协议。它将数据写入二进制格式,整数转换为网络字节顺序,字符串的前面拥有字节长度信息。

5 版本控制

用来确保旧版的请求还可以正常使用。

5.1 字段标识符

Thrift的版本控制是通过字段标识符来实现的。结构体中每个变量都有一个唯一的字段标识符,Thrift可自动分配,但是推荐显示地指定。指定方式如下:

struct Example {
    1:i32 number=10,
    2:i64 bigNumber,
    3:double decimals,
    4:string name="thrifty"
}

字段标识符也可以用在函数的参数列表中,实际上,参数列表不止用在后端,也用在共享代码的前端。

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类型。但是,TProtocol可以将他们编码为任何类型。

5.2 设定

当遇到意外的字段时,它会被安全地忽略和丢弃。当未找到预期的字段时,必须有某种方式向开发人员发出信号,它不存在。在程序内部,针对每个变量都包含一个布尔值,指示字段是否存在,当接收者接收结构时,它应该检查字段是否被设置。

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()时都可以自由控制要发送的内容。它完全由执行者决定如何处理协议
的版本。关键点是,协议编码处理从接口定义中安全地隔离开来。

例如,如果我们想对TFileTransport添加一些校验或错误检测功能,我们可以简单地在结构头部添加一些信息,它将仍然接受旧的数据,除了没有部分头部信息。

6 RPC调用

6.1 TProcessor

Thrift的最后一个核心接口是TProcessor,它可能有着最简单的构造函数,如下:

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

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

6.2 生成的代码

定义服务时,我们通过助手工具生成的TProcessor实例能够处理RPC请求。基本结构如下:

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的定义文件,我们生成virtual serveice interface。同时客户端类也被生成,实现了这些接口,并使用2个TProtocol实例执行I/O操作。生成的代码实现了TProcessor接口,并且具有处理RPC带哦用process()的能力,并且提供了给开发人员操作参数的接口。

使用者提供了应用的接口实现,通过分离的非生成的代码。

6.3 TServer

Thriftcore库提供了TServer接口的抽象,它工作原理如下:
- 使用TServerTransport进行一个TTransport.
- 使用TTransportFactory将原始的TTransport转变为一个合适的传输方式(TBufferdTransportFactory一般用在这里)
- 使用TProtocolFactory为TTransport创建一个输入和输出协议
- 调用TProcessor对象的process()方法
这样,服务器代码不需要知道任何关于传输、编码或者应用程序使用的方式。服务器封装了所有的连接、线程等RPC调用所需要的逻辑处理。开发人员只需要关注Thrift的接口定义。
FaceBook已经开发了多个TServer实现,包含单线程TSimpleServer、每个连接分配一个线程的TThreadedServer、线程池的TThreadPoolServer。
TProcessor接口是一个非常棒的设计,TServer使用TProcessor对象时没有任何附加要求。Thrift能够帮助开发人员写出任何运行TProtocol对象的服务器。

7 实施细则

7.1 目标语言

Thrift目前支持五种语言:C++、Java、Python、Ruby和PHP。(实际上还支持Go、C#、Erlang、Dart、NodeJS、Perl…)
虽然Thrift设计上比传统的Web服务更高效、鲁棒性更好,但是我们也可以支持基于XML的REST API服务。(其实还有JSON REST API服务)

7.2 生成的结构

我们有意地使得生成的结构尽量透明,所有的字段都可通过Set/Get读写。我们不会加入任何FieldNotSetException的限制,使得开发人员可以写出更可靠的代码。 开发人员需要自己决定如何处理字段的忽略情况。

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_;

通过这种方法,能够让字符串处理成本最小。

7.4 服务器和多线程

Thrift服务基本都要使用多线程来处理多个客户端的不同请求,对于Python和Java实现的服务器,标准的多线程库已经能提供足够的支持了。而对于C++来讲,没有标准的多线程运行库,具体而言,不存在鲁棒性好、轻巧的线程管理和定时器实现。我们使用了boost::thread,boost::threads[1]提供多线程基元。
boost::threadpool[2]也看起来前途无量,但并不足够达到我们的目的,我们尽可能限制对第三方库的依赖…,总之等待它更好、更标准了再使用。
ACE除了多线程基元外,拥有两个线程管理器和定时器类,但是它最大的问题也在与它是一个ACE,与Boost不同的是,它的API质量很差…它被拒绝了。
最终,自己实现了管理器部分。

7.5 线程基元

Thrift线程库有三个组成部分:
- 基元(primitives)
- 线程池管理
- 定时器管理

我们使用boost::shared::ptr在线程中,我们也使用标准的互斥锁、条件锁、监视器类。

void run() {
    Synchronized s(manager->monitor); 
    if (manager->state == TimerManager::STARTING) {
        manager->state = TimerManager::STARTED; 
        manager->monitor.notifyAll();
    }
}

7.6 线程可运行、共享的ptr

在ThreadManager和TimerManager中使用了boost::shared::ptr以保证对象清理工作。

7.7 ThreadManager

ThreadManager创建的辅助线程池允许应用程序安排要执行的任务。ThreadManager未实现动态线程池,但是提供基元,以便于应用程序添加和删除基于负载的线程。之所以这样,是因为执行负载和线程池大小是非常特定的程序。….

7.8 TimerManager

TimerManager允许程序在未来某个节点运行程序….
默认实现的TimerManager使用单线程来执行任务,如果需要在定时器中做大量工作,尤其是阻塞I/O操作,需要在单独的线程中处理。

7.9 非阻塞操作

虽然Thrift在一个阻塞的I/O中执行,但是我们已经推出了一个高性能的TNonBlockingServer,它基于libevent和TFramedTransport。我们所有的IO操作通过状态机的一个事件循环进行。从根本上来说,事件循环读取请求到TMemoryBuffer对象,一旦请求准备就绪,他们会被派遣到TProcessor可以直接读取内存的内存对象。

7.10 编译器(完全没看懂)

在使用标准的 c + + 中实现节俭编译器lex/yacc词法分析和语法分析。尽管它可以实现用更少的另一种语言中的代码行 (即 Python Lex Yacc (层) 或 ocamlyacc),使用 c + + 强制显式定义的语言构造。强烈打字的解析树元素 (debatably) 使新开发人员更容易接近代码。

代码生成是使用两种车票。第一遍看起来只对包含文件和类型定义。在此阶段不查了类型定义,因为他们可能取决于包括文件。在第一次传递中按顺序扫描所有的包含的文件。一旦已解决包括树,第二次经过所有文件是采取的类型定义插入解析树,并将引发对任何未定义的类型错误。对解析树,然后生成的程序。

由于固有的复杂性和循环依赖关系的潜力,我们明确地禁止前向声明。两个节俭 structs 不能每个包含另一个的实例。(因为我们不允许为空结构实例生成的 c + + 代码中,这实际上是不可能。)

7.11 TFileTransport

TFileTransport将Thrift请求到的logs数据写入磁盘。TFileWriterTransport使用系统内存中的缓冲区来确保良好的性能,同时记录大量的数据。Thrift的日志文件被分割成一个指定大小,分块存储。

8 Facebook Thrift服务

在FaceBook中使用Thrift服务进行了搜索、移动端、广告、开发平台等多个领域。

8.1 搜索

Thrift被用于基础协议和传输层。

8.2 日志记录

TFileTransport功能被用来进行结构化的日志记录。

9 结论

使用Thrift可以为Facebook构建高效可扩展的后端服务,使得工程师能够分而治之。应用开发人员不用关心socket层。

10 类似的框架

  • SOAP。xml based. 设计通过HTTP协议,xml交换数据。
  • CORBA。相对全面、重量级、过度设计。
  • COM。主要在Windows客户端,不完全。
  • Pillar。轻量级、高性能,缺少版本控制和抽象。
  • Protocol buffers。闭源,谷歌的。(现在也开源了,与这个组合很有看头)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值