蘑菇街11.11:移動流量猛增,如何設計高并發多終端的無線網關

coder_zxt 8年前發布 | 13K 次閱讀 移動設計 軟件架構

自從2011年蘑菇街上線,蘑菇街一直沿用的是以PHP為核心的業務系統架構。但是,隨著業務的增長、業務邏輯的復雜化,對技術架構有了更高要求。另外,隨著移動互聯網的普及,大量用戶流量從PC端到無線端快速轉移,故而移動端架構在保證穩定性的前提下,支持高效開發和迭代顯得尤為重要。

 

而且,電商的大促業務越來越常態化,雙十一作為一年一度的電商大促高峰,更是一年比一年火爆。蘑菇街2016雙十一, 推出買手(紅人)購物清單、紅人買手直播、實時榜單等新功能。這些新的變化對技術上高并發、高可用的考驗自然越來越大。

MWP無線網關的設計

美麗聯合無線平臺(Meili Wireless Platform,以下簡稱MWP),是美麗聯合集團為無線端開發的技術平臺,它主要由無線網關以及圍繞它開發的一系列技術組件和產品組成,是一套覆蓋包括Android、iOS、H5等各類型無線終端技術組件和服務端通用業務開發框架在內的技術解決方案。

MWP面臨的問題和挑戰

基于PHP的架構是蘑菇街早期使用的,當時開發人員較少,業務邏輯相對簡單,在業務迭代過程中更多地將就快速迭代試錯,對于業務邏輯之外的系統優化相對比較少人關注。

 

上圖中,客戶端發起HTTP請求到公網Proxy,由Proxy將請求轉發到業務服務器上,所有蘑菇街的業務都集中在一個PHP服務中,請求路徑非常簡單,基于這種架構的開發和運維也非常簡單。從代碼角度講,所有業務代碼都在一個工程里,相互調用都是簡單的內部調用,使用非常方便,從系統角度講,基于這種簡單的系統架構排查定位問題也會降低很多難度,可以快速反應。

 

上述的架構在早期比較長一段時間都在使用,后面嘗試過在服務化下對業務應用做一些簡單的隔離,但是沒有對架構有根本性的改變。這樣的架構在當時的確行之非常有效,但是隨著業務和團隊成員的增長,越來越多的問題暴露出來。

  • 整體性能較差

從宏觀的角度來看,對用戶來說,性能上其中一個明顯的表現就在于客戶端請求的RT上。有研究顯示,移動端用戶在點開一個頁面的時候,如果RT超過5秒,有74%的用戶將會選擇離開頁面。在原架構中,客戶端請求鏈路只支持HTTP短連接的方式,短鏈接的建立和釋放會消耗很多時間,特別是移動端用戶,在復雜的無線網絡環境下,帶寬資源非常寶貴,頻繁地建立HTTP連接,所消耗的資源和時間成本會非常高,基于原有的架構優化的成本會非常高。

  • 存在嚴重的安全風險

上圖中的HTTP請求鏈路是一個裸露的鏈路,架構層面沒有機制對請求做任務安全校驗,如果黑客修改了請求中的數據,業務也能正常走下去,而業務上如果需要接入這種安全校驗,比如支付、交易這種高危業務,則需要自己去額外接入。

  • 開發效率隨著業務復雜度和人員的增長快速下降

上文提到老架構中,各個業務都在一個工程中,開發人員少的時候開發起來會很方便,但是當業務膨脹之后,里面的大部分業務都從之前的一兩個人維護轉變為一個團隊在維護,在局部代碼里會同時有多人在并行開發,隨之而來的是溝通成本變高,代碼變得臃腫,線上故障也隨之頻發。隨著代碼臃腫復雜,也給新的業務迭代帶來成本的提高,開發的效率急劇下降。

下面我們來看一下MWP通過怎樣的設計來解決這些問題的。

整體設計

