《陈卓钰-Sonic设计原理以及字节跳动微服务场景的应用收益.pdf》由会员分享,可在线阅读,更多相关《陈卓钰-Sonic设计原理以及字节跳动微服务场景的应用收益.pdf(79页珍藏版)》请在三个皮匠报告上搜索。
1、Sonic 设计原理以及字节跳动微服务场景的应用收益主讲人:陈卓钰演讲嘉宾介绍陈卓钰字节跳动基础架构资深研发工程师 字节跳动服务框架 Serdes 团队核心成员 2018 年加入字节跳动,期间发起了 Sonic JSON 项目,目前主攻 JIT 编译器以及高性能服务端组件CONTENT目录2023K+01为什么要优化 JSON 编解码Sonic 初探Sonic 的优化思路0203浅谈底层架构的通用化演进04Part 01为什么要优化 JSON 编解码1.大幅改善服务性能2.节约大量机器成本3.提升终端响应速度4.改善终端用户体验JSON JavaScript Object Notation 一
2、种非常流行的数据交换格式 文本格式,独立于语言 易于阅读和处理 广泛应用于:Web API 接口 文档数据库 元数据存储*注:此处占比统计有重叠,因此相加超过 100%Part 02Sonic 初探 Benchmark 与压力测试 字节线上服务真实收益 应用场景以及与 Hertz 的集成4619.262301.99Benchmark 结果Intel(R)Core(TM)i9-9880H CPU 2.30GHz某服务压测结果 使用 json-iterator某服务压测结果 使用 Sonic线上某服务 CPU 指标部分核心服务收益汇总 API 网关 ETL 引擎 配置中心 文档数据库.API 网关
3、 ETL 引擎 配置中心 文档数据库.等几乎所有需要用到 JSON 的地方Sonic&Hertzhttps:/ Sonic 进行请求的序列化和反序列化 需要 Go 1.15 Linux|macOSPart 03Sonic 的优化思路 消除反射:JIT 编译缓存:RCU 优化算子:SIMD 能省则省:Lazy LoadJSON 编解码到底有多难?JSON 编解码到底有多难?很简单,但也不简单 JSON 只有四种基本类型 number string true/false null 以及两种组合类型 Array Objectuser:id:12345,name:example,entities:ur
4、ls: JSON 是一种文本编码 没有固定长度 基于分隔符,而不是长度前缀 难以预先分配内存 难以实现部分解析.retweeted:false,user:id:1021030416.JSON 是一种无范式(schema-less)编码 可以定义结构体类型 也可以泛型编解码(interface)难以在编译的时候确定类型 高度依赖反射func Unmarshal(data byte,v interface)error 反射是最大的瓶颈 编译期无法确定类型 需要做大量的校验 函数调用不是免费的 无法进行函数间的优化 传参需要额外的指令 函数调用本身需要时间 借助 SIMD 能更高效的处理字符串 JS
5、ON 编解码本质上是串处理 Go 代码难以借助 SIMD 的力量处理字符串 ASM 可以用 SIMD,但是又存在函数调用开销消除反射JIT神奇的黑科技 Just-In-Time 即时编译技术 一种能够在运行时生成代码的技术 适用于动态语言的运行时优化 无法在编译期间确定类型 一些常见的 JIT 实现 Java HotSpot V8 PyPy LuaJIT动态语言无法在编译期确定类型程序语法结构固定用户可能编写任意程序一旦运行就不会改变.JSON无法预知用户的类型结构体定义方法固定用户可以定义任意结构体类型一旦加载就不会改变.如何将任意结构体转换为 JSON?type User struct I
6、DintNamestringEmail stringID:1,Name:Tom,Email: 如何将任意结构体转换为 JSON?1.写下 2.写下字段名,以及 3.写下字段的值4.如果还有更多字段 写一个 然后转到第 2 步5.写下,结束:,type User struct IDintNamestringEmail stringID:1,Name:Tom,Email:type User struct IDintNamestringEmail stringID:1,Name:Tom,Email: 如何将任意结构体转换为 JSON?1.写下 2.写下字段名,以及 3.写下字段的值4.如果还有更多字
7、段 写一个 然后转到第 2 步5.写下,结束print()for moreFields print(Quoted(field.Name)print(:)print(Quoted(field.Value)print(,)if notLastprint():,如何将一个给定的结构体(比如 User)转换为 JSON?type User struct IDintNamestringEmail stringID:1,Name:Tom,Email: 如何将一个给定的结构体(比如 User)转换为 JSON?1.写下 2.写下字段 ID 的值3.写下4.写下字段 Name 的值5.写下6.写下字段 Ema
8、il 的值7.写下,结束type User struct IDintNamestringEmail stringID:1,Name:Tom,Email:ID:,Name:,Email:print(ID:)print(user.ID)print(,Name:)print(Quoted(user.Name)print(,Email:)print(Quoted(user.Email)print()运行时可以知道所有的类型信息type User struct IDintNamestringEmail stringID:1,Name:Tom,Email: 运行时可以知道所有的类型信息1.有几个字段2.叫
9、什么名字3.都是什么类型4.都在什么位置5.附加的属性(tag)type User struct IDintNamestringEmail stringID:1,Name:Tom,Email:type User struct IDintNamestringEmail stringID:1,Name:Tom,Email:text ID:inttext,Name:strtext,Email:strtext MOVB$0 x7b,(DI)(CX*1)MOVL$0 x3a225822,1(DI)(CX*1)ADDQ$5,CXMOVQ(R8),AXLEAQ(DI)(CX*1),DXMOVQ DX,(SP
10、)MOVQ AX,8(SP)CALL i64toa.编译汇编运行type User struct IDintNamestringEmail stringID:1,Name:Tom,Email:text ID:inttext,Name:strtext,Email:strtext MOVB$0 x7b,(DI)(CX*1)MOVL$0 x3a225822,1(DI)(CX*1)ADDQ$5,CXMOVQ(R8),AXLEAQ(DI)(CX*1),DXMOVQ DX,(SP)MOVQ AX,8(SP)CALL i64toa.编译汇编运行type User struct IDintNamestring
11、Email stringID:1,Name:Tom,Email:text ID:inttext,Name:strtext,Email:strtext MOVB$0 x7b,(DI)(CX*1)MOVL$0 x3a225822,1(DI)(CX*1)ADDQ$5,CXMOVQ(R8),AXLEAQ(DI)(CX*1),DXMOVQ DX,(SP)MOVQ AX,8(SP)CALL i64toa.编译汇编运行type User struct IDintNamestringEmail stringID:1,Name:Tom,Email:text ID:inttext,Name:strtext,Ema
12、il:strtext MOVB$0 x7b,(DI)(CX*1)MOVL$0 x3a225822,1(DI)(CX*1)ADDQ$5,CXMOVQ(R8),AXLEAQ(DI)(CX*1),DXMOVQ DX,(SP)MOVQ AX,8(SP)CALL i64toa.编译汇编运行type User struct IDintNamestringEmail stringID:1,Name:Tom,Email:text ID:inttext,Name:strtext,Email:strtext MOVB$0 x7b,(DI)(CX*1)MOVL$0 x3a225822,1(DI)(CX*1)ADDQ
13、$5,CXMOVQ(R8),AXLEAQ(DI)(CX*1),DXMOVQ DX,(SP)MOVQ AX,8(SP)CALL i64toa.编译汇编运行可缓存编译缓存RCU读性能极高的缓存实现 编译缓存的场景是怎样的?编译缓存的场景是怎样的?读性能要求极高 每一次 Serdes 操作都至少需要读一次缓存 并发读取成为常态化 编译缓存的场景是怎样的?读性能要求极高 读 写,数量级上的差距 服务的生命周期可以无限 结构体的数量有上限 编译缓存的场景是怎样的?读性能要求极高 读 写,数量级上的差距 写操作临界区比较宽 编译开销相对较大 每个类型唯一对应一组编解码器 编译缓存的场景是怎样的?读性能要求
14、极高 读 写,数量级上的差距 写操作临界区比较宽 不存在删除、修改等操作 程序里的类型是固定的,不会随时间减少或者改变 编译好加载到内存的代码不能卸载(可能正在运行)编译缓存的场景是怎样的?读性能要求极高 读 写,数量级上的差距 写操作临界区比较宽 不存在删除、修改等操作需要一个支持 Wait-free Reader 的 append-only 缓存 Map+互斥锁:完全不能并发 Map+普通读写锁:Reader 会被 Writer 阻塞 Map+分段读写锁:降低了阻塞的概率,但终究没有避免 无锁 Map:实现过于复杂,并且可能存在“忙等”的问题是否存在一种简单同步机制,使得 Reader 永
15、不阻塞?RCU-Read-Copy Update Reader 无等待:无锁,无自旋 Writer 之间靠 Mutex 同步 牺牲 Writer 性能:适合读多写少的场景 在有 GC 的语言里实现非常方便:会自动回收不用的数据 RCU-Read-Copy Update Reader 无等待:无锁,无自旋 Writer 之间靠 Mutex 同步 牺牲 Writer 性能:适合读多写少的场景 在有 GC 的语言里实现非常方便:会自动回收不用的数据 RCU 写操作流程1.创建一个新的 map2.将数据全部拷贝到新的 map 里 这里对共享数据是读操作 RCU 写操作流程1.创建一个新的 map2.将
16、数据全部拷贝到新的 map 里3.修改新创建的拷贝 此时这个 map 对外界不可见4.更新全局指针 需要保证内存顺序5.老的 map 不用管,GC 会回收优化算子SIMD充分发挥 CPU 性能 SIMD-单指令多数据 一条指令处理多条数据 指令级并行数据量较大对所有的数据进行类似运算并且运算可以并行化的时候就那么几条数据数据与数据之间运算逻辑差异较大数据之间运算存在依赖适用不适用如何用程序高效地实现?012012=0 0+1 1+2 2+Nave 实现依据定义逐字翻译每次处理一个元素循环 N 次需要 N 次乘法和加法Nave 实现mulsd 需要 45 个时钟周期addsd 需要 34 个时钟
17、周期有效计算约为每元素 8 个时钟周期mulsd 指令addsd 指令vfmadd132pd 指令融合乘加运算单指令直接计算 +一次能处理 4 个元素SIMD 实现使用 SIMD 指令集(FMA)每次处理 32 个元素循环 N/32 次需要 N/32 次融合乘加运算SIMD 实现单条 vfmadd132pd 需要 46 个时钟周期多条指令并行,延迟需要乘以 CPI所以有效计算为每元素 0.50.75 时钟周期计算方法:480.532 680.532显著快于 Nave 方法vfmadd132pd 指令 JSON 编解码本质就是串处理 数据量较大(字符串较长)对每条数据(每个字符)处理的方式类似
18、数据之间相对独立(字符与字符之间不干扰)非常适合使用 SIMD 指令来处理/一次比较比较 32 个字符while(likely(nb=32)/vmovd 将 32 个字符加载到 ymm_m256i x=_mm256_load_si256(const void*)sp);/vpcmpeqb 比较字符_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);_m256
19、i 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)/tzcnt 计算末尾零的个数return sp-ss+_builtin_ctzll(uint64_t)ms);sp+=32;nb-=32;ntstatus:200,ntmsg:okn st a
20、t u s:2 0 0,m s g:o k x=sp=_m256i x=_mm256_load_si256(const void*)sp);_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);11a=b=c=11111_m256i v=_mm256_or_si256(a,b);_m256i w=_mm256_or_si256(v,c);v=1111w=111
21、 1111 asm2asm 将 clang 编译生成的 asm 转换为 Go ASM 可以充分利用 clang 的编译优化https:/ Load用最少的成本达成目标BenchmarkParseSmallMap_StdJson-8 11366 ns/op 32.11 MB/sBenchmarkParseSmallMap_JsonIterator-8 12474 ns/op 29.26 MB/sBenchmarkBindSmallStruct_StdJson-8 9826 ns/op 37.15 MB/sBenchmarkBindSmallStruct_JsonIterator-8 2642 n
22、s/op 138.14 MB/s泛型解码定型解码 对于大部分 Go JSON 库,泛型编解码是它们表现最差的场景之一 一部分服务或者业务的特性,决定了需要大量使用到泛型编解码 API 网关 ETL 引擎 动态代理.对于大部分 Go JSON 库,泛型编解码是它们表现最差的场景之一 一部分服务或者业务的特性,决定了需要大量使用到泛型编解码 API 网关 ETL 引擎 动态代理.JSON 其实具备完整的自描述能力 Sonic AST 使用 Node type,length,value 来表示一个数据节点 避免 Go map 的访问开销 与 JSON 本身的结构更贴近 可以直接跳过不需要的节点,减少无用功Part 04浅谈底层架构的通用性演进 任何 Serdes 组件基本的执行逻辑都是差不多的 分析类型信息 生成并优化 OpCode 生成机器码并装载 大部分 JIT 逻辑可以复用 SSA 后端可以进一步优化 将 IR 分级可以更好支持多 CPU 架构结构体元信息编码器 OpCode解码器 OpCodeHIRSSAx86_64aarch64特化逻辑通用逻辑LoaderJitKitTHANKS