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