解密得物Trace2.0:日PB級(jí)數(shù)據(jù)量下的計(jì)算與存儲(chǔ)性能優(yōu)化實(shí)戰(zhàn)
目錄
一、背景
二、客戶端多通道協(xié)議
1. 采集多通道協(xié)議
三、計(jì)算模型
四、數(shù)據(jù)壓縮
五、存儲(chǔ)方案
六、升級(jí) JDK21
1. 升級(jí)后效果
七、結(jié)語(yǔ)
一
背景
Trace2.0 是得物監(jiān)控團(tuán)隊(duì)引入 OpenTelemetry 協(xié)議并落地的全新應(yīng)用監(jiān)控系統(tǒng),從 2021 年底正式開(kāi)始使用。在過(guò)去的兩年里,我們面臨著數(shù)據(jù)量呈爆炸式增長(zhǎng)的巨大挑戰(zhàn)。然而,通過(guò)對(duì)計(jì)算和存儲(chǔ)的不斷優(yōu)化,我們成功地控制了機(jī)器數(shù)量的指數(shù)級(jí)增加。我們每天處理的日增數(shù)據(jù)量數(shù) PB(相比去年增長(zhǎng)了 4 倍),每天產(chǎn)生的 Span 數(shù)超過(guò)了數(shù)萬(wàn)億條。系統(tǒng)面對(duì)的峰值流量可達(dá)到每秒幾千萬(wàn)行 Span,每秒上報(bào)的帶寬壓縮后高達(dá)數(shù)十 GB。我們所使用的存儲(chǔ)引擎 Clickhouse 單機(jī)支持每秒近百萬(wàn)行的寫(xiě)入量。這些數(shù)據(jù)成為 Trace2.0 作為一款強(qiáng)大的應(yīng)用監(jiān)控系統(tǒng)的標(biāo)志,為監(jiān)控團(tuán)隊(duì)提供了全方位的監(jiān)控?cái)?shù)據(jù)分析能力。Trace2.0 使得我們能夠及時(shí)發(fā)現(xiàn)和解決潛在的系統(tǒng)問(wèn)題,確保我們的服務(wù)能夠始終穩(wěn)定可靠地運(yùn)行。
下面是整體的架構(gòu):

二
客戶端多通道協(xié)議
在 OpenTelemetry 中,客戶端會(huì)生成調(diào)用鏈信息并將其推送到遠(yuǎn)程服務(wù)器。 傳輸數(shù)據(jù)的請(qǐng)求協(xié)議通常包括 HTTP 和 gRPC。 gRPC 是基于 Google 開(kāi)發(fā)的高性能開(kāi)源 RPC 框架,使用二進(jìn)制格式傳輸數(shù)據(jù)。 它具有較高的性能和較低的網(wǎng)絡(luò)開(kāi)銷(xiāo),適用于大規(guī)模應(yīng)用和高并發(fā)場(chǎng)景。 gRPC 還提供自動(dòng)化的數(shù)據(jù)序列化和反序列化,以及強(qiáng)類(lèi)型的接口定義。
在 OpenTelemetry 中,默認(rèn)使用的是 gRPC 協(xié)議進(jìn)行上報(bào)。在 gRPC 中,使用長(zhǎng)連接進(jìn)行通信。然而,長(zhǎng)時(shí)間的連接可能會(huì)導(dǎo)致一些問(wèn)題,如服務(wù)器上的資源泄漏、連接狀態(tài)不穩(wěn)定或服務(wù)端單機(jī)負(fù)載過(guò)高。通過(guò)設(shè)置 maxConnectionAge 參數(shù),可以限制連接的持續(xù)時(shí)間,確保不會(huì)因?yàn)殚L(zhǎng)時(shí)間的連接而出現(xiàn)這些問(wèn)題。
NettyServerBuilder.forPort(8081).maxConnectionAge(grpcConfig.getMaxConnectionAgeInSeconds(), TimeUnit.SECONDS).build();
隨著數(shù)據(jù)量的快速增長(zhǎng),我們采用了基于負(fù)載均衡器(SLB)的方式 來(lái)實(shí)現(xiàn)后端機(jī)器的負(fù)載均衡。 然而,隨著全量 Trace下超高流量需求的增加,單個(gè) SLB 的帶寬已經(jīng)無(wú)法滿足要求。 為解決這個(gè)問(wèn)題,我們決定增加 SLB 數(shù)量,每個(gè)后端服務(wù)器開(kāi)啟多個(gè)端口,并使每個(gè) SLB 實(shí)例綁定一個(gè)端口。 這樣通過(guò)水平擴(kuò)展 SLB,可以改善負(fù)載分擔(dān)。

