雙十一壓測&Java應用性能問題排查總結

連續參加了兩年公司的雙十一大促壓測項目,遇到了不少問題,也成長了不少,因而在這裏對大促壓測作一份總結。以及記錄一下大促壓測過程當中出現的一些常見的Java應用性能問題。java

1、爲何要壓測

  1. 找出應用的性能瓶頸
  2. 探究應用的性能基準
  3. 給大促機器擴容提供參考依據

2、如何壓測

關注哪些指標

吞吐率 TPS(每秒響應的請求數量)mysql

響應時長 RT (通常狀況下重點關注90%請求的響應時長,咱們的大促標準通常是1s之內)git

錯誤率 (看業務的可接受程度,咱們的大促標準是不超過2%)github

壓測工具

如今有不少能夠用來進行壓測的工具,例如ab、jmeter、wrk等,此處主要介紹一下ab和jmeter。正則表達式

1. ab

ab是一個命令行工具,使用起來很是簡單,redis

# -c表示併發數,-n表示請求總數,其餘一些參數能夠查詢手冊/相關資料
ab -c 10 -n 200 https://www.baidu.com/
複製代碼

命令執行完成後會得出一份壓測結果報告,其中錯誤請求數、TPS和RT在下圖中有標註算法

image-20191105160037216

2. jmeter

jmeter同時支持圖形化界面和命令行方式,sql

圖形化方式

首先在"Test Plan"中添加一個"線程組",裏面能夠設置併發數、壓測時長等參數。數據庫

接下來須要在「線程組」中添加「HTTP請求取樣器」,裏面是設置HTTP請求的各項參數。json

最後添加查看結果用的監聽組件,我我的比較經常使用的有「查看結果樹」、「聚合報告」和「TPS曲線圖」(須要安裝)。

image-20191105163923823

重點來看一下「聚合報告」,(必定要記得每次壓測前都要清理一下數據才行[上方的"齒輪+2個掃把"圖標],否則回合以前的數據混合在一塊兒)

image-20191105165259837

上面只是一些簡單的介紹,事實上jmeter還支持不少複雜的壓測場景: jdbc壓測、dubbo壓測、動態參數壓測、自定義響應斷言……這些能夠自行網上搜索。

命令行方式

命令行方式主要能夠用來作一些自動化壓測的任務。使用方式以下:

jmeter -n -t [jmx腳本] -l [壓測請求結果文件] -e -o [壓測報告文件夾(會生成一堆網頁文件)]
複製代碼

其中jmx腳本能夠先經過jmeter圖形化界面所有設置好了,而後保存一下就會生成對應的jmx腳本了。

###3. ab與jmeter的對比

ab jmeter
操做難度 簡單 複雜
命令行 支持,操做簡單 支持,操做稍微複雜一些
請求結果列表 沒法顯示 有詳細請求列表
動態參數 不支持 支持
複雜場景支持 極其有限 豐富

基本上,對於一些簡單的固定參數請求而且是自測的狀況下,使用ab會很是簡便。通常狀況下jmeter的適用性會更廣。

3、如何制定大促壓測目標

壓測接口的選取

通常狀況下,沒必要要將公司全部的接口都進行壓測,壓測接口主要包含核心鏈路接口、訪問量大的接口以及新上線的活動接口。獲取方式基本是以下兩種:

  1. 對核心業務進行抓包
  2. 諮詢各業務線負責人

壓測併發數的設定

在上面的兩種壓測工具中,咱們都看到了一個參數爲併發數,這個參數通常須要根據公司的業務量來進行推算,能夠去網上找些資料。不過爲了簡化壓測過程,咱們公司的大促統一使用讀接口200併發,寫接口100併發的標準來執行的。

事實上,我對併發數的設定這塊也比較模糊,所以上述描述僅作參考。

TPS的設定

通常是根據大促銷售目標、平時各接口qps、各接口訪問量按照比例制定出最終的TPS目標,不要忘了最後乘上一個風險係數。具體的算法能夠自行設計,大概思路就是這樣的。

4、關注哪些指標

對於壓測工具的指標上面已經說過了,主要是關注TPS、RT和錯誤率。

那麼還有哪些須要關注的指標呢?其實這個都是根據公司的業務來決定的,例如咱們公司主要使用java應用、mysql做爲數據庫、redis做爲緩存中間件,那麼咱們主要關注的性能數據以下:

監控對象 性能指標
應用服務器(服務化應用包含下游鏈路的應用服務器) CPU、網絡帶寬、磁盤IO、GC
數據庫 CPU、網絡帶寬、慢SQL
REDIS 網絡帶寬

每一個監控對象都有其特性,因此應該根據實際狀況的來制定本身的監控指標。

5、如何去排查問題

因爲公司主要使用的是Java8,所以本文也主要是針對Java8應用作分析。

爲何要找瓶頸點

舉個例子,若是告訴你一個接口稍微一些壓力就能把服務器的cpu跑滿了,致使TPS上不去,裏面一堆複雜邏輯,並且還有很多遠程調用(數據庫查詢、緩存查詢、dubbo調用等)。

你可能對業務很是熟悉,開始大刀闊斧地進行代碼修改、增長緩存、業務降級等,也許指望很美好,可是事實上有極大的多是你作的一切對TPS只能產生輕微的影響。而後只能經過不停地嘗試刪改代碼去查找問題點,那麼顯然只能帶來幾個結果: 1.效率低下 2.把代碼弄的一團糟 3.不具有可複製性 4.對業務會形成或小或大的影響,最最關鍵的是改的時候內心也沒底、改完以後內心依舊沒底。

幾個問題(請根據本身的實際認知回答)

  1. 你認爲cpu達到100%是好是壞?
  2. 你認爲哪些代碼對cpu的開銷大?你認爲大量判斷邏輯對cpu的開銷大嗎?
  3. 你認爲大量的網絡數據傳輸對哪些指標的影響大?
  4. 你認爲對於Java應用監控服務器內存的必要性有多高?

##如何排查性能問題

那麼問題來了,咱們到底應該怎麼去排查問題呢?(如下均爲一些我的經驗,可能會有很多遺漏,或者會有一些錯誤,若是有的話,請及時指出)

排查問題的話,首先咱們須要先有一些排查的突破點和方向。(沒法保證100%找到對應問題,可是大幅提高找到性能問題的效率)

前面有提到,咱們壓測過程當中須要監控各項指標,那麼其實咱們的突破方向通常就在這些監控指標上了。咱們能夠對這些指標進行分類,對於每一類均可以有着相對應的排查策略。

1. 數據庫慢SQL問題

這個問題是最好排查的一類問題了,只須要對慢SQL進行鍼對性地分析優化便可,此處不過多講解。

2. 網絡帶寬過大

那麼一個問題來了,此處的網絡帶寬究竟是指的什麼呢?換個問法吧,假設數據庫的帶寬上限爲1Gbps,實際上壓測致使數據庫的網絡帶寬佔用了800Mbps,那能夠說明這個接口是一個問題接口嗎?

考慮下面這種狀況,這個接口的TPS假設在壓測過程當中達到了80000,遠大於接口實際目標TPS,那該接口將數據庫的帶寬佔到800Mbps是合情合理的。

那麼上面的問題的答案也就呼之欲出了,這裏的網絡帶寬,在不少狀況下,咱們更應該關注的是單個請求的平均佔用帶寬。

如何排查網絡帶寬過大的問題

猜一猜,其實不難想象,就是抓包。我經常使用的抓包方式是經過tcpdump抓包,而後使用wireshark解析抓包內容(若是有更簡單的方式,能夠留言)。下面講一下tcpdump+wireshark的方式如何抓包。

爲了不大量的數據混雜在一塊兒,通常狀況下,我更喜歡是抓單個請求的數據,而不是在壓測中抓包。下面簡單介紹一下tcpdump和wireshark如何抓包,

  1. 在服務器上執行命令sudo tcpdump -w xxx.pcap
  2. 而後請求一下接口
  3. Ctrl+C停掉tcpdump
  4. 將xxx.pcap拷到本地,使用wireshark打開
  5. 以下圖,找到一個請求 > 右鍵"Follow" > "TCP Stream"

image-20191106175128011

打開TCP流後經過調整右下方的"Stream",咱們就能夠看到應用在請求過程當中的網絡數據(包含Http請求數據、Mysql請求數據、Redis請求數據……),如下圖爲例,能夠看到這個請求的mysql請求量很是大,接下來就是查看究竟是哪些SQL語句致使的。

image-20191106175708652

3. 數據庫CPU太高

開啓數據庫日誌,看看壓測期間都執行了哪些SQL語句,而後進行鍼對性的分析便可。通常狀況下,全表掃描、不加索引、大表的count這些都比較容易引發cpu問題。絕大多數狀況下均可以經過技術手段來優化,但也有可能技術手段沒法優化的狀況,則能夠考慮業務上的優化。

4. 應用服務器磁盤IO問題

