效能優化模式

發表於2015-12-10

摘要

效能優化涉及面很廣。一般而言,效能優化指降低響應時間和提高系統吞吐量兩個方面,但在流量高峰時候,效能問題往往會表現為服務可用性下降,所以效能優化也可以包括提高服務可用性。在某些情況下,降低響應時間、提高系統吞吐量和提高服務可用性三者相互矛盾,不可兼得。例如:增加快取可以降低平均響應時間,但是處理執行緒數量會因為快取過大而有所限制,從而降低系統吞吐量;為了提高服務可用性,對異常請求重複呼叫是一個常用的做法,但是這會提高響應時間並降低系統吞吐量。

對於很多像美團這樣的公司,它們的系統會面臨如下三個挑戰:1. 日益增長的使用者數量,2. 日漸複雜的業務,3. 急劇膨脹的資料。這些挑戰對於效能優化而言表現為:在保持和降低系統TP95響應時間(指的是將一段時間內的請求響應時間從低到高排序,高於95%請求響應時間的下確界)的前提下,不斷提高系統吞吐量,提升流量高峰時期的服務可用性。這種場景下,三者的目標和改進方法取得了比較好的一致。本文主要目標是為類似的場景提供優化方案,確保系統在流量高峰時期的快速響應和高可用。

文章第一部分是介紹,包括採用模式方式講解的優點,文章所採用案例的說明,以及後面部分用到的一些設計原則;第二部分介紹幾種典型的“效能惡化模式”,闡述導致系統效能惡化,服務可用性降低的典型場景以及形成惡化迴圈的過程;第三部分是文章重點,闡述典型的“效能優化模式”,這些模式或者可以使服務遠離“惡化模式”,或者直接對服務效能進行優化;文章最後一部分進行總結,並對未來可能出現的新模式進行展望。


介紹

模式講解方式

關於效能優化的文章和圖書已有很多,但就我所知,還沒有采用模式的方式去講解的。本文借鑑《設計模式》(“Design Patterns-Elements of Reusable Object-Oriented Software”)對設計模式的闡述方式,首先為每一種效能優化模式取一個貼切的名字,便於讀者快速理解和深刻記憶,接著講解該模式的動機和原理,然後結合作者在美團的具體工作案例進行深度剖析,最後總結采用該模式的優點以及需要付出的代價。簡而言之,本文采用“命名–>原理和動機–>具體案例–>缺點和優點”的四階段方式進行效能優化模式講解。與其他方式相比,採用模式進行講解有兩個方面的優點:一方面,讀者不僅僅能夠掌握優化手段,而且能夠了解採用該手段進行效能優化的場景以及所需付出的代價,這有利於讀者全面理解和靈活應用;另一方面,模式解決的是特定應用場景下的一類問題,所以應用場景描述貫穿於模式講解之中。如此,即使讀者對原理不太瞭解,只要碰到的問題符合某個特定模式的應用場景(這往往比理解原理要簡單),就可以採用對應的手段進行優化,進一步促進讀者對模式的理解和掌握。

案例說明

文章的所有案例都來自於美團的真實專案。出於兩方面的考慮,作者做了一定的簡化和抽象:一方面,系統可以優化的問題眾多,而一個特定的模式只能解決幾類問題,所以在案例分析過程中會突出與模式相關的問題;另一方面,任何一類問題都需要多維度資料去描述,而應用效能優化模式的前提是多維度資料的組合值超過了某個臨界點,但是精確定義每個維度數值的臨界點是一件很難的事情,更別說多維度資料組合之後臨界點。因此有必要對案例做一些簡化,確保相關取值範圍得到滿足。基於以上以及其他原因,作者所給出的解決方案只是可行性方案,並不保證其是所碰到問題的最佳解決方案。

案例涉及的所有專案都是基於Java語言開發的,嚴格地講,所有模式適用的場景是基於Java語言搭建的服務。從另外一方面講,Java和C++的主要區別在於垃圾回收機制,所以,除去和垃圾回收機制緊密相關的模式之外,文章所描述的模式也適用於採用C++語言搭建的服務。對於基於其他語言開發的服務,讀者在閱讀以及實踐的過程中需要考慮語言之間的差別。

設計原則

必須說明,本文中各種模式所要解決的問題之所以會出現,部分是因為工程師運用了某些深層次的設計原則。有些設計原則看上去和優秀的設計理念相悖,模式所解決的問題似乎完全可以避免,但是它們卻被廣泛使用。“存在即合理”,世界上沒有完美的設計方案,任何方案都是一系列設計原則的妥協結果,所以本文主要關注點是解決所碰到的問題而不是如何繞過這些設計原則。下面對文中重要的設計原則進行詳細闡述,在後面需要運用該原則時將不再解釋。

最小可用原則

最小可用原則(快速接入原則)有兩個關注點:1. 強調快速接入,快速完成;2. 實現核心功能可用。這是一個被普遍運用的原則,其目標是縮短測試周期,增加試錯機會,避免過度設計。為了快速接入就必須最大限度地利用已有的解決方案或系統。從另外一個角度講,一個解決方案或系統只要能夠滿足基本需求,就滿足最小可用原則的應用需求。過度強調快速接入原則會導致重構風險的增加,原則上講,基於該原則去設計系統需要為重構做好準備。

經濟原則

經濟原則關注的是成本問題,看起來很像最小可用原則,但是它們之間關注點不同。最小可用原則的目標是通過降低開發週期,快速接入而實現風險可控,而快速接入並不意味著成本降低,有時候為了實現快速接入可能需要付出巨大的成本。軟體專案的生命週期包括:預研、設計、開發、測試、執行、維護等階段。最小可用原則主要運用在預言階段,而經濟原則可以運用在整個軟體生命週期裡,也可以只關注某一個或者幾個階段。例如:執行時經濟原則需要考慮的系統成本包括單次請求的CPU、記憶體、網路、磁碟消耗等;設計階段的經濟原則要求避免過度設計;開發階段的經濟原則可能關注程式碼複用,工程師資源複用等。

程式碼複用原則

