Go并发大坑:inconsistent mutex state 解决及心得体会

news2024/11/17 0:44:08

文章目录

  • 前言
  • 首次排查(未重视)
  • 问题逐渐严重
  • 加大排查力度
    • 增加锁日志
    • race detector
  • 一次意外的复现
  • 写在最后
    • 解决问题所必需的品格

前言

大概在这篇文章发布数个月之前,某天,线上稳定运行的Go服务突然毫无征兆的发生了一次重启(系统框架是能够兜住普通的panic的),查看线上panic日志后,发现这段异常栈是在底层的标准库中抛出的,异常栈大概长这样:

fatal error: sync: inconsistent mutex state

goroutine 3571787 [running]:
runtime.throw(0x4e6ccbc, 0x1e)
        /usr/local/go/src/runtime/panic.go:1116 +0x72 fp=0xc0021dd378 sp=0xc0021dd348 pc=0x439512
sync.throw(0x4e6ccbc, 0x1e)
        /usr/local/go/src/runtime/panic.go:1102 +0x35 fp=0xc0021dd398 sp=0xc0021dd378 pc=0x46cbb5
sync.(*Mutex).lockSlow(0x8313a38)
        /usr/local/go/src/sync/mutex.go:147 +0x165 fp=0xc0021dd3e0 sp=0xc0021dd398 pc=0x47d2a5
sync.(*Mutex).Lock(...)
        /usr/local/go/src/sync/mutex.go:81
sync.(*Pool).pinSlow(0xc00007ec68, 0x0, 0x0)
        /usr/local/go/src/sync/pool.go:213 +0x271 fp=0xc0021dd460 sp=0xc0021dd3e0 pc=0x47de31
sync.(*Pool).pin(0xc00007ec68, 0xc00bb1fab2, 0x4)
        /usr/local/go/src/sync/pool.go:206 +0x65 fp=0xc0021dd488 sp=0xc0021dd460 pc=0x47db85
sync.(*Pool).Get(0xc00007ec68, 0x113, 0x1)
        /usr/local/go/src/sync/pool.go:128 +0x2f fp=0xc0021dd4d0 sp=0xc0021dd488 pc=0x47d82f
...
再往下就是一些更上层的代码,因为不是重点所以这里省略掉。

本以为就是一个普通的线上问题,很快就能修复,没想到之后就开启了为时2周的异常排查之路…(本篇省去了一些排查过程中走过的弯路)

首次排查(未重视)

出现问题后,笔者和同事们立马组织了排查工作。

第一次看到这串异常栈还是比较懵逼的,因为从异常栈里无法判断是哪里出错了,单从栈信息上看,发现是第三方库调用标准库sync.Pool的时候引发了一个锁异常(这个例子里第三方库是grpc,不是重点,因为后面其他用到sync.Pool的第三方库都会抛出这个异常)。

这时候同事们怀疑是第三方库或者物理机出现了问题(虽然之后证明不是),于是乎,开始有针对性的排查服务中有大量grpc并发调用的地方,并进行了一些尝试,比如将grpc调用切换成http,另一方面联系运维对服务运行的物理机进行排查。

最后的结果是,这段异常栈不在grpc上报了,变成在其他第三方库抛出(比如json-iterator,还有其他的,就不一一列举了)。而运维那边排查下来的结果,也是没有问题。这下笔者和同事们开始犯难了。由于该异常出现的频率较少(刚开始大概一周1~2次实例),加上暂时没有什么排查思路,故而再排查一段时间未果后,只能先记个todo了(毕竟还有其他工作)

另一个阻碍排查进度的问题是,线上的这个异常无法在其他环境复现,这才是要老命的。无法复现的问题只能纯靠经(瞎)验(猜)来排查,运气不好的话,可能几周都查不出来。

问题逐渐严重

刚开始由于上述问题在生产环境出现的不是很频繁(可能一周1~2次),加上在其他环境难以复现,排查的脚步在逐渐放缓(说的直白一点,就是有点麻木了,现在回过头来看,这是一个很不好的现象)。

