服務端渲染 -SSR- 通用技術解決方案

項目背景

服務端渲染 (SSR) 通用技術解決方案的誕生來源於對 360 搜索百科移動端項目的一次重構實踐。而當時決定重構該項目的主要原因有以下幾點:

1.  技術棧陳舊,熟悉、開發以及維護成本都較高

項目重構前的開發技術棧是 smarty + php + jQuery 的模式。在前端技術發展迅速的今天,vue 與 react 裹挾下的組件化開發模式愈發盛行,模塊化語法開發模式也是層出不窮,這使得 smarty 開發模式對新人來說理解起來比較生澀難懂。

2. 項目結構管理混亂

項目重構之前存在 2 個代碼庫並存的狀況。之前的同學爲了項目開發的模式更加與時俱進,對項目的圖冊頁進行了單獨的抽離,採用了當時最新的 esNext 語法 和 webpack 的模塊化開發模式進行了一次重構。但項目的開發流程很複雜:

    - 首先兩個分離的項目需要並存在同一個開發環境下。

    - 其次需要對圖冊頁的 webpack 進行自定義的配置,才能使得圖冊頁項目的開發代碼被構建到另一個整體代碼項目中,這需要對 webpack 有一定的瞭解。

    - 最後編譯到整體代碼中的 css 文件中還需要手動刪除多餘的 js 文件避免內部編譯的報錯。

3. 重構的目標

除去上述兩種原因外,由於該項目的週期已經非常久遠,隨着開發維護的不斷變更,很多項目模塊、代碼文件也已經變的非常臃腫。所以本次的重構,除了要統一所有的頁面技術棧,通過 eslint、commitlint、stylelint 等工具來統一開發規範外,重新梳理組織項目文件和目錄結構以及相關業務模塊也是重中之重。

重構技術選型

 對於該項目的重構,最初我們構想了三種方案:

  1. 保持當前的技術棧 smarty + php + jQuery 模式,對現有的模塊及目錄結構進行重新梳理和調整。

  2. 採用 React + Next + Webpack 的技術棧做 SSR 渲染。

  3. 採用 Vue + Nuxt + Webpack 的技術棧做 SSR 渲染

首先,無論從技術發展的角度,或是從後期項目的維護與開發效率來看,方案一都失去了重構本來的初衷。畢竟,擁抱前沿技術,提高開發效率的重構才更有意義。

其次,我們對 React + Next 和 Vue + Nuxt 的技術棧做了調研對比:

  1. 在相同吞吐量的情況下,兩者的平均響應時間幾乎是相同的,都比 smarty 模式要慢但在可接受範圍內。

  2. 從學習成本來說 vue 的技術棧更爲大家熟悉和容易接受,這在一定程度上可以減少學習和開發時間。

  3. 鑑於同期開展的 solib/m-vue 項目,可以在本次採用 vue 技術棧重構的同時,將可公用的模塊進行組件化抽離到 solib/m-vue 中,形成一個既支持 SSR 又支持 CSR 模式的組件庫,爲後續其他移動端項目的重構提升開發效率。

  4. 團隊的星座項目就採用了 vue + nuxt + webpack 的技術棧,有可以借鑑的案例。

鑑於上述的對比和考慮,我們決定採用 Vue + Nuxt + Webpack 的技術棧進行 SSR 渲染模式的方案。

什麼是 SSR 渲染?

在技術選型上,無論是採用 React 或是 Vue 我們都提到了一個關鍵字 —— SSR 渲染。

什麼是 SSR 渲染模式,以及爲什麼要使用 SSR 渲染呢?我們以 Vue 的 SSR 渲染模式爲例。

CSR 模式

先從 CSR 模式說起,CSR 又稱 "客戶端渲染"(Client Side Render),它還有另一個常見的稱呼 ——"單頁應用" (SPA)。

SPA 的請求流程大概如下:

圖片