MWP提供從客戶端到服務端的一整套技術解決方案,包括如下內容。

  • 客戶端SDK,為各客戶端接入MWP而提供的客戶端SDK。
  • MWP-Router,主要為服務端應用提供統一的服務暴露方式,并為客戶端的請求提供路由分發機制。
  • Actionlet框架,為基于MWP的服務端業務應用提供統一的開發框架。
  • MWP-DSL,是MWP提供的一套面向業務數據的無線端、前后端分離解決方案。

客戶端SDK

MWP-SDK作為客戶端上業務訪問后端服務的入口,與服務端的架構相對應,分層上將通用網絡層和上層應用層分開解耦,最底層的通用網絡層包含建聯策略、HttpDNS、自有協議等網絡優化需要的各方面,上層應用層包含對API請求邏輯、DSL的封裝等,這樣的分離,對網絡層的長期優化是非常必要的。我們將網絡能力整體封裝成了標準的SDK,一方面將整體優化的收益推廣給App群享受,另一方面也將Native通道的能力暴露給H5。

在應用層,基于Pipeline靈活的編排和擴展能力,方便集成了客戶端鑒權、離線緩存、防刷、時間糾偏、狀態管理、性能上報等功能,實現了不同的網絡協議之間實現靈活的切換和降級重試,橫向上與其他客戶端基礎組件打通,比如,與配置中心配合實現配置的準實時下放等。為了解決頁面請求過多、接口回調嵌套等問題,實現了MWP-DSL的調用方式,將原本客戶端對數據的處理邏輯放到服務端的DSL層,解放服務端開發,合并客戶端請求,減少頁面上的網絡請求損耗。

應用層的功能擴展和優化都基于網絡層的穩定性和安全性上,所以網絡層的優化顯得尤為重要。由于移動網絡的差異化和多樣化,使得客戶端的網絡環境問題依然嚴峻,我們都或多或少遭遇到各種域名緩存、內容劫持、用戶跨網訪問緩慢等問題,網絡安全性面臨考驗。MWP的動態調度集成了HttpDNS組件來解決經常遇到的這些問題。動態調度通過策略的下發來控制客戶端使用的協議(長鏈、短鏈、是否加密等)、端口等,通過策略優先級選擇、不同網絡環境下策略表的緩存和后臺跑馬等方式對建聯策略進行優化。

在初期架構中,我們只對重要的接口使用HTTPS,因為傳統的HTTPS的整個握手流程是非常繁重的,尤其是在復雜的無線網絡環境,往往造成建鏈過慢,甚至超時的情況;但是從安全的角度考慮,又必須對用戶數據的傳輸建立在一個安全加密的通道之上。為了解決兩者的平衡,我們加入了安全網關接入層,接入層基于長鏈和自有協議進行數據的傳輸,并通過合并請求、證書預置和優化加密算法實現了一套基于TLS1.3的0-RTT加密機制。在建鏈的效率和數據安全上找到平衡,在不犧牲用戶體驗的基礎上,達到了安全傳輸的目的。另外也正在嘗試接入HTTP2.0協議,為客戶端網絡層帶來更多的優化。

MWP-Router

MWP-Router(以下簡稱Router)是MWP的路由層,它提供多種接入方式及RPC泛化調用方式,基于Servlet 3.0和Pipeline機制提供了高性能高可用的路由服務。

作為蘑菇街無線業務的入口,性能和穩定性是最重要的指標。Router是基于Servlet 3.0和Actor模型的全異步架構,AsyncContext和Event-Loop充分發揮了現代cpu的性能,在隔離各個請求資源的同時,用極小的內存換取了最大的吞吐量,靈活的Pipeline機制提供了強大的流程編排能力,結合RPC泛化調用提供了一整套標準的API服務。

Router的其他特性如下:

  • 通過構建符合協議標準的頭部信息可以方便集成鑒權、防刷、緩存、時間校準及配置準實時下放等特性。
  • 管理后臺通過精細化的配置來管理App和API,包括路由、安全、流控、權限、別名等。
  • 提供定制化的DSL,客戶端開發可以根據自己的業務場景任意組裝和處理后端服務的元數據供端上展示使用,包括但不僅限于API聚合、API依賴分層、數據分段返回等。

在最初的架構中,Router是基于HTTP協議的,重要信息API使用HTTPS。眾所周知,在無線網絡復雜而惡劣的環境下,數據安全和用戶體驗很難取得很好的平衡。為了解決以上問題,最大程度保證用戶體驗,我們增加了網關接入層來管理連接。接入層使用自定義協議和App建立長連接,基于我們自己實現的TLS1.3 0-RTT機制來保證建連的效率保障數據的安全,配合session-ticket-reuse、證書預埋、App加固等機制保證了協議本身的高效穩定及安全性。接入層緩存了部分基于連接的協議數據,對于優化網絡io的效果也非常明顯。另外,我們也接入了SDPY協議,并正在嘗試接入HTTP2.0協議,期間對Nginx性能調優、內核參數調優、協議參數調優等都積累了大量的優化經驗,針對當下流行的微信小程序,后續還會接入WebSocket協議。

Actionlet框架

Actionlet框架的目的

MWP為內部調用定義了API泛化調用方式,后端的業務應用若需要接入MWP需要遵循這種調用方式,所以MWP提供了Actionlet框架,為業務應用提供接入MWP的快速便捷方式,同時也為各業務應用帶來一些額外的好處。

  • 規范化業務對外輸出接口,所有接入MWP的業務都需要按照一定的規則,有統一的輸入和輸出方式,這也是方便后續對API和應用進行統一管理的前提條件。
  • Pipeline等模式隔離開環境和接入方式對業務邏輯的侵入,如果沒有Actionlet框架,各業務開發需要關心請求上下文信息,比如Servlet上下文等,這樣可以提高Actionlet業務代碼在多端接入方式(Android、iOS、H5等)下的復用。
  • 統一的Actionlet框架可以為一些通用的橫向邏輯提供統一實現,各業務開發只要高度關注自己的業務邏輯即可,而不用每個業務都需要接入依賴甚至自己實現這些邏輯,比如用戶Session的處理就是一個很好的例子。

Actionlet框架的技術挑戰

對于一個對外提供服務的業務應用,最關心的應該是服務的輸入和輸出,而各個不同業務API的輸入輸出又會有很大差異。比如,一個注冊接口需要輸入用戶名、密碼和其他用戶信息,而返回的是是否注冊成功的結果,而一個商品列表頁則需要輸入商品類型,返回的則是一個商品列表以及商品內部詳細信息,甚至對應用戶信息的復雜數據結構。在Java這種強類型語言中,如何抽象出一種統一API的規范,滿足各種各樣不通的業務,又能為外部提供統一的接口模型?

在Actionlet框架的目的里我們提到,業務應用本身應該是關注純業務代碼的實現,但是接入Actionlet的業務應用又是面向最終用戶的。那么這里就會有一個矛盾,面向最終用戶的接口必然會帶上環境的上下文,比如,走Web請求就會有Servlet相關的上下文,甚至HTTP的上下文。怎樣處理這些上下文信息,讓業務開發能完全關注業務代碼的實現,而不用花很多心思在處理請求的上下文上?

在接收到請求時,系統需要處理很多邏輯才會走到業務代碼中,比如參數的解析、用戶Session的校驗、API路由的選擇等,這些邏輯串聯在一起作為請求處理的前置流程,框架以怎樣的方式控制這些流程的執行,又如何支持后續在這些流程中添加或修改?

Actionlet框架的設計

上圖中,由MWPBaseService接收請求,下發給ActionletExecutor,ActionletExecutor作為真正的執行器入口。如果要使用整套Actionlet的框架,所有的請求需要由ActionletExecutor為入口來執行,再經過一連串的Valve流程,Valve可以簡單理解是攔截器,實際是閥門配合Pipeline做到對流程的控制,最后調用執行具體的業務Actionlet。

  • Pipeline和Valve

Valve是Pipeline中的概念,而這里詳細提出來,是因為Actionlet的執行流程中很多功能是通過Valve來實現的。比如,請求的路由RouterValve、請求的執行InvokeValve,都是Valve。

