CAT 性能优化的实践和思考
作者簡介
錦華,攜程高級技術專家,超過 10 年互聯網研發經驗,2011 年至今一直從事框架和中間件相關產品研發,對高并發、分布式中間件以及應用性能優化等有濃厚興趣。
*本文來自錦華在Qcon的分享,首發于Qcon公眾號*
作為業界知名的應用監控產品,CAT 已經成功地為多家公司提供了完整的監控領域解決方案。2015 年 CAT 在攜程落地,目前已經成為公司內部非常重要的監控基礎設施,很好地支撐了來自 70000+ 客戶端的 8000 億條消息 / 天、900TB/ 天的實時監控流量。本文將分享攜程在 CAT 性能優化上的實踐,并通過這些實踐總結出一些普適性的性能優化思路與方法。
一、CAT 在攜程的落地及發展情況
CAT 是大眾點評開源的一個基于 Trace 的應用監控系統。2014 年底,攜程開始引入 CAT 并落地。在這幾年里,公司的數據量一直呈現著爆發式的增長,我們也不斷地對 CAT 做了很多大大小小的優化,使得機器數目并沒有像數據量那樣呈現出指數級的增加。到目前為止,有超過 7 萬的客戶端,每天處理的消息樹超過8000 億,每天處理的日志量超過 4 萬億行,峰值流量達到 1.5 億行 / 秒。
二、CAT 性能優化案例
在介紹案例之前,我們簡單介紹一下 CAT 的計算模型。
CAT 客戶端的監控數據會組裝成一種樹狀結構,也就是 CAT 里面的 MessageTree,然后發送到 CAT 服務端。CAT 服務端會把這份數據同時分發給多個不同用途的報表分析器進行實時計算,計算出來的結果會被存到服務端的內存報表里面。
我們的報表是什么樣子的?這里是一個 CAT 最基本的 Transaction 報表截圖,可以把它簡單理解成一段時間內的一些監控指標的聚合。這個例子里面展示的是 RPCService 這個 Transaction 在這一小時內每一分鐘發生的次數、平均耗時、每分鐘的失敗以及這個小時的平均耗時,99 線、95 線。
下面看一下,CAT 服務端經常遇到什么樣的問題。首先,從上面的計算模型可以看到,報表分析其實是一個 CPU 密集型的任務,所以 CPU 跑滿是一個經常遇到的問題。另外,還是根據上面的計算模型,所有實時的報表數據都會放在內存里面,所以 GC 頻繁也是經常遇到的一個問題。
2.1 案例一:線程模型優化
1)CAT 線程模型
CAT 客戶端的監控數據發送到服務端后,服務端會同時將這份數據分發給多個不同用途的報表分析器,報表分析器內部會根據這個監控數據的客戶端信息(APP ID 和 IP)計算一個哈希值,然后再分發到報表分析器內部的一個隊列里面。這個隊列后面會綁定一個線程進行實時分析,計算出來的結果會放到這個線程綁定的內存報表中。這種模型有一個好處,就是它把數據跟線程綁定在一起,使得對同一個客戶端過來的監控數據的實時分析、計算和內存報表的更新都是無鎖的。
2)遇到的問題
隨著流量不斷增加,我們發現,其實不同的應用和客戶端發送過來的數據是非常不均的。數據的不均就會導致隊列堆積,最終的結果就是某些堆積的隊列對應的客戶端監控數據丟失。對于數據不均的問題,天然的我們就會想到通過增加更多的隊列,讓哈希算法更加均勻就可以解決。
但是,在這種模型里面,增加隊列同時意味著也要增加對應的處理線程。一開始我們一直是用這個方法去解決數據不均的問題,直到有一次發現當前的隊列以及處理線程已經太多了,再加下去甚至還會出現處理能力的下降。
3)分析問題
于是就開始去分析,為什么處理能力反而下降了。查看監控指標之后,發現操作系統的上下文切換已經達到了每秒鐘幾百萬次之多。這時候趕緊去看一下服務端的線程,發現經過前面的一頓操作,最后所有的報表加在一起已經有了幾千個線程,非常的多。回過頭來審視我們的模型,當數據不均的時候,核心需求是要通過增加隊列來將數據進一步的打散,為什么處理線程也要隨之增加呢?
所以,第一個想法就是把這模型里面的隊列和線程做個解耦。
在談及隊列和線程解耦的模型時,第一想法就是 IO 模型。可以看看現在的線程模型,它跟 IO 里面最老的 BIO 模型是不是非常相似?它們都是每個隊列或者 channel 后面有一個線程在阻塞等待數據的到來,然后去計算,所以會有非常多的線程。每個 channel 即使沒有數據到達也需要有個線程在阻塞等待,非常浪費。
BIO 模型往下的一個演進就是 NIO 模型。NIO 通過引入一個 Selector 模塊, 很輕量地就可以監聽非常多的隊列數據。于是我們就想,是不是也類似這樣,通過引入一個 Selector 去監聽多個隊列來達到隊列跟線程解耦的目的呢?
4)新線程模型
來思考下,我們所要引入的這個 Selector 在 CAT 的服務端需要實現什么樣的功能?首先,毫無疑問的是需要一個監聽隊列的功能。另外,還需要在上面實現一套調度策略,一方面可以讓它充分利用后面的線程池,另一方面要保持我們的模型跟原來的模型一樣,在同一個客戶端,同一隊列的更新是不需要加鎖的。
這里有個小小的細節,實現 Selector 的時候并不是說直接開一個線程不斷地空輪詢所有的隊列,這樣會非常低效,你永遠不知道哪個隊列什么時候會有數據,只能一直空輪詢。我們的實現方式是在數據進入隊列的時候,反過來去 notify ?Selector,讓 Selector 決定在這個時候是觸發調度還是應該先 hold 一下,后面再調度。
把報表分析器的模型改造好后,把它放到整個 CAT 服務端去看。一個服務端會有很多報表分析器,會看到改造后在 CAT 服務端的每個報表分析器都有自己的 Selector 與線程池。但真的需要這么多線程池嗎?從這個模型就可以知道,其實我們的報表分析任務全都是計算密集型的,按理說整個系統里面應該只需要開 CPU 核數個的線程就可以充分利用所有的 CPU 資源,于是自然而然地就把我們的所有計算線程合并到同一個線程池中。
再看 Selector 模塊。既然設計初衷就是讓一個 Selector 可以非常輕量就可以監聽非常多的隊列,那么其實用一個跟用多個都是能達到同樣的效果。另外,如果只用一個 Selector 的話,可以比較容易地實現一套優先級調度的策略。
為什么需要一套優先級調度的策略呢?因為監控數據其實也是有優先級的,我們會希望在系統負載比較高的時候,一些高優先級的報表和監控數據可以得到更多的資源,優先去計算。于是把 Selector 合并在一起,在它上面實現一套優先級的調度。這就是我們當前所使用的 CAT 服務端的線程模型。
5)小結
稍微總結一下,我們通過線程模型優化,從原來的像 BIO 一樣的線程模型改造成了現在這種像 NIO 一樣的線程模型。
首先它滿足了初衷,讓隊列和線程做一個解耦。那么解耦之后,就可以設置非常多的隊列,一方面很好解決數據均勻的問題,另外一方面,因為增加了隊列,同時也減少了單個隊列的鎖競爭,那么只要開 CPU 核數個線程就可以了,不會像原來那樣每個報表分析器都要開自己的線程,線程數減少了,相應地也能減少很多沒有用的上下文切換。
另外,我們在這個新模型里還提供了一套比原來更加靈活的調度策略,可以實現優先級調度。這個模型上線后,我們從原來每臺機器跑到超過 90% 的 CPU,最后還出現 5% 的丟失,優化到數據不丟 CPU 還下降到 70% 左右。
2.2 案例二:客戶端計算
1)遇到的問題
經過之前的優化,CPU 已經利用得比較充分了。但隨著流量進一步增加,我們最終還是把 CPU 全部用滿,數據又開始丟了。
2)分析問題
現在的資源是不是真的不夠了,是不是要進行機器的擴容?出于成本的考量,我們決定先進行優化,看能不能再節省一些 CPU 出來,從而能夠在成本不變的情況下扛更大的流量。
經過分析,借鑒了當下的一些思路,決定將服務端的部分計算下放到客戶端去。
那么到底什么樣的計算比較適合放到客戶端?首先,一定要是一個不變的,或者是變的很少的邏輯。否則每次更新計算邏輯都會需要更新客戶端,這個過程會變得非常的漫長。其次,肯定要是一些在服務端占用 CPU 資源比較多的計算,否則花了很多力氣去改造,最后卻只能得到一個不是很劃算的效果。
3)Transaction/Event Report 的 CPU 使用
說到不變的邏輯,自然就會想到 CAT 中的兩份基本報表:Transaction 和 Event Report。這兩份報表的分析計算非常簡單,就是針對 Transaction 和 Event 這兩個基本模型進行數據的聚合統計。這兩個報表到底占用了多少服務端的資源呢?通過調整上述 Selector 模型提及的調度策略,我們將這兩份報表的計算放到獨立的線程池中進行,得到了如下的資源使用率:
從以上數據可以看到,服務端中, Transaction 報表的計算用了 7 個線程,分別都會占到 0.8、0.9 個核,所以這 7 個線程總共在服務端占了 5.3 個核。Event 報表有 4 個線程,每個線程也跑到百分之四五十、六十左右,加在一起也有 2.2 個核,這兩張報表的分析在服務端就使用了一臺機器中的 7.5 個核。如果機器是 32 核的話,相當于它占了 23% 的計算資源。如果能把這個計算挪到客戶端,那會省下來很多的資源。
4)Transaction/Event Report 計算
來看這兩份數據是怎么計算的。
首先,看一下服務端的計算方式。客戶端會把一個個監控數據組織成 MessageTree ,以一種樹狀的結構發送到服務端。服務端會遍歷所有發送來的 MessageTree,找到樹里面嵌套的 Transaction 和 Event 結點,然后做一個數值的統計。這個計算量是非常大的,它不僅會跟客戶端發送過來的 MessageTree 的個數有關系,還會跟每個 MessageTree 里面到底有多少個 Event 和 Transaction 也有關系。
那怎么把它挪到客戶端去呢?我們可以把客戶端里多個 MessageTree 內的統計數據合并成一份,一次性把這個統計數據發送過來。這樣的話服務端的計算量將會極大的減少。
這種計算方式下,計算量只會和客戶端數量以及客戶端發送這份統計數據的間隔時間有關系。看看最后改造出來的架構,客戶端增加了兩個用于進行統計的 Aggregator 分別來計算 Transaction 和 Event 的指標,然后再將這些數據定時發送到服務端去。
此外,我們做了一個小小的改動,把原來 CAT 的一個發送隊列改造成了兩個發送隊列,一個隊列用來發送原來的 MessageTree,另外一個則是用來發送客戶端預聚合過的統計數據。統計量隊列的發送優先級會稍微高一些,這樣的話就可以保證客戶端即使在整體負載偏高,數據來不及發送的情況下,也能優先把統計量發送到后端去,從而保證了監控系統統計量的準確性。
在這個架構上線后,可以看到 Transaction 報表從原來的 7 個線程占了五點幾個核,下降到現在 7 個線程,每個線程只要 0.02 個核。
Event 報表也從原來的 4 個線程,每個線程占 0.4、0.5 個核,變到現在 4 個線程,每個線程只能吃掉 0.01 個核。效果還是非常明顯的,省了很多資源出來。
這樣的一個計算,在客戶端到底有多大的影響?在客戶端的視角,內存的占用其實非常少。因為我們僅僅只是在客戶端增加了幾秒鐘的統計數據的聚合,這個數據量非常少,而且過幾秒鐘后這份數據就會被送走了,所以不會對客戶端的內存使用造成很大的影響。
根據我們的統計,整體的內存消耗只在 10M 以下,CPU 的占用則基本上可以忽略。服務端的計算需要對一棵棵監控樹進行遍歷然后去分析統計,但在客戶端這個流程其實是反的。客戶端是先有統計數據再組織成樹,所以其實只要在產生監控數據的時候,在它埋點結束之后多加一個統計量,而省去了遍歷一棵樹的消耗,所以它對客戶端 CPU 的影響幾乎為 0。
5)小結
總結一下這個案例。我們之前很多時候會想讓客戶端做的比較薄,盡量讓邏輯落到服務端來獲得一些靈活性,但其實也可以考慮把一些相對簡單且變更較少的計算邏輯挪到客戶端去。
客戶端相比服務端來說,有足夠多的上下文信息,因而客戶端的分析計算可能會比服務端計算簡單得多。從這個例子里可以看到客戶端計算的時候,它只要在你兩個基本模型 Transaction、Event 埋點結束的時候多做兩個統計的累加就好了。但在服務端就很麻煩,服務端需要把那棵樹重新反序列化出來再去遍歷它。
另外我們做了一個類似于批量處理發送的優化,從而讓服務端的計算量變得只會跟客戶端的數量和間隔時間有關系。這樣做有一個非常大的好處就是服務端的計算會比較平滑,不會因為客戶端這邊突然間有個流量峰值過來,突然間發了很多監控數據,從而導致后端產生抖動影響。
2.3 案例三:Report 雙緩沖
1)遇到的問題
隨著流量又進一步繼續增加,我們發現一個比較奇怪的現象——每個小時切換的前幾分鐘都會發生一定的監控數據丟失。
看了一下網卡監控,發現網卡流量是平穩的,并不會出現每小時前幾分鐘會抖動的現象,因此排除了由于入口流量導致異常的推斷。又看了服務端的應用監控指標,發現 Young GC 也是有著類似的趨勢,每小時的前幾分鐘有個抖動,甚至在某一分鐘里面出現了 60 秒鐘里面有 10 秒鐘在做 Young GC。于是就要去看 CAT 服務端里面到底是什么東西在用我們的內存。
2)CAT 服務端內存使用
從計算模型可以看到, CAT 服務端會接收大量的客戶端發送過來的實時監控數據。這部分數據到了服務端,接下來就要被分發去做實時的報表統計分析,所以這部分數據會占用服務端很多內存。另外,根據 CAT 的計算模型也可以看到 ,CAT 服務端會將大量的當前小時的實時報表數據放在內存中,所以當前小時的報表也是內存占用的一大來源。
3)CAT Report 的生命周期
要看報表的問題,需要先看一下 CAT 報表的生命周期是怎么樣的。首先, CAT 會把當前小時的報表存在內存里面,跨小時的時候重新創建一份新的空報表供下個小時用。小時切換后,上個小時的內存報表在被持久化到存儲之后基本就沒有用了,等待被下一次的 GC 釋放。
報表在內存里面的樹結構是怎么樣的?CAT 報表基本上結構比較相似,我這邊舉兩個基本的 Transaction 和 Event 報表。
我們可以把這兩份報表簡單理解成以字符串為 key,Map 作為 value 的一個 Map 套 Map 的結構。比如這個例子里面,它第一層 Map 就是通過 AppId 作為 key, 去找到這個 AppId 對應的所有 IP 的數據構成的一個子 Map。然后通過 IP 列表的 Map 再往下逐層尋找,直到最后找到對應條件在某個時間點的指標數據。
從這個生命周期里面可以看到,這樣的使用方式第一個問題,就是每小時它都會創建一份新的報表,而這份報表在剛創建出來的時候,由于駐留索引的缺失以及新的數據的不斷投遞,它會被不斷地填充,這個報表會不斷地創建對應的下層的 Map。在不斷地填充過程中,你會發現這個 Map 還會不斷地需要去 resize,扔掉一些之前已經使用的內存,非常浪費。
我們看到 CAT 的當前小時的報表是常駐在內存里面的,到下個小時持久化完它就可以釋放掉了。從這個地方我們可以看到報表生命周期的第二個問題,一個報表一開始創建時在 Young 區,隨著監控指標不斷過來,報表會不斷擴大。這個報表會不斷地被 Young GC 掃描到,但是由于這個報表是常駐內存一小時的,所以其實這些 Young GC 都是沒有用的。當 GC 次數達到閾值后, Young GC 還會把它搬到 Old 區。那么這份 Old 區里面的報表到了下個小時完成持久化之后,就會變成一份沒有用的報表,導致 Old 區里面就無緣無故多了一份沒有用的數據。
4)分析問題
我們可以從這個生命周期里面看到好幾個問題。
針對 Map 會不斷地被 Resize 的問題,很自然會想到,一個公司的監控系統里面的監控指標應該是基本穩定的,所以是不是可以在跨小時的時候直接 clone 上個小時的報表,按照上個小時的報表的索引以及 size 去創建。但我們發現其實 clone 并沒有解決第二個問題,在 clone 的過程中還是會一直在創建下層的 map,而且依然還是在 Young 區產生一份報表,然后慢慢搬到 Old 區,最后還是要丟棄掉,內存的結構并沒有發生變動,所以這個 clone 的方法肯定是不行的。
另外一個方法,能不能直接創建兩份報表,一直輪換著使用。這兩份報表因為一開始就創建好了,它很快就會到 Old 區,然后在輪換使用時,這份報表其實就一直在 Old 區根本就沒有任何的浪費,它也不需要被創建。
那么我們把報表的生命周期改造成了現在的樣子:一開始直接創建兩份報表,它們一直就在 Old 區里面輪換著使用。為了保證這個 Map 不會在經過長時間運行之后慢慢充斥著很多無用的監控指標項目,我們加一個定時清理的策略,把一些內存中里面,比如說兩三個小時已經沒有再接收到的監控數據清除掉。
5)效果
可以看到首先 Young GC 已經達到最開始的期望,達到了穩定的狀態,而且因為它根本不用搬動這份大的數據到 Old 區,也不用掃這份大的內存,GC 速度也快了很多。此外,我們的 Full GC 也從每天的 20 次降到了每天的 3 次。
6)小結
對于 GC 問題可能會想到要去調參數讓效果好一點,但實際上根本問題是需要考慮盡量少分配內存,因為不分配內存才是根本訴求。如果一定要把內存創建出來,就得考慮是否可以復用這份內存。
2.4 案例四:字符串
第四個案例,我這邊起名成字符串,是因為我們在 CAT 里面做了一系列字符串相關的優化。先看一下為什么要做字符串優化。
這是從服務端抓出來的一份 Flight Recorder 的數據。可以看到,我們這里一分鐘內創建的 String,Char[] 以及 UTF_8Decoder 對象總共有 60 多 G,相當于每秒鐘有 1 個 G 的對象產生。如果能把這部分的內存節省下來,系統能夠運行得更好。
1)MessageTree 的傳輸
這個問題得先看一下 CAT 里面 MessageTree 的傳輸過程。首先它會從客戶端把 MessageTree 序列化成一個字節流,通過網絡傳輸到服務端,然后服務端會把這個字節流完全的反序列化復原成原來的 MessageTree 再分發給報表分析器去計算。
MessageTree 其實就是由 CAT 兩大模型,Transaction 和 Event 組成的樹狀嵌套結構。
可以看到這 Transaction 和 Event 里面有四個字段都是字符串的: type,name,status,data。字節流反序列化成 MessageTree 的過程中會把這個 Tree 里面的每個 Transaction 和 Event 里的這幾個字段做一次反解的操作。
這個操作到底有多大的損失?翻看一下 JDK 代碼就可以發現,就這么一個簡單的字符串構造,看起來很簡單的一個構造函數,實際上它里面做了兩次 Char 數組的分配,還做了一次字符集的解碼,將字節流變成 UTF_8 編碼的 Char。所以僅僅只是這么一個簡單的構造函數,既消耗內存和也消耗 CPU。
2)byte[] —> String
那么就要看,剛剛分析出來這幾個字符串字段是不是都需要做這樣的一個反序列化操作。首先看 data,data 我們可以把它簡單理解成 Transaction 和 Event 的一個附加信息。大部分時候我們的報表分析其實根本不關心這個附加信息,附加信息一般都是用來給用戶查問題的時候,他點開這個 MessageTree 去看,但實時分析大部分情況不會用到的。所以這個 data 我們是完全可以按需的解開就好了。
然后再來看第二個字段,status。它是用來描述 Transaction 和 Event 成功與否的狀態,如果失敗的話,它會把失敗原因放進去。所以可以想象,如果它失敗的話,status 里面有可能是一個非常大的字符串。
大部分情況下我們的報表只關心狀態到底是成功還是失敗,具體的失敗原因一般也是用戶點開的時候才關心,它分析的時候是不關心的。所以對于這個字段可以簡單地特殊處理一下,在序列化的時候多引入一個字節去描述它是成功還是失敗,具體失敗原因可以放到后面去,也是一個字符串,但是后面那個字符串基本上也是按需解開即可。
3)type/name 需要 byte[] —> String?
我們來看 type/name 這兩個字段。這兩個字段會稍微復雜一些,我們需要單獨拿出來看。首先,我們來看一下為什么會用到 type/name 這樣的東西。一個 MessageTree 通過序列化變成字節流傳輸到服務端之后,服務端要更新的值其實就是這個字節流里面的 Metrics 部分。報表的內存結構類似于 Map 套 Map 的結構,AppId,IP,Type 和 Name 其實就是一個個的索引 Key,最終的目的就是為了定位并更新他們對應的指標值。
那么為了完成這個 Map 套 Map 結構的報表的計算過程,需要把這些字段一個一個反序列成字符串,然后去跟報表里面的每一層 Map Key 進行比較,一直往下找,找到最下層的 Map 后再去更新 Metrics 的值。而這些字符串基本都是在完成了比較后就會被丟棄。
這里的每一個操作,每一次字符串轉化都會引入剛剛說的兩次損失。看著這個圖思考一下,其實每一次的字符串轉化都只是為了用于在 Map 中進行逐層的尋找和比較,這個過程是不是一定要用字符串呢?
既然監控數據的完整字節流已經過來了,能不能直接在字節流上面做比較,而不用創建出來的字符串去比較?基于這個想法,我們寫了一個 BytesWrapper,它其實就是引用了完整的字節流,通過成員變量標記了對應的字段在這個字節流里面的一個起始位置和長度。我們在定位 Map Value 的時候就可以通過這個 BytesWrapper 來直接進行字節流的比較,從而省掉字符串創建的損失。
4)進一步思考
這樣是不是就已經完美了?我們知道其實每個 Java 對象創建,既使是再小的對象,都會有對象頭的損失,有時候還會有字節對齊的損失。我們可以看到我們這個 BytesWrapper 在 64 位機器,而且打開壓縮指針的情況下,它首先會占掉前面 16 個字節的對象頭,中間有三個 4 字節的字段,最后因為要做 8 字節對齊,它還有 4 字節。整體算上來,它需要用到 32 個字節。
在這個比較里面,雖然我們能夠優化掉字符串比較,但是實際上我們還是要創建一個 BytesWrapper,而且這個 BytesWrapper 依然是不能避免創建出來還是要被扔掉的命運。
其實我們就是為了想直接用字節流里面的數據來做比較,為什么要創建這樣一個對象呢?關鍵的問題就在這里。因為 HashMap 需要一個 Object,通過這個 Object 里面的 Equals 和 HashValue 來做比較。那如果我有這樣一個 Map,它的 get 方法直接接受一個字節流,以及對應的 offset 和 length ,它可以直接幫你進行比較、尋值,是不是就可以避免這個創建對象的損失了?
因此,我們針對這個場景重寫了一個 HashMap ,它內部通過字節流以及它的 offset 和 length 來計算它的 hashcode 和 equals。這個時候就可以把剛剛這個模型變成了報表里面只有一 BytesHashMap 作為它的結構,我們完全就可以在計算時直接在網絡傳輸而來的字節流上做比較,不會有多引入任何一個對象創建,當然也不會有 discard 操作。現在再回過來看 type/name 這兩個字段,它其實也是不需要進行字符串轉換的,只要把報表改成 BytesHashMap 這樣一個結構,就能夠直接利用原始的字節流來完成操作。
改動上線后可以看到, Young GC 減少了 40%。
5)小結
我們一定要去關注代碼中大量使用的對象,以及它的創建過程究竟會有多大損失。即使是這個例子中里面這么簡單的字符串構造函數,實際上它對我們的內存和 CPU 也有一定的消耗。在這個例子里面,我們通過直接引用網絡傳輸而來字節流進行計算處理就可以避免這些損耗了。
三、總結和思考
最后一部分,性能優化的一些思考。
對于 CPU 問題,第一個要想到的是減少額外的損失,額外損失是代碼直接引入的消耗以外的損失。比如說例子里面就是優化線程模型。一方面是減少了上下文切換,另一方面還減少鎖競爭。在 Java 中,一般來說鎖的實現在前面幾次的比較中使用的是 spin lock 的方式,所以你會發現其實減少鎖競爭對的 CPU 也是有著一定的好處。
把額外損失減少之后,還得考慮優化自己的代碼實現,減少不必要的操作。看一下每行代碼,比如說例子中減少字符串的構建就可以減少掉沒有必要的字符集的解碼。除此之外,還可以考慮一下把一些計算邏輯從服務端移到客戶端,幫我們分攤計算。
對于 GC 問題,根本上應該是要減少不必要對象的創建。這幾個案例里面,第一個是減少了創建字符串,直接復用字節流。還有就是案例里面通過復用內存減少了報表的重復創建、填充還有 resize。
總結
以上是生活随笔為你收集整理的CAT 性能优化的实践和思考的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 天天写业务代码?写业务代码中的成长机会!
- 下一篇: 不要再问了,数据库不建议上Docker