處理 PB 級別日誌高效可靠!去哪兒網分佈式追蹤系統實踐

作者介紹

王鵬

先後在電商、生活服務等行業從事基礎架構工作,對於大規模分佈式中間件有豐富的實踐經驗,2017 年加入去哪兒網,先後負責機票報銷憑證、核心訂單數據服務重構等工作,後轉到基礎架構負責 AIOPS 體系建設,包括 Trace 收集處理分析工作,AI 智能報警、鏈路分析以及日誌相關工作,爲業務運維提供快速、準確、高效的服務。

一、背景

隨着分佈式系統架構的普及,系統越來越複雜,常常被切分爲多個獨立子系統並以集羣方式部署在數十甚至成百上千的機器上。爲掌握系統運行狀態,確保系統健康,我們需要一些手段去監控系統,以瞭解系統行爲,分析系統的性能,或在系統出現故障時,能發現問題、記錄問題併發出告警,從而達到先於運營人員發現問題、定位問題。也可以根據監控數據發現系統瓶頸,提前感知故障,預判系統負載能力等。

在去哪兒內部,擁有 Watcher 監控體系(Watcher 包括業務線自定義監控指標,通用中間件指標 Knell 體系 )、報警體系(Watcher 自帶報警)、雷達(根據智能預測算法進行報警)以及日誌體系 (基於 ELK 的實時業務日誌體系以及離線日誌,和錯誤日誌分析系統 Heimdall),但是缺乏一個串聯整體的分佈式鏈路追蹤系統。在衆多開源的 APM 系統裏面我們選擇了自主研發,主要基於去哪兒網歷史技術框架以及 JavaAgent 技術的實現,在整個實施過程中解決了系統大數據量高併發的性能問題以及 Trace 中斷,和整個調用拓撲連通性的問題。

二、技術選型

在雲原生的可觀測性定義中包含了 Monitoring、Logging 以及 Tracing,如下圖 1 所示,這三部分構成了雲原生可觀測的三大基石。

圖 1 雲原生可觀測性的三大基石  

從圖 1 中可以看出,APM 體系是貫穿於整個雲原生開發運行過程中的,是對系統最直觀的感受,那麼我們要建設一套 APM 系統應該如何選型呢?

圖 2 目前技術棧

2.1 監控選型

監控選擇 Prometheus 和 Grafana 的組合,這個組合已經在業界非常流行,只需要做一些內部的適配,比如和內部的系統關聯,指標關聯等等。

2.2 日誌選型

在日誌這部分也是有很多選擇的,從 ELK 體系到 Loki 有很多可以選擇的組件,這裏更多的問題可能是數據量的問題,因爲每天海量日誌的傳輸存儲需要耗費大量的資源,包括存儲、傳輸以及處理分析資源。這塊的技術選型需要根據公司產生日誌大小以及重要性進行分級處理,比如實時日誌可以存儲到 ES 或者 Clickhouse ,一些非重要的日誌可以壓縮存儲到 HDFS 。

2.3 Trace 選型

國內比較火的框架有 SkyWalking,是華爲開源的一款 APM 工具,架構簡單界面優雅,採用 Agent 插裝的模式可以不改動現有框架代碼自動增加 Trace 相關功能,非常適合一些體量中小的公司,因爲這部分公司大量的採用了開源框架,而且數據量不是特別大,存儲也可以有很多選擇,SkyW    alking 適配了非常多的第三方存儲。

Jaeger 是國外比較火的一款 Tracing 工具,可以展示相關的調用鏈路,有一些簡單分析,也是採用了 Agent 插樁的模式,不需要改動代碼就可以實現 Tracing 的功能,整體來說各大 APM 工具的基本功能都是很全面的,更多的需要結合公司的一些技術棧來選擇,在去哪兒這邊, Java 是使用最多的語言,其他還有 Python 和 Go,另外在歷史的某個時段,也做過很多中間件的人工插樁,自研的分佈式 RPC 調用框架,以及 QMQ,分佈式配置系統 Qconfig 以及分佈式任務調度系統 Qschedule 都是進行過改造。基於這種情況,我們需要做的是覆蓋一些常用的開源插件,就可以完成整個插樁工作。

