10 億級(jí)流量的搜索前端,是怎么做架構(gòu)升級(jí)的?

- 前言 -
前端發(fā)展飛速,從最開(kāi)始的靜態(tài)頁(yè)面到 JavaScript,再?gòu)?PC 端到移動(dòng)端,隨著大前端的復(fù)雜度不斷提升,很多公司開(kāi)始前后端分離,剝離出前、后端架構(gòu)設(shè)計(jì)。那我們來(lái)看看,前端架構(gòu)設(shè)計(jì)是什么?曾經(jīng)非常簡(jiǎn)單的前端架構(gòu)發(fā)展到現(xiàn)在有哪些問(wèn)題,遇到前端代碼體量巨大、跨團(tuán)隊(duì)協(xié)作效率、代碼耦合、技術(shù)棧落后等問(wèn)題又該怎么解決?

- 什么是前端架構(gòu)? -
前端架構(gòu)這一詞,相信很多人的定義都不太一樣;按照拆詞的解釋來(lái)看,我理解為“前端”+“架構(gòu)”。前端是指,Web 端的前臺(tái)頁(yè)面,包括網(wǎng)頁(yè)的內(nèi)容、樣式、腳本等,這三者通常封裝在組件中,可能是模板引擎的文件模塊,也可能是 MVVM 框架里的組件。“架構(gòu)”就更好理解了,架構(gòu)一詞來(lái)自建筑行業(yè),可以理解是房屋的整體結(jié)構(gòu)、框架。結(jié)合前端和架構(gòu)的概念,“前端架構(gòu)”可以理解為,Web 頁(yè)面組件的抽象和組織方式。
又因?yàn)楦鱾€(gè)公司的業(yè)務(wù)不同,每個(gè)公司的前端架構(gòu)發(fā)展都不一樣,這里,我會(huì)拿百度移動(dòng)端經(jīng)典的搜索場(chǎng)景來(lái)給大家舉例,希望從百度的移動(dòng)端架構(gòu)演進(jìn)過(guò)程中,發(fā)現(xiàn)一些共性的問(wèn)題。

- 百度移動(dòng)端背景及問(wèn)題 -
為什么是以百度來(lái)舉例?是因?yàn)榘俣仁菄?guó)內(nèi)搜索引擎的領(lǐng)頭人,并且,目前一直處于行業(yè)領(lǐng)先狀態(tài)。據(jù) statcounter 前瞻產(chǎn)業(yè)研究院在 2019 年中國(guó)搜索引擎行中可以知道,百度搜索占全世界搜索引擎市場(chǎng)份額12.3%,居第二位,僅次于谷歌。所以用百度來(lái)舉例,更具有代表性。
言歸正傳,打開(kāi)百度 App 你會(huì)發(fā)現(xiàn),百度前端直接分為首頁(yè)和搜索結(jié)果頁(yè),搜索結(jié)果頁(yè)是搜索的主要入口,每天承載著十億級(jí)流量。
不僅如此,搜索結(jié)果頁(yè)承載著許多產(chǎn)品線(xiàn)的需求和下游模塊的運(yùn)行時(shí),每年內(nèi)部的研發(fā)人員會(huì)提供五百多個(gè)產(chǎn)品需求,為十幾個(gè)下游模塊提供基礎(chǔ)庫(kù)和運(yùn)行時(shí)。甚至還有后端協(xié)同,從圖 1 我們可以看出結(jié)果頁(yè)的整體架構(gòu)。

