阿里P8架構師談:多線程、架構、異步消息、Redis等性能優

常見性能優化策略分類

1.代碼

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

2.數據庫

數據庫的調優,總的來說分為以下三部分:

3.SQL調優

這是最常用、每一個技術人員都應該掌握基本的SQL調優手段(包括方法、工具、輔助系統等)。這裡以MySQL為例,最常見的方式是,由自帶的慢查詢日誌或者開源的慢查詢系統

定位到具體的出問題的SQL,然後使用explain、profile等工具來逐步調優,最後經過測試達到效果後上線。這方面的細節,可以參考MySQL索引原理及慢查詢優化。

4.架構層面的調優

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

5.連接池調優

我們的應用為了實現數據庫連接的高效獲取、對數據庫連接的限流等目的,通常會採用連接池類的方案,即每一個應用節點都管理了一個到各個數據庫的連接池。隨著業務訪問量或者數據量的增長,原有的連接池參數可能不能很好地滿足需求,這個時候就需要結合

當前使用連接池的原理、具體的連接池監控數據和當前的業務量作一個綜合的判斷,通過反覆的幾次調試得到最終的調優參數。

緩存分類:本地緩存+分佈式緩存

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

1.使用場景

什麼情況適合用緩存?考慮以下兩種場景:

  • 短時間內相同數據重複查詢多次且數據更新不頻繁,這個時候可以選擇先從緩存查詢,查詢不到再從數據庫加載並回設到緩存的方式。此種場景較適合用單機緩存。
  • 高併發查詢熱點數據,後端數據庫不堪重負,可以用緩存來扛。

2.選型考慮

  • 如果數據量小,並且不會頻繁地增長又清空(這會導致頻繁地垃圾回收),那麼可以選擇本地緩存。具體的話,如果需要一些策略的支持(比如緩存滿的逐出策略),可以考慮Ehcache;如不需要,可以考慮HashMap;如需要考慮多線程併發的場景,可以考慮ConcurentHashMap。
  • 其他情況,可以考慮緩存服務。目前從資源的投入度、可運維性、是否能動態擴容以及配套設施來考慮,我們優先考慮Tair。除非目前Tair還不能支持的場合(比如分佈式鎖、Hash類型的value),我們考慮用Redis。

3.設計關鍵點

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

  • 更新緩存的策略,需要具體問題具體分析。

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

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

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

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

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

5.緩存是否允許丟失?丟失了怎麼辦?

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

6.緩存被“擊穿”問題

對於一些設置了過期時間的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;

}

}

異步消息調用

1.使用場景

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

2.作用

  • 縮短接口響應時間,使用戶的請求快速返回,用戶體驗更好。
  • 避免線程長時間處於運行狀態,這樣會引起服務線程池的可用線程長時間不夠用,進而引起線程池任務隊列長度增大,從而阻塞更多請求任務,使得更多請求得不到技術處理。
  • 線程長時間處於運行狀態,可能還會引起系統Load、CPU使用率、機器整體性能下降等一系列問題,甚至引發雪崩。異步的思路可以在不增加機器數和CPU數的情況下,有效解決這個問題。

4.常見做法

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

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

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

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

NoSQL和緩存的區別

先說明一下,這裡介紹的和緩存那一節不一樣,雖然可能會使用一樣的數據存儲方案(比如Redis或者Tair),但是使用的方式不一樣,這一節介紹的是把它作為DB來用。如果當作DB來用,需要有效保證數據存儲方案的可用性、可靠性。

1.使用場景

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

如果業務數據不需要和其他數據作關聯,不需要事務或者外鍵之類的支持,而且有可能寫入會異常頻繁,這個時候就比較適合用NoSQL(比如HBase)。

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,基本就能定位到具體的代碼。

多線程與分佈式

1.使用場景

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

注意:線上對響應時間要求較高的場合,儘量少用多線程,尤其是服務線程需要等待任務線程的場合(很多重大事故就是和這個息息相關),如果一定要用,可以對服務線程設置一個最大等待時間。

2.常見做法

如果單機的處理能力可以滿足實際業務的需求,那麼儘可能地使用單機多線程的處理方式,減少複雜性;反之,則需要使用多機多線程的方式。

對於單機多線程,可以引入線程池的機制,作用有二:

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

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

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

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

5.關鍵流程

如果要設計這套系統,總體來說有哪些關鍵流程需要設計呢?

① 確定指標

② 採集數據

③ 計算數據,存儲結果

④ 展現和分析

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

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

  1. 接口性能相關,包括單個接口和全部的QPS、響應時間、調用量(統計時間維度越細越好;最好是,既能以節點為維度,也可以以服務集群為維度,來查看相關數據)。其中還涉及到服務依賴關係的管理,這個時候需要用到服務依賴管理系統
  2. 單個機器節點相關,包括CPU使用率、Load值、內存佔用率、網卡流量等。如果節點是一些特殊類型的服務(比如MySQL、Redis、Tair),還可以監控這些服務特有的一些關鍵指標。

1.數據採集方式

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

2.數據計算

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

3.展現和分析

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

其他

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


分享到:


相關文章: