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 的 int8i16 small_num // 16位,对应 Go 的 int16i32 normal_num // 32位,对应 Go 的 int32i64 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)}生成代码:
thrift --gen go user.thriftgen-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/OProtocolFactory:负责创建序列化协议实例
6. Thrift vs ProtoBuf
6.1 核心差异
| 特性 | Thrift | ProtoBuf |
|---|---|---|
| 定位 | 完整 RPC 框架 | 序列化协议 |
| 类型系统 | 支持 Set、Exception | 仅支持基础类型 |
| 协议层 | 可选(Binary/Compact/JSON) | 固定(Protobuf Wire Format) |
| 传输层 | 可选(Socket/Framed/HTTP) | 依赖 gRPC |
| 服务端模型 | 多种(Simple/ThreadPool/NonBlocking) | 依赖 gRPC |
| 字段修饰符 | required/optional | proto3 全部可选 |
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 性能优化
// 使用对象池复用 Transportvar 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 框架,核心优势在于:
- 灵活的分层设计:协议层、传输层、服务端模型可自由组合
- 丰富的类型系统:原生支持 Set、Exception 等类型
- 跨语言支持:支持 20+ 种编程语言
适用场景:
- 需要灵活选择序列化协议的场景(如带宽敏感时用 Compact)
- 需要使用 Set 类型或显式 required 校验
- 使用 CloudWeGo 生态(Kitex 基于 Thrift)
学习路径建议:
- 先掌握 IDL 语法,理解 required/optional 的区别
- 了解三种序列化协议的适用场景
- 理解传输层的选择(Framed 用于 NonBlocking Server)
- 在实际项目中尝试使用 Kitex(对 Thrift 的高性能封装)
通过本文,你应该已经掌握了 Thrift 的核心原理和使用方法。接下来可以学习 Kitex,它是对 Thrift 的高性能封装,提供了更好的开发体验。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


