news 2026/9/24 23:38:01

Presto Native Execution Thrift 序列化层全解析:从 JSON 到 Thrift 的过渡架构与代码生成管线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Presto Native Execution Thrift 序列化层全解析:从 JSON 到 Thrift 的过渡架构与代码生成管线
  • 大数据
  • 数据库
  • 后端

【免费下载链接】presto

The official home of the Presto distributed SQL query engine for big data

项目地址:https://gitcode.com/gh_mirrors/pre/presto
点击查看免费下载

导读

本文围绕 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.thriftpresto_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.mustachechevron 模板,产出转换代码
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 表示。

脚本的核心逻辑值得注意:

  1. 预处理(preprocess):IDL 中由 drift 生成的注释drift.recursive_reference=true在 C++ 侧会报错,因此先用正则将其剔除,同时去掉行尾的(),生成临时文件temp_presto_thrift.thrift,解析完再删除;
  2. 类型映射(typeMap/str_type):把 Thrift 基础类型映射为 C++ 类型,例如i16 → int16_t、i32 → int32_t、i64 → int64_t,容器类型映射为std::map<...>、std::list<...>、std::set<...>;
  3. 结构提取(enum/struct/items):遍历 AST,把enum提取为{enum, class_name, elements},把struct/union提取为{class_name, fields[...], struct|union},每个字段记录field_name、field_type、optional/required与tag;
  4. 输出:以"// 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 中可以验证这条真实调用链:

  1. getTaskStatus()处理GET /v1/task/{taskId}/status,读取 HTTP 头中的X-Presto-Current-State(getCurrentState)与X-Presto-Max-Wait(getMaxWait);
  2. shouldUseThrift(message)检查请求的Accept头是否包含 Thrift MIME 类型(http::kMimeTypeApplicationThrift),从而决定响应走 Thrift 还是 JSON —— 这正是"过渡期双协议并存"在 HTTP 层的体现;
  3. 通过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,覆盖三条断言:

  1. thriftWrite→thriftRead往返后对象相等;
  2. thriftWriteIOBuf→thriftRead往返后对象相等;
  3. 两种序列化方式产出的字节串完全一致。

具体用例覆盖了各种边界条件:空字符串、大数据量(1000 字符字符串 +INT32_MAX)、负数、零值、嵌套结构(TaskId内含StageExecutionId→StageId两层嵌套)以及BroadcastFileFooter的空/单/多/大/含零负值等多种 pageSizes。此外 TaskStatusTest.cpp 等测试也直接调用thrift::toThrift验证转换函数行为,这些测试由 tests/CMakeLists.txt 接入构建。

如何扩展:新增一个 Thrift 结构的一般步骤

基于上述代码生成管线,向该模块新增结构的一般路径可以归纳为:

  1. 在presto_thrift.thrift中定义或扩展 struct/enum,遵循已有的字段命名与编号风格(如TaskStatus从 1 开始连续编号);
  2. 按需更新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;
  3. 重新运行 Makefile 目标(make presto_protocol-to-thrift-json.json与make ProtocolToThrift.h ProtocolToThrift.cpp),或用 CMake 触发 thrift 代码生成与presto_thrift_extra编译;
  4. 在 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

项目地址:https://gitcode.com/gh_mirrors/pre/presto
点击查看免费下载
上一篇:终极GitHub加速指南:如何让国内访问速度提升10倍的简单方案
下一篇:告别网盘下载困境:这款开源工具如何让九大网盘下载体验焕然一新?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 23:37:56

Claude Code团队级配置:从API密钥治理到AI工程流水线

1. 这不是“装个插件就完事”的配置——Claude Code 是 AI 工程团队的协作操作系统你搜“Claude Code 配置指南”&#xff0c;刷出来的大多是“三步安装 VS Code 插件”“复制粘贴 API Key 就能用”。但如果你真带过 3 人以上的开发团队&#xff0c;或者正在从零搭建一个能稳定…

作者头像 李华
网站建设 2026/9/24 23:37:40

Qt 5.14.2 aarch64静态交叉编译:从x86到ARM Linux部署全攻略

做嵌入式Qt开发&#xff0c;最怕听到的话是什么&#xff1f;"板子环境太简陋&#xff0c;装不了Qt。"早年我在x86上写得好好的程序&#xff0c;一放到ARM板子上就各种崩&#xff0c;缺库、版本不对、平台插件找不到&#xff0c;光排查环境问题就耗掉一半时间。后来我…

作者头像 李华
网站建设 2026/9/24 23:35:55

Excel VBA调用USB转I2C适配器实现100KHz总线扫描与读写测试

1. 项目缘起与整体设计思路1.1 为什么会有这个测试需求做嵌入式开发的朋友大概率都遇到过这样的场景&#xff1a;手头有一批传感器、EEPROM或者IO扩展芯片&#xff0c;都是I2C接口的&#xff0c;需要快速验证读写是否正常。传统做法是拿一块STM32或者树莓派&#xff0c;写一段初…

作者头像 李华
网站建设 2026/9/24 23:35:35

Typecho内网穿透实战:从本地博客到公网访问全链路解析

1. 这不是“搭个博客”那么简单&#xff1a;Typecho 内网穿透的真实价值与典型误区 你搜“Typecho 搭建博客”&#xff0c;十篇教程里八篇开头就是“下载安装包、解压、配置数据库、访问安装向导”——看起来三分钟搞定。但真正用过的人知道&#xff0c;这仅仅是万里长征第一…

作者头像 李华