一波三折,APM監控系統對于OSGI架構的探索實踐
原創2017年我們一直在做分布式服務跟蹤系統的升級改造,為了更好的服務于廣大的開發和運維人員,能夠在數以千萬計的微服務系統中快速的發現問題、定位問題。一年下來,接入了上千個系統,快的幾分鐘,慢的不到10分鐘,最多就是處理一下jar包依賴沖突問題,所以能這么迅速的推廣。不過前幾天遇到一個客戶,他們的系統采用的是OSGI的框架。OSGI這個詞相信大部分人是聽過沒用過的。
當我們的監控系統遇到了OSGI架構系統也是碰撞出了激烈的火花,足足用了兩天的時間,才搞定了各種水土不服。我把期間的各種酸甜苦辣記錄下來,希望為奮斗在APM一線的同行們提供一點點幫助。
在整個過程中,我們遇到了四個技術關鍵點:
- 把一個普通的jar包Bundle化。
- OSGI的類加載機制。
- 引入第三方非Bundle化的jar包。
- 通過Activator在Bundle的啟動和停止實現回調。
項目背景
首先我簡單介紹下兩個項目的背景:
監控系統:這是一個利用動態字節碼技術,自動的無侵入的對目標系統實行秒級實時監控,監控內容包括但不限于容量、性能、成功率、調用鏈、應用拓撲等,詳細內容請參考:http://os.51cto.com/art/201709/552641.htm。
目標系統:分為兩部分,第一部分以war包形式發布,部署到Web容器內,接收http請求;第二部分采用OSGI架構,每一個Bundle是一個業務服務(可以理解為Bundle就是微服務)。這兩部分通過servlet的橋接方式連接,其中一些公用Bundle(如slf4j)會放在war包的指定目錄中,每個業務Bundle不需要再單獨引入。這個架構是我在解決了所有問題之后才總結出來的,在這里提前拋出是為了方便理解后邊的內容。
剛開始的時候,我們就把它當做一個普通的系統來接入,把我們的jar包放到了目標系統war包的WEB-INF/lib下,加入啟動腳本,再啟動系統,可以看到我們的jar打印出的啟動日志,然后就沒有然后了,除了那一行日志,沒有任何效果。這個時候,我就意識到了OSGI系統不是這么玩的,于是開始OSGI的漫漫探索之路......
當看到OSGI的每一個Bundle包都是用單獨ClassLoader負責加載時,我仿佛找到了解決方案。
我們把監控jar包打到了每一個業務的Bundle里邊,然后重啟,結果監控成功了,問題解決了。
從純技術角度來看貌似是沒什么問題,然而噩夢才剛剛開始。由于監控jar包打入了業務Bundle內,帶來了兩個比較棘手的問題:第一,技術上每個業務Bundle都需要這么去引入,加大了開發工作量,這個和我們極致的設計理念是不符的;第二,業務上每一個業務Bundle就成為了一個單獨應用,客戶方表示我們這個是一個應用,而不是多個應用。
所以還是要解決之前的問題,把整個目標系統所有的Bundle當做一個應用去接入監控。
我不熟悉目標系統和OSGI架構,客戶開發方又不了解監控系統的原理和實現過程,經過了長時間的各種嘗試和交流,仍沒有絲毫進展。于是我們決定采用一個最原始,最簡單,最笨,也是最有效的方法,從“hello world”開始。
當一個系統出現問題,你又不知道問題出在哪部分的時候,要么把這些“部分”一個一個去掉,直到發現沒有“問題”為止;要么從“零”開始,然后一個一個的加入,直到出現“問題”為止,我們采取了后者。
首先,針對我們的監控jar包去掉基于動態字節碼的自動監控,采用編程式的直接API調用,并且將API接口做了Mock,去掉了所有的第三方依賴。修改目標系統的代碼,在希望被監控的方法中直接調用監控API。打包,部署,啟動,被監控Bundle不能啟動,報錯......
- Missing Constraint: Import-Package: com..................sgm......
經過和對方開發人員的溝通和網上查閱OSGI相關資料,了解到這是一個OSGI打包的規范問題。
之前我們的監控jar包是沒有按照OSGI的規范去打包的,打的只是一個普通的jar包,所以在OSGI框架下,其他的Bundle是無法訪問到我們的jar包中的類,造成啟動報錯的問題。這里遇到了我們第一個要解決的主要問題:
1. 普通jar包Bundle化
先看下邊的兩張圖:
上邊的兩張圖是jar包中META-INF/MANIFEST.MF文件,第一張圖是普通jar包,第二圖是具有OSGI規范的jar包。既然知道了,那這樣的OSGI的包怎么打?我們用maven來編譯,順理成章的就找到了maven-Bundle-plugin的這個插件,使用很簡單,代碼如下:
- <plugin>
- <groupId>org.apache.felix</groupId>
- <artifactId>maven-Bundle-plugin</artifactId>
- <version>3.3.0</version>
- <extensions>true</extensions>
- <executions>
- <execution>
- <id>Bundle-manifest</id>
- <phase>process-classes</phase>
- <goals>
- <goal>manifest</goal>
- </goals>
- </execution>
- </executions>
- <configuration>
- <instructions>
- <Import-Package>org.slf4j</Import-Package>
- <Export-Package>com.wangyin.sgm.client</Export-Package>
- <Bundle-RequiredExecutionEnvironment>JavaSE-1.6</Bundle-RequiredExecutionEnvironment>
- </instructions>
- </configuration>
- </plugin>
在這么多屬性中, Import-Package和Export-Package尤其重要,一個jar包就是一個Bundle,Bundle和Bundle之間的訪問全靠這兩個屬性來控制。
Import-Package:說明了這個Bundle jar需要調用外部其他Bundle的哪些包(package)。
Export-Package: 說明了這個Bundle jar可以提供哪些包(package)供其他Bundle去調用。
當一個Bundle啟動的時候,會分為Resolved(解析),Installed(安裝),Started(激活)等幾個步驟,解析就是檢查jar是不是正常,安裝的時候會把Export-Package中的指定的包進行注冊,這就知道哪些包由哪些Bundle來提供,激活的時候會檢查當前這個Bundle的Import-Package依賴的包是不是都有對應的Bundle來提供,否則激活失敗。
修改監控客戶端,把父項目,子項目,子子項目全部按照OSGI規范打包,又是一遍部署重啟,這次被監控的業務Bundle沒有報錯,而是監控客戶端啟動報錯:
- Missing Constraint: Import-Package: org.slf4j
有了剛才的經驗,一看就知道是沒有slf4j,可是對方開發人員反饋slf4j是有的,他們自己也在用,這就奇怪了。
這時我發現還有在監控客戶端里MANIFEST.MF的一句話org.slf4j;version="[1.7,2)",version這個詞引起了我的注意。
原來在目標系統里的slf4j是1.5版本,而監控客戶端要求1.7版本,slf4j降級再試,這次目標系統成功調用到了監控客戶端的API,打印出了日志,萬里長征終于邁出了第一步。但這只是一個Mock的版本,真正的監控程序還沒有加入。
先小結一下:首先在OSGI的框架下,所有jar必須按照OSGI的規范去打包,否則不能使用;其次要對Import-Package和Export-Package配置好,需要依賴外部哪些包,自己又可以提供哪些包供外部使用,同時注意版本,依賴時一般都是要高于哪個版本。
接下來,就要加入真正的代碼跑一跑,根據前面的經驗還是要慢慢來,我們分兩步加入代碼,第一次先加入監控代碼,待成功后加入網絡傳輸部分的代碼,后邊遇到的坑讓我們發現這個決定是正確的。
打包部署啟動調用這個流程已經很熟練了,報錯還是如期出現了,這次報錯的是com.lmax的disruptor這個第三方開源jar包:
- Missing Constraint: Import-Package: sun.misc
納尼?sun.misc沒有?這不是jdk里的嗎,為什么會沒有呢?于是,又是一番資料查找,終于明白了其中的道理。
這里就要講到OSGI的類加載機制了,它并不是我們常說的雙親委托機制。關于這個問題,網上已經有很多文章介紹了,但這里我還是結合這個例子簡單的說一下。
2. OSGI的類加載機制
首先每一個Bundle(jar)都會被一個單獨的ClassLoader去加載,當一個Bundle的ClassLoader嘗試去加載一個類的時候:
- 如果這個類的包名是java.*開頭的,那么直接交給bootstrap ClassLoader去加載。
- 查看這個類是否在OSGI的配置文件中有org.osgi.framework.bootdelegation屬性定義,如果定義了,交給bootstrap ClassLoader去加載這個類。
- 查看這個類是否在OSGI的配置文件中有org.osgi.framework.system.packages屬性定義,如果定義了,交給父類加載器去加載,一般就是AppClassLoader。
- 查看這個類是否在本Bundle的MANIFEST.MF文件的Import-Package中定義,如果定義了,交給Export-Package這個類的Bundle去加載。
- 在上邊條件都不滿足的時候,那這個類就是自己的Bundle的內部類,由自己的ClassLoader去加載。
現在我們來看一下,下邊這張圖是disruptor的MANIFEST.MF文件:
在disruptor里使用了sun.misc.Unsafe類,在啟動disruptor的時候,需要加載sun.misc.Safe,那么1,2,3點都不滿足,命中了第4點,就開始尋找帶有Export-Package的Bundle,那肯定找不到。
遵循上邊的原則,在配置文件中加入了org.osgi.framework.bootdelegation=javax.*,sun.*,同時又把disruptor中的Import-Package刪掉了,問題成功解決了,又向勝利邁進了一步。
接下來,就要把網絡傳輸這部分加入進來了,系統就可以監控了,數據需要發送出來才可以真正的使用。有了之前的經驗,感覺應該問題不大,然而現實情況并不是這樣……
當把這部分代碼和依賴加入后,各種的Missing Constraint: Import-Package: xxx。我發現所有的依賴的第三方jar都在里邊,為什么還會報錯,我開始懷疑是這些第三方jar本身的問題。
打開這些jar文件的MANIFEST.MF文件查看,果然如此,我們依賴的這些jar有多一半都不是Bundle化的jar包,這里就涉及到了本文第三個要解決的核心問題。
3. OSGI如何加載第三方非Bundle化的jar包
OSGI如何加載第三方非Bundle化的jar包,有如下幾種方式:
- 通過父類加載器加載,也就是配置org.osgi.framework.system.packages。
- 將jar轉換成Bundle,然后Export-Package。
- 把jar打包進引用方的Bundle。
第一種方式需要目標系統配置,同樣不符合我們的設計理念,顯然不合適,于是我們首先嘗試了第二種方式,重新打包那些非Bundle的第三方jar。
在這個過程中,我們發現這絕對是個苦逼的活,需要找到源碼,下載源碼,修改pom,有的找源碼很費勁,有的還是ant編譯的,有的雖然是maven管理,但又依賴了父項目……
總之想順利的重新打包是個很費勁的事。看來只剩下第三條路了,這又要退回到之前第一個問題,如何打一個Bundle jar。
之前我們是把maven工程的每一個子項目分別Bundle化,如果要把第三方jar打入Bundle,那就有可能一個第三方jar被多次打入不同的子項目Bundle,造成浪費。
所以決定放棄對原有項目的每個子項目單獨Bundle化的方案,而是新建一個子項目,由這個子項目引入所有的其他子項目和第三方依賴jar,把他們所有打成一個大的Bundle jar。
- <Import-Package>org.osgi.framework,org.slf4j</Import-Package>
- <Export-Package>com.wangyin.sgm.client</Export-Package>
- <Private-Package>com.wangyin.*,com.lmax.disruptor.*,……
- </Private-Package>
看一下上邊的配置,比之前多了一個Private-Package,就是說哪些Package是這個Bundle的內部包,也就是要打入最后Bundle jar的東西。
現在通過編程式API直接調用的方式已經可以監控到目標系統了,最后要做的就是引入運行時字節碼增強技術。
還是按照常規的方式,把我們的Agent通過javaagent方式啟動,有了之前的經驗,我知道這個是被bootstrap ClassLoader加載的,于是就直接在org.osgi.framework.bootdelegation中加入了監控Agent的包名。
結果啟動正常,但是無法自動監控,看了日志后發現是監控客戶端沒有啟動。監控客戶端的啟動是通過在每個類加載的時候,嘗試性的使用它的類加載去加載監控客戶端的啟動類。
如果可以加載上,那么就啟動成功了,因為啟動程序是放在了啟動類的static塊中,且啟動類是一個單例模式,記錄著被監控應用的信息。
從這個啟動日志來分析,被監控系統有200多個Bundle,每個Bundle啟動都去加載監控客戶端,然后我們希望監控客戶端被它自己的類加載器去加載。這里就是本文第四個核心問題。
4. 如何在Bundle啟動的時候去做一些初始化操作
在OSGI的規范里提供了一個叫BundleActivator的接口,里邊有start和stop兩個方法,顧名思義,在Bundle啟動和停止的時候會回調這兩個方法,這就好辦了,我們可以在start方法中實現啟動監控客戶端的代碼。
- <instructions>
- <Bundle-Activator>com.wangyin.sgm.client.Activator</Bundle-Activator>
- <Import-Package>org.osgi.framework,org.slf4j</Import-Package>
- <Export-Package>com.wangyin.sgm.client</Export-Package>
- <Private-Package>com.wangyin.*,com.lmax.disruptor.*,org.apache.flume.*
- ,org.apache.avro.*,com.thoughtworks.*,
- ,org.apache.commons.compress.*,org.apache.commons.lang.*
- ,org.codehaus.jackson.*,org.jboss.netty.*,org.apache.velocity.*
- ,org.xerial.snappy,org.tukaani.xz.*
- </Private-Package>
- <Bundle-RequiredExecutionEnvironment>JavaSE-1.6</Bundle-RequiredExecutionEnvironment>
- </instructions>
上邊的Bundle-Activator這個標簽,指定這個Bundle的Activator是哪個類。
至此所有的問題都得到了解決,這次OSGI應用接入APM監控足足花掉了兩天的時間。
由于負責監控系統的人員并沒有使用過OSGI,被監控目標系統的開發人員也不知道監控的原理是什么,一開始我們都以兩個產品整體去接入,做了許多的無用功,耽誤了很多時間。
從這個案例可以看出,當你所做的東西需要應用到一個你不熟悉的技術領域的時候,又不可能有足夠的時間去學習這個領域的知識,有一個最好的辦法就是改造你所做的系統,從零開始,逐漸加碼,去適應那個不熟悉的領域。
千萬不要想著能整體一下解決所有問題,因為可能要解決所有問題有100個技術點需要去修改,這100個問題同時暴露,你只有把它們同時都修改了,才能看到你的成果,這是根本不可能的事情。
而這100個問題一個個暴露出來,把一個不可能完成的大任務拆分成若干個可完成的小任務,修改一個,看到一步成功的效果,問題就得到了解決。
作者簡介:
張晨, 資深研發工程師,目前任職京東金融,曾任職于搜狐等互聯網公司,擅長Java底層技術的研發及疑難問題的定位。從2015年開始從事智能運維監控平臺的研發與實踐,參與并主導了APM等產品的研發與應用,經歷了多次618和雙11的千萬級TPS的運維保障,支撐了京東金融的大量業務應用。
沈建林,曾在多家知名第三方支付公司任職系統架構師,致力于基礎中間件與支付核心平臺的研發,主導過 RPC 服務框架、數據庫分庫分表、統一日志平臺,分布式服務跟蹤、流程編排等一系列中間件的設計與研發,參與過多家支付公司支付核心系統的建設?,F任京東金融集團資深架構師,負責基礎開發部基礎中間件的設計和研發工作。擅長基礎中間件設計與開發,關注大型分布式系統、JVM 原理及調優、服務治理與監控等領域。