golang源码分析(3):thrift

2022-08-02 16:20:11 浏览数 (1)

thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C , Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。

  • thrift最初由facebook开发,07年四月开放源码,08年5月进入apache孵化器。
  • thrift允许定义一个简单的定义文件中的数据类型和服务接口,以作为输入文件,编译器生成代码用来方便地生成RPC客户端和服务器通信的无缝跨编程语言。
  • 类似Thrift的工具,还有Avro、protocol buffer,但相对于Thrift来讲,都没有Thrift支持全面和使用广泛。

Thrift自下到上可以分为4层

  • Server(single-threaded, event-driven etc)
  • 服务器进程调度
  • Processor(compiler generated)
  • RPC接口处理函数分发,IDL定义接口的实现将挂接到这里面
  • Protocol (JSON, compact etc)
  • 协议
  • Transport(raw TCP, HTTP etc)
  • 网络传输

Thrift实际上是实现了C/S模式,通过代码生成工具将接口定义文件生成服务器端和客户端代码(可以为不同语言),从而实现服务端和客户端跨语言的支持。用户在Thirft描述文件中声明自己的服务,这些服务经过编译后会生成相应语言的代码文件,然后用户实现服务(客户端调用服务,服务器端提服务)便可以了。其中protocol(协议层, 定义数据传输格式,可以为二进制或者XML等)和transport(传输层,定义数据传输方式,可以为TCP/IP传输,内存共享或者文件共享等)被用作运行时库。

Thrift支持的传输及服务模型

支持的传输格式:

参数

描述

TBinaryProtocol

二进制格式

TCompactProtocol

压缩格式

TJSONProtocol

JSON格式

TSimpleJSONProtocol

提供JSON只写协议, 生成的文件很容易通过脚本语言解析。

TDebugProtocol

使用易懂的可读的文本格式,以便于debug

支持的数据传输方式:

参数

描述

TSocket

阻塞式socker

TFramedTransport

以frame为单位进行传输,非阻塞式服务中使用。

TFileTransport

以文件形式进行传输。

TMemoryTransport

将内存用于I/O. java实现时内部实际使用了简单的ByteArrayOutputStream。

TZlibTransport

使用zlib进行压缩, 与其他传输方式联合使用。当前无java实现。

支持的服务模型:

参数

描述

TSimpleServer

简单的单线程服务模型,常用于测试

TThreadPoolServer

多线程服务模型,使用标准的阻塞式IO。

TNonblockingServer

多线程服务模型,使用非阻塞式IO(需使用TFramedTransport数据传输方式)

Thrift 下载及安装

如何获取Thrift

  1. 官网:http://thrift.apache.org/
  2. golang的Thrift包:
代码语言:javascript复制
go get git.apache.org/thrift.git/lib/go/thrift

如何安装Thrift

mac下安装Thrift,参考上一篇介绍 其他平台安装自行挖掘,呵呵。 安装后通过

代码语言:javascript复制
liuxinmingMacBook-Rro#:thrift -version
Thrift version 0.9.2 #看到这一行表示安装成功

Golang、PHP通过Thrift调用

先发个官方各种语言DEMO地址 https://git1-us-west.apache.org/repos/asf?p=thrift.git;a=tree;f=tutorial;h=d69498f9f249afaefd9e6257b338515c0ea06390;hb=HEAD

Thrift的协议库IDL文件

语法参考

  • 参考资料
  • http://www.cnblogs.com/tianhuilove/archive/2011/09/05/2167669.html
  • http://my.oschina.net/helight/blog/195015
基本类型
  • bool: 布尔值 (true or false), one byte
  • byte: 有符号字节
  • i16: 16位有符号整型
  • i32: 32位有符号整型
  • i64: 64位有符号整型
  • double: 64位浮点型
  • string: Encoding agnostic text or binary string

基本类型中基本都是有符号数,因为有些语言没有无符号数,所以Thrift不支持无符号整型。

特殊类型
  • binary: Blob (byte array) a sequence of unencoded bytes

这是string类型的一种变形,主要是为java使用

struct结构体

