性能優化模式-美團技術團隊

摘要

性能優化涉及面很廣。通常而言,性能優化指下降響應時間和提升系統吞吐量兩個方面,但在流量高峯時候,性能問題每每會表現爲服務可用性降低,因此性能優化也能夠包括提升服務可用性。在某些狀況下,下降響應時間、提升系統吞吐量和提升服務可用性三者相互矛盾,不可兼得。例如:增長緩存能夠下降平均響應時間,可是處理線程數量會由於緩存過大而有所限制,從而下降系統吞吐量;爲了提升服務可用性,對異常請求重複調用是一個經常使用的作法,可是這會提升響應時間並下降系統吞吐量。html

對於不少像美團這樣的公司,它們的系統會面臨以下三個挑戰: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或者數據庫,參見下圖:
數據局部性模式1

一般而言,客戶端每一個用戶請求都會觸發多個算法。一方面,每一個算法都會召回幾十甚至幾百個團購或者商家ID,團購和商家基礎屬性被均勻地分配到幾十臺Redis裏面(以下圖),產生了大量的Redis請求,極端狀況下,一次客戶端請求所觸發的團購基礎數據請求就超過了上千次;另外一方面,用戶特徵屬性信息有十幾種,每種屬性也由單獨的服務提供,服務端網絡調用次數暴增。在一段時間裏,不少系統都進入了屢次請求槓桿反模式,Redis服務器的網卡常常被打死,屢次進行擴容,提升線程池線程數量,絲毫沒有改善。
數據局部性模式2
在對系統進行分析以後,按照數據局部性模式的原則,咱們採用了以下手段,完全解決了系統屢次請求槓桿反模式的問題:

  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調用(在使用者看起來是一次性調用,可是內部實現採用的是順序單個調用模式)對系統性能影響每每很是巨大