作者: Peter 譚老師
轉發鏈接:https://mp.weixin.qq.com/s/Vs8CwdGJ6JUyH_Kp0H5l4Q
起因
- 有人在思否論壇上向我付費提問

- 當時覺得,這個人問的有問題吧。仔細一看,還是有點東西的
問題重現
- 編寫一段Node.js代碼
var http = require('http');
http.createServer(function (request, response) {
var num = 0
for (var i = 1; i < 5900000000; i++) {
num += i } response.end('Hello' + num);
}).listen(8888);
- 使用nodemon啟動服務,用time curl調用這個接口

- 首次需要7.xxs耗時
- 多次調用后,問題重現

- 為什么這個耗時突然變高,由于我是調用的是本機服務,我看CPU使用當時很高,差不多達到100%了.但是我后面發現不是這個問題.
問題排查
- 排除掉CPU問題,看內存消耗占用。
var http = require('http');
http .createServer(function(request, response) { console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses Approximately ${Math.round(used * 100) / 100} MB`,
'start',
); console.time('測試');
let num = 0;
for (let i = 1; i < 5900000000; i++) {
num += i; } console.timeEnd('測試');
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
); response.end('Hello' + num);


}) .listen(8888);
- 測試結果:
- 內存占用和CPU都正常
- 跟字符串拼接有關,此刻關閉字符串拼接(此時為了快速測試,我把循環次數降到5.9億次)

- 發現耗時穩定下來了
定位問題在字符串拼接,先看看字符串拼接的幾種方式
- 一、使用連接符 “+” 把要連接的字符串連起來
var a = 'JAVA'
var b = a + 'script'
* 只連接100個以下的字符串建議用這種方法最方便
- 二、使用數組的 join 方法連接字符串
var arr = ['hello','java','script']
var str = arr.join("")
- 比第一種消耗更少的資源,速度也更快
- 三、使用模板字符串,以反引號( ` )標識
var a = 'java'
var b = `hello ${a}script`
- 四、使用 JavaScript concat() 方法連接字符串
var a = 'java'
var b = 'script'
var str = a.concat(b)
五、使用對象屬性來連接字符串
function StringConnect(){
this.arr = new Array()
}StringConnect.prototype.append = function(str) {
this.arr.push(str)
}StringConnect.prototype.toString = function() {
return this.arr.join("")
}var mystr = new StringConnect()
mystr.append("abc")
mystr.append("def")
mystr.append("g")
var str = mystr.toString()
更換字符串的拼接方式
- 我把字符串拼接換成了數組的join方式(此時循環5.9億次)
var http = require('http');
http .createServer(function(request, response) { console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
); console.time('測試');
let num = 0;
for (let i = 1; i < 590000000; i++) {
num += i; } const arr = ['Hello'];
arr.push(num); console.timeEnd('測試');
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
); response.end(arr.join(''));
}) .listen(8888);
- 測試結果,發現接口調用的耗時穩定了(注意此時是5.9億次循環)

- 《javascript高級程序設計》中,有一段關于字符串特點的描述,原文大概如下:ECMAScript中的字符串是不可變的,也就是說,字符串一旦創建,他們的值就不能改變。要改變某個變量的保存的的字符串,首先要銷毀原來的字符串,然后再用另外一個包含新值的字符串填充該變量
就完了?
- 用+直接拼接字符串自然會對性能產生一些影響,因為字符串是不可變的,在操作的時候會產生臨時字符串副本,+操作符需要消耗時間,重新賦值分配內存需要消耗時間。
- 但是,我更換了代碼后,發現,即使沒有字符串拼接,也會耗時不穩定
var http = require('http');
http .createServer(function(request, response) {
console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
);
console.time('測試');
let num = 0;
for (let i = 1; i < 5900000000; i++) {
// num++;
}
const arr = ['Hello'];
// arr[1] = num;
console.timeEnd('測試');
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
); response.end('hello');
}) .listen(8888);
- 測試結果:
- 現在我懷疑,不僅僅是字符串拼接的效率問題,更重要的是for循環的耗時不一致
var http = require('http');
http .createServer(function(request, response) {
console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
);
let num = 0;
console.time('測試');
for (let i = 1; i < 5900000000; i++) {
// num++;
}
console.timeEnd('測試');
const arr = ['Hello'];
// arr[1] = num;
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
); response.end('hello');
}) .listen(8888);
- 測試運行結果:
- for循環內部的i++其實就是變量不斷的重新賦值覆蓋
- 經過我的測試發現,40億次跟50億次的區別,差距很大,40億次的for循環,都是穩定的,但是50億次就不穩定了.
- Node.js的EventLoop:

- 我們目前被阻塞的狀態:
- 我電腦的CPU使用情況
優化方案
- 遇到了60億次的循環,像有使用多進程異步計算的,但是本質上沒有解決這部分循環代碼的調用耗時。
- 改變策略,拆解單次次數過大的for循環:
var http = require('http');
http .createServer(function(request, response) { console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
); let num = 0;
console.time('測試');
for (let i = 1; i < 600000; i++) {
num++; for (let j = 0; j < 10000; j++) {
num++; } } console.timeEnd('測試');
const arr = ['Hello'];
console.log(num, 'num');
arr[1] = num;
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
); response.end(arr.join(''));
}) .listen(8888);
- 結果,耗時基本穩定,60億次循環總共:
推翻字符串的拼接耗時說法
- 修改代碼回最原始的+方式拼接字符串
var http = require('http');
http .createServer(function(request, response) { console.log(request.url, 'url');
let used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'start',
); let num = 0;
console.time('測試');
for (let i = 1; i < 600000; i++) {
num++; for (let j = 0; j < 10000; j++) {
num++; } } console.timeEnd('測試');
// const arr = ['Hello'];
console.log(num, 'num');
// arr[1] = num;
used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(
`The script uses approximately ${Math.round(used * 100) / 100} MB`,
'end',
); response.end(`Hello` + num);
}) .listen(8888);
- 測試結果穩定,符合預期:
總結:
- 對于單次循環超過一定閥值次數的,用拆解方式,Node.js的運行耗時是穩定,但是如果是循環次數過多,那么就會出現剛才那種情況,阻塞嚴重,耗時不一樣。
- 為什么?
深度分析問題
- 遍歷60億次,這個數字是有一些大了,如果是40億次,是穩定的
- 這里應該還是跟CPU有一些關系,因為top查看一直是在升高
- 此處雖然不是真正意義上的內存泄漏,但是我們如果在一個循環中不僅要不斷更新i的值到60億,還要不斷更新num的值60億,內存使用會不斷上升,最終出現兩份60億的數據,然后再回收。(因為GC自動垃圾回收,一樣會阻塞主線程,多次接口調用后,CPU占用也會升高)
- 使用for循環拆解后:
for (let i = 1; i < 60000; i++) {
num++;
for (let j = 0; j < 100000; j++) {
num++;
}
}
- 只要num到60億即可,解決了這個問題。
哪些場景會遇到這個類似的超大計算量問題:
- 圖片處理
- 加解密
?
如果是異步的業務場景,也可以用多進程參與解決超大計算量問題,今天這里就不重復介紹了
作者: Peter 譚老師
轉發鏈接:https://mp.weixin.qq.com/s/Vs8CwdGJ6JUyH_Kp0H5l4Q