(轉)常見性能優化策略的總結

轉自美團技術博客,原文地址:https://mp.weixin.qq.com/s/67HR4e8mHcuOcPl4h7FEnQhtml

 

常見性能優化策略分類前端

 

代碼

 

之因此把代碼放到第一位,是由於這一點最容易引發技術人員的忽視。不少技術人員拿到一個性能優化的需求之後,言必稱緩存、異步、JVM等。實際上,第一步就應該是分析相關的代碼,找出相應的瓶頸,再來考慮具體的優化策略。有一些性能問題,徹底是因爲代碼寫的不合理,經過直接修改一下代碼就能解決問題的,好比for循環次數過多、做了不少無謂的條件判斷、相同邏輯重複屢次等。mysql

 

數據庫

 

數據庫的調優,總的來講分爲如下三部分:git

 

SQL調優

 

這是最經常使用、每個技術人員都應該掌握基本的SQL調優手段(包括方法、工具、輔助系統等)。這裏以MySQL爲例,最多見的方式是,由自帶的慢查詢日誌或者開源的慢查詢系統定位到具體的出問題的SQL,而後使用explain、profile等工具來逐步調優,最後通過測試達到效果後上線。這方面的細節,能夠參考MySQL索引原理及慢查詢優化github

 

架構層面的調優

 

這一類調優包括讀寫分離、多從庫負載均衡、水平和垂直分庫分表等方面,通常須要的改動較大,可是頻率沒有SQL調優高,並且通常須要DBA來配合參與。那麼何時須要作這些事情?咱們能夠經過內部監控報警系統(好比Zabbix),按期跟蹤一些指標數據是否達到瓶頸,一旦達到瓶頸或者警惕值,就須要考慮這些事情。一般,DBA也會按期監控這些指標值。redis

 

鏈接池調優

 

咱們的應用爲了實現數據庫鏈接的高效獲取、對數據庫鏈接的限流等目的,一般會採用鏈接池類的方案,即每個應用節點都管理了一個到各個數據庫的鏈接池。隨着業務訪問量或者數據量的增加,原有的鏈接池參數可能不能很好地知足需求,這個時候就須要結合當前使用鏈接池的原理、具體的鏈接池監控數據和當前的業務量做一個綜合的判斷,經過反覆的幾回調試獲得最終的調優參數。算法

 

緩存

 

分類

 

本地緩存(HashMap/ConcurrentHashMap、Ehcache、Guava Cache等),緩存服務(Redis/Tair/Memcache等)。sql

 

使用場景

 

什麼狀況適合用緩存?考慮如下兩種場景:數據庫

  • 短期內相同數據重複查詢屢次且數據更新不頻繁,這個時候能夠選擇先從緩存查詢,查詢不到再從數據庫加載並回設到緩存的方式。此種場景較適合用單機緩存。後端

  • 高併發查詢熱點數據,後端數據庫不堪重負,能夠用緩存來扛。

 

選型考慮

  • 若是數據量小,而且不會頻繁地增加又清空(這會致使頻繁地垃圾回收),那麼能夠選擇本地緩存。具體的話,若是須要一些策略的支持(好比緩存滿的逐出策略),能夠考慮Ehcache;如不須要,能夠考慮HashMap;如須要考慮多線程併發的場景,能夠考慮ConcurentHashMap。

  • 其餘狀況,能夠考慮緩存服務。目前從資源的投入度、可運維性、是否能動態擴容以及配套設施來考慮,咱們優先考慮Tair。除非目前Tair還不能支持的場合(好比分佈式鎖、Hash類型的value),咱們考慮用Redis。

 

設計關鍵點

 

何時更新緩存?如何保障更新的可靠性和實時性?

 

更新緩存的策略,須要具體問題具體分析。這裏以門店POI的緩存數據爲例,來講明一下緩存服務型的緩存更新策略是怎樣的?目前約10萬個POI數據採用了Tair做爲緩存服務,具體更新的策略有兩個:

  • 接收門店變動的消息,準實時更新。

  • 給每個POI緩存數據設置5分鐘的過時時間,過時後從DB加載再回設到DB。這個策略是對第一個策略的有力補充,解決了手動變動DB不發消息、接消息更新程序臨時出錯等問題致使的第一個策略失效的問題。經過這種雙保險機制,有效地保證了POI緩存數據的可靠性和實時性。

 

