高階入門:騰訊構建高性能的 react 同構直出方案

蚊子的博客

騰訊新聞搶金達人活動 node 同構直出渲染方案的總結文章中咱們總體瞭解了下同構直出渲染方案在咱們項目中的使用。正如我在上篇文章結尾所說的:javascript

應用型技術的難點不是在克服技術問題,而是在於可以不斷的結合自身的產品體驗,發現其中存在的體驗問題,不斷使用更好的技術方案去優化用戶的體驗,爲整個產品發展添磚加瓦。html

咱們在根據產品的體驗效果選擇了 react 同構直出渲染方案,必然也要保證當前方案的可用性和可靠性。例如咱們的服務能同時支撐多少人訪問,當用戶量增大時是否能夠依然保證用戶的正常訪問,如何保證 CPU、內存等正常運做,而不被一直佔用沒法釋放等。所以,這裏咱們應當下咱們項目的幾項數據:前端

  1. 項目一天的訪問量是多少,高峯期的訪問量是多少,即併發的用戶量有多少;
  2. 咱們的單機服務最大能支持多少 QPS;
  3. 高併發時的服務響應時間如何,頁面,接口的失敗率有多少;
  4. CPU 和內存的使用狀況,是否存在 CPU 使用不充分或者內存泄露等問題;

這些數據,都是咱們上線前要知道的。壓力測試的重要性就提現出來了,咱們在上線前進行充分的測試,可以讓咱們掌握程序和服務器的運行性能,大體申請多少臺機器等等。java

1. 初次壓力測試

咱們這裏使用autocannon來對項目進行壓測。注意,咱們如今尚未進行任何的優化措施,就是要先暴露出問題來,而後針對性的進行優化。node

每秒鐘 60 的併發,並持續 100 秒:react

autocannon -c 60 -d 100
複製代碼

壓測後的數據:ios

初始壓測的數據-蚊子的前端博客

從圖片中能夠看到,每秒 60 的併發請求量時,服務器大概平都可以處理 266 左右的請求,不過還有 23 個請求超時了,響應時間還能夠,99%的請求在 1817ms 毫秒內完成。就目前這幾項數據來看,數據處理能力並不理想,咱們還有很大的提高空間。nginx

2. 解決方案

針對上面壓測出來的數據不理想,咱們這裏須要採起一些措施了。git

來吧-蚊子的前端博客

2.1 內存管理

咱們如今寫純前端時,幾乎已經不多關注內存的使用了,畢竟在前端發展的過程當中,內存的垃圾回收機制相對來講比較完善,並且前端頁面的生存週期比較短。若是真是要特別注意的話,也是早期在 IE 瀏覽器中,js 與 dom 的交互過程當中可能會產生內存的泄露。並且若是真會真要是泄露的話,也只會影響當前終端的用戶,其餘的用戶暫時不會受到影響。github

而服務端則不一樣,全部用戶都會訪問當前運行的代碼,只要程序有一丁點的內存泄露,在成千上萬的訪問量下,都會形成內存的堆積,垃圾沒法回收,最終形成嚴重的內存泄露,並致使程序崩潰。爲了預防內存泄露,咱們在內存管理方面,主要三方面的內容:

  1. V8 引擎的垃圾回收機制;
  2. 形成內存泄露的緣由;
  3. 如何檢測內存泄露;

Node 將 JavaScript 的主要應用場景擴展到了服務器端,相應要考慮的細節也與瀏覽器端不一樣, 須要更嚴謹地爲每一份資源做出安排。總的來講,內存在 Node 中不能爲所欲爲地使用,但也不是徹底不擅長。

2.1.1 V8 引擎的垃圾回收機制

在 V8 中,主要將內存分爲新生代和老生代兩代。新生代的對象爲存活時間比較短的對象,老生代中的對象爲存活時間較長的或常駐內存的對象。

默認狀況下,新生代的內存最大值在 64 位系統和 32 位系統上分別爲 32 MB 和 16 MB。V8 對內存的最大值在 64 位系統和 32 位系統上分別爲 1464 MB 和 732 MB。

爲何這樣分兩代呢?是爲了最優的 GC 算法。新生代的 GC 算法 Scavenge 速度快,可是不合適大數據量;老生代針使用 Mark-Sweep(標記清除) & Mark-Compact(標記整理) 算法,合適大數據量,可是速度較慢。分別對新舊兩代使用更適合他們的算法來優化 GC 速度。

2.1.2 內存泄露的緣由

內存泄露的狀況有不少,例如內存當緩存、隊列、重複的事件監聽等。

內存當緩存這種狀況中,一般有用一個變量來緩存數據,而後沒有過時時間,一直填充數據,例以下面一個簡單的例子:

let cached = new Map();

server.get('*', (req, res) => {
    if (cached.has(req.url)) {
        return cached.get(req.url);
    }
    const html = app.render(req, res);
    cached.set(req.url, html);
    res.send(html);
});
複製代碼

除此以外,還有閉包也是其中的一種狀況。這種使用內存的很差的地方是,它沒有可用的過時策略,只會讓數據愈來愈多,最終形成內存泄露。更好的方式使用第三方的緩存機制,例如 redis、memcached 等,這些都有良好的過時和淘汰策略。

