0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

抖音内部使用的Go基础库开源,高性能动态处理RPC数据

OSC开源社区 来源:OSC开源社区 2023-03-23 10:12 次阅读

01

背景

当前,Thrift 是字节内部主要使用的 RPC 序列化协议,在 CloudWeGo/Kitex 项目中优化和使用后,性能相比使用支持泛型编解码的协议如 JSON 有较大优势。但是在和业务团队进行深入合作优化的过程中,我们发现一些特殊业务场景并不能享受静态化代码生成所带来的高性能:
  1. 动态反射:动态地 读取、修改、裁剪 数据包中某些字段,如隐私合规场景中字段屏蔽;
  2. 数据编排:组合多个子数据包进行 排序、过滤、位移、归并 等操作,如某些 BFF (Backend For Frontent) 服务;
  3. 协议转换:作为代理将某种协议的数据转换另一种协议,如 http-rpc 协议转换网关。
  4. 泛化调用:需要秒级热更新或迭代非常频繁的 RPC 服务,如大量 Kitex 泛化调用(generic-call)用户

不难发现,这些业务场景都具有难以统一定义静态IDL的特点。即使可以通过分布式 sidecar 技术规避这个问题,也往往因为业务需要动态更新而放弃传统代码生成方式,诉诸某些自研或开源的 Thrift 泛型编解码库进行泛化 RPC 调用。我们经过性能分析发现,目前这些库相比代码生成方式有巨大的性能下降。以字节某 BFF 服务为例,仅仅 Thrift 泛化调用产生的 CPU 开销占比就将近 40%,这几乎是正常 Thrift RPC 服务的4到8倍。因此,我们自研了一套能动态处理 RPC 数据(不需要代码生成)同时保证高性能的 Go 基础库 —— dynamicgo。

02

设计与实现

首先要搞清楚当前这些泛化调用库性能为什么差呢?其核心原因是:采用了某种低效泛型容器来承载中间处理过程中的数据(典型如 thrift-iterator 中的 map[string]interface{})。众所周知,Go 的堆内存管理代价是极高的 (GC +heap bitmap),而采用 interface 不可避免会带来大量的内存分配。但实际上相当多的业务场景并不真正需要这些中间表示。比如 http-thrift API 网关中的纯协议转换场景,其本质诉求只是将 JSON(或其它协议)数据依据用户 IDL 转换为 Thrift 编码(反之亦然),完全可以基于输入的数据流逐字进行翻译。同样,我们也统计了抖音某 BFF 服务中泛化调用的具体代码,发现真正需要进行读(Get)和写(Set)操作的字段占整个数据包字段不到5%,这种场景下完全可以对不需要的字段进行跳过(Skip)处理而不是反序列化。而 dynamicgo 的核心设计思想是:基于 原始字节流 和 动态类型描述 原地(in-place) 进行数据处理与转换。为此,我们针对不同的场景设计了不同的 API 去实现这个目标。动态反射

对于 thrift 反射代理的使用场景,归纳起来有如下使用需求:

  1. 有一套完整结构自描述能力,可表达 scalar 数据类型, 也可表达嵌套结构的映射、序列等关系;
  2. 支持增删查改(Get/Set/Index/Delete/Add)与遍历(ForEach);
  3. 保证数据可并发读,但是不需要支持并发写。等价于 map[string]interface{} 或 []interface{}

这里我们参考了 Go reflect 的设计思想,把通过IDL解析得到的准静态类型描述(只需跟随 IDL 更新一次)TypeDescriptor 和 原始数据单元 Node 打包成一个完全自描述的结构——Value,提供一套完整的反射 API。

//IDL类型描述
typeTypeDescriptorinterface{
Type()Type//数据类型
Name()string//类型名称
Key()*TypeDescriptor//formapkey
Elem()*TypeDescriptor//forsliceormapelement
Struct()*StructDescriptor//forstruct
}
//纯TLV数据单元
typeNodestruct{
tType//数据类型
vunsafe.Pointer//buffer起始位置
lint//数据单元长度
}
//Node+类型描述descriptor
typeValuestruct{
Node
Descthrift.TypeDescriptor
}

这样,只要保证 TypeDescriptor 包含的类型信息足够丰富,以及对应的 thrift 原始字节流处理逻辑足够健壮,甚至可以实现数据裁剪、聚合等各种复杂的业务场景。

协议转换

