nodejs“並行”處理嘗試

cqq626發表於2018-09-08

之前做過一些爬取方面的工作,由於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 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 區別不大
5 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 區別不大
50 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 多程式效果弱於單程式版本
100 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 多程式效果弱於單程式版本
200 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 多程式弱於單程式版本,且多程式版本總是報錯:read ECONNRESET/connect ECONNRESET/socket hang up
300 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 多程式弱於單程式版本,且多程式版本總是報錯:read ECONNRESET/connect ECONNRESET/socket hang up

高效能機器執行結果

對比結果讓我挺吃驚的,這樣看來單程式的模擬效果居然會比多程式好,但突然想到自己電腦上才幾核,怎麼同時跑幾百個程式....... 登入到公司伺服器上(48核)繼續實驗:

48核機器

threads 單程式版本 多程式版本 備註
30 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 區別不大
40 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 區別不大
100 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 qps峰值相同,但多程式更穩定
200 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 多程式版本優於單程式版本
300 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 多程式版本優於單程式版本
1000 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 多程式版本優於單程式版本
1500 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 多程式版本優於單程式版本,但threads增加所帶來的收益較低,多程式版本峰值4318<1500*4,單程式版本峰值3058<1500*4
3000 nodejs“並行”處理嘗試 nodejs“並行”處理嘗試 單程式版本(峰值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是多少
  • 奇怪的曲線產生原因

相關文章