不止性能优化,移动端 APM 产品研发技能
2019獨角獸企業(yè)重金招聘Python工程師標準>>>
江賽,聽云研發(fā)總監(jiān),負責(zé)聽云移動端產(chǎn)品的研發(fā)工作。在 OSC 第 55 期廣州源創(chuàng)會上發(fā)表了題為《移動端 APM 產(chǎn)品研發(fā)技能》的演講。現(xiàn)場介紹移動端 APM 產(chǎn)品底層技術(shù)細節(jié)與實現(xiàn)方法, 演示如何通過在代碼中埋點來解決移動 APP 的性能問題 ;分享在實際產(chǎn)品開發(fā)中碰到的問題和一些經(jīng)驗,以及一些技術(shù)細節(jié)。
一、移動 APM 概況
移動端 APM 產(chǎn)品,從字面上來理解,APM(application performance monitor)就是應(yīng)用性能相關(guān)的監(jiān)測,可隨著現(xiàn)在產(chǎn)品的邊界越來越模糊,監(jiān)測的范圍不僅包括 performance,還包括用戶行為,以及在穩(wěn)定性、卡頓、崩潰這些方面的數(shù)據(jù)都有監(jiān)測,已經(jīng)遠遠超過 performance 這一個角度,畢竟產(chǎn)品結(jié)構(gòu)越來越大了。
所以對于這樣一個產(chǎn)品,要做數(shù)據(jù)監(jiān)控和數(shù)據(jù)分析,它的基本前提是什么呢?就是必須要采集大齡的數(shù)據(jù),包括一些基本的數(shù)據(jù)。將這些數(shù)據(jù)放在不同的維度分析。
舉個例子,從網(wǎng)絡(luò)的角度來說,有用戶反饋某個產(chǎn)品在某個運營商范圍接入的情況下,網(wǎng)絡(luò)性能很差。這個數(shù)據(jù)就會直接從報表里面去體現(xiàn),因為會采集到一些基本的網(wǎng)絡(luò)數(shù)據(jù),也會采集到其他的不同的維護數(shù)據(jù),然后這些問題就會展現(xiàn)出來。
從這張圖來看,數(shù)據(jù)是我們產(chǎn)品的一個移動研究方向,而且我們的產(chǎn)品會支持蘋果、Android 還有 Web 這三端。會采集的數(shù)據(jù)包括:網(wǎng)絡(luò)數(shù)據(jù)、交互行為數(shù)據(jù)、穩(wěn)定性相關(guān)數(shù)據(jù)和一些其他的數(shù)據(jù)(例如采集手機的信號。這些數(shù)據(jù)會有一些不同的應(yīng)用,比如說運營商,它在部署各種基站的時候,會有一個參考值,就是哪個地方信號不太好,它會在那里部署基站,但是怎樣知道信號不好呢?不可能在每一個角落都放一臺手機看信號如何。此時我們的產(chǎn)品就可以完成這個任務(wù),移動端可以采集到這些信號,然后根據(jù)不同的地域來分析手機信號分布情況),這就是采集數(shù)據(jù)的大概內(nèi)容。
然后往下細分會有更多類別。例如網(wǎng)絡(luò)數(shù)據(jù),從應(yīng)用層的數(shù)據(jù)來看,主要是采集 HTTP/HTTPS 的數(shù)據(jù),但又不僅僅是 HTTP/HTTPS 數(shù)據(jù),比如說一條 HTTP 請求,假如從 Web 上或者是瀏覽器中輸入一個網(wǎng)址,我們會把所有的 HTTP 請求內(nèi)容分析出來,例如出去包的長度、回來包的長度和 response 的時間等等。如果出現(xiàn)錯誤的時候,還會把 response 的包和頭部信息打印出來,會把 HTTP 協(xié)議請求全部分析一遍,分析字節(jié)大小,響應(yīng)時間,還有錯誤這些情況。然后還會往下分析,比如 HTTP 請求訪問之前需要做 TCP 鏈接的所用時間。
這些數(shù)據(jù)正常情況下是沒有辦法采集的,需要特定的技術(shù),這個也是今天我要分享的內(nèi)容 —— 我們是如何抓取這些底層數(shù)據(jù)的。
還有一個是頁面加載的數(shù)據(jù),頁面的加載包含三種數(shù)據(jù)(頁面加載、瀏覽器渲染和 DOM 加載)。Android 和 iOS 會通過 JS 注入監(jiān)控一些數(shù)據(jù),和監(jiān)測一些頁面加載的詳細數(shù)據(jù)。
關(guān)于交互行為數(shù)據(jù),舉個例子,產(chǎn)品會監(jiān)控用戶在一個應(yīng)用里的一些點擊行為,像一系列的滑動,對菜單的選中。比如說點擊一個按鈕以后,如果它的響應(yīng)時間過長,一般閾值是 3 秒鐘,如果點擊完按鈕 3 秒后才處理完,我們會自動把事件抓取并上報。現(xiàn)在我們還可以做到,當監(jiān)測到卡頓以后,會自動去把當前的操作截屏(可以做一秒鐘 10 幀的截屏)。通過一秒鐘 10 幀的數(shù)據(jù)而生成的動畫,也就能看到卡頓的時候所在的頁面。這個產(chǎn)品暫時還沒發(fā)布,但技術(shù)上已經(jīng)實現(xiàn)了。現(xiàn)在關(guān)鍵的問題是普通的截屏?xí)浅S绊懶阅芎秃碾?#xff0c;現(xiàn)在能做到 1 幀數(shù)據(jù)在 5 毫秒左右,效率非常高,截屏速度也非常快。
關(guān)于穩(wěn)定性,穩(wěn)定性就是崩潰和 ANR(卡頓)相關(guān)的。有一些開源項目可以支持這種需求,所以類似崩潰、ANR 這些數(shù)據(jù)的采集難度不大。
收集了不同的源數(shù)據(jù)以后,就會接觸到不同的維度,這些維度包括地域、運營商、接入方式、設(shè)備、操作系統(tǒng)、應(yīng)用版本以及其他一些維度數(shù)據(jù)。根據(jù)這些維度數(shù)據(jù)和一些自定義的相關(guān)信息,會做特定的網(wǎng)絡(luò)數(shù)據(jù)監(jiān)控。通過這個,就可以看到對應(yīng)的不同源數(shù)據(jù)在不同的維度組合下的結(jié)果,比如可以選擇某一個地方、某一個運營商或者某個設(shè)備在某種接入方式上,它的 HTTP 請求效率,這就是基本源數(shù)據(jù)以及基本數(shù)據(jù)的應(yīng)用。
很多應(yīng)用廠商也嘗試自己抓取這些龐大的數(shù)據(jù),但如果用傳統(tǒng)的方式來做,就意味著需要打很多的點,比如說一段代碼,需要在 excute 進入的地方打一個點,出去的地方也打一個點,同時還要把參數(shù)抓取下來做參數(shù)的解析,這就意味著如果手工來做這種工作,工作量會非常大,因為所有監(jiān)控的地方都要埋點,而且一旦這段代碼發(fā)生變化,也就要重新去修改埋點的代碼,而且重新去埋點,也會導(dǎo)致工作量非常大。
因此做數(shù)據(jù)采集的時候,我們有一個基本原則:盡量不讓程序員做任何事情。添加一行初始化代碼就夠了。那么如何采集到這些數(shù)據(jù)?這就是數(shù)據(jù)采集的基礎(chǔ),自動埋點技術(shù)。這些埋點的操作不需要自己做,會通過程序自動完成。下面介紹幾種自動埋點的方法。
二、APM 實現(xiàn) —— 自動埋點技術(shù)的介紹
主要通過以下的技術(shù)手段實現(xiàn):
下面對每一個技術(shù)細節(jié)展開進行講述:
對于 ByteCode 的處理,支持 Java ByteCode 的注入以及 Dalvik ByteCode 的注入。在內(nèi)應(yīng)用層會提供 Hook 方法來 Hook 分析 C/C++ 代碼,JavaScript 相關(guān)的會通過 JS 注入的方式來采集數(shù)據(jù)。
看起來比較抽象,下面一一展開來描述:
1. 從 Java 源代碼到 Dalvik Bytecode
對于 Android 程序員來說,大部分代碼都是用 Java 寫的,拓展名是 .java 的文件。但真正打包編譯完以后,會生成 apk 文件。如果你把它解壓會看到有一個 dex 文件,因為現(xiàn)在的包越來越大了,可能會有多個 dex 文件,那么這些 .java 文件是怎么變成 dex 文件的,這個過程是如何的?
編譯的過程是首先從 .java 文件到 class 文件,然后 class 文件再到 dex 文件。.java 文件到 class 文件是通過 javac 編譯,然后再通過 Android SDK 下的一個工具 dx 將 class 文件編譯成 dex 文件。
在 Android 的虛擬機里面,正常情況下編譯完以后,Java 虛擬機里面執(zhí)行的是 .class 文件(即 Java Bytecode),但是在 Android 的 Dalvik 虛擬機或者 ART 里,不能直接執(zhí)行 Java Bytecode,因此需要將 Java Bytecode 做一次轉(zhuǎn)換,轉(zhuǎn)成 Dalvik Bytecode。該過程就是使用 dx 這個工具轉(zhuǎn)換的,而且是在編譯的時候完成。其實就是不同的格式表述,.class 文件只是用了另外一種字節(jié)碼的格式來表述。這個東西看似很簡單,但如果了解編譯的過程,就可以做很多的事情。 class 文件生成了以后,還沒有轉(zhuǎn)成 dex 文件這一步,就可以通過 ASM 技術(shù),對 Java Bytecode 進行改寫,從而插入要監(jiān)控的代碼。
下面通過一個實際的例子來講述。
先來看代碼:
Example Java source: Foo.java class Foo {public static void main(String[] args) {System.out.println("Hello, world");?}?public int method(int i1, int i2) {int i3 = i1 * i2;return i3 * 2;} }這段代碼的功能很簡單,里面有一個方法,傳進來兩個參數(shù),先將這兩個參數(shù)相乘,再把結(jié)果除以 2 返回。通過 javac 把它編譯成 Java Bytecode,然后用 javap?可以看到?Java Bytecode 的指令。這是一個很簡單的 Java Bytecode 指令,取得兩個參數(shù),然后做乘積。imul 指令就是 Java Bytecode 的一個基本指令,之后就是把兩個參數(shù)壓棧,imul 指令會 pop 出棧底的兩個數(shù)。
$ javac Foo.java $ javap -v Foopublic int method(int, int);flags: ACC_PUBLICCode:stack=2, locals=4, args_size=30: iload_11: iload_22: imul3: istore_34: iload_35: iconst_26: imul7: ireturnLineNumberTable:line 6: 0line 7: 4可以看到,方法的名字和參數(shù)都沒變。其實 Java Bytecode 和 Dalvik Bytecode 很大的一個區(qū)別就在這里,Java Bytecode 需要借助堆棧來模擬這種操作(乘法、除法),通過棧來臨時存放這些變量,但在 Dalvik Bytecode 里就不是通過棧來實現(xiàn),而是通過寄存器實現(xiàn)。看一個棧的操作示例:
Stack Before?? ?After?? ? value1?? ?result?? ? value2?? ?...?? ? ...?? ?...?? ?(imul指令對棧的操作)先是傳入兩個變量 value1 和 value2,imul 執(zhí)行完以后就把結(jié)果加到棧里邊,這就是一個典型的棧操作。
因為 Java Bytecode 沒有辦法在安卓手機上運行,因此需要將 Java Bytecode 繼續(xù)通過 dx 工具把它編譯成 Dalvik Bytecode。很多時候大家都是通過編譯工具進行編譯,沒有嘗試通過手工進行編譯,建議可以嘗試一下。通過 dx 就可以把 class 文件編譯成一個 dex 文件,然后通過 dexdump 命令,把 dex 文件 dump 出來。可以看到,剛才的 Java Bytecode 里幾行乘法指令,在這就就變成了一行指令。
可以看到,首先指令長度變小了,第二 Dalvike Bytecode 引入了寄存器的概念。而 Java Bytecode 的函數(shù)調(diào)用全部是通過棧來模擬的。這種方式對代碼性能,以及代碼結(jié)構(gòu)大小有影響,而且寄存器本身的性能要比棧高很多。
再看一下,剛剛那三行代碼兩次 pop 操作,一次乘積,一次 push 操作,現(xiàn)在變成這樣一個操作。就是這個指令,經(jīng)過目標計算器,源計算器,操作完以后,存在源計算機,現(xiàn)在變成這種形式。
下面來看一下 Java Bytecode 與 Dalvik Bytecode 的對比:
Java Bytecode 和 Dalvik Bytecode 有什么區(qū)別?前者用的是棧,后者用的是寄存器。
這些對于自動插碼技術(shù)有什么作用?前面提到的指令級插碼又有什么作用?其實這些是基本工作,首先要對Java Bytecode 非常的熟悉,之后要了解整個編譯過程。
這個代碼就是通過動作分析 Java Bytecode 注入的,反編譯出來就是這樣。我們需要分析一些關(guān)鍵的方法,還有特定方法,找到函數(shù)的頭和尾,插入需要的代碼,第一步為獲取開始時間;第二,獲取完成的時間,之后進行上報。像做一些錯誤處理,會對異常進行捕捉,這樣就可以自動分析你的 Bytecode 來做注入。
還有一個特殊的情況,就是需要監(jiān)控的是這個調(diào)用,或者說監(jiān)控這個調(diào)用的反饋值,這些情況都會出現(xiàn)。但所有的變化都是基于對 Bytecode 上下文的理解,然后插入對應(yīng)的指令。這個技術(shù)不是我們獨創(chuàng)的,ASM 技術(shù)已經(jīng)有很多年了,各位可以去看一些開源的 ASM 項目。
還有一個技術(shù),Java Bytecode 注入是我們產(chǎn)品現(xiàn)在主要的注入方法,但是也還有很多其他注入的方法,下面要講的就是另外一種的方式 ——?通過 .smali 注入,具體的邏輯如下圖所示:
通過一些 smali 反編譯工具,轉(zhuǎn)成 smali 文件,靜態(tài)分析這些文件,分析完以后會做代碼的注入,然后重新打包,再加一個簽名就可以了。smali 不是 Android 官方的 Bytecode,是一個開源的 Bytecode。
這些大家都不陌生,做 APP 開發(fā)很多時候會用這些工具幫助分析一些事情。同樣你也可以借鑒一些新的思路,通過這種方式分析 APK。認為存在惡意行為就分析。另外還可以做動態(tài)調(diào)試,把一些參數(shù)打印出來。
比如說寫了一個工程,可以做一個定制,寫一個簡單的SDK。分析一個 APP 的時候,需要分析其網(wǎng)絡(luò)行為,就把 SDK 注入進去,然后打包,之后看網(wǎng)絡(luò)訪問過程當中訪問的什么主機、IP。如果有加密,那就通過另外一個話題對流做解密,一般的情況下,傳輸?shù)臄?shù)據(jù)都可以看到。
2. APM 實現(xiàn) ——?native inline hook
因為 Android 中很多代碼不一定是用 Java 寫的,也可以用 C/C++ 寫。這種代碼不能通過 Bytecode 的方式來注入。看下面這張圖
這是一個普通的調(diào)用關(guān)系,調(diào)用者調(diào)用被調(diào)用者執(zhí)行,執(zhí)行完以后返回。這是正常的處理流程。但如果要監(jiān)測這個被調(diào)用的方法,想要拿到參數(shù),以及這個方法執(zhí)行多長時間,還想知道這個返回值,如何實現(xiàn)?邏輯上很簡單,把被調(diào)用方法頭幾行指令做修改。把指令改成 JMP 指令,JMP 到這個監(jiān)控方法里面,通過 hook 的方式做跳轉(zhuǎn)。這里做參數(shù)、相關(guān)函數(shù)的記錄,做完以后再重新按照這個軌跡返回。
如何做到這一步呢?首先,把頭幾行做跳轉(zhuǎn)。這需要對 ARM 指令,對各種架構(gòu)比較熟悉才能做到。大部分程序員都學(xué)過匯編指令,但遇到的時候覺得很復(fù)雜。實際上并不復(fù)雜,只是接觸的少,其實 ARM 32 指令不多。根據(jù)后面 3 位,4 位可以做區(qū)分。還有一些分值指令,數(shù)學(xué)預(yù)算指令。那么,分析這些指令的時候,首先對于指令架構(gòu)要很熟悉,而且,要知道源計算機,目標計算器在哪里。比如說,最終跳轉(zhuǎn)指令的時候,要知道跳轉(zhuǎn)怎么計算,24 位 offset 怎么跳轉(zhuǎn),24 位怎么轉(zhuǎn)換為絕對地址。如果把基本概念弄明白,不要求會寫,就可以做下面的事情了。
先看一下剛剛說的方法怎么做到的。
需要改寫這個方法的頭兩行指令,頭兩行指令替換成這樣的指令。PC 指令就是當前運行時的邏輯地址,PC 寄存器。因為 ARM 32 會做一個預(yù)加載,這個會指向下兩行指令。如果將 PC 指令減 4,就是變?yōu)?PC 加 4,這個操作是把下一行指令移到 PC 寄存器中。如果改寫 PC 寄存器就實現(xiàn)了跳轉(zhuǎn),雖然只有兩行代碼,但是可以想到這其實要花很長的時間。
這需要了解 ARM 指令,知道這個 ARM 指令執(zhí)行的過程,還要知道通過修改 PC 指令實現(xiàn)跳轉(zhuǎn)。通過改寫頭兩行指令,就可以把它跳轉(zhuǎn)到任何地址。而且這個地址就是 4 字節(jié),32 位,4G 空間。可以跳轉(zhuǎn)到任何函數(shù),但這還沒結(jié)束。后兩行做了以后,要把頭兩行移到另外一個地方。但是,移動指令的時候因為一些指令本身就是依賴 PC 指令,所以要去做指令的修復(fù)。因此更多的工作其實就是在修復(fù)這些被移走的指令。下面的例子是一個 B 指令修改,是寫實際代碼的一部分。
來看一下這一行代碼是什么意思。123,2 個 0 是 8 位,8123,高位是 0,0,F。如果是 31 到 32 位,我們現(xiàn)在取的值是實際上就是取這 4 位,1234,取 4 位的值,通過這一行指令取這個值。然后通過 4 個值區(qū)分這些指令類型。取出來了以后,如果這個是 A,可以看一下 B 指令的方式,1010,這個是 1010,一個是1,就是 BR 指令,跳出去再跳回來。如果無條件這里就是 0,1010 正好是多少就是 10,就是 A,如果是 B指令。B 指令跳轉(zhuǎn)依賴寄存器,首先算出來這個地址,把絕對地址存在這里,頭一行指令在這里。
如果要真正把這個弄明白,可以通過編寫 C 代碼做到。如果做到這樣覺得很有成就感,把系統(tǒng)的 malloc,或者是 new 給 ?hook 住,可以監(jiān)測所有的 native 內(nèi)存申請和釋放。
將 hook 技術(shù)應(yīng)用在產(chǎn)品上面,發(fā)現(xiàn)很多的產(chǎn)品都是依賴這個技術(shù)的。比如安全方面,很多產(chǎn)品也是通過這種方式做的。還有通過這種方式來做一些底層資源修改和調(diào)度,這個可以用在很多的方面。因為技術(shù)是為了產(chǎn)品服務(wù)的,只要把技術(shù)弄明白就可以了,最終還是會產(chǎn)品化。這是像我這種做很多年技術(shù)的人切身的體會。有時候也是會沉迷在技術(shù)里面,總覺得做一些產(chǎn)品的工作就是浪費時間。現(xiàn)在想想,并不如此。
最后一點,前面講的這些,都是一些自動嵌碼技術(shù),包括 Java 應(yīng)用,還有 C++ 應(yīng)用。數(shù)據(jù)都是自動采集的。在編譯時插碼,在運行時使用 hook,這些都可以做,因為產(chǎn)品已經(jīng)很成熟。聽云現(xiàn)在運行著?5 億終端,有一些大的電商類也已經(jīng)在用聽云的 SDK。
舉個例子,想通過聽云對 TCP 層的監(jiān)測結(jié)果來觀察負載均衡調(diào)度情況,同一個主機有一堆 IP,正常情況下是沒有辦法拿到這個結(jié)果的。我們不僅可以拿到 DNS 時間,還可以拿到 DNS 結(jié)果,真實 IP 是什么,通過這些情況可以看到負載均衡服務(wù)器,即調(diào)度出來的結(jié)果情況以及 IP 分布情況,另外還有 TCP 三次握手時間,SSL 握手時間等。
這些數(shù)據(jù)都非常的有用。安卓程序員經(jīng)常糾結(jié)使用哪些網(wǎng)絡(luò)庫,是 urlconnection,還是 okhttp。分別都有什么優(yōu)缺點。這個我們就給你們做了一個強大的技術(shù)驗證。
第一個問題,比如說,在程序里面連著發(fā)了 10 個 request。現(xiàn)在 HTTP 訪問的傳輸層都是基于 TCP,但每發(fā)一次 request 都要做一次 TCP 連接嗎?仔細想想,對于同一個地址肯定沒有必要,這樣做就是浪費時間。然后遇到的就是 TCP 復(fù)用技術(shù),通過這種技術(shù),就可以監(jiān)測對于一個同一個目標地址發(fā)生多少次?TCP?connect 操作,這就知道在這個訪問時間內(nèi)有沒有復(fù)用之前的連接。所以,就可以得出一個指標數(shù)據(jù),即發(fā)生了多少次 TCP 連接。
下圖是 APM 產(chǎn)品
通過這種技術(shù)可以監(jiān)測一些關(guān)鍵指標數(shù)據(jù),因為采取底層原數(shù)據(jù),很多點就會把這個原數(shù)據(jù)還原出應(yīng)用場景,客戶想出來的場景比我們多。這些原數(shù)據(jù)都是最寶貴的數(shù)據(jù),并且最關(guān)鍵的是不需要你再去做額外的工作,也是 APM 的價值所在。
三、總結(jié)
今天講的內(nèi)容比較抽象,講的是研發(fā)過程中的一些經(jīng)驗,技巧和總結(jié)。這個技術(shù)可能對各位現(xiàn)在的工作不會有直接的幫助,因為太底層,但也希望可以給各位對自己工作的方式帶去一定的思考。無論怎樣,還是需要把底層的知識弄明白,畢竟這對于寫代碼有幫助。
轉(zhuǎn)載于:https://my.oschina.net/osccreate/blog/795760
總結(jié)
以上是生活随笔為你收集整理的不止性能优化,移动端 APM 产品研发技能的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL查看数据库相关信息
- 下一篇: 2、MapReduce的job提交启动过