协议转换的过程可以通过有限状态机(FSM)来表达。以 JSON->Thrift 流程为例,其转换过程大致为:

  1. 预加载用户 IDL,转换为运行时的动态类型描述 TypeDescriptor;
  2. 从输入字节流中读取一个 json 值,并判断其具体类型(object/array/string/number/bool/null):
  3. 如果是 object 类型,继续读取一个 key,再通过对应的 STRUCT 类型描述找到匹配字段的子类型描述;
  4. 如果是 array 类型,递归查找类型描述的子元素类型描述;
  5. 其它类型,直接使用当前类型描述。
  6. 基于得到的动态类型描述信息,将该值转换为等价的 Thrift 字节,写入到输出字节流中 ;
  7. 更新输入和输出字节流位置,跳回2进行循环处理,直到输入终止(EOF)。

95a3aaf0-c8f0-11ed-bfe3-dac502259ad0.png

图1 JSON2Thrift 数据转换流程

整个过程可以完全做到 in-place 进行,仅需为输出字节流分配一次内存即可。

数据编排

与前面两个场景稍微有所不同,数据编排场景下可能涉及数据位置的改变(异构转换),并且往往会访问大量数据节点(最坏复杂度O(N) )。在与抖音隐私合规团队的合作研发中我们就发现了类似问题。它们的一个重要业务场景:要横向遍历某一个 array 的子节点,查找是否有违规数据并进行整行擦除。这种场景下,直接基于原始字节流进行查找和插入可能会带来大量重复的skip 定位、数据拷贝开销,最终导致性能劣化。因此我们需要一种高效的反序列化(带有指针)结构表示来处理数据。根据以往经验,我们想到了DOM(Document Object Model),这种结构被广泛运用在 JSON 的泛型解析场景中(如 rappidJSON、sonic/ast),并且性能相比 map+interface 泛型要好很多。

要用 DOM 来描述一个 Thrift 结构体,首先需要一个能准确描述数据节点之间的关系的定位方式—— Path。其类型应该包括 list index、map key 以及 struct field id等。

typePathTypeuint8

const(
PathFieldIdPathType=1+iota//STRUCT下字段ID
PathFieldName//STRUCT下字段名称
PathIndex//SET/LIST下的序列号
PathStrKey//MAP下的stringkey
PathIntkey//MAP下的integerkey
PathObjKey//MAP下的objectkey
)

typePathNodestruct{
Path//相对父节点路径
Node//原始数据单元
Next[]PathNode//存储子节点
}

在 Path 的基础上,我们组合对应的数据单元Node,然后再通过一个Next 数组动态存储子节点,便可以组装成一个类似于BTree的泛型结构。

9604321c-c8f0-11ed-bfe3-dac502259ad0.png

图2 thrift DOM 数据结构

这种泛型结构比 map+interface 要好在哪呢?首先,底层的数据单元 Node 都是对原始 thrift data 的引用,没有转换 interface 带来的二进制编解码开销;其次,我们的设计保证所有树节点 PathNode 的内存结构是完全一样,并且由于父子关系的底层核心容器是 slice, 我们又可以更进一步采用内存池技术,将整个 DOM 树的子节点内存分配与释放都进行池化从而避免调用 go 堆内存管理。测试结果表明,在理想场景下(后续反序列化的DOM树节点数量小于等于之前反序列化节点数量的最大值——这由于内存池本身的缓冲效应基本可以保证),内存分配次数可为0,性能提升200%!(见【性能测试-全量序列化/反序列化】部分)。

03

性能测试

这里我们分别定义简单(Small)、复杂(Medium) 两个基准结构体分别在比较 不同数据量级 下的性能,同时添加简单部分(SmallPartial)、复杂部分(MediumPartial) 两个对应子集,用于【反射-裁剪】场景的性能比较:

  • Small:114B,6个有效字段
  • SmallPartial:small 的子集,55B,3个有效字段
  • Medium: 6455B,284个有效字段
  • MediumPartial: medium 的子集,1922B,132个有效字段

Small:https://github.com/cloudwego/dynamicgo/blob/main/testdata/idl/baseline.thrift#L3

Medium:https://github.com/cloudwego/dynamicgo/blob/main/testdata/idl/baseline.thrift#L12

SmallPartial:https://github.com/cloudwego/dynamicgo/blob/main/testdata/idl/baseline.thrift#L12

MediumPartial:https://github.com/cloudwego/dynamicgo/blob/main/testdata/idl/baseline.thrift#L36

其次,我们依据上述业务场景划分为 反射、协议转换、全量序列化/反序列化 三套 API,并以代码生成库kitex/FastAPI、泛化调用库kitex/generic、JSON 库sonic为基准进行性能测试。其它测试环境均保持一致:

  • Go 1.18.1
  • CPU intel i9-9880H 2.3GHZ
  • OS macOS Monterey 12.6

