在Node模块中使用cluster

时间:2014-05-20 23:18:05

标签: multithreading node.js cluster-computing

更新:即使这个特定情况不切实际,根据评论,我仍然对如何编写一个使用群集而不重新运行父进程的模块感兴趣时间。


我试图编写一个名为mass-request的Node.js模块,通过将它们分发给子进程来加速大量的HTTP请求。

我希望在外面,它能像这样工作。

var mr = require("mass-request"),
    scraper = mr();

for (var i = 0; i < my_urls_to_visit.length; i += 1) {
    scraper.add(my_urls_to_visit[i], function(resp) {
        // do something with response
    }
}

首先,我为群发请求模块组建了一个骨架。

var cluster = require("cluster"),
    numCPUs = require("os").cpus().length;

module.exports = function() {
    console.log("hello from mass-request!");
    if (cluster.isMaster) {
        for (var i = 0; i < numCPUs; i += 1) {
            var worker = cluster.fork();             
        }

        return {
            add: function(url, cb) {}       
        }       
    } else {
        console.log("worker " + process.pid + " is born!");
    }  
}

然后我在测试脚本中测试它:

var m = mr();
console.log("hello from test.js!", m);

我希望通过群发请求看到#34;你好!&#34;记录了四次(实际上是这样)。令我惊讶的是,我也从test.js&#34;中看到了#34;你好。四次。很明显我不明白cluster.fork()是如何运作的。它是否重新运行整个过程,而不仅仅是第一次调用它的函数?

如果是这样,如何在模块中使用群集而不会让使用该模块的人遇到麻烦的多进程逻辑?

2 个答案:

答案 0 :(得分:4)

我相信你要找的是setupMaster

来自文档:

  

<强> cluster.setupMaster([设置])

     
      
  • 设置对象   
        
    • exec工作文件的字符串文件路径。 (缺省值= process.argv [1])
    •   
    • args传递给worker的数组字符串参数。 (默认值= process.argv.slice(2))
    •   
    • silent Boolean是否将输出发送到父级的stdio。 (默认=假)
    •   
  •   
     

setupMaster用于更改默认的“fork”行为。调用后,设置将出现在cluster.settings

通过使用exec属性,您可以让您的工作人员从不同的模块启动。

重要:作为文档状态,只能调用一次。如果您依赖于模块的此行为,则调用者无法使用cluster或整个事情崩溃。

例如:

<强> index.js

var cluster = require("cluster"),
  path = require("path"),
  numCPUs = require("os").cpus().length;

console.log("hello from mass-request!");
if (cluster.isMaster) {
  cluster.setupMaster({
    exec: path.join(__dirname, 'worker.js')
  });

  for (var i = 0; i < numCPUs; i += 1) {
    var worker = cluster.fork();
  }

  return {
    add: function (url, cb) {
    }
  }
} else {
  console.log("worker " + process.pid + " is born!");
}

<强> worker.js

console.log("worker " + process.pid + " is born!");

<强>输出

node index.js 
hello from mass-request!
worker 38821 is born!
worker 38820 is born!
worker 38822 is born!
worker 38819 is born!

答案 1 :(得分:3)

虽然node.js的异步性质使其非常棒,但它仍然在单个事件循环中在服务器上的单个线程中运行。使用集群多线程化node.js应用程序允许您将应用程序的子进程分叉到自己的线程中,从而使您可以更好地使用多核服务器。我曾经构建了一个游戏服务器架构,使用集群和zmq(ZeroMQ)进行多线程处理,并使进程能够轻松地在各种通道上来回发送消息。我已经将该架构简化为下面的示例,以帮助说明多线程node.js如何组合在一起。我很抱歉,如果它有点粗糙,那是几年前我当时相对较新的节点;)

理想情况下,您不希望在一个脚本中嵌入主/子的所有内容,但我认为这是让您复制/粘贴/运行的最简单方法:)

正如您在评论中提到的,我给出了一个很好的聚类示例,但不是一个适合您特定用例的示例,只要调度一切即可。我没有很多时间,所以我调整了我的例子,使其能够很快地满足您的需求。试一试:

<强>质request.js

var cluster = require('cluster');
var zmq = require('zmq');

module.exports = {
    _childId : null,
    _urls : [],
    _threadCount : 1,
    _readyThreads : 0,
    _callbacks : {},
    zmqReceive : null, //the socket we receive on for this thread
    zmqMaster : null, //the socket to the master
    zmqChildren : {}, //an object storing the sockets for the children
    setThreads : function( threadCount ) {
        this._threadCount = threadCount;
    },
    add : function( url , cb ) {
        this._urls.push( {url: url, cb : cb } );
    },
    run : function() {

        if( cluster.isMaster ) {

            this._masterThread();

        } else {

            this._childThread();

        }

    },
    _masterThread : function() {

        console.log( 'Master Process Starting Up' );

        this.zmqReceive = zmq.socket('pull').bindSync( 'ipc://master.ipc' );

        //bind handler for messages coming into this process using closure to allow us to access the massrequest object inside the callback
        ( function( massRequest ) {
            this.zmqReceive.on( 'message' , function( msg ) {

                msg = JSON.parse(msg);

                //was this an online notification?
                if( msg && msg.status == 'Online' ) {
                    massRequest._threadReady();
                    return; //we're done
                }
                if( msg && msg.html ) {
                    //this was a response from a child, call the callback for it
                    massRequest._callbacks[ msg.sender ].call( massRequest , msg.html );
                    //send the child another URL
                    massRequest._sendUrlToChild( msg.sender );
                }

            } );
        }).call( this , this );

        //fork 4 child processes and set up the sending sockets for them
        for( var i=0; i < this._threadCount; ++i ) {
            //set up the sending socket
            this.zmqChildren[i] = zmq.socket('push').connect( 'ipc://child_' + i + '.ipc' );
            //fork the process and pass it an id
            cluster.fork( {
                _childId:i
            } );
        }

    },
    _sendUrlToChild : function( child ) {
        //if there's no urls left, return (this would also be a good place to send a message to the child to exit gracefully)
        if( !this._urls.length ) return;
        //grab a url to process
        var item = this._urls.pop();
        //set the callback for the child
        this._callbacks[child] = item.cb;
        this.zmqChildren[child].send( JSON.stringify( { url:item.url } ) );
    },
    _processUrls : function() {
        for( var i=0; i < this._threadCount; ++i ) {
            this._sendUrlToChild( i );
        }
    },
    _threadReady : function() {
        if( ++this._readyThreads >= this._threadCount ) {
            //all threads are ready, send out urls to start the mayhem
            console.log( 'All threads online, starting URL processing' );
            this._processUrls();
        }
    },
    _childProcessUrl : function( url ) {
        console.log( 'Child Process ' + this.childId + ' Handling URL: ' + url );
        //do something here to scrape your content however you see fit
        var html = 'HTML';
        this.zmqMaster.send( JSON.stringify( { sender:this.childId, html:html } ) );
    },
    _childThread : function() {

        //get the child id that was passed from cluster
        this.childId = process.env._childId;

        console.log( 'Child Process ' + this.childId + ' Starting Up' );

        //bind the pull socket to receive messages to this process
        this.zmqReceive = zmq.socket('pull').bindSync( 'ipc://child_' + this.childId + '.ipc' );

        //bind the push socket to send to the master
        this.zmqMaster = zmq.socket('push').connect('ipc://master.ipc');

        //bind handler for messages coming into this process
        ( function( massRequest ) {
            this.zmqReceive.on( 'message' , function( msg ) {

                msg = JSON.parse(msg);

                console.log( 'Child ' + this.childId + ': ' + msg );

                //handle the url
                if( msg && msg.url ) massRequest._childProcessUrl( msg.url );

            } );
        }).call( this , this );

        //let the master know we're done setting up
        this.zmqMaster.send( JSON.stringify({sender:this.childId,status:'Online'}) );

    },
}

<强> demo.js

var mr = require( './mass-request.js' );
mr.setThreads( 4 );
mr.add( 'http://foo.com' , function( resp ) {
    console.log( 'http://foo.com is done' );
} );
mr.add( 'http://bar.com' , function( resp ) {
    console.log( 'http://bar.com is done' );
} );
mr.add( 'http://alpha.com' , function( resp ) {
    console.log( 'http://alpha.com is done' );
} );
mr.add( 'http://beta.com' , function( resp ) {
    console.log( 'http://beta.com is done' );
} );
mr.add( 'http://theta.com' , function( resp ) {
    console.log( 'http://theta.com is done' );
} );
mr.add( 'http://apples.com' , function( resp ) {
    console.log( 'http://apples.com is done' );
} );
mr.add( 'http://oranges.com' , function( resp ) {
    console.log( 'http://oranges.com is done' );
} );
mr.run();

将它们放在同一文件夹中并运行node demo.js

我还应该指出,由于这个基础是从我使用[0MQ] [http://zeromq.org/]的其他项目中提取的,你需要在[node.js模块]旁边安装它[ https://github.com/JustinTulloss/zeromq.node] npm install zmq显然是群集模块。您可以将ZMQ部件替换为您希望的任何其他进程间通信方法。这恰好是我熟悉并使用过的。

简要概述:主线程AKA调用run()方法的脚本将启动X子节点(可以通过调用setThreads来设置)。这些孩子在初始化完成后通过ZeroMQ套接字向主线程报告。一旦所有线程都准备就绪,主脚本就会将URL分派给子节点,以便它们可以运行并获取HTML。它们将HTML返回到主服务器,然后将其传递给该URL的相应​​回调函数,然后将另一个URL调度到子脚本。虽然它不是一个完美的解决方案,但回调函数仍然是主(主)线程中的瓶颈,因为你无法轻易将它们移到另一个线程。那些回调可能包含闭包/变量/等,如果没有某种对象共享机制,它们可能无法在父线程之外正常工作。

Anywho,如果你在这里启动我的小演示,你会看到4个线程&#34;处理&#34;网址(为简单起见,他们实际上并没有加载网址)。

希望有帮助;)