但可以肯定的是,问题还一直存在着,并且不会自己消失。随着后面项目的迭代速度变快,直到某个周末,inconsistent mutex state 问题出现的更加频繁(这个时候异常崩溃重启已经达到一天5~8次),这直接影响到了我们线上系统的可用性(运维统计下来已经低于可用性4个9的标准),问题解决迫在眉睫。

加大排查力度

后续的时间里,我们加大了对该问题的排查力度,力求能够在尽可能短的时间内解决问题。笔者在这里将会一一介绍排查问题过程中的思路与手段

增加锁日志

首先,我们考虑以最简单直接的方式来排查线上问题——加日志。虽然程序的异常栈基本各不相同,但都在使用sync.pool途中加锁的代码处出现了异常,那我们就通过加日志的方法来查看加锁过程中哪些数据出现了异常。

这里排查的方法没有问题,但是思路出现了偏差,笔者小伙伴们一度认为是Go的锁代码逻辑可能有问题(其实是业务并发引起的,这个下面会讲到),于是便有了后面的梳理go mutex源码的过程。

既然要排查锁代码的问题(后面证明其实没问题),首先就需要理解go mutex的实现逻辑,我们进到mutex.go标准库文件中查看代码,可以看到整个锁的实现加上注释大概不到200行,可以说go锁的实现十分简洁明了。

这里简单介绍下Go语言中锁的实现逻辑,在 Go 语言中,互斥锁是通过 sync 包中的 Mutex 类型来实现的。这个类型定义如下:

type Mutex struct {
	// 内部存储互斥量和其他信息的结构体
	state int32
	sema uint32
}

Mutex 类型内部存储了一个状态值和一个信号量。状态值用于记录互斥体的当前状态,例如是否被锁定等。信号量则用于在互斥体被锁定时,将其他线程或协程阻塞。

源代码中暴露出去给外面使用的方法就更少了,只有两个:Lock()Unlock(),分别对应加锁和解锁,完整的代码解析已经超出了本篇的范畴,感兴趣了的话可以自行查询资料学习,这里给出一片 参考文章。

刚才提到的加日志主要是为了观察互斥锁中state值的变化,由于state记录了Mutex的四种状态(这里感叹一下state的巧妙设计,如果对与state还不理解的话建议先理解一下):

  1. 是否被锁定
  2. 是否有协程被唤醒
  3. 是否处于饥饿状态
  4. 等待的协程数量

所以我们需要了解哪部分加锁过程导致了锁状态异常。由于这个异常是不可逆的,一旦出现,系统会直接崩溃,也就会导致本篇开头提到的实例重启,影响可用性

// mutex.go
...
// throw出来的异常无法通过recover包掉
throw("sync: inconsistent mutex state")
...

通过日志我们得知报错发生在程序尝试获取锁失败之后到进入阻塞之前,因为我们观测到state值在程序崩溃之前变成非常的大,如果按照state的设计来解读的话,我们能够得出当前有上亿个协程在阻塞队列中,这显然是一个错误的结论;

// 这是在 mutex.go中打的日志 old和new是state的某个瞬时状态
cas fail: old = 8, new = 9
cas fail: old = 8, new = 2
cas fail: old = 16, new = 17
cas fail: old = 8, new = 9
cas fail: old = 8, new = 9
...
fatal error: inconsistent mutex state, new = 692768617

可以看到state在某一时刻突变,导致加锁程序异常奔溃。

但是,在梳理过mutex.go的代码后,我们最终确信锁状态的异常还是大概率由外部引起,只是最终在互斥锁代码这里被程序捕捉到并最终抛出。

至于为什么state的会变成日志中的异常值,我们推测是不规范的并发逻辑写引发了 memory corruption(后视镜角度看的话,目前对于问题的诱因已经判断的比较准确了)

那么剩下的就是继续朝着并发异常的路子继续往下排查啦~

race detector

要检查并发问题,go 语言有一个专门的工具,叫做 race detector。它可以帮助我们发现 go 程序中的数据竞争问题。

race detector的使用非常简单,只需要在项目启动时在命令中加入-race参数即可,像这样

// xxx.go 是项目启动入口
go run -race xxx.go

