作為前端,我們知道 node 在構(gòu)建
方面是成功的,我們也聽說過全棧,那么 node 是否能應(yīng)用在企業(yè)級的后端
?一起來看一下騰訊視頻的 NodeJs 改造
。
Tip: 故事大概是 2018 年,主角楊浩
,來源于:
(資料圖)
背景騰訊視頻是一個內(nèi)容型
的網(wǎng)頁。
在 2014 年以前使用的是 C++ 動態(tài)生成頁面。有兩個問題:
前端不太會維護 C++ 的那套東西C++ 定時生成網(wǎng)頁。有多少個視頻,它就會生成多少個網(wǎng)頁,然后推送到對應(yīng)的服務(wù)器中。如果更改了某個視頻的信息,得等到下次生成網(wǎng)頁才會更新于是打算使用 NodeJs
來對其進行改造。
由于騰訊視頻是內(nèi)容型
的網(wǎng)頁,當(dāng)時有 30% 的流量來自搜索引擎,所以需要更好的 SEO,于是選用 SSR(服務(wù)器渲染)。
Tip: Vue_SSR中也提到服務(wù)端渲染的優(yōu)勢:更快的首屏加載、更好的 SEO
NodeJs 扮演的角色如下:
請求經(jīng)過 cdn,經(jīng)過 nginx 通過負載均衡訪問 NodeJs 服務(wù),NodeJs 從各個后臺服務(wù)拉取數(shù)據(jù),渲染好了在返回給前端。
Tip:相當(dāng)于以前用 c++ 生成頁面,現(xiàn)在由 NodeJs 生成頁面。
打通 RPC 調(diào)用rpc(作用類似 http 協(xié)議) 就是遠端資源調(diào)用,因為 node 需要從各個后臺服務(wù)拉取數(shù)據(jù)。這里涉及4個方面的事情:
負載均衡
。node 和后臺服務(wù)之間有一層負載均衡,用的是一種類DNS負載均衡
,所以得和負載均衡服務(wù)交互,拿到每次需要訪問服務(wù)器的ipMongo/mysql/redis
(redis - 基于鍵值對的內(nèi)存數(shù)據(jù)庫
) 存儲的打通。比較簡單,就是對應(yīng) npm 包的使用后臺私有協(xié)議
。例如二進制的協(xié)議某場景下比http協(xié)議好一些監(jiān)控系統(tǒng)/日志系統(tǒng)Tip: DNS除了能解析域名之外還具有負載均衡的功能
高并發(fā)下進程管理node 是單線程,使用 cluster 模塊創(chuàng)建多個 Nodejs 進程,實現(xiàn)高并發(fā)和高可用性。但 cluster 還有點缺陷,做了以下幾點優(yōu)化:
心跳
- master 定時給 cluster 發(fā)信息,如果有回復(fù)說明它還活著,否則就是僵死,就 kill 它內(nèi)存檢測
- 監(jiān)控 cluster 內(nèi)存,如果內(nèi)存過高,可能就是內(nèi)存泄漏,也殺死它重啟
- cluster kill 后,有的應(yīng)用可能不能用,就需要將其重啟Tip:在 Node.js 中,cluster 模塊提供了一種簡單的方式來創(chuàng)建多個 Node.js 進程,以實現(xiàn)高并發(fā)和高可用性。通過集群模塊,開發(fā)者可以使用現(xiàn)有的單線程程序代碼,并將其自動拆分到多個子進程中執(zhí)行,從而充分利用 CPU 和內(nèi)存資源,提高應(yīng)用的效率和穩(wěn)定性。
第二只怪 - 維護 NodeJs終于把 Node 打通了,現(xiàn)在可以用 node 寫點東西了。
要用 node 寫一個穩(wěn)定的服務(wù),也不是那么簡單。node 很容易掛掉,比如一點語法問題。
Node 人員不足懂前端的人很多,但懂 node 的就相對要少。寫后端需要懂后端那套東西,要會服務(wù)器調(diào)優(yōu),還要懂運維。
為了解決 Node 人員不足,決定使用框架
來平滑 node 曲線。
之前要用 node 寫項目難度大,是因為需要經(jīng)歷這4步:業(yè)務(wù)邏輯 -> 會寫 NodeJs -> 熟悉 rpc 調(diào)用 -> 熟悉運維(性能調(diào)優(yōu))
現(xiàn)在用框架,只需要寫業(yè)務(wù)邏輯
就能開干。
這里框架主要使用配置化
,屏蔽底層復(fù)雜的實現(xiàn),對外暴露友好的配置。就像 webpack,讓前端構(gòu)建生態(tài)非常繁榮。
要做配置化
,就得分析 ssr 本質(zhì):從各個后臺領(lǐng)取數(shù)據(jù),簡單處理后進行渲染。
ssr抽象表示:請求參數(shù) -> 后端數(shù)據(jù) + 模板 -> 頁面文本
ssr 公式:內(nèi)容=f(數(shù)據(jù)源,模板)
只要將數(shù)據(jù)源
和模板
配置化,就可以通過一個函數(shù)解決 ssr 的問題。
研究了如下幾種模板:
art-template 國內(nèi)有名的開源模板引擎es6 template string + vm.runInNewContext(編譯和運行代碼,作用類似 new Function("console.log("1")"))vue ssr、react ssrart-template 中的 forEach 可以使用預(yù)編譯語法
來實現(xiàn),由于交互較少,所以無需使用 vue和react。而且 es6 模板速度測試比 vue-server-render 快很多。
所以最終選取第二種方案:es6 template
。
數(shù)據(jù)源的配置用如下一個 json 表示:
module.exports = { video: { url: "http://...." }, vidviewcount: { dependencies: ["video"], url: "protobuf://union.video.qq.com/...." }, rank: { url: "redis://admin:admin@135246:65535/get?key=haha" }}
這個 json 表示 ssr 過程中數(shù)據(jù)獲取邏輯,其中 vidviewcount 通過 dependencies
字段指明依賴 video。
這里用 http、protobuf、redis三種協(xié)議
(方式)獲取數(shù)據(jù)。一個協(xié)議對應(yīng)一個請求器,不在框架中的協(xié)議可以注冊即可。就像這樣:
factory.registerRequestor("http", requestor);function requestor(){ ...}
為了增加配置的靈活性,這里增加了幾個 hook:
{ ... fixBefore: function(param){ // 檢測參數(shù)合法性 return param }, fixAfter: function(data){ // 檢測返回數(shù)據(jù)合法性 if(!data.vid){ throw Error("xxx") } return data }, onError: function(e){ return err; }}
寫配置就是寫 SSR 邏輯只要學(xué)會寫配置就能搞定 ssr 邏輯。
公式:內(nèi)容=f(數(shù)據(jù)源,模板)(參數(shù))
ssr 外部用 koa(nodejs 的web框架) 封裝一下就是一個服務(wù):
let app = koa()let ssr = pigfarm(data, template)app.use(async ctx => { ctx.body = await ssr(ctx.query)})
第三只怪 - 搶后端飯碗的問題后臺有后臺擅長的地方(邏輯、計算密集),前端有前端擅長的地方(前端網(wǎng)頁優(yōu)化)。
尋找一個合作共贏
的方式。這里做了如下幾個有特色的前端服務(wù):
每次業(yè)務(wù)邏輯的改動需要經(jīng)歷長時間的發(fā)布
和重啟
前面已經(jīng)將數(shù)據(jù)源
和模板
做到了配置化
,現(xiàn)在修改邏輯,只需要更改數(shù)據(jù)庫中的數(shù)據(jù)源和模板即可,做到熱更新。
v.qq.com 首頁包含27個模塊
富含個性化內(nèi)容,無法緩存頁面龐大,速度慢全網(wǎng)頁超過40個rpc個性化接口調(diào)用慢利用 transfer-encoding:chunked 快速返回首屏數(shù)據(jù),后面再加載2、3、4...屏的數(shù)據(jù)
Tip:BigPipe 是一個前端性能優(yōu)化技術(shù),采用分塊渲染的方式。transfer-encoding:chunked
是一種 HTTP協(xié)議中定義的傳輸編碼方式之一。運行服務(wù)器在不知道響應(yīng)體大小的情況下,將響應(yīng)分成若干個固定大小的快進行傳輸。
前端容災(zāi)是指在前端應(yīng)用中,為了保障可靠性和穩(wěn)定性而采用的一系列技術(shù)和策略,以確保即使在系統(tǒng)出現(xiàn)部分異常或錯誤的情況下,仍然可以正常提供服務(wù)。比如網(wǎng)絡(luò)問題、服務(wù)器故障等
這里可以做整頁備份
。
js 中用高階函數(shù)非常容易實現(xiàn)緩存。請看示例:
function memoize(func) { // 用于緩存 const cache = {}; return function(...args) { const key = JSON.stringify(args); // 如果緩存中有值,直接返回 if (cache[key]) { return cache[key]; } const result = func.apply(this, args); cache[key] = result; return result; };}