那我們是如何通過Valve來對流程進行定義和控制的呢?其實默認的ActionletExecutor就是基于Pipeline來實現的,它在初始化的時候就預先定義了一組Valve,在請求進來時依序執行各個Valve。如上圖中,接收到請求后會依次執行RouterValve、ParameterValve、SessionValve、InvokeValve,而如果后續擴展想改變流程或在流程中加入另外自定義的流程就非常方便了,只要在流程定義的地方修改就可以。

Valve的排列順序也是有要求的,因為請求是從第一個Valve執行到最后一個,再從最后一個執行到第一個,這是一個責任鏈模式。但是和攔截器不同,Valve本身還可以做一定的流程控制,比如直接breakPipeline,或直接goto到某個Valve。

  • Actionlet的具體設計

首先,我們先來看一段Actionlet的接口定義。 

從上面的定義中,我們約束了Actionlet的入參parameter和返回的接口ActionResult,強制約束了入參和返回結果只有一個,業務方可以自由定義自己具體的Domain來作為輸入和輸出,這樣做方便使用規約的方式來對外暴露接口,減少要對參數做映射的工作量。而負責在請求Request和返回結果的Response中,這兩個Domain將會被序列化和反序列化成Json來進行傳輸。

那么有人可能會有疑問,大部分業務在獲得自己業務輸入之外,還會需要一些額外的請求信息,比如客戶端來源,甚至HTTP頭等數據,在這么嚴格的封裝之下,如何拿到原始的ActionRequest和ActionResponse呢?可以通過Actionlet的上下文ActionletContext來獲取,因為目前Actionlet都是同步的請求,所以請求的上下文放在ThreadLocal中。

  • 上下文的隔離

上圖中ActionletExecutor配合ActionRequest和ActionResponse,就是為了將環境的上下文抽象出來,從而使Actionlet能更專注在純業務代碼上。

其中,ActionRequest的作用就是將環境上下文中的請求給抽象成通用的模型,比如Servlet中ActionRequest就可以解析HttpServletRequest中的參數,從而封裝成可以被Actionlet直接使用的Request。而ActionResponse就是將Actionlet返回的數據結果進行對應環境的輸出,比如,Servlet中ActionResponse會將結果進行渲染然后輸出給HttpServletResponse。ActionletExecutor就會將整個流程串聯起來。

因為Actionlet的業務邏輯可能會對接多個環境實現,那么就可以針對不同的環境來實現不同的ActionletExecutor和相應的Valve,來達到對環境的隔離。

  • 異步Actionlet的支持

上文提到的業務Actionlet都是同步場景下的Actionlet,在大部分場景下同步Actionlet已經滿足絕大多數業務的請求,而在很多高并發場景下,異步Actionlet會是更好的選擇。Actionlet框架提供了后續提供異步Actionlet的擴展,只需重寫現有Actionlet調用的方式即可,對代碼侵入性也比較小。

MWP-DSL

MWP-DSL在MWP中提供一套DSL,針對無線端(Android、iOS、H5)中和展現層強相關的業務數據的組裝、拼接和轉換,集成原有服務端部分Control層代碼和客戶端View層的代碼,本質上是一套面向業務數據的無線端前后端分離解決方案。 

服務端、客戶端開發對接場景

客戶端沒有太多的Control邏輯,也沒有太深的回調嵌套回調,服務端的Control層做了很多直接瑣碎的直接關系展現層的數據的組裝、拼接和轉換。客戶端一般只有一個大的Callback,數據拿來后直接Mapping,同時整個Activity都會依賴這個Callback。

這種場景下的問題是,客戶端沒法做到分塊加載渲染和BigPipe,同時依賴一個大的Callback,如果后臺有任意接口超時會等待很久,此外服務端同學不能專注自己的Module,任何小的需求改動(包括不需要后臺提供數據Schema無變更的場景)都需要服務端同學參與,并聯調。

服務端同學不為客戶端的個性化展示需求做適配和拼接,客戶端同學需要自己去調用多個不同服務提供方的多個接口,將Control層的邏輯已Callback嵌套的方式寫在客戶端。

