前端微服務在字節跳動的打磨與應用

本文討論了微前端在字節跳動的應用情況。內容主要分析了微前端具體落地的步驟和兩年來的使用情況。其中分析的部分主要講到一些實際問題和我們的應對,落地情況強調了實現的過程。特別講到很多在我們觀念裡面務必要提供的微前端基石,這些方面作為基礎設施幾乎是使用微前端的必要和前提條件。

傳統前端業務通常會根據業務線集成在一個站點上,隨著業務複雜度上升,包體積會迅速變的過大。為了適應這個變化往往需要更多的開發者、更細粒度的團隊組織。分組開發時大家的模塊解耦到各自完成,上線時糅合在一起運行,產生出層出不窮的分支合併、代碼回滾,都會造成合作效率的驟降。這正是頭條號平臺在 17 年時面臨的問題。

過大的代碼集合還會造成發佈頻繁,每個業務分支和功能點都有一定的更新頻率,如果以傳統的獨石系統開發、驗證和上線,每一個業務都會讓項目所有一起升級、測試和上線,發佈頻率的總和會非常高、非常頻繁。如果不解除原有的耦合會徹底失去響應能力。

更進一步來看以如此之高的上線頻率、版本迭代速度,開發者極難追溯哪個版本對應哪個改動。

字節跳動微服務前端解決方案為應對以上挑戰而生。經過幾年發展已經成功支持了幾十個對內和對外的系統。

問題背景

Monolithic 的問題

Monolith 獨石就是一塊石頭的意思。正常翻譯一般是“單體”:單體應用。這個在前端屆概念不普及,用獨石這個翻譯更能體現他是什麼意思。一整個建築(或者什麼其他東西)是一整塊石頭刻出來的。比如石獅子。這就是獨石的應用。這樣做事情在前端工程環境這個快速變化、快速迭代的領域有很多問題。

上線慢

單體應用的一大問題是發佈非常慢。字節跳動的典型業務情況是上一次線需要至少 30 分鐘,前端的上線就需要這麼長的時間。當然這是我們在 17 年經歷的情況,保持我們的發展態勢如果不升級技術,現在可能更慢。然後 17 年底我們開始了大改版,開始拼命的擁抱微前端。

原本回滾一次也是 10 分鐘的。所以當時每天上線不了幾次,風險也很大。逐漸導致變更都要憋著,成了“幾天上線一次、一次多個變更”。

我相信這也是絕大多數聽眾都會有的問題。尤其是那種傳統的後臺工程。沒事 webpack 一下你懂的。

上下線會很多嗎?很多的,業務多了,有多少更新都要一起發佈。

理解困難

當然本次是工程化的議題。更需要關注的影響更大的其實是框架問題。大家都是幾十個項目合作到一個工程裡。工程化在搞什麼呢其中非常重要一個點就是要“人可以理解”。更低的認知成本,能收穫更低的犯錯概率。

那這些項目非得維持完全一致的組織模型就基本上是必須的。比如 model 是充血的還是失血的?是 contorller 全都放一起、還是根據 router 與視圖們放一起?這些事非常雞毛蒜皮的例子。實際上深層次的問題與之類似的非常多。

還有其它問題比如 debug 的時候到底能不能找到。也不是單體應用不行,單純是說解決這個問題的時候投入了多少精力、多少設計,以及維持這個設計規範問題不崩壞,需要多少精力。

這一類都是單體應用本身的代碼問題。“拆了就沒這些事了。”

框架無法調整

真的從架構角度來說,到底如今的前端項目需要怎麼開發、一般是怎麼幹的呢。從上一段內容讀過來,我們知道大部分出色的架構師工程師都已經解決了好多那些困難了,方式是通過傑出的架構設計。

然後都知道前端的各種框架各種實踐實際上非常多。前端工程師有個別名不知道你們聽過嗎,叫“npm install 工程師”哈哈,還有“github search 工程師”。

到底發生了極端困難的情況是來自框架還是來自生產框架的方法呢,這個不好說。但是是個值得琢磨的問題。所以你看接手拿到項目什麼的別說了你就學吧。反正現有架構肯定是挺好的。就是你得學一陣、用對了才好。

微前端在字節跳動

這裡開始講我們的細節,分別是服務發現、運行隔離、環境一致以及其他架構優勢,其實這幾件事都講完就能發現在講的主要意思是,具體是什麼把一個非常特殊、對習慣改變比較大的方案變成可能的。實現這樣一個與眾不同的方案不是大家聊一聊覺得同意並且開心就能做成。涉及到過程、成本、風險等方方面面。

“工程師”的任務不是說證明一個結構在理論上是可以存在的就完了,要有建造這個結構的過程。比如你拿化學鍵可以算出來任何可能存在的分子,畫個小人都可以。但是到底怎麼合成,按照什麼路徑能讓這種分子被製造,哪種路徑最快最便宜,這個是過程的可能性。通常這才是工程師的任務。

服務發現

服務發現的方面我們會首先講一下在整個微服務模式裡他作用是什麼、有哪些方式。然後第二部分講到底在解決什麼問題,以及多說一些他能提供的新能力。我們很重視新能力因為我們的定位不是消防隊,滅了火就完成任務,還有很多新目標、很多新好處可以探索實現。

最後是講一下在字節跳動具體是怎麼實現的。

1. 原理

“服務發現”就是原來的單體服務拆分之後,本來一個項目裡的方法分開部署了,誰也找不到誰。需要有個統一的註冊機構,把提供服務的各個部署都查到。

“發現”就是當你想訪問一個微服務,你要怎麼找到他。

這樣就有兩種構型,一個是以 Netflix OSS 為典型的,它在客戶端的機器裡先拿到一個服務目錄,處理邏輯在客戶端的代碼裡。另一種是服務端的服務發現,AWS 就是如此。

傳統微服務的服務發現更像是函數調用的替代,拆了之後怎麼調到不同容器裡部署的函數。這裡微前端思路非常類似,作用略有區別。微服務的情況是會區分像什麼訂閱、通知、請求、發佈這些,前端很可能都不用或者表現上不在前端運行時使用。還有一些例如“對單”、“對多”這些基本就是前端不太用考慮的東西。

兩者一致的地方是都誰也不認識誰了,如何知道哪些服務存在、誰在提供?下面就要講一下各種構型的服務發現和背後的服務註冊分別具體是什麼情況。

客戶端服務發現 是說客戶端——也就是服務的調用者,去請求一個註冊的目錄,裡面包含所有服務和負載均衡的基本信息這些,然後自己決定如何處理,使用哪種具體的 load balance 策略。比如 Netflix 的 OSS,服務在 Netflix Eureka 註冊一下,它心跳給各個客戶端。客戶端自己搞,簡單直觀。

服務端服務發現 是類似 AWS Elastic LoadBalancer 這種。客戶端請求就完了,服務端決定怎麼給你反向代理、負載均衡。

服務註冊 分自注冊和第三方註冊。自注冊不言而喻。第三方註冊就是一個保活機制,定期檢查服務狀態,幫你去管控該上了還是下了。

我們主要用的是第一種:客戶端服務發現,就是你要多請求一個模塊列表。這個列表給出的資源是根據用戶 session 決定的,有豐富的動態的能力。然後客戶端再根據這個列表裡的各種信息,去加載模塊資源。

2. 給前端帶來了什麼?

用服務發現的方式去組織微前端,除了使複雜的上線流程變得解耦、快捷,還可以使拆散之後的工程版本方便對齊,實現更高的穩定性和可調試性。還對前端工程帶來好多其他好處。下面主要講一下各種收益中的最重要的兩個。

快速上線 是什麼概念呢,前面說了幾十個業務和在一個項目裡,一起發佈,這樣發佈的頻率能有多高?實際考察一下放開限制後的情景,就會發現有超乎意料的高。我們的一個微前端應用的業務是頭條號,它在 2019 年上半年發了 2000 個版本。前面說了傳統上線需要 30 分鐘才能完成打包升級和容器的重啟,並且 10 分鐘才能完一個回滾,這就意味著 1000 小時的上下線等待時間。相比之下我們新的方式點一下 HTTP 請求發出去就生效了,是一個毫秒級的反應速度。

這個擱以前就不是慢、需要乾等著的問題了,直接大家就不這樣去發了。都學 Native 發版那樣火車式發佈。結果是響應效率降低了很多,很多需求漸漸變得不再由開發形成瓶頸,反而是總要等版本排發佈。

獨立切換 我們現在就分別發,可以一個單頁應用分幾十個模塊,各自上各自的、下各自的。而且後面會說到還可以各自配置自己的 AB 測試版:有 10 個模塊就可以產生 1024 個 AB 版的組合,20 個模塊 100 萬個。跟以前完全不敢想象——也就是說一起發版的時代根本做不了這個事。現在不敢想象的反而是,你說字節跳動某個業務裡面不能做 AB 測。

我們的頭條號平臺就是剛才一個典型的微前端項目,包含列出的這麼多模塊,各模塊有獨立的版本,和對服務版本的 session 控制。每個模塊進去都是版本列表,有一個模塊所有的歷史版本。通過這個平臺配置小流量、AB、上線規則。

運行隔離

1. 耦合開發的嚴峻形勢

17 年我們推進項目的時候有一個很不錯的帖子很流行,紅遍朋友圈那種,講 react-loadable 的。ta 從解耦的維度介紹了這個方向。我們當時也有一個很明確的業務需求,要把公司不同部門的人組織到一個項目裡。並且這個項目經過經年累月的增肥,已經非常臃腫並且積攢了很多值得推敲的、非直接技術的工程細節。這就意味著要用不同組織,不同的技術,不同的工程規範和打包工具,去合寫同一個平臺、同一個工程。如果當時用了 iframe 可能就是非常湊合的勉強滿足業務,完全不符合我們追求極致的習慣。

然後當時我們很在意一點就是這種跨團隊合作,想融合不同的技術團隊,實現少費力溝通或者不重溝通,運行隔離是個非常絕對的基本前提,我們其他分享裡面也用了不小的篇幅介紹,有對內的也有對外的。當時的效果是什麼呢。這個是我們 18 年 4 月內部培訓錄製到的當時情況:

前端微服務在字節跳動的打磨與應用

我們把線上的頁面(左圖)通過調試工具插入腳本,臨時移除掉沙盒功能,得到的右圖效果。

2. 運行隔離的目標

運行隔離是啥意思,回想一下剛才說的 AB 測的問題,20 個項目是多少個組合。如果把這個對應到 bug 的維度,大家都在一個應用裡亂跑會有多恐怖。那麼這樣的組合對我們的程序和程序員提出了什麼樣的要求?

不跑掛 說是“對一切工程師最基本要求”,我覺得不算誇張。所有軟件工程師的第一個能力層級都應該是不把系統拖垮。微服務之後這個問題不明顯了,因為有架構層面的方式解決了絕大多數挑戰。我很信服的一個理論是所有程序員都是四個階段:寫完需求,不拖垮別人,能擴容,性能好。

不干擾 也是另一個大問題,我們當時西瓜團隊和頭條是兩個獨立的 App,他們和我們的合作完全跨部門,連 polyfill 的規則都不一樣。事先也是做了很多公共組件、 CSS 約定之類的。但是規範和約定遠遠不夠。協作的境界從最差到最好應該是:

  • 定規範:誰來了都好好學、好好聽,自己對自己的行為負全責。
  • 能 enforce 規範:不憑自覺,而是用工具和流程等手段去發現和強制,實現可靠性。
  • 不需要規範:系統的確定性由系統解決。靠人去發現和執行規範是消耗大量認知資源的,帶來的都是額外的工作量和系統的不確定性。

3. 沙盒

我們還有另外一篇文章專門介紹沙盒的設計和採坑經驗。這篇就快速用幾張圖示意一下。

① 變量保護: 全局變量、 DOM 和 CSS 基本都是走的這條路。前後兩次快照,我們來比較,之後根據需要幫你恢復現場。這塊內容不細說了,看一眼圖就不言而喻:一次比較對照所有 key、兩次遍歷、黑名單 location、白名單 readonly。估計我這樣一說大家都懂。

前端微服務在字節跳動的打磨與應用

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

前端微服務在字節跳動的打磨與應用

這裡面兩個基礎:js 單線程、事件循環

我們用了非常單純的單進程操作系統的思路,比喻一下就是 js 的單線程就像單核 CPU 一樣。你激活一個模塊,相當於激活一個線程,其他都退到背景裡。

實際上單核單進程不是必然,大家都知道這個原理。在事件循環的基礎上,我們可以封裝所有的異步操作,把回調套在沙盒激活後面。比如 setTimeout 和 addEventListener,這樣每個模塊看起來就像是在並行。這塊可以說的很多,但是就想一下操作系統的比喻就好了。

4. 加載方式

React 的項目用 react-loadable 本身不多說了,VUE 和 raw (也就是不包含展示層框架的原始版)的各種項目,我們都提供 masterpage 的樣例,每個版本對應的都實現了一套和 react-loadable 相似的效果。

子模塊(Modules) 就是一個個的 CMD 包,我用 new Function 來包起來。其他就是具體主工程(MasterPage)項目框架的約定,load 過程分為 5 個鉤子:

  • preload 是否預加載,是個 promise,fullfill 的時候就會觸發 Ajax。各種空閒政策阻塞政策都可以由 master 制定;
  • loadCondition 編譯前置條件,fullfill 了才會開始運行這部分,執行結果就是得到那個 CMD 的 exports;
  • provider 是一個模塊的入口的函數,由模塊開發者提供,返回模塊的一切輸出。這個函數的傳入參數由 masterpage 主工程來提供。
  • loaded 完成加載,得到編譯結果了。
  • 等等後面不說太細了。

環境一致

因為我們之前都是在講微服務是什麼和落地效果如何,從來沒有講過 推行一個微服務你得做什麼。現在這部分內容是我們第一次公開分享的,也是一個很獨立的維度。

其實就是在講為什麼對微前端來說這個環境一致工具是必須的,是繞不過的必經之路。如果不搞也很容易就栽進坑裡,項目失敗。然後很可能還不知道是為何失敗的,把問題歸結為框架不好啊、人不好啊甚至微前端就不好啊之類的問題上。

前端微服務在字節跳動的打磨與應用

1. Serverless vs container

container 就是一個寄生環境,儘管這個環境還是挺特殊的,不像 linux 這種完整操作系統。相比之下 Serverless 就特殊多了。特殊到連谷歌雲都曾經在商業上被擊敗。

這裡舉兩個 Serverless 的例子,比如 lambda。它的本地工具是一個 CLI 系統:SAM,是一個非常典型的必要基礎設施。如果說發展容器化 AWS 全是靠 docker,發展 lambda 就是靠的 SAM。

另一個典型的例子 firebase。想必前端的同學們都非常清楚,也都用過他們的開發套件。這些工具都非常重視一點就是本地開發,我做個項目到底能不能先測試再上。或者說先調試在上。

要做的話就是儘可能模擬真實環境了,SAM 的話就模擬了 API Gateway,memory limit 這些。有 live debugging、 local debugging。不然的話發什麼瘋有人敢把線上業務放到一個非常不同的環境下運行。

2. 你是不是環境有問題(在我這是好的)

標題程序員最常說的一句話對不對,另一種表達是“我這是好的”。大家都知道絕大多數情況這麼說話不對,但常常會忍不住說。甚至更像是其實是對自己說。一種捫心自問,自我拷問,“我這是好的啊”。

我們用沙盒把微前端做成了像 container,像瀏覽器裡的 docker。但是不夠,我們還是把寄生在 masterpage 內這種應用框架的特徵,也就是業務具體邏輯,看成是一種 Serverless。

然後我們還把隔離的思路做到極致,我們的 dev 命令是通過啟動參數啟動一個完全獨立的 Chrome 會話,有自己的 cookie 啊緩存啊這些,效果像是裝了 2 個 Chrome 乃至多個 Chrome。然後代理工具默認也配到了啟動參數,是個 pac 文件。所以也可以單獨用或者裝 switchy 用。

代理工具 就是調試環境的整個配置,那些走測試環境、哪些走線上請求全部代理管理。生成一個動態的 pac 地址和代理服務。就是剛才說的。

關鍵請求,比如服務發現的請求,顯然是代理掉的。走一個我們為本地環境定製的返回值。更細節的功能是我們可以協助調試主工程(MasterPage)、 組合上某個 module,你也可以用指定的 MasterPage 版本來調用你正在編寫的模塊。