緩存是否會滿,緩存滿了怎麼辦?

 

對於一個緩存服務,理論上來講,隨着緩存數據的日益增多,在容量有限的狀況下,緩存確定有一天會滿的。如何應對?

① 給緩存服務,選擇合適的緩存逐出算法,好比最多見的LRU。

② 針對當前設置的容量,設置適當的警惕值,好比10G的緩存,當緩存數據達到8G的時候,就開始發出報警,提早排查問題或者擴容。

③ 給一些沒有必要長期保存的key,儘可能設置過時時間。

 

緩存是否容許丟失?丟失了怎麼辦?

 

根據業務場景判斷,是否容許丟失。若是不容許,就須要帶持久化功能的緩存服務來支持,好比Redis或者Tair。更細節的話,能夠根據業務對丟失時間的容忍度,還能夠選擇更具體的持久化策略,好比Redis的RDB或者AOF。

 

緩存被「擊穿」問題

 

對於一些設置了過時時間的key,若是這些key可能會在某些時間點被超高併發地訪問,是一種很是「熱點」的數據。這個時候,須要考慮另一個問題:緩存被「擊穿」的問題。

  • 概念:緩存在某個時間點過時的時候,剛好在這個時間點對這個Key有大量的併發請求過來,這些請求發現緩存過時通常都會從後端DB加載數據並回設到緩存,這個時候大併發的請求可能會瞬間把後端DB壓垮。

  • 如何解決:業界比較經常使用的作法,是使用mutex。簡單地來講,就是在緩存失效的時候(判斷拿出來的值爲空),不是當即去load db,而是先使用緩存工具的某些帶成功操做返回值的操做(好比Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操做返回成功時,再進行load db的操做並回設緩存;不然,就重試整個get緩存的方法。相似下面的代碼:

     public String get(key) {      String value = redis.get(key);      if (value == null) { //表明緩存值過時          //設置3min的超時,防止del操做失敗的時候,下次緩存過時一直不能load db          if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //表明設置成功               value = db.get(key);                      redis.set(key, value, expire_secs);                      redis.del(key_mutex);              } else {  //這個時候表明同時候的其餘線程已經load db並回設到緩存了,這時候重試獲取緩存值便可                      sleep(50);                      get(key);  //重試              }          } else {              return value;                }  }

 

異步

 

使用場景

 

針對某些客戶端的請求,在服務端可能須要針對這些請求作一些附屬的事情,這些事情其實用戶並不關心或者用戶不須要當即拿到這些事情的處理結果,這種狀況就比較適合用異步的方式處理這些事情。

 

做用

  • 縮短接口響應時間,使用戶的請求快速返回,用戶體驗更好。

  • 避免線程長時間處於運行狀態,這樣會引發服務線程池的可用線程長時間不夠用,進而引發線程池任務隊列長度增大,從而阻塞更多請求任務,使得更多請求得不到技術處理。

  • 線程長時間處於運行狀態,可能還會引發系統Load、CPU使用率、機器總體性能降低等一系列問題,甚至引起雪崩。異步的思路能夠在不增長機器數和CPU數的狀況下,有效解決這個問題。

 

常見作法

 

一種作法,是額外開闢線程,這裏能夠採用額外開闢一個線程或者使用線程池的作法,在IO線程(處理請求響應)以外的線程來處理相應的任務,在IO線程中讓response先返回。

 

若是異步線程處理的任務設計的數據量很是巨大,那麼能夠引入阻塞隊列BlockingQueue做進一步的優化。具體作法是讓一批異步線程不斷地往阻塞隊列裏扔數據,而後額外起一個處理線程,循環批量從隊列裏拿預設大小的一批數據,來進行批處理(好比發一個批量的遠程服務請求),這樣進一步提升了性能。

 

另外一種作法,是使用消息隊列(MQ)中間件服務,MQ天生就是異步的。一些額外的任務,可能不須要我這個系統來處理,可是須要其餘系統來處理。這個時候能夠先把它封裝成一個消息,扔到消息隊列裏面,經過消息中間件的可靠性保證把消息投遞到關心它的系統,而後讓這個系統來作相應的處理。

 

好比C端在完成一個提單動做之後,可能須要其它端作一系列的事情,可是這些事情的結果不會馬上對C端用戶產生影響,那麼就能夠先把C端下單的請求響應先返回給用戶,返回以前往MQ中發一個消息便可。並且這些事情理應不是C端的負責範圍,因此這個時候用MQ的方式,來解決這個問題最合適。

 

NoSQL

 

和緩存的區別

 

先說明一下,這裏介紹的和緩存那一節不同,雖然可能會使用同樣的數據存儲方案(好比Redis或者Tair),可是使用的方式不同,這一節介紹的是把它做爲DB來用。若是看成DB來用,須要有效保證數據存儲方案的可用性、可靠性。

 

使用場景

 

須要結合具體的業務場景,看這塊業務涉及的數據是否適合用NoSQL來存儲,對數據的操做方式是否適合用NoSQL的方式來操做,或者是否須要用到NoSQL的一些額外特性(好比原子加減等)。

 

若是業務數據不須要和其餘數據做關聯,不須要事務或者外鍵之類的支持,並且有可能寫入會異常頻繁,這個時候就比較適合用NoSQL(好比HBase)。

 

好比,美團點評內部有一個對exception作的監控系統,若是在應用系統發生嚴重故障的時候,可能會短期產生大量exception數據,這個時候若是選用MySQL,會形成MySQL的瞬間寫壓力飆升,容易致使MySQL服務器的性能急劇惡化以及主從同步延遲之類的問題,這種場景就比較適合用Hbase相似的NoSQL來存儲。

 

JVM調優

 

何時調?

 

經過監控系統(如沒有現成的系統,本身作一個簡單的上報監控的系統也很容易)上對一些機器關鍵指標(gc time、gc count、各個分代的內存大小變化、機器的Load值與CPU使用率、JVM的線程數等)的監控報警,也能夠看gc log和jstat等命令的輸出,再結合線上JVM進程服務的一些關鍵接口的性能數據和請求體驗,基本上就能定位出當前的JVM是否有問題,以及是否須要調優。

 

怎麼調?

  1. 若是發現高峯期CPU使用率與Load值偏大,這個時候能夠觀察一些JVM的thread count以及gc count(可能主要是young gc count),若是這兩個值都比以往偏大(也能夠和一個歷史經驗值做對比),基本上能夠定位是young gc頻率太高致使,這個時候能夠經過適當增大young區大小或者佔比的方式來解決。

  2. 若是發現關鍵接口響應時間很慢,能夠結合gc time以及gc log中的stop the world的時間,看一下整個應用的stop the world的時間是否是比較多。若是是,可能須要減小總的gc time,具體能夠從減少gc的次數和減少單次gc的時間這兩個維度來考慮,通常來講,這兩個因素是一對互斥因素,咱們須要根據實際的監控數據來調整相應的參數(好比新生代與老生代比值、eden與survivor比值、MTT值、觸發cms回收的old區比率閾值等)來達到一個最優值。

  3. 若是發生full gc或者old cms gc很是頻繁,一般這種狀況會誘發STW的時間相應加長,從而也會致使接口響應時間變慢。這種狀況,大機率是出現了「內存泄露」,Java裏的內存泄露指的是一些應該釋放的對象沒有被釋放掉(還有引用拉着它)。那麼這些對象是如何產生的呢?爲啥不會釋放呢?對應的代碼是否是出問題了?問題的關鍵是搞明白這個,找到相應的代碼,而後對症下藥。因此問題的關鍵是轉化成尋找這些對象。怎麼找?綜合使用jmap和MAT,基本就能定位到具體的代碼。

 

多線程與分佈式

 

使用場景

 

離線任務、異步任務、大數據任務、耗時較長任務的運行**,適當地利用,可達到加速的效果。

 

注意:線上對響應時間要求較高的場合,儘可能少用多線程,尤爲是服務線程須要等待任務線程的場合(不少重大事故就是和這個息息相關),若是必定要用,能夠對服務線程設置一個最大等待時間。

 

常見作法

 

若是單機的處理能力能夠知足實際業務的需求,那麼儘量地使用單機多線程的處理方式,減小複雜性;反之,則須要使用多機多線程的方式。

 

對於單機多線程,能夠引入線程池的機制,做用有二:

  • 提升性能,節省線程建立和銷燬的開銷

  • 限流,給線程池一個固定的容量,達到這個容量值後再有任務進來,就進入隊列進行排隊,保障機器極限壓力下的穩定處理能力在使用JDK自帶的線程池時,必定要仔細理解構造方法的各個參數的含義,如core pool size、max pool size、keepAliveTime、worker queue等,在理解的基礎上經過不斷地測試調整這些參數值達到最優效果。

 

若是單機的處理能力不能知足需求,這個時候須要使用多機多線程的方式。這個時候就須要一些分佈式系統的知識了。首先就必須引入一個單獨的節點,做爲調度器,其餘的機器節點都做爲執行器節點。調度器來負責拆分任務,和分發任務到合適的執行器節點;執行器節點按照多線程的方式(也多是單線程)來執行任務。這個時候,咱們整個任務系統就由單擊演變成一個集羣的系統,並且不一樣的機器節點有不一樣的角色,各司其職,各個節點之間還有交互。這個時候除了有多線程、線程池等機制,像RPC、心跳等網絡通訊調用的機制也不可少。後續我會出一個簡單的分佈式調度運行的框架。

 

度量系統(監控、報警、服務依賴管理)

 

嚴格來講,度量系統不屬於性能優化的範疇,可是這方面和性能優化息息相關,能夠說爲性能優化提供一個強有力的數據參考和支撐。沒有度量系統,基本上就沒有辦法定位到系統的問題,也沒有辦法有效衡量優化後的效果。不少人不重視這方面,但我認爲它是系統穩定性和性能保障的基石。

 

關鍵流程

 

若是要設計這套系統,整體來講有哪些關鍵流程須要設計呢?

① 肯定指標
② 採集數據
③ 計算數據,存儲結果
④ 展示和分析

 

須要監控和報警哪些指標數據?須要關注哪些?

 

按照需求出發,主要須要二方面的指標:

  1. 接口性能相關,包括單個接口和所有的QPS、響應時間、調用量(統計時間維度越細越好;最好是,既能以節點爲維度,也能夠以服務集羣爲維度,來查看相關數據)。其中還涉及到服務依賴關係的管理,這個時候須要用到服務依賴管理系統

  2. 單個機器節點相關,包括CPU使用率、Load值、內存佔用率、網卡流量等。若是節點是一些特殊類型的服務(好比MySQL、Redis、Tair),還能夠監控這些服務特有的一些關鍵指標。

 

數據採集方式

 

一般採用異步上報的方式,具體作法有兩種:第一種,發到本地的Flume端口,由Flume進程收集到遠程的Hadoop集羣或者Storm集羣來進行運算;第二種,直接在本地運算好之後,使用異步和本地隊列的方式,發送到監控服務器。

 

數據計算

 

能夠採用離線運算(MapReduce/Hive)或者實時/準實時運算(Storm/Spark)的方式,運算後的結果存入MySQL或者HBase;某些狀況,也能夠不計算,直接採集發往監控服務器。

 

展示和分析

 

提供統一的展示分析平臺,須要帶報表(列表/圖表)監控和報警的功能。

 

真實案例分析

 

案例一:商家與控制區關係的刷新job

 

背景

 

這是一個每小時按期運行一次的job,做用是用來刷新商家與控制區的關係。具體規則就是根據商家的配送範圍(多個)與控制區是否有交集,若是有交集,就把這個商家劃到這個控制區的範圍內。

 

業務需求

 

須要這個過程越短越好,最好保持在20分鐘內。

 

優化過程

 

原有代碼的主要處理流程是:

  1. 拿到全部門店的配送範圍列表和控制區列表。

  2. 遍歷控制區列表,針對每個控制區:
    a. 遍歷商家的配送範圍列表,找到和這個控制區相交的配送範圍列表。
    b. 遍歷上述商家配送範圍列表,對裏面的商家ID去重,保存到一個集合裏。
    c. 批量根據上述商家ID集合,取到對應的商家集合。
    d. 遍歷上述商家集合,從中拿到每個商家對象,進行相應的處理(根據是否已經是熱門商家、自營、在線支付等條件來判斷是否須要插入或者更新以前的商家和控制區的關係)。
    e. 刪除這個控制區當前已有的,可是不該該存在的商家關係列表。

 

分析代碼,發現第2步的a步驟和b步驟,找出和某控制區相交的配送範圍集合並對商家ID去重,能夠採用R樹空間索引的方式來優化。具體作法是:

  • 任務開始先更新R樹,而後利用R樹的結構和匹配算法來拿到和控制區相交的配送範圍ID列表。

  • 再批量根據配送範圍ID列表,拿到配送範圍列表。

  • 而後針對這一批配送範圍列表(數量很小),用原始多邊形相交匹配的方法作進一步過濾,而且對過濾後的商家ID去重。

 

這個優化已經在第一期優化中上線,整個過程耗時由40多分鐘縮短到20分鐘之內。

 

第一期優化改成R樹之後,運行了一段時間,隨着數據量增大,性能又開始逐漸惡化,一個月後已經惡化到50多分鐘。因而繼續深刻代碼分析,尋找了兩個優化點,安排第二期優化並上線。

 

這兩個優化點是:

  • 第2步的c步驟,原來是根據門店ID列表從DB批量獲取門店,如今能夠改爲mget的方式從緩存批量獲取(此時商家數據已被緩存);

  • 第2步的d步驟,根據是否已經是熱門商家、自營、在線支付等條件來判斷是否須要插入或者更新以前的商家和控制區的關係。

 

上線後效果

 

經過日誌觀察,執行時間由50多分鐘縮短到15分鐘之內,下圖是截取了一天的4臺機器的日誌時間(單位:毫秒):




能夠看到,效果仍是很是明顯的。

 

案例二:POI緩存設計與實現

 

背景

 

2014年Q4,數據庫中關於POI(這裏能夠簡單理解爲外賣的門店)相關的數據的讀流量急劇上升,雖說加入從庫節點能夠解決一部分問題,可是畢竟節點的增長是會達到極限的,達到極限後主從複製會達到瓶頸,可能會形成數據不一致。因此此時,急需引入一種新的技術方案來分擔數據庫的壓力,下降數據庫POI相關數據的讀流量。另外,任何場景都考慮加DB從庫的作法,會對資源形成必定的浪費。

 

實現方案

 

基於已有的通過考驗的技術方案,我選擇Tair來做爲緩存的存儲方案,來幫DB分擔來自於各應用端的POI數據的讀流量的壓力。理由主要是從可用性、高性能、可擴展性、是否通過線上大規模數據和高併發流量的考驗、是否有專業運維團隊、是否有成熟工具等幾個方面綜合考量決定。

 

詳細設計

 

初版設計

 

緩存的更新策略,根據業務的特色、已有的技術方案和實現成本,選擇了用MQ來接收POI改變的消息來觸發緩存的更新,可是這個過程有可能失敗;同時啓用了key的過時策略,而且調用端會先判斷是否過時,如過時,會從後端DB加載數據並回設到緩存,再返回。經過兩個方面雙保險確保了緩存數據的可用。

 

第二版設計

 

初版設計運行到一段時間之後,咱們發現了兩個問題:

  1. 某些狀況下不能保證數據的實時一致(好比技術人員手動改動DB數據、利用MQ更新緩存失敗),這個時候只能等待5分鐘的過時時間,有的業務是不容許的。

  2. 加入了過時時間致使另一個問題:Tair在緩存不命中的那一刻,會嘗試從硬盤中Load數據,若是硬盤沒有再去DB中Load數據。這無疑會進一步延長Tair的響應時間,這樣不只使得業務的超時比率加大,並且會致使Tair的性能進一步變差。

 

爲了解決上述問題,咱們從美團點評負責基礎架構的同事那裏瞭解到Databus能夠解決緩存數據在某些狀況下不一致的問題,而且能夠去掉過時時間機制,從而提升查詢效率,避免tair在內存不命中時查詢硬盤。並且爲了防止DataBus單點出現故障影響咱們的業務,咱們保留了以前接MQ消息更新緩存的方案,做了切換開關,利用這個方案做容錯,總體架構以下:


 

上線後效果

 

上線後,經過持續地監控數據發現,隨着調用量的上升,到DB的流量有了明顯地減小,極大地減輕了DB的壓力。同時這些數據接口的響應時間也有了明顯地減小。緩存更新的雙重保障機制,也基本保證了緩存數據的可用。見下圖:



 

案例三:業務運營後臺相關頁面的性能優化

 

背景

 

隨着業務的快速發展,帶來的訪問量和數據量的急劇上升,經過咱們相應的監控系統能夠發現,系統的某些頁面的性能開始出現惡化。 從用戶方的反饋,也證實了這點。此時此刻,有必要迅速排期,敏捷開發,對這些頁面進行調優。

 

歡迎頁

  • 需求背景:歡迎頁是地推人員乃至總部各類角色人員進入外賣運營後臺的首頁,會顯示地推人員最想看到最關心的一些核心數據,其重要性不言而喻,因此該頁面的性能惡化會嚴重影響到用戶體驗。所以,首先須要優化的就是歡迎頁。經過相應定位和分析,發現致使性能惡化的主要緣由有兩個:數據接口層和計算展示層。

  • 解決方案:對症下藥,分而治之。通過仔細排查、分析定位,數據接口層採用接口調用批量化、異步RPC調用的方式來進行有效優化,計算展示層決定採用預先計算、再把計算好的結果緩存的方式來提升查詢速度。其中,緩存方案根據業務場景和技術特色,選用Redis。定好方案後,快速開發上線。

  • 上線效果:上線後性能對比圖,以下:


 

組織架構頁

  • 需求背景:組織架構頁,採用了四層樹形結構圖,一塊兒呈現加載,初版上線後發現性能很是差。用戶迫切但願對這個頁面的性能進行調優。

  • 解決方案:通過分析代碼,定位到一個比較經典的問題:裏面執行了太屢次小數據量的SQL查詢。因而採用多個SQL合併成大SQL的方式,而後使用本地緩存來緩存這些數據,合理預估數據量和性能,充分測試後上線。

  • 上線效果:上線後性能對比圖,以下:


 

訂單關聯樓宇頁

  • 需求背景:隨着訂單量日益增大,訂單表積累的數據日益增多,訂單關聯樓宇頁的性能也日益變差(響應時間線性上升)。而這個頁面和地推人員的業績息息相關,因此地推人員使用該頁面的頻率很是高,性能日益惡化極大地影響了地推人員的用戶體驗。

  • 解決方案:通過分析與設計,決定採用當時已有的訂單二級索引月分表來代替原始的訂單表來供前端的查詢請求;而且限制住篩選的時間條件,使得篩選的開始時間和結束時間不能跨月(事先和用戶溝經過,能夠接受,能知足用戶的基本需求),這樣就只需一個月分索引表便可,經過適當的功能限制來達到性能的調優。這樣從二級索引月分表中根據各類查詢條件查到最終的分頁的訂單ID集合,而後再根據訂單ID從訂單庫來查出相應的訂單數據集合。

  • 上線效果:上線後發如今調用量幾乎沒怎麼變的狀況下,性能提高明顯,以下圖:


 

其餘

 

除了上面介紹的以外,優化還涉及前端、分佈式文件系統、CDN、全文索引、空間索引等幾方面。限於篇幅,咱們留到將來再作介紹。

相關文章
相關標籤/搜索