程式碼複用原則分為兩個層次:第一個層次使用已有的解決方案或呼叫已存在的共享庫(Shared Library),也稱為方案複用;第二個層次是直接在現有的程式碼庫中開發,也稱之為共用程式碼庫。

方案複用是一個非常實用主義的原則,它的出發點就是最大限度地利用手頭已有的解決方案,即使這個方案並不好。方案的形式可以是共享庫,也可以是已存在的服務。方案複用的例子參見避免蚊子大炮模式的具體案例。用搜尋引擎服務來解決查詢附近商家的問題是一個效能很差的方案,但仍被很多工程師使用。方案複用原則的一個顯著優點就是提高生產效率,例如:Java之所以能夠得到如此廣泛應用,原因之一就是有大量可以重複利用的開源庫。實際上“Write once, run anywhere”是Java語言最核心的設計理念之一。基於Java語言開發的程式碼庫因此得以在不同硬體平臺、不同作業系統上更廣泛地使用。

共用程式碼庫要求在同一套程式碼庫中完成所有功能開發。採用這個原則,程式碼庫中的所有功能編譯時可見,新功能程式碼可以無邊界的呼叫老程式碼。另外,原始碼庫已存在的各種執行、編譯、測試、配置環境可複用。主要有兩個方面地好處:1. 充分利用程式碼庫中已有的基礎設施,快速接入新業務;2. 直接呼叫原始碼中的基礎功能或原語,避免網路或程式間呼叫開銷,效能更佳。共用程式碼庫的例子參見垂直分割模式的具體案例。

從設計的角度上講,方案複用類似於微服務架構(Microservice Architecture,有些觀點認為這是一種形式的SOA),而共用程式碼庫和Monolithic Architecture很接近。總的來說,微服務傾向於面向介面程式設計,要求設計出可重用性的元件(Library或Service),通過分層組織各層元件來實現良好的架構。與之相對應,Monolith Architecture則希望儘可能在一套程式碼庫中開發,通過直接呼叫程式碼中的基礎功能或原語而實現效能的優化和快速迭代。使用Monolith Architecture有很大的爭議,被認為不符合“設計模式”的理念。參考文獻[4],Monolithic Design主要的缺點包括:1. 缺乏美感;2. 很難重構;3. 過早優化(參見文獻[6]Optimize judiciously); 4. 不可重用;5. 限制眼界。微服務架構是很多網際網路公司的主流架構,典型的運用公司包括Amazon、美團等。Monolithic Architecture也有其忠實的粉絲,例如:Tripadvisor的全球網站就共用一套程式碼庫;基於效能的考慮,Linux最終選擇的也是Monolithic kernel的模式。

奧卡姆剃刀原則

系統設計以及程式碼編寫要遵循奧卡姆剃刀原則:Entities should not be multiplied unnecessarily。一般而言,一個系統的程式碼量會隨著其功能增加而變多。系統的健壯性有時候也需要通過編寫異常處理程式碼來實現。異常考慮越周全,異常處理程式碼量越大。但是隨著程式碼量的增大,引入Bug的概率也就越大,系統也就越不健壯。從另外一個角度來講,異常流程處理程式碼也要考慮健壯性問題,這就形成了無限迴圈。所以在系統設計和程式碼編寫過程中,奧卡姆剃刀原則要求:一個功能模組如非必要,就不要;一段程式碼如非必寫,就不寫。

奧卡姆剃刀原則和最小可用原則有所區別。最小可用原則主要運用於產品MVP階段,本文所指的奧卡姆剃刀原則主要指系統設計和程式碼編寫兩個方面,這是完全不同的兩個概念。MVP包含系統設計和程式碼編寫,但同時,系統設計和程式碼編寫也可以發生在成熟系統的迭代階段。


效能惡化模式

在講解效能優化模式之前,有必要先探討一下效能惡化模式,因為:

  1. 很多效能優化模式的目標之一就是避免系統進入效能惡化模式;
  2. 不同效能優化模式可能是避免同一種效能惡化模式;
  3. 同一種效能優化模式可能在不同階段避免不同的效能惡化模式。
    在此統一闡述效能惡化模式,避免下文重複解釋。為了便於讀者清晰識別惡化模式和優化模式,惡化模式採用“XXX反模式”的方式進行命名。

長請求擁塞反模式(High Latency Invocating AntiPattern)

這是一種單次請求時延變長而導致系統效能惡化甚至崩潰的惡化模式。對於多執行緒服務,大量請求時間變長會使執行緒堆積、記憶體使用增加,最終可能會通過如下三種方式之一惡化系統效能:

  1. 執行緒數目變多導致執行緒之間CPU資源使用衝突,反過來進一步延長了單次請求時間;
  2. 執行緒數量增多以及執行緒中快取變大,記憶體消耗隨之劇增,對於基於Java語言的服務而言,又會更頻繁地full GC,反過來單次請求時間會變得更長;
  3. 記憶體使用增多,會使作業系統記憶體不足,必須使用Swap,可能導致服務徹底崩潰。
    典型惡化流程圖如下圖:

長請求擁塞反模式所導致的效能惡化現象非常普遍,所以識別該模式非常重要。典型的場景如下:某複雜業務系統依賴於多個服務,其中某個服務的響應時間變長,隨之系統整體響應時間變長,進而出現CPU、記憶體、Swap報警。系統進入長請求擁塞反模式的典型標識包括:被依賴服務可用性變低、響應時間變長、服務的某段計算邏輯時間變長等。

多次請求槓桿反模式(Levered Multilayer Invocating AntiPattern)

客戶端一次使用者點選行為往往會觸發多次服務端請求,這是一次請求槓桿;每個服務端請求進而觸發多個更底層服務的請求,這是第二次請求槓桿。每一層請求可能導致一次請求槓桿,請求層級越多,槓桿效應就越大。在多次請求槓桿反模式下執行的分散式系統,處於深層次的服務需要處理大量請求,容易會成為系統瓶頸。與此同時,大量請求也會給網路帶來巨大壓力,特別是對於單次請求資料量很大的情況,網路可能會成為系統徹底崩潰的導火索。典型惡化流程圖如下圖:

多次請求槓桿所導致的效能惡化現象非常常見,例如:對於美團推薦系統,一個使用者列表請求會有多個演算法參與,每個演算法會召回多個列表單元(商家或者團購),每個列表單元有多種屬性和特徵,而這些屬性和特徵資料服務又分佈在不同服務和機器上面,所以客戶端的一次使用者展現可能導致了成千上萬的最底層服務呼叫。對於存在多次請求槓桿反模式的分散式系統,效能惡化與流量之間往往遵循指數曲線關係。這意味著,在平常流量下正常執行服務系統,在流量高峰時通過線性增加機器解決不了可用性問題。所以,識別並避免系統進入多次請求槓桿反模式對於提高系統可用性而言非常關鍵。

反覆快取反模式(Recurrent Caching AntiPattern)

為了降低響應時間,系統往往在本地記憶體中快取很多資料。快取資料越多,命中率就越高,平均響應時間就越快。為了降低平均響應時間,有些開發者會不加限制地快取各種資料,在正常流量情況下,系統響應時間和吞吐量都有很大改進。但是當流量高峰來臨時,系統記憶體使用開始增多,觸發了JVM進行full GC,進而導致大量快取被釋放(因為主流Java記憶體快取都採用SoftReference和WeakReference所導致的),而大量請求又使得快取被迅速填滿,這就是反覆快取。反覆快取導致了頻繁的full GC,而頻繁full GC往往會導致系統效能急劇惡化。典型惡化流程圖如下圖:

反覆快取所導致效能惡化的原因是無節制地使用快取。快取使用的指導原則是:工程師們在使用快取時必須全域性考慮,精細規劃,確保資料完全快取的情況下,系統仍然不會頻繁full GC。為了確保這一點,對於存在多種型別快取以及系統流量變化很大的系統,設計者必須嚴格控制快取大小,甚至廢除快取(這是典型為了提高流量高峰時可用性,而降低平均響應時間的一個例子)。反覆快取反模式往往發生在流量高峰時候,通過線性增加機器和提高機器記憶體可以大大減少系統崩潰的概率。


效能優化模式

水平分割模式(Horizontal partitioning Pattern)

原理和動機

典型的服務端執行流程包含四個環節:接收請求、獲取資料、處理資料、返回結果。在一次請求中,獲取資料和處理資料往往多次發生。在完全序列執行的系統裡,一次請求總響應時間滿足如下公式:

一次請求總耗時=解析請求耗時 + ∑(獲取資料耗時+處理資料耗時) + 組裝返回結果耗時

大部分耗時長的服務主要時間都花在中間兩個環節,即獲取資料和處理資料環節。對於非計算密集性的系統,主要耗時都用在獲取資料上面。獲取資料主要有三個來源:本地快取,遠端快取或者資料庫,遠端服務。三者之中,進行遠端資料庫訪問或遠端服務呼叫相對耗時較長,特別是對於需要進行多次遠端呼叫的系統,序列呼叫所帶來的累加效應會極大地延長單次請求響應時間,這就增大了系統進入長請求擁塞反模式的概率。如果能夠對不同的業務請求並行處理,請求總耗時就會大大降低。例如下圖中,Client需要對三個服務進行呼叫,如果採用順序呼叫模式,系統的響應時間為18ms,而採用並行呼叫只需要7ms。

水平分割模式首先將整個請求流程切分為必須相互依賴的多個Stage,而每個Stage包含相互獨立的多種業務處理(包括計算和資料獲取)。完成切分之後,水平分割模式序列處理多個Stage,但是在Stage內部並行處理。如此,一次請求總耗時等於各個Stage耗時總和,每個Stage所耗時間等於該Stage內部最長的業務處理時間。

水平分割模式有兩個關鍵優化點:減少Stage數量和降低每個Stage耗時。為了減少Stage數量,需要對一個請求中不同業務之間的依賴關係進行深入分析並進行解耦,將能夠並行處理的業務儘可能地放在同一個Stage中,最終將流程分解成無法獨立執行的多個Stage。降低單個Stage耗時一般有兩種思路:1. 在Stage內部再嘗試水平分割(即遞迴水平分割),2. 對於一些可以放在任意Stage中進行並行處理的流程,將其放在耗時最長的Stage內部進行並行處理,避免耗時較短的Stage被拉長。

水平分割模式不僅可以降低系統平均響應時間,而且可以降低TP95響應時間(這兩者有時候相互矛盾,不可兼得)。通過降低平均響應時間和TP95響應時間,水平分割模式往往能夠大幅度提高系統吞吐量以及高峰時期系統可用性,並大大降低系統進入長請求擁塞反模式的概率。

具體案例

我們的挑戰來自為使用者提供高效能的優質個性化列表服務,每一次列表服務請求會有多個演算法參與,而每個演算法基本上都採用“召回->特徵獲取->計算”的模式。 在進行效能優化之前,演算法之間採用順序執行的方式。伴隨著演算法工程師的持續迭代,演算法數量越來越多,隨之而來的結果就是客戶端響應時間越來越長,系統很容易進入長請求擁塞反模式。曾經有一段時間,一旦流量高峰來臨,出現整條服務鏈路的機器CPU、記憶體報警。在對系統進行分析之後,我們採取瞭如下三個優化措施,最終使得系統TP95時間降低了一半:

  1. 演算法之間平行計算;
  2. 每個演算法內部,多次特徵獲取進行了並行處理;
  3. 在排程執行緒對工作執行緒進行排程的時候,耗時最長的執行緒最先排程,最後處理。

缺點和優點