三、架構設計

圖 3 架構設計 

基於技術選型階段的分析,Trace 這部分採用自研中間件人工手寫代碼插樁,其他開源中間件使用 Java Agent 插樁模式,這樣可以快速構建 Trace 的記錄能力,數據傳輸層採用了 Apache Flume 日誌收集組件以及 Kafka 作爲我們的數據傳輸中介,使用 Flink 作爲整體接受以及處理分析的框架,最終將數據存儲到 HBase 中。展示界面採用了自研的 WEB UI,前端採用了 React 框架,展示調用拓撲結構,調用鏈路,異常日誌和分析結果。

四、數據流圖

圖 4 數據流程圖 

4.1 Trace 日誌打印和收集上報

Agent 部分主要負責日誌的打印和收集上報,這部分分爲兩個中間件,一個是用於產生 Trace 的中間件,另外一個是上報的中間件,目前產生 Trace 的中間件是包裝現有的開源組件以及部分採用 Agent 的動態插樁實現。上報的 Agent 採用了 Apache 的 Flume ,對於 Flume 進行部分改造,支持日誌輪轉不丟日誌,針對行級別收集,以及在應用中心下發配置,配置收集不同的日誌,不僅可以上報 Trace 的日誌,還可以實時收集其他日誌。

4.2 日誌上傳

日誌上傳主要通過 Kafka 中間件,目前公司的所有的機器上報日誌都是通過公共的 kafka 中間件上傳,最終存儲方式會有所不同,Trace 日誌經過 Flink 進行聚合,得到拓撲、和聚合的結果。聚合的結果主要包括失敗的 Trace,超時的 Trace 等等,拓撲的數據主要是整個調用拓撲結構。

4.3 監控上報

監控部分的上報主要是通過 Watcher 的 Agent 進行,Metrics 會在 Trace 經過的地方進行採樣和埋點,這樣在 Metrics 在上報之後,可以通過這種關聯關係,查詢某個時間點的 Metrics,以獲取相對應的 Trace 信息,監控數據會推送到 Watcher 系統進行展示,同時報警配置也在 Watcher 中。

4.4 UI 展示部分

可以通過存儲到 HBase 和 Mysql 的數據查詢出來具體的 Trace 信息以及相關的拓撲結構,通過聚合結果可以查詢到具體的錯誤量失敗率,耗時最大的 Span 等等信息。

4.5 關聯日誌展示部分

這部分數據主要存儲 Clog 中,目前已經轉移到數據組統一的 ELK 平臺進行存儲和查詢關聯,日誌數據格式通過 Logback 配置後,輸出指定的格式,頭部有 Trace 信息,通過這種關聯可以將有問題的 Trace 快速關聯到日誌,進行排查。

五、落地的問題以及解決方案

技術選型和架構確定後,面對兩大問題:trace 中斷問題以及 trace 聯通性問題。

5.1 Trace 中斷問題–Flume 的問題

Trace 中斷問題:在一個 Trace 產生的過程中,這條鏈路會經歷很多節點,雖然整個 Trace 的上下文傳遞下去,但是出現了中間節點的丟失情況。例如:Traceid : t_123  SpanId:1 (初始值) 如果調用深度增長,會變爲 1.1  1.1.1 等,但是在這個過程中出現了丟失現象。由此可以構建出來一個 Trace 的中斷率的公式:

intterrupt_rate = interrupt_span_ount / total_span_cout , 其中 interrupt_span_count 是通過其上下文中 span 的 parent 和 child 是通過計算得到的,因爲有 child 必然存在 parent。

5.1.1 現象以及問題分析

圖 5 日誌

從構建的中斷失敗率率指標可以看出,採樣的 Trace 的丟失率非常高可以達到 80%,那麼是什麼原因導致的呢?整理了一下高失敗率的原因,並進行了統計,結果如下表:

mN8f7Q

 圖表 6 統計終端產生原因

這個組件的問題佔了 50% 以上,繼續分析每個 case 後發現核心問題是 Flume 性能跟不上日誌的寫入速度。

通過分析發現丟數據的問題主要發生在異步寫數據的過程中,因爲後續 Sink 過程中發送的速率跟不上寫入的數據量,導致了內存隊列日誌的堆積,堆積到一定程度就需要丟棄數據,這樣導致了數據無故消失,造成了最開始的現象,只要我們對內存隊列的大小做擴容,以及將 Sink 改爲異步發送,並擴展 TailRead 的併發,就可以提升性能。但是這樣做雖然性能提升上來了,這給 Flume 帶來了巨大的壓力,大量的日誌需要從 Flume 組件上傳,但是不能無限制增加其內存,日誌收集組件的內存限制是配置 JVM 參數實現的,在內存受限的情況下,大量的日誌傳輸會造成頻繁的 OOM 錯誤,可以從圖 7 中的 OOM 監控顯示中看到,整個 OOM 是非常嚴重的,這也導致了大量的數據傳輸丟失。

 圖 7 寫入速度超過生產速度導致的 OOM 監控

5.1.2 OOM 的問題解決 -- 滑動窗口限流

如何解決內存不夠用的情況呢?增加內存顯然不現實,不能佔用過多系統內存,保證業務進程的使用。還要讓日誌穩定的上傳上來——滑動窗口限流。

圖 8 限流

通過對整個傳輸過程做動態限流,將數據佔用的內存容量控制在合理的範圍內即可,圖 8 中通過 SlidingWindow 限制一個批次的大小,以及條數大小,以及單條日誌的大小,這裏爲啥要限制單條大小,已經有全部的大小限制了。因爲有些日誌就一條就可以耗盡你的內存,必須將這些日誌進行截斷,要不然會頻繁的 OOM。通過這些限制,我們就能夠精確控制傳輸組件的流量,以確保不出現 OOM 的情況。

5.1.3 堆外內存溢出如何解決?

但是事情往往不是想的那麼簡單,突發情況!

圖 9 出現問題

在部署整個日誌收集組件的過程中,發生了一次故障,業務系統的進程突然被 Kill,這種情況迅速波及了其他服務,導致了一次 “雪崩” 事件,爲什麼我們部署了這個服務會影響到業務應用呢?而且我們明明做了內存限制?在觸發問題的系統中,我們發現了奇特的現象。

圖 10 CPU 佔比過高

可以看到進程號爲 17800 的日誌收集進程竟然佔用了 197% 的 CPU 資源,這臺機器已經是業務應用下線的機器,CPU 使用率本身不高,經過查看其他機器的 Flume 資源使用情況,發現佔用了大量的堆外內存資源,原因是需要發送大量 kafka 消息,這些消息會緩存在堆外內存中去,大量的堆積會導致整個的系統內存不足,最終 Linux 系統會 Kill 掉佔用內存最大的進程,毫無例外都是業務的應用進程,至此算是瞭解其中原因,如何解決呢?

就是要對 Flume 進程做資源控制,CGroup 技術就是爲此而生,通過爲 Flume 進程設定 CGroup 資源組,可以控制日誌收集進程的資源使用情況,限制其過度使用內存和 CPU 資源。最終整體失敗率從 80% 降低到 20% 左右達到了預想的效果。

 圖 11 失敗率降低

5.2 Trace 中斷問題–Kafka 的問題

在解決完日誌上傳的各種類型問題後,數據傳輸和處理的性能瓶頸逐步展現出來,首先是 Kafka 的性能瓶頸。出現的現象是在日常運行過程中, kafka 集羣會出現劇烈的抖動,如圖 12 所示。

圖 12 kafka 空閒線程數急劇下降

通過監控發現整個集羣的網絡空閒鏈接急劇下降,客戶端連接數也急劇下降,導致大量的客戶端連接超時。通過分析整個 Kafka 的處理流程,我們發現當網絡空閒連接急劇下降的同時,Kafka 的寫入進程耗時嚴重上漲。

圖 13 kafka 工作原理圖

從圖 13 中可以看到紅色標記的部分是 Processor 的數量大量降低,原因是在 RequestChannel 中處理任務的 KafkaRequestHandler 性能下降導致,這個 Handler 主要負責寫入數據和索引,當磁盤 IO 達到機器性能瓶頸時,就會導致這種情況。因此,我們就需要優化寫入性能,分散更多的 Partition 以及寫入慢的機械硬盤升級爲 SSD。

通過這些調整從以前的數據處理不穩定狀態轉爲平穩狀態。

圖 14

5.3 Trace 中斷問題–Flink 背壓

Flink 處理模塊主要是處理所有的數據,包括解析,分析、根據各種維度的聚合,需要聚合相關的拓撲以及各種維度的索引。

由於 Trace 的數據量非常大、平均 QPS 可以到 300w,在數據處理模塊中,最常見的問題就是背壓。何爲背壓?顧名思義就是在數據流從上游生產者向下遊消費者傳輸的過程中,上游生產速度大於下游消費速度,導致下游的 Buffer 溢出,這種現象就叫做背壓。

背壓出現的原因是下游任務處理能力不足,如何優化背壓?

圖 15 背壓

1.SubTask 是否消費均勻,Yarn 集羣分配資源是靜態分配,這會導致運行期資源不足。爲什麼需要看子任務是否處理數據均勻?

如果在分配任務的過程中數據存在傾斜,數據不均勻,會導致整體任務處理緩慢,部分算子背壓嚴重,嚴重影響整個集羣的處理速度。例如在這圖中的 Trace Filter 到 Sink 的過程中,會出現背壓,原因可能是數據不均勻,或者本身存儲數據的緩慢導致處理速度跟不上,上游的生產速度所以產生背壓。遇到這種情況,需要調整內存大小以及觀察 Sink 性能,除此之外還需要關注算子的計算是否均衡。如果不均衡需要看本身的 Hash 算法是否能夠保證 Trace 數據均勻分配,以及不同的 Task 的資源搶佔情況,綜合調配。

  1. 算子的 Input 和 Output 是否一致,內存是否充足。

發佈 Flink 任務時,需要設置 JVM 內存大小,JVM 參數需要根據上下游的算子數量以及傳輸數據量大小進行配置,配置過小就會導致堆積和背壓。算子的內存大小是也需要根據當前存儲的 BathSize 去預估,需要留有一定的 Buffer,防止 OOM 發生。

  1. 使用內存 Map 替代聚合 Window 。

很多情況下,只需要緩存部分數據,則可以採用內存 map ,性能會大大超過 聚合 window 的性能。但是存在丟失數據的風險,需要根據情況確定。

4.Filter 一定小心下游算子的擁堵導致全面的擁堵,壓縮算子傳遞數據,使用 ShargeGroup 共享 JVM。上下游的算子如果配置 ShareGroup 則可以共享 JVM,這樣避免多餘的網絡傳輸,提升整體的執行效率。

5.4 Trace 聯通性問題

何爲 Trace 聯通性?一個調用拓撲其實是一種圖,這種圖可能是有向無環圖 ,也有可能是有向有環圖(存在 QMQ 的回調導致的環裝調用),如果是有向無環圖的情況,即無法從某個頂點出發經過若干條邊回到該點,對於真實的 Trace 來講,有一種情況可能會打斷這種圖結構變爲,多個圖結構。

圖 16 跨線程中斷       

圖中展示的調用在 1.2 → 1.2.1 的過程中出現了跨線程或者跨進程,導致上下文丟失,最終一個新的調用拓撲產生,但是本身他們應該隸屬於同一個拓撲結構,或者圖結構。解決聯通性的問題核心思路就是:保證跨進程或者跨線程保證 Trace 上下文信息傳遞。

5.4.1 跨進程解決方案 - 中間件自編碼

跨進程的解決方案主要依賴於對於內部中間件的硬編碼實現。支持內部中間件體系 (Dubbo QMQ QunarAsyncHttpClient QunarHttpClient Qschedule Qconfig)、常見的開源通信中間件 (Apache http client 3 &4  版本  okhttpclient  ),業務常用組件基本都已經支持列表如下:

igLhM0

5.4.2 跨線程解決方案 --JavaAgent 自動插樁       

對於業務中經常使用的異步編程,採用 Java Agent 探針的模式來支持,內部跨線程問題是導致中斷的主要原因,在 Qtrace Agent 裏面已經全面解決,包括 Jdk 提供的 Runnable Callable ExecuteService  Future Rxjava Reactor java 等跨線程問題,通過 Agent 插樁的方式來彌補丟失的上下文信息。

sSNSq6

如果沒有 Qtracer.wrap 的話,及時有 QtraceAgent 都會中斷

CompletableFuture<Integer> future = CompletableFuture.supplyAsync(new QTraceSupplier<>(()->{
    LOG.info("supplyAsync------"+QTraceClientGetter.getClient().getCurrentTraceId());
    return 1;
}));
  Integer i = future.get();
  LOG.info(String.valueOf(i));
  CompletableFuture<Void> future1= CompletableFuture.runAsync(QTracer.wrap(()->{
      LOG.info("runAsync------"+QTraceClientGetter.getClient().getCurrentTraceId());
  }));
  future1.get();
  executor.submit(QTracer.wrap(() -> {
      LOG.info("in lambda------"+QTraceClientGetter.getClient().getCurrentTraceId());
  }));
  executor.submit(new Runnable() {
      @Override
      public void run() {
          LOG.info("in lambda------"+"in runnable"+QTraceClientGetter.getClient().getCurrentTraceId());
      }
  });

5.4.3 JavaAgent 性能

如果想將 JavaAgent 插樁模式應用在所有的系統上,則性能評測數據,是一個重要的指標,性能問題主要體現在使用 Agent 後,請求耗時、吞吐量是否收到影響。首先明確幾個概念:

5.4.3.1 對於 HTTP 請求的影響

總結:對於 HTTP 請求通過 JavaAgent 插樁後,超過 50ms 的請求,整體吞吐量最多降低 4%,耗時增加最多 4%。

5.4.3.2 對於跨線程的請求影響

結論:對於跨線程的場景,耗時在 50ms 以上的平均吞儲量降低 3% 左右,平均耗時增加 3% 左右。

對於業務線來說,正常的請求一般都是超過 50ms 左右,所以 Agent 對於整體性能的影響不是特別大,針對於某些對於性能要求嚴苛的場景,則採用了編碼的方式來解決,比如訪問 redis,訪問 db 操作等。從功能和性能兩方面驗證了 Agent 的可行性,隨着全面的部署 Agent,整個 Trace 的連通性得到了極大提高,從以前的 20% 左右的聯通度,到現在 80% 以上的聯通度。基本解決了跨線程和跨進程斷掉的問題。爲整個業務拓撲完整性構建打下堅實基礎,也爲後續的全鏈路壓測以及混沌工程提供堅實的基礎。

六、總結

整個分佈式鏈路追蹤系統,從技術選型、架構設計以及落地的過程中,遇到了很多結構性問題和性能問題,從問題分析到定義指標最終解決問題,積累了很多經驗,不通類型的問題都是可以通過定義指標、數字化指標、分析問題、逐步拆解、最終分治解決,希望對大家在建設 APM 系統的過程中有所幫助。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/TClD7_9x68EyxPsN1p2LhQ