記我的一次 Java 服務性能優化

来源:https://www.cnblogs.com/Javajishuzhai/archive/2019/08/09/11329734.html
-Advertisement-
Play Games

前段時間我們的服務遇到了性能瓶頸,由於前期需求太急沒有註意這方面的優化,到了要還技術債的時候就非常痛苦了。 在很低的 QPS 壓力下伺服器 load 就能達到 10-20,CPU 使用率 60% 以上,而且在每次流量峰值時介面都會大量報錯,雖然使用了服務熔斷框架 Hystrix,但熔斷後服務卻... ...


背景

前段時間我們的服務遇到了性能瓶頸,由於前期需求太急沒有註意這方面的優化,到了要還技術債的時候就非常痛苦了。

在很低的 QPS 壓力下伺服器 load 就能達到 10-20,CPU 使用率 60% 以上,而且在每次流量峰值時介面都會大量報錯,雖然使用了服務熔斷框架 Hystrix,但熔斷後服務卻遲遲不能恢復。每次變更上線更是提心吊膽,擔心會成為壓死駱駝的最後一根稻草,導致服務雪崩。

在需求終於緩下來後,leader 給我們定下目標,限我們在兩周內把服務性能問題徹底解決。近兩周的排查和梳理中,發現並解決了多個性能瓶頸,修改了系統熔斷方案,最終實現了服務能處理的 QPS 翻倍,能實現在極高 QPS(3-4倍)壓力下服務正常熔斷,且能在壓力降低後迅速恢復正常,以下是部分問題的排查和解決過程。

伺服器高CPU、高負載

首先要解決的問題就是服務導致伺服器整體負載高、CPU 高的問題。

我們的服務整體可以歸納為從某個存儲或遠程調用獲取到一批數據,然後就對這批數據進行各種花式變換,最後返回。由於數據變換的流程長、操作多,系統 CPU 高一些會正常,但平常情況下就 CPU us 50% 以上,還是有些誇張了。

我們都知道,可以使用 top 命令在伺服器上查詢系統內各個進程的 CPU 和記憶體占用情況。可是 JVM 是 Java 應用的領地,想查看 JVM 里各個線程的資源占用情況該用什麼工具呢?

jmc 是可以的,但使用它比較麻煩,要進行一系列設置。我們還有另一種選擇,就是使用 jtop,jtop 只是一個 jar 包,它的項目地址在 yujikiriki/jtop , 我們可以很方便地把它複製到伺服器上,獲取到 java 應用的 pid 後,使用 java -jar jtop.jar [options] <pid> 即可輸出 JVM 內部統計信息。

jtop 會使用預設參數 -stack n 列印出最耗 CPU 的 5 種線程棧。

形如:

Heap Memory: INIT=134217728  USED=230791968  COMMITED=450363392  MAX=1908932608
NonHeap Memory: INIT=2555904  USED=24834632  COMMITED=26411008  MAX=-1
GC PS Scavenge  VALID  [PS Eden Space, PS Survivor Space]  GC=161  GCT=440
GC PS MarkSweep  VALID  [PS Eden Space, PS Survivor Space, PS Old Gen]  GC=2  GCT=532
ClassLoading LOADED=3118  TOTAL_LOADED=3118  UNLOADED=0
Total threads: 608  CPU=2454 (106.88%)  USER=2142 (93.30%)
NEW=0  RUNNABLE=6  BLOCKED=0  WAITING=2  TIMED_WAITING=600  TERMINATED=0

main  TID=1  STATE=RUNNABLE  CPU_TIME=2039 (88.79%)  USER_TIME=1970 (85.79%) Allocted: 640318696
    com.google.common.util.concurrent.RateLimiter.tryAcquire(RateLimiter.java:337)
    io.zhenbianshu.TestFuturePool.main(TestFuturePool.java:23)

RMI TCP Connection(2)-127.0.0.1  TID=2555  STATE=RUNNABLE  CPU_TIME=89 (3.89%)  USER_TIME=85 (3.70%) Allocted: 7943616
    sun.management.ThreadImpl.dumpThreads0(Native Method)
    sun.management.ThreadImpl.dumpAllThreads(ThreadImpl.java:454)
    me.hatter.tools.jtop.rmi.RmiServer.listThreadInfos(RmiServer.java:59)
    me.hatter.tools.jtop.management.JTopImpl.listThreadInfos(JTopImpl.java:48)
    sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

    ... ...

通過觀察線程棧,我們可以找到要優化的代碼點。

在我們的代碼里,發現了很多 json 序列化和反序列化和 Bean 複製耗 CPU 的點,之後通過代碼優化,通過提升 Bean 的復用率,使用 PB 替代 json 等方式,大大降低了 CPU 壓力。

熔斷框架優化

服務熔斷框架上,我們選用了 Hystrix,雖然它已經宣佈不再維護,更推薦使用 resilience4j和阿裡開源的 sentinel,但由於部門內技術棧是 Hystrix,而且它也沒有明顯的短板,就接著用下去了。

先介紹一下基本情況,我們在控制器介面最外層和內層 RPC 調用處添加了 Hystrix 註解,隔離方式都是線程池模式,介面處超時時間設置為 1000ms,最大線程數是 2000,內部 RPC 調用的超時時間設置為 200ms,最大線程數是 500。

響應時間不正常

要解決的第一個問題是介面的響應時間不正常。在觀察介面的 access 日誌時,可以發現介面有耗時為 1200ms 的請求,有些甚至達到了 2000ms 以上。服務正常時,這種情況對於線程池隔離方式是不可能發生的,因為線程池模式下,Hystrix 會創建一個新的線程去執行真正的業務邏輯,而主線程則一直在等待,一旦等待超時,主線程是可以立刻返回的。所以介面耗時超過超時時間,問題很可能發生在 Hystrix 框架層、Spring 框架層或系統層。

這時候可以對運行時線程棧來分析,我使用 jstack 列印出線程棧,並將多次列印的結果製作成火焰圖(參見應用調試工具-火焰圖)來觀察。

如上圖,可以看到很多線程都停在 LockSupport.park(LockSupport.java:175) 處,這些線程都被鎖住了,向下看來源發現是 HystrixTimer.addTimerListener(HystrixTimer.java:106) , 而再向下就是我們的業務代碼了。

Hystrix 註釋里解釋這些 TimerListener 是 HystrixCommand 用來處理非同步線程超時的,這些 TimerListener 會在調用超時時執行,將超時結果返回。而在調用量大時,進入線程池時這些 TimerListener 的設置就會因為鎖而阻塞,而這些 TimerListener 的設置被阻塞後,就會導致介面設置的超時時間不生效。

要解決這個問題,只能修改服務的隔離策略了,將 Hystrix 的隔離策略改為信號量模式。信號量模式下,Hystrix 會在每次執行 HystrixCommand 時獲取一次信號量,在執行結束後還回。由於信號量的操作效率非常高,而且沒有其他附加操作,所以在使用信號量隔離模式時不會有其他性能損耗。

但使用信號量隔離模式也要註意一個問題:信號量只能限制方法是否能夠進入,如果可以進入執行,則在原來的主線程內執行,執行的過程中 Hystrix 是無法干預的,只能在方法返回後再判斷介面是否超時並對超時進行處理,這可能會導致有部分請求耗時超長時,一直占用一個信號量,但框架卻無法處理。

在修改了 Hystrix 的隔離模式後,介面的最大耗時就穩定了,而且由於方法都在主線程執行,少了 Hystrix 線程池維護和主線程與 Hystrix 線程的上下文切換,系統 CPU 使用率又有進一步下降。

服務隔離和降級

另一個問題是服務不能按照預期的方式進行服務隔離和降級,我們認為流量在非常大的情況下應該會持續熔斷時,而 Hystrix 總表現為半熔斷半執行,我們認為多餘的請求不會進入方法內部時,它們偏偏還能被執行。

開始時,我們對日誌進行觀察,由於日誌被設置成非同步,看不到實時日誌,而且有大量的報錯信息干擾,過程痛苦而低效。後來得知 Hystrix 還有可視化界面後,才算找到正確的調優方式。

Hystrix 可視化模式分為服務端和客戶端,服務端就是我們要觀察的服務,需要在服務內引入 hystrix-metrics-event-stream 包並添加一個介面來輸出 Metrics 信息。要將這些信息展示出來,只需要啟動 hystrix-dashboard 客戶端並填入服務端地址即可。

通過可視化界面,Hystrix 的整體狀態就展示得非常清楚了,我們就可以根據這些狀態信息對它的熔斷配置進行調整了。由於上文的優化,介面的最大響應時間完全可控,可以通過嚴格限制介面方法的併發量來修改服務的拒絕策略了。

 

假設介面平均響應時間為 50ms,而服務能容納的最大 QPS 為 2000,那麼可以通過 2000*50/1000=100 得到適合的信號量限制,如果被拒絕的錯誤數過多,可以再添加一些冗餘。

這樣,在流量突變時,就可以通過拒絕一部分連接來控制進入服務的總請求數,而在進入服務的總請求里,又嚴格限制了平均耗時,如果錯誤數過多,還可以通過熔斷來進行降級。多種策略同時進行,就能保證介面的平均響應時長了。

熔斷時高負載導致無法恢復

接下來就要解決服務熔斷時,服務負載持續升高,而在 QPS 壓力降低後服務遲遲無法恢復的問題。

在伺服器負載特別高時,使用各種工具來觀測服務內部狀態,結果都是不靠譜的,因為觀測一般都採用打點收集的方式,在觀察服務的同時已經改變了服務。例如使用 jtop 在高負載時查看占用 CPU 最高的線程時,獲取到的結果總是 JVM TI(Java 動態位元組碼技術) 相關的棧。

不過,觀察服務外部可以發現,這個時候會有大量的錯誤日誌輸出,往往在服務已經穩定好久了,還有之前的錯誤日誌在列印,延時的單位甚至以分鐘計。大量的錯誤日誌不僅造成 I/O 壓力,而且線程棧的獲取、日誌存儲記憶體的分配都很有可能會增加伺服器壓力。而且我們的服務早因為日誌量大而改為了非同步日誌,這使得通過 I/O 阻塞線程的屏障也消失了。

要驗證這項猜測也很簡單,修改服務內的日誌記錄點,在列印日誌時不再列印異常棧,再重寫 Spring 框架的 ExceptionHandler,徹底減少日誌量的輸出。

結果非常符合預期,在錯誤量極大時,日誌輸出也被控制在正常範圍,這樣熔斷後,就不會再因為日誌給服務增加壓力,一旦 QPS 壓力下降,熔斷開關被關閉,服務很快就能恢復正常狀態。

Spring 數據綁定異常

另外,在查看 jstack 輸出的線程棧時,還偶然發現了一種奇怪的棧。

at java.lang.Throwable.fillInStackTrace(Native Method)
    at java.lang.Throwable.fillInStackTrace(Throwable.java:783)
    - locked <0x00000006a697a0b8> (a org.springframework.beans.NotWritablePropertyException)
    at java.lang.Throwable.<init>(Throwable.java:287)
    at java.lang.Exception.<init>(Exception.java:84)
    at java.lang.RuntimeException.<init>(RuntimeException.java:80)
    at org.springframework.core.NestedRuntimeException.<init>(NestedRuntimeException.java:66)
    at org.springframework.beans.BeansException.<init>(BeansException.java:50)
    at org.springframework.beans.FatalBeanException.<init>(FatalBeanException.java:45)
    at org.springframework.beans.InvalidPropertyException.<init>(InvalidPropertyException.java:54)
    at org.springframework.beans.InvalidPropertyException.<init>(InvalidPropertyException.java:43)
    at org.springframework.beans.NotWritablePropertyException.<init>(NotWritablePropertyException.java:77)
    at org.springframework.beans.BeanWrapperImpl.createNotWritablePropertyException(BeanWrapperImpl.java:243)
    at org.springframework.beans.AbstractNestablePropertyAccessor.processLocalProperty(AbstractNestablePropertyAccessor.java:426)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:278)
    at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:266)
    at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:97)
    at org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:839)
    at org.springframework.validation.DataBinder.doBind(DataBinder.java:735)
    at org.springframework.web.bind.WebDataBinder.doBind(WebDataBinder.java:197)
    at org.springframework.web.bind.ServletRequestDataBinder.bind(ServletRequestDataBinder.java:107)
    at org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.bindRequestParameters(ServletModelAttributeMethodProcessor.java:157)
    at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:153)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:124)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:131)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)

jstack 的一次輸出中,可以看到多個線程的棧頂都停留在 Spring 的異常處理,但這時候也沒有日誌輸出,業務也沒有異常,跟進代碼看了一下,Spring 竟然偷偷捕獲了異常且不做任務處理。

List<PropertyAccessException> propertyAccessExceptions = null;
    List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ?
            ((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
    for (PropertyValue pv : propertyValues) {
        try {
            // This method may throw any BeansException, which won't be caught
            // here, if there is a critical failure such as no matching field.
            // We can attempt to deal only with less serious exceptions.
            setPropertyValue(pv);
        }
        catch (NotWritablePropertyException ex) {
            if (!ignoreUnknown) {
                throw ex;
            }
            // Otherwise, just ignore it and continue...
        }
        ... ...
    }

結合代碼上下文再看,原來 Spring 在處理我們的控制器數據綁定,要處理的數據是我們的一個上下文類 ApiContext,它是由多個欄位組成的參數傳輸 Bean。

控制器代碼類似於:

@RequestMapping("test.json")
 public Map testApi(@RequestParam(name = "id") String id, ApiContext apiContext) {}

按照正常的套路,我們應該為這個 ApiContext 類添加一個參數解析器(HandlerMethodArgumentResolver),這樣 Spring 會在解析這個參數時會調用這個參數解析器為方法生成一個對應類型的參數。可是如果沒有這麼一個參數解析器,Spring 會怎麼處理呢?

答案就是會使用上面的那段”奇怪”代碼,先創建一個空的 ApiContext 類,並將所有的傳入參數依次嘗試 set 進這個類,如果 set 失敗了,就 catch 住異常繼續執行,而 set 成功後,就完成了 ApiContext 類內一個屬性的參數綁定。

而不幸的是,我們的介面上層會為我們統一傳過來三四十個參數,所以每次都會進行大量的”嘗試綁定”,造成的異常和異常處理就會導致大量的性能損失,在使用參數解析器解決這個問題後,介面性能竟然有近十分之一的提升。

小結

性能優化不是一朝一夕的事,把技術債都堆到最後一塊解決絕不是什麼好的選擇。平時多註意一些代碼寫法,在使用黑科技時註意一下其實現有沒有什麼隱藏的坑才是正解,還可以進行定期的性能測試,及時發現並解決代碼里近期引入的不安定因素。

關於本文有什麼疑問可以在下麵留言交流,如果您覺得本文對您有幫助,歡迎關註我的公眾號【Java技術zhai】,有新文章發佈會第一時間通知您。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • == 和 equals的區別 ==是一個運算符 用於比較兩端的內容是否相等 基本數據類型 : 兩端的值是否相等 引用類型 : 比較的是引用的值(記憶體指向的地址)是否相等 equals() : 它是Object類的一個方法 子類繼承到這個方法之後 可以按照自己所需要的邏輯需求 覆蓋這個方法 從而描述自 ...
  • 一般部署項目到伺服器,會安裝uwsgi,但是很多教程在安裝它的時候會讓你測試一下安裝好了沒,於是就有很多像我一樣懵逼的少年掉進一個坑裡出不來,很久、很久... 那就是最後瀏覽器輸入ip:8000埠後伺服器有反應,但是瀏覽器一片空白 原因:因為測試用的代碼是對python2.x 環境測試的,他喵的現 ...
  • 1.到官網下載centOS對應版本的xampp,應該是以tar.gz為尾碼的 2.tar -zxf 下載的包 3.mv lampp /opt 4.service mysqld stop因xampp里自己帶一個mysql,會和之前裝的mysql埠衝突,故停掉單獨裝的mysql 5.關閉防火牆 ser ...
  • 前提: 當在程式測試時,如果你需要定義一個自己的異常,而非現在已經存在的異常,這個時候你需要用到throws和throw,try catch只是一個簡單的捕獲異常的過程。 代碼如下: 其實throw的使用很簡單 首先你必須要定義一個異常類,因為你要自己拋出一個以前沒有見過的異常 其次你要寫你定義的這 ...
  • 首先要搞懂本地操作系統編碼與系統編碼的區別: 1. 本地操作系統編碼方式與操作系統有關,Linux預設編碼方式為utf 8,Windows預設編碼方式為gbk; 2. 系統編碼方式與編譯器or解釋器有關,Python3解釋器預設編碼方式為unicode。 3. 編碼方式不僅僅代表編碼,也包括解碼,因 ...
  • MySQL Binlog簡介 什麼是binlog? 一個二進位日誌,用來記錄對數據發生或潛在發生更改的SQL語句,並以而進行的形式保存在磁碟中。 binlog 的作用? 最主要有3個用途: 數據複製(主從同步) Mysql 的Master Slave協議,讓Slave可以通過監聽binlog實現數據 ...
  • 對於某些運算符號(+,-,*,/....),我們不滿足與它原有的操作方式,而是要在對特有對象(如負數的加減)進行使用,但是編譯器會不允許這樣做,因為會與操作符原本的類型不匹配而導致操作失敗。因此我們需要對運算符進行重載,即賦予它新的意義,從而滿足我們的使用需求。 如complex_a和complex ...
  • 第十一章前端開發 11.1 HTTP 1.1引入了許多關鍵性能優化:keepalive連接,請求流水線,chunked編碼傳輸,位元組範圍請求等 1、keepalive連接: 2、Pipelining(請求流水線) 3、chunked編碼傳輸 4、位元組範圍請求 get 和 post 的區別: 1、參數 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...