內存泄露這類問題的一大特色是它在開發的過程當中難以發現。大部份內存泄露問題的發現都是在生產環境階段發現的,由於內存泄露在一般狀況下,並不會影響應用的功能,直到應用運行時間足夠長,請求或者操做足夠多的話,問題將會暴露,同時也會帶來一些損失。並且讓開發者更頭疼的是,即便發現的應用存在內存泄露,因爲缺少充足的理論知識和調試方法,致使泄露的緣由也很難定位。javascript
本demo選取的是一個簡單的node.js程序,來模擬一次服務器內存泄露致使應用崩潰的事件,並介紹在發現問題以後的一些基本的調試思路和實戰方法。若是你對內存泄露的定位一點都不瞭解的話,本章內容將會給你創建一個基本的調試概念,以便利於後面對問題的詳細分析。前端
本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進程在打印出下圖的錯誤以後就直接跪了。數據庫
老闆很生氣,讓小A總結此次事故的緣由,並後續給團隊開展一次Case Study。npm
小A在遇到問題以後一臉懵逼,我線下測試都是好好的,爲何一上線就跪了呢?抱有疑問的他跑去請教公司內的大佬,大佬看了小A的報錯信息就說:這是內存泄露,來,我幫你看一下吧。小程序
大佬拿到他的代碼以後,在啓動的node命令後面,添加了一個特殊的參數--inspect
。
node --inspect server.js
複製代碼
緊接着,大佬打開了Chrome瀏覽器,在地址欄內輸入: chrome://inspect
,發現剛纔運行的server程序就在頁面上列出來了。
緊接着,大佬點擊了下面的inspect
按鈕,一個Chrome Devtools就彈出來了。大佬選取了頂部Memory的tab,並在Select profilling type
下面選擇了Allocation sampling
。點擊下面的藍色的Start
按鈕,而後錄製就開始了。
大佬對小A說,如今內存錄制搞好了,接下來就是構造一些請求來訪問服務器了。
大佬打開小A電腦的命令行,輸入了下面的命令:
ab -n 1000000 -c 100 http://localhost:3000/
複製代碼
"來,讓咱們再把它打掛吧",大佬邊敲命令,邊對小A說,我如今用ab這個壓力測試工具,向你的服務器以100的併發發送了10W個請求,應該能模擬線上用戶突增的場景。
大佬執行執行這個命令以後,馬上切換到devtools,發現JavaScript VM Instance
顯示的數字突增,不一下子,就從不到10MB膨脹到了700MB。這時,大佬露出了滿意的微笑,說到:"看,問題復現了",隨後點擊了頁面上的Stop
按鈕,中止了內存的錄製,而且退出了剛纔執行的ab進程。
這時,點擊了Stop按鈕以後,devtools顯示出了下圖的界面。
大佬對小A解釋說,看,這個工具把每一行代碼所佔用的內存給你顯示出來了,注意到第一行沒有,那段代碼佔用了99.81%的內存!
大佬點擊了最右邊的server.js
,devtools就自動跳轉到代碼界面了。devtools使用黃色的標識顯示了佔用內存最大的代碼位置——就是小A寫的那段緩存代碼!
大佬閱讀了小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…