0 前言

前文還沒優化完,接着來折騰。

登錄要對個人信息對比驗證,驗證過程又要調用相應加密算法,而加密算法對性能要求很高。複雜加密算法安全性高,但性能差;不復雜加密算法性能好,但安全性低,要取捨。還有Session存儲和同步。大型系統,不管你在哪個系統訪問,調用其他系統時如需驗證身份就要同步Session信息,且做業務時,也要把相應Session信息帶上,不然就識別不了。

分析用户登錄功能,並瞭解在壓力過程中業務邏輯鏈路和整體TPS之間的關係。學會判斷線程中的BLOCKED原因。

1 修改加密算法

【系列07文】在基準場景中對登錄業務的測試結果,10個壓力線程下,TPS達100:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_加密算法

07文中發現加密算法BCrypt效率低後,討論優化方式:

  • 用更快的加密方式
  • 去掉這加密算法

本文試第一種,改為MD5:

  • 更改加密算法。BCrypt加密算法雖安全性高,但性能差,建議改成MD5
  • 加載所有用户到Redis

再跑壓力場景。跑這遍之前,只更改了加密算法,沒有執行加載緩存。一次只做一個動作來判斷結果:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_加密算法_02

性能有些上升,但還沒達期望。繼續使用緩存,看效果。

2 檢驗緩存效果

兩個手段檢驗,緩存對後續性能優化產生的效果:

  • 把參數化數據量降下來,只用少量數據測試(僅嘗試,不是説用少量數據來運行場景是對的)
  • 直接加載全部緩存

得到結果:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_ide_03

曲線看登錄接口達300 TPS。從硬件資源看,再根據經驗,應該還能更高。

分析過程中,再也沒有硬件資源的問題,但這裏通過查看全局監控數據,看到us cpu高,説明確實都是業務邏輯在消耗CPU資源。只有從登錄邏輯入手優化。

3 修改登錄的邏輯

原登錄邏輯:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_java_04

Member服務調auth服務,可Auth服務為何還要到Member取用户名?自己直接查緩存或DB不香?從架構設計角度,為避免共享DB,這樣設計似乎也沒啥。只是在優化過程中,要根據實際環境做判斷。

在我們這環境中,需將DB共用,這樣Auth服務就能直接用DB,而不用再從Member繞一圈。所以,先改成新登錄邏輯,可減少一次調用:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_java_05

修改後,登錄TPS如下:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_ide_06

從結果看,TPS確有增加,已到700以上,nice!

能結束分析了嗎?不,還要知道當前瓶頸點,每個業務都會有瓶頸點,不管優化到啥程度,除非一直把硬件資源耗光。

4 看架構圖

分析性能瓶頸之前,先看架構圖,看用户登錄接口涉及的服務和技術組件:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_ide_07

登錄操作跨Gateway/Member/Auth三服務,連接Redis/MySQL兩組件。圖中的MongoDB雖然看上去有線,但實際上登錄沒用上。

按分析邏輯,一步步分析。

5 拆分時間

修改登錄邏輯後的TPS:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_java_08

響應時間已上升到100ms,現在要找出這時間消耗在哪。圖中的用户增加到150。這是為了把響應時間拉大,便於分析。

下面把這個響應時間拆分,看問題出在哪。

Gateway服務上的時間:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_加密算法_09

Member服務上的時間:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_加密算法_10

Auth服務上的時間:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_加密算法_11

Member服務上的時間消耗是150ms左右,Auth服務上的時間消耗有60ms左右。Member服務要着重分析,因為它響應時間更長。而Auth雖時間不太長,但也達60ms,經驗來説,還是稍長,最好平均能到50ms下,所以也要稍微關心。

6 全局監控

從這張全局監控圖數據看,worker-7/8 CPU使用率較高:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_ide_12

就查這兩節點跑的啥服務呀。看POD分佈,看每個POD在哪個worker節點,以便後面分析POD相互之間影響:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_java_13

在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使用率偏高,就能走如下證據鏈:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_java_14

按這證據鏈,應先看進程。不過,藝高人膽大,我直接看線程狀態,看能不能憑經驗蒙對把。於是,打開Spring Boot Admin的線程頁面:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_加密算法_15

滿目瘡痍,點開一些紅色地方後,看到:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_加密算法_16

阻塞數非常大。此外,鎖擁有者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打印出的棧,在下載後用工具打開的效果:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_java_17

看起來有那麼多的BLOCKED狀態的線程(多達842個),居然一個沒蒙到!我本來想抓BLOCKED狀態的線程,並且線程描述是“Waiting on monitor”,但從上面的線程描述統計來看,一個也沒見。

這時一定要默默把jstack連續再執行幾遍。我這就連續執行10遍,然後再找每個棧的狀態。終於,Waiting on monitor來了:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_加密算法_18

看看究竟誰阻塞住了上面的線程。先在相應的棧裏,找到對應的持有鎖的棧。下面是棧中的阻塞關係。

第一個棧:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_java_19

第二個棧:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_ide_20

這是兩個棧文件。所以,要分別從這兩個棧文件裏找到各自的對應等待關係。下面這段代碼就對應上面的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上的響應時間:

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_java_21

  • Member上的響應時間

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_加密算法_22

  • Auth上的響應時間

從 100 TPS 到 700+!微服務登錄性能調優與 BLOCKED 鎖定位實戰_ide_23

這幾段都有不同時間消耗: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。