動態追蹤技術漫談

本文最初成稿於 2016 年 5 月初,後於 2020 年 2 月中進行了較大的更新和修訂,後續會持續保持更新。

什麼是動態追蹤

我很高興能在這裏和你們分享動態追蹤技術(Dynamic Tracing)這個主題,對我我的來講也是一個很激動人心的話題。那麼,什麼是動態追蹤技術呢?php

動態追蹤技術實際上是一種後現代的高級調試技術。它能夠幫助軟件工程師以很是低的成本,在很是短的時間內,回答一些很難的關於軟件系統方面的問題,從而更快速地排查和解決問題。它興起和繁榮的一個大背景是,咱們正處在一個快速增加的互聯網時代,做爲工程師,面臨着兩大方面的挑戰:一是規模,無論是用戶規模仍是機房的規模、機器的數量都處於快速增加的時代。第二方面的挑戰就是複雜度。咱們的業務邏輯愈來愈複雜,咱們運行的軟件系統也變得愈來愈複雜,咱們知道它會分紅不少不少層次,包括操做系統內核而後上面是各類系統軟件,像數據庫和 Web 服務器,再往上有腳本語言或者其餘高級語言的虛擬機、解釋器及即時(JIT)編譯器,頂上則是應用層面的各類業務邏輯的抽象層次和不少複雜的代碼邏輯。html

這些巨大的挑戰帶來的最嚴重的後果就是,今天的軟件工程師正在迅速地喪失對整個生產系統的洞察力和掌控力。在如此複雜和龐大的系統中,各類問題發生的機率大大提升了。有的問題多是致命的,好比 500 錯誤頁,還有內存泄漏,再好比說返回錯誤結果之類。而另外一大類問題就是性能問題。咱們可能會發現軟件在某些時候運行的很是緩慢,或者在某些機器上運行得很是緩慢,但咱們並不知道爲何。如今你們都在擁抱雲計算和大數據,這種大規模的生產環境中的詭異問題只會愈來愈多,很容易佔據工程師大部分的時間和精力。大部分問題實際上是線上纔有的問題,很難復現,或者幾乎沒法復現。而有些問題出現的比率又很小,只有百分之1、千分之一,甚至更低。咱們最好可以不用摘機器下線,不用修改咱們的代碼或者配置,不用重啓服務,在系統還在運行的時候,就把問題分析出來,定位出來,進而採起有針對性的解決辦法。若是能作到這一點,那纔是完美的,才能每晚都睡上一個好覺。node

動態追蹤技術實際就能幫助咱們實現這種願景,實現這種夢想,從而極大地解放咱們工程師的生產力。我至今還記得當年在雅虎中國工做的時候,有時不得不半夜打車去公司處理線上問題。這顯然是很是無奈和挫敗的生活和工做方式。後來我曾在美國的一家 CDN 公司工做,當時咱們的客戶也會有本身的運維團隊,他們沒事就去翻 CDN 提供的原始日誌。可能對咱們來講是百分之一或者千分之一這樣的問題,可是對他們來講就是比較重要的問題,就會報告上來,咱們則就必須去排查,必須去找出真正的緣由,反饋給他們。這些實際存在的大量的現實問題,激發着新技術的發明和產生。linux

我以爲動態追蹤技術很了不得的一點就是,它是一種「活體分析」技術。就是說,咱們的某個程序或者整個軟件系統仍然在運行,仍然在線上服務,還在處理真實請求的時候,咱們就能夠去對它進行分析(無論它本身願不肯意),就像查詢一個數據庫同樣。這是很是有意思的。不少工程師容易忽略的一點是,正在運行的軟件系統自己其實就包含了絕大部分的寶貴信息,就能夠被直接看成是一個實時變化的數據庫來進行「查詢」。固然了,這種特殊的「數據庫」須是只讀的,不然咱們的分析和調試工做就有可能會影響到系統自己的行爲,就可能會危害到在線服務。咱們能夠在操做系統內核的幫助下,從外部發起一系列有針對性的查詢,獲取關於這個軟件系統自己運行過程中的許多第一手的寶貴的細節信息,從而指導咱們的問題分析和性能分析等不少工做。nginx

動態追蹤技術一般是基於操做系統內核來實現的。操做系統內核其實能夠控制整個軟件世界,由於它實際上是處於「造物主」這樣的一個地位。它擁有絕對的權限,同時它能夠確保咱們針對軟件系統發出的各類「查詢」不會影響到軟件系統自己的正常運行。換句話說,咱們的這種查詢必須是足夠安全的,是能夠在生產系統上大量使用的。把軟件系統做爲「數據庫」進行查詢就會涉及到一個查詢方式的問題,顯然咱們並非經過 SQL 這樣的方式去查詢這種特殊的「數據庫」。git

在動態追蹤裏面通常是經過探針這樣的機制來發起查詢。咱們會在軟件系統的某一個層次,或者某幾個層次上面,安置一些探針,而後咱們會本身定義這些探針所關聯的處理程序。這有點像中醫裏面的鍼灸,就是說若是咱們把軟件系統當作是一我的,咱們能夠往他的一些穴位上扎一些「針」,那麼這些針頭上面一般會有咱們本身定義的一些「傳感器」,咱們能夠自由地採集所須要的那些穴位上的關鍵信息,而後把這些信息彙總起來,產生可靠的病因診斷和可行的治療方案。這裏的追蹤一般涉及兩個緯度。一個是時間緯度,由於這個軟件還一直在運行,它便有一個在時間線上的連續的變化過程。另外一個緯度則是空間緯度,由於可能它涉及到多個不一樣的進程,包含內核進程,而每一個進程常常會有本身的內存空間、進程空間,那麼在不一樣的層次之間,以及在同一層次的內存空間裏面,我能夠同時沿縱向和橫向,獲取不少在空間上的寶貴信息。這有點兒像蛛蛛在蛛網上搜索獵物。程序員

蜘蛛在蛛網上搜索

咱們既能夠在操做系統內核裏面拿一些信息,也能夠在用戶態程序等較高的層面上採集一些信息。這些信息能夠在時間線上面關聯起來,構建出一幅完整的軟件圖景,從而有效地指導咱們作一些很複雜的分析。這裏很是關鍵的一點是,它是非侵入式的。若是把軟件系統比做一我的,那咱們顯然不想把一個活人開膛破肚,卻只是爲了幫他診斷疾病。相反,咱們會去給他拍一張 X 光,給他作一個核磁共振,給他號號脈,或者最簡單的,用聽診器聽一聽,諸如此類。針對一個生產系統的診斷,其實也應如此。動態追蹤技術容許咱們使用非侵入式的方式,不用去修改咱們的操做系統內核,不用去修改咱們的應用程序,也不用去修改咱們的業務代碼或者任何配置,就能夠快速高效地精確獲取咱們想要的信息,第一手的信息,從而幫助定位咱們正在排查的各類問題。github

我以爲大部分工程師可能特別熟悉軟件構造的過程,這實際上是咱的基本功了。咱們一般會創建不一樣的抽象層次,一層一層的把軟件構造起來,不管是自底向上,仍是自頂向下。創建軟件抽象層次的方式不少,好比經過面向對象裏面的類和方法,或者直接經過函數和子例程等方式。而調試的過程,其實與軟件構造的方式恰好相反,咱們偏偏是要可以很輕易地「打破」原先創建起來的這些抽象層次,可以爲所欲爲的拿到任意一個或者任意幾個抽象層次上的任何所需的信息,而無論什麼封裝設計,無論什麼隔離設計,無論任何軟件構造時人爲創建的條條框框。這是由於調試的時候老是但願能拿到儘量多的信息,畢竟問題可能發生在任何層面上。正則表達式

正由於動態追蹤技術通常是基於操做系統內核的,而內核是「造物主」,是絕對的權威,因此這種技術能夠垂手可得地貫通各個軟件層次的抽象和封裝,所以軟件構造時創建的抽象和封裝層次其實並不會成爲阻礙。相反,在軟件構造時創建起來的設計良好的抽象與封裝層次,其實反而有助於調試過程,關於這點,咱們後面還會專門提到。我在本身的工做當中常常會發現,有的工程師在線上出問題的時候,很是慌亂,會去胡亂猜想可能的緣由,但又缺少任何證據去支持或者否證他的猜想與假設。他甚至會在線上反覆地試錯,反覆地折騰,搞得一團亂麻,毫無頭緒,讓本身和身邊的同事都很痛苦,白白浪費了寶貴的排錯時間。當咱們有了動態追蹤技術以後,排查問題自己就可能會變成一個很是有趣的過程,讓咱們遇到線上的詭異問題就感到興奮,就彷彿好不容易又逮着機會,能夠去解一道迷人的謎題。固然了,這一切的前提是,咱們具備趁手的強大的工具,幫助咱們進行信息採集和推理,幫助咱們快速證實或否證任何假設和推測。算法

動態追蹤的優勢

動態追蹤技術通常是不須要目標應用來配合的。好比說,咱們在給一個哥們作體檢的時候,他還在操場上奔跑,咱們就能在他還在運動的過程當中,直接給他拍一張動態的 X 光,並且他本身還不知道。仔細想一下,這實際上是一件很了不得的事情。各類基於動態追蹤的分析工具的運行方式都是一種「熱插拔」的方式,就是說,咱們隨時能夠運行這個工具,隨時進行採樣,隨時結束採樣,而不用管目標系統的當前狀態。不少統計和分析,其實都是目標系統上線以後纔想到的,咱們並不可能在上線前預測將來可能會遇到哪些問題,更不可能預測咱們須要採集的全部信息,用以排查那些未知的問題。動態追蹤的好處就是,能夠實現「隨時隨地,按需採集」。另外還有一個優點是它自身的性能損耗極小。仔細寫出的調試工具對系統的極限性能的影響,一般在百分之五,甚至更低的比例如下,因此它通常不會對咱們的最終用戶產生能夠觀察到的性能影響。另外,即便是這麼小的性能損耗也只發生在咱們實際採樣的那幾十秒或者幾分鐘之內。一旦咱們的調試工具結束運行,在線系統又會自動恢復到原先百分之百的性能,繼續向前狂奔。

被檢查的小人在奔跑

DTrace 與 SystemTap

說到動態追蹤就不能不提到 DTrace。DTrace 算是現代動態追蹤技術的鼻祖了,它於 21 世紀初誕生於 Solaris 操做系統,是由原來的 Sun Microsystems 公司的工程師編寫的。可能不少同窗都據說過 Solaris 系統和 Sun 公司的大名。

最初產生的時候,我記得有這樣一個故事,當時 Solaris 操做系統的幾個工程師花了幾天幾夜去排查一個看似很是詭異的線上問題。開始他們覺得是很高級的問題,就特別賣力,結果折騰了幾天,最後發現實際上是一個很是愚蠢的、某個不起眼的地方的配置問題。自從那件事情以後,這些工程師就痛定思痛,創造了 DTrace 這樣一個很是高級的調試工具,來幫助他們在將來的工做當中避免把過多精力花費在愚蠢問題上面。畢竟大部分所謂的「詭異問題」其實都是低級問題,屬於那種「調不出來很鬱悶,調出來了更鬱悶」的類型。

應該說 DTrace 是一個很是通用的調試平臺,它提供了一種很像 C 語言的腳本語言,叫作 D。基於 DTrace 的調試工具都是使用這種語言編寫的。D 語言支持特殊的語法用以指定「探針」,這個「探針」一般有一個位置描述的信息。你能夠把它定位在某個內核函數的入口或出口,抑或是某個用戶態進程的 函數入口或出口,甚至是任意一條程序語句或機器指令上面。編寫 D 語言的調試程序是須要對系統有必定的瞭解和知識的。這些調試程序是咱們重拾對複雜系統的洞察力的利器。Sun 公司有一位工程師叫作 Brendan Gregg,他是最初的 DTrace 的用戶,甚至早於 DTrace 被開源出來。Brendan 編寫了不少能夠複用的基於 DTrace 的調試工具,一齊放在一個叫作 DTrace Toolkit 的開源項目中。Dtrace 是最先的動態追蹤框架,也是最有名的一個。

DTrace 的優點是它採起了跟操做系統內核緊密集成的一種方式。D 語言的實現實際上是一個虛擬機(VM),有點像 Java 虛擬機(JVM)。它的一個好處在於 D 語言的運行時是常駐內核的,並且很是小巧,因此每一個調試工具的啓動時間和退出時間都很短。可是我以爲 DTrace 也是有明顯缺點的。其中一個讓我很難受的缺點是 D 語言缺少循環結構,這致使許多針對目標進程中的複雜數據結構的分析工具很難編寫。雖然 DTrace 官方聲稱缺乏循環的緣由是爲了不過熱的循環,但顯然 DTrace 是能夠在 VM 級別上面有效限制每個循環的執行次數的。另一個較大的缺點是,DTrace 對於用戶態代碼的追蹤支持比較弱,沒有自動的加載用戶態調試符號的功能,須要本身在 D 語言裏面聲明用到的用戶態 C 語言結構體之類的類型。1

DTrace 的影響是很是大的,不少工程師把它移植到其餘的操做系統。比方說蘋果的 Mac OS X 操做系統上就有 DTrace 的移植。其實近些年發佈的每一臺蘋果筆記本或者臺式機上面,都有現成的 dtrace 命令行工具能夠調用,你們能夠去在蘋果機器的命令行終端上嘗試一下。這是蘋果系統上面的一個 DTrace 的移植。FreeBSD 操做系統也有這樣一個 DTrace 的移植。只不過它並非默認啓用的。你須要經過命令去加載 FreeBSD 的 DTrace 內核模塊。Oracle 也有在它本身的 Oracle Linux 操做系統發行版當中開始針對 Linux 內核進行 DTrace 移植。不過 Oracle 的移植工做好像一直沒有多少轉機,畢竟 Linux 內核並非 Oracle 控制的,而 DTrace 是須要和操做系統內核緊密集成的。出於相似的緣由,民間一些勇敢的工程師嘗試的 DTrace 的 Linux 移植也一直距離生產級別的要求很遠。

相比 Solaris 上面原生的 DTrace,這些 DTrace 移植都或多或少的缺少某些高級特性,因此從能力上來講,還不及最本初的 DTrace。

DTrace 對 Linux 操做系統的另外一個影響反映在 SystemTap 這個開源項目。這是由 Red Hat 公司的工程師建立的較爲獨立的動態追蹤框架。SystemTap 提供了本身的一種小語言,和 D 語言並不相同。顯然,Red Hat 本身服務於很是多的企業級用戶,他們的工程師天天須要處理的各類線上的「詭異問題」天然也是極多的。這種技術的產生必然是現實需求激發的。我以爲 SystemTap 是目前 Linux 世界功能最強大,同時也是最實用的動態追蹤框架。我在本身的工做當中已經成功使用多年。SystemTap 的做者 Frank Ch. Eigler 和 Josh Stone 等人,都是很是熱情、同時很是聰明的工程師。我在 IRC 或者郵件列表裏的提問,他們通常都會很是快且很是詳盡地進行解答。值得一提的是,我也曾給 SystemTap 貢獻過一個較爲重要的新特性,使其能在任意的探針上下文中訪問用戶態的全局變量的取值。我當時合併到 SystemTap 主線的這個 C++ 補丁的規模達到了約一千行,多虧了 SystemTap 做者們的熱心幫助2。這個新特性在我基於 SystemTap 實現的動態腳本語言(好比 Perl 和 Lua)的火焰圖工具中扮演了關鍵角色。

SystemTap 的優勢是它有很是成熟的用戶態調試符號的自動加載,同時也有循環這樣的語言結構能夠去編寫比較複雜的探針處理程序,能夠支持不少很複雜的分析處理。因爲 SystemTap 早些年在實現上的不成熟,致使互聯網上充斥着不少針對它的已通過時了的詬病和批評。最近幾年 SystemTap 已然有了長足的進步。我在 2017 年創辦的 OpenResty Inc. 公司也對 SystemTap 作了很是大的加強和優化。

固然,SystemTap 也是有缺點的。首先,它並非 Linux 內核的一部分,就是說它並無與內核緊密集成,因此它須要一直不停地追趕主線內核的變化。另外一個缺點是,它一般是把它的「小語言」腳本(有點像 D 語言哦)動態編譯成一個 Linux 內核模塊的 C 源碼,所以常常須要在線部署 C 編譯器工具鏈和 Linux 內核的頭文件。出於這些緣由,SystemTap 腳本的啓動相比 DTrace 要慢得多,和 JVM 的啓動時間倒有幾分相似。雖然存在這些缺點3,但總的來講,SystemTap 仍是一個很是成熟的動態追蹤框架。

SystemTap 工做原理圖

不管是 DTrace 仍是 SystemTap,其實都不支持編寫完整的調試工具,由於它們都缺乏方便的命令行交互的原語。因此咱們纔看到現實世界中許多基於它們的工具,其實最外面都有一個 Perl、Python 或者 Shell 腳本編寫的包裹。爲了便於使用一種乾淨的語言編寫完整的調試工具,我曾經給 SystemTap 語言進行了擴展,實現了一個更高層的「宏語言」,叫作 stap++4。我本身用 Perl 實現的 stap++ 解釋器能夠直接解釋執行 stap++ 源碼,並在內部調用 SystemTap 命令行工具。有興趣的朋友能夠查看我開源在 GitHub 上面的 stapxx 這個代碼倉庫。這個倉庫裏面也包含了不少直接使用個人 stap++ 宏語言實現的完整的調試工具。

SystemTap 在生產上的應用

DTrace 有今天這麼大的影響離不開著名的 DTrace 佈道士 Brendan Gregg 老師。前面咱們也提到了他的名字。他最初是在 Sun Microsystems 公司,工做在 Solaris 的文件系統優化團隊,是最先的 DTrace 用戶。他寫過好幾本有關 DTrace 和性能優化方面的書,也寫過不少動態追蹤方面的博客文章。

2011 年我離開淘寶之後,曾經在福州過了一年所謂的「田園生活」。在田園生活的最後幾個月當中,我經過 Brendan 的公開博客較爲系統地學習了 DTrace 和動態追蹤技術。其實最先據說 DTrace 是由於一位微博好友的評論,他只提到了 DTrace 這個名字。因而我便想了解一下這到底是什麼東西。誰知,不瞭解不知道,一瞭解嚇一跳。這居然是一個全新的世界,完全改變了我對整個計算世界的見解。因而我就花了很是多的時間,一篇一篇地仔細精讀 Brendan 的我的博客。後來終於有一天,我有了一種大徹大悟的感受,終於能夠融會貫通,掌握到了動態追蹤技術的精妙。

2012 年我結束了在福州的「田園生活」,來到美國加入前面提到的那家 CDN 公司。而後我就當即開始着手把 SystemTap 以及我已領悟到的動態追蹤的一整套方法,應用到這家 CDN 公司的全球網絡當中去,用於解決那些很是詭異很是奇怪的線上問題。我在這家公司觀察到其實不少工程師在排查線上問題的時候,常常會本身在軟件系統裏面埋點。這主要是在業務代碼裏,乃至於像 Nginx 這樣的系統軟件的代碼基(code base)裏,本身去作修改,添加一些計數器,或者去埋下一些記錄日誌的點。經過這種方式,大量的日誌會在線上被實時地採集起來,進入專門的數據庫,而後再進行離線分析。顯然這種作法的成本是巨大的,不只涉及業務系統自己的修改和維護成本的陡然提升,並且全量採集和存儲大量的埋點信息的在線開銷,也是很是可觀的。並且常常出現的狀況是,張三今天在業務代碼裏面埋了一個採集點,李四明天又埋下另外一個類似的點,過後可能這些點又都被遺忘在了代碼基裏面,而沒有人再去理會。最後這種點會愈來愈多,把代碼基搞得愈來愈凌亂。這種侵入式的修改,會致使相應的軟件,不管是系統軟件仍是業務代碼,變得愈來愈難以維護。

埋點的方式主要存在兩大問題,一個是「太多」的問題,一個是「太少」的問題。「太多」是指咱們每每會採集一些根本不須要的信息,只是一時貪多貪全,從而形成沒必要要的採集和存儲開銷。不少時候咱們經過採樣就能進行分析的問題,可能會習慣性的進行全網全量的採集,這種代價累積起來顯然是很是昂貴的。那「太少」的問題是指,咱們每每很難在一開始就規劃好所需的全部信息採集點,畢竟沒有人是先知,能夠預知將來須要排查的問題。因此當咱們遇到新問題時,現有的採集點蒐集到的信息幾乎老是不夠用的。這就致使頻繁地修改軟件系統,頻繁地進行上線操做,大大增長了開發工程師和運維工程師的工做量,同時增長了線上發生更大故障的風險。

另一種暴力調試的作法也是咱們某些運維工程師常常採用的,即把機器拉下線,而後設置一系列臨時的防火牆規則,以屏蔽用戶流量或者本身的監控流量,而後在生產機上各類折騰。這是很繁瑣影響很大的過程。首先它會讓機器不能再繼續服務,下降了整個在線系統的總的吞吐能力。同時有些只有真實流量才能復現的問題,此時再也沒法復現了。能夠想象這些粗暴的作法有多麼讓人頭疼。

實際上運用 SystemTap 動態追蹤技術能夠很好地解決這樣的問題,有「潤物細無聲」之妙。首先咱們不須要去修改咱們的軟件棧(software stack)自己,無論是系統軟件仍是業務軟件。我常常會編寫一些有針對性的工具,而後在一些關鍵的系統「穴位」上面放置一些通過仔細安排的探針。這些探針會採集各自的信息,同時調試工具會把這些信息彙總起來輸出到終端。用這種方式我能夠在某一臺機器或某幾臺機器上面,經過採樣的方式,很快地獲得我想要的關鍵信息,從而快速地回答一些很是基本的問題,給後續的調試工做指明方向。

正如我前面提到的,與其在生產系統裏面人工去埋點去記日誌,再蒐集日誌入庫,還不如把整個生產系統自己當作是一個能夠直接查詢的「數據庫」,咱們直接從這個「數據庫」裏安全快捷地獲得咱們想要的信息,並且毫不留痕跡,毫不去採集咱們不須要的信息。利用這種思想,我編寫了不少調試工具,絕大部分已經開源在了 GitHub 上面,不少是針對像 Nginx、LuaJIT 和操做系統內核這樣的系統軟件,也有一些是針對更高層面的像 OpenResty 這樣的 Web 框架。有興趣的朋友能夠查看 GitHub 上面的 nginx-systemtap-toolkitperl-systemtap-toolkitstappxx 這幾個代碼倉庫。

個人 SystemTap 工具雲

利用這些工具,我成功地定位了數不清的線上問題,有些問題甚至是我意外發現的。下面就隨便舉幾個例子吧。

第一個例子是,我使用基於 SystemTap 的火焰圖工具分析咱們線上的 Nginx 進程,結果發現有至關一部分 CPU 時間花費在了一條很是奇怪的代碼路徑上面。這實際上是我一位同事在好久以前調試一個老問題時遺留下來的臨時的調試代碼,有點兒像咱們前面提到的「埋點代碼」。結果它就這樣被遺忘在了線上,遺忘在了公司代碼倉庫裏,雖然當時那個問題其實早已解決。因爲這個代價高昂的「埋點代碼」一直沒有去除,因此一直都產生着較大的性能損耗,而一直都沒有人注意到。因此可謂是我意外的發現。當時我就是經過採樣的方式,讓工具自動繪製出一張火焰圖。我一看這張圖就能夠發現問題並能採起措施。這是很是很是有效的方式。

第二個例子是,不多量的請求存在延時較長的問題,即所謂的「長尾請求」。這些請求數目很低,但可能達到「秒級」這樣的延時。當時有同事亂猜說是個人 OpenResty 有 bug,我不服氣,因而當即編寫了一個 SystemTap 工具去在線進行採樣,對那些超過一秒總延時的請求進行分析。該工具會直接測試這些問題請求內部的時間分佈,包括請求處理過程當中各個典型 I/O 操做的延時以及純 CPU 計算延時。結果很快定位到是 OpenResty 在訪問 Go 編寫的 DNS 服務器時,出現延時緩慢。而後我再讓個人工具輸出這些長尾 DNS 查詢的具體內容,發現都是涉及 CNAME 展開。顯然,這與OpenResty 無關了,而進一步的排查和優化也有了明確的方向。

第三個例子是,咱們曾注意到某一個機房的機器存在比例明顯高於其餘機房的網絡超時的問題,但也只有 1% 的比例。一開始咱們很天然的去懷疑網絡協議棧方面的細節。但後來我經過一系列專門的 SystemTap 工具直接分析那些超時請求的內部細節,便定位到了是硬盤 配置方面的問題。從網絡到硬盤,這種調試是很是有趣的。第一手的數據讓咱們快速走上正確的軌道。

還有一個例子是,咱們曾經經過火焰圖在 Nginx 進程裏觀察到文件的打開和關閉操做佔用了較多的 CPU 時間,因而咱們很天然地啓用了 Nginx 自身的文件句柄緩存配置,可是優化效果並不明顯。因而再作出一張新的火焰圖,便發現由於這回輪到 Nginx 的文件句柄緩存的元數據所使用的「自旋鎖」佔用不少 CPU 時間了。這是由於咱們雖然啓用了緩存,但把緩存的大小設置得過大,因此致使元數據的自旋鎖的開銷抵消掉了緩存帶來的好處。這一切都能在火焰圖上面一目瞭然地看出來。假設咱們沒有火焰圖,而只是盲目地試驗,極可能會得出 Nginx 的文件句柄緩存沒用的錯誤結論,而不會去想到去調整緩存的參數。

最後一個例子是,咱們在某一次上線操做以後,在線上最新的火焰圖中觀察到正則表達式的編譯操做佔用了不少 CPU 時間,但其實咱們已經在線上啓用了正則編譯結果的緩存。很顯然,咱們業務系統中用到的正則表達式的數量,已然超出了咱們最初設置的緩存大小,因而很天然地想到把線上的正則緩存調的更大一些。而後,咱們在線上的火焰圖中便再看不到正則編譯操做了。

經過這些例子咱們其實能夠看到,不一樣的數據中心,不一樣的機器,乃至同一臺機器的不一樣時段,都會產生本身特有的一些新問題。咱們須要的是直接對問題自己進行分析,進行採樣,而不是胡亂去猜想去試錯。有了強大的工具,排錯實際上是一個事半功倍的事情。

自從咱們成立了 OpenResty Inc. 公司之後,咱們研發了 OpenResty XRay 這一款全新一代的動態追蹤平臺。咱們已經再也不手動去使用 SystemTap 這樣的開源解決方案了。

火焰圖

前面咱們已經屢次提到了火焰圖(Flame Graph)這種東西,那麼火焰圖是什麼呢?它實際上是一個很是了不得的可視化方法,是由前面已經反覆提到的 Brendan Gregg 同窗發明的。

火焰圖就像是給一個軟件系統拍的 X 光照片,能夠很天然地把時間和空間兩個維度上的信息融合在一張圖上,以很是直觀的形式展示出來,從而反映系統在性能方面的不少定量的統計規律。

Nginx 的 C 級別 on-CPU 火焰圖示例

比方說,最經典的火焰圖是統計某一個軟件的全部代碼路徑在 CPU 上面的時間分佈。經過這張分佈圖咱們就能夠直觀地看出哪些代碼路徑花費的 CPU 時間較多,而哪些則是可有可無的。進一步地,咱們能夠在不一樣的軟件層面上生成火焰圖,好比說能夠在系統軟件的 C/C++ 語言層面上畫出一張圖,而後再在更高的——好比說——動態腳本語言的層面,例如 Lua 和 Perl 代碼的層面,畫出火焰圖。不一樣層面的火焰圖經常會提供不一樣的視角,從而反映出不一樣層面上的代碼熱點。

由於我本身維護着 OpenResty 這樣的開源軟件的社區,咱們有本身的郵件列表,我常常會鼓勵報告問題的用戶主動提供本身繪製的火焰圖,這樣咱們就能夠愜意地看圖說話,幫助用戶快速地進行性能問題的定位,而不至於反覆地試錯,和用戶一塊兒去胡亂猜想,從而節約彼此大量的時間,皆大歡喜。

這裏值得注意的是,即便是遇到咱們並不瞭解的陌生程序,經過看火焰圖,也能夠大體推出性能問題的所在,即便從未閱讀過它的一行源碼。這是一件很是了不得的事情。由於大部分程序實際上是編寫良好的,也就是說它每每在軟件構造的時候就使用了抽象層次,好比經過函數。這些函數的名稱一般會包含語義上的信息,並在火焰圖上面直接顯示出來。經過這些函數名,咱們能夠大體推測出對應的函數,乃至對應的某一條代碼路徑,大體是作什麼事情的,從而推斷出這個程序所存在的性能問題。因此,又回到那句老話,程序代碼中的命名很是重要,不只有助於閱讀源碼,也有助於調試問題。而反過來,火焰圖也爲咱們提供了一條學習陌生的軟件系統的捷徑。畢竟重要的代碼路徑,幾乎老是花費時間較多的那些,因此值得咱們重點研究;不然的話,這個軟件的構造方式必然存在很大的問題。

火焰圖其實能夠拓展到其餘維度,好比剛纔咱們講的火焰圖是看程序運行在 CPU 上的時間在全部代碼路徑上的分佈,這是 on-CPU 時間這個維度。相似地,某一個進程不運行在任何 CPU 上的時間其實也是很是有趣的,咱們稱之爲 off-CPU 時間。off-CPU 時間通常是這個進程由於某種緣由處於休眠狀態,好比說在等待某一個系統級別的鎖,或者被一個很是繁忙的進程調度器(scheduler)強行剝奪 CPU 時間片。這些狀況都會致使這個進程沒法運行在 CPU 上,可是仍然花費不少的掛鐘時間。經過這個維度的火焰圖咱們能夠獲得另外一幅很不同的圖景。經過這個維度上的信息,咱們能夠分析系統鎖方面的開銷(好比 sem_wait 這樣的系統調用),某些阻塞的 I/O 操做(例如 openread 之類),還能夠分析進程或線程之間爭用 CPU 的問題。經過 off-CPU 火焰圖,都一目瞭然。

應該說 off-CPU 火焰圖也算是我本身的一個大膽嘗試。記得最初我在加州和內華達州之間的一個叫作 Tahoe 的湖泊邊,閱讀 Brendan 關於 off-CPU 時間的一篇博客文章。我固然就想到,或許能夠把 off-CPU 時間代替 on-CPU 時間應用到火焰圖這種展示方式上去。因而回來後我就在公司的生產系統中作了這樣一個嘗試,使用 SystemTap 繪製出了 Nginx 進程的 off-CPU 火焰圖。我在推特上公佈了這個成功嘗試以後,Brendan 還專門聯繫到我,說他本身以前也嘗試過這種方式,但效果並不理想。我估計這是由於他當時將之應用於多線程的程序,好比 MySQL,而多線程的程序由於線程同步方面的緣由,off-CPU 圖上會有不少噪音,容易掩蓋真正有趣的那些部分。而我應用 off-CPU 火焰圖的場景是像 Nginx 這樣的單線程程序,因此 off-CPU 火焰圖裏每每會當即指示出那些阻塞 Nginx 事件循環的系統調用,抑或是 sem_wait 之類的鎖操做,又或者是搶佔式的進程調度器的強行介入,因而能夠很是好地幫助分析一大類的性能問題。在這樣的 off-CPU 火焰圖中,惟一的「噪音」其實就是 Nginx 事件循環自己的 epoll_wait 這樣的系統調用,很容易識別並忽略掉。

off-CPU 時間

相似地,咱們能夠把火焰圖拓展到其餘的系統指標維度,好比內存泄漏的字節數。有一回我就使用「內存泄漏火焰圖」快速定位了 Nginx 核心中的一處很微妙的泄漏問題。因爲該泄漏發生在 Nginx 本身的內存池中,因此使用 ValgrindAddressSanitizer 這樣的傳統工具是沒法捕捉到的。還有一次也是使用「內存泄漏火焰圖」輕鬆定位了一位歐洲開發者本身編寫的 Nginx C 模塊中的泄漏。那處泄漏很是細微和緩慢,困撓了他好久,而我幫他定位前都不須要閱讀他的源代碼。細想起來我本身都會以爲有些神奇。固然,咱們也能夠將火焰圖拓展到文件 I/O 的延時和數據量等其餘系統指標。因此這真是一種了不得的可視化方法,能夠用於不少徹底不一樣的問題類別。

咱們的 OpenResty XRay 產品支持自動採樣各類類型的火焰圖,包括 C/C++ 級別火焰圖、Lua 級別火焰圖、off-CPU 和 on-CPU 火焰圖、內存動態分配火焰圖、內存對象引用關係火焰圖、文件 IO 火焰圖、等等。

方法論

前面咱們介紹到火焰圖這樣的基於採樣的可視化方法,它其實算是很是通用的方法了。無論是什麼系統,是用什麼語言編寫的,咱們通常均可以獲得一張某種性能維度上的火焰圖,而後輕鬆進行分析。但更多的時候,咱們可能須要對一些更深層次的更特殊的問題進行分析和排查,此時就須要編寫一系列專門化的動態追蹤工具,有計劃有步驟地去逼近真正的問題。

在這個過程中,咱們推薦的策略是一種所謂的小步推動、連續求問的方式。也就是說咱們並不期望一下編寫一個很龐大很複雜的調試工具,一會兒採集到全部可能須要的信息,從而一會兒解決掉最終的問題。相反,咱們會把最終問題的假設,分解成一系列的小假設,而後逐步求索,逐步驗證,不斷肯定會修正咱們的方向,不斷地調整咱們的軌跡和咱們的假設,以接近最終的問題。這樣作有一個好處是,每個步驟每個階段的工具均可以足夠的簡單,那麼這些工具自己犯錯的可能性就大大下降。Brendan 也注意到他若是嘗試編寫多用途的複雜工具,這種複雜工具自己引入 bug 的可能性也大大提升了。而錯誤的工具會給出錯誤的信息,從而誤導咱們得出錯誤的結論。這是很是危險的。簡單工具的另外一大好處是,在採樣過程中對生產系統產生的開銷也會相對較小,畢竟引入的探針數目較少,每一個探針的處理程序也不會有太多太複雜的計算。這裏的每個調試工具都有本身的針對性,均可以單獨使用,那麼這些工具在將來獲得複用的機會也大大提升。因此總的來講,這種調試策略是很是有益的。

值得一提的是,這裏咱們拒絕所謂的「大數據」的調試作法。即咱們並不會去嘗試一會兒採集儘量全的信息和數據。相反,咱們在每個階段每個步驟上只採集咱們當前步驟真正須要的信息。在每一步上,基於咱們已經採集到的信息,去支持或者修正咱們原來的方案和原來的方向,而後去指導編寫下一步更細化的分析工具。

另外,對於很是小頻率發生的線上事件,咱們一般會採用「守株待兔」的作法,也就是說咱們會設一個閾值或其餘篩選條件,坐等有趣的事件被咱們的探針捕獲到。好比在追蹤小頻率的大延時請求的時候,咱們會在調試工具裏,首先篩選出那些延時超過必定閾值的請求,而後針對這些請求,採集儘量多的實際須要的細節信息。這種策略其實跟咱們傳統的儘量多的採集全量統計數據的作法徹底相反,正由於咱們是有針對性地、有具體策略地進行採樣分析,咱們才能把損耗和開銷降到最低點,避免無謂的資源浪費。

咱們的 OpenResty XRay 產品經過知識庫和推理引擎,能夠自動化應用各類動態追蹤方面的方法論,能夠自動使用系統性的方法,逐步縮小問題範圍,直至定位問題根源,再報告給用戶,並向用戶建議優化或修復方法。

知識就是力量

我以爲動態追蹤技術很好地詮釋了一句老話,那就是「知識就是力量」。

經過動態追蹤工具,咱們能夠把咱們原先對系統的一些認識和知識,轉變成能夠解決實際問題的很是實用的工具。咱們原先在計算機專業教育當中,經過課本瞭解到的那些本來抽象的概念,好比說虛擬文件系統、虛擬內存系統、進程調度器等等,如今均可以變得很是鮮活和具體。咱們第一次能夠在實際的生產系統當中,真切地觀察它們具體的運做,它們的統計規律,而不用把操做系統內核或者系統軟件的源碼改得面目全非。這些非侵入式的實時觀測的能力,都得益於動態追蹤技術。

這項技術就像是金庸小說裏楊過使的那把玄鐵重劍,徹底不懂武功的人天然是使不動的。但只要會一些武功,就能夠越使越好,不斷進步,直至木劍也能橫行天下的境界。因此但凡你有一些系統方面的知識,就能夠把這把「劍」揮動起來,就能夠解決一些雖然基本但原先都沒法想象的問題。並且你積累的系統方面的知識越多,這把「劍」就能夠使得越好。並且,還有一點頗有意思的是,你每多知道一點,就立馬能多解決一些新的問題。反過來,因爲咱們能夠經過這些調試工具解決不少問題,能夠測量和學習到生產系統裏面不少有趣的微觀或宏觀方面的統計規律,這些看得見的成果也會成爲咱們學習更多的系統知識的強大動力。因而很天然地,這也就成爲有追求的工程師的「練級神器」。

記得我在微博上面曾經說過,「鼓勵工程師不斷的深刻學習的工具纔是有前途的好工具」。這實際上是一個良性的相互促進的過程。

開源與調試符號

前面咱們提到,動態追蹤技術能夠把正在運行的軟件系統變成一個能夠查詢的實時只讀數據庫,可是作到這一點一般是有條件的,那就是這個軟件系統得有比較完整的調試符號。那麼調試符號是什麼呢?調試符號通常是軟件在編譯的時候,由編譯器生成的供調試使用的元信息。這些信息能夠把編譯後的二進制程序裏面的不少細節信息,好比說函數和變量的地址、數據結構的內存佈局等等,映射回源代碼裏面的那些抽象實體的名稱,好比說函數名、變量名、類型名之類。Linux 世界常見的調試符號的格式稱爲 DWARF(與英文單詞「矮人」相同)。正是由於有了這些調試符號,咱們在冰冷黑暗的二進制世界裏面纔有了一張地圖,纔有了一座燈塔,纔可能去解釋和還原這個底層世界裏每個細微方面的語義,重建出高層次的抽象概念和關係。

「矮人」爲咱們指明道路

一般也只有開源軟件才容易生成調試符號,由於絕大多數閉源軟件出於保密方面的緣由,並不會提供任何調試符號,以增長逆向工程和破解的難度。其中有一個例子是 Intel 公司的 IPP 這個程序庫。IPP 針對 Intel 的芯片提供了不少常見算法的優化實現。咱們也曾經嘗試過在生產系統上面去使用基於 IPP 的 gzip 壓縮庫,但不幸的是咱們遇到了問題—— IPP 會時不時的在線上崩潰。顯然,沒有調試符號的閉源軟件在調試的時候會很是痛苦。咱們曾經跟 Intel 的工程師遠程溝通了好屢次都沒有辦法定位和解決問題,最後只好放棄。若是有源代碼,或者有調試符號,那麼這個調試過程極可能會變的簡單許多。

關於開源與動態追蹤技術之間這種水乳相容的關係,Brendan Gregg 在他以前的一次分享當中也有說起。特別是當咱們的整個軟件棧(software stack)都是開源的時候,動態追蹤的威力纔有可能獲得最大限度的發揮。軟件棧一般包括操做系統內核、各類系統軟件以及更上層的高級語言程序。當整個棧所有開源的時候,咱們就能夠垂手可得的從各個軟件層面獲得想要的信息,並將之轉化爲知識,轉化爲行動方案。

軟件棧的典型構成

因爲較複雜的動態追蹤都會依賴於調試符號,而有些 C 編譯器生成的調試符號是有問題的。這些含有錯誤的調試信息會致使動態追蹤的效果打上很大的折扣,甚至直接阻礙咱們的分析。比方說使用很是普遍的 GCC 這個 C 編譯器,在 4.5 這個版本以前生成的調試符號質量是不好的,而 4.5 以後則有了長足的進步,尤爲是在開啓編譯器優化的狀況下。

咱們的 OpenResty XRay 動態追蹤平臺會實時抓取公網上常見的開源軟件的調試符號包和二進制包,並進行分析和索引。目前這個數據庫已經索引了接近 10 TB 的數據了。

Linux 內核的支持

前面提到,動態追蹤技術通常是基於操做系統內核的,而對於咱們平時使用很是普遍的 Linux 操做系統內核來講,其動態追蹤的支持之路是一個漫長而艱辛的過程。其中一個主要緣由或許是由於 Linux 的老大 Linus 一直以爲這種技術沒有必要。

最初 Red Hat 公司的工程師爲 Linux 內核準備了一個所謂的 utrace 的補丁,用來支持用戶態的動態追蹤技術。這是 SystemTap 這樣的框架最初仰賴的基礎。在漫長的歲月裏,Red Hat 家族的 Linux 發行版都默認包含了這個 utrace 補丁,好比 RHEL、CentOS 和 Fedora 之類。在那段 utrace 主導的日子裏,SystemTap 只在 Red Hat 系的操做系統中有意義。這個 utrace 補丁最終也未能合併到主線版本的 Linux 內核中,它被另外一種折衷的方案所取代。

Linux 主線版本很早就擁有了 kprobes 這種機制,能夠動態地在指定的內核函數的入口和出口等位置上放置探針,並定義本身的探針處理程序。

用戶態的動態追蹤支持姍姍來遲,經歷了無數次的討論和反覆修改。從官方 Linux 內核的 3.5 這個版本開始,引入了基於 inode 的 uprobes 內核機制,能夠安全地在用戶態函數的入口等位置設置動態探針,並執行本身的探針處理程序。再後來,從 3.10 的內核開始,又融合了所謂的 uretprobes 這個機制5,能夠進一步地在用戶態函數的返回地址上設置動態探針。uprobes 和 uretprobes 加在一塊兒,終於能夠取代 utrace 的主要功能。utrace 補丁今後也完成了它的歷史使命。而 SystemTap 如今也能在較新的內核上面,自動使用 uprobes 和 uretprobes 這些機制,而再也不依賴於 utrace 補丁。

最近幾年 Linux 的主線開發者們,把原來用於防火牆的 netfilter 裏所使用的動態編譯器,即 BPF,擴展了一下,獲得了一個所謂的 eBPF,能夠做爲某種更加通用的內核虛擬機。經過這種機制,咱們其實能夠在 Linux 中構建相似 DTrace 那種常駐內核的動態追蹤虛擬機。而事實上,最近已經有了一些這方面的嘗試,好比說像 BPF 編譯器(BCC)這樣的工具,使用 LLVM 工具鏈來把 C 代碼編譯爲 eBPF 虛擬機所接受的字節碼。總的來講,Linux 的動態追蹤支持是變得愈來愈好的。特別是從 3.15 版本的內核開始,動態追蹤相關的內核機制終於變得比較健壯和穩定了。惋惜的是,eBPF 在設計上一直有嚴重的限制,使得那些基於 eBPF 開發的動態追蹤工具始終停留在較爲簡單的水平上,用個人話來講,還停留在「石器時代」。雖然 SystemTap 最近也開始支持 eBPF 這個運行時,但這個運行時支持的 stap 語言特性也是極爲有限的,即便 SystemTap 的老大 Frank 也表達了這方面的擔憂。

硬件追蹤

咱們看到動態追蹤技術在軟件系統的分析當中能夠扮演很是關鍵的角色,那麼很天然地會想到,是否也能夠用相似的方法和思想去追蹤硬件。

咱們知道其實操做系統是直接和硬件打交道的,那麼經過追蹤操做系統的某些驅動程序或者其餘方面,咱們也能夠間接地去分析與之相接的硬件設備的一些行爲和問題。同時,現代硬件,好比說像 Intel 的 CPU,通常會內置一些性能統計方面的寄存器(Hardware Performance Counter),經過軟件讀取這些特殊寄存器裏的信息,咱們也能夠獲得不少有趣的直接關於硬件的信息。好比說 Linux 世界裏的 perf 工具最初就是爲了這個目的。甚至包括 VMWare 這樣的虛擬機軟件也會去模擬這樣特殊的硬件寄存器。基於這種特殊寄存器,也產生了像 Mozilla rr 這樣有趣的調試工具,能夠高效地進行進程執行過程的錄製與回放。

直接對硬件內部設置動態探針並實施動態追蹤,或許目前還存在於科幻層面,歡迎有興趣的同窗可以貢獻更多的靈感和信息。

死亡進程的遺骸分析

咱們前面看到的其實都是對活着的進程進行分析,或者說正在運行的程序。那麼死的進程呢?對於死掉的進程,其實最多見的形式就是進程發生了異常崩潰,產生了所謂的 core dump 文件。其實對於這樣死掉的進程剩下的「遺骸」,咱們也能夠進行不少深刻的分析,從而有可能肯定它的死亡緣由。從這個意義上來說,咱們做爲程序員扮演着「法醫」這樣的角色。

最經典的針對死進程遺骸進行分析的工具即是鼎鼎大名的 GNU Debugger(GDB)。那麼 LLVM 世界也有一個相似的工具叫作 LLDB。顯然,GDB 原生的命令語言是很是有侷限的,咱們若是手工逐條命令地對 core dump 進行分析其實能獲得地信息也很是有限。其實大部分工程師分析 core dump 也只是用 bt full 命令查看一下當前的 C 調用棧軌跡,抑或是利用 info reg 命令查看一下各個 CPU 寄存器的當前取值,又或者查看一下崩潰位置的機器代碼序列,等等。而其實更多的信息深藏於在堆(heap)中分配的各類複雜的二進制數據結構之中。對堆裏的複雜數據結構進行掃描和分析,顯然須要自動化,咱們須要一種可編程的方式來編寫複雜的 core dump 的分析工具。

順應此需求,GDB 在較新的版本當中(我記得好像是從 7.0 開始的),內置了對 Python 腳本的支持。咱們如今能夠用 Python 來實現較複雜的 GDB 命令,從而對 core dump 這樣的東西進行深度分析。事實上我也用 Python 寫了不少這樣的基於 GDB 的高級調試工具,甚至不少工具是和分析活體進程的 SystemTap 工具一一對應起來的。與動態追蹤相似,藉助於調試符號,咱們能夠在黑暗的「死亡世界」中找到光明之路。

黑暗世界裏的光明之路

不過這種作法帶來的一個問題是,工具的開發和移植變成了一個很大的負擔。用 Python 這樣的腳本語言來對 C 風格的數據結構進行遍歷並非一件有趣的事情。這種奇怪的 Python 代碼寫多了真的會讓人抓狂。另外,同一個工具,咱們既要用 SystemTap 的腳本語言寫一遍,而後又要用 GDB 的 Python 代碼來寫一遍:無疑這是一個很大的負擔,兩種實現都須要仔細地進行開發和測試。它們雖然作的是相似的事情,但實現代碼和相應的 API 都徹底不一樣(這裏值得一提的是,LLVM 世界的 LLDB 工具也提供了相似的 Python 編程支持,而那裏的 Python API 又和 GDB 的不相兼容)。

咱們固然也能夠用 GDB 對活體程序進行分析,但和 SystemTap 相比,GDB 最明顯的就是性能問題。我曾經比較過一個較複雜工具的 SystemTap 版和 GDB Python 版。它們的性能相差有一個數量級。GDB 顯然不是爲這種在線分析來設計的,相反,更多地考慮了交互性的使用方式。雖然它也能以批處理的方式運行,可是內部的實現方式決定了它存在很是嚴重的性能方面的限制。其中最讓我抓狂的莫過於 GDB 內部濫用 longjmp 來作常規的錯誤處理,從而帶來了嚴重的性能損耗,這在 SystemTap 生成的 GDB 火焰圖上是很是明顯的。幸運地是,對死進程的分析老是能夠離線進行,咱們不必在線去作這樣的事情,因此時間上的考慮倒並非那麼重要了。然而不幸的是,咱們的一些很複雜的 GDB Python 工具,須要運行好幾分鐘,即便是離線來作,也是讓人感到很挫敗的。

我本身曾經使用 SystemTap 對 GDB + Python 進行性能分析,並根據火焰圖定位到了 GDB 內部最大的兩處執行熱點。而後,我給 GDB 官方提了兩個 C 補丁,一是針對 Python 字符串操做,一是針對 GDB 的錯誤處理方式。它們使得咱們最複雜的 GDB Python 工具的總體運行速度提升了 100%。GDB 官方目前已經合併了其中一個補丁。使用動態追蹤技術來分析和改進傳統的調試工具,也是很是有趣的。

我已經把不少從前在本身的工做當中編寫的 GDB Python 的調試工具開源到了 GitHub 上面,有興趣的同窗能夠去看一下。通常是放在 nginx-gdb-utils 這樣的 GitHub 倉庫裏面,主要針對 Nginx 和 LuaJIT。我曾經利用這些工具協助 LuaJIT 的做者 Mike Pall 定位到了十多個 LuaJIT 內部的 bug。這些 bug 大多隱藏多年,都是 Just-in-Time (JIT) 編譯器中的很微妙的問題。

因爲死掉的進程不存在隨時間變化的可能性,咱們姑且把這種針對 core dump 的分析稱之爲「靜態追蹤」吧。

我編寫的 GDB 調試命令

咱們的 OpenResty XRay 產品經過 Y 語言 編譯器,可讓各類用 Y 語言編寫的分析工具也能同時支持 GDB 這樣的平臺,從而自動化對 core dump 文件的深刻分析。

傳統的調試技術

說到 GDB,咱們就不得不說一說動態追蹤與傳統的調試方法之間的區別與聯繫。細心的有經驗的工程師應該會發現,其實動態追蹤的「前身」就是在 GDB 裏面設置斷點,而後在斷點處進行一系列檢查的這種方式。只不過不一樣的是,動態追蹤老是強調非交互式的批處理,強調儘量低的性能損耗。而 GDB 這樣的工具自然就是爲交互操做而生的,因此實現並不考慮生產安全性,也不怎麼在意性能損耗。通常它的性能損耗是極大的。同時 GDB 所基於的 ptrace 這種很古老的系統調用,其中的坑和問題也很是多。好比 ptrace 須要改變目標調試進程的父親,還不容許多個調試者同時分析同一個進程。因此,從某種意義上來說,使用 GDB 能夠模擬一種所謂的「窮人的動態追蹤」。

不少編程初學者喜歡使用 GDB 進行「單步執行」,而在真實的工業界的生產開發當中,這種方式常常是很是缺少效率的。這是由於單步執行的時候每每會產生程序執行時序上的變化,致使不少與時序相關的問題沒法再復現。另外,對於複雜的軟件系統,單步執行很容易讓人迷失在紛繁的代碼路徑當中,或者說迷失在所謂的「花園小徑」當中,只見樹木,不見森林。

因此,對於平常的開發過程中的調試,其實咱們仍是推薦最簡單也是看起來最笨的方法,即在關鍵代碼路徑上打印輸出語句。這樣咱們經過查看日誌等輸出獲得一個有很完整的上下文,從而可以有效進行分析的程序執行結果。當這種作法與測試驅動的開發方式結合起來的時候,尤其高效。顯然,這種加日誌和埋點的方式對於在線調試是不切合實際的,關於這一點,前面已經充分地討論了。而另外一方面,傳統的性能分析工具,像 Perl 的 DProf、C 世界裏的 gprof、以及其餘語言和環境的性能分析器(profiler),每每須要用特殊的選項從新編譯程序,或者以特殊的方式從新運行程序。這種須要特別處理和配合的性能分析工具,顯然並不適用在線的實時活體分析。

凌亂的調試世界

當今的調試世界是很凌亂的,正如咱們前面看到的有 DTrace、SystemTap、ePBF/BCC、GDB、LLDB 這些,還有不少不少咱們沒有提到的,你們均可以在網絡上查到。或許這從一個側面反映出了咱們所處的這個真實世界的混亂。

以前有不少年我都在想,咱們能夠設計並實現一種大一統的調試語言。後來我在 OpenResty Inc. 公司終於實現了這樣的一種語言,叫作 Y 語言。它的編譯器可以自動生成各類不一樣的調試框架和技術所接受的輸入代碼。好比說生成 DTrace 接受的 D 語言代碼,生成 SystemTap 接受的 stap 腳本,還有 GDB 接受的 Python 腳本,以及 LLDB 的另外一種不兼容 API 的 Python 腳本,抑或是 eBPF 接受的字節碼,乃至 BCC 接受的某種 C 和 Python 代碼的混合物。

若是咱們設計的一個調試工具須要移植到多個不一樣的調試框架,那麼顯然人工移植的工做量是很是大的,正如我前面所提到的。而若是有這樣一個大一統的 Y 語言,其編譯器可以自動把同一份 Y 代碼轉換爲針對各類不一樣調試平臺的輸入代碼,並針對那些平臺進行自動優化,那麼每一種調試工具咱們就只須要用 Y 語言寫一遍就能夠了。這將是巨大的解脫。而做爲調試者本人,也沒有必要親自去學習全部那些具體的調試技術的凌亂的細節,親自去踩每一種調試技術的「坑」。

Y 語言目前已經做爲 OpenResty XRay 產品的一部分,提供給廣大用戶。

有朋友可能要問爲何要叫作 Y 呢? 這是由於個人名字叫亦春,而亦字的漢語拼音的第一個字母就是 Y……固然了,還有更重要的緣由,那就是它是用來回答以「爲何」開頭的問題的語言,而「爲何」在英語裏面就是「why」,而 why 與 Y 諧音。

OpenResty XRay

OpenResty XRay 是由 OpenResty Inc. 公司提供的商業產品。OpenResty XRay 能夠在無需目標程序任何配合的狀況下,幫助用戶深刻洞察其線上或者線下的各類軟件系統的行爲細節,有效地分析和定位各類性能問題、可靠性問題和安全問題。在技術上,它擁有比 SystemTap 更強的追蹤功能,同時在性能上和易用性上也比 SystemTap 提升不少。它同時也能夠支持自動分析 core dump 文件這樣的程序遺骸。

有興趣的朋友歡迎聯繫咱們,申請免費試用。

OpenResty XRay Console Dashboard

瞭解更多

若是你還想了解更多關於動態追蹤的技術、工具、方法論和案例,能夠關注咱們 OpenResty Inc. 公司的博客網站 。也歡迎掃碼關注咱們的微信公衆號:

咱們的微信公衆號

同時很是歡迎你們試用咱們的 OpenResty XRay 商業產品。

動態追蹤方面的先驅,Brendan Gregg 老師的博客也有不少精采內容。

鳴謝

本文獲得了個人不少朋友和家人的幫助。首先要感謝師蕊辛苦的聽寫筆錄工做;本文其實源自一次長達一小時的語音分享。而後要感謝不少朋友認真的審稿和意見反饋。同時也感謝個人父親和妻子在文字上的耐心幫助。

關於做者

章亦春是開源項目 OpenResty® 的創始人,同時也是 OpenResty Inc. 公司的創始人和 CEO。他貢獻了許多 Nginx 的第三方模塊,至關多 Nginx 和 LuaJIT 核心補丁,而且設計了 OpenResty XRay 等產品。

譯文

咱們目前只提供了中文原文(本文) 。咱們歡迎讀者提供其餘語言的翻譯版本,只要是全文翻譯不帶省略,咱們都將會考慮採用,很是感謝!

咱們也在 OpenResty Inc. 官方博客 同步維護本文。


  1. SystemTap 和 OpenResty XRay 都沒有這些限制和缺點。
  2. 在我成立 OpenResty Inc. 公司以後,咱們團隊也給開源 SystemTap 項目貢獻過數量衆多的補丁,重新功能到 bug 修復。
  3. OpenResty XRay 的動態追蹤平臺並不存在 SystemTap 的這些缺點。
  4. stap++ 項目再也不繼續維護,已被使用全新一代動態追蹤技術的 OpenResty XRay 平臺和工具集所取代。
  5. uretprobes 其實在實現上有很大的問題,由於它會直接修改目標程序的系統運行時棧,從而會破壞不少重要的功能,好比 stack unwinding。咱們的 OpenResty XRay 本身從新實現了相似 uretprobes 的效果,但卻沒有它的這些缺點。
相關文章
相關標籤/搜索