如果在执行过程中发现了数据竞争问题,race detector 会在命令行中输出错误信息,包括并发异常的位置、发生的时间、以及相关的 goroutine 信息。

这里举一个简单的例子看一看,在以下这段代码中,定义了一个全局变量 x,并在两个 goroutine 中同时对其进行修改:

package main

import (
	"fmt"
	"sync"
	"time"
)

var x int

func main() {
	wg := sync.WaitGroup{}
	wg.Add(2)

	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			x++
			time.Sleep(time.Millisecond * 100)
		}
	}()

	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			x++
			time.Sleep(time.Millisecond * 100)
		}
	}()

	wg.Wait()
	fmt.Println(x)
}

执行 go run -race main.go 命令启动这段程序,如果存在数据竞争问题,则会看到如下输出:

==================
WARNING: DATA RACE
Read at 0x00c420086008 by goroutine 7:
  main.main.func2()
      /path/to/main.go:15 +0x7c

Previous write at 0x00c420086008 by goroutine 6:
  main.main.func1()
      /path/to/main.go:9 +0x7c

Goroutine 7 (running) created at:
  main.main()
      /path/to/main.go:14 +0x7b

Goroutine 6 (running) created at:
  main.main()
      /path/to/main.go:10 +0x7b

Found 1 data race(s)
exit status 66

从输出结果可以看出,在程序执行过程中发生了并发写问题。race detector 告诉我们,发生问题的位置在 main.go 文件的第 15 行,是由 goroutine 7 执行的修改全局变量x,而之前这个全局变量 x 又被 goroutine 6 修改了。

那么回到项目上,我们在开发环境开启race detector之后(千万别再生产环境开启,不然会严重拖慢服务),通过脚本模拟线上请求,确实发现了大量数据竞争(十几个人维护了几年的项目,出现问题也并不奇怪)。

由于问题发生已经不是在第一现场(距离第一次出现问题大概过去了1~2个月),不知道是哪一块的数据竞争导致了memory corruption 进而引发inconsistent mutex state,于是乎只能开始吭哧吭哧的一个一个改,这个过程是缓慢且令人沮丧的。

一次意外的复现

由于这个问题无法在开发环境复现,我们也就无法精准定位到具体哪一段代码会出现问题。

而这样的情况,在某个下午突然被打破了,某个同事在测试一个新的定时任务时,在开发环境触发了这个问题,这简直就是溺水之人的救命稻草。

根据同事的任务逻辑,我们在尝试多次执行,并能够稳定复现问题之后,将可疑点缩小到了某个特定的接口。后面,我们通过race detector加代码走查的方式,很快定位到了有问题的代码:一个记录代码耗时日志的方法中并发写了一个公共数组

将这段代码修复上线后观察了1天,之前的问题没有再出现过,至此,inconsistent mutex state的问题终于得以解决,可以睡个好觉啦!!

写在最后

解决问题所必需的品格

回顾一下解决问题的过程可以发现,在问题出现后,未能引起小伙伴们足够的重视,以至于后面问题严重后需要投入更多的人力和时间去解决,这一点在今后的工作中要尽量避免。

另外必须提到的是,尽管这个问题很棘手(Google搜索inconsistent mutex state几乎没有好的解决方案,甚至相关的github issue还停留在一年前且仍未解决),但笔者的同事们依旧迎难而上,坚持寻找解决方案的精神让笔者受益匪浅。虽然过程比较辛苦,但是最终问题解决后的收获是足以让人欣喜的(解决问题的经验,go mutex源代码的理解等等)。

也许这就是咱互联网工作者所必需具备的品格吧:)

在这里插入图片描述

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

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

相关文章

MyBatis的相应API与传统和代理开发的Dao层实现

MyBatis的相应API 1、SqlSession工厂构建器SqlSessionFactoryBuilder 常用API&#xff1a;SqlSessionFactory build(InputStream inputStream) 通过加载mybatis的核心文件的输入流的形式构建一个SqlSessionFactory对象 其中&#xff0c;mybatis-config.xml是类加载器的路径&…

为什么要学编程?为什么是Python?怎么学?