圖 1:百度搜索結(jié)果頁(yè)的整體架構(gòu)
針對(duì)整體的架構(gòu)設(shè)計(jì),有這些問(wèn)題:
細(xì)分業(yè)務(wù)線(xiàn)眾多,單個(gè)庫(kù)代碼龐大;
平均每月有 200+ 提交,3w+ 行代碼;
80+ 開(kāi)發(fā)者在同一個(gè)代碼庫(kù)中開(kāi)發(fā);
沒(méi)有人能完全掌握模塊整體技術(shù)。
于是,梳理出三個(gè)方面的問(wèn)題:
1. 人員職責(zé)不清晰,單個(gè)模塊同時(shí)承擔(dān)了多個(gè)團(tuán)隊(duì)的職責(zé)
框和 Tab:“全部”和垂類(lèi)搜索共用;
運(yùn)營(yíng)產(chǎn)品:滲透在結(jié)果頁(yè)代碼庫(kù)里;
其他:結(jié)果列表、用戶(hù)反饋、搜索推薦、體驗(yàn)日志、速度日志、計(jì)費(fèi)邏輯……
2. 代碼耦合嚴(yán)重
容易出錯(cuò),代碼邏輯脆弱;
結(jié)構(gòu)僵化,不易新增功能;
依賴(lài)?yán)喂蹋a很難復(fù)用。
3. 技術(shù)棧落后
頁(yè)面沒(méi)有組件化。沒(méi)有 Vue、沒(méi)有 React,還在用 Smarty 模板;
無(wú)法支持 Node.js。Smarty 模板強(qiáng)依賴(lài) PHP 環(huán)境;
工具鏈落后。沒(méi)有 TypeScript、沒(méi)有 Jest。
這三個(gè)問(wèn)題最終會(huì)影響到研發(fā)效率以及產(chǎn)品質(zhì)量。那么百度又是怎么去具體做的呢?架構(gòu)優(yōu)化的目標(biāo)只有兩個(gè),一是滿(mǎn)足業(yè)務(wù)需求,二是技術(shù)上能對(duì)框架和工具靈活升級(jí)(也是為了持續(xù)的滿(mǎn)足業(yè)務(wù)需求)。根據(jù)“滿(mǎn)足業(yè)務(wù)需求”這一目標(biāo),百度內(nèi)部是制定了三個(gè)層面的方向。(如圖 2)
底層基礎(chǔ)層是貼近社區(qū),因?yàn)閾?jù)內(nèi)部調(diào)研來(lái)看,造輪子的成本不高,但是維護(hù)這些輪子成本極高,如果想更快的迭代,還是建議貼近社區(qū),去用些開(kāi)源的事情或者去貢獻(xiàn)開(kāi)源。主要是解決技術(shù)棧落后以及職責(zé)不清晰等問(wèn)題。
中間層是獨(dú)立模塊,主要是應(yīng)對(duì)之前提到的職責(zé)不清晰的問(wèn)題以及交付效率低等問(wèn)題。主要是解決職責(zé)不清晰以及交付效率低等問(wèn)題。
頂層就是組件化,在獨(dú)立模塊的基礎(chǔ)上去做組件化,加速業(yè)務(wù)的迭代。
圖 2:業(yè)務(wù)需求的三個(gè)方向

- 解決思路 -
根據(jù)這里提到的方向和目標(biāo),怎么結(jié)合百度自己的架構(gòu)落地呢?首先,回顧下百度的架構(gòu),如下圖 3 可以看到。

圖 3:百度搜索結(jié)果頁(yè)的整體架構(gòu)
1. 這里有兩塊日志,意味著同一套代碼要在兩個(gè)部分維護(hù);除了重復(fù)之外,它們的差異會(huì)對(duì)后續(xù)的維護(hù)引入更高的成本;
2. 底層這個(gè) HHVM+PHP 和社區(qū)更加擁抱 Node.js 會(huì)有沖突。
所以,百度同學(xué)把目標(biāo)架構(gòu)調(diào)整為圖 4 所示。
圖 4:結(jié)果頁(yè)的目標(biāo)架構(gòu)
圖 4 中可以看到:
把日志、搜索框、相關(guān)搜索、性能打點(diǎn)等獨(dú)立成單獨(dú)的模塊,有專(zhuān)門(mén)的同學(xué)來(lái)獨(dú)立維護(hù)和迭代;
在前后端之間加了一層渲染層;讓業(yè)務(wù)代碼和后端的邏輯分開(kāi);
在底層加了 Node.js 機(jī)制。
目標(biāo)、方向都解決好之后,就得看如何實(shí)施。對(duì)于一個(gè)小體量的庫(kù)來(lái)說(shuō),從零構(gòu)建架構(gòu)就行;但是對(duì)于百度來(lái)說(shuō),實(shí)施也是難點(diǎn)。不僅要考慮平滑遷移、性能不退化,還要考慮長(zhǎng)期可維護(hù)性、安全性、跨平臺(tái)等。
前文也提到了,基本思路是按照基礎(chǔ)設(shè)施、模塊拆分、組件化的步驟執(zhí)行;基礎(chǔ)設(shè)施是業(yè)務(wù)模塊劃分的關(guān)鍵,完善的自動(dòng)化和工具鏈?zhǔn)悄K化的前提;模塊化拆分可以為業(yè)務(wù)和團(tuán)隊(duì)提供更好的橫向擴(kuò)展能力;模塊化的基礎(chǔ)上,可以進(jìn)一步在模塊內(nèi)部建設(shè)組件化方案來(lái)加速業(yè)務(wù)迭代。
在基礎(chǔ)設(shè)施需要關(guān)注的事情包括:
TypeScript:大型項(xiàng)目必備,提前發(fā)現(xiàn)問(wèn)題;也是跨平臺(tái)的基礎(chǔ);
持續(xù)集成:確保每次變更新增功能和修復(fù)問(wèn)題的同時(shí),不引入新的問(wèn)題;
單元測(cè)試:在重構(gòu)之初引入,幫助防退化和輔助設(shè)計(jì)。
模塊化拆分需要關(guān)注的事情包括:
識(shí)別和定義業(yè)務(wù)邊界,把大一統(tǒng)的倉(cāng)庫(kù)分割成若干獨(dú)立的小倉(cāng)庫(kù);
在子模塊內(nèi)建設(shè)自動(dòng)化機(jī)制,獨(dú)立地選型、開(kāi)發(fā)、上線(xiàn)。
注意:
模塊化拆分不是技術(shù)問(wèn)題,而是業(yè)務(wù)問(wèn)題。只有根據(jù)業(yè)務(wù)和產(chǎn)品進(jìn)行垂直劃分,才有可能達(dá)到解耦和獨(dú)立迭代的目的。否則只是形式上拆分耦合的代碼,會(huì)造成更大的維護(hù)和溝通成本。
由于組件是業(yè)務(wù)模塊內(nèi)部的選型,組件化的方案相對(duì)比較自由。只需要不嚴(yán)重影響性能,且能夠平滑過(guò)渡即可。

