面向億萬級用戶的QQ一般做什么?——興趣部落的 Web 同構直出分享


歡迎大家前往騰訊雲社區,獲取更多騰訊海量技術實踐干貨哦~

作者:李強,騰訊web開發工程師
商業轉載請聯系騰訊WeTest獲得授權,非商業轉載請注明出處。
原文鏈接:http://wetest.qq.com/lab/view/348.html


一、什么是同構直出?

直出這個名詞是在node出現后才有的,在node出現前叫做服務端渲染。

所以可以把直出定義為:“以node作為后端語言實現的服務端渲染並輸出HTML字符串到客戶端的一項技術”。這樣瀏覽器渲染首屏的過程就由非直出下的先請求HTML,再請求js、css,最后再請求后台數據。改為直出下的直接向node服務器發起請求,然后通過內網獲取到首屏數據后,組裝成HTML直接返回給瀏覽器。這里說明下:直出並不一定就比非直出快,但是它能保證用戶在不同機型、不同網絡條件下都有一個比較好的體驗。

那什么是同構呢?

同構就是解決直出的一種思想,node出現后使得javascript腳本也可以在服務器端執行,通過維護一套項目代碼,實現在前后端都可以執行的目的。

QQ興趣部落擁有頁面80多個,開發人員14個,參與改造直出人力2個,使用同構的做法無疑可以最大程度上降低改造和維護成本。

億萬級用戶意味着什么呢?目前部落用戶注冊和使用量達億萬級, 這樣大量的用戶意味着存在高並發,服務隨時都有可能掛掉的風險。前端頁面作為整個web服務中最直接面向用戶的,一旦服務不可用就將是件讓所有人都很崩潰的事情了。

本文的目的在於解決兩個問題:

1、 部落是怎樣從一個純前端項目改造成同構直出項目的

2、在訪問量這么大的情況下,如何保證直出服務的可用性的問題。

二、如何改造同構直出項目

首先明確同構直出要做好哪些工作,總結下來有三點,可稱之為同構直出三要素。

1、保證DOM的一致性,如果說本來瀏覽器通過純客戶端代碼渲染出來的頁面結構是下圖這樣,服務端渲染出來卻少了一個dom節點,那肯定會導致頁面顯示有問題。

2、保證前后端數據的一致性,服務端不能執行dom操作,所以像綁定事件這樣的工作,就需要瀏覽器拉取到js腳本后才能進行,如果使用服務端獲取到的數據渲染出來的HTML結構與前端綁定事件時用到的數據不一致,就會導致問題。

3、保證路由的一致性,不能讓用戶訪問a頁面的時候,返回b頁面給用戶。

這樣就可以明確做同構直出的方向,對於部落來說,原來的項目中就使用了react和redux,所以接下來會使用這兩個框架進行講解。

同構直出是一種優化的思想,不受任何框架限制,理解其中的原理才是最重要的。那么問題就來了,如何使用react來保證dom一致性,又如何使用redux保證數據一致性?先來看一下dom一致性的實現。

在使用react做同構直出時,很關鍵的一個因素就是它提供了虛擬DOM的支持,是一種在內存中的對象數,使其可以支持在瀏覽器和node環境下執行,這也是代碼可以同構的關鍵所在。在瀏覽器端通過render方法生成虛擬dom並掛載到真實DOM上。在服務端通過renderToString方法將虛擬dom拼裝成HTML字符串。使用這兩個方法就可以解決dom一致性的問題了,來看一下具體的實現。

首先服務端通過調用rendertostring方法將react組件渲染為html字符串,但是通過react組件渲染出來的並不是標准的html格式,需要將其嵌入HTML模板中才能夠被瀏覽器解析。當瀏覽器向直出服務器發起請求后,服務端將渲染好的html字符串返回,瀏覽器收到響應后進行渲染。瀏覽器通過解析html拉取到js腳本后,會執行render方法,在render方法處理過程中會校驗節點中的checksum屬性,該屬性是在服務端調用rendertostring方法時追加的,用於前端校驗dom一致性,當校驗一致時,直接執行腳本中后續的綁定事件等行為,如果不一致,將會進行虛擬DOM的diff操作,然后再進行增量更新DOM、綁定事件。在紅框處,可以看到同構代碼的部分。

但是,Node環境和瀏覽器環境畢竟還是不一樣的,有這么多前端代碼是不能直接在node端執行的,應該怎樣在同構代碼上做好平台區分呢?

解答這個問題之前,再來看一下數據一致性是如何保證的。

Redux使用單一的Store對象保存、管理頁面中的所有狀態,和虛擬dom一樣,是一種駐在內存中的對象,代碼完全可以同構。

保證數據一致性的原理其實很簡單。只要在最后組裝HTML字符串時,將服務端的狀態通過script標簽一起輸出給前端,然后在前端初始化 Store 時使用該數據,即可完成了數據的傳遞和共享,達到保證數據一致性的目的。

