揭秘Crashpad系統(tǒng)如何幫助Dropbox這樣復(fù)雜的桌面程序捕獲并報告崩潰,且兼容Python的多種語言。
維護(hù)像Dropbox這樣的復(fù)雜桌面應(yīng)用程序最大挑戰(zhàn)之一就是同時處理數(shù)億次的安裝,一個小小的錯誤就會影響到大量的用戶。
這些錯誤會攻擊程序,雖然應(yīng)用程序大多數(shù)情況下都可以恢復(fù),但有時也會導(dǎo)致程序終止。這樣的終止或“崩潰”對程序具有很高的破壞性:當(dāng)Dropbox程序終止時,程序就無法同步了。為了確保我們的用戶可以不間斷的同步,我們會自動檢測并報告所有崩潰,同時采取措施重新啟動程序。
2016年,隨著逐步的過渡到Python 3,我們開始著手改進(jìn)我們檢測和報告崩潰的方式。目前,對于我們的桌面團(tuán)隊來說,我們的崩潰報告流程無論在報告的數(shù)量還是在質(zhì)量上都是非常可靠的。在本文中,我們將深入探討我們是如何設(shè)計這個新系統(tǒng)的。
Python不會崩潰,真是這樣的嗎?
部分Dropbox程序是用Python編寫的,雖然Python是一種安全的高級語言,但它還是會崩潰。大多數(shù)出現(xiàn)在Python中的崩潰(即未處理的異常)很容易處理,但很多異常來自“底層“:非Python代碼、解釋器代碼本身中,或在Python的擴(kuò)展中。這些“原始”的崩潰并不是什么新鮮事:例如,幾十年來錯誤的內(nèi)存操作一直困擾著開發(fā)者們。
隨著我們的應(yīng)用程序變得越來越復(fù)雜,我們開始使用其他編程語言來構(gòu)建我們的一些功能。在與操作系統(tǒng)集成時尤其如此,其中最簡單的路徑往往是使用平臺特定的工具和語言(例如,Windows上的COM和macOS上的Objective-C)。這增加了我們的代碼庫中非Python代碼的比例,這就不可避免的帶來懸空指針、內(nèi)存錯誤、數(shù)據(jù)競爭和未經(jīng)檢查的數(shù)組訪問的風(fēng)險,所有這些都可能導(dǎo)致Dropbox被暴力終結(jié)。結(jié)果就是,一個崩潰報告的堆棧軌跡中會包含Python,C ++,Objective-C和C多種代碼!
早期的做法
幾年前,我們使用簡單的進(jìn)程內(nèi)崩潰檢測機(jī)制:信號處理程序。我們能夠“捕獲”各種UNIX系統(tǒng)信號,當(dāng)遇到致命信號(即SIGFPE)時,我們的信號處理程序?qū)L試以下操作:
捕獲每個線程的Python堆棧軌跡(使用faulthandler模塊)
捕獲該線程的本機(jī)堆棧軌跡(通常使用libc的backtrace和backtrace_symbols函數(shù))
然后,我們會將這些數(shù)據(jù)安全地上傳到Dropbox的服務(wù)器。
雖然做到這些已經(jīng)足矣,但有一些基本問題會影響程序的可靠性或限制其在調(diào)試中的實用性:
如果問題發(fā)生在設(shè)置處理程序之前,那我們會收不到任何報告。這通常是由導(dǎo)入庫錯誤或安裝錯誤引起的。這些基本的“啟動錯誤”是最嚴(yán)重的,因為它們導(dǎo)致用戶無法啟動應(yīng)用程序,這是一個無法接受的狀況,因為這時我們根本無法捕捉這些錯誤。出現(xiàn)這樣問題時,我們的工程師只能通過客戶支持系統(tǒng)獲取相關(guān)報告。雖然我們構(gòu)建了一個的錯誤對話框來幫助完成這一過程,但這仍然會使我們的團(tuán)隊在干預(yù)啟動/早期代碼方面增加了風(fēng)險。
信號處理程序穩(wěn)定性不足。處理程序不僅負(fù)責(zé)捕獲狀態(tài),還負(fù)責(zé)將其發(fā)送到我們的服務(wù)器上。隨著時間的推移,我們意識到盡管能夠成功地生成報告,但它仍有可能無法完成發(fā)送。此外,特別嚴(yán)重的崩潰可能導(dǎo)致無法在崩潰時正確提取出狀態(tài)。例如,如果解釋器狀態(tài)本身就已經(jīng)損壞了,則可能會阻止我們進(jìn)行Python堆棧跟蹤,或者更糟糕,整個處理過程可能會破壞。
其中一個根本原因是信號處理程序本身的特性導(dǎo)致的:幸運的是,Python的信號模塊考慮了大部分情況,而且還增加了一些限制。例如,信號只能從主線程調(diào)用,并且可能無法同步運行。這種異步性意味著一些最常見的SIGSEGV通常不會被Python困住!
Crashpad大顯神通
通過在主進(jìn)程外部提取報告器可以構(gòu)建更可靠的崩潰報告機(jī)制。這很容易實現(xiàn),因為Windows和MacOS都提供了系統(tǒng)工具來捕獲進(jìn)程外的崩潰。Chromium項目開發(fā)了一個全面的崩潰捕獲/報告解決方案,該解決方案利用了可獨立使用的工具庫:Crashpad。
Crashpad作為一個小的幫助程序進(jìn)程監(jiān)視你的應(yīng)用程序,當(dāng)出現(xiàn)崩潰的信號時,它就會捕獲有用的信息,包括:
1.進(jìn)程崩潰的原因和導(dǎo)致崩潰的線程;
2.所有線程的堆棧軌跡;
3.堆的部分內(nèi)容;
4.開發(fā)人員添加到應(yīng)用程序的額外注釋(可靈活使用)。
以上這些都是在minidump有效負(fù)載中捕獲的,它是一種最初微軟開發(fā)的在Windows上使用編寫格式,有點類似于Unix風(fēng)格的核心轉(zhuǎn)儲。這種格式是開源的,并且有優(yōu)秀的服務(wù)器端工具(主要來自Google和Mozilla)來處理這些數(shù)據(jù)。
下圖概述了Crashpad的基本架構(gòu):
應(yīng)用程序通過實例化一個進(jìn)程內(nèi)對象(稱為“客戶端”)來使用Crashpad,當(dāng)檢測到崩潰時,該對象報告給進(jìn)程外的幫助程序—稱為“處理程序”。
我們決定使用此庫來解決與進(jìn)程內(nèi)信號處理程序相關(guān)的許多可靠性問題。這個選擇對我們來說很容易,因為Chromium是有史以來發(fā)布的最受歡迎的桌面應(yīng)用程序之一。我們也對Windows的更復(fù)雜支持感到滿意,這是一個與UNIX完全不同的平臺。faulthandler(在當(dāng)時)僅支持Windows平臺的崩潰,因為它非常依賴信號,一個UNIX / POSIX平臺的概念。Crashpad利用結(jié)構(gòu)化異常處理(或SEH)可以捕獲到更全面的致命Windows特定異常。
關(guān)于Linux的說明:盡管最近引入了Linux支持,但是當(dāng)我們第一次部署時,Crashpad僅適用于Windows和MacOS,因此我們將庫的使用限制在這些平臺上。在Linux上,我們繼續(xù)使用進(jìn)程內(nèi)信號處理程序,但我們將來會做進(jìn)一步的改進(jìn)。
符號化
與大多數(shù)已編譯的應(yīng)用程序一樣,Dropbox將發(fā)布版本發(fā)送給用戶,發(fā)布版本中啟用了多個編譯器進(jìn)行優(yōu)化,同時去除符號表示以減少二進(jìn)制存儲大小。這意味著Dropbox收集到的信息幾乎是無用的,除非它可以“映射”回源代碼,這個過程就被稱為“符號化”。
為此我們?yōu)閮?nèi)部服務(wù)器上的每個Dropbox構(gòu)建保留符號。這是我們構(gòu)建過程的核心部分,若符號生成失敗則被認(rèn)為是構(gòu)建失敗,我們不會使用這種無法被符號化的發(fā)布版本。
當(dāng)應(yīng)用的崩潰報告中含有minidump(小存儲器轉(zhuǎn)儲文件:可幫助確定計算機(jī)為什么意外停止的最小的有用信息集)時, 我們使用之前生成的符號來跟蹤應(yīng)用里每個堆棧內(nèi)容并將其鏈接到源代碼中。使用開發(fā)框架系統(tǒng)庫時, 我們會遵循特定平臺的符號表示。此過程使我們的開發(fā)人員能夠快速定位到應(yīng)用崩潰位置,判斷其是源自框架平臺還是第三方代碼。
Microsoft維護(hù)所有 windows 版本的公共符號服務(wù)器,以便映射涉及各版本功能的堆棧幀。不幸的是,Apple沒有類似的系統(tǒng),但是Apple的平臺框架中包括了各版本的匹配符號。為了讓Dropbox支持各種版本, 我們使用測試虛擬機(jī)緩存各種 macOS框架(適用于各種操作系統(tǒng)版本)的符號(盡管我們?nèi)匀慌紶枙龅桨姹疚窗膯栴})。
挎斗驗證
從數(shù)百萬次安裝中更改崩潰報告的基礎(chǔ)架構(gòu)是一項冒險嘗試,但是我們需要這樣來驗證我們的新機(jī)制是否有效。同樣需要注意的是,并非所有終止都是應(yīng)用崩潰(例如用戶關(guān)閉應(yīng)用程序或應(yīng)用自動更新就不屬于應(yīng)用崩潰)。盡管如此,有一些終止情況仍然表明應(yīng)用可能存在問題。因此,我們希望有一種方法能來記錄和判斷出哪種情況算是應(yīng)用正常退出,哪種情況算是應(yīng)用意外崩潰。 這也為我們提供一個基線,用來驗證我們的新崩潰報告構(gòu)架是否捕獲了大部分應(yīng)用崩潰情況。
為了解決這個問題, 我們建立了一個被稱為 " watchdog "(看門狗) 的 "sidecar" (挎斗)過程。這是一個具有單一責(zé)任的小型 "配套" 進(jìn)程 (類似于Crashpad):當(dāng)桌面應(yīng)用退出時, 它會捕獲其退出狀態(tài), 以確定它是否 "成功" (即用戶或應(yīng)用程序啟動的關(guān)閉而不是被強(qiáng)行終止)。因為我們希望它具有高度可靠性,所以該過程被設(shè)計的非常簡單。
我們讓應(yīng)用程序在啟動時發(fā)送事件來生成啟動事件,通過比較啟動和退出事件,可以測量退出監(jiān)控的準(zhǔn)確性。我們可以確保退出監(jiān)控對絕大部分用戶是成功的 (請注意防火墻等其他程序會阻止它一直運行)。此外, 我們可以將此退出事件與來自Crashpad的崩潰報告進(jìn)行匹配,以確保我們預(yù)計會引起崩潰的退出代碼確實包括大多數(shù)用戶的崩潰情況。下圖顯示了我們的退出監(jiān)控:
看門狗允許我們驗證崩潰報告是否正確
看門狗允許我們在單個圖中對崩潰和終止進(jìn)行分類
我們用Rust編寫了看門狗進(jìn)程,為什么會選擇Rust呢:
1.Rust的安全設(shè)置使代碼可靠性非常高。
2.與操作系統(tǒng)的抽象接口設(shè)計良好,屬于系統(tǒng)標(biāo)準(zhǔn)庫的一部分,并且在需要時可以通過FFI輕松擴(kuò)展接口。
3.我們在開發(fā)Dropbox時很大一部分都使用了Rust,這讓Dropbox的搭建變得更加容易。
教Crashpad兼容Python
Crashpad主要是為本機(jī)代碼設(shè)計的,因為Chromium主要是用C ++編寫的。但是,Dropbox客戶端大多是用Python編寫的。由于Python是一種解釋型語言,因此我們收到的大多數(shù)本機(jī)崩潰報告往往如下所示:
0 _ctypes.cpython-35m-darwin.so!_i_get + 0x4
1 _ctypes.cpython-35m-darwin.so!_Simple_repr + 0x4a
2 libdropbox_python.3.5.dylib!_PyObject_Str + 0x8e
3 libdropbox_python.3.5.dylib!_PyFile_WriteObject + 0x79
4 libdropbox_python.3.5.dylib!_builtin_print + 0x1dc
5 libdropbox_python.3.5.dylib!_PyCFunction_Call + 0x7a
6 libdropbox_python.3.5.dylib!_PyEval_EvalFrameEx + 0x5f12
7 libdropbox_python.3.5.dylib!_fast_function + 0x19d
8 libdropbox_python.3.5.dylib!_PyEval_EvalFrameEx + 0x5770
9 libdropbox_python.3.5.dylib!__PyEval_EvalCodeWithName + 0xc9e
10 libdropbox_python.3.5.dylib!_PyEval_EvalCodeEx + 0x24
11 libdropbox_python.3.5.dylib!_function_call + 0x16f
12 libdropbox_python.3.5.dylib!_PyObject_Call + 0x65
13 libdropbox_python.3.5.dylib!_PyEval_EvalFrameEx + 0x666a
14 libdropbox_python.3.5.dylib!__PyEval_EvalCodeWithName + 0xc9e
15 libdropbox_python.3.5.dylib!_PyEval_EvalCodeEx + 0x24
16 libdropbox_python.3.5.dylib!_function_call + 0x16f
17 libdropbox_python.3.5.dylib!_PyObject_Call + 0x65
18 libdropbox_python.3.5.dylib!_PyEval_EvalFrameEx + 0x666a
19 libdropbox_python.3.5.dylib!__PyEval_EvalCodeWithName + 0xc9e
20 libdropbox_python.3.5.dylib!_PyEval_EvalCodeEx + 0x24
21 libdropbox_python.3.5.dylib!_function_call + 0x16f
22 libdropbox_python.3.5.dylib!_PyObject_Call + 0x65
...onandon
這個堆棧跟蹤對于試圖發(fā)現(xiàn)崩潰原因的開發(fā)人員來說并不是很有幫助。雖然faulthandler包含了所有線程的Python堆棧幀,但默認(rèn)情況下Crashpad并沒有此功能。為了讓這個報告變得有用,我們需要加入相關(guān)的Python狀態(tài)。 但是,由于Crashpad不是用Python編寫的并且在進(jìn)程之外,我們無法訪問faulthandler本身,那我們要如何處理呢?
當(dāng)崩潰程序暫停時,Crashpad可以讀取它的所有內(nèi)存以捕獲程序狀態(tài)。 由于程序可能處于錯誤狀態(tài),因此我們無法執(zhí)行任何代碼。接下來我們就需要:
1.弄清楚Python數(shù)據(jù)在內(nèi)存中的結(jié)構(gòu)布局
2.遍歷相關(guān)數(shù)據(jù)結(jié)構(gòu)以定位程序崩潰時正在運行的代碼
3.存儲此信息并將其安全地上傳到我們的服務(wù)器
我們之所以會選擇 Crashpad,,部分原因是它的可定制性,它非常容易被擴(kuò)展。因此,我們在 ProcessSnapshot 類中添加了代碼來捕獲 Python堆棧, 并引入了我們自己的自定義小型轉(zhuǎn)儲 "流" (文件格式符合,同時Crashpad本身支持) 來保留和報告此信息。
Python 和線程本地存儲
首先, 我們需要知道去哪里找它們。在CPython中,解釋器線程始終由本機(jī)線程支持。因此,在 Dropbox應(yīng)用程序中, Python創(chuàng)建的每個本機(jī)線程都有一個關(guān)聯(lián)的 PyThreadState 結(jié)構(gòu)。解釋器使用本機(jī)線程特定的存儲來創(chuàng)建此對象和本機(jī)線程之間的連接。由于Crashpad可以訪問受監(jiān)視進(jìn)程的內(nèi)存,因此它可以讀取這個狀態(tài)并將其作為報告的一部分。
由于 Dropbox提供了CPython的自定義分支,因此我們可以有效地控制它的行為。這意味著我們不僅可以利用它改善Dropbox,而且可以依賴它, 因為我們知道它的可靠性非常高。
在Python中,特定于線程的存儲在不同平臺的實現(xiàn)方式不一樣:
在POSIX上,pthread_key_create 用于分配密鑰,而pthread_(get/set)specific用于交互
在Windows上,TlsAlloc 用于分配存儲在線程環(huán)境Block.aspx中可預(yù)測/記錄位置的線程本地“slots”
注意:我們?yōu)镃rashpad提供了修復(fù)程序以使其隨時可用。
參見:
https://chromium-review.googlesource.com/c/crashpad/crashpad/+/717040
但是,所有平臺的共同點是特定于Python的狀態(tài)存儲在本機(jī)線程狀態(tài)的特定偏移量處。遺憾的是,這種偏移不是靜態(tài)的:它可以根據(jù)各種因素而改變。此偏移量在Python運行時的設(shè)置早期確定:這稱為特定于線程的存儲“密鑰”。此步驟為進(jìn)程中的所有線程創(chuàng)建一個特定于線程的存儲的“插槽”,然后由Python用它來存儲其特定于線程的狀態(tài)。
因此,如果crashpad可以為進(jìn)程實例檢索TSS“key”,它將能夠讀取任何給定線程的PyThreadState。
獲取線程本地存儲“密鑰”
我們考慮了多種方法,但最終選擇了一種受Crashpad本身啟發(fā)的方法。最后,我們修改了Python的fork【fork不知道怎么翻譯】,用在二進(jìn)制的命名部分(即__DATA)中公開運行時狀態(tài)(包括TSS密鑰)。因此,Dropbox的所有實例現(xiàn)在都會以一種易于從Crashpad檢索它的方式公開Python運行時狀態(tài)。
這是通過使用Clang中的__attribute__和在Windows上使用__declspec實現(xiàn)的。
這在Crashpad中使用起來很簡單,因為它使用相同的技術(shù)允許客戶端向自己的進(jìn)程添加注釋(請參閱CrashpadInfo)。
這也很好地與Python自己不斷發(fā)展的解釋器的內(nèi)部設(shè)計保持一致,因為它最近重組了自己,運行時狀態(tài)能夠整合到單個結(jié)構(gòu)_PyRuntime。(在Python / pylifecycle.c中)。此結(jié)構(gòu)包括TSS密鑰以及其他有趣的調(diào)試工具。
注意:我們已將此更改作為拉取上傳到github,希望能對大眾有所裨益。
https://github.com/python/cpython/pull/4802/files
現(xiàn)在Crashpad可以確定TSS密鑰,它可以訪問每個線程的PyThreadState。下一步是解釋此狀態(tài),提取相關(guān)信息,并將其作為崩潰報告的一部分發(fā)送。
解析Python堆棧幀
在CPython中,“frames”是函數(shù)執(zhí)行的單位,Python類似于本機(jī)堆棧幀。 PyThreadState將它們維護(hù)為PyFrameObjects的堆棧。線程狀態(tài)使用單個指針指向任何給定時間的最頂層幀。給定以上設(shè)置和TSS密鑰,我們可以從本機(jī)線程開始,找到PyThreadState,然后“遍歷堆?!盤yFrameObjects。
然而,事實比理論更加棘手一些。我們不能只是#include
對于每一幀,我們的目標(biāo)是將其解析為代碼位置。每個PyFrameObject都有一個指向PyCodeObject的指針,包括有關(guān)函數(shù)名,文件名和行號的信息(faulthandler利用相同的信息)。
文件名和函數(shù)名稱保存為Python字符串。解碼Python字符串可以相當(dāng)復(fù)雜,因為它們構(gòu)建在類型的層次結(jié)構(gòu)上。為簡單起見,我們假設(shè)所有函數(shù)和文件名都是ASCII編碼的(就可以映射到簡單的PyASCIIObject)。
獲取行號稍微復(fù)雜一些。為了節(jié)省空間,Python能夠?qū)⒚總€字節(jié)代碼指令映射到Python源,同時將行號壓縮成一個表(PyCodeObject的co_lnotab)。
解碼此表的算法是明確定義的,因此我們在Crashpad fork【fork】中重新實現(xiàn)了它。
算法參照:https://github.com/python/cpython/blob/3df85404d4bf420db3362eeae1345f2cad948a71/Objects/lnotab_notes.txt
關(guān)于Python 3轉(zhuǎn)換的注釋:由于Python 2和3的實現(xiàn)略有不同,我們在轉(zhuǎn)換過程中保持對Crashpad fork中兩個版本的Python結(jié)構(gòu)的支持。
堆棧框架重建
現(xiàn)在Crashpad的報告包含了所有Python堆棧幀,我們可以改進(jìn)符號化。為此,我們修改了我們的服務(wù)器基礎(chǔ)結(jié)構(gòu),以解析我們對minidump的擴(kuò)展并提取這些堆棧。具體來說,我們擴(kuò)充了崩潰管理系統(tǒng)Crashdash,以顯示本機(jī)崩潰報告的Python堆??蚣苄畔ⅲㄈ绻捎茫?。
這是通過再次“遍歷堆棧”來實現(xiàn)的,但這次,對于調(diào)用PyEval_EvalFrameEx的每個本機(jī)幀,我們從報告中“彈出”匹配的PyFrameObjectcapture。由于我們現(xiàn)在擁有每個幀的函數(shù)名,文件名和行號,現(xiàn)在我們可以顯示匹配的函數(shù)調(diào)用。因此,我們可以從上面提取基礎(chǔ)Python堆棧跟蹤:
file "ui/common/tray.py", line 758,in_do_segfault
file "dropbox/client/ui/cocoa/menu.py", line 169,inmenuAction_
file "dropbox/gui.py", line 274,inguarantee_message_queue
file "dropbox/gui.py", line 299,inhandle_exceptions
file "PyObjCTools/AppHelper.py", line 303,inrunEventLoop
file "ui/cocoa/uikit.py", line 256,inmainloop
file "ui/cocoa/uikit.py", line 929,inmainloop
file "dropbox/client/main.py", line 3263,inrun
file "dropbox/client/main.py", line 6904,inmain_startup
file "dropbox/client/main.py", line 7000,inmain
結(jié)語
有了這個系統(tǒng),我們的開發(fā)人員就可以直接調(diào)查所有崩潰,無論是Python,C,C ++還是Objective-C。此外,我們?yōu)闇y量系統(tǒng)可靠性而引入的新監(jiān)控使我們對應(yīng)用程序正常運行的信心增加了。結(jié)果是為我們的桌面用戶提供了更穩(wěn)定的應(yīng)用程序。舉個例子:使用這個新系統(tǒng),我們能夠執(zhí)行Python 2到3的轉(zhuǎn)換,而不用擔(dān)心我們的用戶會受到負(fù)面影響。
-
服務(wù)器
+關(guān)注
關(guān)注
12文章
9255瀏覽量
85752 -
python
+關(guān)注
關(guān)注
56文章
4801瀏覽量
84863
原文標(biāo)題:Dropbox力薦!我們?nèi)绾螒?yīng)對Python桌面應(yīng)用程序的崩潰
文章出處:【微信號:worldofai,微信公眾號:worldofai】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論