對成熟系統進行水平切割,意味著對原系統的重大重構,工程師必須對業務和系統非常熟悉,所以要謹慎使用。水平切割主要有兩方面的難點:

  1. 平行計算將原本單一執行緒的工作分配給多執行緒處理,提高了系統的複雜度。而多執行緒所引入的安全問題讓系統變得脆弱。與此同時,多執行緒程式測試很難,因此重構後系統很難與原系統在業務上保持一致。
  2. 對於一開始就基於單執行緒處理模式編寫的系統,有些流程在邏輯上能夠並行處理,但是在程式碼層次上由於相互引用已經難以分解。所以並行重構意味著對共用程式碼進行重複撰寫,增大系統的整體程式碼量,違背奧卡姆剃刀原則。
    對於上面提到的第二點,舉例如下:A和B是邏輯可以並行處理的兩個流程,基於單執行緒設計的程式碼,假定處理完A後再處理B。在編寫處理B邏輯程式碼時候,如果B需要的資源已經在處理A的過程中產生,工程師往往會直接使用A所產生的資料,A和B之間因此出現了緊耦合。並行化需要對它們之間的公共程式碼進行拆解,這往往需要引入新的抽象,更改原資料結構的可見域。

在如下兩種情況,水平切割所帶來的好處不明顯:

  1. 一個請求中每個處理流程需要獲取和快取的資料量很大,而不同流程之間存在大量共享的資料,但是請求之間資料共享卻很少。在這種情況下,流程處理完之後,資料和快取都會清空。採用順序處理模式,資料可以被快取線上程區域性儲存(ThreadLocal)中而減少重複獲取資料的成本;如果採用水平切割的模式,在一次請求中,不同流程會多次獲取並快取的同一型別資料,對於記憶體原本就很緊張的系統,可能會導致頻繁full GC,進入反覆快取反模式。
  2. 某一個處理流程所需時間遠遠大於其他所有流程所需時間的總和。這種情況下,水平切割不能實質性地降低請求響應時間。

採用水平切割的模式可以降低系統的平均響應時間和TP95響應時間,以及流量高峰時系統崩潰的概率。雖然進行程式碼重構比較複雜,但是水平切割模式非常容易理解,只要熟悉系統的業務,識別出可以並行處理的流程,就能夠進行水平切割。有時候,即使少量的並行化也可以顯著提高整體效能。對於新系統而言,如果存在可預見的效能問題,把水平分割模式作為一個重要的設計理念將會大大地提高系統的可用性、降低系統的重構風險。總的來說,雖然存在一些具體實施的難點,水平分割模式是一個非常有效、容易識別和理解的模式。

垂直分割模式(Vertical partitioning Pattern)

原理和動機

對於移動網際網路節奏的公司,新需求往往是一波接一波。基於程式碼複用原則,工程師們往往會在一個系統實現大量相似卻完全不相干的功能。伴隨著功能的增強,系統實際上變得越來越脆弱。這種脆弱可能表現在系統響應時間變長、吞吐量降低或者可用性降低。導致系統脆弱原因主要來自兩方面的衝突:資源使用衝突和可用性不一致衝突。

資源使用衝突是導致系統脆弱的一個重要原因。不同業務功能並存於同一個執行系統裡面意味著資源共享,同時也意味著資源使用衝突。可能產生衝突的資源包括:CPU、記憶體、網路、I/O等。例如:一種業務功能,無論其呼叫量多麼小,都有一些記憶體開銷。對於存在大量快取的業務功能,業務功能數量的增加會極大地提高記憶體消耗,從而增大系統進入反覆快取反模式的概率。對於CPU密集型業務,當產生衝突的時候,響應時間會變慢,從而增大了系統進入長請求擁塞反模式的可能性。

不加區別地將不同可用性要求的業務功能放入一個系統裡,會導致系統整體可用性變低。當不同業務功能糅合在同一執行系統裡面的時候,在運維和機器層面對不同業務的可用性、可靠性進行調配將會變得很困難。但是,在高峰流量導致系統瀕臨崩潰的時候,最有效的解決手段往往是運維,而最有效手段的失效也就意味著核心業務的可用性降低。

垂直分割思路就是將系統按照不同的業務功能進行分割,主要有兩種分割模式:部署垂直分割和程式碼垂直分割。部署垂直分割主要是按照可用性要求將系統進行等價分類,不同可用性業務部署在不同機器上,高可用業務單獨部署;程式碼垂直分割就是讓不同業務系統不共享程式碼,徹底解決系統資源使用衝突問題。

具體案例

我們的挑戰來自於美團推薦系統,美團客戶端的多個頁面都有推薦列表。雖然不同的推薦產品需求來源不同,但是為了實現快速的接入,基於共用程式碼庫原則,所有的推薦業務共享同一套推薦程式碼,同一套部署。在一段時間內,我們發現push推薦和首頁“猜你喜歡推薦”的資源消耗巨大。特別是在push推薦的高峰時刻,CPU和記憶體頻繁報警,系統不停地full GC,造成美團使用者進入客戶端時,首頁出現大片空白。

在對系統進行分析之後,得出兩個結論:

  1. 首頁“猜你喜歡”對使用者體驗影響更大,應該給予最高可用性保障,而push推薦給予較低可用性保障;
  2. 首頁“猜你喜歡”和push推薦都需要很大的本地快取,有較大的記憶體使用衝突,並且響應時間都很長,有嚴重的CPU使用衝突。

因此我們採取瞭如下措施,一方面,解決了首頁“猜你喜歡”的可用性低問題,減少了未來出現可用性問題的概率,最終將其TP95響應時間降低了40%;另一方面也提高了其他推薦產品的服務可用性和高峰吞吐量。

  1. 將首頁“猜你喜歡”推薦進行單獨部署,而將push推薦和其他對系統資源要求不高的推薦部署在另一個叢集上面;
  2. 對於新承接的推薦業務,新建一套程式碼,避免影響首頁推薦這種最高可用性的業務。

缺點和優點

垂直分割主要的缺點主要有兩個:

  1. 增加了維護成本。一方面程式碼庫數量增多提高了開發工程師的維護成本,另一方面,部署叢集的變多會增加運維工程師的工作量;
  2. 程式碼不共享所導致的重複編碼工作。

解決重複編碼工作問題的一個思路就是為不同的系統提供共享庫(Shared Library),但是這種耦合反過來可能導致部署機器中引入未部署業務的開銷。所以在共享庫中要減少靜態程式碼的初始化開銷,並將類似快取初始化等工作交給上層系統。總的來說,通過共享庫的方式引入的開銷可以得到控制。但是對於業務密集型的系統,由於業務往往是高度定製化的,共用一套程式碼庫的好處是開發工程師可以採用Copy-on-write的模式進行開發,需要修改的時候隨時拷貝並修改。共享庫中應該存放不容易變化的程式碼,避免使用者頻繁升級,所以並不適合這種場景。因此,對於業務密集型的系統,分程式碼所導致的重複編碼量是需要權衡的一個因素。

