自2011年上線以來,蘑菇街一直沿用的是以PHP為核心的業(yè)務系統(tǒng)架構。但是,隨著業(yè)務的增長、業(yè)務邏輯的復雜化,對技術架構有了更高要求。同時,隨著移動互聯(lián)網(wǎng)的普及,大量用戶流量從PC端到無線端快速轉(zhuǎn)移,故而移動端架構在保證穩(wěn)定性的前提下,支持高效開發(fā)和迭代顯得尤為重要。
電商的大促業(yè)務越來越常態(tài)化,雙十一作為一年一度的電商大促高峰,更是一年比一年火爆。今年雙十一,蘑菇街推出買手(紅人)購物清單、紅人買手直播、實時榜單等新功能:新的變化對技術上高并發(fā)、高可用的考驗也越來越大。
MWP無線網(wǎng)關的設計
基于PHP的架構是蘑菇街早期使用的,當時開發(fā)人員較少,業(yè)務邏輯相對簡單,在業(yè)務迭代過程中更多地將就快速迭代試錯,對于業(yè)務邏輯之外的系統(tǒng)優(yōu)化相對比較少人關注。
上述的架構在早期比較長一段時間都在使用,后面嘗試過在服務化下對業(yè)務應用做一些簡單的隔離,但是沒有對架構有根本性的改變。這樣的架構在當時的確行之非常有效,但是隨著業(yè)務和團隊成員的增長,越來越多的問題暴露出來。
整體性能較差
從宏觀的角度來看,對用戶來說,性能上其中一個明顯的表現(xiàn)就在于客戶端請求的RT上。有研究顯示,移動端用戶在點開一個頁面的時候,如果RT超過5秒,有74%的用戶將會選擇離開頁面。在原架構中,客戶端請求鏈路只支持HTTP短連接的方式,短鏈接的建立和釋放會消耗很多時間,特別是移動端用戶,在復雜的無線網(wǎng)絡環(huán)境下,帶寬資源非常寶貴,頻繁地建立HTTP連接,所消耗的資源和時間成本會非常高,基于原有的架構優(yōu)化的成本會非常高。
存在嚴重的安全風險
上圖中的HTTP請求鏈路是一個裸露的鏈路,架構層面沒有機制對請求做任務安全校驗,如果黑客修改了請求中的數(shù)據(jù),業(yè)務也能正常走下去,而業(yè)務上如果需要接入這種安全校驗,比如支付、交易這種高危業(yè)務,則需要自己去額外接入。
開發(fā)效率隨著業(yè)務復雜度和人員的增長快速下降
上文提到老架構中,各個業(yè)務都在一個工程中,開發(fā)人員少的時候開發(fā)起來會很方便,但是當業(yè)務膨脹之后,里面的大部分業(yè)務都從之前的一兩個人維護轉(zhuǎn)變?yōu)橐粋€團隊在維護,在局部代碼里會同時有多人在并行開發(fā),隨之而來的是溝通成本變高,代碼變得臃腫,線上故障也隨之頻發(fā)。隨著代碼臃腫復雜,也給新的業(yè)務迭代帶來成本的提高,開發(fā)的效率急劇下降。
下面我們來看一下MWP通過怎樣的設計來解決這些問題的。
MWP提供從客戶端到服務端的一整套技術解決方案,包括如下內(nèi)容。
客戶端SDK,為各客戶端接入MWP而提供的客戶端SDK。
MWP-Router,主要為服務端應用提供統(tǒng)一的服務暴露方式,并為客戶端的請求提供路由分發(fā)機制。
Actionlet框架,為基于MWP的服務端業(yè)務應用提供統(tǒng)一的開發(fā)框架。
MWP-DSL,是MWP提供的一套面向業(yè)務數(shù)據(jù)的無線端、前后端分離解決方案。
MWP-SDK作為客戶端上業(yè)務訪問后端服務的入口,與服務端的架構相對應,分層上將通用網(wǎng)絡層和上層應用層分開解耦,最底層的通用網(wǎng)絡層包含建聯(lián)策略、HttpDNS、自有協(xié)議等網(wǎng)絡優(yōu)化需要的各方面,上層應用層包含對API請求邏輯、DSL的封裝等,這樣的分離,對網(wǎng)絡層的長期優(yōu)化是非常必要的。我們將網(wǎng)絡能力整體封裝成了標準的SDK,一方面將整體優(yōu)化的收益推廣給App群享受,另一方面也將Native通道的能力暴露給H5。
在應用層,基于Pipeline靈活的編排和擴展能力,方便集成了客戶端鑒權、離線緩存、防刷、時間糾偏、狀態(tài)管理、性能上報等功能,實現(xiàn)了不同的網(wǎng)絡協(xié)議之間實現(xiàn)靈活的切換和降級重試,橫向上與其他客戶端基礎組件打通,比如,與配置中心配合實現(xiàn)配置的準實時下放等。
為了解決頁面請求過多、接口回調(diào)嵌套等問題,實現(xiàn)了MWP-DSL的調(diào)用方式,將原本客戶端對數(shù)據(jù)的處理邏輯放到服務端的DSL層,解放服務端開發(fā),合并客戶端請求,減少頁面上的網(wǎng)絡請求損耗。
應用層的功能擴展和優(yōu)化都基于網(wǎng)絡層的穩(wěn)定性和安全性上,所以網(wǎng)絡層的優(yōu)化顯得尤為重要。由于移動網(wǎng)絡的差異化和多樣化,使得客戶端的網(wǎng)絡環(huán)境問題依然嚴峻,我們都或多或少遭遇到各種域名緩存、內(nèi)容劫持、用戶跨網(wǎng)訪問緩慢等問題,網(wǎng)絡安全性面臨考驗。
MWP的動態(tài)調(diào)度集成了HttpDNS組件來解決經(jīng)常遇到的這些問題。動態(tài)調(diào)度通過策略的下發(fā)來控制客戶端使用的協(xié)議(長鏈、短鏈、是否加密等)、端口等,通過策略優(yōu)先級選擇、不同網(wǎng)絡環(huán)境下策略表的緩存和后臺跑馬等方式對建聯(lián)策略進行優(yōu)化。
在初期架構中,我們只對重要的接口使用HTTPS,因為傳統(tǒng)的HTTPS的整個握手流程是非常繁重的,尤其是在復雜的無線網(wǎng)絡環(huán)境,往往造成建鏈過慢,甚至超時的情況;但是從安全的角度考慮,又必須對用戶數(shù)據(jù)的傳輸建立在一個安全加密的通道之上。
為了解決兩者的平衡,我們加入了安全網(wǎng)關接入層,接入層基于長鏈和自有協(xié)議進行數(shù)據(jù)的傳輸,并通過合并請求、證書預置和優(yōu)化加密算法實現(xiàn)了一套基于TLS1.3的0-RTT加密機制。在建鏈的效率和數(shù)據(jù)安全上找到平衡,在不犧牲用戶體驗的基礎上,達到了安全傳輸?shù)哪康?。另外也正在嘗試接入HTTP2.0協(xié)議,為客戶端網(wǎng)絡層帶來更多的優(yōu)化。
MWP-Router(以下簡稱Router)是MWP的路由層,它提供多種接入方式及RPC泛化調(diào)用方式,基于Servlet 3.0和Pipeline機制提供了高性能高可用的路由服務
作為蘑菇街無線業(yè)務的入口,性能和穩(wěn)定性是最重要的指標。Router是基于Servlet 3.0和Actor模型的全異步架構,AsyncContext和Event-Loop充分發(fā)揮了現(xiàn)代cpu的性能,在隔離各個請求資源的同時,用極小的內(nèi)存換取了最大的吞吐量,靈活的Pipeline機制提供了強大的流程編排能力,結合RPC泛化調(diào)用提供了一整套標準的API服務。
Router的其他特性如下:
通過構建符合協(xié)議標準的頭部信息可以方便集成鑒權、防刷、緩存、時間校準及配置準實時下放等特性。
管理后臺通過精細化的配置來管理App和API,包括路由、安全、流控、權限、別名等。
提供定制化的DSL,客戶端開發(fā)可以根據(jù)自己的業(yè)務場景任意組裝和處理后端服務的元數(shù)據(jù)供端上展示使用,包括但不僅限于API聚合、API依賴分層、數(shù)據(jù)分段返回等。
在最初的架構中,Router是基于HTTP協(xié)議的,重要信息API使用HTTPS。眾所周知,在無線網(wǎng)絡復雜而惡劣的環(huán)境下,數(shù)據(jù)安全和用戶體驗很難取得很好的平衡。為了解決以上問題,最大程度保證用戶體驗,我們增加了網(wǎng)關接入層來管理連接。
接入層使用自定義協(xié)議和App建立長連接,基于我們自己實現(xiàn)的TLS1.3 0-RTT機制來保證建連的效率保障數(shù)據(jù)的安全,配合session-ticket-reuse、證書預埋、App加固等機制保證了協(xié)議本身的高效穩(wěn)定及安全性。接入層緩存了部分基于連接的協(xié)議數(shù)據(jù),對于優(yōu)化網(wǎng)絡io的效果也非常明顯。
另外,我們也接入了SDPY協(xié)議,并正在嘗試接入HTTP2.0協(xié)議,期間對Nginx性能調(diào)優(yōu)、內(nèi)核參數(shù)調(diào)優(yōu)、協(xié)議參數(shù)調(diào)優(yōu)等都積累了大量的優(yōu)化經(jīng)驗,針對當下流行的微信小程序,后續(xù)還會接入WebSocket協(xié)議。
MWP為內(nèi)部調(diào)用定義了API泛化調(diào)用方式,后端的業(yè)務應用若需要接入MWP需要遵循這種調(diào)用方式,所以MWP提供了Actionlet框架,為業(yè)務應用提供接入MWP的快速便捷方式,同時也為各業(yè)務應用帶來一些額外的好處。
規(guī)范化業(yè)務對外輸出接口,所有接入MWP的業(yè)務都需要按照一定的規(guī)則,有統(tǒng)一的輸入和輸出方式,這也是方便后續(xù)對API和應用進行統(tǒng)一管理的前提條件。
Pipeline等模式隔離開環(huán)境和接入方式對業(yè)務邏輯的侵入,如果沒有Actionlet框架,各業(yè)務開發(fā)需要關心請求上下文信息,比如Servlet上下文等,這樣可以提高Actionlet業(yè)務代碼在多端接入方式(Android、iOS、H5等)下的復用。
統(tǒng)一的Actionlet框架可以為一些通用的橫向邏輯提供統(tǒng)一實現(xiàn),各業(yè)務開發(fā)只要高度關注自己的業(yè)務邏輯即可,而不用每個業(yè)務都需要接入依賴甚至自己實現(xiàn)這些邏輯,比如用戶Session的處理就是一個很好的例子。
對于一個對外提供服務的業(yè)務應用,最關心的應該是服務的輸入和輸出,而各個不同業(yè)務API的輸入輸出又會有很大差異。比如,一個注冊接口需要輸入用戶名、密碼和其他用戶信息,而返回的是是否注冊成功的結果,而一個商品列表頁則需要輸入商品類型,返回的則是一個商品列表以及商品內(nèi)部詳細信息,甚至對應用戶信息的復雜數(shù)據(jù)結構。
在Java這種強類型語言中,如何抽象出一種統(tǒng)一API的規(guī)范,滿足各種各樣不通的業(yè)務,又能為外部提供統(tǒng)一的接口模型?
在Actionlet框架的目的里我們提到,業(yè)務應用本身應該是關注純業(yè)務代碼的實現(xiàn),但是接入Actionlet的業(yè)務應用又是面向最終用戶的。那么這里就會有一個矛盾,面向最終用戶的接口必然會帶上環(huán)境的上下文,比如,走Web請求就會有Servlet相關的上下文,甚至HTTP的上下文。怎樣處理這些上下文信息,讓業(yè)務開發(fā)能完全關注業(yè)務代碼的實現(xiàn),而不用花很多心思在處理請求的上下文上?
在接收到請求時,系統(tǒng)需要處理很多邏輯才會走到業(yè)務代碼中,比如參數(shù)的解析、用戶Session的校驗、API路由的選擇等,這些邏輯串聯(lián)在一起作為請求處理的前置流程,框架以怎樣的方式控制這些流程的執(zhí)行,又如何支持后續(xù)在這些流程中添加或修改?
Pipeline和Valve
Valve是Pipeline中的概念,而這里詳細提出來,是因為Actionlet的執(zhí)行流程中很多功能是通過Valve來實現(xiàn)的。比如,請求的路由RouterValve、請求的執(zhí)行InvokeValve,都是Valve。
那我們是如何通過Valve來對流程進行定義和控制的呢?其實默認的ActionletExecutor就是基于Pipeline來實現(xiàn)的,它在初始化的時候就預先定義了一組Valve,在請求進來時依序執(zhí)行各個Valve。如上圖中,接收到請求后會依次執(zhí)行RouterValve、ParameterValve、SessionValve、InvokeValve,而如果后續(xù)擴展想改變流程或在流程中加入另外自定義的流程就非常方便了,只要在流程定義的地方修改就可以。
Valve的排列順序也是有要求的,因為請求是從第一個Valve執(zhí)行到最后一個,再從最后一個執(zhí)行到第一個,這是一個責任鏈模式。但是和攔截器不同,Valve本身還可以做一定的流程控制,比如直接breakPipeline,或直接goto到某個Valve。
Actionlet的具體設計
首先,我們先來看一段Actionlet的接口定義。
從上面的定義中,我們約束了Actionlet的入?yún)arameter和返回的接口ActionResult,強制約束了入?yún)⒑头祷亟Y果只有一個,業(yè)務方可以自由定義自己具體的Domain來作為輸入和輸出,這樣做方便使用規(guī)約的方式來對外暴露接口,減少要對參數(shù)做映射的工作量。而負責在請求Request和返回結果的Response中,這兩個Domain將會被序列化和反序列化成Json來進行傳輸。
那么有人可能會有疑問,大部分業(yè)務在獲得自己業(yè)務輸入之外,還會需要一些額外的請求信息,比如客戶端來源,甚至HTTP頭等數(shù)據(jù),在這么嚴格的封裝之下,如何拿到原始的ActionRequest和ActionResponse呢?可以通過Actionlet的上下文ActionletContext來獲取,因為目前Actionlet都是同步的請求,所以請求的上下文放在ThreadLocal中。
上下文的隔離
上圖中ActionletExecutor配合ActionRequest和ActionResponse,就是為了將環(huán)境的上下文抽象出來,從而使Actionlet能更專注在純業(yè)務代碼上。
其中,ActionRequest的作用就是將環(huán)境上下文中的請求給抽象成通用的模型,比如Servlet中ActionRequest就可以解析HttpServletRequest中的參數(shù),從而封裝成可以被Actionlet直接使用的Request。而ActionResponse就是將Actionlet返回的數(shù)據(jù)結果進行對應環(huán)境的輸出,比如,Servlet中ActionResponse會將結果進行渲染然后輸出給HttpServletResponse。ActionletExecutor就會將整個流程串聯(lián)起來。
因為Actionlet的業(yè)務邏輯可能會對接多個環(huán)境實現(xiàn),那么就可以針對不同的環(huán)境來實現(xiàn)不同的ActionletExecutor和相應的Valve,來達到對環(huán)境的隔離。
異步Actionlet的支持
MWP-DSL在MWP中提供一套DSL,針對無線端(Android、iOS、H5)中和展現(xiàn)層強相關的業(yè)務數(shù)據(jù)的組裝、拼接和轉(zhuǎn)換,集成原有服務端部分Control層代碼和客戶端View層的代碼,本質(zhì)上是一套面向業(yè)務數(shù)據(jù)的無線端前后端分離解決方案。
這種場景下的問題是,客戶端沒法做到分塊加載渲染和BigPipe,同時依賴一個大的Callback,如果后臺有任意接口超時會等待很久,此外服務端同學不能專注自己的Module,任何小的需求改動(包括不需要后臺提供數(shù)據(jù)Schema無變更的場景)都需要服務端同學參與,并聯(lián)調(diào)。
在這種場景下,客戶端同學代碼Callback嵌套嚴重難以維護,三端Control層代碼沒法復用,任何業(yè)務上微小的改動都需要客戶端同學發(fā)版。
工程上
客戶端MVC強制分離,避免callback嵌套,提高客戶端代碼可維護性。
Android、iOS、H5三端Control層邏輯復用。
客戶端Control層邏輯變更不依賴發(fā)版,控制力更強。
專人做專事,面向業(yè)務數(shù)據(jù)的無線領域前后端分離方案,后端同學專注Module層,客戶端同學專注在View層和Control層。甚至只要業(yè)務需求沒有底層數(shù)據(jù)Schema的改變,完全不需求服務端同學介入,只要相應客戶端同學自己組合下數(shù)據(jù)接口就好,減少前后端聯(lián)調(diào)成本。
有限的DSL,提高上手速度,可維護性、安全性等。
技術上
MWP-DSL具備熱更新的能力。
通過BigPipe支持分段返回,從而支持客戶端諸如Lazy Load等,提升客戶端用戶體驗。
DSL對于MWP異步化和并行改造,提升整體接口性能。
所謂微服務的可能落地方式。
Fackbook GraphQL專注于提供面向業(yè)務數(shù)據(jù)的一種新的數(shù)據(jù)查詢和檢索方案,關注點在客戶端數(shù)據(jù)查詢的易用性,本質(zhì)上希望客戶端直接通過寫類似SQL的方式(但是比SQL更直接,類似于面向數(shù)據(jù)JSON)來對后臺數(shù)據(jù)(把后臺的一個接口類比于數(shù)據(jù)庫中的一張表)做過濾和查詢。
相比之下,本質(zhì)上MWP DSL支持的業(yè)務場景更為復雜。
DSL包含大量的業(yè)務邏輯,也就是if else和for。
同時,我們對于元數(shù)據(jù)的新增和變更比較靈活,而不僅僅是數(shù)據(jù)的篩選。
所以,和GraphQL的異同可以理解為MapReduce和Hive的區(qū)別。
業(yè)務上
真實業(yè)務場景足夠復雜,會出現(xiàn)任意N個MWP接口隨機組合和callback情況。
MWP-DSL提供的能力如何即受限又足夠,同時易擴展,并且易用,也就是學習接入成本低
技術上(主要是性能、穩(wěn)定性與安全)
從輕量級MWP請求轉(zhuǎn)發(fā)到多MWP組合并運算帶來的系統(tǒng)壓力。
為了做到非阻塞、全異步編碼,帶來的排查問題、線程模型與調(diào)度復雜度的增加。
MWP特點(高穩(wěn)定性、高QPS、低RT、性能問題)會被放大。
MWP-DSL的業(yè)務本質(zhì)(業(yè)務模型)
N個接口任意情況組合。
M個flush到客戶端(M>1,即為BigPipe的情況)。
T個獨立的callback(包含錯誤的細粒度處理,完全由業(yè)務方自己定制)。
三種基本原子情況組合(獨立、merge、時序依賴)。
DSL客戶端編碼框架
多個flush處理各自的callback。
flushkey和BigPipe解耦。
多個flushkey相互隔離,更細粒度的錯誤處理。
性能方面
全異步化與線程調(diào)度模型(rxjava、netty eventloop, 多callback仍然交給觸發(fā)線程,避免加鎖的并發(fā)控制與線程拷貝)。
高性能Groovy集成(靜態(tài)編譯執(zhí)行效率與原生java接近、jvm調(diào)優(yōu)與GroovyClassLoader隔離避免GC問題,與perm區(qū)無用類爆炸、Groovy版本自身的bug)。
穩(wěn)定性方面
線程隔離,避免極端情況影響MWP。
DSL接口隔離與容錯。
DSL相關開關。
灰度發(fā)布與切流量。
安全方面
DSL代碼靜態(tài)掃描,通過白名單和黑名單機制,明確業(yè)務方同學用DSL可以做什么和不可以做什么。
DSL接口錯誤隔離。
DSL接口級別資源控制,比如,限制DSL中的循環(huán)次數(shù)避免死循環(huán)對CPU的消耗,以及對于總體內(nèi)存的監(jiān)控與報警。
DSL接口級別性能監(jiān)控與自動化運維,比如,監(jiān)控接口rt、自動對異常接口做降級操作等。
MWP上線運行情況
性能方面
安全方面
開發(fā)效率方面
基于MWP的周邊生態(tài)的建設
對于業(yè)務系統(tǒng)而言,安全、反垃圾、限流等這些橫向的功能是每個業(yè)務都需要去考慮和實現(xiàn)的。過去的方式是,每個業(yè)務都需要引入一堆的依賴來實現(xiàn)每一個功能,甚至有些功能各個業(yè)務都自己實現(xiàn)一套,工作量復雜又冗余,業(yè)務開發(fā)的注意力被分散在周邊邏輯中而不是聚焦在業(yè)務邏輯。
而MWP已經(jīng)實現(xiàn)或者接入了這些功能,對于具體業(yè)務開發(fā)來說只要接入MWP,默認地或者可以用簡單的配置來接入這些功能,非常方便。后續(xù)如果有更多的橫向功能,只要MWP來實現(xiàn)就可以,業(yè)務應用只要拿來就用即可。
對于客戶端App和服務端應用而言,一些周邊系統(tǒng)的功能非常重要,比如一個強大的配置中心提供配置管理,方便地修改客戶端配置,你想隨時推送最新的啟動圖到客戶端,又或者讓客戶端網(wǎng)絡連接方式在HTTP和長連接之間切換。MWP就為這些系統(tǒng)提供了一個強大的平臺,支持了這些系統(tǒng)的接入,如目前支持通過通道準實時推送配置等。
MWP提供了配套的管理后臺,對API、DSL、服務端應用和客戶端App進行管理。在此基礎上,用戶可以在后臺查看API的QPS、RT這些實時基礎數(shù)據(jù),方便了解線上運行情況。除此之外,還支持在后臺對接口進行簡單的測試,以及對客戶端權限、接口流控、超時控制等參數(shù)進行配置,并實時生效。
未來展望
目前我們正在使用Go重寫網(wǎng)關接入層,優(yōu)化現(xiàn)有的長連接機制。一方面,MWP客戶端和服務端之間的鏈路主要支持上行請求,這樣服務端的一些變更只有在客戶端主動請求或拉取的時候才能下發(fā)到用戶,如果能支持下行通道,不管是對于業(yè)務的拓展還是系統(tǒng)機制的優(yōu)化都會打來很大好處。
另一方面,MWP-Router是MWP系統(tǒng)中的重心節(jié)點,如果網(wǎng)關接入層足夠強大,后續(xù)可以輕松地將Router的功能下沉到業(yè)務服務,實現(xiàn)去中心化。
MWP是一個基礎平臺,隨著集團業(yè)務的發(fā)展,還需要圍繞這個平臺建立更多更完善的功能和系統(tǒng),以支撐集團業(yè)務更長遠的業(yè)務發(fā)展。