nodejs「並行」處理嘗試

以前作過一些爬取方面的工做,因爲node不能多線程,爲了提升抓取效率,都是使用child_process.fork來多進程跑任務,而後經過message事件與主進程進行通訊,代碼編寫的時候都是用的yield/await之類的同步寫法,因而此次嘗試利用node非阻塞I/O的機制,利用多個函數同時運行來模擬多線程,效果如何呢?html

嘗試「並行」發送HTTP請求

server.jsnode

用來統計qps,將產出的數據status.txt裏的內容複製到echarts的官方示例裏進行可視化,從而驗證是否能達到「並行」的效果linux

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.jsios

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.jsaxios

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.jsbash

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是多少
  • 奇怪的曲線產生緣由
相關文章
相關標籤/搜索