问题排查: 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 = 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 = ''的一部分,最终我们使用了更贴合业务的形式将查询性能优化了99%以上。

参考:

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nginx 精解:正则表达式、location 匹配与 rewrite 重写

一、常见的 Nginx 正则表达式 在 Nginx 配置中&#xff0c;正则表达式用于匹配和重写 URL 请求。以下是一些常见的 Nginx 正则表达式示例&#xff1a; 当涉及正则表达式时&#xff0c;理解各个特殊字符的含义是非常重要的。以下是每个特殊字符的例子&#xff1a; ^&#xff1…

讯飞星火大模型个人API账号免费使用申请教程

文章目录 1.登录讯飞星火大模型官网 https://www.xfyun.cn/ 2.下滑找到Spark Lite&#xff0c;点击立即调用 3.星火大模型需要和具体的应用绑定&#xff0c;我们需要先创建一个新应用 https://console.xfyun.cn/app/myapp&#xff0c;应用名称可以按照自己的意愿起。 4.填写应用…

打造智慧工厂核心:ARMxy工业PC与Linux系统

智能制造正以前所未有的速度重塑全球工业格局&#xff0c;而位于这场革命核心的&#xff0c;正是那些能够精准响应复杂生产需求、高效驱动自动化流程的先进设备。钡铼技术ARMxy工业计算机&#xff0c;以其独特的设计哲学与卓越的技术性能&#xff0c;正成为众多现代化生产线背后…

ViT:2 理解CLIP

大模型技术论文不断&#xff0c;每个月总会新增上千篇。本专栏精选论文重点解读&#xff0c;主题还是围绕着行业实践和工程量产。若在某个环节出现卡点&#xff0c;可以回到大模型必备腔调或者LLM背后的基础模型新阅读。而最新科技&#xff08;Mamba,xLSTM,KAN&#xff09;则提…

vuInhub靶场实战系列--Kioptrix Level #3

免责声明 本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关。 目录 免责声明前言一、环境配置1.1 靶场信息1.2 靶场配置 二、信息收集2.1 主机发现2.1.1 netdiscover2.1.2 arp-scan主机扫描 2.2 端口扫描2.3 指纹识别2.4 目…

快速测试 Mybatis 复杂SQL,无需启动 Spring

快速测试mybatis的sql 当我们写完sql后&#xff0c;我们需要测试下sql是否符合预期&#xff0c;在填入各种参数后能否正常工作&#xff0c;尤其是对于复杂的sql。 一般我们测试可能是如下的代码: 由于需要启动spring&#xff0c;当项目较大的时候启动速度很慢&#xff0c;有些…

④-2单细胞学习-cellchat单数据代码补充版(通讯网络)

目录 通讯网络系统分析 ①社会网络分析 1&#xff0c;计算每个细胞群的网络中心性指标 2&#xff0c;识别细胞的信号流模式 ②非负矩阵分解&#xff08;NMF&#xff09;识别细胞的通讯模式 1&#xff0c;信号输出细胞的模式识别 2&#xff0c;信号输入细胞的模式识别 信…

RocketMq源码解析六:消息存储

一、消息存储核心类 rocketmq消息存储的功能主要在store这个模块下。 核心类就是DefaultMessageStore。我们看下其属性 // 配置文件 private final MessageStoreConfig messageStoreConfig; // CommitLog 文件存储实现类 private final CommitLog commitLog; …

【研发日记】Matlab/Simulink软件优化(三)——利用NaNFlag为数据处理算法降阶

文章目录 前言 背景介绍 初始算法 优化算法 分析和应用 总结 前言 见《【研发日记】Matlab/Simulink软件优化(一)——动态内存负荷压缩》 见《【研发日记】Matlab/Simulink软件优化(二)——通信负载柔性均衡算法》 背景介绍 在一个嵌入式软件开发项目中&#xff0c;需要开…

FedAvg论文

论文&#xff1a;Communication-Efficient Learning of Deep Networks from Decentralized Data 原code Reproducing 通过阅读帖子进行的了解。 联邦平均算法就是最典型的平均算法之一。将每个客户端上的本地随机梯度下降和执行模型的平均服务器结合在一起。 联邦优化问题 数…

开发小Tips:切换淘宝,腾讯,官方,yarn,cnpm镜像源,nrm包管理工具的具体使用方式(方便切换镜像源)

由于开发中经常要下载一些软件或者依赖&#xff0c;且大多数的官方源的服务器都在国外&#xff0c;网速比较慢&#xff0c;国内为了方便&#xff0c;国内一些大厂就建立一些镜像&#xff0c;加快下载速度。 1.各大镜像源的切换&#xff1a; 切换淘宝镜像源&#xff1a; npm …

Bio-Info每日一题:Rosalind-06-Counting Point Mutations

&#x1f389; 进入生物信息学的世界&#xff0c;与Rosalind一起探索吧&#xff01;&#x1f9ec; Rosalind是一个在线平台&#xff0c;专为学习和实践生物信息学而设计。该平台提供了一系列循序渐进的编程挑战&#xff0c;帮助用户从基础到高级掌握生物信息学知识。无论你是初…

数据结构笔记 线性表的查找 顺序,折半,分块查找

顺序查找&#xff1a;从头找到尾&#xff0c;或者从尾找到头 顺序查找的性能&#xff1a; 其中&#xff0c;辅助空间的O&#xff08;1&#xff09;用于存放哨兵的 折半查找&#xff1a;向下取整&#xff1a;指当计算的结果不为整数时取小于计算结果的整数。 折半查找的性能&am…

类和对象的学习总结(一)

面向对象和面向过程编程初步认识 C语言是面向过程的&#xff0c;关注过程&#xff08;分析求解问题的步骤&#xff09; 例如&#xff1a;外卖&#xff0c;关注点菜&#xff0c;接单&#xff0c;送单等 C是面向对象的&#xff0c;关注对象&#xff0c;把一件事拆分成不同的对象&…

十大排序

本文将以「 通俗易懂」的方式来描述排序的基本实现。 &#x1f9d1;‍&#x1f4bb;阅读本文前&#xff0c;需要一点点编程基础和一点点数据结构知识 本文的所有代码以cpp实现 文章目录 排序的定义 插入排序 ⭐ &#x1f9d0;算法描述 &#x1f496;具体实现 &#x1f…

记一次Linux下Docker镜像服务器磁盘空间清理

我们开发环境Jenkins构建项目时报服务器磁盘空间不足&#xff0c;导致项目自动化构建部署失败&#xff0c; Docker镜像服务器磁盘空间清理我们做了多次了&#xff0c;之前在清理Docker镜像服务器时走了不少弯路&#xff0c;查了不少Docker镜像服务器空间清理&#xff0c;都大同…

架构设计-全局异常处理器404、405的问题

java web 项目中经常会遇到异常处理的问题&#xff0c;普遍的做法是使用全局异常处理&#xff0c;这样做有以下几种原因&#xff1a; 集中化处理&#xff1a;全局异常处理允许你在一个集中的地方处理整个应用程序中的异常。这有助于减少代码重复&#xff0c;因为你不必在每个可…

C++的string类

目录 一、导入 二、接口学习 1.默认成员函数 2.迭代器相关的函数iterator 3.与容量相关的函数Capacity系列 4.与成员权限相关的函数Element access: 5.修改器相关的函数Modifiers&#xff1a; 6.字符串操作接口函数String operations:​编辑 三、扩展 一、导入 学习过…

修复损坏的Excel文件比你想象的要简单,这里提供几种常见的修复方法

打开重要的Excel文件时遇到问题吗?Microsoft Excel是否要求你验证文件是否已损坏?Excel文件可能由于各种原因而损坏,从而无法打开。但不要失去希望;你可以轻松修复损坏的Excel文件。 更改Excel信任中心设置 Microsoft Excel有一个内置的安全功能,可以在受限模式下打开有…

阿里通义千问 Qwen2 大模型开源发布

阿里通义千问 Qwen2 大模型开源发布 Qwen2 系列模型是 Qwen1.5 系列模型的重大升级。该系列包括了五个不同尺寸的预训练和指令微调模型&#xff1a;Qwen2-0.5B、Qwen2-1.5B、Qwen2-7B、Qwen2-57B-A14B 以及 Qwen2-72B。 在中文和英文的基础上&#xff0c;Qwen2 系列的训练数…