成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

一個全新的 Go pprof 視角 - 對象引用分析

開發
為了更高效地分析和解決這些問題,CloudWeGo 團隊開發了一款新的工具——Goref。Goref 基于 Delve,能夠深入分析Go程序的堆對象引用,顯示內存引用的分布,幫助開發者快速定位內存泄漏或優化GC開銷。

在Go語言開發中,內存泄漏問題往往難以定位,傳統的Pprof工具雖然能提供一定幫助,但在復雜場景下其能力有限。為了更高效地分析和解決這些問題,CloudWeGo 團隊開發了一款新的工具——Goref。Goref 基于 Delve,能夠深入分析Go程序的堆對象引用,顯示內存引用的分布,幫助開發者快速定位內存泄漏或優化GC開銷。該工具不僅支持運行時進程的分析,還能分析核心轉儲文件,為 Go 開發者提供了一個強大的內存分析工具。項目已開源在 GitHub 上,歡迎社區貢獻和使用。

Pprof的局限性

作為 Go 研發有時會遇到內存泄露的情況,大部分人第一時間會嘗試打一個 heap profile 看問題原因。但很多時候,heap profile 火焰圖對問題排查起不到什么幫助,因為它只記錄了對象是在哪創建的。在一些復雜業務場景下,對象經過多層依賴傳遞或者內存池復用,幾乎已經無法根據創建的堆棧信息定位根因。

以如下 heap profile 為例,FastRead 函數棧是 Kitex 框架反序列化函數,如果業務協程泄露了請求對象,實際上并不能反映到對應泄露的代碼位置,而只能體現在 FastRead 函數棧占據了內存。

圖片

眾所周知, Go 是帶 GC 的語言,一個對象無法釋放,幾乎 100% 是由于 GC 通過引用分析將其標記為存活。而同樣作為 GC 語言,Java 的分析工具就更加完善了,比如 JProfiler 可以有效地展示對象引用關系。因此,我們也想在 Go 上實現一個高效的引用分析工具,能夠準確直接地告訴我們內存引用分布和引用關系,幫我們從艱難的靜態分析中解放出來。好消息是,我們已基本完成了這個工具的開發工作,已開源在 https://github.com/cloudwego/goref 倉庫下,使用方式見 README 文檔。

以下將分享這個工具的設計思路和詳細實現。

思路

GC 標記過程

在講具體實現之前,我們先回顧一下 GC 是怎么標記對象的存活的。

Go 采用類似于 tcmalloc 的分級分配方案,每個堆對象在分配時會指定到一個mspan上,它的 size 是固定的。在 GC 時,一個堆地址會調用runtime.spanOf從多級索引中查找到這個mspan,從而得到原始對象的 base address 和 size。

// simplified code
func spanOf(p uintptr) *mspan {
    ri := arenaIndex(p)
    ha := mheap_.arenas[ri.l1()][ri.l2()]
    return ha.spans[(p/pageSize)%pagesPerArena]
}

通過 runtime.heapBitsForAddr 函數可以獲得一個對象地址范圍內的 GC bitmap。而 GC bitmap 中標記了一個對象所在內存的每 8 字節對齊的地址是否是一個指針類型,從而判斷是否進一步標記下游對象。

例如以下 Go 代碼片段:

type Object struct {
    A string
    B int64
    C *[]byte
}
// global variables
var a = echo()
var b *int64 = &echo().B
func echo() *Object {
    bytes := make([]byte, 1024)
    return &Object{A: string(bytes), C: &bytes}
}

GC 在掃描變量b時,不是只簡單地掃描B int64這個字段的內存,而是通過mspan索引查找出baseelem size后再進行掃描,因此,字段 A 和 C 以及它們的下游對象的內存都會被標記為存活。

GC 掃描變量a變量時,發現對應的 GC bit 是1001,怎么理解呢?可以認為是base+0base+24的地址是指針,要繼續掃描下游對象,這里A stringC *[]byte都包含了一個指向下游對象的指針。

圖片

基于以上的簡要分析,我們可以發現,要找到所有存活的對象,簡單的原理就是從 GC Root 出發,挨個掃描對象的 GC bit,如果某個地址被標記為1,就繼續向下掃描,每個下游地址都要確定它的 mspan,從而獲取完整的對象基地址、大小和 GC bit。

DWARF 類型信息

然而,光知道對象的引用關系對于問題排查幾乎沒有任何幫助。因為它不能輸出任何有效的可供研發定位問題的變量名稱。所以,還有一個很關鍵的步驟是,獲取到這些對象的變量名和類型信息。

Go 本身是靜態語言,對象一般不直接包含其類型信息,比如我們通過obj=new(Object)調用創建一個對象,實際內存只存儲了A/B/C三個字段的值,在內存中只有 32 字節大小。既然如此,有什么辦法能拿到類型信息呢?

Goref 的實現

Delve 工具介紹

有過 Go 開發經歷的同學應該都用過 Delve,如果你覺得自己沒用過,不要懷疑,你在 Goland IDE 上玩的代碼調試功能,底層就是基于 Delve 的。說到這里,相信大家已經回憶起 Debug 時調試窗口的畫面了,沒錯,調試窗口所展示的變量名,變量值,變量類型這些信息,不正是我們需要的類型信息嗎!

$ ./dlv attach 270
(dlv) ...
(dlv) locals
tccCli = ("*code.byted.org/gopkg/tccclient.ClientV2")(0xc000782240)
ticker = (*time.Ticker)(0xc001086be0)

那么,Delve 是怎么獲取這些變量信息的呢?在我們 attach 進程時,Delve 從/proc/<pid>/exe讀取軟鏈接到實際 elf 文件路徑的可執行文件。Go 編譯時會生成一些調試信息,以 DWARF 標準格式存儲在可執行文件的 .debug_* 前綴的 section 節里。引用分析所需要的全局變量和局部變量的類型信息就可以通過這些 DWARF 信息解析得到。

對于全局變量:Delve 迭代讀取所有 DWARF Entry ,解析出帶Variable標簽的全局變量的 DWARF Entry。這些 Entry 包含了 Location、Type、Name 等屬性。

  1. 1. 其中,Type 屬性記錄了它的類型信息,按 DWARF 格式遞歸遍歷,可以進一步確定變量的每一個子對象類型;
  2. 2. Location 則是一個相對復雜的屬性,它記錄了一個可執行的表達式或者一個簡單的變量地址,作用是確定一個變量的內存地址,或者返回寄存器的值。在全局變量解析時,Delve 通過它獲得了變量的內存地址。

Goroutine 中的局部變量解析的原理與全局變量大同小異,不過還是要更復雜一些。比如需要根據 PC 確定 DWARF offset,同時 location 表達式也會更復雜,還涉及到寄存器訪問。這里不再展開。

GC 分析的元信息構建

通過 Delve 提供的進程 attach 和 core 文件分析功能,我們還可以獲取到內存訪問權限。我們仿照 GC 標記對象的做法,在工具的運行時內存中構建待分析進程的必要元信息。這包括:

  1. 1. 待分析進程的各個 Goroutine stack 的地址空間范圍,并包括每個 Goroutine stack 存儲 gcmask 的 stackmap,用來標記是否可能指向一個存活的堆對象;
  2. 2. 待分析進程的各個 data/bss segment 的地址空間范圍,包括每個 segment 的 gcmask,也是用來標記是否可能指向一個存活的堆對象;
  3. 3. 以上兩步都是獲取 GC Roots 的必要信息;
  4. 4. 最后一步是讀取待分析進程的 mspan 索引,以及每個 mspan 的 base、elem size、gcmask等信息,在工具的內存中復原這個索引;

以上步驟是大概的流程,其中還有一些細節問題的處理,例如對 GC finalizer 對象的處理,以及對 Go 1.22 版本 allocation header 特性的特殊處理,這里不再展開。

DWARF 類型掃描

萬事俱備,只欠東風。不管是堆掃描的 GC 元信息,還是 GC Root 變量的類型信息都已經完成解析。那么所謂的“東風”就是最關鍵的對象引用關系分析環節了。

對于每個 GC Root 變量,我們調用findRef函數,按不同的 DWARF 類型訪問對象的內存,假設是一個可能指向下游對象的指針,則讀取指針的值,在 GC 元信息里找到這個下游對象。這時,按前所述,我們得到了對象的 base address、elem size、gcmask 等信息。

如果對象被訪問到,記錄一個 mark bit 位,以避免對象被重復訪問。通過 DWARF 子對象類型構造一個新的變量,再次遞歸調用findRef直至所有已知類型的對象被全部確認。

然而,這種引用掃描方式和 GC 的做法是完全相悖的。主要原因在于,Go 里面有大量不安全的類型轉換,可能某個對象在創建后是帶了指針字段的對象,比如:

func echo() *byte {
    bytes := make([]byte, 1024)
    obj := &Object{A: string(bytes), C: &bytes}
    return (*byte)(unsafe.Pointer(obj))
}

從 GC 的角度出發,雖然 unsafe 轉換了類型為*byte,但并沒有影響其 gcmask 的標記,所以在掃描下游對象時,仍然能掃描到完整的Object對象,識別到bytes這個下游對象,從而將其標記為存活。

但 DWARF 類型掃描可做不到,在掃描到 byte 類型時,會被認為是無指針的對象,直接跳過進一步的掃描了。所以,唯一的辦法是,優先以 DWARF 類型掃描,對于無法掃到的對象,再用 GC 的方式來標記。

要實現這一點,做法是每當我們用 DWARF 類型訪問一個對象的指針時,都將其對應的 gcmask 從 1 標記為 0,這樣在掃描完一個對象后,如果對象的地址空間范圍內仍然有非 0 標記的指針,就把它記錄到最終標記的任務里。等到所有對象通過 DWARF 類型掃描完成后,再把這些最終標記任務取出來,以 GC 的做法二次掃描。

圖片

例如,上述 Object 對象訪問時,其 gcmask 是1001,讀取字段 A 后,gcmask 變成 1000,如果字段 C 因為類型強轉沒有訪問到,則在最終掃描的 GC 標記時就會被統計到。

除了類型強轉外,引用內存越界問題也很常見,如上文示例代碼var b *int64 = &echo().B所示,字段 A 和 C 都屬于無法被 DWARF 類型掃描的內存,也會在最終掃描時被統計。

最終掃描

上述的被類型強轉的字段,或者因為超過了 DWARF 定義的地址范圍而無法訪問到的字段,又或者像 unsafe.Pointer 這種無法確定類型的變量,都會在最終掃描時被標記。因為這些對象沒法確定具體的類型,所以不需要專門輸出,只需要把 size 和 count 記錄到已知的引用鏈路中即可。

在 Go 原生實現中,有不少常用庫都采用了unsafe.Pointer,導致子對象識別出現問題,這類類型要做特殊處理。

輸出文件格式

所有對象掃描完畢后,將引用鏈路及其對象數、對象內存空間輸出到文件,文件對齊 pprof 二進制文件格式,采用 protobuf 編碼。


  1. 1. 輸出的根對象格式:
  • ? 棧變量格式:包名 + 函數名 + 棧變量名 github.com/cloudwego/kitex/client.invokeHandleEndpoint.func1.sendMsg
  • ? 全局變量格式:包名 + 全局變量名 github.com/cloudwego/kitex/pkg/loadbalance/lbcache.balancerFactories

  1. 2. 輸出的子對象格式:
  • ? 輸出子對象的字段名和類型名,形如:Conn.(net.Conn)
  • ? 如果是 map key 或 value 字段,則以 $mapkey. (type_name) 或 $mapval. (type_name) 的形式輸出;
  • ? 如果是數組的元素,以 [0]. (type_name) 格式輸出,大于等于 10 的以 [10+]. (type_name) 格式輸出;

效果展示

以下是一個真實業務用工具采樣后的對象引用火焰圖:

圖片

圖中展示了每個 root 變量的名稱,以及其引用的字段名和類型名。注:由于 Go1.23 之前 DWARF Info 沒有支持閉包類型的字段 offset,所以閉包變量wpool.(*Pool).GoCtx.func1.task暫時無法展示下游對象。

選擇 inuse_objects 標簽,還可以查看對象數分布火焰圖:

圖片

項目地址:https://github.com/cloudwego/goref

責任編輯:龐桂玉 來源: 字節跳動技術團隊
相關推薦

2021-03-16 08:56:35

Go interface面試

2015-08-19 09:29:35

Git協議編寫

2013-01-25 09:53:40

GitHub

2022-03-21 08:49:01

存儲引擎LotusDB

2024-01-25 11:41:00

Python開發前端

2015-10-12 15:50:07

PaaS云平臺開發go

2011-07-22 17:00:14

java

2020-06-02 10:04:58

IT部門首席信息官CIO

2021-05-30 07:59:00

String引用類型

2023-02-17 15:03:30

人工智能DevOps團隊

2010-03-31 17:21:04

云計算

2015-12-02 11:23:38

DockerUber容器服務

2021-04-19 14:18:17

數據分析互聯網運營大數據

2024-03-01 18:55:54

內存調試Go 語言

2023-02-26 01:37:57

goORM代碼

2023-05-10 08:05:41

GoWeb應用

2022-06-15 08:14:40

Go線程遞歸

2014-10-15 11:01:02

Web應用測試應用

2024-05-27 00:00:20

2021-04-25 08:58:00

Go拍照云盤
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 成人在线免费网站 | 久久精品国产一区二区电影 | 成人精品啪啪欧美成 | 在线成人免费视频 | 毛片a区 | 精品国产一区二区三区久久影院 | 三级黄色片在线播放 | 久久国产精品99久久久大便 | 国产91中文 | 中文字幕精品视频在线观看 | 成人a视频片观看免费 | 中文字幕一区二区三区在线观看 | 国产精品一区视频 | 国产黄色大片 | 99热精品在线 | 国产精品伦一区二区三级视频 | 在线播放国产视频 | 青青激情网 | 欧美综合久久久 | 国产婷婷色综合av蜜臀av | 精品美女视频在线观看免费软件 | 亚洲精品久久久 | 欧美成人激情 | 尤物视频在线免费观看 | 黄色毛片网站在线观看 | 国产视频二区在线观看 | 极品一区| 成人在线小视频 | www.99热.com | 国产精品福利视频 | 欧美成年人视频在线观看 | 久久99精品久久久久蜜桃tv | 亚洲精品乱码久久久久v最新版 | 日韩成人在线电影 | 亚洲国产精品久久久久 | 成人午夜免费在线视频 | 久久婷婷av | 男女激情网站免费 | 精品欧美一区二区在线观看欧美熟 | 精品国产乱码久久久久久图片 | 色婷婷精品久久二区二区蜜臂av |