6.1 thrift #
一、RPC与IDL的基本概念 #
- RPC (Remote Procedure Call,/prəˈsiːdʒər/) ,即远程过程调用。
- 费曼学习法版本:像调用本地函数一样调用远端函数;所以实现RPC框架需要: (通俗来讲,就是调用远端服务的某个方法(psm.method),并获取到对应的响应。)
- 通信协议(协议的编解码、序列化)
- 传输通信
- 服务治理(如服务发现、负载均衡、熔断等)
- 代码生成
- 补充:RPC 本质上定义了一种通信的流程,而具体的实现技术没有约束,核心需要解决的问题为序列化与网络通信。(如可以通过
gob/json/pb/thrift
来序列化和反序列化消息内容,通过socket/http
来进行网络通信。)只要客户端与服务端在这两方面达成共识,能够做到消息正确的解析接口即可。
- 费曼学习法版本:像调用本地函数一样调用远端函数;所以实现RPC框架需要: (通俗来讲,就是调用远端服务的某个方法(psm.method),并获取到对应的响应。)
- 服务架构的演进:单体应用架构 → 垂直应用架构(按应用拆分) → 分布式服务架构(独立自治、可维护性高、交付速度快)
- rpc开发流程:如基于 Thrift (/θrɪft/,n***.***** 节俭, 节约)**的RPC服务开发,通常包括如下过程:
- 编写ThriftIDL 服务接口定义;
- kitex自动生成客户端、服务端代码;
- Server端****修改handler的业务逻辑;
- Server端编译运行服务监听端口(发布)、接口测试;
- Client端编写客户端程序(overpass),经过服务发现连接上Server端程序,发起请求并接收响应;
- 一次 rpc 调用的基本流程:
Client:
- 构造请求参数,发起调用
- 通过服务发现、负载均衡等得到服务端实例地址,并建立连接; (此步骤中包含的流程称为「服务治理」,通常包括并不限于服务发现(consul) 、负载均衡、ACL(服务鉴权)、熔断、限流等等功能。这些功能是由其他组件提供的,并不是 Thrift 框架所具有的功能。)
- 请求参数序列化成二进制数据
- 通过网络将数据发送给服务端
Client:
- 接收数据
- 反序列化出结果
- 得到调用的结果
Server:
- 服务端接收数据
- 反序列化出请求参数
- handler 处理请求并返回响应结果
- 将响应结果序列化成二进制数据
- 通过网络将数据返回给客户端
- **IDL:**Interface Definition Language ( /ˌdefɪˈnɪʃn/),接口定义/描述语言。
- 费曼版本:确保双方在说的是同一个语言、同一件事;如果我们要使用 RPC 进行调用,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的。需要IDL 就是为了解决这样的问题,通过 IDL来约定双方的协议,就像在写代码的时候需要调用某个函数,我们需要知道
签名
一样。 - 优势:
- 跨语言:中立的方式描述接口和数据结构
- 规范:明确的定义接口和数据结构和行为
- 代码生成:IDL编译器,根据IDL自动生成不同编程语言的代码,极大简化了开发工作
- 费曼版本:确保双方在说的是同一个语言、同一件事;如果我们要使用 RPC 进行调用,就需要知道对方的接口是什么,需要传什么参数,同时也需要知道返回值是什么样的。需要IDL 就是为了解决这样的问题,通过 IDL来约定双方的协议,就像在写代码的时候需要调用某个函数,我们需要知道
二、thrift语法 #
- 文档: https://github.com/apache/thrift
- Thrift 2007年由Facebook开源的RPC框架,其定义了自己的IDL(接口描述语言)和RPC消息协议,与编程语言无关,可实现跨语言通信。
- 核心协议:TBinary
- 常用组合:Framed TBinary
- 缺陷:由于开源时间早于微服务框架流行时间,对微服务的支持并不是很好,需要基于源码定制。但由于字节、美团等大厂早期已经使用了Thrift,迁移到gRPC成本大,所以大厂一直沿用了Thrift
Kitex 默认支持 thrift
和 proto3
两种 IDL。本文简单介绍 Thrift IDL 语法,
注意:Thrift 是一款 RPC 框架,其使用的 IDL 以 .thrift 为后缀,故常常也使用 thrift 来表示 IDL,请根据上下文判断语意。
类型 #
1. 基本类型 #
Thrift IDL 有以下几种基本类型:
- bool: 布尔型
- byte: 有符号字节
- i8: 8位有符号整型(低版本不支持)
- i16: 16位有符号整型
- i32: 32位有符号整型
- i64: 64位有符号整型 (注意:Thrift IDL 没有无符号整数类型。因为许多编程语言中没有原生的无符号整数类型。)
- double: 64位浮点型
- string: 字符串(编码方式未知或二进制字符串)
- binary: 表示无编码要求的 byte 二进制数组。因此是字节数组情况下(比如 json/pb 序列化后数据在 thrift rpc 间传输)请使用 binary 类型,不要使用 string 类型;
- Golang 的实现 string/binary 都使用 string 类型进行存储,string 底层只是字节数组不保证是 UTF-8 编码的,可能与其他语言的行为不一致。
2. 容器类型 #
Thrift 提供的容器是强类型容器,映射到大多数编程语言中常用的容器类型。具体包括以下三种容器:
- list< t1 >: 元素类型为 t1 的有序列表,允许元素重复。Translates to an STL vector, Java ArrayList, native arrays in scripting languages, etc.
- set< t1 >: 元素类型为 t1 的无序表,不允许元素重复。
- map<t1,t2>: 键类型为 t1,值类型为 t2 的 map。
3. 枚举类型 #
Thrift 提供了枚举类型
- 编译器默认从 0 开始赋值
- 可以对某个变量进行赋值(整数)
- 不支持嵌套的 enum
|
|
4. 类型定义 #
Thrift 支持类似 C/C++ 的类型定义,注意:typedef 定义的末尾没有分号
|
|
5. 常量定义 #
Thrift 内定义常量的方式如下:
|
|
7. Struct 及 Requiredness 说明 #
Struct 由不同的 fields 构成,其中每个 field 有唯一的整型 id,类型 type,名字 name 和 一个可选择设置的默认值 default value。
- Field id:每个 field 必须有一个正整数的标志符
- type:包括三种类型:
- required:必填字段,如果对端没有收到该字段会返回error。(从维护角度不建议用 required 修饰字段)
- 注意:该修饰在 thrift 官方的解释如下,期望被 set,在 Golang 的实现里如果某个字段为 nil,实际还是会编码,所以对端收到的是空struct。
- optional:可选字段。未赋值时不会编码,对端也不够早
- 若没有设置该字段且没有默认值的话,则不对该字段进行序列化
- 对于非指针字段,需要调用 NewXXX 方法来初始化结构体才能填入默认值,不能用 &XXX{} 方式
- default:不加修饰则是 default 类型,即使未赋值也会编码
- 注意:发送方发 nil,接收方会构造默认值,如果希望接收方同样接收 nil 需要用 optional 修饰
- required:必填字段,如果对端没有收到该字段会返回error。(从维护角度不建议用 required 修饰字段)
|
|
注意:
- Thrift 不支持嵌套定义 Struct
- 如果 struct 已经在使用了,请不要更改各个 field 的 id 和 type
- 如果没有特殊需求,建议都使用 optional。由于 Kitex 需要保留和 apache 官方 Go 实现兼容性,也保留了对于 required 和 default 修饰的字段处理逻辑的不合理之处。例如 Request(struct类型).User(struct类型).Name(string类型) 这样一个结构,如果 User 和 Name 都是 required,但 client 端没有给 User 赋值(即 request.User == nil), 在 client 端编码不会报错,但是会将 User 的 id 和 type(struct) 写入,在 server 端解码时会初始化 User(即 request.User != nil),但是在继续解码 User 时读不到 Name 字段,就会报错。
8. Exception #
Exception 与 struct 类似,但它被用来集成 目标编程语言 中的异常处理机制。Exception 内定义的所有 field 的名字都是唯一的。
IDL示例 #
1. 注释 #
Thrift 支持 c风格的多行注释 和 c++/Java 风格的单行注释
|
|
2. 命名空间namespace #
Thrift 的命名空间与 C++ 的 namespace 和 go 的 package 类似,提供了一种组织(隔离)代码的方式,也可避免类型定义内名字冲突的问题。
Thrift 提供了针对不同语言的 namespace 定义方式,各语言建议单独定义:
|
|
3. Include #
引用其他IDL文件中的定义,通过include可以很少的实现模块化、复用、拆分结构体定义,方便管理、维护 IDL。用户可利用文件名作为前缀对具体定义进行访问。
|
|
4. RPC Service定义 #
Thrift 内的 service 定义在语义上和 oop 内的接口是相同的。代码生成工具会根据 service 的定义生成 client 和 service 端的接口实现。
- Service 的参数和返回值类型可以是 基础类型 或者 struct
oneway 本身不具有可靠性,且在处理上比较特殊会带来一些隐患,不建议使用
|
|
5. IDL 示例 #
以下为简单的 thrift idl 示例,包含 common.thrift 和 service.thrift 两个文件。
- common.thrift:包含各种类型的使用和 struct 的定义。
|
|
- service.thrift:引用 common.thrift,定义 service。
- tMethod:接收一个类型为 TestRequest 的参数,返回一个类型为 TestResponse 的返回值。
|
|
6. Kitex Thrift IDL 规范 #
为满足服务调用的规范,Kitex 对 IDL 的定义提出了一些必须遵守的要求:
- 方法只能拥有一个参数,并且这个参数类型必须是自定义的 Struct 类型,参数类型名字使用驼峰命名法,通常为:
XXXRequest
- 方法的返回值类型必须是自定义的 Struct 类型,不可以为 void,使用驼峰命名法,通常为:
XXXResponse
Thrift消息协议-TBinary编码 #
- 补充:由于JSON数据格式传输体积大,编码低效,性能损失较多,而RPC场景对性能有更高的要求,所以很少采用TJSON协议。