連續參加了兩年公司的雙十一大促壓測項目,遇到了不少問題,也成長了不少,因而在這裏對大促壓測作一份總結。以及記錄一下大促壓測過程當中出現的一些常見的Java應用性能問題。java
吞吐率 TPS(每秒響應的請求數量)mysql
響應時長 RT (通常狀況下重點關注90%請求的響應時長,咱們的大促標準通常是1s之內)git
錯誤率 (看業務的可接受程度,咱們的大促標準是不超過2%)github
如今有不少能夠用來進行壓測的工具,例如ab、jmeter、wrk等,此處主要介紹一下ab和jmeter。正則表達式
ab是一個命令行工具,使用起來很是簡單,redis
# -c表示併發數,-n表示請求總數,其餘一些參數能夠查詢手冊/相關資料
ab -c 10 -n 200 https://www.baidu.com/
複製代碼
命令執行完成後會得出一份壓測結果報告,其中錯誤請求數、TPS和RT在下圖中有標註算法
jmeter同時支持圖形化界面和命令行方式,sql
首先在"Test Plan"中添加一個"線程組",裏面能夠設置併發數、壓測時長等參數。數據庫
接下來須要在「線程組」中添加「HTTP請求取樣器」,裏面是設置HTTP請求的各項參數。json
最後添加查看結果用的監聽組件,我我的比較經常使用的有「查看結果樹」、「聚合報告」和「TPS曲線圖」(須要安裝)。
重點來看一下「聚合報告」,(必定要記得每次壓測前都要清理一下數據才行[上方的"齒輪+2個掃把"圖標],否則回合以前的數據混合在一塊兒)
上面只是一些簡單的介紹,事實上jmeter還支持不少複雜的壓測場景: jdbc壓測、dubbo壓測、動態參數壓測、自定義響應斷言……這些能夠自行網上搜索。
命令行方式主要能夠用來作一些自動化壓測的任務。使用方式以下:
jmeter -n -t [jmx腳本] -l [壓測請求結果文件] -e -o [壓測報告文件夾(會生成一堆網頁文件)]
複製代碼
其中jmx腳本能夠先經過jmeter圖形化界面所有設置好了,而後保存一下就會生成對應的jmx腳本了。
###3. ab與jmeter的對比
ab | jmeter | |
---|---|---|
操做難度 | 簡單 | 複雜 |
命令行 | 支持,操做簡單 | 支持,操做稍微複雜一些 |
請求結果列表 | 沒法顯示 | 有詳細請求列表 |
動態參數 | 不支持 | 支持 |
複雜場景支持 | 極其有限 | 豐富 |
基本上,對於一些簡單的固定參數請求而且是自測的狀況下,使用ab會很是簡便。通常狀況下jmeter的適用性會更廣。
通常狀況下,沒必要要將公司全部的接口都進行壓測,壓測接口主要包含核心鏈路接口、訪問量大的接口以及新上線的活動接口。獲取方式基本是以下兩種:
在上面的兩種壓測工具中,咱們都看到了一個參數爲併發數,這個參數通常須要根據公司的業務量來進行推算,能夠去網上找些資料。不過爲了簡化壓測過程,咱們公司的大促統一使用讀接口200併發,寫接口100併發的標準來執行的。
事實上,我對併發數的設定這塊也比較模糊,所以上述描述僅作參考。
通常是根據大促銷售目標、平時各接口qps、各接口訪問量按照比例制定出最終的TPS目標,不要忘了最後乘上一個風險係數。具體的算法能夠自行設計,大概思路就是這樣的。
對於壓測工具的指標上面已經說過了,主要是關注TPS、RT和錯誤率。
那麼還有哪些須要關注的指標呢?其實這個都是根據公司的業務來決定的,例如咱們公司主要使用java應用、mysql做爲數據庫、redis做爲緩存中間件,那麼咱們主要關注的性能數據以下:
監控對象 | 性能指標 |
---|---|
應用服務器(服務化應用包含下游鏈路的應用服務器) | CPU、網絡帶寬、磁盤IO、GC |
數據庫 | CPU、網絡帶寬、慢SQL |
REDIS | 網絡帶寬 |
每一個監控對象都有其特性,因此應該根據實際狀況的來制定本身的監控指標。
因爲公司主要使用的是Java8,所以本文也主要是針對Java8應用作分析。
舉個例子,若是告訴你一個接口稍微一些壓力就能把服務器的cpu跑滿了,致使TPS上不去,裏面一堆複雜邏輯,並且還有很多遠程調用(數據庫查詢、緩存查詢、dubbo調用等)。
你可能對業務很是熟悉,開始大刀闊斧地進行代碼修改、增長緩存、業務降級等,也許指望很美好,可是事實上有極大的多是你作的一切對TPS只能產生輕微的影響。而後只能經過不停地嘗試刪改代碼去查找問題點,那麼顯然只能帶來幾個結果: 1.效率低下 2.把代碼弄的一團糟 3.不具有可複製性 4.對業務會形成或小或大的影響,最最關鍵的是改的時候內心也沒底、改完以後內心依舊沒底。
##如何排查性能問題
那麼問題來了,咱們到底應該怎麼去排查問題呢?(如下均爲一些我的經驗,可能會有很多遺漏,或者會有一些錯誤,若是有的話,請及時指出)
排查問題的話,首先咱們須要先有一些排查的突破點和方向。(沒法保證100%找到對應問題,可是大幅提高找到性能問題的效率)
前面有提到,咱們壓測過程當中須要監控各項指標,那麼其實咱們的突破方向通常就在這些監控指標上了。咱們能夠對這些指標進行分類,對於每一類均可以有着相對應的排查策略。
這個問題是最好排查的一類問題了,只須要對慢SQL進行鍼對性地分析優化便可,此處不過多講解。
那麼一個問題來了,此處的網絡帶寬究竟是指的什麼呢?換個問法吧,假設數據庫的帶寬上限爲1Gbps,實際上壓測致使數據庫的網絡帶寬佔用了800Mbps,那能夠說明這個接口是一個問題接口嗎?
考慮下面這種狀況,這個接口的TPS假設在壓測過程當中達到了80000,遠大於接口實際目標TPS,那該接口將數據庫的帶寬佔到800Mbps是合情合理的。
那麼上面的問題的答案也就呼之欲出了,這裏的網絡帶寬,在不少狀況下,咱們更應該關注的是單個請求的平均佔用帶寬。
猜一猜,其實不難想象,就是抓包。我經常使用的抓包方式是經過tcpdump抓包,而後使用wireshark解析抓包內容(若是有更簡單的方式,能夠留言)。下面講一下tcpdump+wireshark的方式如何抓包。
爲了不大量的數據混雜在一塊兒,通常狀況下,我更喜歡是抓單個請求的數據,而不是在壓測中抓包。下面簡單介紹一下tcpdump和wireshark如何抓包,
sudo tcpdump -w xxx.pcap
打開TCP流後經過調整右下方的"Stream",咱們就能夠看到應用在請求過程當中的網絡數據(包含Http請求數據、Mysql請求數據、Redis請求數據……),如下圖爲例,能夠看到這個請求的mysql請求量很是大,接下來就是查看究竟是哪些SQL語句致使的。
開啓數據庫日誌,看看壓測期間都執行了哪些SQL語句,而後進行鍼對性的分析便可。通常狀況下,全表掃描、不加索引、大表的count這些都比較容易引發cpu問題。絕大多數狀況下均可以經過技術手段來優化,但也有可能技術手段沒法優化的狀況,則能夠考慮業務上的優化。
絕大多數狀況下是因爲日誌問題致使的,日誌問題通常分爲以下兩種狀況:
至於其餘的磁盤IO問題,則須要根據實際業務去分析了,暫時未遇到過,此處略過。
通常狀況下,咱們不太須要去關注YoungGC,更多地只須要關注FullGC就好了,若是隻是偶爾出現一次FullGC,那基本上沒有太大問題,若是頻繁FullGC(幾秒就有一次FullGC,甚至可能一秒幾回),那就要作相應排查了。
通常能夠經過jstat來監測,命令以下:
jstat -gccause [PID] 1000
複製代碼
具體的每一個參數的含義能夠查看man jstat
手冊。
其實用visualvm裝個GC插件而後監測java進程,能夠很直觀地看到java應用的內存和GC狀況,就是操做相對而言比較繁瑣。
不少狀況下(主要是大對象/大量堆對象致使FullGC的狀況),均可以經過將Java堆dump下來,而後經過MAT、jhat等內存分析工具來分析。流程以下:
jmap -dump:format=b,file=heap.bin [PID]
此處以一個真實的出現過宕機的Java應用的堆做爲舉例(加上-XX:+HeapDumpOnOutOfMemoryError這個參數就能夠在出現OOM的時候自動將堆dump下來了)
本文簡單看一下"Leak Suspects",至於Histogram則能夠自行去研究。
這個堆文件其實仍是比較簡單的,由於可懷疑點只有一個,八九不離十就是這塊出現問題了。點擊"Details"能夠看到更詳細的信息(ps:不是每種懷疑對象都有Details的)。
在詳細信息裏面基本上能夠很明顯地看出來,有一個SQL語句查出來了超多的數據,致使內存塞不下了。事實上,最終在數據庫日誌中找到了這條語句,共查詢了200W+條數據。
這個例子比較簡單,事實上咱們可能會遇到更多複雜的狀況,例如懷疑對象特別多,甚至真正緣由並不在懷疑對象中,或者metaspace致使的FullGC,這些狀況下,咱們可能又須要採用其餘方式去處理這些問題。
還記得以前的有個問題——你認爲cpu達到100%是好是壞嗎?
那麼在這裏我揭曉一下答案,若是接口的TPS高,那麼咱們的服務器的cpu固然是越高越好了,由於這說明了資源被充分利用了。可是,若是接口的TPS低,那麼cpu達到100%就說明頗有多是有問題了,很大多是存在問題代碼佔據了大量的cpu。
那麼還有一個問題就是,你認爲哪些代碼對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…
主要使用方式,有以下兩種:
火焰圖示例
下面展現一下本次大促壓測solr優化過程當中生成的火焰圖,從圖中能夠看到YoungGC就佔用了將近一半的cpu,
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給佔用了,(下面還有不少展現不出來的,事實上會更多)
有了這些工具以後,絕大多數問題都已經能夠比較容易地找到性能優化點了。
上面那套組合實際用起來十分繁瑣,大促壓測結束後又瞭解到了一些其餘工具,不過未通過真實實踐,因此列出來僅作參考:
當咱們在系統的全部環節都沒法找到硬件瓶頸的時候,那每每就是線程產生了阻塞,通常狀況下線程阻塞可使用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 [PID]
複製代碼
從圖中能夠看到大量的線程都卡在Block.sleep()
上。通常狀況下,jstack能夠配合grep來使用,一般關注得比較多的狀態更可能是BLOCKED。jstack相對而言沒那麼直觀,可是比較輕量級,不少時候也能夠比較容易地看出來一些常見的線程阻塞問題。
arthas實際上是一個比較全能的jvm性能分析工具,用起來也是各類舒服,並且相對而言也比較輕量,強烈推薦。
此處主要介紹arthas在排查線程阻塞方面的應用,
java -jar arthas-boot.jar
trace [包名.類名] [方法名] -n [數量] '#cost>[執行時間]'
就能夠查看了,更多參數能夠查詢arthas的文檔如上圖,咱們能夠看到各個方法的執行時間(包含了阻塞時間),篩選出執行時間長的方法,很大可能就能發現形成線程阻塞的瓶頸點。
內存泄漏問題每每都伴隨着宕機,我所碰見的狀況有以下幾種:
這種狀況屬於相對而言比較容易處理的狀況,使用-XX:+HeapDumpOnOutOfMemoryError參數能夠在應用宕機的時候自動dump下堆文件,而後使用MAT等內存分析工具在絕大多數狀況下均可以找到問題緣由。
這個有見過JVM調用groovy在某些狀況下會產生內存泄漏。不過沒有真實排查過相關問題,此處略過。
防風有一篇文章能夠參考一下GroovyClassLoader 引起的 FullGC
nonheap內存泄漏問題屬於很是難排查的問題,通常狀況下比較難dump下堆文件,即便dump下來了,通常狀況下也很難肯定緣由,以前有用過tcmalloc、jemalloc等工具進行排查過。暫時沒找到什麼比較通用的套路,通常也是特事特辦。根據以前的排查經驗來看,以下幾種狀況會比較容易出現nonheap內存泄漏(若是遺漏,請留言補充):
排查不少問題以前,最好可以先去了解一下相關業務邏輯,由於不少性能問題是因爲大量的問題業務代碼引發的,不少時候從業務角度去考慮、輔以技術手段每每可以獲得更好的效果。
上面的各類方式只是提供一些策略,沒法保證100%可以找到問題,甚至可能連70%都保證不了,更多狀況下咱們須要靈活使用各類工具進行問題分析。總結一下上面的性能分析工具,能夠大概以下分類:
類型 | 工具 |
---|---|
全能型分析工具 | arthas、visualvm |
cpu分析工具 | perf、jvmtop |
內存分析工具 | jmap、jhat、MAT |
網絡分析工具 | tcpdump、wireshark |
GC分析工具 | jstat、gc日誌文件、visualvm |
堆棧分析工具 | jstack、arthas |
有些工具甚至有更多的功能,例如arthas和visualvm,可能會漏掉一些分類,每種分類也一樣還有着各類各樣其餘的分析工具,此處就不求盡善盡美了。
如下總結一下我在大促壓測過程當中所遇到的一些比較典型的性能問題。
公司的部分老應用仍然使用的Log4j,打印日誌所有爲同步方式,就會致使在併發高且業務日誌多的狀況下,會形成日誌大量阻塞。
有些代碼不論有多大的數據都直接往redis裏面塞,只要併發稍微一高,就很容易致使redis的帶寬達到上限。
不少代碼查詢mysql的時候,不管什麼場景都會將表的全部的字段都查詢出來,會致使兩個結果:
比較容易犯的問題,通常會產生慢SQL,甚至可能致使數據庫cpu消耗嚴重。
也是比較容器犯的問題,會對應用自己和數據庫都產生或多或少的性能影響,至於具體的影響度暫時尚未直觀數據。
正則表達式在業務中也是比較經常使用的,可是有些糟糕的正則表達式可能會致使一些可怕的後果,會嚴重消耗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-…
大量使用DateFormat致使極大地cpu資源消耗,通常狀況下請使用FastDateFormat替代SimpleDateFormat,性能能提高一倍以上。對於一些時間點比較規整的且瓶頸點仍在DateFormat上的,能夠考慮使用緩存等方案。
遠程調用時長遠大於cpu消耗的業務直接使用默認線程池或着線程數設置太少,很容易致使線程阻塞。
只據說過,可是我還從未真實見到過,