傳統(tǒng)前端業(yè)務(wù)通常會根據(jù)業(yè)務(wù)線集成在一個站點上,隨著業(yè)務(wù)復雜度上升,包體積會迅速變的過大。為了適應(yīng)這個變化往往需要更多的開發(fā)者、更細粒度的團隊組織。分組開發(fā)時大家的模塊解耦到各自完成,上線時糅合在一起運行,產(chǎn)生出層出不窮的分支合并、代碼回滾,都會造成合作效率的驟降。這正是頭條號平臺在 17 年時面臨的問題。
過大的代碼集合還會造成發(fā)布頻繁,每個業(yè)務(wù)分支和功能點都有一定的更新頻率,如果以傳統(tǒng)的獨石系統(tǒng)開發(fā)、驗證和上線,每一個業(yè)務(wù)都會讓項目所有一起升級、測試和上線,發(fā)布頻率的總和會非常高、非常頻繁。如果不解除原有的耦合會徹底失去響應(yīng)能力。
更進一步來看以如此之高的上線頻率、版本迭代速度,開發(fā)者極難追溯哪個版本對應(yīng)哪個改動。
字節(jié)跳動微服務(wù)前端解決方案為應(yīng)對以上挑戰(zhàn)而生。經(jīng)過幾年發(fā)展已經(jīng)成功支持了幾十個對內(nèi)和對外的系統(tǒng)。
問題背景
Monolithic 的問題
Monolith 獨石就是一塊石頭的意思。正常翻譯一般是“單體”:單體應(yīng)用。這個在前端屆概念不普及,用獨石這個翻譯更能體現(xiàn)他是什么意思。一整個建筑(或者什么其他東西)是一整塊石頭刻出來的。比如石獅子。這就是獨石的應(yīng)用。這樣做事情在前端工程環(huán)境這個快速變化、快速迭代的領(lǐng)域有很多問題。
上線慢
單體應(yīng)用的一大問題是發(fā)布非常慢。字節(jié)跳動的典型業(yè)務(wù)情況是上一次線需要至少 30 分鐘,前端的上線就需要這么長的時間。當然這是我們在 17 年經(jīng)歷的情況,保持我們的發(fā)展態(tài)勢如果不升級技術(shù),現(xiàn)在可能更慢。然后 17 年底我們開始了大改版,開始拼命的擁抱微前端。
原本回滾一次也是 10 分鐘的。所以當時每天上線不了幾次,風險也很大。逐漸導致變更都要憋著,成了“幾天上線一次、一次多個變更”。
我相信這也是絕大多數(shù)聽眾都會有的問題。尤其是那種傳統(tǒng)的后臺工程。沒事 webpack 一下你懂的。
上下線會很多嗎?很多的,業(yè)務(wù)多了,有多少更新都要一起發(fā)布。
理解困難
當然本次是工程化的議題。更需要關(guān)注的影響更大的其實是框架問題。大家都是幾十個項目合作到一個工程里。工程化在搞什么呢其中非常重要一個點就是要“人可以理解”。更低的認知成本,能收獲更低的犯錯概率。
那這些項目非得維持完全一致的組織模型就基本上是必須的。比如 model 是充血的還是失血的?是 contorller 全都放一起、還是根據(jù) router 與視圖們放一起?這些事非常雞毛蒜皮的例子。實際上深層次的問題與之類似的非常多。
還有其它問題比如 debug 的時候到底能不能找到。也不是單體應(yīng)用不行,單純是說解決這個問題的時候投入了多少精力、多少設(shè)計,以及維持這個設(shè)計規(guī)范問題不崩壞,需要多少精力。
這一類都是單體應(yīng)用本身的代碼問題?!安鹆司蜎]這些事了?!?/p>
框架無法調(diào)整
真的從架構(gòu)角度來說,到底如今的前端項目需要怎么開發(fā)、一般是怎么干的呢。從上一段內(nèi)容讀過來,我們知道大部分出色的架構(gòu)師工程師都已經(jīng)解決了好多那些困難了,方式是通過杰出的架構(gòu)設(shè)計。
然后都知道前端的各種框架各種實踐實際上非常多。前端工程師有個別名不知道你們聽過嗎,叫“npm install 工程師”哈哈,還有“github search 工程師”。
到底發(fā)生了極端困難的情況是來自框架還是來自生產(chǎn)框架的方法呢,這個不好說。但是是個值得琢磨的問題。所以你看接手拿到項目什么的別說了你就學吧。反正現(xiàn)有架構(gòu)肯定是挺好的。就是你得學一陣、用對了才好。
微前端在字節(jié)跳動
這里開始講我們的細節(jié),分別是服務(wù)發(fā)現(xiàn)、運行隔離、環(huán)境一致以及其他架構(gòu)優(yōu)勢,其實這幾件事都講完就能發(fā)現(xiàn)在講的主要意思是,具體是什么把一個非常特殊、對習慣改變比較大的方案變成可能的。實現(xiàn)這樣一個與眾不同的方案不是大家聊一聊覺得同意并且開心就能做成。涉及到過程、成本、風險等方方面面。
“工程師”的任務(wù)不是說證明一個結(jié)構(gòu)在理論上是可以存在的就完了,要有建造這個結(jié)構(gòu)的過程。比如你拿化學鍵可以算出來任何可能存在的分子,畫個小人都可以。但是到底怎么合成,按照什么路徑能讓這種分子被制造,哪種路徑最快最便宜,這個是過程的可能性。通常這才是工程師的任務(wù)。
服務(wù)發(fā)現(xiàn)
服務(wù)發(fā)現(xiàn)的方面我們會首先講一下在整個微服務(wù)模式里他作用是什么、有哪些方式。然后第二部分講到底在解決什么問題,以及多說一些他能提供的新能力。我們很重視新能力因為我們的定位不是消防隊,滅了火就完成任務(wù),還有很多新目標、很多新好處可以探索實現(xiàn)。
最后是講一下在字節(jié)跳動具體是怎么實現(xiàn)的。
1. 原理
“服務(wù)發(fā)現(xiàn)”就是原來的單體服務(wù)拆分之后,本來一個項目里的方法分開部署了,誰也找不到誰。需要有個統(tǒng)一的注冊機構(gòu),把提供服務(wù)的各個部署都查到。
“發(fā)現(xiàn)”就是當你想訪問一個微服務(wù),你要怎么找到他。
這樣就有兩種構(gòu)型,一個是以 Netflix OSS 為典型的,它在客戶端的機器里先拿到一個服務(wù)目錄,處理邏輯在客戶端的代碼里。另一種是服務(wù)端的服務(wù)發(fā)現(xiàn),AWS 就是如此。
傳統(tǒng)微服務(wù)的服務(wù)發(fā)現(xiàn)更像是函數(shù)調(diào)用的替代,拆了之后怎么調(diào)到不同容器里部署的函數(shù)。這里微前端思路非常類似,作用略有區(qū)別。微服務(wù)的情況是會區(qū)分像什么訂閱、通知、請求、發(fā)布這些,前端很可能都不用或者表現(xiàn)上不在前端運行時使用。還有一些例如“對單”、“對多”這些基本就是前端不太用考慮的東西。
兩者一致的地方是都誰也不認識誰了,如何知道哪些服務(wù)存在、誰在提供?下面就要講一下各種構(gòu)型的服務(wù)發(fā)現(xiàn)和背后的服務(wù)注冊分別具體是什么情況。
客戶端服務(wù)發(fā)現(xiàn) 是說客戶端——也就是服務(wù)的調(diào)用者,去請求一個注冊的目錄,里面包含所有服務(wù)和負載均衡的基本信息這些,然后自己決定如何處理,使用哪種具體的 load balance 策略。比如 Netflix 的 OSS,服務(wù)在 Netflix Eureka 注冊一下,它心跳給各個客戶端??蛻舳俗约焊?,簡單直觀。
服務(wù)端服務(wù)發(fā)現(xiàn) 是類似 AWS Elastic LoadBalancer 這種??蛻舳苏埱缶屯炅?,服務(wù)端決定怎么給你反向代理、負載均衡。
服務(wù)注冊 分自注冊和第三方注冊。自注冊不言而喻。第三方注冊就是一個保活機制,定期檢查服務(wù)狀態(tài),幫你去管控該上了還是下了。
我們主要用的是第一種:客戶端服務(wù)發(fā)現(xiàn),就是你要多請求一個模塊列表。這個列表給出的資源是根據(jù)用戶 session 決定的,有豐富的動態(tài)的能力。然后客戶端再根據(jù)這個列表里的各種信息,去加載模塊資源。
2. 給前端帶來了什么?
用服務(wù)發(fā)現(xiàn)的方式去組織微前端,除了使復雜的上線流程變得解耦、快捷,還可以使拆散之后的工程版本方便對齊,實現(xiàn)更高的穩(wěn)定性和可調(diào)試性。還對前端工程帶來好多其他好處。下面主要講一下各種收益中的最重要的兩個。
快速上線 是什么概念呢,前面說了幾十個業(yè)務(wù)和在一個項目里,一起發(fā)布,這樣發(fā)布的頻率能有多高?實際考察一下放開限制后的情景,就會發(fā)現(xiàn)有超乎意料的高。我們的一個微前端應(yīng)用的業(yè)務(wù)是頭條號,它在 2019 年上半年發(fā)了 2000 個版本。前面說了傳統(tǒng)上線需要 30 分鐘才能完成打包升級和容器的重啟,并且 10 分鐘才能完一個回滾,這就意味著 1000 小時的上下線等待時間。相比之下我們新的方式點一下 HTTP 請求發(fā)出去就生效了,是一個毫秒級的反應(yīng)速度。
這個擱以前就不是慢、需要干等著的問題了,直接大家就不這樣去發(fā)了。都學 Native 發(fā)版那樣火車式發(fā)布。結(jié)果是響應(yīng)效率降低了很多,很多需求漸漸變得不再由開發(fā)形成瓶頸,反而是總要等版本排發(fā)布。
獨立切換 我們現(xiàn)在就分別發(fā),可以一個單頁應(yīng)用分幾十個模塊,各自上各自的、下各自的。而且后面會說到還可以各自配置自己的 AB 測試版:有 10 個模塊就可以產(chǎn)生 1024 個 AB 版的組合,20 個模塊 100 萬個。跟以前完全不敢想象——也就是說一起發(fā)版的時代根本做不了這個事?,F(xiàn)在不敢想象的反而是,你說字節(jié)跳動某個業(yè)務(wù)里面不能做 AB 測。
我們的頭條號平臺就是剛才一個典型的微前端項目,包含列出的這么多模塊,各模塊有獨立的版本,和對服務(wù)版本的 session 控制。每個模塊進去都是版本列表,有一個模塊所有的歷史版本。通過這個平臺配置小流量、AB、上線規(guī)則。
運行隔離
1. 耦合開發(fā)的嚴峻形勢
17 年我們推進項目的時候有一個很不錯的帖子很流行,紅遍朋友圈那種,講 react-loadable 的。ta 從解耦的維度介紹了這個方向。我們當時也有一個很明確的業(yè)務(wù)需求,要把公司不同部門的人組織到一個項目里。并且這個項目經(jīng)過經(jīng)年累月的增肥,已經(jīng)非常臃腫并且積攢了很多值得推敲的、非直接技術(shù)的工程細節(jié)。這就意味著要用不同組織,不同的技術(shù),不同的工程規(guī)范和打包工具,去合寫同一個平臺、同一個工程。如果當時用了 iframe 可能就是非常湊合的勉強滿足業(yè)務(wù),完全不符合我們追求極致的習慣。
然后當時我們很在意一點就是這種跨團隊合作,想融合不同的技術(shù)團隊,實現(xiàn)少費力溝通或者不重溝通,運行隔離是個非常絕對的基本前提,我們其他分享里面也用了不小的篇幅介紹,有對內(nèi)的也有對外的。當時的效果是什么呢。這個是我們 18 年 4 月內(nèi)部培訓錄制到的當時情況:

我們把線上的頁面(左圖)通過調(diào)試工具插入腳本,臨時移除掉沙盒功能,得到的右圖效果。
2. 運行隔離的目標
運行隔離是啥意思,回想一下剛才說的 AB 測的問題,20 個項目是多少個組合。如果把這個對應(yīng)到 bug 的維度,大家都在一個應(yīng)用里亂跑會有多恐怖。那么這樣的組合對我們的程序和程序員提出了什么樣的要求?
不跑掛 說是“對一切工程師最基本要求”,我覺得不算夸張。所有軟件工程師的第一個能力層級都應(yīng)該是不把系統(tǒng)拖垮。微服務(wù)之后這個問題不明顯了,因為有架構(gòu)層面的方式解決了絕大多數(shù)挑戰(zhàn)。我很信服的一個理論是所有程序員都是四個階段:寫完需求,不拖垮別人,能擴容,性能好。
不干擾 也是另一個大問題,我們當時西瓜團隊和頭條是兩個獨立的 App,他們和我們的合作完全跨部門,連 polyfill 的規(guī)則都不一樣。事先也是做了很多公共組件、 CSS 約定之類的。但是規(guī)范和約定遠遠不夠。協(xié)作的境界從最差到最好應(yīng)該是:
- 定規(guī)范:誰來了都好好學、好好聽,自己對自己的行為負全責。
- 能 enforce 規(guī)范:不憑自覺,而是用工具和流程等手段去發(fā)現(xiàn)和強制,實現(xiàn)可靠性。
- 不需要規(guī)范:系統(tǒng)的確定性由系統(tǒng)解決??咳巳グl(fā)現(xiàn)和執(zhí)行規(guī)范是消耗大量認知資源的,帶來的都是額外的工作量和系統(tǒng)的不確定性。
3. 沙盒
我們還有另外一篇文章專門介紹沙盒的設(shè)計和采坑經(jīng)驗。這篇就快速用幾張圖示意一下。
① 變量保護: 全局變量、 DOM 和 CSS 基本都是走的這條路。前后兩次快照,我們來比較,之后根據(jù)需要幫你恢復現(xiàn)場。這塊內(nèi)容不細說了,看一眼圖就不言而喻:一次比較對照所有 key、兩次遍歷、黑名單 location、白名單 readonly。估計我這樣一說大家都懂。