在這種場景下,客戶端同學代碼Callback嵌套嚴重難以維護,三端Control層代碼沒法復用,任何業務上微小的改動都需要客戶端同學發版。

MWP-DSL目的

  • 工程上

  1. 客戶端MVC強制分離,避免callback嵌套,提高客戶端代碼可維護性。
  1. Android、iOS、H5三端Control層邏輯復用。
  1. 客戶端Control層邏輯變更不依賴發版,控制力更強。
  1. 專人做專事,面向業務數據的無線領域前后端分離方案,后端同學專注Module層,客戶端同學專注在View層和Control層。甚至只要業務需求沒有底層數據Schema的改變,完全不需求服務端同學介入,只要相應客戶端同學自己組合下數據接口就好,減少前后端聯調成本。
  1. 有限的DSL,提高上手速度,可維護性、安全性等。
  • 技術上

  1. MWP-DSL具備熱更新的能力。
  1. 通過BigPipe支持分段返回,從而支持客戶端諸如Lazy Load等,提升客戶端用戶體驗。
  1. DSL對于MWP異步化和并行改造,提升整體接口性能。
  1. 所謂微服務的可能落地方式。

業界現狀

Fackbook GraphQL專注于提供面向業務數據的一種新的數據查詢和檢索方案,關注點在客戶端數據查詢的易用性,本質上希望客戶端直接通過寫類似SQL的方式(但是比SQL更直接,類似于面向數據JSON)來對后臺數據(把后臺的一個接口類比于數據庫中的一張表)做過濾和查詢。

相比之下,本質上MWP DSL支持的業務場景更為復雜。

  1. DSL包含大量的業務邏輯,也就是if else和for。
  1. 同時,我們對于元數據的新增和變更比較靈活,而不僅僅是數據的篩選。

所以,和GraphQL的異同可以理解為MapReduce和Hive的區別。

MWP-DSL的挑戰

  • 業務上

  1. 真實業務場景足夠復雜,會出現任意N個MWP接口隨機組合和callback情況。
  1. MWP-DSL提供的能力如何即受限又足夠,同時易擴展,并且易用,也就是學習接入成本低
  • 技術上(主要是性能、穩定性與安全)
  1. 從輕量級MWP請求轉發到多MWP組合并運算帶來的系統壓力。
  1. 為了做到非阻塞、全異步編碼,帶來的排查問題、線程模型與調度復雜度的增加。

MWP特點(高穩定性、高QPS、低RT、性能問題)會被放大。

MWP-DSL的解決方案

  • MWP-DSL的業務本質(業務模型)
  1. N個接口任意情況組合。
  1. M個flush到客戶端(M>1,即為BigPipe的情況)。
  1. T個獨立的callback(包含錯誤的細粒度處理,完全由業務方自己定制)。
  1. 三種基本原子情況組合(獨立、merge、時序依賴)。

  • DSL客戶端編碼框架

  1. 多個flush處理各自的callback。
  1. flushkey和BigPipe解耦。
  1. 多個flushkey相互隔離,更細粒度的錯誤處理。
  • 性能方面

  1. 全異步化與線程調度模型(rxjava、netty eventloop, 多callback仍然交給觸發線程,避免加鎖的并發控制與線程拷貝)。
  1. 高性能Groovy集成(靜態編譯執行效率與原生java接近、jvm調優與GroovyClassLoader隔離避免GC問題,與perm區無用類爆炸、Groovy版本自身的bug)。
  • 穩定性方面

  1. 線程隔離,避免極端情況影響MWP。
  1. DSL接口隔離與容錯。
  1. DSL相關開關。
  1. 灰度發布與切流量。
  • 安全方面

  1. DSL代碼靜態掃描,通過白名單和黑名單機制,明確業務方同學用DSL可以做什么和不可以做什么。
  1. DSL接口錯誤隔離。
  1. DSL接口級別資源控制,比如,限制DSL中的循環次數避免死循環對CPU的消耗,以及對于總體內存的監控與報警。
  1. DSL接口級別性能監控與自動化運維,比如,監控接口rt、自動對異常接口做降級操作等。

