Thrift IDL

最近在复习 DDIA, 本来打算把一些序列化相关的内容写一篇很长的文章,预期内容从可读的 json 到 thrift/pb 到 avro, 再到 MySQL 的 Row format,到 Apache Arrow。结果最近被自己写的 bug 坑到想死,然后今天13机兵又到了…那我们从简吧,写完了赶紧打游戏。

Thrift 是一个提供了代码生成、服务端的库,允许你在上面编程:

Apache_Thrift_architecture

Facebook 内部有各种语言编写的系统,而 thrift 需要为所有的语言进行服务。他需要实现 C/S 协议,实现对应的应用层的传输,然后给用户一层透明的编写的抽象。

抽象很重要的一部分是 IDL. 相对于 JSON 来说,thrift 给我们提供了编写 idl 的能力,我们可以以此来生成特定语言的 stub 代码。但是一个很重要的事情是:

  • 假设我使用的是 JSON + REST 我可以通过 REST 的 /v 来标注对应的版本,同时小的版本修改,我们可以在服务端嵌入对应的逻辑,比如我们变更了一个字段的类型,或者添加了一个字段,我们只要在服务端处理了对应的逻辑即可。

而 Thrift 相对来说是不可读的:我们编写可读的 idl, 生成我们自己都懒得看的静态的 idl 代码,然后我们在 idl 代码上瞎搞。相对于 Json 那种谁看了都明白的结构,thrift 可能优化了空间,但是我们要面对一些问题:

  • 他生成的 binary 是怎么样的(其实不那么重要,但是理解这个才能理解下面重要的东西)
  • 变更的时候,怎么样修改 idl 是合理的

第一个问题本身没那么重要,但是他对理解“怎么样修改 idl 是合理的”很重要。实际上,对于这点,我们可以看到,thrift 会支持:

  1. Types
  2. Versioning

Types in Thrift

https://thrift.apache.org/docs/types.html

Base types

Thrift 支持:

  • boolA 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

这里需要注意:

  1. 不支持 float, 因为部分语言没有。
  2. 不支持 unsigned, 如果需要 unsigned, 你得 cast 了。

需要说明的时候,在传输的时候,他是按网络序传输的。同时,我们之前谈大小端的时候说到 double 的问题。这里在传输的时候,会把 doublereinterpret_casti64, 然后按网络序发送。

bool 会被按 i8 传输。string 会被组织成 prefix_length + data 的形式。

containers

同时,除了上面的基础类型, 它还支持容器,对应 list setmap . 这些类型要求是 iterable 的。

structs

表示结构,需要

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

你甚至可以设置 default 值,同时,可以显式设置 tag。

Thrift 还支持了 message 和 service,但是今天略过不表。

Interface

内存结构和二进制表示是分开的,实际上,thrift 甚至可以指定 JSON,xml。不过我们今天就介绍他的 IDL,所以别的不表。

在生成的时候我们有如下接口:

F7DF206F-44D2-4486-AF34-71DFB47E8821

我相信没耐心看完…有几个重要的是:

  1. read 对应的复合类型的 readBegin + readEnd
  2. write对应的复合类型的 readBegin + readEnd
  3. write 对应的 writeFieldStop

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.

上述的内容会在代码中被生成,然后按照类型被写入。

这个同时跟 idl 中的 optional required 和 default 是挂钩的,可以看看这个 SO:

https://stackoverflow.com/questions/34100425/apache-thrift-when-use-optional-before-a-list-c-server-seems-not-to-return

这里显示,idl 生成的代码,写入的时候、读取的时候会生成字段,而optional 会影响对应的写入。

Binary

binary 可以详见 Compact 和 Binary 的文档:

我们介绍 binary:

2B9FDC8D-470E-4F5E-8E55-F1B7FE54BCC2

50C3BFC4-24E5-4C9B-9466-69A9981B1E62

实际上,你会知道一点:

  1. Stop field 能 explicit 的表示结束,而且别的编码不会影响他的正确性
  2. 这里没有 name,只有 field id
  3. field 可能是乱序的

那么我们实际上知道,field id 是不能乱设的!我们也可以读读 spec 里面的: https://github.com/apache/thrift/blob/master/doc/specs/SequenceNumbers.md

编程 & Version

88075741-4060-4010-B4EF-0B9F9CDD53F8

以 C++ 为例,用户实际上除了数据,还能看到一个 __isset ,这个字段是 public 的。这个被用来识别版本:

  • 不认识的 tag 字段被丢弃
  • 有的字段设置 __isset

上述功能可以实现版本。具体可以看 whitepaper 5.3 的 case analysis:

0CB17CF3-7E2C-4DA2-A825-BAF31A220D10