從2013年開始,咱們前後進行了不一樣路徑的多樣性架構探索,在實踐過程當中也經歷了各類曲折與壓力,最終實現了2015年的這個全新架構,實現了無線服務端基於API Gateway的架構框架、客戶端的模塊化開發、測試與部署,支持運行期間的模塊實時加載、按需Lazyloding、Remote加載,從而實現模塊級動態升級以及代碼級熱修復,並
且逐步推進數百人的客戶端研發團隊由不堪重負、效率低下的大版本大火車開發模式向模塊間獨立迭代、發佈輕量級的開發方向演進。前端
同時在架構探索期間,攜程作了App相關的不少性能優化,好比底層網絡通道治理的優化、應用層插件容器加載啓動速度以及存的優化、業務中間件Hybrid的優化等等,逐步保證隨着業務的不斷的迭代,能保證用戶的比較好的優化體驗。node
早期App服務端架構使用了傳統的PC無線開發架構,即在PC Web應用基礎上增長一些無線端的REST接口直接供給App訪問,沒有考慮架構的擴展性、 靈活性、安全型等因素。android
如圖1所示,服務端系統一方面以Web應用的方式提供給PC端瀏覽器訪問,另外一方面爲支持移動,在Web應用基礎上增長一些REST接口直接供App訪問。相應地,無線接口和Web應用做爲同一工程開發,做爲同一個應用部署,這種架構設計思路是很直接和天然的,能夠快速把PC端功能複製到App上,其思想設計是在現有Web應用上打補丁,體現的是PC思惟無線化,把App簡單做爲PC端應用的翻版,並把二者物理上捆綁在一塊兒,在早期也能知足當時的業務需求,可是隨着平臺化的發展,以及業務愈來愈複雜和多樣性,這種架構設計帶來的一些列的問題逐步暴露出來,其中最突出的急需解決的有三個問題:耦合、重複造輪子、系統穩定性,具體以下所示:ios
無線接口和Web應用緊耦合,Web端的修改會影響無線接口,Web端的發佈致使無線接口被動連帶發佈,Web端的Bug影響無線接口的可用性,反過來也同樣,無線接口的任何變化會影響Web應用。算法
此外其中酒店無線接口和機票的無線接口,或者其餘BU無線的接口,也存在着較爲嚴重的耦合問題,這種耦合帶來的問題,最嚴重最明顯的就是這個BU的接口調整或者修改Bug,有可能會影響其餘BU接口的穩定型,從而帶來每次發佈,要帶來更多的測試迴歸工做。後端
無線接口除了給App提供業務數據,還須要考慮一系列非功能性因素的接口功能驗證,如通信協議和數據格式封裝、安全控制、日誌記錄,性能監控等,這些對每一個無線接口都適用。若是App和後端系統直連,意味着每一個後端系統都須要單獨支持這些通用功能,致使重複開發。一旦這些通用需求有變化(如對數據傳輸進行加密加強),全部後端系統都要強制同步修改和上線,給項目管理和產品發佈帶來很大挑戰。瀏覽器
App和多個後端系統直連,只要一個系統出問題,就會影響App的可用性,好比酒店服務出了問題好比變慢或者耗用CPU過多資源,其機票服務或者其餘服務會受到必定影響,其典型的弊端就是缺少故障隔離機制,缺乏負載均衡、缺乏監控、缺乏熔斷等影響後端穩定性的問題,致使App的健壯性不好,很是脆弱。安全
基於架構V1.0三個比較嚴重的缺點,因而咱們開始嘗試使用一種新的無線架構V2:基於API Gateway的無線服務端架構。性能優化
基於如圖2所示的無線API Gateway架構,具有以下功能特色。服務器
App實際上和PC端瀏覽器是對等的,PC端應用有服務端,App也須要本身獨立的服務端,兩個服務端都須要針對自身的特色,獨立開發,獨立部署,同時實現邏輯和物理層面的解耦,從架構層面完全擺脫PC思惟無線化。
核心邏輯從Web應用剝離出來,進行服務化改造,服務實現時不區分PC和無線,App和Web應用都依賴於這些服務,一套接口,多方調用。
提供統一的無線網關,全部App調用指向此網關,網關包括通用層、接口路由層、適配層。通用層包括通信協議適配、數據封裝、安全、監控、日誌、隔離、熔斷、限流、反爬這些系統級功能,每一個接口調用都須要一樣邏輯,這些功能統一由網關前置處理,避免重複開發。具體實現時,每一個通用處理邏輯封裝成攔截器,遵循統一的過濾接口,而且作到可配置,網關依次調用這些攔截器,這樣能夠支持通用邏輯的靈活擴展。
無線API Gateway應該目前不少公司都有本身的實現,目前市場上也提供了不少開源項目Zuul、Archaius、Hystrix、Eureka等幫助咱們去實現本身的Gatway。
攜程基於Netflix的開源項目Zuul開發了無線APIGateway架構如上圖2所示,其Gateway的職能是負責接收來自無線端的全部API請求,並將他們路由到正確的目標應用服務器,而且提供限流、隔離、熔斷等功能,保證了無線服務的長期穩定運行,擁有的彈性容錯機制也減小了平常運維工做。同時該Gateway提供了多維度的監控數據,並與報警系統對接,實時監控線上狀況,達到運維自動化。其API Gateway具備的幾個核心職能:路由、隔離、限流、熔斷、反爬、監控報警,具體以下所示:
Gateway支持集中管控的同時,也帶來單點問題。假設後臺某個服務接口,因爲某種緣由,性能有嚴重問題,對應Adapter處理很慢,那麼網關所在服務器的線程很快被耗盡,致使單個接口拖垮整個系統。這種問題,單純經過增長機器,水平擴展網關數量是解決不了的,實踐中,咱們引入了智能升降級機制來快速隔離單個接口的影響,從而實現了接口的自動隔離熔斷機制,其實現原理如圖3所示。
針對特定一個接口,若是在必定時間間隔內(好比5分鐘),它的超時失敗率到了必定比例(好比5%),網關會對該接口作降級處理,隨機拋棄部分流量,好比只容許50%流量經過。下一個5分鐘再評估,若是失敗率尚未改善,容許經過的流量降到25%,以此類推。若是成功率好轉,網關對該接口作升級處理,提高經過的流量比例,爲了快速恢復,通常提高到原流量4倍,而後在下一個時間段再評估是否觸發升降級。
整個過程全自動智能處理(爲防止誤判,可支持人工干預),這樣單個接口出問題,不會影響整個網關的處理能力。
攜程App服務端架構經過一系列的拆分和整合,既優化了公司總體應用架構,又爲App作大作強奠基良好基礎,其帶來的好處是全方面的,增長了架構的可擴展性、健壯性、穩定性、靈活性,而且提升了團隊的開發效率和團隊長遠的收益,其具體表如今:
攜程App的第一個版本在2011發佈,那時候App架構很簡單,基本上就是在傳統的MVC的架構基礎上封裝了一個數據服務層即代理數據層,如圖4所示。
在攜程業務發展的早期,移動App經歷從無到有的階段,爲了快速上線搶佔市場,其移動App開發的MVC架構成了「短平快」思路的首選。
在如上圖4所示的MVC的體系架構中,業務控制層負責整個App中主要邏輯功能的實現;業務邏輯Model層則負責數據結構的描述以及數據持久化的功能;數據服務層做爲數據的代理媒介層,主要負責與Control層進行數據通訊,包括實現基礎框架數據通訊,序列化和反序列的機制等;而移動界面UI View層做爲展示層負責渲染整個App的UI。這種架構分工清晰,簡潔明瞭,而且這種系統架構在語言框架層就獲得了Android和iOS的支持,因此很是適用於App的startup開發。
可是這種架構在開發的後期會因爲其超高耦和性,從而造就龐大Controller層,而這也是一直被人所詬病。最終的MVC都從Model-View-Controller走向了Massive-View-Controller的終點,其最嚴重的結果就是Control層的代碼愈來愈多,在攜程內部不少類,早期都超過了2000行,同時Control層和View層之間存在一些較高的耦合。其對應的App工程結構架構如圖5所示:當時不管ios和android工程,都只有一個工程結構CtripWireless。
單個工程去實現一個App的好處就是各個業務線的接口通訊方便,調用簡單隨意,能夠隨意使用工程中的任何公共和業務組件,而且接入學習成本低。可是隨着業務愈來愈複雜,以及各BU業務通訊交互的需求愈來愈多,其各個BU的業務耦合愈來愈嚴重,這個直接爲後期插件化Bundle架構埋下了伏筆。
基於攜程業務不斷快速發展,後來活躍用戶已經超過1億,日活用戶千萬,很快觸及到了當時Android虛擬機機制的設計缺陷,即移動端在Android上面臨了兩個比較嚴重的問題,這兩個問題致使的嚴重後果就是在2.3的系統裏面,用戶直接都不能安裝和使用。
一是單dex 65535方法數限制,二是線性內存分配器(LinearAlloc)限制。今天的Android開發者看到這兩個限制都不會陌生。前者是由於Android的早
期設計中,對dex文件中方法id用16位整型標記,單個dex文件中的方法數沒法超過65535,eclipse環境中生成不了未作過proguard的deBug apk。
後者則是dalvik虛擬機用來加載類的堆內存大小被硬編碼了,2.3如下是5M,2.3以上是8M,導致App沒法安裝的緣由就是由於這個堆內存被耗盡致使dexopt失敗。
如今來看確定你們都以爲不是問題,由於Google已經給出了一些可靠的解決方案,輔以更加先進的gradle + Android Studio,開發者們可能根本不會再遇到這兩個經典問題,官方的MultiDex分dex機制解決了方法數限制的問題,其中main dex最小化原則,結合dalvik LinearAlloc heap size調整(修改
到了16M),使得dexopt的失敗概率大幅降低。而ART的出現完全再也不存在LinearAlloc這樣的限制。
可是咱們回過來再看,那個在用戶Android 2.3還佔50%的時代裏,是如何經過軟件架構調整解決這個問題的,其中的經驗有咱們值得借鑑和學習的地方。
基於上述咱們遇到的問題,咱們在原來的傳統架構上又作了從新調整和優化,提出了移動端架構V2.0,其主要設計思路就是:
在業務快速發展過程中,發展到5.0的時候App上已經承載了不少業務功能,但其中一些功能用戶使用頻率比較低,而且以前快速試錯被證實效果不佳的一些功能也大量存留在現有版本中。這些不常使用的功能不該該始終佔用程序資源,因此從架構上進行縱向分離,保證主要重要場景的體驗,是這一時期的主要設計思路,這時期的架構設計圖如圖6所示。
要實現這個架構,第一步就是進行各個BU業務線的功能解耦,這個工做花費了整個團隊大概3個月時間3個App大版本的週期去進行。
進行功能解耦的重要思想,就是實行輕重分離,主次分明的思想;在代碼模塊的組織架構上進行重要的調整,保證主要重要的App功能快速迭代和性能穩定,將附屬的使用頻率不高的新功能,使用H5容器進行動態加載,因此在V2.0的架構上,攜程App就是個典型的Hybrid App ,能夠看到剛開始就核心模塊酒店和機票採用Native 進行開發,其餘模塊基本是採用H5去實現。
V2.0架構基礎上,作了一系列的工做就是將App中比較雞肋的功能好比客戶價值和轉化率低的功能轉成H5實現。這樣作的好處就是集中精力去優化Native業務體驗,同時也能減少Android由於方法數超標的限制壓力。
在V2.0這個階段還作了一件事情去解決dex 65535的問題,即將工程項目裏面出現的再也不使用的類和再也不使用的方法進行了集中清理,這樣的好處是代碼也整理乾淨了,若是方法數超出的不是太多的話經過清理就可讓方法數減小到65536如下,同時還清理了不使用的jar包、重複引入的jar包以及對第三方jar包進行瘦身,通常來講jar裏面的方法數最好,清除一兩個無用的jar包就能大大的減小方法數。
同時這個階段還定義了一個原則,一些信息說明展現或者活動優惠頁面,非用戶主流程的頁面都是採用H5去實現,一方面減小開發成本,同時也是爲了應對方法數增多的壓力。
上面三種方法都是從傳統的技術防守的角度即防止引入更多的方法和類,以及在原有工程角度上去瘦身,可是這兩個方法都不能本質上去解決單dex 65535方法數限制App不能安裝的問題,要想根本解決這個問題,就必須減小單個Dex的大小,使用新的技術進攻的手段去一勞永逸的去解決這個問題。
因此接下來作了比較重大的決定就是各個BU進行解耦,每一個BU單獨獨立一個工程,每一個獨立插件有獨立的UI界面邏輯和資源、存儲及網絡通訊數據處理邏輯,經過共用統一的基礎庫接口訪問網絡服務、圖片庫、定位庫等。V2.0架構對應的App工程結構如圖7所示。
在當時爲了完全解決方法數溢出的問題,基於上面解耦的基礎上採用了多Dex分包方案,當時攜程的作法是借鑑Facebook提供的方案去動態分包,將一個apk中的dex文件分割成多個,而後動態加載dex文件。首先簡單描述下Facebook的思路:
攜程與Facebook的dex形式徹底一致,這是由於咱們也是使用Facebook開源工具buck編譯的。
Facebook將加載Dex的邏輯放於單獨的nodex進程,這是一個很是簡單、輕量級的進程。它沒有任何的ContentProvider,只有有限的幾個Activity、Service。
android:name="com.facebook.nodex.startup.splashscreen.NodexSplashActivity">
因此依賴集爲Application、NodexSplashActivity的間接依賴集便可,並且這部分邏輯應該相對穩定,咱們無須作動態掃描。這就實現了一個很是輕量級的依賴集方案。
加載Dex邏輯也很是簡單,因爲NodexSplashActivity的intent-f ilter指定爲Main與LAUNCHER。首先拉起nodex進程,而後初始化NodexSplashActivityActivity
,若此時Dex已經初始化過,即直接跳轉到主頁面。
Facebook加載Dex的方案,其加載流程圖如圖8所示。
這種方式好處在於依賴集很是簡單,同時首次加載Dex時也不會卡死。可是它的缺點也很明顯,即每次啓動主進程時,都需先額外啓動一個nodex進程。儘管nodex進程邏輯很是簡單,可是也須要加載時間100ms以上。可是攜程對這個啓動時間很是敏感,當時推進產品很難會去採用這個方案。
基於這個方案的缺點,咱們在其基礎上進行了優化方案,即能不能主進程直接加載Dex方案,具體定的方案策略以下。
Dex形式並非重點,假定咱們使用當前的Dex形式,即assets/secondary-program-dex-jars/secondary-N.dex.jar。
主Dex應該保證簡單,即相似Facebook,只須要少許與Dex加載相關的類便可,而且這部分代碼是相對穩定。我也無須去更改任何非加載相關的代碼。
這個是重點,咱們應該經過什麼加載方案去實現這樣的分包規則。首先你們明確如果點擊圖標,的確無須再起一個進程是可行的方案,可是問題就在於在Application初始化時,或是在attachBaseContext時,咱們沒法確保即將進入的是主界面Activity。可能系統要起的是某一個Service或Receiver或者Notification,這種跳轉方式是不行的。
如圖9所示,有兩個關鍵問題須要解決:
關於問題1,進程同步可使用pthread_mutex_xxx、 pthread_cond_xxx,可是mutex或cond要放於共享內存中,這種實現方式較爲複雜,因此我最後實現時採用的是一個最簡單的方法即每隔95ms去檢測TempFile是否存在,若是存在則直接進入主程序,同時在加載dex的工做線程中去判斷,若是加載dex成功,則建立TempFile。
關於問題2,在掛起主進程的同時,去啓動一個工做線程去加載dex,也就是這個線程是非UI主線程,不會形成阻塞UI主線程的狀況,通過屢次測試,也確實沒發生ANR現象,這個經過分析ANR現象的本質就能得出這個結論。
基於Facebook的基礎上咱們優化實現了動態加載Dex的方案,比較完美完全地解決了由於方法數超標而沒法安裝的問題,同時也不用擔憂隨着業務發展,代碼中方法愈來愈多的問題。
同時在這個階段,也就是2015年初的時候,攜程開始全面由Eclipse工具遷移到Android studio + Gradle的構建方式,同時因爲Google支持了MutilDex方
案,因此後來就直接使用了官方提供的方案。
V2.0架構解耦以後,不一樣BU工程的依賴是解除了,良好的解決了之前各個不一樣BU相互依賴的問題,同時也能夠支持多個團隊進行並行開發。可是這個階段的階段架構存在如下兩個明顯嚴重的問題:
即會存在若是其餘BU的工程修改了,若是沒及時通知對方人員,全全局報錯,整個工程編譯都沒法經過,影響到其餘BU的正常開發工做。
打包不可配置,構建編譯速度慢,由於攜程BU不少,業務也很全而複雜,大概解耦成有10幾個工程,由於不可選擇因此需全量編譯,因此形成一次構建速度最慢的時候差很少30分鐘,通常10分鐘以上,因此整個開發效率比較低,開發人員的體驗感也比較差。
基於上述缺點,咱們在V2.0的架構基礎上又進行了優化,提出了V3.0的架構,具體的架構圖如圖10所示。V3.0架構在V2.0的工程解耦升級的基礎上去完成了,V3.0架構是基於Bundle的動態加載插件化架構,即幾乎工程中的任何組織形態均可以當作Bundle, 而最終攜程App 由一系列的Bundle組合而成,運行在能夠容納加載的Bundle容器DynamlicLoader中。
如圖10所示,應用層的酒店、機票、火車票等都是一個個獨立的APK,它們之間獨立開發,互相不受影響。最終統一以插件的方式集成到統一的攜程APK裏面。酒店和機票之間通迅方式採起兩種方式,BUS數據總線跳轉 和 URL Scheme跳轉。
V3.0架構對應的工程結構圖如圖11所示。
如圖11所示,現有的工程結構,有超過30個Bundle(apk),而且隨着將來業務的發展,其Bundle是愈來愈多。爲了解決Bundle過多形成編譯速度過慢的問題,咱們採用配置文件去動態靈活配置,各個BU須要使用什麼Bundle,經過簡單的一句配置,將其加到工程中便可,同時其餘不須要打進來的Bundle支持aar(.a)和源碼依賴,按需添加依賴便可。
爲了一勞永逸解決咱們V2.0遇到的Dex方法數超標的問題,咱們內部基於目前攜程App的現狀研發實現了一個動態加載的插件化框架DynamicLoader,支持即時加載,按需加載,遠程加載三種方式。即時加載,即剛開始就直接加載進來,按需加載是使用的時候纔去加載,遠程加載即剛開始沒有這個工程,而後用戶經過遠程安
裝就能夠直接使用這個功能。這種機制同時也支持了咱們後續使用到了Hotfix機制。在這裏首先簡單總結下目前市場上出現了比較著名的開源的插件化框架如表1所示。
表1 市場主流插件化技術對比
如表1所示,攜程在2015上半年開始着手研究本身的插件化框架,同時也對當時市場上的插件化技術作了調研,最終得出結果,當時市場上的主流框架都不能知足攜程當時工程結構的現狀和當時插件化的需求,也就是接入其插件化以後,攜程的各個BU團隊須要不少額外的開發成本去實現總體遷移,同時還不能有效保證後續的插件化穩定性,基於此背景下,攜程的插件化應運而生,其實現原理是經過系統的ClassLoader動態加載類,經過系統的AssetManager去動態加載插件的資源,同時經過修改aapt的源碼去替換系統的Appt解決各BU資源之間衝突的問題。關鍵是各BU原有的代碼和現有的開發模式都不須要額外的去改動從而增長額外的開發成本,插件化的思想即一切皆Bundle組件的思想,每一個Bundle有本身的版本號,經過BundleManager 去管理Bundle的升級。
在V3.0架構推動階段,爲了須要支持按需加載的時候,其Bundle加載的速度,咱們約定了一個規則:即每一個Bundle加載的時間不須要超過500ms。因此須要對大Bundle進行拆分,好比酒店和機票內部又拆分了本身的6個Bundle。
V3.0架構就比較適合中到大型團隊,而且解耦以後,能夠支持多個團隊的並行開發,也能夠知足多個版本的同時開發和發佈。每一個BU團隊所作的工做就是在發佈以前提供一個Bundle便可,而後到發佈集成階段,將其集成到攜程的統一APK裏面。
進入到2015年後,攜程在軟件架構上逐漸趨於平穩。在V2.0原有插件加載基礎上,研究了更多行業內Android應用的技術架構,而且也結合官方MultiDex的實現。
V3.0在V2.0解耦的基礎上,本身實現了動態加載插件化框架,而且在此基礎上增長動態熱補丁功能,經過攜程內部的Hotfix發佈平臺,實現了攜程客戶端補丁版本更新直接覆蓋,用戶無需安裝新版本就能夠將嚴重的Bug修復掉。相似阿里的AndFix熱修復技術框架。
V3.0架構已經能夠支持多個團隊的快速高效並行開發,可是技術永遠在前進,因此將來的V4.x架構咱們還在進一步推動探索中,好比咱們作Native App可否像Web網站同樣隨時部署,即用即取,可否作到跨平臺的體驗良好的Native App開發,可否實現數十個工程秒級部署編譯,從而大大提升開發效率,這些問題是咱們Native開發人員一直在探索追求的話題。
目前攜程正在推動和已經進行的技術架構:
架構是很是值得分享和討論的,好的技術架構可以持續支持偉大的商業夢想。可是不管什麼優秀的可擴展性好的技術架構,都不能脫離於業務而存在,最終都會隨着業務的不斷髮展,而同時其架構也在進行不一樣程度的演進與優化。一個好的架構首先是必須是能解決公司遇到的現實技術問題和符合知足公司目前架構技術現狀,其次能帶來技術性的革新從而引領業務的發展。
其次作架構以前,要想清楚這樣設計的目的是什麼,經過架構設計使程序模塊化,作到模塊內部的高聚合和模塊之間的低耦合,作到基本符合迪米特、依賴倒置、里氏替換、接口隔離等原則。這樣作的好處是使得程序在開發的過程當中,開發人員只須要專一於一點,提升程序開發的效率,而且更容易進行後續的測試以及定位問題。但設計不能違背目的,對於不一樣量級的工程,具體架構的實現方式必然是不一樣的,切忌犯爲了設計而設計,爲了架構而架構的毛病。