這里其實也存在一點問題,頁面的狀態大都來自於后台數據,而發送異步請求的方法在前端是ajax方法,在node端是使用http模塊的request方法,這樣,我們又該怎樣保證代碼的同構呢?

三、同構直出的改造方案

接下來可以了解下怎樣解決上面遇到的一些問題,以及部落同構直出的改造方案。

整個解決問題和改造的過程我把它比作是一次裝修房子的過程,在裝修房子過程中有這樣一些關鍵的角色,戶型結構圖、設計師、通過設計師設計出來的效果圖、還有房子,如果此時又買了一套戶型結構完全一樣的房子需要裝修,那就和前后端需要渲染出來的HTML結構一樣是類似的場景了。所以可以就戶型結構圖看做是源碼,設計師看做構建工具,效果圖看做構建打包后的bundle,已經裝修好的房子看做瀏覽器,等待裝修的房子看做node服務器。大家還記得我們前面提到的第一個問題嗎?前端代碼中有些代碼是不能在node端執行的,該怎么解決呢?

先來看一下如果在設計過程中,想去掉一些東西該怎么做?

是不是只需要在戶型結構圖上做些標識,然后告訴設計師紅圈中的內容表示想去掉這部分的內容就可以了?

就是按照這種思路,我們在源碼中做了些標記,然后告訴構建工具被這個標記包裹的代碼是打包node端代碼時需要刪掉的,讓構建工具識別這個標簽的方法可以使用自定義webpack loader或者babel插件。

然后回想下第二個問題,發送異步請求前端使用的是ajax方法,node端使用的是http模塊的request方法,這個問題怎么解決?同樣的,在設計過程中,如果想改個門,是不是直接告訴設計師就可以了? 都沒必要在原始圖上進行任何修改了。

借助這種思考方式,通過構建工具處理,就不需要對源碼進行任何更改。源碼中使用的是ajax方法,同時在node服務器上在全局變量下實現了一個window.ajax的方法,這樣通過自定義babel插件,在對源碼打包時,將ajax方法名替換成為window.ajax方法名,問題就得到了解決。

到了這一階段——結束了設計工作,有了效果圖,也就是已經打包出了一份可以在node端執行的bundle,就下來就是需要到房子里面去還原設計稿的時候了。

施工的話,單憑我們自己肯定不行,所以需要一個施工隊。

施工隊里面有包工頭,負責承接項目,分發任務給各個工種按照設計稿進行施工。

同樣的原理,我們在node服務器上引入了直出框架機的概念,幫我們統一管理直出服務。框架機的第一層就是玄武和TSW(不理解玄武的同學,這里可以把它當做是起了一個koa的server,負責監聽端口,接受請求並轉發到業務邏輯層按照打包好的bundle去處理。)為了讓業務邏輯層不必針對每個頁面做兼容,所以需要打包出來的server bundle具有固定的結構,那我們就來看一下bundle是怎樣的一個結構。

源碼的結構大致是這樣子的,大家可以看到這里面有一個前端程序的打包入口,實現上是這樣的,里面有對store和main組件入口的引用。因為源碼中沒有對服務端程序的打包入口,所以需要對store和main進行單獨打包。

最終構建出來的目錄大致是這樣的,以a頁面為例,有HTML模板、組件入口腳本、創建store對象的腳本,最后還有一個首屏action的腳本。

這個腳本是做什么的呢?

在action的腳本中封裝了所有異步請求的方法,對於頁面來說,由很多組件構成,每個組件調用各自的action方法更新自身狀態,但是,首屏並不一定需要渲染所有組件,可能只需要展示組件1和組件2,所以這時就需要提取出首屏所需的action creator方法了,我們把它封裝在了名為firstAction腳本中以便構建工具打包后在服務端進行調用。這樣打包后的bundle中每個頁面就都有了相同的結構。

這時就可以在框架機中的業務邏輯層統一對直出頁面做處理了。當瀏覽器發起對頁面A的請求時,通過玄武將請求轉發到業務邏輯層,首先進行路由解析,確保路由一致性,這里使用正則匹配獲取url中的模塊名,通過模塊名獲取頁面A的存放路徑。

然后為請求創建沙箱環境,讓每個請求都能在獨立的上下文環境中執行,實現上使用的是node的vm模塊,如果之前沒有接觸過的話可以把框架機想象成是瀏覽器,每當有一個請求過來就會新開一個tab頁,請求處理完后關閉tab頁。

接着就是初始化一些全局對象,比如前面提到的window.ajax方法。然后將頁面A的腳本引入,通過store腳本創建store對象,通過firstAction腳本獲取首屏所需數據,執行rendertostring方法渲染組件,最后讀取A頁面的HTML模板,組裝成HTML字符串輸出給瀏覽器。這就是框架機基本的一個工作流程了。