今天我们聊了为什么要去学编程&#xff0c;学编程为什么要选择Python。上大学的时候我学的信息管理&#xff0c;毕业后选择了和网络相关的网络安全公司做售后。 从上学的时候自己就想过去学学编程&#xff0c;感觉他们敲代码都敲的那么酷&#xff0c;看着满屏的代码还有命令行…

2022广航蓝桥杯选拔赛压轴题:取数博弈游戏

原题链接 对于这种「判断先手后手的必胜必败」的题目&#xff0c;博弈论方向是一个优先考虑的方向。 博弈论的重要思想就是决策者都要做出全局最优的决策而非局部最优&#xff0c;就好比专业的棋手走一步看五步&#xff0c;而博弈论则要求棋手走一步看无限步。 解题思路 凡…

[洛谷]P3613 【深基15.例2】寄包柜

[洛谷]P3613 【深基15.例2】寄包柜一、问题描述题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1提示二、思路分析1、算法标签2、算法思路三、代码实现一、问题描述 [洛谷]P3613 【深基15.例2】寄包柜 题目描述 超市里有 n(1≤n≤105)n(1\le n\le10^5)n(1≤n≤105) 个…

《Quarkus实战》总结

《Quarkus实战》总结 目录 一、优势二、搭建脚手架三、Rest 1)启用跨源资源共享2)拦截HTTP请求3)使用SSL进行安全连接 四、配置 1)以程序化的方式访问配置属性2)在外部覆盖配置值3)修改日志配置 五、编程模型 1)校验输入值和输出值2)全局异常处理3)创建自定义校验4)以程序化的方…

荣耀加冕 大道可期 | WotoKOL卧兔荣获星球奖BrandStar Awards 2022服务创新奖

2022年11月&#xff0c;星球奖BrandStar Awards 2022获奖名单揭晓&#xff0c;WotoKOL卧兔荣获服务创新奖 &#xff01; 据了解&#xff0c;星球奖BrandStar Awards 是品牌星球于 2021 年发起的&#xff0c;以「品牌创新」为核心指标的奖项。星球奖系统囊括品牌建设的核心板块&…

【C语言】你知道程序是如何调用函数的吗?

目录 1.函数栈帧的含义 概念 要用到的汇编语言的知识 示例 2.理解栈帧 2.1 main函数栈帧的创建 2.2 局部变量的创建 2.3 函数传参 2.4 调用函数 2.5 函数返回 一个.c文件在调用函数的时候&#xff08;包括main 函数&#xff09;&#xff0c;其内存中的栈区有什么变…

祝贺誉天多位学员通过HCIE-Datacom,喜提誉天助跑奖学金

华为HCIE-Datacom自发布以来&#xff0c;就一直牵动着数通学员们的心。不论是版本之间的取舍&#xff0c;还是新增模块的学习&#xff0c;都一度引发热议。为了帮助各位小伙伴完成过渡顺利通过考试&#xff0c;誉天除了在师资、设备、教研等方面下足功夫之外&#xff0c;还专门…

12. 【gRPC系列学习】失败重试Retry原理

本节分析gRPC的失败重试机制,从原理、源码上进行深入分析,下节编写测试代码,验证Retry机制。 官方文档在失败重试方面的讲解比较透彻,下面内容是对官方文档的整理、归类、总结。 1. 两种重试策略简介 1)retry策略 retry策略:发送RPC,服务端应用层返回指定的异常码后,…

ES6--》了解并使用模块化规范

目录 ES6模块化规范 模块化的分类 ES6模块化的语法 ES6模块化规范 ES6模块化规范是浏览器端与服务器端通用的模块化开发规范。它的出现极大的降低了前端开发者的模块化学习成本&#xff0c;开发者不需要再额外的学习AMD、CMD或CommonJS等模块化规范。 ES6模块化规范定义&am…

激活海量数据价值,实现生产过程优化

背景 在全球掀起的新一轮工业转型浪潮中&#xff0c;智能制造面临巨大发展机遇。得益于云计算、大数据和人工智能技术的加持&#xff0c;工业转型升级进入新阶段&#xff0c;人们逐渐意识到由数据驱动催生的新商业模式所带来的巨大价值&#xff0c;数据和算法模型的结合与碰撞…

