應用監控系統演進:從選型到落地,鏈路追蹤一氣呵成

語言: CN / TW / HK

一、引言

隨著分散式系統和微服務的日益發展,系統的開發和運維對於可觀測性的需求越來越迫切。可觀測性[1]一詞的來源最初是從控制理論中借鑑而來的。目前我們在談論可觀測性的時候,我們通常是指以下三個方面:

  • 鏈路 Tracing

  • 指標 Metrics

  • 日誌 Logging

這三者並不完全是三個獨立的概念,而是相輔相成的。談及這三個方面,我們總是不得不提及Peter Bourgon的文章[2],以及其中最經典的Venn diagram:

二、收錢吧監控系統的歷史發展

收錢吧從在2017年開始逐步建設應用監控系統,系統建設主要的方向是提供鏈路追蹤(Tracing)以及效能監控(Metrics)兩方面的能力。

在監控系統的選型方面,我們儘量使用開源的系統:

  • Tracing

我們選擇的是twitter開源的 Zipkin [3],它為我們提供了鏈路追蹤的後端系統,使用Elasticsearch作為Tracing的後端儲存。

  • Metrics

我們在Tracing資料的基礎上,通過從Kafka中消費Zipkin格式的資料聚合得到分鐘級別的指標,時序資料簡單地使用MySQL作為後端儲存。

在接入層,我們採用最原始的方式,為各個Java的模組、元件提供各種各樣的instrumentation工具包來進行埋點,業務研發同學以pom依賴的形式引用到自己的業務服務中,比如:

  • 通過MySQL Driver提供的 攔截器機制 [4]來對MySQL資料庫的請求進行取樣;

  • 通過封裝一個新的JSON-RPC包來實現對RPC層的埋點;

  • 通過Spring的 HandlerInterceptor [5]來實現Rest風格介面的攔截;

  • 通過Spring AOP來進行Redis訪問的鏈路採集;

  • ...

這套系統支撐我們走過了業務發展最迅猛的一段時間,為大量的問題排查和故障診斷提供了一些線索,然而業務開發逐漸開始對這套系統產生不滿,主要集中在以下幾個方面,

1)由於我們在初期採用MySQL作為底層時序資料的儲存,這在當時看起來是一個主流的方案[6],但我們碰到了很大的效能問題,畢竟MySQL這類資料庫提供的 儲存引擎並沒有對此類場景進行優化 [7]。同時,MySQL並沒有提供豐富的針對時間序列的查詢運算元。

PgSQL 9.6.2 資料插入的吞吐量隨著表大小的變化關係[8]。

在鏈路追蹤或者說應用監控的場景,我們需要的是 高吞吐量以及線性的效能 [9],同時我們也需要增加資料的生命週期管理的功能:因為隨著新資料的寫入,歷史資料的價值會隨著時間的流逝而價值降低。

2)由於我們需要從Tracing資料反推得到指標資料Metrics,我們“魔改“了Zipkin傳輸部分的邏輯,對所有不採樣資料(Unsampled)在客戶端進行聚合以後批量上報,導致我們在Zipkin的升級方面產生了很大的困難。尤其是在https://github.com/openzipkin/zipkin/pull/1968,以後不再允許使用者定製開發服務端。

3)業務方升級依賴需要採集器元件升級支援,從而產生了額外的工作量。同時,也有大量的元件難以通過這種侵入性的方式進行支援,或者需要投入很大的人力成本來進行研發、適配。

三、新一代應用監控系統 - Hera

基於以上原因,我們決定研發一套新的系統來同時滿足幾個條件:

  • 低儲存成本: 能夠以低成本儲存較長週期的資料,對於指標能夠儲存至少四周,對於鏈路儲存一週,這讓我們排除了ElasticSearch這個選項;

  • 高實時查詢效能、高靈活度: 不再使用MySQL這類關係型資料庫作為時間序列的儲存,使用Prometheus或Prometheus相容的儲存系統;

  • 優化研發效率: 使用位元組碼編織技術,無侵入地進行埋點,並更緊密地與DevOps流程結合。

1、鏈路追蹤

分散式鏈路追蹤的概念和心智模型(Mental Model)大多是受到2010年發表的Google’s Dapper論文[10]的影響。在Dapper論文中,作者明確地指出了Trace的樹形結構:

We tend to think of a Dapper trace as a tree of nested RPCs.

以及提出了所謂Span的概念:

In a Dapper trace tree, the tree nodes are basic units of work which we refer to as spans. The edges indicate a casual relationship between a span and its parent span.

