騰訊視頻 Node.js 服務是如何支撐國慶閱兵直播高併發的?

導語 | 上個月,我有幸參與了騰訊視頻國慶閱兵直播頁面開發的相關工作,最終,累計觀看2.38億人次,經受住了高併發的考驗。在參於Glama框架的開發維護及平時基礎建設相關討論實踐中,對高併發有一些部分實踐心得,正好老友也想了解騰訊視頻這邊的經驗,特撰寫本文,對相關經驗進行梳理總結,與大家探討。(本文作者:Lucienduan,騰訊視頻Web前端高級工程師)


本文將從服務可用性、緩存、日誌三個維度總結視頻側開發高併發 Node.js 服務的一些經驗。

一、視頻前端網絡結構

先介紹騰訊視頻前端網絡結構,網絡架構如下圖所示。

騰訊視頻 Node.js 服務的網絡示意圖

流程簡述如下:

用戶首先請求GSLB,找到最佳接入IP,就近訪問CDN節點;

CDN緩存命中時,直接響應緩存, 如果有CDN緩存失效或未配緩存, 會直接回源到TGW(Tencent Gateway), TGW主要處理容災、負載勻衡;

請求從TGW(STGW)轉發到業務層Nginx,在Nginx中也會有簡單的容災策略(主要由max_failsfail_timeout兩個設置配置)和緩存機制,最後到達Node服務;

在Node中用cluster模板轉發到對應的worker進程處理,worker中會跑具體的業務, 請求對應的後臺服務器。

這裡TGW主要功能:負載均衡、容災、IP收斂、多通接入。

系統整體的可靠性需要各個節點相互配合,本文主要針對由前端開發的負責的模塊, Node和業務這一節點為中心從可用性, 緩存和日誌發散來說高併發服務需要關注的點。

二、可用性

運維老司機說:沒有絕對可靠的系統,局部故障是常態。

但通過一些方法兜底和保護,可以保證核心業務無異常。

保證業務可用首先需要保證相關的進程工作正常,進程異常時能容災兜底。

進程守護

Node.js主進程守護,騰訊視頻這邊用shell腳本來描述執行:

通過 crontab 命令,定時1min鍾去檢查一次進程(用ps指令)和端口(用nc指令)是否正常, 異常時重啟服務。

在Nodejs Cluster模塊,主進程會把TCP分配給worker進程處理,worker進程主要三個問題, 殭屍進程, 內存洩露和進程異常退出。

殭屍(無響應)進程:當程序運行到死循環,就不再響應任何請求了,需要及時重啟:

在Master進程定時向worker進程發心跳包,當worker進程在一段時間多次不回包時, 殺死重啟。

內存監聽:主要為了兜底內存洩露問題, 當worker進程達到閾值時, 殺死重啟

進程退出:進程異常退出時, 需要重啟。

目前社區有比較多的工具可以實現進程守護,比如pm2。

頁面靜態化/預渲染

最安全的進程是沒有進程……即整個請求鏈中不依賴的Node.js服務。

靜態化示意圖

對於一些只有少數的幾個運營同學更新數據且可用性要求極高的頁面,可以直接由運營的發佈動作觸發頁面更新的CDN。

整個請求鏈環節少,無回源請求,異常的概率最低。

即使Node.js有多級的守護,但還是有可能進程內的分支邏輯或接口出現異常,當分支邏輯或接口異常出現時,合理的容災策略可以提供降級服務讓核心業務無影響,用戶無感知。

三、三層容災策略

如果上面守護異常,或是底層的依賴服務掛了,H5頁面有三層容災策略。

容災策略示意圖

1. 接口容災

接口容災主要應對依賴的底層接口異常。當後臺接口正常返回時,把數據緩存到redis,異常時,用redis的舊數據兜底。

2. 頁面HTML

兜底思路與口容災差不多,當頁面渲染異常時,中間件檢測到返回5xx,同樣用正常的緩存在redis的舊HTML兜底。

3. NodeJS容災

主要應對NodeJS工作異常,當NodeJS進程正常響應時,把靜態的HTML推到CDN作為備份文件, 如果NodeJS返回5xx時, 在Nginx代理層重定向到靜態備份文件。

從實踐來看,上面的進程worker的守護和容災兜底,可以很好的保證源站業務的穩定性,對於高併發業務,緩存和告警必不可少。

四、緩存

緩存在高併發的系統扮演著至關主要的角色,除了用戶態、推薦等少數業務場景不能用緩存外,緩存是應對流量衝擊簡單有效的方式, 目前視頻側主要有三級緩存, CDN緩存,代理層Nginx緩存,應用層redis緩存。

三級緩存示意圖

圖片來源:《Web前端與中間層緩存的故事》

CDN 緩存

CDN的OC節點不但可以減少用戶的訪問延時,也可以減少源站的負載,但Node.js站點在用CDN抗量時同時需要注意兩個問題。

1. 更新時間

由於CDN一般用於緩存靜態文件或更新粒度比較小的頁面,默認的緩存時間比較長,在接口上使用時需要注意更新時間,同時接口不能帶有隨機參數。

在70週年閱兵主持人頁面中,輪詢請求量非常大,接口用了CDN緩存,由於沒有太關注更新時間,導致接口更新不及時, 對於自建CDN, 需要注意cache-controllast-modified字段的更新,或是關注status頭查看回源狀態。

2. 緩存穿透、雪崩

目前自建CDN緩存沒有緩存鎖,當緩存失效到下一次緩存更新這一小段時間(一般在40~500ms),所有的請求都回源到源站,併發比較高時,會有大量穿透到源站,底層沒有保護的話可能引起雪崩, 所以需要多級緩存。

Nginx自帶緩存鎖,通過簡單的配置就可以解決這個問題。

Nginx代理層緩存

Nginx 除了提供基本的緩存能力外,還提供緩存鎖、緩存容錯能力,

proxy_cache_use_stale可以配置,錯誤, 超時,更新中和其它異常狀態時, 使用舊緩存兜底和避免過多的的流量穿透到源站。

同時proxy_cache_lock配置,可以防止配置沒有預熱時,緩存的穿透的問題。

當proxy_cache_lock被啟用時,當多個客戶端請求一個緩存中不存在的文件(或稱之為一個MISS),只有這些請求中的第一個被允許發送至服務器。其他請求在第一個請求得到滿意結果之後在緩存中得到文件。如果不啟用proxy_cache_lock,則所有在緩存中找不到文件的請求都會直接與服務器通信。

所以Nginx通過正常的配置,可以大大減少回源的請求,減輕源站的負載。

頁面緩存

在應用層或框架層,可以用redis實現第三層緩存,這層的redis緩存也是HTML渲染異常時兜底的基礎。實現思路比較簡單,需要關注兩個問題:

頁面緩存版本不同步時,有無適配問題,如果需要識別版本,版本不匹配的緩存直接失效。

是否需要設計緩存鎖來避免穿透問題,如果上層已處理(比如Nginx),或下層能抗量流量可以忽略不加鎖。

整頁緩存粒度比較大,可以針對業務場景做拆分,比如針對部分推薦數據的頁面拆分頁面片緩存或接口緩存。

從CDN、Nginx到redis,每一層的工作量、業務侵入性,粒度不一樣,業務需要根據自身場景, 選用適合自己業務的緩存即可。

五、日誌與告警

告警和日誌,對故障早發現早處理,覆盤根本原因至關重要。

目前視頻除了基礎平臺提供的CDN、TGW和機器的物理資源的監控外,在用戶側和代理層, 源站均有不同維度的告警和監控。

監控示意圖

客戶端提供了前端監控和告警,提供用戶側的監控,比如頁面質量,CGI質量, 用戶流水及手動上報的能力。

反向代理層 由Nginx上報監控,監控訪問波動,錯誤量佔比(4xx, 5xx)時耗時。

請求日誌 主要記錄原站的總請求數,請求失敗數據及平均耗時。

Nodejs進程日誌 主要進程異常退出,內存洩露,殭屍進程等進程日誌, 對業務穩定運行, 非常重要。

Node請求流水日誌 主要記錄請求維度的開發自定義日誌,用於問題的定位覆盤, 進程狀態觀測。

模調監控 監控請求方和服務方的錯誤和響應時間的情況,當前模塊與底層依賴模塊的接口實時接口質量。

每層的監控和日誌可以幫助業務快速瞭解業務狀態,定位業務異常。

總結來說:單個用戶異常,查看客戶端啄木鳥流水和Node請求流水日誌,服務大概率異常查模調和請求日誌,Node進程異常查看 代理層日誌和進程日誌,響應時間異常可以從客戶端、代理層、源站及模調的耗時逐步分析。

六、總結

可用性永遠是做框架和業務的同學需要關心重要指標,但是現狀如文初所說:沒有絕對可靠的系統

除了關注Node.js的業務開發質量,如何在流程和架構層面避免局部異常不影響整體業務和用戶體驗更值得更進一步思考。騰訊視頻在架構和框架的設計層面防呆,故障前進程守護,監控告警等方法避免和發現問題;故障中通過多級容災兜底提供降級服務;故障後通過各個節點的日誌定位問題改進回顧。保證質量參差不齊業務都能抗住高併發且高可用。