天天看點

後端服務慢成狗?試試這 7 招!

後端服務慢成狗?試試這 7 招!

Java技術棧

www.javastack.cn

關注閱讀更多優質文章

作者:阿凡盧

出處:http://www.cnblogs.com/luxiaoxun/

本文簡單介紹下後端服務開發中常用的一些性能優化政策。

後端服務慢成狗?試試這 7 招!

1、代碼

優化代碼實作是第一位的,特别是一些不合理的複雜實作。如果結合需求能從代碼實作的角度,使用更高效的算法或方案實作,進而解決問題,那是最簡單有效的。

2、資料庫

資料庫的優化,總體上有3個方面:

1) SQL調優:除了掌握SQL基本的優化手段,使用慢日志定位到具體問題SQL,使用explain、profile等工具來逐漸調優。

2) 連接配接池調優:選擇高效适用的連接配接池,結合目前使用連接配接池的原理、具體的連接配接池監控資料和目前的業務量作一個綜合的判斷,通過反複的幾次調試得到最終的調優參數。

3) 架構層面:包括讀寫分離、主從庫負載均衡、水準和垂直分庫分表等方面,一般需要的改動較大,需要從整體架構方面綜合考慮。

3、緩存

分類

本地緩存(HashMap/ConcurrentHashMap、Ehcache、RocksDB、Guava Cache等)。

緩存服務(Redis/Tair/Memcache等)。系列緩存教程請關注公衆号Java技術棧閱讀,都是實戰幹貨。

設計關鍵點

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、多線程與分布式

使用場景

離線任務、異步任務、大資料任務、耗時較長任務的運作,适當地利用,可達到加速的效果。系列多線程教程請關注公衆号Java技術棧閱讀,都是實戰幹貨。

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

常見做法

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

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

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

https://github.com/oldratlee/useful-scripts/blob/master/docs/java.md#-show-busy-java-threads

2)arthas

https://alibaba.github.io/arthas/index.html

優化方向

比如,JVM的堆大小(Xms、Xmx),垃圾回收政策等。

要進行JVM層面的調優,需要對JVM的執行原理有一定的了解,如記憶體的結構,GC的種類等,然後根據應用程式的特點設定合理的JVM參數,但是GC tuning is the last task to be done.大家關注公衆号Java技術棧回複jvm46可以擷取一份JVM調優指南。

參考:

  • 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

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。