② 沙盒時序: 稍微多說一些。右圖是我們做的 ABCDE 五個模塊的加載和混行的時序圖。虛線左邊是加載,右邊是獨占線程所占用的時間。也就是說有 ABCDE 五個模塊五個沙盒,分別在這個模塊編譯(下載、創(chuàng)建 js 變量和函數(shù)、運行這些語句、最終生成一個 React Component)和運行時(這個模塊被打開、渲染對應(yīng)的所有功能)。

這里面兩個基礎(chǔ):js 單線程、事件循環(huán)
我們用了非常單純的單進程操作系統(tǒng)的思路,比喻一下就是 js 的單線程就像單核 CPU 一樣。你激活一個模塊,相當于激活一個線程,其他都退到背景里。
實際上單核單進程不是必然,大家都知道這個原理。在事件循環(huán)的基礎(chǔ)上,我們可以封裝所有的異步操作,把回調(diào)套在沙盒激活后面。比如 setTimeout 和 addEventListener,這樣每個模塊看起來就像是在并行。這塊可以說的很多,但是就想一下操作系統(tǒng)的比喻就好了。
4. 加載方式
React 的項目用 react-loadable 本身不多說了,VUE 和 raw (也就是不包含展示層框架的原始版)的各種項目,我們都提供 masterpage 的樣例,每個版本對應(yīng)的都實現(xiàn)了一套和 react-loadable 相似的效果。
子模塊(Modules) 就是一個個的 CMD 包,我用 new Function 來包起來。其他就是具體主工程(MasterPage)項目框架的約定,load 過程分為 5 個鉤子:
-
preload是否預加載,是個promise,fullfill的時候就會觸發(fā) Ajax。各種空閑政策阻塞政策都可以由 master 制定; -
loadCondition編譯前置條件,fullfill了才會開始運行這部分,執(zhí)行結(jié)果就是得到那個 CMD 的exports; -
provider是一個模塊的入口的函數(shù),由模塊開發(fā)者提供,返回模塊的一切輸出。這個函數(shù)的傳入?yún)?shù)由 masterpage 主工程來提供。 -
loaded完成加載,得到編譯結(jié)果了。 - 等等后面不說太細了。
環(huán)境一致
因為我們之前都是在講微服務(wù)是什么和落地效果如何,從來沒有講過 推行一個微服務(wù)你得做什么?,F(xiàn)在這部分內(nèi)容是我們第一次公開分享的,也是一個很獨立的維度。
其實就是在講為什么對微前端來說這個環(huán)境一致工具是必須的,是繞不過的必經(jīng)之路。如果不搞也很容易就栽進坑里,項目失敗。然后很可能還不知道是為何失敗的,把問題歸結(jié)為框架不好啊、人不好啊甚至微前端就不好啊之類的問題上。