最后對直出改造方案進行一下總結。首先是在node服務器上部署了一個直出框架機的服務,使用單獨的代碼倉庫進行維護和發布。

然后通過打包構建工具構建出客戶端的bundle和服務端的bundle。由於客戶端和服務端的一些差異,需要在源碼中使用特定的標簽將node端不能執行的代碼做個標記,同時還要新增一個供服務端使用的封裝了首屏action的腳本,在構建工具中新增server端的打包配置,並加入一些自定義的loader和babel插件幫助我們構建出server端的bundle。

然后將server bundle發送到node服務器上,當瀏覽器發起請求后,框架機幫我們組裝首屏html字符串並輸出給瀏覽器。瀏覽器進行渲染后,引入前端的js腳本,進行后續的dom更新和綁定事件等工作。

以上就是改造直出的整套方案。

四、如何保證直出服務的高可用性?

1、業務可用性的開發調試

首先要講的是本地開發調試在保證服務可用性方面的問題。

前面提到了框架機,那就先來說一下框架機的開發調試模式。本地開發是以tnpm命令行工具包的形式。對於本地開發調試模式也是和命令行工具包一樣,使用 tnpm link命令,建立命令的全局鏈接。Tnpm其實就是npm,只不過是企業內部私有npm倉庫,外部訪問不到。

有人說,平時開發時我連這一步也不想要怎么辦?於是我們增加了自動化測試。

可以利用Mocha + Chai 幫助我們實現一些代碼邏輯上的測試。

2、業務的容災

接下來就是容災。在代碼報錯、服務器崩潰的時候,需要一套容災方案來讓業務盡量正常運作。

興趣部落設計了一套柔性可用的容災方案。當直出報錯的時候,會讓請求自動轉發到靜態資源,讓相對穩定的靜態資源接受用戶的請求,以保證業務不受干擾。

具體的原理是怎么樣的呢?首先由一群Nginx服務器集群去調度用戶的請求,這些請求包括了直出服務器、CDN、后台等等。一旦直出服務器掛掉了,它會自動將請求轉發到CDN服務器。

上面這里是Nginx接入集群的示例代碼。

業務上線前,需要先預估請求的量級,才能預先准備足夠的服務器,以抗住大量用戶的請求。因此需要做好壓力測試。

3、業務的壓力測試

興趣部落在做同構直出的過程中,使用了騰訊 WeTest 壓測大師,實現更智能和自動化地壓力測試。上圖是壓測大師的入口界面,能分別從系統角度、用戶角度、業務角度,多角度幫助開發人員發析直出業務的“接客”能力。

瞬時TPS圖表,分析了服務最優的承載能力。

通過服務器性能趨勢圖得到CPU、內存的性能瓶頸。

還支持報告的一個對比,幫助比對分析每次業務更新后的壓測情況。

4、業務的用戶灰度

直出順利完成,服務器也准備妥當了,此時就已具備了產品發布的基本條件。但為了讓產品對業務成效更有把握,這里需要先做一個用戶灰度。

興趣部落這里主要是詳情頁做了同構直出。因此針對業務場景,我們通過在列表頁做一個區分,通過前端來控制灰度。直出的用戶走帶v2的鏈接,而非直出用戶則不帶。

5、業務的監控告警

產品發布上線時,還需要對它進行全方位監控,以防出亂子。

以上的這些數據指標,都是需要時刻關注的。

五、成果

興趣部落同構直出順利落地,成果也是相當不錯的。頁面能達到秒出,慢用戶占比也從6.8%,下降到1.25%。


為了幫助開發者發現服務器端的性能瓶頸,騰訊WeTest開放了上文提到的壓力測試功能,通過基於真實業務場景和用戶行為進行壓力測試,實現針對性的性能調優,降低服務器采購和維護成本。

除了興趣部落以外,壓測大師還服務了包括王者榮耀、龍之谷手游、軒轅傳奇手游、火影忍者等多款高星級手游,也包括QQ、NOW直播等明星產品。

為了讓外部更多產品能夠享受到簡單易用的壓測產品,騰訊WeTest決定將這份服務器測試能力產品化,以產品”壓測大師“的形式,正式對外開放,點擊鏈接:http://wetest.qq.com/gaps/ 即可使用。

如果對使用當中有任何疑問,歡迎聯系騰訊WeTest企業QQ:800024531

相關閱讀

網頁加速特技之 AMP

表格行與列邊框樣式處理的原理分析及實戰應用

「騰訊雲游戲開發者技術沙龍」11月24 日深圳站報名開啟 暢談游戲加速

 

此文已由作者授權騰訊雲技術社區發布,轉載請注明原文出處

原文鏈接:https://cloud.tencent.com/community/article/365371?utm_source=bky

海量技術實踐經驗,盡在騰訊雲社區


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM