CloudWeGo-Thrift

2124 字
11 分钟
CloudWeGo-Thrift

1. 为什么需要 Thrift#

如果你已经熟悉 ProtoBuf,可能会问:为什么还需要 Thrift?

1.1 ProtoBuf 的局限性#

ProtoBuf 是优秀的序列化协议,但它有一些限制:

  • 仅定义数据结构:ProtoBuf 主要关注 Message 定义,服务定义(Service)是后来才加入的
  • RPC 框架绑定:使用 gRPC 时,你被绑定到 Google 的生态(HTTP/2、gRPC 协议)
  • 类型系统有限:不支持某些复杂类型(如 Set)

1.2 Thrift 的核心优势#

Thrift 是 Facebook 开源的跨语言 RPC 框架,它的设计目标是:

底层逻辑:Thrift 不仅是序列化协议,更是完整的 RPC 解决方案。它包含:

  • IDL 层:定义数据结构和服务接口
  • 协议层:多种序列化协议(Binary/Compact/JSON)可选
  • 传输层:多种传输方式(Socket/HTTP/FramedTransport)可选
  • 服务端模型:多种并发模型(Simple/ThreadPool/NonBlocking)可选

与 ProtoBuf 的核心差异:

  • Thrift 是”全家桶”,ProtoBuf 是”单一工具”
  • Thrift 的协议层和传输层可以自由组合
  • Thrift 原生支持更丰富的类型系统(Set、Exception)

2. Thrift IDL 语法#

2.1 基础类型#

Thrift 支持的基础类型比 ProtoBuf 更丰富:

// 布尔型
bool is_active
// 整数型(有符号)
i8 tiny_num // 8位,对应 Go 的 int8
i16 small_num // 16位,对应 Go 的 int16
i32 normal_num // 32位,对应 Go 的 int32
i64 big_num // 64位,对应 Go 的 int64
// 浮点型
double price // 64位浮点数,对应 Go 的 float64
// 字符串和二进制
string username // UTF-8 字符串
binary avatar // 二进制数据(对应 Go 的 []byte)

底层逻辑:Thrift 的类型系统直接映射到各语言的原生类型,不像 ProtoBuf 需要通过 varint 编码优化。

2.2 容器类型#

Thrift 支持三种容器类型:

// List:有序列表,允许重复
list<string> hobbies
// Set:无序集合,不允许重复
set<i64> follower_ids
// Map:键值对映射
map<string, string> metadata

与 ProtoBuf 的差异:

  • ProtoBuf 只有 repeated(对应 List)和 map
  • Thrift 原生支持 set,ProtoBuf 需要用 repeated + 去重逻辑模拟

2.3 结构体定义#

struct User {
1: required i64 id // 必填字段
2: required string username // 必填字段
3: optional string email // 可选字段
4: optional bool is_active = true // 可选字段,带默认值
}

关键概念:

  • 字段编号:1:, 2: 是字段的唯一标识,类似 ProtoBuf 的 Tag
  • required/optional:显式声明字段是否必填
    • required:序列化时必须有值,反序列化时必须存在
    • optional:可以不设置,反序列化时缺失不会报错

底层逻辑:Thrift 的 required 会在运行时检查,如果缺失会抛出异常。ProtoBuf 在 proto3 中移除了 required,所有字段都是可选的。

2.4 枚举和异常#

// 枚举定义
enum UserStatus {
NORMAL = 1,
BANNED = 2,
DELETED = 3
}
// 异常定义(类似结构体,但用于错误处理)
exception UserNotFoundException {
1: string message
2: i64 user_id
}

底层逻辑:Thrift 的 exception 会被映射到各语言的异常机制:

  • Go:生成实现 error 接口的结构体
  • Java:生成继承 Exception 的类
  • Python:生成继承 Exception 的类

2.5 服务定义#

service UserService {
// 定义 RPC 方法
User getUser(1: i64 user_id) throws (1: UserNotFoundException e)
// void 返回类型
void deleteUser(1: i64 user_id)
// oneway:不等待响应(异步调用)
oneway void logEvent(1: string event)
}

关键概念:

  • throws:声明可能抛出的异常
  • oneway:客户端发送请求后立即返回,不等待服务端响应(类似消息队列)

底层逻辑:oneway 方法在网络层不会等待响应包,适合日志、监控等场景。

3. 序列化协议#

Thrift 的核心优势是协议层可插拔,支持多种序列化格式。

3.1 Binary Protocol#

最常用的协议,紧凑且高效。

transport := thrift.NewTSocket("localhost:9090")
protocol := thrift.NewTBinaryProtocol(transport, true, true)
client := userservice.NewUserServiceClient(thrift.NewTStandardClient(protocol, protocol))

底层逻辑:

  • 使用固定长度编码(i32 占 4 字节,i64 占 8 字节)
  • 字段按顺序编码:[字段类型][字段ID][字段值]
  • 不压缩,但解析速度快

3.2 Compact Protocol#

压缩版本,牺牲少量性能换取更小的体积。

protocol := thrift.NewTCompactProtocol(transport)

底层逻辑:

  • 使用 varint 编码整数(类似 ProtoBuf)
  • 字段 ID 使用 zigzag 编码
  • 体积比 Binary 小 20-30%,但编解码稍慢

3.3 JSON Protocol#

人类可读的格式,用于调试或与前端交互。

protocol := thrift.NewTJSONProtocol(transport)

底层逻辑:

  • 直接序列化为 JSON 字符串
  • 体积最大,性能最差
  • 适合调试或跨语言兼容性要求高的场景

3.4 协议对比#

协议体积速度可读性适用场景
Binary中等最快不可读生产环境(默认)
Compact最小较快不可读带宽敏感场景
JSON最大最慢可读调试、前端交互

4. 传输层#

Thrift 的传输层负责数据的读写,也是可插拔的。

4.1 TSocket#

最基础的 TCP Socket 传输。

transport := thrift.NewTSocket("localhost:9090")

底层逻辑:直接使用 TCP 连接,每次 RPC 调用对应一次完整的请求-响应。

4.2 TFramedTransport#

带帧头的传输,用于非阻塞服务器。

transport := thrift.NewTFramedTransport(thrift.NewTSocket("localhost:9090"))

底层逻辑:

  • 每个消息前加 4 字节的长度头:[长度][消息体]
  • 服务端可以先读取长度,再读取完整消息体
  • 适合 NonBlocking Server(基于 epoll/kqueue)

4.3 TBufferedTransport#

带缓冲的传输,减少系统调用次数。

transport := thrift.NewTBufferedTransport(thrift.NewTSocket("localhost:9090"), 8192)

底层逻辑:在内存中维护读写缓冲区,积累到一定大小再调用 write(),减少系统调用开销。

5. 代码生成与使用#

5.1 生成 Go 代码#

假设你有一个 user.thrift 文件:

namespace go user
struct User {
1: required i64 id
2: required string username
}
service UserService {
User getUser(1: i64 user_id)
}

生成代码:

Terminal window
thrift --gen go user.thrift
  • gen-go/user/user.go:所有 Thrift 生成的接口、客户端、服务端代码都在这一个 user.go 文件中。

5.2 实现服务端#

package main
import (
"context"
"fmt"
"github.com/apache/thrift/lib/go/thrift"
"your-module/gen-go/user"
)
// 实现服务接口
type UserServiceHandler struct
func (h *UserServiceHandler) GetUser(ctx context.Context, userID int64) (*user.User, error) {
return &user.User{
ID: userID,
Username: "TestUser",
}, nil
}
func main() {
handler := &UserServiceHandler{}
processor := user.NewUserServiceProcessor(handler)
transport, _ := thrift.NewTServerSocket(":9090")
server := thrift.NewTSimpleServer4(
processor,
transport,
thrift.NewTBufferedTransportFactory(8192),
thrift.NewTBinaryProtocolFactoryDefault(),
)
fmt.Println("Starting server on :9090")
server.Serve()
}

