十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Apache Thrift 部分反序列化(Partial Deserialization)完整指南:按需字段读取、高效跳过与 Java 实现原理

Apache Thrift 部分反序列化(Partial Deserialization)完整指南:按需字段读取、高效跳过与 Java 实现原理 Apache Thrift 部分反序列化Partial Deserialization完整指南按需字段读取、高效跳过与 Java 实现原理【免费下载链接】thriftApache Thrift项目地址: https://gitcode.com/GitHub_Trending/thr/thriftApache Thrift 的部分反序列化Partial Deserialization是一种面向大数据处理场景的优化技术当业务只需要某个 Thrift 对象的少数几个字段时不再把整个序列化对象完整反序列化而是只解码目标字段、对其余字段做高效跳过。本文以 partial/README.md 为主线结合 lib/java 中org.apache.thrift.partial包的源码实现系统讲解部分反序列化的动机、字段子集定义语法、五大核心组件Thrift Metadata、Partial Thrift Protocol、Partial Thrift Deserializer、Field Value Processor、辅助工具类的职责与底层原理。读完本文你将理解如何在 Java 中使用TDeserializer只反序列化指定字段掌握skip*()协议层的跳过机制并能在此基础上为其他语言实现同样的能力。背景与动机为什么需要部分反序列化在大数据生态中Thrift 序列化格式常被用于存储海量数据例如以SequenceFile形式保存序列化后的 Thrift 值。下游的数据处理任务如 Spark、MapReduce 作业会反复读取这些数据但并不是每个任务都需要访问对象中的每一个字段。如果任务在运行前就知道自己只关心哪些字段那么就可以只反序列化这一小部分字段把其余字段跳过从而获得两方面的收益见 README 的 Motivation 小节节省 CPU 周期无需为用不到的字段执行类型读取、对象分配和值写入等反序列化逻辑降低 GC 压力跳过的字段不会产生中间对象减少了垃圾回收器的负担。当数据处理作业需要处理数十亿条记录时这两项节省会快速累积成非常可观的性能提升。这也是 Pinterest 等大流量公司在内部把该特性引入数据处理链路的原因——README 中提到的 SparkInternalRow直接反序列化实现就是该思路的一个典型落地案例。核心概念什么是部分反序列化部分反序列化是指只反序列化一个已序列化 Thrift 对象中字段子集所对应的那部分数据同时对其余数据做高效跳过。这里有一个非常关键的收益部分反序列化的输出并不局限于TBase派生对象——通过选择合适的ThriftFieldValueProcessor你可以把序列化的二进制数据直接反序列化成任意目标类型例如 Spark 的InternalRow。如何定义要反序列化的字段子集字段子集通过完全限定字段名fully qualified field name列表来定义。考虑 README 中给出的 Thriftstruct定义struct SmallStruct { 1: optional string stringValue; 2: optional i16 i16Value; } struct TestStruct { 1: optional i16 i16Field; 2: optional listSmallStruct structList; 3: optional setSmallStruct structSet; 4: optional mapstring, SmallStruct structMap; 5: optional SmallStruct structField; }对于上面的TestStruct下面每一行都是一个合法的完全限定字段定义部分反序列化使用这些定义的非空集合来确定要反序列化的字段子集- i16Field - structList.stringValue - structSet.i16Value - structMap.stringValue - structField.i16Value语法要点嵌套字段使用点号.连接例如structList.stringValue表示structList列表中每个SmallStruct元素的stringValue字段structMap.stringValue这类路径中叶子段stringValue指代的是map 的 value 类型中的字段限制当前语法不支持定义 map 的 key 类型内部子字段例如mapstring, SmallStruct中的 key 是string本身没有可下钻的子字段但对于 key 是 struct 的 map目前也没有对应的语法。README 指出该限制可以通过向后兼容的方式修订语法来解决属于未来可扩展点。从 ThriftField.java 的实现 可以看到fromNames()会按大小写不敏感的顺序对字段名排序然后用.切分并逐步构建一棵n-ary 字段树每个节点是一个ThriftField持有name和子字段列表fields树的比较equals/hashCode同样采用大小写不敏感的方式见 ThriftField.java#L78-L119。组件一Thrift Metadata字段子集的内部表示对应源码文件lib/java/src/main/java/org/apache/thrift/partial/ThriftField.javalib/java/src/main/java/org/apache/thrift/partial/ThriftMetadata.javalib/java/src/main/java/org/apache/thrift/TDeserializer.java字段名列表只是用户侧的输入真正干活前第一步是把字段名集合编译成运行时可以高效遍历的内部数据结构。这个编译动作发生在用接受字段名列表的构造函数创建TDeserializer实例时// 第一步创建完全限定字段名的集合 ListString fieldNames Arrays.asList(i16Field, structField.i16Value); // 第二步创建支持部分反序列化的 TDeserializer 实例 TDeserializer deserializer new TDeserializer(TestStruct.class, fieldNames, new TBinaryProtocol.Factory());此刻TDeserializer内部就持有了一份针对TestStruct、只关心i16Field与structField.i16Value的高效元数据。在 ThriftMetadata.java 内部元数据被组织为一棵ThriftObject树按字段类型分为以下几类元数据类对应 Thrift 类型关键成员说明ThriftPrimitivebool / byte / i16 / i32 / i64 / double / string / binaryisBinary()叶子节点ThriftEnumenum—结合EnumCache做枚举值查找ThriftListlistelementData元素类型元数据字段 id 用FieldTypeEnum.LIST_ELEMENT占位ThriftSetsetelementData元素类型元数据字段 id 用FieldTypeEnum.SET_ELEMENT占位ThriftMapmapkeyData、valueDatakey/value 各自独立描述key 始终以空子字段集合编译对应上文语法限制ThriftStructstructMapInteger, ThriftObject fields按字段 id 索引的子字段映射ThriftUnionunion—源码注释明确currently not adequately supported当前支持不充分编译时见 ThriftMetadata.java#L136-L171 的 FactoryFieldMetaData.getStructMetaDataMap(clasz)负责把编译期生成的 struct 字段元数据取出来Factory.createNew根据字段类型TType.STRUCT/LIST/MAP/SET/ENUM/BOOL/BYTE/I16/I32/I64/DOUBLE/STRING创建对应的ThriftObject节点传入的字段集合为空时getFields()会退化为全量反序列化模式见 ThriftMetadata.java#L495-L505即把所有字段都加入元数据这保证了 API 的向后兼容。ThriftStruct还提供了fromFieldNames(clasz, fieldNames)/fromFields(clasz, fields)等静态工厂方法ThriftMetadata.java#L440-L463以及createNewStruct()通过反射调用无参构造器生成空实例的能力。另外元数据节点支持toPrettyString()ThriftStruct.toString()可以把当前字段子集以缩进的伪代码形式打印出来例如struct TestStruct { ... }方便调试确认子集定义是否正确。组件二Partial Thrift Protocol协议层的字段跳过对应源码文件lib/java/src/main/java/org/apache/thrift/protocol/TProtocol.javalib/java/src/main/java/org/apache/thrift/protocol/TBinaryProtocol.javalib/java/src/main/java/org/apache/thrift/protocol/TCompactProtocol.java这是高效跳过的落地点。实现方式是在上述协议类中新增一组skip*()方法。基类TProtocol中每个skip*()的默认实现就是简单调用对应的read*()方法跳过 读掉但不使用而派生协议如TBinaryProtocol、TCompactProtocol则覆盖这些方法提供更高效的实现。以TBinaryProtocol为例它的跳过实现是直接移动传输缓冲区的内部偏移完全不产生对象分配。见 TBinaryProtocol.java#L542-L575Override protected void skipBool() throws TException { this.skipBytes(1); } Override protected void skipI16() throws TException { this.skipBytes(2); } Override protected void skipI32() throws TException { this.skipBytes(4); } Override protected void skipI64() throws TException { this.skipBytes(8); } Override protected void skipBinary() throws TException { int size readI32(); this.skipBytes(size); }可以看到定长类型bool/byte 各 1 字节、i16 2 字节、i32 4 字节、i64/double 各 8 字节直接跳过固定字节数binary/string先读 4 字节长度再跳过长度的字节。这种按字节偏移跳的方式相比逐字段read*()再丢弃省去了几乎所有解码开销。此外协议层还引入了一个配套优化TBinaryProtocol.readFieldBeginData()TBinaryProtocol.java#L530-L539把字段的type和id合并编码进一个 int返回配合TFieldData见下文辅助类避免在反序列化热路径上实例化TField对象。组件三Partial Thrift Deserializer反序列化主控对应源码文件lib/java/src/main/java/org/apache/thrift/TDeserializer.java该组件负责按顺序逐个字段地遍历序列化 blob。其工作逻辑是在每一个字段开始时查询编译好的ThriftMetadata判断当前字段是否在目标子集中如果在就按常规反序列化流程把该字段解码成值交给ThriftFieldValueProcessor处理如果不在就调用协议层的skip*()方法高效跳过该字段。除了构造器注入字段名列表之外TDeserializer还暴露了一批直接按字段路径反序列化单个字段的便捷方法见 TDeserializer.java 中partialDeserialize*系列方法返回类型说明partialDeserialize(byte[] bytes, int offset, int length)Object入口方法partialDeserializeBool / Byte / I16 / I32 / I64 / Double对应基本类型包装类直接取出路径指向的单个基本类型字段partialDeserializeStringString取出字符串字段partialDeserializeByteArrayByteBuffer取出 binary 字段partialDeserializeSetFieldIdInUnionShortunion 场景辅助方法partialDeserializeThriftObject(TBase base, byte[] bytes, int offset, int length)Object输出到指定的TBase实例partialDeserializeObject(byte[] bytes, int offset, int length)Object通过ThriftFieldValueProcessor输出任意目标类型这些方法的fieldIdPathFirst / fieldIdPathRest参数允许运行时动态指定字段路径与构造器里预编译子集的方式互为补充。组件四Field Value Processor输出目标抽象对应源码文件lib/java/src/main/java/org/apache/thrift/partial/ThriftFieldValueProcessor.javalib/java/src/main/java/org/apache/thrift/partial/ThriftStructProcessor.java这是输出不限于TBase这一核心能力的关键抽象。当部分反序列化器解出某个字段的值后它不直接决定值放在哪而是把值交给ThriftFieldValueProcessor由处理器决定值是原样保存、还是以某种中间形态保存。接口ThriftFieldValueProcessorV的方法按职责分组接口源码struct 相关createNewStruct(metadata)、prepareStruct(instance)、以及一系列setBool/setByte/setInt16/setInt32/setInt64/setDouble/setBinary/setString/setEnumField/setListField/setMapField/setSetField/setStructField写入方法值准备prepareEnum(enumClass, ordinal)、prepareString(ByteBuffer)、prepareBinary(ByteBuffer)list 相关createNewList(expectedSize)、setListElement(instance, index, value)、prepareList(instance)map 相关createNewMap(expectedSize)、setMapElement(instance, index, key, value)、prepareMap(instance)set 相关createNewSet(expectedSize)、setSetElement(instance, index, value)、prepareSet(instance)。接口的默认实现是ThriftStructProcessorTBase源码它把所有值塞进TBase派生对象行为与常规反序列化一致。其内部实现细节也体现了性能取向list 先用Object[]数组按索引填充prepareList时再转成List避免反复扩容map 用HashMap、set 用HashSet字符串统一按 UTF-8 从ByteBuffer解码enum 通过共享的EnumCache按序数查找实例。README 还明确提到还存在未随本次发布包含的其他处理器实现例如把 Thrift blob 直接反序列化成 Spark 使用的InternalRow的实现——相对于用默认反序列化器消费 Thrift 数据的 Spark 引擎该方案获得了数量级orders of magnitude的性能提升。这正好印证了选择合适的ThriftFieldValueProcessor即可把反序列化输出定向到任意目标的设计价值。组件五辅助工具类对应源码文件均在 lib/java/src/main/java/org/apache/thrift/partial/ 目录下TFieldDataTField 的 int 编码TFieldData.java 把TField的 type 与 id 两个成员压缩进一个 int低 8 位存 typetype 0xffid 左移 8 位放在高 16 位(type 0xff) | (((int) id) 8)。配套提供getType(int)/getId(int)解码。这种编码方式让部分反序列化流程无需实例化TField进一步削减热路径上的对象分配。EnumCache枚举记忆化查找EnumCache.java 提供按值getValue()返回值查找枚举实例的记忆化memoized能力第一次遇到某个枚举类时通过反射调用其values()方法建立MapInteger, TEnum缓存之后所有同类型枚举字段的查找都直接命中缓存避免重复反射。该类仅供TDeserializer内部使用。PartialThriftComparer子集范围内的对象比较PartialThriftComparer.java 用于比较两个TBase实例但比较范围被限定在给定元数据定义的字段子集内。典型用途是验证部分反序列化的正确性把一个 blob 分别用全量反序列化和部分反序列化产出两个实例再用PartialThriftComparer.areEqual(t1, t2, sb)断言两者在子集范围内等价。实现上它会递归比较 struct/list/set/map 各层binary字段支持byte[]Arrays.equals与ByteBuffercompareTo两种形态并可通过非空的StringBuilder收集差异明细例如fieldName : o1 (xx) ! o2 (yy)。整体工作流程从字段名到部分反序列化结果综合上述组件一次完整的部分反序列化流程如下定义子集用户提供完全限定字段名列表如[i16Field, structField.i16Value]编译元数据TDeserializer构造器内部通过ThriftField.fromNames()构建 n-ary 字段树再结合FieldMetaData.getStructMetaDataMap()编译成ThriftMetadata.ThriftStruct树遇到空集合则编译为全量元数据顺序遍历反序列化器逐字段读取序列化 blob借助readFieldBeginData()TFieldData的低成本方式拿到字段 type/id分支决策在ThriftMetadata中命中则正常反序列化并把值交给ThriftFieldValueProcessor未命中则调用协议层skip*()高效跳过输出处理器把值组装进目标容器默认是TBase也可以是 SparkInternalRow等自定义目标可选校验用PartialThriftComparer在子集范围内与全量反序列化结果比对验证正确性。注意事项与已知限制以下限制均有明确的源码或文档依据使用前需要留意map key 子字段语法缺失字段路径语法不支持定义 map key 类型内部的子字段只能下钻 map 的 value 类型README 明确说明union 支持不充分ThriftMetadata.ThriftUnion的源码注释写明 Currently not adequately supportedtoPrettyString()输出中也会标注// unions not adequately supported at present.ThriftMetadata.java#L395-L413类型覆盖范围Factory.createNew只覆盖 STRUCT/LIST/MAP/SET/ENUM 及 BOOL/BYTE/I16/I32/I64/DOUBLE/STRING 等类型其他类型会抛出UnsupportedOperationExceptionThriftMetadata.java#L549-L551PartialThriftComparer同样有该限制字段名匹配字段树构建与比较采用大小写不敏感方式ThriftField.java但字段名本身以 thrift 文件中的写法为准字段不存在编译元数据时若字段名在 struct 中找不到会抛出IllegalArgumentException(field not found: ...)ThriftMetadata.java#L545-L547。为其他语言实现部分反序列化的路线图README 开篇就点明了这份文档的双重目的帮助理解当前 Java 实现以及为在其他语言中实现部分反序列化提供参考。从本文的分析可以提炼出实现该特性的四个必备构件任何语言都可以按此蓝图落地字段路径语法与编译实现完全限定字段名的解析参考ThriftField.fromNames编译成可遍历的字段树/元数据协议层跳过原语为各协议实现高效的skip*()方法定长类型按字节数跳过变长类型先读长度再跳参考TBinaryProtocol遍历与决策主控反序列化器逐字段读取、按元数据决定反序列化 or 跳过参考TDeserializer可插拔的值处理器抽象出接收字段值并决定输出形态的处理器接口让输出摆脱具体对象类型的束缚参考ThriftFieldValueProcessor。总结Apache Thrift 的部分反序列化在全量反序列化与完全不反序列化之间提供了精准的中间态通过一行字段路径即可声明任务关心的字段子集由TDeserializerThriftMetadata决定读什么、由协议层skip*()决定怎么省由ThriftFieldValueProcessor决定输出成什么。这套设计在 CPU 与 GC 两个维度上同时获益特别适合以 Thrift 为存储格式、海量读取但按需取列的大数据处理链路也为其他语言实现同类能力给出了清晰的组件化参考。相关源码均位于 lib/java/src/main/java/org/apache/thrift/partial/ 与 lib/java/src/main/java/org/apache/thrift/TDeserializer.java读者可结合本文逐文件对照研读。【免费下载链接】thriftApache Thrift项目地址: https://gitcode.com/GitHub_Trending/thr/thrift创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表