本文簡單介紹下后端服務開發中常用的一些性能優化策略。
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
查看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://tech.meituan.com/2016/12/02/performance-tunning.html
https://blog.csdn.net/qq_42894896/article/details/82256770
https://www.cnblogs.com/java-chen-hao/p/10656304.html