- 落地方案 -
1. 模塊化
具體的落地方案,我們也用一張圖(圖5)來(lái)表示??梢钥吹剿譃榉?wù)端和瀏覽器端兩部分。
服務(wù)端關(guān)心的問(wèn)題是業(yè)務(wù)模塊的劃分以及運(yùn)行時(shí)的組合;
瀏覽器端關(guān)心的問(wèn)題是依賴(lài)的解決以及如何支持組件化方案。
圖 5:具體的落地方案
2. 服務(wù)端
百度是把整個(gè)大模塊拆分成多個(gè)獨(dú)立業(yè)務(wù)模塊,最終頁(yè)面由模塊組合而成。這要求業(yè)務(wù)模塊具有統(tǒng)一的接口,即上圖所示的 Molecule 接口,它定義了模塊如何渲染、有哪些依賴(lài)等信息。因?yàn)殇秩具^(guò)程封裝在了模塊內(nèi)部,所以整個(gè)架構(gòu)可以支持多語(yǔ)言、多框架。
相信你也發(fā)現(xiàn),Molecule 和微服務(wù)非常相似。它們的關(guān)鍵區(qū)別在于,微服務(wù)的服務(wù)之間通過(guò) IPC 互相操作,且每個(gè)服務(wù)可以獨(dú)立伸縮、獨(dú)立部署;而 Molecule 的各模塊存在于同一個(gè)進(jìn)程里。雖然有這樣的區(qū)別,Molecule 仍然可以實(shí)現(xiàn)和微服務(wù)近乎相同的特性,如圖 6 所示。
圖 6:Molecule 和微服務(wù)的比較
圖 7 展示的是一個(gè)具體的業(yè)務(wù)模塊的服務(wù)端入口文件,其中 ToptipController 是實(shí)現(xiàn)了由 Molecule 提供的控制器接口;這個(gè)接口要求提供一個(gè)渲染函數(shù),接受一個(gè)字典類(lèi)型的數(shù)據(jù),返回渲染之后的頁(yè)面內(nèi)容。由調(diào)用方?jīng)Q定如何組裝頁(yè)面。
圖 7:具體的業(yè)務(wù)模塊的服務(wù)端入口文件
如上是業(yè)務(wù)模塊提供方的接口。此外 Molecule 機(jī)制還為調(diào)用方(組裝最終頁(yè)面的那一側(cè))提供了方便的接口,可以在需要引入子模塊的地方,傳入子模塊名稱(chēng)和參數(shù)即可在運(yùn)行時(shí)渲染出來(lái)。整個(gè)機(jī)制的原理很簡(jiǎn)單,但實(shí)際使用中可能還需要引入命名空間、考慮模塊版本等問(wèn)題。
3. 客戶(hù)端
那么客戶(hù)端如何運(yùn)行起來(lái)呢?我們也需要把每個(gè)模塊的瀏覽器端組件運(yùn)行起來(lái),困難在于組件之間的依賴(lài)和代碼共享。這些組件可能位于不同的代碼庫(kù)并屬于不同的業(yè)務(wù),所以我們需要一個(gè)非常松散的依賴(lài)方式。
這里我們引入的是一個(gè)依賴(lài)注入的容器(圖 8),總的來(lái)說(shuō),框架邏輯和通用工具都封裝成具體的Service提供給業(yè)務(wù)模塊使用,每個(gè)業(yè)務(wù)模塊則需要定義它依賴(lài)于哪些Service。
圖 8:客戶(hù)端設(shè)計(jì)
圖 9 形象地描述了組件、Service 和容器間的關(guān)系。
圖 9:組件、Service 和容器之間的關(guān)系
其中藍(lán)色代表具體的Service,其他顏色表示獨(dú)立的業(yè)務(wù)模塊。運(yùn)行時(shí)容器會(huì)負(fù)責(zé)解決每個(gè)業(yè)務(wù)模塊的依賴(lài),并把這些業(yè)務(wù)模塊組裝起來(lái),最終得到可交互的 Web 頁(yè)面。
注意:
業(yè)務(wù)模塊之間是獨(dú)立的,一個(gè)業(yè)務(wù)模塊無(wú)法依賴(lài)于其他業(yè)務(wù)模塊,只能依賴(lài)于通用 Service。因此如果存在業(yè)務(wù)模塊之間的產(chǎn)品邏輯耦合,可能需要一個(gè)通用 Service 作為媒介,比如容器里提供一個(gè)起事件總線(xiàn)作用的 EventService。
圖10是業(yè)務(wù)模塊的客戶(hù)端代碼示例。它的依賴(lài)通過(guò)構(gòu)造函數(shù)來(lái)聲明,運(yùn)行時(shí)容器負(fù)責(zé)依賴(lài)的創(chuàng)建,而業(yè)務(wù)模塊只需要關(guān)心依賴(lài)的使用。正是使用和創(chuàng)建操作的分離,使得業(yè)務(wù)模塊之間、業(yè)務(wù)模塊和頁(yè)面框架之間可以解耦,可以獨(dú)立地開(kāi)發(fā)、獨(dú)立地測(cè)試。

