Apache Thrift使用解析

  之前的一篇文章中,读摘了Apache Thrift技术白皮书的中的一些内容。Apache Thrift当前已经在公司的很多业务中使用,而自己负责的重构的服务,也使用Thrift作为通信手段对大而全的巨无霸服务进行拆分解耦。理论归理论,现实使用起来还是有些东西需要休息的,这次又借着另外一本书,对Thrift再进行一次接地气的学习和实践。
thrift

一、Thrift IDL的类型

1.1 数据类型

  Thrift提供的基本数据类型有:bool、byte、i16、i32、i64、double、string,今后可能会提供binary数据类型,作为string类型的一种特例化,提供更好的操作性和正确性。Thrift不支持无符号整形。
  可以使用struct自定义复合数据类型,每个字段都有一个唯一性标识符,这些标识符不需要连续;字段可以是required和optional的,required字段如果在新版本上删除的话,老版本和新版本交互时候可能会产生问题,所以有些人推荐所有的字段都设置成optional的;可以给struct的字段设置默认值,那么如果没有显式设置字段值的话变量就会使用这些默认值。此外,struct不支持继承。
  联合类型使用union定义,由于任何时候只有一个值能被使用,所以其字段不能是required的。
  Thrift还支持三种基本的容器类型:(1) list类型,其在C++中被映射成std::vector类型;(2) set类型,其在C++中被映射成std::set类型;(3) map类型,其在C++中被映射成std::map类型。
  枚举类型使用enum关键字定义,其成员的枚举值从0开始标号,通常使用全大写字母命名。

1.2 服务类型

  服务使用service关键字定义,声明一个个的函数接口。
  函数接口还可以使用oneway关键字修饰,这类函数的返回类型必须是void,意在告诉该接口客户端不需要等待服务器的响应结果。比如在将日志信息传递给服务端存储的时候,客户端不需要阻塞等待响应结果,从而增强客户端的效能。
  服务还可以使用extends关键字进行扩展,以类似于继承的方式获得其他服务中描述的接口。Apache Thrift只允许单继承,所以不能同时扩展多个服务。

二、Thrift文件语法规则

  Thrift既支持井号格式的注释,也支持C++格式的注释类型。
  Thrift的IDL文件可以通过include包含来进行头文件扩充,如果包含的头文件是.thrift类型,则直接使用include即可;如果是C++头文件则需要使用cpp来指明;同时thrift文件还可以使用namespace进行名字空间的修饰。在编译的时候如果遇到包含文件会在当前目录进行查找,如果相关文件在非标准目录,需要使用-I参数来指明搜索路径。

1
2
3
include "shared.thrift"
cpp include "<vector>"
namespace cpp my_space

  Thrift中的类型可以方便使用typedef定义别名。
  Thrift中的基本数据类型和符合数据类型都可以使用const来定义为常量,基本数据类型很容易理解,如果是符合数据类型、容器类型的话,需要使用json格式的方式进行列表初始化。

1
2
3
4
5
6
const map<i32,string> CITIES = {0: "New York", 1: "London", 2: "Madrid"}
struct city {
1: string name,
2: i32 population
}
const city NEW_YORK = {"name": "New York", "population": 8500000}

三、Thrift内部结构

  通过以下命令,就可以根据thrift IDL文件生成相应语言的辅助代码:

1
thrift -r --gen cpp calc.thrift

  其中的calc_constants.[h|cpp]、calc_types.[h|cpp]都是和常量和数据存取相关的接口,比较简单也不用关注他们;addService.h包含最重要的接口定义,而且其命名规则也符合Google编码规则(If命名后缀,纯虚函数接口),这个文件服务端和客户端都需要使用;addService.cpp则是客户端创建所需代码,包括数据的传输封装等细节;addService_server.skeleton.cpp则是实现了一个可以直接运行的TSimpleServer例子,告知服务端该怎样使用框架对业务进行实现,其实实现业务代码的过程就是override纯虚接口的过程。
  网络栈在Thrift的工作过程中扮演者十分重要的角色,服务端根据需要选择对应的processor、transport、protocol组件,而客户端也需要选用和服务端相匹配的组件类型,保证通信的过程中相互协调兼容,当然客户端可以采用不同的语言进行实现,这也是Thrift这类RPC的优势所在。
netstack