SPA 模式的請求執行順序至少包含兩個來回(空的 html 頁面和數據的獲取渲染),它有兩個痛點:

  1. 首屏內容到達時間較長,也就是常說的白屏時間較長。畢竟先渲染的是一個空白的 html 結構,然後再發送請求獲取相關的頁面數據信息進行渲染。

  2. SEO 不夠友好,搜索引擎爬蟲抓取工具需要直接查看完全渲染的頁面。

    爬蟲可以很好的對同步 JavaScript 應用程序進行索引,同步是關鍵。如果應用程序初始展示 loading 狀態,然後通過 Ajax 獲取內容,抓取工具並不會等待這個異步的過程。所以,如果 SEO 對站點至關重要,而頁面又是異步獲取內容,CSR 顯然不能滿足需求。

於是,便有了 SSR 渲染模式。

SSR 模式

SSR 被稱爲 "服務端渲染"(Server Side Render)。

Vue.js 是構建客戶端應⽤程序的框架。默認情況下,它是在瀏覽器中輸出 Vue 組件,進⾏ DOM 的⽣成與操作。

然而,如果將⼀個組件在服務器端渲染成帶有靜態標****記的 HTML 字符串,然後將它們直接發送到瀏覽器,最後將這些靜態標記進行 "激活",使其成爲客戶端上完全可交互的應⽤程序,一個 SSR 渲染模式的雛形就形成了。

更進一步,如果只針對頁面刷新或第一次訪問服務的 url 在服務端渲染出對應的 dom 結構進行返回。然後將類似於路由信息狀態信息等以⼀種延遲的⽅式返回到客戶端,當客戶端獲取到⾸屏結構和 spa 運⾏機制後,對帶有靜態標記的部分進⾏激活,然後使其按照 spa 的模式運⾏就更完美了,這就是常說的 "首屏渲染" 模式。

由於程序⼤部分的代碼可以在服務器和客戶端上同時使⽤,Vue SSR 很多時候也被稱爲 "同構" 應⽤。

來看一下它的請求流程圖:

圖片

Vue SSR 構建基礎原理

下圖就是 vue ssr 構建的基礎原理圖,詳情可以參考 Vue.js 服務器端渲染指南

它是通過 webpack 在構建服務時,生成兩份配置:一份客戶端 bundle.js、一份服務端 bundle.js。

服務端的 bundle 用來渲染生成首次訪問的 html 字符串並返回給客戶端,到了客戶端之後實際上掛載執行的是客戶端的 bundle,這個流程在客戶端叫 "Hydrate" 。類似於泡麪,服務端負責包裝出售,客戶端負責拆袋加水放調料即可。

圖片

SSR 通用解決方案都有什麼?

說完 SSR 渲染的原理,我們來看看 SSR 通用解決方案都有什麼內容。

  1. so_nuxt 腳手架

    首先,項目的重構是站在巨人的肩膀上開展的,我們使用了針對 Vue.js 設計的 ssr 開發框架 —— Nuxt.js。而 so_nuxt 腳手架則是以 nuxt 爲基礎,在適合我們當前開發和部署的環境下進行了功能補充。

    我們對重構的項目結構進行抽象化剝離,去掉業務代碼部分,將公共部分封裝成通用模板文件。接入了詢問流程,根據使用者的意願生成不同開發形式的項目結構。同時,將 so_nuxt 腳手架接入了我們搜索前端團隊開發的 so-cli 通用腳手架工程中。使用者在安裝 so-cli 後,可以像使用 npm 或 yarn 指令一樣在全局通過 socli run 創建 SSR 項目,便利且快速!

    在 so_nuxt 腳手架創建的項目中,提供了服務端日誌打印、node 服務的探針配置以及 solib/m-vue 組件庫接入等示例方案。

  2. nuxt2-qcdn-plugin 插件

    靜態資源上傳至 qcdn 是一個項目上線前最基礎的優化方案。它不僅能加速網站的訪問速度,還擁有實現跨運營商、跨地域的全網覆蓋,更能保障網站的安全,節約成本的投入等等好處。nuxt 獨特的資源構建目錄無法兼容先前組內同學開發的 webpack-qcdn-file 插件。於是,爲了便於後續的維護和其他項目的接入,我們爲 nuxt 單獨開發了上傳靜態資源的插件。

  3. CSR 兜底策略

    爲了保證服務的穩定性,確保在 node 服務出現無法預料的突發狀況下仍能保證線上服務可以訪問,我們設計了 CSR 兜底策略。項目中接入了團隊同學開發的 vm-spider 工程,在服務變更成 CSR 模式後,不僅保證了服務的可訪問性,更不影響爬蟲的收錄。

  4. 所有信息記錄在案

    鑑於 SSR 通用解決方案並非都是代碼角度的實現,我們對其他的流程的配置提供了詳細的 wiki 信息,如報警配置參考方案、項目壓測參考方案、報警問題排查參考方案等等。

