性能問題從發(fā)現(xiàn)到優(yōu)化一般思路
JAVA前線
歡迎大家關(guān)注公眾號(hào)「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實(shí)際應(yīng)用、架構(gòu)思維、職場(chǎng)分享、產(chǎn)品思考等等,同時(shí)也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
1 文章概述
技術(shù)系統(tǒng)有一個(gè)發(fā)展過程,在業(yè)務(wù)初期主要是實(shí)現(xiàn)業(yè)務(wù)功能和目標(biāo),由于數(shù)據(jù)和訪問量都不大,所以性能問題并不作為首要考慮。
但是隨著業(yè)務(wù)發(fā)展,隨著數(shù)據(jù)和訪問量增大甚至激增,造成例如首頁五秒鐘才能展示等問題,這種不佳體驗(yàn)會(huì)造成用戶流失,此時(shí)性能就是必須面對(duì)之問題。我們把技術(shù)系統(tǒng)分為早期、中期、后期三個(gè)階段:
早期:主要實(shí)現(xiàn)業(yè)務(wù)需求,性能非重點(diǎn)考慮 中期:性能問題注解顯現(xiàn),影響業(yè)務(wù)發(fā)展 后期:技術(shù)迭代性能與業(yè)務(wù)必須同時(shí)考慮
如何發(fā)現(xiàn)性能問題,并且最終如何解決性能問題就是本文討論之要點(diǎn)。
2 什么是性能
我們可以從四個(gè)維度介紹什么是性能:
兩個(gè)維度定義性能: 速度慢 壓力大 兩個(gè)維度描述性能: 定性:直觀感受 定量:指標(biāo)分析
3 發(fā)現(xiàn)性能問題
3.1 定性 + 速度
一個(gè)頁面需要長時(shí)間打開,一個(gè)列表很慢才能加載完成,一個(gè)接口訪問導(dǎo)致超時(shí)異常,這些顯而易見之問題可以歸為此類。
3.2 定量 + 速度
3.2.1 速度指標(biāo)
一個(gè)公司有7200名員工,每天上班打卡時(shí)間是早上8點(diǎn)到8點(diǎn)30分,每次打卡時(shí)間系統(tǒng)執(zhí)行時(shí)長5秒,那么RT、QPS、并發(fā)量分別是多少?
RT表示響應(yīng)時(shí)間,問題已經(jīng)包含答案:
RT = 5秒
QPS表示每秒訪問量,假設(shè)行為平均分布:
QPS = 7200 / (30 * 60) = 4
并發(fā)量表示系統(tǒng)同時(shí)處理請(qǐng)求數(shù):
并發(fā)量 = QPS x RT = 4 x 5 = 20
根據(jù)上述實(shí)例引出公式:
并發(fā)量 = QPS x RT
3.2.2 QPS VS TPS
QPS(Queries Per Second):每秒查詢量
TPS(Transactions Per Second):每秒事務(wù)數(shù)
需要注意此事務(wù)并不是指數(shù)據(jù)庫事務(wù),而是包括以下三個(gè)階段:
接收請(qǐng)求 處理業(yè)務(wù) 返回結(jié)果
QPS = N * TPS (N>=1)
N=1表示接口有一個(gè)事務(wù):
public class OrderService {
public Order queryOrderById(String orderId) {
return orderMapper.selectById(orderId);
}
}
N>1表示接口有多個(gè)事務(wù):
public class OrderService {
public void updateOrder(Order order) {
// transaction1
orderMapper.update(order);
// transaction2
sendOrderUpdateMessage(order);
}
}
3.2.3 發(fā)現(xiàn)問題
(1) 打印日志
public class FastTestService {
public void test01() {
long start = System.currentTimeMillis();
biz1();
biz2();
long costTime = System.currentTimeMillis() - start;
System.out.println("costTime=" + costTime);
}
private void biz1() {
try {
System.out.println("biz1");
Thread.sleep(500L);
} catch (Exception ex) {
log.error("error", ex);
}
}
private void biz2() {
try {
System.out.println("biz2");
Thread.sleep(1000L);
} catch (Exception ex) {
log.error("error", ex);
}
}
}
(2) StopWatch
import org.springframework.util.StopWatch;
import org.springframework.util.StopWatch.TaskInfo;
public class FastTestService {
public void test02() {
StopWatch sw = new StopWatch("testWatch");
sw.start("biz1");
biz1();
sw.stop();
sw.start("biz2");
biz2();
sw.stop();
// 簡單輸出耗時(shí)
System.out.println("costTime=" + sw.getTotalTimeMillis());
System.out.println();
// 輸出任務(wù)信息
TaskInfo[] taskInfos = sw.getTaskInfo();
for (TaskInfo task : taskInfos) {
System.out.println("taskInfo=" + JSON.toJSONString(task));
}
System.out.println();
// 格式化任務(wù)信息
System.out.println(sw.prettyPrint());
}
}
輸出結(jié)果:
costTime=1526
taskInfo={"taskName":"biz1","timeMillis":510,"timeNanos":510811200,"timeSeconds":0.5108112}
taskInfo={"taskName":"biz2","timeMillis":1015,"timeNanos":1015439700,"timeSeconds":1.0154397}
StopWatch 'testWatch': running time = 1526250900 ns
---------------------------------------------
ns % Task name
---------------------------------------------
510811200 033% biz1
1015439700 067% biz2
(3) trace
Arthas是阿里開源Java診斷工具:
Arthas是一款線上監(jiān)控診斷產(chǎn)品,通過全局視角實(shí)時(shí)查看應(yīng)用 load、內(nèi)存、gc、線程的狀態(tài)信息,并能在不修改應(yīng)用代碼的情況下,對(duì)業(yè)務(wù)問題進(jìn)行診斷,包括查看方法調(diào)用的出入?yún)ⅰ惓#O(jiān)測(cè)方法執(zhí)行耗時(shí),類加載信息等,大大提升線上問題排查效率
Arthas trace命令監(jiān)控鏈路每個(gè)節(jié)點(diǎn)耗時(shí):
https://arthas.aliyun.com/doc/trace.html
我們通過實(shí)例說明,首先編寫并運(yùn)行代碼:
package java.front.optimize;
public class FastTestService {
public static void main(String[] args) {
FastTestService service = new FastTestService();
while (true) {
service.test03();
}
}
public void test03() {
biz1();
biz2();
}
private void biz1() {
try {
System.out.println("biz1");
Thread.sleep(500L);
} catch (Exception ex) {
log.error("error", ex);
}
}
private void biz2() {
try {
System.out.println("biz2");
Thread.sleep(1000L);
} catch (Exception ex) {
log.error("error", ex);
}
}
}
第一步進(jìn)入arthas控制臺(tái):
$ java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.6.2
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER
* [1]: 14121
[2]: 20196 java.front.optimize.FastTestService
第二步輸入監(jiān)控進(jìn)程號(hào)并回車:
2
第三步trace命令監(jiān)控相應(yīng)方法:
trace java.front.optimize.FastTestService test03
第四步查看鏈路耗時(shí):
`---[1518.7362ms] java.front.optimize.FastTestService:test03()
+---[33.66% 511.2817ms ] java.front.optimize.FastTestService:biz1() #54
`---[66.32% 1007.2962ms ] java.front.optimize.FastTestService:biz2() #55
3.3 定性 + 壓力
系統(tǒng)壓力大也會(huì)表現(xiàn)出速度慢之特征,但是這種慢不僅僅是需要幾秒后才能打開網(wǎng)頁,而是網(wǎng)頁一直處于加載狀態(tài)最終白屏。
3.4 定量 + 壓力
服務(wù)器常見壓力指標(biāo)如下:
內(nèi)存 CPU 磁盤 網(wǎng)絡(luò)
服務(wù)端開發(fā)比較容易引發(fā)內(nèi)存和CPU問題,所以我們重點(diǎn)關(guān)注。
3.4.1 發(fā)現(xiàn)CPU問題
首先編寫一段造成CPU飆高之代碼并運(yùn)行:
public class FastTestService {
public static void main(String[] args) {
FastTestService service = new FastTestService();
while (true) {
service.test();
}
}
public void test() {
biz();
}
private void biz() {
System.out.println("biz");
}
}
(1) dashboard + thread
dashboard查看當(dāng)前系統(tǒng)實(shí)時(shí)面板,發(fā)現(xiàn)線程ID=1 CPU占用非常高(這個(gè)ID不可以與jstack nativeID相對(duì)應(yīng)):
$ dashboard
ID NAME GROUP PRIORI STATE %CPU DELTA TIME TIME INTERRU DAEMON
1 main main 5 RUNNA 96.06 4.812 2:41.2 false false
thread查看最忙前N個(gè)線程:
$ thread -n 1
"main" Id=1 deltaTime=203ms time=1714000ms RUNNABLE
at app//java.front.optimize.FastTestService.biz(FastTestService.java:83)
at app//java.front.optimize.FastTestService.test(FastTestService.java:61)
at app//java.front.optimize.FastTestService.main(FastTestService.java:17)
3.4.2 發(fā)現(xiàn)內(nèi)存問題
(1) free
$ free -h
total used free shared buff/cache available
Mem: 10G 5.5G 3.1G 28M 1.4G 4.4G
Swap: 2.0G 435M 1.6G
total
服務(wù)器總內(nèi)存
used
已使用內(nèi)存
free
未被任何應(yīng)用使用空閑內(nèi)存
shared
被共享物理內(nèi)存
cache
IO設(shè)備讀緩存(Page Cache)
buff
IO設(shè)備寫緩存(Buffer Cache)
available
可以被程序應(yīng)用之內(nèi)存
(2) memory
Arthas memory命令查看JVM內(nèi)存信息:
https://arthas.aliyun.com/doc/heapdump.html
查看JVM內(nèi)存信息(官方實(shí)例)
$ memory
Memory used total max usage
heap 32M 256M 4096M 0.79%
g1_eden_space 11M 68M -1 16.18%
g1_old_gen 17M 184M 4096M 0.43%
g1_survivor_space 4M 4M -1 100.00%
nonheap 35M 39M -1 89.55%
codeheap_'non-nmethods' 1M 2M 5M 20.53%
metaspace 26M 27M -1 96.88%
codeheap_'profiled_nmethods' 4M 4M 117M 3.57%
compressed_class_space 2M 3M 1024M 0.29%
codeheap_'non-profiled_nmethods' 685K 2496K 120032K 0.57%
mapped 0K 0K - 0.00%
direct 48M 48M - 100.00%
(3) jmap
查看JAVA程序進(jìn)程號(hào)
jps -l
查看實(shí)時(shí)內(nèi)存占用
jhsdb jmap --heap --pid 20196
導(dǎo)出快照文件
jmap -dump:format=b,file=/home/tmp/my-dump.hprof 20196
內(nèi)存溢出自動(dòng)導(dǎo)出堆快照
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath==/home/tmp/my-dump.hprof
(4) heapdump
Arthas heapdump命令支持導(dǎo)出堆快照:
https://arthas.aliyun.com/doc/heapdump.html
dump至指定文件
heapdump /home/tmp/my-dump.hprof
dump live對(duì)象至指定文件
heapdump --live /home/tmp/my-dump.hprof
dump至臨時(shí)文件
heapdump
(5) 垃圾回收
jstat可以查看垃圾回收情況,觀察程序是否頻繁GC或者GC用時(shí)是否過長:
jstat -gcutil <pid> <interval(ms)>
每秒查看垃圾回收情況
$ jstat -gcutil 20196 1000
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 0.00 57.69 0.00 - - 0 0.000 0 0.000 0 0.000 0.000
0.00 0.00 57.69 0.00 - - 0 0.000 0 0.000 0 0.000 0.000
0.00 0.00 57.69 0.00 - - 0 0.000 0 0.000 0 0.000 0.000
各參數(shù)說明如下:
S0:新生代中Survivor 0區(qū)占已使用空間比例
S1:新生代中Survivor 1區(qū)占已使用空間比例
E:新生代占已使用空間比例
O:老年代占已使用空間比例
P:永久帶占已使用空間比例
YGC:應(yīng)用程序啟動(dòng)至今,發(fā)生Young GC次數(shù)
YGCT:應(yīng)用程序啟動(dòng)至今,Young GC所用時(shí)間(秒)
FGC:應(yīng)用程序啟動(dòng)至今,發(fā)生Full GC次數(shù)
FGCT:應(yīng)用程序啟動(dòng)至今,F(xiàn)ull GC所用時(shí)間(秒)
GCT:應(yīng)用程序啟動(dòng)至今,所用垃圾回收總時(shí)間(秒)
3.4.3 綜合發(fā)現(xiàn)問題
(1) 壓力測(cè)試
進(jìn)行系統(tǒng)壓測(cè)可以主動(dòng)暴露系統(tǒng)問題,評(píng)估系統(tǒng)容量,簡單常用參數(shù)如下:
常用工具:JMeter 階梯發(fā)壓:線程數(shù)10、20、30遞增至瓶頸 持續(xù)時(shí)間:持續(xù)1分鐘,Ramp-Up=0 TPS:Throughput 響應(yīng)時(shí)間:重點(diǎn)關(guān)注95Line
(2) 監(jiān)控系統(tǒng)
監(jiān)控系統(tǒng)可以更加友好展示相關(guān)指標(biāo),如果公司具有一定技術(shù)實(shí)力可以自研,否則可以選擇使用業(yè)界通用方案。
4 優(yōu)化性能問題
4.1 四個(gè)方法
減少請(qǐng)求
空間換時(shí)間
任務(wù)并行化
任務(wù)異步化
4.2 五個(gè)層面
代理層 前端層 服務(wù)層 緩存層 數(shù)據(jù)層
4.3 優(yōu)化說明
一說到性能優(yōu)化不難想到例如加索引、加緩存等方案,這也許是正確的,但是這樣思考可能會(huì)造成遺漏,因?yàn)檫@只是緩存層和數(shù)據(jù)層的方案。
如果可以將無效流量在最外層拒絕,那么這是對(duì)系統(tǒng)更好地好保護(hù)。四個(gè)方法可以應(yīng)用在每一個(gè)層面,我們不妨舉一些例子:
(1) 減少請(qǐng)求 + 前端層
在秒殺場(chǎng)景中設(shè)置前置驗(yàn)證碼
(2) 減少請(qǐng)求 + 服務(wù)層
多次RPC是否可以轉(zhuǎn)換為一次批量RPC
(3) 空間換時(shí)間 + 服務(wù)層
引入緩存
(4) 空間換時(shí)間 + 緩存層
引入多級(jí)緩存
(5) 空間換時(shí)間 + 數(shù)據(jù)層
新增索引
(6) 任務(wù)并行化 + 服務(wù)層
如果多個(gè)調(diào)用互不依賴,使用Future并行化
(7) 任務(wù)異步化 + 服務(wù)層
如果無需等待返回結(jié)果,可以異步執(zhí)行
5 文章總結(jié)
第一本文討論了系統(tǒng)早期、中期、后期如何看待性能問題,第二討論了什么是性能,第三討論了如果發(fā)現(xiàn)性能問題,第四討論了如何優(yōu)化性能問題,希望本文對(duì)大家有所幫助。
JAVA前線
歡迎大家關(guān)注公眾號(hào)「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實(shí)際應(yīng)用、架構(gòu)思維、職場(chǎng)分享、產(chǎn)品思考等等,同時(shí)也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