然而,隨著 SLB 數(shù)量的增加,維護(hù)成本也隨之增加,并且仍然可能導(dǎo)致某個(gè)后端服務(wù)器負(fù)載較高,形成熱點(diǎn)問(wèn)題。為了解決這個(gè)問(wèn)題,我們做出了一個(gè)決定——去除 SLB,直接將流量分擔(dān)到后端服務(wù)器上。這樣做不僅可以簡(jiǎn)化系統(tǒng)架構(gòu),還可以更均衡地分配負(fù)載,提高整體性能。
采集多通道協(xié)議
-
服務(wù)注冊(cè)和心跳:服務(wù)端啟動(dòng)后,會(huì)向控制平面注冊(cè)服務(wù)信息,并定時(shí)發(fā)送心跳來(lái)進(jìn)行健康檢查。如果服務(wù)端在一定時(shí)間內(nèi)沒(méi)有進(jìn)行心跳上報(bào),控制平面將把其剔除。
-
定時(shí)拉取服務(wù)列表:客戶端通過(guò)和控制平面進(jìn)行通信,定時(shí)獲取最新的服務(wù)端實(shí)例信息。通過(guò)這種方式,客戶端可以獲得最新的服務(wù)端列表,以保證與可靠的后端實(shí)例進(jìn)行通信。
-
多通道協(xié)議:在多通道協(xié)議中,不再使用負(fù)載均衡器,而是直接將請(qǐng)求發(fā)送到多個(gè)后端服務(wù)器上。每個(gè)后端服務(wù)器都可以獨(dú)立處理請(qǐng)求,實(shí)現(xiàn)流量的均衡負(fù)載,提高系統(tǒng)性能,并且減輕熱點(diǎn)問(wèn)題的影響。
-
提高系統(tǒng)性能:通過(guò)直連后端服務(wù)器,可以充分利用每個(gè)服務(wù)器的計(jì)算能力和帶寬,從而提高整個(gè)系統(tǒng)的性能和吞吐量。
-
減少維護(hù)成本:去除了負(fù)載均衡器,減少了系統(tǒng)的維護(hù)成本,避免了負(fù)載均衡器成為性能瓶頸的問(wèn)題。
-
避免熱點(diǎn)問(wèn)題:直連后端服務(wù)器并分擔(dān)流量的方式可以減輕系統(tǒng)中可能出現(xiàn)的熱點(diǎn)問(wèn)題,提高系統(tǒng)的穩(wěn)定性和可靠性。
三
計(jì)算模型
Trace2.0 后端的整體架構(gòu)參考 Pipeline 架構(gòu)。 在這個(gè)架構(gòu)中,消息的采集會(huì)被放到隊(duì)列里進(jìn)行處理,處理之后再進(jìn)行存儲(chǔ)。 整個(gè)計(jì)算程序采用 Source、Processor、Sink 多管道多任務(wù)處理方式,下面是詳細(xì)的流程:

