APM全稱是 ApplicationPerformanceManagement
,即應用性能管理平臺。對於公司而言,應用發展到如今這個階段,在人員不斷擴展、業務不斷複雜、新產品不斷孵化,擁有一個統一質量、安全管理平臺對於應用健康情況全方位實時的把脈,以及對於業務的可持續化發展的保駕護航是尤其重要的。git
在去年其實這樣的平臺還不多像今年這般被頻繁說起。尤記得前幾天剛剛參加綠色聯盟會議,對於應用的質量、安全不管是阿里、騰訊、新美大仍是360都繞不過這個主題,而你們都知道谷歌更是在今年發力推Vitals監控,彷佛在今年這個時間點,各大公司都使出渾身解數不約而同的加大了這方面的投入。數據庫
無獨有偶,2018年流利說Android架構組有一個小目標,就是將本來雜亂的交付、監控等流程系統化、體系化,便於橫向項目的發展以及確保各業務線、產品線可持續發展,其中APM就是很重要的一塊:後端
其實在年初咱們公司組織架構擴大後,流利說總體已經進入了人員與業務的快速增加的快車道,對於幾十個模塊、業務線與多條產品線的狀況下,原有的那套應對中小應用的體系已經顯現了一些問題。所以咱們着手從各個階段入手對其一一擊破,今天,咱們與你們分享的就是咱們在搭建APM過程當中,對於大盤搭建與數據整合收集部分,但願你們都可以有所收穫。安全
特別值得一提的是以前在談到[《客戶端持續交付工程實踐》這篇文章時,咱們談到了爲了確保質量,在最後出了各種的質量報告,並將其經過郵件的方式落地,在最後的時候甚至提出瞭如何避免線上裸奔,其實這些最後都在APM大盤中獲得了進行的落地,這些特性組合讓APM大盤展現實現了經過數據與綠線告知測試完備程度以及應用質量程度的任務,後面咱們會談到,你們跟上腳步👣,咱們往下看:服務器
首先談到APM大盤,就必須須要肯定咱們須要監控哪些維度,咱們先來看下最終的決定:架構
之因此選這三個維度,是結合咱們目前的發佈流程以及開發節奏的特性來肯定的:框架
最直接的是監控24小時內的全局的實時數據變化 --> 實時變化大盤
函數
而對於每一個版本兩兩比較也是十分重要的,所以便衍生了橫跨一年的版本維度的變化大盤 --> 版本變化大盤
工具
因爲咱們每一個版本內的開發週期有 dev
、 alpha
、 beta
,擁有較長的固定的迭代週期,所以咱們須要在版本發佈以前對當前版本特別的關照,從而衍生了一個僅僅只包含當前版本的數據大盤 --> 開發週期大盤
性能
在肯定了三大維度以後,咱們要作技術選型的肯定,其實這塊在搭建流利說APM平臺之初咱們就遇到了一個問題,對於流利說的Android架構團隊來講,咱們還很難像騰訊、阿里、餓了麼、愛奇藝那樣投入足夠的人力創建很是完善的自有的各種監控平臺與體系,所以咱們採用了一些現有成熟的體系框架與監控平臺來減輕咱們的工做量,將有限的精力聚焦在監控整合以及如何展現測試完備程度與應用質量的核心問題上。所以咱們採用了咱們的雲架構團隊提供基於開源方案的Promethues數據庫與Grafana展現平臺,以及在一些監控方面咱們採用了第三方數據平臺做爲數據依據以及問題處理平臺(如bugly)再作二次數據採集整合。這也祭奠了流利說APM大盤的另一個對全部數據的整合與入口的特性,固然這也就影響了咱們主要的技術選型:
而對於各種的自動化收集與大盤維度數據控制的自動化,咱們須要依賴如下實體媒介來搭載咱們的各種服務:
在第一期APM大盤須要監控的數據方面咱們選擇了 包大小
、 關鍵頁面打開耗時95線
、 內存泄漏
、 遺留BUG數
、 冷/熱啓動
、 代碼質量狀況
以及 ANR
與 CRASH
,再加上權衡測試完備程度的 參加測試狀況
與 綜合覆蓋率
,這樣的選擇的主要緣由是,第一期其實是APM大盤的從無到有,咱們核心工做是將整個框架搭建起來,所以在數據選擇方面咱們主要選擇從對用戶直觀感覺與影響最爲明顯的維度出發進行選擇。
其中的 代碼質量狀況
相信有同窗會提出疑惑,這塊實際上是 FindBugs
、 PMD
、 Lint
這三塊掃描出的不須要強制處理 Warning
級別的各種數量狀況,而對應的 Error
級別的問題,在代碼合併的時候已經被強制處理了,這塊若是感興趣,咱們在《客戶端持續交付工程實踐》中有詳細的進行了闡述。
對於剛剛談到咱們有依賴第三方數據平臺做爲數據依據,主要是將其做爲問題處理平臺,而涉及到數據處理主要涉及這三塊: Crash
、 ANR
、 內存泄漏
。對這三個數據而言咱們不得不依賴一個解決問題的平臺用於對具體問題的進一步分析、歸類,以及對解決進度的標註。其中 Crash
與 ANR
大多數平臺都擁有,而內存泄漏咱們採起的方案是基於 LeakCanary
進行收集而後做爲錯誤進行上報,具體後文會提到。
後端的APM服務的開發,對於服務而言主要就是提供各種RESTful接口提供後端數據庫的數據寫入,一些頁面的展現(如綜合覆蓋率詳情頁)以及文件上傳的支持(如 jacoco
的 ec
文件的收集),這邊因爲咱們Android技術棧的緣由直接使用了 kotlin
基於 SpringBoot
進行快速開發迭代。
開發週期是跟着版本走的,所以這塊是強綁開發流程的,在開發週期不斷的迭代中會進行自動的跟進,而且整個大盤數據維度切換時機是根據以小版本的切換來跟進的,好比 6.8.x
發佈後,大盤所展現的全部數據就會自動變遷爲 6.9.x
,而最後一位的 patch
版本因爲只會在 hotfix
分支上出現,週期很短會有其餘流程跟進,不會在該開發週期大盤中體現。
可以作到對單獨某個版本維度的展現,得益於 Grafana
中可使用 MySQL
做爲數據源,並支持靈活的使用各種 SQL
語句對數據進行塞選,所以咱們只須要在CI上建立週期任務,在當前開發分支按期掃描當前版本,當版本發生變更的時候寫入到對應的版本數據庫中便可,而全部其餘數據獲取數據時,只須要進行聯表查詢取最新版本相關的數據便可。
這裏提到的開發分支在不一樣的階段會有所不一樣,還記得在[客戶端持續交付工程實踐》中咱們提到的開發流程時的那張圖:
這裏能夠清晰的看到,在版本提測前當前的開發分支是 develop
,接收各類類型的合入,而且合入審覈只 OkCheck
跑過,代碼 Owner
與另一位同窗 Approve
便可合入;當提測後,當前的開發分支會變遷爲 release
,一般只接收代碼 fix
,此時也就是隻接收 fix/xxx
的分支合入,而且在 beta
後代碼合入還須要發佈經理的 Approve
。爲了減小你們合併代碼可能照成的誤操做,這邊還開發了 lit
工具,因爲篇幅限制,這個話題咱們就沒有再深刻了(咱們有計劃在明年對這個發佈流程進行較大幅度的調整,之後有機會也會與你們進行分享的)。
咱們來簡單經過一個例子來看下開發週期大盤這塊的數據展現的狀況,假如咱們擁有一個開發版本的表以下:
此時咱們即可以經過取降序限制 1
個結果直接拿到最新版本的數據,做爲當前版本週期:
如上圖,其中 $__timeFilter
函數是 Grafana
提供的,因爲 Grafana
支持設置當前面板所展現的數據的時間範圍,所以這裏的 $__timeFilter(date_created)
等價於 date_created
的時間戳在設定範圍內的條件。
緊接着咱們嘗試拿到當前版本的內存泄漏泄漏個數:
完美,符合預期!其餘的數據也是以此類推的獲取便可。
對於開發週期大盤,咱們但願你們在不一樣的時間點關注不一樣的維度,在開發階段只須要重點關注 包變化
與 代碼質量
狀況,而當提測後因爲代碼趨於穩定,咱們此時應該須要開始關注各種重要的質量指標。
特別是在提測後(當前版本週期進入 alpha0
後),你們須要經過在大盤中根據 參加測試人數
與 綜合覆蓋率
做爲衡量測試完備程度的一個標準,在測試完備程度儘量高的狀況下,咱們跟進其餘的數據指標對相關問題進行修復,修復後因爲代碼的修改 綜合覆蓋率
會降低,此時再經過灰度等方式提升完備程度,再修復問題打磨應用,以此造成一個良性的閉環。
其中咱們經過明確關鍵發佈節點的規則來對這塊的落地,在 alpha
階段測試同窗開始介入對測試完備程度的跟進,在 beta
前測試同窗須要將完備程度提升到咱們在 Grafana
中定義的 SingleStat
圖表中的綠線的閾值。而相同的,開發同窗也須要在 beta
前將幾個基礎指標也提升到對應的綠線閾值之內,在達標並提交 beta
包後,這邊須要保持不管是完備程度仍是各項質量數值都在綠線閾值範圍內便可。
其實在創建APM時,相信每個團隊都會遇到一個問題,那就是咱們如何去權衡裏面所羅列出的每一項質量指標數值所表明的實際含義是好,仍是壞。其實對於咱們而言,這個問題十分簡單清晰,只須要與本身的上一個版本進行對比,所以就有了版本變化大盤。
版本變化大盤即是收集各個版本的綜合表現,以最直觀的形式進行呈現。你們從上圖能夠看到咱們清晰的將版本變化大盤拆解爲了5個部分,囊括了 基礎性能指標
、 關鍵頁面耗時
、 包狀況
、 Qark安全掃描
以及 潛在代碼質量問題
。
全部的數據也是一樣來自 MariaDB
的數據源,主要緣由是版本週期一般來講跨度比較大,間隔也會比較長,而且考慮到咱們有不少聯表查詢的場景,所以這邊便沒有使用 Prometheus
這種偏向於實時監控數據庫做爲數據源。哪怕是有些數據是直接打到 Prometheus
的(如關鍵頁面耗時),咱們也會在適當的時候週期性的根據版本拉回一個綜合數值塞給 MariaDB
。
這邊舉一個案例,咱們須要知道每相鄰兩個包的大小變化狀況:
爲了讓包大小的變動更加直觀,咱們實際須要知道的是每一個版本相對於上個版本的變化值,這裏利用了 SQL
能夠靈活的定義變量並進行先後的比較進行實現。
實時大盤實際上展現的就是最近24小時內,應用的健康狀況,以及線上用戶的直觀感覺,全部數據都是全版本的綜合值。
在數據收集方面,這塊和前面提到的技術選型有着很大的關係。對於數據的獲取,主要採用三種方式:
應用運行時上報: 關鍵頁面耗時
、 運行時Jacocoec
CI週期性掃描上報: 包狀況
、 安全報告
、 代碼質量
、 AndroidTest與UnitTestec
、 綜合覆蓋率詳情
、
應用API調用: Phabricator上遺留BUG數
服務器爬蟲部署: Crash
、 ANR
、 內存泄露
、 冷/熱啓動耗時
、 參與測試狀況
這邊使用爬蟲而非一些站點的提供的API的緣由是由於第三方的平臺提供的API基本上是不符合咱們要求的,而咱們這邊沒法Push對方新增/修改接口,So...要不本身寫平臺,要不爬蟲,因爲人手不足時間有限,咱們選擇了後者。
其中 Crash
、 ANR
、 冷/熱啓動耗時
、 關鍵頁面耗時
是集成在應用中帶到線上的,而 內存泄露
、 綜合覆蓋率相關
是隻有在測試包纔會帶有,其餘的是基本上解耦應用自己的數據分析。
這邊對於數據存儲方面也十分明確,對於實時性很是強的 Crash
、 ANR
、 內存泄露
、 冷/熱啓動耗時
、 關鍵頁面耗時
、 參與測試狀況
、 Phabricator上遺留BUG數
這邊是直接打到Promethues上,而對於在持續有 ec
刷新的狀況下才會15min刷一次的 綜合覆蓋率
、以及天天刷新一次的 包狀況
、 安全報告
、 代碼質量報告
這些便直接打到 MariaDB
便可。不過爲了版本變化大盤以及當前週期大盤的聯表查詢,這邊還有另一個週期性半天任務,就是從Prometheus上掃描全部類型的數據,進行綜合計算後會刷新到 MariaDB
,也就是說實際上 MariaDB
上擁有全部數據只不過沒有Prometheus實時。
P.S. 關於Prometheus的使用以及每一個數據Metric類型的選擇直接參看Promethues的官方文檔便可,Prometheus的各種文檔仍是很是全的。
相信你們在大盤上,可以注意到有一個數據可能不少廠商的APM大盤都沒有包含的 --- 綜合覆蓋率
談綜合覆蓋率的數據整合以前,先和你們說說咱們統計這塊的緣由,其實對於應用發佈而言,如何權衡測試的完備程度一直是一個十分棘手的問題。而綜合覆蓋率自己其實並無神奇的效果,可是他能夠十分明確的作到一件事情,那就是若是綜合覆蓋率是100%,那說明全部代碼都有被執行到,那麼剩下的事情就是咱們如何作到只要存在問題的代碼被執行到,咱們就可以自動化的將其進行上報。這塊其實很好的可以與幾個基礎指標融合,如 ANR
、 Crash
等,這些原本就是遇到了就會自動上報的。咱們經過結合當發生這些問題的時候,當前的覆蓋率 ec
文件將不被上報,來讓覆蓋率這件事情行之有效的在很大意義上體現測試完備程度而且驅動問題修復造成閉環。固然對於一個體系化的集成測試而言,實際上代碼的簡單的單次執行並沒有法暴露全部問題,更多的是與該次執行的上下文有很強的聯繫,這塊就須要不管是接口測試、功能測試以及Monkey等自動化測試的完善了,不過在這裏對於測試完備程度而言一個完善的綜合測試的覆蓋率機制依然是一個相對可靠的參考緯度。
對於開發大盤而言,咱們須要綜合覆蓋率與參與測試的人數、啓動次數做爲測試完備度的一個衡量標準,全部數據都是創建在綜合覆蓋率達標而且參與人數達標的狀況下,才用於其階段性的意義。在早期咱們嘗試推過全量的綜合覆蓋率,可是因爲測試同窗與開發同窗很難就很老的代碼花大量的人力去進行覆蓋,所以咱們結合咱們的開發週期對jacoco進行了定製,實現了基於版本的差分的綜合覆蓋率計算與輸出。
這裏涉及了較多維度的定義,首先簡單來講:
綜合覆蓋率
= AndroidTest覆蓋率
merge UnitTest覆蓋率
merge 運行時覆蓋率
其實,真正的計算方式是:
綜合覆蓋率
= 該版本修改代碼中被執行過的行數的總和
/ 該版本中所修改代碼的總行數
而具體的該版本修改的被執行的行數總和:
該版本修改代碼中被執行過的行數的總和
= AndroidTest中執行到被修改的
merge UnitTest中執行到被修改的
+ 運行時中執行到被修改的
雖然看起來公式彷佛挺複雜的,可是實際上基於Jacoco Gradle Plugin的 JacocoMerge
咱們就能夠作合併的操做,所以只須要分別獲得三種狀況下執行的 ec
文件便可,在具體分析以前對其進行合併,而後在使用 JacocoReport
出報告後針對報告根據 git blame
拿到當前版本修改過的代碼進行二次標註便可。
針對 AndroidTest
與 UnitTest
較爲簡單,咱們在 GitLab
上直接建立週期性的任務進行 ec
文件生成便可,而針對 運行時
,咱們在每次應用推到後臺時主動將當前的 ec
文件上傳到後臺,惟一須要注意的是,多進程狀況,在調用 org.jacoco.core.runtime.RuntimeData#reset
以前,每次經過 org.jacoco.agent.rt.RT.getAgent#getExecutionData()
取到的 ec
文件都是帶有全部的 ec
數據,所以這邊能夠採用同一個進程使用相同的文件名進行覆蓋上傳便可,最簡單的方法就是在當前進程第一次收集的時候生成一個 UUID
,後續不斷複用便可,然後臺 RESTful
接口的行爲須要確保相同名稱文件使用覆蓋的措施。
這邊咱們在設計綜合覆蓋率的時候也並不是一路順風,遇到很多有意思的問題,咱們撿一些與你們分享下:
因爲咱們須要收集的是 AndroidTest
、 UnitTest
、 Runtime
這幾種環境下的 ec
文件,這裏其實有一個頗有意思的問題,就是最終的綜合覆蓋率報告,咱們須要從報告上獲知具體哪些行被覆蓋了,這裏報告內確定須要非混淆的源碼纔可讀,而這裏所產生的 ec
文件倒是來自幾個環境, AndroidTest
與 UnitTest
所執行的是未混淆的環境, Runtime
因爲須要測試同窗的參與、甚至灰度內部大量同窗的參與,咱們確定是須要給到混淆包的,這樣一來理論上來講,這兩種一個來自未混淆包的 ec
,一個來自混淆包的 ec
最終合併而後生成報告確定是對應不上的,後來在咱們來回比對,並解析 ec
文件後發現,經過jacoco注入後的包,jacoco會自動注入一個對當前類的說明:
所以這塊問題Jacoco自己就已經給咱們解決了。
而在作針對版本的差量的綜合覆蓋率時,咱們遇到了一個相對棘手的問題,以下圖:
咱們假設當前版本是 6.9.0
,而下一個版本是 6.10.0
,還記得前面提到的咱們目前的發佈流程,在應用提測以前你們都是在 develop
分支上進行合併代碼,而當應用提測後, 6.9.0
就會遷出 release
分支,而且在 release
分支上進行開發,而 develop
此時的任何代碼的合併都將不會在這個版本帶上,由於此時 develop
分支已經變爲 6.10.0
的開發分支, 6.9.0
的開發分支已經變爲 release
,當 6.9.0
發佈後會合併回 develop
分支,此時 develop
分支依然是 6.10.0
的開發分支只不過合入了 6.9.0
提測後的代碼。
這裏就遇到了一個問題,咱們計算當前版本的綜合覆蓋率時所選取的變動來源,不能是從 A
到 HEAD
,由於這樣一來就包含了 6.9.0
提測後的全部代碼,也不能是 B
到 HEAD
,由於這樣就丟失了 6.9.0
提測期間 6.10.0
並行開發的那部分提交。實際上咱們所須要的是途中綠點的全部變動。這塊得益於咱們以前在作變動集時的相關積累,經過 git
提供的方法,計算出從 A
到 HEAD
的Diff中每一行的 Commit
,而後將屬於紅色這條分支的 Commit
的行過濾掉,這樣就獲得了全部綠色部分修改的行。
Jacoco還有一些版本的兼容問題,好比低版本的Jacoco生成的 ec
文件跨度一大在高版本上就直接解析失敗,好比 0.7.4
與 0.8.2
(這是截止本文發佈的最新穩定版本)生成的 ec
是相互解不開的,所以咱們在接收 ec
文件的時候須要注意作好根據版本區分開。還有另一點, Jacoco
從 0.8.2
開始對 kotlin
作了一些支持,不過 0.8.2
版本須要依賴 AndroidGradlePlugin
的 3.2.1
或更高版本。
最後,除了外層的覆蓋率方面咱們添加了基於版本的覆蓋率,在詳情頁中,咱們經過左側的標註來講明該代碼是當前版本修改的,該標註是超連接到Gitlab上的相關Commit的,其餘部分保留了 Jacoco
本來的面貌。
這些內容其實在《流利說客戶端持續交付工程實踐》中咱們就有提到,只不過當時是經過郵件進行通知的,後來咱們將其上報到APM的 MariaDB
上,若是感興趣能夠在那邊文章進行查看。
前面也提到了,關於內存泄漏,咱們是直接經過 LeakCanary
註冊 RefWatcher
對全部的泄漏進行監聽,其中也包含了 isExcluded=true
的系統級別的泄漏,可是會在上報時進行區分,主要緣由是不管是 Framework
層的泄漏仍是咱們上層代碼邏輯泄漏,泄漏的都是咱們應用的虛擬機,其對用戶的影響也是直接表如今咱們本身的應用上的。
其中存在的問題是,咱們在流程上須要將應用控制在綠線內,所以咱們不能讓系統級別Stick級別的泄露阻礙了應用的發佈,也不能讓Stick級別的泄露被隱藏,所以咱們將一些已知的Stick級別的泄露單獨提Task跟進,而直接將這塊的問題標註爲了解決。
內存泄漏這塊還有一個小坑,咱們剛開始是直接將泄漏的堆調用信息做爲 message
以錯誤的形式上報給Bugly,利用Bugly中根據不一樣 message
歸爲不一樣的錯誤的形式,將相同堆調用信息的泄漏合併起來,可是後來發現,對於不一樣的泄漏,在上報的錯誤中雖然使用了不一樣的 message
可是因爲他們的錯誤棧可能相同(都是走上報的那個棧),致使不一樣的泄漏被合併爲了同一個,這個問題最後咱們將內存泄漏的堆調用信息寫入到棧中後獲得瞭解決。
對於關鍵頁面耗時這塊因爲咱們須要計算的是95線的數值,簡單來講就是95%的用戶所感知的耗時都是在該數值以內,因爲這塊數據方面的特色,咱們分享下其在Promethues上的坑點與解決方案。
這邊很顯然咱們須要使用 Histogram
這個類型,因爲咱們除了95線,在作進一步分析中還須要更多的數據緯度,在各方面的權衡下,咱們對每一個版本的每一個頁面監控取20個 Bucket
,那麼問題就來了,咱們平均發一個版本,整個流程下來 dev
、 alpha0
、 beta0
、 beta1
、 release
,一共5個版本,假定咱們須要監控10個頁面,組合下來就是發佈一個版本總共就須要 20*5*10
= 1000
個新增,而且版本是隨着時間的推移不斷新增的,這樣累計下去將是至關浪費的。
所以這邊咱們修改策略,由於咱們線上在週期中最新的長期只會有一個版本在跑,而且本地的發佈流程常年也只會關注正在開發的版本,所以咱們這邊再也不採用特定版本,而是規定版本只爲兩個: 發佈版本
、 開發版本
,這麼一來總體的數目就被固定了,只須要在版本發佈時,將對應的版本告知數據接收方,刷新 發佈版本
與 開發版本
的定義,存儲關注的綜合數據後,清空數據,而且只接收定義的這兩個版本數據便可。
今天咱們主要分享了咱們在APM大盤這塊的工程實踐,而對於APM而言,擁有大盤只是其中必不可少的第一步,更重要的是對於大盤的使用,咱們經過在幾個關鍵節點的流程上進行落地,在應用提測後,測試同窗、架構團隊、業務團隊分別有輪值的同窗進行跟進相關綠線,測試同窗主要負責完備的測試的綠線,而業務與架構的同窗確保應用質量的綠線,再達成後纔可 beta
;另外咱們的Android團隊有每週的AWTT&Party會議,其中有一趴就是對APM所展示的質量狀況的Review。固然結合APM大盤對測試完備的定義,對應用質量的定義不管是用於咱們如今這套開發流程,仍是將來其餘的開發流程對於應用的快速迭代以及可持續發展都是十分重要的,因爲篇幅有限,咱們就不作細說了,下次有機會再與你們分享,也十分歡迎你們多多拍磚,評論。