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

news2024/12/23 3:21:50

在这里插入图片描述本作品采用知识共享署名-非商业性使用-相同方式共享 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 = result
	s.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 main

import (
	"sync"
	"time"
	"fmt"
	"net/http"
	_ "net/http/pprof"
)

type SeriesIDSet struct {
	sync.RWMutex
	bitmap *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 = result
	s.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 = result
	s.Unlock()
}

func main() {
	go func() {
		http.ListenAndServe("localhost:6060", nil)
	}()

	mainSet := &SeriesIDSet{bitmap: &Bitmap{}}

	var others []*SeriesIDSet
	cardinality := 5000000
	for 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.WaitGroup
	startConcurrent := 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.coloradmin.cn/o/1806800.html

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

相关文章

TinyVision V851s 使用 OpenCV + NPU 实现 Mobilenet v2 目标分类识别

用39块钱的V851se视觉开发板做了个小相机。 可以进行物品识别、自动追焦&#xff01; 这个超低成本的小相机是在V851se上移植使用全志在线开源版本的Tina Linux与OpenCV框架开启摄像头拍照捕获视频&#xff0c;并结合NPU实现Mobilenet v2目标分类识别以及运动追踪等功能......并…

Springboot整合SpringCache+redis简化缓存开发

使用步骤&#xff1a; 1.引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId> </dependency><dependency><groupId>org.springframework.boot</groupI…

定个小目标之刷LeetCode热题(14)

了解股票的都知道&#xff0c;只需要选择股票最低价格那天购入&#xff0c;在股票价格与最低价差值最大时卖出即可获取最大收益&#xff0c;总之本题只需要维护两个变量即可&#xff0c;minPrice和maxProfit&#xff0c;收益 prices[i] - minPrice,直接用代码描述如下 class …

AIRNet模型使用与代码分析(All-In-One Image Restoration Network)

AIRNet提出了一种较为简易的pipeline&#xff0c;以单一网络结构应对多种任务需求&#xff08;不同类型&#xff0c;不同程度&#xff09;。但在效果上看&#xff0c;ALL-In-One是不如One-By-One的&#xff0c;且本文方法的亮点是batch内选择patch进行对比学习。在与sota对比上…

电影制作中的版本控制:Perforce Helix Core帮助某电影短片避免灾难性文件损坏,简化艺术资产管理

Zubaida Nila是来自马来西亚的一名视觉特效师和虚拟制作研究员&#xff0c;她参加了Epic Games的一个为期六周的虚拟培训和指导项目——女性创作者计划。该计划提供了虚幻引擎工作流程的实践经验以及其他课程。Zubaida希望从中获得更多关于虚幻引擎的灯光、后期处理和特效技能方…

csrf与xss差别 别在弄乱了 直接靶场实操pikachu的csrf题 token绕过可以吗???

我们现在来说说这2个之间的关系&#xff0c;因为昨天的我也没有弄清楚这2者的关系&#xff0c;总感觉迷迷糊糊的。 xss这个漏洞是大家并不怎么陌生&#xff0c;导致xss漏洞的产生是服务器没有对用户提交数据过滤不严格&#xff0c;导致浏览器把用户输入的当作js代码返回客户端…

玉米粒计数检测数据集VOC+YOLO格式107张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;107 标注数量(xml文件个数)&#xff1a;107 标注数量(txt文件个数)&#xff1a;107 标注类别…

群体优化算法----树蛙优化算法介绍以及应用于资源分配示例

介绍 树蛙优化算法&#xff08;Tree Frog Optimization Algorithm, TFO&#xff09;是一种基于群体智能的优化算法&#xff0c;模拟了树蛙在自然环境中的跳跃和觅食行为。该算法通过模拟树蛙在树枝间的跳跃来寻找最优解&#xff0c;属于近年来发展起来的自然启发式算法的一种 …

c# iText使用

引入包 用nuget安装itext和itext.bouncy-castle-adapter包&#xff1a; 创建pdf string path "a.pdf"; PdfWriter writer new PdfWriter(path); PdfDocument pdfDoc new PdfDocument(writer); var docnew Document(pdfDoc); Paragraph p new Paragraph(&quo…

基于I2C协议的OLED显示(利用U82G库)

目录 一、实验目的 二、 U8g2下载 三、利用stm32f103的GPIO管脚、VCC和GND连接 OLED屏的I2C接口&#xff0c;采用cubemx设计一个HAL库程序框架&#xff0c;然后下载U82G源码&#xff0c;针对stm32f103和 0.96寸的I2C接口OLED屏&#xff0c;进行代码裁剪&#xff0c;然后移植到…

Fences 5 激活码 - 电脑桌面整理软件

提起桌面整理&#xff0c;经典老牌工具 Fences 必有一席之地&#xff0c;Stardock 发布了最新的 Fences 5 版本。 可以将文件和图标归类放入各个栅栏分区&#xff0c;并支持文件夹展开至桌面、分区置顶、淡化隐藏图标等功能&#xff0c;能让你的桌面焕然一新&#xff0c;不再混…

电阻十大品牌供应商

选型时选择热门的电阻品牌&#xff0c;主要是产品丰富&#xff0c;需求基本都能满足。 所所有的电路中&#xff0c;基本没有不用电阻的&#xff0c;电阻的选型需要参考阻值、精度、封装、温度范围&#xff0c;贴片/插件等参数&#xff0c;优秀的供应商如下&#xff1a; 十大电…

Cweek4+5

C语言学习 十.指针详解 6.有关函数指针的代码 代码1&#xff1a;(*(void (*)())0)(); void(*)()是函数指针类型&#xff0c;0是一个函数的地址 (void(*)())是强制转换 总的是调用0地址处的函数&#xff0c;传入参数为空 代码2&#xff1a;void (*signal(int, void(*)(int))…

系统思考—心智模式

凯恩斯说&#xff1a;“介绍新观念倒不是很难&#xff0c;难的是清除那些旧观念。”在过去的任何一年&#xff0c;如果你一次都没有推翻过自己最中意的想法&#xff0c;那么你这一年就算浪费了。旧观念像是根深蒂固的杂草&#xff0c;即使在新知识的光照下&#xff0c;也需要时…

Docker Desktop - WSL distro terminated abruptly

打开 PowerShell 或以管理员身份运行的命令提示符。运行以下命令以列出已安装的 WSL 分发&#xff1a; wsl --list 运行以下命令以注销 Docker 相关的分发 wsl --unregister <distro_name> 将<distro_name>替换为实际的 Docker 相关分发的名称。将<distro_…

模型 利特尔法则

说明&#xff1a;系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。揭示流量、存量、时间的数学关系。 1 利特尔法则的应用 1.1 银行服务系统的优化 一家银行希望优化其服务系统以减少客户的等待时间并提高服务效率。银行决定使用利特尔法则来分析和…

string经典题目(C++)

文章目录 前言一、最长回文子串1.题目解析2.算法原理3.代码编写 二、字符串相乘1.题目解析2.算法原理3.代码编写 总结 前言 一、最长回文子串 1.题目解析 给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 示例 1&#xff1a; 输入&#xff1a;s “babad” 输出&am…

人工智能系统越来越擅长欺骗我们?

人工智能系统越来越擅长欺骗我们&#xff1f; 一波人工智能系统以他们没有被明确训练过的方式“欺骗”人类&#xff0c;通过为他们的行为提供不真实的解释&#xff0c;或者向人类用户隐瞒真相并误导他们以达到战略目的。 发表在《模式》(Patterns)杂志上的一篇综述论文总结了之…

红黑树的介绍与实现

前言 前面我们介绍了AVL树&#xff0c;AVL树是一棵非常自律的树&#xff0c;有着严格的高度可控制&#xff01;但是正它的自律给他带来了另一个问题&#xff0c;即虽然他的查找效率很高&#xff0c;但是插入和删除由于旋转而导致效率没有那么高。我们上一期的结尾说过经常修改…

Java SE(Java Platform, Standard Edition)

Java SE&#xff08;Java Platform, Standard Edition&#xff09; 是Java平台的一个版本&#xff0c;面向桌面应用程序、服务器和嵌入式环境。Java SE提供了开发和运行Java应用程序的基础API&#xff08;Application Programming Interface&#xff0c;应用程序编程接口&…