3.1 Transport

  该层提供了从网络或者其他媒介读取和写入数据的方式,不同的类型都实现了TTransport接口。常用的类型有:
  (1) TSock:主要用于阻塞型的socket类型,任意时刻只能有一个连接是活跃的,因此不是很常用;
  (2) TBufferedTransport:对输入输出数据提供缓冲功能,因此常和其他传输类型封装结合使用;
  (3) TFramedTransport:也是一个封装类型,用于传输帧类型的负载;
  (4) TFileTransport:主要用于向文件进行读写;
  (5) TMemoryBuffer:从内存buffer上读取和写入数据;
  (6) THttpTransport:基础的HTTP层传输类型,没有外部依赖;
  (7) TSSLSocket:Sockets添加SSL安全支持;
  (8) TZlibTransport:使用zlib压缩传输。

3.2 Protocol

  主要是将内存的数据结构映射成一种格式方便在Transport上面传输,也就是序列化(编码)和反序列化(解码)的过程,不同的类型都实现了TProtocol接口。通常用到的类型有:
  (1) TBinaryProtocol:简单的将所有的数据映射成二进制数据,是最常用的类型;
  (2) TCompactProtocal:是Apache Thrift私有的协议格式,用于优化减少传输的流量;
  (3) TJsonProtocol:负载被编码为JSON字符串;
  (4) TMultiplexedProtocol:主要是一个装饰器的功能,用于在一个port地址上,提供多个服务路由的功能。

3.3 Processor

  Processor主要是通过compiler生成的,主要负责将Protocol提供的数据读取给handler,然后将结果数据传递给Protocol返回。

3.4 Server、Client

  Server创建的时候需要选择上面所有的组件,然后在指定的端口上等待客户端的请求。常用的服务端类型有:
  (1) TNonblockingServer:是一个多线程、非阻塞IO的服务端,用于并发请求的服务场景;
  (2) TThreadPoolServer:是一个多线程、阻塞IO类型的服务端,并发量大的时候会比上面那种类型更耗费资源,但可能会提供更好的吞吐量;
  (3) TThreadedServer:传统的one-connection-per-thread工作模式;
  (4) TSimpleServer:主要用于测试使用,是单线程阻塞IO的服务端。
  客户端的实现就比较的简单,在创建客户端的过程中选择和服务端兼容的上述网络栈组件就可以了。

四、其他

  我们在公司使用的代码,是一位厉害的前同事进行封装后的结果,其服务端采用TNonblockingServer的方式提供服务,该种类型的Server还有一个额外的参数NumIOThreads控制用于专职于IO的线程数目。Thrift服务调用会抛出异常代表错误,将其捕获并生成对应错我码返回,会简化客户端的调用。
  上面说到的TThreadPoolServer可能会有更大的吞吐量,其实还不知道该结论是从何而来的,m1ch1的测试也没有给出该类型的测试结果。其实内网服务通信,感觉不会有C10K这样高并发的情况存在,内网通信延迟也会很小,所以线程池和异步两种工作模式的效能还真的很难说。
  还有就是TSock有个Timeout超时机制,大家在平时时候的时候还是可以加上的,因为当时我在跑稳定性测试的时候主服务通过Thrift调用代理服务,代理服务再调用外网的合作方API,第二天过来所有的Thrift客户端阻塞在recv读取数据的调用上,整个服务被卡的死死的,所以此时只能重启来恢复服务了,真是蛋疼。不过这个超时设置需要额外注意和讲究的是:客户端在超时时间到达后,会直接返回EAGAIN表示超时发生,但是这个请求本身还是会堆积到服务端排队(而没有被撤销),这跟gRPC是一样的,当客户端请求取消一个RPC调用的时候,服务端是不能保证这个请求会被取消的。如果你的业务不是幂等的,那么将EAGAIN超时当做出错来处理,那么后果将可能是灾难性的,所以对于这种情况,需要小心合理选取TSock的超时时间和超时发生时候对应的业务补偿,而且如果客户端超时后继续重发旧业务或者发起新业务请求的话,极有可能发生雪崩效应导致Thrift请求越积越多,所以既然把Thrift RPC当做一个同步RPC调用,那么最好预留服务端处理能力或者做好客户端的流控机制,不要发生请求堆积的现象。
  当然,如果你实在不放心的话,可以给每个调用增加一个时间戳参数,然后服务端处理请求的时候事先检查时间错,约定对于超时的请求直接拒绝不做业务处理。
  虽然Thrift常常用于内部服务间的通信,但是在移动互联网时代将其用于移动端和服务端的通信也十分常见。此外,还可见到evernote将自己服务的API使用Thrift方式开放出来,真是厉害啊!

本文完!

参考