0 前言
前文還沒優化完,接着來折騰。
登錄要對個人信息對比驗證,驗證過程又要調用相應加密算法,而加密算法對性能要求很高。複雜加密算法安全性高,但性能差;不復雜加密算法性能好,但安全性低,要取捨。還有Session存儲和同步。大型系統,不管你在哪個系統訪問,調用其他系統時如需驗證身份就要同步Session信息,且做業務時,也要把相應Session信息帶上,不然就識別不了。
分析用户登錄功能,並瞭解在壓力過程中業務邏輯鏈路和整體TPS之間的關係。學會判斷線程中的BLOCKED原因。
1 修改加密算法
【系列07文】在基準場景中對登錄業務的測試結果,10個壓力線程下,TPS達100:
07文中發現加密算法BCrypt效率低後,討論優化方式:
- 用更快的加密方式
- 去掉這加密算法
本文試第一種,改為MD5:
- 更改加密算法。BCrypt加密算法雖安全性高,但性能差,建議改成MD5
- 加載所有用户到Redis
再跑壓力場景。跑這遍之前,只更改了加密算法,沒有執行加載緩存。一次只做一個動作來判斷結果:
性能有些上升,但還沒達期望。繼續使用緩存,看效果。
2 檢驗緩存效果
兩個手段檢驗,緩存對後續性能優化產生的效果:
- 把參數化數據量降下來,只用少量數據測試(僅嘗試,不是説用少量數據來運行場景是對的)
- 直接加載全部緩存
得到結果:
曲線看登錄接口達300 TPS。從硬件資源看,再根據經驗,應該還能更高。
分析過程中,再也沒有硬件資源的問題,但這裏通過查看全局監控數據,看到us cpu高,説明確實都是業務邏輯在消耗CPU資源。只有從登錄邏輯入手優化。
3 修改登錄的邏輯
原登錄邏輯:
Member服務調auth服務,可Auth服務為何還要到Member取用户名?自己直接查緩存或DB不香?從架構設計角度,為避免共享DB,這樣設計似乎也沒啥。只是在優化過程中,要根據實際環境做判斷。
在我們這環境中,需將DB共用,這樣Auth服務就能直接用DB,而不用再從Member繞一圈。所以,先改成新登錄邏輯,可減少一次調用:
修改後,登錄TPS如下:
從結果看,TPS確有增加,已到700以上,nice!
能結束分析了嗎?不,還要知道當前瓶頸點,每個業務都會有瓶頸點,不管優化到啥程度,除非一直把硬件資源耗光。
4 看架構圖
分析性能瓶頸之前,先看架構圖,看用户登錄接口涉及的服務和技術組件:
登錄操作跨Gateway/Member/Auth三服務,連接Redis/MySQL兩組件。圖中的MongoDB雖然看上去有線,但實際上登錄沒用上。
按分析邏輯,一步步分析。
5 拆分時間
修改登錄邏輯後的TPS:
響應時間已上升到100ms,現在要找出這時間消耗在哪。圖中的用户增加到150。這是為了把響應時間拉大,便於分析。
下面把這個響應時間拆分,看問題出在哪。
Gateway服務上的時間:
Member服務上的時間:
Auth服務上的時間:
Member服務上的時間消耗是150ms左右,Auth服務上的時間消耗有60ms左右。Member服務要着重分析,因為它響應時間更長。而Auth雖時間不太長,但也達60ms,經驗來説,還是稍長,最好平均能到50ms下,所以也要稍微關心。
6 全局監控
從這張全局監控圖數據看,worker-7/8 CPU使用率較高:
就查這兩節點跑的啥服務呀。看POD分佈,看每個POD在哪個worker節點,以便後面分析POD相互之間影響:
在worker-7/8,分別運行響應時間高的Auth、Member服務。對這兩個服務,都要分析,只得一個個來,就從auth服務開始吧。
為何先從Auth服務下手?它CPU更紅點。圖中其他地方也紅了,為什麼不關注呢?逐一給你分析:
- 圖中worker-1/2,內存使用率較大,達70%以上。經驗上,我幾乎沒咋關心過Linux內存使用率,除非出現大量page faults。因為Linux內存在分配給應用程序使用後,會體現在Cache當中。被應用程序Cache住的內存在os上來看都是被使用的,但實際上可能並未真被使用,這時os會把這部分Cache內存計算到available內存,所以直接看os級別的空閒內存是分析不出問題的。
- worker-2上,看到TCP的Time Wait達3萬,不過這也不是我關心的點,因為Time Wait是正常的TCP狀態,只有端口不夠用、內存嚴重不足,我才稍看一眼。
- 至於worker-1和worker-2的上下行帶寬,看起來真是不大。在內網結構中,我們在測試的時候,內網帶寬達到過好幾Gbps,這點帶寬還不足以引起我們的重視。
所以,我們要“收拾”的還是worker-7/8。
既然Auth服務在worker-7上,member服務在worker-8上,不如就從Auth開始。
7 Auth服務定向分析
既然Auth導致worker-7 CPU使用率偏高,就能走如下證據鏈:
按這證據鏈,應先看進程。不過,藝高人膽大,我直接看線程狀態,看能不能憑經驗蒙對把。於是,打開Spring Boot Admin的線程頁面:
滿目瘡痍,點開一些紅色地方後,看到:
阻塞數非常大。此外,鎖擁有者ID=86676,鎖擁有者名稱=線程823。
抓兩段棧看一下,找下鎖之間的關係,使用jstack、jvisualvm、spring boot admin、arthas皆可:
-- 第一處:
"http-nio-8401-exec-884" #86813 daemon prio=5 os_prio=0 tid=0x00007f2868073000 nid=0x559e waiting for monitor entry [0x00007f2800c6d000]
java.lang.Thread.State: BLOCKED (on object monitor
at java.security.Provider.getService(Provider.java:1035)
- waiting to lock <0x000000071ab1a5d8> (a sun.security.provider.Sun)
at sun.security.jca.ProviderList.getService(ProviderList.java:332)
.....................
at com.javaedge.mall.auth.util.MD5Util.toMD5(MD5Util.java:11)
at com.javaedge.mall.auth.config.MyPasswordEncoder.matches(MyPasswordEncoder.java:23)
.....................
at com.javaedge.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46
at com.javaedge.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
at com.javaedge.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
.....................
at com.javaedge.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
.....................
-- 第二處:
"http-nio-8401-exec-862" #86728 daemon prio=5 os_prio=0 tid=0x00007f28680d6000 nid=0x553a waiting for monitor entry [0x00007f2802b8c000]
java.lang.Thread.State: BLOCKED (on object monitor
at sun.security.rsa.RSACore$BlindingParameters.getBlindingRandomPair(RSACore.java:404)
- waiting to lock <0x000000071ddad410> (a sun.security.rsa.RSACore$BlindingParameters)
at sun.security.rsa.RSACore.getBlindingRandomPair(RSACore.java:443)
.....................
at com.javaedge.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46)
at com.javaedge.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
at com.javaedge.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
.....................
at com.javaedge.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
.....................
這兩個棧的內容不是同一時刻出現,説明這BLOCKED一直存在。但不管怎樣,這個棧在做RSA加密,和Token部分有關。
線程http-nio-8401-exec-884是BLOCKED狀態,説明有其他線程持有這個鎖,要看線程棧中的waiting to lock <0x000000071ab1a5d8>。如你有經驗,一下就能知道這裏是什麼問題。不過,做性能分析要講邏輯。
當你碰到這種鎖問題,又不知道具體原因,要打印一個完整棧看,而不是再到Spring Boot Admin亂點。不建議你這麼做的原因:
- 由於線程太多,點着看邏輯關係累
- 不斷在刷,暈
所以,對於前面遇到的鎖問題,先到容器中的jstack打印棧,把它下下來,然後工具打開看。
為何不用Arthas之類直接容器裏看?因為Arthas的Dashboard在Thread較多時,看起來累。
這圖就是jstack打印出的棧,在下載後用工具打開的效果:
看起來有那麼多的BLOCKED狀態的線程(多達842個),居然一個沒蒙到!我本來想抓BLOCKED狀態的線程,並且線程描述是“Waiting on monitor”,但從上面的線程描述統計來看,一個也沒見。
這時一定要默默把jstack連續再執行幾遍。我這就連續執行10遍,然後再找每個棧的狀態。終於,Waiting on monitor來了:
看看究竟誰阻塞住了上面的線程。先在相應的棧裏,找到對應的持有鎖的棧。下面是棧中的阻塞關係。
第一個棧:
第二個棧:
這是兩個棧文件。所以,要分別從這兩個棧文件裏找到各自的對應等待關係。下面這段代碼就對應上面的Waiting線程:
-- 第一處
"http-nio-8401-exec-890" #86930 daemon prio=5 os_prio=0 tid=0x00007f28680a5800 nid=0x561d waiting for monitor entry [0x00007f2800263000]
java.lang.Thread.State: BLOCKED (on object monitor
at java.security.Provider.getService(Provider.java:1035)
- locked <0x000000071ab1a5d8> (a sun.security.provider.Sun)
at sun.security.jca.ProviderList.getService(ProviderList.java:332)
.....................
at com.javaedge.mall.auth.util.MD5Util.toMD5(MD5Util.java:11)
at com.javaedge.mall.auth.config.MyPasswordEncoder.matches(MyPasswordEncoder.java:23)
.....................
at com.javaedge.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46)
at com.javaedge.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
at com.javaedge.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
at com.javaedge.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
-- 第二處
"http-nio-8401-exec-871" #86739 daemon prio=5 os_prio=0 tid=0x00007f28681d6800 nid=0x5545 waiting for monitor entry [0x00007f2801a7b000]
java.lang.Thread.State: BLOCKED (on object monitor
at sun.security.rsa.RSACore$BlindingParameters.getBlindingRandomPair(RSACore.java:404)
- locked <0x000000071ddad410> (a sun.security.rsa.RSACore$BlindingParameters)
at sun.security.rsa.RSACore.getBlindingRandomPair(RSACore.java:443)
.....................
at com.javaedge.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2(AuthController.java:46)
at com.javaedge.mall.auth.controller.AuthController.postAccessToken$original$sWMe48t2$accessor$jl0WbQJB(AuthController.java)
at com.javaedge.mall.auth.controller.AuthController$auxiliary$z8kF9l34.call(Unknown Source)
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
at com.javaedge.mall.auth.controller.AuthController.postAccessToken(AuthController.java)
看上面locked這行的鎖ID,既然找到這兩處持有鎖的棧,那就通過上面的棧,到源碼中找到這兩處棧的代碼:
-- 第一處同步代碼塊
public synchronized Service getService(String type, String algorithm) {
checkInitialized()
// avoid allocating a new key object if possible
ServiceKey key = previousKey
if (key.matches(type, algorithm) == false) {
key = new ServiceKey(type, algorithm, false);
previousKey = key
if (serviceMap != null) {
Service service = serviceMap.get(key)
if (service != null) {
return service;
ensureLegacyParsed()
return (legacyMap != null) ? legacyMap.get(key) : null;
-- 第二處同步代碼塊
// return null if need to reset the parameters
BlindingRandomPair getBlindingRandomPair(
BigInteger e, BigInteger d, BigInteger n) {
if ((this.e != null && this.e.equals(e)) ||
(this.d != null && this.d.equals(d))) {
BlindingRandomPair brp = null;
synchronized (this) {
if (!u.equals(BigInteger.ZERO) &&
!v.equals(BigInteger.ZERO))
brp = new BlindingRandomPair(u, v);
if (u.compareTo(BigInteger.ONE) <= 0 ||
v.compareTo(BigInteger.ONE) <= 0) {
// need to reset the random pair next time
u = BigInteger.ZERO
v = BigInteger.ZERO
} else {
u = u.modPow(BIG_TWO, n)
v = v.modPow(BIG_TWO, n)
} // Otherwise, need to reset the random pair.
return brp;
return null;
第一處是JDK中提供的getService類採用全局同步鎖定,導致的分配key時產生爭用,這其實在JDK的Bug List中有過描述,詳見JDK-7092821。它不算是Bug,如果你想改的話,可以換一個庫。
第二處是JDK提供的RSA方法,是為防範時序攻擊特意設計成這樣。RSA中有大素數的計算,為線程安全,RSA又加鎖。關於RSA的邏輯,可看源代碼的/sun/security/rsa/RSACore.java。
不過,RSA是低效加密方法,當壓力發起時,這樣的synchronized類必導致BLOCKED。對此,在源碼中有下面這樣一段註釋,其中建議先計算u/v,可以提高加密效率。
* Computing inverses mod n and random number generation is slow, s
* it is often not practical to generate a new random (u, v) pair for
* each new exponentiation. The calculation of parameters might even be
* subject to timing attacks. However, (u, v) pairs should not be
* reused since they themselves might be compromised by timing attacks,
* leaving the private exponent vulnerable. An efficient solution to
* this problem is update u and v before each modular exponentiation
* step by computing:
*
* u = u ^ 2
* v = v ^ 2
* The total performance cost is small
知道這兩BLOCKED原因,對症下藥:
- 第一處鎖:實現自己的方法,如實現一個自己的分佈式鎖
- 第二處鎖:換個高效實現
至此,就找到了應用中BLOCKED的邏輯。
作為性能分析人員,要給出合情合理並且有證據鏈的分析過程,這樣我們和其他團隊成員溝通的時候,才會更加簡單、高效。
8 Member服務定向分析
因為全局監控數據前面我們已經展示了,不再重複,直接拆分對Member服務調用時的響應時間。
Gateway上的響應時間:
- Member上的響應時間
- Auth上的響應時間
這幾段都有不同時間消耗:Member服務上有80毫秒左右,Auth服務上已經有60毫秒左右,明顯有點高。
登錄到Member服務,先看整體的資源使用情況,top:
%Cpu0 : 63.8 us, 12.4 sy, 0.0 ni, 9.2 id, 0.0 wa, 0.0 hi, 14.2 si, 0.4 st
%Cpu1 : 60.3 us, 11.7 sy, 0.0 ni, 11.0 id, 0.0 wa, 0.0 hi, 16.6 si, 0.3 st
%Cpu2 : 59.4 us, 12.0 sy, 0.0 ni, 14.1 id, 0.0 wa, 0.0 hi, 13.8 si, 0.7 st
%Cpu3 : 59.8 us, 12.1 sy, 0.0 ni, 11.7 id, 0.0 wa, 0.0 hi, 15.7 si, 0.7 st
從CPU使用分佈上來看,其他計數器都還正常,只是si有點高。這是一個網絡中斷的問題,雖然有優化的空間,但是受基礎架構所限,性能提升得不太多,這也是為什麼現在很多企業都放棄了虛擬化,直接選擇容器化的一個原因。
針對這個網絡中斷的問題,我將在後面的課程中仔細給你扒一扒,這節課我們暫且不做過多的講解。
9 總結
用登錄功能演示了完整的性能分析場景。
前面代碼修改的部分,性能分析過程是比較快的,我們就是看看哪裏的代碼邏輯會消耗更多的時間。這思路就是前面提到的us cpu的證據鏈。
而接下來我們在分析Auth服務的時候,是先從拆分時間開始一步步走到代碼裏的,其中最核心的部分是從CPU到棧,再到BLOCKED的判斷。當我們看到棧上有BLOCKED的時候,要記得打印棧信息。但是因為有些鎖會非常快速地獲取和釋放,所以就可能會出現打印棧時,看到等某個鎖的棧信息,但是整個棧文件中卻沒有這把鎖的情況。這個時候,你就要注意了,連續多打幾次棧,直到抓到對應的鎖。
這是分析棧中鎖的一個關鍵,因為經常看到等鎖的棧信息,看不到持有鎖的棧信息。連續多打幾次棧,就是為了把持有鎖和等待鎖的棧同時打出來,否則就找不出分析的邏輯了。
接着,看到持有鎖的棧後,就根據自己業務代碼的調用邏輯,一層層找是哪裏加的鎖。至於這個鎖加的合理不合理,就和業務邏輯有關了。就可以把開發、業務、架構等人拉到一起討論。這個鎖要不要改,不是做性能的人説了算,而是大家一起説了算。
性能分析邏輯,從現象到原理,都要搞清楚。
堆棧分析工具:IBM Thread and Monitor Dump Analyzer for Java。