HTTP/2 与 RPC 精讲教程 / 12 - Apache Thrift
第 12 章:Apache Thrift
Facebook 出品的跨语言 RPC 框架——另一种选择
12.1 Thrift 概述
Apache Thrift 是 Facebook 于 2007 年开源的跨语言 RPC 框架,后捐赠给 Apache 基金会。它通过接口定义语言(IDL)定义服务,自动生成多语言的客户端和服务器代码。
12.1.1 Thrift vs gRPC 概览
| 维度 | Thrift | gRPC |
|---|---|---|
| 开发者 | Facebook (Apache) | |
| IDL | Thrift IDL | Protocol Buffers |
| 传输协议 | 多种(TCP/HTTP/…) | HTTP/2 |
| 序列化格式 | 多种(Binary/Compact/JSON) | Protobuf |
| 流式传输 | 不支持 | 原生支持 |
| 浏览器支持 | 有限 | gRPC-Web |
| 语言支持 | 25+ | 11+ 官方 |
| 成熟度 | 高(2007 年至今) | 高(2015 年至今) |
| 社区活跃度 | 中等 | 非常活跃 |
12.1.2 Thrift 架构
┌───────────────────────────────────────────────────────┐
│ 应用代码 │
├───────────────────────────────────────────────────────┤
│ 生成的 Stub/Skeleton │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Client Stub │ │ Server Skeleton│ │
│ └───────┬───────┘ └───────┬───────┘ │
├──────────┼─────────────────────────┼──────────────────┤
│ Processor (业务逻辑处理器) │
├───────────────────────────────────────────────────────┤
│ Protocol (序列化/反序列化) │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐ │
│ │ Binary │ │ Compact │ │ JSON │ │ ... │ │
│ └──────────┘ └───────────┘ └──────────┘ └─────────┘ │
├───────────────────────────────────────────────────────┤
│ Transport (传输层) │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐ │
│ │TSocket │ │TBuffered │ │TFramed │ │THttp │ │
│ │(TCP) │ │(带缓冲) │ │(分帧) │ │(HTTP) │ │
│ └──────────┘ └───────────┘ └──────────┘ └─────────┘ │
└───────────────────────────────────────────────────────┘
12.2 Thrift IDL
12.2.1 基本语法
// user.thrift
namespace go example.user
namespace java example.user
namespace py example.user
// 基本类型
// bool, byte, i16, i32, i64, double, string, binary
// 枚举
enum UserStatus {
ACTIVE = 1,
INACTIVE = 2,
BANNED = 3,
}
// 结构体
struct User {
1: required i64 id,
2: required string name,
3: required string email,
4: optional UserStatus status = UserStatus.ACTIVE,
5: optional list<string> roles,
6: optional map<string, string> metadata,
}
// 异常
exception UserNotFound {
1: i64 id,
2: string message,
}
exception InvalidRequest {
1: string field,
2: string message,
}
// 服务定义
service UserService {
// 简单 RPC
User getUser(1: i64 id) throws (1: UserNotFound err),
void createUser(1: User user) throws (1: InvalidRequest err),
list<User> listUsers(1: i32 page_size, 2: string cursor),
// 单向调用(fire-and-forget)
oneway void notifyUser(1: i64 id, 2: string message),
}
12.2.2 IDL 高级特性
// advanced.thrift
namespace go example.advanced
// 常量
const i32 MAX_PAGE_SIZE = 100
const string DEFAULT_LANG = "zh-CN"
// 类型别名
typedef map<string, string> Headers
typedef list<User> UserList
// 结构体继承
struct BaseResponse {
1: required i32 code,
2: required string message,
}
struct UserResponse extends BaseResponse {
3: optional User user,
}
// 包含其他 thrift 文件
include "user.thrift"
// 服务继承
service AdminService extends user.UserService {
void banUser(1: i64 id, 2: string reason),
list<User> searchUsers(1: string query, 2: i32 limit = MAX_PAGE_SIZE),
}
// 协程安全注解(Go 特有)
service SafeService {
// cpp 和 go 中生成线程安全代码
User getUser(1: i64 id),
}(concurrent)
12.3 传输协议(Transport)
12.3.1 传输类型
| Transport | 说明 | 适用场景 |
|---|---|---|
| TSocket | 原始 TCP socket | 简单的点对点通信 |
| TBufferedTransport | 带缓冲的传输 | 减少系统调用次数 |
| TFramedTransport | 分帧传输 | 非阻塞/异步服务器 |
| THttpTransport | HTTP 传输 | 穿越防火墙/代理 |
| TZlibTransport | 压缩传输 | 带宽受限场景 |
// Go 中使用不同的 Transport
package main
import (
"github.com/apache/thrift/lib/go/thrift"
)
func createSocketTransport() thrift.TTransport {
// 原始 TCP
return thrift.NewTSocketConf("localhost:9090", &thrift.TConfiguration{})
}
func createBufferedTransport() thrift.TTransport {
socket := thrift.NewTSocketConf("localhost:9090", &thrift.TConfiguration{})
// 带缓冲,缓冲区 8KB
return thrift.NewTBufferedTransport(socket, 8192)
}
func createFramedTransport() thrift.TTransport {
socket := thrift.NewTSocketConf("localhost:9090", &thrift.TConfiguration{})
// 分帧传输,适合非阻塞服务器
return thrift.NewTFramedTransportConf(socket, &thrift.TConfiguration{})
}
12.4 序列化协议(Protocol)
12.4.1 协议对比
| Protocol | 编码格式 | 特点 | 适用场景 |
|---|---|---|---|
| TBinaryProtocol | 二进制 | 简单直接,空间效率中等 | 通用场景 |
| TCompactProtocol | 压缩二进制 | 变长整数编码,空间最优 | 带宽受限 |
| TJSONProtocol | JSON | 人类可读 | 调试/浏览器 |
| THeaderProtocol | 二进制 + 头部 | 支持元数据、压缩 | 现代应用 |
# Python 中使用不同协议
from thrift.protocol import TBinaryProtocol
from thrift.protocol import TCompactProtocol
from thrift.protocol import TJSONProtocol
from thrift.transport import TSocket, TTransport
# 二进制协议(默认,推荐)
def create_binary_client(host, port):
transport = TSocket.TSocket(host, port)
transport = TTransport.TBufferedTransport(transport)
protocol = TBinaryProtocol.TBinaryProtocol(transport)
transport.open()
return protocol
# 紧凑协议(更小的编码体积)
def create_compact_client(host, port):
transport = TSocket.TSocket(host, port)
transport = TTransport.TBufferedTransport(transport)
protocol = TCompactProtocol.TCompactProtocol(transport)
transport.open()
return protocol
# JSON 协议(便于调试)
def create_json_client(host, port):
transport = TSocket.TSocket(host, port)
transport = TTransport.TBufferedTransport(transport)
protocol = TJSONProtocol.TJSONProtocol(transport)
transport.open()
return protocol
12.5 代码生成与服务实现
12.5.1 生成命令
# 安装 Thrift 编译器
# Ubuntu
sudo apt-get install thrift-compiler
# macOS
brew install thrift
# 生成 Go 代码
thrift --gen go user.thrift
# 生成 Python 代码
thrift --gen py user.thrift
# 生成 Java 代码
thrift --gen java user.thrift
# 生成多个语言
thrift --gen go --gen py --gen java user.thrift
12.5.2 Go 服务实现
package main
import (
"context"
"fmt"
"log"
"github.com/apache/thrift/lib/go/thrift"
"example/gen-go/user"
)
// 实现 UserService 接口
type UserHandler struct {
users map[int64]*user.User
}
func NewUserHandler() *UserHandler {
return &UserHandler{
users: map[int64]*user.User{
1: {Id: 1, Name: "Alice", Email: "[email protected]"},
2: {Id: 2, Name: "Bob", Email: "[email protected]"},
},
}
}
func (h *UserHandler) GetUser(ctx context.Context, id int64) (*user.User, error) {
u, ok := h.users[id]
if !ok {
return nil, &user.UserNotFound{
Id: id,
Message: fmt.Sprintf("用户 %d 不存在", id),
}
}
return u, nil
}
func (h *UserHandler) CreateUser(ctx context.Context, u *user.User) error {
if u.Name == "" {
return &user.InvalidRequest{
Field: "name",
Message: "用户名不能为空",
}
}
h.users[u.Id] = u
return nil
}
func (h *UserHandler) ListUsers(ctx context.Context, pageSize int32, cursor string) ([]*user.User, error) {
result := make([]*user.User, 0, len(h.users))
for _, u := range h.users {
result = append(result, u)
if int32(len(result)) >= pageSize {
break
}
}
return result, nil
}
func (h *UserHandler) NotifyUser(ctx context.Context, id int64, message string) error {
log.Printf("通知用户 %d: %s", id, message)
return nil
}
func main() {
handler := NewUserHandler()
processor := user.NewUserServiceProcessor(handler)
transport, err := thrift.NewTServerSocket(":9090")
if err != nil {
log.Fatalf("创建 socket 失败: %v", err)
}
// 使用 TFramedTransport + TBinaryProtocol
transportFactory := thrift.NewTFramedTransportFactoryConf(
thrift.NewTTransportFactory(),
&thrift.TConfiguration{},
)
protocolFactory := thrift.NewTBinaryProtocolFactoryConf(&thrift.TConfiguration{})
server := thrift.NewTSimpleServer4(
processor,
transport,
transportFactory,
protocolFactory,
)
log.Println("Thrift 服务器启动于 :9090")
if err := server.Serve(); err != nil {
log.Fatalf("服务失败: %v", err)
}
}
12.5.3 Go 客户端
package main
import (
"context"
"fmt"
"log"
"github.com/apache/thrift/lib/go/thrift"
"example/gen-go/user"
)
func main() {
socket := thrift.NewTSocketConf("localhost:9090", &thrift.TConfiguration{})
transport := thrift.NewTFramedTransportConf(socket, &thrift.TConfiguration{})
protocol := thrift.NewTBinaryProtocolFactoryConf(&thrift.TConfiguration{})
if err := transport.Open(); err != nil {
log.Fatalf("连接失败: %v", err)
}
defer transport.Close()
client := user.NewUserServiceClientFactory(transport, protocol)
// 获取用户
u, err := client.GetUser(context.Background(), 1)
if err != nil {
if notFound, ok := err.(*user.UserNotFound); ok {
fmt.Printf("用户不存在: %s\n", notFound.Message)
} else {
log.Fatalf("调用失败: %v", err)
}
} else {
fmt.Printf("用户: %s (%s)\n", u.Name, u.Email)
}
// 创建用户
newUser := &user.User{
Id: 3,
Name: "Charlie",
Email: "[email protected]",
}
if err := client.CreateUser(context.Background(), newUser); err != nil {
log.Fatalf("创建失败: %v", err)
}
fmt.Println("用户创建成功")
}
12.6 Thrift vs gRPC 深度对比
| 维度 | Thrift | gRPC | 胜出 |
|---|---|---|---|
| 传输层灵活性 | 多种 Transport | 仅 HTTP/2 | Thrift |
| 流式通信 | ❌ | ✅ 4种模式 | gRPC |
| 浏览器支持 | 有限 | gRPC-Web | gRPC |
| 负载均衡 | 需自建 | 内建支持 | gRPC |
| 生态活跃度 | 中等 | 非常活跃 | gRPC |
| 头部压缩 | ❌ | HPACK/QPACK | gRPC |
| 0-RTT | ❌ | QUIC支持 | gRPC |
| 学习曲线 | 低 | 中 | Thrift |
| 代码生成 | 简单 | 复杂 | 持平 |
| 错误处理 | 异常 | 状态码 | 持平 |
| 元数据 | Header | Metadata | gRPC |
12.7 业务场景
12.7.1 适合使用 Thrift 的场景
场景 1:遗留系统集成
- 已有大量 Thrift 接口定义
- 团队熟悉 Thrift 生态
- 无需流式通信
场景 2:多语言 RPC
- 需要支持 20+ 编程语言
- Thrift 的语言覆盖更广
- 如 Erlang, Haskell, OCaml 等冷门语言
场景 3:简单 TCP 通信
- 不需要 HTTP/2 的额外开销
- 内网环境,无需 TLS
- 追求极简架构
12.7.2 Thrift 在大规模系统的应用
Facebook 的 Thrift 使用经验:
- 每天处理数十亿次 RPC 调用
- 用于后端服务间的通信
- 自研了 TChannel 协议(基于 Thrift)
LinkedIn 的实践:
- 使用 Thrift 构建微服务架构
- 开源了 Norbert(Thrift 服务发现框架)
- 后续迁移到 gRPC
Uber 的实践:
- 早期使用 Thrift
- 逐步迁移到 gRPC
- 主要原因:gRPC 的流式支持和 HTTP/2
12.8 注意事项
⚠️ 版本兼容性:
- Thrift 编译器版本与运行时库版本需匹配
- 不同语言的运行时库版本可能不同步
- 升级前需充分测试
⚠️ 线程安全:
- 默认生成的客户端不保证线程安全
- 多线程环境需使用连接池
oneway方法不保证送达
⚠️ 错误传播:
- Thrift 使用异常传播错误(非 gRPC 状态码)
- 只有声明的异常会被传播,其他异常会被包装
- 注意异常的向前兼容性
💡 最佳实践:
- 使用 TFramedTransport 代替 TSocket(性能更好)
- 优先使用 TCompactProtocol(更小的编码体积)
- 为每个服务方法设置超时
- 使用连接池管理客户端连接