kitex/FastAPI:https://github.com/cloudwego/kitex/blob/aed28371eb88b2668854759ce9f4666595ebc8de/pkg/remote/codec/thrift/thrift.go

kitex/generic:https://github.com/cloudwego/kitex/tree/develop/pkg/generic

sonic:https://github.com/bytedance/sonic

反射

1. 代码

dynamicgo/testdata/baseline_tg_test.go

2. 用例

  • GetOne:查找字节流中最后1个数据字段
  • GetMany:查找前中后5个数据字段
  • MarshalMany:将 GetMany 中的结果进行二次序列化
  • SetOne:设置最后一个数据字段
  • SetMany:设置前中后3个节点数据
  • MarshalTo:将大 Thrift 数据包裁剪为小 thrift 数据包 (Small -> SmallPartial 或 Medium -> MediumParital)
  • UnmarshalAll+MarshalPartial:代码生成/泛化调用方式裁剪——先反序列化全量数据再序列化部分数据。效果等同于 MarshalTo。

3. 结果

  • 简单(ns/OP)
962bbf62-c8f0-11ed-bfe3-dac502259ad0.png
  • 复杂(ns/OP)
965b93d6-c8f0-11ed-bfe3-dac502259ad0.png

4. 结论

  • dynamicgo 一次查找+写入 开销大约为代码生成方式的 2 ~ 1/3、为泛化调用方式的 1/12 ~ 1/15,并随着数据量级增大优势加大;
  • dynamicgo thrift 裁剪 开销接近于代码生成方式、约为泛化调用方式的 1/10~1/6,并且随着数据量级增大优势减弱。

协议转换

1. 代码

  • JSON2Thrift:dynamicgo/testdata/baseline_j2t_test.go
  • ThriftToJSON:dynamicgo/testdata/baseline_t2j_test.go

2. 用例

  • JSON2thrift:JSON 数据转换为等价结构的 thrift 数据
  • thrift2JSON:将 thrift 数据转换为等价结构的 JSON 数据
  • sonic + kitex-fast:表示通过 sonic 处理 json 数据(有结构体),通过kitex代码生成处理thrift数据

3. 结果

  • 简单(ns/OP)
96762c8c-c8f0-11ed-bfe3-dac502259ad0.png
  • 复杂(ns/OP)
969ea66c-c8f0-11ed-bfe3-dac502259ad0.png

4. 结论

  • dynamicgo 协议转换开销约为代码生成方式的 1~2/3、泛化调用方式的 1/4~1/9,并且随着数据量级增大优势加大;

全量序列化/反序列化

1. 代码

dynamicgo/testdata/baseline_tg_test.go#BenchmarkThriftGetAll

2. 用例

  • UnmarshalAll:反序列化所有字段。其中对于 dynamicgo 有两种模式:

    • new:每次重新分配 DOM 内存;
    • reuse:使用内存池复用 DOM 内存。
  • MarshalAll:序列化所有字段。

3. 结果

  • 简单(ns/OP)
96b50a74-c8f0-11ed-bfe3-dac502259ad0.png
  • 复杂(ns/OP)
96cd3108-c8f0-11ed-bfe3-dac502259ad0.png

4. 结论

  • dynamicgo 全量序列化 开销约为代码生成方式的 6~3倍、泛化调用方式的 1/4~1/2,并且随着数据量级增大优势减弱;
  • Dynamigo 全量反序列化+内存复用 场景下开销约为代码生成方式的 1.8~0.7、泛化调用方式的 1/13~1/8,并且随着数据量级增大优势加大。

04

应用与展望

当前,dynamicgo 已经应用到许多重要业务场景中,包括:

  1. 业务隐私合规 中间件(thrift 反射);
  2. 抖音某 BFF 服务下游数据按需下发(thrift 裁剪);
  3. 字节跳动某 API 网关协议转换(JSON<>thrift 协议转换)。

并且逐步上线并取得收益。目前 dynamic 还在迭代中,接下来的工作包括:

  1. 集成到 Kitex 泛化调用模块中,为更多用户提供高性能的 thrift 泛化调用模块;
  2. Thrift DOM 接入 DSL(GraphQL)组件,进一步提升 BFF 动态网关性能;
  3. 支持 Protobuf 协议。

也欢迎感兴趣的个人或团队参与进来,共同开发!

项目地址

GitHub:https://github.com/cloudwego

官网:www.cloudwego.io


审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 编解码
    +关注

    关注

    1

    文章

    138

    浏览量

    19340
  • 开源
    +关注

    关注

    3

    文章

    2991

    浏览量

    41723
  • 数据包
    +关注

    关注

    0

    文章

    231

    浏览量

    24095

