部署 Node.js 应用以完成服务器端渲染 Server Side Rendering 的性能调优
原文:Operationalizing Node.js for Server Side Rendering
在 Airbnb,我們花了數(shù)年時間將所有前端代碼穩(wěn)定地遷移到一致的架構(gòu)中,在該架構(gòu)中,整個網(wǎng)頁都被編寫為 React 組件的層次結(jié)構(gòu),其中包含來自我們 API 的數(shù)據(jù)。 Ruby on Rails 在將 Web 連接到瀏覽器方面所扮演的角色每天都在減少。事實上,很快我們將過渡到一項新服務(wù),該服務(wù)將完全在 Node.js 中提供完全形成的、服務(wù)器呈現(xiàn)的網(wǎng)頁。此服務(wù)將為所有 Airbnb 產(chǎn)品呈現(xiàn)大部分 HTML。這個渲染引擎不同于我們運行的大多數(shù)后端服務(wù),因為它不是用 Ruby 或 Java 編寫的。但它也不同于我們的心智模型和通用工具所圍繞的那種常見的 I/O 密集型 Node.js 服務(wù)。
當(dāng)您想到 Node.js 時,您會設(shè)想您的高度異步應(yīng)用程序同時高效地為數(shù)百或數(shù)千個連接提供服務(wù)。您的服務(wù)正在從整個城鎮(zhèn)提取數(shù)據(jù),并進(jìn)行應(yīng)用輕量級處理,以使其適合眾多客戶。也許您正在處理一大堆長期存在的 WebSocket 連接。您對非常適合該任務(wù)的輕量級并發(fā)模型感到滿意和自信。
服務(wù)器端渲染 (SSR) 打破了導(dǎo)致該愿景的假設(shè)。它是計算密集型的。 Node.js 中的用戶代碼在單個線程中運行,因此對于計算操作(與 I/O 相對),您可以并發(fā)執(zhí)行它們,但不能并行執(zhí)行。 Node.js 能夠并行處理大量異步 I/O,但會遇到計算限制。隨著請求的計算部分相對于 I/O 的增加,并發(fā)請求將對延遲產(chǎn)生越來越大的影響,因為 CPU 爭用。
考慮 Promise.all([fn1, fn2])。如果 fn1 或 fn2 是由 I/O 解析的承諾,您可以像這樣實現(xiàn)并行性:
如果 fn1 和 fn2 是計算的,它們將改為這樣執(zhí)行:
一個操作必須等待另一個完成才能運行,因為只有一個執(zhí)行線程。
對于服務(wù)器端渲染,當(dāng)服務(wù)器進(jìn)程處理多個并發(fā)請求時會出現(xiàn)這種情況。 并發(fā)請求將被正在處理的其他請求延遲:
在實踐中,請求通常由許多不同的異步階段組成,即使仍然主要是計算。 這可能導(dǎo)致更糟糕的交織。 如果我們的請求由一個像 renderPromise().then(out => formatResponsePromise(out)).then(body => res.send(body)) 這樣的鏈組成,我們可以有像這樣的請求交錯:
在這種情況下,兩個請求最終都會花費兩倍的時間。隨著并發(fā)性的增加,這個問題變得更糟。
此外,SSR 的共同目標(biāo)之一是能夠在客戶端和服務(wù)器上使用相同或相似的代碼。這些環(huán)境之間的一個很大區(qū)別是客戶端上下文本質(zhì)上是單租戶,而服務(wù)器上下文是多租戶的。在客戶端輕松工作的技術(shù)(如單例或其他全局狀態(tài))將導(dǎo)致服務(wù)器上并發(fā)請求負(fù)載下的錯誤、數(shù)據(jù)泄漏和一般混亂。
這兩個問題只會成為并發(fā)問題。在較低的負(fù)載水平下或在您的開發(fā)環(huán)境的舒適單一租戶中,一切通常都能正常工作。
這導(dǎo)致了與 Node 應(yīng)用程序的規(guī)范示例完全不同的情況。我們使用 JavaScript 運行時是因為它的庫支持和瀏覽器特性,而不是它的并發(fā)模型。在這個應(yīng)用程序中,異步并發(fā)模型強加了它的所有成本,沒有或只有很少的好處。
一些經(jīng)驗分享
用戶發(fā)送請求到我們的主要 Rails 應(yīng)用程序 Monorail,它將希望在任何給定頁面上呈現(xiàn)的 React 組件的 props 拼湊在一起,并使用這些 props 和組件名稱向 Hypernova 發(fā)出請求。 Hypernova 使用 props 渲染組件以生成 HTML 以返回到 Monorail,然后將其嵌入到頁面模板中并將整個內(nèi)容發(fā)送回客戶端。
在 SSR 渲染失敗(由于錯誤或超時)的情況下,回退是將組件及其道具嵌入頁面而不渲染 HTML,允許它們(希望)被客戶端成功渲染。 這導(dǎo)致我們將 SSR 視為一種可選的依賴項,并且我們能夠容忍一定數(shù)量的超時和失敗。 我們將調(diào)用超時設(shè)置為大約在我們調(diào)整值時觀察到的值。不出所料,我們以略低于 5% 的超時基線運行。
在日常流量負(fù)載高峰期進(jìn)行部署時,我們會看到高達(dá) 40% 的 SSR 請求發(fā)生超時。類似 BadRequestError: Request aborted on deploys 的這些錯誤,掩蓋了所有其他應(yīng)用程序/編碼錯誤。
我們曾將延遲歸咎于啟動延遲,而延遲實際上是由并發(fā)請求相互等待以使用 CPU 造成的。 從我們的性能指標(biāo)來看,由于其他正在運行的請求而等待執(zhí)行所花費的時間與執(zhí)行請求所花費的時間無法區(qū)分。 這也意味著并發(fā)導(dǎo)致的延遲增加看起來與新代碼路徑或功能導(dǎo)致的延遲增加相同——實際上增加了任何單個請求的成本。
BadRequestError: Request aborted 錯誤也變得越來越明顯,不能用一般的慢啟動性能來解釋。 該錯誤來自正文解析器,特別是在客戶端在服務(wù)器能夠完全讀取請求正文之前中止請求的情況下發(fā)生。 客戶端放棄并關(guān)閉連接,帶走我們繼續(xù)處理請求所需的寶貴數(shù)據(jù)。 發(fā)生這種情況的可能性要大得多,因為我們開始處理一個請求,然后我們的事件循環(huán)被另一個請求的渲染阻塞,然后從我們被中斷的地方返回完成,卻發(fā)現(xiàn)客戶端已經(jīng)離開了。
我們決定通過使用我們擁有大量現(xiàn)有操作經(jīng)驗的兩個現(xiàn)成組件來解決這個問題:反向代理 (nginx) 和負(fù)載均衡器 (haproxy)。
Reverse Proxying and Load Balancing
為了利用我們的 SSR 服務(wù)器上存在的多個 CPU 內(nèi)核,我們通過內(nèi)置的 Node.js 集群模塊運行多個 SSR 進(jìn)程。 由于這些是獨立的進(jìn)程,我們能夠并行處理并發(fā)請求。
這里的問題是每個節(jié)點進(jìn)程在請求的整個持續(xù)時間內(nèi)都被有效占用,包括從客戶端讀取請求正文。
雖然我們可以在單個進(jìn)程中并行讀取多個請求,但這會導(dǎo)致在進(jìn)行渲染時計算操作的交錯。
節(jié)點進(jìn)程的使用與客戶端和網(wǎng)絡(luò)的速度耦合。
解決方案是使用緩沖反向代理來處理與客戶端的通信。 為此,我們使用 nginx。 Nginx 將來自客戶端的請求讀入緩沖區(qū),并在完全讀取后將完整請求傳遞給節(jié)點服務(wù)器。
這種傳輸通過環(huán)回或 unix 域套接字在機器上本地發(fā)生,這比機器之間的通信更快、更可靠。
通過 nginx 處理讀取請求,我們能夠?qū)崿F(xiàn)節(jié)點進(jìn)程的更高利用率。
總結(jié)
服務(wù)器端渲染代表與 Node.js 擅長的規(guī)范的、主要是 I/O 工作負(fù)載不同的工作負(fù)載。了解異常行為的原因使我們能夠使用我們擁有現(xiàn)有操作經(jīng)驗的現(xiàn)成組件來解決它。
異步渲染仍然存在資源爭用。異步渲染解決進(jìn)程或瀏覽器的響應(yīng)問題,但不解決并行性或延遲問題。 這篇翻譯的博文重點介紹的是純計算工作負(fù)載的簡單模型。對于 IO 和計算的混合工作負(fù)載,請求并發(fā)會增加延遲,但具有允許更高吞吐量的好處。
更多Jerry的原創(chuàng)文章,盡在:“汪子熙”:
總結(jié)
以上是生活随笔為你收集整理的部署 Node.js 应用以完成服务器端渲染 Server Side Rendering 的性能调优的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 与诸公送陈郎将归衡阳翻译赏析
- 下一篇: esn是什么意思_IMEI MEID