Node.js固有的单线程模型经常被认为是它的一个软肋。不管你的机器上有多少CPU内核,Node.js能用上的也仅仅是其中之一(某些操作会被有条件地卸载到线程池中。大多数程序只是在CPU的总时间上分了一杯羹,所以更好地利用可用的处理能力并不能起到多大作用)。
所以Node.js从v0.8开始,新增加了一个内置的‘cluster’模块。你可以用cluster模块设置一个主进程作为管理者,由一或多个工人进程完成实际工作。
让创建“发完就忘”的多进程服务器变得更容易是其目标之一。在完美的世界中,你应该可以一行代码都不用改,就能让一个已有的单进程程序繁衍出任意多的工人进程。
当然,事情不可能那么容易,但对于没有共享状态,或共享状态很少,或把共享状态存放在数据库或Web服务之类的外部资源中的程序而言,cluster模块的确让这件事变得简单直接了。把那样的程序变成集群化的通常只需要几行代码:
var cluster = require('cluster'); var os = require('os'); if (cluster.isMaster) // 繁衍工人进程,数量跟系统中的CPU数量一样 for (var i = 0, n = os.cpus().length; i < n; i += 1) cluster.fork(); else // 启动程序 app();
这个程序不需要知道它运行在集群化环境中。比如说你有一个这样的app()
:
var http = require('http'); function app() { var server = http.createServer(function(req, res) { res.end('OK'); }); server.listen(8080, 'www.example.com'); }
cluster模块最神奇之处在于所有工人线程都可以绑定到相同的请求处理端口和地址上。另外,它可以确保接进来的连接会被均匀地分配给监听着的工人线程...最起码理论上是这样的。
Node.js v0.8和v0.10中分配连接的算法很简单。当工人进程调用http.Server#listen()
或net.Server#listen()
时,Node.js会给主进程发送一条消息,让它创建一个服务器socket,绑定好,并分享给这个工人进程。如果已经有绑定好的socket了,主进程就会跳过“创建和绑定”那一步,只需分享已有的socket就可以了。
也就是说所有的工人进程监听的都是同一个socket。有新连接进来时,操作系统会唤醒一个工人进程。被唤醒的工人进程就会接受连接,开始提供服务。
一切都还不错。操作系统会针对运行中的进程收集大量的指标,所以应该最有资格决定唤醒哪个进程。
现在,我们进入了理论与杂乱的现实相遇的环节,因为它慢慢地水落石出了,操作系统并不能总是跟程序员想得一样做到‘最好’。特别是在某些情况下,我们观测到–特别是在Linux和Solaris中–大多数连接最终都落在了两或三个进程里。
从操作系统的角度来看这是可以理解的:上下文切换(挂起一个进程,然后重新激活另一个)是相当昂贵的操作。如果你有n个进程全都等在同一个socket上,那么唤醒最近被阻塞的进程是明智之举,因为那样可以最大限度地避免上下文切换。(当然,调度器是一种复杂而又多变的野兽;上面只是对真实情况泛泛的解释。基本前提是那些得到优待的进程会仍然受到优待)。
并不是所有的程序都会受到这个怪癖的影响,实际上大多数都不会,但那些确实会受到影响的会出现非常不均衡的负载。
一旦确定了根本原因,缓解措施就可以用上了。但还没有特别令人满意的。比如暂时放弃监听socket以便让其它工人进程有机会接受新连接,这有点儿用,但还不够。‘选定几个’中的连接数从90%下降到了60-70%,有改善,但还是不够好。更别提它对那些要处理非常短命的连接的程序的剧烈影响了。
更重要的是,我们清楚地意识到,就像随机数的生成一样,接入连接的分配太重要了,不能靠运气。经过多次讨论,我们达成了共识-我们最后,也是最好的希望是可以简单地抛弃目前的做法,切换到完全不同的东西上。这就是Node.js v0.11.2中的cluster模块换成了round-robin方式的原因,新连接由主进程接受,然后由它选择一个工人进程把连接交出去。
现在这个选择工人进程的算法还不是特别精巧。就像它的名字一样,它用的是轮转法– 只是拿起下一个可用的工人进程– 但经过核心开发人员和用户的测试,证明它很好用:连接在工人进程之间分配得很均衡。我们正在考虑将选定的算法变成可以由开发人员配置或插入的东西。
如果你还想用老办法分配连接,可以在程序中设定cluster.schedulingPolicy
:
var cluster = require('cluster'); // 在调用其他cluster函数前设定这个 cluster.schedulingPolicy = cluster.SCHED_NONE; cluster.fork();
或者通过环境变量NODE_CLUSTER_SCHED_POLICY
调整调度策略:
$ export NODE_CLUSTER_SCHED_POLICY="none" # "rr" is round-robin $ node app.js
如果不想影响你的shell环境,就放在一行命令里:
$ env NODE_CLUSTER_SCHED_POLICY="none" node app.js
MS Windows是默认使用老办法的唯一平台。为了达到性能最优,Node.js在Windows上使用了IOCP。尽管这在大多数情况下都不错,但这样将HANDLE对象(连接)发送给其它进程代价十分高昂。 尽管有可能在libuv中解决这个问题,但我们还不清楚是否真的有必要这么做:Windows的端口几乎不会受到负载均衡问题的影响,而Linux和Solaris的端口确实会受影响。
本文最初由Ben Noordhuis发表在StrongLoop上。Ben Noordhuis从2010年就跟着Ryan Dahl开发Node.js的核心代码。他一直在为改进Node核心代码而努力做着编码、调试和基准测试等工作。作为最高产的Node核心开发者之一,Ben编写了Node.js和libuv中的很多代码。StrongLoop降低了在Node中开发APIs的难度,还添加了监测、集群化以及私有注册的支持等DevOps能力。
原文英文链接:What’s New in Node.js v0.12: Cluster Round-Robin Load Balancing