thrift中struct是定义为一种对象,和面向对象语言的class差不多.,但是struct有以下一些约束: struct不能继承,但是可以嵌套,不能嵌套自己。 1. 其成员都是有明确类型 2. 成员是被正整数编号过的,其中的编号使不能重复的,这个是为了在传输过程中编码使用。 3. 成员分割符可以是逗号(,)或是分号(;),而且可以混用,但是为了清晰期间,建议在定义中只使用一种,比如C 学习者可以就使用分号(;)。 4. 字段会有optional和required之分和protobuf一样,但是如果不指定则为无类型–可以不填充该值,但是在序列化传输的时候也会序列化进去, optional是不填充则部序列化。 required是必须填充也必须序列化。 5. 每个字段可以设置默认值 6. 同一文件可以定义多个struct,也可以定义在不同的文件,进行include引入。

代码语言:javascript复制
struct Work {
  1: i32 num1 = 0,
  2: i32 num2,
  3: Operation op,
  4: optional string comment,
}
容器(Containers)

Thrift3种可用容器类型:

  • list(t): 元素类型为t的有序表,容许元素重复。
  • set(t):元素类型为t的无序表,不容许元素重复。对应c 中的set,java中的HashSet,python中的set,php中没有set,则转换为list类型。
  • map(t,t): 键类型为t,值类型为t的kv对,键不容许重复。对用c 中的map, Java的HashMap, PHP 对应 array, Python/Ruby 的dictionary。

容器中元素类型可以是除了service外的任何合法Thrift类型(包括结构体和异常)。为了最大的兼容性,map的key最好是thrift的基本类型,有些语言不支持复杂类型的key,JSON协议只支持那些基本类型的key。 容器都是同构容器,不失异构容器。

