sonic :基于 JIT 技术的开源全场景高性能 JSON 库
Posted GoCN
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了sonic :基于 JIT 技术的开源全场景高性能 JSON 库相关的知识,希望对你有一定的参考价值。
项目仓库:https://github.com/bytedance/sonic
sonic 是字节跳动开源的一款 Golang JSON 库,基于即时编译(Just-In-Time Compilation)与向量化编程(Single Instruction Multiple Data)技术,大幅提升了 Go 程序的 JSON 编解码性能。同时结合 lazy-load 设计思想,它也为不同业务场景打造了一套全面高效的 API。
自 2021 年 7 月份发布以来, sonic 已被抖音、今日头条等业务采用,累计为字节跳动节省了数十万 CPU 核。
与 SIMD 。JIT对于有 schema 的定型编解码场景而言,很多运算其实不需要在“运行时”执行。这里的“运行时”是指程序真正开始解析 JSON 数据的时间段。
举个例子,如果业务模型中确定了某个 JSON key 的值一定是布尔类型,那么我们就可以在序列化阶段直接输出这个对象对应的 JSON 值(‘true’或‘false’),并不需要再检查这个对象的具体类型。
sonic-JIT 的核心思想就是:将模型解释与数据处理逻辑分离,让前者在“编译期”固定下来。
这种思想也存在于标准库和某些第三方 JSON 库,如 json-iterator 的函数组装模式:把 Go struct 拆分解释成一个个字段类型的编解码函数,然后组装并缓存为整个对象对应的编解码器(codec),运行时再加载出来处理 JSON。但是这种实现难以避免转化成大量 interface 和 function 调用栈,随着 JSON 数据量级的增长,function-call 开销也成倍放大。只有将模型解释逻辑真正编译出来,实现 stack-less 的执行体,才能最大化 schema 带来的性能收益。
业界实现方式目前主要有两种:代码生成 code-gen(或模版 template)和 即时编译 JIT。前者的优点是库开发者实现起来相对简单,缺点是增加业务代码的维护成本和局限性,无法做到秒级热更新——这也是代码生成方式的 JSON 库受众并不广泛的原因之一。JIT 则将编译过程移到了程序的加载(或首次解析)阶段,只需要提供 JSON schema 对应的结构体类型信息,就可以一次性编译生成对应的 codec 并高效执行。
sonic-JIT 大致过程如下:
sonic-JIT 体系
初次运行时,基于 Go 反射来获取需要编译的 schema 信息; 结合 JSON 编解码算法生成一套自定义的中间代码 OP codes; 将 OP codes 翻译为 Plan9 汇编; 使用第三方库 golang-asm 将 Plan 9 转为机器码; 将生成的二进制码注入到内存 cache 中并封装为 go function; 后续解析,直接根据 type ID (rtype.hash)从 cache 中加载对应的 codec 处理 JSON。
从最终实现的结果来看,sonic-JIT 生成的 codec 性能不仅好于 json-iterator,甚至超过了代码生成方式的 easyjson(见后文“性能测试”章节)。这一方面跟底层文本处理算子的优化有关(见后文“SIMD & asm2asm”章节),另一方面来自于 sonic-JIT 能控制底层 CPU 指令,在运行时建立了一套独立高效的 ABI(Application Binary Interface)体系:
对于大部分 Go JSON 库,泛型编解码是它们性能表现最差的场景之一,然而由于业务本身需要或业务开发者的选型不当,它往往也是被应用得最频繁的场景。
泛型编解码性能差仅仅是因为没有 schema 吗?其实不然。我们可以对比一下 C++ 的 JSON 库,如 rappidjson、simdjson,它们的解析方式都是泛型的,但性能仍然很好(simdjson 可达 2GB/s 以上)。标准库泛型解析性能差的根本原因在于它采用了 Go 原生泛型——interface(map[string]interface)作为 JSON 的编解码对象。
这其实是一种糟糕的选择:首先是数据反序列化的过程中,map 插入的开销很高;其次在数据序列化过程中,map 遍历也远不如数组高效。
回过头来看,JSON 本身就具有完整的自描述能力,如果我们用一种与 JSON AST 更贴近的数据结构来描述,不但可以让转换过程更加简单,甚至可以实现按需加载(lazy-load)——这便是 sonic-ast 的核心逻辑:它是一种 JSON 在 Go 中的编解码对象,用 node type, length, pointer 表示任意一个 JSON 数据节点,并结合树与数组结构描述节点之间的层级关系。
sonic-ast 结构示意
sonic-ast 实现了一种有状态、可伸缩的 JSON 解析过程:当使用者 get 某个 key 时,sonic 采用 skip 计算来轻量化跳过要获取的 key 之前的 json 文本;对于该 key 之后的 JSON 节点,直接不做任何的解析处理;仅使用者真正需要的 key 才完全解析(转为某种 Go 原始类型)。由于节点转换相比解析 JSON 代价小得多,在并不需要完整数据的业务场景下收益相当可观。
虽然 skip 是一种轻量的文本解析(处理 JSON 控制字符“[”、“”等),但是使用类似 gjson 这种纯粹的 JSON 查找库时,往往会有相同路径查找导致的重复开销。
针对该问题,sonic 在对于子节点 skip 处理过程增加了一个步骤,将跳过 JSON 的 key、起始位、结束位记录下来,分配一个 Raw-JSON 类型的节点保存下来,这样二次 skip 就可以直接基于节点的 offset 进行。同时 sonic-ast 支持了节点的更新、插入和序列化,甚至支持将任意 Go types 转为节点并保存下来。
换言之,sonic-ast 可以作为一种通用的泛型数据容器替代 Go interface,在协议转换、动态代理等服务场景有巨大潜力。
SIMD & asm2asm无论是定型编解码场景还是泛型编解码场景,核心都离不开 JSON 文本的处理与计算。其中一些问题在业界已经有比较成熟高效的解决方案,如浮点数转字符串算法 Ryu,整数转字符串的查表法等,这些都被实现到 sonic 的底层文本算子中。
还有一些问题逻辑相对简单,但是可能会面对较大数量级的文本,如 JSON string 的 unquote\\quote 处理、空白字符的跳过等。此时我们就需要某种技术手段来提升处理能力。SIMD 就是这样一种用于并行处理大规模数据的技术,目前大部分 CPU 已具备 SIMD 指令集(例如 Intel AVX),并且在 simdjson 中有比较成功的实践。
下面是一段 sonic 中 skip 空白字符的算法代码:
#if USE_AVX2
// 一次比较比较32个字符
while (likely(nb >= 32))
// vmovd 将单个字符转成YMM
__m256i x = _mm256_load_si256 ((const void *)sp);
// vpcmpeqb 比较字符,同时为了充分利用CPU 超标量特性使用4 倍循环
__m256i a = _mm256_cmpeq_epi8 (x, _mm256_set1_epi8(\' \'));
__m256i b = _mm256_cmpeq_epi8 (x, _mm256_set1_epi8(\'\\t\'));
__m256i c = _mm256_cmpeq_epi8 (x, _mm256_set1_epi8(\'\\n\'));
__m256i d = _mm256_cmpeq_epi8 (x, _mm256_set1_epi8(\'\\r\'));
// vpor 融合4次结果
__m256i u = _mm256_or_si256 (a, b);
__m256i v = _mm256_or_si256 (c, d);
__m256i w = _mm256_or_si256 (u, v);
// vpmovmskb 将比较结果按位展示
if ((ms = _mm256_movemask_epi8(w)) != -1)
_mm256_zeroupper();
// tzcnt 计算末尾零的个数N
return sp - ss + __builtin_ctzll(~(uint64_t)ms);
/* move to next block */
sp += 32;
nb -= 32;
/* clear upper half to avoid AVX-SSE transition penalty */
_mm256_zeroupper();
#endif
sonic 中 strnchr() 实现(SIMD 部分)
开发者们会发现这段代码其实是用 C 语言编写的 —— 其实 sonic 中绝大多数文本处理函数都是用 C 实现的:一方面 SIMD 指令集在 C 语言下有较好的封装,实现起来较为容易;另一方面这些 C 代码通过 clang 编译能充分享受其编译优化带来的提升。为此我们开发了一套 x86 汇编转 Plan9 汇编的工具 asm2asm,将 clang 输出的汇编通过 Go Assembly 机制静态嵌入到 sonic 中。同时在 JIT 生成的 codec 中我们利用 asm2asm 工具计算好的 C 函数 PC 值,直接调用 CALL 指令跳转,从而绕过 Go Assembly 不能寄存器传参的限制,压榨最后一丝 CPU 性能。
其它除了上述提到的技术外,sonic 内部还有很多的细节优化,比如使用 RCU 替换 sync.Map 提升 codec cache 的加载速度,使用内存池减少 encode buffer 的内存分配,等等。这里限于篇幅便不详细展开介绍了,感兴趣的同学可以自行搜索阅读 sonic 源码进行了解。
性能测试我们以前文中的不同测试场景进行测试,得到结果如下:
小数据(400B,11 个 key,深度 3 层)
中数据(110KB,300+ key,深度 4 层)
大数据(550KB,10000+ key,深度 6 层)
可以看到 sonic 在几乎所有场景下都处于领先(sonic-ast 由于直接使用了 Go Assembly 导入的 C 函数导致小数据集下有一定性能折损)
并且在生产环境中,sonic 中也验证了良好的收益,服务高峰期占用核数减少将近三分之一:
字节某服务在 sonic 上线前后的 CPU 占用(核数)对比
结语由于底层基于汇编进行开发,sonic 当前仅支持 amd64 架构下的 darwin/linux 平台 ,后续会逐步扩展到其它操作系统及架构。除此之外,我们也考虑将 sonic 在 Go 语言上的成功经验移植到不同语言及序列化协议中。目前 sonic 的 C++ 版本正在开发中,其定位是基于 sonic 核心思想及底层算子实现一套通用的高性能 JSON 编解码接口。
近日,sonic 发布了第一个大版本 v1.0.0,标志着其除了可被企业灵活用于生产环境,也正在积极响应社区需求、拥抱开源生态。我们期待 sonic 未来在使用场景和性能方面可以有更多突破,欢迎开发者们加入进来贡献 PR,一起打造业界最佳的 JSON 库!
相关链接
项目地址:https://github.com/bytedance/sonic
BenchMark:https://github.com/bytedance/sonic/blob/main/bench.sh
开源云真机平台-Sonic应用实践
前言
Sonic是一款开源、支持分布式部署、在线自动化测试的私有云真机平台。偶然接触到这个平台是源于虫师的一篇公众号文章《基于Linux 部署 Sonic》,于是结合文章内容和官网尝试搭建了一套,并试用了一番,直到现在也一直在用。后来陆续也加了作者的微信号、粉丝群,对于部署和使用过程中的一些问题和建议,作者和粉丝群中的小伙伴都能及时给予解答,整体体验很不错,遂决定写一篇总结分享。
一、云真机平台
1.云真机平台对比
目前市面上常见的云真机平台有两种,一种是各大服务商如阿里、腾讯、百度推出的公共云真机平台,如:WeTest、EMAS,除了收费高点其他好像没什么缺点;一种是网上各种开源云真机平台,支持私有化部署,如:Sonic、STF,这类平台的特点是免费、支持二次开发,缺点是功能支持相对薄弱(如自动化、性能)、真机采购成本高(当然,这并不是平台的缺点,如果你司有足够的测试机资源的话,而且也不是每个企业都有全面的兼容性测试需求,你也可以选择接入少量真机)。
公共 | 私有 | |
平台 | testin云测、Testbird、腾讯的WeTest、优测、阿里的EMAS、百度MTC | Sonic、ATX-Server2、STF |
特点 | 面向各大企业或个人 | 私有化部署、面向企业内部 |
优点 | 机型覆盖全,功能丰富,如:APP性能监控、自动化测试 | 开源、免费 |
缺点 | 收费高 | 自建成本高,尤其是真机采购费用 |
2.云真机平台能够解决的问题
- 兼容性测试:需接入一定数量、具有代表性的测试机资源;
- 自动化测试:平台本身自带自动化测试能力,也可以自己写代码、连接真机运行测试;
- 测试机资源紧张:云真机平台都秉承着用完即走的设计理念,因此使用完退出后,下一个登录用户可以接着使用,一定程度上能够解决研发团队内测试机资源紧张的问题;
- 远程演示:如验收测试或是客户演示过程中,手机和电脑无法实时投屏在同一个显示器或电视等外接设备,这是只需要在已投屏的电脑上登录远程真机地址即可实时访问,方便同步观看;
- 测试数据共享不便问题:测试过程中,有时需要在一个历史版本上验证回溯问题,云真机可以安装历史版本的APP,并提前预置好测试数据,以便共享使用;
二、sonic介绍
1.关于Sonic
Sonic,一站式开源分布式集群云真机测试平台,致力服务于中小企业的客户端UI测试。 Sonic当前的愿景是能帮助中小型企业解决在客户端自动化或远控方面缺少工具和测试手段的问题。
官网:https://sonic-cloud.gitee.io/#/Home
github: https://github.com/SonicCloudOrg
Testhome:https://testerhome.com/opensource_projects/sonic
2.功能特性
3.Sonic架构
架构介绍:http://testerhome.com/opensource_projects/sonic
三、Sonic环境搭建(docker-compose)
1.前置环境准备
硬件环境主要就是手机和Linux服务器一台(CentOS或Ubuntu均可,以下部署以CentOS系统为例),这个没什么好说的,主要说一下软件环境,Linux需安装:
- Docker、docker-compose:网上有很多教程,可以自行搜索安装,在此不过多赘述;
- MySQL:可以接入现有的同一内网环境下的MySQL,也可以使用Docker快速安装部署:
docker run -it -d -e MYSQL_ROOT_PASSWORD=123456 --name=mymysql -p 3307:3306 mysql
- ADB环境:ADB即Android调试桥,是接入Android设备的前提,搭建步骤可以参照《如何在Linux快速搭建一套ADB环境》;
- Python环境:可选,主要为了方便自动化测试代码也可以运行在Linux本地,搭建步骤可以参照《Linux下一键安装Python3&更改镜像源&虚拟环境管理技巧》;
2.下载依赖文件
部署文档:https://sonic-cloud.gitee.io/#/Deploy
wget https://github.com/SonicCloudOrg/sonic-server-simple/archive/refs/tags/v1.3.2-release.tar.gz # 下载压缩包
tar -zxvf v1.3.2-release.tar.gz # 解压后会得到docker-compose.yml LICENSE pom.xml README_CN.md README.md src
3.编辑配置文件
vi docker-compose.yml,可以对照如下内容进行修改:
version: 3
services:
sonic-server-simple:
image: "sonicorg/sonic-server-simple:v1.3.2-release"
environment:
# 以下为MySql配置,localhost请替换为自己MySql服务的ipv4地址
- MYSQL_HOST=192.168.1.122 # MySQL主机地址
- MYSQL_PORT=3308 # MySQL端口号
- MYSQL_DATABASE=sonic # MySQL为sonic单独创建一个数据库-sonic
- MYSQL_USERNAME=root # MySQL登录用户名
- MYSQL_PASSWORD=123456 # MySQL登录密码
# 在服务器部署的话,localhost改为服务器ip
# port更改为sonic-server-simple暴露的port(一般不变)
- SONIC_API_HOST=192.168.1.122
- SONIC_API_PORT=8094
- SONIC_NETTY_PORT=8095
# token加密的key值
- SECRET_KEY=sonic
# 身份验证token有效天数
- EXPIRE_DAY=150
# 前端页面访问地址,不填默认为http://localhost:3000
- CLIENT_HOST=http://192.168.1.122:3000
# 文件保留天数(指测试过程产生的文件,包括图片、录像等等)
- FILE_KEEP_DAY=60
# 测试结果保留天数
- RESULT_KEEP_DAY=60
# 以下均为Cron表达式
# 清理文件定时任务
- FILE_CRON=0 0 12 * * ?
# 清理测试结果定时任务
- RESULT_CRON=0 0 12 * * ?
# 发送日报定时任务
- DAY_CRON=0 0 10 * * ?
# 发送周报定时任务
- WEEK_CRON=0 0 10 ? * Mon
networks:
- sonic-network
# 数据卷
volumes:
- files:/keepFiles/
- files:/imageFiles/
- files:/recordFiles/
- files:/packageFiles/
- files:/logs/
# 端口映射
ports:
- "8094:8094"
- "8095:8095"
sonic-client-web:
image: "sonicorg/sonic-client-web:v1.3.2-release"
environment:
#192.168.1.1改为你的ipv4,port更改为sonic-server-simple暴露的port(一般不变)
- SONIC_API_HOST=192.168.1.122
- SONIC_API_PORT=8094
networks:
- sonic-network
# 端口映射
ports:
- "3000:80"
volumes:
files:
networks:
sonic-network:
driver: bridge
4.启动容器
docker-compose up -d
执行上述命令会按照先后顺序自动执行以下过程:
- 先拉取镜像
- 创建容器
- 启动容器
通过上图可以看出sonic自动创建了两个容器:
- sonic-server-simple-132-release_sonic-client-web_1:前端相关容器
- sonic-server-simple-132-release_sonic-server-simple_1:服务端相关容器
四、创建Sonic项目
1.注册账号
前台访问地址:http://192.168.1.122:3000/
2.创建项目
填写项目信息
3.创建agent
填写agent名称即可自动创建agent,下图为agent详情,记住AgentKey,后续部署sonic agent的时候会用到
五、部署Agent
1.下载agent相关资源
wget https://github.com/SonicCloudOrg/sonic-agent/releases/download/v1.3.2-release/docker-compose.yml
2.修改配置文件
编译sonic-agent的配置文件docker-compose.yml
version: 3
services:
sonic-agent:
#下方为Docker Hub镜像,推荐海外用户使用,默认使用国内加速镜像
# image: "sonicorg/sonic-agent-linux:v1.3.2-release"
image: "registry.cn-hangzhou.aliyuncs.com/sonic-cloud/sonic-agent-linux:v1.3.2-release"
environment:
# 替换为部署Agent机器的ipv4
- AGENT_HOST=192.168.1.122
# 替换为Agent服务的端口,可以自行更改
- AGENT_PORT=7777
# 替换为前面新增Agent生成的key
- AGENT_KEY=828aa5a4-ce30-4ebb-9f9b-5a01bbe5ea5e
# 后端的host
- SERVER_HOST=192.168.1.122
# 这个port改成后端文件中心的port(一般不变)
- SERVER_FOLDER_PORT=8094
# 这个port改成后端传输中心的port(一般不变)
- SERVER_TRANSPORT_PORT=8095
# 是否使用安卓模块
- ANDROID_ENABLE=true
# 是否开启远程adb调试功能
- USE_ADBKIT=true
# 是否使用iOS模块
- IOS_ENABLE=true
# 替换为你自己使用的wda的bundleId,如果没有.xctrunner后缀会自动补全,建议使用公司的开发者证书
- WDA_BUNDLE_ID=com.facebook.WebDriverAgentRunner.xctrunner
# 是否启用Appium
- APPIUM_ENABLE=true
# 默认为0会自动寻找随机端口启动,如果需要指定appium server端口,请在这里设置
- APPIUM_PORT=0
# 是否启用webview调试功能
- WEBVIEW_ENABLE=true
# 谷歌调试端口,一般不需要修改(默认0使用随机端口,如果需要开启防火墙给外部使用,请设置固定端口如7778)
- CHROME_DRIVER_PORT=0
network_mode: "host"
privileged: true
# 数据卷
volumes:
- /dev/bus/usb:/dev/bus/usb
- /var/run/usbmuxd:/var/run/usbmuxd
3.启动容器
docker-compose up -d
同前面部署sonic一样,执行上述命令部署agent,会按照先后顺序自动执行以下过程:
- 先拉取镜像
- 创建容器
- 启动容器
4.查看容器相关信息
docker ps -a | grep sonic
通过上图可以看出:
- sonic一共3个相关容器,分别为agent相关容器、前端相关容器、服务端相关容器;
- 前端访问的80端口被映射到了3000端口,与docker-compose.yml中配置的一致;
- 后端服务相关的8094、8095分别映射8094、8095,与docker-compose.yml中配置的一致;
六、Sonic使用
1.接入设备
以Android设备为例,Android设备需开启USB调试权限,并在插入服务器主机后、手机页面弹出的“是否允许USB调试本台设备”的选项中选择“是”。
接入设备后的页面:
2.使用设备
首次进入设备系统会初始化设备连接,可能需要等待几秒。
3.连接adb
同Windows电脑本地连接Android一样,Windows也可以通过adb远程连接sonic上的设备(进入设备页面,远程ADB窗口会显示一串连接命令“adb connect 192.168.1.122:37345”)。连接完adb后,就可以当做本地设备进行自动化测试等操作了。
adb connect 192.168.1.122:37345
4.自动化测试效果
小结
1.部署感受
总体来说,部署过程比较流畅,除了sib工具、 iOS配置WebDrierAgent由于资源限制,暂未尝试外,其他没有遇到什么卡住的点,对于初学者来说,需要:
- 熟练使用Linux常用操作命令;
- docker相关知识、docker-compose文件定义规范等;
- MySQL相关技能,如创建用户、创建数据库、用户赋予数据库权限等,当然也可以借助工具实现;
2.使用感受
无论是界面体验、流畅度、还是UI自动化测试功能,都非常不错,粉丝群内对于反馈的问题也能及时指导解决,唯一美中不足之处就是:
- 手机屏幕展示框首次未自适应显示,需要左右拖动屏幕右侧框条才能看到整个屏幕,也可以通过放大浏览器页面比例来达到查看整个手机屏幕的目的,但此时,其他区域也同样会被放大;
3.避坑指南
- 推荐使用Chrome浏览器,切勿使用火狐浏览器;
- 需在手机系统设置-输入法设置中,关闭安全键盘,否则会在调起键盘、输入字符时黑屏或无法输入;
- 如果进入手机页面,显示准备图像中,但手机端没安装sonic助手之类的APP,可以尝试重启agent容器、再重新插拔手机;
- 如果是小米手机连接,需要关闭MIUI优化;
- 目前在远程连接鸿蒙系统设备、执行uiautomator2自动化脚本过程中会报错“GatewayError”(刚开始初始化设备连接、启动APP时有反应,随后报错,关闭纯净模式仍存在);
更多测试开发实战干货,欢迎扫码关注,一起交流、学习!
以上是关于sonic :基于 JIT 技术的开源全场景高性能 JSON 库的主要内容,如果未能解决你的问题,请参考以下文章
Fury:一个基于JIT动态编译的高性能多语言原生序列化框架