天天看點

Facebook 如何做大規模服務的自主測試

作者 | Paul Marinescu

譯者 | Sambodhi

策劃 | 褚杏娟

允許開發者快速開發新特性的原型設計、測試和疊代,對于 Facebook 的成功至關重要。要想有效地實作這一點,關鍵是要有一個穩定的基礎設施,并且不會帶來不必要的摩擦。如果相關的基礎設施還必須擴大,以支援全球 30 多億人口,利用日益增長的算力,以及應對一個極其龐大且不斷增長的代碼庫,那麼這一點顯然更加具有挑戰性。

解決這個問題的兩種方式是更好的抽象和自動化測試。抽象包含了面向服務的基礎設施,它允許業務邏輯結構變成獨立編寫、部署和擴充元件。盡管這對于快速疊代非常重要,但是也增加了測試的複雜性。在檢查服務中的邏輯時,單元測試是有用的,但是無法測試服務間的依賴關系。內建測試可以起到拯救作用,但是,相對标準化的單元測試架構來說,沒有現成的內建測試架構可供我們用于後端服務。是以我們設計并建構了這個。

Facebook 如何做大規模服務的自主測試

Facebook 的基礎設施俯瞰視圖,強調了後端測試選項。

今天,我們将詳細介紹在這個內建測試基礎設施之上建立的一個新的自主測試擴充,并且也是對這一基礎設施本身的幕後觀察。這個擴充借鑒了模糊測試的理念,即一種自動化技術,它使用随機輸入來發現 bug,并利用軟體棧的同質性來提供無縫的開發體驗,鼓勵快速疊代。迄今為止,Facebook 的大多數自主測試集中在我們的前端,或者通過 Infer、Sapienz 和 Zoncolan 等工具進行的安全測試。在此,我們将讨論我們如何自主測試後端服務。

1

內建測試基礎設施

內建測試基礎設施需要鼓勵工程師編寫有效的測試,在需要時自動運作測試,并以直覺的方式顯示結果。這種方式通過提供代碼架構、測試排程和執行能力,以及持續內建系統的适當鈎子來實作這一點。代碼架構封裝了樣闆檔案,并提供了常見的抽象和模式,以消除編寫測試時使用片狀結構等常見陷阱。本文讨論了編寫測試的三個方面,着重于與內建測試相關的部分:定義測試環境、指定輸入以及檢查輸出。

Facebook 如何做大規模服務的自主測試

內建測試的元件。測試基礎設施為工程師編寫測試提供了基礎,并為運作測試提供了執行平台。

2

定義測試環境

為提供确定的結果和避免副作用,測試通常不在生産環境中運作。這對于單元測試尤其适用,單元測試集中于一個小的代碼單元,使用模拟或假象來替代外部依賴。盡管這樣做可以避免副作用,但是它有不利的一面,那就是測試系統沒有足夠逼近。模拟程式本身僅實作了一些真正依賴關系中的行為。是以,有些 bug 可能無法檢測到。維護模拟也需要相當大的工程努力。

內建測試較少依賴模拟。測試後端服務通常涉及一個或多個未修改的服務。無需修改服務進行測試,這有一些好處。第一,它避免了服務所有者的負擔,但更重要的是,它使測試與在生産中運作的代碼相同,進而使它們更具代表性。

這提出了兩個必須解決的重要挑戰。首先是建立測試環境,适合運作未修改的服務。其次,必須确定如何設定測試環境的邊界以及如何處理這些邊界之間的連接配接。

這些挑戰要求采取務實的做法。我們的解決方案重用生産基礎設施,尤其是用于建構測試環境的容器化和路由器系統。但是,我們在基礎設施中為每一個測試建立單獨的短暫實體。這樣,測試環境就可以不接受生産請求,而不自動限制連接配接到生産系統。這使得一些測試可以與生産環境共享隻讀資産或 API。與此同時,我們使用附加的隔離層來限制其他測試到生産系統的連接配接。

通過基于希望檢查的服務互動,我們授權服務所有者定義測試環境的邊界。在相同環境中,與調用者運作的服務優先提供網絡請求。如果環境中不存在适當的服務,請求可以轉到模拟、轉到生産系統的記憶體副本,或者被阻止,或者轉發到生産環境(例如,隻讀請求)。

對于此設定,模拟是通過動态建立和啟動一個服務來工作的,這個服務與原始服務具有相同的接口,但實作方式簡單。模拟服務運作在測試工具所在的位址空間中。這樣可以友善地進行互動。可以在運作時更改模拟實作,這與更改單元測試模拟的方式相似。我們把每個模拟方法處理程式包裝成一個标準的 Python MagicMock 或 StrictMock。通過這樣操作,可以輕松地檢查它的調用次數以及它接收的參數。

對于常見的依賴關系,例如存儲,記憶體複制非常有用。出這些選項外,測試基礎設施還組織了測試環境中的連接配接。稍後,我們将在自主測試中詳細讨論這個問題。

測試輸入

通常來說,測試輸入是以測試工具的方式提供的,它是一個在測試環境中與測試服務一起執行的程式。一個測試工具可以直接執行服務,也可以通過遠端調用(RPC),或者在測試環境中間接執行更改。舉例來說,它可以應用新的全局配置設定或者關閉測試服務的副本,盡管測試架構為這些操作提供了原語,但是建構測試由服務所有者負責。

在內建測試中,模闆是另一種輸入源。可将其配置為發送特定的響應,從根本上作為被測服務的輸入。依賴失敗代表輸入的一種特殊情況,可以通過抛出模拟中的異常來模拟。

測試 Oracle

大多數測試 Oracle 都是針對服務行為的自定義斷言。盡管這些斷言原則上和單元測試斷言類似,但是它們隻能檢查被測服務的外部可見行為,而非内部狀态。它包括 RPC 響應、傳遞到模拟調用的參數、寫入到短暫的測試資料庫的資料以及其他示例。

測試基礎設施還可以檢測常見的錯誤,比如崩潰或由消毒器标記的 bug,以及被監控基礎設施标記的服務健康問題。

可擴充性

內建測試基礎設施的設計目标之一就是讓團隊能夠在其之上建構擴充。對于這個擴充,我們主要有兩種用途。首先要解決團隊服務中心出現的常見模式,例如測試環境設定或者經常使用的自定義。它們還可以為特定類型的測試定義基礎,例如災難準備測試。在需要時,這些測試驗證了我們能夠從頭啟動最基本的基礎設施服務。比如 ZooKeeper。

3

自主內建測試

上述架構為服務所有者編寫內建測試提供了架構。但是,在很多情況下,測試基礎設施可以通過提供合理的預設值甚至自動生成所有測試元件來做得更好。

要定義測試環境,這個基礎設施将反映服務的生産環境。在 Facebook 的叢集管理系統 Twine 上,它以一種标準的方式定義了所有的服務。但是,它也可以通過程式設計的方式對這些元件進行檢查。在進行特定修改之前,測試可以檢查環境,并檢查它的合理性,然後将它傳回到 Twine 進行執行個體化。當服務所有者在某些情況下需要幹預時,比如一個服務需要特殊的硬體,而在預設的測試機池中不存在,則健全檢查負責通知服務所有者。特定的修改測試包括将測試執行個體與生産系統隔離,減少服務的資源需求以節省容量,以及其他較小的修改。

隔離是自主測試中一個特别重要的元素。這是因為測試基礎設施決定了使用哪些輸入。不過,無論它的選擇如何,測試一定不能産生副作用。舉例來說,有一種情況是,一個測試中的 API 失敗的資料到達了監控基礎設施,它錯誤地認為故障是來自生産系統。結果,它産生了虛假的警報。

盡管從技術角度來看非常簡單,但是将測試環境與基礎設施的其他部分完全隔離常常會導緻測試失敗。正因為如此,我們必須采用更細粒度的方法:

我們可以通過已知的隻讀流量。我們讓服務所有者可以為安全目标設定允許清單。我們将所有标準 PRC 流量都重新路由到通用模拟中,這樣就可以模拟任何服務并傳回虛假的值。我們禁止所有其他網絡請求。