KMP算法,686. 重复叠加字符串匹配,

首先&#xff0c;要明白&#xff0c;如果b串的第一个字母b[0]在a串中没有找到&#xff0c;那么不管a串复制多少次&#xff0c;b串都不会是a的子串。 如果b串的第一个字母b[0]在a串中能找到&#xff0c;那么我们看一下a串需要复制几次呢&#xff1f; 总结,可以发现这四种情况是…

数据库系统概论第六章(关系数据理论)知识点总结(3)—— 范式知识点总结

本专栏收录了数据库的知识点&#xff0c;而从本文起&#xff0c;将讲述有关于关系数据理论中的第一范式、第二范式、第三范式以及BC范式有关知识点&#xff0c;提供给有需要的小伙伴进行学习&#xff0c;本专栏地址可以戳下面链接查看 &#x1f388; 数据库知识点总结&#xff…

windows CMD “ZEBAR ZPL 命令打印“驱动打印机

一、命令预览 copy WS20_8891898136BB520221221003.txt "\\10.165.98.33\IP-print-ZDesigner ZT411-203dpi ZPL" 二、难点打印机名称 1. copy 命令必须为共享打印机&#xff0c;及打印机共享名称&#xff0c;可以参考(163条消息) 发送ZPL指令到斑马打印机&#xff…

【Java对象拷贝机制】「实战开发专题」高性能使用CGlib实现Bean拷贝(BeanCopier)指南

对象拷贝现状 业务系统中经常需要两个对象进行属性的拷贝&#xff0c;不能否认逐个的对象拷贝是最快速最安全的做法&#xff0c;但是当数据对象的属性字段数量超过程序员的容忍的程度&#xff0c;代码因此变得臃肿不堪&#xff0c;使用一些方便的对象拷贝工具类将是很好的选择…

训练seq2seq模型的一些Tips——李宏毅机器学习笔记

Copy Mechanism 有时我们并不需要decoder创造一些东西出来&#xff0c;有些内容是可以从encoder复制而来。 最早具有复制能力的模型&#xff1a;Pointer Network 例如&#xff1a; chat-bot Summarization 至少要训练百万篇文章 Guided Attention Monotonic Attention Lo…

PPT怎么转化成PDF?这两种方法教你快速解决!

今天我们将向您介绍如何将 PowerPoint 演示文稿 (PPT) 转换为 PDF 文件。 我们将通过两种方法来做到这一点&#xff1a; 将 PowerPoint 演示文稿另存为 PDF 文件。使用 奇客免费PDF转换 将 PowerPoint 演示文稿转换为 PDF。方法一&#xff1a;使用 Microsoft PowerPoint 将 P…

合理利用chatGpt之新冠阳性

&#x1f3c6;今日学习目标&#xff1a; &#x1f340;合理利用chatGpt之新冠阳性 ✅创作者&#xff1a;林在闪闪发光 ⏰预计时间&#xff1a;30分钟 &#x1f389;个人主页&#xff1a;林在闪闪发光的个人主页 &#x1f341;林在闪闪发光的个人社区&#xff0c;欢迎你的加入:…

AI 预测到了正确的世界杯胜利者吗 ?

在过去的一个月里&#xff0c;卡塔尔 2022 年世界杯让我们目睹了一个又一个令人震惊的结果&#xff0c;甚至在决赛之前&#xff0c;让所有人的目光都聚焦在法国和阿根廷两支球队上&#xff0c;其实在球迷们心中&#xff0c;早就开始了各种猜测。&#x1f604;现在是时候回顾一下…

基于CNN和LSTM的气象图降水预测示例

我们是否可以通过气象图来预测降水量呢&#xff1f;今天我们来使用CNN和LSTM进行一个有趣的实验。 我们这里使用荷兰皇家气象研究所(也称为KNMI)提供的开放数据集和公共api&#xff0c;来获取数据集并且构建模型预测当地的降水量。 数据收集 KNMI提供的数据集&#xff0c;我…