底层逻辑:

  • Processor:负责解析请求、调用 Handler、序列化响应
  • Transport:负责网络 I/O
  • ProtocolFactory:负责创建序列化协议实例

6. Thrift vs ProtoBuf#

6.1 核心差异#

特性ThriftProtoBuf
定位完整 RPC 框架序列化协议
类型系统支持 Set、Exception仅支持基础类型
协议层可选(Binary/Compact/JSON)固定(Protobuf Wire Format)
传输层可选(Socket/Framed/HTTP)依赖 gRPC
服务端模型多种(Simple/ThreadPool/NonBlocking)依赖 gRPC
字段修饰符required/optionalproto3 全部可选

6.2 选择建议#

选择 Thrift 的场景:

  • 需要灵活的协议层和传输层组合
  • 需要使用 Set 类型
  • 需要显式的 required 字段校验
  • 已有 Thrift 生态(如使用 Kitex)

选择 ProtoBuf 的场景:

  • 需要与 gRPC 生态集成
  • 需要更好的向后兼容性(proto3 的设计更宽松)
  • 需要更广泛的语言支持(ProtoBuf 社区更活跃)

7. 实战技巧#

7.1 字段演化#

Thrift 支持字段的增删改,但需要遵循规则:

struct User {
1: required i64 id
2: required string username
// 3: string old_field // 已废弃,不要删除编号
4: optional string email // 新增字段,使用新编号
}

底层逻辑:

  • 不要修改已有字段的编号
  • 不要将 optional 改为 required(会导致旧客户端反序列化失败)
  • 废弃字段保留编号,避免未来冲突

7.2 性能优化#

// 使用对象池复用 Transport
var transportPool = sync.Pool{
New: func() interface{} {
transport, _ := thrift.NewTSocket("localhost:9090")
return thrift.NewTBufferedTransport(transport, 8192)
},
}
func callRPC() {
transport := transportPool.Get().(thrift.TTransport)
defer transportPool.Put(transport)
transport.Open()
defer transport.Close()
protocol := thrift.NewTBinaryProtocol(transport, true, true)
client := user.NewUserServiceClient(thrift.NewTStandardClient(protocol, protocol))
result, _ := client.GetUser(context.Background(), 123)
fmt.Println(result)
}

底层逻辑:复用 Transport 对象可以减少连接建立开销和内存分配。

8. 总结#

Thrift 是一个功能完整的 RPC 框架,核心优势在于:

  1. 灵活的分层设计:协议层、传输层、服务端模型可自由组合
  2. 丰富的类型系统:原生支持 Set、Exception 等类型
  3. 跨语言支持:支持 20+ 种编程语言

适用场景:

  • 需要灵活选择序列化协议的场景(如带宽敏感时用 Compact)
  • 需要使用 Set 类型或显式 required 校验
  • 使用 CloudWeGo 生态(Kitex 基于 Thrift)

学习路径建议:

  1. 先掌握 IDL 语法,理解 required/optional 的区别
  2. 了解三种序列化协议的适用场景
  3. 理解传输层的选择(Framed 用于 NonBlocking Server)
  4. 在实际项目中尝试使用 Kitex(对 Thrift 的高性能封装)

通过本文,你应该已经掌握了 Thrift 的核心原理和使用方法。接下来可以学习 Kitex,它是对 Thrift 的高性能封装,提供了更好的开发体验。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

CloudWeGo-Thrift
https://www.lansganbs.cn/posts/项目开发/cloudwego-thrift/
作者
Zowely
发布于
2026-02-03
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Zowely
红叶最多情,一舞寄相思。
公告
欢迎来到我的博客!这里分享计算机等相关内容。