1. Serverless vs container
container 就是一個寄生環(huán)境,盡管這個環(huán)境還是挺特殊的,不像 linux 這種完整操作系統(tǒng)。相比之下 Serverless 就特殊多了。特殊到連谷歌云都曾經(jīng)在商業(yè)上被擊敗。
這里舉兩個 Serverless 的例子,比如 lambda。它的本地工具是一個 CLI 系統(tǒng):SAM,是一個非常典型的必要基礎(chǔ)設(shè)施。如果說發(fā)展容器化 AWS 全是靠 docker,發(fā)展 lambda 就是靠的 SAM。
另一個典型的例子 firebase。想必前端的同學們都非常清楚,也都用過他們的開發(fā)套件。這些工具都非常重視一點就是本地開發(fā),我做個項目到底能不能先測試再上。或者說先調(diào)試在上。
要做的話就是盡可能模擬真實環(huán)境了,SAM 的話就模擬了 API Gateway,memory limit 這些。有 live debugging、 local debugging。不然的話發(fā)什么瘋有人敢把線上業(yè)務(wù)放到一個非常不同的環(huán)境下運行。
2. 你是不是環(huán)境有問題(在我這是好的)
標題程序員最常說的一句話對不對,另一種表達是“我這是好的”。大家都知道絕大多數(shù)情況這么說話不對,但常常會忍不住說。甚至更像是其實是對自己說。一種捫心自問,自我拷問,“我這是好的啊”。
我們用沙盒把微前端做成了像 container,像瀏覽器里的 docker。但是不夠,我們還是把寄生在 masterpage 內(nèi)這種應(yīng)用框架的特征,也就是業(yè)務(wù)具體邏輯,看成是一種 Serverless。
然后我們還把隔離的思路做到極致,我們的 dev 命令是通過啟動參數(shù)啟動一個完全獨立的 Chrome 會話,有自己的 cookie 啊緩存啊這些,效果像是裝了 2 個 Chrome 乃至多個 Chrome。然后代理工具默認也配到了啟動參數(shù),是個 pac 文件。所以也可以單獨用或者裝 switchy 用。
代理工具 就是調(diào)試環(huán)境的整個配置,那些走測試環(huán)境、哪些走線上請求全部代理管理。生成一個動態(tài)的 pac 地址和代理服務(wù)。就是剛才說的。
關(guān)鍵請求,比如服務(wù)發(fā)現(xiàn)的請求,顯然是代理掉的。走一個我們?yōu)楸镜丨h(huán)境定制的返回值。更細節(jié)的功能是我們可以協(xié)助調(diào)試主工程(MasterPage)、 組合上某個 module,你也可以用指定的 MasterPage 版本來調(diào)用你正在編寫的模塊。
你也可以指定是否加載完整的線上模塊列表、只替換你正在調(diào)試的模塊。
我們也還有完整的植入 webpack dev server 的服務(wù)供選擇。前面說了支持任意打包工具,這塊是解耦的,只不過你用了我們可以幫你 reload,部分刷新動態(tài)刷新。后面再細說。
發(fā)布檢查 是針對服務(wù)注冊這一塊。這塊的一部分,我們的 build 命令有一套檢查,對應(yīng) git 鉤子。
方便調(diào)試 我們還在一定程度上支持了 HMR。我們可以像開發(fā)一個普通前端應(yīng)用一樣開發(fā)主工程(MasterPage)和子模塊(Module),子模塊更新后改變模塊管理器狀態(tài),并由內(nèi)置的 eventbus 機制來重新渲染 HMR,這個機制也可以用到盛傳環(huán)境。
我們的公共庫可以通過在 MasterPage 項目里引入、子模塊里 external 的方式實現(xiàn)模塊間共享。也支持子模塊使用特定版本的基礎(chǔ)庫。
Vue 用到了全局變量及原型鏈擴展,暫時還不支持 Hot Reload 的調(diào)試。
其他的框架優(yōu)勢
框架上看就是 serverless 的方向。不是真的 serverless 是前端 serverless,業(yè)務(wù) module 開發(fā)者很多東西都不用再關(guān)心了。舉個例子就是 console.log。現(xiàn)在大家都知道線上業(yè)務(wù)要干干凈凈體體面面,把 console 都收拾整齊。這是我們之前提到的規(guī)范的層面,我們可以做到吸收所有 console,存儲錯誤堆棧。然后用戶反饋的時候作為 trace 元數(shù)據(jù)提交到反饋后臺等等。
這些都是 masterpage 層面的框架了。當然不是必然關(guān)系。但是可以說微前端給了一個非常方便像這樣組織項目的渠道。
我們線上的 sourcemap 也是根據(jù)服務(wù)發(fā)現(xiàn)的管理后臺權(quán)限控制的,只有開發(fā)者能看。
下一代前端展望
前面講了,服務(wù)發(fā)現(xiàn)是一個對前端可用資源的總體管理。這個能力是不局限于運行時的微服務(wù)前端的。對一切資源都適用,下面說一下這塊。
服務(wù)發(fā)現(xiàn) + CDN
抽象一個完整的前端訪問,首先拆成 3 步:A 頁面加載,B“服務(wù)發(fā)現(xiàn)”,C 根據(jù)服務(wù)發(fā)現(xiàn)結(jié)果加載資源。那就有不同的變種。最直觀的就是 AB 結(jié)合,SSR 畫上去,把 html 請求下來,module list 資源列表已經(jīng)全了。這個系統(tǒng)我們這代號 GOOFY。當然也可以 ABC 都組裝進去。這個后面細說。