你也可以指定是否加載完整的線上模塊列表、只替換你正在調試的模塊。

我們也還有完整的植入 webpack dev server 的服務供選擇。前面說了支持任意打包工具,這塊是解耦的,只不過你用了我們可以幫你 reload,部分刷新動態刷新。後面再細說。

發佈檢查 是針對服務註冊這一塊。這塊的一部分,我們的 build 命令有一套檢查,對應 git 鉤子。

方便調試 我們還在一定程度上支持了 HMR。我們可以像開發一個普通前端應用一樣開發主工程(MasterPage)和子模塊(Module),子模塊更新後改變模塊管理器狀態,並由內置的 eventbus 機制來重新渲染 HMR,這個機制也可以用到盛傳環境。

我們的公共庫可以通過在 MasterPage 項目裡引入、子模塊裡 external 的方式實現模塊間共享。 也支持子模塊使用特定版本的基礎庫。

Vue 用到了全局變量及原型鏈擴展,暫時還不支持 Hot Reload 的調試。

其他的框架優勢

框架上看就是 serverless 的方向。不是真的 serverless 是前端 serverless,業務 module 開發者很多東西都不用再關心了。舉個例子就是 console.log。現在大家都知道線上業務要乾乾淨淨體體面面,把 console 都收拾整齊。這是我們之前提到的規範的層面,我們可以做到吸收所有 console,存儲錯誤堆棧。然後用戶反饋的時候作為 trace 元數據提交到反饋後臺等等。

這些都是 masterpage 層面的框架了。當然不是必然關係。但是可以說微前端給了一個非常方便像這樣組織項目的渠道。

我們線上的 sourcemap 也是根據服務發現的管理後臺權限控制的,只有開發者能看。

下一代前端展望

前面講了,服務發現是一個對前端可用資源的總體管理。這個能力是不侷限於運行時的微服務前端的。對一切資源都適用,下面說一下這塊。

服務發現 + CDN

抽象一個完整的前端訪問,首先拆成 3 步:A 頁面加載,B“服務發現”,C 根據服務發現結果加載資源。那就有不同的變種。最直觀的就是 AB 結合,SSR 畫上去,把 html 請求下來,module list 資源列表已經全了。這個系統我們這代號 GOOFY。當然也可以 ABC 都組裝進去。這個後面細說。

前端微服務在字節跳動的打磨與應用

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

前端微服務在字節跳動的打磨與應用

總之大意就是這個 ABC。

Token 解析

前端微服務在字節跳動的打磨與應用

中心服務從中心機房把規則心跳給邊緣節點,邊緣節點接收客戶請求,就近解析出基本的 token,這個過程中不依賴其他服務。

這個 token 由同樣在邊緣的頁面服務提供。因為脫離了到中心機房驗證的步驟所以 token 時效有一定依賴前端 SDK。

高可用

高可用可以說是邊緣計算的一個極大的好處,額外給了我們一個收益。這套系統的容災基本等同於智能 DNS 對應的探針保活這一套成熟技術了。

我們需要的就是把邊緣節點心跳到一個監控服務上,他們會分鐘級動態修改 DNS。如果沒有足夠的邊緣節點生存,還可以 DNS 到傳統的中心機房。

這樣絕大多數流量都不需要進出中心機房,資源都是就近的、多播的。

結尾

以上就是本次分享的全部內容,我們從落地的細節分享了字節跳動兩年來使用微前端的經驗,以及面對這些挑戰時的思考過程。非常幸運我們的項目有足夠多給力的夥伴們支持,最終獲得了比較大的成功,也非常明顯地提升了重量級的產品的質量。

微前端和很多前沿和剛剛發展的概念一樣,本身還在快速的演進和驗證的過程中,我們的具體實踐也一直在快速的變化,在不斷地發現弱點和糾正它們,也在努力發展更多的可能。在這個從種種不完美到更完美的奮鬥過程中,能給讀者分享我們的成果是我們的一種榮幸。而且在分享後,如果能收到指教、討論和建議我們會更加感激,並且非常歡迎。也歡迎更多的有識之士加入我們,具體可參見內推鏈接: 。


分享到:


相關文章: