在Qcon London 2016上,Peter Alvaro和Kolton Andrus分享了一項企業與學院合作的成功案例,這次合作最終為Netflix找到了一條自動化故障注入測試(failure injection testing)的嶄新途徑。在這一案例中他們收獲了許多寶貴經驗,其中主要包括:
從已知熟悉的事物出發
通常這有助于縮小解決方案的搜索空間,比如,Netflix的價值觀非常重視用戶體驗
求同存異
通過有效的合作,大家制定并朝著共同的目標一起前進,這是非常必要的
將理論付諸于實踐
真實世界往往比實驗室復雜得多
主講人Alvaro現任圣克魯斯大學的助理教授,雖然他個人更樂意被簡稱為“教授”;而另一位主講人Andrus曾經是Netflix“混亂(chaos)工程”團隊的一員,在離開Netflix后創立了Gremlin Inc。兩位主講人在開始時談到,企業和學院的合作,往往因為各自目標上的差異而變得困難重重。比如,“教授”Alvaro十分享受建模以及嘗試各種解決方案的過程。他在學院內是否成功,往往體現在其研究成果是否被大量引用(citation impact,h-index),有沒有廣泛的應用前景,有時甚至只是一個學院內的評級。而作為“實踐者”,Andrus更熱衷于積極尋找可以切實解決的問題,并將其解決方案落地施行。他在企業內是否成功,則具體地體現在提高系統可得性(availability),減少各種事故的數量,以及降低運維成本。
合作的契機出現在2014年。當時還在Netfix任職的Andrus,負責在著名的“混亂工程”的基礎之上,搭建一套所謂故障即服務(failure as a service)的測試框架。在生產環境上進行故障測試,主要有兩個關鍵的概念:故障范圍(failure scope)和注入點(injection points)。故障范圍指的是,把一次故障測試可能產生的影響,限制在一個可控的范圍內,這個范圍可以小到某個特定的用戶或者設備,也可以大到所有用戶的1%。而注入點指的是系統內計劃會發生故障的組件,比如RPC層,緩存層,或者持久層。
故障測試的最終目的,是為了當真的有故障發生時,生產環境不會停止服務,并且整套系統可以在沒有人為干預的情況下,非常優雅地通過降級(degrade)將發生故障的部分組件排除出去。Andrus還描述了一幕,在整套測試框架正確執行的情況下,發生在故障后的場景。
某天,你來到辦公室和同事聊天。他們問:“嗨,你知道昨晚什么什么服務跪了么?”你可以回答說:“不知道啊,你接到電話了么?” “沒有啊,誰接到了么?” “沒有啊……” 這種感覺真的是爽到了!
我喜歡聊各種故障,但是是在事后,是在上班時間,而不是半夜
依照Netflix的慣例,故障測試的流程是人工完成的。Andrus會找各種團隊開會,討論哪些地方發生故障,發生故障的場景是怎么樣的,然后手動實現并執行一系列對應的故障測試。直覺告訴Andrus應該有更好的辦法。在網上大量搜索之后,Andrus找到了一段Alvaro在RICON Talk上做的演講和論文,《路徑驅動的故障注入(Lineage-driven fault injection)》。Andrus相信這就是他苦苦尋找的那種,能夠安全地自動化故障注入測試的“更好”的辦法。
Andrus開始和Alvaro頻繁地交換各種奇思妙想,最終兩人決定一起合作,將故障測試領域的最前沿提升到新的高度。合作建立在一種企業和學院都奉行的信條之上,即“在各自明確的職責范圍內保有最大程度的自由(freedom and responsibility)”。這次合作的主要目標有以下這些:
證明Alvaro提出的方法可以應用于真實世界 證明這種方法的應用規模可以輕松擴展 使用這種方法找到真實系統中存在的真實問題為了完成這些目標,Andrus和Alvaro決定在暑假的幾個月中非常緊密地協作。為此,Alvaro還以合同工的身份加入了Netflix,在Netflix的辦公室中工作。
合作開始之后,他們首先試圖回答這樣兩個問題:“在一套基于微服務(microservice)架構的常規系統中,有多少種可能的故障?”以及“所有這些故障,能組合出多少種故障情景?”。Alvaro做了個估算,很快給出了大致的結論:假設Netflix有100種服務,并且每種服務只會發生1種故障,那么總共會有種不同的故障場景。換言之,必須執行這么多次故障注入測試之后,Andrus才能詳盡地檢查每一種故障場景,然后徹底地找到每個缺陷。從時間上來看,這幾乎是不可能的。退而求其次,如果限定同時只會有1種故障發生,那么只須要執行100次故障注入測試就足夠了;如果這個限制放開到4種,那么須要執行300萬次測試;如果進一步放開到7種,那將需要160億次。
從某種抽象的學術角度來說,討論容錯性根本是多余的。
理論上,一套擁有容錯性的系統,必須在任何可預見的故障發生時,始終能自動找到替代路徑來繞開故障,繼續正常工作。因此,如果一套系統,可預見的故障場景多達種,那么這套系統就必須在滿足業務邏輯的基礎上,兼顧每種故障場景下的替代路徑。
測試數量的指數級增長,意味著必須有自動化的實施方案,然而這并非易事。一種簡單的策略是說,如果無法做到遍歷所有故障場景,那可以隨機抽取故障場景進行注入測試;但這無異于大海撈針,而且這種策略的耗時依舊太長。既然如此,可以考慮將隨機抽樣改為人為引導挑選,利用人在業務領域內的專業意見以及直覺,來遴選甄別那些有潛在問題的故障場景;然而這樣做就失去了自動化的意義,因為其應用規模受限于人的數量。
使用常規的驗證手段時,人們經常思索的是一個很難回答,同時也沒有正確答案的問題:怎樣才會出故障呢?這個問題對尋找系統缺陷,幾乎沒有任何幫助。在《路徑驅動的故障注入》中提到的重要概念之一,是說人們應該從一套系統的無故障狀態出發,然后試圖去回答說“系統是如何達到目前這種無故障的狀態的?”,以及“整套邏輯鏈條中,是不是有哪里出錯,就能導致系統發生故障?”。邏輯鏈條中的錯誤會將鏈條打破,導致系統無法到達最終的無故障狀態,從而發生故障;而從無故障狀態推演出的各種邏輯鏈條,它們組成的圖(graph)能幫助人們找到那些最有價值的故障場景。
當圖里的每一條邏輯鏈條上,每一個能出錯的環節都被找到之后,整張圖就能簡化為一個合取范式(conjunctive normal form);其中每個能出錯的環節是一個布爾變量,而剩下的環節則被省略。得益于這個簡化,“系統出錯”這一抽象的概念,就變成了一個具體的問題:“如何讓這個合取范式的結果為假(false)?”,而這個問題的答案就是故障注入測試須要檢查的那些目標故障場景。再次得益于這個簡化,目標故障場景的搜索變得非常高效,因為事實上這是一個布爾滿足性問題,數學上已經有很多完備且高效的解法。
Alvaro在論文中提到了一套名為“Molly”的原型(prototype)系統,這套系統可以通過以下算法來尋找目標故障場景:
找到一個被測試系統給出的正確輸出 從這個正確輸出反推,找到并構建支持其正確性的邏輯鏈條圖 將圖簡化為合取范式并求解 回到第1步繼續下一個正確輸出,直到遍歷完所有正確輸出在算法中的第3步中,存在兩種可能的結果:一種是Alvaro自嘲為“好”的結果,即找到了一些目標故障場景;另一種則是沒有找到目標故障場景,那算法將繼續尋找。如果邏輯鏈條圖的構建是完備的,那么被測系統在算法找到的每個目標故障場景中,都有很大的幾率無法正常工作,或者說存在問題。
在將這套理論應用到Netflix故障測試的過程中,第一個遇到的挑戰是說,如何定義一次常規用戶請求是“正確”的,因為在Netflix的服務棧中,一個返回200的HTTP請求,其結果并不一定是正確的。Andrus提到,此時一條Amazon核心指引(leadership)原則啟發了他們,即“一切從用戶出發”,因此他們不再糾結于單次請求是否正確,轉而試圖回答這樣的問題:用戶有沒有看到正確的結果?
在Netflix的服務棧中,有一種名為“真實用戶監控(real user monitoring,簡稱RUM)”的服務,可以監控系統特征以及用戶體驗。RUM數據以流的形式,不斷地從各種客戶端異步地發送給Netflix的后臺服務,并在那里與同樣來自客戶端的用戶請求數據聯接(join),從中判斷用戶是否看到了正確的結果。同時,Netflix還使用一種帶故障點標記的分布式追蹤系統,這套系統可以判斷某個用戶當前是否進入了一個,由故障注入測試生成的故障站點,并能追蹤到當前測試的目標故障場景中,具體有哪些被注入的故障。
例如,如果一套Netflix的服務被部署到多個區域的不同可得帶(availability zone)中,那么這套服務必須擁有冗余性。當某個可得帶中的一項服務不再工作時,Netflix的Hystrix組件,會從代碼層面上自動向其他可得帶的同一服務重播請求,從而做到了時間上的冗余(redundancy through history),即多次請求返回一次正確結果。這種冗余性因其應用普遍,在Netflix服務的邏輯鏈條圖中,占到了相當大的份額。
Alvaro表示,唯有通過企業和學院的緊密合作,大家在共同目標的指引下求同存異,才有可能發現這樣創新的途徑。Andrus也強調說,只有一起協同工作,頻繁的討論以及在白板上交流想法,這樣才是有效的合作。
這就是并肩工作的優點。我們能夠討論各種各樣的問題,通過在白板上作圖解來提高溝通質量。這是寫email或者打電話無法做到的,只能依靠經常性的肩并肩工作。
隨著故障注入點和邏輯鏈條圖的確定,接下來的步驟是實現這套算法。作為學術派的Alvro決定證明一下自己的軟件研發功力,而他實現的算法現在已經被部署到Netflix生產環境下運行。
Alvaro:我自豪地宣布,我提交的代碼現在正跑在Netflix上!
Andrus:嚴格來說,是你提交的刨去這些println之后的代碼...
Alvaro:呃,抱歉我忘了刪了...
算法實現之后,整個項目到了最終的執行階段。每當有一個用戶請求進入系統,并且在處理過程中沒有引發任何故障,那么這個請求就會成為系統的一次“正確輸出”,并被故障注入測試系統在各種故障場景下重播。但是,在Netflix的分布式系統中,并不是每一種服務都具有等冪性(idempotent),因此有在確認不會造成意外后果之前,故障注入測試系統并不能簡單地重播所有能產生“正確輸出”的用戶請求。
解決這一問題的方法,并不是讓所有服務都擁有等冪性,而是將產生“正確輸出”的用戶請求進行分類;故障注入測試并不是重播所有能產生“正確輸出”的用戶請求,而是從每一類用戶請求中重播一個,從而將由等冪性缺失造成的意外后果降到最低。如果兩個用戶請求在系統中的處理路徑是相同的,或者出錯的路徑是相同的,那么從故障注入測試的角度來說,這兩個請求就是相同的,在故障注入測試時可歸為一類。然而,一個用戶請求在系統中的路徑,只有處理完才能知道,但兩個用戶請求是否屬于同一類必須在開始處理他們之前就決定,否則故障注入測試無法恰當地引導用戶請求進入故障場景。本質上,我們須要建立一種映射,將用戶請求映射到Netflix系統內的某條路徑上。
這種映射可能可以用機器學習來找,但從時間上來看這個方法并不可取。最終兩人找到一種替代方案,使用一套被稱作Falcor的框架(一套由Netflix開源,用來提高數據傳輸效率的JavaScript庫)來確定,某個用戶請求將會涉及到哪些后臺服務,從而近似地找到從用戶請求到系統內路徑的映射。雖然并不完美,Avlaro表示這一近似確實幫助他們將理論應用于現實,并有效地推進了整個項目的進行。在這套方案上線運行幾周之后,Alvaro和Andrus確認說,這條嶄新的故障注入測試的自動化方法是非常成功的。
在最后,兩位主講人還展示了一個名為“Netflix AppBoot”的用例分析。在這個用例中,最新的自動化故障注入測試被應用于一個在Netflix app啟動時所發出的用戶請求上,而這個用戶請求的故障搜索空間大約是100個服務。雖然徹底檢查整個故障搜索空間需要次測試,自動化故障注入測試篩選并最終只針對200個故障場景進行了測試,其測試結果幫助開發人員找到并修復了6個比較嚴重的問題。
從學院的角度來看,Alvaro后續可能會研究故障搜索的優先級,嘗試更復雜的邏輯鏈條,以及探索故障之間的時間交錯(temporal interleavings);而從企業的角度,Andrus日后會更專注于豐富設備的指標,尋求更有效的辦法來對用戶請求進行分類,以及優化測試選擇的策略。
Peter Alvaro和Kolton Andrus在QCon London演講的視頻可以在InfoQ上找到。
查看英文原文:“Monkeys in Labs Coats”: Applied Failure Testing Research at Netflix