另外一個思路就是 BC 結(jié)合,我請求一個列表,不用說我可以把 js 內(nèi)容都 combo 進去。少一些額外的請求。

總之大意就是這個 ABC。
Token 解析

中心服務(wù)從中心機房把規(guī)則心跳給邊緣節(jié)點,邊緣節(jié)點接收客戶請求,就近解析出基本的 token,這個過程中不依賴其他服務(wù)。
這個 token 由同樣在邊緣的頁面服務(wù)提供。因為脫離了到中心機房驗證的步驟所以 token 時效有一定依賴前端 SDK。
高可用
高可用可以說是邊緣計算的一個極大的好處,額外給了我們一個收益。這套系統(tǒng)的容災基本等同于智能 DNS 對應(yīng)的探針保活這一套成熟技術(shù)了。
我們需要的就是把邊緣節(jié)點心跳到一個監(jiān)控服務(wù)上,他們會分鐘級動態(tài)修改 DNS。如果沒有足夠的邊緣節(jié)點生存,還可以 DNS 到傳統(tǒng)的中心機房。
這樣絕大多數(shù)流量都不需要進出中心機房,資源都是就近的、多播的。
結(jié)尾
以上就是本次分享的全部內(nèi)容,我們從落地的細節(jié)分享了字節(jié)跳動兩年來使用微前端的經(jīng)驗,以及面對這些挑戰(zhàn)時的思考過程。非常幸運我們的項目有足夠多給力的伙伴們支持,最終獲得了比較大的成功,也非常明顯地提升了重量級的產(chǎn)品的質(zhì)量。
微前端和很多前沿和剛剛發(fā)展的概念一樣,本身還在快速的演進和驗證的過程中,我們的具體實踐也一直在快速的變化,在不斷地發(fā)現(xiàn)弱點和糾正它們,也在努力發(fā)展更多的可能。在這個從種種不完美到更完美的奮斗過程中,能給讀者分享我們的成果是我們的一種榮幸。而且在分享后,如果能收到指教、討論和建議我們會更加感激,并且非常歡迎。也歡迎更多的有識之士加入我們,具體可參見 job.bytedance.com
原文標題:前端微服務(wù)在字節(jié)跳動的打磨與應(yīng)用
原文鏈接:https://mp.weixin.qq.com/s/iLdAH9p2-S8pFyZrNzYaNg