同時,也有一些隊列方面的處理,例若有些日誌的寫入操做,當海量的數據須要寫入時,就會形成隊列的堆積。這時,咱們設置隊列的超時策略和拒絕策略,讓一些操做盡快地釋放掉。

再一個就是事件的重複監聽。例如對同一個事件重複監聽,忘記移除(removeListener),將形成內存泄漏。這種狀況很容易在複用對象上添加事件時出現,因此事件重複監聽可能收到以下警告:

setMaxListeners-蚊子的前端博客

Warning: Possible EventEmitter memory leak detected. 11 /question listeners added。Use emitter。setMaxListeners() to increase limit

2.1.3 排查的手段

內存泄露-蚊子的前端博客

咱們從內存的監控圖中能夠看到,在用戶量基本保持不變的狀況下,內存是一直在緩慢上漲,說明咱們產生了內存泄露,使用的內存並無被釋放掉。

這裏咱們能夠經過node-heapdump等工具來進行判斷,或者稍微簡單點,使用--inspect命令實現:

node --inspect server.js
複製代碼

而後打開 chrome 連接chrome://inspect來查看內存的使用狀況。

chrome-inspect-蚊子的前端博客

經過兩次的內存抓取對比發現,handleRequestTimeout()方法一直在產生,且每一個 handle 方法中有無數個回調,資源沒法被釋放。

經過定位查看使用的 axios 代碼是:

if (config.timeout) {
    timer = setTimeout(function handleRequestTimeout() {
        req.abort();
        reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req));
    }
}
複製代碼

這裏代碼看起來是沒任何問題的,這是在前端處理中一個很典型的超時處理解決方式。

因爲 Nodejs 中,io 的連接會阻塞 timer 處理,所以這個 setTimeout 並不會按時觸發,也就有了 10s 以上才返回的狀況。

貌似問題解決了,巨大的流量和阻塞的 connection 致使請求堆積,服務器處理不過來,CPU 也就下不來了。

經過定位並查看axios 的源碼

if (config.timeout) {
    // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system.
    // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET.
    // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up.
    // And then these socket which be hang up will devoring CPU little by little.
    // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect.
    req.setTimeout(config.timeout, function handleRequestTimeout() {
        req.abort();
        reject(
            createError(
                'timeout of ' + config.timeout + 'ms exceeded',
                config,
                'ECONNABORTED',
                req
            )
        );
    });
}
複製代碼

額,我以前使用的版本比較早,跟我本地使用的代碼不同,說明是更新過了,再查看這個文件的9 月 16 日的改動歷史

seTimeout-axios-蚊子的前端博客

這裏咱們就須要把 axios 更新到最新的版本了。並且通過本地大量測試,發如今高負載下 CPU 和內存都在正常範圍內了。

2.2 緩存

緩存真是性能優化的一把好手,服務不夠,緩存來湊。不過緩存的類型有不少種,咱們應當根據項目的實際狀況,合理地選擇使用緩存的策略。這裏咱們使用了 3 層的緩存策略。

緩存-蚊子的前端博客

在 nginx 中,可使用 proxy_cache 設置要緩存的路徑和緩存的時間,同時能夠啓用proxy_cache_lock

當 proxy_cache_lock 被啓用時,當多個客戶端請求一個緩存中不存在的文件(或稱之爲一個 MISS),只有這些請求中的第一個被容許發送至服務器。其餘請求在第一個請求獲得滿意結果以後在緩存中獲得文件。若是不啓用 proxy_cache_lock,則全部在緩存中找不到文件的請求都會直接與服務器通訊。

不過這個字段的啓用也要很是慎重,當訪問量過大時,會形成請求的堆積,必須等待第一個請求返回完成後,才能處理後面的請求。

proxy_cache_path /data/cached keys_zone=answer:16m levels=1:2 inactive=60m;

server {
    location / {
        proxy_cache answer;
        proxy_cache_valid 1m;
    }
}
複製代碼

在業務層面,咱們能夠啓用 redis 緩存,來緩存整個頁面、頁面的某個部分或者接口等等,當穿透 nginx 緩存時,能夠啓用 redis 緩存。使用第三方緩存的特色咱們在以前的文章也說了:多個進程之間能夠共享,同時減小項目自己對緩存淘汰算法的處理。

當前面的兩層緩存失效時,進入到咱們的 node 服務層。二層的緩存機制,能實現不一樣的緩存策略和緩存粒度,業務須要根據自身場景, 選用適合本身業務的緩存便可。

3. 效果

這時咱們項目的性能怎樣了呢?

autocanon -c 1000 -d 100
複製代碼

壓力測試-蚊子的前端博客

從圖片裏能夠看到,99%的請求在182ms內完成,每秒平均處理的請求有15707左右,相比咱們最開始只能處理200多個請求,性能足足提高了60倍多。

THE END

人以爲寂寞,想盡各類辦法排遣,最終仍是逃離不了寂寞。寂寞是造化對羣居者的詛咒,孤獨纔是面對寂寞惟一的出路。

--《百年孤獨》

▼ 我是來騰訊的小小前端開發工程師,長按識別二維碼關注,與你們共同窗習、討論 ▼

蚊子的公衆號
相關文章
相關標籤/搜索