架構必知:後端服務實戰之性能優化
本文簡單介紹下後端服務開發中常用的一些性能優化策略。
1、代碼
優化代碼實現是第一位的,特別是一些不合理的複雜實現。如果結合需求能從代碼實現的角度,使用更高效的算法或方案實現,進而解決問題,那是最簡單有效的。
2、數據庫
數據庫的優化,總體上有 3 個方面:
1) SQL 調優:除了掌握 SQL 基本的優化手段,使用慢日誌定位到具體問題 SQL,使用 explain、profile 等工具來逐步調優。
2) 連接池調優:選擇高效適用的連接池,結合當前使用連接池的原理、具體的連接池監控數據和當前的業務量作一個綜合的判斷,通過反覆的幾次調試得到最終的調優參數。
3) 架構層面:包括讀寫分離、主從庫負載均衡、水平和垂直分庫分表等方面,一般需要的改動較大,需要從整體架構方面綜合考慮。
3、緩存
分類
本地緩存(HashMap/ConcurrentHashMap、Ehcache、RocksDB、Guava Cache 等)。
緩存服務(Redis/Tair/Memcache 等)。
設計關鍵點
1、什麼時候更新緩存?如何保障更新的可靠性和實時性?
更新緩存的策略,需要具體問題具體分析。基本的更新策略有兩個:
1) 接收變更的消息,準實時更新。
2) 給每一個緩存數據設置 5 分鐘的過期時間,過期後從 DB 加載再回設到 DB。這個策略是對第一個策略的有力補充,解決了手動變更 DB 不發消息、接收消息更新程序臨時出錯等問題導致的第一個策略失效的問題。通過這種雙保險機制,有效地保證了緩存數據的可靠性和實時性。
2、緩存是否會滿,緩存滿了怎麼辦?
對於一個緩存服務,理論上來說,隨着緩存數據的日益增多,在容量有限的情況下,緩存肯定有一天會滿的。如何應對?
1) 給緩存服務,選擇合適的緩存逐出算法,比如最常見的 LRU。
2) 針對當前設置的容量,設置適當的警戒值,比如 10G 的緩存,當緩存數據達到 8G 的時候,就開始發出報警,提前排查問題或者擴容。
3) 給一些沒有必要長期保存的 key,儘量設置過期時間。
3、緩存是否允許丟失?丟失了怎麼辦?
根據業務場景判斷,是否允許丟失。如果不允許,就需要帶持久化功能的緩存服務來支持,比如 Redis 或者 Tair。更細節的話,可以根據業務對丟失時間的容忍度,還可以選擇更具體的持久化策略,比如 Redis 的 RDB 或者 AOF。
緩存問題
1、緩存穿透
描述:緩存穿透是指緩存和數據庫中都沒有的數據,而用戶不斷髮起請求,如發起爲 id 爲 “-1” 的數據或 id 爲特別大不存在的數據。這時的用戶很可能是攻擊者,攻擊會導致數據庫壓力過大。
解決方案:
1) 接口層增加校驗,如用戶鑑權校驗,id 做基礎校驗,id<=0 的直接攔截。
2) 從緩存取不到的數據,在數據庫中也沒有取到,這時也可以將 key-value 對寫爲 key-null,緩存有效時間可以設置短點,如 30 秒(設置太長會導致正常情況也沒法使用),這樣可以防止攻擊用戶反覆用同一個 id 暴力攻擊。
2、緩存擊穿
描述:緩存擊穿是指緩存中沒有但數據庫中有的數據(一般是緩存時間到期),這時由於併發用戶特別多,同時讀緩存沒讀到數據,又同時去數據庫去取數據,引起數據庫壓力瞬間增大,造成過大壓力。
解決方案:
1) 設置熱點數據永遠不過期。
2) 加互斥鎖,業界比較常用的做法,是使用 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;
}
}
3、緩存雪崩
描述:緩存雪崩是指緩存中數據大批量到過期時間,而查詢數據量巨大,引起數據庫壓力過大甚至 down 機。和緩存擊穿不同的是,緩存擊穿是併發查同一條數據,緩存雪崩是不同數據都過期了,很多數據都查不到從而查數據庫。
解決方案:
1)緩存數據的過期時間設置隨機,防止同一時間大量數據過期現象發生。
2)如果緩存系統是分佈式部署,將熱點數據均勻分佈在不同的緩存節點中。
3)設置熱點數據永遠不過期。
4、緩存更新
Cache Aside 模式:這是最常用最常用的 pattern 了。其具體邏輯如下:
失效:應用程序先從 cache 取數據,沒有得到,則從數據庫中取數據,成功後,放到緩存中。
命中:應用程序從 cache 中取數據,取到後返回。
更新:先把數據存到數據庫中,成功後,再讓緩存失效。
4、異步
使用場景
針對某些客戶端的請求,在服務端可能需要針對這些請求做一些附屬額外的事情,這些事情其實用戶並不關心或者不需要立即拿到這些事情的處理結果,這種情況就比較適合用異步的方式去處理。
作用
異步處理的好處:
1) 縮短接口響應時間,使用戶的請求快速返回,用戶體驗更好。
2) 避免線程長時間處於運行狀態,這樣會引起服務線程池的可用線程長時間不夠用,進而引起線程池任務隊列長度增大,從而阻塞更多請求任務,使得更多請求得不到及時處理。
3) 提升服務的處理性能。
實現方式
1、線程(線程池)
採用額外開闢一個線程或者使用線程池的做法,在 IO 線程(處理請求響應)之外的線程來處理相應的任務,在 IO 線程中讓 response 先返回。
如果異步線程處理的任務設計的數據量非常大,那麼可以引入阻塞隊列 BlockingQueue 作進一步的優化。具體做法是讓一批異步線程不斷地往阻塞隊列裏添加要處理的數據,然後額外起一個或一批處理線程,循環批量從隊列裏拿預設大小的數據,來進行批處理,這樣進一步提高了性能。
2、消息隊列(MQ)
使用消息隊列(MQ)中間件服務,MQ 天生就是異步的。一些額外的任務,可能不需要這個系統來處理,但是需要其他系統來處理。這個時候可以先把它封裝成一個消息,扔到消息隊列裏面,通過消息中間件的可靠性保證把消息投遞到關心它的系統,然後讓其他系統來做相應的處理。
5、NoSQL
和緩存的區別
這裏介紹的 NoSQL 和緩存不一樣,雖然可能會使用一樣的數據存儲方案(比如 Redis 或者 Tair),但是使用的方式不一樣,這一節介紹的是把它作爲 DB 來用。如果當作 DB 來用,需要有效保證數據存儲方案的可用性、可靠性。
使用場景
需要結合具體的業務場景,看這塊業務涉及的數據是否適合用 NoSQL 來存儲,對數據的操作方式是否適合用 NoSQL 的方式來操作,或者是否需要用到 NoSQL 的一些額外特性(比如原子加減等)。
如果業務數據不需要和其他數據作關聯,不需要事務或者外鍵之類的支持,而且有可能寫入會異常頻繁,這個時候就比較適合用 NoSQL(比如 HBase)。監控類、日誌類系統通常會採集大量的時序數據,這類時序指標數據往往都是 “讀少寫多” 的類型,可以使用 Elasticsearch、OpenTSDB 等。
6、多線程與分佈式
使用場景
離線任務、異步任務、大數據任務、耗時較長任務的運行,適當地利用,可達到加速的效果。
注意:線上對響應時間要求較高的場合,儘量少用多線程,尤其是服務線程需要等待任務線程的場合(很多重大事故就是和這個息息相關),如果一定要用,可以對服務線程設置一個最大等待時間。
常見做法
如果單機的處理能力可以滿足實際業務的需求,那麼儘可能地使用單機多線程的處理方式,減少複雜性;反之,則需要使用多機多線程的方式。
對於單機多線程,可以引入線程池的機制,作用有二:
1) 提高性能,節省線程創建和銷燬的開銷。
2) 限流,給線程池一個固定的容量,達到這個容量值後再有任務進來,就進入隊列進行排隊,保障機器極限壓力下的穩定處理能力在使用 JDK 自帶的線程池時,一定要仔細理解構造方法的各個參數的含義,如 core pool size、max pool size、keepAliveTime、worker queue 等,在理解的基礎上通過不斷地測試調整這些參數值達到最優效果。
如果單機的處理能力不能滿足需求,這個時候需要使用多機多線程的方式。這個時候就需要一些分佈式系統的知識了,可以選用一些開源成熟的分佈式任務調度系統如 xxl-job。
7、JVM 優化
個人主要的後端語言是 JAVA,對 JVM 進行優化也能一定程度上的提升 JAVA 程序的性能。JVM 通常能夠在軟件開發後期進行,如在開發完畢或者是軟件開發的某一里程碑階段,JVM 的各項參數將會直接影響 JAVA 程序的性能。
性能指標
關注以下指標:內存使用情況、CPU 使用率、CPU load、GC count、GC time、GC 日誌。
-
查看 java 進程 GC 狀態:jstat -gcutil {pid} 1000
-
查看 JVM 默認的配置:java -XX:+PrintFlagsFinal -version | grep -iE 'HeapSize|PermSize|ThreadStackSize'
-
jps:用來輸出 JVM 中運行的進程狀態信息。
-
jstack:用來查看某個 Java 進程內的線程堆棧信息。
-
jmap:用來查看堆內存使用狀況。使用 jmap -heap pid 查看進程堆內存使用情況,包括使用的 GC 算法、堆配置參數和各代中堆內存使用情況。
查看 java 進程 CPU 高原因:
1) 獲取 java 進程 pid:ps –ef|grep java
2) 分析是哪個線程佔用率過高:top -H -p ‘PID’
3) 線程 id 轉換爲 16 進制:printf "%x\n" ‘NID’
4) Jstack 查看線程堆棧:jstack PID | grep 'NID' -C 行數 –color
推薦 2 個 java 工具:1)show-busy-java-threads 2)arthas
優化方向
比如,JVM 的堆大小(Xms、Xmx),垃圾回收策略等。要進行 JVM 層面的調優,需要對 JVM 的執行原理有一定的瞭解,如內存的結構,GC 的種類等,然後根據應用程序的特點設置合理的 JVM 參數,但是 GC tuning is the last task to be done.
出處:https://www.cnblogs.com/luxiaoxun/p/11755177.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gYIGknml8WSQ4XuxbbkBGg