解密得物Trace2.0:日PB級資料量下的計算與儲存效能最佳化實戰
來源:得物技術
目錄
一、背景
二、客戶端多通道協議
1. 採集多通道協議
三、計算模型
四、資料壓縮
五、儲存方案
六、升級 JDK21
1. 升級後效果
七、結語
一
背景
Trace2.0 是得物監控團隊引入 OpenTelemetry 協議並落地的全新應用監控系統,從 2021 年底正式開始使用。在過去的兩年裡,我們面臨著資料量呈爆炸式增長的巨大挑戰。然而,透過對計算和儲存的不斷最佳化,我們成功地控制了機器數量的指數級增加。我們每天處理的日增資料量數 PB(相比去年增長了 4 倍),每天產生的 Span 數超過了數萬億條。系統面對的峰值流量可達到每秒幾千萬行 Span,每秒上報的頻寬壓縮後高達數十 GB。我們所使用的儲存引擎 Clickhouse 單機支援每秒近百萬行的寫入量。這些資料成為 Trace2.0 作為一款強大的應用監控系統的標誌,為監控團隊提供了全方位的監控資料分析能力。Trace2.0 使得我們能夠及時發現和解決潛在的系統問題,確保我們的服務能夠始終穩定可靠地執行。
下面是整體的架構:
二
客戶端多通道協議
在 OpenTelemetry 中,客戶端會生成呼叫鏈資訊並將其推送到遠端伺服器。傳輸資料的請求協議通常包括 HTTP 和 gRPC。gRPC 是基於 Google 開發的高效能開源 RPC 框架,使用二進位制格式傳輸資料。它具有較高的效能和較低的網路開銷,適用於大規模應用和高併發場景。gRPC 還提供自動化的資料序列化和反序列化,以及強型別的介面定義。
在 OpenTelemetry 中,預設使用的是 gRPC 協議進行上報。在 gRPC 中,使用長連線進行通訊。然而,長時間的連線可能會導致一些問題,如伺服器上的資源洩漏、連線狀態不穩定或服務端單機負載過高。透過設定 maxConnectionAge 引數,可以限制連線的持續時間,確保不會因為長時間的連線而出現這些問題。
NettyServerBuilder.forPort(8081) .maxConnectionAge(grpcConfig.getMaxConnectionAgeInSeconds(), TimeUnit.SECONDS) .build();
隨著資料量的快速增長,我們採用了基於負載均衡器(SLB)的方式來實現後端機器的負載均衡。然而,隨著全量 Trace下超高流量需求的增加,單個 SLB 的頻寬已經無法滿足要求。為解決這個問題,我們決定增加 SLB 數量,每個後端伺服器開啟多個埠,並使每個 SLB 例項繫結一個埠。這樣透過水平擴充套件 SLB,可以改善負載分擔。
然而,隨著 SLB 數量的增加,維護成本也隨之增加,並且仍然可能導致某個後端伺服器負載較高,形成熱點問題。為了解決這個問題,我們做出了一個決定——去除 SLB,直接將流量分擔到後端伺服器上。這樣做不僅可以簡化系統架構,還可以更均衡地分配負載,提高整體效能。
採集多通道協議
服務註冊和心跳:服務端啟動後,會向控制平面註冊服務資訊,並定時傳送心跳來進行健康檢查。如果服務端在一定時間內沒有進行心跳上報,控制平面將把其剔除。
定時拉取服務列表:客戶端透過和控制平面進行通訊,定時獲取最新的服務端例項資訊。透過這種方式,客戶端可以獲得最新的服務端列表,以保證與可靠的後端例項進行通訊。
多通道協議:在多通道協議中,不再使用負載均衡器,而是直接將請求傳送到多個後端伺服器上。每個後端伺服器都可以獨立處理請求,實現流量的均衡負載,提高系統效能,並且減輕熱點問題的影響。
提高系統效能:透過直連後端伺服器,可以充分利用每個伺服器的計算能力和頻寬,從而提高整個系統的效能和吞吐量。
減少維護成本:去除了負載均衡器,減少了系統的維護成本,避免了負載均衡器成為效能瓶頸的問題。
避免熱點問題:直連後端伺服器並分擔流量的方式可以減輕系統中可能出現的熱點問題,提高系統的穩定性和可靠性。
三
計算模型
Trace2.0 後端的整體架構參考 Pipeline 架構。在這個架構中,訊息的採集會被放到佇列裡進行處理,處理之後再進行儲存。整個計算程式採用 Source、Processor、Sink 多管道多工處理方式,下面是詳細的流程:
component: source: kafka: - name: "otelTraceKafkaConsumer" ## Trace消費 topics: "otel-span" consumerGroup: "otel_storage_trace" parallel: 1 # 消費的執行緒數 servers: "otel-kafka.com:9092" targets: "decodeProcessor" processor: - name: "decodeProcessor" clazz: "org.poizon.apm.component.processor.DecodeProcessor" parallel: 4 targets: "filterProcessor" - name: "filterProcessor" clazz: "org.poizon.apm.component.processor.FilterProcessor" parallel: 2 targets: "spanMetricExtractor,metadataExtractor,topologyExtractor" - name: "spanMetricExtractor" clazz: "org.poizon.apm.component.processor.SpanMetricExtractor" parallel: 2 props: topic: "otel-spanMetric" targets: "otel_kafka" - name: "metadataExtractor" clazz: "org.poizon.apm.component.processor.MetadataExtractor" parallel: 2 props: topic: "otel-metadata" targets: "otel_kafka" - name: "topologyExtractor" clazz: "org.poizon.apm.component.processor.MetadataExtractor" parallel: 2 props: topic: "otel-topology" targets: "otel_kafka" sink: kafka: - name: "otel_kafka" topics: "otel-spanMetric,otel-metadata,otel-topology" props: bootstrap.servers: otel-kafka.com:9092 key.serializer: org.apache.kafka.common.serialization.ByteArraySerializer value.serializer: org.apache.kafka.common.serialization.ByteArraySerializer compression.type: zstd
客戶端的 Trace 資料傳送到服務端 OTel Server 後,根據應用的 AppName 傳送到不同的 Kafka Topic 中。
接收到資料後,資料會經過反序列化、清洗、轉換等模組的處理。
為了實現更高效的任務處理,系統選擇了使用 Disruptor 緩衝佇列。這個緩衝佇列採用了多生產者單消費者的模式,可以有效地減少執行緒之間的競爭,提高系統的併發處理能力。
採用多工多管道方式進行處理,透過緩衝佇列將各個任務之間進行解耦。
每個任務都會採用特定的路由策略,例如輪詢或雜湊等,來確定該任務應該處理的資料。
透過以上架構和流程,系統能夠實現高效的任務處理,減少執行緒競爭,並提高系統的併發處理能力。同時,任務間的解耦和路由策略的應用,使得系統能夠根據具體需求對資料進行靈活的處理和分發。
四
資料壓縮
為了提高資料的合併壓縮比,我們採用了增加時間視窗並使用 keyBy 對資料進行分組的方法,將 Span 轉換為 SpanList,並進行批次合併操作。這樣的流程中,我們無需事先將所有原始資料載入到記憶體中,而是逐個或者分塊地將其寫入到 ZstdOutputStream 中進行實時壓縮處理。壓縮後的資料也不會一次性儲存在記憶體中,而是透過 OutputStream 逐個或者分塊地寫入到 Kafka(或其他儲存介質)中。這種採用 OutputStream 和 Zstd 進行資料流式壓縮的方式,有效地提升了資料的壓縮率。
以下是壓縮核心程式碼的示例:
private FixedByteArrayOutputStream baos; private OutputStream out; public void write(byte[] body) { out.write(Bytes.toBytes(body.length)); out.write(body); } public byte[] flush() throws IOException { out.close(); baos.flush(); byte[] data = baos.toByteArray(); baos.reset(); out = new ZstdOutputStream(baos); return data; } public void initOutputStream() throws IOException { this.baos = new FixedByteArrayOutputStream(4096); this.out = new ZstdOutputStream(this.baos, 3); }
透過線上資料觀察,我們發現 Trace 索引資料的壓縮比提高了 5 倍,而 Trace 明細資料(使用ZSTD Level 3)的壓縮比更是提高了 17 倍。這意味著我們能以更低的儲存成本和更高的儲存效率來處理大量的監控資料。
五
儲存方案
面對如此大的資料量(全量 Trace),平衡成本並確儲存儲系統如何支援如此高的 TPS 寫入是業界關注的熱門話題。以下是一些最佳化儲存方案的關鍵策略:
最佳化儲存引擎配置,包括緩衝區大小、日誌重新整理策略等,以提高效能。
水平擴充套件,採用分割槽和分片等技術對資料進行分散式儲存,以及採用分散式儲存引擎,如 Cassandra、HBase 等,來實現水平擴充套件,提高寫入吞吐量。
非同步寫入,採用訊息佇列或非同步處理來緩解寫入壓力,提高系統的寫入併發能力。
批次寫入,透過批次寫入來減少寫入操作的次數,減少對儲存層的壓力。
資料壓縮和索引最佳化,採用高效的資料壓縮演算法和合理的索引策略,以減少儲存空間佔用和提高寫入效能。
負載均衡和故障恢復,合理設計負載均衡策略,並實施有效的故障恢復機制,以確保系統在寫入壓力大時能夠保持穩定和可靠。
監控和效能調優,持續監控系統的效能指標,進行效能調優,及時發現和解決效能瓶頸。
來看看我們的架構圖:
為了充分利用批次寫入的優勢,資料在流入 Kafka 之前使用預定的路由策略將資料寫入相應的 Kafka 分割槽,從而提高了寫入 Kafka 的壓縮率。這樣做不僅可以減少網路傳輸的開銷,還可以進一步提升儲存效率。
同時,儲存服務 OTel-Exporter 充分利用記憶體進行資料的“攢批”操作。他們將一個 POD 專門處理兩個 Kafka 分割槽的資料(實際根據各場景確定),這樣每個 POD 可以獨佔一個執行緒處理資料,減少了執行緒之間的上下文切換和競爭。當記憶體中的資料達到一定閾值時,這部分資料會被刷寫到遠端的儲存 ClickHouse 中。
這種方式與面向列儲存引擎 ClickHouse 的低 TPS(每秒事務處理次數)和高吞吐量寫入特性非常契合。目前,他們的單機 ClickHouse 每秒可支援超過 90 萬行的寫入吞吐量,這遠遠超過了 HBase 和 ES 的寫入能力。
這種高效的資料寫入與儲存策略不僅可以保證資料的快速處理和儲存,還能夠節約成本並提高整體系統的效能。
六
升級 JDK21
2023 年,公司內部多個系統成功升級至 JDK 17,並且收穫了顯著的好處。相對於使用 JDK 8,JDK 17 在效能方面表現更高效。它能夠利用更少的記憶體和 CPU 資源,從而提高系統效能並降低執行成本。JDK 17 中包含了許多效能最佳化的功能,包括改進的 JIT 編譯器和垃圾回收器等。這些最佳化措施明顯提高了應用程式的效能。僅僅從 Java 8 升級到 Java 17,即使沒有其他改動,效能就直接提升了 10%。這主要得益於對 NIO 底層的重寫。在升級過程中,JVM 也伴隨著一系列相關的最佳化措施,進一步提升了系統效能。
同時,JDK 19 推出了虛擬執行緒(也稱為協程),以解決讀寫作業系統中執行緒依賴核心執行緒實現時帶來的額外開銷問題。最終,我們選擇升級到 JDK 21。
以 Trace2.0 後端計算程式為例,其採用的是基礎庫,比如 Guava、Lombok、Jackson、Netty 和 Maven 進行構建。整個升級流程也相對簡單,僅需以下四步:
第一步:指定 JDK 版本
<properties> <java.version>21</java.version> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target></properties>
第二步:引入 javax.annotation 程式包、升級 lombok
<dependency> <groupId>javax.annotation</groupId> <artifactId>jsr250-api</artifactId> <version>1.0</version></dependency><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version></dependency>
第三步:JVM 引數設定
-Xms22g -Xmx22g#開啟ZGC-XX:+UseZGC-XX:MaxMetaspaceSize=512m-XX:+UseStringDeduplication#GC週期之間的最大間隔(單位秒)-XX:ZCollectionInterval=120-XX:ReservedCodeCacheSize=256m-XX:InitialCodeCacheSize=256m-XX:ConcGCThreads=2-XX:ParallelGCThreads=6#官方的解釋是 ZGC 的分配尖峰容忍度,數值越大越早觸發GC-XX:ZAllocationSpikeTolerance=5-XX:+UnlockDiagnosticVMOptions#關閉主動GC週期,在主動回收模式下,ZGC 會在系統空閒時自動執行垃圾回收,以減少垃圾回收在應用程式忙碌時所造成的影響。如果未指定此引數(預設情況),ZGC 會在需要時(即堆記憶體不足以滿足分配請求時)執行垃圾回收。-XX:-ZProactive-Xlog:safepoint,classhisto*=trace,age*,gc*=info:file=/logs/gc-%t.log:time,tid,tags:filecount=5,filesize=50m
第四步:採用虛擬執行緒處理計算任務虛擬碼如下
// 只需要更改ExecutorService的實現類
ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
List<CompletableFuture<Void>> completableFutureList = combinerList.stream()
.map(task -> CompletableFuture.runAsync(() -> {
// xxx 業務邏輯
}, executorService))
.toList();
completableFutureList.stream()
.map(CompletableFuture::join) //用join阻塞獲取結果
.toList();
僅需 30 分鐘即可完成 JDK 升級,現在讓我們一起來看看線上升級後的效果吧。
升級後效果
備註:由於容器限制,同配置的容器升級到 JDK21 後 JVM 堆記憶體容量比升級前少 20%。
先給出結論:
JDK21 配合使用 ZGC 效能提升非常明顯,雖然 GC 次數出現翻倍現象但 ZGC 的停頓時間達到微妙級別,吞吐量提高了不少。
8c32g 機器使用 ZGC 後,各叢集平均 CPU 利用率下降 10+%。
七
結語
通過上述的最佳化,在 2023 年全年資料量增長 4 倍的情況下,實際成本僅增加了 75%,而流量每增加一倍,實際成本只增加 20%。儘管這套最佳化方案已經很好地應對了流量翻倍的情況,但我們也注意到水平擴充套件能力有待提高。每個資料鏈路都需要提前按照預定的路由策略進行分組,一旦某個分片過載,就需要手動進行調整,比如擴充套件分片、擴增機器、增加執行緒等方式。在極端情況下,需要對每個服務都進行調整,這樣的配置維護與當前彈性資源的潮流有些不符合。因此,下一步我們需要面向彈性資源進行設計。
來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70027824/viewspace-3003969/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 得物新一代可觀測性架構:海量資料下的存算分離設計與實踐架構
- Spark+Hbase 億級流量分析實戰(日誌儲存設計)Spark
- DataLeap資料資產實戰:如何實現儲存最佳化?
- RocketMQ 多級儲存設計與實現MQ
- vivo 軒轅檔案系統:AI 計算平臺儲存效能最佳化實踐AI
- 億級流量系統架構之如何支撐百億級資料的儲存與計算架構
- AES實現財務資料的加密解密儲存加密解密
- StarRocks存算分離在得物的降本增效實踐
- Elasticsearch 億級資料檢索效能最佳化案例實戰!Elasticsearch
- 可計算儲存技術全面升級CSD 3000儲存體驗
- 存算分離實踐:JuiceFS 在中國電信日均 PB 級資料場景的應用UI
- 存貨庫存模型升級始末|得物技術模型
- 各種儲存效能瓶頸場景的分析與最佳化手段
- 我們NetCore下日誌儲存設計NetCore
- HarmonyOS Next關鍵資產儲存開發:效能最佳化與注意事項
- 談一談資料儲存與物聯網
- SpringCloud Alibaba實戰(3:儲存設計與基礎架構設計)SpringGCCloud架構
- 大模型儲存實踐:效能、成本與多雲大模型
- RocketMQ(十):資料儲存模型設計與實現MQ模型
- 得物社群計數系統設計與實現
- Spring Boot 揭祕與實戰(二) 資料儲存篇 – MongoDBSpring BootMongoDB
- Spring Boot 揭祕與實戰(二) 資料儲存篇 – MySQLSpring BootMySql
- 高效能 Java 計算服務的效能調優實戰Java
- Serverless 架構下的 AI 應用開發:入門、實戰與效能最佳化Server架構AI
- 億級流量系統架構之如何支撐百億級資料的儲存與計算【石杉的架構筆記】架構筆記
- Crust “方舟計劃”播報# 2|儲存總量34PB!Rust
- 容器化RDS—— 計算儲存分離 or 本地儲存
- MySQL如何實現萬億級資料儲存?MySql
- 實現多資料來源混合計算的效能最佳化方案之一
- 瀏覽器儲存密碼獲取與解密瀏覽器密碼解密
- 計算機補碼儲存計算機
- Spring Boot 揭祕與實戰(二) 資料儲存篇 – MyBatis整合Spring BootMyBatis
- 密集計算場景下的 JNI 實戰
- 計算機儲存器的容量計算和地址轉換計算機
- SpEL應用實戰|得物技術
- Java效能測試利器:JMH入門與實踐|得物技術Java
- 3-04. 實現箱子儲物空間的儲存和資料交換
- 阿里雲 PB 級 Kubernetes 日誌平臺建設實踐阿里