當前很多應用都是data-intensive
型,而非compute-intensive
。如
- 存儲數據后供其他應用訪問(Database)
- 記錄一些比較重的操作的結果用以加速獲取操作(Cache)
- 支持用戶通過關鍵詞或者過濾器檢索數據(search index)
- 通過發消息到進程以異步化(stream processing)
- 定期獲取大量數據(batch processing)
這些(data-intensive
)應用各自擁有各自的特性,我們需要根據他們提供的能力來設計我們的應用,甚至在當前沒有符合我們使用場景的應用組件時,構建相應的組件。
關于數據系統的思考
我們是做業務系統開發的,為什么我們需要思考data-intensive
應用?
在過去,一個業務系統使用的數據應用可能比較單一,一個數據庫就可以完成全部數據的處理,表的join、唯一性控制、存儲優化、事務等等。現在來看,各種數據應用所承擔的責任越來越精專:rdbms、nosql、mq、cache、es,我們選擇每個數據應用時都要考慮這些應用的專長與缺陷。以至于業務系統開發時我們不得不去思考:如何保證緩存與主存的一致性、分布式事務方案選型、使用MQ異步化、高可用及降級。
很多方面都會影響一個數據系統的設計,包括但不限于:開發者經驗、依賴的遺留系統、交付周期、組織對各種風險的忍受力及其他限制。
這里主要關注三個層面:
- 可靠性
- 伸縮性
- 可維護性
可靠性
軟件系統關于可靠性的期望包括:
- 符合用戶預期的表現
- 是否能支持用戶的異常行為(輸入、操作等)
- 高負載、大數據量的情況下對要求的用戶場景是否能表現良好
- 系統可以防止越權(unauthorized access)和濫用(abuse)
簡言之:無論如何都能正常表現(continuing to work correctly, even when thingsgo wrong
)
正常情況下的符合預期是軟件系統最基本保證的。當系統出現問題時,我們稱之fault,對于能夠抵御錯誤的系統我們稱之容錯能力。但是并非所有的問題都可以被抵銷或避免,比如說地球毀滅。所以我們所說的容錯也指特定類型。這里還要區分fault
和failure
: -
fault
:也是在spec中描述的。fault不能百分之百避免,所以需要系統能夠處理可預知的fault,防止fault導致failure -
failure
:在意料之外的,當failure出現時,系統將不再對外提供服務
和直覺相反的是,對于容錯能力比較強的系統是經常觸發一些fault(斷網演練、隨機切斷進程等)的系統,只有通過觸發fault才能夠檢驗容錯機制是否可靠,很多時候系統問題來自于對于異常的處理之上。就像Netflix引入chaos monkey隨機產生問題來驗證系統的容錯能力。
硬件錯誤
據報告稱一個硬件發生問題的平均時長為10-50年,對于一個10000塊磁盤的集群來說,按概率平均一天就會有一塊磁盤發生問題。
對于這類問題,常規解決方案就是冗余,比如說通過RAID卡、備用電源、可熱替換CPU等方式,這些方法不能避免硬件發生問題,而是在發生問題時能夠讓運行其上的系統無感知硬件問題以長時間運行。
隨著集群規模的擴大,硬件故障發生的越來越頻繁,也越來越傾向使用軟件或系統設計方式來解決硬件故障,如數據中心、多機房、軟負載等。
軟件錯誤
硬件問題大多隨機發生,但是大面積的硬件問題同時爆發確實小概率事件。軟件系統異常則更難預測,且通常一個軟件問題可能會關聯其他系統、節點服務,導致大面積不可用。如:
- 由于軟件bug某個特定輸入導致所有節點崩潰(如之前Stack Overflow發生的regex解析cpu耗時過高)。
- 失控的線程消耗過多的系統資源
- 依賴服務耗時過多拖垮當前應用,或者返回異常信息對當前應用造成影響
- 雪崩。一個小的問題引發一連串的故障,從而導致整個服務不可用
通常導致這些問題的bug在系統中隱藏很久,當特定的實際時被觸發。一個系統運行正常是基于一些假設,通常在這些假定的環境下系統是不會發生問題的,而且在一段時間內這些假定也是成立的,不過當這些假定不成立時,就可能發生一些不可知的情況。
沒有一個好的方案避免上述所有問題,不過一些小細節可以帶來些助益:
- 仔細思考與系統交互的環境與場景
- 完備的測試
- process isolation
- 進程能夠崩潰及重啟 #對于不允許崩潰的系統,就要保證足夠健壯,否則墨菲定律教你做人
- 生產環境下監控、測量、分析
如果一個系統確保其行為可靠,則需要時常進行自檢,且當出現與預期不符是能夠及時告警。
人為錯誤
系統的問題歸根結底是人的問題。
人設計、搭建系統,維護系統運轉的也是人。
基于不可靠的「人」如何設計、構建可靠系統:
- 設計系統以盡可能低的可能發生錯誤。如:設計良好的抽象、API能夠讓使用者容易選擇正確的姿勢而非錯誤的方式。但是如果接口設計太死,用戶則會嘗試使用他們更舒服的姿勢包裝(work around)再使用,這點是需要權衡的。
- 將最可能發生問題的行為與最可能導致故障環境隔離。比如構建預發環境,避免生產環境測試。
- 完備的測試。從單元測試到系統集成測試以及人工測試。自動化測試不僅覆蓋大部分場景,也要覆蓋那些比較少觸發的邊緣場景。
- 能夠簡單、快速地從人為異常中恢復,將異常所造成的影響降低到最小。如提供配置回滾機制、灰度發布、一些工具能夠對消息進行重演來修復數據計算異常等。
- 構建清晰詳盡異常監控,包括但不限性能指標、錯誤率等。監控可以在異常發生前進行預警,在異常發生后幫助分析
- 項目管理
為什么可靠性很重要:
伸縮性
一個當前穩定的系統在將來不一定仍舊穩定,除非是一個非核心應用或者一個不再有新需求或用戶增長的產品。當一個產品承載的請求量從1kw增長到1b,原有應用服務一定會遇到挑戰。
伸縮性
指當應用請求增長時,可以通過一定手段使應用能夠繼續承載新的流量壓力。我們需要常常問自己:當系統擴張、用戶請求增長、數據增多,我們該如何應對?
負載
QPS?IOPS?并發請求數?緩存命中?這些指標都是單一層面,對于一個系統而言需要從更全局的角度來度量。以twitter為例,主要操作包括:
- 發推:用戶發送一個推文到其跟隨者(followers)(4.6k均值QPS,12k峰值QPS)
- 時間線:用戶可以看到他關注者的推文列表(300k QPS)
如果單純處理12kQPS寫入操作其實很簡單,但是Twitter面臨的伸縮性挑戰不是推文容量,而是fan-out:每個用戶關注多個用戶,每個用戶也被很多用戶所關注。上述兩個操作真正實現有兩種:
- 用戶發推文,將其存儲到集中存儲中。當用戶拉取自己的時間線時,需要將自己關注的所有用戶的推文拉取出來,并且按照一定的規則進行排序。
- 維護一個緩存來存儲用戶時間限,當用戶發表新推文,則查找所有關注該用戶跟隨者的時間線緩存,并更新該推文。
當然實際Twitter并不是這樣實現,但是可以作為一個典型的場景來說明負載不是單純的來自于入口側的流量,與具體的業務場景及實現方式有關。
性能
負載是外部因素,性能是具體表現。當負載發生變化時,可以用以下兩種方式表述系統的:
- 負載升高系統資源不調整,此時系統性能如何?
- 負載升高,通過增加多少資源可以保持系統性能保持恒定?
性能也是一系列指標,如web應用為平均請求耗時、數據庫為讀寫速率、大數據應用為吞吐量。
通常有一系列數值可以用來衡量性能,如:
- 平均數:最常用指標,但是并不能完全反映實際情況
- 中位數:用以反映半數用戶實際獲得的性能
- 99/95百分位數:絕大部分占比用戶實際獲得性能??梢员苊庖恍O端情況將整體均值拉大
- 高位:通常被稱為尾部延遲(tail lantencies)。雖然這些請求占比很低,但是通常是一些臨界情況導致,如一些大商家數據、大批量請求、訪問部分問題機器等。對于大商家用戶的請求需要格外關注,如果忽略尾部延遲的話可能會造成大商家用戶體驗喪失??蛻舳送ǔ6紩褂弥卦囎鳛閒ailover機制,當請求超時時可能會觸發重試,對于尾部延遲由于客戶端重試可能會進一步增加超時請求的觸發,此現象被稱為:
尾部延遲放大
(tail lantency amplification)
大規模集群的數值統計一定要有各自服務的統計數據,全局平均會掩蓋部分機器的異常。
通常會使用SLO/SLA來約定服務性能,因為一些隨機事件就是可能會導致系統出現各種各樣的問題。對于性能的追求也要考慮投入產出比。
注意響應時間測量不能只測量server端的處理時間,因為有些慢請求可能會導致請求堆積,對于服務端而言小部分請求處理時間長,后續請求處理時間很短,但是對于客戶端而言大部分的請求的耗時因為堆積而變長。因而可以考慮記錄客戶端請求的時延,可以保證最真實的服務質量。
壓測時也要注意各自壓測請求的獨立,等待前次響應返回后再觸發后續可能會導致壓測流量下降。
如何處理負載
對于處理不同負載指標的系統其架構設計、技術選型、部署方式以及后續的擴展方式(scale-out/scale-up)都會有所不同。一個系統是否能夠適于拓展,來自于對負載場景的分析,如果基于一個錯誤的假設(如后續流量瓶頸在本機CPU算力,但是實際在磁盤IO),好一點來說整個擴展方案僅僅是浪費資源,糟糕的則會適得其反。
可維護性
一個軟件最愉悅的時間莫過于在起初開發的過程中,但是最最耗費精力階段是在上線后的維護,問題處理、保證運行、適配新平臺、添加新特性、償還技術債務等。
幾乎大部分人都不喜歡與遺留系統打交道,修改其他人的bug、維護陳年代碼、使用了一個幾乎沒人了解過往技術棧。但是轉念想想,一個系統一旦開發發布上線之后,立馬就變成了別人的遺留系統。如果沒有良好設計,那么在別人接手之前,你需要維護你這坨狗屎(如果確實是狗屎)。所以在軟件系統實際是,需要對三個設計原則格外關注:
- 可操作性(Operability):便于運維同學方便保證系統平滑運行
- 簡單:可以讓新的工程師很好的了解該系統,盡可能的將所有復雜邏輯移除。(與接口的簡單性是不同的)
- 可進化(Evolvability):新增功能、系統變更、適配未知需求等都很方便。也稱為:可擴展性、可適配變化性、柔性等
可操作性:讓運維更美好
Good operations can often work around the limitations of bad software, but good software cannot run reliably with bad operations
簡言之:有時候良好的操作性比好的軟件(設計)更重要。
運維團隊(不僅僅指ops,還包含devs)對于系統是否能良好執行也起到至關重要的作用,其職責通常包括:
- 監控系統健康情況,在系統異常時能夠迅速反應
- 當系統異?;蛘咝阅芟陆禃r,能夠追根溯源
- 保持系統、平臺更新,包括最新安全補丁
- 了解系統間關系,當可能有問題變更發生時,能夠在真正發生問題前避免。
- 預防可能發生的問題(容量預估)
- 良好的發布、配置變更等工具
- 執行復雜的維護任務,如將應用從一個平臺遷移到另一個平臺
- 流程定義,以使操作可期,保證環境穩定
- 維護組織信息,以便人員流動時仍能夠傳承
高可操作性意味著日常工作處理順暢,可以讓運維團隊專注于更有價值的工作。同時數據系統也能提供相應的支持,如:
- 提供運行時行為、系統內運行態可視化,以及相應的監控服務
- 避免單點依賴,允許節點動態摘除仍保證系統運行穩定
- 使用標準工具提供自動化及集成支持
- 良好文檔及易懂的運維模型(如果操作X,那么Y將發生)
- 提供可靠的默認行為,同時提供管理員足夠權限
- 可期的行為可以自愈,當系統異常時(給予足夠權限)系統管理員能夠介入處理
- 行為可期,避免意外(「驚喜」)
簡單:處理復雜
當需求不斷擴充、系統變大時,必然會由簡單變得復雜。一個系統變得復雜的表現通常為:
狀態的膨脹、模型緊耦合、糾纏不清的依賴、不一致的命名和術語、為了解決性能問題而做的hack、特殊場景的兼容方案等等。
系統變得復雜以后,新的變更將會更易引入問題。當系統讓開發者難以理解,一些潛規則、未預料的后果、不可預期交互更容易被忽視。因而讓系統變得簡單應當是我們很關鍵的目標。
Moseley和Marks如下定義復雜:
Complex as accidental if it is not inherent in the problem that the software solves(as seen by the users) but arise only from the implementation.
當一個軟件產生了或解決了原本不是這個軟件應該提供給用戶的功能或問題解決方案時,那么這些問題就是復雜度所引起的。
解決意外復雜度的一個利器是抽象
。抽象隱藏了實現細節,提供了簡單易懂的接口,同時可以在很多場合進行復用。不僅僅是因為復用避免了重復開發所引入的新問題,更可以通過對同一抽象的優化可以實現使用該抽象的全部優化。抽象也可以讓我們避免去理解那些不需要理解的細節內容,如高級語言將機器語言進行封裝,對于一般的軟件開發者而言,使用Java并不需要關心其機器碼,可以讓開發人員更聚焦。
可擴展性(Evolvability):讓變更更容易
系統不可能一成不變:過去為預料的場景發生了、新的業務需求、平臺更替、架構升級等等。
通過引入敏捷可以協助完成變更,同時進可能避免問題發生。如引入TDD(Test-Driven Development)及重構。
如上所說,簡單及抽象可以保證變更的可預期,所以一個擴展性良好的系統也必然是足夠抽象簡單的。
小結
-
可靠性
:即便當問題發生時,系統也能正常運作。異常可能發生自硬件、軟件甚至更多來自人為。容錯技術可以在問題發生時讓用戶無感知 -
可擴展性
:當負載升高,系統也能通過相應策略保證性能穩定。討論可擴展性之前,需要先明確負載
及性能
分表代表什么。 -
可維護性
:簡言之就是讓運維及開發在處理系統時能更從容。好的抽象能夠降低復雜度,讓系統更易于變更及添加新功能。系統指標數據可視化也便于系統的維護。