手撕APM系統(tǒng),痛快!
圖片來(lái)自 包圖網(wǎng)
我們?yōu)?Dog 規(guī)劃的目標(biāo)是接入公司的大部分應(yīng)用,預(yù)計(jì)每秒處理 500MB-1000MB 的數(shù)據(jù),單機(jī)每秒 100MB 左右,使用多臺(tái)普通的 AWS EC2。
因?yàn)楸疚牡暮芏嘧x者供職的公司不一定有比較全面的 APM 系統(tǒng),所以我盡量照顧更多讀者的閱讀感受,會(huì)在有些內(nèi)容上啰嗦一些,希望大家可以理解。
我會(huì)在文中提到 Prometheus、Grafana、Cat、Pinpoint、Skywalking、Zipkin 等一系列工具,如果你沒(méi)有用過(guò)也不要緊,我會(huì)充分考慮到這一點(diǎn)。
本文預(yù)設(shè)的一些背景:Java 語(yǔ)言、Web 服務(wù)、每個(gè)應(yīng)用有多個(gè)實(shí)例、以微服務(wù)方式部署。
另外,從文章的可閱讀性上考慮,我假設(shè)每個(gè)應(yīng)用的不同實(shí)例分布在不同的 IP 上,可能你的應(yīng)用場(chǎng)景不一定是這樣的。
APM 簡(jiǎn)介
APM 通常認(rèn)為是 Application Performance Management 的簡(jiǎn)寫(xiě),它主要有三個(gè)方面的內(nèi)容,分別是 Logs(日志)、Traces(鏈路追蹤)和 Metrics(報(bào)表統(tǒng)計(jì))。
以后大家接觸任何一個(gè) APM 系統(tǒng)的時(shí)候,都可以從這三個(gè)方面去分析它到底是什么樣的一個(gè)系統(tǒng)。
有些場(chǎng)景中,APM 特指上面三個(gè)中的 Metrics,我們這里不去討論這個(gè)概念。
這節(jié)我們先對(duì)這 3 個(gè)方面進(jìn)行介紹,同時(shí)介紹一下這 3 個(gè)領(lǐng)域里面一些常用的工具。
①首先 Logs 最好理解,就是對(duì)各個(gè)應(yīng)用中打印的 log 進(jìn)行收集和提供查詢能力。
Logs 系統(tǒng)的重要性不言而喻,通常我們?cè)谂挪樘囟ǖ恼?qǐng)求的時(shí)候,是非常依賴于上下文的日志的。
以前我們都是通過(guò) terminal 登錄到機(jī)器里面去查 log(我好幾年都是這樣過(guò)來(lái)的),但是由于集群化和微服務(wù)化的原因,繼續(xù)使用這種方式工作效率會(huì)比較低。
因?yàn)槟憧赡苄枰卿浐脦着_(tái)機(jī)器搜索日志才能找到需要的信息,所以需要有一個(gè)地方中心化存儲(chǔ)日志,并且提供日志查詢。
Logs 的典型實(shí)現(xiàn)是 ELK(ElasticSearch、Logstash、Kibana),三個(gè)項(xiàng)目都是由 Elastic 開(kāi)源,其中最核心的就是 ES 的儲(chǔ)存和查詢的性能得到了大家的認(rèn)可,經(jīng)受了非常多公司的業(yè)務(wù)考驗(yàn)。
Logstash 負(fù)責(zé)收集日志,然后解析并存儲(chǔ)到 ES。通常有兩種比較主流的日志采集方式:
- 一種是通過(guò)一個(gè)客戶端程序 FileBeat,收集每個(gè)應(yīng)用打印到本地磁盤(pán)的日志,發(fā)送給 Logstash。
- 另一種則是每個(gè)應(yīng)用不需要將日志存儲(chǔ)到磁盤(pán),而是直接發(fā)送到 Kafka 集群中,由 Logstash 來(lái)消費(fèi)。
Kibana 是一個(gè)非常好用的工具,用于對(duì) ES 的數(shù)據(jù)進(jìn)行可視化,簡(jiǎn)單來(lái)說(shuō),它就是 ES 的客戶端。
kibana-discover
我們回過(guò)頭來(lái)分析 Logs 系統(tǒng),Logs 系統(tǒng)的數(shù)據(jù)來(lái)自于應(yīng)用中打印的日志,它的特點(diǎn)是數(shù)據(jù)量可能很大,取決于應(yīng)用開(kāi)發(fā)者怎么打日志,Logs 系統(tǒng)需要存儲(chǔ)全量數(shù)據(jù),通常都要支持至少 1 周的儲(chǔ)存。
每條日志包含 ip、thread、class、timestamp、traceId、message 等信息,它涉及到的技術(shù)點(diǎn)非常容易理解,就是日志的存儲(chǔ)和查詢。
使用也非常簡(jiǎn)單,排查問(wèn)題時(shí),通常先通過(guò)關(guān)鍵字搜到一條日志,然后通過(guò)它的 traceId 來(lái)搜索整個(gè)鏈路的日志。
題外話,Elastic 其實(shí)除了 Logs 以外,也提供了 Metrics 和 Traces 的解決方案,不過(guò)目前國(guó)內(nèi)用戶主要是使用它的 Logs 功能。
②我們?cè)賮?lái)看看 Traces 系統(tǒng),它用于記錄整個(gè)調(diào)用鏈路。
前面介紹的 Logs 系統(tǒng)使用的是開(kāi)發(fā)者打印的日志,所以它是最貼近業(yè)務(wù)的。
而 Traces 系統(tǒng)就離業(yè)務(wù)更遠(yuǎn)一些了,它關(guān)注的是一個(gè)請(qǐng)求進(jìn)來(lái)以后,經(jīng)過(guò)了哪些應(yīng)用、哪些方法,分別在各個(gè)節(jié)點(diǎn)耗費(fèi)了多少時(shí)間,在哪個(gè)地方拋出的異常等,用來(lái)快速定位問(wèn)題。
經(jīng)過(guò)多年的發(fā)展,Traces 系統(tǒng)雖然在服務(wù)端的設(shè)計(jì)很多樣,但是客戶端的設(shè)計(jì)慢慢地趨于統(tǒng)一,所以有了 OpenTracing 項(xiàng)目,我們可以簡(jiǎn)單理解為它是一個(gè)規(guī)范,它定義了一套 API,把客戶端的模型固化下來(lái)。
當(dāng)前比較主流的 Traces 系統(tǒng)中,Jaeger、SkyWalking 是使用這個(gè)規(guī)范的,而 Zipkin、Pinpoint 沒(méi)有使用該規(guī)范。限于篇幅,本文不對(duì) OpenTracing 展開(kāi)介紹。
下面這張圖是我畫(huà)的一個(gè)請(qǐng)求的時(shí)序圖:
trace
從上面這個(gè)圖中,可以非常方便地看出,這個(gè)請(qǐng)求經(jīng)過(guò)了 3 個(gè)應(yīng)用,通過(guò)線的長(zhǎng)短可以非常容易看出各個(gè)節(jié)點(diǎn)的耗時(shí)情況。
通常點(diǎn)擊某個(gè)節(jié)點(diǎn),我們可以有更多的信息展示,比如點(diǎn)擊 HttpClient 節(jié)點(diǎn)我們可能有 request 和 response 的數(shù)據(jù)。
下面這張圖是 Skywalking 的圖,它的 UI 也是蠻好的:
skywalking-trace
SkyWalking 在國(guó)內(nèi)應(yīng)該比較多公司使用,是一個(gè)比較優(yōu)秀的由國(guó)人發(fā)起的開(kāi)源項(xiàng)目,已進(jìn)入 Apache 基金會(huì)。
另一個(gè)比較好的開(kāi)源 Traces 系統(tǒng)是由韓國(guó)人開(kāi)源的 Pinpoint,它的打點(diǎn)數(shù)據(jù)非常豐富,這里有官方提供的 Live Demo,大家可以去玩一玩。
pinpoint
最近比較火的是由 CNCF(Cloud Native Computing Foundation)基金會(huì)管理的 Jeager:
jaeger
當(dāng)然也有很多人使用的是 Zipkin,算是 Traces 系統(tǒng)中開(kāi)源項(xiàng)目的老前輩了:
zipkin
上面介紹的是目前比較主流的 Traces 系統(tǒng),在排查具體問(wèn)題的時(shí)候它們非常有用,通過(guò)鏈路分析,很容易就可以看出來(lái)這個(gè)請(qǐng)求經(jīng)過(guò)了哪些節(jié)點(diǎn)、在每個(gè)節(jié)點(diǎn)的耗時(shí)、是否在某個(gè)節(jié)點(diǎn)執(zhí)行異常等。
雖然這里介紹的幾個(gè) Traces 系統(tǒng)的 UI 不一樣,大家可能有所偏好,但是具體說(shuō)起來(lái),表達(dá)的都是一個(gè)東西,那就是一顆調(diào)用樹(shù),所以我們要來(lái)說(shuō)說(shuō)每個(gè)項(xiàng)目除了 UI 以外不一樣的地方。
首先肯定是數(shù)據(jù)的豐富度,你往上拉看 Pinpoint 的樹(shù),你會(huì)發(fā)現(xiàn)它的埋點(diǎn)非常豐富,真的實(shí)現(xiàn)了一個(gè)請(qǐng)求經(jīng)過(guò)哪些方法一目了然。
但是這真的是一個(gè)好事嗎?值得大家去思考一下。兩個(gè)方面,一個(gè)是對(duì)客戶端的性能影響,另一個(gè)是服務(wù)端的壓力。
其次,Traces 系統(tǒng)因?yàn)橛邢到y(tǒng)間調(diào)用的數(shù)據(jù),所以很多 Traces 系統(tǒng)會(huì)使用這個(gè)數(shù)據(jù)做系統(tǒng)間的調(diào)用統(tǒng)計(jì),比如下面這個(gè)圖其實(shí)也蠻有用的:
trace-statistics
另外,前面說(shuō)的是某個(gè)請(qǐng)求的完整鏈路分析,那么就引出另一個(gè)問(wèn)題,我們?cè)趺传@取這個(gè)“某個(gè)請(qǐng)求”,這也是每個(gè) Traces 系統(tǒng)的不同之處。
比如上圖,它是 Pinpoint 的圖,我們看到前面兩個(gè)節(jié)點(diǎn)的圓圈是不完美的,點(diǎn)擊前面這個(gè)圓圈,就可以看出來(lái)原因了:
pinpoint-dashboard
圖中右邊的兩個(gè)紅圈是我加的。我們可以看到在 Shopping-api 調(diào)用 Shopping-order 的請(qǐng)求中,有 1 個(gè)失敗的請(qǐng)求。
我們用鼠標(biāo)在散點(diǎn)圖中把這個(gè)紅點(diǎn)框出來(lái),就可以進(jìn)入到 trace 視圖,查看具體的調(diào)用鏈路了。限于篇幅,我這里就不去演示其他 Traces 系統(tǒng)的入口了。
還是看上面這個(gè)圖,我們看右下角的兩個(gè)統(tǒng)計(jì)圖,我們可以看出來(lái)在最近 5 分鐘內(nèi) Shopping-api 調(diào)用 Shopping-order 的所有請(qǐng)求的耗時(shí)情況,以及時(shí)間分布。在發(fā)生異常的情況,比如流量突發(fā),這些圖的作用就出來(lái)了。
對(duì)于 Traces 系統(tǒng)來(lái)說(shuō),最有用的就是這些東西了,當(dāng)然大家在使用過(guò)程中,可能也發(fā)現(xiàn)了 Traces 系統(tǒng)有很多的統(tǒng)計(jì)功能或者機(jī)器健康情況的監(jiān)控,這些是每個(gè) Traces 系統(tǒng)的差異化功能,我們就不去具體分析了。
③最后,我們?cè)賮?lái)討論 Metrics,它側(cè)重于各種報(bào)表數(shù)據(jù)的收集和展示。
在 Metrics 方面做得比較好的開(kāi)源系統(tǒng),是大眾點(diǎn)評(píng)開(kāi)源的 Cat,下面這個(gè)圖是 Cat 中的 transaction 視圖,它展示了很多的我們經(jīng)常需要關(guān)心的統(tǒng)計(jì)數(shù)據(jù):
cat-transaction
下圖是 Cat 的 problem 視圖,對(duì)我們開(kāi)發(fā)者來(lái)說(shuō)就太有用了,應(yīng)用開(kāi)發(fā)者的目標(biāo)就是讓這個(gè)視圖中的數(shù)據(jù)越少越好。
cat-problem
本文之后的內(nèi)容主要都是圍繞著 Metrics 展開(kāi)的,所以這里就不再展開(kāi)更多的內(nèi)容了。
另外,說(shuō)到 APM 或系統(tǒng)監(jiān)控,就不得不提 Prometheus+Grafana 這對(duì)組合,它們對(duì)機(jī)器健康情況、URL 訪問(wèn)統(tǒng)計(jì)、QPS、P90、P99 等等這些需求,支持得非常好,它們用來(lái)做監(jiān)控大屏是非常合適的。
但是通常不能幫助我們排查問(wèn)題,它看到的是系統(tǒng)壓力高了、系統(tǒng)不行了,但不能一下子看出來(lái)為啥高了、為啥不行了。
科普:Prometheus 是一個(gè)使用內(nèi)存進(jìn)行存儲(chǔ)和計(jì)算的服務(wù),每個(gè)機(jī)器/應(yīng)用通過(guò) Prometheus 的接口上報(bào)數(shù)據(jù),它的特點(diǎn)是快,但是機(jī)器宕機(jī)或重啟會(huì)丟失所有數(shù)據(jù)。
Grafana 是一個(gè)好玩的東西,它通過(guò)各種插件來(lái)可視化各種系統(tǒng)數(shù)據(jù),比如查詢 Prometheus、ElasticSearch、ClickHouse、MySQL 等等,它的特點(diǎn)就是酷炫,用來(lái)做監(jiān)控大屏再好不過(guò)了。
Metrics 和 Traces
因?yàn)楸疚闹笠榻B的我們開(kāi)發(fā)的 Dog 系統(tǒng)從分類來(lái)說(shuō),側(cè)重于 Metrics,同時(shí)我們也提供 tracing 功能,所以這里單獨(dú)寫(xiě)一小節(jié),分析一下 Metrics 和 Traces 系統(tǒng)之間的聯(lián)系和區(qū)別。
使用上的區(qū)別很好理解,Metrics 做的是數(shù)據(jù)統(tǒng)計(jì),比如某個(gè) URL 或 DB 訪問(wèn)被請(qǐng)求多少次,P90 是多少毫秒,錯(cuò)誤數(shù)是多少等這種問(wèn)題。
而 Traces 是用來(lái)分析某次請(qǐng)求,它經(jīng)過(guò)了哪些鏈路,比如進(jìn)入 A 應(yīng)用后,調(diào)用了哪些方法,之后可能又請(qǐng)求了 B 應(yīng)用,在 B 應(yīng)用里面又調(diào)用了哪些方法,或者整個(gè)鏈路在哪個(gè)地方出錯(cuò)等這些問(wèn)題。
不過(guò)在前面介紹 Traces 的時(shí)候,我們也發(fā)現(xiàn)這類系統(tǒng)也會(huì)做很多的統(tǒng)計(jì)工作,它也覆蓋了很多的 Metrics 的內(nèi)容。
所以大家先要有個(gè)概念,Metrics 和 Traces 之間的聯(lián)系是非常緊密的,它們的數(shù)據(jù)結(jié)構(gòu)都是一顆調(diào)用樹(shù),區(qū)別在于這顆樹(shù)的枝干和葉子多不多。
在 Traces 系統(tǒng)中,一個(gè)請(qǐng)求所經(jīng)過(guò)的鏈路數(shù)據(jù)是非常全的,這樣對(duì)排查問(wèn)題的時(shí)候非常有用,但是如果要對(duì) Traces 中的所有節(jié)點(diǎn)的數(shù)據(jù)做報(bào)表統(tǒng)計(jì),將會(huì)非常地耗費(fèi)資源,性價(jià)比太低。
而 Metrics 系統(tǒng)就是面向數(shù)據(jù)統(tǒng)計(jì)而生的,所以樹(shù)上的每個(gè)節(jié)點(diǎn)我們都會(huì)進(jìn)行統(tǒng)計(jì),所以這棵樹(shù)不能太“茂盛”。
我們關(guān)心的其實(shí)是,哪些數(shù)據(jù)值得統(tǒng)計(jì)?首先是入口,其次是耗時(shí)比較大的地方,比如 db 訪問(wèn)、http 請(qǐng)求、redis 請(qǐng)求、跨服務(wù)調(diào)用等。
當(dāng)我們有了這些關(guān)鍵節(jié)點(diǎn)的統(tǒng)計(jì)數(shù)據(jù)以后,對(duì)于系統(tǒng)的健康監(jiān)控就非常容易了。
我這里不再具體去介紹他們的區(qū)別,大家看完本文介紹的 Metrics 系統(tǒng)實(shí)現(xiàn)以后,再回來(lái)思考這個(gè)問(wèn)題會(huì)比較好。
Dog 在設(shè)計(jì)上,主要是做一個(gè) Metrics 系統(tǒng),統(tǒng)計(jì)關(guān)鍵節(jié)點(diǎn)的數(shù)據(jù),另外也提供 trace 的能力,不過(guò)因?yàn)槲覀兊臉?shù)不是很”茂盛“,所以鏈路上可能是斷斷續(xù)續(xù)的,中間會(huì)有很多缺失的地帶,當(dāng)然應(yīng)用開(kāi)發(fā)者也可以加入手動(dòng)埋點(diǎn)來(lái)彌補(bǔ)。
Dog 因?yàn)槭枪緝?nèi)部的監(jiān)控系統(tǒng),所以對(duì)于公司內(nèi)部大家會(huì)使用到的中間件相對(duì)是比較確定的,不需要像開(kāi)源的 APM 一樣需要打很多點(diǎn)。
我們主要實(shí)現(xiàn)了以下節(jié)點(diǎn)的自動(dòng)打點(diǎn):
- HTTP 入口:通過(guò)實(shí)現(xiàn)一個(gè) Filter 來(lái)攔截所有的請(qǐng)求。
- MySQL:通過(guò) Mybatis Interceptor 的方式。
- Redis:通過(guò) javassist 增強(qiáng) RedisTemplate 的方式。
- 跨應(yīng)用調(diào)用:通過(guò)代理 feign client 的方式,dubbo、grpc 等方式可能需要通過(guò)攔截器。
- HTTP 調(diào)用:通過(guò) javassist 為 HttpClient 和 OkHttp 增加 interceptor 的方式。
- Log 打點(diǎn):通過(guò) plugin 的方式,將 log 中打印的 error 上報(bào)上來(lái)。
打點(diǎn)的技術(shù)細(xì)節(jié),就不在這里展開(kāi)了,主要還是用了各個(gè)框架提供的一些接口,另外就是用到了 javassist 做字節(jié)碼增強(qiáng)。
這些打點(diǎn)數(shù)據(jù)就是我們需要做統(tǒng)計(jì)的,當(dāng)然因?yàn)榇螯c(diǎn)有限,我們的 tracing 功能相對(duì)于專業(yè)的 Traces 系統(tǒng)來(lái)說(shuō)單薄了很多。
Dog 簡(jiǎn)介
下面是 DOG 的架構(gòu)圖,客戶端將消息投遞給 Kafka,由 dog-server 來(lái)消費(fèi)消息,存儲(chǔ)用到了 Cassandra 和 ClickHouse,后面再介紹具體存哪些數(shù)據(jù)。
architecture
也有 APM 系統(tǒng)是不通過(guò)消息中間件的,比如 Cat 就是客戶端通過(guò) Netty 連接到服務(wù)端來(lái)發(fā)送消息的。
Server 端使用了 Lambda 架構(gòu)模式,Dog UI 上查詢的數(shù)據(jù),由每一個(gè) Dog-server 的內(nèi)存數(shù)據(jù)和下游儲(chǔ)存的數(shù)據(jù)聚合而來(lái)。
下面,我們簡(jiǎn)單介紹下 Dog UI 上一些比較重要的功能,我們之后再去分析怎么實(shí)現(xiàn)相應(yīng)的功能。
注意:下面的圖都是我自己畫(huà)的,不是真的頁(yè)面截圖,數(shù)值上可能不太準(zhǔn)確。
下圖示例 transaction 報(bào)表:
transaction-type
點(diǎn)擊上圖中 type 中的某一項(xiàng),我們有這個(gè) type 下面每個(gè) name 的報(bào)表。比如點(diǎn)擊 URL,我們可以得到每個(gè)接口的數(shù)據(jù)統(tǒng)計(jì):
transaction-name
當(dāng)然,上圖中點(diǎn)擊具體的 name,還有下一個(gè)層級(jí) status 的統(tǒng)計(jì)數(shù)據(jù),這里就不再貼圖了。Dog 總共設(shè)計(jì)了 type、name、status 三級(jí)屬性。
上面兩個(gè)圖中的最后一列是 sample,它可以指引到 sample 視圖:
sample
Sample 就是取樣的意思,當(dāng)我們看到有個(gè)接口失敗率很高,或者 P90 很高的時(shí)候,你知道出了問(wèn)題,但因?yàn)樗挥薪y(tǒng)計(jì)數(shù)據(jù),所以你不知道到底哪里出了問(wèn)題,這個(gè)時(shí)候,就需要有一些樣本數(shù)據(jù)了。
我們每分鐘對(duì) type、name、status 的不同組合分別保存最多 5 個(gè)成功、5 個(gè)失敗、5 個(gè)慢處理的樣本數(shù)據(jù)。
點(diǎn)擊上面的 sample 表中的某個(gè) T、F、L 其實(shí)就會(huì)進(jìn)入到我們的 trace 視圖,展示出這個(gè)請(qǐng)求的整個(gè)鏈路:
trace
通過(guò)上面這個(gè) trace 視圖,可以非??焖俚刂朗悄膫€(gè)環(huán)節(jié)出了問(wèn)題。
當(dāng)然,我們之前也說(shuō)過(guò),我們的 trace 依賴于我們的埋點(diǎn)豐富度,但是 Dog 是一個(gè) Metrics 為主的系統(tǒng),所以它的 Traces 能力是不夠的,不過(guò)大部分情況下,對(duì)于排查問(wèn)題應(yīng)該是足夠用的。
對(duì)于應(yīng)用開(kāi)發(fā)者來(lái)說(shuō),下面這個(gè) Problem 視圖應(yīng)該是非常有用的:
problem
它展示了各種錯(cuò)誤的數(shù)據(jù)統(tǒng)計(jì),并且提供了 sample 讓開(kāi)發(fā)者去排查問(wèn)題。
最后,我們?cè)俸?jiǎn)單介紹下 Heartbeat 視圖,它和前面的功能沒(méi)什么關(guān)系,就是大量的圖,我們有 gc、heap、os、thread 等各種數(shù)據(jù),讓我們可以觀察到系統(tǒng)的健康情況。
heartbeat-heap
這節(jié)主要介紹了一個(gè) APM 系統(tǒng)通常包含哪些功能,其實(shí)也很簡(jiǎn)單對(duì)不對(duì),接下來(lái)我們從開(kāi)發(fā)者的角度,來(lái)聊聊具體的實(shí)現(xiàn)細(xì)節(jié)問(wèn)題。
客戶端數(shù)據(jù)模型
大家都是開(kāi)發(fā)者,我就直接一些了,下圖介紹了客戶端的數(shù)據(jù)模型:
data-model
對(duì)于一條 Message 來(lái)說(shuō),用于統(tǒng)計(jì)的字段是 type, name, status,所以我們能基于 type、type+name、type+name+status 三種維度的數(shù)據(jù)進(jìn)行統(tǒng)計(jì)。
Message 中其他的字段:
- timestamp 表示事件發(fā)生的時(shí)間。
- success 如果是 false,那么該事件會(huì)在 problem 報(bào)表中進(jìn)行統(tǒng)計(jì)。
- data 不具有統(tǒng)計(jì)意義,它只在鏈路追蹤排查問(wèn)題的時(shí)候有用。
- businessData 用來(lái)給業(yè)務(wù)系統(tǒng)上報(bào)業(yè)務(wù)數(shù)據(jù),需要手動(dòng)打點(diǎn),之后用來(lái)做業(yè)務(wù)數(shù)據(jù)分析。
Message 有兩個(gè)子類 Event 和 Transaction,區(qū)別在于 Transaction 帶有 duration 屬性,用來(lái)標(biāo)識(shí)該 transaction 耗時(shí)多久,可以用來(lái)做 max time,min time,avg time,p90,p95 等。
而 event 指的是發(fā)生了某件事,只能用來(lái)統(tǒng)計(jì)發(fā)生了多少次,并沒(méi)有時(shí)間長(zhǎng)短的概念。
Transaction 有個(gè)屬性 children,可以嵌套 Transaction 或者 Event,最后形成一顆樹(shù)狀結(jié)構(gòu),用來(lái)做 trace,我們稍后再介紹。
下面表格示例一下打點(diǎn)數(shù)據(jù),這樣比較直觀一些:
client
簡(jiǎn)單介紹幾點(diǎn)內(nèi)容:
①type 為 URL、SQL、Redis、FeignClient、HttpClient 等這些數(shù)據(jù),屬于自動(dòng)埋點(diǎn)的范疇。
通常做 APM 系統(tǒng)的,都要完成一些自動(dòng)埋點(diǎn)的工作,這樣應(yīng)用開(kāi)發(fā)者不需要做任何的埋點(diǎn)工作,就能看到很多有用的數(shù)據(jù)。像最后兩行的 type=Order 屬于手動(dòng)埋點(diǎn)的數(shù)據(jù)。
②打點(diǎn)需要特別注意 type、name、status 的維度“爆炸”,它們的組合太多會(huì)非常消耗資源,它可能會(huì)直接拖垮我們的 Dog 系統(tǒng)。
type 的維度可能不會(huì)太多,但是我們可能需要注意開(kāi)發(fā)者可能會(huì)濫用 name 和 status,所以我們一定要做 normalize(如 url 可能是帶動(dòng)態(tài)參數(shù)的,需要格式化處理一下)。
③表格中的最后兩條是開(kāi)發(fā)者手動(dòng)埋點(diǎn)的數(shù)據(jù),通常用來(lái)統(tǒng)計(jì)特定的場(chǎng)景,比如我想知道某個(gè)方法被調(diào)用的情況,調(diào)用次數(shù)、耗時(shí)、是否拋異常、入?yún)?、返回值等?/p>
因?yàn)樽詣?dòng)埋點(diǎn)是業(yè)務(wù)不想關(guān)的,冷冰冰的數(shù)據(jù),開(kāi)發(fā)者可能想要埋一些自己想要統(tǒng)計(jì)的數(shù)據(jù)。
④開(kāi)發(fā)者在手動(dòng)埋點(diǎn)的時(shí)候,還可以上報(bào)更多的業(yè)務(wù)相關(guān)的數(shù)據(jù)上來(lái),參考表格最后一列,這些數(shù)據(jù)可以做業(yè)務(wù)分析來(lái)用。
比如我是做支付系統(tǒng)的,通常一筆支付訂單會(huì)涉及到非常多的步驟(國(guó)外的支付和大家平時(shí)使用的微信、支付寶稍微有點(diǎn)不一樣),通過(guò)上報(bào)每一個(gè)節(jié)點(diǎn)的數(shù)據(jù),最后我就可以在 Dog 上使用 bizId 來(lái)將整個(gè)鏈路串起來(lái),在排查問(wèn)題的時(shí)候是非常有用的(我們?cè)谧鲋Ц稑I(yè)務(wù)的時(shí)候,支付的成功率并沒(méi)有大家想象的那么高,很多節(jié)點(diǎn)可能出問(wèn)題)。
客戶端設(shè)計(jì)
上一節(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 something
- Transaction sql = Dog.newTransaction("SQL", "UserMapper.insert");
- // try-catch-finally
- transaction.setStatus("xxxx");
- transaction.setSuccess(true/false);
- } catch (Throwable throwable) {
- transaction.setSuccess(false);
- transaction.setData(Throwables.getStackTraceAsString(throwable));
- throw throwable;
- } finally {
- transaction.finish();
- }
- }
上面的代碼示例了如何嵌套使用 Transaction 和 Event,當(dāng)最外層的 Transaction 在 finally 代碼塊調(diào)用 finish() 的時(shí)候,完成了一棵樹(shù)的創(chuàng)建,進(jìn)行消息投遞。
我們往 Kafka 中投遞的并不是一個(gè) Message 實(shí)例,因?yàn)橐淮握?qǐng)求會(huì)產(chǎn)生很多的 Message 實(shí)例,而是應(yīng)該組織成 一個(gè) Tree 實(shí)例以后進(jìn)行投遞。
下圖描述 Tree 的各個(gè)屬性:
tree
Tree 的屬性很好理解,它持有 root transaction 的引用,用來(lái)遍歷整顆樹(shù)。另外就是需要攜帶機(jī)器信息 messageEnv。
treeId 應(yīng)該有個(gè)算法能保證全局唯一,簡(jiǎn)單介紹下 Dog 的實(shí)現(xiàn):${appName}-${encode(ip)}-${當(dāng)前分鐘}-${自增id}。
下面簡(jiǎn)單介紹幾個(gè) tree id 相關(guān)的內(nèi)容,假設(shè)一個(gè)請(qǐng)求從 A->B->C->D 經(jīng)過(guò) 4 個(gè)應(yīng)用。
A 是入口應(yīng)用,那么會(huì)有:
- 總共會(huì)有 4 個(gè) Tree 對(duì)象實(shí)例從 4 個(gè)應(yīng)用投遞到 Kafka,跨應(yīng)用調(diào)用的時(shí)候需要傳遞 treeId, parentTreeId, rootTreeId 三個(gè)參數(shù)。
- A 應(yīng)用的 treeId 是所有節(jié)點(diǎn)的 rootTreeId。
- B 應(yīng)用的 parentTreeId 是 A 的 treeId,同理 C 的 parentTreeId 是 B 應(yīng)用的 treeId。
- 在跨應(yīng)用調(diào)用的時(shí)候,比如從 A 調(diào)用 B 的時(shí)候,為了知道 A 的下一個(gè)節(jié)點(diǎn)是什么,所以在 A 中提前為 B 生成 treeId,B 收到請(qǐng)求后,如果發(fā)現(xiàn) A 已經(jīng)為它生成了 treeId,直接使用該 treeId。
大家應(yīng)該也很容易知道,通過(guò)這幾個(gè) tree id,我們是想要實(shí)現(xiàn) trace 的功能。
介紹完了 tree 的內(nèi)容,我們?cè)俸?jiǎn)單討論下應(yīng)用集成方案。
集成無(wú)外乎兩種技術(shù),一種是通過(guò) javaagent 的方式,在啟動(dòng)腳本中,加上相應(yīng)的 agent。
這種方式的優(yōu)點(diǎn)是開(kāi)發(fā)人員無(wú)感知,運(yùn)維層面就可以做掉,當(dāng)然開(kāi)發(fā)者如果想要手動(dòng)做一些埋點(diǎn),可能需要再提供一個(gè)簡(jiǎn)單的 client jar 包給開(kāi)發(fā)者,用來(lái)橋接到 agent 里。另一種就是提供一個(gè) jar 包,由開(kāi)發(fā)者來(lái)引入這個(gè)依賴。
兩種方案各有優(yōu)缺點(diǎn),Pinpoint 和 Skywalking 使用的是 javaagent 方案,Zipkin、Jaeger、Cat 使用的是第二種方案,Dog 也使用第二種手動(dòng)添加依賴的方案。
通常來(lái)說(shuō),做 Traces 的系統(tǒng)選擇使用 javaagent 方案比較省心,因?yàn)檫@類系統(tǒng) agent 做完了所有需要的埋點(diǎn),無(wú)需應(yīng)用開(kāi)發(fā)者感知。
最后,我再簡(jiǎn)單介紹一下 Heartbeat 的內(nèi)容,這部分內(nèi)容其實(shí)最簡(jiǎn)單,但是能做出很多花花綠綠的圖表出來(lái),可以實(shí)現(xiàn)面向老板編程。
heartbeat-sample
前面我們介紹了 Message 有兩個(gè)子類 Event 和 Transaction,這里我們?cè)偌右粋€(gè)子類 Heartbeat,用來(lái)上報(bào)心跳數(shù)據(jù)。
我們主要收集了 thread、os、gc、heap、client 運(yùn)行情況(產(chǎn)生多少個(gè) tree,數(shù)據(jù)大小,發(fā)送失敗數(shù))等,同時(shí)也提供了 api 讓開(kāi)發(fā)者自定義數(shù)據(jù)進(jìn)行上報(bào)。
Dog client 會(huì)開(kāi)啟一個(gè)后臺(tái)線程,每分鐘運(yùn)行一次 Heartbeat 收集程序,上報(bào)數(shù)據(jù)。
再介紹細(xì)一些。核心結(jié)構(gòu)是一個(gè) Map
關(guān)于客戶端,這里就介紹這么多了,其實(shí)實(shí)際編碼過(guò)程中,還有一些細(xì)節(jié)需要處理。
比如如果一棵樹(shù)太大了要怎么處理,比如沒(méi)有 rootTransaction 的情況怎么處理(開(kāi)發(fā)者只調(diào)用了 Dog.logEvent(...)),比如內(nèi)層嵌套的 transaction 沒(méi)有調(diào)用 finish 怎么處理等等。
Dog server 設(shè)計(jì)
下圖示例了 server 的整體設(shè)計(jì),值得注意的是,我們這里對(duì)線程的使用非常地克制,圖中只有 3 個(gè)工作線程。
server-design
首先是 Kafka Consumer 線程,它負(fù)責(zé)批量消費(fèi)消息,從 Kafka 集群中消費(fèi)到的是一個(gè)個(gè) Tree 的實(shí)例,接下來(lái)考慮怎么處理它。
在這里,我們需要將樹(shù)狀結(jié)構(gòu)的 message 鋪平,我們把這一步叫做 deflate,并且做一些預(yù)處理,形成下面的結(jié)構(gòu):
deflate
接下來(lái),我們就將 DeflateTree 分別投遞到兩個(gè) Disruptor 實(shí)例中,我們把 Disruptor 設(shè)計(jì)成單線程生產(chǎn)和單線程消費(fèi),主要是性能上的考慮。
消費(fèi)線程根據(jù) DeflateTree 的屬性使用綁定好的 Processor 進(jìn)行處理,比如 DeflateTree 中 List
科普時(shí)間:Disruptor 是一個(gè)高性能的隊(duì)列,性能比 JDK 中的 BlockingQueue 要好。
這里我們使用了 2 個(gè) Disruptor 實(shí)例,當(dāng)然也可以考慮使用更多的實(shí)例,這樣每個(gè)消費(fèi)線程綁定的 processor 就更少。
我們這里把 Processor 綁定到了 Disruptor 實(shí)例上,其實(shí)原因也很簡(jiǎn)單,為了性能考慮,我們想讓每個(gè) processor 只有單線程使用它。
單線程操作可以減少線程切換帶來(lái)的開(kāi)銷,可以充分利用到系統(tǒng)緩存,以及在設(shè)計(jì) processor 的時(shí)候,不用考慮并發(fā)讀寫(xiě)的問(wèn)題。
這里要考慮負(fù)載均衡的情況,有些 processor 是比較耗費(fèi) CPU 和內(nèi)存資源的,一定要合理分配,不能把壓力最大的幾個(gè)任務(wù)分到同一個(gè)線程中去了。
核心的處理邏輯都在各個(gè) processor 中,它們負(fù)責(zé)數(shù)據(jù)計(jì)算。接下來(lái),我把各個(gè) processor 需要做的主要內(nèi)容介紹一下,畢竟能看到這里的開(kāi)發(fā)者,應(yīng)該真的是對(duì) APM 的數(shù)據(jù)處理比較感興趣的。
①Transaction processor
transaction processor 是系統(tǒng)壓力最大的地方,它負(fù)責(zé)報(bào)表統(tǒng)計(jì),雖然 Message 有 Transaction 和 Event 兩個(gè)主要的子類,但是在實(shí)際的一顆樹(shù)中,絕大部分的節(jié)點(diǎn)都是 transaction 類型的數(shù)據(jù)。
transaction-type
下圖是 transaction processor 內(nèi)部的一個(gè)主要的數(shù)據(jù)結(jié)構(gòu),最外層是一個(gè)時(shí)間,我們用分鐘時(shí)間來(lái)組織,我們最后在持久化的時(shí)候,也是按照分鐘來(lái)存的。
第二層的 HostKey 代表哪個(gè)應(yīng)用以及哪個(gè) ip 來(lái)的數(shù)據(jù),第三層是 type、name、status 的組合。最內(nèi)層的 Statistics 是我們的數(shù)據(jù)統(tǒng)計(jì)模塊。
transaction-statistics
另外我們也可以看到,這個(gè)結(jié)構(gòu)到底會(huì)消耗多少內(nèi)存,其實(shí)主要取決于我們的 type、name、status 的組合也就是 ReportKey 會(huì)不會(huì)很多,也就是我們前面在說(shuō)客戶端打點(diǎn)的時(shí)候,要避免維度爆炸。
最外層結(jié)構(gòu)代表的是時(shí)間的分鐘表示,我們的報(bào)表是基于每分鐘來(lái)進(jìn)行統(tǒng)計(jì)的,之后持久化到 ClickHouse 中。
但是我們的使用者在看數(shù)據(jù)的時(shí)候,可不是一分鐘一分鐘看的,所以需要做數(shù)據(jù)聚合。
下面展示兩條數(shù)據(jù)是如何做聚合的,在很多數(shù)據(jù)的時(shí)候,都是按照同樣的方法進(jìn)行合并。
transaction-cal
你仔細(xì)想想就會(huì)發(fā)現(xiàn),前面幾個(gè)數(shù)據(jù)的計(jì)算都沒(méi)毛病,但是 P90,P95 和 P99 的計(jì)算是不是有點(diǎn)欺騙人啊?
其實(shí)這個(gè)問(wèn)題是真的無(wú)解的,我們只能想一個(gè)合適的數(shù)據(jù)計(jì)算規(guī)則,然后我們?cè)傧胂脒@種計(jì)算規(guī)則,可能算出來(lái)的值也是差不多可用的就好了。
另外有一個(gè)細(xì)節(jié)問(wèn)題,我們需要讓內(nèi)存中的數(shù)據(jù)提供最近 30 分鐘的統(tǒng)計(jì)信息,30 分鐘以上的才從 DB 讀取。然后做上面介紹的 merge 操作。
討論:我們是否可以丟棄一部分實(shí)時(shí)性,我們每分鐘持久化一次,我們讀取的數(shù)據(jù)都是從 DB 來(lái)的,這樣可行嗎?
不行,因?yàn)槲覀兊臄?shù)據(jù)是從 Kafka 消費(fèi)來(lái)的,本身就有一定的滯后性,我們?nèi)绻陂_(kāi)始一分鐘的時(shí)候就持久化上一分鐘的數(shù)據(jù),可能之后還會(huì)收到前面時(shí)間的消息,這種情況處理不了。
比如我們要統(tǒng)計(jì)最近一小時(shí)的情況,那么就會(huì)有 30 分鐘的數(shù)據(jù)從各個(gè)機(jī)器中獲得,有 30 分鐘的數(shù)據(jù)從 DB 獲得,然后做合并。
這里值得一提的是,在 transaction 報(bào)表中,count、failCount、min、max、avg 是比較好算的。
但是 P90、P95、P99 其實(shí)不太好算,我們需要一個(gè)數(shù)組結(jié)構(gòu),來(lái)記錄這一分鐘內(nèi)所有的事件的時(shí)間,然后進(jìn)行計(jì)算。
我們這里討巧使用了 Apache DataSketches,它非常好用,這里我就不展開(kāi)了,感興趣的同學(xué)可以自己去看一下。
到這里,大家可以去想一想儲(chǔ)存到 ClickHouse 的數(shù)據(jù)量的問(wèn)題。app_name、ip、type、name、status 的不同組合,每分鐘一條數(shù)據(jù)。
②Sample Processor
sample processor 消費(fèi) deflate tree 中的 List
我們也是按照分鐘來(lái)采樣,最終每分鐘,對(duì)每個(gè) type、name、status 的不同組合,采集最多 5 個(gè)成功、5 個(gè)失敗、5 個(gè)慢處理。
相對(duì)來(lái)說(shuō),這個(gè)還是非常簡(jiǎn)單的,它的核心結(jié)構(gòu)如下圖:
sample-structure
結(jié)合 Sample 的功能來(lái)看比較容易理解:
sample
③Problem Processor
在做 deflate 的時(shí)候,所有 success=false 的 Message,都會(huì)被放入 List
Problem 內(nèi)部的數(shù)據(jù)結(jié)構(gòu)如下圖:
problem-structure
大家看下這個(gè)圖,其實(shí)也就知道要做什么了,我就不啰嗦了。其中 samples 我們每分鐘保存 5 個(gè) treeId。
順便也再展示下 Problem 的視圖:
problem
關(guān)于持久化,我們是存到了 ClickHouse 中,其中 sample 用逗號(hào)連接成一個(gè)字符串,problem_data 的列如下:
- event_date, event_time, app_name, ip, type, name, status, count, sample
④Heartbeat Processor
Heartbeat 處理 List
前面我也簡(jiǎn)單提到了一下,我們 Heartbeat 中用來(lái)展示圖表的核心數(shù)據(jù)結(jié)構(gòu)是一個(gè) Map
收集到的 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ù)進(jìn)行上報(bào),然后就可以做很多的圖了,比如下圖展示了在 heap 分類下的各種圖:
heartbeat-heap
Heartbeat processor 要做的事情很簡(jiǎn)單,就是數(shù)據(jù)存儲(chǔ),Dog UI 上的數(shù)據(jù)是直接從 ClickHouse 中讀取的。
heartbeat_data 的列如下:
- event_date, event_time, timestamp, app_name, ip, name, value
⑤MessageTree Processor
前面我們多次提到了 Sample 的功能,這些采樣的數(shù)據(jù)幫助我們恢復(fù)現(xiàn)場(chǎng),這樣我們可以通過(guò) trace 視圖來(lái)跟蹤調(diào)用鏈。
trace
要做上面的這個(gè) trace 視圖,我們需要上下游的所有的 tree 的數(shù)據(jù),比如上圖是 3 個(gè) tree 實(shí)例的數(shù)據(jù)。
之前我們?cè)诳蛻舳私榻B的時(shí)候說(shuō)過(guò),這幾個(gè) tree 通過(guò) parent treeId 和 root treeId 來(lái)組織。要做這個(gè)視圖,給我們提出的挑戰(zhàn)就是,我們需要保存全量的數(shù)據(jù)。
大家可以想一想這個(gè)問(wèn)題,為啥要保存全量數(shù)據(jù),我們直接保存被 sample 到的數(shù)據(jù)不就好了嗎?
這里我們用到了 Cassandra 的能力,Cassandra 在這種 kv 的場(chǎng)景中,有非常不錯(cuò)的性能,而且它的運(yùn)維成本很低。
我們以 treeId 作為主鍵,另外再加 data 一個(gè)列即可,它是整個(gè) tree 的實(shí)例數(shù)據(jù),數(shù)據(jù)類型是 blob,我們會(huì)先做一次 gzip 壓縮,然后再扔給 Cassandra。
⑥Business Processor
我們?cè)诮榻B客戶端的時(shí)候說(shuō)過(guò),每個(gè) Message 都可以攜帶 Business Data,不過(guò)只有應(yīng)用開(kāi)發(fā)者自己手動(dòng)埋點(diǎn)的時(shí)候才會(huì)有。
當(dāng)我們發(fā)現(xiàn)有業(yè)務(wù)數(shù)據(jù)的時(shí)候,我們會(huì)做另一個(gè)事情,就是把這個(gè)數(shù)據(jù)存儲(chǔ)到 ClickHouse 中,用來(lái)做業(yè)務(wù)分析。
我們其實(shí)不知道應(yīng)用開(kāi)發(fā)者到底會(huì)把它用在什么場(chǎng)景中,因?yàn)槊總€(gè)人負(fù)責(zé)的項(xiàng)目都不一樣,所以我們只能做一個(gè)通用的數(shù)據(jù)模型。
data-model
回過(guò)頭來(lái)看這個(gè)圖,BusinessData 中我們定義了比較通用的 userId 和 bizId,我們認(rèn)為它們可能是每個(gè)業(yè)務(wù)場(chǎng)景會(huì)用到的東西。userId 就不用說(shuō)了,bizId 大家可以做來(lái)記錄訂單 id,支付單 id 等。
然后我們提供了 3 個(gè) String 類型的列 ext1、ext2、ext3 和兩個(gè)數(shù)值類型的列 extVal1 和 extVal2,它們可以用來(lái)表達(dá)你的業(yè)務(wù)相關(guān)的參數(shù)。
我們的處理當(dāng)然也非常簡(jiǎn)單,將這些數(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ù)對(duì)我們 Dog 系統(tǒng)來(lái)說(shuō)肯定不認(rèn)識(shí),因?yàn)槲覀円膊恢滥惚磉_(dá)的是什么業(yè)務(wù),type、name、status 是開(kāi)發(fā)者自己定義的,ext1,ext2,ext3 分別代表什么意思,我們都不知道,我們只負(fù)責(zé)存儲(chǔ)和查詢。
這些業(yè)務(wù)數(shù)據(jù)非常有用,基于這些數(shù)據(jù),我們可以做很多的數(shù)據(jù)報(bào)表出來(lái)。因?yàn)楸疚氖怯懻?APM 的,所以該部分內(nèi)容就不再贅述了。
其他
ClickHouse 需要批量寫(xiě)入,不然肯定是撐不住的,一般一個(gè) batch 至少 10000 行數(shù)據(jù)。
我們?cè)?Kafka 這層控制了,一個(gè) app_name + ip 的數(shù)據(jù),只會(huì)被同一個(gè) dog-server 消費(fèi),當(dāng)然也不是說(shuō)被多個(gè) dog-server 消費(fèi)會(huì)有問(wèn)題,但是這樣寫(xiě)入 ClickHouse 的數(shù)據(jù)就會(huì)更多。
還有個(gè)關(guān)鍵的點(diǎn),前面我們說(shuō)了每個(gè) processor 是由單線程進(jìn)行訪問(wèn)的,但是有一個(gè)問(wèn)題,那就是來(lái)自 Dog UI 上的請(qǐng)求可怎么辦?
這里我想了個(gè)辦法,那就是將請(qǐng)求放到一個(gè) Queue 中,由 Kafka Consumer 那個(gè)線程來(lái)消費(fèi),它會(huì)將任務(wù)扔到兩個(gè) Disruptor 中。
比如這個(gè)請(qǐng)求是 transaction 報(bào)表請(qǐng)求,其中一個(gè) Disruptor 的消費(fèi)者會(huì)發(fā)現(xiàn)這個(gè)是自己要干的,就會(huì)去執(zhí)行這個(gè)任務(wù)。
小結(jié)
如果你了解 Cat 的話,可以看到 Dog 在很多地方和 Cat 有相似之處,或者直接說(shuō)“抄”也行,之前我們也考慮過(guò)直接使用 Cat 或者在 Cat 的基礎(chǔ)上做二次開(kāi)發(fā)。
但是我看完 Cat 的源碼后,就放棄了這個(gè)想法,仔細(xì)想想,只是借鑒 Cat 的數(shù)據(jù)模型,然后我們自己寫(xiě)一套 APM 其實(shí)不是很難,所以有了我們這個(gè)項(xiàng)目。
行文需要,很多地方我都避重就輕,因?yàn)檫@不是什么源碼分析的文章,沒(méi)必要處處談細(xì)節(jié),主要是給讀者一個(gè)全貌,讀者能通過(guò)我的描述大致想到需要處理哪些事情,需要寫(xiě)哪些代碼,那就當(dāng)我表述清楚了。
歡迎大家提出自己的疑問(wèn)或者想法,有不懂或者我有錯(cuò)漏的地方,歡迎指正~
作者:javadoop
編輯:陶家龍
出處:www.javadoop.com/post/apm