问题排查: Goalng Defer 带来的性能损耗

在这里插入图片描述本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。

文章目录

  • 引言
  • 问题背景
  • 结论

引言

性能优化之路道阻且长,因为脱敏规定,此文记录一个问题排查的简化过程。

问题背景

一个业务的查询绝大多数查询如下所示:

SELECT max(field) FROM m WHERE ((a = ‘1254801811’ AND b = ‘’ AND c = ‘lzl’ AND d = ‘lzl2’ AND e = ‘’ AND f = ‘’ AND g = ‘5702962’ AND h = ‘20466’) OR (a = ‘1254801811’ AND b = ‘’ AND c = ‘lzl’ AND d = ‘lzl2’ AND e = ‘’ AND f = ‘’ AND g = ‘5702962’ AND h = ‘20467’) OR (a = ‘1254801811’ AND b = ‘’ AND c = ‘lzl’ AND d = ‘lzl2’ AND e = ‘’ AND f = ‘’ AND g = ‘5702962’ AND h = ‘20468’) OR (a = ‘1254801811’ AND b = ‘’ AND c = ‘lzl’ AND d = ‘lzl2’ AND e = ‘’ AND f = ‘’ AND g = ‘5702962’ AND h = ‘20469’) OR (a = ‘1254801811’ AND b = ‘’ AND c = ‘lzl’ AND d = ‘lzl2’ AND e = ‘’ AND f = ‘’ AND g = ‘5702962’ AND h = ‘20470’) OR (a = ‘1254801811’ AND b = ‘’ AND c = ‘lzl’ AND d = ‘lzl2’ AND e = ‘’ AND f = ‘’ AND g = ‘5702962’ AND h = ‘20471’) OR (a = ‘1254801811’ AND b = ‘’ AND c = ‘lzl’ AND d = ‘lzl2’ AND e = ‘’ AND f = ‘’ AND g = ‘5702962’ AND h = ‘20472’) OR (a = ‘1254801811’ AND b = ‘’ AND c = ‘lzl’ AND d = ‘lzl2’ AND e = ‘’ AND f = ‘’ AND g = ‘5702962’ AND h = ‘20473’) OR (a = ‘1254801811’ AND b = ‘’ AND c = ‘lzl’ AND d = ‘lzl2’ AND e = ‘’ AND f = ‘’ AND g = ‘5702962’ AND h = ‘20474’) OR (a = ‘1254801811’ AND b = ‘’ AND c = ‘lzl’ AND d = ‘lzl2’ AND e = ‘’ AND f = ‘’ AND g = ‘5702962’ AND h = ‘20475’)) AND time >= 477088h AND time <= 28626779m GROUP BY time(1h), a, b, c, d, e, f, g, h fill(none)

内部发现这种sql在多条并发执行时不但执行时间较长,而且会吃满节点cpu,因为内部存在限流,读读分离等逻辑,这种行为会导致大量排队,从而一段时间内查询平均时延从20ms升高到1000ms,严重影响业务。

最终经过一系列的定位,我们定位到瓶颈在 e = '' 的条件筛选上。

时序数据库中需要基于条件去做时间线的筛选,在得到最终时间线后去实际的数据文件中去获取数据。等于空和不等于空这样的条件相对于c = 'lzl'的条件查询大不相同。前者需要首先定位到所有tagkey所有的时间线,然后获取measurement所有的时间线,最后求差集才可以。因为引擎实现的原因,并没有在索引中存储tagkey所属的全部时间线,而是存储tag的所有tagvalue对应的所有时间线,所以在求tagkey所属的全部时间线时需要做一个交集。而e的tagvalue数量在10w级别。

tagkey所属tagvalue的合并逻辑类似下述代码:

func (s *SeriesIDSet) Merge(others ...*SeriesIDSet) {bms := make([]*Bitmap, 0, len(others)+1)s.RLock()bms = append(bms, s.bitmap)s.RUnlock()for _, other := range others {other.RLock()defer other.RUnlock()bms = append(bms, other.bitmap)}result := FastOr(bms...)s.Lock()s.bitmap = results.Unlock()
}