圖 10:業(yè)務(wù)模塊的客戶(hù)端代碼示例
以上是模塊拆分的整體方案,我們回顧一下:在服務(wù)端通過(guò)一個(gè)叫做 Molecule 的接口來(lái)組合業(yè)務(wù)模塊;在瀏覽器端通過(guò)一個(gè) DI 容器來(lái)解決依賴(lài)關(guān)系,并啟動(dòng)所有業(yè)務(wù)模塊。
4. 組件化
組件化方案直接影響業(yè)務(wù)開(kāi)發(fā)的的效率,換句話(huà)說(shuō),組件化方案某種程度上決定了業(yè)務(wù)同學(xué)寫(xiě)怎樣的代碼。組件化也可以幫助解決職責(zé)不清晰等問(wèn)題。我們選的組件化方案是 San,你也可以基于你的業(yè)務(wù)或偏好選則 Vue 或者 React。業(yè)務(wù)代碼的遷移比較直觀,就是從 Smarty 模板遷移到 San 組件,從 HTML 字符串拼接變成有業(yè)務(wù)語(yǔ)義的組件結(jié)構(gòu)。
接下來(lái)重點(diǎn)關(guān)注組件化方案的兩個(gè)關(guān)鍵技術(shù)問(wèn)題,跨平臺(tái)和頁(yè)面性能。
一、跨平臺(tái)
我們有非常多的業(yè)務(wù)代碼,有上千個(gè)模板、幾十萬(wàn)行代碼,這些代碼需要遷移到組件化方案上來(lái),而且要確保后端從 PHP 遷移到 Node.js 的整個(gè)過(guò)程中,業(yè)務(wù)代碼不需要重新開(kāi)發(fā)。所以業(yè)務(wù)組件如何跨平臺(tái)呢?關(guān)鍵在于抽象。
高層語(yǔ)言:我們業(yè)務(wù)代碼需要使用一個(gè)足夠高層的語(yǔ)言,這里我們用的是 TypeScript,可以翻譯到多個(gè)平臺(tái);
依賴(lài)反轉(zhuǎn):我們的高層的業(yè)務(wù)的模塊不應(yīng)該依賴(lài)于具體的底層模塊,而是它只依賴(lài)于接口,這樣才有可能在不同的平臺(tái)給它替換掉不同的底層的實(shí)現(xiàn);
抽象接口:最后是 Molecule 這個(gè)接口的設(shè)計(jì)應(yīng)該足夠的簡(jiǎn)單;Molecule 接口不依賴(lài)底層實(shí)現(xiàn),比如 PHP 的具體 API。
做到以上幾點(diǎn)就可以完成平滑的過(guò)渡。這個(gè)過(guò)程中又分為三個(gè)階段(圖 11)。
圖 11:平臺(tái)過(guò)渡的三個(gè)階段
二、頁(yè)面性能
引入前端框架通常意味著體積增加,性能下降,而性能直接影響搜索收入,因此頁(yè)面性能是項(xiàng)目成功的關(guān)鍵。如果性能會(huì)比模板引擎的性能差,那么這個(gè)項(xiàng)目很可能會(huì)夭折。如何去保證頁(yè)面性能?著重介紹兩個(gè)優(yōu)化點(diǎn)。
引入 SSR:引入服務(wù)端渲染,首屏性能可以得到明顯提升;
SSR 優(yōu)化:傳統(tǒng)的 SSR 上還需要進(jìn)一步優(yōu)化性能。
引入SSR。為了解釋SSR的重要性,請(qǐng)看圖12。瀏覽器加載頁(yè)面分為四步:請(qǐng)求頁(yè)面、請(qǐng)求外鏈資源、執(zhí)行腳本、渲染組件。從圖中的對(duì)比可以看出,CSR在前面三步的時(shí)候,用戶(hù)都是看不到頁(yè)面的;而引入SSR之后,在第二步用戶(hù)就能看到請(qǐng)求回來(lái)的頁(yè)面。SSR它最大的一個(gè)用途就是提升首屏?xí)r間。
圖 12:CSR和SSR的比較
SSR 優(yōu)化。只是引入 SSR 還不能讓性能達(dá)到預(yù)期,因?yàn)橄啾扔谀0逡嬷苯悠唇幼址?,SSR 需要遞歸渲染組件,尤其是遞歸 VNode 比較耗時(shí)。對(duì)此 San SSR 相比于 Vue/React SSR 做了很多改進(jìn)。
去 VNode:編譯期遞歸 VNode,運(yùn)行時(shí)只做 HTML 拼接;
編譯期計(jì)算:盡可能把工作移到編譯期,減小運(yùn)行時(shí)開(kāi)銷(xiāo);
圖 13 展示了最終的 San SSR 和改造前的 Smarty 模板引擎的性能對(duì)比。
圖 13:最終的 San SSR 和改造前的 Smarty 模板引擎的性能對(duì)比
可以看到 Smarty 和 San SSR 在不同的場(chǎng)景會(huì)有不同的表現(xiàn),因?yàn)樗鼈兊匿秩痉绞椒浅2煌?。最終搜索結(jié)果頁(yè)的組件化的 SSR 上線(xiàn)之后,線(xiàn)上實(shí)驗(yàn)效果顯示比 Smarty 要快 10ms左右。這個(gè)已經(jīng)是一個(gè)很不錯(cuò)的效果了,我們用組件化從性能上打敗了模版引擎。

- 總結(jié) -
針對(duì)百度搜索引擎在架構(gòu)演化中遇到的問(wèn)題,相信在其他領(lǐng)域也會(huì)有一些共性的東西。通過(guò)百度的解決思路,希望能對(duì)正在做前端架構(gòu)的你有一些啟發(fā)。
作者:Harttle
簡(jiǎn)介:百度資深研發(fā)工程師,北京大學(xué)物理學(xué)學(xué)士和計(jì)算機(jī)科學(xué)碩士。2016年加入百度,曾負(fù)責(zé)和參與百度搜索Web極速瀏覽框架、MIP開(kāi)源項(xiàng)目的研發(fā),目前負(fù)責(zé)搜索結(jié)果頁(yè)和搜索推薦業(yè)務(wù)。LiquidJS 的作者,貢獻(xiàn)于San、Realworld Apps、hightlight.js、ALE、HTML5 Standard等項(xiàng)目。
來(lái)源:百度架構(gòu)師

