http://blog.factual.com/factuals-api-a-good-fit-for-node (作者:Jeff Su 译者:Forrest Cao)
大家都听说过Node了。快、可扩展、并发性,这些都是它的特点。在Factual公司,让我们感到自豪的是,往往可以找到正确的工具来完成工作,从jquery到hadoop,postgresql到mongodb。更幸运的是,我们有种工程师文化,鼓励我们实验一些新的用法。但是,每种投入产品环境的技术,都必须经过严格的检验和衡量。我们考察的方面包括敏捷性、性能、稳定性和成本。
关于Node,有很多开发者被误导,以为用上了node,你就马上拥有了并发性和性能。事实是,你得配合各种技术,才能解决一个非常特别的问题。那么,Node到底能解决什么问题?它是怎么解决的?必须付上什么代价?下面介绍的就是我们的一些简要的经验,以及我们是怎么使用Node的。
我们产品的第一个迭代是一个可以互动的网格(grid),用户可以上载、更新、重组我们的数据,并把数据表(dataset)展现为各种有用的表现形式(visualization)。那是一个复杂的web app,我们用了很多JavaScript。那时我做了一个Ruby库,给JavaScript加上了一点基本的面向对象编程的特性(现在,这个库叫做Mochiscript)。
从那以后,我们的业务重心偏向提供更有质量的数据,并通过API快速传递这些数据。这个业务重心的转移带来了一系列新的需求。我们现在可以支持的相应速度是:200毫秒一个请求,包括用户认证,权限检查,实时统计和查询处理。我们必须用一个敏捷的语言实现它,因为需求会不断地增加变化。
当时由于我们都擅长Ruby,我们的第一个原型是用轻量级的Sinatra实现的。那个版本的表现的还不错,可以支持同时120个连接,最小相应时间20毫秒。但是从我们所需要支持的流量来看,这个方案的扩展性还不够好。
然后我们考虑了Java和Clojure(这两种语言都正应用在Factual的其他项目中),但是最后我们决定试试Node,因为敏捷的需要,也因为我们对JavaScript的熟悉。
首先我们把Mochiscript从Ruby导到Node,看看Google的V8引擎有多快,然后,就决定写一个原型,让它来跟Ruby版本的原型竞争上岗。差距是明显的!Node版本的性能是,可以支持同时400个连接,最小相应时间10毫秒。在接下来的几个月里,原型变成了产品,Node的高性能和Mochiscript组织代码的优越性帮了我们大忙。我们又做了一些优化,现在的相应时间已经可以达到最小5毫秒。
在尽情歌颂Node之前,先罗列一下我们付上的代价:
- 这个框架还不够成熟(在他们的早期http库中,我们发现了一个socket泄露的bug)
- 像意大利面一样杂乱的代码:到处都是callbacks。这个问题可以通过一些办法来解决,比如:一个好的设计,遵循一些编程规范,等等。但即便如此,还是不轻松。
- 有时候调试程序会让人抓狂(大部分是因为上面两个原因)
为什么Node适合我们的原因如下:
- 我们以前的性能瓶颈在IO
- 我们每个请求需要用到的CPU很少
- 我们可以用事件驱动编程,来帮我们大量使用缓存
前面两点理由并不新鲜,而这两点的结合正是Node的强项。这两点完美地适用于我们的情况。而第三点,是我们想着重介绍的。
对于很多工程师习惯的方式来说,用Node写程序需要有点儿思路转换。那些Callbacks会失控的。然而,Javascript提供的闭包带来了新方法。对我们很有用的一个方法,可以用来减少多余的数据库访问。
考虑一下下面这个例子──从数据库中获得用户数据:
var connect = require('connect');
var users = require('./lib/users');
var app = connect.createServer();
app.get('/users/:userId', function (req, res) {
users.get(req.params.userId, function (err, user) {
res.end("Welcome " + user.name);
});
});
app.get('/stats', function (req, res) {
res.end(JSON.stringify(counts));
});
现在让我们加入一个缓存层:
var connect = require('connect');
var users = require('./lib/users');
var app = connect.createServer();
var userCache = {};
function getUser(id, cb) {
if (id in userCache) {
cb(userCache[id]);
} else {
users.get(id, cb);
}
}
app.get('/users/:userId', function (req, res) {
getUser(req.params.userId, function (user) {
res.end("Welcome " + user.name);
});
});
既然我们用到了缓存,就得考虑怎么清除它。Redis的pubsub功能绝对适用这种用法,我们用它来实时更新缓存:
var connect = require('connect');
var users = require('./lib/users');
var app = connect.createServer();
var redis = require('redis').createClient();
var userCache = {};
function getUser(id, cb) {
if (id in userCache) {
cb(userCache[id]);
} else {
users.get(id, cb);
}
}
redis.subscribe('update-user', function (id) { delete userCache[id] });
app.get('/users/:userId', function (req, res) {
getUser(req.params.userId, function (user) {
res.end("Welcome " + user.name);
});
});
再来加点儿好玩的东西──访问统计:
var connect = require('connect');
var users = require('./lib/users');
var app = connect.createServer();
var userCache = {};
var counts = {};
function getUser(id, cb) {
if (id in userCache) {
cb(userCache[id]);
} else {
users.get(id, cb);
}
}
app.get('/users/:userId', function (req, res) {
getUser(req.params.userId, function (user) {
if (req.url in counts) {
counts[req.url]++;
} else {
counts[req.url] = 1;
}
res.end("Welcome " + user.name);
});
});
这样的模式是Node或者JavaScript这样事件驱动模型的一个副产品,还真是给了我们不少灵活度,我们可以方便地加入缓存、实时统计、还有实时配置等等。根据我们的经验,用redis或者memcache来做缓存也是很不错的,但是我们的目标是榨干Node服务器的最后一滴性能,所以我们要限制任何多余的操作(甚至尽量不去parse json)。
我们理解Node不是所有问题的解决方案。但在我们的应用里,Node是一个绝配。我们大概从一年前开始适用Node,这一年来Node获得了长足的长进,而我们也从Joyent和开源社区获得了很多帮助。我们开始在其他各种内部项目中适用Node,渐渐地这成为一种通用解决方案而不再小众。我鼓励各位开发者来探索Node,看看它是否适合你的问题。至少,它会让你思考IO,以及你要花多少时间等待IO。
感谢翻译,学习了。 另: 像意大利面一样杂乱的代码:到处都是callbacks。这个问题可以通过一些办法来解决,比如:一个好的设计,遵循一些编程规范,等等。但即便如此,还是不轻松。
有没有好的编程规范或分层设计贴下?
@suqian 分享的编程规范就不错: http://fengmk2.github.com/ppt/nodejs_programming_style.html , 我们基本上用的也是这些“国际范例”。