MWP上線運行情況

MWP已經上線運行接近一年,集團蘑菇街業務相關的主要服務都已經從老的系統架構遷移到MWP上,目前整體運行非常穩定,對整體服務質量有了很大的提升。

  • 性能方面

MWP目前通過對網絡鏈路做的優化,已經使用長連接的方式替換原來短連接的請求,這樣建立連接的資源消耗只在打開或喚醒App的時候產生,而不會每次請求都重新建立連接。在實際應用中,客戶端平均RT時間從原來的841毫秒優化到現在的282毫秒,優化非常明顯。

  • 安全方面

MWP通過收斂請求鏈路,在網絡鏈路上做安全校驗,防止數據包篡改等安全問題。針對HTTP短鏈方式,MWP對請求頭和數據包進行驗簽,驗簽失敗的請求直接返回客戶端失敗信息,而針對長連接,我們自己實現的TLS1.3 0-RTT機制,有效保障數據的安全,并在此技術上做了更多優化。

  • 開發效率方面

通過MWP的路由分發和Actionlet框架,為業務提供快速業務迭代的可能。按照過去的方式,各業務代碼雜糅在一起,各業務開發需要考慮請求的上下文信息,比如從移動端過來請求和從PC端過來請求的不同處理方式,參數防篡改,以及安全和反垃圾等。而如今接入MWP之后,各業務應用天然獨立,業務開發只需關注業務邏輯,其他的像網絡鏈路、上下文解析等MWP都已經封裝處理掉,無需業務關心,有效提升多人協作下的開發效率。

基于MWP的周邊生態的建設

支持橫向功能的擴展

對于業務系統而言,安全、反垃圾、限流等這些橫向的功能是每個業務都需要去考慮和實現的。過去的方式是,每個業務都需要引入一堆的依賴來實現每一個功能,甚至有些功能各個業務都自己實現一套,工作量復雜又冗余,業務開發的注意力被分散在周邊邏輯中而不是聚焦在業務邏輯。而MWP已經實現或者接入了這些功能,對于具體業務開發來說只要接入MWP,默認地或者可以用簡單的配置來接入這些功能,非常方便。后續如果有更多的橫向功能,只要MWP來實現就可以,業務應用只要拿來就用即可。

外部系統接入

對于客戶端App和服務端應用而言,一些周邊系統的功能非常重要,比如一個強大的配置中心提供配置管理,方便地修改客戶端配置,你想隨時推送最新的啟動圖到客戶端,又或者讓客戶端網絡連接方式在HTTP和長連接之間切換。MWP就為這些系統提供了一個強大的平臺,支持了這些系統的接入,如目前支持通過通道準實時推送配置等。

API和應用管理

MWP提供了配套的管理后臺,對API、DSL、服務端應用和客戶端App進行管理。在此基礎上,用戶可以在后臺查看API的QPS、RT這些實時基礎數據,方便了解線上運行情況。除此之外,還支持在后臺對接口進行簡單的測試,以及對客戶端權限、接口流控、超時控制等參數進行配置,并實時生效。

未來展望

目前我們正在使用Go重寫網關接入層,優化現有的長連接機制。一方面,MWP客戶端和服務端之間的鏈路主要支持上行請求,這樣服務端的一些變更只有在客戶端主動請求或拉取的時候才能下發到用戶,如果能支持下行通道,不管是對于業務的拓展還是系統機制的優化都會打來很大好處。另一方面,MWP-Router是MWP系統中的重心節點,如果網關接入層足夠強大,后續可以輕松地將Router的功能下沉到業務服務,實現去中心化。

MWP是一個基礎平臺,隨著集團業務的發展,還需要圍繞這個平臺建立更多更完善的功能和系統,以支撐集團業務更長遠的業務發展。

 

來自:http://www.infoq.com/cn/articles/meili-11-11

 

 本文由用戶 coder_zxt 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!