當前架構設計

下圖是當前項目重構的 SSR 通用解決方案的架構設計,如圖:

圖片

SSR 項目結構下,服務端只負責提供接口數據即可,數據的加工與視圖的結合均由 node 服務來處理。服務部署時通過配置的環境變量 ssr 爲 true 或是 false 來決定選用 ssr 服務還是 csr 服務。不過請注意,在選用 csr 服務時,就不需要部署 node 環境了。

當開發 SSR 服務時,我們目前的建議是將服務端項目與前端項目放在同一個部署下。

一方面可以直接通過 node 服務去請求本地環境下的接口數據,也就是將 axios 的 baseURL 設置成 https://127.0.0.1:443,這種 baseURL 的設置方式是我們通過實踐得到的最佳方案,我們將在後續性能優化章節中說到。

另一方面,由於當前強跳 https 的配置是在 nginx 的 80 端口中設置的,爲了避免每次的接口請求都會多一次轉發操作,我們選擇在 node 服務端直接請求 443 端口。

項目重構都經歷了什麼?

理論來源於實踐,SSR 通用解決方案的架構設計之所以是當前的模樣,還要從項目重構過程中的各種細節說起。項目的重構大致經歷了以下環節:

solib/m-vue 結合 nuxt

使用 nuxt 做服務端渲染,包含 window、document 等關鍵字的組件或者工具方法是不能直接通過 import 引入的,這會導致在編譯階段發生報錯。

經過摸索,我們同時對 solib 庫做了 cjs 的構建處理,然後在特殊的地方進行動態的 require 引用模式。另外 nuxt 對於 lodash 等庫的編譯也存在一些問題,這些問題的解決方案都已經補充到 so_nuxt 通用腳手架中了。

最初的架構圖

下圖是項目初始階段重構個別頁面的架構設計圖:

圖片

重構最初的想法是將前後端項目進行分離,便於開發時前後端同學只需要關注各自的開發層面即可。服務端同學不需要關心前端 package.json 中的依賴安裝,前端同學也不需要關注服務端的初始化流程。另一個也是最重要的原因,前端和服務端可以分開部署,兩者的各種部署配置也都可以分離。

而訪問的流程就是服務先進入服務端的 nginx,然後判斷是當前重構的頁面時再代理到前端的 node 服務,接下來就進入我們所說的 ssr 渲染流程。

但是重構結束後,我們進入了漫長的性能優化階段。

性能優化(一)

最初的版本,壓測信息是:平均響應時間 1275.75ms,QPS:38/s,完全不滿足上線的條件。

最初的關注焦點是 nuxt 的性能問題,比如 dom 節點過多導致的渲染瓶頸。於是嘗試減少服務端渲染 dom 的數量,讓那些對爬蟲不重要的部分 dom 模塊(如信息流板塊)在客戶端層面渲染,同時將部分插件的引入時機也調整到客戶端層面,比如打點用的 log.js,其他細節優化暫時忽略。

部署時在同一個機房部署了 6 個 pod,配置是 nginx: 4G_2 核,node:6G_4 核,壓測流程是從平臺配置的 lvs 作爲入口。

經過壓測,平均響應時間已經降低到了 500ms 以下,最佳的併發數是 30,最大的 QPS 是 221/s。但是 P99 的毛刺較多且都在 3s 以上,P95 更是大於了 250ms。雖然滿足了上線的基礎條件,但是小流量上線後,報警頻率比較高,考慮到沒有完善的兜底方案,暫時做了下線處理,繼續尋找優化點。

壓測信息如下:

圖片

20-40 併發,不固定 QPS

圖片

30 併發,QPS = 120

性能優化(二)

鑑於當時沒有找到針對 P99 和 P95 的合適優化點,同時團隊同學開發的 vm-spider 工程取得了一定的成果。我們嘗試了 CSR 渲染模式,通過在 nginx 中判斷 UA 信息,使得用戶訪問是 CSR 模式,爬蟲則通過 vm-spider 走代理模式。同時添加了骨架屏代替白屏做體驗優化以及別的工作點。

同樣的部署配置:6 個 pod、nginx: 4G_2 核,node:6G_4 核,壓測流程是從平臺配置的 lvs 作爲入口。

CSR 渲染模式在響應時間和 QPS 上肯定是沒有問題的。但是通過壓測響應曲線發現,P99 和 P95 還是會出現個別延遲較高的毛刺。另外,接口本身有內網限制,同時還要接入內容保護策略,這都需要服務端同學配合處理,導致開發成本的增加。

壓測信息如下:

圖片

25-50 併發,不固定 QPS

性能優化小結

經過上述兩輪的優化,我們發現 P99 和 P95 的毛刺延遲較高的情況一直存在,這是上線後經常報警的原因,更是一直需要做性能優化的元兇。經過多方的溝通排查,發現壓測的 LVS 地址在配置時被直接指向了雲平臺的 VIP 節點,這使得服務的中間又多了一層負載均衡。如下:

圖片

錯誤的配置

圖片

正確的配置

爲了確定後續性能優化的着力點確實是在 node 服務之前,我們專門壓測了前端部署的 VIP 節點地址進行了驗證。

部署配置:6 個 pod、nginx: 2G_2 核、node:4G_2 核

圖片

不固定 QPS, 20-50 併發

可以看到,在 50 併發之前,P99 與 P95 均在 500ms 波動,最大 QPS 不到 300,並沒有出現毛刺較高的情況。所以,可以肯定毛刺的頻繁出現且延遲較高的原因在 node 服務之外。

性能優化(三)

經過上述的排查與結論可知,目前的性能瓶頸並不是 SSR 渲染模式的問題。而 CSR 模式針對爬蟲的處理也主要依賴於 vm-spider 工程這種外力,從項目的穩定性與耦合性來看是不合適的。

所以,我們決定重新迴歸 SSR 渲染模式。

在前期的壓測過程中,我們發現前端部署服務的 cpu 和 內存 的使用率都比較低,我們又嘗試了三種方案:

pm2 模式

1. 壓測信息對比

部署配置:6 個 pod、nginx: 2G_2 核、node:4G_2 核,直接壓測前端的 VIP 節點地址。

pm2 模式下,分別實驗了在一個 pod 中開啓最多實例數、2 個實例數、以及一半實例數的方案,進行了壓測對比。

發現每個 pod 開啓 2 個實例數,性能是最佳的。對比信息如下:

圖片

pm2 壓測對比

2. pm2 接入的問題

**問題 1:**pm2 在 docker 環境和非 docker 環境下的啓動命令執行的結果如下:

圖片

pm2 與 pm2-runtime 的區別:

docker 容器的生命週期就是 CMD 或 entrypoint 的生命週期。在這種情況下,使用 pm2 start 容器將在運行過程後立即死亡,導致無法訪問,所以我們需要採用 docker-runtime start 的方式來啓動。

**問題 2:**然而在 nuxt.js 的源碼中,啓動腳本 nuxt start 會將啓動時傳入的配置文件(假如叫 ecosystem.config.js )當成項目的目錄進行嵌套,導致找不到最終構建的應用文件而出現報錯。

報錯信息如下:

圖片

pm2 啓動下 nuxt 報錯信息

**解決方案:**將 nuxt 的源碼中 @nuxt/config/dist/config.js 中的 loadNuxtConfig 下的 rootDir 賦值成 process.cwd(),也就是取當前項目的根目錄即可。

**問題 3:**在容器啓動時,原來版本的雲平臺沒有對環境變量進行隔離,導致所有的環境變量都可以被取到。環境變量過多會導致 pm2 的啓動失敗。

解決方案: 通過啓動服務時,執行腳本遍歷並刪除不相關的環境變量解決了該問題。

從上述情況來看,pm2 的接入方案本身就存在很大的問題。

調整 pod 數模式

部署配置:

    - 6 個 pod

    - 單 pod nginx: 1G_1 核、node:1G_1.5 核

    - 不固定 QPS,併發 20 -- 60

    - 壓測流程是從平臺配置的 lvs 作爲入口

該模式下分別壓測了 12、14、20 個 pod 數。隨着 pod 數的增加,併發、響應時間和 QPS 都會在一定程度上增加。14 個 pod 明顯比 12pod 性能要好,但增加到 20pod 後,與 14pod 相比,性能並沒有達到預期。

所以,選擇了 14pod 爲最優解。對比信息如下:

圖片

不同 pod 數壓測對比

多方案對比

緩存方案,我們僅嘗試了 lru-cache 方案的可行性,使用了總內存(1G)的 1/8,大約能存儲 1000 多條信息。

採用不固定 QPS 的壓測方式,同時對三種方案進行了壓測對比。對比信息如下:

圖片

多方案對比

從開發角度上,pm2 的接入、維護以及監控都會增加開發成本,同時流程也會變得很複雜,而動態調整 pod 數不存在這些問題。

從性能角度上,14pod 在響應時間、qps 以及併發上都優於 pm2。

所以,動態調整 pod 數可以作爲最優解決方案。考慮到 lru-cache 緩存的數據在多 pod 間是不共享的,爲了避免線上出現內存溢出等問題,上線的方案中沒有采用該緩存方案。

上線後的新問題

經過多輪性能優化的 SSR 渲染模式上線後,延遲過高的情況基本消失了。但 P99 報警仍過於頻繁,幾乎每兩天都會有一輪報警,每幾天就會出現個別前端 pod 重啓的情況。

對於單 pod 重啓的因素大概有以下幾點:

  1. 外部因素。首先,同一個服務下的多個 pod 部署時會隨機落在各個機房上。當機房出現問題時,會自動重啓當前機器上的全部 pod。

  2. 服務自身因素。當延遲過高積壓的任務足夠多時,如果 cpu 和 內存的使用率出現了暴增的情況就會導致 pod 的重啓;另外,如果服務端或 node 服務的探針一直沒有響應時,會導致相關的容器重啓,重複如此也會導致整個 pod 的重啓。

  3. 單 pod 自愈方案。它通過配置延遲時間的最大臨界點,在服務的響應效率已經不達標時立刻做出重啓的回調處理,解決人力反應不及時的突發情況。該方案是後期做通用解決方案時爲了保證服務穩定性才添加的。

根據上述因素並結合 Prometheus 平臺中 nginx 狀態碼 499 出現頻率較高的情況,考慮是第二個因素的問題。 

分析多天的日誌信息後,我們發現重啓的 pod 中 axios 請求超時的量確實很多,而且前端 nginx 日誌中部分請求超時的日誌信息在服務端 nginx 日誌中卻找不到對應的記錄,請求信息丟失了。

經過排查發現是服務的邊緣節點之間會有丟包的情況。

而且,當前的服務鏈路也很長,如 nginx 多層轉發,域名解析等等。所以,最終決定將前後端項目放在一起。首頁渲染時,每個 pod 中的 node 服務去請求對應 pod 中的服務端即可,於是就有了文章中最初的架構設計模式。

最終的結論

部署配置:nginx 2G_2 核、php 4G_2 核、node 4G_2 核

不同 pod 數的壓測結果如下:

圖片

不同 pod 數壓測記錄

圖片

對標當前線上情況的穩定性壓測

依據團隊內部定製的前端項目代碼質量衡量標準對重構前後的版本做了性能方面的數據對比,兩個版本整體的性能質量幾乎持平:

圖片

總結

對於知識產品類的服務網站來說,SEO 對搜索引擎的爬蟲抓取工具的重要性以及快速的內容到達時間都是非常重要的指標。而本次的項目重構,不僅驗證了 SSR 渲染模式的可行性,更爲前端同學在後續工作中擁抱前沿技術的嘗試提供了可行性參考方案。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/A-loxh4_vMZRqTV1kU5TCQ