之前做過一些爬取方面的工作,由於node不能多執行緒,為了提高抓取效率,都是使用child_process.fork來多程式跑任務,然後通過message事件與主程式進行通訊,程式碼編寫的時候都是用的yield/await之類的同步寫法,於是這次嘗試利用node非阻塞I/O的機制,利用多個函式同時執行來模擬多執行緒,效果如何呢?
嘗試“並行”傳送HTTP請求
server.js
用來統計qps,將產出的資料status.txt裡的內容複製到echarts的官方示例裡進行視覺化,從而驗證是否能達到“並行”的效果
const fs = require('fs');
const Koa = require('koa');
const app = new Koa();
// 用來統計qps
let last_time = new Date(),
init_timestamp = last_time.getTime(),
count = 0;
// 執行時長60s
let run_secs = 60;
// 用來儲存qps歷史,用於繪製曲線圖
let qps_list = [],
timestr_list = [];
app.use(async ctx => {
// 簡單的模擬計算qps
let cur_time = new Date(),
cur_timestr = cur_time.toLocaleTimeString(),
cur_timestamp = cur_time.getTime(),
last_timestr = last_time.toLocaleTimeString();
if (cur_timestr !== last_timestr) {
let timestamp_cost = Math.round((cur_timestamp - init_timestamp) / 1000);
console.log(`\n${cur_timestr}: ${timestamp_cost} qps*********************************`);
console.log(count);
qps_list.push(count);
timestr_list.push(cur_timestr);
if (timestamp_cost >= run_secs) {
// 將執行結果儲存起來,開啟http://echarts.baidu.com/examples/editor.html?c=line-smooth,複製內容檢視曲線圖
let option_str = JSON.stringify({
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: timestr_list
},
yAxis: {
type: 'value'
},
series: [{
data: qps_list,
type: 'line',
smooth: true
}]
}, null, 2);
fs.writeFileSync('./status.txt', `option=${option_str}`);
console.log('1.複製status.txt的內容');
console.log('2.開啟http://echarts.baidu.com/examples/editor.html?c=line-smooth');
console.log('3.貼上在左邊程式碼區域');
console.log('4.點選"執行",在右側區域檢視');
process.exit();
}
last_time = cur_time;
count = 1;
} else {
count++;
}
// 模擬服務端處理請求的時間
await delay();
ctx.body = 'hello';
});
function delay () {
return new Promise((resolve) => {
setTimeout(resolve, 250);
});
}
app.listen(3000);
複製程式碼
單程式版本
client.js
const axios = require('axios');
async function sendRequest (id) {
return new Promise((resolve, reject) => {
axios.get(`http://localhost:3000?id=${id}`).then(res => {
resolve(res.data);
}).catch(e => {
reject(e);
});
});
}
function run () {
let threads = 1;
for (let i = 0; i < threads; i++) {
makeThread(i);
}
}
async function makeThread (id) {
while (true) {
try {
await sendRequest(id);
} catch (e) {
console.log(id, e.message);
process.exit();
}
}
}
run();
複製程式碼
多程式版本(進行對照)
client_center.js
const fork = require('child_process').fork;
function run () {
let threads = 1;
for (let i = 0; i < threads; i++) {
fork('./client_worker.js', [i]);
}
}
run();
複製程式碼
client_worker.js
const axios = require('axios');
let id = process.argv[2];
async function sendRequest () {
return new Promise((resolve, reject) => {
axios.get(`http://localhost:3000?id=${id}`).then(res => {
resolve(res.data);
}).catch(e => {
reject(e);
});
});
}
async function makeThread () {
while (true) {
try {
await sendRequest();
} catch (e) {
console.log(id, e.message);
process.exit();
}
}
}
makeThread();
複製程式碼
低效能機器執行結果(服務設定延時250ms)
2核機器
threads | 單程式版本 | 多程式版本 | 備註 |
---|---|---|---|
1 | 區別不大 | ||
5 | 區別不大 | ||
50 | 多程式效果弱於單程式版本 | ||
100 | 多程式效果弱於單程式版本 | ||
200 | 多程式弱於單程式版本,且多程式版本總是報錯:read ECONNRESET/connect ECONNRESET/socket hang up | ||
300 | 多程式弱於單程式版本,且多程式版本總是報錯:read ECONNRESET/connect ECONNRESET/socket hang up |
高效能機器執行結果
對比結果讓我挺吃驚的,這樣看來單程式的模擬效果居然會比多程式好,但突然想到自己電腦上才幾核,怎麼同時跑幾百個程式....... 登入到公司伺服器上(48核)繼續實驗:
48核機器
threads | 單程式版本 | 多程式版本 | 備註 |
---|---|---|---|
30 | 區別不大 | ||
40 | 區別不大 | ||
100 | qps峰值相同,但多程式更穩定 | ||
200 | 多程式版本優於單程式版本 | ||
300 | 多程式版本優於單程式版本 | ||
1000 | 多程式版本優於單程式版本 | ||
1500 | 多程式版本優於單程式版本,但threads增加所帶來的收益較低,多程式版本峰值4318<1500*4,單程式版本峰值3058<1500*4 | ||
3000 | 單程式版本(峰值2969)優於多程式版本(峰值1500) |
觀察
- 確實能通過多個函式同時執行的方式來模擬多執行緒的效果
- 當threads設定與核數差距不大時,兩者效果差不多。
- 在高效能機器上,在一定範圍(大部分範圍)內,threads越大,多程式版本的效果越好,但超過這個範圍(極端情況),單程式版本反而表現突出
- 在低效能機器上,單程式版本表現更好,由於出現的read ECONNRESET/connect ECONNRESET/socket hang up等錯誤使得無法繼續增大threads數量觀察下去
- 低效能機器上兩個版本都會表現出奇怪的週期性,在高效能機器上多程式版本會更穩定一些
分析
- client.js能模擬“並行”的效果實際上是利用網路耗時遠大於程式碼迴圈的原理,第一次for迴圈連續傳送threads個網路請求,然後在處理回撥的時候又傳送新的網路請求,效果就變成了多個執行緒在不停的發請求。
不負責任的猜測
- 高/低效能表現不一致,低效能機器是mac,libuv中使用kqueue處理網路I/O,高效能機器時linux,libuv中使用epoll處理
新的問題
- 該服務效能的極限QPS是多少
- 奇怪的曲線產生原因