在一個Dapper鏈路樹中,各個Span之間存在因果和時序關係。

在鏈路追蹤的系統選型方面,我們對比了在當時比較活躍的幾個開源專案:

  • Zipkin

  • Apache/Skywalking v6.6.0

  • Jaeger v1.16

Jaeger是Uber[11]在2016年開源的鏈路追蹤平臺,並捐獻給了CNCF雲原生基金會。

Jaeger的主要元件和控制流、資料流示意圖,其中使用Kafka作為緩衝管道。

Jaeger受到了開源社群的廣泛支援,比如:

  • Istio[12]原生支援使用Jaeger增強Service Mesh服務網格的可觀測性;

  • 服務網格的資料面實現Envoy[13]支援使用Jaeger作為鏈路追蹤的服務提供方;

  • ...

1)鏈路追蹤後端系統和儲存的選型

我們重點考慮的是他們對於儲存系統方面的支援情況和擴充套件能力。

① 各個開源鏈路追蹤實現的儲存能力

Jaeger社群對於儲存的擴充套件性極佳,提供了基於gRPC的 外掛機制 [14],方便定製擴充套件。

+----------------------------------+                  +-----------------------------+
| | | |
| +-------------+ | unix-socket | +-------------+ |
| | | | | | | |
| jaeger-component | grpc-client +----------------------> grpc-server | plugin-impl |
| | | | | | | |
| +-------------+ | | +-------------+ |
| | | |
+----------------------------------+ +-----------------------------+




parent process child sub-process

在儲存的具體選擇方面,我們在當時注意到了Aliyun SLS能夠支援作為鏈路追蹤的後端,並且官方提供了一個實現https://github.com/aliyun/aliyun-log-jaeger,我們內部基於這個思路實現了gRPC外掛版本的SLS後端實現,目前穩定執行在生產環境。

  • 儲存週期: SLS能夠提供長達30天的儲存週期。

  • 儲存量: 一天儲存的Span數量超過4億,使用約6TB儲存空間。

  • 效能: 在SLS Query介面進行條件查詢可以在3-5s以內返回結果。

  • 成本: 每天成本約為70元,一年約2萬元左右(大約為2臺8U32G的ECS的按年付費的價格)。

Jaeger operator在 https://github.com/jaegertracing/jaeger-operator/pull/1517 中引入了對gRPC外掛的原生支援,gRPC外掛可以作為InitContainer[15]在啟動時將外掛的二進位制檔案複製到共享的EmptyDir儲存卷中。同時,我們也積極向社群反饋,向社群提供了gRPC外掛的自觀測功能(Self Observability):

  • aeger-grpc外掛支援opentracing上下文傳遞:https://github.com/jaegertracing/jaeger/pull/2870

  • go-plugin外掛支援引數配置: https://github.com/hashicorp/go-plugin/pull/168

2)業務方接入優化

SkyWalking 的美妙不僅在於其強大的功能,還在於 其優秀的程式碼實現 [16]。

在過去我們使用侵入性的方式提供應用監控接入,監控服務的提供方需要為各個業務方提供的外掛、模組,並且需要花費大量的精力來實現版本相容性等工作,這種方式缺乏統一的切面和工作機制,需要對各個元件逐個”攻破”。Skywalking是華為的吳晟等人在2015年開源的一款APM產品,併成為Apache的頂級專案,Skywalking-Java使用了位元組碼增強技術,提供了無侵入性的鏈路埋點,大大降低了使用成本。在Java中,常用的位元組碼工具有以下幾種。

ASM,BCEL屬於Low Level,而CGLib、Javassist和ByteBuddy更易用。

對於位元組碼技術的具體分析可以參考StackOverflow上的回答[17]。

其中ByteBuddy的易用性和效能都達到一流的水準:

ByteBuddy官方提供的效能測試結果。

為了充分利用Skywalking-Java提供的外掛,我們在OpenTracing的介面上實現了整套Skywalking鏈路追蹤的模型。具體來說,Skywalking的鏈路追蹤語義包括三層:

① Skywalking中的Trace與OpenTracing語義中的Trace類似

② Skywalking中的Span與OpenTracing語義中的Span類似

  • EntrySpan: 等價於OpenTracing中Kind=Consumer或者Server的Span;

  • ExitSpan: 等價於OpenTracing中Kind=Producer或者Client的Span;

  • LocalSpan: 不屬於上述兩者的其他型別。

③ Skywalking增加了一層Segment的概念

一個Segment被約束在一個執行緒上,其中包含的所有AbstractTracingSpan 都在此執行緒上建立和銷燬。這裡SegmentID對應於OT中的SpanID,在Skywalking中的Span 是按照建立的順序從0開始編號的。

當然模型上也有不同之處:

  • 跨執行緒

OpenTracing的標準要求實現者將Span 設計成執行緒安全的,因為Span允許被跨執行緒傳遞。而在Skywalking中,跨執行緒是通過對當前Segment進行快照[18]實現的,而Span 在絕大部分場景下不需要保證是執行緒安全的。

  • 非同步

非同步Span主要應用於記錄非同步操作真正的起始和結束時刻。以Spring Reactive為例[19]:使用者編寫的Controller返回的是一個可被執行任務(通常是Mono型別),而不是最後的結果,Dispatcher會將任務通過執行緒池去執行,那麼我們需要記錄的是真正這個請求從任務建立到被“計算“完成的整個週期。在OpenTracing標準中沒有提及這部分的實現。而Skywalking的多個外掛中使用了這個機制,比如Redis客戶端Lettuce,Spring Webflux,Apache AsyncHttpClient等。

我們通過在OpenTracing介面上實現與Skywalking一致的語義從而實現幾乎零成本地移植並使用它所有的外掛。我們在使用Skywalking-Java的過程中也發現了不少問題,也與社群積極地反饋,做出了一些貢獻,主要包括:

  • JSON日誌格式的實現:https://github.com/apache/skywalking/pull/5357

  • Spring Kafka 1.x外掛:https://github.com/apache/skywalking/pull/5879

  • Spring DevTools支援和多類載入器優化:https://github.com/apache/skywalking/pull/6973

  • Jedis Transaction支援:https://github.com/apache/skywalking-java/pull/57

3)服務依賴分析

服務的依賴分析在公司內部一直是業務開發迫切需要的功能,它在服務容量規劃、問題診斷和服務強弱性依賴判斷中都有比較實用的價值。在Jeager社群的實現中,推薦生產使用 Spark批處理 [20]的方式實現了全域性的依賴分析,也有 基於Flink的實時處理 [21],但已經沒有在維護狀態。

為了實現這個功能,我們使用了Apache Flink,通過消費Kafka中的鏈路資料,實時計算出服務之間的依賴關係,將Tuple<downsampled timestamp, caller, sub-caller, callee, sub-callee> 格式的資料通過OpenTSDB協議傳輸到我們的時序資料庫VictoriaMetrics 。

前端根據使用者提供的時間視窗,通過Java服務暴露的API進行上游/下游的查詢:

後續我們將在使用者互動和呼叫量的分析展示方面進行進一步的優化。

2、指標監控

在老版本的監控程式中,我們使用了關係型資料庫作為時序資料的儲存系統,使得我們在查詢的靈活性和效能方面遭遇到了很大的瓶頸,我們有必要在新系統設計的時候去進行一定的反思。在過去幾年中,雲原生的概念逐漸深入人心,而Prometheus是雲原生時代監控的事實標準。

在進行了一些調研之後,我們認為單機版本的Prometheus並不能支撐超過百萬級別活躍的指標和超過一週的資料儲存。 我們的目光主要聚焦到了Thanos、Cortex和VictoriaMetrics,在國內技術社群分享比較多的是Cortex和Thanos,但我們對比發現Cortex的架構非常複雜,對系統運維提出了新的挑戰,而Thanos也有一定的運維複雜性,且由於使用物件儲存(S3等)作為冷資料儲存,查詢可能存在一部分服務不可用導致返回部分資料。 同時,我們也發現國內的知乎在QCon 2020[22]上分享了他們使用VictoriaMetrics的經驗。 我們基於以下原因最終選擇了VictoriaMetrics。

  • VictoriaMetrics在各項效能測試[23]中都表現卓著;

  • 作者Aliaksandr Valialkin精於Go語言的效能優化,是fasthttp等高效能Go語言元件的作者;

  • 最重要的是VM的叢集架構簡單,易於運維。

VM的各個元件都是獨立的,可以水平擴充套件,只有核心的vmstorage是有狀態的,其他元件均是無狀態的。

1)推還是拉 (push or pull)

對於指標類的資料,採用主動推還是被動拉的模式,一直以來都是存在較大的爭議[24]。我們與Prometheus一樣使用推的模式,基於以下原因,

  • 服務發現

一個詬病Pull模式的原因是認為Pull模式需要大規模的服務發現,但這一問題在Kubernetes上反而不存在任何問題,我們藉助CRD[25]可以很輕易地實現服務抓取目標的定義。同時可以將Pod,Service上的標籤附加到指標上,幫助查詢的時候區分例項,服務所屬的業務團隊等。反而,這在Push模式中是不容易實現,或者需要業務研發去改造的。

  • 問題排查

當指標查詢失敗的時候,我們通常需要去判斷到底是哪一步出了問題。在Push模式中,我們需要去檢查業務的程式碼和日誌來判斷問題。然而在Pull模式,我們可以手動在瀏覽器中去請求指標暴露的介面(比如/metrics )就可以判斷服務的健康狀況,業務是否正常匯出指標。

目前我們使用VictoriaMetrics的一些統計資訊:

  • 儲存週期60天,共有6990億個資料點,佔用磁碟空間800GB;

  • 活躍時間序列約500萬,資料點插入的QPS約13萬每秒;

  • 範圍查詢的P99線平均值約為1.5s。

我們也自研了查詢面板,以限定查詢的時間範圍(最長3天)和查詢的模式(針對服務job查詢)。

我們自研了一些重要的指標外掛,其中在應用效能分析、故障定位中比較實用的維度有:

  • Servlet容器指標: Tomcat忙碌執行緒數,百分比;

  • 資料庫連線池指標: 支援HikariCP和Alibaba/Druid連線池,等待連線池的執行緒數,資料庫連接獲取時間,資料庫連線池使用佔比;

  • 快取指標: 支援Redis,Caffeine和EhCache。快取命中率;

  • Kubernetes監控: Pod CPU、記憶體使用量,我們也在系統中也集成了Kubernetes事件的檢視和搜尋。

由於公司部分核心服務還使用Docker部署在ECS上,我們在VictoriaMetrics中 實現了基於Dockerd API的服務發現機制 [26],也已經合併到社群版本。

3、全面擁抱雲原生

在2020年,Kubernetes已然成為了分散式作業系統的事實標準,公司內部的絕大多數服務也已經全面遷移到自建的Kubernetes叢集。為了更好的利用新特性,我們在2020年中啟動Kubernetes的叢集升級計劃,將叢集升級到1.16版本(目前已經升級到1.20),並遷移至阿里雲的ACK託管叢集。監控系統的落地將全面依賴於Kubernetes系統。

1)我們提供Docker映象版本的Java Agent,方便業務開發接入。

2)在生產環境,我們使用InitContainer[27]在容器啟動階段注入Java Agent,兩者之間通過貢獻的EmptyDir[28]來傳遞Agent Jar包。這便於我們在生產環境中靜默升級Agent版本:即使Agent在生產出現問題,我們可以快速修復問題,然後升級初始化容器即可。

3)時序資料庫VictoriaMetrics的運維和Jaeger元件的運維也是通過Kubernetes Operator實現的:

  • 我們對jaeger-ingester和jaeger-collector元件啟用了HPA,即基於CPU和記憶體使用率的水平動態擴容。

  • VictoriaMetrics叢集版本的各個元件也是通過Kubernetes operator[29]進行維護的。

四、展望

1、後取樣的實現

由於我們目前取樣的是頭取樣(Head-Based Sampling)方案,一旦在鏈路中間的服務發生丟擲異常且這條鏈路沒有被取樣,那麼就會出現有錯誤日誌和報警,但鏈路追蹤系統無法查詢到這條鏈路的情況,這給開發排查問題帶來很大的阻礙。目前,業界有幾種典型的實現方案,

1)OpenTelemetry方案

https://github.com/open-telemetry/opentelemetry-collector-contrib/pull/4958

OT社群的 tailsampling方案 [30]主要來自於 Grafana公司的貢獻 [31],同時可以利用以下幾個processor和exporter實現高伸縮性。

  • (第一層)loadbalancingexporter: 把屬於同一個TraceID的所有Trace和Log分發給一組固定的下游Collector;

  • groupbytraceprocessor: 等待足夠長的一段時間,將屬於一個TraceID的所有Span(s)打包傳遞到下游;

  • tailsamplingprocessor: 通過預定義的組合策略進行取樣。

2)位元組跳動方案

發生錯誤的服務將取樣決定強制進行翻轉,如果這條鏈路沒有進行取樣的話。但這樣的話會丟失取樣決策改變之前的所有鏈路以及其他分支鏈路的資料。

3)貨拉拉方案

基於Kafka延遲消費+布隆過濾器實現:

  • 實時消費佇列: 根據取樣規則寫入Bloom過濾器,熱資料全量寫入熱儲存。

  • 延遲消費佇列: 根據Bloom過濾器實現條件過濾邏輯,冷資料寫入冷儲存。

2、時間序列的異常檢測

時間序列的異常檢測一直是一個比較火的話題,尤其是針對具有時間週期特徵的資料。

1)Gitlab方案

Gitlab在2019年分享了他們基於Prometheus實現的簡單的異常檢測[32],比如我們想判斷 t 當前時間對應的值 f (t) ,我們可以根據前三週的資料的中位數通過最近一週的增量進行修正,得到當前時間的預測值 f ' (t)。

其中增量  offset   是指最近一週的指標的時間平均值與往前偏移offset 以後的時間平均值,比如  1w  是指最近一週的平均值與上一個週期的平均值之差(用PromQL表示為job:http_requests:rate5m:avg_over_time_1w - job:http_requests:rate5m:avg_over_time_1w offset 1w),用於補償週期之間的平均值變化。

3)其他的方案

  • Prophet - 攜程實時智慧異常檢測平臺實踐[33]

  • 外賣訂單量預測異常報警模型實踐[34]

從業界發展的大勢來看,通過大資料、AI手段對系統異常進行檢測也是大勢所趨。

> > > >

參考資料

  • [1]Observability

    https://en.wikipedia.org/wiki/Observability

  • [2]Metrics, tracing, and logging

    https://peter.bourgon.org/blog/2017/02/21/metrics-tracing-and-logging.html

  • [3]Zipkin

    https://zipkin.io/,

  • [4]Using the Connector/J Interceptor Classes

    https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-interceptors.html

  • [5]HandlerInterceptor

    https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/HandlerInterceptor.html

  • [6]What Database Engine Are You Using to Store Time Series Data?

    https://www.percona.com/blog/2017/02/10/percona-blog-poll-database-engine-using-store-time-series-data/

  • [7]Time-series data: Why (and how) to use a relational database instead of NoSQL

    https://www.timescale.com/blog/time-series-data-why-and-how-to-use-a-relational-database-instead-of-nosql-d0cd6975e87c/

  • [8]Time-series data: Why (and how) to use a relational database instead of NoSQL

    https://www.timescale.com/blog/time-series-data-why-and-how-to-use-a-relational-database-instead-of-nosql-d0cd6975e87c/

  • [9]面向分散式追蹤系統的儲存方案 https://mp.weixin.qq.com/s/wostd5_PdG9X-qpitPdA7A

  • [10]Dapper, a Large-Scale Distributed Systems Tracing Infrastructure

    https://research.google/pubs/pub36356/

  • [11]Evolving Distributed Tracing at Uber Engineering

    https://eng.uber.com/distributed-tracing/

  • [12]Istio/Jaeger

    https://istio.io/latest/docs/tasks/observability/distributed-tracing/jaeger/

  • [13]Jaeger tracing

    https://www.envoyproxy.io/docs/envoy/latest/start/sandboxes/jaeger_tracing.html

  • [14]Golang plugin system over RPC

    https://github.com/hashicorp/go-plugin

  • [15]Init Containers

    https://kubernetes.io/docs/concepts/workloads/pods/init-containers/

  • [16]SkyWalking 8.7.0 原始碼分析

    https://skywalking.apache.org/zh/2022-03-25-skywalking-source-code-analyzation/

  • [17]Dynamic Java Bytecode Manipulation Framework Comparison

    https://stackoverflow.com/questions/9167436/dynamic-java-bytecode-manipulation-framework-comparison

  • [18]ContextSnapshot

    https://github.com/apache/skywalking-java/blob/5607f87a54719baad002cb00248e250cbdaae69a/apm-sniffer/apm-agent-core/src/main/java/org/apache/skywalking/apm/agent/core/context/AbstractTracerContext.java#L42-L56

  • [19]Understanding Reactive types

    https://spring.io/blog/2016/04/19/understanding-reactive-types

  • [20]Spark job for dependency links

    https://github.com/jaegertracing/spark-dependencies

  • [21]Big data analytics for Jaeger using Apache  Flink

    https://github.com/jaegertracing/jaeger-analytics-flink

  • [22]演講:單機 20 億指標,知乎 Graphite 極致優化! https://qcon.infoq.cn/2020/shenzhen/presentation/2881

  • [23]Prominent features

    https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#prominent-features

  • [24]Pull doesn't scale - or does it? https://prometheus.io/blog/2016/07/23/pull-does-not-scale-or-does-it/

作者丨陸家靖

來源丨公眾號:SQB Blog(ID:gh_9916f2313b7a)

dbaplus社群歡迎廣大技術人員投稿,投稿郵箱: [email protected]