垂直分割是一個非常簡單而又有效的效能優化模式,特別適用於系統已經出現問題而又需要快速解決的場景。部署層次的分割既安全又有效。需要說明的是部署分割和簡單意義上的加機器不是一回事,在大部分情況下,即使不增加機器,僅通過部署分割,系統整體吞吐量和可用性都有可能提升。所以就短期而言,這幾乎是一個零成本方案。對於程式碼層次的分割,開發工程師需要在業務承接效率和系統可用性上面做一些折衷考慮。

恆變分離模式(Runtime 3NF Pattern)

原理和動機

基於效能的設計要求變化的資料和不變的資料分開,這一點和基於物件導向的設計原則相悖。在物件導向的設計中,為了便於對一個物件有整體的把握,緊密相關的資料集合往往被組裝進一個類,儲存在一個資料庫表,即使有部分資料冗餘(關於物件導向與效能衝突的討論網上有很多文章,本文不細講)。很多系統的主要工作是處理變化的資料,如果變化的資料和不變的資料被緊密組裝在一起,系統對變化資料的操作將引入額外的開銷。而如果易變資料佔總資料比例非常小,這種額外開銷將會通過槓桿效應惡化系統效能。分離易變和恆定不變的資料在物件建立、記憶體管理、網路傳輸等方面都有助於效能提高。

恆變分離模式的原理非常類似與資料庫設計中的第三正規化(3NF):第三正規化主要解決的是靜態儲存中重複儲存的問題,而恆變分離模式解決的是系統動態執行時候恆定資料重複建立、傳輸、儲存和處理的問題。按照3NF,如果一個資料表的每一記錄都依賴於一些非主屬性集合,而這些非主屬性集合大量重複出現,那麼應該考慮對被依賴的非主屬性集合定義一個新的實體(構建一個新的資料表),原資料庫的記錄依賴於新實體的ID。如此一來資料庫重複儲存資料量將大大降低。類似的,按照恆變分離模式,對於一個實體,如果系統處理的只是這個實體的少量變化屬性,應該將不變的屬性定義為一個新實體(執行時的另一個類,資料庫中的另一個表),原來實體通過ID來引用新實體,那麼原有實體在執行系統中的資料傳輸、建立、網路開銷都會大大降低。

案例分析

我們的挑戰是提供一個高效能、高一致性要求的團購服務(DealService)。系統存在一些多次請求槓桿反模式問題,客戶端一次請求會導致幾十次DealService讀取請求,每次獲取上百個團購詳情資訊,服務端單機需要支援每秒萬次級別的吞吐量。基於需求,系統大體框架設計如下:

每個DealService定期從持久層同步所有發生變化的deal資訊,所有的deal資訊儲存在記憶體裡面。在最初的設計裡面,資料庫只有一個資料表DealModelTable,程式裡面也只有一個實體類DealModel。由於銷量、價格、使用者評價等資訊的頻發變化,為了達到高一致性要求,服務系統每分鐘需要從資料庫同步幾萬條記錄。隨著美團團購數量的增多和使用者活躍度的增加,系統出現了三個問題:

  1. 團購服務網路卡頻繁報警,由於這是高效能低延時服務,又導致了大量的客戶端超時異常;
  2. 頻繁的full GC,這是由於每條資料庫記錄更新都會導致執行系統裡面老的DealModel實體被銷燬,新的DealModels實體被建立;
  3. 資料庫從庫滯後主庫,使得服務資料一致性降低,原因是資料庫系統寫資料量巨大。

在對系統進行分析之後,我們採用瞭如下措施,大大降低了網路傳輸的資料量,緩解了主從資料庫同步壓力,使得客戶端的超時異常從高峰時候的9%降低到了小於0.01%(低於萬分之一):

  1. 將DealModelTable中的銷量、價格、使用者評價等常變的資訊單獨構建一張資料表VariableDealModel;
  2. 同時在程式碼中為銷量、價格、使用者評價等常變資料建立一個單獨的類VariableDealModel;
  3. DealService對兩張表進行分別同步;
  4. 如果DealModelTable的記錄產生了更新,執行系統銷燬老的DealModel實體並建立新的DealModel實體;
  5. 如果只是VariableDealModel的記錄產生了更新,只對VariableDealModel的屬性進行更改。

缺點和優點

採用恆變分離模式,主要有三個缺點:

  1. 不符合物件導向的設計原則。原本概念上統一的實體被切分成多個實體,會給開發工程師帶來一些理解上的困難,因此增加維護成本。進一步而言,這會增加引入額外Bug的概率(實際上物件導向之所以如此受歡迎的一個重要原因就是容易理解)。
  2. 增加了類不變數(Class invariant)的維護難度。很多情況下,Class invariant是通過語言所提供的封裝(Encapsulation)特性來維護的。當一個類變成多個類,Class invariant可能會被破壞。如果必須維護Class invariant,而這種Class invariant又發生在不同實體之間,那麼往往是把不變的屬性從不變實體移到易變的實體中去。
  3. 一張資料庫表變成多張,也會增加維護成本。

在如下兩種場景下,恆變分離模式所帶來的好處有限:

  1. 易變資料導致的操作和傳輸並不頻繁,不是系統主要操作;
  2. 易變資料佔整體資料的比例很高,槓桿效應不顯著,通過恆變分離模式不能根本性地解決系統效能問題。

總的來說,恆變分離模式非常容易理解,其應用往往需要滿足兩個條件:易變資料佔整體資料比例很低(比例越低,槓桿效應越大)和易變資料所導致的操作又是系統的主要操作。在該場景下,如果系統效能已經出現問題,犧牲一些可維護性就顯得物有所值。