1. RpcServer 负责导出(export)远程接口 2. RpcClient 负责导入(import)远程接口的代理实现 3. RpcProxy 远程接口的代理实现 4. RpcInvoker 客户方实现:负责编码调用信息和发送调用请求到服务方并等待调用结果返回 服务方实现:负责调用服务端接口的具体实现并返回调用结果 5. RpcProtocol 负责协议编/解码 6. RpcConnector 负责维持客户方和服务方的连接通道和发送数据到服务方 7. RpcAcceptor 负责接收客户方请求并返回请求结果 8. RpcProcessor 负责在服务方控制调用过程,包括管理调用线程池、超时时间等 9. RpcChannel 数据传输通道 协议编解码 客户端代理在发起调用前需要对调用信息进行编码,这就要考虑需要编码些什么信息并以什么格式传输到服务端才能让服务端完成调用。出于效率考虑,编码的信息越少越好(传输数据少),编码的规则越简单越好(执行效率高)。我们先看下需要编码些什么信息: -- 调用编码 -- 1. 接口方法 包括接口名、方法名 2. 方法参数 包括参数类型、参数值 3. 调用属性 包括调用属性信息,例如调用附件隐式参数、调用超时时间等 -- 返回编码 -- 1. 返回结果 接口方法中定义的返回值 2. 返回码 异常返回码 3. 返回异常信息 调用异常信息 除了以上这些必须的调用信息,我们可能还需要一些元信息以方便程序编解码以及未来可能的扩展。这样我们的编码消息里面就分成了两部分,一部分是元信息、另一部分是调用的必要信息。如果设计一种 RPC 协议消息的话,元信息我们把它放在协议消息头中,而必要信息放在协议消息体中。下面给出一种概念上的 RPC 协议消息设计格式: -- 消息头 -- magic : 协议魔数,为解码设计 header size: 协议头长度,为扩展设计 version : 协议版本,为兼容设计 st : 消息体序列化类型 hb : 心跳消息标记,为长连接传输层心跳设计 ow : 单向消息标记, rp : 响应消息标记,不置位默认是请求消息 status code: 响应消息状态码 reserved : 为字节对齐保留 message id : 消息 id body size : 消息体长度 -- 消息体 -- 采用序列化编码,常见有以下格式 xml : 如 webservie soap json : 如 JSON-RPC binary: 如 thrift; hession; kryo 等 格式确定后编解码就简单了,由于头长度一定所以我们比较关心的就是消息体的序列化方式。序列化我们关心三个方面: 1. 序列化和反序列化的效率,越快越好。 2. 序列化后的字节长度,越小越好。 3. 序列化和反序列化的兼容性,接口参数对象若增加了字段,是否兼容。 上面这三点有时是鱼与熊掌不可兼得,这里面涉及到具体的序列化库实现细节,就不在本文进一步展开分析了。 传输服务 协议编码之后,自然就是需要将编码后的 RPC 请求消息传输到服务方,服务方执行后返回结果消息或确认消息给客户方。RPC 的应用场景实质是一种可靠的请求应答消息流,和 HTTP 类似。因此选择长连接方式的 TCP 协议会更高效,与 HTTP 不同的是在协议层面我们定义了每个消息的唯一 id,因此可以更容易的复用连接。 既然使用长连接,那么第一个问题是到底 client 和 server 之间需要多少根连接?实际上单连接和多连接在使用上没有区别,对于数据传输量较小的应用类型,单连接基本足够。单连接和多连接最大的区别在于,每根连接都有自己私有的发送和接收缓冲区,因此大数据量传输时分散在不同的连接缓冲区会得到更好的吞吐效率。所以,如果你的数据传输量不足以让单连接的缓冲区一直处于饱和状态的话,那么使用多连接并不会产生任何明显的提升,反而会增加连接管理的开销。 连接是由 client 端发起建立并维持。如果 client 和 server 之间是直连的,那么连接一般不会中断(当然物理链路故障除外)。如果 client 和 server 连接经过一些负载中转设备,有可能连接一段时间不活跃时会被这些中间设备中断。为了保持连接有必要定时为每个连接发送心跳数据以维持连接不中断。心跳消息是 RPC 框架库使用的内部消息,在前文协议头结构中也有一个专门的心跳位,就是用来标记心跳消息的,它对业务应用透明。 执行调用 client stub 所做的事情仅仅是编码消息并传输给服务方,而真正调用过程发生在服务方。server stub 从前文的结构拆解中我们细分了 RpcProcessor 和 RpcInvoker 两个组件,一个负责控制调用过程,一个负责真正调用。这里我们还是以 java 中实现这两个组件为例来分析下它们到底需要做什么? java 中实现代码的动态接口调用目前一般通过反射调用。除了原生的 jdk 自带的反射,一些第三方库也提供了性能更优的反射调用,因此 RpcInvoker 就是封装了反射调用的实现细节。 调用过程的控制需要考虑哪些因素,RpcProcessor 需要提供什么样地调用控制服务呢?下面提出几点以启发思考: 1. 效率提升 每个请求应该尽快被执行,因此我们不能每请求来再创建线程去执行,需要提供线程池服务。 2. 资源隔离 当我们导出多个远程接口时,如何避免单一接口调用占据所有线程资源,而引发其他接口执行阻塞。 3. 超时控制 当某个接口执行缓慢,而 client 端已经超时放弃等待后,server 端的线程继续执行此时显得毫无意义。 RPC 异常处理 无论 RPC 怎样努力把远程调用伪装的像本地调用,但它们依然有很大的不同点,而且有一些异常情况是在本地调用时绝对不会碰到的。在说异常处理之前,我们先比较下本地调用和 RPC 调用的一些差异: 1. 本地调用一定会执行,而远程调用则不一定,调用消息可能因为网络原因并未发送到服务方。 2. 本地调用只会抛出接口声明的异常,而远程调用还会跑出 RPC 框架运行时的其他异常。 3. 本地调用和远程调用的性能可能差距很大,这取决于 RPC 固有消耗所占的比重。 正是这些区别决定了使用 RPC 时需要更多考量。当调用远程接口抛出异常时,异常可能是一个业务异常,也可能是 RPC 框架抛出的运行时异常(如:网络中断等)。业务异常表明服务方已经执行了调用,可能因为某些原因导致未能正常执行,而 RPC 运行时异常则有可能服务方根本没有执行,对调用方而言的异常处理策略自然需要区分。 由于 RPC 固有的消耗相对本地调用高出几个数量级,本地调用的固有消耗是纳秒级,而 RPC 的固有消耗是在毫秒级。那么对于过于轻量的计算任务就并不合适导出远程接口由独立的进程提供服务,只有花在计算任务上时间远远高于 RPC 的固有消耗才值得导出为远程接口提供服务。

0 人点赞