可以看到当others过多时,会存在相当多的defer函数,在排查中我们发现这个函数的瓶颈来自于defer。

当defer在函数内在8个以上时,会使用堆上分配的方式[2],在允许时执行runtime.deferprocruntime.deferprocruntime.newdefer 会基于go本身的内存分配器获得runtime._defer结构体,这里包含三种路径:

  1. 从调度器的延迟调用缓存池 sched.deferpool 中取出结构体并将该结构体追加到当前 Goroutine 的缓存池中;
  2. 从 Goroutine 的延迟调用缓存池 pp.deferpool 中取出结构体;
  3. 通过 runtime.mallocgc 在堆上创建一个新的结构体;

然后将runtime._defer插入Goroutine _defer 链表的最前面。

在函数结束时runtime.deferreturn 会从 Goroutine 的 _defer 链表中取出最前面的runtime._defer 并调用 runtime.jmpdefer 传入需要执行的函数和参数。

当defer在函数内在8个以下时,在Go 1.14及之后的版本会使用open coded defer[1],简单讲就是会把defer的内容拷贝到栈上,并给函数准备一个FUNCDATA,这样就不存在对象的申请和释放了,性能相当优秀。

在函数内部defer过多时,会大量触发runtime.mallocgc ,用于分配defer对象。

下面的代码模拟了现网场景以及对应优化:

package mainimport ("sync""time""fmt""net/http"_ "net/http/pprof"
)type SeriesIDSet struct {sync.RWMutexbitmap *Bitmap
}type Bitmap struct{}func FastOr(bitmaps ...*Bitmap) *Bitmap {time.Sleep(10 * time.Millisecond)return &Bitmap{}
}func (s *SeriesIDSet) Merge(others ...*SeriesIDSet) {bms := make([]*Bitmap, 0, len(others)+1)s.RLock()bms = append(bms, s.bitmap)s.RUnlock()for _, other := range others {other.RLock()defer other.RUnlock()bms = append(bms, other.bitmap)}result := FastOr(bms...)s.Lock()s.bitmap = results.Unlock()
}func (s *SeriesIDSet) OptimizationMerge(others ...*SeriesIDSet) {bms := make([]*Bitmap, 0, len(others)+1)s.RLock()bms = append(bms, s.bitmap)s.RUnlock()for _, other := range others {other.RLock()bms = append(bms, other.bitmap)}defer func() {for _, other := range others {other.RUnlock()}}()result := FastOr(bms...)s.Lock()s.bitmap = results.Unlock()
}func main() {go func() {http.ListenAndServe("localhost:6060", nil)}()mainSet := &SeriesIDSet{bitmap: &Bitmap{}}var others []*SeriesIDSetcardinality := 5000000for i := 0; i < cardinality; i++ {others = append(others, &SeriesIDSet{bitmap: &Bitmap{}})}startSerial := time.Now()for i := 0; i < 50; i++ {mainSet.Merge(others...)}elapsedSerial := time.Since(startSerial)fmt.Printf("Cardinality %d, Serial Merge took %s\n", cardinality, elapsedSerial)var wg sync.WaitGroupstartConcurrent := time.Now()for i := 0; i < 50; i++ {wg.Add(1)go func() {defer wg.Done()mainSet.Merge(others...)}()}wg.Wait()elapsedConcurrent := time.Since(startConcurrent)fmt.Printf("Cardinality %d, Concurrent Merge took %s\n", cardinality, elapsedConcurrent)startConcurrent = time.Now()for i := 0; i < 50; i++ {mainSet.OptimizationMerge(others...)}elapsedConcurrent = time.Since(startConcurrent)fmt.Printf("Cardinality %d, Serial OptimizationMerge took %s\n", cardinality, elapsedConcurrent)startConcurrent = time.Now()for i := 0; i < 50; i++ {wg.Add(1)go func() {defer wg.Done()mainSet.OptimizationMerge(others...)}()}wg.Wait()elapsedConcurrent = time.Since(startConcurrent)fmt.Printf("Cardinality %d, Concurrent OptimizationMerge took %s\n", cardinality, elapsedConcurrent)
}

开发机器规格32c64g
在这里插入图片描述

可以看到在未优化defer的情况下,并行只比串行快了四倍不到,这证明对象申请的过程存在竞争,多个goroutine互相影响。也符合我们现网的观察。

在优化defer后,并行比串行快了近十倍。

仅串行优化前后性能也差了九倍。

未优化时cpu profile如下:
在这里插入图片描述

结论

这里当然只是在讨论defer。修改是顺便的事情,这只是e = ''的一部分,最终我们使用了更贴合业务的形式将查询性能优化了95%以上。

参考:

  1. open coded defer 是怎么实现的
  2. golang defer原理
  3. golang 内存分配器

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/25199.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vite常识性报错解决方案

1.导入路径不能以“.ts”扩展名结束。考虑改为导入“xxx.js” 原因&#xff1a;当你尝试从一个以 .ts 结尾的路径导入文件时&#xff0c;ESLint 可能会报告这个错误&#xff0c;因为它期望导入的是 JavaScript 文件&#xff08;.js 或 .jsx&#xff09;而不是 TypeScript 文件&…

coap-emqx:使用libcoap与emqx通信

# emqx开启CoAP网关 请参考【https://blog.csdn.net/chenhz2284/article/details/139562749?spm1001.2014.3001.5502】 # 写一个emqx的客户端程序&#xff0c;不断地往topic【server/1】发消息 【pom.xml】 <dependency><groupId>org.springframework.boot<…

开源与新质生产力

在这个信息技术迅猛发展的时代&#xff0c;全球范围内的产业都在经历着深刻的变革。在这样的背景下&#xff0c;“新质生产力”的概念引起了广泛的讨论。无论是已经成为或正努力转型成为新质生产力的企业&#xff0c;都在寻求新的增长动力和竞争优势。作为一名长期从事开源领域…

Linux用户和用户组的管理

目录 前言一、系统环境二、Linux用户组的管理2.1 新增用户组2.2 删除用户组2.3 修改用户组2.4 查看用户组 三、Linux用户的管理3.1 新增用户3.2 删除用户3.3 修改用户3.4 查看用户3.5 用户口令&#xff08;密码&#xff09;的管理 总结 前言 本篇文章介绍如何在Linux系统上实现…

OrangePi Kunpeng Pro深度评测:性能与体验的完美融合

文章目录 一、引言二、硬件开箱与介绍1.硬件清单2.硬件介绍 三、软件介绍四、性能测试1. 功率测试2. cpu测试2.1 单线程cpu测试2.2 多线程cpu测试 五、实际开发体验1. 搭建API服务器2. ONNX推理测试3. 在线推理平台 五、测评总结1. 能与硬件配置2. 系统与软件3. 实际开发体验个…

探索智慧商场的功能架构与应用

在数字化和智能化的浪潮下&#xff0c;智慧商场已经成为零售业的重要发展方向之一。智慧商场系统的功能架构设计与应用&#xff0c;结合了现代信息技术和零售业的实际需求&#xff0c;为商场的管理和运营提供了全新的解决方案。本文将深入探讨智慧商场的功能架构与应用&#xf…

matlab---app

一 基础 标签和信号灯没有回调函数 clc,clear,close all %清理命令区、工作区&#xff0c;关闭显示图形 warning off %消除警告 feature jit off %加速代码运行 ysw{i}i %循环赋值 celldisp(ysw) %显示元胞数组ysw.y1{1}[1,2] …

《软件定义安全》之二:SDN/NFV环境中的安全问题

第2章 SDN/NFV环境中的安全问题 1.架构安全 SDN强调了控制平面的集中化&#xff0c;从架构上颠覆了原有的网络管理&#xff0c;所以SDN的架构安全就是首先要解决的问题。例如&#xff0c;SDN实现中网络控制器相关的安全问题。 1.1 SDN架构的安全综述 从网络安全的角度&…

@BeforeAll 和 @AfterAll 必须是 static 的原因

BeforeAll 和 AfterAll 必须是 static 的原因 执行时机&#xff1a; BeforeAll 方法在所有测试方法之前运行。AfterAll 方法在所有测试方法之后运行。 实例化前/后的执行&#xff1a; 因为 BeforeAll 是在所有测试方法执行之前运行的&#xff0c;所以它在任何一个测试实例创建…

基于springboot的教学管理系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;教师管理&#xff0c;学生管理&#xff0c;课程管理 教师账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;课程管理&#xff0c;课程表…

数据结构---树与二叉树

个人介绍 hello hello~ &#xff0c;这里是 code袁~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的…

【CTF MISC】XCTF GFSJ0290 reverseMe Writeup(图像处理)

reverseMe 暂无 解法 导入 Photoshop。 水平翻转&#xff0c;得到 flag。 Flag flag{4f7548f93c7bef1dc6a0542cf04e796e}声明 本博客上发布的所有关于网络攻防技术的文章&#xff0c;仅用于教育和研究目的。所有涉及到的实验操作都在虚拟机或者专门设计的靶机上进行&#xf…

QField如何打开工程或数据文件

Field有个文件选择器&#xff0c;允许从本地设备打开工程。如果想从云端打开文件&#xff0c;请参阅 QFieldCloud 。 注意&#xff1a;请注意&#xff0c;卸载QField时&#xff0c;应用程序文件夹将被删除&#xff0c;而更新则不会。 导入并打开本地工程 QField界面 当转到 …

了解Synchronized对象头?

1、对象头的结构 Java对象存储在内存中结构为&#xff1a; 对象头&#xff08;Header&#xff09;&#xff1a;实例数据&#xff08;Instance Data&#xff09;&#xff1a;定义类中的成员属性对齐填充字节&#xff08;Padding&#xff09;&#xff1a;由于HotSpot虚拟机的自…

Linux--进程间通信(system V共享内存)

目录 1.原理部分 2.系统调用接口 参数说明 返回值 1. 函数原型 2. 参数说明 3. 返回值 4. 原理 5. 注意事项 3.使用一下shmget&#xff08;一段代码&#xff09; 4.一个案例&#xff08;一段代码) 1.简单封装一下 2.使用共享内存 2.1挂接&#xff08;shmat&#x…

Java 语言概述 -- Java 语言的介绍、现在、过去与将来

大家好&#xff0c;我是栗筝i&#xff0c;这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 001 篇文章&#xff0c;在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验&#xff0c;并希望进…

LLVM Cpu0 新后端7 第一部分 DAG调试 dot文件 Machine Pass

想好好熟悉一下llvm开发一个新后端都要干什么&#xff0c;于是参考了老师的系列文章&#xff1a; LLVM 后端实践笔记 代码在这里&#xff08;还没来得及准备&#xff0c;先用网盘暂存一下&#xff09;&#xff1a; 链接: https://pan.baidu.com/s/1V_tZkt9uvxo5bnUufhMQ_Q?…

contos7使用docker安装vulhub

contos7下使用docker安装vulhub 1. 安装docker 1. 更新yum &#xff08;1&#xff09;切换root用户 su root &#xff08;2&#xff09;更新yum yum update 2. 卸载旧版本的docker sudo yum remove docker sudo yum remove docker-client sudo yum remove docker-clien…

高并发ping多台主机IP

简介 社区或者是大型公司往往有成千上万或者几百台设备&#xff0c;保持设备始终在线对网络运维人员来说至关重要&#xff0c;然而一个一个登录检查&#xff0c;或者一个一个ping并不明智&#xff0c;累人且效率极低&#xff0c;并出错率高。花钱买检测服务当我没说。 shell编…

K210视觉识别模块学习笔记5:(嘉楠)训练使用模型_识别人脸

今日开始学习K210视觉识别模块:(嘉楠)训练与使用模型_识别人脸 亚博智能的K210视觉识别模块...... 固件库版本: canmv_yahboom_v2.1.1.bin 之前的训练网址部署模型时需要我们自己更换固件&#xff0c;而且还不能用亚博的图像操作库函数了&#xff0c;这十分不友好&#xff0…