原文标题:抖音内部使用的Go基础库开源,高性能动态处理RPC数据

文章出处:【微信号:OSC开源社区,微信公众号:OSC开源社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    【GoRK3288】1.Rockchip RK3288, GO!GO!!GO!!!

    语言是谷歌2009发布的第二款开源编程语言, 专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程,而且可以在不损失应用程序
    发表于 08-14 21:07

    【TL6748 DSP申请】基于DSP的、视频处理系统

    申请理由:本人是一名大四学生,想利用一款高性能的DSP做一些、视频处理方面的工作。项目描述:利用DSP对音频和视频信号进行处理,实现数据
    发表于 09-10 11:17

    go语言能做什么工作?

    让程序员更容易地进行维护和修改。它融合了传统编译型语言的高效性和脚本语言的易用性和富于表达性。Go语言作为服务器编程语言,很适合处理日志、数据打包、虚拟机处理、文件系统、分布式系统、
    发表于 03-22 15:03

    做个最火的黄鸭子控件,喜欢的下载

    `做个最火的黄鸭子控件,喜欢的下载`
    发表于 10-20 18:01

    时钟IC怎么满足高性能时序需求?

    应用系统的子系统,例如处理器、FPGA、数据转换器等。此类复杂系统需要动态更新参考时钟的频率,以实现 PCIe 和以太网等其它诸多协议。
    发表于 08-12 06:50

    日活用户破6亿可能吗

    日活用户破6亿可能吗✦宣布日活跃用户破6亿,未来一年让创作者收入800亿✦ TikTok:已向美国***提交解决方案,相信可以解决安全顾虑✦ 百度CTO王海峰发布百度大脑6.0
    发表于 07-28 09:49

    Go 相关的框架,和软件的精选清单 精选资料分享

    和音乐用于处理音频的。EasyMIDI -EasyMidi是一个简单可靠的,用于处理标准Midi文件(SMF)。flac支持FLAC流的Native
    发表于 08-12 07:53

    精选的 Go 框架,和软件的精选清单 精选资料分享

    翻译补充而来这是一个 Go 相关的框架,和软件的精选清单,引用自awesome-go项目,并翻译补充而来如果看到不再维护的项目,请及时联系发帖者或留言,谢谢!*音频和音乐用于处理音频
    发表于 08-12 06:32

    高性能开源伺服器ODRIVE的规格是什么?

    高性能开源伺服器ODRIVE的规格是什么?
    发表于 09-30 06:38

    香山是什么?“香山” 高性能开源 RISC-V 处理器项目介绍

    香山是什么2019 年,在中国科学院支持下,由 中国科学院计算技术研究所 牵头发起 “香山” 高性能开源 RISC-V 处理器项目,研发出目前国际上性能最高的开源
    发表于 04-07 14:20

    基于LabView平台的齿轮箱性能动态测试与诊断_李贵明

    基于LabView平台的齿轮箱性能动态测试与诊断_李贵明
    发表于 03-18 09:41 3次下载

    为什么做开源高性能RISC-v核,香山开源高性能RISC-V处理器开发流程

    RISC-V是一个基于精简指令集原则的开源指令集架构,那么为什么做开源高性能RISC-v核?
    发表于 06-22 14:25 2763次阅读
    为什么做<b class='flag-5'>开源</b><b class='flag-5'>高性能</b>RISC-v核,香山<b class='flag-5'>开源</b><b class='flag-5'>高性能</b>RISC-V<b class='flag-5'>处理</b>器开发流程

    商业级RISC-V 64位高性能处理开源了?!

    RISC-V虽然指令集开源,但从处理器IP的研发角度,整个行业属于浪费性竞争,这也严重制约着目前RISC-V高性能处理器从0到1的发展过程。
    发表于 10-19 14:06 1512次阅读
    商业级RISC-V 64位<b class='flag-5'>高性能</b><b class='flag-5'>处理</b>器<b class='flag-5'>开源</b>了?!

    Go开源13周年 2022发布更多改变的Go 1.18 和 Go 1.19版本

    Go 语言开发团队技术 leader Russ Cox 在博客中庆祝 Go 开源 13 周年。2009 年 11 月 10 日,Go 作为开源
    的头像 发表于 11-17 16:37 893次阅读

    Tars框架使用NIO进行网络编程的源码分析

    Tars是腾讯开源的支持多语言的高性能RPC框架,起源于腾讯内部2008年至今一直使用的统一应用框架TAF(Total Application Framework),目前支持C++、J
    的头像 发表于 06-26 17:31 387次阅读
    Tars框架使用NIO进行网络编程的源码分析