component:source:kafka:- name: "otelTraceKafkaConsumer" ## Trace消費(fèi)topics: "otel-span"consumerGroup: "otel_storage_trace"parallel: 1 # 消費(fèi)的線程數(shù)servers: "otel-kafka.com:9092"targets: "decodeProcessor"processor:- name: "decodeProcessor"clazz: "org.poizon.apm.component.processor.DecodeProcessor"parallel: 4targets: "filterProcessor"- name: "filterProcessor"clazz: "org.poizon.apm.component.processor.FilterProcessor"parallel: 2targets: "spanMetricExtractor,metadataExtractor,topologyExtractor"- name: "spanMetricExtractor"clazz: "org.poizon.apm.component.processor.SpanMetricExtractor"parallel: 2props:topic: "otel-spanMetric"targets: "otel_kafka"- name: "metadataExtractor"clazz: "org.poizon.apm.component.processor.MetadataExtractor"parallel: 2props:topic: "otel-metadata"targets: "otel_kafka"- name: "topologyExtractor"clazz: "org.poizon.apm.component.processor.MetadataExtractor"parallel: 2props:topic: "otel-topology"targets: "otel_kafka"sink:kafka:- name: "otel_kafka"topics: "otel-spanMetric,otel-metadata,otel-topology"props:bootstrap.servers: otel-kafka.com:9092key.serializer: org.apache.kafka.common.serialization.ByteArraySerializervalue.serializer: org.apache.kafka.common.serialization.ByteArraySerializercompression.type: zstd
-
客戶端的 Trace 數(shù)據(jù)發(fā)送到服務(wù)端 OTel Server 后,根據(jù)應(yīng)用的 AppName 發(fā)送到不同的 Kafka Topic 中。
-
接收到數(shù)據(jù)后,數(shù)據(jù)會(huì)經(jīng)過(guò)反序列化、清洗、轉(zhuǎn)換等模塊的處理。
-
為了實(shí)現(xiàn)更高效的任務(wù)處理,系統(tǒng)選擇了使用 Disruptor 緩沖隊(duì)列。這個(gè)緩沖隊(duì)列采用了多生產(chǎn)者單消費(fèi)者的模式,可以有效地減少線程之間的競(jìng)爭(zhēng),提高系統(tǒng)的并發(fā)處理能力。
-
采用多任務(wù)多管道方式進(jìn)行處理,通過(guò)緩沖隊(duì)列將各個(gè)任務(wù)之間進(jìn)行解耦。

-
每個(gè)任務(wù)都會(huì)采用特定的路由策略,例如輪詢或哈希等,來(lái)確定該任務(wù)應(yīng)該處理的數(shù)據(jù)。
通過(guò)以上架構(gòu)和流程,系統(tǒng)能夠?qū)崿F(xiàn)高效的任務(wù)處理,減少線程競(jìng)爭(zhēng),并提高系統(tǒng)的并發(fā)處理能力。同時(shí),任務(wù)間的解耦和路由策略的應(yīng)用,使得系統(tǒng)能夠根據(jù)具體需求對(duì)數(shù)據(jù)進(jìn)行靈活的處理和分發(fā)。
四
數(shù)據(jù)壓縮
為了提高數(shù)據(jù)的合并壓縮比,我們采用了增加時(shí)間窗口并使用 keyBy 對(duì)數(shù)據(jù)進(jìn)行分組的方法,將 Span 轉(zhuǎn)換為 SpanList,并進(jìn)行批量合并操作。 這樣的流程中,我們無(wú)需事先將所有原始數(shù)據(jù)加載到內(nèi)存中,而是逐個(gè)或者分塊地將其寫(xiě)入到 ZstdOutputStream 中進(jìn)行實(shí)時(shí)壓縮處理。 壓縮后的數(shù)據(jù)也不會(huì)一次性保存在內(nèi)存中,而是通過(guò) OutputStream 逐個(gè)或者分塊地寫(xiě)入到 Kafka(或其他存儲(chǔ)介質(zhì))中。 這種采用 OutputStream 和 Zstd 進(jìn)行數(shù)據(jù)流式壓縮的方式,有效地提升了數(shù)據(jù)的壓縮率。
以下是壓縮核心代碼的示例:
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);}
通過(guò)線上數(shù)據(jù)觀察,我們發(fā)現(xiàn) Trace 索引數(shù)據(jù)的壓縮比提高了 5 倍,而 Trace 明細(xì)數(shù)據(jù)(使用ZSTD Level 3)的壓縮比更是提高了 17 倍。這意味著我們能以更低的存儲(chǔ)成本和更高的存儲(chǔ)效率來(lái)處理大量的監(jiān)控?cái)?shù)據(jù)。
五
存儲(chǔ)方案
面對(duì)如此大的數(shù)據(jù)量(全量 Trace),平衡成本并確保存儲(chǔ)系統(tǒng)如何支持如此高的 TPS 寫(xiě)入是業(yè)界關(guān)注的熱門(mén)話題。以下是一些優(yōu)化存儲(chǔ)方案的關(guān)鍵策略:
-
優(yōu)化存儲(chǔ)引擎配置,包括緩沖區(qū)大小、日志刷新策略等,以提高性能。
-
水平擴(kuò)展,采用分區(qū)和分片等技術(shù)對(duì)數(shù)據(jù)進(jìn)行分布式存儲(chǔ),以及采用分布式存儲(chǔ)引擎,如 Cassandra、HBase 等,來(lái)實(shí)現(xiàn)水平擴(kuò)展,提高寫(xiě)入吞吐量。
-
異步寫(xiě)入,采用消息隊(duì)列或異步處理來(lái)緩解寫(xiě)入壓力,提高系統(tǒng)的寫(xiě)入并發(fā)能力。
-
批量寫(xiě)入,通過(guò)批量寫(xiě)入來(lái)減少寫(xiě)入操作的次數(shù),減少對(duì)存儲(chǔ)層的壓力。
-
數(shù)據(jù)壓縮和索引優(yōu)化,采用高效的數(shù)據(jù)壓縮算法和合理的索引策略,以減少存儲(chǔ)空間占用和提高寫(xiě)入性能。
-
負(fù)載均衡和故障恢復(fù),合理設(shè)計(jì)負(fù)載均衡策略,并實(shí)施有效的故障恢復(fù)機(jī)制,以確保系統(tǒng)在寫(xiě)入壓力大時(shí)能夠保持穩(wěn)定和可靠。
-
監(jiān)控和性能調(diào)優(yōu),持續(xù)監(jiān)控系統(tǒng)的性能指標(biāo),進(jìn)行性能調(diào)優(yōu),及時(shí)發(fā)現(xiàn)和解決性能瓶頸。
來(lái)看看我們的架構(gòu)圖:

為了充分利用批量寫(xiě)入的優(yōu)勢(shì),數(shù)據(jù)在流入 Kafka 之前使用預(yù)定的路由策略將數(shù)據(jù)寫(xiě)入相應(yīng)的 Kafka 分區(qū),從而提高了寫(xiě)入 Kafka 的壓縮率。這樣做不僅可以減少網(wǎng)絡(luò)傳輸?shù)拈_(kāi)銷(xiāo),還可以進(jìn)一步提升存儲(chǔ)效率。
同時(shí),存儲(chǔ)服務(wù) OTel-Exporter 充分利用內(nèi)存進(jìn)行數(shù)據(jù)的“攢批”操作。他們將一個(gè) POD 專(zhuān)門(mén)處理兩個(gè) Kafka 分區(qū)的數(shù)據(jù)(實(shí)際根據(jù)各場(chǎng)景確定),這樣每個(gè) POD 可以獨(dú)占一個(gè)線程處理數(shù)據(jù),減少了線程之間的上下文切換和競(jìng)爭(zhēng)。當(dāng)內(nèi)存中的數(shù)據(jù)達(dá)到一定閾值時(shí),這部分?jǐn)?shù)據(jù)會(huì)被刷寫(xiě)到遠(yuǎn)端的存儲(chǔ) ClickHouse 中。
這種方式與面向列存儲(chǔ)引擎 ClickHouse 的低 TPS(每秒事務(wù)處理次數(shù))和高吞吐量寫(xiě)入特性非常契合。目前,他們的單機(jī) ClickHouse 每秒可支持超過(guò) 90 萬(wàn)行的寫(xiě)入吞吐量,這遠(yuǎn)遠(yuǎn)超過(guò)了 HBase 和 ES 的寫(xiě)入能力。
這種高效的數(shù)據(jù)寫(xiě)入與存儲(chǔ)策略不僅可以保證數(shù)據(jù)的快速處理和存儲(chǔ),還能夠節(jié)約成本并提高整體系統(tǒng)的性能。
六
升級(jí) JDK21
2023 年,公司內(nèi)部多個(gè)系統(tǒng)成功升級(jí)至 JDK 17,并且收獲了顯著的好處。相對(duì)于使用 JDK 8,JDK 17 在性能方面表現(xiàn)更高效。它能夠利用更少的內(nèi)存和 CPU 資源,從而提高系統(tǒng)性能并降低運(yùn)行成本。JDK 17 中包含了許多性能優(yōu)化的功能,包括改進(jìn)的 JIT 編譯器和垃圾回收器等。這些優(yōu)化措施明顯提高了應(yīng)用程序的性能。僅僅從 Java 8 升級(jí)到 Java 17,即使沒(méi)有其他改動(dòng),性能就直接提升了 10%。這主要得益于對(duì) NIO 底層的重寫(xiě)。在升級(jí)過(guò)程中,JVM 也伴隨著一系列相關(guān)的優(yōu)化措施,進(jìn)一步提升了系統(tǒng)性能。
同時(shí),JDK 19 推出了虛擬線程(也稱(chēng)為協(xié)程),以解決讀寫(xiě)操作系統(tǒng)中線程依賴內(nèi)核線程實(shí)現(xiàn)時(shí)帶來(lái)的額外開(kāi)銷(xiāo)問(wèn)題。最終,我們選擇升級(jí)到 JDK 21。
以 Trace2.0 后端計(jì)算程序?yàn)槔?,其采用的是基礎(chǔ)庫(kù),比如 Guava、Lombok、Jackson、Netty 和 Maven 進(jìn)行構(gòu)建。整個(gè)升級(jí)流程也相對(duì)簡(jiǎn)單,僅需以下四步:
第一步:指定 JDK 版本
<properties><java.version>21</java.version><maven.compiler.source>21</maven.compiler.source><maven.compiler.target>21</maven.compiler.target></properties>
第二步:引入 javax.annotation 程序包、升級(jí) 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 參數(shù)設(shè)置
-Xms22g -Xmx22g#開(kāi)啟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 的分配尖峰容忍度,數(shù)值越大越早觸發(fā)GC-XX:ZAllocationSpikeTolerance=5-XX:+UnlockDiagnosticVMOptions#關(guān)閉主動(dòng)GC周期,在主動(dòng)回收模式下,ZGC 會(huì)在系統(tǒng)空閑時(shí)自動(dòng)執(zhí)行垃圾回收,以減少垃圾回收在應(yīng)用程序忙碌時(shí)所造成的影響。如果未指定此參數(shù)(默認(rèn)情況),ZGC 會(huì)在需要時(shí)(即堆內(nèi)存不足以滿足分配請(qǐng)求時(shí))執(zhí)行垃圾回收。-XX:-ZProactive-Xlog:safepoint,classhisto*=trace,age*,gc*=info:file=/logs/gc-%t.log:time,tid,tags:filecount=5,filesize=50m
第四步:采用虛擬線程處理計(jì)算任務(wù)偽代碼如下
// 只需要更改ExecutorService的實(shí)現(xiàn)類(lèi)ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
List<CompletableFuture<Void>> completableFutureList = combinerList.stream().map(task -> CompletableFuture.runAsync(() -> {// xxx 業(yè)務(wù)邏輯}, executorService)).toList();
completableFutureList.stream().map(CompletableFuture::join) //用join阻塞獲取結(jié)果.toList();
僅需 30 分鐘即可完成 JDK 升級(jí),現(xiàn)在讓我們一起來(lái)看看線上升級(jí)后的效果吧。
升級(jí)后效果
備注:由于容器限制,同配置的容器升級(jí)到 JDK21 后 JVM 堆內(nèi)存容量比升級(jí)前少 20%。
先給出結(jié)論:
-
JDK21 配合使用 ZGC 性能提升非常明顯,雖然 GC 次數(shù)出現(xiàn)翻倍現(xiàn)象但 ZGC 的停頓時(shí)間達(dá)到微妙級(jí)別,吞吐量提高了不少。
-
8c32g 機(jī)器使用 ZGC 后,各集群平均 CPU 利用率下降 10+%。

七
結(jié)語(yǔ)

