1 行命令引发的 Go 应用崩溃

1 行命令引发的Go应用崩溃

一、前言

不久前,阿里云 ARMS 团队、编译器团队、MSE 团队携手合作,共同发布并开源了 Go 语言的编译时自动插桩技术。该技术以其零侵入的特性,为 Go 应用提供了与 Java 监控能力相媲美的解决方案。开发者只需将 go build 替换为新编译命令 otel go build,就能实现对 Go 应用的全面监控和治理。

二、问题描述

近期,我们收到用户反馈,使用 otel go build -race 替代正常的 go build -race 命令后,编译生成的程序会导致崩溃。-race[3]是 Go 编译器的一个参数,用于检测数据竞争(data race)问题。通过为每个变量的访问添加额外检查,确保多个 goroutine 不会以不安全方式同时访问这些变量。

理论上,我们的工具不应影响-race 竞态检查的代码,因此出现崩溃的现象是非预期的,所以我们花了一些时间排查这个崩溃问题,崩溃的堆栈信息如下:

(gdb) bt#0  0x000000000041e1c0 in __tsan_func_enter ()#1  0x00000000004ad05a in racecall ()#2  0x0000000000000001 in ?? ()#3  0x00000000004acf99 in racefuncenter ()#4  0x00000000004ae7f1 in runtime.racefuncenter (callpc=4317632)#5  0x0000000000a247d8 in ../sdk/trace.(*traceContext).TakeSnapShot (tc=<optimized out>, ~r0=...)#6  0x00000000004a2c25 in runtime.contextPropagate#7  0x0000000000480185 in runtime.newproc1.func1 () #8  0x00000000004800e2 in runtime.newproc1 (fn=0xc00030a1f0, callergp=0xc0000061e0, callerpc=12379404, retVal0=0xc0002c8f00)#9  0x000000000047fc3f in runtime.newproc.func1 () #10 0x00000000004a992a in runtime.systemstack ()....

可以看到崩溃源于 __tsan_func_enter,而引发该问题的关键点是 runtime.contextPropagate。我们的工具在 runtime.newproc1 函数的开头插入了以下代码:

func newproc1(fn *funcval, callergp *g, callerpc uintptr) (retVal0 *g) {    // 我们插入的代码    retVal0.otel_trace_context = contextPropagate(callergp.otel_trace_context)
    ...}
// 我们插入的代码func contextPropagate(tls interface{}) interface{} {  if tls == nil {    return nil  }  if taker, ok := tls.(ContextSnapshoter); ok {    return taker.TakeSnapShot()  }  return tls}
// 我们插入的代码func (tc *traceContext) TakeSnapShot() interface{} {  ...}

TakeSnapShot 被 Go 编译器在函数入口和出口分别注入了 racefuncenter() 和 racefuncexit(),最终调用 __tsan_func_enter 导致崩溃。由此确定崩溃问题确实是我们的注入代码导致的,继续深入排查。

三、排查过程

3.1 崩溃根源

使用 objdump 查看 __tsan_func_enter 的源码,看到它接收两个函数参数,出错的地方是第一行 mov 0x10(%rdi),%rdx,它约等于 rdx = *(rdi + 0x10)。打印寄存器后发现 rdi = 0,根据调用约定,rdi 存放的是第一个函数参数,因此这里的问题就是函数第一个参数 thr 为 0。

// void __tsan_func_enter(ThreadState *thr, void *pc);000000000041e1c0 <__tsan_func_enter>:  41e1c0:  48 8b 57 10            mov    0x10(%rdi),%rdx  41e1c4:  48 8d 42 08            lea    0x8(%rdx),%rax  41e1c8:  a9 f0 0f 00 00         test   $0xff0,%eax  ...

那么第一个参数 thr 是谁传进来的呢?接着往上分析调用链。

3.2 调用链分析

出错的整个调用链是 racefuncenter(Go) -> racecall(Go) -> __tsan_func_enter(C)。需要注意的是,前两个函数都是 Go 代码,Go 函数调用 Go 函数遵循 Go 的调用约定。在 amd64 平台,前九个函数参数使用以下寄存器:

另外以下寄存器用于特殊用途:

后两个函数一个 Go 代码一个 C 代码,Go 调用 C 的情况下,遵循 System V AMD64 调用约定,在 Linux 平台上使用以下寄存器作为前六个参数:

理解了 Go 和 C 的调用约定之后,再来看整个调用链的代码:

TEXT  racefuncenter<>(SB), NOSPLIT|NOFRAME, $0-0  MOVQ  DX, BXx  MOVQ  g_racectx(R14), RARG0     // RSI存放thr  MOVQ  R11, RARG1                 // RDI存放pc  MOVQ  $__tsan_func_enter(SB), AX // AX存放__tsan_func_enter函数指针  CALL  racecall<>(SB)  MOVQ  BX, DX  RETTEXT  racecall<>(SB), NOSPLIT|NOFRAME, $0-0  ...  CALL  AX  // 调用__tsan_func_enter函数指针  ...

racefuncenter 将 g_racectx(R14) 和 R11 分别放入 C 调用约定的参数寄存器 RSI(RARG0) 和 RDI(RARG1),并将 __tsan_func_enter 放入 Go 调用约定的参数寄存器 RAX,然后调用 racecall,它进一步调用 __tsan_func_enter(RAX),这一系列操作大致相当于 __tsan_func_enter(g_racectx(R14), R11)。

不难看出,问题的根源在于 g_racectx(R14) 为 0。根据 Go 的调用约定 R14 存放当前 goroutine ,它不可能为 0 ,因此出问题的必然是 R14.racectx 字段为 0。为了避免无效努力,通过调试器 dlv 二次确认:

(dlv) p *(*runtime.g)(R14)runtime.g {        racectx: 0,        ...}

那么为什么当前 R14.racectx 为 0?下一步看看 R14 具体的状态。

3.3 协调程度

func newproc(fn *funcval) {  gp := getg()  pc := sys.GetCallerPC() #1  systemstack(func() {    newg := newproc1(fn, gp, pc, false, waitReasonZero) #2    ...  })}

经过排查,在代码 #1 处,R14.racectx 是正常的,但到了代码 #2 处,R14.racectx 就为空了,原因是 systemstack 被调用,它有一个切换协程的动作,具体如下:

// func systemstack(fn func())TEXT runtime·systemstack(SB), NOSPLIT, $0-8  ...  // 切换到g0协程  MOVQ  DX, g(CX)  MOVQ  DX, R14 // 设置 R14 寄存器  MOVQ  (g_sched+gobuf_sp)(DX), SP
  // 在g0协程上运行目标函数fn  MOVQ  DI, DX  MOVQ  0(DI), DI  CALL  DI
  // 切换回原始协程    ...

原来 systemstack 有一个切换协程的动作,会先把当前协程切换成 g0,然后执行 fn,最后恢复原始协程执行。

在 Go 语言的 GMP(Goroutine-Machine-Processor)调度模型中,每个系统级线程 M 都拥有一个特殊的 g0 协程,以及若干用于执行用户任务的普通协程 g。g0 协程主要负责当前 M 上用户 g 的调度工作。由于协程调度是不可抢占的,调度过程中会临时切换到系统栈(system stack)上执行代码。在系统栈上运行的代码是隐式不可抢占的,并且垃圾回收器不会扫描系统栈。

到这里我们已经知道执行 newproc1 时的协程总是 g0,而 g0.racectx 是在 main 执行开始时被主动设置为 0,最终导致程序崩溃:

// src/runtime/proc.go#main// The main goroutine.func main() {  mp := getg().m
  // g0 的 racectx 仅用于作为主 goroutine 的父级。    // 不应将其用作其他目的。  mp.g0.racectx = 0  ...

四、解决方案

到这里基本上可以做一个总结了,程序崩溃的原因如下:

  • newproc1 中插入的 contextPropagate 调用 TakeSnapshot,而 TakeSnapshot 被 go build -race 强行在函数开始插入了 racefuncenter() 函数调用,该函数将使用 racectx。

  • newproc1 是在 g0 协程执行下运行,该协程的 racectx 字段是 0,最终导致崩溃。

一个解决办法是给 TakeSnapshot 加上 Go 编译器的特殊指令 //go:norace,该指令需紧跟在函数声明后面,用于指定该函数的内存访问将被竞态检测器忽略,Go 编译器将不会强行插入 racefuncenter()调用。

五、疑惑一

runtime.newproc1 中不只调用了我们注入的 contextPropagate,还有其他函数调用,为什么这些函数没有被编译器插入 race 检查的代码(如 racefuncenter)?

经过排查后发现,Go 编译器会特殊处理 runtime 包,针对 runtime 包中的代码设置 NoInstrument 标志,从而跳过生成 race 检查的代码:

// /src/cmd/internal/objabi/pkgspecial.govar pkgSpecialsOnce = sync.OnceValue(func() map[string]PkgSpecial {    ...    for _, pkg := range runtimePkgs {        set(pkg, func(ps *PkgSpecial) {             ps.Runtime = true            ps.NoInstrument = true        })    }    ...})

六、疑惑二

理论上插入 //go:norace 之后问题应该得到解决,但实际上程序还是发生了崩溃。经过排查发现,TakeSnapShot 中有 map 初始化和 map 循环操作,这些操作会被编译器展开成 mapinititer() 等函数调用。这些函数直接手动启用了竞态检测器,而且无法加上 //go:norace:

func mapiterinit(t *abi.SwissMapType, m *maps.Map, it *maps.Iter) {  if raceenabled && m != nil {        // 主动的race检查    callerpc := sys.GetCallerPC()    racereadpc(unsafe.Pointer(m), callerpc, abi.FuncPCABIInternal(mapiterinit))  }    ...}

对此问题的解决办法是在 newproc1 注入的代码里面,避免使用 map 数据结构。

七、总结

以上就是 Go 自动插桩工具在使用 go build -race 时出现崩溃的分析全过程。通过对崩溃内容和调用链的排查,我们找到了产生问题的根本原因以及相应的解决方案。这将有助于我们在理解运行时机制的基础上,更加谨慎地编写注入到运行时的代码。

参考链接

[01] Go 自动插桩开源项目

https://github.com/alibaba/opentelemetry-go-auto-instrumentation

[02] 阿里云 ARMS Go Agent 商业版

https://help.aliyun.com/zh/arms/tracing-analysis/monitor-go-applications/

[03] Go 竞态检查

https://go.dev/doc/articles/race_detector

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

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

相关文章

R语言的并发编程

R语言的并发编程 引言 在现代计算中&#xff0c;如何有效地利用计算资源进行数据处理和分析已成为一个重要的研究方向。尤其在大数据时代&#xff0c;数据量的急剧增加让单线程处理方式显得力不从心。为了解决这一问题&#xff0c;各种编程语言都开展了并发编程的研究和应用。…

Flink(十):DataStream API (七) 状态

1. 状态的定义 在 Apache Flink 中&#xff0c;状态&#xff08;State&#xff09; 是指在数据流处理过程中需要持久化和追踪的中间数据&#xff0c;它允许 Flink 在处理事件时保持上下文信息&#xff0c;从而支持复杂的流式计算任务&#xff0c;如聚合、窗口计算、联接等。状…

C#项目生成时提示缺少引用

问题描述 刚从git或svn拉取下来的C#项目&#xff0c;在VS生成时提示缺少引用 解决方案 1、从“管理NuGet程序包”中下载并安装缺少的引用&#xff0c;如果引用较多逐个下载安装会比较麻烦&#xff0c;建议采用下面第2种方案处理 2、通过命令对所有缺少引用进行安装 &#…

EAMM: 通过基于音频的情感感知运动模型实现的一次性情感对话人脸合成

EAMM: 通过基于音频的情感感知运动模型实现的一次性情感对话人脸合成 1所有的材料都可以在EAMM: One-Shot Emotional Talking Face via Audio-Based Emotion-Aware Motion Model网站上找到。 摘要 尽管音频驱动的对话人脸生成技术已取得显著进展&#xff0c;但现有方法要么忽…

BeanFactory 是什么?它与 ApplicationContext 有什么区别?

谈到Spring&#xff0c;那势必要讲讲容器 BeanFactory 和 ApplicationContext。 BeanFactory是什么&#xff1f; BeanFactory&#xff0c;其实就是 Spring 容器&#xff0c;用于管理和操作 Spring 容器中的 Bean。可能此时又有初学的小伙伴会问&#xff1a;Bean 是什么&#x…

【深度学习】Huber Loss详解

文章目录 1. Huber Loss 原理详解2. Pytorch 代码详解3.与 MSELoss、MAELoss 区别及各自优缺点3.1 MSELoss 均方误差损失3.2 MAELoss 平均绝对误差损失3.3 Huber Loss 4. 总结4.1 优化平滑4.2 梯度较好4.3 为什么说 MSE 是平滑的 1. Huber Loss 原理详解 Huber Loss 是一种结合…

python实现pdf转word和excel

一、引言   在办公中&#xff0c;我们经常遇收到pdf文件格式&#xff0c;因为pdf格式文件不易修改&#xff0c;当我们需要编辑这些pdf文件时&#xff0c;经常需要开通会员或收费功能才能使用编辑功能。今天&#xff0c;我要和大家分享的&#xff0c;是如何使用python编程实现…

【PyCharm】连接Jupyter Notebook

【PyCharm】相关链接 【PyCharm】连接 Git【PyCharm】连接Jupyter Notebook【PyCharm】快捷键使用【PyCharm】远程连接Linux服务器【PyCharm】设置为中文界面 【PyCharm】连接Jupyter Notebook PyCharm连接Jupyter Notebook的过程可以根据不同的需求分为 本地连接 和 远程连…

Java锁 公平锁和非公平锁 ReentrantLock() 深入源码解析

卖票问题 我们现在有五个售票员 五个线程分别卖票 卖票 ReentrantLock(); 运行后全是 a 对象获取 非公平锁缺点之一 容易出现锁饥饿 默认是使用的非公平锁 也可以传入一个 true 参数 使其变成公平锁 生活中排队讲求先来后到 视为公平 程序中的公平性也是符合请求锁的绝对…

「刘一哥GIS」系列专栏《GRASS GIS零基础入门实验教程(配套案例数据)》专栏上线了

「刘一哥GIS」系列专栏《GRASS GIS零基础入门实验教程》全新上线了&#xff0c;欢迎广大GISer朋友关注&#xff0c;一起探索GIS奥秘&#xff0c;分享GIS价值&#xff01; 本专栏以实战案例的形式&#xff0c;深入浅出地介绍了GRASS GIS的基本使用方法&#xff0c;用一个个实例讲…

企业级NoSQL数据库Redis

1.浏览器缓存过期机制 1.1 最后修改时间 last-modified 浏览器缓存机制是优化网页加载速度和减少服务器负载的重要手段。以下是关于浏览器缓存过期机制、Last-Modified 和 ETag 的详细讲解&#xff1a; 一、Last-Modified 头部 定义&#xff1a;Last-Modified 表示服务器上资源…

使用Flask和Pydantic实现参数验证

使用Flask和Pydantic实现参数验证 1 简介 Pydantic是一个用于数据验证和解析的 Python 库&#xff0c;版本2的性能有较大提升&#xff0c;很多框架使用Pydantic做数据校验。 # 官方参考文档 https://docs.pydantic.dev/latest/# Github地址 https://github.com/pydantic/pyd…

ScratchLLMStepByStep:训练自己的Tokenizer

1. 引言 分词器是每个大语言模型必不可少的组件&#xff0c;但每个大语言模型的分词器几乎都不相同。如果要训练自己的分词器&#xff0c;可以使用huggingface的tokenizers框架&#xff0c;tokenizers包含以下主要组件&#xff1a; Tokenizer: 分词器的核心组件&#xff0c;定…

C# OpenCvSharp 部署3D人脸重建3DDFA-V3

目录 说明 效果 模型信息 landmark.onnx net_recon.onnx net_recon_mbnet.onnx retinaface_resnet50.onnx 项目 代码 下载 参考 C# OpenCvSharp 部署3D人脸重建3DDFA-V3 说明 地址&#xff1a;https://github.com/wang-zidu/3DDFA-V3 3DDFA_V3 uses the geometri…

从零开始学数据库 day2 DML

从零开始学数据库&#xff1a;DML操作详解 在今天的数字化时代&#xff0c;数据库的使用已经成为了各行各业的必备技能。无论你是想开发一个简单的应用&#xff0c;还是想要管理复杂的数据&#xff0c;掌握数据库的基本操作都是至关重要的。在这篇博客中&#xff0c;我们将专注…

Java 8 Stream API

文章目录 Java 8 Stream API1. Stream2. Stream 的创建3. 常见的 Stream 操作3.1 中间操作3.2 终止操作 4. Stream 的并行操作 Java 8 Stream API Java 8 引入了 Stream API&#xff0c;使得对集合类&#xff08;如 List、Set 等&#xff09;的操作变得更加简洁和直观。Stream…

运行fastGPT 第五步 配置FastGPT和上传知识库 打造AI客服

运行fastGPT 第五步 配置FastGPT和上传知识库 打造AI客服 根据上一步的步骤&#xff0c;已经调试了ONE API的接口&#xff0c;下面&#xff0c;我们就登陆fastGPT吧 http://xxx.xxx.xxx.xxx:3000/ 这个就是你的fastGPT后台地址&#xff0c;可以在configer文件中找到。 账号是…

第4章 Kafka核心API——Kafka客户端操作

Kafka客户端操作 一. 客户端操作1. AdminClient API 一. 客户端操作 1. AdminClient API

【王树森搜索引擎技术】相关性02:评价指标(AUC、正逆序比、DCG)

相关性的评价指标 Pointwise评价指标&#xff1a;Area Under the Curve&#xff08;AUC&#xff09;Pairwise评价指标&#xff1a;正逆序比&#xff08;Positive to Negative Ratio, PNR&#xff09;Listwise评价指标&#xff1a;Discounted Cumulative Gain(DCG)用AUC和PNR作…

人物一致性训练测评数据集

1.Pulid 训练:由1.5M张从互联网收集的高质量人类图像组成,图像标题由blip2自动生成。 测试:从互联网上收集了一个多样化的肖像测试集,该数据集涵盖了多种肤色、年龄和性别,共计120张图像,我们称之为DivID-120,作为补充资源,还使用了最近开源的测试集Unsplash-50,包含…