万字长文剖析 APM 系统?如何设计与实现?
-? ? ?前言? ? -
本文來說說什么是 APM 系統(tǒng),也就是大家平時說的監(jiān)控系統(tǒng),以及怎么實現(xiàn)一個 APM 系統(tǒng)。因為一些特殊的原因,我在文中會使用 Dog 作為我們的系統(tǒng)名稱進行介紹。
我們?yōu)?Dog 規(guī)劃的目標是接入公司的大部分應用,預計每秒處理 500MB-1000MB 的數(shù)據(jù),單機每秒 100MB 左右,使用多臺普通的 AWS EC2。
因為本文的很多讀者供職的公司不一定有比較全面的 APM 系統(tǒng),所以我盡量照顧更多讀者的閱讀感受,會在有些內(nèi)容上啰嗦一些,希望大家可以理解。我會在文中提到 prometheus、grafana、cat、pinpoint、skywalking、zipkin 等一系列工具,如果你沒有用過也不要緊,我會充分考慮到這一點。
本文預設的一些背景:Java 語言、web 服務、每個應用有多個實例、以微服務方式部署。另外,從文章的可閱讀性上考慮,我假設每個應用的不同實例分布在不同的 IP 上,可能你的應用場景不一定是這樣的。
-? ? ?APM 簡介? ? -
APM 通常認為是 Application Performance Management 的簡寫,它主要有三個方面的內(nèi)容,分別是 Logs(日志)、Traces(鏈路追蹤) 和 Metrics(報表統(tǒng)計)。以后大家接觸任何一個 APM 系統(tǒng)的時候,都可以從這三個方面去分析它到底是什么樣的一個系統(tǒng)。
有些場景中,APM 特指上面三個中的 Metrics,我們這里不去討論這個概念。
這節(jié)我們先對這 3 個方面進行介紹,同時介紹一下這 3 個領域里面一些常用的工具。
1、首先 Logs 最好理解,就是對各個應用中打印的 log 進行收集和提供查詢能力。
Logs 系統(tǒng)的重要性不言而喻,通常我們在排查特定的請求的時候,是非常依賴于上下文的日志的。
以前我們都是通過 terminal 登錄到機器里面去查 log(我好幾年都是這樣過來的),但是由于集群化和微服務化的原因,繼續(xù)使用這種方式工作效率會比較低,因為你可能需要登錄好幾臺機器搜索日志才能找到需要的信息,所以需要有一個地方中心化存儲日志,并且提供日志查詢。
Logs 的典型實現(xiàn)是 ELK (ElasticSearch、Logstash、Kibana),三個項目都是由 Elastic 開源,其中最核心的就是 ES 的儲存和查詢的性能得到了大家的認可,經(jīng)受了非常多公司的業(yè)務考驗。
Logstash 負責收集日志,然后解析并存儲到 ES。通常有兩種比較主流的日志采集方式,一種是通過一個客戶端程序 FileBeat,收集每個應用打印到本地磁盤的日志,發(fā)送給 Logstash;另一種則是每個應用不需要將日志存儲到磁盤,而是直接發(fā)送到 Kafka 集群中,由 Logstash 來消費。
Kibana 是一個非常好用的工具,用于對 ES 的數(shù)據(jù)進行可視化,簡單來說,它就是 ES 的客戶端。
我們回過頭來分析 Logs 系統(tǒng),Logs 系統(tǒng)的數(shù)據(jù)來自于應用中打印的日志,它的特點是數(shù)據(jù)量可能很大,取決于應用開發(fā)者怎么打日志,Logs 系統(tǒng)需要存儲全量數(shù)據(jù),通常都要支持至少 1 周的儲存。
每條日志包含 ip、thread、class、timestamp、traceId、message 等信息,它涉及到的技術(shù)點非常容易理解,就是日志的存儲和查詢。
使用也非常簡單,排查問題時,通常先通過關(guān)鍵字搜到一條日志,然后通過它的 traceId 來搜索整個鏈路的日志。
題外話,Elastic 其實除了 Logs 以外,也提供了 Metrics 和 Traces 的解決方案,不過目前國內(nèi)用戶主要是使用它的 Logs 功能。
2、我們再來看看 Traces 系統(tǒng),它用于記錄整個調(diào)用鏈路。
前面介紹的 Logs 系統(tǒng)使用的是開發(fā)者打印的日志,所以它是最貼近業(yè)務的。而 Traces 系統(tǒng)就離業(yè)務更遠一些了,它關(guān)注的是一個請求進來以后,經(jīng)過了哪些應用、哪些方法,分別在各個節(jié)點耗費了多少時間,在哪個地方拋出的異常等,用來快速定位問題。
經(jīng)過多年的發(fā)展,Traces 系統(tǒng)雖然在服務端的設計很多樣,但是客戶端的設計慢慢地趨于統(tǒng)一,所以有了 OpenTracing 項目,我們可以簡單理解為它是一個規(guī)范,它定義了一套 API,把客戶端的模型固化下來。
當前比較主流的 Traces 系統(tǒng)中,Jaeger、SkyWalking 是使用這個規(guī)范的,而 Zipkin、Pinpoint 沒有使用該規(guī)范。限于篇幅,本文不對 OpenTracing 展開介紹。
下面這張圖是我畫的一個請求的時序圖:
從上面這個圖中,可以非常方便地看出,這個請求經(jīng)過了 3 個應用,通過線的長短可以非常容易看出各個節(jié)點的耗時情況。通常點擊某個節(jié)點,我們可以有更多的信息展示,比如點擊 HttpClient 節(jié)點我們可能有 request 和 response 的數(shù)據(jù)。
下面這張圖是 Skywalking 的圖,它的 UI 也是蠻好的:
SkyWalking 在國內(nèi)應該比較多公司使用,是一個比較優(yōu)秀的由國人發(fā)起的開源項目,已進入 Apache 基金會。
另一個比較好的開源 Traces 系統(tǒng)是由韓國人開源的 Pinpoint,它的打點數(shù)據(jù)非常豐富,這里有官方提供的 Live Demo,大家可以去玩一玩。
最近比較火的是由 CNCF(Cloud Native Computing Foundation) 基金會管理的 Jeager:
當然也有很多人使用的是 Zipkin,算是 Traces 系統(tǒng)中開源項目的老前輩了:
上面介紹的是目前比較主流的 Traces 系統(tǒng),在排查具體問題的時候它們非常有用,通過鏈路分析,很容易就可以看出來這個請求經(jīng)過了哪些節(jié)點、在每個節(jié)點的耗時、是否在某個節(jié)點執(zhí)行異常等。
雖然這里介紹的幾個 Traces 系統(tǒng)的 UI 不一樣,大家可能有所偏好,但是具體說起來,表達的都是一個東西,那就是一顆調(diào)用樹,所以我們要來說說每個項目除了 UI 以外不一樣的地方。
首先肯定是數(shù)據(jù)的豐富度,你往上拉看 Pinpoint 的樹,你會發(fā)現(xiàn)它的埋點非常豐富,真的實現(xiàn)了一個請求經(jīng)過哪些方法一目了然。
但是這真的是一個好事嗎?值得大家去思考一下。兩個方面,一個是對客戶端的性能影響,另一個是服務端的壓力。
其次,Traces 系統(tǒng)因為有系統(tǒng)間調(diào)用的數(shù)據(jù),所以很多 Traces 系統(tǒng)會使用這個數(shù)據(jù)做系統(tǒng)間的調(diào)用統(tǒng)計,比如下面這個圖其實也蠻有用的:
另外,前面說的是某個請求的完整鏈路分析,那么就引出另一個問題,我們怎么獲取這個“某個請求”,這也是每個 Traces 系統(tǒng)的不同之處。
比如上圖,它是 Pinpoint 的圖,我們看到前面兩個節(jié)點的圓圈是不完美的,點擊前面這個圓圈,就可以看出來原因了:
圖中右邊的兩個紅圈是我加的。我們可以看到在 Shopping-api 調(diào)用 Shopping-order 的請求中,有 1 個失敗的請求,我們用鼠標在散點圖中把這個紅點框出來,就可以進入到 trace 視圖,查看具體的調(diào)用鏈路了。限于篇幅,我這里就不去演示其他 Traces 系統(tǒng)的入口了。
還是看上面這個圖,我們看右下角的兩個統(tǒng)計圖,我們可以看出來在最近 5 分鐘內(nèi) Shopping-api 調(diào)用 Shopping-order 的所有請求的耗時情況,以及時間分布。在發(fā)生異常的情況,比如流量突發(fā),這些圖的作用就出來了。
對于 Traces 系統(tǒng)來說,最有用的就是這些東西了,當然大家在使用過程中,可能也發(fā)現(xiàn)了 Traces 系統(tǒng)有很多的統(tǒng)計功能或者機器健康情況的監(jiān)控,這些是每個 Traces 系統(tǒng)的差異化功能,我們就不去具體分析了。
3、最后,我們再來討論 Metrics,它側(cè)重于各種報表數(shù)據(jù)的收集和展示。
在 Metrics 方面做得比較好的開源系統(tǒng),是大眾點評開源的 Cat,下面這個圖是 Cat 中的 transaction 視圖,它展示了很多的我們經(jīng)常需要關(guān)心的統(tǒng)計數(shù)據(jù):
下圖是 Cat 的 problem 視圖,對我們開發(fā)者來說就太有用了,應用開發(fā)者的目標就是讓這個視圖中的數(shù)據(jù)越少越好。
本文之后的內(nèi)容主要都是圍繞著 Metrics 展開的,所以這里就不再展開更多的內(nèi)容了。
另外,說到 APM 或系統(tǒng)監(jiān)控,就不得不提 Prometheus+Grafana 這對組合,它們對機器健康情況、URL 訪問統(tǒng)計、QPS、P90、P99 等等這些需求,支持得非常好,它們用來做監(jiān)控大屏是非常合適的,但是通常不能幫助我們排查問題,它看到的是系統(tǒng)壓力高了、系統(tǒng)不行了,但不能一下子看出來為啥高了、為啥不行了。
科普:Prometheus 是一個使用內(nèi)存進行存儲和計算的服務,每個機器/應用通過 Prometheus 的接口上報數(shù)據(jù),它的特點是快,但是機器宕機或重啟會丟失所有數(shù)據(jù)。
Grafana 是一個好玩的東西,它通過各種插件來可視化各種系統(tǒng)數(shù)據(jù),比如查詢 Prometheus、ElasticSearch、ClickHouse、MySQL 等等,它的特點就是酷炫,用來做監(jiān)控大屏再好不過了。
-? ? ?Metrics 和 Traces? ? -
因為本文之后要介紹的我們開發(fā)的 Dog 系統(tǒng)從分類來說,側(cè)重于 Metrics,同時我們也提供 tracing 功能,所以這里單獨寫一小節(jié),分析一下 Metrics 和 Traces 系統(tǒng)之間的聯(lián)系和區(qū)別。
使用上的區(qū)別很好理解,Metrics 做的是數(shù)據(jù)統(tǒng)計,比如某個 URL 或 DB 訪問被請求多少次,P90 是多少毫秒,錯誤數(shù)是多少等這種問題。而 Traces 是用來分析某次請求,它經(jīng)過了哪些鏈路,比如進入 A 應用后,調(diào)用了哪些方法,之后可能又請求了 B 應用,在 B 應用里面又調(diào)用了哪些方法,或者整個鏈路在哪個地方出錯等這些問題。
不過在前面介紹 Traces 的時候,我們也發(fā)現(xiàn)這類系統(tǒng)也會做很多的統(tǒng)計工作,它也覆蓋了很多的 Metrics 的內(nèi)容。
所以大家先要有個概念,Metrics 和 Traces 之間的聯(lián)系是非常緊密的,它們的數(shù)據(jù)結(jié)構(gòu)都是一顆調(diào)用樹,區(qū)別在于這顆樹的枝干和葉子多不多。在 Traces 系統(tǒng)中,一個請求所經(jīng)過的鏈路數(shù)據(jù)是非常全的,這樣對排查問題的時候非常有用,但是如果要對 Traces 中的所有節(jié)點的數(shù)據(jù)做報表統(tǒng)計,將會非常地耗費資源,性價比太低。而 Metrics 系統(tǒng)就是面向數(shù)據(jù)統(tǒng)計而生的,所以樹上的每個節(jié)點我們都會進行統(tǒng)計,所以這棵樹不能太“茂盛”。
我們關(guān)心的其實是,哪些數(shù)據(jù)值得統(tǒng)計?首先是入口,其次是耗時比較大的地方,比如 db 訪問、http 請求、redis 請求、跨服務調(diào)用等。當我們有了這些關(guān)鍵節(jié)點的統(tǒng)計數(shù)據(jù)以后,對于系統(tǒng)的健康監(jiān)控就非常容易了。
我這里不再具體去介紹他們的區(qū)別,大家看完本文介紹的 Metrics 系統(tǒng)實現(xiàn)以后,再回來思考這個問題會比較好。
Dog 在設計上,主要是做一個 Metrics 系統(tǒng),統(tǒng)計關(guān)鍵節(jié)點的數(shù)據(jù),另外也提供 trace 的能力,不過因為我們的樹不是很”茂盛“,所以鏈路上可能是斷斷續(xù)續(xù)的,中間會有很多缺失的地帶,當然應用開發(fā)者也可以加入手動埋點來彌補。
Dog 因為是公司內(nèi)部的監(jiān)控系統(tǒng),所以對于公司內(nèi)部大家會使用到的中間件相對是比較確定的,不需要像開源的 APM 一樣需要打很多點,我們主要實現(xiàn)了以下節(jié)點的自動打點:
http 入口:通過實現(xiàn)一個 Filter 來攔截所有的請求;
MySQL: 通過 Mybatis Interceptor 的方式;
Redis: 通過 javassist 增強 RedisTemplate 的方式;
跨應用調(diào)用: 通過代理 feign client 的方式,dubbo、grpc 等方式可能需要通過攔截器;
http 調(diào)用: 通過 javassist 為 HttpClient 和 OkHttp 增加 interceptor 的方式;
Log 打點: 通過 plugin 的方式,將 log 中打印的 error 上報上來。
打點的技術(shù)細節(jié),就不在這里展開了,主要還是用了各個框架提供的一些接口,另外就是用到了 javassist 做字節(jié)碼增強。
這些打點數(shù)據(jù)就是我們需要做統(tǒng)計的,當然因為打點有限,我們的 tracing 功能相對于專業(yè)的 Traces 系統(tǒng)來說單薄了很多。
-? ? ?Dog 簡介? ? -
下面是 DOG 的架構(gòu)圖,客戶端將消息投遞給 Kafka,由 dog-server 來消費消息,存儲用到了 Cassandra 和 ClickHouse,后面再介紹具體存哪些數(shù)據(jù)。
1、也有 APM 系統(tǒng)是不通過消息中間件的,比如 Cat 就是客戶端通過 Netty 連接到服務端來發(fā)送消息的。
2、Server 端使用了 Lambda 架構(gòu)模式,Dog UI 上查詢的數(shù)據(jù),由每一個 Dog-server 的內(nèi)存數(shù)據(jù)和下游儲存的數(shù)據(jù)聚合而來。
下面,我們簡單介紹下 Dog UI 上一些比較重要的功能,我們之后再去分析怎么實現(xiàn)相應的功能。
注意:下面的圖都是我自己畫的,不是真的頁面截圖,數(shù)值上可能不太準確
下圖示例 transaction 報表:
點擊上圖中 type 中的某一項,我們有這個 type 下面每個 name 的報表。比如點擊 URL,我們可以得到每個接口的數(shù)據(jù)統(tǒng)計:
當然,上圖中點擊具體的 name,還有下一個層級 status 的統(tǒng)計數(shù)據(jù),這里就不再貼圖了。Dog 總共設計了 type、name、status 三級屬性。上面兩個圖中的最后一列是 sample,它可以指引到 sample 視圖:
Sample 就是取樣的意思,當我們看到有個接口失敗率很高,或者 P90 很高的時候,你知道出了問題,但因為它只有統(tǒng)計數(shù)據(jù),所以你不知道到底哪里出了問題,這個時候,就需要有一些樣本數(shù)據(jù)了。我們每分鐘對 type、name、status 的不同組合分別保存最多 5 個成功、5 個失敗、5 個慢處理的樣本數(shù)據(jù)。
點擊上面的 sample 表中的某個 T、F、L 其實就會進入到我們的 trace 視圖,展示出這個請求的整個鏈路:
通過上面這個 trace 視圖,可以非常快速地知道是哪個環(huán)節(jié)出了問題。當然,我們之前也說過,我們的 trace 依賴于我們的埋點豐富度,但是 Dog 是一個 Metrics 為主的系統(tǒng),所以它的 Traces 能力是不夠的,不過大部分情況下,對于排查問題應該是足夠用的。
對于應用開發(fā)者來說,下面這個 Problem 視圖應該是非常有用的:
它展示了各種錯誤的數(shù)據(jù)統(tǒng)計,并且提供了 sample 讓開發(fā)者去排查問題。
最后,我們再簡單介紹下 Heartbeat 視圖,它和前面的功能沒什么關(guān)系,就是大量的圖,我們有 gc、heap、os、thread 等各種數(shù)據(jù),讓我們可以觀察到系統(tǒng)的健康情況。
這節(jié)主要介紹了一個 APM 系統(tǒng)通常包含哪些功能,其實也很簡單對不對,接下來我們從開發(fā)者的角度,來聊聊具體的實現(xiàn)細節(jié)問題。
-? ? ?客戶端數(shù)據(jù)模型? ? -
大家都是開發(fā)者,我就直接一些了,下圖介紹了客戶端的數(shù)據(jù)模型:
對于一條 Message 來說,用于統(tǒng)計的字段是 type, name, status,所以我們能基于 type、type+name、type+name+status 三種維度的數(shù)據(jù)進行統(tǒng)計。
Message 中其他的字段:timestamp 表示事件發(fā)生的時間;success 如果是 false,那么該事件會在 problem 報表中進行統(tǒng)計;data 不具有統(tǒng)計意義,它只在鏈路追蹤排查問題的時候有用;businessData 用來給業(yè)務系統(tǒng)上報業(yè)務數(shù)據(jù),需要手動打點,之后用來做業(yè)務數(shù)據(jù)分析。
Message 有兩個子類 Event 和 Transaction,區(qū)別在于 Transaction 帶有 duration 屬性,用來標識該 transaction 耗時多久,可以用來做 max time, min time, avg time, p90, p95 等,而 event 指的是發(fā)生了某件事,只能用來統(tǒng)計發(fā)生了多少次,并沒有時間長短的概念。
Transaction 有個屬性 children,可以嵌套 Transaction 或者 Event,最后形成一顆樹狀結(jié)構(gòu),用來做 trace,我們稍后再介紹。
下面表格示例一下打點數(shù)據(jù),這樣比較直觀一些:
簡單介紹幾點內(nèi)容:
type 為 URL、SQL、Redis、FeignClient、HttpClient 等這些數(shù)據(jù),屬于自動埋點的范疇。通常做 APM 系統(tǒng)的,都要完成一些自動埋點的工作,這樣應用開發(fā)者不需要做任何的埋點工作,就能看到很多有用的數(shù)據(jù)。像最后兩行的 type=Order 屬于手動埋點的數(shù)據(jù)。
打點需要特別注意 type、name、status 的維度“爆炸”,它們的組合太多會非常消耗資源,它可能會直接拖垮我們的 Dog 系統(tǒng)。type 的維度可能不會太多,但是我們可能需要注意開發(fā)者可能會濫用 name 和 status,所以我們一定要做 normalize(如 url 可能是帶動態(tài)參數(shù)的,需要格式化處理一下)。
表格中的最后兩條是開發(fā)者手動埋點的數(shù)據(jù),通常用來統(tǒng)計特定的場景,比如我想知道某個方法被調(diào)用的情況,調(diào)用次數(shù)、耗時、是否拋異常、入?yún)ⅰ⒎祷刂档取R驗樽詣勇顸c是業(yè)務不想關(guān)的,冷冰冰的數(shù)據(jù),開發(fā)者可能想要埋一些自己想要統(tǒng)計的數(shù)據(jù)。
開發(fā)者在手動埋點的時候,還可以上報更多的業(yè)務相關(guān)的數(shù)據(jù)上來,參考表格最后一列,這些數(shù)據(jù)可以做業(yè)務分析來用。比如我是做支付系統(tǒng)的,通常一筆支付訂單會涉及到非常多的步驟(國外的支付和大家平時使用的微信、支付寶稍微有點不一樣),通過上報每一個節(jié)點的數(shù)據(jù),最后我就可以在 Dog 上使用 bizId 來將整個鏈路串起來,在排查問題的時候是非常有用的(我們在做支付業(yè)務的時候,支付的成功率并沒有大家想象的那么高,很多節(jié)點可能出問題)。
-? ? ?客戶端設計? ? -
上一節(jié)我們介紹了單條 message 的數(shù)據(jù),這節(jié)我們覆蓋一下其他內(nèi)容。
首先,我們介紹客戶端的 API 使用:
public void test() {Transaction transaction = Dog.newTransaction("URL", "/test/user");try {Dog.logEvent("User", "name-xxx", "status-yyy");// do somethingTransaction sql = Dog.newTransaction("SQL", "UserMapper.insert");// try-catch-finallytransaction.setStatus("xxxx"); transaction.setSuccess(true/false);} catch (Throwable throwable) {transaction.setSuccess(false);transaction.setData(Throwables.getStackTraceAsString(throwable));throw throwable;} finally {transaction.finish();} }上面的代碼示例了如何嵌套使用 Transaction 和 Event,當最外層的 Transaction 在 finally 代碼塊調(diào)用 finish() 的時候,完成了一棵樹的創(chuàng)建,進行消息投遞。
我們往 Kafka 中投遞的并不是一個 Message 實例,因為一次請求會產(chǎn)生很多的 Message 實例,而是應該組織成 一個 Tree 實例以后進行投遞。下圖描述 Tree 的各個屬性:
Tree 的屬性很好理解,它持有 root transaction 的引用,用來遍歷整顆樹。另外就是需要攜帶機器信息 messageEnv。
treeId 應該有個算法能保證全局唯一,簡單介紹下 Dog 的實現(xiàn):${appName}-${encode(ip)}-${當前分鐘}-${自增id}。
下面簡單介紹幾個 tree id 相關(guān)的內(nèi)容,假設一個請求從 A->B->C->D 經(jīng)過 4 個應用,A 是入口應用,那么會有:
1、總共會有 4 個 Tree 對象實例從 4 個應用投遞到 Kafka,跨應用調(diào)用的時候需要傳遞 treeId, parentTreeId, rootTreeId 三個參數(shù);
2、A 應用的 treeId 是所有節(jié)點的 rootTreeId;
3、B 應用的 parentTreeId 是 A 的 treeId,同理 C 的 parentTreeId 是 B 應用的 treeId;
4、在跨應用調(diào)用的時候,比如從 A 調(diào)用 B 的時候,為了知道 A 的下一個節(jié)點是什么,所以在 A 中提前為 B 生成 treeId,B 收到請求后,如果發(fā)現(xiàn) A 已經(jīng)為它生成了 treeId,直接使用該 treeId。
大家應該也很容易知道,通過這幾個 tree id,我們是想要實現(xiàn) trace 的功能。
介紹完了 tree 的內(nèi)容,我們再簡單討論下應用集成方案。
集成無外乎兩種技術(shù),一種是通過 javaagent 的方式,在啟動腳本中,加上相應的 agent,這種方式的優(yōu)點是開發(fā)人員無感知,運維層面就可以做掉,當然開發(fā)者如果想要手動做一些埋點,可能需要再提供一個簡單的 client jar 包給開發(fā)者,用來橋接到 agent 里。
另一種就是提供一個 jar 包,由開發(fā)者來引入這個依賴。
兩種方案各有優(yōu)缺點,Pinpoint 和 Skywalking 使用的是 javaagent 方案,Zipkin、Jaeger、Cat 使用的是第二種方案,Dog 也使用第二種手動添加依賴的方案。
通常來說,做 Traces 的系統(tǒng)選擇使用 javaagent 方案比較省心,因為這類系統(tǒng) agent 做完了所有需要的埋點,無需應用開發(fā)者感知。
最后,我再簡單介紹一下 Heartbeat 的內(nèi)容,這部分內(nèi)容其實最簡單,但是能做出很多花花綠綠的圖表出來,可以實現(xiàn)面向老板編程。
前面我們介紹了 Message 有兩個子類 Event 和 Transaction,這里我們再加一個子類 Heartbeat,用來上報心跳數(shù)據(jù)。
我們主要收集了 thread、os、gc、heap、client 運行情況(產(chǎn)生多少個 tree,數(shù)據(jù)大小,發(fā)送失敗數(shù))等,同時也提供了 api 讓開發(fā)者自定義數(shù)據(jù)進行上報。Dog client 會開啟一個后臺線程,每分鐘運行一次 Heartbeat 收集程序,上報數(shù)據(jù)。
再介紹細一些。核心結(jié)構(gòu)是一個 Map\<String, Double>,key 類似于 “os.systemLoadAverage”, “thread.count” 等,前綴 os,thread,gc 等其實是用來在頁面上的分類,后綴是顯示的折線圖的名稱。
關(guān)于客戶端,這里就介紹這么多了,其實實際編碼過程中,還有一些細節(jié)需要處理,比如如果一棵樹太大了要怎么處理,比如沒有 rootTransaction 的情況怎么處理(開發(fā)者只調(diào)用了 Dog.logEvent(...)),比如內(nèi)層嵌套的 transaction 沒有調(diào)用 finish 怎么處理等等。
-? ? ?Dog Server 設計? ? -
下圖示例了 server 的整體設計,值得注意的是,我們這里對線程的使用非常地克制,圖中只有 3 個工作線程。
首先是 Kafka Consumer 線程,它負責批量消費消息,從 kafka 集群中消費到的是一個個 Tree 的實例,接下來考慮怎么處理它。
在這里,我們需要將樹狀結(jié)構(gòu)的 message 鋪平,我們把這一步叫做 deflate,并且做一些預處理,形成下面的結(jié)構(gòu):
接下來,我們就將 DeflateTree 分別投遞到兩個 Disruptor 實例中,我們把 Disruptor 設計成單線程生產(chǎn)和單線程消費,主要是性能上的考慮。
消費線程根據(jù) DeflateTree 的屬性使用綁定好的 Processor 進行處理,比如 DeflateTree 中 List<Message> problmes 不為空,同時自己綁定了 ProblemProcessor,那么就需要調(diào)用 ProblemProcessor 來處理。
科普時間:Disruptor 是一個高性能的隊列,性能比 JDK 中的 BlockingQueue 要好
這里我們使用了 2 個 Disruptor 實例,當然也可以考慮使用更多的實例,這樣每個消費線程綁定的 processor 就更少。
我們這里把 Processor 綁定到了 Disruptor 實例上,其實原因也很簡單,為了性能考慮,我們想讓每個 processor 只有單線程使用它,單線程操作可以減少線程切換帶來的開銷,可以充分利用到系統(tǒng)緩存,以及在設計 processor 的時候,不用考慮并發(fā)讀寫的問題。
這里要考慮負載均衡的情況,有些 processor 是比較耗費 CPU 和內(nèi)存資源的,一定要合理分配,不能把壓力最大的幾個任務分到同一個線程中去了。
核心的處理邏輯都在各個 processor 中,它們負責數(shù)據(jù)計算。接下來,我把各個 processor 需要做的主要內(nèi)容介紹一下,畢竟能看到這里的開發(fā)者,應該真的是對 APM 的數(shù)據(jù)處理比較感興趣的。
-? ? ?Transaction Processor? ? -
Transaction processor 是系統(tǒng)壓力最大的地方,它負責報表統(tǒng)計,雖然 Message 有 Transaction 和 Event 兩個主要的子類,但是在實際的一顆樹中,絕大部分的節(jié)點都是 transaction 類型的數(shù)據(jù)。
下圖是 transaction processor 內(nèi)部的一個主要的數(shù)據(jù)結(jié)構(gòu),最外層是一個時間,我們用分鐘時間來組織,我們最后在持久化的時候,也是按照分鐘來存的。
第二層的 HostKey 代表哪個應用以及哪個 ip 來的數(shù)據(jù),第三層是 type、name、status 的組合。最內(nèi)層的 Statistics 是我們的數(shù)據(jù)統(tǒng)計模塊。
另外我們也可以看到,這個結(jié)構(gòu)到底會消耗多少內(nèi)存,其實主要取決于我們的 type、name、status 的組合也就是 ReportKey 會不會很多,也就是我們前面在說客戶端打點的時候,要避免維度爆炸。
最外層結(jié)構(gòu)代表的是時間的分鐘表示,我們的報表是基于每分鐘來進行統(tǒng)計的,之后持久化到 ClickHouse 中,但是我們的使用者在看數(shù)據(jù)的時候,可不是一分鐘一分鐘看的,所以需要做數(shù)據(jù)聚合,下面展示兩條數(shù)據(jù)是如何做聚合的,在很多數(shù)據(jù)的時候,都是按照同樣的方法進行合并。
你仔細想想就會發(fā)現(xiàn),前面幾個數(shù)據(jù)的計算都沒毛病,但是 P90, P95 和 P99 的計算是不是有點欺騙人啊?其實這個問題是真的無解的,我們只能想一個合適的數(shù)據(jù)計算規(guī)則,然后我們再想想這種計算規(guī)則,可能算出來的值也是差不多可用的就好了。
另外有一個細節(jié)問題,我們需要讓內(nèi)存中的數(shù)據(jù)提供最近 30 分鐘的統(tǒng)計信息,30 分鐘以上的才從 DB 讀取。然后做上面介紹的 merge 操作。
討論:我們是否可以丟棄一部分實時性,我們每分鐘持久化一次,我們讀取的數(shù)據(jù)都是從 DB 來的,這樣可行嗎?
不行,因為我們的數(shù)據(jù)是從 kafka 消費來的,本身就有一定的滯后性,我們?nèi)绻陂_始一分鐘的時候就持久化上一分鐘的數(shù)據(jù),可能之后還會收到前面時間的消息,這種情況處理不了。
比如我們要統(tǒng)計最近一小時的情況,那么就會有 30 分鐘的數(shù)據(jù)從各個機器中獲得,有 30 分鐘的數(shù)據(jù)從 DB 獲得,然后做合并。
這里值得一提的是,在 transaction 報表中,count、failCount、min、max、avg 是比較好算的,但是 P90、P95、P99 其實不太好算,我們需要一個數(shù)組結(jié)構(gòu),來記錄這一分鐘內(nèi)所有的事件的時間,然后進行計算,我們這里討巧使用了 Apache DataSketches,它非常好用,這里我就不展開了,感興趣的同學可以自己去看一下。
到這里,大家可以去想一想儲存到 ClickHouse 的數(shù)據(jù)量的問題。app_name、ip、type、name、status 的不同組合,每分鐘一條數(shù)據(jù)。
-? ? ?Sample Processor? ? -
sample processor 消費 deflate tree 中的 List<Transaction> transactions 和 List<Event> events 的數(shù)據(jù)。
我們也是按照分鐘來采樣,最終每分鐘,對每個 type、name、status 的不同組合,采集最多 5 個成功、5 個失敗、5 個慢處理。
相對來說,這個還是非常簡單的,它的核心結(jié)構(gòu)如下圖:
結(jié)合 Sample 的功能來看比較容易理解:
-? ? ?Problem Processor? ? -
在做 deflate 的時候,所有 success=false 的 Message,都會被放入 List<Message> problmes 中,用來做錯誤統(tǒng)計。
Problem 內(nèi)部的數(shù)據(jù)結(jié)構(gòu)如下圖:
大家看下這個圖,其實也就知道要做什么了,我就不啰嗦了。其中 samples 我們每分鐘保存 5 個 treeId。
順便也再展示下 Problem 的視圖:
關(guān)于持久化,我們是存到了 ClickHouse 中,其中 sample 用逗號連接成一個字符串,problem_data 的列如下:
event_date, event_time, app_name, ip, type, name, status, count, sample-? ? ?HeartBeat Processor? ? -
Heartbeat 處理 List<Heartbeat> heartbeats 的數(shù)據(jù),題外話,正常情況下,一顆樹里面只有一個 Heartbeat 實例。
前面我也簡單提到了一下,我們 Heartbeat 中用來展示圖表的核心數(shù)據(jù)結(jié)構(gòu)是一個 Map<String, Double> 。
收集到的 key-value 數(shù)據(jù)如下所示:
{"os.systemLoadAverage": 1.5,"os.committedVirtualMemory": 1234562342,"os.openFileDescriptorCount": 800,"thread.count": 600,"thread.httpThreadsCount": 250,"gc.ZGC Count": 234,"gc.ZGC Time(ms)": 123435,"heap.ZHeap": 4051233219,"heap.Metaspace": 280123212 }前綴是分類,后綴是圖的名稱。客戶端每分鐘收集一次數(shù)據(jù)進行上報,然后就可以做很多的圖了,比如下圖展示了在 heap 分類下的各種圖:
Heartbeat processor 要做的事情很簡單,就是數(shù)據(jù)存儲,Dog UI 上的數(shù)據(jù)是直接從 ClickHouse 中讀取的。
heartbeat_data 的列如下:
event_date, event_time, timestamp, app_name, ip, name, value-? ? ?MessageTree Processor? ? -
前面我們多次提到了 Sample 的功能,這些采樣的數(shù)據(jù)幫助我們恢復現(xiàn)場,這樣我們可以通過 trace 視圖來跟蹤調(diào)用鏈。
要做上面的這個 trace 視圖,我們需要上下游的所有的 tree 的數(shù)據(jù),比如上圖是 3 個 tree 實例的數(shù)據(jù)。
之前我們在客戶端介紹的時候說過,這幾個 tree 通過 parent treeId 和 root treeId 來組織。
要做這個視圖,給我們提出的挑戰(zhàn)就是,我們需要保存全量的數(shù)據(jù)。
大家可以想一想這個問題,為啥要保存全量數(shù)據(jù),我們直接保存被 sample 到的數(shù)據(jù)不就好了嗎?
這里我們用到了 Cassandra 的能力,Cassandra 在這種 kv 的場景中,有非常不錯的性能,而且它的運維成本很低。
我們以 treeId 作為主鍵,另外再加 data 一個列即可,它是整個 tree 的實例數(shù)據(jù),數(shù)據(jù)類型是 blob,我們會先做一次 gzip 壓縮,然后再扔給 Cassandra。
-? ? ?Business Processor? ? -
我們在介紹客戶端的時候說過,每個 Message 都可以攜帶 Business Data,不過只有應用開發(fā)者自己手動埋點的時候才會有,當我們發(fā)現(xiàn)有業(yè)務數(shù)據(jù)的時候,我們會做另一個事情,就是把這個數(shù)據(jù)存儲到 ClickHouse 中,用來做業(yè)務分析。
我們其實不知道應用開發(fā)者到底會把它用在什么場景中,因為每個人負責的項目都不一樣,所以我們只能做一個通用的數(shù)據(jù)模型。
回過頭來看這個圖,BusinessData 中我們定義了比較通用的 userId 和 bizId,我們認為它們可能是每個業(yè)務場景會用到的東西。userId 就不用說了,bizId 大家可以做來記錄訂單 id,支付單 id 等。
然后我們提供了 3 個 String 類型的列 ext1、ext2、ext3 和兩個數(shù)值類型的列 extVal1 和 extVal2,它們可以用來表達你的業(yè)務相關(guān)的參數(shù)。
我們的處理當然也非常簡單,將這些數(shù)據(jù)存到 ClickHouse 中就可以了,表中主要有這些列:
event_data, event_time, user, biz_id, timestamp, type, name, status, app_name、ip、success、ext1、ext2、ext3、ext_val1、ext_val2這些數(shù)據(jù)對我們 Dog 系統(tǒng)來說肯定不認識,因為我們也不知道你表達的是什么業(yè)務,type、name、status 是開發(fā)者自己定義的,ext1, ext2, ext3 分別代表什么意思,我們都不知道,我們只負責存儲和查詢。
這些業(yè)務數(shù)據(jù)非常有用,基于這些數(shù)據(jù),我們可以做很多的數(shù)據(jù)報表出來。因為本文是討論 APM 的,所以該部分內(nèi)容就不再贅述了。
-? ? ?其他??? -
ClickHouse 需要批量寫入,不然肯定是撐不住的,一般一個 batch 至少 10000 行數(shù)據(jù)。
我們在 Kafka 這層控制了,一個 app_name + ip 的數(shù)據(jù),只會被同一個 dog-server 消費,當然也不是說被多個 dog-server 消費會有問題,但是這樣寫入 ClickHouse 的數(shù)據(jù)就會更多。
還有個關(guān)鍵的點,前面我們說了每個 processor 是由單線程進行訪問的,但是有一個問題,那就是來自 Dog UI 上的請求可怎么辦?這里我想了個辦法,那就是將請求放到一個 Queue 中,由 Kafka Consumer 那個線程來消費,它會將任務扔到兩個 Disruptor 中。比如這個請求是 transaction 報表請求,其中一個 Disruptor 的消費者會發(fā)現(xiàn)這個是自己要干的,就會去執(zhí)行這個任務。
-? ? ?小結(jié)??? -
如果你了解 Cat 的話,可以看到 Dog 在很多地方和 Cat 有相似之處,或者直接說”抄“也行,之前我們也考慮過直接使用 Cat 或者在 Cat 的基礎上做二次開發(fā)。
但是我看完 Cat 的源碼后,就放棄了這個想法,仔細想想,只是借鑒 Cat 的數(shù)據(jù)模型,然后我們自己寫一套 APM 其實不是很難,所以有了我們這個項目。
行文需要,很多地方我都避重就輕,因為這不是什么源碼分析的文章,沒必要處處談細節(jié),主要是給讀者一個全貌,讀者能通過我的描述大致想到需要處理哪些事情,需要寫哪些代碼,那就當我表述清楚了。
歡迎大家提出自己的疑問或者想法,有不懂或者我有錯漏的地方,歡迎指正~
作者:javadoop
來源:https://www.javadoop.com/post/apm
往期推薦
右軍:為張逸《解構(gòu)領域驅(qū)動設計》推薦序
2021-08-09
如何打造一支有超強戰(zhàn)斗力的技術(shù)團隊?
2021-08-03
亞馬遜首席科學家李沐:工作5年反思!
2021-06-03
彭榮新:喜馬拉雅自研網(wǎng)關(guān)架構(gòu)演進過程
2021-05-21
架構(gòu)概述之架構(gòu)演化、模式與核心要素
2021-05-19
美團陶云霜:CRM平臺建設實踐(膠片)
2021-08-18
阿里忘禪:螞蟻集團分布式注冊中心建設分享
2021-08-16
總結(jié)
以上是生活随笔為你收集整理的万字长文剖析 APM 系统?如何设计与实现?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NYOJ 661 亲亲串
- 下一篇: 30条架构原则:助你成为大牛架构师