大部分系統都是由多種型別的資料構成,大多數資料型別的都包含易變、少變和不變的屬性。盲目地進行恆變分離會導致系統的複雜度指數級別的增加,系統變得很難維護,所以系統設計者必須在高效能和高維護性之間找到一個平衡點。作者的建議是:對於複雜的業務系統,儘量按照物件導向的原則進行設計,只有在效能出現問題的時候才開始考慮恆變分離模式;而對於高效能,業務簡單的基礎資料服務,恆變分離模式應該是設計之初的一個重要原則。

資料區域性性模式(Locality Pattern)

原理和動機

資料區域性性模式是多次請求槓桿反模式的針對性解決方案。在大資料和強調個性化服務的時代,一個服務消費幾十種不同型別資料的現象非常常見,同時每一種型別的資料服務都有可能需要一個大的叢集(多臺機器)提供服務。這就意味著客戶端的一次請求有可能會導致服務端成千上萬次呼叫操作,很容易使系統進入多次請求槓桿反模式。在具體開發過程中,導致資料服務數量暴增的主要原因有兩個:1. 快取濫用以及缺乏規劃,2. 資料量太大以至於無法在一臺機器上提供全量資料服務。資料區域性性模的核心思想是合理組織資料服務,減少服務呼叫次數。具體而言,可以從服務端和客戶端兩個方面進行優化。

服務端優化方案的手段是對服務進行重新規劃。對於資料量太大以至於無法在一臺機器上儲存全量資料的場景,建議採用Bigtable或類似的解決方案提供資料服務。典型的Bigtable的實現包括Hbase、Google Cloud Bigtable等。實際上資料區域性性是Bigtable的一個重要設計原則,其原理是通過Row key和Column key兩個主鍵來對資料進行索引,並確保同一個Row key索引的所有資料都在一臺伺服器上面。通過這種資料組織方式,一次網路請求可以獲取同一個Row key對應的多個Column key索引的資料。缺乏規劃也是造成服務數量劇增的一個重要原因。很多通過統計和挖掘出來的特徵資料往往是在漫長的時間裡由不同team獨立產生的。而對於每種型別資料,在其產生之初,由於不確定其實際效果以及生命週期,基於快速接入原則,服務提供者往往會用手頭最容易實施的方案,例如採用Redis Cache(不加選擇地使用快取會導致快取濫用)。資料服務之間缺乏聯動以及缺乏標準接入規劃流程就會導致資料服務數量膨脹。資料區域性性原則對規劃的要求,具體而言是指:1. 資料由儘可能少的伺服器來提供,2. 經常被一起使用的資料儘可能放在同一臺伺服器上。

客戶端優化有如下幾個手段:

  1. 本地快取,對於一致性要求不高且快取命中率較高的資料服務,本地快取可以減少服務端呼叫次數;
  2. 批處理,對於單機或者由等價的機器叢集提供的資料服務,儘可能採用批處理方式,將多個請求合成在一個請求中;
  3. 客戶端Hash,對於需要通過Hash將請求分配到不同資料服務機器的服務,儘量在客戶端進行Hash,對於落入同一等價叢集的請求採用批處理方式進行呼叫。

案例分析

我們的挑戰來自於美團的推薦、個性化列表和個性化搜尋服務。這些個性化系統需要獲取各種使用者、商家和團購資訊。資訊型別包括基本屬性和統計屬性。最初,不同屬性資料由不同的服務提供,有些是RPC服務,有些是Redis服務,有些是HBase或者資料庫,參見下圖:

通常而言,客戶端每個使用者請求都會觸發多個演算法。一方面,每個演算法都會召回幾十甚至幾百個團購或者商家ID,團購和商家基礎屬性被均勻地分配到幾十臺Redis裡面(如下圖),產生了大量的Redis請求,極端情況下,一次客戶端請求所觸發的團購基礎資料請求就超過了上千次;另一方面,使用者特徵屬性資訊有十幾種,每種屬性也由單獨的服務提供,服務端網路呼叫次數暴增。在一段時間裡,很多系統都進入了多次請求槓桿反模式,Redis伺服器的網路卡經常被打死,多次進行擴容,提高執行緒池執行緒數量,絲毫沒有改善。

在對系統進行分析之後,按照資料區域性性模式的原則,我們採用瞭如下手段,徹底解決了系統多次請求槓桿反模式的問題:

  1. 採用大記憶體伺服器儲存所有的團購和商家基礎資訊,每個演算法只要一次網路請求就可以獲取所有的資訊;
  2. 服務端採用多執行緒方式提供服務,避免了Redis單一執行緒模式下單個請求慢所帶來的連鎖效應;
  3. 借鑑類似Bigtable的資料組織方式,將使用者的多種特徵採用兩個維度(使用者維度和特徵型別)進行索引,確保同一使用者的資訊只存放在一臺機器上面,減少網路呼叫數量。

缺點和優點

資料區域性性模式並不適用於系統初級階段。在初級階段,最小可用原則往往是主要設計原則之一,出於兩方面的考慮:一方面,在初級階段,很難預測所要提供服務的資料是否有效而且能夠長期使用,以及未來的呼叫量;另一方面,在初級階段,工程師可能無法預測最終的呼叫模式,而不同的呼叫模式會導致資料區域性性方案的設計不同。對於已經大量使用的資料服務,採用資料區域性性模式進行重構必然要改變老的呼叫模式,這一方面會引入新的Bug,另一方面也意味著巨大的工作量。需要特別強調的是,資料處於系統的最底層,對於結構複雜而又重要的資料,重構所帶來可靠性、一致性和工作量都是需要權衡的因素。對於請求量比較小的資料服務,即使一次請求會觸發嚴重的請求槓桿效應,但是如果原始觸發請求數量在可預見的時間內沒有明顯變多的跡象,進行資料服務重構可能得不償失。

資料區域性性模式能夠解決多次請求槓桿反模式所導致的問題,但它並非大資料的產物,CPU、編譯器的設計理念裡早就融入了該模式,所以很容易被工程師理解。雖然過度設計在系統初級階段是一個要儘量避免的事情,但是理解和掌握資料區域性性模式對於設計出一個可擴充套件、可重用的系統有很大幫助。很多成熟的系統因為多次請求槓桿反模式而導致系統頻繁崩潰,理解資料區域性性模式的原則有助於提高工程師分析解決問題的能力,而在確認了系統存在請求槓桿問題後,資料區域性性原則是一件非常銳利的武器。

避免蚊子大炮模式(Avoiding Over-generalized Solution Pattern)

原理和動機

“用大炮打蚊子”本來是大材小用的意思,但是細緻想一想,用大炮打蚊子,成功率不高。對於開發工程師而言,一方面為了快速承接業務,按照方案複用原則,總是儘可能地利用現有系統,這使得系統功能越來越強大;另一方面,提高系統的通用性或可重用性也是工程師們在設計系統的一個重要目標。隨著這兩個過程的相互獨立演化,採用通用方案解決特定問題的現象隨處可見,形象地說,這就像大炮打蚊子。大炮成本很高,蚊子的數量眾多,最終的結局往往是蚊子戰勝了大炮。

“避免蚊子大炮模式”是經濟原則在執行時系統的運用,它要求採用最節省資源(CPU、記憶體等)的方法來解決所面臨的問題,資源浪費會帶來未來潛在的風險。工程師接到一個需求的時候,需要思考的不僅僅是如何複用現有的系統,減少開發時間,還需要考慮現有系統為處理每個新需求訪問所需執行時成本,以及新需求的預期訪問量。否則,不加辨別地利用現有系統,不僅僅增大了重構風險,還有可能交叉影響,對現有系統所支援的服務造成影響。從另外一個角度講,工程師在構建一個可重用系統的時候,要明確其所不能解決和不建議解決的問題,而對於不建議解決的問題,在文件中標明潛在的風險。

案例分析

我們的挑戰是為移動使用者尋找其所在位置附近的商家資訊。美團有非常完善的搜尋系統,也有資深的搜尋工程師,所以一個系統需要查詢附近的商家的時候,往往第一方案就是呼叫搜尋服務。但是在美團,太多的服務有基於LBS的查詢需求,導致搜尋請求量直線上升,這本來不屬於搜尋的主營業務,在一段時間裡面反倒成了搜尋的最多請求來源。而搜尋引擎在如何從幾十萬商家裡面找最近的幾百商家方面的效能非常差,因此一段時間裡,搜尋服務頻繁報警。不僅僅搜尋服務可用性受到了影響,所有依賴於LBS的服務的可用性都大大降低。

在對系統分析之後,我們認為更適合解決最短直線距離的演算法應該是k-d tree,在快速實現了基於k-d tree的LBS Search解決方案之後,我們用4臺伺服器輕鬆解決了30多臺搜尋伺服器無法解決的問題,平均響應時間從高峰時的100ms降低到300ns,效能取得了幾百倍的提高。

缺點和優點

避免蚊子大炮模式的問題和資料區域性性模式類似,都與最小可用原則相沖突。在系統設計初級階段,尋求最優方案往往意味著過度設計,整個專案在時間和成本變得不可控,而為每個問題去找最優秀的解決方案是不現實的奢求。最優化原則的要求是全面的,不僅僅要考慮的執行時資源,還需要考慮工程師資源和時間成本等,而這些點往往相互矛盾。在如下情況下,避免蚊子大炮模式所帶來的好處有限:在可預見的未來,某個業務請求量非常小,這時候花大量精力去找最優技術方案效果不明顯。

在設計階段,避免蚊子大炮模式是一個需要工程師去權衡的選擇,需要在開發成本和系統執行成本之間保持一個平衡點。當很多功能融入到一個通用系統裡而出現效能問題的時候,要拆分出來每一個功能點所造成的影響也不是件輕易的事情,所以採用分開部署而共用程式碼庫的原則可以快速定位問題,然後有針對性地解決“蚊子大炮”問題。總的來說,在設計階段,避免蚊子大炮模式是工程師們進行分析和設計的一個重要準則,工程師可以暫時不解決潛在的問題,但是一定要清楚潛在的危害。構建可重用系統或方案,一定要明確其所不能解決和不建議解決的問題,避免過度使用。

實時離線分離模式(Sandbox Pattern)

原理和動機

本模式的極端要求是:離線服務永遠不要呼叫實時服務。該模式比較簡單也容易理解,但是,嚴格地講它不是一種系統設計模式,而是一種管理規範。離線服務和線上服務從可用性、可靠性、一致性的要求上完全不同。原則上,工程師在編寫離線服務程式碼的時候,應該遵循的就是離線服務程式設計規範,按照線上服務程式設計規範要求,成本就會大大提高,不符合經濟原則;從另外一方面講,按照離線服務的需求去寫線上服務程式碼,可用性、可靠性、一致性等往往得不到滿足。

具體而言,實時離線分離模式建議如下幾種規範:

  1. 如果離執行緒序需要訪問線上服務,應該給離執行緒序單獨部署一套服務;
  2. 類似於MapReduce的雲端多程式離執行緒序禁止直接訪問線上服務;
  3. 分散式系統永遠不要直接寫傳統的DBMS。

案例分析

因為違反實時離線分離模式而導致的事故非常常見。有一次,因為一個離執行緒序頻繁的向Tair叢集寫資料,每一次寫10M資料,使得整個Tair叢集當機。另一次,因為Storm系統直接寫MySQL資料庫導致資料庫連線數耗盡,從而使線上系統無法連線資料庫。

缺點和優點

為了實現實時線上分離,可能需要為線上環境和離線環境單獨部署,維護多套環境所帶來運維成本是工程師需要考慮的問題。另一方面,線上環境的資料在離線環境中可能很難獲取,這也是很多離線系統直接訪問線上系統的原因。但是,遵從實時離線分離模式是一個非常重要的安全管理準則,任何違背這個準則的行為都意味著系統性安全漏洞,都會增大線上故障概率。

降級模式(Degradation Pattern)

原理和動機

降級模式是系統效能保障的最後一道防線。理論上講,不存在絕對沒有漏洞的系統,或者說,最好的安全措施就是為處於崩潰狀態的系統提供預案。從系統效能優化的角度來講,不管系統設計地多麼完善,總會有一些意料之外的情況會導致系統效能惡化,最終可能導致崩潰,所以對於要求高可用性的服務,在系統設計之初,就必須做好降級設計。根據作者的經驗,良好的降級方案應該包含如下措施:

  1. 在設計階段,確定系統的開始惡化數值指標(例如:響應時間,記憶體使用量);
  2. 當系統開始惡化時,需要第一時間報警;
  3. 在收到報警後,或者人工手動控制系統進入降級狀態,或者編寫一個智慧程式讓系統自動降級;
  4. 區分系統所依賴服務的必要性,一般分為:必要服務和可選服務。必要服務在降級狀態下需要提供一個快速返回結果的權宜方案(快取是常見的一種方案),而對於可選服務,在降級時系統果斷不呼叫;
  5. 在系統遠離惡化情況時,需要人工恢復,或者智慧程式自動升級。

典型的降級策略有三種:流量降級、效果降級和功能性降級。流量降級是指當通過主動拒絕處理部分流量的方式讓系統正常服務未降級的流量,這會造成部分使用者服務不可用;效果降級表現為服務質量的降級,即在流量高峰時期用相對低質量、低延時的服務來替換高質量、高延時的服務,保障所有使用者的服務可用性;功能性降級也表現為服務質量的降級,指的是通過減少功能的方式來提高使用者的服務可用性。效果降級和功能性降級比較接近,效果降級強調的是主功能服務質量的下降,功能性降級更多強調的是輔助性功能的缺失。做一個類比如下:計劃將100個工程師從北京送到夏威夷度假,但是預算不夠。採用流量降級策略,只有50工程師做頭等艙去了夏威夷度假,其餘工程師繼續編寫程式(這可不好);效果降級策略下,100個工程師都坐經濟艙去夏威夷;採用功能性降級策略,100個工程師都坐頭等艙去夏威夷,但是飛機上不提供食品和飲料。

案例分析

我們的系統大量使用了智慧降級程式。在系統惡化的時候,智慧降級程式自動降級部分流量,當系統恢復的時候,智慧降級程式自動升級為正常狀態。在採用智慧降級程式之前,因為系統降級問題,整體系統不可用的情況偶爾發生。採用智慧降級程式之後,基本上沒有因為效能問題而導致的系統整體不可用。我們的智慧降級程式的主要判定策略是服務響應時間,如果出現大量長時間的響應異常或超時異常,系統就會走降級流程,如果異常數量變少,系統就會自動恢復。

缺點和優點

為了使系統具備降級功能,需要撰寫大量的程式碼,而降級程式碼往往比正常業務程式碼更難寫,更容易出錯,所以並不符合奧卡姆剃刀原則。在確定使用降級模式的前提下,工程師需要權衡這三種降級策略的利弊。大多數面向C端的系統傾向於採用效果降級和功能性降級策略,但是有些功能性模組(比如下單功能)是不能進行效果和功能性降級的,只能採用流量降級策略。對於不能接受降級後果的系統,必須要通過其他方式來提高系統的可用性。

總的來說,降級模式是一種設計安全準則,任何高可用性要求的服務,必須要按照降級模式的準則去設計。對於違背這條設計原則的系統,或早或晚,系統總會因為某些問題導致崩潰而降低可用性。不過,降級模式並非不需要成本,也不符合最小可用原則,所以對於處於MVP階段的系統,或者對於可用性要求不高的系統,降級模式並非必須採納的原則。

其他效能優化建議

對於無法採用系統性的模式方式講解的效能優化手段,作者也給出一些總結性的建議:

  1. 刪除無用程式碼有時候可以解決效能問題,例如:有些程式碼已經不再被呼叫但是可能被初始化,甚至佔有大量記憶體;有些程式碼雖然在呼叫但是對於業務而言已經無用,這種呼叫佔用CPU資源。
  2. 避免跨機房呼叫,跨機房呼叫經常成為系統的效能瓶頸,特別是那些偽batch呼叫(在使用者看起來是一次性呼叫,但是內部實現採用的是順序單個呼叫模式)對系統效能影響往往非常巨大

總結

Christopher Alexander曾說過:”Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice” 。 儘管Christopher Alexander指的是建築模式,軟體設計模式適用,基於同樣的原因,效能優化模式也適用。每個效能優化模式描述的都是工程師們日常工作中經常出現的問題,一個效能優化模式可以解決確定場景下的某一型別的問題。所以要理解一個效能優化模式不僅僅要了解效能模式的所能解決的問題以及解決手段,還需要清楚該問題所發生的場景和需要付出的代價。

最後,本文所描述的效能優化模式只是作者的工作經驗總結,都是為了解決由以下三種情況所造成的效能問題:1. 日益增長的使用者數量,2. 日漸複雜的業務,3. 急劇膨脹的資料,但是這些遠非該領域裡面的所有模式。對於文章中提到的其他效能優化建議,以及現在和將來可能碰到的效能問題,作者還會不斷抽象,在未來總結出更多的模式。效能問題涉及領域非常廣泛,而模式是一個非常好的講解效能問題以及解決方案的方式,作者有理由相信,無論是在作者所從事的工作領域裡面還是在其他的領域裡面,新的效能優化模式會不斷湧現。希望通過本文的講述,對碰到同樣問題的工程師們有所幫助,同時也拋磚引玉,期待出現更多的基於模式方式講解效能優化的文章。

參考文獻:
[1] Chang F, Dean J, Ghemawat S, et al. Bigtable: A Distributed Storage System for Structured Data
[2] Gamma E, Helm R, Johnson R, et al. Design Patterns-Elements of Reusable Object-Oriented Software. Machinery Industry, 2003
[3] Motlik F. Monolithic Core Versus Full Microservice Architecture
[4] Monolithic Design WikiWikiWeb.
[5] Bovet D P, Cesati M. Understanding the Linux Kernel. 3rd ed. O’Reilly Media, 2005.
[6] Bloch J. Effective Java. 2nd ed. Addison-Wesley, 2008.
[7] Alexander C, Ishikawa S, Silverstein M. A Pattern Language: Towns, Buildings, Construction. Oxford University Press, 1977.

相關文章