陆金所 CAT 优化实践
1 背景
CAT 介紹
CAT (Central Application Tracking)是一個實時監控系統,由美團點評開發并開源,定位于后端應用監控。應用集成客戶端的方式上報中間件和業務數據,支持 Transaction、Event 和 Heartbeat 等數據類型 Metrics 報表,也支持調用鏈路 Trace,對于發現和定位應用問題有很大幫助。
CAT 服務端也可以認為是一個 Lamda 架構的報表系統,通過匯聚客戶端上報的原始消息 MessageTree,實時計算出 Transaction、Event、Problem、heartbeat 等報表,保存在內存中;歷史報表序列化后保存到本地并上傳到 DB 存儲,原始上報數據壓縮和建立索引后上傳至 hdfs。
陸金所的后端應用監控也主要基于 CAT, 是在前幾年的一個版本上做二次開發,增加了新的報表。各類架構中間件大量使用 CAT 埋點,并一直在豐富各類場景,在各類問題發現和問題定位發揮了很大作用。
下圖是應用的 Transaction 報表,集成了多個中間件打點:
下圖是某一個 MessageTree,用戶通過報表中 Sample 或者通過額外的 ES 索引等搜索到某個應用的 MessageTree,trace 到具體調用事件
?遭遇性能問題
由于業務擴大,應用數量劇增,生產環境的機器數從半年前的 6000+ 增加到 10000+;另外新版本中間件增加了埋點量,隨著應用升級,單個應用實例上傳的 CAT 數據也在增加。
在 19 年 12 月份, 發現會出現偶爾某些 CAT 實例無響應的問題。由于當時手上有更緊急的問題處理,這些偶爾的崩潰往往通過重啟來解決。直到 20 年 1 月份,開發同學開始抱怨 CAT 界面響應慢,“重啟大法” 不再管用了,往往上個小時剛剛重啟,下個小時就又掛了。
?具體表現
生產上的 CAT 用的都是物理機, 配置是志強物理核心 E5 雙路 CPU(CPU 8*2,超線程 32 核), 128G 內存, OS redhat 6.5。高峰時的 CAT 的 context switch 相當高,達到 120 萬 / 秒, 系統負載偶爾到 20 以上,一次較大的 Full GC 往往耗時 3-5 秒;一次長時間的 GC 就可能造成 CAT 的端口無響應,只有重啟才能解決。
?臨時治理
GC 方式從 CMS 改到了 G1,并調大了 heap 到 80G
宕機的實例總是那么幾臺,這是負載不均衡造成的,因此我們修改客戶端上報數據的路由規則讓負載更加均衡
申請緊急擴容,無奈年末硬件資源和人員都非常緊張,遠水解決不了近渴
生產上的應用集群還在擴大,明年還有更多項目需要上線,硬件擴容不僅增加硬件成本,也會增加運維成本;直接 提升性能應該是最好的方案。
測試環境 CAT 也存在類似的容量問題, 我們有 3 臺物理機來跑 CAT,但我們的測試環境一共有 15000 個應用實例。之前嘗試過應用開啟 CAT,但導致 CAT 崩潰,當前的策略是部分環境和應用開啟 CAT 打點的方式,是能提供了部分的監控能力,但也對開發測試人員造成了不小的困擾。
對 CAT 做性能優化,一方面能解決生產容量不足的問題,另一方面也能協助規劃測試環境的集群容量,大幅提升開發測試效率。
優化準備
性能優化不是沒有方向的,其實我們在 2018 年就觀察到 CAT 在業務高峰時刻的上下文切換特別高(>1mil/s。在 19 年的 Qcon 會議上,攜程的梁錦華介紹了他們對 CAT 的性能優化工作, 服務端的優化集中在改進線程模型來降低上下文切換,改進內存模型來降低 GC。所以我們的優化也要覆蓋這兩個方向,另外,我們也要看下在 JVM、OS 配置層面能做哪些改進。
?線程模型優化
目的是降低上下文切換帶來的開銷;
我們來看下什么是上下文切換,我們都知道現代 OS 基本是多任務的,CPU 資源在 OS 在不同的任務(線程)需求之間切換分配。為了確保正確性,每一次切換 OS 都需要保存上一次線程的運行狀態,并加載下一個線程的狀態,這些狀態往往涉及 CPU 上的多種寄存器;另外,在切換到下一個的線程之后,還會造成內存訪問效率的損失,這主要是不同線程運行時需要訪問的數據不同,由此帶來的多級緩存命中率下降而降低運行效率。
一次上下文切換的直接開銷在 1-5ns 級別,而帶來的間接開銷則可能到 1us 到數個 ms 之間,有興趣的同學可以參考這兩篇文章: Quantifying The Cost of Context Switch 、 Measuring context switching and memory overheads for Linux threads
?內存優化
CAT 作為 APM 應用,每秒攝入的數據在幾十到一百多 MB 級別,數據經過反序列化之后,還需要對內存報表做大量的更新操作,這個過程會創建特別多的臨時對象,會造成頻繁的 Young GC。CAT 內存中維護了當前小時的報表,每一個小時中中,常駐內存隨著時間推移逐漸增大,造成可用內存減少,頻繁觸發 Full GC。
?JVM/OS/ 網絡設置優化
JVM 已經發布到版本 11,新版本帶來了一部分免費的性能提升, 另外 GC 的方式和參數也可以調整。開啟 OS 內存大頁和調整網絡參數等在理論上也能帶來性能提升。
?核心指標
作為一個實時數據攝入的報表系統,我們很快就確定了幾個核心的性能指標:
-
服務端穩定性: 功能核心功能正常工作,服務端是否有 OOM、無響應甚至進程崩潰現象
-
服務端負載: 操作系統系統負載
-
攝入數據速率:主要考察單位時間(1 小時)消費消息數量和數據大小
-
服務端消息丟失量:因為來不及處理而丟棄的消息數量
-
客戶端失敗消息數量:客戶端由于發送速率低于生產速率造成的消息丟棄量
2 第一輪優化
下圖描述了 CAT 的消息處理和線程模型,Netty Worker 線程生成 MessageTree 后,offer 到每個 Analyzer 專有隊列(Blocking Queue)中,由 Analyzer 線程從隊列中拉去后處理并生成對應的內存報表。
不難理解這里的設計初衷是讓每個 Analyzer 獨立使用其隊列,實現了 Analyzer 處理的隔離,慢的 Analyer 不會影響那些快的 Analzyer。
對于某些重要且計算量較大的 Analyzer(例如圖中 Transaction Analyzer),使用了多個隊列,并根據客戶端應用名的 hash 來均衡多個隊列任務;CAT 內部自建報表合并機制來合并多份報表。
如果某一個 Analyzer 的隊列滿了導致無法推送,Netty 線程則會直接丟棄該消息,并統計丟棄次數。
下面的代碼描述了插入消息隊列的過程:
public void distribute(MessageTree tree) {String domain = tree.getDomain(); // domain 就是上傳消息的應用名for(Entry<Strig, List<PeriodTask>> entry: m_tasks.entrySet()){List<PeriodTask> tasks = entry.getValue(); // PeriodTask 封裝了消息隊列int index = 0;int length = tasks.size(); // 多個 Analyzer 隊列if (length > 1) {index = Math.abs(domain.hashCode()) % length;}PeriodTask task = tasks.get(index); if(!task.enqueue(tree)) { // 記錄的消息丟失}} }Analyzer 拉取并消費消息的代碼如下:
while(true) // 無限循環拉取數據,最大 5ms 超時MessageTree message = m_queue.poll(5, TimeUnit.MILLISECONDS); if(message != null) {process(message) } }現在一共有 22 個 Analyzer,略微有點多,我們也不能刪除現有的 Analyzer,因為不少系統已經依賴 CAT 的各類報表來協助監控。
通過線下 profiling 并結合研究代碼,我們發現:
隊列的 offer 和 poll 占用了超過 7% 的 CPU 處理時間
從線程 dump 來看,Analyzer 線程經常處于 LockSupport.parkNanos 調用上
由于部分 Analyzer 有多個線程,Analyzer 線程總數量約 30 個,其線程 CPU 占用又不太高 (<30%)
不同類型的 Analyzer 只會處理滿足特定條件的 MessageTree,但是 Netty Worker 線程在做 queue.offer 動作時沒有判斷 MessageTree 能否被該 Analyzer 處理,Analyzer 獲取到部分 MessageTree 之后又丟棄
回到系統設計模式上來, 一組線程生成 MessageTree,并采用 BlockingQueue 發送到另一組線程來處理,這是典型的消息傳遞場景。提到跨線程的消息傳遞,我們不能不提到大名鼎鼎的 Disruptor 的 RingBuffer 模型。
Disruptor 框架是 LMAX Exchange 開發的高性能隊列模型,該框架充分利用了 Java 語言中的 volatile 語義,創新性地使用了 RingBuffer 數據結構,實現了在線程之間快速消息傳遞,支持批量消費。吞吐量和延時性能都高于 Java 標準庫中的 BlockingQueue,其性能關系是:
Disruptor > ArrayBlockingQueue > LinkedBlockingQueue
由于篇幅關系,我們就不在這里詳細介紹 Disruptor 內部原理了,有興趣的小伙伴請參考 Disruptor 介紹。
線程模型嘗試和調整
MessageTree 做預過濾是必須要做的,這部分很快做完了,但在線程模型的改動上我們經過了幾次嘗試:
?嘗試一
考慮到 Disruptor 做線程間的消息傳遞效率,我們將 BlockingQueue 簡單替換成了 Disruptor 實現。效果不是很明顯,總體的 CPU 使用并沒有下降多少。
由于 Disruptor 需要 Event 對象放入 RingBuffer,封裝 MessageTree 的類定義如下:
class MessageTreeEvent {MessageTree message; }??????? 嘗試二
為降低 Analyzer 線程數, 我們想到將多個 Analyzer 線程合并,在 Disruptor 框架下需使用同一個 RingBuffer。于是我們將一個 MessageTree 映射到多個 MessageTreeEvent,并通過1個全局的的 RingBuffer,分發給一個線程池來處理??紤]到 Ringbuffer 中 MessageTreeEvent 數量增加,我們將 RingBuffer 大小調整到 262144 (1<<18)
新的 MessageTreeEvent 定位如下:
class MessageTreeEvent {MessageTree message;String analyzerId; }???????如果 22 個 Analyzer 都采用這個方法,并假設 MessageTree 速率為每秒 5 萬,那么最大就有 22 * 5w/s = 110w/s 速率的消息需要通過 Ringbuffer。這個數字乍一看非常大,但如果對照性能 Disruptor 測試結果, 這個速率對于 Disruptor 框架來說壓力不大。
我們挑選了大概 10 個 Analyzer 加入這個大的 RingBuffer 來處理,但無論如何如何增大 buffer 消息丟棄情況還是有點多,特別是較為重要的 Transaction/Problem 等 Analyzer 的消息。
?嘗試三
考慮到不同的 Analyzer 重要程度不同,我們的盡量保證核心 Analyzer 能正常工作,那些不太重要的 Analyzer 丟一點消息是可以接受的。于是我們給 Analyzer 引入了優先級概念,
enum AnalyzerLevel {HIGH(1),MID(GLOBAL_REPORT_QUEUE_SIZE/16),LOW(GLOBAL_REPORT_QUEUE_SIZE/4);public final int requiredCapacity;AnalyzerLevel(int requiredCapacity) {this.requiredCapacity=requirecapacity;} }???????下面是往 RingBuffer 插入數據的代碼, 也體現了 disruptor 的優點,hasAvailableCapacity 這個方法與 BlockingQueue 的 size 相比,其內部實現是無鎖的。
RingBuffer<MessageTreeEvent> ringBuffer = disruptor.getRingBuffer(); if(ringBuffer.hasAvailableCapacity(m_analyzer.getLevel().requiredCapacity)) { long seq=ringBuffer.next();try {// 準備 MessageTreeEvent 對象MessageTreeEvent event = ringBuffer.get(sequence);event.message = messageTree;event.analyzerName = m_analyzerName;} finally {ringBuffer.publish(seq) ;} } else {// 丟棄并記錄 }???????我們又引入了分組的概念,將 Analyzer 分為 2 組,每一組使用一個 RingBuffer,每一個 RingBuffer 使用 2 個線程來消費。CAT 一共 22 個 Analyzer,我們將 15 個 Analyzer 改造到了新的線程模型 。
Disruptor 消費和啟動代碼如下:
// int threadsPerRingBuffer = 2 WorkHandler<MessageTreeEvent> [] handlers = new WorkHanlder[threadsPerRingBuffer]; for(int index = 0; index < threadsPerRingBuffer; index ++) {handlers[index] = createHanlder(index); // 創建多個消費線程對等 } disruptor.handleEventWithWokerPool(handlers); // 設置 disruptor 的消費者 disruptor.start(); // 啟動private WorkHanlder<MessageTreeEvent> createHandler(int threadIdx) {return WorkerHanlder<MessageTreeEvent> () {public void onEvent(MessageTreeEvent event) {String analyzerName = event.analyzerName;getAnalyzer(threadIdx).process(event.message);}}; }???????另外, 有了之前合并線程成功的經驗, 在仔細檢查代碼時和檢查線程棧時,發現 Netty 的 worker 線程數為 24, 確實有點多。我們逐步降低,測試表明 Netty work 線程數為 2 時仍然一切正常,從 top -H 的輸出來看,在 100MB/ 秒的網絡攝入流量下,Netty Worker 線程的 CPU 也就在 70% 左右,未見客戶端發送失敗的情況。
最終的線程模型如下:
?JVM 設置改動
在 JVM 和 GC 方式的選擇上,我們選用了 open Jdk 11 和 G1 的方式,在測試環境,這個組合的運行穩定,GC 的延時較低, CAT 的頁面響應也比較快。
優化工作做了 2 周, 快到了過年的時間,我們先找了 2 臺機器驗證,驗證通過后更新到了所有實例。
改造效果
我們將測試環境 4500 臺機器左右的流量導入到一臺機器, 在修改前,這臺 CAT 機器剛起來 1 分鐘后就會陷入無響應狀態。
改造后測試環境的這臺服務器順利跑了起來, 在小時消息量 0.94 億,消息大小 210G 情況下 “top -H” 輸出如下, 可以看到 Netty work 線程 (圖中 epollEventLoopG)的占用不高,4 個全局的 Analyzer 線程 (圖中 Cat-Global 開頭線程) 的占用也不太高,無消息丟失。
?
在生產環境中也找出一臺機器,通過配置路由規則,讓其承載較大流量,這臺機器在不同負載情況下表現如下:
注:我們區分了核心消息(優先級為 High 的 Analyzer)與非核心消息丟失。G1GC 在生產環境表現穩定,一次 young G1GC 平均耗時約 200ms,未見 Old GC。
上下文切換下降了一半以上,CPU 負載也下來了很多 ,沒有出現超過負載 20+ 的情況,應該可以安穩過年了!
未解決的問題
春節前的一輪優化主要覆蓋線程模型優化與 JVM 設定, 內存優化還沒做。
生產環境中 CAT 在日常的高峰流量中 CPU 負載依然超過 10,并隨著小時報表在內存中積累,10 分鐘后的 CPU 負載明顯攀升 (如下圖)
結合測試環境中 CAT 進程的堆 dump,"jmap -histo $pid" 的輸出的分析中,我們發現還存在如下幾個問題:
CPU 使用率還是有點高,承載較大流量是出現核心消息丟失
上下文切換較高,平時負載在 40 萬 / 秒, 高峰時間到 50 多萬 / 秒
臨時對象較多,例如 SimpleDateFormat/DecimalFormat 等對象
LinkedHashMap 中的內存使用效率較低
駐留內存中簡單對象數量太多
詳細優化過程先從內存優化部分說起
3 內存優化
有效內存使用率概念
關于內存使用效率,和大家分享下 Java 中對象的大小概念
-
Shallow Size: 包含當前對象 Header 和對象直接擁有的內部數據,以下面的對象 s 為例,除了對象 Header 之外,包含 1 個數組引用、1 個 Map 引用、1 個 double 和 1 個 int, 其內部數據大小是 8*3+4 = 28 byte
在 64 位 JVM 未開啟指針壓縮情況下加上對象 Header 16 byte 并保持 8 byte 的對齊,最終 Shallow Size 大小 28 + 16 + 4 = 48 byte
class Sample {int[] intArray; // reference size 8Map<String,String> map; // reference size 8double doubleValue; // double size 8int intValue; // int size 4}Sample?s?=?new?Sample();希望了解更多 java 對象內存布局的朋友可以使用 open jdk jol 工具 ,下面是利用 jol 打印上述對象 layout 的代碼
import org.openjdk.jol.info.ClassLayout; import org.openjdk.jol.vm.VM;public class ObjectLayoutMain {public static void main(String[] args) throws Exception {System.out.println(VM.current().details());System.out.println(ClassLayout.parseClass(Sample.class).toPrintable());} }以下是使用 “-Xms40g -Xmx40g” 的 vm 參數在 64 位 jvm11 下的輸出
# Running 64-bit HotSpot VM.# Objects are 8 bytes aligned.# Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]# Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]org.jacky.playground.jol.Sample object internals:OFFSET SIZE TYPE DESCRIPTION VALUE0 16 (object header) N/A16 8 double Sample.doubleValue N/A24 4 int Sample.intValue N/A28 4 (alignment/padding gap) 32 8 int[] Sample.intArray N/A40 8 java.util.Map Sample.map N/AInstance size: 48 bytes???????注: 設置堆內存大于 32G 會關閉引用壓縮 ,興趣的同學可以自己跑一下看應用壓縮或者是 32 位 JVM 下的輸出。
-
Retain Size: 內存中的對象存在引用關系,消除循環后可以認為是一個個對象樹。對象的 Retain Size 是該對象對應的對象樹的大小。與對象的 Shallow Size 相比,Retain Size 是一個相對動態的值,隨著其下層對象具體值變化而變化。內存有效使用率定義如下:
內存使用率 = 實際數據占用大小 / Retain Size
更多內存效率理論請參考:Building Memory-efficient Java Applications: Practices and Challenges
優化實踐
?現狀
從 CAT 的 heap dump 中我們看到最大的對象主要是當前小時的各種 Report 對象, 這些 Report 大量使用了多層級的 Map 結構,如下圖 (圖中的數字是經驗估計數量)??梢钥吹?Map 對象非常多, 特別是層次往下的那些對象。
現有代碼采用 java 標準庫中的 LinkedHashMap 來表示這些層次結構,這也就產生了大量 LinkedHashMap 以及子對象 LinkedhashMap$Entry,從下面堆 dump 的內存文件分析看到這幾個 package 的對象在內存占用按照類型排行上非常靠前:
-
heartbeat.model.event.*
-
transaction.model.event.*
-
event.model.entity.*
?開放地址 HashMap 實現
我們發現對于那些靠近葉子節點的報表對象,采用 LinkedHashMap 在大多數時候有點多余,因為不需要記錄插入順序,可以簡化成 HashMap,下面是這兩者 Entry/Node 節點類的定義比較:
//java.util.Hashstatic class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;Node<K,V> next;}//java.util.LinkedHashMapstatic class Entry<K,V> extends HashMap.Node<K,V> {Entry<K,V> before, after; // 額外的 before & after 引用Entry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}}是不是還能進一步優化呢?答案是可以,而且改動很小
很多 Java 技術棧的同學對標準庫中利用鏈表法實現的 HashMap 比較熟悉, 但大學學過《數據結構》課程的同學可能還記得另一種 Hash 的實現 開放地址 Hash 。與鏈表法開一個鏈表來解決沖突的方式不同, 開放地址 Map 通過在線性表中重新計算一個新位置來解決。
?實測大小對比:
下述測試代碼生成一個包含大小為 size 的數組,其保存大小從 0 到 size-1 的 HashMap
import org.agrona.collections.Int2ObjectHashMap; import java.util.*private static Map[] populate(int size, boolean useOpenMap) {List<Map<Integer, Range>> result = new ArrayList<>();for (int i = 1; i <= size; i++) {Map<Integer, Range> m = useOpenMap ? new Int2ObjectHashMap<>() : new LinkedHashMap<>();for (int j = 0; j < i; j++) {m.put(j, new Range());}result.add(m);}return result.toArray(new Map[]{}); } public static class Range { // retain size=56int id = 0;int count = 0;int fails = 1;double sum = 1.0;double avg = 1.0;double max = 1; }???????運行結果整理如下, 可以看到切換到 Int2ObjectHashMap 的實現就能輕松節省 30% 以上內存,如果值類型的 shallow 更小,節省還會更多。
值得說明的是:
在 CPU 性能測試中,開放地址的 Map 的 get/put 都比 HashMap 性能略差,但絕對值差距很小
開放地址的 Map 在刪除時需要在將這個位置 mark 成已刪除,會造成空間浪費,但在 CAT 計算中無刪除操作
?對象消除
在上一個圖中,可以注意到 heartbeat.model.event.Detail 對象數量非常之多,其占用內存就超過 1G!
對應到業務邏輯,每一個 Detail 都是描述應用 heartbeat 的某一個屬性,例如 "SystenLoad"、“PhysicalFreeMemory”、"GC Count" 等,這些 Details 存在如下幾個特點:
-
Key 大量重復, Key 去重后數量很少,同一個應用的 CAT 客戶端在不同時間、不同實例的 heartbeat 中的 Key 都一樣;還有部分 Key 是 CAT 客戶端自帶的,這部分 Key 對所有應用都一樣
-
Detail 的定義非常簡單,m_label 總是為 null,可以直接去掉
-
Detail 對象保存在 Extension 對象中,其中的 key 與 value 中 id 值相同
從上面的幾個特點,我們可以將這里的 key 對象映射成 int,一個 detail 對象的有效數據就是一個 int 和一個 double 對象,總的有效大小為 12byte。
我們來找兩個例子來計算下內存使用效率:
這是一個非典型場景, m_details 中 key 數量為 122,比較多
我們來計算上面 m_details hashmap 的內存使用效率:
Retained size 17632
有效大小 122 * 12
內存有效率 122 * 12 / 17632 = 8.3%
下面這個 m_details,key 數量較小,內存使用效率: (12 *2)/472=5.1%
在使用 eclipse collection 的 LongDoubleMap 替代后,上述兩例的使用效率分別提高到 46.1 和 15.8%。
?其他內存優化
考慮到線程安全問題,SimpleDateFormat 和 DecimalFormat 等對象在使用時創建新實例,使用線程安全的實現來代替即可。
4 繼續線程優化
為了可以更方便地調整全局線程 /ringBuffer,并始終保持不同線程之間負載和優先級的均衡,我們引入了 Analyzer 動態分組。
Analyzer 動態分組
我們對大約 20 個 Analyzer 按照重要正度和計算復雜度綜合考慮排序,用于 Analyzer 分組。
動態分組保證那些計算量大且優先級又高的 Analyzer 不集中競爭計算資源, 實現規則如下
effectiveRingBufferIndex?=?analyzer.getGlobalIndex()?%?ringbufferCount?我們將剩下的幾個 Analyzer 合并到了全局線程組,對 Netty Worker 數、全局線程數和每個 RingBuffer 的消費線程數做了配置化。默認開啟 2 個 Netty Worker 線程,3 個全局線程 /ringBuffer,考慮到維護多份報表的內存開銷較大,每個 RingBuffer 的消費線程數默認設置為 1。優化后的典型的線程配置如下:
另外繼續增加了 Ringbuffer 大小到 524288 (2^19) ,當然我們也清楚增加緩存大小有兩個壞處:
最大處理延時增加,考慮到 CAT 的處理能力,這個影響最大不超過 5 秒,業務上可以接受
buffer 增大導致內存使用增加,由于 CAT 進程都是動輒幾十 G 的堆,額外的百萬個 buffer 對象帶來的影響微乎其微
其他優化與嘗試
-
對 ConcurrentHashMap 做 null 檢查后使用 synchronize 改到使用 ConcurrentMap.computeIfAbsent
CAT 啟動或者跨小時的時候會集中創建 bucket,采用 null 檢查 + synchronize 的方法會造成集中的線程堵塞
-
縮減 CAT 集群內部請求的線程數量,增加其 buffer 大小,并使用連接池來管理連接
-
增加磁盤寫入線程數量和 buffer 來緩解測試環境磁盤寫入較慢的問題
-
測試環境 OS 的電源管理從 on-demand 改成 performance 模式,與生產對齊
-
測試環境嘗試開啟內存大頁,效果不太明顯,生產環境也需要運維協助配置,暫放棄
5 效果
單機性能
為了驗證優化效果,我們對某一臺機器又加大了流量,比較了不同負載的表現
注:1.74 億消息量是人為加大負載,每秒網絡流量 114MB(402GB/3600) ,已打滿千兆線路。
下圖為小時消息量 1.45 億下的系統表現:
可以看到上下文切換、CPU 的使用率和 GC 都非常平穩,核心消息丟失為 0;非核心消息丟失略高??煽紤]增加全局處理線程數到4甚至5來緩解極端負載下的非核心消息丟失。
容量評估
基于最新的單機性能和總的生產數據量,現有生產環境集群還有約 50% 的冗余容量,未來 2 年都無需擴容。
測試環境的 CAT 容量也評估了出來,現有 3 臺 CAT 支撐 15000 個測試應用實例有點勉強,正在申請額外 3 臺服務器,這樣就能支持所有的測試集群,并留有部分冗余。
6 思考
超線程
超線程(Hyper Thread,HT)給 OS 提供了更多的可用核心,但這些核心是畢竟是硬件虛擬出來的,目的是更好地使用 CPU 多余的計算和緩存資源,提供更高的吞吐量。
簡單地認為開啟 HT 可以免費獲得一倍的可用線程并計算能力能翻倍是不可取的,物理核心和虛擬核心會競爭使用計算和緩存資源,在某些情況下甚至會降低吞吐量。
在計算密集的場景下,HT 的虛擬核心是不能計算在可用核心里面的,因為虛擬 CPU 的計算能力有限。這可能也是我們生產環境 CPU 飆到 20 左右就會出現計算能力嚴重不足,帶來端口無響應等問題。
Java 內存使用效率
Java 有很好的面向對象的特性,在書寫程序時帶來了很多便利,但也帶來了運行時刻的內存負擔,每個對象都有個很大的 Header,有時 Header 甚至超過了本身數據的大小。
這有兩個比較好的解決方案值得期待:
?Java 語言支持 struct 類型
Java 語言 struct 類型需求很早就被提了出來,struct 類型和原生類型一樣,不屬于對象范疇,沒有對象 Header 的內存成本。近年放在 valhalla 項目中, 19 年 5 月份發布了原型版,有興趣的同學可以看下。
?java 與原生語言混合編程
Oracle 的 graalvm 項目,支持 Java 語言與其他原生語言混合編程,在 Java 應用的性能瓶頸的部分采用 C 或者 Rust 語言來實現。該項目已經開源,已經取得了一定的進展,可在官網下載社區版的 graalvm 的 JDK。
7 總結
兩輪性能優化各耗時 2 周,回顧整個優化過程,我們制定了大體的方向,找到核心的性能指標,大量查找資料,從原理驗證做起,并結合線下環境的逐步驗證,直到目標達成為止。
在優化過程中,我們也學習了 CAT 本身設計巧妙的地方,例如異步化的實時數據處理、支持水平擴容、高效的序列化 / 反序列化和集群數據路由等。在此感謝美團點評的朋友把這個項目開源出來,讓大量的開發者收益。
性能優化是一個綜合的話題,并沒有什么圣杯,只需在工作中勤摸索、常思考、積極與他人交流并敢于嘗試總能有收獲。我們把這次優化經歷寫出來,希望能拋磚引玉,也歡迎各位同行指正。
8 作者介紹
蔡健,陸金所應用架構師。2008 年復旦大學碩士畢業后加入大摩, 2016 年加入陸金所,負責 Java 架構中間件和應用監控;職業理念是專注,并對新技術時刻充滿熱情。
方超,陸金所應用架構師。十年工作經驗。熱愛生活和技術。
總結
以上是生活随笔為你收集整理的陆金所 CAT 优化实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 错误日志这样排查,干活更得劲了!!
- 下一篇: 一个线程池 bug 引发的 GC 思考!