如何搭建一個高可用的服務端渲染工程

可能大家在看到這個標題的時候,會覺得,只不過又是一篇爛大街的 SSR 從零入門的教程而已。別急,往下看,相信你或多或少會有一些不一樣的收穫呢。

在落地一種技術的時候,我們首先要想一想:

  1. 是否一定需要引入這種技術呢?他能解決什麼問題,或者能帶來什麼收益?
  2. 為什麼要採用這種技術選型而不是其他的?
  3. 引入了這種技術後,會帶來什麼問題嗎(比如額外的開發成本等)?

上面三個問題思考清楚之後,才能真正地去落地。上面三個問題思考清楚之後,才能真正地去落地。而有贊教育接入服務端渲染,正是為了優化 H5 頁面的首屏內容到達時間,帶來更好的用戶體驗(順便利於 SEO)。

說了這麼多,以下開始正文。

一、後端模版引擎時代

在較早時期,前後端的配合模式為:後端負責服務層、業務邏輯層和模版渲染層(表現層);前端只是實現頁面的交互邏輯以及發送 AJAX。比較典型的例子就是 JSP 或 FreeMarker 模板引擎負責渲染出 html 字符串模版,字符串模版裡的 js 靜態資源才是真正前端負責的東西。

而這種形式,就是天然的服務端渲染模式:用戶請求頁面 -> 請求發送到應用服務器 -> 後端根據用戶和請求信息獲取底層服務 -> 根據服務返回的數據進行組裝,同時 JSP 或 FreeMarker 模版引擎根據組裝的數據渲染為 html 字符串 -> 應用服務器講 html 字符串返回給瀏覽器 -> 瀏覽器解析 html 字符串渲染 UI 及加載靜態資源 -> js 靜態資源加載完畢界面可交互。

如何搭建一個高可用的服務端渲染工程

那麼既然後端模版引擎時代帶來的效果就是我們想要的,那為啥還有以後讓前端發展服務端渲染呢?因為很明顯,這種模式從開發角度來講還有挺多的問題,比如:

  1. 後端需要寫表現層的邏輯,但其實後端更應該注重服務層(和部分業務邏輯層)。當然,其實也可以讓前端寫 JSP 或 FreeMarker,但從體驗上來說,肯定不如寫 JS 來的爽;
  2. 本地開發的時候,需要啟動後端環境,比如 Tomcat,影響開發效率,對前端也不友好;
  3. 所賦予前端的能力太少,使得前端需要的一些功能只能由後端提供,比如路由控制;
  4. 前後端耦合。

二、SPA 時代

後來,誕生了 SPA(Single Page Application),解決了上面說的部分問題:

  1. 後端不需要關心表現層的邏輯,只需要注重服務層和業務邏輯層就可以了,暴露出相應的接口供前端調用。這種模式也同時實現了前後端解耦。
  2. 本地開發的時候,前端只需要啟動一個本地服務,如:dev-server 就可以開始開發了。
  3. 賦予了前端更多的能力,比如前端的路由控制和鑑權,比如通過 SPA + 路由懶加載的模式可以帶來更好的用戶體驗。
如何搭建一個高可用的服務端渲染工程

但同時,也帶來了一些問題:

  1. 頁面的 DOM 完全由 js 來渲染,使得大部分搜索引擎無法爬取渲染後真實的 DOM,不利於 SEO。
  2. 頁面的首屏內容到達時間強依賴於 js 靜態資源的加載(因為 DOM 的渲染由 js 來執行),使得在網絡越差的情況下,白屏時間大幅上升。

三、服務端渲染

正因為 SPA 帶來的一些問題(尤其是首屏白屏的問題),接入服務端渲染顯得尤為必要。// 終於講到服務端渲染這個重點了。

而正是 Node 的發展和基於 Virtual DOM 的前端框架的出現,使得用 js 實現服務端渲染成為可能。因此在 SPA 的優勢基礎上,我們順便解決了因為 SPA 引入的問題:

  1. 服務端渲染的首屏直出,使得輸出到瀏覽器的就是完備的 html 字符串模板,瀏覽器可以直接解析該字符串模版,因此首屏的內容不再依賴 js 的渲染。
  2. 正是因為服務端渲染輸出到瀏覽器的是完備的 html 字符串,使得搜索引擎能抓取到真實的內容,利於 SEO。
  3. 同時,通過基於 Node 和前端 MVVM 框架結合的服務端渲染,有著比後端模版引擎的服務端渲染更明顯的優勢:可以優雅降級為客戶端渲染(這個後續會講,先佔個坑)。

3.1 實現

既然服務端渲染能帶來這麼多好處,那具體怎麼實現呢?從官網給出的原理圖,我們可以清晰地看出:

  • Source 為我們的源代碼區,即工程代碼;
  • Universal Appliation Code 和我們平時的客戶端渲染的代碼組織形式完全一致,只是需要注意這些代碼在 Node 端執行過程觸發的生命週期鉤子不要涉及 DOM 和 BOM 對象即可;
  • 比客戶端渲染多出來的 app.js、Server entry 、Client entry 的主要作用為:app.js 分別給 Server entry 、Client entry 暴露出 createApp() 方法,使得每個請求進來會生成新的 app 實例。而 Server entry 和 Client entry 分別會被 webpack 打包成 vue-ssr-server-bundle.json 和 vue-ssr-client-manifest.json(這兩個 json 文件才是有用的,app.js、Server entry 、Client entry 可以抽離,開發者不感知);
  • Node 端會根據 webpack 打包好的 vue-ssr-server-bundle.json,通過調用 createBundleRenderer 生成 renderer 實例,再通過調用 renderer.renderToString 生成完備的 html 字符串;
  • Node 端將 render 好的 html 字符串返回給 Browser,同時 Node 端根據 vue-ssr-client-manifest.json 生成的 js 會和 html 字符串 hydrate,完成客戶端激活 html,使得頁面可交互。
如何搭建一個高可用的服務端渲染工程

3.2 優化

按照 Vue SSR 官方文檔建立起一個服務端渲染的工程後,是否就可以直接上線了呢?別急,我們先看看是否有什麼可以優化的地方。

3.2.1 路由和代碼分割

一個大的 SPA,主文件 js 往往很大,通過代碼分割可以將主文件 js 拆分為一個個單獨的路由組件 js 文件,可以很大程度上減小首屏的資源加載體積,其他路由組件可以預加載。

 複製代碼

// router.js
constIndex =()=>import(/* webpackChunkName: "index" */'./pages/Index.vue');
constDetail =()=>import(/* webpackChunkName: "detail" */'./pages/Detail.vue');
constroutes = [
{
path:'/',
component: Index
},
{
path:'/detail',
component: Detail
}
];
constrouter =newRouter({
mode:'history',
routes
});

3.2.2 部分模塊(不需要 SSR 的模塊)客戶端渲染

因為服務端渲染是 CPU 密集型操作,非首屏的模塊或者不重要的模塊(比如底部的推薦列表)完全可以採用客戶端渲染,只有首屏的核心模塊採用服務端渲染。這樣做的好處是明顯的:1. 較大地節省 CPU 資源;2. 減小了服務端渲染直出的 html 字符串長度,能夠更快地響應給瀏覽器,減小白屏時間。

 複製代碼

// Index.vue
asyncData({ store }) {
returnthis.methods.dispatch(store);// 核心模塊數據預取,服務端渲染
}
mounted() {
this.initOtherModules();// 非核心模塊,客戶端渲染,在 mounted 生命週期鉤子裡觸發
}

3.2 3 頁面緩存 / 組件緩存

頁面緩存一般適用於狀態無關的靜態頁面,命中緩存直接返回頁面;組件緩存一般適用於純靜態組件,也可以一定程度上提升性能。

 複製代碼

// page-level caching
constmicroCache = LRU({
max:100,
maxAge:1000// 重要提示:條目在 1 秒後過期。
})
server.get('*', (req, res) => {
consthit = microCache.get(req.url)
if(hit) {// 命中緩存,直接返回頁面
returnres.end(hit)
}
// 服務端渲染邏輯
...
})
 

 複製代碼

// component-level caching
// server.js
constLRU =require('lru-cache')
constrenderer = createRenderer({
cache: LRU({
max:10000,
maxAge: ...
})
});
// component.js
exportdefault{
name:'item',// 必填選項
props: ['item'],
serverCacheKey:props=>props.item.id,
render (h) {
returnh('div',this.item.id)
}
};

3.2.4 頁面靜態化

如果工程中大部分頁面都是狀態相關的,所以技術選型採用了服務端渲染,但有部分頁面是狀態無關的,這個時候用服務端渲染就有點浪費資源了。像這些狀態無關的頁面,完全可以通過 Nginx Proxy Cache 緩存到 Nginx 服務器,可以避免這些流量打到應用服務器集群,同時也能減少響應的時間。

3.3 降級

進行優化之後,是否就可以上線了呢?這時我們想一想,萬一服務端渲染出錯了怎麼辦?萬一服務器壓力飆升了怎麼辦(因為服務端渲染是 CPU 密集型操作,很耗 CPU 資源)?為了保證系統的高可用,我們需要設計一些降級方案來避免這些。具體可以採用的降級方案有:

  • 單個流量降級 – 偶發的服務端渲染失敗降級為客戶端渲染
  • Disconf / Apollo 配置降級 – 分佈式配置平臺修改配置主動降級,比如可預見性的大流量情況下(雙十一),可提前通過配置平臺將整個應用集群都降級為客戶端渲染
  • CPU 閾值降級 – 物理機 / Docker 實例 CPU 資源佔用達到閾值觸發降級,避免負載均衡服務器在某些情況下給某臺應用服務器導入過多流量,使得單臺應用服務器的 CPU 負載過高
  • 旁路系統降級 – 旁路系統跑定時任務監控應用集群狀態,集群資源佔用達到設定閾值將整個集群降級(或觸發集群的自動擴容)
  • 渲染服務集群降級 – 若渲染服務和接口服務是獨立的服務,當渲染服務集群宕機,html 的獲取邏輯回溯到 Nginx 獲取,此時觸發客戶端渲染,通過 ajax 調用接口服務獲取數據
如何搭建一個高可用的服務端渲染工程

3.4 上線前準備

3.4.1 壓測

壓測可以分為多個階段:本地開發階段、QA 性能測試階段、線上階段。

  • 本地開發階段:當本地的服務端渲染開發完成之後,首先需要用 loadtest 之類的壓測工具壓下性能如何,同時可以根據壓測出來的數據做一些優化,如果有內存洩漏之類的 bug 也可以在這個階段就能被發現。
  • QA 性能測試階段:當通過本地開發階段的壓測之後,我們的代碼已經是經過性能優化且沒有內存洩漏之類嚴重 bug 的。部署到 QA 性能測試環境之後,通過壓真實 QA 環境,和原來的客戶端渲染做對比,看 QPS 會下降多少(因為服務端渲染耗更多的 CPU 資源,所以 QPS 對比客戶端渲染肯定會有下降)。
  • 線上階段:QA 性能測試階段壓測過後,若性能指標達到原來的預期,部署到線上環境,同時可以開啟一定量的壓測,確保服務的可用性。

3.4.2 日誌

作為生產環境的應用,肯定不能“裸奔”,必須接入日誌平臺,將一些報錯信息收集起來,以便之後問題的排查。

3.4.3 灰度

如果上線服務端渲染的工程是提供核心服務的應用,應該採用灰度發佈的方式,避免全量上線。一般灰度方案可以採用:百分比灰度、白名單灰度、自定義標籤灰度。具體採用哪種灰度方式看場景自由選擇,每隔一段時間觀察灰度集群沒有問題,所以漸漸增大灰度比例 / 覆蓋範圍,直到全量發佈。

3.5 落地

在有贊電商的服務端渲染的落地場景中,我們抽離了單獨的依賴包,提供各個能力。

如何搭建一個高可用的服務端渲染工程

3.6 效果

從最終的上線效果來看,相同功能的頁面,服務端渲染的首屏內容時間比客戶端渲染提升了 300%+。

如何搭建一個高可用的服務端渲染工程

3.7 Q & A

Q1:為什麼服務端渲染就比客戶端渲染快呢?

A:首先我們明確一點,服務端渲染比客戶端渲染快的是首屏的內容到達時間(而非首屏可交互時間)。至於為什麼會更快,我們可以從兩者的 DOM 渲染過程來對比:

客戶端渲染:瀏覽器發送請求 -> CDN / 應用服務器返回空 html 文件 -> 瀏覽器接收到空 html 文件,加載的 css 和 js 資源 -> 瀏覽器發送 css 和 js 資源請求 -> CDN / 應用服務器返回 css 和 js 文件 -> 瀏覽器解析 css 和 js -> js 中發送 ajax 請求到 Node 應用服務器 -> Node 服務器調用底層服務後返回結果 -> 前端拿到結果 setData 觸發 vue 組件渲染 -> 組件渲染完成

如何搭建一個高可用的服務端渲染工程

服務端渲染:瀏覽器發送請求 -> Node 應用服務器匹配路由 -> 數據預取:Node 服務器調用底層服務拿到 asyncData 存入 store -> Node 端根據 store 生成 html 字符串返回給瀏覽器 -> 瀏覽器接收到 html 字符串將其激活:

如何搭建一個高可用的服務端渲染工程

我們可以很明顯地看出,客戶端渲染的組件渲染強依賴 js 靜態資源的加載以及 ajax 接口的返回時間,而通常一個 page.js 可能會達到幾十 KB 甚至更多,很大程度上制約了 DOM 生成的時間。而服務端渲染從用戶發出一次頁面 url 請求之後,應用服務器返回的 html 字符串就是完備的計算好的,可以交給瀏覽器直接渲染,使得 DOM 的渲染不再受靜態資源和 ajax 的限制。

Q2:服務端渲染有哪些限制?

A:比較常見的限制比如:

  1. 因為渲染過程是在 Node 端,所以沒有 DOM 和 BOM 對象,因此不要在常見的 Vue 的 beforeCreate 和 created 生命週期鉤子裡做涉及 DOM 和 BOM 的操作
  2. 對第三方庫的要求比較高,如果想直接在 Node 渲染過程中調用第三方庫,那這個庫必須支持服務端渲染

Q3:如果我的需求只是生成文案類的靜態頁面,需要用到服務端渲染嗎?

A:像這些和用戶狀態無關的靜態頁面,完全可以採用預渲染的方式(具體見 Vue SSR 官方指南),服務端渲染適用的更多場景會是狀態相關的(比如用戶信息相關),需要經過 CPU 計算才能輸出完備的 html 字符串,因此服務端渲染是一個 CPU 密集型的操作。而靜態頁面完全不需要涉及任何複雜計算,通過預渲染更快且更節省 CPU 資源。


分享到:


相關文章: