- 大数据
- 数据库
- 后端
【免费下载链接】presto
The official home of the Presto distributed SQL query engine for big data
导读
本文围绕 Presto 原生执行引擎(presto-native-execution / presto_cpp)中与 Coordinator 通信所用的 Thrift 序列化层展开,完整讲解其设计动机、双层代码生成管线、toThrift/fromThrift转换机制以及构建与测试方式。读完本文,你将理解 presto_cpp 如何从 JSON 协议平滑过渡到 fbthrift 协议,掌握presto_thrift.thrift→ProtocolToThrift.[h|cpp]的完整生成链路,并能在此基础上自行扩展新的 Thrift 结构。
背景:presto_cpp 为什么需要一套 Thrift 序列化层
Presto 是一个分布式 SQL 查询引擎,Coordinator 负责查询规划与调度,Worker 负责执行。presto-native-execution(即 presto_cpp)是用 C++ 实现的原生执行引擎,它与 Presto Coordinator 之间需要频繁交换任务状态(TaskStatus)、任务信息(TaskInfo)等数据。
presto-native-execution/presto_cpp/main/thrift/README.md 开宗明义地指出:这个目录承载的正是与 Presto Coordinator 通信所使用的 Thrift 序列化(serde)。
历史上 presto_cpp 与 Coordinator 的通信采用 JSON 序列化(presto_protocol 中定义了与 Java 端一致的 JSON 协议结构)。随着演进,项目决定转向更紧凑、高性能的 Thrift 二进制协议,但迁移并非一蹴而就,而是处于"JSON 与 Thrift 并存"的过渡期。因此 README 明确描述了最终状态与过渡期状态:
- 最终状态:只保留
presto_thrift.thrift文件及其 fbthrift 生成的代码; - 过渡期状态:需要维护两套内部数据结构(JSON 派生结构与 Thrift 派生结构)之间的转换代码。
换句话说,这个目录的核心使命是:在过渡期内,把 JSON 派生的内部对象转换成对应的 Thrift struct,供 Thrift 协议端点使用。
从源码结构看,这个目标已经基本实现:当前 thrift 目录下同时存在 IDL 定义(presto_thrift.thrift、presto_native.thrift)、代码生成脚本(thrift2json.py、presto_protocol-to-thrift-json.py)、mustache 模板(ProtocolToThrift-cpp.mustache、ProtocolToThrift-hpp.mustache)、生成产物(ProtocolToThrift.h、ProtocolToThrift.cpp)以及通用读写封装(ThriftIO.h),并配有独立的单元测试(tests/ThriftIOTest.cpp)。
目录结构与核心文件一览
先列出该模块的关键文件及其职责,方便后续对照阅读:
| 文件 | 职责 |
|---|---|
| presto_thrift.thrift | 与 Presto Java 协议对应的 Thrift IDL 定义(任务、算子、会话等结构) |
| presto_native.thrift | presto_cpp 原生扩展的 Thrift IDL |
| thrift2json.py | 将 thrift IDL 解析为 JSON 描述(presto_thrift.json) |
| presto_protocol-to-thrift-json.py | 将 thrift JSON 描述与 presto_protocol JSON 合并,生成转换映射 |
| presto_protocol-to-thrift-json.yml | 上述脚本的配置:字段映射、跳过列表、wrapper/connector/special 分类 |
| ProtocolToThrift-cpp.mustache / ProtocolToThrift-hpp.mustache | chevron 模板,产出转换代码 |
| ProtocolToThrift.h / ProtocolToThrift.cpp | 生成的toThrift/fromThrift转换实现 |
| ThriftIO.h | 基于 BinaryProtocol 的通用读写封装(thriftRead/thriftWrite) |
| Makefile / CMakeLists.txt | 生成与构建入口 |
| special/ | 需要手写特殊转换逻辑的结构(如TaskId、ConnectorSplit、OperatorInfoUnion等) |
| tests/ThriftIOTest.cpp | 序列化往返一致性测试 |
双层代码生成管线:从 .thrift 到 ProtocolToThrift
README 给出了完整的代码生成流程图,这是理解整个模块的钥匙。将其整理为更清晰的文字版:
presto_thrift.thrift ──→ fbthrift ──→ $BUILDDIR/presto_cpp/main/thrift/ProtocolToThrift.[h|cpp] │ ▼ thrift2json.py │ ▼ presto_thrift.json presto_protocol/presto_protocol.json presto_protocol-to-thrift-json.yml │ │ │ │ ▼ │ └──────────────→ presto_protocol-to-thrift-json.py ←─────────────┘ │ ▼ presto_protocol-to-thrift-json.json │ │ ProtocolToThrift-cpp.mustache │ │ ProtocolToThrift-hpp.mustache │ │ │ │ ▼ │ ▼ ▼ chevron ←──────────────┘ chevron ←─────────────┘ │ │ ▼ ▼ ProtocolToThrift.cpp ProtocolToThrift.h整条链路可以拆成两大部分,下面分别展开。
第一步:thrift2json.py —— 把 Thrift IDL 转成 JSON 描述
thrift2json.py 的作用是:使用ptsd_jbroll(ptsd Thrift 解析器的一个 fork)把presto_thrift.thrift解析成 JSON 表示。
脚本的核心逻辑值得注意:
- 预处理(
preprocess):IDL 中由 drift 生成的注释drift.recursive_reference=true在 C++ 侧会报错,因此先用正则将其剔除,同时去掉行尾的(),生成临时文件temp_presto_thrift.thrift,解析完再删除; - 类型映射(
typeMap/str_type):把 Thrift 基础类型映射为 C++ 类型,例如i16 → int16_t、i32 → int32_t、i64 → int64_t,容器类型映射为std::map<...>、std::list<...>、std::set<...>; - 结构提取(
enum/struct/items):遍历 AST,把enum提取为{enum, class_name, elements},把struct/union提取为{class_name, fields[...], struct|union},每个字段记录field_name、field_type、optional/required与tag; - 输出:以
"// This file is generated DO NOT EDIT @generated"注释开头,打印完整的 JSON。
命令级用法(对应 Makefile 中的规则):
./thrift2json.py presto_thrift.thrift | jq . > presto_thrift.json第二步:presto_protocol-to-thrift-json.py —— 合并两份协议描述
presto_protocol-to-thrift-json.py 的输入有两个:
presto_thrift.json:上一步从 Thrift IDL 得到的 JSON;presto_protocol.json(实际构建中使用../../presto_protocol/core/presto_protocol_core.json):presto_protocol 代码生成器产出的、与 Java 端一致的 JSON 协议描述。
脚本将它们合并为presto_protocol-to-thrift-json.json,这个合并文件描述了"从 JSON 内部结构到 Thrift 结构的转换"。其关键处理逻辑包括:
- 字段级转换标记:
verify()计算 Thrift 字段集合与 protocol 字段集合的交集,落在交集内的字段会被标记convert = true,其余字段会向 stderr 打印Missing protocol fields/Missing thrift fields告警; - 字段名映射:
process_fields()依据 YAML 配置中的fields映射,为字段挂上proto_name(即 protocol 侧的字段名); - 分类处理:
SkipStruct:跳过 presto_cpp 中不使用的结构(如ExchangeClientStatus、StageId等,共 14 个);StructInProtocolCore:标记定义在presto_protocol_core.h中的结构;WrapperStruct:单字段包装结构(如ConnectorId、PlanNodeId、TransactionId、HostAddress等),直接展开其唯一字段;ConnectorStruct:连接器相关结构(如ConnectorSplit、ConnectorTableHandle等);Special:需要手写特殊实现的 8 个结构(ConnectorTransactionHandle、ConnectorSplit、QualifiedObjectName、OperatorInfoUnion、OutputBufferId、TaskId、TypeSignature),其实现位于special/*.inc;union结构:如OperatorInfoUnion、ExecutionWriterTargetUnion,通过removesuffix("Union")推断 protocol 侧名称,并按StructMap配置覆盖字段类型。
第三步:chevron 模板渲染生成 C++ 代码
presto_protocol-to-thrift-json.json最终交给 chevron(Mustache 模板引擎)渲染两个模板,产出实际的 C++ 代码:
# Makefile 中的生成规则 ./presto_protocol-to-thrift-json.py presto_thrift.json ../../presto_protocol/core/presto_protocol_core.json | jq . > presto_protocol-to-thrift-json.json echo "// DO NOT EDIT : This file is generated by presto_protocol-to-thrift-json.py" > ProtocolToThrift.h chevron -d presto_protocol-to-thrift-json.json ProtocolToThrift-hpp.mustache >> ProtocolToThrift.h clang-format -style=file -i ProtocolToThrift.h # ProtocolToThrift.cpp 同理使用 ProtocolToThrift-cpp.mustache模板会为每种结构生成对应的toThrift(JSON 结构 → Thrift 结构)与fromThrift(Thrift 结构 → JSON 结构)函数声明/定义。从生成的 ProtocolToThrift.cpp 首行// DO NOT EDIT : This file is generated by presto_protocol-to-thrift-json.py可以看出它是构建期产物,开发者不应手工修改。
转换语义:toThrift 与 fromThrift
README 指出:"代码生成会为每一个同时存在于 JSON 协议中的 Thrift 结构生成toThrift函数"。当前唯一被 Thrift 协议端点真正使用的根类是TaskStatus,用于返回.getTaskStatus端点的结果,为此需要把 JSON 派生的TaskStatus转成对应的 Thrift struct。
基础类型的转换
ProtocolToThrift-cpp.mustache 开头定义了基础类型转换,注释明确说明:"这些本可以用更通用的模板覆盖,但这样做可以保证只生成到受支持的 Thrift 数据类型的转换":
void toThrift(const std::string& proto, std::string& thrift) { thrift = proto; } void toThrift(const bool& proto, bool& thrift) { thrift = proto; } void toThrift(const int16_t& proto, int16_t& thrift) { thrift = proto; } void toThrift(const int32_t& proto, int32_t& thrift) { thrift = proto; } void toThrift(const int64_t& proto, int64_t& thrift) { thrift = proto; } void toThrift(const double& proto, double& thrift) { thrift = proto; }两个特殊映射值得一提:Duration与DataSize这类"带单位的数值"在 Thrift 侧统一以纯数值表示:
void toThrift(const facebook::presto::protocol::Duration& duration, double& thrift) { thrift = duration.getValue(facebook::presto::protocol::TimeUnit::MILLISECONDS); } void toThrift(const facebook::presto::protocol::DataSize& dataSize, double& thrift) { thrift = dataSize.getValue(facebook::presto::protocol::DataUnit::BYTE); }即:时长统一换算为毫秒,数据大小统一换算为字节;fromThrift方向则反向包装回Duration/DataSize。这保证了跨协议传输时单位语义不失真。
容器与指针的泛型转换
模板还生成了一系列泛型转换,覆盖std::shared_ptr、std::vector、std::set、std::map以及 Thrift 的optional_field_ref,例如:
template <typename P, typename T> void toThrift(const std::shared_ptr<P>& proto, std::shared_ptr<T>& thrift) { if (proto) { thrift = std::make_shared<T>(); toThrift(*proto, *thrift); } } template <typename V, typename S> void toThrift(const std::vector<V>& v, std::set<S>& s) { S toItem; for (const auto& fromItem : v) { toThrift(fromItem, toItem); s.insert(std::move(toItem)); } }值得关注的是std::vector → std::set的转换(JSON 侧为 list、Thrift 侧为 set,例如TaskStatus.completedDriverGroups和blockedReasons),它逐元素转换并去重。toThrift/fromThrift是对称成对设计的,模板中两类函数均覆盖了相同的数据形状。
struct / enum / union / wrapper 的分支模板
对于每种 Thrift 结构,模板按类型展开不同的代码:
- struct:逐字段调用
toThrift(proto.<proto_name>, thrift.<field_name>_ref()),可选字段(optional)走optional_field_ref路径; - enum:直接按底层 int 值强转,保证两套枚举数值一致:
void toThrift(const facebook::presto::protocol::Type& proto, Type& thrift) { thrift = (Type)(static_cast<int>(proto)); } - union(如
OperatorInfoUnion):toThrift用std::dynamic_pointer_cast判断 protocol 侧具体子类型并调用set_<field_name>;fromThrift用thrift.getType() == Union::Type::<field_name>反向分派; - wrapper(如
ConnectorId、PlanNodeId):只展开唯一字段; - connector(如
ConnectorSplit):fromThrift优先用connectorId + customSerializedValue走连接器协议反序列化(getConnectorProtocol(...).deserialize(...)),否则退回jsonValue的 JSON 解析; - special:引入
special/*.inc的手写实现。例如 TaskId.cpp.inc 把 JSON 侧形如queryId.stageId.taskId.attempt的字符串 TaskId 按.拆分为 5 段,映射到嵌套的StageExecutionId → StageId结构;ConnectorSplit.cpp.inc 则额外处理了connectorId == "$remote"的远程 Split 场景,将自定义序列化值作为RemoteSplit的 Thrift 负载读取后再转回 protocol 结构。
ThriftIO.h:二进制协议读写封装
转换出的 Thrift 结构最终要落到线缆上。ThriftIO.h 基于 fbthrift 的 BinaryProtocol 提供两个通用函数:
template <typename T> void thriftRead(const std::string& data, std::shared_ptr<T>& buffer) { auto inBuf = folly::IOBuf::wrapBuffer(data.data(), data.size()); apache::thrift::BinaryProtocolReader reader; reader.setInput(inBuf.get()); buffer->read(&reader); } template <typename T> std::unique_ptr<folly::IOBuf> thriftWriteIOBuf(T& data) { folly::IOBufQueue outQueue; apache::thrift::BinaryProtocolWriter writer; writer.setOutput(&outQueue); data.write(&writer); return outQueue.move(); } template <typename T> std::string thriftWrite(T& data) { return thriftWriteIOBuf(data)->moveToFbString().toStdString(); }即:thriftWrite用 BinaryProtocolWriter 序列化为std::string,thriftRead用 BinaryProtocolReader 反序列化。这套封装让上层只需关心类型,无需接触 IOBuf/队列细节。
构建集成:CMake 与 fbthrift 代码生成
CMakeLists.txt 展示了该模块如何接入 presto_cpp 的构建:
- 通过
find_program(THRIFT1 thrift1)与find_path(THRIFT_INCLUDES ...)定位 fbthrift 工具链; include(ThriftLibrary.cmake)后,调用两次thrift_library(...):presto_thrift:由presto_thrift.thrift生成PrestoThrift服务与类型(cpp2);presto_native:由presto_native.thrift生成原生扩展类型;- 两者都依赖
FBThrift::thriftcpp2与 Folly;
- 手工转换代码编译为静态库
presto_thrift_extra(包含ProtocolToThrift.cpp),并声明对presto_thrift-cpp2的构建依赖; - 仅当
PRESTO_ENABLE_TESTING开启时,才add_subdirectory(tests)编译测试。
README 中流程图顶部的$BUILDDIR/presto_cpp/main/thrift/ProtocolToThrift.[h|cpp]正对应 CMake 里${CMAKE_CURRENT_BINARY_DIR}/presto_cpp/main/thrift的输出目录。
端到端调用链:/v1/task/{taskId}/status 如何返回 Thrift
README 提到"当前只有一个 Thrift 根类TaskStatus被用于返回.getTaskStatus端点结果"。在 TaskResource.cpp 中可以验证这条真实调用链:
getTaskStatus()处理GET /v1/task/{taskId}/status,读取 HTTP 头中的X-Presto-Current-State(getCurrentState)与X-Presto-Max-Wait(getMaxWait);shouldUseThrift(message)检查请求的Accept头是否包含 Thrift MIME 类型(http::kMimeTypeApplicationThrift),从而决定响应走 Thrift 还是 JSON —— 这正是"过渡期双协议并存"在 HTTP 层的体现;- 通过
sendPrestoResponse<protocol::TaskStatus, thrift::TaskStatus>(...)发送响应,其实现为:
template <typename T, typename ThriftT> void sendPrestoResponse( proxygen::ResponseHandler* downstream, const T& data, bool sendThrift) { if (sendThrift) { ThriftT thriftData; toThrift(data, thriftData); http::sendOkThriftResponse(downstream, thriftWrite(thriftData)); } else { http::sendOkResponse(downstream, json(data)); } }即:先调用生成的toThrift把 JSON 派生的protocol::TaskStatus转为thrift::TaskStatus,再用thriftWrite序列化为二进制并作为 Thrift 响应返回。getTaskInfo端点同样以sendPrestoResponse<protocol::TaskInfo, thrift::TaskInfo>的方式支持双协议。也就是说,虽然 README 写于迁移早期(只有TaskStatus一个根类),当前仓库中TaskInfo等结构也已接入同一套转换机制。
测试验证:ThriftIO 往返一致性
tests/ThriftIOTest.cpp 使用 GoogleTest 对序列化层做验证。测试基类ThriftIOTest提供testThriftRoundTrips,覆盖三条断言:
thriftWrite→thriftRead往返后对象相等;thriftWriteIOBuf→thriftRead往返后对象相等;- 两种序列化方式产出的字节串完全一致。
具体用例覆盖了各种边界条件:空字符串、大数据量(1000 字符字符串 +INT32_MAX)、负数、零值、嵌套结构(TaskId内含StageExecutionId→StageId两层嵌套)以及BroadcastFileFooter的空/单/多/大/含零负值等多种 pageSizes。此外 TaskStatusTest.cpp 等测试也直接调用thrift::toThrift验证转换函数行为,这些测试由 tests/CMakeLists.txt 接入构建。
如何扩展:新增一个 Thrift 结构的一般步骤
基于上述代码生成管线,向该模块新增结构的一般路径可以归纳为:
- 在
presto_thrift.thrift中定义或扩展 struct/enum,遵循已有的字段命名与编号风格(如TaskStatus从 1 开始连续编号); - 按需更新
presto_protocol-to-thrift-json.yml配置:- 字段名不一致时加入
StructMap.fields(例如TaskStatus.selfUri ↔ self、Lifespan.grouped ↔ isgroup); - 若结构在 presto_cpp 中不使用,加入
SkipStruct; - 若是单字段包装结构,加入
WrapperStruct; - 若是连接器结构,加入
ConnectorStruct; - 若需要手写转换,加入
Special并在 special/ 下提供xxx.cpp.inc/xxx.hpp.inc;
- 字段名不一致时加入
- 重新运行 Makefile 目标(
make presto_protocol-to-thrift-json.json与make ProtocolToThrift.h ProtocolToThrift.cpp),或用 CMake 触发 thrift 代码生成与presto_thrift_extra编译; - 在 tests/ThriftIOTest.cpp 中补充往返测试用例,遵循
testThriftRoundTrips模式。
小结
presto-native-execution 的 thrift 目录是"JSON 协议向 Thrift 协议迁移"的过渡性基础设施:它以presto_thrift.thrift为单一事实来源,通过thrift2json.py与presto_protocol-to-thrift-json.py两段脚本将 Thrift IDL 与 presto_protocol 的 JSON 协议合并为转换映射,再经 chevron + mustache 模板批量产出toThrift/fromThrift转换代码,配合ThriftIO.h的 BinaryProtocol 读写封装,最终在TaskResource的 HTTP 端点中按Accept头动态选择 Thrift 或 JSON 响应。理解这条管线,不仅能看到 presto_cpp 与 Coordinator 通信协议的演进路径,也为后续扩展新的 Thrift 结构提供了清晰的落点。
- 大数据
- 数据库
- 后端
【免费下载链接】presto
The official home of the Presto distributed SQL query engine for big data
相关推荐
dromara/disjob的序列化对比:JSON/Protobuf/Thrift
dromara/disjob的序列化对比:JSON/Protobuf/Thrift 引言 在分布式任务调度框架dromara/disjob中,序列化(Seria
Presto Thrift Connector 完全指南:通过 Thrift 协议集成任意外部存储系统
Presto Thrift Connector 完全指南:通过 Thrift 协议集成任意外部存储系统 导读 本文全面讲解 Presto 中 Thrift Co
大数据数据库后端Apache Thrift Delphi 库使用指南:版本要求、分层架构与序列化实战
Apache Thrift Delphi 库使用指南:版本要求、分层架构与序列化实战 Apache Thrift 的 Delphi 软件库( lib/delph
后端RPC框架序列化代码生成
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考