給新前端程序員來一打內存泄漏(Memory Leak)

實例分析:一個內存泄露的demo

內存泄露這類問題的一大特色是它在開發的過程當中難以發現。大部份內存泄露問題的發現都是在生產環境階段發現的,由於內存泄露在一般狀況下,並不會影響應用的功能,直到應用運行時間足夠長,請求或者操做足夠多的話,問題將會暴露,同時也會帶來一些損失。並且讓開發者更頭疼的是,即便發現的應用存在內存泄露,因爲缺少充足的理論知識和調試方法,致使泄露的緣由也很難定位。javascript

本demo選取的是一個簡單的node.js程序,來模擬一次服務器內存泄露致使應用崩潰的事件,並介紹在發現問題以後的一些基本的調試思路和實戰方法。若是你對內存泄露的定位一點都不瞭解的話,本章內容將會給你創建一個基本的調試概念,以便利於後面對問題的詳細分析。前端

一個內存泄露的HTTP服務器

本demo的代碼均可以在github上進行獲取:獲取地址java

小A在某互聯網公司工做,負責一些線上運營活動的後端開發。這個運營活動的服務是一個簡易的HTTP服務器,它在每次請求都會『讀取』數據庫來返回一段數據。node

const http = require('http');
const uuid = require('uuid');

function readDataFromDataBase() {
    return new Array(10000).fill('xxxx');
}

const server = http.createServer((req, res) => {
    let key = uuid();
    let data = readDataFromDataBase(key);
    res.end(JSON.stringify({
        data: data
    }));
});

server.listen(3000);
console.log('Server listening to port 3000. Press Ctrl+C to stop it.');
複製代碼

小A寫完代碼以後,在生產環境服務器上,使用下面的命令就把服務啓動起來了。git

node server.js
複製代碼

因爲數據庫的性能並非特別好,因此總體服務的QPS並非很高。直到有一天,老闆找到小A,下週公司要作一個運營活動,用戶量應該會增加很多,因而讓他優化這個服務器,讓它可以支撐更多的流量。小A因而經過一個簡單的對象,給readDataFromDataBase函數添加了緩存功能。github

const http = require('http');
const uuid = require('uuid');

function readDataFromDataBase() {
    let cache = {};
    return function(key) {
        if (cache[key]) {
            return cache[key];
        }

        let data = new Array(10000).fill('xxxx');
        cache[key] = data;
        return data;
    };
}

const database = readDataFromDataBase();

const server = http.createServer((req, res) => {
    let key = uuid();
    let data = database(key);
    res.end(JSON.stringify({
        data: data
    }));
});

server.listen(3000);
console.log('Server listening to port 3000. Press Ctrl+C to stop it.');
複製代碼

經過這樣的改造,緩存生效了,老闆也很開心,因而小A就把這段代碼上到了生產環境,內心想着,等下週公司運營活動搞完,年終獎就有着落啦。chrome

公司的運營活動得到了很大的成功,用戶量一會兒就增加了10倍。這時,小A忽然接到用戶反饋,說運營頁面打不開了,小A緊忙登陸到線上看報錯日誌,就發現node進程在打印出下圖的錯誤以後就直接跪了。數據庫

Crash

老闆很生氣,讓小A總結此次事故的緣由,並後續給團隊開展一次Case Study。npm

問題調查

小A在遇到問題以後一臉懵逼,我線下測試都是好好的,爲何一上線就跪了呢?抱有疑問的他跑去請教公司內的大佬,大佬看了小A的報錯信息就說:這是內存泄露,來,我幫你看一下吧。小程序

內存錄制

大佬拿到他的代碼以後,在啓動的node命令後面,添加了一個特殊的參數--inspect

node --inspect server.js
複製代碼

緊接着,大佬打開了Chrome瀏覽器,在地址欄內輸入: chrome://inspect,發現剛纔運行的server程序就在頁面上列出來了。

inspect

緊接着,大佬點擊了下面的inspect按鈕,一個Chrome Devtools就彈出來了。大佬選取了頂部Memory的tab,並在Select profilling type下面選擇了Allocation sampling。點擊下面的藍色的Start按鈕,而後錄製就開始了。

devtools

大佬對小A說,如今內存錄制搞好了,接下來就是構造一些請求來訪問服務器了。

請求模擬

大佬打開小A電腦的命令行,輸入了下面的命令:

ab -n 1000000 -c 100 http://localhost:3000/
複製代碼

"來,讓咱們再把它打掛吧",大佬邊敲命令,邊對小A說,我如今用ab這個壓力測試工具,向你的服務器以100的併發發送了10W個請求,應該能模擬線上用戶突增的場景。

大佬執行執行這個命令以後,馬上切換到devtools,發現JavaScript VM Instance顯示的數字突增,不一下子,就從不到10MB膨脹到了700MB。這時,大佬露出了滿意的微笑,說到:"看,問題復現了",隨後點擊了頁面上的Stop按鈕,中止了內存的錄製,而且退出了剛纔執行的ab進程。

badend

內存分析

這時,點擊了Stop按鈕以後,devtools顯示出了下圖的界面。

memory status

大佬對小A解釋說,看,這個工具把每一行代碼所佔用的內存給你顯示出來了,注意到第一行沒有,那段代碼佔用了99.81%的內存!

toCode

大佬點擊了最右邊的server.js,devtools就自動跳轉到代碼界面了。devtools使用黃色的標識顯示了佔用內存最大的代碼位置——就是小A寫的那段緩存代碼!

code

問題修復

大佬閱讀了小A寫的緩存代碼,說道:你這樣寫確定會泄露的!你把每次請求的數據都寫入到cache這個對象中,那請求愈來愈多,cache確定會愈來愈大嘛。小A說道:但是我須要緩存一些請求的數據,那如今我改怎麼辦?

大佬思考了一下,你可使用LRU Cache這個數據結構,LRU Cache只會緩存最頻繁訪問的內容,那些不常常訪問的內容都會被自動拋棄掉,這樣的話,緩存的大小就不會無限制的增加了,並且還能保證最頻繁的內容能夠命中緩存。

說完,大佬就在命令行中執行下面的命令,安裝了一個叫作lru-cache的npm包

npm i lru-cache
複製代碼

而後大佬經過這個包,替換到了小A代碼中的緩存實現。

const http = require('http');
const uuid = require('uuid');
const LRU = require('lru-cache');

function readDataFromDataBase() {
    let cache = new LRU({
        max: 50
    });

    return function(key) {
        if (cache.has(key)) {
            return cache.get(key);
        }

        let data = new Array(10000).fill('xxxx');
        cache.set(key, data);
        return data;
    };
}

const cachedDataBase = readDataFromDataBase();

const server = http.createServer((req, res) => {
    let key = uuid();
    let data = cachedDataBase(key);
    res.end(JSON.stringify({
        data: data
    }));
});

server.listen(3000);
console.log('Server listening to port 3000. Press Ctrl+C to stop it.');
複製代碼

而後大佬再從新運行服務器,並使用ab工具來進行壓力測試,發現整個服務器的內存會一直穩定在100MB之內。

總結

小A同窗在開發過程當中不注意緩存的大小限制,致使內存一直飆升直至服務崩潰。經過請教大佬,學會了如何經過Chrome devtools來發現內存問題,並採起LRU cache來做爲應用緩存,避免了緩存過大的問題。

聯繫

新前端技術交流羣召集前端技術人,這裏有Node.js/Vue.js/React.js/React-Native.js/微信小程序 技術問題交流。歡迎加入!羣號:426334209

點擊連接加入羣聊【前端技術交流羣】:jq.qq.com/?_wv=1027&a…

相關文章
相關標籤/搜索