這樣,我們就可以在一個安全的測試環境中運作三分之一的服務,而不需要人工幹預。

在實施隔離時,我們結合了兩種方法:一種是細粒度的應用級隔離,另一種是粗粒度的網絡級隔離。我們對 RPC 調用了應用級隔離。這可以基于調用的 API 來阻止連接配接。在 IP: 端口的粒度級别,網絡級隔離是工作的。一般情況下,我們使用它允許連接配接到諸如 DNS 等知名端口上的監聽服務。除基于連接配接目的地作出決定外,隔離系統還可以根據啟動連接配接的代碼作出決定。這樣做是有價值的,因為某些代碼可以安全地使用可能不安全的 API,該隔離邏輯通過在運作時檢查堆棧跟蹤來識别調用者。

對于隔離層的建構,我們考慮了兩種實作方案。BPF 和 LD_PRELOAD。最後,我們決定采用後者,因為它提供了更大的靈活性。預加載邏輯從我們的配置管理系統中檢索特定于服務的隔離配置,并通過攔截對 libc connect、sendto、sendmsg 和 sendmmsg 函數的調用,進而相應地阻止連接配接。

為深入探讨測試自動化,我們研究了現有的自動化技術。模糊測試是我們內建測試結構的自然比對。它的動态性質非常适合經典的測試範式,而它的自動輸入生成則補充了手動編寫的測試。

模糊測試的核心是一種随機測試形式。不過,盡管它很簡單,但是設定模糊測試仍然需要幾個手工步驟:

将需要模糊測試的代碼分割成一個獨立的單元(測試目标)。與單元測試相同,典型的模糊測試是在一個相對較小的代碼單元上進行的。編寫一個模糊測試工具,負責将随機資料生成為模糊測試代碼所需的類型,并在測試目标中調用正确的函數。保證随機資料符合測試目标的預期限制條件。

最後一點需要進一步澄清。假定需要對 strlen 函數進行模糊處理,該函數期望一個有效的指針指向一個以 NULL 結尾的字元串。在模糊處理生成輸入時,它需要確定所有輸入都是指向 NULL 結束的字元串的有效指針。否則可能會導緻代碼崩潰——并不是因為在代碼中存在 bug,而是由于調用者參數與被調用者期望之間的不比對。這些期望(也稱為 API 契約)常常是隐式的,是以需要手工完成。

在結合了模糊測試和內建測試時,我們可以實作上述手工步驟的自動化:

我們根據前面描述的生産環境來建立環境。這使得我們無需手工劃出代碼,而這些代碼必須進行測試。這要歸功于 Facebook 的 Thrift RPC 架構,這個服務的 API 契約是顯式的,可以通過程式設計的方式獲得。Thrift 提供了一個接口定義語言,它具有反射特性,可以枚舉 API 及其參數。而且,參數類型和其屬性也可以像需求一樣被遞歸地檢查。基于這些資訊生成相應的值之後,就可以動态地執行個體化每個參數。用兩種方法來使用這個能力。第一,建構所測試服務的輸入。其次,自動模拟服務的依賴關系,并将其傳回預設值。這樣就可以用正确的格式自動生成随機資料,自動建立模糊控制。最後,一個服務可能不期望接收到網絡上的輸入。這樣可以消除由于輸入值和服務預期不比對而導緻崩潰的所有機會,這意味着所有遇到的崩潰都會指向實際的 bug。

建構輸入最簡單的方法是為每個資料類型随機地選擇合适的值。這種方法自動提供了一個測試基線,并且具有确定工程師在手工設計測試時可能忽略的極端情況的優勢。對于每一個資料類型,像 MAX_INT 這樣的極端情況值,可以增強這一過程。

随機(和模糊)測試的弱點是,當輸入的有效性涉及複雜的限制時,例如校驗和,它是無效的。單純的模糊測試難以在這種情況下找到有效的輸入。除了任何初始的輸入驗證外,它不會執行服務邏輯。

我們使用請求記錄來克服這一問題和提高自主測試的效率。對于通路生産服務的一小部分請求,我們定期記錄,對這些請求進行“清理”,并将它們提供給測試基礎設施。在這種情況下,測試不會以完全随機的輸入運作,而是記錄的請求會有不同程度的變化。這種方法的基本原則是,所得到的的輸入将保留足夠的原始請求的有效結構,以執行“深層路徑”,但是也有足夠的随機性,可以鍛煉這些路徑的極端情況。

在純粹的模糊處理的另一端,我們使用記錄的請求,而不改變它。它證明,新版本的服務能夠在沒有異常行為的情況下處理上一版本的流量,就像經典的金絲雀測試一樣。以記錄和重放的方式解決了這個問題,好處是不需要獨立的測試基礎設施,也不會影響生産系統。

除崩潰和類似 ASAN 等“消毒器”外,我們還要查找未聲明的異常,并檢查日志中的可疑資訊。MySQL ProgrammingError 異常是一個有趣的示例。這種異常通常由模糊器能夠影響 SQL 查詢的功能所觸發。可以通過調用帶有意外參數的 API 實作,這表示存在 SQL 注入漏洞。Python 文法錯誤(SyntaxError)異常也是相似的。本例中,模糊器可以修改傳遞給 eval 的字元串,并指向可能執行的任意代碼。

4

部署與經驗教訓

自主內建測試的部署政策包括兩個步驟。首先,我們開始在背景為盡可能多的服務運作測試,而不需要服務所有者的參與。這樣,我們就可以知道有什麼機會可以改善,以及報告問題的最佳方式。下一步,我們鼓勵服務所有者選擇在部署其新版本服務之前自動運作該測試。我們選擇了 opt-in 模式,因為測試失敗需要立即采取行動來解除服務的部署管道。我們現在從第一步進入第二步。

在第一步中,通過應用于模糊內建測試的隔離,我們可以安全、自動地對大約三分之一的 Facebook 的 Thrift 服務進行模糊測試。這個模糊測試發現了超過 1000 個 bug。對于每一個 bug,我們都給服務所有者配置設定了一份報告。餘下的三分之二的服務都具有非标準的設定,或者具有嚴格的權限,使得我們不能重用他們的生産工件,或者由于我們執行的嚴格隔離而失敗。在這一步中,我們隻通過 bug 報告與服務所有者接觸。

通過這一過程,我們學到了一些東西。首先,通過我們的測試,我們發現在隔離測試環境方面有許多機會可以改進。是以,我們支援使用更細粒度和可擴充的方法來标記隻讀 API。此外,我們還繼續考慮如何為內建測試環境提供一個一流的抽象,并通過可組合性提供重複使用測試環境的能力。

第二,我們學到了,向服務所有者提供盡可能多的有關我們檢測到的 bug 的資訊是非常重要的。與單元測試相比,在內建測試失敗的情況下,調試本來就很困難。盡管堆棧跟蹤有助于了解崩潰,但是有效的調試還需要對崩潰的服務及其使用的庫有很好的了解。我們注意到,從總體上看,違反 API 契約要比崩潰更容易調試。

第三,我們注意到,随機輸入使得解釋 bug 變得更加困難,工程師們也更難找出 bug 的根源。通過更廣泛地使用記錄的流量和提供大多數形式良好的輸入,我們可以解決這一問題。有些情況下,工程師可能會認為,給定随機輸入,服務可能會通過抛出未聲明的異常或崩潰而破壞其 API 契約。建議反對這一做法,而應依靠全面的輸入驗證。

最後,了解模式測試的有效性也非常重要。迄今為止,我們僅将發現的 bug 作為有效性度量,現在我們開始測量整個服務覆寫範圍。我們希望這能讓服務所有者深入了解服務的哪些部分需要額外的測試。本文也指出了我們可以對內建模糊測試基礎設施進行改變,以增加未來的整體覆寫。

作者簡介:

Paul Marinescu,Facebook 研究科學家。

https://engineering.fb.com/2021/10/20/developer-tools/autonomous-testing/

繼續閱讀