强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 概览

维度ThriftgRPC
开发者Facebook (Apache)Google
IDLThrift IDLProtocol 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分帧传输非阻塞/异步服务器
THttpTransportHTTP 传输穿越防火墙/代理
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压缩二进制变长整数编码,空间最优带宽受限
TJSONProtocolJSON人类可读调试/浏览器
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 深度对比

维度ThriftgRPC胜出
传输层灵活性多种 Transport仅 HTTP/2Thrift
流式通信✅ 4种模式gRPC
浏览器支持有限gRPC-WebgRPC
负载均衡需自建内建支持gRPC
生态活跃度中等非常活跃gRPC
头部压缩HPACK/QPACKgRPC
0-RTTQUIC支持gRPC
学习曲线Thrift
代码生成简单复杂持平
错误处理异常状态码持平
元数据HeaderMetadatagRPC

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(更小的编码体积)
  • 为每个服务方法设置超时
  • 使用连接池管理客户端连接

12.9 扩展阅读


第 11 章 - REST vs gRPC 选型 | 第 13 章 - Connect 协议