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.pswp.cn/web/66435.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/66435.shtml
英文地址,請注明出處:http://en.pswp.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,包含…