絕大多數狀況下是因爲日誌問題致使的,日誌問題通常分爲以下兩種狀況:

  1. 壓測接口參數/環境有問題,致使接口不停地打印異常
  2. 打印了大量的業務日誌

至於其餘的磁盤IO問題,則須要根據實際業務去分析了,暫時未遇到過,此處略過。

5. GC問題

通常狀況下,咱們不太須要去關注YoungGC,更多地只須要關注FullGC就好了,若是隻是偶爾出現一次FullGC,那基本上沒有太大問題,若是頻繁FullGC(幾秒就有一次FullGC,甚至可能一秒幾回),那就要作相應排查了。

如何監測FullGC

通常能夠經過jstat來監測,命令以下:

jstat -gccause [PID] 1000
複製代碼

image-20191106141050865

具體的每一個參數的含義能夠查看man jstat手冊。

其實用visualvm裝個GC插件而後監測java進程,能夠很直觀地看到java應用的內存和GC狀況,就是操做相對而言比較繁瑣。

如何排查GC問題

不少狀況下(主要是大對象/大量堆對象致使FullGC的狀況),均可以經過將Java堆dump下來,而後經過MAT、jhat等內存分析工具來分析。流程以下:

  1. 首先須要dump堆文件,在服務器上執行以下命令: jmap -dump:format=b,file=heap.bin [PID]
  2. 而後將堆文件拷到本地,使用MAT打開(須要調大內存啓動參數,必需要比堆文件大),我的比較經常使用的MAT功能是"Leak Suspects"和"Histogram",前者是可能出現內存泄漏的懷疑點,後者是堆中類的直方圖
舉個例子

此處以一個真實的出現過宕機的Java應用的堆做爲舉例(加上-XX:+HeapDumpOnOutOfMemoryError這個參數就能夠在出現OOM的時候自動將堆dump下來了)

image-20191106144338830

本文簡單看一下"Leak Suspects",至於Histogram則能夠自行去研究。

image-20191106145139680

這個堆文件其實仍是比較簡單的,由於可懷疑點只有一個,八九不離十就是這塊出現問題了。點擊"Details"能夠看到更詳細的信息(ps:不是每種懷疑對象都有Details的)。

image-20191106145600510

在詳細信息裏面基本上能夠很明顯地看出來,有一個SQL語句查出來了超多的數據,致使內存塞不下了。事實上,最終在數據庫日誌中找到了這條語句,共查詢了200W+條數據。

這個例子比較簡單,事實上咱們可能會遇到更多複雜的狀況,例如懷疑對象特別多,甚至真正緣由並不在懷疑對象中,或者metaspace致使的FullGC,這些狀況下,咱們可能又須要採用其餘方式去處理這些問題。

6. 應用服務器cpu達到100%問題(如下只針對常規業務應用,不考慮追求極端性能應用)

還記得以前的有個問題——你認爲cpu達到100%是好是壞嗎?

那麼在這裏我揭曉一下答案,若是接口的TPS高,那麼咱們的服務器的cpu固然是越高越好了,由於這說明了資源被充分利用了。可是,若是接口的TPS低,那麼cpu達到100%就說明頗有多是有問題了,很大多是存在問題代碼佔據了大量的cpu。

那麼還有一個問題就是,你認爲哪些代碼對cpu的開銷大?

  1. 大量的業務邏輯判斷——幾乎無影響 (上萬個if語句可能總的執行時間都不會超過1ms)
  2. 大量的網絡傳輸——幾乎無影響 (會有一些cpu開銷,可是極其有限,能夠找個應用生成火焰圖看看)
  3. 線程阻塞——幾乎無影響 (若是不考慮極其大量的線程切換的話,那麼線程阻塞是不會佔用cpu的)
  4. 大量的內存拷貝——幾乎無影響 (會有一些cpu開銷,可是極其有限,能夠找個應用生成火焰圖看看)

這些都沒有影響,那到底什麼纔對cpu有影響呢?常見的業務場景總結以下(若有遺漏請留言補充)

  1. 長度特別大的循環,尤爲是多層嵌套循環
  2. 字符串處理,常見的消耗cpu的操做是json解析、正則表達式
  3. 日期格式化,Date.format很是消耗cpu
  4. 大量的sql數據處理,sql數據處理量不少的狀況下是會大量佔用cpu的,這個狀況最爲常見
  5. 大量的日誌輸出有時也會佔用很多的cpu,不過更多的狀況是產生線程阻塞

排查cpu問題的方式

我在大促壓測中實踐的比較多的方式是perf + perf-map-agent + FlamaGraph工具組合,其中perf是用來監控各個函數的cpu消耗(能夠實時監控,也能夠記錄一段時間的數據),perf-map-agent是用來輔助perf使用的,用來生成java堆的映射文件,FlamaGraph則是用來生成火焰圖的。

這套工具的安裝使用就不作介紹了,能夠參考一下下面這兩篇文章,

senlinzhan.github.io/2018/03/18/… www.jianshu.com/p/bea2b6a1e…

主要使用方式,有以下兩種:

  1. sudo perf top [-g],能夠實時觀察cpu的消耗,操做相對比較輕量級
  2. 生成火焰圖(必定要用瀏覽器打開),操做至關繁瑣,不過生成的信息也更詳細,更易閱讀,對於那些沒法一眼看出來的問題會有不錯的效果

火焰圖示例

下面展現一下本次大促壓測solr優化過程當中生成的火焰圖,從圖中能夠看到YoungGC就佔用了將近一半的cpu,

image-20191106155941396

perf-top示例

用這個示例代碼作個perf-top的使用示範:

import java.text.SimpleDateFormat;
import java.util.Date;

public class Cpu {

    private static final int LIMIT = 100000000;

    public static void main(String[] args) {
        simple();
    }

    private static void simple() {
        int count = 0;
        long startTime = System.currentTimeMillis();
        while (count < LIMIT) {
            Date date = new Date();
            SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String sd = df.format(date);
            if (sd.length() == 23) {
                count++;
            }
        }
        System.out.println(System.currentTimeMillis() - startTime);
    }
}
複製代碼

首先使用perf-map-agent/bin/create-java-perf-map.sh [PID]生成JVM映射文件,而後使用sudo perf top能夠看到cpu基本上都被SimpleDateFormat給佔用了,(下面還有不少展現不出來的,事實上會更多)

image-20191107113205727

有了這些工具以後,絕大多數問題都已經能夠比較容易地找到性能優化點了。

其餘一些方式

上面那套組合實際用起來十分繁瑣,大促壓測結束後又瞭解到了一些其餘工具,不過未通過真實實踐,因此列出來僅作參考:

  1. jvmtop
  2. github.com/oldratlee/u… ,這個裏面有個show-busy-java-threads腳本,試用了一下,感受超方便,後續考慮在真實排查問題中實踐一下

7. 線程阻塞問題

當咱們在系統的全部環節都沒法找到硬件瓶頸的時候,那每每就是線程產生了阻塞,通常狀況下線程阻塞可使用jstack和arthas來排查,分別舉個例子吧,用下面這段樣例代碼:

public class Main {

    public static void main(String[] args) {
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                while (true) {
                    run();
                }
            }, "myThread-" + i);
        }
    }

    private static void run() {
        Integer x = 1;
        for (int i = 0; i < 100000; i++) {
            x *= i;
        }
        System.out.println(x);
        sleep(10);
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
複製代碼

jstack的方式查看

jstack [PID]
複製代碼

image-20191106184616989

從圖中能夠看到大量的線程都卡在Block.sleep()上。通常狀況下,jstack能夠配合grep來使用,一般關注得比較多的狀態更可能是BLOCKED。jstack相對而言沒那麼直觀,可是比較輕量級,不少時候也能夠比較容易地看出來一些常見的線程阻塞問題。

arthas的方式查看

arthas實際上是一個比較全能的jvm性能分析工具,用起來也是各類舒服,並且相對而言也比較輕量,強烈推薦。

此處主要介紹arthas在排查線程阻塞方面的應用,

  1. 執行arthas,命令java -jar arthas-boot.jar
  2. 選擇目標java進程
  3. 執行trace [包名.類名] [方法名] -n [數量] '#cost>[執行時間]'就能夠查看了,更多參數能夠查詢arthas的文檔

image-20191106185440691

如上圖,咱們能夠看到各個方法的執行時間(包含了阻塞時間),篩選出執行時間長的方法,很大可能就能發現形成線程阻塞的瓶頸點。

8. 內存泄漏問題

內存泄漏問題每每都伴隨着宕機,我所碰見的狀況有以下幾種:

Heap內存泄漏

這種狀況屬於相對而言比較容易處理的狀況,使用-XX:+HeapDumpOnOutOfMemoryError參數能夠在應用宕機的時候自動dump下堆文件,而後使用MAT等內存分析工具在絕大多數狀況下均可以找到問題緣由。

metaspace內存泄漏

這個有見過JVM調用groovy在某些狀況下會產生內存泄漏。不過沒有真實排查過相關問題,此處略過。

防風有一篇文章能夠參考一下GroovyClassLoader 引起的 FullGC

nonheap內存泄漏

nonheap內存泄漏問題屬於很是難排查的問題,通常狀況下比較難dump下堆文件,即便dump下來了,通常狀況下也很難肯定緣由,以前有用過tcmalloc、jemalloc等工具進行排查過。暫時沒找到什麼比較通用的套路,通常也是特事特辦。根據以前的排查經驗來看,以下幾種狀況會比較容易出現nonheap內存泄漏(若是遺漏,請留言補充):

  1. 圖片合成業務中,涉及到Font的建立,能夠詳見以前的文章記一次Font致使JVM堆外內存泄漏分析
  2. 使用了JNI的狀況下頗有可能會致使JVM的arena內存區恰好超過機器內存限制 / nonheap內存泄漏(能夠參考防風的文章JNI 引起的堆外內存泄露
  3. GZIPStream未關閉的狀況會致使nonheap泄漏 (來源於網上資料,未真實遇到過)

9. 從業務角度去排查問題

排查不少問題以前,最好可以先去了解一下相關業務邏輯,由於不少性能問題是因爲大量的問題業務代碼引發的,不少時候從業務角度去考慮、輔以技術手段每每可以獲得更好的效果。

10. 總結

上面的各類方式只是提供一些策略,沒法保證100%可以找到問題,甚至可能連70%都保證不了,更多狀況下咱們須要靈活使用各類工具進行問題分析。總結一下上面的性能分析工具,能夠大概以下分類:

類型 工具
全能型分析工具 arthas、visualvm
cpu分析工具 perf、jvmtop
內存分析工具 jmap、jhat、MAT
網絡分析工具 tcpdump、wireshark
GC分析工具 jstat、gc日誌文件、visualvm
堆棧分析工具 jstack、arthas

有些工具甚至有更多的功能,例如arthas和visualvm,可能會漏掉一些分類,每種分類也一樣還有着各類各樣其餘的分析工具,此處就不求盡善盡美了。

6、壓測中出現的典型性能問題

如下總結一下我在大促壓測過程當中所遇到的一些比較典型的性能問題。

1. Log4j日誌阻塞問題

公司的部分老應用仍然使用的Log4j,打印日誌所有爲同步方式,就會致使在併發高且業務日誌多的狀況下,會形成日誌大量阻塞。

2. redis大value問題

有些代碼不論有多大的數據都直接往redis裏面塞,只要併發稍微一高,就很容易致使redis的帶寬達到上限。

3. sql全字段查詢問題

不少代碼查詢mysql的時候,不管什麼場景都會將表的全部的字段都查詢出來,會致使兩個結果:

  1. 網絡帶寬極大浪費,尤爲是查詢中包含了沒必要要的"描述"等超大字段
  2. 極大地消耗cpu資源

4. sql未加索引問題

比較容易犯的問題,通常會產生慢SQL,甚至可能致使數據庫cpu消耗嚴重。

5. sql N+1問題

也是比較容器犯的問題,會對應用自己和數據庫都產生或多或少的性能影響,至於具體的影響度暫時尚未直觀數據。

6. 正則表達式問題

正則表達式在業務中也是比較經常使用的,可是有些糟糕的正則表達式可能會致使一些可怕的後果,會嚴重消耗cpu資源,舉個例子,以下

public class Regex {

    public static void main(String[] args) {
        String regex = "(\\w+,?)+";
        String val = "abcdefghijklmno,abcdefghijklmno+";
        System.out.println(val.matches(regex));
    }
}
複製代碼

就這麼一段看上去簡單的代碼,會一直保持着cpu單核100%的狀態,並且會執行15秒左右。具體緣由能夠詳見防風的文章 www.ffutop.com/posts/2018-…

7. DateFormat問題

大量使用DateFormat致使極大地cpu資源消耗,通常狀況下請使用FastDateFormat替代SimpleDateFormat,性能能提高一倍以上。對於一些時間點比較規整的且瓶頸點仍在DateFormat上的,能夠考慮使用緩存等方案。

8. 線程池不正確使用問題

遠程調用時長遠大於cpu消耗的業務直接使用默認線程池或着線程數設置太少,很容易致使線程阻塞。

9. 傳說中的問題

只據說過,可是我還從未真實見到過,

  1. 大量線程切換致使cpu開銷大
  2. 數據庫死鎖問題
  3. 數據庫鏈接池設置問題(有發生過幾回,可是我都不在現場)
  4. ... ...
相關文章
相關標籤/搜索